socialgekon.com
  • Principal
  • Talento Ágil
  • De Outros
  • Europa
  • Processo Interno
Web Front-End

WebAssembly / Rust Tutorial: Pitch-perfect Audio Processing

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:

  • Usar uma única base de código de aplicativo para várias plataformas de destino, mas fazer com que o aplicativo funcione bem em todas elas
  • Criar uma experiência do usuário suave e bonita na área de trabalho e ambientes móveis
  • Aproveitando o ecossistema de biblioteca de código aberto para evitar 'reinventar a roda' durante o desenvolvimento de aplicativos

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.

O que este tutorial de WebAssembly / Rust cobre

  • Criar uma função simples em Rust e chamá-la de JavaScript (via WebAssembly)
  • Usando o moderno AudioWorklet API do navegador para processamento de áudio de alto desempenho no navegador
  • Comunicação entre trabalhadores em JavaScript
  • Juntando tudo em um aplicativo básico do React

Observação: se você estiver mais interessado no 'como' do que no 'porquê' deste artigo, sinta-se à vontade para pular direto para o tutorial .

Por que Wasm?

Existem vários motivos pelos quais pode fazer sentido usar WebAssembly:

  • Ele permite a execução de código dentro do navegador que foi escrito em concebivelmente qualquer idioma .
    • Isso inclui fazendo uso de bibliotecas existentes (numérico, processamento de áudio, aprendizado de máquina etc.) que são escritos em outras linguagens que não JavaScript.
  • Dependendo da escolha do idioma usado, o Wasm é capaz de operar em velocidades quase nativas. Isso tem o potencial de trazer características de desempenho de aplicativos da web muito mais perto para experiências nativas para celular e desktop .

Por que não Sempre Usar Wasm?

A popularidade do WebAssembly certamente continuará a crescer; no entanto, não é adequado para todo o desenvolvimento da web:

  • Para projetos simples, usar JavaScript, HTML e CSS provavelmente entregará um produto funcional em menos tempo.
  • Navegadores mais antigos, como o Internet Explorer, não oferecem suporte direto ao Wasm.
  • Os usos típicos do WebAssembly requerem a adição de ferramentas, como um compilador de linguagem, ao conjunto de ferramentas. Se sua equipe prioriza manter o desenvolvimento e as ferramentas de integração contínua o mais simples possível, o uso do Wasm vai contra isso.

Por que um tutorial Wasm / Rust, especificamente?

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:

  • Em primeiro lugar, o Rust tem um pequeno tempo de execução, o que significa menos código é enviado para o navegador quando um usuário acessa o site, ajudando a manter a pegada do site baixa.
  • Rust tem excelente suporte Wasm, apoiando interoperabilidade de alto nível com JavaScript.
  • Rust fornece perto Desempenho de nível C / C ++ , ainda tem um modelo de memória muito seguro . Quando comparado a outras linguagens, o Rust executa verificações de segurança extras enquanto compila seu código, reduzindo enormemente o potencial de travamentos causados ​​por variáveis ​​vazias ou não inicializadas. Isso pode levar a um tratamento de erros mais simples e uma chance maior de manter um bom UX quando ocorrerem problemas imprevistos.
  • O resto é não coletado no lixo . Isso significa que o código Rust está totalmente no controle de quando a memória é alocada e limpa, permitindo consistente desempenho - um requisito fundamental em sistemas em tempo real.

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.

Desempenho de WebAssembly: Mantendo aplicativos da web suaves como seda

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.

Noções básicas de áudio da web

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?

  • O processamento de áudio geralmente exige muitos recursos de computação. Isso se deve ao grande número de amostras que precisam ser processadas a cada segundo. Por exemplo, a detecção do pitch de áudio de forma confiável requer a análise do espectro de 44.100 amostras a cada segundo.
  • A compilação JIT e a coleta de lixo de JavaScript acontecem no encadeamento principal e queremos evitar isso no código de processamento de áudio para um desempenho consistente.
  • Se o tempo gasto para processar um quadro de áudio consumisse significativamente o orçamento de quadros de 16,7 ms, o UX sofreria com a animação instável.
  • Queremos que nosso aplicativo funcione sem problemas, mesmo em dispositivos móveis de baixo desempenho!

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.

WebAssembly / Rust Tutorial: Primeiros passos

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

Crie um aplicativo da web

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.

Visão geral do aplicativo

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.

