الگوی طراحی کارنامه صوتی

Hongchan Choi

مقاله قبلی در مورد Audio Worklet مفاهیم اساسی و کاربرد آن را شرح داد. از زمان راه‌اندازی آن در Chrome 66، درخواست‌های زیادی برای نمونه‌های بیشتری از نحوه استفاده از آن در برنامه‌های کاربردی واقعی وجود داشته است. Audio Worklet پتانسیل کامل WebAudio را باز می کند، اما استفاده از آن می تواند چالش برانگیز باشد زیرا نیاز به درک برنامه نویسی همزمان با چندین API JS دارد. حتی برای توسعه دهندگانی که با WebAudio آشنا هستند، ادغام Audio Worklet با سایر APIها (مثلا WebAssembly) می تواند دشوار باشد.

این مقاله به خواننده درک بهتری از نحوه استفاده از Audio Worklet در تنظیمات دنیای واقعی و ارائه نکاتی برای استفاده از حداکثر قدرت آن ارائه می دهد. حتماً نمونه کدها و دموهای زنده را نیز بررسی کنید!

خلاصه: Worklet صوتی

قبل از غواصی، اجازه دهید به سرعت اصطلاحات و حقایق مربوط به سیستم Audio Worklet را که قبلاً در این پست معرفی شده بود، مرور کنیم.

  • BaseAudioContext : شی اصلی Web Audio API.
  • Audio Worklet : یک بارکننده فایل اسکریپت ویژه برای عملیات Audio Worklet. متعلق به BaseAudioContext است. BaseAudioContext می تواند یک Audio Worklet داشته باشد. فایل اسکریپت بارگذاری شده در AudioWorkletGlobalScope ارزیابی می شود و برای ایجاد نمونه های AudioWorkletProcessor استفاده می شود.
  • AudioWorkletGlobalScope : یک حوزه جهانی ویژه JS برای عملیات Audio Worklet. روی یک رشته رندر اختصاصی برای WebAudio اجرا می شود. یک BaseAudioContext می تواند یک AudioWorkletGlobalScope داشته باشد.
  • AudioWorkletNode : یک AudioNode که برای عملیات Audio Worklet طراحی شده است. نمونه سازی شده از BaseAudioContext. یک BaseAudioContext می‌تواند چندین AudioWorkletNode مشابه AudioNodes اصلی داشته باشد.
  • AudioWorkletProcessor : همتای AudioWorkletNode. جرات واقعی AudioWorkletNode که جریان صوتی را توسط کد ارائه شده توسط کاربر پردازش می کند. هنگامی که AudioWorkletNode ساخته می شود در AudioWorkletGlobalScope نمونه سازی می شود. یک AudioWorkletNode می تواند یک AudioWorkletProcessor مشابه داشته باشد.

الگوهای طراحی

استفاده از Audio Worklet با WebAssembly

WebAssembly یک همراه عالی برای AudioWorkletProcessor است. ترکیب این دو ویژگی مزایای مختلفی را برای پردازش صدا در وب به ارمغان می‌آورد، اما دو مزیت بزرگ عبارتند از: الف) وارد کردن کد پردازش صوتی C/C++ موجود به اکوسیستم WebAudio و ب) اجتناب از سربار کامپایل JS JIT و جمع‌آوری زباله در کد پردازش صدا.

مورد اول برای توسعه‌دهندگانی که سرمایه‌گذاری در کدهای پردازش صوتی و کتابخانه‌ها دارند، مهم است، اما دومی تقریباً برای همه کاربران API حیاتی است. در دنیای WebAudio، بودجه زمان‌بندی برای جریان صوتی پایدار بسیار سخت است: تنها 3 میلی‌ثانیه با نرخ نمونه 44.1 کیلوهرتز است. حتی یک وقفه جزئی در کد پردازش صدا می تواند باعث اشکال شود. توسعه‌دهنده باید کد را برای پردازش سریع‌تر بهینه‌سازی کند، اما همچنین میزان تولید زباله JS را به حداقل برساند. استفاده از WebAssembly می تواند راه حلی باشد که هر دو مشکل را همزمان برطرف می کند: سریعتر است و هیچ زباله ای از کد تولید نمی کند.

بخش بعدی نحوه استفاده از WebAssembly را با یک Audio Worklet توضیح می دهد و نمونه کد همراه را می توان در اینجا یافت. برای آموزش اولیه نحوه استفاده از Emscripten و WebAssembly (مخصوصاً کد چسب Emscripten)، لطفاً به این مقاله نگاهی بیندازید.

راه اندازی

همه چیز عالی به نظر می رسد، اما برای تنظیم درست چیزها به کمی ساختار نیاز داریم. اولین سوال طراحی این است که چگونه و کجا یک ماژول WebAssembly را نمونه سازی کنیم. پس از واکشی کد چسب Emscripten، دو مسیر برای نمونه سازی ماژول وجود دارد:

  1. یک ماژول WebAssembly را با بارگذاری کد چسب در AudioWorkletGlobalScope از طریق audioContext.audioWorklet.addModule() نمونه سازی کنید.
  2. یک ماژول WebAssembly را در محدوده اصلی نمونه سازی کنید، سپس ماژول را از طریق گزینه های سازنده AudioWorkletNode منتقل کنید.

تصمیم تا حد زیادی به طراحی و ترجیح شما بستگی دارد، اما ایده این است که ماژول WebAssembly می تواند یک نمونه WebAssembly در AudioWorkletGlobalScope ایجاد کند، که به یک هسته پردازش صوتی در یک نمونه AudioWorkletProcessor تبدیل می شود.

الگوی نمونه سازی ماژول WebAssembly A: با استفاده از فراخوانی ()addModule
الگوی نمونه سازی ماژول WebAssembly A: با استفاده از فراخوانی .addModule() addModule

برای اینکه الگوی A به درستی کار کند، Emscripten به چند گزینه برای ایجاد کد چسب WebAssembly صحیح برای پیکربندی ما نیاز دارد:

-s BINARYEN_ASYNC_COMPILATION=0 -s SINGLE_FILE=1 --post-js mycode.js

این گزینه ها کامپایل همزمان یک ماژول WebAssembly را در AudioWorkletGlobalScope تضمین می کنند. همچنین تعریف کلاس AudioWorkletProcessor را در mycode.js اضافه می‌کند تا بتوان پس از مقداردهی اولیه ماژول بارگذاری کرد. دلیل اصلی برای استفاده از کامپایل همزمان این است که وضوح وعده audioWorklet.addModule() منتظر رزولوشن وعده ها در AudioWorkletGlobalScope نمی ماند. بارگیری یا کامپایل همزمان در رشته اصلی معمولاً توصیه نمی شود زیرا سایر وظایف را در همان رشته مسدود می کند، اما در اینجا می توانیم قانون را دور بزنیم زیرا کامپایل در AudioWorkletGlobalScope انجام می شود که از رشته اصلی خارج می شود. (برای اطلاعات بیشتر این را ببینید.)

الگوی نمونه سازی ماژول WASM B: با استفاده از سازنده AudioWorkletNode     انتقال بین رشته ای
الگوی نمونه سازی ماژول WASM B: استفاده از انتقال بین رشته ای سازنده AudioWorkletNode

الگوی B می تواند در صورت نیاز به بلند کردن ناهمزمان سنگین مفید باشد. از رشته اصلی برای واکشی کد چسب از سرور و کامپایل ماژول استفاده می کند. سپس ماژول WASM را از طریق سازنده AudioWorkletNode منتقل می کند. این الگو زمانی منطقی‌تر می‌شود که بعد از اینکه AudioWorkletGlobalScope شروع به رندر کردن جریان صوتی کرد، ماژول را به صورت پویا بارگیری کنید. بسته به اندازه ماژول، کامپایل کردن آن در وسط رندر می تواند باعث ایجاد اشکال در جریان شود.

WASM Heap و داده های صوتی

