Pierwsze kroki z interfejsem Web Audio API

Zanim pojawił się element HTML5 <audio>, do odtwarzania dźwięku w internecie potrzebny był Flash lub inna wtyczka. Chociaż dźwięk w internecie nie wymaga już wtyczki, tag audio wprowadza znaczne ograniczenia w implementacji zaawansowanych gier i aplikacji interaktywnych.

Web Audio API to wysokopoziomowy interfejs JavaScript API do przetwarzania i syntezy dźwięku w aplikacjach internetowych. Celem tego interfejsu API jest uwzględnienie funkcji dostępnych w nowoczesnych silnikach dźwięku w grach oraz niektórych zadań związanych z miksowaniem, przetwarzaniem i filtrowaniem, które można znaleźć 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

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. To połączenie nie musi być bezpośrednie i może przechodzić przez dowolną liczbę pośrednich węzłów AudioNodes, które działają jako moduły przetwarzania sygnału audio. To routing jest 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 do każdej tworzonej aplikacji audio potrzebujemy tylko jednego takiego elementu.

Ten fragment kodu tworzy obiekt 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 WebKit użyj prefiksu webkit, tak jak w przypadku webkitAudioContext.

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

Wczytuję dźwięki

Interfejs Web Audio API używa AudioBuffer do odtwarzania 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 wielu formatach, takich jak WAV, MP3, AAC, OGG i inne. Obsługa różnych formatów audio przez przeglądarki może się różnić.

Poniższy fragment kodu pokazuje, jak wczytać próbkę 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 ustawiamy responseType żądania na 'arraybuffer'. Więcej informacji o ArrayBuffers znajdziesz w tym artykule o XHR2.

Po odebraniu (niezdekodowanych) danych pliku audio można je zachować do późniejszego zdekodowania lub zdekodować od razu za pomocą metody AudioContext decodeAudioData(). Ta metoda pobiera ArrayBuffer danych pliku audio przechowywanych w request.response i dekoduje je asynchronicznie (nie blokując głównego wątku wykonywania kodu JavaScript).

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

Odtwarzanie dźwięków

Po wczytaniu co najmniej 1 AudioBuffers możemy odtwarzać dźwięki. Załóżmy, że właśnie załadowaliśmy do AudioBuffer dźwięk szczekania psa i że ładowanie zostało zakończone. 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
}

Ta 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 planowanie precyzyjnego odtwarzania dźwięku w grach i innych aplikacjach, w których czas ma kluczowe znaczenie. Aby jednak harmonogram działał prawidłowo, upewnij się, że bufory dźwięku są wstępnie załadowane.

Abstrakcja interfejsu Web Audio API

Oczywiście lepiej byłoby utworzyć bardziej ogólny system wczytywania, który nie jest zakodowany na stałe do wczytywania tego konkretnego dźwięku. Istnieje wiele sposobów na radzenie sobie z wieloma krótkimi i średnimi dźwiękami, które mogą być używane przez aplikację lub grę audio. Oto jeden ze sposobów wykorzystujący BufferLoader (nie jest częścią standardu internetowego).

Poniżej znajdziesz przykład użycia klasy BufferLoader. Utwórzmy 2 obiekty AudioBuffers i gdy tylko zostaną załadowane, odtwórzmy je jednocześnie.

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

Czas: odtwarzanie dźwięków w rytmie

Interfejs Web Audio API umożliwia deweloperom precyzyjne planowanie odtwarzania. Aby to zademonstrować, skonfigurujmy prostą ścieżkę rytmiczną. Prawdopodobnie najbardziej znanym wzorcem perkusyjnym jest ten:

Prosty rockowy wzór perkusyjny

w którym hi-hat jest grany co ósmą nutę, a stopa i werbel są grane naprzemiennie co ćwierćnutę w metrum 4/4.

Załóżmy, że wczytaliśmy bufory kick, snare i hihat. Kod, który to robi, 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 powtarzamy tylko raz, a nie w nieskończoność, jak w nutach. Funkcja playSound to metoda, która odtwarza bufor w określonym czasie w następujący 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 najbardziej podstawowych operacji, jakie można 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 AudioGainNode, aby manipulować głośnością:

Prosty schemat blokowy przedstawiający dźwięk przepływający ze źródła do węzła GainNode i do miejsca docelowego.

Takie połączenie można skonfigurować 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 programowo zmienić głośność, manipulując elementem gainNode.gain.value w ten sposób:

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

płynne przechodzenie między 2 dźwiękami,

