使用SoLoud库的Flutter音频插件

使用 SoLoud 库和 FFI 的 Flutter 音频插件

style: very good analysis

Linux Windows Android MacOS iOS Web
? ? ? ? ? ?
  • 支持 Linux、Windows、Mac、Android 和 iOS
  • 多声道,能够同时播放不同的声音,甚至可以将相同的声音叠加播放多次
  • 包含语音合成器
  • 支持多种常见格式,如 8、16 和 32 位 WAV、浮点 WAV、OGG、MP3 和 FLAC
  • 支持实时检索音频 FFT 和波形数据

Buy Me A Coffee

概述

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();
}

soundHashhandle 列表随后在 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 启用或禁用从 getFftgetWavegetAudioTexture* 获取数据。
getFft Pointer<Float> fft 返回包含 FFT 数据的 256 个浮点数组。
getWave Pointer<Float> wave 返回包含波形数据(幅度)的 256 个浮点数组。
getAudioTexture Pointer<Float> samples samples 中返回一个 512 个浮点的数组。– 前 256 个浮点表示 FFT 频率数据 [0.01.0]。– 其他 256 个浮点表示波形数据(幅度)[-1.01.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 文件,我遵循了这个流程

  1. 将要生成的函数声明复制到 src/ffi_gen_tmp.h
  2. lib/flutter_soloud_bindings_ffi_TMP.dart 文件将自动生成。
  3. 将新函数的相关代码从 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.wasmlibflutter_soloud_web_plugin.bc

最初,我尝试使用 wasm_interop 插件,但在加载和初始化 Module 时遇到了错误。

然后,我尝试使用 web_ffi,但它似乎已被弃用,因为它只支持旧的 dart:ffi API 2.12.0,这在这里无法使用。

待办事项

还有很多事情可以做。

FFT 数据与我的预期不符。在 src/analyzer.cppAnalyzer::calcFFT() 中仍有一些工作需要完成。

spectrum1 spectrum2
flutter_soloud 频谱 audacity 频谱

目前,只实现了 SoLoud 提供的一部分功能。请在此 查看

  • 音频滤镜效果
  • 3D 音频
  • TED 和 SID 声音芯片模拟器(Commodore 64/plus)
  • 噪声和波形生成等等,我想!

GitHub

查看 Github