کد WebAssembly فقط روی حافظه اختصاص داده شده در یک پشته اختصاصی WASM کار می کند. برای استفاده از آن، داده‌های صوتی باید بین پشته WASM و آرایه‌های داده صوتی شبیه‌سازی شوند. کلاس HeapAudioBuffer در کد مثال به خوبی این عملیات را انجام می دهد.

کلاس HeapAudioBuffer برای استفاده راحت تر از پشته WASM
کلاس HeapAudioBuffer برای استفاده راحت تر از پشته WASM

یک پیشنهاد اولیه برای ادغام پشته WASM به طور مستقیم در سیستم Worklet صوتی وجود دارد. خلاص شدن از شر این شبیه سازی اطلاعات اضافی بین حافظه JS و پشته WASM طبیعی به نظر می رسد، اما جزئیات خاص باید بررسی شوند.

مدیریت عدم تطابق اندازه بافر

یک جفت AudioWorkletNode و AudioWorkletProcessor طوری طراحی شده است که مانند یک AudioNode معمولی کار کند. AudioWorkletNode تعامل با کدهای دیگر را کنترل می کند در حالی که AudioWorkletProcessor از پردازش صوتی داخلی مراقبت می کند. از آنجا که یک AudioNode معمولی 128 فریم را در یک زمان پردازش می کند، AudioWorkletProcessor باید همین کار را انجام دهد تا به یک ویژگی اصلی تبدیل شود. این یکی از مزایای طراحی Audio Worklet است که تضمین می‌کند هیچ تأخیر اضافی به دلیل بافر داخلی در AudioWorkletProcessor معرفی نمی‌شود، اما اگر یک تابع پردازشی به اندازه بافری متفاوت از 128 فریم نیاز داشته باشد، می‌تواند مشکل ساز شود. راه حل رایج برای چنین مواردی استفاده از بافر حلقه ای است که به عنوان بافر دایره ای یا FIFO نیز شناخته می شود.

در اینجا نمودار AudioWorkletProcessor با استفاده از دو بافر حلقه در داخل برای قرار دادن یک تابع WASM است که 512 فریم را به داخل و خارج می کند. (عدد 512 در اینجا خودسرانه انتخاب شده است.)

استفاده از RingBuffer در روش «process()» AudioWorkletProcessor
استفاده از RingBuffer در روش «process()» AudioWorkletProcessor

الگوریتم نمودار به صورت زیر خواهد بود:

  1. AudioWorkletProcessor 128 فریم را از ورودی خود به داخل RingBuffer ورودی فشار می دهد.
  2. فقط در صورتی مراحل زیر را انجام دهید که RingBuffer ورودی بزرگتر یا مساوی 512 فریم باشد.
    1. 512 فریم را از RingBuffer ورودی بکشید.
    2. 512 فریم را با تابع WASM داده شده پردازش کنید.
    3. 512 فریم را به خروجی RingBuffer فشار دهید.
  3. AudioWorkletProcessor 128 فریم را از RingBuffer خروجی می کشد تا خروجی آن را پر کند.

همانطور که در نمودار نشان داده شده است، فریم های ورودی همیشه در ورودی RingBuffer جمع می شوند و با بازنویسی قدیمی ترین بلوک فریم در بافر، سرریز بافر را کنترل می کند. این یک کار معقول برای یک برنامه صوتی بلادرنگ است. به طور مشابه، بلوک فریم خروجی همیشه توسط سیستم کشیده می شود. جریان جریان بافر (داده کافی نیست) در خروجی RingBuffer باعث خاموشی و ایجاد یک اشکال در جریان می شود.

این الگو هنگام جایگزینی ScriptProcessorNode (SPN) با AudioWorkletNode مفید است. از آنجایی که SPN به توسعه‌دهنده اجازه می‌دهد تا اندازه بافری بین 256 تا 16384 فریم انتخاب کند، بنابراین جایگزینی SPN با AudioWorkletNode می‌تواند دشوار باشد و استفاده از بافر حلقه راه‌حل خوبی را فراهم می‌کند. یک ضبط کننده صدا نمونه بسیار خوبی است که می تواند در بالای این طرح ساخته شود.

با این حال، درک این نکته مهم است که این طرح فقط عدم تطابق اندازه بافر را تطبیق می دهد و زمان بیشتری برای اجرای کد اسکریپت داده شده نمی دهد. اگر کد نتواند کار را در بودجه زمانی رندر کوانتومی (~ 3 میلی‌ثانیه در 44.1 کیلوهرتز) به پایان برساند، بر زمان‌بندی شروع عملکرد برگشت تماس بعدی تأثیر می‌گذارد و در نهایت باعث ایجاد اشکال می‌شود.

اختلاط این طرح با WebAssembly به دلیل مدیریت حافظه در اطراف پشته WASM می تواند پیچیده باشد. در زمان نوشتن، داده‌هایی که وارد و خارج از هیپ WASM می‌شوند باید شبیه‌سازی شوند، اما می‌توانیم از کلاس HeapAudioBuffer برای تسهیل مدیریت حافظه استفاده کنیم. ایده استفاده از حافظه اختصاص داده شده توسط کاربر برای کاهش شبیه سازی داده های اضافی در آینده مورد بحث قرار خواهد گرفت.

کلاس RingBuffer را می توانید در اینجا پیدا کنید.

WebAudio Powerhouse: Audio Worklet و SharedArrayBuffer

آخرین الگوی طراحی در این مقاله قرار دادن چندین API پیشرفته در یک مکان است. Audio Worklet، SharedArrayBuffer ، Atomics و Worker . با این تنظیمات غیر پیش پا افتاده، مسیری را برای نرم افزارهای صوتی موجود نوشته شده با C/C++ باز می کند تا در مرورگر وب اجرا شود و در عین حال تجربه کاربری روانی را حفظ کند.

مروری بر آخرین الگوی طراحی: Audio Worklet، SharedArrayBuffer و Worker
مروری بر آخرین الگوی طراحی: Audio Worklet، SharedArrayBuffer و Worker

بزرگترین مزیت این طراحی استفاده از DedicatedWorkerGlobalScope تنها برای پردازش صدا است. در Chrome، WorkerGlobalScope روی رشته‌ای با اولویت پایین‌تر از رشته رندر WebAudio اجرا می‌شود، اما چندین مزیت نسبت به AudioWorkletGlobalScope دارد. DedicatedWorkerGlobalScope از نظر سطح API موجود در محدوده کمتر محدود است. همچنین می توانید انتظار پشتیبانی بهتری از Emscripten داشته باشید زیرا Worker API چند سالی است که وجود دارد.

SharedArrayBuffer نقش مهمی برای این طراحی ایفا می کند تا کارآمد باشد. اگرچه Worker و AudioWorkletProcessor هر دو به پیام‌رسانی ناهمزمان ( MessagePort ) مجهز هستند، به دلیل تخصیص مکرر حافظه و تأخیر پیام‌رسانی، برای پردازش صدا در زمان واقعی بسیار مناسب نیست. بنابراین ما یک بلوک حافظه را در جلو اختصاص می دهیم که می تواند از هر دو رشته برای انتقال سریع داده های دو طرفه قابل دسترسی باشد.

از دیدگاه ناظر Web Audio API، این طراحی ممکن است بهینه به نظر نرسد زیرا از Audio Worklet به عنوان یک "سینک صوتی" ساده استفاده می کند و همه کارها را در Worker انجام می دهد. اما با توجه به هزینه بازنویسی پروژه های C/C++ در جاوا اسکریپت می تواند بسیار زیاد و یا حتی غیرممکن باشد، این طراحی می تواند کارآمدترین مسیر پیاده سازی برای چنین پروژه هایی باشد.

ایالات مشترک و اتمی

هنگام استفاده از حافظه مشترک برای داده های صوتی، دسترسی از هر دو طرف باید به دقت هماهنگ شود. به اشتراک گذاشتن حالت های قابل دسترسی اتمی راه حلی برای چنین مشکلی است. ما می توانیم از Int32Array که توسط SAB پشتیبانی می شود برای این منظور استفاده کنیم.

مکانیسم همگام سازی: SharedArrayBuffer و Atomics
مکانیسم همگام سازی: SharedArrayBuffer و Atomics

