WebAssembly/Pas Eğitimi: Mükemmel Ses İşleme

Yayınlanan: 2022-03-11

Tüm modern tarayıcılar tarafından desteklenen WebAssembly (veya “Wasm”), web için kullanıcı deneyimleri geliştirme şeklimizi değiştiriyor. Kitaplıkların ve hatta diğer programlama dillerinde yazılmış tüm programların web tarayıcısında çalışmasına izin veren basit bir ikili yürütülebilir formattır.

Geliştiriciler genellikle daha üretken olmanın yollarını ararlar, örneğin:

  • Birden çok hedef platform için tek bir uygulama kod tabanı kullanma, ancak uygulamanın hepsinde iyi çalışmasını sağlama
  • Masaüstü ve mobil ortamlarda sorunsuz ve güzel bir UX oluşturma
  • Uygulama geliştirme sırasında "tekerleği yeniden icat etmekten" kaçınmak için açık kaynaklı kitaplık ekosisteminden yararlanma

Ön uç geliştiriciler için WebAssembly, yerel mobil veya masaüstü deneyimine gerçekten rakip olan bir web uygulaması kullanıcı arabirimi aramasına yanıt vererek üçünü de sağlar. Hatta C++ veya Go gibi JavaScript olmayan dillerde yazılmış kitaplıkların kullanılmasına bile izin verir!

Bu Wasm/Rust eğitiminde, gitar tuner gibi basit bir perde algılayıcı uygulaması oluşturacağız. Tarayıcının yerleşik ses özelliklerini kullanacak ve mobil cihazlarda bile saniyede 60 kare (FPS) hızında çalışacak. Bu öğreticiyi takip etmek için Web Audio API'sini anlamanıza veya hatta Rust'a aşina olmanıza gerek yok; ancak JavaScript ile rahatlık beklenir.

Not: Ne yazık ki, bu yazı itibariyle, bu makalede kullanılan teknik - Web Ses API'sine özgü - henüz Firefox'ta çalışmıyor. Bu nedenle, Firefox'taki mükemmel Wasm ve Web Audio API desteğine rağmen, şimdilik bu eğitim için Chrome, Chromium veya Edge önerilir.

Bu Web Montajı/Pas Eğitimi Neleri Kapsar?

  • Rust'ta basit bir işlev oluşturma ve onu JavaScript'ten çağırma (WebAssembly aracılığıyla)
  • Tarayıcıda yüksek performanslı ses işleme için tarayıcının modern AudioWorklet API'sini kullanma
  • JavaScript'te çalışanlar arasında iletişim
  • Hepsini bir araya getirerek çıplak bir React uygulamasına dönüştürün

Not: Bu makalenin "neden"inden çok "nasıl"ı ile ilgileniyorsanız, doğrudan öğreticiye atlamaktan çekinmeyin.

Neden Wasm?

WebAssembly kullanmanın mantıklı olmasının birkaç nedeni vardır:

  • Muhtemelen herhangi bir dilde yazılmış olan kodun tarayıcı içinde yürütülmesine izin verir.
    • Bu, JavaScript dışındaki dillerde yazılmış mevcut kitaplıklardan (sayısal, ses işleme, makine öğrenimi vb.) yararlanmayı içerir.
  • Kullanılan dilin seçimine bağlı olarak Wasm, yerel hızlara yakın hızlarda çalışabilir. Bu, web uygulaması performans özelliklerini hem mobil hem de masaüstü için yerel deneyimlere çok daha yakın hale getirme potansiyeline sahiptir.

Neden Her Zaman Wasm Kullanmıyorsunuz?

WebAssembly'nin popülaritesi kesinlikle artmaya devam edecek; ancak, tüm web geliştirmeleri için uygun değildir:

  • Basit projeler için JavaScript, HTML ve CSS'ye bağlı kalmak, muhtemelen daha kısa sürede çalışan bir ürün sunacaktır.
  • Internet Explorer gibi eski tarayıcılar doğrudan Wasm'ı desteklemez.
  • WebAssembly'nin tipik kullanımları, araç zincirinize dil derleyici gibi araçların eklenmesini gerektirir. Ekibiniz geliştirmeyi ve sürekli entegrasyon araçlarını olabildiğince basit tutmaya öncelik veriyorsa, Wasm'ı kullanmak buna ters düşecektir.

Neden Özellikle Wasm/Pas Eğitimi?

