WebAssembly/Rust Tutorial: การประมวลผลเสียงที่สมบูรณ์แบบ

เผยแพร่แล้ว: 2022-03-11

WebAssembly (หรือ “Wasm”) ซึ่งรองรับโดยเบราว์เซอร์รุ่นใหม่ทั้งหมด กำลังเปลี่ยนแปลงวิธีที่เราพัฒนาประสบการณ์ผู้ใช้สำหรับเว็บ เป็นรูปแบบไบนารีที่สามารถเรียกใช้งานได้ง่าย ซึ่งช่วยให้ไลบรารีหรือแม้แต่โปรแกรมทั้งหมดที่เขียนด้วยภาษาโปรแกรมอื่นสามารถทำงานในเว็บเบราว์เซอร์ได้

นักพัฒนามักจะมองหาวิธีที่จะทำให้มีประสิทธิผลมากขึ้น เช่น:

  • ใช้ฐานรหัสแอปเดียวสำหรับแพลตฟอร์มเป้าหมายหลายแพลตฟอร์ม แต่ให้แอปทำงานได้ดีกับทุกแพลตฟอร์ม
  • การสร้าง UX ที่ราบรื่นและสวยงามบนเดสก์ท็อป และ อุปกรณ์พกพา
  • ใช้ประโยชน์จากระบบนิเวศของห้องสมุดโอเพ่นซอร์สเพื่อหลีกเลี่ยง "การคิดค้นล้อใหม่" ในระหว่างการพัฒนาแอป

สำหรับนักพัฒนา front-end WebAssembly มีทั้งสามแบบเพื่อตอบการค้นหา UI ของเว็บแอปที่แข่งขันกับประสบการณ์มือถือหรือเดสก์ท็อปดั้งเดิมอย่างแท้จริง มันยังอนุญาตให้ใช้ไลบรารีที่เขียนด้วยภาษาที่ไม่ใช่ JavaScript เช่น C++ หรือ Go!

ในบทช่วยสอน Wasm/Rust นี้ เราจะสร้างแอปตัวตรวจจับระดับเสียงแบบง่ายๆ เช่น จูนเนอร์กีตาร์ จะใช้ความสามารถด้านเสียงในตัวของเบราว์เซอร์ และ ทำงานที่ 60 เฟรมต่อวินาที (FPS) แม้กระทั่งบนอุปกรณ์เคลื่อนที่ คุณไม่จำเป็นต้องเข้าใจ Web Audio API หรือแม้แต่คุ้นเคยกับ Rust เพื่อทำตามบทช่วยสอนนี้ อย่างไรก็ตาม คาดว่าน่าจะสะดวกกับ JavaScript

หมายเหตุ: ในระหว่างที่เขียนบทความนี้ เทคนิคที่ใช้ในบทความนี้ เฉพาะสำหรับ Web Audio API ยังไม่สามารถใช้งานได้ใน Firefox ดังนั้น ในขณะนี้ ขอแนะนำให้ใช้ Chrome, Chromium หรือ Edge สำหรับบทช่วยสอนนี้ แม้ว่าจะมีการรองรับ Wasm และ Web Audio API ที่ยอดเยี่ยมใน Firefox

บทแนะนำ WebAssembly/Rust ครอบคลุมอะไรบ้าง

  • การสร้างฟังก์ชันอย่างง่ายใน Rust และเรียกใช้จาก JavaScript (ผ่าน WebAssembly)
  • การใช้ AudioWorklet API ที่ทันสมัยของเบราว์เซอร์สำหรับการประมวลผลเสียงที่มีประสิทธิภาพสูงในเบราว์เซอร์
  • การสื่อสารระหว่างคนงานใน JavaScript
  • ผูกมันทั้งหมดเข้าด้วยกันเป็นแอปพลิเคชั่น React ที่ไร้กระดูก

หมายเหตุ: หากคุณสนใจใน "วิธีการ" มากกว่า "ทำไม" ของบทความนี้ อย่าลังเลที่จะเข้าสู่บทช่วยสอนนี้

ทำไมต้อง Wasm?

มีเหตุผลหลายประการที่ทำให้การใช้ WebAssembly เหมาะสม:

  • อนุญาตให้รันโค้ดภายในเบราว์เซอร์ที่เขียนใน ภาษาใดก็ได้
    • ซึ่งรวมถึง การใช้ไลบรารีที่มีอยู่ (ตัวเลข การประมวลผลเสียง การเรียนรู้ของเครื่อง ฯลฯ) ที่เขียนในภาษาอื่นที่ไม่ใช่ JavaScript
  • ขึ้นอยู่กับทางเลือกของภาษาที่ใช้ Wasm สามารถทำงานได้ด้วยความเร็วที่ใกล้เคียงกับเจ้าของภาษา มีศักยภาพในการทำให้คุณลักษณะด้านประสิทธิภาพของเว็บแอปพลิเคชันใกล้เคียงกับ ประสบการณ์ใช้งานจริงสำหรับทั้งอุปกรณ์เคลื่อนที่และเดสก์ท็อป มากขึ้น

