บทความก่อนหน้าเกี่ยวกับเวิร์กลิสต์เสียงจะอธิบายแนวคิดพื้นฐานและการใช้งานโดยละเอียด นับตั้งแต่เปิดตัวใน Chrome 66 เราได้คำขอตัวอย่างการใช้งานเพิ่มเติมในแอปพลิเคชันจริงเป็นจำนวนมาก เวิร์กเลตเสียงจะปลดล็อกศักยภาพของ WebAudio อย่างเต็มที่ แต่การใช้ประโยชน์จากเวิร์กเลตอาจเป็นเรื่องยากเนื่องจากต้องเข้าใจการเขียนโปรแกรมแบบพร้อมกันที่รวมอยู่ใน JS API หลายรายการ แม้แต่นักพัฒนาแอปที่คุ้นเคยกับ WebAudio ก็อาจพบว่าการผสานรวม Audio Worklet เข้ากับ API อื่นๆ (เช่น WebAssembly) เป็นเรื่องยาก
บทความนี้จะทําให้ผู้อ่านเข้าใจวิธีใช้ Audio Worklet ในสถานการณ์จริงมากขึ้น รวมถึงให้คําแนะนําในการใช้ฟีเจอร์นี้อย่างเต็มประสิทธิภาพ อย่าลืมดูตัวอย่างโค้ดและการสาธิตแบบสดด้วย
สรุป: เวิร์กเลตเสียง
ก่อนที่จะเจาะลึกต่อไป เรามาทบทวนคำศัพท์และข้อเท็จจริงเกี่ยวกับระบบเวิร์กเลตเสียงซึ่งได้เปิดตัวไปก่อนหน้านี้ในโพสต์นี้กัน
- BaseAudioContext: ออบเจ็กต์หลักของ Web Audio API
- เวิร์กเลตเสียง: โปรแกรมโหลดไฟล์สคริปต์พิเศษสำหรับการดำเนินการเวิร์กเลตเสียง เป็นของ BaseAudioContext BaseAudioContext มี Audio Worklet ได้ 1 รายการ ระบบจะประเมินไฟล์สคริปต์ที่โหลดใน AudioWorkletGlobalScope และใช้เพื่อสร้างอินสแตนซ์ AudioWorkletProcessor
- AudioWorkletGlobalScope : ขอบเขตส่วนกลาง JS พิเศษสําหรับการดําเนินการของ Audio Worklet ทำงานบนเธรดการแสดงผลเฉพาะสำหรับ WebAudio BaseAudioContext อาจมี 1 รายการ AudioWorkletGlobalScope
- AudioWorkletNode : AudioNode ที่ออกแบบมาสำหรับการดำเนินการของ Audio Worklet สร้างจาก BaseAudioContext BaseAudioContext อาจมี AudioWorkletNode หลายรายการ คล้ายกับ AudioNode เดิม
- AudioWorkletProcessor : รายการที่สอดคล้องกับ AudioWorkletNode ข้อมูลจริงของ AudioWorkletNode ที่ประมวลผลสตรีมเสียงด้วยโค้ดที่ผู้ใช้ระบุ อินสแตนซ์จะสร้างขึ้นใน AudioWorkletGlobalScope เมื่อสร้าง AudioWorkletNode AudioWorkletNode อาจมี AudioWorkletProcessor ที่ตรงกัน 1 รายการ
รูปแบบการออกแบบ
การใช้ Audio Worklet กับ WebAssembly
WebAssembly เป็นคู่หูที่สมบูรณ์แบบสำหรับ AudioWorkletProcessor การใช้ทั้ง 2 ฟีเจอร์นี้ร่วมกันจะทำให้เกิดข้อดีหลายประการในการประมวลผลเสียงบนเว็บ แต่ข้อดีที่สำคัญที่สุด 2 ข้อ ได้แก่ ก) การนำโค้ดการประมวลผลเสียง C/C++ ที่มีอยู่มาใช้ในระบบนิเวศ WebAudio และข) การหลีกเลี่ยงค่าใช้จ่ายเพิ่มเติมในการคอมไพล์ JIT ของ JS และการรวบรวมขยะในโค้ดการประมวลผลเสียง
ตัวเลือกแรกสำคัญสำหรับนักพัฒนาแอปที่มีการลงทุนในโค้ดและไลบรารีการประมวลผลเสียงอยู่แล้ว แต่ตัวเลือกที่ 2 สำคัญอย่างยิ่งสำหรับผู้ใช้ API เกือบทั้งหมด ใน WebAudio งบประมาณเวลาสำหรับสตรีมเสียงที่เสถียรนั้นค่อนข้างจำกัด โดยมีเพียง 3 มิลลิวินาทีที่อัตราการสุ่มตัวอย่าง 44.1 Khz แม้แต่ปัญหาเล็กน้อยในโค้ดการประมวลผลเสียงก็อาจทำให้เกิดข้อบกพร่องได้ นักพัฒนาซอฟต์แวร์ต้องเพิ่มประสิทธิภาพโค้ดเพื่อให้ประมวลผลได้เร็วขึ้น แต่ก็ต้องลดจำนวนขยะ JS ที่สร้างขึ้นด้วย การใช้ WebAssembly อาจเป็นโซลูชันที่แก้ปัญหาทั้ง 2 อย่างพร้อมกันได้ เนื่องจากทำงานได้เร็วขึ้นและไม่สร้างขยะจากโค้ด
ส่วนถัดไปจะอธิบายวิธีใช้ WebAssembly กับ Audio Worklet และดูตัวอย่างโค้ดประกอบได้ที่นี่ ดูบทแนะนําพื้นฐานเกี่ยวกับวิธีใช้ Emscripten และ WebAssembly (โดยเฉพาะโค้ดกาว Emscripten) ได้ที่บทความนี้
การตั้งค่า
ฟังดูดีมาก แต่เราจำเป็นต้องมีโครงสร้างเล็กน้อยเพื่อตั้งค่าอย่างถูกต้อง คำถามแรกเกี่ยวกับการออกแบบที่ควรถามคือวิธีและตําแหน่งในการสร้างอินสแตนซ์ของ WebAssembly module หลังจากดึงข้อมูลโค้ดกาวของ Emscripten แล้ว การสร้างอินสแตนซ์ของโมดูลจะมี 2 เส้นทางดังนี้
- สร้างอินสแตนซ์ของโมดูล WebAssembly โดยโหลดโค้ดกาวลงใน
AudioWorkletGlobalScope ผ่าน
audioContext.audioWorklet.addModule()
- สร้างอินสแตนซ์ของโมดูล WebAssembly ในขอบเขตหลัก จากนั้นโอนโมดูลผ่านตัวเลือกคอนสตรัคเตอร์ของ AudioWorkletNode
การตัดสินใจนี้ขึ้นอยู่กับการออกแบบและความต้องการของคุณเป็นหลัก แต่แนวคิดคือ โมดูล WebAssembly สามารถสร้างอินสแตนซ์ WebAssembly ใน AudioWorkletGlobalScope ซึ่งจะกลายเป็นเคอร์เนลการประมวลผลเสียงภายในอินสแตนซ์ AudioWorkletProcessor
Emscripten ต้องมีตัวเลือก 2 รายการเพื่อสร้างโค้ดกาว WebAssembly ที่ถูกต้องสำหรับการกำหนดค่าของเราเพื่อให้รูปแบบ A ทำงานได้อย่างถูกต้อง
-s BINARYEN_ASYNC_COMPILATION=0 -s SINGLE_FILE=1 --post-js mycode.js
ตัวเลือกเหล่านี้ช่วยให้การคอมไพล์โมดูล WebAssembly ใน
AudioWorkletGlobalScope เป็นแบบซิงค์ นอกจากนี้ ยังเพิ่มคำจำกัดความของคลาส AudioWorkletProcessor ใน mycode.js
เพื่อให้โหลดได้หลังจากโมดูลเริ่มต้นแล้ว
เหตุผลหลักในการใช้การคอมไพล์แบบซิงค์คือ การแก้ปัญหาของ audioWorklet.addModule()
จะไม่รอการแก้ปัญหาของ Promise ใน AudioWorkletGlobalScope โดยทั่วไปเราไม่แนะนำให้ใช้การโหลดหรือการคอมไพล์แบบซิงค์ในเธรดหลัก เนื่องจากจะบล็อกงานอื่นๆ ที่อยู่ในเธรดเดียวกัน แต่ในกรณีนี้เราสามารถข้ามกฎได้เนื่องจากการคอมไพล์เกิดขึ้นใน AudioWorkletGlobalScope ซึ่งทำงานจากเธรดหลัก (ดูข้อมูลเพิ่มเติมจากบทความนี้)
รูปแบบ ข. มีประโยชน์ในกรณีที่ต้องดำเนินการแบบไม่สอดคล้องกัน โดยจะใช้เธรดหลักในการดึงข้อมูลโค้ดกาวจากเซิร์ฟเวอร์และคอมไพล์โมดูล จากนั้นจะโอนโมดูล WASM ผ่านคอนสตรัคเตอร์ของ AudioWorkletNode รูปแบบนี้จะเหมาะอย่างยิ่งเมื่อคุณต้องโหลดโมดูลแบบไดนามิกหลังจากที่ AudioWorkletGlobalScope เริ่มแสดงผลสตรีมเสียง การคอมไพล์โมดูลระหว่างการแสดงผลอาจทำให้เกิดข้อบกพร่องในสตรีม ทั้งนี้ขึ้นอยู่กับขนาดของโมดูล
ข้อมูลฮีป WASM และเสียง
โค้ด WebAssembly ใช้งานได้กับหน่วยความจำที่จัดสรรภายในกอง WASM โดยเฉพาะเท่านั้น หากต้องการใช้ประโยชน์จากฟีเจอร์นี้ จะต้องโคลนข้อมูลเสียงไปมาระหว่างกอง WASM กับอาร์เรย์ข้อมูลเสียง คลาส HeapAudioBuffer ในโค้ดตัวอย่างจัดการการดำเนินการนี้ได้อย่างดี
มีข้อเสนอเบื้องต้นที่อยู่ระหว่างการพูดคุยเพื่อผสานรวมกอง WASM เข้ากับระบบเวิร์กเลตเสียงโดยตรง การกำจัดการโคลนข้อมูลที่ซ้ำซ้อนนี้ระหว่างหน่วยความจำ JS กับกอง WASM ดูเหมือนจะเป็นเรื่องปกติ แต่รายละเอียดที่เฉพาะเจาะจงต้องได้รับการแก้ไข
การจัดการขนาดบัฟเฟอร์ไม่ตรงกัน
คู่ AudioWorkletNode และ AudioWorkletProcessor ออกแบบมาให้ทำงานเหมือน AudioNode ปกติ โดย AudioWorkletNode จะจัดการการโต้ตอบกับโค้ดอื่นๆ ส่วน AudioWorkletProcessor จะจัดการการประมวลผลเสียงภายใน เนื่องจาก AudioNode ปกติจะประมวลผล 128 เฟรมพร้อมกัน AudioWorkletProcessor จึงต้องทําแบบเดียวกันจึงจะกลายเป็นฟีเจอร์หลักได้ นี่เป็นข้อดีอย่างหนึ่งของการออกแบบ Audio Worklet ที่ช่วยให้มั่นใจว่าจะไม่มีเวลาในการตอบสนองเพิ่มเติมเนื่องจากการบัฟเฟอร์ภายในภายใน AudioWorkletProcessor แต่อาจเกิดปัญหาได้หากฟังก์ชันการประมวลผลต้องใช้บัฟเฟอร์ขนาดอื่นที่ไม่ใช่ 128 เฟรม โซลูชันทั่วไปสำหรับกรณีเช่นนี้คือการใช้บัฟเฟอร์แบบวงแหวน หรือที่เรียกว่าบัฟเฟอร์แบบวนซ้ำหรือ FIFO
แผนภาพ AudioWorkletProcessor ที่ใช้บัฟเฟอร์แบบวงแหวน 2 วงภายในเพื่อรองรับฟังก์ชัน WASM ที่รับและส่งออก 512 เฟรม (ตัวเลข 512 ที่นี่คือตัวเลขที่เลือกมาโดยพล)
อัลกอริทึมของแผนภาพจะเป็นดังนี้
- AudioWorkletProcessor จะส่ง 128 เฟรมไปยัง Input RingBuffer จากอินพุต
- ทําตามขั้นตอนต่อไปนี้เฉพาะในกรณีที่ Input RingBuffer มีมากกว่าหรือเท่ากับ 512 เฟรม
- ดึง 512 เฟรมจาก Input RingBuffer
- ประมวลผล 512 เฟรมด้วยฟังก์ชัน WASM ที่ระบุ
- พุชเฟรม 512 เฟรมไปยัง Output RingBuffer
- AudioWorkletProcessor จะดึง 128 เฟรมจาก Output RingBuffer เพื่อเติมเอาต์พุต
ดังที่แสดงในแผนภาพ เฟรมอินพุตจะสะสมอยู่ใน Input RingBuffer เสมอ และระบบจะจัดการบัฟเฟอร์ที่ล้นด้วยการเขียนทับบล็อกเฟรมที่เก่าที่สุดในบัฟเฟอร์ ซึ่งเป็นสิ่งที่ควรทำสำหรับแอปพลิเคชันเสียงแบบเรียลไทม์ ในทำนองเดียวกัน ระบบจะดึงบล็อกเฟรมเอาต์พุตออกเสมอ บัฟเฟอร์ที่ต่ำกว่าเกณฑ์ (ข้อมูลไม่เพียงพอ) ใน Output RingBuffer จะส่งผลให้เกิดความเงียบซึ่งทำให้เกิดข้อบกพร่องในสตรีม
รูปแบบนี้มีประโยชน์เมื่อแทนที่ ScriptProcessorNode (SPN) ด้วย AudioWorkletNode เนื่องจาก SPN อนุญาตให้นักพัฒนาแอปเลือกขนาดบัฟเฟอร์ได้ระหว่าง 256 ถึง 16384 เฟรม การแทนที่ SPN ด้วย AudioWorkletNode แบบทันทีจึงอาจทำได้ยาก และการใช้บัฟเฟอร์แบบวงแหวนเป็นวิธีแก้ปัญหาที่ดี เครื่องอัดเสียงเป็นตัวอย่างที่ดีในการสร้างบนพื้นฐานของการออกแบบนี้
อย่างไรก็ตาม โปรดทราบว่าการออกแบบนี้จะปรับยอดขนาดบัฟเฟอร์ที่ไม่ตรงกันเท่านั้น และจะไม่เพิ่มเวลาในการเรียกใช้สคริปต์โค้ดที่ระบุ หากโค้ดทำงานไม่เสร็จภายในงบประมาณเวลาของรีนเดอร์ควอนตัม (~3 มิลลิวินาทีที่ 44.1 Khz) ก็จะส่งผลต่อเวลาเริ่มต้นของฟังก์ชันการเรียกคืนถัดไป และทำให้เกิดข้อบกพร่องในที่สุด
การผสมผสานการออกแบบนี้กับ WebAssembly อาจมีความซับซ้อนเนื่องจากการจัดการหน่วยความจำรอบกอง WASM ขณะเขียนข้อมูลนี้ ข้อมูลขาเข้าและขาออกของกอง WASM ต้องได้รับการโคลน แต่เราสามารถใช้คลาส HeapAudioBuffer เพื่อจัดการหน่วยความจำได้ง่ายขึ้นเล็กน้อย เราจะกล่าวถึงแนวคิดในการใช้หน่วยความจําที่ผู้ใช้จัดสรรเพื่อลดการโคลนข้อมูลที่ซ้ำซ้อนในอนาคต
ดูคลาส RingBuffer ได้ที่นี่
เครื่องมือที่มีประสิทธิภาพของ WebAudio: Audio Worklet และ SharedArrayBuffer
รูปแบบการออกแบบสุดท้ายในบทความนี้คือการนำ API ล้ำสมัยหลายรายการมาไว้ด้วยกัน เช่น Audio Worklet, SharedArrayBuffer, Atomics และ Worker การตั้งค่าที่ไม่ใช่เรื่องง่ายนี้เปิดโอกาสให้ซอฟต์แวร์เสียงที่มีอยู่ซึ่งเขียนด้วย C/C++ ทำงานในเว็บเบราว์เซอร์ได้โดยไม่ทำให้ประสบการณ์ของผู้ใช้สะดุด
ข้อดีข้อใหญ่ที่สุดของการออกแบบนี้คือความสามารถในการใช้ DedicatedWorkerGlobalScope สำหรับการประมวลผลเสียงเท่านั้น ใน Chrome WorkerGlobalScope จะทํางานบนเธรดที่มีลําดับความสําคัญต่ำกว่าเธรดการแสดงผล WebAudio แต่มีข้อดีหลายประการเหนือกว่า AudioWorkletGlobalScope DedicatedWorkerGlobalScope มีข้อจำกัดน้อยกว่าในแง่ของแพลตฟอร์ม API ที่พร้อมใช้งานในขอบเขต นอกจากนี้ คุณยังจะได้รับการสนับสนุนที่ดียิ่งขึ้นจาก Emscripten เนื่องจาก Worker API มีมานานแล้ว
SharedArrayBuffer มีบทบาทสําคัญต่อการออกแบบนี้ให้ทํางานได้อย่างมีประสิทธิภาพ แม้ว่าทั้ง Worker และ AudioWorkletProcessor จะมีการรับส่งข้อความแบบแอซิงโครนัส (MessagePort) แต่ก็ไม่เหมาะสําหรับการประมวลผลเสียงแบบเรียลไทม์เนื่องจากการจัดสรรหน่วยความจําซ้ำๆ และเวลาในการตอบสนองของการรับส่งข้อความ เราจึงจัดสรรบล็อกหน่วยความจำล่วงหน้าที่เข้าถึงได้จากทั้ง 2 เธรดเพื่อการโอนข้อมูลแบบ 2 ทิศทางที่รวดเร็ว
จากมุมมองของผู้ที่เชี่ยวชาญ Web Audio API การออกแบบนี้อาจดูไม่เหมาะที่สุดเนื่องจากใช้ Audio Worklet เป็น "Audio Sink" ที่เรียบง่ายและทำทุกอย่างใน Worker แต่เนื่องจากค่าใช้จ่ายในการเขียนโปรเจ็กต์ C/C++ ใหม่ใน JavaScript อาจสูงเกินกว่าที่ยอมรับได้หรือเป็นไปไม่ได้เลย การออกแบบนี้จึงอาจเป็นเส้นทางการใช้งานที่มีประสิทธิภาพที่สุดสำหรับโปรเจ็กต์ดังกล่าว
สถานะและอะตอมที่แชร์
เมื่อใช้หน่วยความจําที่แชร์สําหรับข้อมูลเสียง คุณต้องประสานการเข้าถึงจากทั้ง 2 ด้านอย่างระมัดระวัง การแชร์สถานะที่เข้าถึงแบบอะตอมเป็นวิธีแก้ปัญหาดังกล่าว เราสามารถใช้ Int32Array
ที่สนับสนุนโดย SAB เพื่อวัตถุประสงค์นี้ได้
กลไกการซิงค์: SharedArrayBuffer และ Atomics
แต่ละช่องของอาร์เรย์ States จะแสดงข้อมูลสําคัญเกี่ยวกับบัฟเฟอร์ที่แชร์ ฟิลด์ที่สําคัญที่สุดคือฟิลด์สําหรับการซิงค์ (REQUEST_RENDER
) แนวคิดคือ Worker จะรอให้ AudioWorkletProcessor เข้าถึงฟิลด์นี้และประมวลผลเสียงเมื่อตื่นขึ้นมา Atomics API ช่วยให้กลไกนี้เป็นไปได้ นอกเหนือจาก SharedArrayBuffer (SAB)
โปรดทราบว่าการซิงค์ของ 2 เทรดค่อนข้างหลวม Worker.process()
จะเริ่มต้นด้วยวิธี AudioWorkletProcessor.process()
แต่ AudioWorkletProcessor จะไม่รอจนกว่า Worker.process()
จะเสร็จสิ้น การดำเนินการนี้เป็นไปตามการออกแบบ AudioWorkletProcessor ทำงานด้วยคอลแบ็กเสียง จึงต้องไม่บล็อกแบบซิงค์ ในกรณีที่แย่ที่สุด สตรีมเสียงอาจซ้ำกันหรือตัดออก แต่ในที่สุดก็จะกลับมาทำงานได้เมื่อประสิทธิภาพการแสดงผลมีเสถียร
การตั้งค่าและเรียกใช้
ดังที่แสดงในแผนภาพด้านบน การออกแบบนี้มีคอมโพเนนต์หลายอย่างที่ต้องจัดเรียง ได้แก่ DedicatedWorkerGlobalScope (DWGS), AudioWorkletGlobalScope (AWGS), SharedArrayBuffer และ main thread ขั้นตอนต่อไปนี้อธิบายสิ่งที่ควรเกิดขึ้นในระยะเริ่มต้น
การเริ่มต้น
- [Main] เครื่องมือสร้าง AudioWorkletNode ได้รับคําเรียก
- สร้างผู้ปฏิบัติงาน
- ระบบจะสร้าง AudioWorkletProcessor ที่เชื่อมโยง
- [DWGS] Worker สร้าง SharedArrayBuffer 2 รายการ (1 รายการสำหรับสถานะที่แชร์และอีกรายการสำหรับข้อมูลเสียง)
- [DWGS] Worker ส่งการอ้างอิง SharedArrayBuffer ไปยัง AudioWorkletNode
- [Main] AudioWorkletNode ส่งการอ้างอิง SharedArrayBuffer ไปยัง AudioWorkletProcessor
- [AWGS] AudioWorkletProcessor แจ้งให้ AudioWorkletNode ทราบว่าการตั้งค่าเสร็จสมบูรณ์แล้ว
เมื่อการเริ่มต้นเสร็จสมบูรณ์ AudioWorkletProcessor.process()
จะเริ่มเรียกใช้ ต่อไปนี้คือสิ่งที่ควรเกิดขึ้นในการวนซ้ำแต่ละครั้งของลูปการแสดงผล
ลูปการแสดงผล
- [AWGS]
AudioWorkletProcessor.process(inputs, outputs)
gets called for every render quantum.- ระบบจะส่ง
inputs
ไปยัง Input SAB outputs
จะได้รับการกรอกข้อมูลด้วยการใช้ข้อมูลเสียงใน Output SAB- อัปเดต States SAB ด้วยดัชนีบัฟเฟอร์ใหม่ตามความเหมาะสม
- หาก Output SAB ใกล้กับเกณฑ์ Underflow ให้ Wake Worker เพื่อแสดงผลข้อมูลเสียงเพิ่มเติม
- ระบบจะส่ง
- [DWGS] ผู้ปฏิบัติงานรอ (สลีป) สัญญาณการปลุกจาก
AudioWorkletProcessor.process()
สิ่งที่จะเกิดขึ้นเมื่ออุปกรณ์ตื่นขึ้นมีดังนี้- ดึงข้อมูลดัชนีบัฟเฟอร์จาก States SAB
- เรียกใช้ฟังก์ชันการประมวลผลด้วยข้อมูลจาก Input SAB เพื่อป้อนข้อมูลใน Output SAB
- อัปเดต States SAB ด้วยดัชนีบัฟเฟอร์ตามความเหมาะสม
- เข้าสู่โหมดสลีปและรอสัญญาณถัดไป
ดูโค้ดตัวอย่างได้ที่นี่ แต่โปรดทราบว่าต้องเปิดใช้ Flag แบบทดลองของ SharedArrayBuffer เพื่อให้การสาธิตนี้ทำงานได้ โค้ดนี้เขียนด้วยโค้ด JS ล้วนๆ เพื่อความเรียบง่าย แต่สามารถแทนที่ด้วยโค้ด WebAssembly ได้หากต้องการ กรณีเช่นนี้ควรจัดการอย่างระมัดระวังเป็นพิเศษโดยรวมการจัดการหน่วยความจำไว้ในคลาส HeapAudioBuffer
บทสรุป
เป้าหมายสูงสุดของ Audio Worklet คือทำให้ Web Audio API "ขยายได้" อย่างแท้จริง การออกแบบนี้ใช้เวลาหลายปีเพื่อให้สามารถติดตั้งใช้งาน Web Audio API ที่เหลือกับ Audio Worklet ได้ ด้วยเหตุนี้ การออกแบบจึงมีความซับซ้อนมากขึ้น ซึ่งอาจเป็นความท้าทายที่ไม่คาดคิด
แต่ความซับซ้อนนี้เกิดขึ้นเพื่อช่วยให้นักพัฒนาแอปสามารถดำเนินการต่างๆ ได้อย่างเต็มที่ ความสามารถในการเรียกใช้ WebAssembly ใน AudioWorkletGlobalScope จะปลดล็อกศักยภาพอันยิ่งใหญ่สำหรับการประมวลผลเสียงที่มีประสิทธิภาพสูงบนเว็บ สําหรับแอปพลิเคชันเสียงขนาดใหญ่ที่เขียนด้วย C หรือ C++ การใช้เวิร์กเลตเสียงที่มี SharedArrayBuffers และ Workers อาจเป็นตัวเลือกที่น่าสนใจ
เครดิต
ขอขอบคุณเป็นพิเศษ Chris Wilson, Jason Miller, Joshua Bell และ Raymond Toy ที่ช่วยตรวจสอบร่างบทความนี้และให้ความคิดเห็นที่เป็นประโยชน์