Birçok programlama dili Wasm'ı derlerken ben bu örnek için Rust'ı seçtim. Rust, 2010 yılında Mozilla tarafından oluşturuldu ve popülaritesi artıyor. Rust, Stack Overflow'un 2020 geliştirici anketinde "en sevilen dil" için en üst sırada yer alıyor. Ancak Rust'ı WebAssembly ile kullanmanın nedenleri yalnızca trend olmanın ötesine geçer:

  • Her şeyden önce, Rust'ın küçük bir çalışma süresi vardır, bu da bir kullanıcı siteye eriştiğinde tarayıcıya daha az kod gönderildiği anlamına gelir ve bu da web sitesinin kapladığı alanı düşük tutmaya yardımcı olur.
  • Rust, JavaScript ile üst düzey birlikte çalışabilirliği destekleyen mükemmel Wasm desteğine sahiptir.
  • Rust, C/C++ düzeyine yakın performans sağlar, ancak yine de çok güvenli bir bellek modeline sahiptir. Diğer dillerle karşılaştırıldığında Rust, kodunuzu derlerken ekstra güvenlik kontrolleri gerçekleştirerek boş veya başlatılmamış değişkenlerin neden olduğu çökme olasılığını büyük ölçüde azaltır. Bu, daha basit hata işlemeye ve beklenmeyen sorunlar oluştuğunda iyi bir UX sürdürme şansına yol açabilir.
  • Pas çöp toplanmaz . Bu, Rust kodunun belleğin ne zaman ayrılacağını ve ne zaman temizleneceğini tam olarak kontrol ettiği ve gerçek zamanlı sistemlerde önemli bir gereksinim olan tutarlı performansa izin verdiği anlamına gelir.

Rust'ın birçok avantajı aynı zamanda dik bir öğrenme eğrisi ile birlikte gelir, bu nedenle doğru programlama dilini seçmek, kodu geliştirecek ve sürdürecek ekibin yapısı gibi çeşitli faktörlere bağlıdır.

WebAssembly Performansı: İpeksi pürüzsüzlükteki Web Uygulamalarını Koruma

WebAssembly'de Rust ile programlama yaptığımıza göre, bizi Wasm'a yönlendiren performans avantajlarını elde etmek için Rust'ı nasıl kullanabiliriz? Hızla güncellenen bir GUI'ye sahip bir uygulamanın kullanıcılara "pürüzsüz" hissetmesi için, ekranı ekran donanımı kadar düzenli olarak yenileyebilmesi gerekir. Bu genellikle 60 FPS'dir, bu nedenle uygulamamız kullanıcı arayüzünü ~16,7 ms (1.000 ms / 60 FPS) içinde yeniden çizebilmelidir.

Uygulamamız mevcut perdeyi gerçek zamanlı olarak algılar ve gösterir; bu, birleşik algılama hesaplaması ve çiziminin kare başına 16,7 ms içinde kalması gerektiği anlamına gelir. Bir sonraki bölümde, ana iş parçacığı işini yaparken başka bir iş parçacığındaki sesi analiz etmek için tarayıcı desteğinden yararlanacağız. Bu, performans için büyük bir kazançtır, çünkü hesaplama ve çizim her birinin emrinde 16.7 ms'ye sahiptir.

Web Sesi Temelleri

Bu uygulamada, perde algılamasını gerçekleştirmek için yüksek performanslı bir WebAssembly ses modülü kullanacağız. Ayrıca, hesaplamanın ana iş parçacığında çalışmamasını sağlayacağız.

Neden işleri basit tutamıyoruz ve ana iş parçacığında adım algılaması gerçekleştiremiyoruz?

  • Ses işleme genellikle yoğun hesaplama gerektirir. Bunun nedeni, her saniye işlenmesi gereken çok sayıda numunedir. Örneğin, ses perdesini güvenilir bir şekilde tespit etmek, her saniye 44.100 örneğin spektrumunun analiz edilmesini gerektirir.
  • JIT derlemesi ve JavaScript'in çöp toplaması ana iş parçacığında gerçekleşir ve tutarlı performans için ses işleme kodunda bundan kaçınmak istiyoruz.
  • Bir ses çerçevesini işlemek için geçen süre, 16.7 ms'lik çerçeve bütçesini önemli ölçüde tüketecek olsaydı, UX dalgalı animasyondan zarar görürdü.
  • Uygulamamızın düşük performanslı mobil cihazlarda bile sorunsuz çalışmasını istiyoruz!

Web Ses işletleri, ses işleme ana iş parçacığını tutamadığından, uygulamaların düzgün bir 60 FPS elde etmeye devam etmesine izin verir. Ses işleme çok yavaşsa ve geride kalıyorsa, gecikmeli ses gibi başka etkiler olacaktır. Ancak, UX kullanıcıya yanıt vermeye devam edecektir.

WebAssembly/Pas Eğitimi: Başlarken

Bu öğretici, Node.js'nin ve npx kurulu olduğunu varsayar. Zaten npx yoksa, yüklemek için npm (Node.js ile birlikte gelir) kullanabilirsiniz:

 npm install -g npx

