用于 Android 的 FluidSynth(译)

前言(by Leon Yeu)

最近的工作涉及到在 Android 上适配一个基于 GStreamer 的媒体组件。GStreamer 在播放 MIDI 时使用一个开源的 MIDI 软件合成器叫做 FluidSynth,因此调研了一下将 FluidSynth 移植到 Android 上的解决方案,并找到了一篇文章,感觉很有帮助。经原作者授权,将全文翻译并转载在本 Blog。
其中 NFluidSynth 相关的内容,由于本人已经许久没有摸过 .NET 相关的技术,原文就没有理解的非常清楚,如有翻译上的错误,敬请指出。

注意:本文为译文,如需转载或引用,请注明并附上英文原作的链接。

以下正文开始。

动机

我有一个不为人知的项目是 FluidSynth for Android。Android 从 6.0 开始支持自己的 MIDI API 以及基于 BLE 的 MIDI 连接,这就使得连接 MIDI 设备到 Android 成为了可能(如果想阅读我的日文文章,它是『Android Masters!』一书的一部分)。其中有一个技术上很酷炫的特性是支持虚拟 MIDI 设备服务,这样就像在其他操作系统(Windows、Mac、Linux、iOS)一样,任何人都可以实现一个 MIDI 设备服务,作为 MIDI 输入设备或输出设备(或同为二者)提供出来,其他应用程序就可以作为客户端连接上去。
这个特性被设计为可以通过服务运行一个软件 MIDI 合成器,所以为何不移植一个现成的方案呢?我这么想着,所以最终将 FluidSynth 搬到了 Android 上。

编译系统

这并不简单。首先,如果最终没有声音输出,这就毫无意义。 FluidSynth 作为一个软件合成器,支持各种音频 API,但并不包括 Android。Android 平台提供了两种音频接口:AudioTrack 和 OpenSL ES(在我进行实现的时候,还没有出现 AAudio 和 Oboe)。FluidSynth 的源码目录 drivers 中提供了音频抽象层,所以只需要在其中添加一个针对 Android 的实现就可以了,是么?

然而,知易行难。

首先,FluidSynth 需要使用 Android NDK 进行编译。原生的 FluidSynth 并不支持编译为 Android 版。这很可能是因为 FluidSynth 依赖于 glib,没有很简单的方法能使其构建在 Android 上。glib 是基于 autotools 进行构建的,和 NDK 不太对付。

但是等等,已经有一些已知的依赖 glib 的应用和库可以在 Android 上运行了。比如 Gimp,对吧?但它是运行在一些奇怪的 X server 上层的。那么,GStreamer 呢?它是支持 Android 的。但怎么构建呢?……于是我找到了 Cerbero 构建系统。它通过 autotools 的方式,为 Android 和其他平台构建包括 glib 在内的所有模块。

在 Cerbero 中,所有的依赖库都通过定制“recipe”来进行构建。Cerbero 的终极目标是构建 GStreamer,但可以通过添加 recipe 的方式编译任何软件。并且很容易就可以通过这种方式将 FluidSynth 添加到构建中。

但是为了支持一些自定义的 Android NDK 设置,我在原生基础上进行了一些修改,所以我的 Cerbero 构建树并不兼容原生,但最终可以编译出一个适用于 Android 平台的 libfluidsynth.so。

最终,所有的代码可以在如下的 Github 仓库查阅。
https://github.com/atsushieno/android-fluidsynth/

Android/OpenSLES 的实现

接下来,需要添加 opensles 的实现了。由于代码结构很清晰,所以很容易就可以添加一个 fluid_opensles.c。一个小问题是,只能找到 Victor Lazzarini 的一个参考示例他的 blog 曾是公开的,但现在不开放访问了……)。甚至在一本日本发行的 NDK 的书中找到的示例,也和它基本一样。

另外,我还需要为 SoundFont 文件实现一个支持自定义流的加载器。FluidSynth 只提供了一个基于文件名,通过本地文件 I/O 接口实现的加载器。所以我需要扩展 FluidSynth 来支持自定义的 SF 加载器——包括抽象 API 和基于 Android assets 的实现。

