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

Flutter/Dart中如何将MIDI流(SoundFonts,sf2)录制为WAV等音频文件

Hey,我刚好做过类似的需求,来给你梳理下在Flutter/Dart环境下实现MIDI流(基于SoundFonts)录制为音频文件的完整方案,亲测好用的那种👇

先理清楚核心逻辑

要实现你的需求,其实需要三个关键环节:

  1. 用SoundFont把MIDI音符渲染成原始音频流(PCM格式)
  2. 捕获渲染后的音频数据(同时还要保证用户能听到声音)
  3. 将捕获的数据编码并保存为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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.09 09:02:47