Web Uygulaması Oluşturun

Bu Wasm/Rust öğreticisi için React'i kullanacağız.

Bir terminalde aşağıdaki komutları çalıştıracağız:

 npx create-react-app wasm-audio-app cd wasm-audio-app

Bu, wasm-audio-app dizininde yeni bir React uygulaması oluşturmak için create-react-app komutunu (Facebook tarafından sağlanan ilgili pakette bulunur) yürütmek için npx kullanır.

create-react-app , React tabanlı tek sayfalı uygulamalar (SPA) oluşturmak için bir CLI'dir. React ile yeni bir projeye başlamayı inanılmaz derecede kolaylaştırır. Ancak çıktı projesi, değiştirilmesi gereken ortak kod kodunu içerir.

İlk olarak, geliştirme boyunca uygulamanızın birim tarafından test edilmesini şiddetle tavsiye etsem de, test etme bu eğitimin kapsamı dışındadır. Bu yüzden devam edip src/App.test.js ve src/setupTests.js .

Uygulamaya Genel Bakış

Uygulamamızda beş ana JavaScript bileşeni olacaktır:

  • public/wasm-audio/wasm-audio.js , adım algılama algoritmasını sağlayan Wasm modülüne JavaScript bağlamaları içerir.
  • public/PitchProcessor.js , ses işlemenin gerçekleştiği yerdir. Web Sesi oluşturma iş parçacığında çalışır ve Wasm API'sini tüketir.
  • src/PitchNode.js , Web Audio grafiğine bağlı olan ve ana iş parçacığında çalışan bir Web Audio düğümünün uygulamasını içerir.
  • src/setupAudio.js , kullanılabilir bir ses kayıt cihazına erişmek için web tarayıcı API'lerini kullanır.
  • src/App.js ve src/App.css , uygulama kullanıcı arabirimini oluşturur.

Adım algılama uygulaması için bir akış şeması. Blok 1 ve 2, Web Sesi iş parçacığında çalışır. Blok 1, wasm-audio/lib.rs dosyasındaki Wasm (Pas) Pitch Dedektörüdür. Blok 2, PitchProcessor.js dosyasında Web Ses Algılama + İletişim'dir. Dedektörden başlatmasını ister ve dedektör algılanan perdeleri Web Audio arayüzüne geri gönderir. 3, 4 ve 5 numaralı bloklar ana iş parçacığında çalışır. Blok 3, PitchNode.js dosyasındaki Web Ses Denetleyicisidir. Wasm modülünü PitchProcessor.js'ye gönderir ve ondan algılanan perdeleri alır. Blok 4, setupAudio.js'de Web Ses Kurulumu'dur. Bir PitchNode nesnesi oluşturur. Blok 5, App.js ve App.css'den oluşan Web Uygulaması Kullanıcı Arayüzü'dür. Başlangıçta setupAudio.js'yi çağırır. Ayrıca, kullanıcıya göstermek üzere algılanan perdeleri aldığı PitchNode'a bir mesaj göndererek ses kaydını duraklatır veya sürdürür.
Wasm ses uygulamasına genel bakış.

Doğrudan uygulamamızın kalbine inelim ve Wasm modülümüz için Rust kodunu tanımlayalım. Ardından Web Sesi ile ilgili JavaScript'imizin çeşitli bölümlerini kodlayacağız ve kullanıcı arayüzü ile sonlandıracağız.

1. Rust ve WebAssembly Kullanarak Adım Algılama

Rust kodumuz, bir dizi ses örneğinden bir müzik perdesi hesaplayacaktır.

Rust'ı Alın

Geliştirmek üzere Rust zincirini oluşturmak için bu talimatları takip edebilirsiniz.

Rust'ta WebAssembly Bileşenleri Oluşturmak için Araçlar Kurun

wasm-pack , Rust tarafından oluşturulan WebAssembly bileşenlerini oluşturmanıza, test etmenize ve yayınlamanıza olanak tanır. Henüz yapmadıysanız, wasm-pack yükleyin.

cargo-generate , önceden var olan bir Git deposunu şablon olarak kullanarak yeni bir Rust projesinin çalışır durumda olmasına yardımcı olur. Bunu, tarayıcıdan WebAssembly kullanılarak erişilebilen Rust'ta basit bir ses çözümleyicisini önyüklemek için kullanacağız.

Rust zinciriyle birlikte gelen cargo aracını kullanarak, kargo cargo-generate yükleyebilirsiniz:

 cargo install cargo-generate

Kurulum (birkaç dakika sürebilir) tamamlandığında, Rust projemizi oluşturmaya hazırız.

WebAssembly Modülümüzü Oluşturun