مکانیسم همگام سازی: SharedArrayBuffer و Atomics

هر فیلد از آرایه ایالات نشان دهنده اطلاعات حیاتی در مورد بافرهای مشترک است. مهمترین آنها یک فیلد برای همگام سازی است ( REQUEST_RENDER ). ایده این است که Worker منتظر می ماند تا این فیلد توسط AudioWorkletProcessor لمس شود و هنگامی که از خواب بیدار شد، صدا را پردازش کند. همراه با SharedArrayBuffer (SAB)، Atomics API این مکانیسم را ممکن می سازد.

توجه داشته باشید که همگام سازی دو رشته نسبتا شل است. شروع Worker.process() با روش AudioWorkletProcessor.process() راه اندازی می شود، اما AudioWorkletProcessor منتظر نمی ماند تا Worker.process() تمام شود. این بر اساس طراحی است. AudioWorkletProcessor توسط پاسخ تماس صوتی هدایت می شود، بنابراین نباید به طور همزمان مسدود شود. در بدترین حالت ممکن است جریان صوتی تکراری یا حذف شود، اما در نهایت وقتی عملکرد رندر تثبیت شود، بازیابی می‌شود.

راه اندازی و اجرا

همانطور که در نمودار بالا نشان داده شده است، این طرح دارای چندین مؤلفه برای ترتیب است: DedicatedWorkerGlobalScope (DWGS)، AudioWorkletGlobalScope (AWGS)، SharedArrayBuffer و رشته اصلی. مراحل زیر آنچه را که باید در مرحله اولیه سازی اتفاق بیفتد توضیح می دهد.

مقداردهی اولیه
  1. [اصلی] سازنده AudioWorkletNode فراخوانی می شود.
    1. کارگر ایجاد کنید.
    2. AudioWorkletProcessor مرتبط ایجاد خواهد شد.
  2. [DWGS] Worker 2 SharedArrayBuffer ایجاد می کند. (یکی برای حالت های مشترک و دیگری برای داده های صوتی)
  3. [DWGS] Worker ارجاعات SharedArrayBuffer را به AudioWorkletNode می فرستد.
  4. [اصلی] AudioWorkletNode ارجاعات SharedArrayBuffer را به AudioWorkletProcessor ارسال می کند.
  5. [AWGS] AudioWorkletProcessor به AudioWorkletNode اطلاع می دهد که راه اندازی کامل شده است.

پس از تکمیل اولیه، AudioWorkletProcessor.process() شروع به فراخوانی می کند. آنچه در زیر باید در هر تکرار از حلقه رندر اتفاق بیفتد، آمده است.

حلقه رندرینگ
رندر چند رشته ای با SharedArrayBuffers
رندر چند رشته ای با SharedArrayBuffers
  1. [AWGS] AudioWorkletProcessor.process(inputs, outputs) برای هر کوانتوم رندر فراخوانی می شود.
    1. inputs به ورودی SAB فشار داده می شوند.
    2. outputs با مصرف داده های صوتی در خروجی SAB پر می شوند.
    3. SAB ایالات را با شاخص های بافر جدید به روز می کند.
    4. اگر خروجی SAB به آستانه underflow نزدیک شود، Wake Worker داده های صوتی بیشتری را ارائه می دهد.
  2. [DWGS] Worker منتظر سیگنال بیداری از AudioWorkletProcessor.process() می‌ماند (می‌خوابد). وقتی بیدار شد:
    1. شاخص های بافر را از State SAB واکشی می کند.
    2. تابع فرآیند را با داده های ورودی SAB اجرا کنید تا خروجی SAB پر شود.
    3. SAB ایالات را با شاخص های بافر بر این اساس به روز می کند.
    4. به خواب می رود و منتظر سیگنال بعدی است.

کد مثال را می‌توانید در اینجا پیدا کنید، اما توجه داشته باشید که پرچم آزمایشی SharedArrayBuffer باید فعال باشد تا این نسخه نمایشی کار کند. کد با کد JS خالص برای سادگی نوشته شده است، اما در صورت نیاز می توان آن را با کد WebAssembly جایگزین کرد. چنین موردی باید با احتیاط بیشتری با قرار دادن مدیریت حافظه با کلاس HeapAudioBuffer رسیدگی شود.

نتیجه گیری

هدف نهایی Audio Worklet این است که Web Audio API را واقعاً "بسط پذیر" کند. تلاش چند ساله ای برای طراحی آن انجام شد تا امکان پیاده سازی بقیه Web Audio API با Audio Worklet فراهم شود. به نوبه خود، اکنون ما پیچیدگی بالاتری در طراحی آن داریم و این می تواند یک چالش غیرمنتظره باشد.

خوشبختانه، دلیل چنین پیچیدگی صرفاً توانمندسازی توسعه دهندگان است. امکان اجرای WebAssembly در AudioWorkletGlobalScope پتانسیل عظیمی را برای پردازش صوتی با کارایی بالا در وب باز می کند. برای برنامه های صوتی در مقیاس بزرگ که به زبان C یا C++ نوشته شده اند، استفاده از Audio Worklet با SharedArrayBuffers و Workers می تواند گزینه جذابی برای کاوش باشد.

اعتبارات

تشکر ویژه از کریس ویلسون، جیسون میلر، جاشوا بل و ریموند اسباب بازی برای بررسی پیش نویس این مقاله و ارائه بازخورد روشنگرانه.

،

Hongchan Choi

مقاله قبلی در مورد Audio Worklet مفاهیم اساسی و کاربرد آن را شرح داد. از زمان راه‌اندازی آن در Chrome 66، درخواست‌های زیادی برای نمونه‌های بیشتری از نحوه استفاده از آن در برنامه‌های کاربردی واقعی وجود داشته است. Audio Worklet پتانسیل کامل WebAudio را باز می کند، اما استفاده از آن می تواند چالش برانگیز باشد زیرا نیاز به درک برنامه نویسی همزمان با چندین API JS دارد. حتی برای توسعه دهندگانی که با WebAudio آشنا هستند، ادغام Audio Worklet با سایر APIها (مثلا WebAssembly) می تواند دشوار باشد.

این مقاله به خواننده درک بهتری از نحوه استفاده از Audio Worklet در تنظیمات دنیای واقعی و ارائه نکاتی برای استفاده از حداکثر قدرت آن ارائه می دهد. حتماً نمونه کدها و دموهای زنده را نیز بررسی کنید!

خلاصه: Worklet صوتی

قبل از غواصی، اجازه دهید به سرعت اصطلاحات و حقایق مربوط به سیستم Audio Worklet را که قبلاً در این پست معرفی شده بود، مرور کنیم.

  • BaseAudioContext : شی اصلی Web Audio API.
  • Audio Worklet : یک بارکننده فایل اسکریپت ویژه برای عملیات Audio Worklet. متعلق به BaseAudioContext است. BaseAudioContext می تواند یک Audio Worklet داشته باشد. فایل اسکریپت بارگذاری شده در AudioWorkletGlobalScope ارزیابی می شود و برای ایجاد نمونه های AudioWorkletProcessor استفاده می شود.
  • AudioWorkletGlobalScope : یک حوزه جهانی ویژه JS برای عملیات Audio Worklet. روی یک رشته رندر اختصاصی برای WebAudio اجرا می شود. یک BaseAudioContext می تواند یک AudioWorkletGlobalScope داشته باشد.
  • AudioWorkletNode : یک AudioNode که برای عملیات Audio Worklet طراحی شده است. نمونه سازی شده از BaseAudioContext. یک BaseAudioContext می‌تواند چندین AudioWorkletNode مشابه AudioNodes اصلی داشته باشد.
  • AudioWorkletProcessor : همتای AudioWorkletNode. جرات واقعی AudioWorkletNode که جریان صوتی را توسط کد ارائه شده توسط کاربر پردازش می کند. هنگامی که AudioWorkletNode ساخته می شود در AudioWorkletGlobalScope نمونه سازی می شود. یک AudioWorkletNode می تواند یک AudioWorkletProcessor مشابه داشته باشد.

