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.
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 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
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:
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
, snare
i hihat
, 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ą:
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:
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.
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ś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. linearRampToValueAtTime
i exponentialRampToValueAtTime
.
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
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.