Uygulamamızın kök klasöründen proje şablonunu kopyalayacağız:

 $ cargo generate --git https://github.com/rustwasm/wasm-pack-template

Yeni bir proje adı sorulduğunda wasm-audio gireceğiz.

wasm-audio dizininde artık aşağıdaki içeriğe sahip bir Cargo.toml dosyası olacaktır:

 [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 , bir Rust paketini (Rust'un "sandık" olarak adlandırdığı) tanımlamak için kullanılır ve Rust uygulamaları için package.json JavaScript uygulamaları için yaptığına benzer bir işlev sunar.

[package] bölümü, paketi Rust'ın resmi paket kayıt defterine yayınlarken kullanılan meta verileri tanımlar.

[lib] bölümü, Rust derleme işleminden çıktı biçimini açıklar. Burada "cdylib", Rust'a başka bir dilden (bizim durumumuzda JavaScript) yüklenebilen bir "dinamik sistem kitaplığı" oluşturmasını söyler ve "rlib" dahil, Rust'a üretilen kitaplık hakkında meta veriler içeren statik bir kitaplık eklemesini söyler. Bu ikinci belirleyici, amaçlarımız için gerekli değildir - bu sandığı bir bağımlılık olarak tüketen başka Rust modüllerinin geliştirilmesine yardımcı olur - ancak içinde bırakılması güvenlidir.

[features] içinde, Rust'ın işlenmeyen hatalar mekanizmasını ( panic olarak adlandırılır) hata ayıklama için geliştirme araçlarında görünen konsol hatalarına dönüştüren işlevsellik sağlamak için Rust'tan isteğe bağlı bir console_error_panic_hook özelliği eklemesini istiyoruz.

Son olarak, [dependencies] bunun bağlı olduğu tüm kasaları listeler. Kutunun dışında sağlanan tek bağımlılık, Wasm modülümüze JavaScript bağlamalarının otomatik olarak oluşturulmasını sağlayan wasm-bindgen .

Rust'ta Pitch Dedektörü Uygulamak

Bu uygulamanın amacı, bir müzisyenin sesini veya bir enstrümanın perdesini gerçek zamanlı olarak tespit edebilmektir. Bunun mümkün olan en hızlı şekilde yürütülmesini sağlamak için, adımın hesaplanmasıyla bir WebAssembly modülü görevlendirilir. Tek sesli perde algılama için, mevcut Rust pitch-detection kitaplığında uygulanan “McLeod” perde yöntemini kullanacağız.

Node.js paket yöneticisine (npm) çok benzer şekilde, Rust da Kargo adlı kendi paket yöneticisini içerir. Bu, Rust sandık kayıt defterinde yayınlanmış olan paketlerin kolayca yüklenmesini sağlar.

Bağımlılığı eklemek için, bağımlılıklar bölümüne pitch-detection satırını ekleyerek Cargo.toml düzenleyin:

 [dependencies] wasm-bindgen = "0.2.63" pitch-detection = "0.1"

Bu, Cargo'ya bir sonraki cargo build sırasında pitch-detection bağımlılığını indirmesi ve yüklemesi talimatını verir veya WebAssembly'yi hedeflediğimiz için bu, bir sonraki wasm-pack gerçekleştirilecektir.

Rust'ta JavaScript ile çağrılabilir bir Pitch Detector oluşturun

İlk önce, amacını daha sonra tartışacağımız yararlı bir yardımcı programı tanımlayan bir dosya ekleyeceğiz:

wasm-audio/src/utils.rs ve bu dosyanın içeriğini ona yapıştırın.

wasm-audio/lib.rs içinde oluşturulan kodu, hızlı bir Fourier dönüşümü (FFT) algoritması aracılığıyla perde algılaması yapan aşağıdaki kodla değiştireceğiz:

 use pitch_detection::{McLeodDetector, PitchDetector}; use wasm_bindgen::prelude::*; mod utils; #[wasm_bindgen] pub struct WasmPitchDetector { sample_rate: usize, fft_size: usize, detector: McLeodDetector<f32>, } #[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::<f32>::new(fft_size, fft_pad), } } pub fn detect_pitch(&mut self, audio_samples: Vec<f32>) -> f32 { if audio_samples.len() < self.fft_size { panic!("Insufficient samples passed to detect_pitch(). Expected an array containing {} elements but got {}", self.fft_size, audio_samples.len()); } // Include only notes that exceed a power threshold which relates to the // amplitude of frequencies in the signal. Use the suggested default // value of 5.0 from the library. const POWER_THRESHOLD: f32 = 5.0; // The clarity measure describes how coherent the sound of a note is. For // example, the background sound in a crowded room would typically be would // have low clarity and a ringing tuning fork would have high clarity. // This threshold is used to accept detect notes that are clear enough // (valid values are in the range 0-1). const CLARITY_THRESHOLD: f32 = 0.6; let optional_pitch = self.detector.get_pitch( &audio_samples, self.sample_rate, POWER_THRESHOLD, CLARITY_THRESHOLD, ); match optional_pitch { Some(pitch) => pitch.frequency, None => 0.0, } } }

Bunu daha detaylı inceleyelim:

 #[wasm_bindgen]

wasm_bindgen , JavaScript ile Rust arasındaki bağlamayı uygulamaya yardımcı olan bir Rust makrosudur. WebAssembly'de derlendiğinde, bu makro derleyiciye bir sınıfa bir JavaScript bağlaması oluşturma talimatı verir. Yukarıdaki Rust kodu, Wasm modülüne gelen ve giden çağrılar için yalnızca ince paketleyiciler olan JavaScript bağlamalarına çevrilecektir. JavaScript arasında doğrudan paylaşılan bellekle birleştirilmiş hafif soyutlama katmanı, Wasm'ın mükemmel performans sağlamasına yardımcı olur.

 #[wasm_bindgen] pub struct WasmPitchDetector { sample_rate: usize, fft_size: usize, detector: McLeodDetector<f32>, } #[wasm_bindgen] impl WasmPitchDetector { ... }

Rust'ın sınıf kavramı yoktur. Bunun yerine, bir nesnenin verileri bir struct ve onun davranışı impl s veya s trait aracılığıyla tanımlanır.

Adım algılama işlevini neden düz bir işlev yerine bir nesne aracılığıyla ortaya çıkaralım? Bu şekilde, WasmPitchDetector oluşturulurken dahili McLeodDetector tarafından kullanılan veri yapılarını yalnızca bir kez WasmPitchDetector . Bu, işlem sırasında pahalı bellek tahsisinden kaçınarak detect_pitch işlevini hızlı tutar.

 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::<f32>::new(fft_size, fft_pad), } }