由于我的应用是用 C# 编写的,我将这些扩展添加到了我的 P/Invoke wrapper 里(如果是 Java 应用,那么就需要添加 JNI 调用)。

NFluidsynth 和 FluidsynthMidiService

FluidSynth是跨平台的,在 Linux 上运行良好,这使我更容易开发 C# 绑定。FluidSynth 本身可用作虚拟 MIDI 合成器,但要从系统的 MIDI 服务入口点桥接,我们必须使 FluidSynth 函数可调用,并映射这些入口点。

因此,我使用 P/Invoke 为 FluidSynth API 进行了绑定,发布在 https://github.com/atsushieno/nfluidsynth 。并使它成为 Xamarin。Android 的库几乎没花工夫。我只需要关心如何将 Android 特定的扩展构建在 Android 上。

最后,我在这个 NFluidsynth.Android 之上创建了一个 Android MIDI 设备服务。要构建这样的服务,我们需要实现 android.media.midi.MidiDeviceService。整个API在输入和输出方向上非常奇怪,但实现起来倒不是很困难。

通过这次尝试,我发现支持 Android MIDI API 几乎没有任何好处。我的 managed-midi API 受到了 Web MIDI API 的启发,作成了跨平台的,而且更适合为它实现 Android 后端。 因此我想出了两个实现:一个用于 MidiDeviceService,另一个用于我的 API。

最终,所有的代码可以在如下的 Github 仓库查阅。
https://github.com/atsushieno/fluidsynth-midi-service

CMake 的切换和 android-fluidsynth 移植

以上这些都是早些时候完成的,因此在 2018 年写这篇文章的时候,部分内容会显得有些奇怪。我原本以为会出现更多用于 Android 的软件合成器,但看来并没有。而我因为做过 android-fluidsynth 的移植,时而会被咨询相关问题。由于整个编译系统异常的复杂,所以绝大多数想尝试的人都会碰到钉子(对此深表同情)。

没有文档化的一个原因是,(对我而言)状况时常变化——当我开始这个项目的时候,是基于 autotools 并托管在 Sourceforge 上的。而现在已经迁移到 CMake 上,并托管在 Github

CMake 现在也是个问题——Cerbero 技术上支持 CMake,但似乎并不支持 Android。Cerbero 中可以指定自定义的工具链(例如 CC、LD 等)。但当你覆盖 CMake 配置(Android NDK 中有一个)指定的一些工具链时,会被识别为构建规格不一致,然后构建会被再次重启,而不使用指定的 CMake 配置(!)这使得我修改的构建选项被忽略掉,而构建最终会失败。目前我还没找到解决这个问题的办法。

摆脱 Cerbero 是一种方法,并且存在一些备选方案——为了解决 glib 依赖问题而引入了 Cerbero。为了去掉 Cerbero,我们必须找到办法来解决 glib 的构建问题。

例如 VolcanoMobile/fluidsynth-android 中移除了所有 glib 的依赖。但这会导致其与原生完全不兼容,所以并不是一个很完美的方案。不过,经过努力,还是将我的 OpenSLES 实现整合到了这个分支中。我正在考虑将二进制构建工件的基础切换到这个分支上。

其他还有诸如 Google cdep (我不是很喜欢他们在原始版本分叉,并添加自己的修改,这会导致合并时不必要的麻烦),或是整合 mono eglib 来替换 glib 的依赖。不过目前为止,我对以上这些 glib-less 的方案没什么特别感觉。

2018-03/12 更新:由于 eglib 没有提供 gthreads,所以这个方案不可行。

从用户观点来看有何不同

FluidSynth 针对不同平台,自动选择可能的选项进行构建。而 opensles 会在我的 Android 移植中作为默认的驱动。

SoundFont 加载器的 API 上有些变化,Android 开发人员会愿意去使用(上面稍微解释了一点)。除此之外,它和原生代码没有什么区别。