Flutter/Dart中如何将MIDI流(SoundFonts,sf2)录制为WAV等音频文件
Hey,我刚好做过类似的需求,来给你梳理下在Flutter/Dart环境下实现MIDI流(基于SoundFonts)录制为音频文件的完整方案,亲测好用的那种👇
先理清楚核心逻辑
要实现你的需求,其实需要三个关键环节:
- 用SoundFont把MIDI音符渲染成原始音频流(PCM格式)
- 捕获渲染后的音频数据(同时还要保证用户能听到声音)
- 将捕获的数据编码并保存为WAV/PCM-16/MP3格式的文件
具体实现步骤
1. 选对库处理MIDI + SoundFont渲染
首先得找个能加载sf2文件、渲染MIDI音符还能输出PCM数据的库,推荐两个靠谱的:
- tonic:纯Dart编写的MIDI合成器,直接生成PCM数据,适合自定义流程
- flutter_midi_pro:Flutter端封装好的库,支持SoundFont加载和MIDI播放,上手快
这里以tonic为例,灵活性更高:
首先在pubspec.yaml加依赖:
dependencies: tonic: ^0.4.0 audioplayers: ^4.0.0 # 用来播放音频给用户听 path_provider: ^2.0.0 # 用来获取文件保存路径
然后初始化合成器并加载SoundFont:
import 'package:tonic/tonic.dart'; import 'package:audioplayers/audioplayers.dart'; final synth = Synthesizer(); final audioPlayer = AudioPlayer(); bool isRecording = false; List<int> sessionPcmBuffer = []; // 存储整个会话的PCM数据 Future<void> initSynth() async { // 加载assets目录下的sf2文件 await synth.loadSoundFont('assets/your_soundfont.sf2'); // 设置音频回调:每次渲染出PCM数据时触发 synth.onAudioMidiReceived = (List<double> samples) { // 把double类型的样本转换成PCM-16位(int16) List<int> intSamples = samples .map((s) => (s * 32767).round().clamp(-32768, 32767)) .toList(); // 如果正在录制,就把数据追加到会话缓冲区 if (isRecording) { sessionPcmBuffer.addAll(intSamples); } // 同时播放音频给用户听 audioPlayer.playBytes( Int8List.fromList(intSamples), isLocal: true, sampleRate: 44100, // 要和合成器的采样率一致 ); }; }
2. 处理会话录制流程
针对你“每隔X时间触发播放音符,保存整个操作会话”的需求,写个简单的录制控制逻辑:
import 'dart:math'; import 'dart:async'; import 'package:path_provider/path_provider.dart'; Timer? noteTimer; // 开始录制:清空缓冲区,启动定时触发音符 void startRecording() { isRecording = true; sessionPcmBuffer.clear(); // 每隔2秒随机触发一个音符(你可以改成用户选择的音符) noteTimer = Timer.periodic(const Duration(seconds: 2), (timer) { if (!isRecording) { timer.cancel(); return; } // 随机选一个中央C附近的音符 final note = MidiNote(Random().nextInt(24) + 60); synth.noteOn(note, velocity: 127); // 500ms后停止音符 Future.delayed(const Duration(milliseconds: 500), () { synth.noteOff(note); }); }); } // 停止录制:编码并保存音频文件 Future<void> stopRecording() async { isRecording = false; noteTimer?.cancel(); // 先保存为WAV格式(最简单,无压缩) final wavBytes = _generateWavBytes(sessionPcmBuffer); final saveDir = await getApplicationDocumentsDirectory(); final wavFile = File('${saveDir.path}/midi_session.wav'); await wavFile.writeAsBytes(wavBytes); // 如果需要MP3,看下面的编码步骤 }
3. 编码为WAV/MP3格式
生成WAV文件
WAV就是PCM数据加个文件头,自己写个生成逻辑就行:
List<int> _generateWavBytes(List<int> pcmData) { final sampleRate = 44100; final channels = 2; final bitDepth = 16; final byteRate = sampleRate * channels * bitDepth ~/ 8; final blockAlign = channels * bitDepth ~/ 8; final dataSize = pcmData.length; final fileSize = 36 + dataSize; final wavHeader = <int>[ // RIFF标记 82, 73, 70, 70, // 文件大小 fileSize & 0xff, (fileSize >> 8) & 0xff, (fileSize >> 16) & 0xff, (fileSize >> 24) & 0xff, // WAVE标记 87, 65, 86, 69, // fmt子块 102, 109, 116, 32, 16, 0, 0, 0, 1, 0, channels, 0, sampleRate & 0xff, (sampleRate >> 8) & 0xff, (sampleRate >> 16) & 0xff, (sampleRate >> 24) & 0xff, byteRate & 0xff, (byteRate >> 8) & 0xff, (byteRate >> 16) & 0xff, (byteRate >> 24) & 0xff, blockAlign & 0xff, (blockAlign >> 8) & 0xff, bitDepth, 0, // data子块 100, 97, 116, 97, dataSize & 0xff, (dataSize >> 8) & 0xff, (dataSize >> 16) & 0xff, (dataSize >> 24) & 0xff, ]; return [...wavHeader, ...pcmData]; }
编码为MP3格式
MP3需要用LAME编码器,用lame_encoder库就行:
先加依赖:
dependencies: lame_encoder: ^0.1.0
然后编码逻辑:
import 'package:lame_encoder/lame_encoder.dart'; Future<void> encodeToMp3(List<int> pcmData) async { final encoder = LameEncoder( sampleRate: 44100, channels: 2, bitRate: 128, // 比特率,越高音质越好 ); await encoder.initialize(); List<int> mp3Bytes = []; // 分块编码(避免内存溢出) for (int i = 0; i < pcmData.length; i += 4096) { final chunk = pcmData.sublist( i, i + 4096 > pcmData.length ? pcmData.length : i + 4096 ); final mp3Chunk = await encoder.encodeChunk(chunk); mp3Bytes.addAll(mp3Chunk); } // 获取最后一段编码数据 final finalChunk = await encoder.finish(); mp3Bytes.addAll(finalChunk); // 保存MP3文件 final saveDir = await getApplicationDocumentsDirectory(); final mp3File = File('${saveDir.path}/midi_session.mp3'); await mp3File.writeAsBytes(mp3Bytes); }
几个注意事项
- 权限问题:如果是直接从合成器获取PCM数据,不需要麦克风权限;如果要捕获系统音频输出,Android需要额外的权限,推荐用合成器直接输出的方式,更省心
- 内存优化:如果录制长时间会话,别把所有PCM数据都存在内存里,可以分块写入文件,避免OOM
- 跨平台测试:不同平台的音频处理有细微差异,记得在Android、iOS上都测一遍
- SoundFont大小:尽量选体积合适的sf2文件,太大的话加载时间长,影响用户体验
内容的提问来源于stack exchange,提问作者AIX
相关产品推荐
相关产品推荐