Bir Rust uygulaması kolayca kurtaramayacağı bir hatayla karşılaştığında, panic! makro. Bu, Rust'a bir hata bildirmesi ve uygulamayı derhal sonlandırması talimatını verir. Paniklerden yararlanmak, yanlış varsayımları hızlı bir şekilde yakalamanıza izin verdiği için, bir hata işleme stratejisi uygulanmadan önce özellikle erken geliştirme için yararlı olabilir.

Kurulum sırasında utils::set_panic_hook() bir kez çağrılması, tarayıcı geliştirme araçlarında panik mesajlarının görünmesini sağlayacaktır.

Ardından, her analiz FFT'sine uygulanan sıfır doldurma miktarı olan fft_pad tanımlarız. Doldurma, algoritma tarafından kullanılan pencereleme işleviyle birlikte, analiz gelen örneklenmiş ses verileri boyunca ilerlerken sonuçları "düzgünleştirmeye" yardımcı olur. FFT uzunluğunun yarısı kadar bir ped kullanmak birçok enstrüman için iyi sonuç verir.

Son olarak, Rust son ifadenin sonucunu otomatik olarak döndürür, bu nedenle WasmPitchDetector struct ifadesi new() öğesinin dönüş değeridir.

impl WasmPitchDetector Rust kodumuzun geri kalanı, sahaları algılamak için API'yi tanımlar:

 pub fn detect_pitch(&mut self, audio_samples: Vec<f32>) -> f32 { ... }

Rust'ta bir üye işlev tanımı böyle görünür. detect_pitch bir genel üye dedektör_pitch WasmPitchDetector . İlk argümanı, struct ve impl alanlarını içeren aynı türdeki somutlaştırılmış bir nesneye değiştirilebilir bir referanstır ( &mut ) - ancak aşağıda göreceğimiz gibi bu, çağrılırken otomatik olarak iletilir.

Buna ek olarak, üye işlevimiz rastgele boyutta bir 32-bit kayan noktalı sayı dizisini alır ve tek bir sayı döndürür. Burada, bu örnekler arasında hesaplanan sonuç aralığı (Hz olarak) olacaktır.

 if audio_samples.len() < self.fft_size { panic!("Insufficient samples passed to detect_pitch(). Expected an array containing {} elements but got {}", self.fft_size, audio_samples.len()); }

Yukarıdaki kod, gerçekleştirilecek geçerli bir adım analizi için fonksiyona yeterli örneklerin sağlanıp sağlanmadığını tespit eder. Değilse, Rust panic! makrosu çağrılır, bu da Wasm'den hemen çıkışa ve tarayıcı geliştirme araçları konsoluna yazdırılan hata mesajına neden olur.

 let optional_pitch = self.detector.get_pitch( &audio_samples, self.sample_rate, POWER_THRESHOLD, CLARITY_THRESHOLD, );

