You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.28 05:43:17