ทำไมไม่ใช้ Wasm เสมอ ?

ความนิยมของ WebAssembly จะยังคงเติบโตต่อไปอย่างแน่นอน อย่างไรก็ตาม ไม่เหมาะสำหรับการพัฒนาเว็บทั้งหมด:

  • สำหรับโปรเจ็กต์ธรรมดา การใช้ JavaScript, HTML และ CSS มีแนวโน้มที่จะส่งมอบผลิตภัณฑ์ที่ใช้งานได้ในเวลาอันสั้น
  • เบราว์เซอร์รุ่นเก่า เช่น Internet Explorer ไม่สนับสนุน Wasm โดยตรง
  • การใช้งาน WebAssembly โดยทั่วไปจำเป็นต้องมีการเพิ่มเครื่องมือ เช่น คอมไพเลอร์ภาษา ลงใน toolchain ของคุณ หากทีมของคุณให้ความสำคัญกับการรักษาการพัฒนาและการผสานรวมอย่างต่อเนื่องให้เรียบง่ายที่สุด การใช้ Wasm จะทำให้เกิดปัญหานี้

ทำไมถึงต้องสอน Wasm/Rust โดยเฉพาะ?

ในขณะที่ภาษาโปรแกรมหลายภาษาคอมไพล์เป็น Wasm ฉันเลือก Rust สำหรับตัวอย่างนี้ สนิมถูกสร้างขึ้นโดย Mozilla ในปี 2010 และกำลังได้รับความนิยมเพิ่มขึ้น Rust ครองตำแหน่งสูงสุดสำหรับ "ภาษาที่เป็นที่รักมากที่สุด" ในการสำรวจนักพัฒนา 2020 จาก Stack Overflow แต่เหตุผลที่ใช้ Rust กับ WebAssembly มีมากกว่าแค่ความทันสมัย:

  • ก่อนอื่น Rust มีรันไทม์ขนาดเล็ก ซึ่งหมายความว่า มีการส่งโค้ดไปยังเบราว์เซอร์น้อยลง เมื่อผู้ใช้เข้าถึงไซต์ ช่วยรักษารอยเท้าของเว็บไซต์
  • Rust มีการรองรับ Wasm ที่ยอดเยี่ยม รองรับ การทำงานร่วมกันในระดับสูง กับ JavaScript
  • Rust ให้ประสิทธิภาพเกือบ ระดับ C/C++ แต่มี รูปแบบหน่วยความจำที่ปลอดภัยมาก เมื่อเปรียบเทียบกับภาษาอื่น Rust จะทำการตรวจสอบความปลอดภัยเพิ่มเติมในขณะที่คอมไพล์โค้ดของคุณ ซึ่งช่วยลดโอกาสที่เกิดปัญหาจากตัวแปรว่างหรือตัวแปรที่ไม่ได้เตรียมการได้อย่างมาก สิ่งนี้สามารถนำไปสู่การจัดการข้อผิดพลาดที่ง่ายขึ้นและมีโอกาสสูงในการรักษา UX ที่ดีเมื่อเกิดปัญหาที่ไม่คาดคิด
  • สนิม ไม่เก็บขยะ ซึ่งหมายความว่ารหัส Rust สามารถควบคุมได้อย่างเต็มที่ว่าเมื่อใดที่หน่วยความจำได้รับการจัดสรรและล้างข้อมูล ทำให้เกิดประสิทธิภาพที่ สม่ำเสมอ ซึ่งเป็นข้อกำหนดหลักในระบบเรียลไทม์

ประโยชน์มากมายของ Rust ยังมาพร้อมกับช่วงการเรียนรู้ที่สูงชัน ดังนั้นการเลือกภาษาการเขียนโปรแกรมที่เหมาะสมจึงขึ้นอยู่กับปัจจัยหลายประการ เช่น องค์ประกอบของทีมที่จะพัฒนาและบำรุงรักษาโค้ด

ประสิทธิภาพของ WebAssembly: การรักษา Web Apps ที่ลื่นไหล

เนื่องจากเรากำลังเขียนโปรแกรมใน WebAssembly ด้วย Rust เราจะใช้ Rust เพื่อให้ได้ประโยชน์ด้านประสิทธิภาพที่นำเราไปสู่ ​​Wasm ตั้งแต่แรกได้อย่างไร เพื่อให้แอปพลิเคชันที่มี GUI ที่อัปเดตอย่างรวดเร็วเพื่อให้ผู้ใช้รู้สึก "ราบรื่น" จะต้องสามารถรีเฟรชการแสดงผลได้เป็นประจำเช่นเดียวกับฮาร์ดแวร์ของหน้าจอ โดยทั่วไปคือ 60 FPS ดังนั้นแอปพลิเคชันของเราต้องสามารถวาดอินเทอร์เฟซผู้ใช้ใหม่ได้ภายใน ~16.7 ms (1,000 ms / 60 FPS)

