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ń 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.

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 działają jako moduły 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 WebKit użyj prefiksu webkit, tak jak w przypadku webkitAudioContext.

Wiele interesujących funkcji 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 bufora audio do dźwięków o krótkiej lub średniej długości. 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. Obsługa różnych formatów audio przez 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 (a nie tekstowe), więc ustawiliśmy responseTypeżądania na '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 pliku audio zapisanych w request.response i dekoduje je asynchronicznie (nie blokując głównego wątku JavaScript).

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

Odtwarzanie dźwięków

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 dokładnego 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.

Abstrakcyjne 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 śledzenia rytmu. Prawdopodobnie najbardziej znany wzór bębnów to:

prosty wzór perkusji rockowej.
Prosty wzór perkusji rockowej

w którym hi-hat jest grany co ósma, a kick i snare są grane na przemian co kwartał w takcie 4/4.

Zakładając, że mamy załadowane bufory kick, snarehihat, kod do tego 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 zamiast nieograniczonej pętli, którą widzimy w zapisie nutowym. Funkcja playSound to metoda odtwarzania bufora w określonym czasie:

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 programowo zmienić jego głośność, manipulując parametrem gainNode.gain.value w następujący sposób:

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

Przejście 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 za pomocą węzłów 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
   
};
}

Przejścia z równą mocą

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

Przejście liniowe
Przejście liniowe

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. Dzięki temu zminimalizujesz spadki głośności między regionami dźwięku, co zapewni płynniejsze przejście między regionami, które mogą się nieznacznie różnić poziomem.

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. Gdy zmienia się utwór, chcemy płynnie wyciszyć obecny ścieżkę i włączyć nową, aby uniknąć twardego 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 przełączać się 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ą stopniowe zmienianie wartości parametru, np. linearRampToValueAtTimeexponentialRampToValueAtTime.

Funkcję czasu trwania przejścia można wybrać spośród wbudowanych funkcji liniowych i wykładniczych (jak wyżej), ale możesz 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 węzłem BiquadFilterNode
Graf dźwięku 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 pasmowy
  • Filtr niskiej półki
  • Filtr wysokiej półki
  • Filtr szczytowy
  • Filtr Notch
  • Filtr dopuszczający wszystko

Wszystkie filtry zawierają parametry, które umożliwiają określenie wzmocnienia, częstotliwości stosowania filtra oraz współczynnika jakości. Filtr dolnoprzepustowy zachowuje zakres niskich częstotliwości, ale odrzuca 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 muszą być dostrojone, aby działały na 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.

Pamiętaj też, że przykładowy kod umożliwia połączenie i rozłączenie filtra, dynamicznie zmieniając wykres AudioContext. Możemy odłączyć węzły 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);

Więcej treści do słuchania

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

Jeśli szukasz inspiracji, możesz skorzystać z dobrych przykładów od innych deweloperów, którzy korzystali z 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, sekwenser 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.