الگوهای طراحی

استفاده از Audio Worklet با WebAssembly

WebAssembly یک همراه عالی برای AudioWorkletProcessor است. ترکیب این دو ویژگی مزایای مختلفی را برای پردازش صدا در وب به ارمغان می‌آورد، اما دو مزیت بزرگ عبارتند از: الف) وارد کردن کد پردازش صوتی C/C++ موجود به اکوسیستم WebAudio و ب) اجتناب از سربار کامپایل JS JIT و جمع‌آوری زباله در کد پردازش صدا.

مورد اول برای توسعه‌دهندگانی که سرمایه‌گذاری در کدهای پردازش صوتی و کتابخانه‌ها دارند، مهم است، اما دومی تقریباً برای همه کاربران API حیاتی است. در دنیای WebAudio، بودجه زمان‌بندی برای جریان صوتی پایدار بسیار سخت است: تنها 3 میلی‌ثانیه با نرخ نمونه 44.1 کیلوهرتز است. حتی یک وقفه جزئی در کد پردازش صدا می تواند باعث اشکال شود. توسعه‌دهنده باید کد را برای پردازش سریع‌تر بهینه‌سازی کند، اما همچنین میزان تولید زباله JS را به حداقل برساند. استفاده از WebAssembly می تواند راه حلی باشد که هر دو مشکل را همزمان برطرف می کند: سریعتر است و هیچ زباله ای از کد تولید نمی کند.

بخش بعدی نحوه استفاده از WebAssembly را با یک Audio Worklet توضیح می دهد و نمونه کد همراه را می توان در اینجا یافت. برای آموزش اولیه نحوه استفاده از Emscripten و WebAssembly (مخصوصاً کد چسب Emscripten)، لطفاً به این مقاله نگاهی بیندازید.

راه اندازی

همه چیز عالی به نظر می رسد، اما برای تنظیم درست چیزها به کمی ساختار نیاز داریم. اولین سوال طراحی این است که چگونه و کجا یک ماژول WebAssembly را نمونه سازی کنیم. پس از واکشی کد چسب Emscripten، دو مسیر برای نمونه سازی ماژول وجود دارد:

  1. یک ماژول WebAssembly را با بارگذاری کد چسب در AudioWorkletGlobalScope از طریق audioContext.audioWorklet.addModule() نمونه سازی کنید.
  2. یک ماژول WebAssembly را در محدوده اصلی نمونه سازی کنید، سپس ماژول را از طریق گزینه های سازنده AudioWorkletNode منتقل کنید.

تصمیم تا حد زیادی به طراحی و ترجیح شما بستگی دارد، اما ایده این است که ماژول WebAssembly می تواند یک نمونه WebAssembly در AudioWorkletGlobalScope ایجاد کند، که به یک هسته پردازش صوتی در یک نمونه AudioWorkletProcessor تبدیل می شود.

الگوی نمونه سازی ماژول WebAssembly A: با استفاده از فراخوانی ()addModule
الگوی نمونه سازی ماژول WebAssembly A: با استفاده از فراخوانی .addModule() addModule

برای اینکه الگوی A به درستی کار کند، Emscripten به چند گزینه برای ایجاد کد چسب WebAssembly صحیح برای پیکربندی ما نیاز دارد:

-s BINARYEN_ASYNC_COMPILATION=0 -s SINGLE_FILE=1 --post-js mycode.js

این گزینه ها کامپایل همزمان یک ماژول WebAssembly را در AudioWorkletGlobalScope تضمین می کنند. همچنین تعریف کلاس AudioWorkletProcessor را در mycode.js اضافه می‌کند تا بتوان پس از مقداردهی اولیه ماژول بارگذاری کرد. دلیل اصلی برای استفاده از کامپایل همزمان این است که وضوح وعده audioWorklet.addModule() منتظر رزولوشن وعده ها در AudioWorkletGlobalScope نمی ماند. بارگیری یا کامپایل همزمان در رشته اصلی معمولاً توصیه نمی شود زیرا سایر وظایف را در همان رشته مسدود می کند، اما در اینجا می توانیم قانون را دور بزنیم زیرا کامپایل در AudioWorkletGlobalScope انجام می شود که از رشته اصلی خارج می شود. (برای اطلاعات بیشتر این را ببینید.)

الگوی نمونه سازی ماژول WASM B: با استفاده از سازنده AudioWorkletNode     انتقال بین رشته ای
الگوی نمونه سازی ماژول WASM B: استفاده از انتقال بین رشته ای سازنده AudioWorkletNode

الگوی B می تواند در صورت نیاز به بلند کردن ناهمزمان سنگین مفید باشد. از رشته اصلی برای واکشی کد چسب از سرور و کامپایل ماژول استفاده می کند. سپس ماژول WASM را از طریق سازنده AudioWorkletNode منتقل می کند. این الگو زمانی منطقی‌تر می‌شود که بعد از اینکه AudioWorkletGlobalScope شروع به رندر کردن جریان صوتی کرد، ماژول را به صورت پویا بارگیری کنید. بسته به اندازه ماژول، کامپایل کردن آن در وسط رندر می تواند باعث ایجاد اشکال در جریان شود.

WASM Heap و داده های صوتی

کد WebAssembly فقط روی حافظه اختصاص داده شده در یک پشته اختصاصی WASM کار می کند. برای استفاده از آن، داده‌های صوتی باید بین پشته WASM و آرایه‌های داده صوتی شبیه‌سازی شوند. کلاس HeapAudioBuffer در کد مثال به خوبی این عملیات را انجام می دهد.

کلاس HeapAudioBuffer برای استفاده راحت تر از پشته WASM
کلاس HeapAudioBuffer برای استفاده راحت تر از پشته WASM

یک پیشنهاد اولیه برای ادغام پشته WASM به طور مستقیم در سیستم Worklet صوتی وجود دارد. خلاص شدن از شر این شبیه سازی اطلاعات اضافی بین حافظه JS و پشته WASM طبیعی به نظر می رسد، اما جزئیات خاص باید بررسی شوند.

مدیریت عدم تطابق اندازه بافر

یک جفت AudioWorkletNode و AudioWorkletProcessor طوری طراحی شده است که مانند یک AudioNode معمولی کار کند. AudioWorkletNode تعامل با کدهای دیگر را کنترل می کند در حالی که AudioWorkletProcessor از پردازش صوتی داخلی مراقبت می کند. از آنجا که یک AudioNode معمولی 128 فریم را در یک زمان پردازش می کند، AudioWorkletProcessor باید همین کار را انجام دهد تا به یک ویژگی اصلی تبدیل شود. این یکی از مزایای طراحی Audio Worklet است که تضمین می‌کند هیچ تأخیر اضافی به دلیل بافر داخلی در AudioWorkletProcessor معرفی نمی‌شود، اما اگر یک تابع پردازشی به اندازه بافری متفاوت از 128 فریم نیاز داشته باشد، می‌تواند مشکل ساز شود. راه حل رایج برای چنین مواردی استفاده از بافر حلقه ای است که به عنوان بافر دایره ای یا FIFO نیز شناخته می شود.

در اینجا نمودار AudioWorkletProcessor با استفاده از دو بافر حلقه در داخل برای قرار دادن یک تابع WASM است که 512 فریم را به داخل و خارج می کند. (عدد 512 در اینجا خودسرانه انتخاب شده است.)

استفاده از RingBuffer در روش «process()» AudioWorkletProcessor
استفاده از RingBuffer در روش «process()» AudioWorkletProcessor

الگوریتم نمودار به صورت زیر خواهد بود:

  1. AudioWorkletProcessor 128 فریم را از ورودی خود به داخل RingBuffer ورودی فشار می دهد.
  2. فقط در صورتی مراحل زیر را انجام دهید که RingBuffer ورودی بزرگتر یا مساوی 512 فریم باشد.
    1. 512 فریم را از RingBuffer ورودی بکشید.
    2. 512 فریم را با تابع WASM داده شده پردازش کنید.
    3. 512 فریم را به خروجی RingBuffer فشار دهید.
  3. AudioWorkletProcessor 128 فریم را از RingBuffer خروجی می کشد تا خروجی آن را پر کند.