แอปพลิเคชันของเราจะตรวจจับและแสดงระยะพิทช์ปัจจุบันแบบเรียลไทม์ ซึ่งหมายความว่าการคำนวณและการวาดภาพแบบรวมของการตรวจจับจะต้องอยู่ภายใน 16.7 มิลลิวินาทีต่อเฟรม ในส่วนถัดไป เราจะใช้ประโยชน์จากการสนับสนุนเบราว์เซอร์สำหรับการวิเคราะห์เสียงในเธรดอื่น ในขณะ ที่เธรดหลักทำงาน นี่เป็นชัยชนะครั้งสำคัญสำหรับประสิทธิภาพ เนื่องจากการคำนวณและการวาด แต่ละครั้ง มี 16.7 ms พร้อมใช้งาน

ข้อมูลพื้นฐานเกี่ยวกับเสียงบนเว็บ

ในแอปพลิเคชันนี้ เราจะใช้โมดูลเสียง WebAssembly ประสิทธิภาพสูงในการตรวจจับระดับเสียง นอกจากนี้ เราจะตรวจสอบให้แน่ใจว่าการคำนวณไม่ทำงานบนเธรดหลัก

เหตุใดเราจึงไม่ทำให้สิ่งต่างๆ เรียบง่ายและทำการตรวจจับระยะห่างบนเธรดหลักไม่ได้

  • การประมวลผลเสียงมักใช้การประมวลผลสูง นี่เป็นเพราะตัวอย่างจำนวนมากที่ต้องดำเนินการทุกวินาที ตัวอย่างเช่น การตรวจจับระดับเสียงอย่างน่าเชื่อถือนั้นต้องการการวิเคราะห์สเปกตรัม 44,100 ตัวอย่างในแต่ละวินาที
  • การรวบรวม JIT และการรวบรวมขยะของ JavaScript เกิดขึ้นในเธรดหลัก และเราต้องการหลีกเลี่ยงสิ่งนี้ในโค้ดการประมวลผลเสียงเพื่อประสิทธิภาพที่สม่ำเสมอ
  • หากเวลาที่ใช้ในการประมวลผลเฟรมของเสียงกินมากเกินไปในงบประมาณเฟรม 16.7 มิลลิวินาที UX จะได้รับผลกระทบจากแอนิเมชั่นที่กระตุก
  • เราต้องการให้แอปของเราทำงานได้อย่างราบรื่นแม้ในอุปกรณ์มือถือที่มีประสิทธิภาพต่ำ!

เวิร์กเล็ต Web Audio ช่วยให้แอปสามารถบรรลุ 60 FPS ได้อย่างราบรื่นต่อไป เนื่องจากการประมวลผลเสียงไม่สามารถรองรับเธรดหลักได้ หากการประมวลผลเสียงช้าเกินไปและล้าหลัง จะมีผลกระทบอื่นๆ เช่น เสียงที่ล้าหลัง อย่างไรก็ตาม UX จะยังคงตอบสนองต่อผู้ใช้

WebAssembly/Rust Tutorial: เริ่มต้นใช้งาน

บทช่วยสอนนี้ถือว่าคุณติดตั้ง Node.js และ npx แล้ว หากคุณยังไม่มี npx คุณสามารถใช้ npm (ซึ่งมาพร้อมกับ Node.js) เพื่อติดตั้ง:

 npm install -g npx

สร้างเว็บแอป

สำหรับบทช่วยสอน Wasm/Rust นี้ เราจะใช้ React

ในเทอร์มินัล เราจะเรียกใช้คำสั่งต่อไปนี้:

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

สิ่งนี้ใช้ npx เพื่อรันคำสั่ง create-react-app (มีอยู่ในแพ็คเกจที่เกี่ยวข้องที่ดูแลโดย Facebook) เพื่อสร้างแอปพลิเคชั่น React ใหม่ในไดเร็กทอรี wasm-audio-app

create-react-app คือ CLI สำหรับสร้างแอปพลิเคชันหน้าเดียวที่ใช้ React (SPA) มันทำให้ง่ายอย่างเหลือเชื่อในการเริ่มโครงการใหม่ด้วย React อย่างไรก็ตาม โปรเจ็กต์เอาท์พุตมีโค้ดสำเร็จรูปที่ต้องเปลี่ยน

อย่างแรก แม้ว่าฉันจะแนะนำอย่างยิ่งให้ทดสอบแอปพลิเคชันของคุณตลอดการพัฒนา แต่การทดสอบอยู่นอกเหนือขอบเขตของบทช่วยสอนนี้ ดังนั้นเราจะดำเนินการต่อและลบ src/App.test.js และ src/setupTests.js

ภาพรวมการสมัคร

