Pierwsze kroki z interfejsem Web Audio API

Zanim pojawił się element HTML5 <audio>, do przerwania ciszy w internecie potrzebny był Flash lub inny wtyczek. Chociaż dźwięk w internecie nie wymaga już wtyczki, tag audio nakłada znaczne ograniczenia na implementację zaawansowanych gier i aplikacji interaktywnych.

Web Audio API to interfejs API JavaScriptu do przetwarzania i syntezowania dźwięku w aplikacjach internetowych. Celem tego interfejsu API jest udostępnienie funkcji dostępnych w ramach nowoczesnych silników audio do gier oraz niektórych zadań dotyczących miksowania, przetwarzania i filtrowania, które są dostępne w nowoczesnych aplikacjach do produkcji dźwięku na komputery. Poniżej znajdziesz krótkie wprowadzenie do korzystania z tego zaawansowanego interfejsu API.

Pierwsze kroki z AudioContext

Obiekt 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ępnionym przez instancję AudioContext. To połączenie nie musi być bezpośrednie i może przechodzić przez dowolną liczbę pośrednich AudioNodes, które pełnią funkcję modułów przetwarzania sygnału audio. Ten routing jest opisany bardziej szczegółowo w specyfikacji Web Audio.

Pojedynczy egzemplarz AudioContext może obsługiwać wiele wejść dźwiękowych i złożone wykresy dźwiękowe, więc w przypadku każdej tworzonej przez nas aplikacji audio będziemy potrzebować tylko jednego.

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 przypadku starszych przeglądarek opartych na silniku WebKit użyj prefiksu webkit, tak jak w przypadku webkitAudioContext.

Wiele interesujących funkcji interfejsu Web Audio API, takich jak tworzenie węzłów AudioNodes i dekodowanie danych pliku audio, to metody interfejsu AudioContext.

Wczytuję dźwięki

Interfejs Web Audio API używa AudioBuffer do obsługi krótkich i średnich dźwięków. Podstawowym podejściem jest użycie XMLHttpRequest do pobierania plików dźwiękowych.

Interfejs API obsługuje wczytywanie danych plików audio w różnych formatach, takich jak WAV, MP3, AAC, OGG i inne. Różne formaty audio obsługują przeglądarki różnią się.

Ten fragment kodu demonstruje 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), dlatego jako responseType żądania ustawiamy wartość 'arraybuffer'. Więcej informacji o ArrayBuffers znajdziesz w tym artykule o XHR2.

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

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

Odtwarzam dźwięki

Prosty wykres dźwięku
Prosty wykres audio

Gdy wczytasz co najmniej 1 AudioBuffers, możesz odtwarzać dźwięki. Załóżmy, że właśnie załadowaliśmy AudioBuffer z dźwiękiem szczekającego psa i że wczytanie się zakończyło. Następnie możemy odtworzyć ten bufor za pomocą tego 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ś myszką.

Funkcja noteOn(time) ułatwia planowanie precyzyjnego odtwarzania dźwięku w przypadku gier i innych aplikacji o krytycznym czasie działania. Aby jednak to zaplanowanie działało prawidłowo, upewnij się, że bufor dźwięku jest wstępnie załadowany.

Abstrakcyjność interfejsu Web Audio API

Oczywiście lepiej byłoby utworzyć bardziej ogólny system wczytywania, który nie jest zakodowany na potrzeby wczytywania tego konkretnego dźwięku. Istnieje wiele sposobów na obsługę wielu krótkich i średnich dźwięków, których używa aplikacja audio lub gra. Oto jeden z nich: użyj klasy BufferLoader (nie jest to część standardu internetowego).

Poniżej znajdziesz przykład użycia klasy BufferLoader. Utwórzmy 2 AudioBuffers i odtwórzmy je jednocześnie, gdy tylko się wczytają.

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);
}

Zarządzanie czasem: odtwarzanie dźwięków z rytmem

Interfejs Web Audio API umożliwia programistom precyzyjne planowanie odtwarzania. Aby to zademonstrować, skonfigurujmy prostą ścieżkę do rytmu. Prawdopodobnie najbardziej znany wzór bębnów to:

prosty wzór perkusji rockowej.
Prosty wzór bębnów skalnych

w którym hihat gra co ósmą nutę, a kopnięcie i werble grają na zmianę co kwartał, po 4/4 czasu.

Jeśli załadowaliśmy bufory kick, snare i hihat, kod do wykonania tej czynności 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 nieograniczony pętlę, jaką widać 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);
}

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

Jedną z najprostszych operacji, jakie możesz wykonać na dźwięku, jest zmiana jego głośności. Za pomocą interfejsu Web Audio API możemy przekierować źródło do miejsca docelowego przez węzeł AudioGainNode, aby manipulować głośnością:

Wykres audio z węzłem wzmocnienia
Grafik dźwięku z węzłem wzmocnienia

Konfigurację połączenia można wykonać w ten 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 zmienić jego głośność za pomocą kodu, manipulując parametrem gainNode.gain.value w następujący sposób:

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

Przejścia między dwoma dźwiękami

Załóżmy, że mamy nieco bardziej złożony scenariusz, w którym odtwarzamy kilka dźwięków, ale chcemy je płynnie połączyć. Jest to typowy przypadek w aplikacji typu DJ, w której mamy 2 gramofony i chcemy płynnie przechodzić od jednego źródła dźwięku do drugiego.

