Pierwsze kroki z interfejsem Web Audio API

Przed elementem <audio> HTML5 do zakłócania ciszy internetu wymagana była Flash lub inna wtyczka. Chociaż dźwięk w internecie nie wymaga już wtyczki, tag audio stwarza znaczne ograniczenia w implementacji zaawansowanych gier i aplikacji interaktywnych.

Web Audio API to wysoki poziom interfejsu API JavaScript, który służy do przetwarzania i syntetyzowania dźwięku w aplikacjach internetowych. Ten interfejs API ma obejmować funkcje stosowane w nowoczesnych silnikach audio gier oraz niektóre zadania związane z miksowaniem, przetwarzaniem i filtrowaniem, które są stosowane w nowoczesnych komputerowych aplikacjach do produkcji dźwięku. Poniżej znajdziesz krótkie wprowadzenie do korzystania z tego wydajnego interfejsu API.

Pierwsze kroki z elementem AudioContext

AudioContext służy do zarządzania wszystkimi dźwiękami i ich odtwarzania. Aby wygenerować dźwięk za pomocą interfejsu Web Audio API, utwórz co najmniej 1 źródło dźwięku i połącz je z miejscem docelowym dźwięku udostępnianym przez instancję AudioContext. Połączenie nie musi być bezpośrednie i może przechodzić przez dowolną liczbę pośrednich AudioNodes, które działają jako moduły przetwarzania sygnału audio. To kierowanie zostało szczegółowo opisane w specyfikacji Web Audio.

Pojedyncza instancja AudioContext może obsługiwać wiele wejść dźwiękowych i złożone wykresy audio, więc będziemy potrzebować tylko 1 z tych danych do każdej naszej aplikacji audio.

Ten fragment kodu tworzy AudioContext:

var context;
window.addEventListener('load', init, false);
function init() {
    try {
    context = new AudioContext();
    }
    catch(e) {
    alert('Web Audio API is not supported in this browser');
    }
}

W starszych przeglądarkach opartych na silniku WebKit użyj prefiksu webkit, tak jak webkitAudioContext.

Wiele interesujących funkcji interfejsu Web Audio API, takich jak tworzenie węzłów audio i dekodowanie danych plików audio, należy do metod AudioContext.

Wczytuję dźwięki

Interfejs Web Audio API wykorzystuje Buffer do obsługi dźwięków o krótkiej i średniej długości. Podstawowym sposobem jest użycie żądania XMLHttpRequest do pobierania plików dźwiękowych.

Interfejs API obsługuje wczytywanie danych plików audio w wielu formatach, np. WAV, MP3, AAC, OGG i innych. Obsługa różnych formatów dźwięku przez przeglądarki różni się.

Ten fragment kodu pokazuje wczytywanie próbki dźwięku:

var dogBarkingBuffer = null;
var context = new AudioContext();

function loadDogSound(url) {
    var request = new XMLHttpRequest();
    request.open('GET', url, true);
    request.responseType = 'arraybuffer';

    // Decode asynchronously
    request.onload = function() {
    context.decodeAudioData(request.response, function(buffer) {
        dogBarkingBuffer = buffer;
    }, onError);
    }
    request.send();
}

Dane pliku audio są binarne (nie tekstowe), więc ustawiliśmy responseType żądania na 'arraybuffer'. Więcej informacji o ArrayBuffers znajdziesz w tym artykule o XHR2.

Po odebranym (niezakodowanym) pliku audio można je zachować do późniejszego dekodowania lub od razu zdekodować za pomocą metody AudioContext decodeAudioData(). Ta metoda pobiera ArrayBuffer danych z plików audio przechowywanych w usłudze request.response i dekoduje je asynchronicznie (nie blokuje głównego wątku wykonywania JavaScriptu).

Po zakończeniu decodeAudioData() wywołuje funkcję wywołania zwrotnego, która dostarcza zdekodowane dane audio PCM jako AudioBuffer.

Odtwarzam dźwięki

Prosty wykres audio
Prosty wykres audio

Po wczytaniu co najmniej 1 pliku AudioBuffers możemy odtworzyć dźwięki. Załóżmy, że wczytaliśmy urządzenie AudioBuffer z dźwiękiem szczekania psa i ładowanie się zakończyło. Potem możemy uruchomić ten bufor za pomocą poniższego kodu.