Bu, en son ses örneklerinden perdeyi hesaplamak için üçüncü taraf kitaplığına çağrı yapar. POWER_THRESHOLD ve CLARITY_THRESHOLD , algoritmanın hassasiyetini ayarlamak için ayarlanabilir.

Diğer dillerdeki bir switch deyimine benzer şekilde çalışan match anahtar sözcüğü aracılığıyla bir kayan nokta değerinin zımni dönüşüyle ​​bitiriyoruz. Some() ve None , durumları boş işaretçi istisnasına girmeden uygun şekilde ele almamıza izin verir.

WebAssembly Uygulamaları Oluşturma

Rust uygulamalarını geliştirirken, olağan derleme prosedürü, cargo build kullanan bir derlemeyi çağırmaktır. Ancak, bir Wasm modülü oluşturuyoruz, bu nedenle Wasm'ı hedeflerken daha basit sözdizimi sağlayan wasm-pack pack'i kullanacağız. (Ayrıca, elde edilen JavaScript bağlamalarının npm kayıt defterine yayınlanmasına da izin verir, ancak bu, bu öğreticinin kapsamı dışındadır.)

wasm-pack çeşitli yapı hedeflerini destekler. Modülü doğrudan Web Audio worklet'ten tüketeceğimiz için web seçeneğini hedefleyeceğiz. Diğer hedefler arasında web paketi gibi bir paketleyici oluşturma veya Node.js'den tüketim için oluşturma yer alır. Bunu wasm-audio/ alt dizininden çalıştıracağız:

 wasm-pack build --target web

Başarılı olursa, ./pkg altında bir ./pkg modülü oluşturulur.

Bu, kendi otomatik olarak oluşturulmuş package.json sahip bir JavaScript modülüdür. Bu, istenirse npm kayıt defterinde yayınlanabilir. Şimdilik işleri basit tutmak için, bu pkg kopyalayıp public/wasm-audio altına yapıştırabiliriz:

 cp -R ./wasm-audio/pkg ./public/wasm-audio

Bununla, web uygulaması veya daha spesifik olarak PitchProcessor tarafından tüketilmeye hazır bir Rust Wasm modülü oluşturduk.

2. PitchProcessor (Yerel AudioWorkletProcessor temel alır)

Bu uygulama için, yakın zamanda yaygın tarayıcı uyumluluğu kazanmış bir ses işleme standardı kullanacağız. Spesifik olarak, Web Audio API'sini kullanacağız ve özel bir AudioWorkletProcessor içinde pahalı hesaplamalar yapacağız. Daha sonra, ana iş parçacığına bir köprü olarak karşılık gelen özel AudioWorkletNode sınıfını ( PitchNode olarak adlandıracağız) oluşturacağız.