Um fluxograma para o aplicativo de detecção de pitch. Os blocos 1 e 2 são executados no thread de áudio da web. O Bloco 1 é o Detector de Pitch Wasm (Rust), no arquivo wasm-audio / lib.rs. O bloco 2 é Web Audio Detection + Communication, no arquivo PitchProcessor.js. Ele pede para o detector inicializar e o detector envia os tons detectados de volta para a interface do Web Audio. Os blocos 3, 4 e 5 são executados no encadeamento principal. O Bloco 3 é o Web Audio Controller, no arquivo PitchNode.js. Ele envia o módulo Wasm para PitchProcessor.js e recebe pitches detectados dele. O bloco 4 é a configuração de áudio da Web, em setupAudio.js. Ele cria um objeto PitchNode. O bloco 5 é a IU do aplicativo da Web, composta de App.js e App.css. Ele chama setupAudio.js na inicialização. Ele também pausa ou retoma a gravação de áudio enviando uma mensagem para PitchNode, a partir da qual recebe os tons detectados para exibir ao usuário.

Visão geral do aplicativo de áudio Wasm.

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.

1. Detecção de inclinação usando ferrugem e WebAssembly

Nosso código Rust calculará um tom musical a partir de uma série de amostras de áudio.

Obter ferrugem

Você pode seguir essas instruções para construir a cadeia Rust para o desenvolvimento.

Instale ferramentas para construir componentes de WebAssembly no Rust

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.

Crie Nosso Módulo WebAssembly

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.

Implementar um detector de inclinação em ferrugem

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.

Crie um detector de pitch que pode ser chamado de JavaScript no Rust

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:

Um fluxograma mais detalhado comparando as interações entre os objetos PitchNode e PitchProcess no tempo de execução. Durante a configuração inicial, PitchNode envia o módulo Wasm como um array de bytes para PitchProcessor, que os compila e os envia de volta para PitchNode, que finalmente responde com uma mensagem de evento solicitando que PitchProcessor inicialize a si mesmo. Durante a gravação de áudio, o PitchNode não envia nada e recebe dois tipos de mensagens de evento do PitchProcessor: Um pitch detectado ou um erro, se ocorrer no Wasm ou no worklet.

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:

  1. Obtenha a permissão do usuário para que o navegador acesse qualquer microfone conectado
  2. Acesse a saída do microfone como um objeto de stream de áudio
  3. 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:

Na linha 24 wasm_audio.js lá

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:

Uma captura de tela do aplicativo mostrando seu título,

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.

Apresentando Battlescripts: Bots, Ships, Mayhem!

Web Front-End

Apresentando Battlescripts: Bots, Ships, Mayhem!
O impacto do Brexit no setor de serviços financeiros

O impacto do Brexit no setor de serviços financeiros

Processos Financeiros

Publicações Populares
Melhores práticas de fusões e aquisições na América Latina
Melhores práticas de fusões e aquisições na América Latina
As 25 melhores predefinições do Lightroom Mobile para fotos impressionantes do iPhone
As 25 melhores predefinições do Lightroom Mobile para fotos impressionantes do iPhone
EUA: corrida para prefeito de Honolulu segue para segundo turno
EUA: corrida para prefeito de Honolulu segue para segundo turno
Como fazer autenticação JWT com um Angular 6 SPA
Como fazer autenticação JWT com um Angular 6 SPA
Como editar fotos no iPhone com iOS Photos
Como editar fotos no iPhone com iOS Photos
 
React Test-driven Development: From User Stories to Production
React Test-driven Development: From User Stories to Production
Criação de uma API REST segura em Node.js
Criação de uma API REST segura em Node.js
Um guia de campo para DesignOps
Um guia de campo para DesignOps
Donald Trump encontra o Papa Francisco no Vaticano, jura não esquecer sua mensagem
Donald Trump encontra o Papa Francisco no Vaticano, jura não esquecer sua mensagem
Automação no Selenium: Modelo de objeto de página e fábrica de página
Automação no Selenium: Modelo de objeto de página e fábrica de página
Categorias
NutriçãoVida DesignerGestão De EngenhariaPessoas E Equipes De ProdutoFamíliaCiclo De Vida Do ProdutoAprendendoFerramentas E TutoriaisKpis E AnálisesFuturo Do Trabalho

© 2023 | Todos Os Direitos Reservados

socialgekon.com