แอปพลิเคชันของเราจะมีส่วนประกอบ JavaScript หลักห้าองค์ประกอบ:

  • public/wasm-audio/wasm-audio.js มีการโยง JavaScript กับโมดูล Wasm ที่มีอัลกอริธึมการตรวจจับระดับเสียง
  • public/PitchProcessor.js คือที่ที่การประมวลผลเสียงเกิดขึ้น มันทำงานในเธรดการแสดงผล Web Audio และจะใช้ Wasm API
  • src/PitchNode.js มีการใช้งานโหนด Web Audio ซึ่งเชื่อมต่อกับกราฟ Web Audio และทำงานในเธรดหลัก
  • src/setupAudio.js ใช้เว็บเบราว์เซอร์ API เพื่อเข้าถึงอุปกรณ์บันทึกเสียงที่มีอยู่
  • src/App.js และ src/App.css ประกอบด้วยอินเทอร์เฟซผู้ใช้ของแอปพลิเคชัน

ผังงานสำหรับแอปตรวจจับระดับเสียง บล็อก 1 และ 2 ทำงานบนเธรด Web Audio Block 1 คือ Wasm (Rust) Pitch Detector ในไฟล์ wasm-audio/lib.rs บล็อก 2 คือ Web Audio Detection + Communication ในไฟล์ PitchProcessor.js จะขอให้ตัวตรวจจับเริ่มต้น และตัวตรวจจับจะส่งระดับเสียงที่ตรวจพบกลับไปที่อินเทอร์เฟซ Web Audio บล็อก 3, 4 และ 5 ทำงานบนเธรดหลัก Block 3 คือ Web Audio Controller ในไฟล์ PitchNode.js มันส่งโมดูล Wasm ไปยัง PitchProcessor.js และรับระดับเสียงที่ตรวจพบจากโมดูล Block 4 คือ Web Audio Setup ใน setupAudio.js มันสร้างวัตถุ PitchNode Block 5 คือ Web Application UI ที่ประกอบด้วย App.js และ App.css มันเรียก setupAudio.js เมื่อเริ่มต้น นอกจากนี้ยังหยุดชั่วคราวหรือกลับมาบันทึกเสียงต่อโดยส่งข้อความไปยัง PitchNode ซึ่งระบบจะรับระดับเสียงที่ตรวจพบเพื่อแสดงให้ผู้ใช้เห็น
ภาพรวมแอปเสียง Wasm

เจาะจงไปที่ใจกลางของแอปพลิเคชันของเราและกำหนดรหัส Rust สำหรับโมดูล Wasm ของเรา จากนั้นเราจะเขียนโค้ดส่วนต่างๆ ของ JavaScript ที่เกี่ยวข้องกับ Web Audio และลงท้ายด้วย UI

1. การตรวจจับระยะห่างโดยใช้ Rust และ WebAssembly

รหัส Rust ของเราจะคำนวณระดับเสียงดนตรีจากอาร์เรย์ของตัวอย่างเสียง

รับสนิม

คุณสามารถทำตามคำแนะนำเหล่านี้เพื่อสร้างห่วงโซ่ Rust สำหรับการพัฒนา

ติดตั้งเครื่องมือสำหรับสร้างส่วนประกอบ WebAssembly ใน Rust

wasm-pack ให้คุณสร้าง ทดสอบ และเผยแพร่ส่วนประกอบ WebAssembly ที่สร้างโดย Rust หากคุณยังไม่ได้ติดตั้ง ให้ติดตั้ง wasm-pack

cargo-generate ช่วยให้ได้รับโครงการ Rust ใหม่และดำเนินการโดยใช้ประโยชน์จากที่เก็บ Git ที่มีอยู่ก่อนเป็นเทมเพลต เราจะใช้สิ่งนี้เพื่อบูตเครื่องวิเคราะห์เสียงอย่างง่ายใน Rust ที่สามารถเข้าถึงได้โดยใช้ WebAssembly จากเบราว์เซอร์

โดยใช้เครื่องมือ cargo ที่มาพร้อมกับห่วงโซ่ Rust คุณสามารถติดตั้ง cargo-generate :

 cargo install cargo-generate

เมื่อการติดตั้ง (ซึ่งอาจใช้เวลาหลายนาที) เสร็จสมบูรณ์ เราก็พร้อมที่จะสร้างโครงการ Rust ของเรา

สร้างโมดูล WebAssembly ของเรา

จากโฟลเดอร์รูทของแอปของเรา เราจะทำการโคลนแม่แบบโครงการ:

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

เมื่อได้รับพร้อมท์ให้ใส่ชื่อโปรเจ็กต์ใหม่ เราจะป้อน wasm-audio

