Flutter中如何通过麦克风流识别音频音阶?
Flutter 麦克风流音频频率提取与音阶匹配问题
问题背景
我在Flutter项目中使用mic_stream包监听麦克风流,目标是识别音频对应的音阶。已知核心思路是获取音频频率后匹配音阶表,但当前代码计算出的结果范围异常,无法有效获取频率:
- 流长度固定为8192
- 采样值求和结果在90-106区间
- 现有公式计算出的频率值在0.0013-0.0019区间,播放不同歌曲时数值无明显变化
现有代码
Future<bool> _startListening() async { MicStream.shouldRequestPermission(true); stream = await MicStream.microphone( audioSource: AudioSource.DEFAULT, sampleRate: 48000, channelConfig: ChannelConfig.CHANNEL_IN_MONO, audioFormat: AudioFormat.ENCODING_PCM_16BIT); print( "Start Listening to the microphone, sample rate is ${await MicStream.sampleRate}, bit depth is ${await MicStream.bitDepth}, bufferSize: ${await MicStream.bufferSize}"); bytesPerSample = (await MicStream.bitDepth)! ~/ 8; samplesPerSecond = (await MicStream.sampleRate)!.toInt(); localMax = null; localMin = null; setState(() { _frequency = samplesPerSecond; isRecording = true; startTime = DateTime.now(); }); visibleSamples = []; listener = stream!.listen(_calculateSamples); StreamSubscription<List<int>> _listener = stream!.listen((samples) => _textFrequency = [ 'Total: ${samples.length}', 'Added: ${samples.reduce((a, b) => a + b)}', 'Added Frequency: ${int.parse(samples.reduce((a, b) => a + b).toString()) / (65536 * 10000)}' ].toString()); return true; }
问题分析
当前代码的频率计算逻辑完全错误:
- 直接对原始PCM采样值求和没有任何频率意义,PCM值是音频波形的振幅采样,求和结果仅能反映整体能量的大致情况,和频率无关
- 后续的除法公式也没有理论依据,自然无法得到有效频率值
解决方案步骤
1. 正确解析PCM采样数据
由于使用的是ENCODING_PCM_16BIT,每个采样是2字节的有符号整数,需要先把字节流转换成16位整数数组:
List<int> convertBytesToInt16(List<int> bytes) { List<int> int16Samples = []; for (int i = 0; i < bytes.length; i += 2) { // 小端模式解析16位有符号整数(多数设备采用) int sample = ((bytes[i + 1] << 8) | bytes[i]) - 32768; int16Samples.add(sample); } return int16Samples; }
2. 使用FFT计算频率
要提取音频的基频,需要对采样数据进行快速傅里叶变换(FFT)。可以使用audio_fft包实现:
- 添加依赖到
pubspec.yaml:
dependencies: audio_fft: ^0.0.1 # 可替换为最新版本
- 在监听流中执行FFT:
StreamSubscription<List<int>> _listener = stream!.listen((bytes) { List<int> samples = convertBytesToInt16(bytes); // 执行FFT计算幅度谱 final fft = AudioFFT(); final magnitudes = fft.computeFFT(samples); // 找到幅度最大的频率点索引 int maxIndex = magnitudes.indexOf(magnitudes.reduce((a, b) => a > b ? a : b)); // 计算对应的实际频率:频率 = (最大索引 × 采样率) / 采样点数 double frequency = (maxIndex * samplesPerSecond) / samples.length; _textFrequency = [ '采样点数: ${samples.length}', '基频: ${frequency.toStringAsFixed(2)} Hz' ].toString(); setState(() {}); });
3. 频率匹配音阶表
定义标准音阶频率表(以A4=440Hz为基准),计算当前频率与各音阶的误差,取误差最小的作为识别结果:
// 标准音阶频率表(示例,可扩展其他八度) final Map<String, double> noteFrequencies = { 'C4': 261.63, 'C#4': 277.18, 'D4': 293.66, 'D#4': 311.13, 'E4': 329.63, 'F4': 349.23, 'F#4': 369.99, 'G4': 392.00, 'G#4': 415.30, 'A4': 440.00, 'A#4': 466.16, 'B4': 493.88, }; String matchNote(double frequency) { if (frequency < 20) return '无有效音频'; // 过滤低频噪声 double minError = double.infinity; String matchedNote = ''; noteFrequencies.forEach((note, freq) { double error = (frequency - freq).abs(); if (error < minError) { minError = error; matchedNote = note; } }); return matchedNote; }
4. 优化建议
- 噪声过滤:忽略幅度低于阈值的频率点,避免环境噪声干扰
- 平滑处理:对连续几次识别的结果取平均值,提升稳定性
- 采样参数调整:如果48000采样率计算压力大,可尝试降低到22050或16000,减少计算量
内容的提问来源于stack exchange,提问作者mk117
相关产品推荐
相关产品推荐

