使用SoLoud库的Flutter音频插件
使用 SoLoud 库和 FFI 的 Flutter 音频插件
| Linux | Windows | Android | MacOS | iOS | Web |
|---|---|---|---|---|---|
| ? | ? | ? | ? | ? | ? |
- 支持 Linux、Windows、Mac、Android 和 iOS
- 多声道,能够同时播放不同的声音,甚至可以将相同的声音叠加播放多次
- 包含语音合成器
- 支持多种常见格式,如 8、16 和 32 位 WAV、浮点 WAV、OGG、MP3 和 FLAC
- 支持实时检索音频 FFT 和波形数据
概述
flutter_soloud 插件使用了 fork 后的 SoLoud 仓库,其中 miniaudio 音频后端已更新,并作为 git 子模块位于 src/soloud 目录下。
为确保您拥有正确的依赖项,必须使用以下命令克隆此仓库
git clone --recursive https://github.com/alnitak/flutter_soloud.git 如果您已经克隆了仓库但未使用 recursive 选项,您可以导航到仓库目录并执行以下命令来更新 git 子模块
git submodule update --init --recursive 有关 SoLoud 许可证的信息,请参阅 此链接。
有3个示例
第一个是一个简单的用例。
第二个旨在展示频率和波形数据的可视化。文件 [Visualizer.dart] 在每次 tick 时使用 getAudioTexture2D 将新的音频数据存储到 audioData 中。
下面的视频演示了数据如何转换为图像(上方的小部件)并发送到着色器(中间的小部件)。下方的小部件在左侧使用 FFT 数据,在右侧使用由一系列黄色垂直容器表示的波形数据,其高度取自 audioData。
getAudioTexture2D 返回一个 512×256 的数组。每一行包含 256 个 FFT 数据和 256 个波形数据,这使得编写像频谱图(着色器 #8)或 3D 可视化(着色器 #9)这样的着色器成为可能。
着色器 1 到 7 仅使用 audioData 的一行。因此,生成用于着色器的纹理应该是 256×2 像素。第一行代表 FFT 数据,第二行代表波形数据。
由于每帧都需要进行许多操作,CPU 和 GPU 可能会承受压力,导致移动设备过热。将图像(使用 setImageSampler())发送到着色器似乎非常耗费资源。您可以通过禁用着色器小部件来观察这一点。
soloud6.mp4
第三个 示例演示了如何通过句柄管理声音:每个声音都必须在播放前加载。加载声音可能需要一些时间,并且不应该在游戏过程中进行,例如在游戏中。一旦声音加载完成,就可以播放,并且该声音的每个实例都将由其句柄标识。
该示例展示了如何播放背景音乐并多次播放枪声。
soloud6-B.mp4
用法
首先,必须初始化AudioIsolate
Future<bool> start() async{
final value = AudioIsolate().startIsolate();
if (value == PlayerErrors.noError) {
debugPrint('isolate started');
return true;
} else {
debugPrint('isolate starting error: $value');
return false;
}
}
成功启动后,可以加载声音
Future<SoundProps?> loadSound(String completeFileName) {
final load = await AudioIsolate().loadFile(completeFileName);
if (load.error != PlayerErrors.noError) return null;
return load.sound;
}
返回的 [SoundProps]
class SoundProps {
SoundProps(this.soundHash);
// the [hash] returned by [loadFile]
final int soundHash;
/// handles of this sound. Multiple instances of this sound can be
/// played, each with their unique handle
List<int> handle = [];
/// the user can listed ie when a sound ends or key events (TODO)
StreamController<StreamSoundEvent> soundEvents = StreamController.broadcast();
}
soundHash 和 handle 列表随后在 AudioIsolate() 类中使用。
AudioIsolate 实例
AudioIsolate 实例负责接收命令并将它们发送到单独的 Isolate,同时将结果返回到主 UI isolate。
| 功能 | 返回 | 参数 | 描述 |
|---|---|---|---|
| startIsolate | PlayerErrors | – | 启动音频 isolate 并监听来自它的消息。 |
| stopIsolate | 布尔值 | – | 停止循环、停止引擎并终止 isolate。当不再需要播放器或关闭应用程序时,必须调用此函数。 |
| isIsolateRunning | 布尔值 | – | 如果音频 isolate 正在运行,则返回 true。 |
| initEngine | PlayerErrors | – | 初始化音频引擎。默认值为:采样率 44100、缓冲区 2048 和 Miniaudio 音频后端。 |
| dispose | – | – | 停止音频引擎。 |
| loadFile | ({PlayerErrors error, SoundProps? sound}) | String fileName |
加载一个新声音,以便稍后播放一次或多次。 |
| play | ({PlayerErrors error, SoundProps sound, int newHandle}) | SoundProps sound, {double volume = 1,double pan = 0,bool paused = false,} |
播放已加载的声音,由 [sound] 标识。 |
| speechText | ({PlayerErrors error, SoundProps sound}) | String textToSpeech |
根据提供的文本进行语音合成。 |
| pauseSwitch | PlayerErrors | int handle |
暂停或取消暂停已加载的声音,由 [handle] 标识。 |
| getPause | ({PlayerErrors error, bool pause}) | int handle |
获取已加载的声音的暂停状态,由 [handle] 标识。 |
| stop | PlayerErrors | int handle |
停止已加载的声音,由 [handle] 标识,并将其清除。 |
| stopSound | PlayerErrors | int handle |
停止已加载的声音的所有句柄,由 [soundHash] 标识,并将其清除。 |
| getLength | ({PlayerErrors error, double length}) | int soundHash |
获取声音长度(秒)。 |
| seek | PlayerErrors | int handle, double time |
按秒定位播放。 |
| getPosition | ({PlayerErrors error, double position}) | int handle |
获取当前声音的位置(秒)。 |
| getIsValidVoiceHandle | ({PlayerErrors error, bool isValid}) | int handle |
检查句柄是否仍然有效。 |
| setVisualizationEnabled | – | bool enabled |
启用或禁用从 getFft、getWave、getAudioTexture* 获取数据。 |
| getFft | – | Pointer<Float> fft |
返回包含 FFT 数据的 256 个浮点数组。 |
| getWave | – | Pointer<Float> wave |
返回包含波形数据(幅度)的 256 个浮点数组。 |
| getAudioTexture | – | Pointer<Float> samples |
在 samples 中返回一个 512 个浮点的数组。– 前 256 个浮点表示 FFT 频率数据 [0.0 |
| getAudioTexture2D | – | Pointer<Pointer<Float>> samples |
返回一个 256×512 的浮点矩阵。每一行由 256 个 FFT 值和 256 个波形数据组成。每次调用时,都会将新的一行存储在第一行,并将所有之前的行向上移动(最后一行将被丢弃)。 |
| setFftSmoothing | – | double smooth |
平滑 FFT 数据。当读取新数据且值减小时,新值将与旧值和新值之间的幅度进行平滑。这将导致可视化效果不那么抖动。0 = 无平滑 1 = 完全平滑新值计算方式:newFreq = smooth * oldFreq + (1 - smooth) * newFreq |
PlayerErrors 枚举
| 名称 | 描述 |
|---|---|
| noError | 无错误 |
| invalidParameter | 某些参数无效 |
| fileNotFound | 文件未找到 |
| fileLoadFailed | 找到文件,但无法加载 |
| dllNotFound | DLL未找到或DLL错误 |
| outOfMemory | 内存不足 |
| notImplemented | 功能未实现 |
| unknownError | 其他错误 |
| backendNotInited | 播放器未初始化 |
| nullPointer | 空指针。当传递未初始化的指针(使用calloc())来检索 FFT 或波形数据时可能会发生此情况 |
| soundHashNotFound | 未找到指定哈希的声音 |
| isolateAlreadyStarted | 音频 isolate 已启动 |
| isolateNotStarted | 音频 isolate 尚未启动 |
| engineNotStarted | 引擎尚未启动 |
AudioIsolate() 包含一个 StreamController,目前只能用于获知声音句柄何时结束
StreamSubscription<StreamSoundEvent>? _subscription;
void listedToEndPlaying(SoundProps sound) {
_subscription = sound!.soundEvents.stream.listen(
(event) {
/// Here the [event.handle] of [sound] has naturally finished
/// and [sound.handle] doesn't contains [envent.handle] anymore.
/// Not passing here when calling [AudioIsolate().stop()]
/// or [AudioIsolate().stopSound()]
},
);
}
它还有一个 StreamController 来监控引擎何时启动或停止
AudioIsolate().audioEvent.stream.listen(
(event) {
/// event == AudioEvent.isolateStarted
/// or
/// event == AudioEvent.isolateStopped
},
);
贡献
要使用原生代码,需要从 Dart 到 C/C++ 的绑定。为了避免手动编写这些代码,它们是通过 package:ffigen 从头文件(src/ffi_gen_tmp.h)生成的,并临时存储在 lib/flutter_soloud_bindings_ffi_TMP.dart 中。您可以运行 dart run ffigen 来生成绑定。
由于我需要修改生成的 .dart 文件,我遵循了这个流程
- 将要生成的函数声明复制到
src/ffi_gen_tmp.h。 lib/flutter_soloud_bindings_ffi_TMP.dart文件将自动生成。- 将新函数的相关代码从
lib/flutter_soloud_bindings_ffi_TMP.dart复制到lib/flutter_soloud_bindings_ffi.dart。
此外,我还 fork 了 SoLoud 仓库并进行了修改,以包含最新的 Miniaudio 音频后端。该后端位于我的 fork 的 [new_miniaudio] 分支中,并且是默认设置。
项目结构
此插件使用以下结构
-
lib: 包含定义插件相对于所有平台 API 的 Dart 代码。 -
src: 包含原生源代码。Linux、Android 和 Windows 在各自的子目录中有自己的 CmakeFile.txt 文件,用于将代码构建为动态库。 -
src/soloud: 包含我 fork 的 SoLoud 源代码
调试
我在 .vscode 目录中提供了必要的设置,用于在 Linux 和 Windows 上调试原生 C++ 代码。要在 Android 上调试,请使用 Android Studio 并打开位于 example/android 目录中的项目。但是,我对在 Mac 和 iOS 上调试原生代码的过程不熟悉。
Linux
如果您遇到任何问题,可能是由 PulseAudio 引起的。要解决此问题,您可以尝试在 linux/src.cmake 文件中禁用 PulseAudio。找到行 add_definitions(-DMA_NO_PULSEAUDIO) 并取消注释它(现在是默认行为)。
Android
默认音频后端是 miniaudio,它将根据您的 Android 版本自动选择合适的音频后端
- Android 8.0 及更高版本使用 AAudio。
- 较旧 Android 版本使用 OpenSL|ES。
Windows
对于 Windows 用户,SoLoud 通过 DLL 使用 Openmpt,可以从 https://lib.openmpt.org/ 获取。如果您想使用此功能,请安装 DLL 并通过修改 windows/src.cmake 中的第一行来启用它。
Openmpt 作为一个模块播放引擎,能够重放各种多声道音乐格式(669、amf、ams、dbm、digi、dmf、dsm、far、gdm、ice、imf、it、itp、j2b、m15、mdl、med、mid、mo3、mod、mptm、mt2、mtm、okt、plm、psm、ptm、s3m、stm、ult、umx、wow、xm)。此外,它还可以加载 wav 文件,并且对 wav 文件的支持可能优于独立的 wav 音频源。
iOS
在模拟器上,Impeller 引擎不起作用(2023 年 7 月 20 日)。要禁用它,请运行以下命令:flutter run --no-enable-impeller 不幸的是,我没有真机可以测试。
Web
我付出了很多努力才使其在 Web 上正常工作!? 我已成功使用 Emscripten 编译了源代码。在 web 目录中,有一个脚本可以使用 CmakeLists.txt 文件自动完成编译过程。这将生成 libflutter_soloud_web_plugin.wasm 和 libflutter_soloud_web_plugin.bc。
最初,我尝试使用 wasm_interop 插件,但在加载和初始化 Module 时遇到了错误。
然后,我尝试使用 web_ffi,但它似乎已被弃用,因为它只支持旧的 dart:ffi API 2.12.0,这在这里无法使用。
待办事项
还有很多事情可以做。
FFT 数据与我的预期不符。在 src/analyzer.cpp 的 Analyzer::calcFFT() 中仍有一些工作需要完成。
![]() |
![]() |
|---|---|
| flutter_soloud 频谱 | audacity 频谱 |
目前,只实现了 SoLoud 提供的一部分功能。请在此 查看。
- 音频滤镜效果
- 3D 音频
- TED 和 SID 声音芯片模拟器(Commodore 64/plus)
- 噪声和波形生成等等,我想!