همانطور که در نمودار نشان داده شده است، فریم های ورودی همیشه در ورودی RingBuffer جمع می شوند و با بازنویسی قدیمی ترین بلوک فریم در بافر، سرریز بافر را کنترل می کند. این یک کار معقول برای یک برنامه صوتی بلادرنگ است. به طور مشابه، بلوک فریم خروجی همیشه توسط سیستم کشیده می شود. جریان جریان بافر (داده کافی نیست) در خروجی RingBuffer باعث خاموشی و ایجاد یک اشکال در جریان می شود.

این الگو هنگام جایگزینی ScriptProcessorNode (SPN) با AudioWorkletNode مفید است. از آنجایی که SPN به توسعه‌دهنده اجازه می‌دهد تا اندازه بافری بین 256 تا 16384 فریم انتخاب کند، بنابراین جایگزینی SPN با AudioWorkletNode می‌تواند دشوار باشد و استفاده از بافر حلقه راه‌حل خوبی را فراهم می‌کند. یک ضبط کننده صدا نمونه بسیار خوبی است که می تواند در بالای این طرح ساخته شود.

با این حال، درک این نکته مهم است که این طرح فقط عدم تطابق اندازه بافر را تطبیق می دهد و زمان بیشتری برای اجرای کد اسکریپت داده شده نمی دهد. اگر کد نتواند کار را در بودجه زمانی رندر کوانتومی (~ 3 میلی‌ثانیه در 44.1 کیلوهرتز) به پایان برساند، بر زمان‌بندی شروع عملکرد برگشت تماس بعدی تأثیر می‌گذارد و در نهایت باعث ایجاد اشکال می‌شود.

اختلاط این طرح با WebAssembly به دلیل مدیریت حافظه در اطراف پشته WASM می تواند پیچیده باشد. در زمان نوشتن، داده‌هایی که وارد و خارج از هیپ WASM می‌شوند باید شبیه‌سازی شوند، اما می‌توانیم از کلاس HeapAudioBuffer برای تسهیل مدیریت حافظه استفاده کنیم. ایده استفاده از حافظه اختصاص داده شده توسط کاربر برای کاهش شبیه سازی داده های اضافی در آینده مورد بحث قرار خواهد گرفت.

کلاس RingBuffer را می توانید در اینجا پیدا کنید.

WebAudio Powerhouse: Audio Worklet و SharedArrayBuffer

آخرین الگوی طراحی در این مقاله قرار دادن چندین API پیشرفته در یک مکان است. Audio Worklet، SharedArrayBuffer ، Atomics و Worker . با این تنظیمات غیر پیش پا افتاده، مسیری را برای نرم افزارهای صوتی موجود نوشته شده با C/C++ باز می کند تا در مرورگر وب اجرا شود و در عین حال تجربه کاربری روانی را حفظ کند.

مروری بر آخرین الگوی طراحی: Audio Worklet، SharedArrayBuffer و Worker
مروری بر آخرین الگوی طراحی: Audio Worklet، SharedArrayBuffer و Worker

بزرگترین مزیت این طراحی استفاده از DedicatedWorkerGlobalScope تنها برای پردازش صدا است. در Chrome، WorkerGlobalScope روی رشته‌ای با اولویت پایین‌تر از رشته رندر WebAudio اجرا می‌شود، اما چندین مزیت نسبت به AudioWorkletGlobalScope دارد. DedicatedWorkerGlobalScope از نظر سطح API موجود در محدوده کمتر محدود است. همچنین می توانید انتظار پشتیبانی بهتری از Emscripten داشته باشید زیرا Worker API چند سالی است که وجود دارد.

SharedArrayBuffer نقش مهمی برای این طراحی ایفا می کند تا کارآمد باشد. اگرچه Worker و AudioWorkletProcessor هر دو به پیام‌رسانی ناهمزمان ( MessagePort ) مجهز هستند، به دلیل تخصیص مکرر حافظه و تأخیر پیام‌رسانی، برای پردازش صدا در زمان واقعی بسیار مناسب نیست. بنابراین ما یک بلوک حافظه را در جلو اختصاص می دهیم که می تواند از هر دو رشته برای انتقال سریع داده های دو طرفه قابل دسترسی باشد.

از دیدگاه ناظر Web Audio API، این طراحی ممکن است بهینه به نظر نرسد زیرا از Audio Worklet به عنوان یک "سینک صوتی" ساده استفاده می کند و همه کارها را در Worker انجام می دهد. اما با توجه به هزینه بازنویسی پروژه های C/C++ در جاوا اسکریپت می تواند بسیار زیاد و یا حتی غیرممکن باشد، این طراحی می تواند کارآمدترین مسیر پیاده سازی برای چنین پروژه هایی باشد.

ایالات مشترک و اتمی

هنگام استفاده از حافظه مشترک برای داده های صوتی، دسترسی از هر دو طرف باید به دقت هماهنگ شود. به اشتراک گذاشتن حالت های قابل دسترسی اتمی راه حلی برای چنین مشکلی است. ما می توانیم از Int32Array که توسط SAB پشتیبانی می شود برای این منظور استفاده کنیم.

مکانیسم همگام سازی: SharedArrayBuffer و Atomics
مکانیسم همگام سازی: SharedArrayBuffer و Atomics

مکانیسم همگام سازی: SharedArrayBuffer و Atomics

هر فیلد از آرایه ایالات نشان دهنده اطلاعات حیاتی در مورد بافرهای مشترک است. مهمترین آنها یک فیلد برای همگام سازی است ( REQUEST_RENDER ). ایده این است که Worker منتظر می ماند تا این فیلد توسط AudioWorkletProcessor لمس شود و هنگامی که از خواب بیدار شد، صدا را پردازش کند. همراه با SharedArrayBuffer (SAB)، Atomics API این مکانیسم را ممکن می سازد.

توجه داشته باشید که همگام سازی دو رشته نسبتا شل است. شروع Worker.process() با روش AudioWorkletProcessor.process() راه اندازی می شود، اما AudioWorkletProcessor منتظر نمی ماند تا Worker.process() تمام شود. این بر اساس طراحی است. AudioWorkletProcessor توسط پاسخ تماس صوتی هدایت می شود، بنابراین نباید به طور همزمان مسدود شود. در بدترین حالت ممکن است جریان صوتی تکراری یا حذف شود، اما در نهایت وقتی عملکرد رندر تثبیت شود، بازیابی می‌شود.

راه اندازی و اجرا

همانطور که در نمودار بالا نشان داده شده است، این طرح دارای چندین مؤلفه برای ترتیب است: DedicatedWorkerGlobalScope (DWGS)، AudioWorkletGlobalScope (AWGS)، SharedArrayBuffer و رشته اصلی. مراحل زیر آنچه را که باید در مرحله اولیه سازی اتفاق بیفتد توضیح می دهد.

مقداردهی اولیه
  1. [اصلی] سازنده AudioWorkletNode فراخوانی می شود.
    1. کارگر ایجاد کنید.
    2. AudioWorkletProcessor مرتبط ایجاد خواهد شد.
  2. [DWGS] Worker 2 SharedArrayBuffer ایجاد می کند. (یکی برای حالت های مشترک و دیگری برای داده های صوتی)
  3. [DWGS] Worker ارجاعات SharedArrayBuffer را به AudioWorkletNode می فرستد.
  4. [اصلی] AudioWorkletNode ارجاعات SharedArrayBuffer را به AudioWorkletProcessor ارسال می کند.
  5. [AWGS] AudioWorkletProcessor به AudioWorkletNode اطلاع می دهد که راه اندازی کامل شده است.

پس از تکمیل اولیه، AudioWorkletProcessor.process() شروع به فراخوانی می کند. آنچه در زیر باید در هر تکرار از حلقه رندر اتفاق بیفتد، آمده است.