Można to zrobić za pomocą tego wykresu audio:

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

Aby to skonfigurować, wystarczy utworzyć 2 AudioGainNodes i połączyć każde źródło za pomocą węzłów, używając funkcji podobnej do tej:

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 o równej mocy

Proste podejście z liniowym przejściem powoduje spadek głośności podczas przełączania się między próbkami.

Przenikanie liniowe
Liniowy przenikanie

Aby rozwiązać ten problem, używamy krzywej mocy o równej wartości, w której odpowiadające jej krzywe wzmocnienia są nieliniowe i przecinają się przy większej amplitudzie. Pozwala to zminimalizować spadki głośności w poszczególnych regionach dźwięku, co skutkuje bardziej równomiernym przenikaniem między regionami, które mogą się nieznacznie różnić pod względem poziomu.

Przejście z równą mocą.
Przejście z równą mocą

Przejścia między utworami na playliście

Innym typowym zastosowaniem crossfadera jest odtwarzacz muzyczny. Przy zmianie utworu chcemy zagłuszać bieżący utwór i przyłapywać nowy, aby uniknąć drażniącego przejścia. Aby to zrobić, zaplanuj przejście płynne na przyszłość. Możemy użyć do tego celu funkcji setTimeout, ale nie jest to dokładne. Dzięki interfejsowi Web Audio API możemy używać interfejsu AudioParam do planowania przyszłych wartości parametrów, takich jak wartość wzmocnienia AudioGainNode.

W przypadku playlisty możemy więc planować przejście między utworami, które są obecnie odtwarzane, i zwiększenie wzmocnienia na kolejnym – oba trochę przed zakończeniem odtwarzania 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, które umożliwiają stopniowe zmienianie wartości parametru, np. linearRampToValueAtTimeexponentialRampToValueAtTime.

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

Stosowanie prostego efektu filtra do dźwięku

Wykres audio z urządzeniem BiquadFilterNode
Grafik audio z węzłem BiquadFilterNode

Interfejs Web Audio API umożliwia przesyłanie dźwięku z jednego węzła audio do drugiego, tworząc potencjalnie złożony łańcuch procesorów, aby dodawać złożone efekty do Soundform.

Jednym ze sposobów jest umieszczenie między źródłem a miejscem docelowym BiquadFilterNode. Ten typ węzła audio może wykonywać różne filtry niskiego rzędu, które można wykorzystać do tworzenia korektorów graficznych, a nawet bardziej złożonych efektów, głównie do wyboru, które części widma częstotliwości dźwięku mają być podkreślone, a które stłumione.

Obsługiwane typy filtrów:

  • Filtr dolnoprzepustowy
  • Filtr górnoprzepustowy
  • Filtr pasma
  • Filtr niskiej półki
  • Filtr wysokiej półki
  • Filtr szczytowy
  • Filtr z wycięciem
  • Filtr wszystkich kart

Wszystkie filtry zawierają parametry, które umożliwiają określenie wzmocnienia, częstotliwości stosowania filtra oraz współczynnika jakości. Filtr dolnoprzepustowy zachowuje niższy zakres częstotliwości, ale odrzuca te wysokie. Punkt przecięcia określa wartość częstotliwości, a współczynnik Q jest bezwymiarowy i określa kształt wykresu. Nawet jeśli wzmocnienie wpływa tylko na niektóre filtry, takie jak filtry dolnoprzepustowe i wysokoprzepustowe, a nie filtr dolnoprzepustowy.

Skonfigurujmy prosty filtr dolnoprzepustowy, aby wyodrębnić tylko basy 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 rzecz biorąc, ustawienia częstotliwości należy dostosować do skali logarytmicznej, ponieważ słuch człowieka działa na tej samej zasadzie (czyli A4 to 440 Hz, a A5 to 880 Hz). Więcej informacji znajdziesz w funkcji FilterSample.changeFrequency w linku do kodu źródłowego powyżej.

Pamiętaj też, że przykładowy kod umożliwia połączenie i rozłączenie filtra, dynamicznie zmieniając wykres AudioContext. Możemy odłączyć AudioNodes od wykresu, wywołując funkcję node.disconnect(outputNumber). Aby na przykład zmienić trasę grafu z przechodzenia przez filtr na połączenie bezpośrednie, wykonaj te czynności:

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

Dalsze nasłuchiwanie

Omówiliśmy podstawy interfejsu API, w tym wczytywanie i odtwarzanie próbek dźwięku. Stworzyliśmy wykresy audio z węzłami wzmocnienia i filtrami oraz zaplanowane modyfikacje dźwięków i parametrów audio, aby umożliwić stosowanie typowych efektów dźwiękowych. Teraz możesz tworzyć świetne aplikacje internetowe do obsługi dźwięku.

Jeśli szukasz inspiracji, wielu deweloperów już stworzyło świetne materiały za pomocą interfejsu Web Audio API. Do moich ulubionych należą:

  • AudioJedit – narzędzie do zszywania dźwięku w przeglądarce, które wykorzystuje linki stałe SoundCloud.
  • ToneCraft – sekwencer dźwięku, w którym dźwięki są tworzone przez układanie bloków 3D.
  • Plink, gra do wspólnego tworzenia muzyki wykorzystująca Web Audio i Web Sockets.