[Dart翻译]Dart、WASM和AssemblyScript--哦,我的天啊!

2,034 阅读6分钟

原文链接:medium.com/flutter-com…

原文作者:medium.com/@mksl

发布时间:2021年7月30日 - 9分钟阅读

简介

随着DFI在Dart中的出现,以及现在WebAssembly FFI的互通,为使用Dart在桌面操作系统(如Linux、Windows和MacOS)上创建丰富的、非GUI的应用程序,甚至在 "大型嵌入式 "设备(如Raspberry Pis)上创建应用程序提供了全新的可能性。

在这篇文章中,我想介绍一下我是如何利用全新的Dart WASM包,让我建立一个基于Dart的音频合成器的雏形,利用AssemblyScript开发的丰富的现有代码库,这可以作为如何利用Dart的AssemblyScript代码的工作范例。

从Synth1到as-audio

image.png

对我来说,这个特殊的旅程开始于我第一次看到Peter Salomonsen2020年2月做的这个伟大的演讲中关于创建基于浏览器的合成器音乐的奇妙工作。虽然Peter的AssemblyScript代码(编译为Wasm)非常酷,但它并不真正适合我已经开始工作的基于Dart硬件的音频实验项目,所以虽然我玩了一段有趣的在线演示,并从看演讲和看代码中学到了很多东西,但我继续做其他事情。

直到我碰巧遇到了正在开发的Dart Wasm软件包,这时我看到了一条道路,可以利用Peter优秀的wasm "synth1 "代码库的一部分。

虽然我想按原样使用synth1,但考虑到它是要在网络浏览器环境下运行的,这意味着有些部分与我正在做的事情并不相关,而其他部分,如midi处理和排序,我已经在自己的Dart代码中处理过了,所以我决定创建一个新的迷你项目,叫做as-audio,我可以把Synth1 AssemblyScript代码库中我需要的部分逐一引入,并轻松地建立一个独立的wasm二进制文件。

从Dart使用AssemblyScript Wasm

即使在发布前的状态下,wasm包也有很好的文档和使用由C++编译的wasm的例子,但可以理解的是没有关于如何使用由AssemblyScript编译的wasm的文档。因此,为了首先了解如何从Dart中使用AS wasm,我和亲爱的读者现在需要转移注意力,学习一下AssemblyScript本身。

汇编什么?

AssemblyScript将自己描述为 "为WebAssembly设计。AssemblyScript特别针对WebAssembly的功能集,让开发者对他们的代码进行低层次的控制"。就语法而言,我认为这种语言基本上是Typescript的一个子集,这个子集的重点是以适合编译成Webassembly的方式严格类型化。

从我的角度来看,AS的另一个优点是,由于我有C、Java、JS、Kotlin和Dart的背景,我发现它很容易上手,它确实有一个简单的设置工具链来编译成二进制wasm(正如它的网站上所宣传的那样,只需npm安装),重要的是,它在内存管理等方面也非常灵活。

鉴于这篇文章的重点是使用来自Dart的wasm,而这恰好是从AS中编译出来的,我不会进一步详细说明我如何设置我的as-audio项目来建立二进制的.wasm文件,而是引导感兴趣的读者去看as-audio的github repo

Dart和还没有Wasm

一旦我有了初始版本的as-audio设置来建立一个二进制的.wasm文件,我就准备开始尝试从Dart使用它。但在我开始调用Webassembly代码之前,我决定要有一个更简单的起始参考点,所以我首先为振荡器创建了一个接口。

abstract class Oscillator { 
  double next(); 
}

然后继续创建AS代码的Dart端口,用于类似音频合成代码的 "helloworld",一个正弦波振荡器。

class DartSineOscillator implements Oscillator {
  int position = 0;
  final double frequency;
  final sampleRate;

  DartSineOscillator(this.sampleRate, this.frequency);

  @override
  double next() {
    final ret = sin(pi * 2 * (position) / (1 << 16));
    position =
        (((position) + (frequency / sampleRate) * 0x10000).toInt()) & 0xffff;

    return ret;
  }
}

