TSM - HTML5: WebAudio API

Radu Olaru - Senior Software Developer


În acești ani aplicațiile online au împins tot mai departe limitele platformei web. Probabil cu toții am fost iritați la un moment dat de aplicațiile web. Cu toții am sperat că până la urmă lumea se va liniști, se va vedea clar că webul nu poate suporta aplicații serioase și Universul își va reveni la normal. Aplicațiile mari sunt pentru desktop, iar aplicațiile web sunt doar visele doctoranzilor entuziaști de la Google.

Dar apoi a apărut parcă de nicăieri HTML5. Și HTML5 a revoluționat nu doar web-ul aplicațiilor business, ci a deschis larg porțile web-ului aplicațiilor multimedia - în special a jocurilor.

Noutatea multimedia

HTML5 vine cu o mare surpriză pentru dezvoltatorii de aplicații multimedia. Deja știm pe de rost că există tagul audio și video. Nu vom vorbi despre ele în acest articol. O examinare superficială arată că tagul audio nu este suficient pentru aplicații serioase, fiind gândit să ascundă complexitatea redării sunetelor. Tagul asigură eventual un background muzical - excelent pentru nostalgicii site-urilor din anii "90. Pentru cei ce vor să dezvolte procesoare de sunet în timp real, HTML5 descrie o interfață specială: WebAudio.

WebAudio permite poziționarea sunetelor într-un spațiu tridimensional, mixarea surselor multiple de sunet redate simultan și rutarea lor printr-un sistem modular puternic de procesare. Efectele aplicabile includ un motor de calitate înaltă pentru reverberații și simulare completă a spațiului de audiție. De asemenea, datele audio pot fi analizate în timp real.

Aceste facilități au o țintă foarte specifică: jocurile și aplicațiile de procesare audio. Stațiile de procesare audio software au fost strict de domeniul desktopului. Niciodată nu s-a pus problema ca un artist să pornească browserul pentru a executa o performanță live. Nici nu s-a crezut că va avea vreodată sens ca o claviatură MIDI să comunice cu un browser web. Ei bine, totul s-a schimbat cu apariția WebAudio.

Infrastructură mai sofisticată

Pentru a ridica standardul multimedia în browsere atât de sus, WebAudio necesită o fundație specială. Pentru început, datele audio există într-un thread separat de restul aplicației - deci orice procesare este ferită de interferențele aplicației web. Datele audio, sursele, destinațiile sunetelor și nodurile de procesare există într-un context independent, cu un timp de sincronizare independent, asigurând păstrarea controlului asupra mai multor seturi de surse audio sincronizate.

În clipa în care se instanțiază un asemenea context, deja se pot specifica surse de sunet, destinații și se pot defini procesoare de sunet conectate în diferite configurații:

var context = new AudioContext();
var source = context.createBufferSource();
var volume = context.createGainNode();
 
source.buffer = buffer;
volume.gain.value = 0.6;
source.connect(volume);
volume.connect(context.destination);
source.start(0);

Arhitectura este de tip plugin: sunetul este încărcat într-un buffer (vom vedea mai târziu cum), buffer-ul este trimis la o sursă de date, apoi la un procesor de sunet, iar în final la o destinație. Destinația implicită a contextului audio este cea standard configurată în sistemul de operare - de obicei ieșirea "line out" a plăcii de sunet.

Dincolo de sintaxa intuitivă, procesarea sunetelor este efectuată direct de driverul audio. Dacă sistemul deține o placă de sunet profesională, driver-ul va executa instrucțiunile folosind hardware-ul dedicat al plăcii de sunet. Dacă este vorba doar de un "codec", rularea va regresa spre microprocesor. Indiferent de situație, arbitrul final este browser-ul. Modul în care browserul compilează, execută și decide rularea instrucțiunilor determină soarta datelor audio. O implementare bună însă va tinde să folosească resurse dedicate și să evite blocajele din fluxul de procesare și redare a datelor audio.

Noduri și grafuri orientate

