作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.< / div >< / div >
Peter是一名拥有15年网络和桌面应用开发经验的全栈开发者.
< / div >< / div >< / div >15
< / div >< / div >< / div >< / div >< / div >< / div >< / div >< / div >< / div >所有现代浏览器都支持, WebAssembly(或“Wasm”)正在改变我们为网络开发用户体验的方式. 它是一种简单的二进制可执行格式,允许用其他编程语言编写的库甚至整个程序在网络浏览器中运行.
开发人员经常寻找提高生产效率的方法,例如:
前端开发人员, WebAssembly提供了这三者, 满足用户对网页应用UI的需求,使其能够真正与原生手机或桌面体验相媲美. 它甚至允许使用非javascript语言(如c++或Go)编写的库!
在这个Wasm/生锈教程中,我们将创建一个简单的音调检测器应用程序,就像吉他调谐器一样. 它将使用浏览器内置的音频功能 以每秒60帧(FPS)运行——即使在移动设备上也是如此. 你不需要理解Web Audio API,甚至不需要熟悉 生锈 to follow along with 这 tutorial; however, comfort with JavaScript is expected.
Note: Unfortunately, as of 这 writing, 本文中使用的技术(特定于Web Audio api)还不能在Firefox中使用. 的refore, for the time being, 铬, 铬, 或Edge在本教程中推荐使用, 尽管Firefox支持出色的Wasm和Web Audio API.
AudioWorklet
浏览器的API,用于在浏览器中进行高性能的音频处理注意:如果您对本文的“如何”比对“为什么”更感兴趣, 请随意直接进入 the tutorial.
使用WebAssembly可能有几个原因:
的 popularity of WebAssembly will surely continue to grow; however, 它并不适合所有的网络开发:
虽然很多编程语言都可以编译成Wasm,但我选择了生锈作为这个例子. 生锈是由Mozilla在2010年创建的,并且越来越受欢迎. 生锈 occupies the 榜首 在Stack Overflow的2020年开发者调查中被评为“最受欢迎的语言”. 但是在WebAssembly中使用生锈的原因不仅仅是为了赶时髦:
生锈的许多好处也伴随着陡峭的学习曲线, 因此,选择正确的编程语言取决于多种因素, 比如开发和维护代码的团队组成.
因为我们是用WebAssembly和生锈编程的, 我们如何使用生锈来获得最初让我们选择Wasm的性能优势? 对于具有快速更新的GUI的应用程序,让用户感觉“流畅”, 它必须能够像屏幕硬件一样定期刷新显示. 这通常是60 FPS,所以我们的应用程序必须能够在~16帧内重新绘制它的用户界面.7毫秒(1,000毫秒/ 60 FPS).
我们的应用程序检测并实时显示当前的音高, 这意味着合并检测计算和绘图必须保持在16以内.7 ms per frame. 在下一节中,我们将利用浏览器支持在另一个线程上分析音频 而 主线程完成它的工作. 这对性能来说是一个重大的胜利,因为计算和绘图都是如此 每一个 有16个.7毫秒任他们支配.
在这个应用程序中, 我们将使用高性能WebAssembly音频模块来执行音高检测. 此外,我们将确保计算不会在主线程上运行.
为什么我们不能保持简单,在主线程上执行音高检测?
Web Audio worklets允许应用程序继续实现60 FPS的平滑速度,因为音频处理不能占用主线程. 如果音频处理太慢而落后, 还会有其他影响, 比如音频滞后. 然而,用户体验将保持对用户的响应.
本教程假设您有Node.Js安装,以及 npx
. If you don’t have npx
你已经可以使用 npm
(这是Node自带的.js) to install it:
npm install -g npx
在这个Wasm/生锈教程中,我们将使用反应.
在终端中,我们将运行以下命令:
NPX创建-react-app wasm-audio-app
cd wasm-audio-app
This uses npx
to execute the create-react-app
命令(包含在Facebook维护的相应包中)在该目录中创建一个新的反应应用程序 wasm-audio-app
.
create-react-app
是一个CLI,用于生成基于react的单页应用程序(spa)。. 这使得用反应开始一个新项目变得非常容易. 但是,输出项目包含需要替换的样板代码.
第一个, 尽管我强烈建议在整个开发过程中对应用程序进行单元测试, 测试超出了本教程的范围. 我们继续删除 src /应用程序.测试.js
和 src/setupTests.js
.
在我们的应用程序中将有五个主要的JavaScript组件:
公共/ wasm-audio / wasm-audio.js
包含JavaScript绑定到提供音高检测算法的Wasm模块.公共/ PitchProcessor.js
音频处理发生在哪里. 它运行在Web Audio渲染线程中,并将使用Wasm API.src/PitchNode.js
包含一个Web Audio节点的实现, 连接到Web音频图形并在主线程中运行.src/setupAudio.js
使用网络浏览器api访问可用的录音设备.src /应用程序.js
和 src /应用程序.css
包含应用程序用户界面.让我们直接深入到应用程序的核心,为Wasm模块定义生锈代码. 然后,我们将编写与Web音频相关的JavaScript的各个部分,并以UI结束.
我们的生锈代码将从一组音频样本中计算出一个音高.
You can follow these in结构体ions 构建生锈开发链.
wasm-pack
允许您构建、测试和发布rust生成的WebAssembly组件. 如果你还没有, install wasm-pack.
货物-generate
通过利用已有的Git存储库作为模板,帮助启动并运行新的生锈项目. 我们将使用它在生锈中引导一个简单的音频分析器,它可以在浏览器中使用WebAssembly访问.
Using the 货物
工具,你可以安装 货物-generate
:
货物安装,货物生成
一旦安装完成(可能需要几分钟), 我们已经准备好创建生锈项目了.
从我们的应用程序的根文件夹,我们将克隆项目模板:
$ 货物 generate—git http://github.com/rustwasm/wasm-pack-template
当提示输入新的项目名称时,我们将输入 wasm-audio
.
在 wasm-audio
目录,现在会有 货物.toml
包含以下内容的文件:
[包]
Name = "wasm-audio"
version = "0.1.0"
authors = ["Your Name
货物.toml
用来定义一个生锈包(生锈称之为“crate”), 为生锈应用程序提供类似的功能 包.json
用于JavaScript应用程序.
的 [包]
节定义将包发布到官方文件时使用的元数据 包 registry 生锈的.
的 (自由)
节描述生锈编译过程的输出格式. 在这里, “cdylib”告诉生锈生成一个“动态系统库”,该库可以从另一种语言加载(在我们的例子中), 包含“rlib”会告诉生锈添加一个包含生成库元数据的静态库. 对于我们的目的来说,第二个说明符不是必需的——它有助于开发更多的生锈模块,这些模块将这个crate作为依赖项来使用——但是保留它是安全的.
In [features]
,我们要求生锈包含一个可选特性 控制台_error_恐慌_hook
提供转换生锈的未处理错误机制的功能(称为 恐慌
)来控制出现在开发工具中的错误,以便进行调试.
最后, [dependencies]
列出这个依赖的所有板条箱. 唯一现成的依赖项是 wasm-bindgen
,它为Wasm模块提供了自动生成JavaScript绑定的功能.
这款应用的目的是能够实时检测音乐家的声音或乐器的音高. 以确保它能尽快执行, WebAssembly模块的任务是计算音高. 用于单音调检测, 我们将使用在现有生锈中实现的“McLeod”pitch方法 pitch-detection
图书馆.
Much like the Node.生锈包含了一个自己的包管理器,叫做货物. 这允许轻松地安装已经发布到生锈 crate注册表的包.
要添加依赖项,请编辑 货物.toml
,添加一行for pitch-detection
到依赖性部分:
[dependencies]
wasm-bindgen = "0.2.63"
Pitch-detection =“0”.1"
这将指示货物下载并安装 pitch-detection
依赖关系 货物 build
或者,因为我们的目标是WebAssembly,所以这将在下一个中执行 wasm-pack
.
首先,我们将添加一个文件来定义一个有用的实用程序,我们将在后面讨论它的用途:
创建 wasm-audio / src /跑龙套.rs
和 paste 这个文件的内容 进去.
我们将替换生成的代码 wasm-audio/lib.rs
使用以下代码, 它通过快速傅里叶变换(FFT)算法执行基音检测:
使用pitch_detection::{McLeodDetector, PitchDetector};
使用wasm_bindgen::序曲::*;
mod utils;
#[wasm_bindgen]
pub 结构体 WasmPitchDetector {
sample_rate: usize,
fft_size: usize,
探测器: McLeodDetector,
}
#[wasm_bindgen]
impl wasmpitch检测器{
pub fn new(sample_rate: usize, fft_size: usize) -> WasmPitchDetector {
跑龙套:set_恐慌_hook ();
让fft_pad = fft_size / 2;
WasmPitchDetector {
sample_rate,
fft_size,
探测器: McLeodDetector::::new(fft_size, fft_pad),
}
}
Pub fn detect_pitch()&无足轻重的人 自我, audio_样品: Vec) -> f32 {
if audio_样品.len() < 自我.fft_size {
恐慌!没有足够的样本传递给detect_pitch(). 期望一个包含{}元素的数组,但得到{}",自我.fft_size, audio_样品.len ());
}
//只包含超过功率阈值的注释
//信号中频率的幅度. 使用建议的默认值
// value of 5.0 from the 图书馆.
const POWER_THRESHOLD: f32 = 5.0;
//清晰度衡量的是一个音符的连贯程度. 为
//例如,拥挤房间的背景声音通常是
//音叉的清晰度较低,而音叉的清晰度较高.
//该阈值用于接受足够清晰的检测音符
//(有效值范围是0-1).
const CLARITY_THRESHOLD: f32 = 0.6;
让optional_pitch = 自我.探测器.get_pitch(
&audio_样品,
自我.sample_rate,
POWER_THRESHOLD,
CLARITY_THRESHOLD,
);
匹配optional_pitch {
Some(pitch) => pitch.frequency,
没有一个 => 0.0,
}
}
}
让我们更详细地检查一下:
#[wasm_bindgen]
wasm_bindgen
是一个生锈宏,帮助实现JavaScript和生锈之间的绑定. 编译为WebAssembly时, 这个宏指示编译器创建一个到类的JavaScript绑定. 上面的生锈代码将转换为JavaScript绑定,这些绑定只是对Wasm模块调用的瘦包装器. 轻量级抽象层与JavaScript之间的直接共享内存相结合,有助于Wasm提供出色的性能.
#[wasm_bindgen]
pub 结构体 WasmPitchDetector {
sample_rate: usize,
fft_size: usize,
探测器: McLeodDetector,
}
#[wasm_bindgen]
impl wasmpitch检测器{
...
}
生锈没有类的概念. 相反, 对象的数据 is described by a 结构体
和 its behaviour 通过 impl
年代或 特征
s.
为什么要通过对象而不是普通的函数来公开音高检测功能? 因为这样,我们只初始化内部McLeodDetector使用的数据结构 一次,在创建期间 WasmPitchDetector
. This keeps the detect_pitch
通过在操作期间避免昂贵的内存分配来快速运行.
pub fn new(sample_rate: usize, fft_size: usize) -> WasmPitchDetector {
跑龙套:set_恐慌_hook ();
让fft_pad = fft_size / 2;
WasmPitchDetector {
sample_rate,
fft_size,
探测器: McLeodDetector::::new(fft_size, fft_pad),
}
}
当生锈应用程序遇到无法轻松恢复的错误时, 调用 恐慌!
宏. 这指示生锈报告错误并立即终止应用程序. 在错误处理策略到位之前,利用恐慌可能特别有用,因为它允许您快速捕获错误假设.
调用 跑龙套:set_恐慌_hook ()
一旦在设置过程中,将确保在浏览器开发工具中出现恐慌消息.
Next, we define fft_pad
表示应用于每个分析FFT的补零量. 填充, 结合窗函数所使用的算法, 帮助“平滑”的结果,因为分析移动传入的采样音频数据. 使用一半FFT长度的衬垫对许多仪器都很有效.
最后,生锈自动返回最后一条语句的结果,所以 WasmPitchDetector
的返回值 新()
.
的 rest of our impl WasmPitchDetector
生锈代码定义了检测音调的API:
Pub fn detect_pitch()&无足轻重的人 自我, audio_样品: Vec) -> f32 {
...
}
这就是生锈中的成员函数定义. A public member detect_pitch
is added to WasmPitchDetector
. 它的第一个参数是一个可变引用(&无足轻重的人
)的实例化对象,该实例化对象包含 结构体
和 impl
字段——但这是在调用时自动传递的,我们将在下面看到.
In addition, 我们的成员函数接受一个任意大小的32位浮点数数组,并返回一个数字. 在这里,这将是这些样本计算的结果音高(以Hz为单位).
if audio_样品.len() < 自我.fft_size {
恐慌!没有足够的样本传递给detect_pitch(). 期望一个包含{}元素的数组,但得到{}",自我.fft_size, audio_样品.len ());
}
上面的代码检测是否有足够的样本提供给函数以进行有效的音高分析. If not, the 生锈 恐慌!
这将导致立即退出Wasm,并将错误消息打印到浏览器的开发工具控制台.
让optional_pitch = 自我.探测器.get_pitch(
&audio_样品,
自我.sample_rate,
POWER_THRESHOLD,
CLARITY_THRESHOLD,
);
这将调用第三方库来从最新的音频样本中计算音高. POWER_THRESHOLD
和 CLARITY_THRESHOLD
可以调整算法的灵敏度吗.
方法返回一个浮点值作为结束 匹配
关键字,其工作原理与 开关
其他语文声明. 一些()
和 没有一个
让我们适当地处理这些情况,而不会遇到空指针异常.
在开发生锈应用程序时,通常的构建过程是使用 货物 build
. 但是,我们正在生成一个Wasm模块,因此我们将使用 wasm-pack
,它在针对Wasm时提供了更简单的语法. (它还允许将生成的JavaScript绑定发布到npm注册表, 但这超出了本教程的范围.)
wasm-pack
支持各种构建目标. 因为我们将直接从Web Audio worklet中使用该模块,所以我们将针对 网络
option. 其他目标包括为网络pack之类的打包器构建或为Node使用构建.js. 我们从 wasm-audio/
subdirectory:
Wasm-pack构建——目标网络
如果成功,一个npm模块将在 ./包裹
.
这是一个JavaScript模块,有自己的自动生成功能 包.json
. 如果需要,可以将其发布到npm注册表中. 为了使事情保持简单,我们可以简单地复制并粘贴它 包裹
under our folder public/wasm-audio
:
cp - r ./wasm-audio/包裹 ./public/wasm-audio
With that, 我们已经创建了一个可供网络应用使用的生锈 Wasm模块, 或者更具体地说, by PitchProcessor
.
PitchProcessor
类(基于本地) AudioWorkletProcessor
)对于这个应用程序,我们将使用最近获得广泛浏览器兼容性的音频处理标准. 具体来说,我们将使用Web Audio API并在自定义中运行昂贵的计算 AudioWorkletProcessor
. 之后,我们将创建相应的自定义 AudioWorkletNode
类(我们称之为 PitchNode
)作为返回主线程的桥梁.
创建 a new file 公共/ PitchProcessor.js
并将以下代码粘贴到其中:
导入init, {WasmPitchDetector}./ wasm-audio wasm_audio.js";
类PitchProcessor扩展AudioWorkletProcessor {
con结构体or() {
超级();
//初始化为保存样本缓冲区的数组,以便稍后进行分析
//一旦我们知道需要存储多少样本. Mean而, an empty
//使用array,以便早期调用process()时使用空通道
//不破坏初始化.
这.样品 = [];
这.totalSamples = 0;
//监听运行在主线程上的PitchNode的事件.
这.港口.onmessage = (事件) => 这.onmessage(事件.数据);
这.探测器 = null;
}
onmessage(事件) {
if (事件.Type === "send-wasm-module") {
// PitchNode向我们发送了一个消息,其中包含要加载的Wasm库
//我们的上下文以及用于的音频设备的信息
// recording.
init(WebAssembly.compile(事件.wasmBytes)).then(() => {
这.港口.postMessage({type: 'wasm-module-loaded'});
});
} else if (事件.Type === 'init-探测器') {
const {sampleRate, numAudioSamplesPerAnalysis} =事件;
//保存这个值,因为我们以后会用它来检测是否有足够的记录
//我们第一次分析的音频样本.
这.numAudioSamplesPerAnalysis = numAudioSamplesPerAnalysis;
这.检测器= wasmpitch检测器.新(sampleRate numAudioSamplesPerAnalysis);
//保存我们将发送给Wasm模块的音频样本值的缓冲区
//定期分析.
这.样品 = new Array(numAudioSamplesPerAnalysis).填充(0);
这.totalSamples = 0;
}
};
流程(输入,输出){
//输入包含进一步处理的传入音频样本. 输出
//包含由我们执行的任何处理产生的音频样本.
//在这里,我们只执行分析来检测音高,所以不要修改
// 输出.
//输入保存一个或多个采样“通道”. 例如,麦克风
//记录“立体声”将提供两个通道. 对于这个简单的应用程序,
//我们使用假设“单声道”输入或“左”通道,如果麦克风是
// stereo.
const inputChannels =输入[0];
// inputSamples保存要处理的新样本数组.
const inputSamples = inputChannels[0];
//在AudioWorklet规范中,process()被调用时正好是128
//音频样本已经到达. 我们简化了填充的逻辑
//假设分析大小为128个样本或
//更大,是2的幂.
如果(这.totalSamples < 这.numAudioSamplesPerAnalysis) {
for (const sampleValue of inputSamples) {
这.样品[这.totalSamples++] = sampleValue;
}
} else {
//缓冲区已满. 我们不希望缓冲区持续增长,
// so将通过它“循环”采样,这样它总是
//保存长度为的最新有序样本
/ / numAudioSamplesPerAnalysis.
//将现有的样本移动到新样本的长度(128).
const numNewSamples = inputSamples.长度;
const numExistingSamples = 这.样品.length - numNewSamples;
for (let i = 0; i < numExistingSamples; i++) {
这.样品[i] = 这.[i + numNewSamples];
}
//将新样品添加到末端,放入由空出的128宽槽中
//前一个副本.
for (let i = 0; i < numNewSamples; i++) {
这.样品[numExistingSamples + i] = inputSamples[i];
}
这.totalSamples += inputSamples.长度;
}
//一旦我们的缓冲区有足够的样本,将它们传递给Wasm音调检测器.
如果(这.totalSamples >= 这.numAudioSamplesPerAnalysis && 这.探测器) {
const result = 这.探测器.detect_pitch(这.样品);
if (result !== 0) {
这.港口.postMessage({type: "pitch", pitch: result});
}
}
//返回true告诉音频系统继续运行.
return true;
}
}
registerProcessor(“PitchProcessor”,PitchProcessor);
的 PitchProcessor
是伴侣的吗 PitchNode
但是在一个单独的线程中运行,因此音频处理计算可以在不阻塞主线程上完成的工作的情况下执行.
Mainly, the PitchProcessor
:
"send-wasm-module"
事件 sent from PitchNode
通过编译Wasm模块并将其加载到worklet中. Once done, it lets PitchNode
know by sending a “wasm-module-loaded”
事件. 这个回调方法是必需的,因为所有的通信 PitchNode
和 PitchProcessor
跨越线程边界并且不能同步执行."init-探测器"
事件 from PitchNode
by configuring the WasmPitchDetector
.PitchNode
(它通过它的 onPitchDetectedCallback
).PitchNode
, the native AudioWorkletNode
-如何实例化我们的 PitchProcessor
later when PitchNode
is con结构体ed. 看到 setupAudio.js
.事件之间的事件流可视化 PitchNode
和 PitchProcessor
:
PitchNode.js
提供自定义音高检测音频处理的接口. 的 PitchNode
对象中工作的WebAssembly模块检测音调的机制 AudioWorklet
线程将进入主线程和反应进行渲染.
In src/PitchNode.js
,我们将创建内置的子类 AudioWorkletNode
Web Audio API:
导出默认类PitchNode扩展AudioWorkletNode {
/**
*通过发送获取的WebAssembly模块初始化音频处理器
*处理器工作表.
*
* @param {ArrayBuffer} wasmBytes表示整个的字节序列
* WASM模块,将处理音调检测.
* @param {number} numAudioSamplesPerAnalysis使用的音频样本数量
*每项分析. 一定是2的幂.
*/
init(wasmBytes, onPitchDetectedCallback, numAudioSamplesPerAnalysis) {
这.onPitchDetectedCallback = onPitchDetectedCallback;
这.numAudioSamplesPerAnalysis = numAudioSamplesPerAnalysis;
//监听来自音频处理器的消息.
这.港口.onmessage = (事件) => 这.onmessage(事件.数据);
这.港口.postMessage({
类型:“send-wasm-module”,
wasmBytes,
});
}
//处理PitchProcessor中抛出的未捕获异常.
onprocessorerror (err) {
控制台.日志(
AudioWorkletProcessor错误.进程()发生:${err} '
);
};
onmessage(事件) {
if (事件.类型=== 'wasm-module-loaded') {
// Wasm模块被成功发送到运行在
// AudioWorklet线程和编译. 这是我们配置音高的提示
// 探测器.
这.港口.postMessage({
类型:“init-探测器”,
sampleRate: 这.上下文.sampleRate,
numAudioSamplesPerAnalysis:这.numAudioSamplesPerAnalysis
});
} else if (事件.Type === "pitch") {
//检测到螺距. 调用我们的回调,这将导致UI更新.
这.onPitchDetectedCallback(事件.节);
}
}
}
执行的关键任务 PitchNode
是:
setupAudio.js
——的 PitchProcessor
,运行在 AudioWorklet
线程. This is how the PitchProcessor
加载音高检测Wasm模块.PitchProcessor
成功编译Wasm, 并向它发送另一个传递音高检测配置信息的事件.PitchProcessor
并将它们转发给UI函数 setLa测试Pitch()
通过 onPitchDetectedCallback ()
.注意:对象的代码在主线程上运行, 所以它应该避免在检测到的音调上执行进一步的处理,以防这是昂贵的并导致帧率下降.
为了让网络应用程序访问和处理来自客户端机器麦克风的实时输入, 它必须:
In src/setupAudio.js
, we’ll do that, 并且还异步加载Wasm模块,以便我们可以用它初始化我们的PitchNode, 在附加我们的PitchNode之前:
导入PitchNode./PitchNode";
getWebAudioMediaStream() {
if (!窗口.navigator.mediaDevices) {
throw new Error(
"此浏览器不支持网络音频或未启用."
);
}
尝试{
Const result =等待窗口.navigator.mediaDevices.getUserMedia({
audio: true,
video: false,
});
return result;
} catch (e) {
开关 (e.名字){
例“NotAllowedError”:
throw new Error(
"发现了一个录音设备,但是这个应用程序不允许使用. 在浏览器设置中启用设备."
);
例“NotFoundError”:
throw new Error(
“没有发现录音设备. 请附上麦克风,然后点击重试."
);
default:
throw e;
}
}
}
导出异步函数setupAudio(onPitchDetectedCallback) {
//获取浏览器音频. 等待用户“允许”它进入当前选项卡.
const mediaStream = await getWebAudioMediaStream();
Const 上下文 =新建窗口.AudioContext();
const audioSource = 上下文.createMediaStreamSource (mediaStream);
let 节点;
尝试{
//获取执行音调检测的WebAssembly模块.
Const响应=等待窗口.fetch(“wasm-audio / wasm_audio_bg.wasm”);
const wasmBytes =等待响应.arrayBuffer();
//将我们的音频处理器worklet添加到上下文.
const processorUrl = "PitchProcessor . php ".js";
尝试{
await 上下文.audioWorklet.addModule (processorUrl);
} catch (e) {
throw new Error(
在url: ${processorUrl}加载音频分析器工作失败. 进一步 info: ${e.message}`
);
}
//创建AudioWorkletNode,使JavaScript主线程能够
//与音频处理器通信(在Worklet中运行).
节点 = new PitchNode(上下文, "PitchProcessor");
/ / numAudioSamplesPerAnalysis指定连续的音频采样数
//基音检测算法计算每个工作单元. Larger values tend
//生成更精确的结果,但计算成本更高
//会导致在快速的段落中漏掉音符.e. 音符在哪里
// changing rapidly. 1024通常是效率和准确性之间的一个很好的平衡点
//用于音乐分析.
const numAudioSamplesPerAnalysis = 1024;
//将Wasm模块发送到音频节点,音频节点再将其传递给
// Worklet线程中运行的处理器. 另外,传递任何配置
// Wasm检测算法的参数.
节点.init(wasmBytes, onPitchDetectedCallback, numAudioSamplesPerAnalysis);
//将音频源(麦克风输出)连接到分析节点.
audioSource.connect(节点);
//将分析节点连接到输出. 即使我们不需要
// output any audio. 允许进一步的下游音频处理或输出到
// occur.
节点.connect(上下文.destination);
} catch (err) {
throw new Error(
加载音频分析器WASM模块失败. 进一步 info: ${err.message}`
);
}
返回{上下文, 节点};
}
这里假设有一个WebAssembly模块可供加载 public/wasm-audio
,这是我们在前面的生锈部分中完成的.
让我们为音高检测器定义一个基本的用户界面. 我们将替换的内容 src /应用程序.js
使用以下代码:
从“反应”导入反应;
进口 "./应用程序.css”;
导入{setupAudio}./setupAudio";
函数PitchReadout({running, la测试Pitch}) {
return (
{la测试Pitch
? 最新投球:${la测试Pitch.toFixed(1)} Hz`
: running
? "Listening..."
: "Paused"}
);
}
AudioRecorderControl() {
//确保音频模块的最新状态反映在UI中
//通过定义一些变量(和一个用于更新它们的setter函数)
//由反应管理,将其初始值传递给useState.
// 1. Audio是从初始音频设置返回的对象
//将用于根据用户输入启动/停止音频. 而
//在我们的简单应用程序中初始化一次,这很好
//让反应知道_d_可能改变的状态
// again.
const [audio, setAudio] = 反应.useState(定义);
// 2. Running保存应用程序当前是否正在记录和
//处理音频,并用于提供按钮文本(开始与停止).
const [running, setRunning] = 反应.useState(false);
// 3. la测试Pitch保存要显示的最新检测到的音调
// the UI.
const [la测试Pitch, setLa测试Pitch] = 反应.useState(定义);
// Initial 状态. 一旦用户在页面上做了手势,就初始化网络音频
//已注册.
if (!audio) {
return (
);
}
//音频已经初始化. 根据当前状态挂起/恢复.
Const {上下文} = audio;
return (
);
}
function 应用程序() {
return (
Wasm Audio Tutorial
);
}
ex港口 default 应用程序;
And we’ll replace 应用程序.css
有一些基本款式:
.应用{
display: flex;
flex-direction:列;
对齐项目:中心;
text-align: center;
background - color: # 282 c34;
min-height: 100vh;
color: white;
justify-content:中心;
}
.应用程序-header {
font-size: 1.5眼动;
margin: 10%;
}
.应用程序-content {
margin-top: 15vh;
height: 85vh;
}
.Pitch-readout {
margin-top: 5vh;
font-size: 3rem;
}
按钮{
Background-color: rgb(26,115,232);
border: none;
outline: none;
color: white;
margin: 1em;
padding: 10px 14px;
border-radius: 4px;
width: 190px;
首字母:大写;
cursor: pointer;
font-size: 1.5眼动;
}
button:hover {
Background-color: rgb(45,125,252);
}
有了这些,我们应该准备好运行我们的应用程序了——但是首先要解决一个陷阱.
Now when we run 纱
和 纱 start
, 切换到浏览器, 并尝试录制音频(使用铬或铬), 打开开发人员工具), 我们遇到了一些错误:
的 first error, TextDecoder没有定义
的内容时发生 wasm_audio.js
. 这又导致无法加载Wasm JavaScript包装器, 是什么产生了我们在控制台中看到的第二个错误.
这个问题的根本原因是生锈的Wasm包生成器生成的模块假设 TextDecoder
(和 TextEncoder
)将由浏览器提供. 当Wasm模块从主线程甚至工作线程运行时,这种假设适用于现代浏览器. 但是,对于worklet(例如 AudioWorklet
本教程中需要的上下文), TextDecoder
和 TextEncoder
还不是规范的一部分,因此不可用.
TextDecoder
需要由生锈 Wasm代码生成器从平面, 包装, 将生锈的共享内存表示转换为JavaScript使用的字符串格式. 换句话说,为了查看Wasm代码生成器生成的字符串, TextEncoder
和 TextDecoder
must be defined.
这个问题是WebAssembly相对较新的一个症状. 随着浏览器支持的改进,可以开箱即用地支持常见的WebAssembly模式, 这些问题可能会消失.
现在,我们可以通过定义一个polyfill来解决这个问题 TextDecoder
.
创建 a new file public/TextEncoder.js
和 进口 it from 公共/ PitchProcessor.js
:
进口 "./TextEncoder.js";
Make sure that 这 进口
语句出现在 wasm_audio
进口.
最后, paste 这 implementation 成 TextEncoder.js
(courtesy of @Yaffle on GitHub).
如前所述, 在我们的应用中,我们将Wasm与Web Audio worklets结合在一起的方式在Firefox中不起作用. 即使有了上面的提示,点击“开始收听”按钮将导致以下结果:
未处理的拒绝(错误):加载音频分析器WASM模块失败. 在url: PitchProcessor加载音频分析器工作程序失败.js. 进一步信息:操作被中止.
这是因为火狐浏览器 还不支持 从 AudioWorklets
—for us, that’s PitchProcessor.js
running in the AudioWorklet
线程.
完成后,我们只需重新加载页面. 应用程序应该没有错误加载. 点击“开始收听”,让浏览器访问麦克风. 你将看到一个用JavaScript编写的非常基本的音调检测器,使用Wasm:
In 这 tutorial, 我们已经从零开始构建了一个网络应用程序,它使用WebAssembly执行计算成本高昂的音频处理. WebAssembly允许我们利用生锈近乎原生的性能来执行音高检测. 进一步, 这项工作可以在另一个线程上执行, 允许主JavaScript线程专注于渲染以支持流畅的帧率,即使在移动设备上也是如此.
尽管WebAssembly有很多优点,但还是有一些Wasm的缺陷需要注意:
AudioWorklet
上下文. 然后从Javascript中导入Javascript绑定以支持Wasm AudioWorklet
在Firefox中还不可用.AudioWorklet
需要重要的设置. 在项目中引入Wasm确实会增加工具的复杂性, 记住哪一点很重要.为方便起见, 这 GitHub repo 包含最终完成的项目. 如果你也做后端开发,你可能也会对通过WebAssembly使用生锈感兴趣 within Node.js.
< / div >< / div >< / div >< / div >WebAssembly是一种编程语言,但不是一种旨在由人类直接编写的语言. 相反, 它是编译自其他, 更高级的语言变成了一个契约, 二进制字节码的形式,有效地通过网络传输和执行在今天的浏览器.
< / div >< / div >WebAssembly允许用JavaScript以外的语言编写的软件在浏览器中无缝运行. 这允许网络开发人员利用特定语言的独特优势,或者通过网络的便利性和普遍性来重用现有的库.
< / div >< / div >WebAssembly程序传输到浏览器的速度比JavaScript更快,因为它们使用紧凑的二进制表示. 像生锈这样的高性能语言通常也会被转换成快速运行的Wasm字节码.
< / div >< / div >WebAssembly程序使用紧凑的二进制字节码表示,这使得它在网络上的传输速度比JavaScript更快. 这个字节码不是由人类直接编写的,而是在编译用C/ c++或生锈等高级语言编写的代码时生成的.
< / div >< / div >生锈有一个健壮的内存模型, 良好的并发支持, 并且运行时占用空间小, 使其非常适合系统级软件,如操作系统, device drivers, 以及嵌入式程序. 对于具有苛刻图形或数据处理要求的网络应用程序,它也是一个强大的WebAssembly选项.
< / div >< / div >生锈程序很快,因为它们的代码编译为优化的机器级指令, 生锈不使用垃圾收集, 让程序员完全控制如何使用内存. 这将产生一致且可预测的性能.
< / div >< / div >< / div >< / div >Located in 达尼丁,奥塔哥,新西兰
Member since June 12, 2020
< / div >< / div >Peter是一名拥有15年网络和桌面应用开发经验的全栈开发者.
< / div >< / div >< / div >15
< / div >< / div >