ใน wasm-audio ตอนนี้จะมีไฟล์ Cargo.toml ที่มีเนื้อหาดังต่อไปนี้:

 [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 ใช้เพื่อกำหนดแพ็คเกจ Rust (ซึ่ง Rust เรียกว่า "ลัง") ซึ่งทำหน้าที่คล้ายคลึงกันสำหรับแอป Rust ที่ package.json ทำสำหรับแอปพลิเคชัน JavaScript

ส่วน [package] กำหนดข้อมูลเมตาที่ใช้เมื่อเผยแพร่แพ็คเกจไปยังการลงทะเบียนแพ็คเกจอย่างเป็นทางการของ Rust

ส่วน [lib] อธิบายรูปแบบเอาต์พุตจากกระบวนการคอมไพล์ Rust ที่นี่ "cdylib" บอกให้ Rust สร้าง "ไลบรารีระบบแบบไดนามิก" ที่สามารถโหลดจากภาษาอื่น (ในกรณีของเราคือ JavaScript) และรวมถึง "rlib" บอกให้ Rust เพิ่มไลบรารีสแตติกที่มีข้อมูลเมตาเกี่ยวกับไลบรารีที่ผลิต ตัวระบุตัวที่สองนี้ไม่จำเป็นสำหรับวัตถุประสงค์ของเรา - มันช่วยในการพัฒนาโมดูล Rust เพิ่มเติมที่ใช้ลังนี้เป็นการพึ่งพา - แต่สามารถปล่อยไว้ได้อย่างปลอดภัย

ใน [features] เราขอให้ Rust รวมฟีเจอร์เสริม console_error_panic_hook เพื่อให้ฟังก์ชันการทำงานที่แปลงกลไกข้อผิดพลาดที่ไม่สามารถจัดการได้ของ Rust (เรียกว่า panic นิค ) เป็นคอนโซลข้อผิดพลาดที่แสดงในเครื่องมือ dev สำหรับการดีบัก

ในที่สุด [dependencies] จะแสดงรายการลังทั้งหมดที่ขึ้นอยู่กับสิ่งนี้ การพึ่งพาเพียงอย่างเดียวที่ให้มานอกกรอบคือ wasm-bindgen ซึ่งให้การสร้างการเชื่อมโยง JavaScript โดยอัตโนมัติกับโมดูล Wasm ของเรา

ติดตั้ง Pitch Detector ใน Rust

จุดประสงค์ของแอปนี้คือเพื่อให้สามารถตรวจจับเสียงของนักดนตรีหรือระดับเสียงของเครื่องดนตรีได้แบบเรียลไทม์ เพื่อให้แน่ใจว่าการดำเนินการนี้จะดำเนินการโดยเร็วที่สุด โมดูล WebAssembly จะได้รับมอบหมายให้คำนวณระยะห่าง สำหรับการตรวจจับระดับเสียงเดียว เราจะใช้วิธีระดับเสียง "McLeod" ที่ใช้ในไลบรารี pitch-detection ของ Rust ที่มีอยู่

เหมือนกับตัวจัดการแพ็คเกจ Node.js (npm) Rust มีตัวจัดการแพ็คเกจของตัวเองที่เรียกว่า Cargo ซึ่งช่วยให้ติดตั้งแพ็คเกจที่เผยแพร่ไปยังรีจิสทรีลัง Rust ได้อย่างง่ายดาย

หากต้องการเพิ่มการพึ่งพา ให้แก้ไข Cargo.toml เพิ่มบรรทัดสำหรับ pitch-detection ในส่วนการพึ่งพา:

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

สิ่งนี้แนะนำให้ Cargo ดาวน์โหลดและติดตั้งการพึ่งพา pitch-detection ระหว่างการ cargo build ถัดไป หรือเนื่องจากเรากำหนดเป้าหมายไปที่ WebAssembly สิ่งนี้จะดำเนินการใน wasm-pack ถัดไป

สร้าง Pitch Detector ที่เรียกใช้ JavaScript ใน Rust

ขั้นแรก เราจะเพิ่มไฟล์ที่กำหนดยูทิลิตี้ที่มีประโยชน์ซึ่งเราจะพูดถึงวัตถุประสงค์ในภายหลัง:

สร้าง wasm-audio/src/utils.rs และวางเนื้อหาของไฟล์นี้ลงไป

เราจะแทนที่โค้ดที่สร้างขึ้นใน wasm-audio/lib.rs ด้วยโค้ดต่อไปนี้ ซึ่งทำการตรวจจับพิทช์ผ่านอัลกอริธึมการแปลงฟูริเยร์ (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<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, } } }

มาตรวจสอบรายละเอียดเพิ่มเติมกัน:

 #[wasm_bindgen]

