1. JS: Audio Data Processing
Ingvar Stepanyan
Web Developer, MSP
@RReverser (http://rreverser.com/)
2. Native implementations:
[Mozilla] Audio Data API (low-level processing)
[WebKit] Web Audio API (+ some high-level sugar)
[Flash] Sound API (low-level processing)
Unifying wrappers:
dynamicaudio.js (Audio Data API + Flash)
XAudioJS (Audio Data API + Web Audio API + Flash)
sink.js & co. (Audio Data API + Web Audio API)
Current status
Low
Level
4. audio.addEventListener('MozAudioAvailable', function () {
var buffer = event.frameBuffer,
time = event.time;
for (var i = 0; i < bufferLength / channels; i++ ) {
// use buffer[i * channels + channelNumber]
}
});
Reading audio data
Доброго дня.Мене звати Ігор Степанян.Сьогодні я хотів би коротко розповісти про основи обробки звуку за допомогою JavaScript (наперед прошу побачення за невеликі математичні викладки, без них тут просто не обійтись).
На даний момент у нас є три реалізації для роботи з аудіоданими на клієнтській частині:це дві, абсолютно різні реалізації Audio API у Firefox та Chromeну і, звісно ж, Flash (підтримка Sound API у ньому наявна в 10-й версії та вищих).При цьому Firefox та Flash дають можливості для низькорівневої обробки аудіоданих, в той час як Chrome, окрім них, надає API і для різноманітних стандартних високорівневих операцій над звуком.Звісно ж, цих всіх розбіжностей у реалізації не могла помітити JavaScript-спільнота, і тому невдовзі з’явились бібліотеки, що об’єднують доступ до тих чи інших операцій через єдине API – dynamicaudio.js, що дозволяє виводити звук за допомогою Mozilla API та Flash-fallback для інших браузерів, XAudioJS, що аналогічним чином об’єднує уже всі три реалізації, sink.js з декількома дочірніми бібліотеками, що надає можливості для синхронного та асинхронного виводу аудіоданих та інші.Але яку б бібліотеку ви не використовували, вам доведеться якимсь чином створювати чи оброблювати існуючі аудіодані перед їх виводом. На щастя, ця частина коду незалежна від конкретної реалізації Audio API.Для демонстрації я використовуватиму Audio Data API, представлене Mozilla. Воно було обране практично випадково – просто хотілось випробувати низькорівневу обробку даних в «чистих» умовах, не покладаючись на високорівневі плюшки, специфічні тільки для Chrome.
Для початку розглянемо, як відбувається читання даних з аудіопотоку і що вони собою представляють.Для цього в FF використовуються дві події.Перша – стандартна подія HTML5 – loadedmetadata, яка відбувається після розпізнавання заголовку з метаданими. Ми можем зберегти ці дані у змінні, провести ініціалізацію даних, бібліотек, всього що приймає за параметри якісь з наведених властивостей аудіофайлу.
Друга подія – уже більшспецифічна – MozAudioAvailable – вона повертає черговий уривок декодованих аудіоданих (вони можуть бути як уже програні, так і ні), а також час в секундах відносно початку програвання.
Коли я вперше завантажив таким способом музичний файл і отримав в буфері, здавалось би, випадковий набір чисел між -1 та 1, виникло досить закономірне питання – гм... скажімо, «а що це?» :)
Для тих хто досі ніколи не мав справи зі звуковими даними – ці числа називаються «семплами». Семпл являє собою значення хвилі у моменти часу, розділені періодом дискретизації (обернене до частоти дискретизації). У нашому випадку семпли даються уже нормалізованими значеннями відносно максимально можливої амплітуди хвилі.Таким чином, ми можемо оперувати самими цими значеннями, якщо хочемо змінювати загальну амплітуду – домножувати/ділити на сталий коефіцієнт, здійснити затухання чи, навпаки, плавне наростання за власним законом від часу тощо.
Якщо ж ми захочемо отримати частотну характеристику вхідного сигналу, наприклад, для візуалізації його спектру чи реалізації еквалайзеру, то доведеться використати перетворення Фур’є (для не-математиків – воно розкладає будь-яку функцію на суму синусоїд, що у нашому випадку якраз і дасть бажаний розклад по частотах). В принципі реалізацію цього алгоритму вам вручну писати не доведеться, вони є готові, в тому числі в бібліотеці DSP.js, посилання для якої наведене на екрані.
Окремий момент про частоту дискретизації. Для виводу звуку вам необхідно спершу задати частоту дискретизації, на якій буде здійснюватись вивід. Звісно, можна використовувати стандартні значення типу 44100 Hz, 48000 Hz тощо, але якщо ви наперед знаєте максимальну частоту звуків, що будуть виводитись, то є сенс скористатись теоремою Найквіста-Шеннона, яка стверджує, що для успішної інтерполяції хвилі з дискретизованих даних достатньо частоти дискретизації, що лише вдвічі більша за максимальну з частот хвиль, з яких складається дана (з цього ж закону і зрозуміло, звідки взявся стандарт в 44100 Hz). Це може дозволити відчутно скоротити час генерації аудіо, адже кожна секунда звуку представляється масивом з f_sampleелементів.На малюнку вище можна побачити приклад правильно (знизу) і неправильно (зверху) підібраних частот дискретизаціїта ефектів від цього.
Цієї теорії нам досить, перейдем до власне виводу аудіо.На перший погляд, тут все просто.Маємо функцію mozSetup, що задає налаштування для аудіопотоку – кількість каналів і частоту дискретизації, та є функція mozWriteAudio для запису масиву семплів. Відповідно, для програвання звуку хотілось би писати в такому вигляді: ...
Та проблема в тому, що дані для mozWriteAudio мають передаватись у порціях, що не перевищують внутрішній буфер аудіопотоку (його ми наперед визначити не можем). При цьому при передачі завеликого масиву семплів функція поверне кількість семплів, що були успішно записані. Відповідно усі інші доведеться зберегти і спробувати вивести повторно пізніше. Можна зберігати цей «хвіст» в окрему змінну і повісити обробник, що буде намагатись через деякий час знову або вивести цей хвіст, або, якщо він порожній – викликатиме наш callback для генерації нового набору семплів.В принципі на цьому можна було б зупинитись і уже використовувати цю функцію для виводу звуку. Та є ще один момент.
Та є ще один неприємний момент. Якщо проводити складні обчислення у тому ж потоці, що і сам UI, звук може починати тріщати, приторможувати тощо. Тому є сенс винести самі обчислення у Web Worker і викликати його за необхідності, а на callback від нього уже вішати наш writeAudio. Таким чином отримуєм наступну схему.