import 'dart:async'; import 'dart:typed_data'; import 'package:record/record.dart'; import '../../core/utils/logger.dart'; import 'pcm_stream_player.dart'; /// 手机麦克风的 PCM 流采集,专供百炼上行使用。 /// /// `record` 的 `startStream` 回调块大小由平台决定(Android 通常几百字节到 2KB), /// 百炼建议每包 ~100ms(16k/16bit/单声道 = 3200 字节),所以这里做一层重新分包: /// 攒够 [_packetBytes] 再发一包,尾巴不足一包的在 [stop] 时补发。 class MicPcmStreamer { MicPcmStreamer({this.sampleRate = 16000}); static const String _tag = 'MicPcmStreamer'; final int sampleRate; /// 100ms 一包 int get _packetBytes => sampleRate * 2 ~/ 10; final AudioRecorder _recorder = AudioRecorder(); StreamSubscription? _sub; final _buffer = BytesBuilder(copy: false); final _packets = StreamController.broadcast(); /// 重新分包后的上行 PCM Stream get packets => _packets.stream; final _levelCtl = StreamController.broadcast(); /// 麦克风实时音量(0~1),UI 音浪用 Stream get level => _levelCtl.stream; bool _running = false; bool get isRunning => _running; Future hasPermission() => _recorder.hasPermission(); Future start() async { if (_running) return true; try { if (!await _recorder.hasPermission()) { Logger.w(_tag, '没有麦克风权限'); return false; } final stream = await _recorder.startStream( RecordConfig( encoder: AudioEncoder.pcm16bits, sampleRate: sampleRate, numChannels: 1, // 语音助手场景:开回声消除和降噪,否则外放播报会被自己的麦克风收回去, // duplex 模式下会直接把服务端 VAD 打乱 echoCancel: true, noiseSuppress: true, ), ); _running = true; _buffer.clear(); _sub = stream.listen( _onChunk, onError: (e) => Logger.e(_tag, '录音流错误: $e'), ); Logger.i(_tag, '麦克风采集启动 ${sampleRate}Hz'); return true; } catch (e) { Logger.e(_tag, '麦克风采集启动失败: $e'); _running = false; return false; } } void _onChunk(Uint8List chunk) { if (chunk.isEmpty) return; if (!_levelCtl.isClosed) _levelCtl.add(PcmStreamPlayer.rmsLevel(chunk)); _buffer.add(chunk); while (_buffer.length >= _packetBytes) { // BytesBuilder 只能整体取出,取出后把余量放回去 final all = _buffer.takeBytes(); var offset = 0; while (all.length - offset >= _packetBytes) { _emit(Uint8List.sublistView(all, offset, offset + _packetBytes)); offset += _packetBytes; } if (offset < all.length) { _buffer.add(Uint8List.sublistView(all, offset)); } } } void _emit(Uint8List packet) { if (_packets.isClosed) return; // sublistView 是原缓冲的视图,下游可能异步持有,复制一份避免被后续写入覆盖 _packets.add(Uint8List.fromList(packet)); } Future stop() async { if (!_running) return; _running = false; await _sub?.cancel(); _sub = null; try { await _recorder.stop(); } catch (e) { Logger.w(_tag, '停止录音异常: $e'); } // 尾包补发,否则最后不足 100ms 的语音会被丢掉 if (_buffer.length > 0) { _emit(_buffer.takeBytes()); } _buffer.clear(); if (!_levelCtl.isClosed) _levelCtl.add(0); } Future dispose() async { await stop(); await _packets.close(); await _levelCtl.close(); await _recorder.dispose(); } }