wasm_bindgen เป็นมาโคร Rust ที่ช่วยเชื่อมโยงระหว่าง JavaScript และ Rust เมื่อคอมไพล์ไปที่ WebAssembly แมโครนี้จะสั่งให้คอมไพเลอร์สร้างการเชื่อมโยง JavaScript กับคลาส โค้ด Rust ด้านบนจะแปลเป็นการโยง JavaScript ที่เป็นเพียง wrapper แบบบางสำหรับการเรียกเข้าและออกจากโมดูล Wasm เลเยอร์นามธรรมที่บางเบารวมกับหน่วยความจำที่แชร์โดยตรงระหว่าง JavaScript คือสิ่งที่ช่วยให้ Wasm ส่งมอบประสิทธิภาพที่ยอดเยี่ยม

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

สนิมไม่มีแนวคิดเรื่องการเรียน ข้อมูลของอ็อบเจกต์ถูกอธิบายโดย struct และพฤติกรรมของมันผ่าน impl s หรือ trait s

เหตุใดจึงเปิดเผยฟังก์ชันการตรวจจับระดับเสียงผ่านวัตถุแทนที่จะเป็นฟังก์ชันธรรมดา เพราะด้วยวิธีนี้ เราจะเริ่มต้นโครงสร้างข้อมูลที่ใช้โดย McLeodDetector ภายใน เพียงครั้งเดียว ระหว่างการสร้าง WasmPitchDetector ซึ่งช่วยให้ฟังก์ชัน detect_pitch ทำงานได้รวดเร็วโดยหลีกเลี่ยงการจัดสรรหน่วยความจำราคาแพงระหว่างการดำเนินการ

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

เมื่อแอปพลิเคชัน Rust พบข้อผิดพลาดที่ไม่สามารถกู้คืนได้อย่างง่ายดาย เป็นเรื่องปกติที่จะทำให้เกิด panic! มาโคร สิ่งนี้สั่งให้ Rust รายงานข้อผิดพลาดและยุติแอปพลิเคชันทันที การใช้ความตื่นตระหนกมีประโยชน์โดยเฉพาะอย่างยิ่งสำหรับการพัฒนาในระยะเริ่มต้น ก่อนที่จะมีกลยุทธ์การจัดการข้อผิดพลาด เนื่องจากจะช่วยให้คุณจับสมมติฐานที่ผิดพลาดได้อย่างรวดเร็ว

การเรียก utils::set_panic_hook() หนึ่งครั้งระหว่างการตั้งค่าจะทำให้ข้อความแพนิคปรากฏในเครื่องมือพัฒนาเบราว์เซอร์

ต่อไป เรากำหนด fft_pad จำนวนการเติมศูนย์ที่ใช้กับการวิเคราะห์ FFT แต่ละรายการ Padding ร่วมกับฟังก์ชัน windowing ที่ใช้โดยอัลกอริธึม ช่วยให้ผลลัพธ์ "ราบรื่น" เมื่อการวิเคราะห์เคลื่อนข้ามข้อมูลเสียงตัวอย่างที่เข้ามา การใช้แผ่นรองความยาว FFT ครึ่งหนึ่งทำงานได้ดีกับเครื่องดนตรีหลายชนิด

สุดท้าย Rust จะคืนค่าผลลัพธ์ของคำสั่งสุดท้ายโดยอัตโนมัติ ดังนั้นคำสั่ง WasmPitchDetector struct จึงเป็นค่าที่ส่งคืนของ new()

รหัส impl WasmPitchDetector Rust ที่เหลือของเรากำหนด API สำหรับตรวจจับระดับเสียง:

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

นี่คือลักษณะของนิยามฟังก์ชันสมาชิกใน Rust สมาชิกสาธารณะ detect_pitch ถูกเพิ่มใน WasmPitchDetector อาร์กิวเมนต์แรกคือการอ้างอิงที่เปลี่ยนแปลงได้ ( &mut ) ไปยังอ็อบเจ็กต์ที่สร้างอินสแตนซ์ประเภทเดียวกันที่มีฟิลด์ struct และ impl แต่สิ่งนี้จะถูกส่งต่อโดยอัตโนมัติเมื่อมีการโทร ดังที่เราเห็นด้านล่าง

นอกจากนี้ ฟังก์ชันสมาชิกของเราใช้อาร์เรย์ตัวเลขทศนิยมแบบ 32 บิตขนาด 32 บิตและส่งคืนตัวเลขเดี่ยว นี่จะเป็นระยะพิทช์ผลลัพธ์ที่คำนวณจากตัวอย่างเหล่านั้น (ในหน่วย Hz)

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

โค้ดด้านบนตรวจสอบว่ามีตัวอย่างเพียงพอสำหรับฟังก์ชันสำหรับการวิเคราะห์พิทช์ที่ถูกต้องหรือไม่ ถ้าไม่อย่างนั้น สนิมก็ panic! มีการเรียกมาโครซึ่งส่งผลให้ออกจาก Wasm ทันที และข้อความแสดงข้อผิดพลาดที่พิมพ์ไปยังคอนโซล dev-tools ของเบราว์เซอร์

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