Inițierea unui flux audio se face prin definirea unei surse de sunet. Aceasta poate fi o intrare live a plăcii de sunet (un microfon), un fișier audio existent sau un sintetizator de sunet (un oscilator). Desigur, în graf pot exista mai multe surse de sunet - fie generate, fie fișiere, fie live. Toate sursele de sunet pot fi redate simultan sau la cerere (la apăsarea unei taste de exemplu sau la primirea unui mesaj MIDI).

Mai departe sunetul poate fi trimis spre diferite noduri de procesare. Procesoarele audio includ localizare în spațiu, efecte specifice sunetelor aflate la distanță, efecte doppler, filtre, reverberații, ecouri, altele. Orice nod suportă mai multe intrări și mai multe ieșiri, permițând legături în serie și în paralel.

Există inclusiv un context audio offline, deconectat de o sursă hardware de redare a sunetelor, ce poate fi folosit pentru generarea datelor audio independent de timp. Contextul "redă" sunetele ca pachet de date, accesibil într-o funcție callback:

callback OfflineRenderSuccessCallback = void (AudioBuffer renderedData)
 
interface OfflineAudioContext : AudioContext {
   void startRendering();
   attribute OfflineRenderSuccessCallback onComplete;
}

Surse audio și conexiuni în graf

Graful de procesare audio primește informații de la surse audio. Datele care trec prin graf sunt sunete reformulate într-o formă accesibilă browserul-ui pentru procesare și redare ulterioară. Există mai multe tipuri de surse audio, în funcție de metoda de achiziție a sunetului: fișier existent pe disc, sample-uri generate în cod, oscilatoare și intrări hardware ale plăcii de sunet. După achiziția și formatarea sunetului, acesta poate fi trimis mai departe spre procesoare sau spre destinația audio.

AudioBuffer

Buffer-ele audio codifică sunetele în sample-uri, cum sunt fișierele audio. Caracteristica de bază a unui buffer audio se numește sample rate - frecvența cu care sunetul păstrat în buffer a fost analizat pentru a obține informații utile procesării audio. Aceste valori sunt specifice procesării digitale audio și intră într-un domeniu de cunoștințe mai vast, despre care nu vom vorbi aici.

Pentru a folosi un fișier audio, acesta se încarcă într-un buffer care apoi este conectat la destinația unui context audio:

var request = new XMLHttpRequest();
request.open("GET", "sound.mp3", true);
request.responseType = "arraybuffer";
	
request.onload = function() {
    audioContext.decodeAudioData(request.response, function(buffer) {
        // buffer conține un obiect de tip AudioBuffer
    });
}

Buffer-ul permite accesarea sample-urilor în mod direct:

var buffer = audioContext.createBuffer(1, 5, 44100);
var bufferData = buffer.getChannelData(0);
 
bufferData[i] = 0.3;

Codul de mai sus creează un buffer cu un singur canal audio (mono), care conține cinci sample-uri cu sample rate de 44100 Hz. Datele din buffer sunt accesibile într-un vector folosind metoda getChannelData - vector în care apoi se poate scrie. În acest mod se pot construi sunete folosind ecuații sau alte generatoare.

În fine, redarea unui buffer audio se face folosind nodul de tip AudioBufferSourceNode:

var source = audioContext.createBufferSource();
 
source.buffer = buffer;
source.connect(audioContext.destination);
source.start(0);

Oscillator

O altă sursă audio este oscilatorul. Oscilatorul codifică sunetele în funcții periodice, repetate apoi la infinit - astfel caracteristica sa de bază este funcția generatoare. Există patru tipuri de oscilatoare predefinite: sinusoidal, pătrat, triunghiular isoscel și triunghiular drept. Oscilatoarele pot fi conectate în paralel pentru a compune funcții, generând astfel sunete complexe.

var source = context.createOscillator();
 
source.type = 0;  // oscilator sinusoidal
source.connect(context.destination);
source.start(0);

De asemenea, se pot formula oscilatoare particulare prin compunerea mai multor funcții matematice într-un singur generator:

 

var first = new Float32Array(100);
var second = new Float32Array(100);
 