这意味着在我需要处理使用Webassembly的任何复杂问题之前,我现在有了一种方法来生成声音样本并测试它们的播放(不久之后会有更多的内容)。

一朵玫瑰的另一个名字

最后,在这一点上我可以尝试使用Dart的wasm模块。正如关于新包wasm的优秀公告/教程文章中所描述的那样,使用一个编译好的wasm二进制模块是非常简单的。

var wasmfile = Platform.script.resolve(wasmfilepath); 
var moduleData = File(wasmfile.path).readAsBytesSync(); 
WasmModule _wasmModule = WasmModule(moduleData); 
WasmInstance _instance = _wasmModule.builder().build();

同样,它显示在包中有一个非常好的方法来获得所有模块的进口和出口列表。

print(module.describe());

在我的as-audio模块的例子中,你得到了。

export global: var float32 SAMPLERATE 
export global: const int32 SineOscillator 
export function: int32 SineOscillator#get:position(int32) 
export function: void SineOscillator#set:position(int32, int32) export function: float32 SineOscillator#get:frequency(int32) 
export function: void SineOscillator#set:frequency(int32, float32) export function: float32 SineOscillator#next(int32) 
export function: int32 SineOscillator#constructor(int32) 
...

我稍后会回到SAMPLERATE,但现在如果你将上述内容与SineOscillator类的源AS进行比较。

export class SineOscillator { 
  position: u32 = 0; frequency: f32 = 0; 
  next(): f32 { 
    let ret = sin(PI * 2 * (this.position as f32) / (1 << 16 as f32)); 
    this.position = (((this.position as f32) + (this.frequency / SAMPLERATE) * 0x10000 as f32) as u32) & 0xffff; 
    return ret as f32; 
  }
}

有一件事你注意到了,在文章中的C代码 "Brotli "的例子中没有涉及到,如果你把源代码和导出的列表进行比较,就是导出的函数名称与AS代码中的不完全一致。这是因为WebAssembly没有OOP(面向对象编程)的概念,所以没有AS的类的概念。正因为如此,AS的编译器对这些名字进行了 "混搭",将类的名字和方法的名字都编码在结果的Wasm函数名中。

这个过程对于任何曾经处理过C到C++代码接口和C++编译器默认使用的类似名称处理的人来说应该是相当熟悉的,尽管在AS的情况下,名称处理方案是非常直接的,正如你在上面看到的那样。

当涉及到与AS编译的wasm和从C代码编译的wasm的接口时,另一件不同的事情是在导出的wasm函数中添加了额外的int32参数的奇怪情况。同样,对于有OOP背景的开发者来说,这可能很熟悉,因为这是一个 "经典 "的OOP技巧,即在对象的每个方法调用中添加一个隐式thisreference。但是你从哪里得到对象的引用呢?当然,这来自于调用对象类构造函数的返回值,所以在Dart中实例化并使用一个正弦振荡器类,你会有这样的想法。

final cons = helper.getMethod(oscClassname, 'constructor'); 
// calling a Assemblyscript constructor returns a i32 which is "reference" to the object created by it 
_oscObjectRef = cons(0);
_setFrequency = helper.getMethod(oscClassname, 'set:frequency');

而helper只是简单地编码了我们对AS名称混合方案的理解。

dynamic getMethod(String className, String methodName) { 
  return _instance.lookupFunction('$className#$methodName');
}

我故意把设置频率作为上面的例子,因为它也证明了AS对象属性没有任何魔力,AS编译器只是使用冒号分隔符的名称混杂惯例定义了隐含的getter和setter方法,这对Dart用户来说应该很熟悉,Dart做的事情几乎一样。更多关于AS对导出函数的名称处理的细节,包括如何定制它,可以在其非常好的文档中找到。

原生FFI绕道

当然,如果没有办法播放音频样本,生成的音频样本就没有什么用处,所以我需要一种方法将音频样本发送到我的电脑音频卡上。虽然已经有几个音频插件可以在Flutter应用程序中使用,但不幸的是,它们都集中在播放音频(通常是压缩音频)文件或网络流这种公认的更常见的用例上,而不是实时生成的小型音频样本集。