حلقه رندرینگ
رندر چند رشته ای با SharedArrayBuffers
رندر چند رشته ای با SharedArrayBuffers
  1. [AWGS] AudioWorkletProcessor.process(inputs, outputs) برای هر کوانتوم رندر فراخوانی می شود.
    1. inputs به ورودی SAB فشار داده می شوند.
    2. outputs با مصرف داده های صوتی در خروجی SAB پر می شوند.
    3. SAB ایالات را با شاخص های بافر جدید به روز می کند.
    4. اگر خروجی SAB به آستانه underflow نزدیک شود، Wake Worker داده های صوتی بیشتری را ارائه می دهد.
  2. [DWGS] Worker منتظر سیگنال بیداری از AudioWorkletProcessor.process() می‌ماند (می‌خوابد). وقتی بیدار شد:
    1. شاخص های بافر را از State SAB واکشی می کند.
    2. تابع فرآیند را با داده های ورودی SAB اجرا کنید تا خروجی SAB پر شود.
    3. SAB ایالات را با شاخص های بافر بر این اساس به روز می کند.
    4. به خواب می رود و منتظر سیگنال بعدی است.

کد مثال را می‌توانید در اینجا پیدا کنید، اما توجه داشته باشید که پرچم آزمایشی SharedArrayBuffer باید فعال باشد تا این نسخه نمایشی کار کند. کد با کد JS خالص برای سادگی نوشته شده است، اما در صورت نیاز می توان آن را با کد WebAssembly جایگزین کرد. چنین موردی باید با احتیاط بیشتری با قرار دادن مدیریت حافظه با کلاس HeapAudioBuffer رسیدگی شود.

نتیجه گیری

هدف نهایی Audio Worklet این است که Web Audio API را واقعاً "بسط پذیر" کند. تلاش چند ساله ای برای طراحی آن انجام شد تا امکان پیاده سازی بقیه Web Audio API با Audio Worklet فراهم شود. به نوبه خود، اکنون ما پیچیدگی بالاتری در طراحی آن داریم و این می تواند یک چالش غیرمنتظره باشد.

خوشبختانه، دلیل چنین پیچیدگی صرفاً توانمندسازی توسعه دهندگان است. امکان اجرای WebAssembly در AudioWorkletGlobalScope پتانسیل عظیمی را برای پردازش صوتی با کارایی بالا در وب باز می کند. برای برنامه های صوتی در مقیاس بزرگ که به زبان C یا C++ نوشته شده اند، استفاده از Audio Worklet با SharedArrayBuffers و Workers می تواند گزینه جذابی برای کاوش باشد.

اعتبارات

تشکر ویژه از کریس ویلسون، جیسون میلر، جاشوا بل و ریموند اسباب بازی برای بررسی پیش نویس این مقاله و ارائه بازخورد روشنگرانه.

،

Hongchan Choi

مقاله قبلی در مورد Audio Worklet مفاهیم اساسی و کاربرد آن را شرح داد. از زمان راه‌اندازی آن در Chrome 66، درخواست‌های زیادی برای نمونه‌های بیشتری از نحوه استفاده از آن در برنامه‌های کاربردی واقعی وجود داشته است. کار صوتی پتانسیل کامل WebAudio را باز می کند ، اما استفاده از آن می تواند چالش برانگیز باشد زیرا نیاز به درک برنامه نویسی همزمان پیچیده شده با چندین API JS دارد. حتی برای توسعه دهندگان که با WebAudio آشنا هستند ، ادغام کار صوتی با سایر API ها (به عنوان مثال WebAssembly) می تواند دشوار باشد.

در این مقاله درک بهتری از نحوه استفاده از کار صوتی در تنظیمات دنیای واقعی و ارائه نکاتی برای ترسیم کامل آن ارائه می شود. حتماً مثالهای کد و نسخه های نمایشی را نیز ببینید!

recap: کار صوتی

قبل از غواصی ، بیایید به سرعت شرایط و حقایق پیرامون سیستم کار صوتی را که قبلاً در این پست معرفی شده بود ، بازگو کنیم.

  • BaseaudioContext : شیء اصلی API AUDIO AUDIO.
  • کار صوتی : یک لودر فایل اسکریپت ویژه برای عملکرد کار صوتی. متعلق به baseaudiocontext است. یک baseaudiocontext می تواند یک کار صوتی داشته باشد. پرونده اسکریپت بارگذاری شده در AudiOworkletGlobalScope ارزیابی می شود و برای ایجاد نمونه های AudiOworkletProcessor استفاده می شود.
  • Audioworkletglobalscope : دامنه ویژه جهانی JS برای عملکرد کار صوتی. در یک موضوع ارائه دهنده اختصاصی برای WebAudio اجرا می شود. یک baseaudiocontext می تواند یک AudiOworkletGlobalScope داشته باشد.
  • AudiOworkletNode : یک Audionode که برای عملکرد کار صوتی طراحی شده است. از یک baseaudiocontext فوری. یک baseaudiocontext می تواند چندین AudiOworkletNodes را به طور مشابه با Audionodes بومی داشته باشد.
  • AudiOworkletProcessor : همتای AudiOworkletNode. روده واقعی AudiOworkletNode در حال پردازش جریان صوتی توسط کد تهیه شده توسط کاربر است. هنگامی که یک AudiOworkletNode ساخته می شود ، در AudiOworkletGlobalScope فوری می شود. یک AudiOworkletNode می تواند یک AudiOworkletProcessor مطابقت داشته باشد.

الگوهای طراحی

استفاده از کار صوتی با WebAssembly

WebAssembly یک همراه مناسب برای AudiOworkletProcessor است. ترکیبی از این دو ویژگی ، مزایای متنوعی را برای پردازش صوتی در وب به ارمغان می آورد ، اما دو مزیت بزرگ عبارتند از: الف) ورود کد پردازش صوتی C/C ++ موجود در اکوسیستم Webaudio و ب) جلوگیری از سربار جمع آوری JS JIT و جمع آوری زباله ها در کد پردازش صوتی.

اولی برای توسعه دهندگان با سرمایه گذاری موجود در کد پردازش صوتی و کتابخانه ها مهم است ، اما دومی برای تقریباً همه کاربران API بسیار مهم است. در دنیای Webaudio ، بودجه زمان بندی برای جریان صوتی پایدار کاملاً خواستار است: فقط 3MS با نرخ نمونه 44.1kHz است. حتی یک سکسکه جزئی در کد پردازش صوتی می تواند باعث ایجاد اشکالات شود. توسعه دهنده باید کد را برای پردازش سریعتر بهینه کند ، اما همچنین میزان زباله JS را نیز به حداقل می رساند. استفاده از WebAssembly می تواند راه حلی باشد که به طور همزمان هر دو مشکل را برطرف کند: سریعتر است و هیچ زباله ای از کد ایجاد نمی کند.

در بخش بعدی توضیح می دهد که چگونه می توان از WebAssembly با یک کار صوتی استفاده کرد و مثال کد همراه را می توان در اینجا یافت. برای آموزش اساسی در مورد نحوه استفاده از EMScriptten و WebAssembly (به ویژه کد چسب EMScriptten) ، لطفاً به این مقاله نگاهی بیندازید.

راه اندازی

همه چیز عالی به نظر می رسد ، اما ما به کمی ساختار نیاز داریم تا کارها را به درستی تنظیم کنیم. اولین سؤال طراحی که باید بپرسید این است که چگونه و از کجا می توان یک ماژول WebAnsembly را فوری کرد. پس از واکشی کد چسب Emscripten ، دو مسیر برای فوری ماژول وجود دارد:

  1. یک ماژول WebAssembly را با بارگذاری کد چسب در AudiOworkletGlobalScope از طریق audioContext.audioWorklet.addModule() فوری کنید.
  2. یک ماژول WebAssembly را در محدوده اصلی فوری کنید ، سپس ماژول را از طریق گزینه های سازنده AudiOworkletNode منتقل کنید.