public/PitchProcessor.js adlı yeni bir dosya oluşturun ve içine aşağıdaki kodu yapıştırın:

 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 < this.numAudioSamplesPerAnalysis) { for (const sampleValue of inputSamples) { this.samples[this.totalSamples++] = sampleValue; } } else { // Buffer is already full. We do not want the buffer to grow continually, // so instead will "cycle" the samples through it so that it always // holds the latest ordered samples of length equal to // numAudioSamplesPerAnalysis. // Shift the existing samples left by the length of new samples (128). const numNewSamples = inputSamples.length; const numExistingSamples = this.samples.length - numNewSamples; for (let i = 0; i < numExistingSamples; i++) { this.samples[i] = this.samples[i + numNewSamples]; } // Add the new samples onto the end, into the 128-wide slot vacated by // the previous copy. for (let i = 0; i < numNewSamples; i++) { this.samples[numExistingSamples + i] = inputSamples[i]; } this.totalSamples += inputSamples.length; } // Once our buffer has enough samples, pass them to the Wasm pitch detector. if (this.totalSamples >= this.numAudioSamplesPerAnalysis && this.detector) { const result = this.detector.detect_pitch(this.samples); if (result !== 0) { this.port.postMessage({ type: "pitch", pitch: result }); } } // Returning true tells the Audio system to keep going. return true; } } registerProcessor("PitchProcessor", PitchProcessor);

PitchProcessor , PitchNode eşlik eder ancak ayrı bir iş parçacığında çalışır, böylece ana iş parçacığı üzerinde yapılan işi engellemeden ses işleme hesaplaması gerçekleştirilebilir.

Esas olarak, PitchProcessor :

  • Wasm modülünü derleyerek ve iş uygulamasına yükleyerek PitchNode gönderilen "send-wasm-module" olayını işler. Tamamlandığında, "wasm-module-loaded" olayı göndererek PitchNode bunu bilmesini sağlar. PitchNode ve PitchProcessor arasındaki tüm iletişim bir iş parçacığı sınırını geçtiği ve eşzamanlı olarak gerçekleştirilemediği için bu geri arama yaklaşımı gereklidir.
  • Ayrıca PitchNode yapılandırarak WasmPitchDetector "init-detector" olayına yanıt verir.
  • Tarayıcı ses grafiğinden alınan ses örneklerini işler, perde algılama hesaplamasını Wasm modülüne devreder ve ardından algılanan perdeyi PitchNode geri gönderir (bu, onPitchDetectedCallback aracılığıyla React katmanına gönderir).
  • Kendini belirli, benzersiz bir adla kaydeder. Bu şekilde tarayıcı, yerel AudioWorkletNode olan PitchNode temel sınıfı aracılığıyla, PitchProcessor daha sonra PitchNode nasıl başlatacağını bilir. setupAudio.js bakın.

Aşağıdaki şema, PitchNode ve PitchProcessor arasındaki olay akışını görselleştirir:

Çalışma zamanında PitchNode ve PitchProcess nesneleri arasındaki etkileşimleri karşılaştıran daha ayrıntılı bir akış şeması. İlk kurulum sırasında PitchNode, Wasm modülünü bir bayt dizisi olarak PitchProcessor'a gönderir, bu da onları derler ve PitchNode'a geri gönderir, bu da sonunda PitchProcessor'ın kendisini başlatmasını isteyen bir olay mesajıyla yanıt verir. Ses kaydederken, PitchNode hiçbir şey göndermez ve PitchProcessor'dan iki tür olay mesajı alır: Algılanan bir perde veya bir hata, Wasm'dan veya iş uygulamasından oluşursa.
Çalışma zamanı olay mesajları.

3. Web Ses Çalışma Kodu Ekle

PitchNode.js , özel adım algılamalı ses işlememize arabirim sağlar. PitchNode nesnesi, AudioWorklet iş parçacığında çalışan AudioWorklet modülü kullanılarak algılanan perdelerin ana iş parçacığına ve işleme için React'e gideceği mekanizmadır.

src/PitchNode.js , Web Audio API'sinin yerleşik AudioWorkletNode alt sınıflara ayıracağız:

 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); } } }

PitchNode tarafından gerçekleştirilen temel görevler şunlardır:

  • WebAssembly modülünü, setupAudio.js geçirilen ham bayt dizisi olarak AudioWorklet PitchProcessor PitchProcessor , adım algılama Wasm modülünü bu şekilde yükler.
  • PitchProcessor başarıyla derlediğinde PitchProcessor tarafından gönderilen olayı işleyin ve ona adım algılama yapılandırma bilgilerini ileten başka bir olay gönderin.
  • Algılanan perdeleri, PitchProcessor geldiklerinde işleyin ve bunları setLatestPitch() aracılığıyla onPitchDetectedCallback() UI işlevine iletin.

Not: Nesnenin bu kodu ana iş parçacığında çalışır, bu nedenle pahalı olması ve kare hızının düşmesine neden olması durumunda algılanan aralıklar üzerinde daha fazla işlem yapmaktan kaçınmalıdır.

4. Web Sesini Ayarlamak için Kod Ekleyin

Web uygulamasının, istemci makinenin mikrofonundan canlı girişe erişmesi ve bunları işlemesi için şunları yapması gerekir:

  1. Tarayıcının bağlı herhangi bir mikrofona erişmesi için kullanıcının iznini alın
  2. Mikrofonun çıkışına bir ses akışı nesnesi olarak erişin
  3. Gelen ses akışı örneklerini işlemek ve bir dizi algılanan perde üretmek için kod ekleyin

src/setupAudio.js 'de bunu yapacağız ve ayrıca PitchNode'umuzu eklemeden önce PitchNode'umuzu başlatabilmemiz için Wasm modülünü eşzamansız olarak yükleyeceğiz:

 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 ie 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 }; }

This assumes a WebAssembly module is available to be loaded at public/wasm-audio , which we accomplished in the earlier Rust section.

5. Define the Application UI

Let's define a basic user interface for the pitch detector. We'll replace the contents of src/App.js with the following code:

 import React from "react"; import "./App.css"; import { setupAudio } from "./setupAudio"; function PitchReadout({ running, latestPitch }) { return ( <div className="Pitch-readout"> {latestPitch ? `Latest pitch: ${latestPitch.toFixed(1)} Hz` : running ? "Listening..." : "Paused"} </div> ); } 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 ( <button onClick={async () => { setAudio(await setupAudio(setLatestPitch)); setRunning(true); }} > Start listening </button> ); } // Audio already initialized. Suspend / resume based on its current state. const { context } = audio; return ( <div> <button onClick={async () => { 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"} </button> <PitchReadout running={running} latestPitch={latestPitch} /> </div> ); } function App() { return ( <div className="App"> <header className="App-header"> Wasm Audio Tutorial </header> <div className="App-content"> <AudioRecorderControl /> </div> </div> ); } export default App;

