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

مقاله قبلی در مورد 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()

برای اینکه الگوی 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 می تواند گزینه جذابی برای کاوش باشد.

وام

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