Suportado por todos os navegadores modernos, o WebAssembly (ou “Wasm”) está transformando a maneira como desenvolvemos experiências de usuário para a web. É um formato binário executável simples que permite que bibliotecas ou mesmo programas inteiros que foram escritos em outras linguagens de programação sejam executados no navegador da web.
Os desenvolvedores geralmente procuram maneiras de ser mais produtivos, como:
Para desenvolvedores de front-end, o WebAssembly fornece todos os três, respondendo à busca por uma IU de aplicativo da web que realmente rivaliza com a experiência móvel ou desktop nativa. Ele ainda permite o uso de bibliotecas escritas em linguagens não JavaScript, como C ++ ou Go!
Neste tutorial do Wasm / Rust, criaremos um aplicativo detector de afinação simples, como um afinador de guitarra. Ele usará os recursos de áudio integrados do navegador e executado a 60 quadros por segundo (FPS) - mesmo em dispositivos móveis. Você não precisa entender a API de áudio da web ou mesmo estar familiarizado com Ferrugem para acompanhar este tutorial; no entanto, espera-se conforto com JavaScript.
Nota: Infelizmente, no momento da redação deste artigo, a técnica usada neste artigo - específica para a API de áudio da Web - ainda não funciona no Firefox. Portanto, por enquanto, Chrome, Chromium ou Edge são recomendados para este tutorial, apesar do excelente suporte para Wasm e API de áudio da Web no Firefox.
AudioWorklet
API do navegador para processamento de áudio de alto desempenho no navegadorObservação: se você estiver mais interessado no 'como' do que no 'porquê' deste artigo, sinta-se à vontade para pular direto para o tutorial .
Existem vários motivos pelos quais pode fazer sentido usar WebAssembly:
A popularidade do WebAssembly certamente continuará a crescer; no entanto, não é adequado para todo o desenvolvimento da web:
Embora muitas linguagens de programação compilem para Wasm, escolhi Rust para este exemplo. Rust foi criado pela Mozilla em 2010 e está crescendo em popularidade. A ferrugem ocupa o melhor lugar para a 'linguagem mais amada' na pesquisa de desenvolvedor de 2020 da Stack Overflow. Mas as razões para usar Rust com WebAssembly vão além da mera tendência:
Os muitos benefícios do Rust também vêm com uma curva de aprendizado íngreme, portanto, escolher a linguagem de programação correta depende de uma variedade de fatores, como a composição da equipe que desenvolverá e manterá o código.
Já que estamos programando em WebAssembly com Rust, como podemos usar o Rust para obter os benefícios de desempenho que nos levaram ao Wasm em primeiro lugar? Para que um aplicativo com uma GUI de atualização rápida pareça “suave” para os usuários, ele deve ser capaz de atualizar a tela tão regularmente quanto o hardware da tela. Isso é normalmente 60 FPS, portanto, nosso aplicativo deve ser capaz de redesenhar sua interface do usuário em aproximadamente 16,7 ms (1.000 ms / 60 FPS).
Nosso aplicativo detecta e mostra a inclinação atual em tempo real, o que significa que o cálculo de detecção combinado e o desenho teriam que ficar dentro de 16,7 ms por quadro. Na próxima seção, tiraremos vantagem do suporte do navegador para analisar áudio em outro tópico enquanto o thread principal faz seu trabalho. Esta é uma grande vitória para o desempenho, desde computação e desenho, então cada têm 16,7 ms à sua disposição.
Neste aplicativo, usaremos um módulo de áudio WebAssembly de alto desempenho para realizar a detecção de pitch. Além disso, vamos garantir que o cálculo não seja executado no thread principal.
Por que não podemos manter as coisas simples e realizar a detecção de pitch no tópico principal?
Worklets de áudio da web permitem que os aplicativos continuem a atingir 60 FPS suaves porque o processamento de áudio não pode segurar o thread principal. Se o processamento de áudio for muito lento e ficar para trás, haverá outros efeitos, como áudio atrasado. No entanto, a UX permanecerá responsiva ao usuário.
Este tutorial presume que você tenha o Node.js instalado, bem como npx
. Se você não tiver npx
já, você pode usar npm
(que vem com o Node.js) para instalá-lo:
npm install -g npx
Para este tutorial Wasm / Rust, usaremos React.
Em um terminal, executaremos os seguintes comandos:
npx create-react-app wasm-audio-app cd wasm-audio-app
Isso usa npx
para executar o create-react-app
(contido no pacote correspondente mantido pelo Facebook) para criar um novo aplicativo React no diretório wasm-audio-app
.
create-react-app
é uma CLI para gerar aplicativos de página única (SPAs) baseados em React. Isso torna incrivelmente fácil iniciar um novo projeto com React. No entanto, o projeto de saída inclui código clichê que precisará ser substituído.
Primeiro, embora eu recomende fortemente o teste de unidade de seu aplicativo durante o desenvolvimento, o teste está além do escopo deste tutorial. Portanto, iremos em frente e excluiremos src/App.test.js
e src/setupTests.js
.
Haverá cinco componentes principais de JavaScript em nosso aplicativo:
public/wasm-audio/wasm-audio.js
contém ligações JavaScript para o módulo Wasm que fornece o algoritmo de detecção de pitch.public/PitchProcessor.js
é onde o processamento de áudio acontece. Ele é executado no thread de renderização de áudio da Web e consumirá a API Wasm.src/PitchNode.js
contém uma implementação de um nó Web Audio, que está conectado ao gráfico Web Audio e é executado no thread principal.src/setupAudio.js
usa APIs de navegador da web para acessar um dispositivo de gravação de áudio disponível.src/App.js
e src/App.css
compreendem a interface de usuário do aplicativo.
Vamos mergulhar direto no coração de nosso aplicativo e definir o código Rust para nosso módulo Wasm. Em seguida, codificaremos as várias partes de nosso JavaScript relacionado ao áudio da web e terminaremos com a IU.
Nosso código Rust calculará um tom musical a partir de uma série de amostras de áudio.
Você pode seguir essas instruções para construir a cadeia Rust para o desenvolvimento.
wasm-pack
permite construir, testar e publicar componentes WebAssembly gerados pelo Rust. Se você ainda não o fez, instalar wasm-pack .
cargo-generate
ajuda a colocar um novo projeto Rust em funcionamento, aproveitando um repositório Git preexistente como modelo. Usaremos isso para inicializar um analisador de áudio simples no Rust que pode ser acessado usando WebAssembly no navegador.
Usando o cargo
ferramenta que veio com a corrente Rust, você pode instalar cargo-generate
:
cargo install cargo-generate
Assim que a instalação (que pode levar vários minutos) for concluída, estamos prontos para criar nosso projeto Rust.
Na pasta raiz do nosso aplicativo, clonaremos o modelo de projeto:
$ cargo generate --git https://github.com/rustwasm/wasm-pack-template
Quando for solicitado um novo nome de projeto, inseriremos wasm-audio
.
No wasm-audio
diretório, agora haverá um Cargo.toml
arquivo com o seguinte conteúdo:
[package] name = 'wasm-audio' version = '0.1.0' authors = ['Your Name < [email protected] '] edition = '2018' [lib] crate-type = ['cdylib', 'rlib'] [features] default = ['console_error_panic_hook'] [dependencies] wasm-bindgen = '0.2.63' ...
Cargo.toml
é usado para definir um pacote Rust (que Rust chama de “engradado”), servindo uma função semelhante para aplicativos Rust que package.json
faz para aplicativos JavaScript.
O [package]
seção define metadados que são usados ao publicar o pacote para o oficial registro de pacote de ferrugem.
O [lib]
A seção descreve o formato de saída do processo de compilação do Rust. Aqui, “cdylib” diz ao Rust para produzir uma “biblioteca de sistema dinâmico” que pode ser carregada de outra linguagem (em nosso caso, JavaScript) e incluindo “rlib” diz ao Rust para adicionar uma biblioteca estática contendo metadados sobre a biblioteca produzida. Este segundo especificador não é necessário para nossos propósitos - ele auxilia no desenvolvimento de outros módulos Rust que consomem esta caixa como uma dependência - mas é seguro deixá-lo.
Em [features]
, pedimos a Rust para incluir um recurso opcional console_error_panic_hook
para fornecer funcionalidade que converte o mecanismo de erros não tratados do Rust (chamado de panic
) em erros de console que aparecem nas ferramentas de desenvolvimento para depuração.
Finalmente, [dependencies]
lista todas as caixas das quais este depende. A única dependência fornecida imediatamente é wasm-bindgen
, que fornece geração automática de ligações JavaScript para nosso módulo Wasm.
O objetivo deste aplicativo é ser capaz de detectar a voz de um músico ou o tom de um instrumento em tempo real. Para garantir que isso seja executado o mais rápido possível, um módulo WebAssembly é encarregado de calcular o pitch. Para detecção de tom de voz única, usaremos o método de tom 'McLeod' que é implementado no Rust existente pitch-detection
biblioteca.
Muito parecido com o gerenciador de pacotes Node.js (npm), o Rust inclui um gerenciador de pacotes próprio, chamado Cargo. Isso permite instalar facilmente os pacotes que foram publicados no registro da caixa do Rust.
Para adicionar a dependência, edite Cargo.toml
, adicionando a linha para pitch-detection
para a seção de dependências:
[dependencies] wasm-bindgen = '0.2.63' pitch-detection = '0.1'
Isso instrui o Cargo a baixar e instalar o pitch-detection
dependência durante o próximo cargo build
ou, como estamos visando WebAssembly, isso será realizado no próximo wasm-pack
.
Primeiro, adicionaremos um arquivo que define um utilitário útil, cujo propósito discutiremos mais tarde:
Criar wasm-audio/src/utils.rs
e colar o conteúdo deste arquivo afim disso.
Substituiremos o código gerado em wasm-audio/lib.rs
com o seguinte código, que realiza a detecção de pitch por meio de um algoritmo de transformação rápida de Fourier (FFT):
use pitch_detection::{McLeodDetector, PitchDetector}; use wasm_bindgen::prelude::*; mod utils; #[wasm_bindgen] pub struct WasmPitchDetector { sample_rate: usize, fft_size: usize, detector: McLeodDetector, } #[wasm_bindgen] impl WasmPitchDetector { pub fn new(sample_rate: usize, fft_size: usize) -> WasmPitchDetector { utils::set_panic_hook(); let fft_pad = fft_size / 2; WasmPitchDetector { sample_rate, fft_size, detector: McLeodDetector::::new(fft_size, fft_pad), } } pub fn detect_pitch(&mut self, audio_samples: Vec) -> f32 { if audio_samples.len() pitch.frequency, None => 0.0, } } }
Vamos examinar isso em mais detalhes:
#[wasm_bindgen]
wasm_bindgen
é uma macro Rust que ajuda a implementar a vinculação entre JavaScript e Rust. Quando compilado para WebAssembly, esta macro instrui o compilador a criar uma ligação JavaScript para uma classe. O código Rust acima será convertido em ligações JavaScript que são simplesmente thin wrappers para chamadas de e para o módulo Wasm. A leve camada de abstração combinada com a memória compartilhada direta entre JavaScript é o que ajuda o Wasm a oferecer um desempenho excelente.
#[wasm_bindgen] pub struct WasmPitchDetector { sample_rate: usize, fft_size: usize, detector: McLeodDetector, } #[wasm_bindgen] impl WasmPitchDetector { ... }
A ferrugem não tem um conceito de classes. Em vez, os dados de um objeto é descrito por um struct
e seu comportamento por meio de impl
s ou trait
s.
Por que expor a funcionalidade de detecção de pitch por meio de um objeto em vez de uma função simples? Porque dessa forma, nós apenas inicializamos as estruturas de dados usadas pelo McLeodDetector interno uma vez , durante a criação do WasmPitchDetector
. Isso mantém o detect_pitch
funcionar rapidamente, evitando a dispendiosa alocação de memória durante a operação.
pub fn new(sample_rate: usize, fft_size: usize) -> WasmPitchDetector { utils::set_panic_hook(); let fft_pad = fft_size / 2; WasmPitchDetector { sample_rate, fft_size, detector: McLeodDetector::::new(fft_size, fft_pad), } }
Quando um aplicativo Rust encontra um erro do qual não pode se recuperar facilmente, é bastante comum invocar um panic!
macro. Isso instrui Rust a relatar um erro e encerrar o aplicativo imediatamente. Fazer uso de panics pode ser útil especialmente para o desenvolvimento inicial, antes que uma estratégia de tratamento de erros seja implementada, pois permite que você capture suposições falsas rapidamente.
Ligando utils::set_panic_hook()
uma vez durante a configuração, irá garantir que mensagens de pânico apareçam nas ferramentas de desenvolvimento do navegador.
Em seguida, definimos fft_pad
, a quantidade de preenchimento de zero aplicada a cada FFT de análise. O preenchimento, em combinação com a função de janelamento usada pelo algoritmo, ajuda a “suavizar” os resultados conforme a análise se move pelos dados de áudio amostrados de entrada. Usar um pad com metade do comprimento da FFT funciona bem para muitos instrumentos.
Finalmente, Rust retorna o resultado da última instrução automaticamente, então WasmPitchDetector
instrução struct é o valor de retorno de new()
.
O resto do nosso impl WasmPitchDetector
O código Rust define a API para detecção de argumentos de venda:
pub fn detect_pitch(&mut self, audio_samples: Vec) -> f32 { ... }
Esta é a aparência de uma definição de função de membro no Rust. Um membro público detect_pitch
é adicionado a WasmPitchDetector
. Seu primeiro argumento é uma referência mutável (&mut
) a um objeto instanciado do mesmo tipo contendo struct
e impl
campos, mas isso é passado automaticamente ao chamar, como veremos a seguir.
Além disso, nossa função de membro pega uma matriz de tamanho arbitrário de números de ponto flutuante de 32 bits e retorna um único número. Aqui, essa será a afinação resultante calculada nessas amostras (em Hz).
if audio_samples.len() O código acima detecta se amostras suficientes foram fornecidas para a função para uma análise de pitch válida a ser realizada. Caso contrário, a função Rust panic!
macro é chamada, o que resulta na saída imediata do Wasm e a mensagem de erro é impressa no console do navegador dev-tools.
let optional_pitch = self.detector.get_pitch( &audio_samples, self.sample_rate, POWER_THRESHOLD, CLARITY_THRESHOLD, );
Isso chama a biblioteca de terceiros para calcular o tom das amostras de áudio mais recentes. POWER_THRESHOLD
e CLARITY_THRESHOLD
pode ser ajustado para ajustar a sensibilidade do algoritmo.
Terminamos com um retorno implícito de um valor de ponto flutuante por meio do match
palavra-chave, que funciona de maneira semelhante a uma switch
declaração em outras línguas. Some()
e None
deixe-nos tratar os casos apropriadamente sem encontrar uma exceção de ponteiro nulo.
Criação de aplicativos WebAssembly
Ao desenvolver aplicativos Rust, o procedimento usual de construção é invocar uma construção usando cargo build
. No entanto, estamos gerando um módulo Wasm, então usaremos wasm-pack
, que fornece uma sintaxe mais simples ao direcionar o Wasm. (Também permite publicar as ligações JavaScript resultantes no registro npm, mas isso está fora do escopo deste tutorial.)
wasm-pack
suporta uma variedade de metas de construção. Como consumiremos o módulo diretamente de um worklet de Áudio da Web, direcionaremos o web
opção. Outros alvos incluem a construção de um bundler, como webpack ou para consumo de Node.js. Vamos executar isso a partir de wasm-audio/
subdiretório:
wasm-pack build --target web
Se for bem-sucedido, um módulo npm será criado em ./pkg
.
Este é um módulo JavaScript com seu próprio package.json
gerado automaticamente. Isso pode ser publicado no registro npm, se desejado. Para manter as coisas simples por enquanto, podemos simplesmente copiar e colar isso pkg
em nossa pasta public/wasm-audio
:
cp -R ./wasm-audio/pkg ./public/wasm-audio
Com isso, criamos um módulo Rust Wasm pronto para ser consumido pelo aplicativo web, ou mais especificamente, por PitchProcessor
.
2. Nosso PitchProcessor
Classe (com base no nativo AudioWorkletProcessor
)
Para este aplicativo, usaremos um padrão de processamento de áudio que recentemente ganhou ampla compatibilidade com o navegador. Especificamente, usaremos a API de áudio da web e executaremos cálculos caros em uma AudioWorkletProcessor
. Em seguida, criaremos o custom correspondente AudioWorkletNode
classe (que chamaremos de PitchNode
) como uma ponte de volta ao encadeamento principal.
Criar um novo arquivo public/PitchProcessor.js
e cole o seguinte código nele:
import init, { WasmPitchDetector } from './wasm-audio/wasm_audio.js'; class PitchProcessor extends AudioWorkletProcessor { constructor() { super(); // Initialized to an array holding a buffer of samples for analysis later - // once we know how many samples need to be stored. Meanwhile, an empty // array is used, so that early calls to process() with empty channels // do not break initialization. this.samples = []; this.totalSamples = 0; // Listen to events from the PitchNode running on the main thread. this.port.onmessage = (event) => this.onmessage(event.data); this.detector = null; } onmessage(event) { if (event.type === 'send-wasm-module') { // PitchNode has sent us a message containing the Wasm library to load into // our context as well as information about the audio device used for // recording. init(WebAssembly.compile(event.wasmBytes)).then(() => { this.port.postMessage({ type: 'wasm-module-loaded' }); }); } else if (event.type === 'init-detector') { const { sampleRate, numAudioSamplesPerAnalysis } = event; // Store this because we use it later to detect when we have enough recorded // audio samples for our first analysis. this.numAudioSamplesPerAnalysis = numAudioSamplesPerAnalysis; this.detector = WasmPitchDetector.new(sampleRate, numAudioSamplesPerAnalysis); // Holds a buffer of audio sample values that we'll send to the Wasm module // for analysis at regular intervals. this.samples = new Array(numAudioSamplesPerAnalysis).fill(0); this.totalSamples = 0; } }; process(inputs, outputs) { // inputs contains incoming audio samples for further processing. outputs // contains the audio samples resulting from any processing performed by us. // Here, we are performing analysis only to detect pitches so do not modify // outputs. // inputs holds one or more 'channels' of samples. For example, a microphone // that records 'in stereo' would provide two channels. For this simple app, // we use assume either 'mono' input or the 'left' channel if microphone is // stereo. const inputChannels = inputs[0]; // inputSamples holds an array of new samples to process. const inputSamples = inputChannels[0]; // In the AudioWorklet spec, process() is called whenever exactly 128 new // audio samples have arrived. We simplify the logic for filling up the // buffer by making an assumption that the analysis size is 128 samples or // larger and is a power of 2. if (this.totalSamples O PitchProcessor
é um companheiro de PitchNode
mas é executado em uma thread separada para que a computação do processamento de áudio possa ser realizada sem bloquear o trabalho feito na thread principal.
Principalmente, o PitchProcessor
:
- Lida com
'send-wasm-module'
evento enviado de PitchNode
compilando e carregando o módulo Wasm no worklet. Uma vez feito isso, ele permite PitchNode
saber enviando um 'wasm-module-loaded'
evento. Essa abordagem de retorno de chamada é necessária porque toda a comunicação entre PitchNode
e PitchProcessor
cruza um limite de rosca e não pode ser executado de forma síncrona. - Também responde ao
'init-detector'
evento de PitchNode
configurando o WasmPitchDetector
. - Processa amostras de áudio recebidas do gráfico de áudio do navegador, delega o cálculo de detecção de tom para o módulo Wasm e, em seguida, envia qualquer tom detectado de volta para
PitchNode
(que envia o pitch para a camada React por meio de seu onPitchDetectedCallback
). - Se registra sob um nome específico e exclusivo. Desta forma, o navegador sabe — por meio da classe base de
PitchNode
, o nativo AudioWorkletNode
—como instanciar nosso PitchProcessor
mais tarde, quando PitchNode
É construído. Consulte setupAudio.js
.
O diagrama a seguir visualiza o fluxo de eventos entre PitchNode
e PitchProcessor
:

Mensagens de eventos de tempo de execução.
3. Adicionar código de worklet de áudio da web
PitchNode.js
fornece a interface para nosso processamento de áudio de detecção de pitch personalizado. O PitchNode
objeto é o mecanismo pelo qual os pitches detectados usando o módulo WebAssembly trabalhando no AudioWorklet
thread fará seu caminho para o thread principal e React para renderização.
Em src/PitchNode.js
, criaremos uma subclasse do integrado AudioWorkletNode
da API de áudio da web:
export default class PitchNode extends AudioWorkletNode { /** * Initialize the Audio processor by sending the fetched WebAssembly module to * the processor worklet. * * @param {ArrayBuffer} wasmBytes Sequence of bytes representing the entire * WASM module that will handle pitch detection. * @param {number} numAudioSamplesPerAnalysis Number of audio samples used * for each analysis. Must be a power of 2. */ init(wasmBytes, onPitchDetectedCallback, numAudioSamplesPerAnalysis) { this.onPitchDetectedCallback = onPitchDetectedCallback; this.numAudioSamplesPerAnalysis = numAudioSamplesPerAnalysis; // Listen to messages sent from the audio processor. this.port.onmessage = (event) => this.onmessage(event.data); this.port.postMessage({ type: 'send-wasm-module', wasmBytes, }); } // Handle an uncaught exception thrown in the PitchProcessor. onprocessorerror(err) { console.log( `An error from AudioWorkletProcessor.process() occurred: ${err}` ); }; onmessage(event) { if (event.type === 'wasm-module-loaded') { // The Wasm module was successfully sent to the PitchProcessor running on the // AudioWorklet thread and compiled. This is our cue to configure the pitch // detector. this.port.postMessage({ type: 'init-detector', sampleRate: this.context.sampleRate, numAudioSamplesPerAnalysis: this.numAudioSamplesPerAnalysis }); } else if (event.type === 'pitch') { // A pitch was detected. Invoke our callback which will result in the UI updating. this.onPitchDetectedCallback(event.pitch); } } }
As principais tarefas realizadas por PitchNode
está:
- Envie o módulo WebAssembly como uma sequência de bytes brutos — aqueles passados de
setupAudio.js
—para o PitchProcessor
, que é executado no AudioWorklet
fio. É assim que o PitchProcessor
carrega o módulo Wasm de detecção de inclinação. - Gerenciar o evento enviado por
PitchProcessor
quando ele compila o Wasm com sucesso e envia outro evento que passa informações de configuração de detecção de pitch para ele. - Trate os pitches detectados conforme eles chegam do
PitchProcessor
e encaminhá-los para a função UI setLatestPitch()
via onPitchDetectedCallback()
.
Nota: Este código do objeto é executado no thread principal, portanto, ele deve evitar a execução de processamento adicional em pitches detectados, caso seja caro e cause quedas na taxa de quadros.
4. Adicione o código para configurar o áudio da web
Para que o aplicativo da web acesse e processe a entrada ao vivo do microfone da máquina cliente, ele deve:
- Obtenha a permissão do usuário para que o navegador acesse qualquer microfone conectado
- Acesse a saída do microfone como um objeto de stream de áudio
- Anexe o código para processar as amostras de fluxo de áudio de entrada e produzir uma sequência de tons detectados
Em src/setupAudio.js
, faremos isso e também carregaremos o módulo Wasm de forma assíncrona para que possamos inicializar nosso PitchNode com ele, antes de anexar nosso PitchNode:
import PitchNode from './PitchNode'; async function getWebAudioMediaStream() { if (!window.navigator.mediaDevices) { throw new Error( 'This browser does not support web audio or it is not enabled.' ); } try { const result = await window.navigator.mediaDevices.getUserMedia({ audio: true, video: false, }); return result; } catch (e) { switch (e.name) { case 'NotAllowedError': throw new Error( 'A recording device was found but has been disallowed for this application. Enable the device in the browser settings.' ); case 'NotFoundError': throw new Error( 'No recording device was found. Please attach a microphone and click Retry.' ); default: throw e; } } } export async function setupAudio(onPitchDetectedCallback) { // Get the browser audio. Awaits user 'allowing' it for the current tab. const mediaStream = await getWebAudioMediaStream(); const context = new window.AudioContext(); const audioSource = context.createMediaStreamSource(mediaStream); let node; try { // Fetch the WebAssembly module that performs pitch detection. const response = await window.fetch('wasm-audio/wasm_audio_bg.wasm'); const wasmBytes = await response.arrayBuffer(); // Add our audio processor worklet to the context. const processorUrl = 'PitchProcessor.js'; try { await context.audioWorklet.addModule(processorUrl); } catch (e) { throw new Error( `Failed to load audio analyzer worklet at url: ${processorUrl}. Further info: ${e.message}` ); } // Create the AudioWorkletNode which enables the main JavaScript thread to // communicate with the audio processor (which runs in a Worklet). node = new PitchNode(context, 'PitchProcessor'); // numAudioSamplesPerAnalysis specifies the number of consecutive audio samples that // the pitch detection algorithm calculates for each unit of work. Larger values tend // to produce slightly more accurate results but are more expensive to compute and // can lead to notes being missed in faster passages i.e. where the music note is // changing rapidly. 1024 is usually a good balance between efficiency and accuracy // for music analysis. const numAudioSamplesPerAnalysis = 1024; // Send the Wasm module to the audio node which in turn passes it to the // processor running in the Worklet thread. Also, pass any configuration // parameters for the Wasm detection algorithm. node.init(wasmBytes, onPitchDetectedCallback, numAudioSamplesPerAnalysis); // Connect the audio source (microphone output) to our analysis node. audioSource.connect(node); // Connect our analysis node to the output. Required even though we do not // output any audio. Allows further downstream audio processing or output to // occur. node.connect(context.destination); } catch (err) { throw new Error( `Failed to load audio analyzer WASM module. Further info: ${err.message}` ); } return { context, node }; }
Isso pressupõe que um módulo WebAssembly está disponível para ser carregado em public/wasm-audio
, o que foi feito na seção Rust anterior.
5. Defina a IU do aplicativo
Vamos definir uma interface de usuário básica para o detector de inclinação. Substituiremos o conteúdo de src/App.js
com o seguinte código:
import React from 'react'; import './App.css'; import { setupAudio } from './setupAudio'; function PitchReadout({ running, latestPitch }) { return ( {latestPitch ? `Latest pitch: ${latestPitch.toFixed(1)} Hz` : running ? 'Listening...' : 'Paused'} ); } function AudioRecorderControl() { // Ensure the latest state of the audio module is reflected in the UI // by defining some variables (and a setter function for updating them) // that are managed by React, passing their initial values to useState. // 1. audio is the object returned from the initial audio setup that // will be used to start/stop the audio based on user input. While // this is initialized once in our simple application, it is good // practice to let React know about any state that _could_ change // again. const [audio, setAudio] = React.useState(undefined); // 2. running holds whether the application is currently recording and // processing audio and is used to provide button text (Start vs Stop). const [running, setRunning] = React.useState(false); // 3. latestPitch holds the latest detected pitch to be displayed in // the UI. const [latestPitch, setLatestPitch] = React.useState(undefined); // Initial state. Initialize the web audio once a user gesture on the page // has been registered. if (!audio) { return ( { setAudio(await setupAudio(setLatestPitch)); setRunning(true); }} > Start listening ); } // Audio already initialized. Suspend / resume based on its current state. const { context } = audio; return ( { if (running) { await context.suspend(); setRunning(context.state === 'running'); } else { await context.resume(); setRunning(context.state === 'running'); } }} disabled={context.state !== 'running' && context.state !== 'suspended'} > {running ? 'Pause' : 'Resume'} ); } function App() { return ( Wasm Audio Tutorial ); } export default App;
E vamos substituir App.css
com alguns estilos básicos:
.App { display: flex; flex-direction: column; align-items: center; text-align: center; background-color: #282c34; min-height: 100vh; color: white; justify-content: center; } .App-header { font-size: 1.5rem; margin: 10%; } .App-content { margin-top: 15vh; height: 85vh; } .Pitch-readout { margin-top: 5vh; font-size: 3rem; } button { background-color: rgb(26, 115, 232); border: none; outline: none; color: white; margin: 1em; padding: 10px 14px; border-radius: 4px; width: 190px; text-transform: capitalize; cursor: pointer; font-size: 1.5rem; } button:hover { background-color: rgb(45, 125, 252); }
Com isso, devemos estar prontos para executar nosso aplicativo, mas há uma armadilha a ser resolvida primeiro.
Tutorial de WebAssembly / Rust: Tão perto!
Agora, quando executamos yarn
e yarn start
, mude para o navegador e tente gravar áudio (usando o Chrome ou Chromium, com as ferramentas do desenvolvedor abertas), encontramos alguns erros:

Os requisitos do Wasm têm amplo suporte - mas ainda não nas especificações do Worklet.
O primeiro erro, TextDecoder is not defined
, ocorre quando o navegador tenta executar o conteúdo de wasm_audio.js
. Isso, por sua vez, resulta na falha ao carregar o wrapper JavaScript do Wasm, o que produz o segundo erro que vemos no console.
A causa subjacente do problema é que os módulos produzidos pelo gerador de pacote Wasm do Rust assumem que TextDecoder
(e TextEncoder
) será fornecido pelo navegador. Essa suposição é válida para navegadores modernos quando o módulo Wasm está sendo executado a partir do thread principal ou mesmo de um thread de trabalho. No entanto, para worklets (como o contexto AudioWorklet
necessário neste tutorial), TextDecoder
e TextEncoder
ainda não fazem parte das especificações e, portanto, não estão disponíveis.
TextDecoder
é necessário para o gerador de código Rust Wasm para converter da representação plana e compactada de memória compartilhada do Rust para o formato de string que o JavaScript usa. Dito de outra forma, para ver as strings produzidas pelo gerador de código Wasm, TextEncoder
e TextDecoder
deve ser definido .
Esse problema é um sintoma da relativa novidade do WebAssembly. À medida que o suporte do navegador melhora para suportar padrões comuns de WebAssembly prontos para uso, esses problemas provavelmente desaparecerão.
Por enquanto, podemos contornar isso definindo um polyfill para TextDecoder
.
Criar um novo arquivo public/TextEncoder.js
e importe-o de public/PitchProcessor.js
:
import './TextEncoder.js';
Certifique-se de que import
instrução vem antes de wasm_audio
importar.
Finalmente, cole esta implementação para TextEncoder.js
(cortesia de @Yaffle no GitHub).
A pergunta do Firefox
Como mencionado anteriormente, a maneira como combinamos Wasm com worklets de áudio da Web em nosso aplicativo não funcionará no Firefox. Mesmo com o shim acima, clicar no botão “Começar a ouvir” resultará em:
Unhandled Rejection (Error): Failed to load audio analyzer WASM module. Further info: Failed to load audio analyzer worklet at url: PitchProcessor.js. Further info: The operation was aborted.
Isso porque o Firefox ainda não suporta importar módulos de AudioWorklets
—para nós, isso é PitchProcessor.js
em execução no AudioWorklet
fio.
O aplicativo concluído
Uma vez feito isso, simplesmente recarregamos a página. O aplicativo deve carregar sem erros. Clique em “Começar a ouvir” e permita que seu navegador acesse seu microfone. Você verá um detector de pitch muito básico escrito em JavaScript usando Wasm:

Detecção de pitch em tempo real.
Programação em WebAssembly com Rust: uma solução de áudio da web em tempo real
Neste tutorial, construímos um aplicativo da web do zero que executa processamento de áudio caro computacionalmente usando WebAssembly. O WebAssembly nos permitiu tirar vantagem do desempenho quase nativo do Rust para realizar a detecção de pitch. Além disso, este trabalho pode ser executado em outro thread, permitindo que o thread principal do JavaScript se concentre na renderização para oferecer suporte a taxas de quadros suaves, mesmo em dispositivos móveis.
Wasm / Rust e itens de áudio da web
- Os navegadores modernos fornecem captura e processamento de áudio (e vídeo) de alto desempenho dentro de aplicativos da web.
- Ferrugem tem ótimo ferramental para Wasm , o que ajuda a recomendá-lo como o idioma de escolha para projetos que incorporam WebAssembly.
- O trabalho de computação intensiva pode ser executado de forma eficiente no navegador usando o Wasm.
Apesar das muitas vantagens do WebAssembly, existem algumas armadilhas do Wasm a serem observadas:
- O ferramental para Wasm dentro dos worklets ainda está evoluindo. Por exemplo, precisávamos implementar nossas próprias versões da funcionalidade TextEncoder e TextDecoder necessária para a passagem de strings entre JavaScript e Wasm porque elas estavam faltando no
AudioWorklet
contexto. Isso, e importar ligações Javascript para o nosso suporte Wasm de um AudioWorklet
ainda não está disponível no Firefox. - Embora o aplicativo que desenvolvemos seja muito simples, construir o módulo WebAssembly e carregá-lo a partir do
AudioWorklet
necessária configuração significativa. Apresentar o Wasm aos projetos apresenta um aumento na complexidade das ferramentas, o que é importante ter em mente.
Para sua conveniência, este repositório GitHub contém o projeto final concluído. Se você também faz desenvolvimento de back-end, também pode estar interessado em usar Rust via WebAssembly em Node.js .
Leituras adicionais no blog de engenharia ApeeScape:
- API de áudio da web: Por que escrever quando você pode codificar?
- WebVR Parte 3: Revelando o potencial de WebAssembly e AssemblyScript
Compreender o básico
WebAssembly é um idioma?
WebAssembly é uma linguagem de programação - mas não uma linguagem que se destina a ser escrita diretamente por humanos. Em vez disso, ele é compilado a partir de outras linguagens de nível superior em um formato de bytecode binário compacto para transporte eficiente pela web e execução nos navegadores de hoje.
Para que serve o WebAssembly?
O WebAssembly permite que o software escrito em outras linguagens além do JavaScript seja executado perfeitamente no navegador. Isso permite que os desenvolvedores da web aproveitem os benefícios exclusivos de uma linguagem específica ou reutilizem as bibliotecas existentes por meio da conveniência e onipresença da web.
O que torna o WebAssembly rápido?
Os programas WebAssembly são mais rápidos de transferir para o navegador do que o JavaScript, pois usam uma representação binária compacta. Linguagens de alto desempenho como Rust também geralmente transpilam em bytecode Wasm de execução rápida.
O que está escrito em WebAssembly?
Os programas WebAssembly usam uma representação de bytecode binária compacta que torna a transferência pela web mais rápida do que o JavaScript. Este bytecode não se destina a ser escrito diretamente por humanos e, em vez disso, é gerado durante a compilação de código escrito em linguagem de nível superior, como C / C ++ ou Rust.
Para que é usada a linguagem de programação Rust?
O Rust tem um modelo de memória robusto, bom suporte à simultaneidade e uma pequena pegada de tempo de execução, tornando-o adequado para software de nível de sistema, como sistemas operacionais, drivers de dispositivo e programas incorporados. É também uma opção de WebAssembly poderosa para aplicativos da web com gráficos exigentes ou requisitos de processamento de dados.
Por que a ferrugem é tão rápida?
Os programas Rust são rápidos porque seu código é compilado para instruções otimizadas no nível da máquina, e o Rust não usa a coleta de lixo, deixando aos programadores controle total sobre como a memória é usada. Isso resulta em um desempenho consistente e previsível.