From b78ad696a019a44e250d6ff7056f948359a48b11 Mon Sep 17 00:00:00 2001 From: lxm <1274844347@qq.com> Date: Sat, 23 Aug 2025 10:35:22 +0800 Subject: [PATCH 1/3] =?UTF-8?q?refactor(home):=20=E7=AE=80=E5=8C=96?= =?UTF-8?q?=E4=B8=BB=E9=A1=B5=E5=B8=83=E5=B1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/modules/agent/views/agent_view.dart | 5 +- lib/modules/home/views/home_view.dart | 287 ++++++++++++------------ 2 files changed, 139 insertions(+), 153 deletions(-) diff --git a/lib/modules/agent/views/agent_view.dart b/lib/modules/agent/views/agent_view.dart index 4681a6fba..910b9ccdc 100644 --- a/lib/modules/agent/views/agent_view.dart +++ b/lib/modules/agent/views/agent_view.dart @@ -405,8 +405,7 @@ class _AgentViewState extends State with WidgetsBindingObserver { SizedBox(height: 12.h), ], ); - } else if (controller.isTextInputMode.value && - !controller.hasTextInput.value) { + } else { // 文本输入模式下,且输入框为空 return Column( children: [ @@ -449,8 +448,6 @@ class _AgentViewState extends State with WidgetsBindingObserver { SizedBox(height: 12.h), ], ); - } else { - return const SizedBox.shrink(); } }, ), diff --git a/lib/modules/home/views/home_view.dart b/lib/modules/home/views/home_view.dart index df4d421ca..e51be533d 100644 --- a/lib/modules/home/views/home_view.dart +++ b/lib/modules/home/views/home_view.dart @@ -72,86 +72,81 @@ class HomeView extends GetView { @override Widget build(BuildContext context) { - // 预缓存图片 - precacheImage( - const AssetImage('assets/images/headphone_new3.png'), context); - precacheImage( - const AssetImage('assets/images/headphone_new4.png'), context); final bool isDarkMode = Theme.of(context).brightness == Brightness.dark; return Scaffold( backgroundColor: isDarkMode ? Colors.black : const Color(0xFFF9FAFC), body: SafeArea( - child: Padding( + child: SingleChildScrollView( padding: EdgeInsets.all(16.w), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 顶部标题栏 _buildAppBar(isDarkMode), - // 可变高度的顶部图片区域 - 使用Expanded让它占用剩余空间 - Expanded( - child: Column( - children: [ - Obx(() => _buildAdaptiveEarbudsImage()), - SizedBox(height: 20.h), - // 设备状态卡片 - IntroStepBuilder( - group: 'home', - order: 1, - onWidgetLoad: () { - controller.noviceGuideService.startHomeGuide(); - }, - getOverlayPosition: IntroOverlayBuilder.overlayPosition, - overlayBuilder: (params) { - return IntroOverlayBuilder.aiBuilder( - params, - '欢迎使用Voitrans!首先,让我们唤醒你的专属 AI—— 小言', - onNext: () { - controller.noviceGuideService.startWakeupListen(); - }, - ); - }, - builder: (BuildContext context, GlobalKey key) => - SizedBox( - key: key, - child: _buildAnimatedDeviceStatusCard(isDarkMode), - ), - ), - SizedBox(height: 20.h), - IntroStepBuilder( - group: 'home', - order: 2, - getOverlayPosition: IntroOverlayBuilder.overlayPosition, - overlayBuilder: (params) { - return Obx( - () => IntroOverlayBuilder.homeBuilder( - params, - controller.noviceGuideService.isWakeup.value, - onNext: () { - controller.noviceGuideService.closeWakeupListen(); - }, - ), - ); + + // 顶部图片 + _buildEarbudsImage(), + + SizedBox(height: 20.h), + + // 设备状态卡片 + IntroStepBuilder( + group: 'home', + order: 1, + onWidgetLoad: () { + controller.noviceGuideService.startHomeGuide(); + }, + overlayBuilder: (params) { + return IntroOverlayBuilder.aiBuilder( + params, + '欢迎使用Voitrans!首先,让我们唤醒你的专属 AI—— 小言', + onNext: () { + controller.noviceGuideService.startWakeupListen(); + }, + ); + }, + builder: (BuildContext context, GlobalKey key) => SizedBox( + key: key, + child: _buildDeviceStatusCard(isDarkMode), + ), + ), + + SizedBox(height: 20.h), + + IntroStepBuilder( + group: 'home', + order: 2, + getOverlayPosition: ({ + required Offset offset, + required Size screenSize, + required Size size, + }) { + return OverlayPosition( + top: 200.h, + width: screenSize.width, + crossAxisAlignment: CrossAxisAlignment.end, + ); + }, + overlayBuilder: (params) { + return Obx( + () => IntroOverlayBuilder.homeBuilder( + params, + controller.noviceGuideService.isWakeup.value, + onNext: () { + controller.noviceGuideService.closeWakeupListen(); }, - builder: (BuildContext context, GlobalKey key) => - SizedBox( - key: key, - ), ), - ], + ); + }, + builder: (BuildContext context, GlobalKey key) => SizedBox( + key: key, ), ), - // 热门AI体验 - 固定在底部,基于内容的固定高度 - Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, // 重要:让高度基于内容 - children: [ - _buildSectionTitle(isDarkMode), - SizedBox(height: 16.h), - // 给AI布局一个固定的高度,或者让它基于内容自适应 - _buildPremiumAILayout(), - ], - ), + + // 热门AI体验 + _buildSectionTitle(isDarkMode), + SizedBox(height: 16.h), + _buildPremiumAILayout(), ], ), ), @@ -201,47 +196,70 @@ class HomeView extends GetView { ); } - // 自适应高度的耳机图片 - 根据设备连接状态动态调整高度 - // 在自适应图片组件中添加RepaintBoundary - Widget _buildAdaptiveEarbudsImage() { + // 耳机图片 + Widget _buildEarbudsImage() { final bool isDarkMode = Get.isDarkMode; - final double imageHeight = - controller.isFullyConnectedRx.value ? 160.h : 220.h; - - return RepaintBoundary( - // 添加重绘边界,优化动画性能 - child: AnimatedContainer( - duration: const Duration(milliseconds: 500), - curve: Curves.easeInOut, - height: imageHeight, - child: PageView( - children: [ - Center( - child: ClipRRect( - borderRadius: BorderRadius.circular(16.r), - child: Image.asset( - isDarkMode - ? 'assets/images/headphone_new3.png' - : 'assets/images/headphone_new4.png', - height: imageHeight, - fit: BoxFit.contain, - ), + return SizedBox( + height: 220.h, + child: PageView( + children: [ + // Center( + // child: ClipRRect( + // // 添加 ClipRRect 来实现圆角 + // borderRadius: BorderRadius.circular(16.r), // 使用与其他部件一致的圆角大小 + // child: SizedBox( + // width: double.infinity, + // height: 200.h, + // child: Image.asset( + // isDarkMode + // ? 'assets/images/robot_light.png' + // : 'assets/images/robot_light.png', + // fit: BoxFit.fitWidth, + // ), + // ), + // ), + // ), + Center( + child: ClipRRect( + // 添加 ClipRRect + borderRadius: BorderRadius.circular(16.r), + child: Image.asset( + isDarkMode + ? 'assets/images/headphone_new3.png' + : 'assets/images/headphone_new4.png', + height: 220.h, + fit: BoxFit.contain, ), ), - ], - ), + ), + // Center( + // child: ClipRRect( + // // 添加 ClipRRect + // borderRadius: BorderRadius.circular(16.r), + // child: SizedBox( + // width: double.infinity, + // height: 200.h, + // child: Image.asset( + // isDarkMode + // ? 'assets/images/glasses_light.png' + // : 'assets/images/glasses_light.png', + // fit: BoxFit.fitWidth, + // ), + // ), + // ), + // ), + ], ), ); } - // 带动画的设备状态卡片 - 实现平滑的高度变化过渡 - Widget _buildAnimatedDeviceStatusCard(bool isDarkMode) { + // 设备状态卡片 + Widget _buildDeviceStatusCard(bool isDarkMode) { return Obx(() { + // 使用连接状态作为key,强制在状态改变时重建widget final isConnected = controller.isFullyConnectedRx.value; - - return AnimatedContainer( - duration: const Duration(milliseconds: 500), // 平滑过渡动画 - curve: Curves.easeInOut, + return Container( + key: ValueKey('device_status_$isConnected'), decoration: BoxDecoration( color: isDarkMode ? const Color(0xFF1A1A1A) : Colors.white, borderRadius: BorderRadius.circular(16.r), @@ -255,58 +273,30 @@ class HomeView extends GetView { ), padding: EdgeInsets.fromLTRB(16.w, 16.h, 16.w, 16.h), child: Column( - mainAxisSize: MainAxisSize.min, // 重要:让容器高度自适应内容 children: [ // 状态标题行 _buildDeviceStatusHeader(isDarkMode), - // 设备信息 - 使用 AnimatedSwitcher 实现内容切换动画 + // 设备信息 SizedBox(height: 12.h), - AnimatedSwitcher( - duration: const Duration(milliseconds: 300), - transitionBuilder: (Widget child, Animation animation) { - return FadeTransition( - opacity: animation, - child: SlideTransition( - position: Tween( - begin: const Offset(0, 0.1), - end: Offset.zero, - ).animate(animation), - child: child, - ), - ); - }, - child: Container( - key: ValueKey('device_info_$isConnected'), - child: isConnected - ? _buildConnectedDeviceInfo(isDarkMode) - : _buildDisconnectedInfo(isDarkMode), - ), - ), - - // 连接按钮 - 使用 AnimatedSwitcher 实现按钮切换动画 + Obx(() { + // 移除连接失败和连接中的状态判断,直接根据是否完全连接来判断 + // 使用响应式访问确保UI更新 + return controller.isFullyConnectedRx.value + ? _buildConnectedDeviceInfo(isDarkMode) //已连接设备信息 + : _buildDisconnectedInfo(isDarkMode); //未连接提示 + }), + + // 连接按钮(仅在未连接时显示) SizedBox(height: 12.h), - AnimatedSwitcher( - duration: const Duration(milliseconds: 300), - transitionBuilder: (Widget child, Animation animation) { - return FadeTransition( - opacity: animation, - child: SlideTransition( - position: Tween( - begin: const Offset(0, 0.1), - end: Offset.zero, - ).animate(animation), - child: child, - ), - ); - }, - child: Container( - key: ValueKey('device_button_$isConnected'), - child: !isConnected - ? _buildConnectButton() - : _buildDeviceButton(isDarkMode), - ), - ), + Obx(() { + // 使用响应式访问确保UI更新 + if (!controller.isFullyConnectedRx.value) { + return _buildConnectButton(); + } else { + return _buildDeviceButton(isDarkMode); + } + }), ], ), ); @@ -740,15 +730,14 @@ class HomeView extends GetView { child: IntroStepBuilder( group: 'home', order: 3, - getOverlayPosition: IntroOverlayBuilder.overlayPosition, + onHighlightWidgetTap: () { + Intro.of(Get.context!).dispose(); + controller.exploreAIFeatures(); + }, overlayBuilder: (params) { return IntroOverlayBuilder.aiBuilder( params, '太棒了!你已成功唤醒小言,接下来试试让它帮你做件事吧', - onTap: () { - params.onFinish(); - controller.exploreAIFeatures(); - }, ); }, builder: (BuildContext context, GlobalKey key) => SizedBox( From d9d87d07072f34d2f5fcd48a420ad425b49b62f1 Mon Sep 17 00:00:00 2001 From: fdp <1286779656@qq.com> Date: Sat, 23 Aug 2025 14:06:47 +0800 Subject: [PATCH 2/3] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E9=BA=A6=E5=85=8B?= =?UTF-8?q?=E9=A3=8E=E6=97=B6=E5=A4=A7=E6=97=B6=E5=B0=8F=E7=9A=84=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/azure_speech/AzureAsrHelper.swift | 22 +- .../Sources/azure_speech/AzureTtsHelper.swift | 26 +- .../Sources/tools/SimpleAudioReceiver.swift | 453 ++++++++++-------- 3 files changed, 263 insertions(+), 238 deletions(-) diff --git a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift index f5d2cf052..0ee8b5c0a 100644 --- a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift +++ b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift @@ -206,12 +206,11 @@ public class AzureAsrHelper: NSObject { audioStream = SimpleAudioReceiver() audioStream?.initAudioRecord() } - // 预创建音频配置 - if let pushStream = audioStream?.pushAudioStream { + if let pushStream = audioStream?.pushAudioStream { audioConfig = SPXAudioConfiguration(streamInput: pushStream) os_log("预初始化音频组件完成", log: log, type: .info) - } + } } @@ -465,12 +464,17 @@ public class AzureAsrHelper: NSObject { * 设置麦克风音频流 */ private func setupMicrophoneStream() { - audioStream = SimpleAudioReceiver() - // 【优化】检查音频配置是否已存在 - if audioConfig == nil, let pushStream = audioStream?.pushAudioStream { - audioConfig = SPXAudioConfiguration(streamInput: pushStream) - os_log("设置麦克风流完成", log: log, type: .info) - } + // 只有在 audioStream 为 nil 时才创建新实例 + if audioStream == nil { + audioStream = SimpleAudioReceiver() + audioStream?.initAudioRecord() // 确保调用初始化 + } + + // 检查音频配置是否已存在 + if audioConfig == nil, let pushStream = audioStream?.pushAudioStream { + audioConfig = SPXAudioConfiguration(streamInput: pushStream) + os_log("设置麦克风流完成", log: log, type: .info) + } } diff --git a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureTtsHelper.swift b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureTtsHelper.swift index 86ac66bb9..225bf71c2 100644 --- a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureTtsHelper.swift +++ b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureTtsHelper.swift @@ -319,31 +319,7 @@ public class AzureTtsHelper: NSObject, ITtsService { return true } - // /** - // * 设置音频输出设备 - // */ - // public func setAudioOutputDevice(_ device: AudioOutputDevice) -> Bool { - // do { - // let session = AVAudioSession.sharedInstance() - // try session.setCategory(.playAndRecord, options: [.defaultToSpeaker, .allowBluetooth]) - // - // switch device { - // case .default: - // try session.overrideOutputAudioPort(.none) - // case .speaker: - // try session.overrideOutputAudioPort(.speaker) - // case .headphones: - // try session.overrideOutputAudioPort(.none) - // } - // - // try session.setActive(true) - // return true - // } catch { - // os_log("设置音频输出设备失败: %{public}@", log: log, type: .error, error.localizedDescription) - // return false - // } - // } - // + // MARK: - 私有方法 /** diff --git a/local_plugins/azure_speech/ios/azure_speech/Sources/tools/SimpleAudioReceiver.swift b/local_plugins/azure_speech/ios/azure_speech/Sources/tools/SimpleAudioReceiver.swift index 0cf7951ae..710f7a1e2 100644 --- a/local_plugins/azure_speech/ios/azure_speech/Sources/tools/SimpleAudioReceiver.swift +++ b/local_plugins/azure_speech/ios/azure_speech/Sources/tools/SimpleAudioReceiver.swift @@ -54,11 +54,28 @@ public class SimpleAudioReceiver: NSObject { // 初始化音频引擎 audioEngine = AVAudioEngine() + isRunning = true // 创建新的写线程 writeThread = DispatchQueue(label: "audio.stream.writer") startWriteThread() + // 配置音频引擎 + do { + try startCapture { buffer in + // 处理实时音频数据 + let channelData = buffer.floatChannelData?[0] + let frameCount = buffer.frameLength + print("麦克风启动: \(frameCount)") + print("🎵 音频数据: 帧数=\(frameCount), 声道数据=\(channelData != nil ? "有效" : "无效")") + self.writeQueue.put(self.audioBufferToData(buffer)) + // 分析或传输数据... + } + + } catch { + print("麦克风启动失败: \(error)") + + } } /// 获取最佳音频格式 (iOS 通常支持标准采样率) @@ -145,6 +162,7 @@ public class SimpleAudioReceiver: NSObject { print("写入数据长度: \(dataToWrite.count)") self.audioDataCallback!.onAudio(dataToWrite) } + print("写入数据长度: \(dataToWrite.count)") try self.pushAudioStream?.write(dataToWrite) if self.recordfile != nil { // print("写入recordfile数据长度: \(dataToWrite.count)") @@ -173,107 +191,113 @@ public class SimpleAudioReceiver: NSObject { // 放入队列,由写线程写入 writeQueue.put(data) } + + /** + * 开始采集音频数据 + * @param bufferHandler 音频缓冲区处理回调 + * @throws 音频配置或启动错误 + */ + func startCapture(bufferHandler: @escaping (AVAudioPCMBuffer) -> Void) throws { + try setupAudioSession() + + guard let audioEngine = audioEngine else { + throw NSError(domain: "SimpleAudioReceiver", code: -1, userInfo: [NSLocalizedDescriptionKey: "Audio engine not initialized"]) + } + + // 确保音频引擎完全停止 + if audioEngine.isRunning { + audioEngine.stop() + // 等待一小段时间确保完全停止 + Thread.sleep(forTimeInterval: 0.1) + } + + // 先安全地移除已存在的 tap + removeTapSafely() + + // 重置音频引擎(在获取格式之前) + audioEngine.reset() + + // 重新获取输入节点和格式(reset后需要重新获取) + let inputNode = audioEngine.inputNode + let inputFormat = inputNode.outputFormat(forBus: 0) + + print("硬件输入格式: 采样率=\(inputFormat.sampleRate)Hz, 声道数=\(inputFormat.channelCount), 格式=\(inputFormat.commonFormat.rawValue)") + + // 验证格式是否有效 + if inputFormat.sampleRate <= 0 || inputFormat.channelCount <= 0 { + // 如果格式无效,使用默认格式 + let defaultFormat = AVAudioFormat(standardFormatWithSampleRate: 48000, channels: 1)! + print("使用默认音频格式: 采样率=\(defaultFormat.sampleRate)Hz, 声道数=\(defaultFormat.channelCount)") + + inputNode.installTap(onBus: 0, + bufferSize: 1024, + format: defaultFormat) { (buffer, time) in + bufferHandler(buffer) + } + } else { + // 使用硬件原生格式 + inputNode.installTap(onBus: 0, + bufferSize: 1024, + format: inputFormat) { (buffer, time) in + bufferHandler(buffer) + } + } + + if #available(iOS 13.0, *) { + try inputNode.setVoiceProcessingEnabled(true) + } + + audioEngine.prepare() + + + print("音频引擎启动成功") + } - // 私有方法:启动麦克风捕获 /** - * 启动麦克风捕获 - * 优化:复用已初始化的音频引擎,减少启动延迟 + * 安全地移除音频tap + * 避免在移除不存在的tap时出错 */ + private func removeTapSafely() { + guard let audioEngine = audioEngine else { return } + + let inputNode = audioEngine.inputNode + + // 尝试移除tap,如果没有tap也不会出错 + do { + inputNode.removeTap(onBus: 0) + print("成功移除音频tap") + } catch { + // 如果没有tap可移除,这是正常的 + print("移除tap时出现错误(可能没有tap存在): \(error)") + } + } private func runMicrophoneCapture() { do { - // 【新增】首先配置音频会话 - try audioSession.setCategory( - .playAndRecord, - mode: .measurement, // 使用 measurement 模式获得最佳录音质量 - options: [.allowBluetooth, .defaultToSpeaker] - ) - - // 【新增】请求麦克风权限(如果尚未授权) - if audioSession.recordPermission != .granted { - audioSession.requestRecordPermission { granted in - if !granted { - print("麦克风权限被拒绝") - } - } - } - - // 【新增】激活音频会话(在配置音频引擎之前) - try audioSession.setActive(true, options: [.notifyOthersOnDeactivation]) + print("🎤 开始配置麦克风捕获") - // 检查音频引擎是否已初始化,避免重复创建 if audioEngine == nil { audioEngine = AVAudioEngine() + print("🔧 创建新的音频引擎") } // 如果音频引擎正在运行,先停止 if audioEngine?.isRunning == true { audioEngine?.stop() + print("⏹️ 停止现有音频引擎") } - // 获取音频输入节点(麦克风) - guard let inputNode = audioEngine?.inputNode else { - throw NSError(domain: "AudioSetup", code: 1, userInfo: [NSLocalizedDescriptionKey: "无法获取音频输入节点"]) - } - - // 移除之前的音频处理块,避免重复添加 - inputNode.removeTap(onBus: 0) - - // 获取硬件支持的原始音频格式 - let hardwareFormat = inputNode.inputFormat(forBus: 0) - - // iOS 13+ 启用语音处理 - if #available(iOS 13.0, *) { - try inputNode.setVoiceProcessingEnabled(true) - } - - // 检查目标音频格式和转换器是否可用 - guard let targetFormat = audioFormat, // 外部定义的期望音频格式 - let converter = AVAudioConverter(from: hardwareFormat, to: targetFormat) else { - throw NSError(domain: "AudioSetup", code: 2) - } - - // 在输入节点上安装录音回调 - inputNode.installTap(onBus: 0, - bufferSize: UInt32(bufferSize), // 每次回调的缓冲区大小 - format: hardwareFormat) { // 使用原始硬件格式 - [weak self] buffer, time in // 弱引用避免循环引用 - - // 确保实例存在且正在写入状态 - guard let self = self, self._isWriting else { return } - - // 创建目标格式的音频缓冲区 - let convertedBuffer = AVAudioPCMBuffer( - pcmFormat: targetFormat, - // 计算转换后的帧容量(考虑采样率差异) - frameCapacity: AVAudioFrameCount( - targetFormat.sampleRate * Double(buffer.frameLength) / buffer.format.sampleRate - ) - )! - - var error: NSError? - // 执行音频格式转换 - let status = converter.convert( - to: convertedBuffer, - error: &error, - withInputFrom: { inNumPackets, outStatus in - outStatus.pointee = .haveData // 标记有数据可用 - return buffer // 返回原始音频数据 - } - ) - - // 转换成功且无错误 - if status == .haveData, error == nil { - // 将音频缓冲区转换为二进制数据 - let data = self.audioBufferToData(convertedBuffer) - // 将数据放入写入队列(后续处理) - self.writeQueue.put(data) - } - } - - // 激活音频会话(允许录音) - try audioSession.setActive(true, options: [.notifyOthersOnDeactivation]) + // try startCapture { buffer in + // // 处理实时音频数据 + // let channelData = buffer.floatChannelData?[0] + // let frameCount = buffer.frameLength + // print("麦克风启动: \(frameCount)") + // print("🎵 音频数据: 帧数=\(frameCount), 声道数据=\(channelData != nil ? "有效" : "无效")") + // self.writeQueue.put(self.audioBufferToData(buffer)) + // // 分析或传输数据... + // } + try audioEngine?.start() + print("✅ 麦克风捕获配置完成") - try audioEngine?.start() } catch { print("麦克风启动失败: \(error)") // 【新增】添加详细错误处理 @@ -283,38 +307,7 @@ public class SimpleAudioReceiver: NSObject { } } } - private func audioBufferToData(_ buffer: AVAudioPCMBuffer) -> Data { - let frameLength = Int(buffer.frameLength) - let channelCount = 1 - let dataLength = frameLength * channelCount * MemoryLayout.size - - // Handle 16-bit integer format - if let int16Data = buffer.int16ChannelData { - return Data( - bytes: int16Data.pointee, - count: dataLength - ) - } - // Handle float format - else if let floatData = buffer.floatChannelData { - var int16Array = [Int16](repeating: 0, count: frameLength) - let floatBuffer = floatData.pointee - - for i in 0.. Data { + guard let floatChannelData = buffer.floatChannelData else { + return Data() } - public func setAudioOutputRoute(_ route: AudioOutputRoute) { + + let frameLength = Int(buffer.frameLength) + let channelCount = Int(buffer.format.channelCount) + let inputSampleRate = buffer.format.sampleRate + let targetSampleRate: Double = 16000 + + // 统一处理:只使用第一个声道(单声道),确保一致性 + let firstChannelData = floatChannelData[0] + + // 如果采样率不是16kHz,进行降采样 + if inputSampleRate != targetSampleRate { + let ratio = inputSampleRate / targetSampleRate + let outputFrameCount = Int(Double(frameLength) / ratio) - do { - // - if self._isWriting { - print("当前正在识别") - try audioEngine?.pause() - // 先停用以避免冲突 - try audioSession.setActive(false) - - } - print("setAudioOutputRoutecurrent,route=\(route)") - switch route { - case .speaker: - print("setAudioOutputRoutecurrent,speaker") - // 使用扬声器时必须用videoChat模式 - try audioSession.setCategory( - .playAndRecord, - mode: .videoChat, - options: [.defaultToSpeaker] - ) - try audioSession.overrideOutputAudioPort(.speaker) + var outputData = Data() + outputData.reserveCapacity(outputFrameCount * 2) // 16位 = 2字节 + + for outputIndex in 0.. \(targetSampleRate)Hz, 帧数: \(frameLength) -> \(outputFrameCount)") + return outputData + } else { + // 采样率已经是16kHz,直接转换格式(只处理第一个声道) + var data = Data() + data.reserveCapacity(frameLength * 2) // 单声道,16位 + + for frame in 0.. { } } -// MARK: - 协议定义 - From 74965f4887d4c3ce911a93902f5c1d808e809462 Mon Sep 17 00:00:00 2001 From: tanlongsheng <252620078@qq.com> Date: Sat, 23 Aug 2025 16:05:21 +0800 Subject: [PATCH 3/3] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E6=96=B0=E6=89=8B?= =?UTF-8?q?=E6=8C=87=E5=BC=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/data/services/novice_guide_service.dart | 65 ++--- lib/modules/agent/views/agent_view.dart | 258 ++++++++++---------- lib/modules/home/views/home_view.dart | 22 +- 3 files changed, 165 insertions(+), 180 deletions(-) diff --git a/lib/data/services/novice_guide_service.dart b/lib/data/services/novice_guide_service.dart index fb124a77a..485004914 100644 --- a/lib/data/services/novice_guide_service.dart +++ b/lib/data/services/novice_guide_service.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:io'; import 'package:ble_service/ble_service.dart'; import 'package:flutter_intro/flutter_intro.dart'; @@ -82,21 +83,23 @@ class NoviceGuideService extends GetxService { 'send': '小言会为你拨打电话', 'end': '小言已成功为您拨打电话,试试其他指令吧', 'method': () async { - // 拨打电话需要电话权限 - await PermissionUtil.instance.requestPermission( - permissionType: Permission.phone, - permissionName: 'phone'.tr, // 电话 - explanationText: 'phonePermissionDesc'.tr, // 用于拨打电话功能 - permanentDenialText: 'phonePermissionDetailDesc'.tr, // 拨打电话功能需要电话权限 - ); - //拨打电话还需要联系人权限 - await PermissionUtil.instance.requestPermission( - permissionType: Permission.contacts, - permissionName: 'contacts'.tr, // 联系人 - explanationText: 'contactsPermissionDesc'.tr, // 用于访问联系人信息 - permanentDenialText: - 'contactsPermissionDetailDesc'.tr, // 拨打电话功能需要联系人权限 - ); + if (Platform.isAndroid) { + // 拨打电话需要电话权限 + await PermissionUtil.instance.requestPermission( + permissionType: Permission.phone, + permissionName: 'phone'.tr, // 电话 + explanationText: 'phonePermissionDesc'.tr, // 用于拨打电话功能 + permanentDenialText: 'phonePermissionDetailDesc'.tr, // 拨打电话功能需要电话权限 + ); + //拨打电话还需要联系人权限 + await PermissionUtil.instance.requestPermission( + permissionType: Permission.contacts, + permissionName: 'contacts'.tr, // 联系人 + explanationText: 'contactsPermissionDesc'.tr, // 用于访问联系人信息 + permanentDenialText: + 'contactsPermissionDetailDesc'.tr, // 拨打电话功能需要联系人权限 + ); + } }, }, { @@ -105,21 +108,23 @@ class NoviceGuideService extends GetxService { 'send': '小言会为你发送短信', 'end': '短信已发送,语音操控更便捷', 'method': () async { - // 发送短信需要短信权限 - await PermissionUtil.instance.requestPermission( - permissionType: Permission.sms, - permissionName: 'sms'.tr, // 短信 - explanationText: 'smsPermissionDesc'.tr, // 用于发送短信功能 - permanentDenialText: 'smsPermissionDetailDesc'.tr, // 发送短信功能需要短信权限 - ); - //发送短信还需要联系人权限 - await PermissionUtil.instance.requestPermission( - permissionType: Permission.contacts, - permissionName: 'contacts'.tr, // 联系人 - explanationText: 'contactsPermissionDesc'.tr, // 用于访问联系人信息 - permanentDenialText: - 'contactsPermissionDetailDesc'.tr, // 发送短信功能需要联系人权限 - ); + if (Platform.isAndroid) { + // 发送短信需要短信权限 + await PermissionUtil.instance.requestPermission( + permissionType: Permission.sms, + permissionName: 'sms'.tr, // 短信 + explanationText: 'smsPermissionDesc'.tr, // 用于发送短信功能 + permanentDenialText: 'smsPermissionDetailDesc'.tr, // 发送短信功能需要短信权限 + ); + //发送短信还需要联系人权限 + await PermissionUtil.instance.requestPermission( + permissionType: Permission.contacts, + permissionName: 'contacts'.tr, // 联系人 + explanationText: 'contactsPermissionDesc'.tr, // 用于访问联系人信息 + permanentDenialText: + 'contactsPermissionDetailDesc'.tr, // 发送短信功能需要联系人权限 + ); + } }, }, ]; diff --git a/lib/modules/agent/views/agent_view.dart b/lib/modules/agent/views/agent_view.dart index 22fc128f0..7ed4c0ddd 100644 --- a/lib/modules/agent/views/agent_view.dart +++ b/lib/modules/agent/views/agent_view.dart @@ -196,17 +196,7 @@ class _AgentViewState extends State with WidgetsBindingObserver { IntroStepBuilder( group: 'ai', order: 3, - getOverlayPosition: ({ - required Offset offset, - required Size screenSize, - required Size size, - }) { - return OverlayPosition( - top: 200.h, - width: screenSize.width, - crossAxisAlignment: CrossAxisAlignment.end, - ); - }, + getOverlayPosition: IntroOverlayBuilder.overlayPosition, overlayBuilder: (params) { return Obx( () => IntroOverlayBuilder.homeBuilder( @@ -252,6 +242,8 @@ class _AgentViewState extends State with WidgetsBindingObserver { ? IntroStepBuilder( group: 'ai', order: 4, + getOverlayPosition: + IntroOverlayBuilder.overlayPosition, overlayBuilder: (params) { return IntroOverlayBuilder.aiBuilder( params, @@ -511,6 +503,8 @@ class _AgentViewState extends State with WidgetsBindingObserver { return IntroStepBuilder( group: 'ai', order: 1, + getOverlayPosition: + IntroOverlayBuilder.overlayPosition, overlayBuilder: (params) { return IntroOverlayBuilder.aiBuilder( params, @@ -756,149 +750,143 @@ class _AgentViewState extends State with WidgetsBindingObserver { SizedBox(width: 8.w), // 动态按钮区域:展开按钮 + 发送按钮 或 图片选择按钮 - Builder(builder: (context) { - StepWidgetParams? widgetParams; - return IntroStepBuilder( - group: 'ai', - order: 2, - onHighlightWidgetTap: () async { - await controller - .noviceGuideService.aiStepsList[ - controller.noviceGuideService.aiStep] - ['method'](); - widgetParams!.onNext!(); - controller.sendMessage(); - }, - overlayBuilder: (params) { - widgetParams = params; - return IntroOverlayBuilder.aiBuilder( - params, - '点击发送,${controller.noviceGuideService.aiStepsList[controller.noviceGuideService.aiStep]['send']}', - alignment: false, - ); - }, - builder: - (BuildContext context, GlobalKey key) => - Obx( - key: key, - () { - final hasText = - controller.hasTextInput.value; - final hasImage = - controller.isImageInputActive.value; - final shouldShowSend = hasText || hasImage; - final shouldShowExpand = - controller.shouldShowExpandButton.value; + IntroStepBuilder( + group: 'ai', + order: 2, + getOverlayPosition: + IntroOverlayBuilder.overlayPosition, + overlayBuilder: (params) { + return IntroOverlayBuilder.aiBuilder( + params, + '点击发送,${controller.noviceGuideService.aiStepsList[controller.noviceGuideService.aiStep]['send']}', + alignment: false, + onTap: () async { + await controller + .noviceGuideService.aiStepsList[ + controller.noviceGuideService + .aiStep]['method'](); + params.onNext!(); + controller.sendMessage(); + }, + ); + }, + builder: (BuildContext context, GlobalKey key) => + Obx( + key: key, + () { + final hasText = controller.hasTextInput.value; + final hasImage = + controller.isImageInputActive.value; + final shouldShowSend = hasText || hasImage; + final shouldShowExpand = + controller.shouldShowExpandButton.value; - if (shouldShowSend) { - // 显示发送按钮(可能还有展开按钮) - return Column( - children: [ - // 展开按钮(在发送按钮上方) - if (shouldShowExpand && hasText) - Padding( - padding: - EdgeInsets.only(bottom: 40.h), - child: GestureDetector( - onTap: () { - // 打开底部弹出编辑模式 + if (shouldShowSend) { + // 显示发送按钮(可能还有展开按钮) + return Column( + children: [ + // 展开按钮(在发送按钮上方) + if (shouldShowExpand && hasText) + Padding( + padding: + EdgeInsets.only(bottom: 40.h), + child: GestureDetector( + onTap: () { + // 打开底部弹出编辑模式 + controller.openFullScreenEdit(); + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: + Colors.transparent, + builder: (context) => + const FullScreenEditor(), + ).then((_) { + // 弹出页关闭时的回调 controller - .openFullScreenEdit(); - showModalBottomSheet( - context: context, - isScrollControlled: true, - backgroundColor: - Colors.transparent, - builder: (context) => - const FullScreenEditor(), - ).then((_) { - // 弹出页关闭时的回调 - controller - .closeFullScreenEdit(); - }); - }, - child: Container( - width: 32.w, - height: 32.w, - decoration: BoxDecoration( - color: isDarkMode - ? Colors.grey[700] - : Colors.grey[300], - borderRadius: - BorderRadius.circular( - 16.w), - ), - child: Icon( - Icons.expand_less, - color: isDarkMode - ? Colors.grey[300] - : Colors.grey[700], - size: 20.sp, - ), + .closeFullScreenEdit(); + }); + }, + child: Container( + width: 32.w, + height: 32.w, + decoration: BoxDecoration( + color: isDarkMode + ? Colors.grey[700] + : Colors.grey[300], + borderRadius: + BorderRadius.circular( + 16.w), + ), + child: Icon( + Icons.expand_less, + color: isDarkMode + ? Colors.grey[300] + : Colors.grey[700], + size: 20.sp, ), - ), - ), - // 发送按钮 - GestureDetector( - onTap: () => - controller.sendMessage(), - child: Container( - width: 44.w, - height: 44.w, - margin: EdgeInsets.symmetric( - vertical: 2.w), - decoration: BoxDecoration( - color: primaryColor, - borderRadius: - BorderRadius.circular(22.w), - ), - child: Icon( - Icons.send, - color: Colors.white, - size: 22.sp, ), ), ), - ], - ); - } else { - // 显示'+'选择按钮 - return Obx(() { - final isExpanded = controller - .isPlusOptionsExpanded.value; - return GestureDetector( - onTap: () { - controller.togglePlusOptions(); - }, + // 发送按钮 + GestureDetector( + onTap: () => controller.sendMessage(), child: Container( width: 44.w, height: 44.w, margin: EdgeInsets.symmetric( vertical: 2.w), decoration: BoxDecoration( - color: isDarkMode - ? Colors.grey[800] - : Colors.grey[200], + color: primaryColor, borderRadius: BorderRadius.circular(22.w), ), child: Icon( - isExpanded - ? Icons.close - : Icons.add, - color: isDarkMode - ? Colors.grey[300] - : Colors.grey[700], + Icons.send, + color: Colors.white, size: 22.sp, ), ), - ); - }); - } - }, - ), - ); - }), + ), + ], + ); + } else { + // 显示'+'选择按钮 + return Obx(() { + final isExpanded = controller + .isPlusOptionsExpanded.value; + return GestureDetector( + onTap: () { + controller.togglePlusOptions(); + }, + child: Container( + width: 44.w, + height: 44.w, + margin: EdgeInsets.symmetric( + vertical: 2.w), + decoration: BoxDecoration( + color: isDarkMode + ? Colors.grey[800] + : Colors.grey[200], + borderRadius: + BorderRadius.circular(22.w), + ), + child: Icon( + isExpanded + ? Icons.close + : Icons.add, + color: isDarkMode + ? Colors.grey[300] + : Colors.grey[700], + size: 22.sp, + ), + ), + ); + }); + } + }, + ), + ), ], ), diff --git a/lib/modules/home/views/home_view.dart b/lib/modules/home/views/home_view.dart index e51be533d..d24e726fb 100644 --- a/lib/modules/home/views/home_view.dart +++ b/lib/modules/home/views/home_view.dart @@ -96,6 +96,7 @@ class HomeView extends GetView { onWidgetLoad: () { controller.noviceGuideService.startHomeGuide(); }, + getOverlayPosition: IntroOverlayBuilder.overlayPosition, overlayBuilder: (params) { return IntroOverlayBuilder.aiBuilder( params, @@ -116,17 +117,7 @@ class HomeView extends GetView { IntroStepBuilder( group: 'home', order: 2, - getOverlayPosition: ({ - required Offset offset, - required Size screenSize, - required Size size, - }) { - return OverlayPosition( - top: 200.h, - width: screenSize.width, - crossAxisAlignment: CrossAxisAlignment.end, - ); - }, + getOverlayPosition: IntroOverlayBuilder.overlayPosition, overlayBuilder: (params) { return Obx( () => IntroOverlayBuilder.homeBuilder( @@ -730,14 +721,15 @@ class HomeView extends GetView { child: IntroStepBuilder( group: 'home', order: 3, - onHighlightWidgetTap: () { - Intro.of(Get.context!).dispose(); - controller.exploreAIFeatures(); - }, + getOverlayPosition: IntroOverlayBuilder.overlayPosition, overlayBuilder: (params) { return IntroOverlayBuilder.aiBuilder( params, '太棒了!你已成功唤醒小言,接下来试试让它帮你做件事吧', + onTap: () { + params.onFinish(); + controller.exploreAIFeatures(); + }, ); }, builder: (BuildContext context, GlobalKey key) => SizedBox(