And we'll replace App.css with some basic styles:

 .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); }

With that, we should be ready to run our app—but there's a pitfall to address first.

WebAssembly/Rust Tutorial: So Close!

Now when we run yarn and yarn start , switch to the browser, and attempt to record audio (using Chrome or Chromium, with developer tools open), we're met with some errors:

At wasm_audio.js line 24 there's the error, "Uncaught ReferenceError: TextDecoder is not defined," followed by one at setupAudio.js line 84 triggered by the async onClick from App.js line 43, which reads, "Uncaught (in promise) Error: Failed to load audio analyzer WASM module. Further info: Failed to construct 'AudioWorkletNode': AudioWorkletNode cannot be created: The node name 'PitchProcessor' is not defined in AudioWorkletGlobalScope."
Wasm requirements have wide support—just not yet in the Worklet spec.

The first error, TextDecoder is not defined , occurs when the browser attempts to execute the contents of wasm_audio.js . This in turn results in the failure to load the Wasm JavaScript wrapper, which produces the second error we see in the console.

The underlying cause of the issue is that modules produced by the Wasm package generator of Rust assume that TextDecoder (and TextEncoder ) will be provided by the browser. This assumption holds for modern browsers when the Wasm module is being run from the main thread or even a worker thread. However, for worklets (such as the AudioWorklet context needed in this tutorial), TextDecoder and TextEncoder are not yet part of the spec and so are not available.

TextDecoder is needed by the Rust Wasm code generator to convert from the flat, packed, shared-memory representation of Rust to the string format that JavaScript uses. Put another way, in order to see strings produced by the Wasm code generator, TextEncoder and TextDecoder must be defined.

This issue is a symptom of the relative newness of WebAssembly. As browser support improves to support common WebAssembly patterns out of the box, these issues will likely disappear.

For now, we are able to work around it by defining a polyfill for TextDecoder .

Create a new file public/TextEncoder.js and import it from public/PitchProcessor.js :

 import "./TextEncoder.js";

Make sure that this import statement comes before the wasm_audio import.

Finally, paste this implementation into TextEncoder.js (courtesy of @Yaffle on GitHub).

The Firefox Question

As mentioned earlier, the way we combine Wasm with Web Audio worklets in our app will not work in Firefox. Even with the above shim, clicking the “Start Listening” button will result in this:

 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.

That's because Firefox doesn't yet support importing modules from AudioWorklets —for us, that's PitchProcessor.js running in the AudioWorklet thread.

The Completed Application

Once done, we simply reload the page. The app should load without error. Click “Start Listening” and allow your browser to access your microphone. You'll see a very basic pitch detector written in JavaScript using Wasm:

A screenshot of the app showing its title,
Real-time pitch detection.

Programming in WebAssembly with Rust: A Real-time Web Audio Solution

In this tutorial, we have built a web application from scratch that performs computationally expensive audio processing using WebAssembly. WebAssembly allowed us to take advantage of near-native performance of Rust to perform the pitch detection. Further, this work could be performed on another thread, allowing the main JavaScript thread to focus on rendering to support silky-smooth frame rates even on mobile devices.

Wasm/Rust and Web Audio Takeaways

  • Modern browsers provide performant audio (and video) capture and processing inside web apps.
  • Rust has great tooling for Wasm, which helps recommend it as the language of choice for projects incorporating WebAssembly.
  • Compute-intensive work can be performed efficiently in the browser using Wasm.

Despite the many WebAssembly advantages, there are a couple Wasm pitfalls to watch out for:

  • Tooling for Wasm within worklets is still evolving. For example, we needed to implement our own versions of TextEncoder and TextDecoder functionality required for passing strings between JavaScript and Wasm because they were missing from the AudioWorklet context. That, and importing Javascript bindings for our Wasm support from an AudioWorklet is not yet available in Firefox.
  • Although the application we developed was very simple, building the WebAssembly module and loading it from the AudioWorklet required significant setup. Introducing Wasm to projects does introduce an increase in tooling complexity, which is important to keep in mind.

For your convenience, this GitHub repo contains the final, completed project. If you also do back-end development, you may also be interested in using Rust via WebAssembly within Node.js.


Toptal Mühendislik Blogunda Daha Fazla Okuma:

  • Web Ses API'sı: Kod Yazabildiğiniz Zaman Neden Oluşturun?
  • WebVR Bölüm 3: WebAssembly ve AssemblyScript'in Potansiyelinin Kilidini Açma