Flutter Sound Recorder捕获音频RMS计算异常,疑数据捕获有误
Flutter Sound录制PCM16音频RMS计算异常问题排查
问题重现
使用Flutter Sound Recorder捕获单声道16kHz PCM16音频,实时计算RMS音量时无法区分静音与说话状态,但将原始数据通过FFmpeg转成WAV再转回PCM后,RMS结果恢复正常。
初始化录制器代码:
await myRecorder.startRecorder( toStream: recordingDataController.sink, codec: Codec.pcm16, numChannels: 1, sampleRate: 16000, );
实时计算RMS的代码:
recordingDataController.stream.listen((buffer) => {bufferToBufferData(buffer)}); void bufferToBufferData(FoodData buffer) { rawRecordingData.addAll(buffer.data.buffer.asInt16List()); _calculateVolume(); } // Calculate RMS for certain interval of samples void _calculateVolume({int interval = 800}) { for (int i = volume.length + 1; i < rawRecordingData.length ~/ interval; i++) { double sumOfSquares = 0; for (var element in rawRecordingData.sublist(i * interval, i * interval + interval)) { sumOfSquares += pow(element / 32767, 2); } double meanSquare = sumOfSquares / interval; double rms = sqrt(meanSquare); volume.add(rms); } }
FFmpeg转换后数据正常的处理代码:
// Raw recording data is Int16List from buffer and gives me the 'wrong' RMS Uint8List list = Uint8List.fromList(rawRecordingData); File file = File('$path/raw.bytes'); await file.create(); file.writeAsBytesSync(list); await FFmpegKit.execute('-f s16le -ar 16000 -i ${file.path} -y $path/rawtowav.wav'); await FFmpegKit.execute('-i $path/rawtowav.wav -f s16le -ar 16000 $path/data.bytes'); file = File('$path/data.bytes'); // This gives me a better RMS from the looks of it List<int> test = file.readAsBytesSync().buffer.asInt16List();
问题分析
1. 字节序(Endianness)不匹配
Flutter Sound输出的PCM16数据默认是小端字节序(Little-Endian),但直接使用buffer.data.buffer.asInt16List()转换时,会采用当前平台的主机字节序(部分大端平台会出现转换错误)。而FFmpeg在处理-f s16le参数时,明确指定了小端字节序,转换后的数据字节序被修正,因此RMS计算恢复正常。
2. RMS计算逻辑存在遗漏
_calculateVolume函数中,循环起始值i = volume.length + 1会跳过第一个完整的采样区间:
- 当
rawRecordingData首次积累到800个样本时,rawRecordingData.length ~/ interval = 1,此时i = 0 + 1 = 1,不满足i < 1,导致第一个区间(0-800)的RMS未被计算。 - 后续积累数据时,会持续跳过对应区间,导致音量数据不完整,无法准确区分静音与说话状态。
解决方案
修正字节序转换
明确使用小端字节序将Uint8List转换为Int16List:
void bufferToBufferData(FoodData buffer) { // 明确按小端字节序转换PCM16数据 final int16List = buffer.data.buffer.asByteData().getInt16List(0, buffer.data.length ~/ 2, Endian.little); rawRecordingData.addAll(int16List); _calculateVolume(); }
修复RMS计算逻辑
调整循环起始值为i = volume.length,确保所有完整的采样区间都被计算:
void _calculateVolume({int interval = 800}) { final totalIntervals = rawRecordingData.length ~/ interval; // 从已计算的下一个区间开始,避免重复计算 for (int i = volume.length; i < totalIntervals; i++) { double sumOfSquares = 0; final start = i * interval; final end = start + interval; for (var element in rawRecordingData.sublist(start, end)) { sumOfSquares += pow(element / 32767, 2); } double meanSquare = sumOfSquares / interval; double rms = sqrt(meanSquare); volume.add(rms); } }
额外优化建议
- 避免每次计算都遍历子列表,直接通过索引遍历减少内存开销:
void _calculateVolume({int interval = 800}) { final totalIntervals = rawRecordingData.length ~/ interval; for (int i = volume.length; i < totalIntervals; i++) { double sumOfSquares = 0; final start = i * interval; for (int j = start; j < start + interval; j++) { final sample = rawRecordingData[j] / 32767; sumOfSquares += sample * sample; } double meanSquare = sumOfSquares / interval; double rms = sqrt(meanSquare); volume.add(rms); } }
- 定期清理
rawRecordingData中已计算过的旧数据,防止内存占用过高:
void _calculateVolume({int interval = 800}) { final totalIntervals = rawRecordingData.length ~/ interval; for (int i = volume.length; i < totalIntervals; i++) { // ... 计算RMS逻辑 ... } // 清理已计算的区间数据 final processedSamples = volume.length * interval; if (processedSamples > 0) { rawRecordingData.removeRange(0, processedSamples); } }
内容的提问来源于stack exchange,提问作者Zak Albreht
相关产品推荐
相关产品推荐