สิ่งนี้เรียกเข้าสู่ห้องสมุดบุคคลที่สามเพื่อคำนวณระดับเสียงจากตัวอย่างเสียงล่าสุด สามารถปรับ POWER_THRESHOLD และ CLARITY_THRESHOLD เพื่อปรับความไวของอัลกอริทึมได้

เราลงท้ายด้วยการคืนค่าโดยนัยของค่าทศนิยมผ่านคีย์เวิร์ดที่ match ซึ่งทำงานคล้ายกับคำสั่ง switch ในภาษาอื่นๆ Some() และ None ช่วยให้เราจัดการกรณีต่างๆ ได้อย่างเหมาะสมโดยไม่ต้องรันข้อยกเว้น null-pointer

การสร้างแอปพลิเคชัน WebAssembly

เมื่อพัฒนาแอพพลิเคชั่น Rust ขั้นตอนการสร้างปกติคือการเรียกใช้งานบิลด์โดยใช้ cargo build อย่างไรก็ตาม เรากำลังสร้างโมดูล Wasm ดังนั้นเราจะใช้ประโยชน์จาก wasm-pack ซึ่งมีไวยากรณ์ที่ง่ายกว่าเมื่อกำหนดเป้าหมาย Wasm (นอกจากนี้ยังอนุญาตให้เผยแพร่การโยง JavaScript ที่เป็นผลลัพธ์กับรีจิสตรี npm แต่นั่นอยู่นอกขอบเขตของบทช่วยสอนนี้)

wasm-pack รองรับเป้าหมายการสร้างที่หลากหลาย เนื่องจากเราจะใช้โมดูลโดยตรงจากเวิร์กเล็ต Web Audio เราจะกำหนดเป้าหมายตัวเลือก web เป้าหมายอื่นๆ ได้แก่ การสร้างบันเดิล เช่น webpack หรือเพื่อการบริโภคจาก Node.js เราจะเรียกใช้จากไดเร็กทอรีย่อย wasm-audio/ :

 wasm-pack build --target web

หากสำเร็จ โมดูล npm จะถูกสร้างขึ้นภายใต้ ./ ./pkg

นี่คือโมดูล JavaScript ที่มี package.json ที่สร้างขึ้นโดยอัตโนมัติ สามารถเผยแพร่ไปยังรีจิสทรี npm ได้หากต้องการ เพื่อให้ง่ายขึ้นในตอนนี้ เราสามารถคัดลอกและวาง pkg นี้ภายใต้โฟลเดอร์ของเรา public/wasm-audio :

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

ด้วยเหตุนี้ เราจึงได้สร้างโมดูล Rust Wasm ที่พร้อมใช้งานโดยเว็บแอป หรือโดยเฉพาะอย่างยิ่งโดย PitchProcessor

2. คลาส PitchProcessor ของเรา (อิงตาม Native AudioWorkletProcessor )

สำหรับแอปพลิเคชันนี้ เราจะใช้มาตรฐานการประมวลผลเสียงที่เพิ่งได้รับความเข้ากันได้กับเบราว์เซอร์อย่างกว้างขวางเมื่อเร็วๆ นี้ โดยเฉพาะอย่างยิ่ง เราจะใช้ Web Audio API และเรียกใช้การคำนวณราคาแพงใน AudioWorkletProcessor แบบกำหนดเอง หลังจากนั้น เราจะสร้างคลาส AudioWorkletNode แบบกำหนดเองที่เกี่ยวข้อง (ซึ่งเราจะเรียกว่า PitchNode ) เป็นบริดจ์กลับไปยังเธรดหลัก

สร้างไฟล์ใหม่ public/PitchProcessor.js แล้ววางโค้ดต่อไปนี้ลงไป:

 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 แต่ทำงานในเธรดที่แยกจากกัน เพื่อให้สามารถดำเนินการคำนวณการประมวลผลเสียงโดยไม่ต้องบล็อกงานที่ทำบนเธรดหลัก

ส่วนใหญ่ PitchProcessor :

  • จัดการเหตุการณ์ "send-wasm-module" ที่ส่งจาก PitchNode โดยการรวบรวมและโหลดโมดูล Wasm ลงในเวิร์กเล็ต เมื่อเสร็จแล้ว มันทำให้ PitchNode ทราบโดยส่งเหตุการณ์ "wasm-module-loaded" วิธีการเรียกกลับนี้จำเป็นเนื่องจากการสื่อสารทั้งหมดระหว่าง PitchNode และ PitchProcessor ข้ามขอบเขตของเธรดและไม่สามารถทำได้แบบซิงโครนัส
  • ตอบสนองต่อเหตุการณ์ "init-detector" จาก PitchNode ด้วยการกำหนดค่า WasmPitchDetector
  • ประมวลผลตัวอย่างเสียงที่ได้รับจากกราฟเสียงของเบราว์เซอร์ มอบหมายการคำนวณการตรวจจับระดับเสียงไปยังโมดูล Wasm จากนั้นส่งระดับเสียงที่ตรวจพบกลับไปที่ PitchNode (ซึ่งส่งระดับเสียงไปยังเลเยอร์ React ผ่าน onPitchDetectedCallback )
  • ลงทะเบียนตัวเองภายใต้ชื่อเฉพาะเจาะจง วิธีนี้เบราว์เซอร์จะทราบ—ผ่านคลาสพื้นฐานของ PitchNode ซึ่งเป็น AudioWorkletNode ดั้งเดิม—วิธีสร้างอินสแตนซ์ PitchProcessor ของเราในภายหลังเมื่อ PitchNode ดู setupAudio.js