Załóżmy teraz, że mamy nieco bardziej złożony scenariusz, w którym odtwarzamy kilka dźwięków, ale chcemy, aby płynnie przechodziły jeden w drugi. To typowy przypadek w aplikacji DJ-skiej, w której mamy 2 gramofony i chcemy mieć możliwość przechodzenia z jednego źródła dźwięku na drugie.

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

Wykres audio z 2 źródłami połączonymi z osobnymi węzłami wzmocnienia, które są następnie kierowane do tego samego miejsca docelowego.

Aby to skonfigurować, wystarczy utworzyć 2 węzły 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
    };
}

Płynne przejście z równą mocą

Naiwne podejście do liniowego przenikania powoduje spadek głośności podczas przesuwania między próbkami.

Liniowe przejście przedstawione jako 2 uzupełniające się linie proste odwzorowane jako poziomy amplitudy w czasie.
Liniowe przenikanie

Aby rozwiązać ten problem, używamy krzywej równej mocy, w której odpowiednie krzywe wzmocnienia są nieliniowe i przecinają się przy wyższej amplitudzie. Minimalizuje to spadki głośności między regionami audio, co skutkuje bardziej równomiernym przejściem między regionami, które mogą mieć nieco inny poziom.

Przenikanie o równej mocy przedstawione jako 2 krzywe linie odwzorowane jako poziomy amplitudy w czasie.
Przenikanie z równą mocą

Płynne przechodzenie między utworami na playliście

Innym typowym zastosowaniem suwaka jest aplikacja odtwarzacza muzyki. Gdy utwór się zmienia, chcemy, aby bieżący utwór został wyciszony, a nowy został podgłośniony, aby uniknąć gwałtownego przejścia. Aby to zrobić, zaplanuj płynne przejście w przyszłości. Do planowania moglibyśmy użyć setTimeout, ale nie jest to precyzyjne. 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.

Dzięki temu, mając playlistę, możemy przechodzić między utworami, planując zmniejszenie wzmocnienia w obecnie odtwarzanym utworze i zwiększenie wzmocnienia w następnym, oba nieco 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ą stopniową zmianę wartości parametru, takich jak linearRampToValueAtTime i exponentialRampToValueAtTime.

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

Zastosowanie prostego efektu filtra do dźwięku

Prosty schemat blokowy przedstawiający przepływ dźwięku ze źródła do węzła BiquadFilterNode i do miejsca docelowego.

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, który pozwala dodawać złożone efekty do form dźwiękowych.

Możesz to zrobić, umieszczając BiquadFilterNode między źródłem dźwięku a miejscem docelowym. 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 związanych z wyborem, które części widma częstotliwości dźwięku mają być podkreślone, a które wyciszone.

Obsługiwane typy filtrów:

  • Filtr dolnoprzepustowy
  • Filtr górnoprzepustowy
  • Filtr pasmowo-przepustowy
  • Filtr dolnopółkowy
  • Filtr półkowy górnoprzepustowy
  • Filtr szczytowy
  • Filtr wycinający
  • Filtr wszechprzepustowy

Wszystkie filtry zawierają parametry określające poziom wzmocnienia, częstotliwość, z jaką filtr ma być stosowany, oraz współczynnik jakości. Filtr dolnoprzepustowy zachowuje niższy zakres częstotliwości, ale odrzuca wysokie częstotliwości. Punkt odcięcia jest określany przez wartość częstotliwości, a współczynnik Q jest bezwymiarowy i określa kształt wykresu. Wzmocnienie wpływa tylko na niektóre filtry, takie jak filtr dolnopółkowy i filtr szczytowy, a nie na ten 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, elementy sterujące częstotliwością muszą być dostosowane do działania w skali logarytmicznej, ponieważ ludzki słuch 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.

Na koniec zwróć uwagę, że przykładowy kod umożliwia podłączanie i odłączanie filtra, co dynamicznie zmienia wykres AudioContext. Możemy odłączyć węzły AudioNode od grafu, wywołując metodę node.disconnect(outputNumber). Aby na przykład przekierować wykres z filtra na połączenie bezpośrednie, możesz wykonać te czynności:

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

Dalsze słuchanie

Omówiliśmy podstawy interfejsu API, w tym wczytywanie i odtwarzanie próbek dźwiękowych. Utworzyliśmy wykresy audio z węzłami wzmocnienia i filtrami oraz zaplanowaliśmy dźwięki i modyfikacje parametrów audio, aby umożliwić korzystanie z niektórych typowych efektów dźwiękowych. Teraz możesz już tworzyć świetne aplikacje audio w internecie.

Jeśli szukasz inspiracji, wielu deweloperów stworzyło już świetne projekty przy użyciu interfejsu Web Audio API. Niektóre z moich ulubionych to:

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