این تصمیم تا حد زیادی به طراحی و اولویت شما بستگی دارد ، اما ایده این است که ماژول WebAnsembly می تواند یک نمونه WebAssembly را در Audioworkletglobalscope ایجاد کند ، که به یک هسته پردازش صوتی در یک نمونه AudiOworkletProcessor تبدیل می شود.

الگوی فوری ماژول WebAssembly A: با استفاده از. addmodule () تماس
الگوی فوری ماژول WebAssembly A: با استفاده از .addModule() تماس

برای اینکه الگوی A به درستی کار کند ، Emscripten برای تولید صحیح کد چسب WebAnsembly برای پیکربندی ما به چند گزینه نیاز دارد:

-s BINARYEN_ASYNC_COMPILATION=0 -s SINGLE_FILE=1 --post-js mycode.js

این گزینه ها از تدوین همزمان یک ماژول WebAssembly در AudiOworkletGlobalScope اطمینان می دهند. همچنین تعریف کلاس AudiOworkletProcessor را در mycode.js ضمیمه می کند تا پس از شروع ماژول بارگیری شود. دلیل اصلی استفاده از تدوین همزمان این است که وضوح وعده audioWorklet.addModule() منتظر وضوح وعده ها در AudiOworkletGlobalScope نیست. بارگذاری یا تدوین همزمان در موضوع اصلی به طور کلی توصیه نمی شود زیرا سایر کارها را در همان موضوع مسدود می کند ، اما در اینجا می توانیم این قانون را دور بزنیم زیرا تدوین در Audioworkletglobalscope اتفاق می افتد ، که از موضوع اصلی خارج می شود. (برای اطلاعات بیشتر به این موضوع مراجعه کنید.)

الگوی فوری ماژول WASM B: با استفاده از سازنده AudiOworkletNode     انتقال صریح
الگوی فوری ماژول WASM B: با استفاده از انتقال موضوعی AudiOworkletNode

در صورت نیاز به بلند کردن سنگین ناهمزمان ، الگوی B می تواند مفید باشد. این موضوع از موضوع اصلی برای واکشی کد چسب از سرور و تهیه ماژول استفاده می کند. سپس ماژول WASM را از طریق سازنده AudiOworkletNode منتقل می کند. این الگوی هنگامی که مجبور شوید ماژول را به صورت پویا بارگیری کنید ، بعد از شروع AudiOworkletGlobalScope شروع به ارائه جریان صوتی می کند. بسته به اندازه ماژول ، تهیه آن در وسط رندر می تواند باعث ایجاد اشکالات در جریان شود.

داده های پشته و صوتی

کد WebAssembly فقط روی حافظه اختصاص داده شده در یک پشته HASM اختصاصی کار می کند. برای استفاده از آن ، داده های صوتی باید بین HASM Heap و آرایه های داده صوتی به عقب و جلو کلون شوند. کلاس HeapaudioBuffer در کد مثال این عملیات را به خوبی انجام می دهد.

کلاس HeapaudioBuffer برای استفاده آسان تر از Heap Heap
کلاس HeapaudioBuffer برای استفاده آسان تر از Heap Heap

یک پیشنهاد اولیه در حال بحث و گفتگو برای ادغام پشته WASM به طور مستقیم در سیستم کار صوتی است. خلاص شدن از شر این کلون کردن داده های اضافی بین حافظه JS و HASM Heap طبیعی به نظر می رسد ، اما جزئیات خاص باید به دست بیاید.

عدم تطابق اندازه بافر

یک جفت AudiOworkletNode و AudiOworkletProcessor به گونه ای طراحی شده است که مانند یک Audionode معمولی کار کند. AudiOworkletNode تعامل با کدهای دیگر را انجام می دهد در حالی که AudiOworkletProcessor از پردازش صوتی داخلی مراقبت می کند. از آنجا که یک Audionode منظم 128 فریم را به طور همزمان پردازش می کند ، AudiOworkletProcessor باید همین کار را انجام دهد تا به یک ویژگی اصلی تبدیل شود. این یکی از مزایای طراحی کار صوتی است که تضمین نمی کند هیچ تأخیر اضافی به دلیل بافر داخلی در AudiOworkletProcessor معرفی شود ، اما اگر یک عملکرد پردازش به اندازه بافر متفاوت از 128 فریم نیاز داشته باشد ، می تواند یک مشکل باشد. راه حل مشترک برای چنین موردی استفاده از بافر حلقه است که به عنوان یک بافر دایره ای یا FIFO نیز شناخته می شود.

در اینجا نمودار AudiOworkletProcessor با استفاده از دو بافر حلقه ای در داخل برای قرار دادن یک عملکرد WASM که 512 فریم در داخل و خارج می گیرد ، آورده شده است. (شماره 512 در اینجا به طور خودسرانه انتخاب می شود.)

استفاده از Ringbuffer در داخل روش AudiOworkletProcessor () روش ()
استفاده از Ringbuffer در داخل روش AudiOworkletProcessor () روش ()

الگوریتم نمودار:

  1. AudiOworkletProcessor 128 فریم را از ورودی خود به داخل حلقه ورودی فشار می دهد.
  2. مراحل زیر را فقط در صورتی انجام دهید که حلقه ورودی بیشتر از یا مساوی 512 فریم باشد.
    1. 512 فریم را از Ringbuffer ورودی بکشید.
    2. فرآیند 512 فریم با عملکرد WASM داده شده.
    3. 512 فریم را به حلقه خروجی فشار دهید.
  3. AudiOworkletProcessor 128 فریم را از حلقه خروجی بیرون می کشد تا خروجی آن را پر کند.

همانطور که در نمودار نشان داده شده است ، فریم های ورودی همیشه در Ringbuffer ورودی جمع می شوند و با بازنویسی قدیمی ترین بلوک فریم در بافر ، سرریز بافر را کنترل می کند. این یک کار منطقی است که باید برای یک برنامه صوتی در زمان واقعی انجام دهید. به طور مشابه ، بلوک فریم خروجی همیشه توسط سیستم کشیده می شود. زیر جریان بافر (داده کافی) در خروجی Ringbuffer باعث سکوت می شود و باعث ایجاد یک درخشش در جریان می شود.

این الگوی هنگام تعویض ScriptProcessornode (SPN) با AudiOworkletNode مفید است. از آنجا که SPN به توسعه دهنده اجازه می دهد تا اندازه بافر را بین 256 تا 16384 فریم انتخاب کند ، بنابراین تعویض قطره SPN با AudiOworkletNode می تواند دشوار باشد و استفاده از بافر حلقه راه حل خوبی را فراهم می کند. ضبط کننده صوتی نمونه ای عالی خواهد بود که می تواند در بالای این طرح ساخته شود.

با این حال ، درک این نکته حائز اهمیت است که این طرح فقط عدم تطابق اندازه بافر را آشتی می دهد و زمان بیشتری برای اجرای کد اسکریپت داده شده نمی دهد. اگر کد نتواند کار را در بودجه زمان بندی رندر کوانتومی (3 میلیون پوند در 44.1 کیلو هرتز) به پایان برساند ، بر زمان شروع عملکرد پاسخ به تماس بعدی تأثیر می گذارد و در نهایت باعث ایجاد اشکالات می شود.

مخلوط کردن این طرح با WebAssembly می تواند به دلیل مدیریت حافظه در اطراف HAP HEAP پیچیده باشد. در زمان نوشتن ، داده های وارد شده و خارج از WASM باید کلون شوند اما می توانیم از کلاس HeapaudioBuffer استفاده کنیم تا مدیریت حافظه کمی آسانتر شود. ایده استفاده از حافظه اختصاص داده شده کاربر برای کاهش کلون سازی داده های اضافی در آینده مورد بحث قرار خواهد گرفت.

کلاس Ringbuffer را می توان در اینجا یافت.

WebAudio Powerhouse: Worklet Audio و SharedArrayBuffer

آخرین الگوی طراحی در این مقاله ، قرار دادن چندین API های لبه برش در یک مکان است. کار صوتی ، مشترک ArarrayBuffer ، Atomics و Worker . با استفاده از این تنظیم غیر مهم ، مسیری را برای نرم افزار صوتی موجود که در C/C ++ نوشته شده است ، باز می کند تا ضمن حفظ یک تجربه کاربر صاف ، در یک مرورگر وب اجرا شود.