由于现有Flutter插件的上述限制,我之前已经开始为标准的Linux ALSA asound库构建了一个DFI包装器(FFI又来了!)。但是,虽然我确实得到了初步的播放工作,但我非常高兴地发现,在此期间,有人已经为libao做了同样的事情,它不仅为ALSA提供了一个更好/更简单的API,而且还为PulseAudio声音服务器以及其他一些操作系统(macos、windows等)提供了API,所以我决定利用它来播放。

使用libao包和音频样本播放一样简单,基本上包括一些初始参数的设置和打开播放设备。

const bits = 16; 
const channels = 2; 
const rate = 44100; 
final device = ao.openLive( driverId, bits: bits, channels: channels, rate: rate, matrix: 'R' );

接下来是我们之前在合成器代码中生成的音频样本缓冲区的实际播放。

for (var i = 0; i < rate; i++) {
    final sample = (osc.next() * volume * 32768.0).toInt();
    // Left = Right.
    buffer[4 * i] = buffer[4 * i + 2] = sample & 0xff;
    buffer[4 * i + 1] = buffer[4 * i + 3] = (sample >> 8) & 0xff;
  }

ao.play(device, buffer);

关于如何从Dart调用AS wasm代码,然后将其传递给libao进行播放的完整而简单的例子可以在这里找到。我在之前的文章中介绍了更多关于使用Dart FFI与本地库的情况。

全局也适合于客人

在解决了从Dart调用wasm函数的基本问题后,我最初遇到的一个障碍是,AS合成器的代码使用了 "客体球",而我发现在它最初的预发布状态下,wasm包并没有公开这方面的API。然而,让我感到非常惊喜的是,在我提出这个问题的几天内,这个功能就被Dart团队实现了!这让我感到非常高兴。

那么,什么是客人球状物,为什么我需要它们?

正如wasmer文档中所描述的那样,它们顾名思义是WebAssembly代码可以暴露给它的宿主环境来读/写的全局变量。在as-audio代码的情况下,为了让主机环境能够设置所使用的音频采样率,需要这样做。

export const SAMPLERATE: f32 = 44100;

不需要,对速度没有要求

虽然使用AS编译的wasm可能是合理的性能(虽然我还没有真正费心去做任何基准测试),我想指出我这样做的动机不是为了性能的原因,而是为了能够利用现有的、经过良好编写和测试的音频处理的代码体,我可以在我的Dart应用程序中使用。同样,正如Michael Thomsen在宣布wasm包的文章中指出的那样。

然而,由于C模块是特定于平台的,分发带有本地C模块的共享包是很复杂的:它需要一个通用的构建系统,或者分发多个二进制模块(每个所需平台一个)。如果一个单一的Wasm二进制汇编格式可以在所有的平台上使用,分发就会容易得多。那么,与其为每个目标平台将你的库编译成特定平台的二进制代码,你可以将它编译成一个Wasm二进制模块并在所有地方运行。这将有可能为在pub.dev上轻松发布包含本地代码的软件包打开大门。

这条路还在继续

我刚刚开始计划把Synth1的所有振荡器、滤波器、效果器甚至乐器都转移到as-audio上,然后暴露在Dart上,但是如果你想使用它或者跟踪我的进展,代码已经发布了,当然也欢迎你提出PR

虽然我在这里介绍了从Dart中使用AS生成的wasm的基本原理和一个基本的工作例子,但这对于在真正的合成器应用中的实际使用是不够的,因为所有的代码都运行在同一个单一的Dart运行循环中,并且使用了一个阻塞的音频输出API,这意味着如果我们试图在实时生成音频样本的同时连续播放音频,我们很快就会听到音频输出的严重故障。

为此,我们需要利用Dart Isolates来有效地使用多线程并发执行,这是我将在接下来的文章中介绍的内容,如果你想在文章中得到通知,请订阅。


原文发表于manichord.com。

在Twitter上关注Flutter社区,了解更多精彩文章:www.twitter.com/FlutterComm


www.deepl.com 翻译