基于 miniaudio 构建的高性能、跨平台 Flutter 低延迟音频插件。
专为实时应用设计:模拟器、游戏、VoIP 和合成器。
标准的 Flutter 音频插件(如 audioplayers 或 flutter_soloud)通常使用 推模式 (Push-Mode):
- Dart 代码生成或解码音频数据。
- 将数据“推”送到原生层。
- 原生层进行缓冲并在准备好时播放。
推模式在实时应用中的问题:
- 时钟漂移 (Clock Drift): 你的游戏循环运行在系统时钟(Time)上,而音频硬件运行在自己的晶振时钟(Sample Rate)上。这两个时钟永远无法完美同步。
- 后果:
- 如果 Dart 跑得太快 -> 缓冲区堆积 -> 高延迟 (声音滞后)。
- 如果 Dart 跑得太慢 -> 缓冲区耗尽 -> 爆音/卡顿 (Underrun)。
- 复杂的修复: 开发者通常尝试在 Dart 中使用
sleep()来阻塞线程以等待缓冲区消耗,但 Dart 是单线程模型(isolate),精确阻塞非常困难,会导致帧率不稳定。
miniaudio_ffi 采用了 拉模式 架构,这是模拟器(如 RetroArch)和专业音频工具的行业标准:
- 原生回调 (Native Callback): 操作系统音频驱动 (AAudio, CoreAudio, WASAPI) 会在需要数据时触发高优先级回调。“我现在需要 512 个采样。”
- 共享内存环形缓冲区 (Ring Buffer): 我们在 Dart 和 C 之间使用一个无锁环形缓冲区 (FIFO)。
- 流程:
- 原生端 (消费者): 唤醒,从环形缓冲区读取数据,然后休眠。这严格按照硬件采样率发生。
- Dart 端 (生产者): 你只需要往环形缓冲区写入数据。
为什么这能解决同步问题: 你不需要猜测时间,只需要检查 “环形缓冲区有多满?”。
- 如果缓冲区太满:等待 (Wait)。(让游戏循环慢下来)
- 如果缓冲区空了:加速 (Speed up)。(让模拟器跑快点)
这使得 音频硬件 成为你整个应用的 主时钟 (Master Clock),从而确保完美的音画同步和零延迟。
Dart 层:
游戏循环 / 模拟器→ 生成 Int16 采样 →write()→ 拷贝数据到 共享环形缓冲区。
原生层 (C):
音频驱动 (AAudio/CoreAudio/etc)→ 触发回调 →数据回调→ 从 共享环形缓冲区 读取 → 输出 PCM 数据到 扬声器。
- 零拷贝 (接近): 数据只从 Dart 拷贝一次到共享环形缓冲区。原生端直接读取此缓冲区。
- 无 JNI/MethodChannel 开销: 使用纯
dart:ffi调用,性能最大化。
- 极低延迟:可配置缓冲区大小(低至 ~10ms)。
- 跨平台支持:
- 🤖 Android: AAudio (高性能) / OpenSL ES (兼容)。
- 🍎 iOS/macOS: CoreAudio / AudioUnit。
- 🪟 Windows: WASAPI。
- 🐧 Linux: ALSA / PulseAudio。
- 轻量级:无外部依赖。仅编译一个 C 头文件库。
- 简单 API:
start(),stop(),write(),dispose()。
在 pubspec.yaml 中添加 miniaudio_ffi:
dependencies:
dependencies:
flutter_miniaudio:
git: https://github.com/bill0015/flutter_miniaudio.git
# 如果是本地路径:
# path: packages/flutter_miniaudioimport 'package:flutter_miniaudio/flutter_miniaudio.dart';
import 'dart:ffi'; // 用于 Pointer 操作
// 1. 初始化
final player = MiniaudioPlayer(
sampleRate: 48000, // 标准采样率
channels: 2, // 立体声
bufferFrames: 1024 // 硬件缓冲区大小 (越小延迟越低)
);
// 2. 启动设备
player.start();
// 3. 生成并写入音频 (例如在循环中)
// 获取你的音频数据指针 (Pointer<Int16>)
player.write(audioDataPointer, frameCount);
// 4. 清理
player.dispose();以下是如何将游戏循环通过音频进行同步:
void gameLoop() {
// 1. 运行一帧模拟器/游戏
emulator.runFrame();
// 2. 将生成的音频推送到播放器
player.write(emulator.audioPointer, emulator.audioFrames);
// 3. 节流逻辑 (关键点)
// 检查缓冲区积压了多少音频。
// 如果积压超过 64ms,说明模拟器跑得比音频硬件快!
while (player.bufferLatency > 0.064) {
// 等待一小会儿,让音频硬件消耗掉一些数据。
// 在真实应用中,你可能会 sleep 1ms。
sleep(Duration(milliseconds: 1));
}
}| 属性/方法 | 描述 |
|---|---|
MiniaudioPlayer(...) |
构造函数。bufferFrames 控制硬件延迟。fifoCapacityFrames 控制环形缓冲区大小。 |
start() |
启动音频设备。 |
stop() |
暂停音频设备。 |
write(Pointer<Int16>, int) |
将原始 PCM 采样写入环形缓冲区。返回实际写入的帧数。 |
writeList(List<int>) |
辅助方法,从 Dart List 写入 (比 Pointer 慢)。 |
volume |
设置 主音量 (0.0 到 1.0). 默认为 1.0。 |
deviceSampleRate |
获取 实际硬件采样率 (如 48000)。用于检测是否发生重采样。 |
deviceChannels |
获取 实际硬件声道数。 |
framesConsumed |
自启动以来播放的总帧数。 |
fifoAvailable |
当前可以写入多少采样数据。 |
fifoAvailableFrames |
当前可以写入多少帧数据。 |
bufferLatency |
当前缓冲区积压的时长(秒),用于同步控制。 |
dispose() |
停止设备并释放原生资源。 |
对于游戏或 UI 音效,一般使用 MiniaudioEngine。它支持多轨混音、文件播放和 3D 空间音效。
final engine = MiniaudioEngine();
engine.start();
// 播放即忘的 UI 音效
engine.playOneShot("assets/click.wav");
// 播放背景音乐 (支持循环和控制)
final bgm = await engine.loadSound("assets/music.mp3");
bgm.looping = true;
bgm.volume = 0.5;
bgm.play();
// 3D 音效示例
final sfx = await engine.loadSound("assets/explosion.wav");
sfx.setPosition3D(10, 0, 0); // 在听者右侧
sfx.setFadeIn(0, 1.0, 48000); // 1秒钟淡入 (假设采样率48k)
sfx.play();| API | 描述 |
|---|---|
start() |
启动混音引擎。 |
stop() |
停止引擎。 |
playOneShot(path) |
播放一次性音效文件,自动释放。 |
loadSound(path) |
加载声音对象以进行精细控制。 |
| API | 描述 |
|---|---|
play(), stop() |
控制播放状态。 |
seekToFrame(int) |
跳转到指定 PCM 帧位置。 |
volume, pitch, pan |
基础属性控制。 |
setPosition3D(x,y,z) |
设置声源的 3D 空间位置。 |
setDirection(x,y,z) |
设置声源的朝向。 |
setVelocity(x,y,z) |
设置声源速度 (用于多普勒效应)。 |
setFadeIn(beg,end,len) |
自动音量淡入淡出。 |
dispose() |
必须调用 以释放原生内存。 |
| API | 描述 |
|---|---|
getPlaybackDevices() |
获取可用输出设备列表 (包含名称和 ID)。 |
getCaptureDevices() |
获取可用输入设备列表。 |
Miniaudio 支持强大的节点图系统。您可以串联 EQ、高通滤波器和分频器等特效。
默认情况下,声音直接连接到引擎的终点(扬声器)。您可以将其断开并重新连接到特效节点。
// 1. 创建节点 (例如 Peaking EQ)
final eq = engine.createPeakingEq();
eq.setParams(gainDB: 10, q: 1.0, frequency: 1000);
// 2. 将 EQ 连接到扬声器 (Master)
eq.connectTo(engine.master);
// 3. 加载声音 (默认自动连接到 Master)
final sound = await engine.loadSound("music.mp3");
// 4. 重新路由: Sound -> EQ -> Master
sound.detach();
sound.connectTo(eq);
sound.play();PeakingEqNode: 参量均衡器。LowShelfNode: 低频架式滤波器。HighShelfNode: 高频架式滤波器。LowPassFilterNode(LPF): 低通滤波器 (削减高频)。HighPassFilterNode(HPF): 高通滤波器 (削减低频)。DelayNode: 延迟/回声节点。ReverbNode: 混响节点。SplitterNode: 信号分离器 (通过connectTo(..., inputBusIndex)连接到不同输入)。