var context = new AudioContext();

function playSound(buffer) {
    var source = context.createBufferSource(); // creates a sound source
    source.buffer = buffer;                    // tell the source which sound to play
    source.connect(context.destination);       // connect the source to the context's destination (the speakers)
    source.noteOn(0);                          // play the source now
}

Funkcja playSound() może być wywoływana za każdym razem, gdy ktoś naciśnie klawisz lub kliknie coś myszą.

Funkcja noteOn(time) ułatwia zaplanowanie precyzyjnego odtwarzania dźwięku w grach i innych aplikacjach o znaczeniu czasowym. Aby jednak ten harmonogram działał prawidłowo, upewnij się, że Twoje bufory dźwięku są wstępnie wczytane.

Wykorzystanie interfejsu Web Audio API

Oczywiście lepiej byłoby utworzyć bardziej ogólny system wczytywania, który nie jest na stałe zakodowany w celu wczytywania tego konkretnego dźwięku. Jest wiele metod radzenia sobie z mnóstwem krótkich i średnich dźwięków, z których mogą korzystać aplikacja lub gra audio. Oto jeden ze sposobów wykorzystania komponentu BufferLoader (nie wchodzącego w skład standardu internetowego).

Poniżej znajduje się przykład użycia klasy BufferLoader. Utwórzmy 2 AudioBuffers. Gdy tylko zostaną wczytane, odtworzymy je w tym samym czasie.

window.onload = init;
var context;
var bufferLoader;

function init() {
    context = new AudioContext();

    bufferLoader = new BufferLoader(
    context,
    [
        '../sounds/hyper-reality/br-jam-loop.wav',
        '../sounds/hyper-reality/laughter.wav',
    ],
    finishedLoading
    );

    bufferLoader.load();
}

function finishedLoading(bufferList) {
    // Create two sources and play them both together.
    var source1 = context.createBufferSource();
    var source2 = context.createBufferSource();
    source1.buffer = bufferList[0];
    source2.buffer = bufferList[1];

    source1.connect(context.destination);
    source2.connect(context.destination);
    source1.noteOn(0);
    source2.noteOn(0);
}

Radzenie sobie z czasem: granie dźwięków w rytm

Interfejs Web Audio API umożliwia programistom dokładne planowanie odtwarzania. Aby to dla tego zademonstrować, ustawmy prostą ścieżkę rytmiczną. Prawdopodobnie najbardziej znany model perkusyjny to:

Prosty wzór perkusji rockowej
Prosty wzór perkusji rockowej

w których hihat jest grany co ósemkę nuty, a kop i schwyt – na przemian co kwartał, w 4/4.

Jeśli załadowaliśmy bufory kick, snare i hihat, ten kod jest prosty:

for (var bar = 0; bar < 2; bar++) {
    var time = startTime + bar * 8 * eighthNoteTime;
    // Play the bass (kick) drum on beats 1, 5
    playSound(kick, time);
    playSound(kick, time + 4 * eighthNoteTime);

    // Play the snare drum on beats 3, 7
    playSound(snare, time + 2 * eighthNoteTime);
    playSound(snare, time + 6 * eighthNoteTime);

    // Play the hi-hat every eighth note.
    for (var i = 0; i < 8; ++i) {
    playSound(hihat, time + i * eighthNoteTime);
    }
}

Tutaj robimy tylko jedno powtórzenie, a nie nieograniczoną pętlę, jaką widzimy w nutach. Funkcja playSound to metoda, która odtwarza bufor o określonym czasie w ten sposób:

function playSound(buffer, time) {
    var source = context.createBufferSource();
    source.buffer = buffer;
    source.connect(context.destination);
    source.noteOn(time);
}

Zmienianie głośności dźwięku

Jedną z najbardziej podstawowych operacji na dźwięku jest zmiana jego głośności. Za pomocą interfejsu Web Audio API możemy kierować źródło do miejsca docelowego przez interfejs AudioGainNode, aby manipulować głośnością:

Wykres audio z węzłem wzmocnienia
Wykres audio z węzłem wzmocnienia

Taką konfigurację połączenia możesz uzyskać w następujący sposób:

// Create a gain node.
var gainNode = context.createGainNode();
// Connect the source to the gain node.
source.connect(gainNode);
// Connect the gain node to the destination.
gainNode.connect(context.destination);

Po skonfigurowaniu wykresu możesz programowo zmienić głośność, modyfikując gainNode.gain.value w ten sposób:

// Reduce the volume.
gainNode.gain.value = 0.5;

Przenikanie się dwóch dźwięków

Załóżmy teraz, że mamy nieco bardziej złożony scenariusz, w którym odtwarzamy wiele dźwięków, ale chcemy je przenikać. Jest to typowy przypadek w aplikacjach znanych z pracy DJ-ów, w których mamy 2 gramofony i chcemy przesuwać dźwięk z jednego źródła dźwięku do drugiego.

Możesz to zrobić, korzystając z tego wykresu audio:

Wykres audio z 2 źródłami połączonymi przez węzły wzmocnienia
Wykres audio z 2 źródłami połączonymi przez węzły wzmocnienia

Aby to zrobić, tworzymy po prostu 2 obiekty AudioGainNodes i łączymy je przez węzły, korzystając z podobnej funkcji:

function createSource(buffer) {
    var source = context.createBufferSource();
    // Create a gain node.
    var gainNode = context.createGainNode();
    source.buffer = buffer;
    // Turn on looping.
    source.loop = true;
    // Connect source to gain.
    source.connect(gainNode);
    // Connect gain to destination.
    gainNode.connect(context.destination);

    return {
    source: source,
    gainNode: gainNode
    };
}

Przenikanie z równą mocą

Naiwne, liniowe przenikanie polega na spadku objętości podczas przesuwania między próbkami.

Przenikanie liniowe
Przenikanie liniowe

Aby rozwiązać ten problem, używamy krzywej jednakowej mocy, w której odpowiadające jej krzywe zysku są nieliniowe i przecinają się z większą amplitudą. Pozwala to zminimalizować spadki głośności między regionami audio, powodując bardziej równomierne przenikanie między regionami, gdzie poziom może być nieco inny.

Przenikanie mocy równego.
Przenikanie z równą mocą

Przenikanie playlisty

Kolejną często stosowaną aplikacją jest odtwarzacz muzyki. Gdy utwór się zmieni, chcemy zanikać aktualny utwór i przyciemniać nowy, aby uniknąć irytującego przejścia. Aby to zrobić, zaplanuj przejście w przyszłość. Do realizacji tego harmonogramu moglibyśmy użyć właściwości setTimeout, ale nie jest to precyzyjne. Dzięki interfejsowi Web Audio API możemy użyć interfejsu AudioParam do planowania przyszłych wartości parametrów, takich jak wartość wzmocnienia AudioGainNode.

W przypadku playlisty możemy więc przejść między utworami, planując zmniejszenie wartości aktualnie odtwarzanego utworu i wzrost w przypadku kolejnej, zarówno na krótko przed zakończeniem odtwarzania bieżącego, jak i bieżącego utworu:

function playHelper(bufferNow, bufferLater) {
    var playNow = createSource(bufferNow);
    var source = playNow.source;
    var gainNode = playNow.gainNode;
    var duration = bufferNow.duration;
    var currTime = context.currentTime;
    // Fade the playNow track in.
    gainNode.gain.linearRampToValueAtTime(0, currTime);
    gainNode.gain.linearRampToValueAtTime(1, currTime + ctx.FADE_TIME);
    // Play the playNow track.
    source.noteOn(0);
    // At the end of the track, fade it out.
    gainNode.gain.linearRampToValueAtTime(1, currTime + duration-ctx.FADE_TIME);
    gainNode.gain.linearRampToValueAtTime(0, currTime + duration);
    // Schedule a recursive track change with the tracks swapped.
    var recurse = arguments.callee;
    ctx.timer = setTimeout(function() {
    recurse(bufferLater, bufferNow);
    }, (duration - ctx.FADE_TIME) - 1000);
}

Interfejs Web Audio API udostępnia wygodny zestaw metod RampToValue do stopniowej zmiany wartości parametru, np. linearRampToValueAtTime i exponentialRampToValueAtTime.