for (var i = 0; i < 100; i++) first[i] = Math.sin(Math.PI * i / 100);
for (var i = 0; I < 100; i++) second[i] = Math.cos(Math.PI * i / 100);
 
source.setWaveTable(context.createWaveTable(first, second));

Live

Pentru folosirea surselor audio live cum ar fi un microfon, browser-ul trebuie să obțină un flux audio de la sistemul de operare. Din acest motiv, aceste surse sunt accesibile doar din obiectul Javascript ce reprezintă browser-ul:

navigator.getUserMedia( { audio: true }, function(stream) {
    var context = new audioContext();
    var source = context.createMediaStreamSource(stream);
 
    source.connect(context.destination);
});

Fluxul audio nu poate fi alterat în mod direct cum este cazul buffer-elor sau a oscilatoarelor dar, ca și celelalte surse audio, poate fi conectat la oricare din nodurile procesoare definite în graf. Acest tip de sursă audio depinde direct de hardware și de driverele instalate, iar achiziția datelor audio poate întârzia sau poate interfera cu alte procese care să afecteze claritatea semnalului.

Browser-ul Chrome optimizează în mod special achiziția datelor audio live și permite crearea unor adevărate procesoare de sunet în timp real. Implementarea se bazează pe rutarea execuției cât mai rapid spre driverul audio. Dacă sistemul conține o placă de sunet dedicată, prelucrarea audio va avea întârzieri minime. În secțiunea Resurse există câteva link-uri ce demonstrează viteza de execuție în Chrome a platformei WebAudio.

Modificarea volumului audio

Există o serie de procesoare ale semnalului audio, fiecare cu specificele ei, dar în acest articol ne vom referi doar la modificarea volumului audio. Modificarea volumului se face conectând una sau mai multe surse audio la un nod de tip GainNode. Sursele audio pot fi cele descrise mai sus sau alte procesoare audio care primesc date audio:

var gainNode = context.createGainNode();
 
source.connect(gainNode);
gainNode.connect(context.destination);
gainNode.gain.value = 0.8;

Modificarea volumului se poate automatiza și sincroniza cu alte evenimente dependente de timp:

var envelope = context.createGainNode();
var now = context.currentTime;
 
source.connect(envelope);
envelope.connect(context.destination);
envelope.gain.setValueAtTime(0, now);
envelope.gain.linearRampToValueAtTime(1.0, now + 2.0);

În acest mod se pot stabili crossfade-uri între două surse de sunet de exemplu.

Tendințe

Situația actuală a platformei WebAudio nu este tocmai fericită. Practic ea este suportată în mod complet doar de browser-ul Chrome - și doar pe desktop. Doar ultimele versiuni de Chrome de pe platforma Android 4 suportă WebAudio, iar browser-ul implicit din Android nu este inclus aici. WebAudio mai este suportat de iOS6, de ChromeOS și de ChromeFrame.

Însă platforma este definită și acceptată în standardul HTML5, ceea ce înseamnă că viitorul nu poate fi decât strălucit pentru dezvoltatorii de soft multimedia. Deja se pot construi sintetizatoare audio și procesoare în timp real a sunetelor analogice, iar în curând Chrome va implementa comunicarea cu dispozitive MIDI, permițând compoziția audio folosind claviaturi și alte dispozitive profesionale.

Și ca o inovație, standardul WebAudio alături de WebSockets permite pentru prima dată crearea performanțelor audio în mod distribuit, pe web. Utilizatorii se pot conecta la un singur site web și pot crea împreună, simultan, piese audio de înaltă calitate - prima demonstrație a acestui tandem de tehnologii fiind site-ul jamwithchrome.

Resurse și referințe

http://dashersw.github.com/pedalboard.js/demo

http://www.jamwithchrome.com

http://kevincennis.com/mix

http://www.tenthcirclesound.com/sympyrean

http://docs.webplatform.org/wiki/apis/webaudio

http://webaudio-io2012.appspot.com

https://dvcs.w3.org/hg/audio/raw-file/tip/webaudio/specification.html