مروری بر آخرین الگوی طراحی: کار صوتی ، اشتراک ArarrayBuffer و کارگر
مروری بر آخرین الگوی طراحی: کار صوتی ، اشتراک ArarrayBuffer و کارگر

بزرگترین مزیت این طرح این است که قادر به استفاده از یک WorkworkerGlobalScope صرفاً برای پردازش صوتی است. در Chrome ، WorkloglobalScope با موضوع اولویت پایین تر از موضوع ارائه WebAudio اجرا می شود اما مزایای آن نسبت به AudiOworkletGlobalScope چندین مزیت دارد. از نظر سطح API موجود در دامنه ، اختصاصی WorklerGlobalScope کمتر محدود است. همچنین می توانید از Emscripten انتظار بیشتری داشته باشید زیرا API کارگر چند سال است که وجود داشته است.

SharedArrayBuffer نقش مهمی در این طرح دارد تا بتواند به طور مؤثر کار کند. اگرچه هر دو کارگر و AudiOworkletProcessor مجهز به پیام رسانی ناهمزمان ( MessagePort ) هستند ، اما به دلیل تخصیص حافظه تکراری و تأخیر پیام رسانی ، برای پردازش صوتی در زمان واقعی بسیار پایین است. بنابراین ما یک بلوک حافظه را از جلو اختصاص می دهیم که برای انتقال سریع داده های دو طرفه از هر دو موضوع قابل دسترسی است.

از دیدگاه وب API Purist ، این طرح ممکن است زیر حد متوسط ​​به نظر برسد زیرا از کار صوتی به عنوان یک "سینک صوتی" ساده استفاده می کند و همه چیز را در کارگر انجام می دهد. اما با توجه به هزینه بازنویسی پروژه های C/C ++ در JavaScript می تواند ممنوع باشد یا حتی غیرممکن باشد ، این طرح می تواند کارآمدترین مسیر اجرای چنین پروژه هایی باشد.

کشورهای مشترک و اتمیک

هنگام استفاده از یک حافظه مشترک برای داده های صوتی ، دسترسی از هر دو طرف باید با دقت هماهنگ شود. به اشتراک گذاشتن حالتهای در دسترس اتمی ، راه حلی برای چنین مشکلی است. ما می توانیم از Int32Array که توسط یک SAB برای این منظور پشتیبانی می شود ، استفاده کنیم.

مکانیسم هماهنگ سازی: ArarrayBuffer و اتمیک
مکانیسم هماهنگ سازی: ArarrayBuffer و اتمیک

مکانیسم هماهنگ سازی: ArarrayBuffer و اتمیک

هر زمینه از آرایه ایالات متحده اطلاعات حیاتی در مورد بافرهای مشترک را نشان می دهد. مهمترین مورد ، زمینه ای برای همگام سازی ( REQUEST_RENDER ) است. ایده این است که کارگر منتظر است تا این زمینه توسط AudiOworkletProcessor لمس شود و هنگام بیدار شدن صدا را پردازش کند. API ATOMICS همراه با SharedArrayBuffer (SAB) ، این مکانیسم را ممکن می سازد.

توجه داشته باشید که هماهنگ سازی دو موضوع بسیار سست است. شروع Worker.process() با روش AudioWorkletProcessor.process() ایجاد می شود ، اما AudiOworkletProcessor منتظر نمی ماند تا Worker.process() تمام شود. این با طراحی است ؛ AudiOworkletProcessor توسط پاسخ به تماس صوتی هدایت می شود ، بنابراین نباید همزمان مسدود شود. در بدترین حالت ، جریان صوتی ممکن است از کپی یا رها شدن رنج ببرد اما با تثبیت عملکرد ارائه ، در نهایت بهبود می یابد.

راه اندازی و اجرا

همانطور که در نمودار بالا نشان داده شده است ، این طرح دارای چندین مؤلفه برای ترتیب: اختصاصی WorklerGlobalScope (DWGS) ، AudiOworkletGlobalScope (AWGS) ، مشترک AarrayBuffer و موضوع اصلی است. مراحل زیر توضیح می دهد که چه اتفاقی باید در مرحله اولیه سازی رخ دهد.

مقداردهی اولیه
  1. [اصلی] سازنده AudiOworkletNode فراخوانی می شود.
    1. کارگر ایجاد کنید.
    2. AudiOworkletProcessor همراه ایجاد می شود.
  2. [DWGS] کارگر 2 مشترک ArarrayBuffer را ایجاد می کند. (یکی برای حالتهای مشترک و دیگری برای داده های صوتی)
  3. [DWGS] کارگر منابع مشترک ArarrayBuffer را به AudiOworkletNode ارسال می کند.
  4. [اصلی] AudiOworkletNode منابع مشترک ArarrayBuffer را به AudiOworkletProcessor ارسال می کند.
  5. [AWGS] AudiOworkletProcessor به AudiOworkletNode اطلاع می دهد که تنظیمات تکمیل شده است.

پس از اتمام اولیه سازی ، AudioWorkletProcessor.process() شروع به فراخوانی می کند. آنچه در زیر باید در هر تکرار از حلقه رندر اتفاق بیفتد.

حلقه رندرینگ
رندر چند رشته ای با اشتراک های مشترک
رندر چند رشته ای با اشتراک های مشترک
  1. [AWGS] AudioWorkletProcessor.process(inputs, outputs) برای هر کوانتومی ارائه می شود.
    1. inputs به SAB ورودی منتقل می شوند.
    2. outputs با مصرف داده های صوتی در خروجی SAB پر می شوند.
    3. به روزرسانی ها SAB را با شاخص های بافر جدید بر این اساس بیان می کنند.
    4. اگر خروجی SAB به آستانه Underflow نزدیک شود ، Wake Worker برای ارائه داده های صوتی بیشتر.
  2. [DWGS] کارگر منتظر (می خوابد) برای سیگنال بیدار از AudioWorkletProcessor.process() . وقتی بیدار شد:
    1. شاخص های بافر از ایالت های SAB را واگذار می کند.
    2. عملکرد فرآیند را با داده های ورودی SAB برای پر کردن خروجی SAB اجرا کنید.
    3. به روزرسانی ها SAB را با شاخص های بافر بر این اساس بیان می کنند.
    4. به خواب می رود و منتظر سیگنال بعدی است.

کد مثال را می توان در اینجا یافت ، اما توجه داشته باشید که پرچم تجربی SharedArrayBuffer برای کار این نسخه ی نمایشی باید فعال شود. این کد با کد JS خالص برای سادگی نوشته شده است ، اما در صورت لزوم می توان آن را با کد WebAssembly جایگزین کرد. چنین موردی باید با بسته بندی مدیریت حافظه با کلاس HeapaudioBuffer با مراقبت های اضافی انجام شود.

نتیجه گیری

هدف نهایی کار صوتی این است که API صوتی وب را واقعاً "گسترده" کند. یک تلاش چند ساله در طراحی خود انجام شد تا امکان پیاده سازی بقیه API صوتی وب با کار صوتی را فراهم کند. به نوبه خود ، اکنون ما در طراحی آن پیچیدگی بالاتری داریم و این می تواند یک چالش غیر منتظره باشد.

خوشبختانه ، دلیل چنین پیچیدگی ها صرفاً توانمندسازی توسعه دهندگان است. قادر به اجرای WebAssembly در AudiOworkletGlobalScope پتانسیل عظیمی را برای پردازش صوتی با کارایی بالا در وب باز می کند. برای برنامه های صوتی در مقیاس بزرگ که در C یا C ++ نوشته شده است ، با استفاده از یک کار صوتی با ArarrayBuffers و کارگران می تواند گزینه ای جذاب برای کشف باشد.

اعتبارات

با تشکر ویژه از کریس ویلسون ، جیسون میلر ، جوشوا بل و اسباب بازی ریموند برای بررسی پیش نویس این مقاله و بازخورد روشنگری.