Funkcję czasu przejścia możesz wybrać z wbudowanych funkcji liniowych i wykładniczych (jak powyżej), ale możesz też określić własną krzywą wartości za pomocą tablicy wartości za pomocą funkcji setValueCurveAtTime.

Stosuję prosty efekt filtra do dźwięku

Wykres audio z węzłem BiquadFilterNode
Wykres audio z węzłem BiquadFilterNode

Interfejs Web Audio API umożliwia przesyłanie dźwięku z jednego węzła audio do innego, co tworzy potencjalnie skomplikowany łańcuch procesorów w celu dodawania złożonych efektów do form dźwiękowych.

Jednym ze sposobów jest umieszczenie obiektu BiquadFilterNode między źródłem dźwięku a miejscem docelowym. Ten typ węzła audio może korzystać z różnych filtrów niskiej kolejności, które można wykorzystać do tworzenia korektorów graficznych i jeszcze bardziej złożonych efektów. Dotyczy to głównie wyboru części widma częstotliwości dźwięku, które ma być uwydatnione, a które wyciszone.

Obsługiwane typy filtrów:

  • Filtr dolnoprzepustowy
  • Filtr górnoprzepustowy
  • Filtr pasmowy
  • Filtr niskiej półki
  • Filtr wysokiej półki
  • Filtr szczytu
  • Filtr z wcięciem
  • Filtr wszystkich kart

Wszystkie filtry zawierają parametry określające ilość wzrostu, częstotliwość, z jaką filtr ma być stosowany, oraz współczynnik jakości. Filtr dolnoprzepustowy zachowuje dolny zakres częstotliwości, ale odrzuca wysokie częstotliwości. Punkt przerwania jest określany na podstawie wartości częstotliwości, a współczynnik Q nie jest wartością jednostkową i określa kształt wykresu. Wzmocnienie ma wpływ tylko na określone filtry, takie jak filtr niskiej półki i szczytu, a nie filtr dolnoprzepustowy.

Skonfigurujmy prosty filtr dolnoprzepustowy, aby wyodrębnić tylko podstawy z próbki dźwięku:

// Create the filter
var filter = context.createBiquadFilter();
// Create the audio graph.
source.connect(filter);
filter.connect(context.destination);
// Create and specify parameters for the low-pass filter.
filter.type = 0; // Low-pass filter. See BiquadFilterNode docs
filter.frequency.value = 440; // Set cutoff to 440 HZ
// Playback the sound.
source.noteOn(0);

Ogólnie należy dostosować ustawienia częstotliwości tak, aby działały w skali logarytmicznej, ponieważ ludzki słuch działa na tej samej zasadzie (tzn. A4 to 440 Hz, a A5 – 880 Hz). Więcej informacji znajdziesz w opisie funkcji FilterSample.changeFrequency w linku do kodu źródłowego powyżej.

Pamiętaj też, że przykładowy kod pozwala podłączyć i odłączyć filtr, dynamicznie zmieniając wykres AudioContext. Możemy odłączyć węzły audio od wykresu, wywołując metodę node.disconnect(outputNumber). Aby na przykład przekierować wykres z filtra do połączenia bezpośredniego, możemy wykonać następujące czynności:

// Disconnect the source and filter.
source.disconnect(0);
filter.disconnect(0);
// Connect the source directly.
source.connect(context.destination);

Słuchaj dalej

Omówiliśmy podstawy interfejsu API, w tym wczytywanie i odtwarzanie próbek audio. Zbudowaliśmy wykresy audio z węzłami i filtrami oraz zaplanowanymi zmianami w dźwiękach i parametrach audio, aby umożliwić działanie typowych efektów dźwiękowych. Teraz możesz zacząć tworzyć internetowe aplikacje audio.

Jeśli szukasz inspiracji, wielu programistów stworzyło już świetne materiały za pomocą interfejsu Web Audio API. Moje ulubione to między innymi:

  • AudioJedit – dostępne w przeglądarce narzędzie do łączenia dźwięku, które wykorzystuje linki bezpośrednie do SoundCloud.
  • ToneCraft – sekwencer dźwięku, w którym dźwięki są tworzone przez układanie bloków w elementy 3D.
  • Plink – gra do wspólnego tworzenia muzyki, która wykorzystuje Web Audio i WebSockets.