ไดอะแกรมต่อไปนี้แสดงภาพการไหลของเหตุการณ์ระหว่าง PitchNode และ PitchProcessor :

ผังงานที่มีรายละเอียดมากขึ้นโดยเปรียบเทียบการโต้ตอบระหว่างอ็อบเจ็กต์ PitchNode และ PitchProcess ขณะรันไทม์ ระหว่างการตั้งค่าเริ่มต้น PitchNode จะส่งโมดูล Wasm เป็นอาร์เรย์ของไบต์ไปยัง PitchProcessor ซึ่งจะคอมไพล์และส่งกลับไปยัง PitchNode ซึ่งสุดท้ายจะตอบกลับด้วยข้อความเหตุการณ์ที่ขอให้ PitchProcessor กำหนดค่าเริ่มต้นเอง ขณะบันทึกเสียง PitchNode จะไม่ส่งข้อมูลใดๆ และรับข้อความเหตุการณ์สองประเภทจาก PitchProcessor: ระดับเสียงที่ตรวจพบหรือข้อผิดพลาด หากเกิดขึ้นจาก Wasm หรือเวิร์กเล็ต
ข้อความเหตุการณ์รันไทม์

3. เพิ่มรหัสงานเสียงบนเว็บ

PitchNode.js จัดเตรียมอินเทอร์เฟซสำหรับการประมวลผลเสียงสำหรับการตรวจจับระดับเสียงแบบกำหนดเองของเรา ออบเจ็กต์ PitchNode เป็นกลไกที่ตรวจพบระดับเสียงโดยใช้โมดูล WebAssembly ที่ทำงานในเธรด AudioWorklet จะไปที่เธรดหลักและ React สำหรับการแสดงผล

ใน src/PitchNode.js เราจะซับคลาส AudioWorkletNode ในตัวของ Web Audio API:

 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 คือ:

  • ส่งโมดูล WebAssembly เป็นลำดับของไบต์ดิบ ซึ่งส่งผ่านจาก setupAudio.js ไปยัง PitchProcessor ซึ่งทำงานบนเธรด AudioWorklet นี่คือวิธีที่ PitchProcessor โหลดโมดูล Wasm การตรวจจับระยะห่าง
  • จัดการเหตุการณ์ที่ส่งโดย PitchProcessor เมื่อคอมไพล์ Wasm สำเร็จ และส่งเหตุการณ์อื่นที่ส่งข้อมูลการกำหนดค่าการตรวจจับระยะห่างไปยังเหตุการณ์
  • จัดการระดับเสียงที่ตรวจพบเมื่อมาจาก PitchProcessor และส่งต่อไปยังฟังก์ชัน UI setLatestPitch() ผ่าน onPitchDetectedCallback()

หมายเหตุ: โค้ดของอ็อบเจ็กต์นี้ทำงานบนเธรดหลัก ดังนั้นจึงควรหลีกเลี่ยงการประมวลผลเพิ่มเติมกับระดับเสียงที่ตรวจพบ ในกรณีที่มีราคาแพงและทำให้อัตราเฟรมลดลง

4. เพิ่มโค้ดเพื่อตั้งค่า Web Audio

เพื่อให้เว็บแอปพลิเคชันเข้าถึงและประมวลผลการป้อนข้อมูลสดจากไมโครโฟนของเครื่องไคลเอ็นต์ จะต้อง:

  1. ได้รับอนุญาตจากผู้ใช้สำหรับเบราว์เซอร์ในการเข้าถึงไมโครโฟนที่เชื่อมต่อ
  2. เข้าถึงเอาต์พุตของไมโครโฟนเป็นออบเจ็กต์สตรีมเสียง
  3. แนบโค้ดเพื่อประมวลผลตัวอย่างสตรีมเสียงที่เข้ามาและสร้างลำดับของระดับเสียงที่ตรวจพบ

ใน src/setupAudio.js เราจะทำอย่างนั้น และโหลดโมดูล Wasm แบบอะซิงโครนัสเพื่อให้เราสามารถเริ่มต้น PitchNode ได้ก่อนที่จะแนบ 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 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 Engineering:

  • Web Audio API: ทำไมต้องเขียนเมื่อเขียนโค้ดได้?
  • WebVR ตอนที่ 3: การปลดล็อกศักยภาพของ WebAssembly และ AssemblyScript