Wprowadzenie
Dźwięk jest jednym z głównych elementów, które sprawiają, że multimedia są tak atrakcyjne. Jeśli kiedykolwiek próbowałeś/próbowałaś oglądać film z wyłączonym dźwiękiem, prawdopodobnie zauważyłeś/zauważyłaś ten problem.
Gry nie są wyjątkiem. Najmilej wspominam muzykę i efekty dźwiękowe. W wielu przypadkach, prawie 20 lat po zagraniu w ulubione gry, wciąż nie mogę zapomnieć o kompozycjach Kojiego Kondo z Zelda i o atmosferycznym Diablo Matta Uelmena. To samo dotyczy chwytliwych efektów dźwiękowych, takich jak natychmiast rozpoznawalne dźwięki klikania jednostek w Warcraft i sample z klasycznych gier Nintendo.
Dźwięk w grze stwarza pewne ciekawe wyzwania. Aby stworzyć wiarygodną muzykę do gry, projektanci muszą dostosować się do potencjalnie nieprzewidywalnego stanu gry, w którym znajduje się gracz. W praktyce niektóre części gry mogą trwać przez nieokreślony czas, dźwięki mogą wchodzić w interakcje ze środowiskiem i mieszać się w skomplikowany sposób, np. poprzez efekty pomieszczeniowe i względne pozycjonowanie dźwięku. Na koniec należy pamiętać, że może być odtwarzanych jednocześnie wiele dźwięków, które muszą dobrze brzmieć i renderować bez obniżania wydajności.
Dźwięk w grze w przeglądarce
W przypadku prostych gier może wystarczyć użycie tagu <audio>
. Jednak wiele przeglądarek ma nieodpowiednią implementację, co powoduje zakłócenia dźwięku i wysoki czas reakcji. Mamy nadzieję, że jest to problem przejściowy, ponieważ dostawcy ciężko pracują nad ulepszaniem swoich implementacji. Aby sprawdzić stan tagu <audio>
, możesz skorzystać z testów na stronie areweplayingyet.org.
Przyjrzeliśmy się bliżej specyfikacji tagu <audio>
i okazało się, że nie można za jego pomocą wykonać wielu czynności, co nie jest zaskakujące, ponieważ został on zaprojektowany do odtwarzania multimediów. Oto niektóre z nich:
- Brak możliwości zastosowania filtrów do sygnału dźwiękowego
- Brak dostępu do nieprzetworzonych danych PCM
- brak pojęcia pozycji i kierunku źródeł i słuchaczy;
- Brak dokładnego określenia czasu.
W pozostałej części artykułu omawiam niektóre z tych tematów w kontekście dźwięku w grze stworzonego za pomocą interfejsu Web Audio API. Krótkie wprowadzenie do tego interfejsu API znajdziesz w tym samouczku.
Podkład muzyczny
Muzyka w treściach z gier często odtwarzana jest w pętli.
Może to być bardzo uciążliwe, jeśli pętla jest krótka i przewidywalna. Jeśli gracz utknie w jakiejś lokacji lub na jakimś poziomie, a w tle będzie ciągle odtwarzany ten sam sample, warto stopniowo wyciszyć ścieżkę, aby uniknąć dalszych frustracji. Inną strategią jest tworzenie miksów o różnej intensywności, które stopniowo przechodzą jeden w drugi w zależności od kontekstu gry.
Jeśli na przykład gracz znajduje się w strefie z epicką walką z bossem, możesz mieć kilka miksów o różnym natężeniu emocjonalnym, od nastrojowego po zapowiadający coś lub intensywny. Oprogramowanie do syntezy dźwięku często umożliwia eksportowanie kilku miksów (o tej samej długości) na podstawie utworu przez wybranie zestawu ścieżek do wyeksportowania. Dzięki temu uzyskasz spójność wewnętrzną i unikniesz nieprzyjemnych przejść podczas przechodzenia z jednego utworu na drugi.

Następnie za pomocą interfejsu Web Audio API możesz zaimportować wszystkie te próbki, korzystając z czegoś takiego jak klasa BufferLoader za pomocą XHR (temat ten jest szczegółowo omówiony w artykule wprowadzającym do Web Audio API). Ładowanie dźwięków zajmuje czas, dlatego zasoby używane w grze powinny być wczytywane podczas wczytywania strony, na początku poziomu lub stopniowo podczas gry.
Następnie tworzysz źródło dla każdego węzła oraz węzeł wzmocnienia dla każdego źródła i łączysz je na wykresie.
Następnie możesz odtwarzać wszystkie te źródła jednocześnie w pętli. Ponieważ mają one taką samą długość, interfejs Web Audio API będzie je odpowiednio dopasowywać. W miarę zbliżania się do ostatniej walki z bossem gra może zmieniać wartości przyrostu dla poszczególnych węzłów w łańcuchu, używając algorytmu wartości przyrostu, takiego jak ten:
// Assume gains is an array of AudioGainNode, normVal is the intensity
// between 0 and 1.
var value = normVal - (gains.length - 1);
// First reset gains on all nodes.
for (var i = 0; i < gains.length; i++) {
gains[i].gain.value = 0;
}
// Decide which two nodes we are currently between, and do an equal
// power crossfade between them.
var leftNode = Math.floor(value);
// Normalize the value between 0 and 1.
var x = value - leftNode;
var gain1 = Math.cos(x - 0.5*Math.PI);
var gain2 = Math.cos((1.0 - x) - 0.5*Math.PI);
// Set the two gains accordingly.
gains[leftNode].gain.value = gain1;
// Check to make sure that there's a right node.
if (leftNode < gains.length - 1) {
// If there is, adjust its gain.
gains[leftNode + 1].gain.value = gain2;
}
W przypadku tego podejścia 2 źródła odtwarzane są jednocześnie, a my przechodzimy między nimi, używając krzywej mocy o równej wartości (jak opisano w wprowadzeniu).
Brakujący element: tag audio do Web Audio
Wielu deweloperów gier używa obecnie tagu <audio>
do muzyki w tle, ponieważ jest on odpowiedni do strumieniowego przesyłania treści. Teraz możesz przenosić treści z tagu <audio>
do kontekstu Web Audio.
Ta technika może być przydatna, ponieważ tag <audio>
może działać z treściami strumieniowymi, co pozwala od razu odtwarzać muzykę w tle, zamiast czekać na jej pobranie. Przesyłając strumień do interfejsu Web Audio API, możesz go analizować i zmieniać. W tym przykładzie filtr dolnoprzepustowy jest stosowany do muzyki odtwarzanej za pomocą tagu <audio>
:
var audioElement = document.querySelector('audio');
var mediaSourceNode = context.createMediaElementSource(audioElement);
// Create the filter
var filter = context.createBiquadFilter();
// Create the audio graph.
mediaSourceNode.connect(filter);
filter.connect(context.destination);
Więcej informacji o integracji tagu <audio>
z interfejsem Web Audio API znajdziesz w tym krótkim artykule.
Efekty dźwiękowe
Gry często odtwarzają efekty dźwiękowe w reakcji na działania użytkownika lub zmiany w stanie gry. Jednak podobnie jak muzyka w tle, efekty dźwiękowe mogą bardzo szybko stać się uciążliwe. Aby tego uniknąć, warto mieć pulę podobnych, ale różnych dźwięków. Może to być niewielka zmiana w próbkach dźwięku kroków lub bardzo duża, jak w przypadku klikania jednostek w serii Warcraft.
Kolejną ważną cechą efektów dźwiękowych w grach jest to, że może ich być wiele jednocześnie. Wyobraź sobie, że jesteś w środku strzelaniny z wieloma aktorami strzelając z karabinów maszynowych. Każdy karabin maszynowy strzela wiele razy na sekundę, co powoduje, że jednocześnie odtwarzane są dziesiątki efektów dźwiękowych. Odtwarzanie dźwięku z wielu źródeł z dokładnym ustawieniem czasu to jedna z funkcji, w której interfejs Web Audio API sprawdza się szczególnie dobrze.
W tym przykładzie tworzymy serię strzałów z karabinu maszynowego z wielu próbek pojedynczych pocisków, tworząc wiele źródeł dźwięku, których odtwarzanie jest przesunięte w czasie.
var time = context.currentTime;
for (var i = 0; i < rounds; i++) {
var source = this.makeSource(this.buffers[M4A1]);
source.noteOn(time + i - interval);
}
Gdyby wszystkie karabiny maszynowe w grze brzmiały tak samo, byłoby to dość nudne. Oczywiście dźwięki będą się różnić w zależności od odległości od celu i względnej pozycji (więcej na ten temat w następnych krokach), ale nawet to może nie wystarczyć. Na szczęście interfejs Web Audio API umożliwia łatwe dostosowanie powyższego przykładu na 2 sposoby:
- z delikatną zmianą czasu między strzałami;
- Zmiana szybkości odtwarzania każdego próbki (a także zmiany wysokości dźwięku) w celu lepszego odwzorowania losowości w rzeczywistym świecie.
Aby zobaczyć, jak te techniki działają w praktyce, obejrzyj prezentację stołu do bilarda, która wykorzystuje losowe próbkowanie i zmienia współczynnik playbackRate, aby uzyskać bardziej interesujący dźwięk uderzenia kulki.
Dźwięk przestrzenny 3D
Gry często rozgrywają się w świecie o określonych właściwościach geometrycznych, w 2D lub 3D. W takim przypadku dźwięk stereo może znacznie zwiększyć wrażenia. Na szczęście interfejs Web Audio API jest wyposażony w wbudowane funkcje dźwięku pozycyjnego przyspieszonego sprzętowo, które są dość proste w użyciu. Upewnij się, że masz głośniki stereo (najlepiej słuchawki), aby zrozumieć ten przykład.
W tym przykładzie na środku płótna znajduje się słuchacz (ikona osoby), a mysz wpływa na pozycję źródła (ikona głośnika). Powyższy przykład pokazuje, jak za pomocą węzła AudioPannerNode uzyskać ten efekt. Podstawowym założeniem tego przykładu jest reagowanie na ruchy myszy przez ustawianie pozycji źródła dźwięku w ten sposób:
PositionSample.prototype.changePosition = function(position) {
// Position coordinates are in normalized canvas coordinates
// with -0.5 < x, y < 0.5
if (position) {
if (!this.isPlaying) {
this.play();
}
var mul = 2;
var x = position.x / this.size.width;
var y = -position.y / this.size.height;
this.panner.setPosition(x - mul, y - mul, -0.5);
} else {
this.stop();
}
};
Co warto wiedzieć o przetwarzaniu przez Web Audio danych dotyczących lokalizacji:
- Odbiornik znajduje się domyślnie w początku układu współrzędnych (0, 0, 0).
- Interfejsy API do pozycjonowania dźwięku w Web Audio nie mają jednostek, więc wprowadziłem mnożnik, aby demo brzmiało lepiej.
- Web Audio używa układu współrzędnych kartezjańskich, w którym oś Y jest skierowana w górę (w przeciwieństwie do większości systemów grafiki komputerowej). Dlatego w tym fragmencie kodu zamieniam oś y.
Zaawansowane: stożki dźwięku
Model pozycji jest bardzo wydajny i dosyć zaawansowany, głównie ze względu na to, że opiera się na OpenAL. Więcej informacji znajdziesz w sekcji 3 i 4 specyfikacji, do której link znajduje się powyżej.

Do kontekstu Web Audio API jest dołączony pojedynczy obiekt AudioListener, który można skonfigurować w przestrzeni za pomocą pozycji i orientacji. Każde źródło może być przekazywane przez węzeł AudioPannerNode, który przetwarza dźwięk wejściowy w przestrzeń. Węzeł panner ma pozycję i orientację, a także model odległości i kierunku.
Model odległości określa wielkość wzmocnienia w zależności od odległości od źródła, natomiast model kierunkowy można skonfigurować, podając stożek wewnętrzny i zewnętrzny, które określają wielkość (zwykle ujemnego) wzmocnienia, jeśli słuchacz znajduje się w stożku wewnętrznym, między stożkiem wewnętrznym a zewnętrznym lub poza stożkiem zewnętrznym.
var panner = context.createPanner();
panner.coneOuterGain = 0.5;
panner.coneOuterAngle = 180;
panner.coneInnerAngle = 0;
Chociaż mój przykład jest w 2D, ten model łatwo zastosować do trzeciej wymiany. Przykład dźwięku przestrzennego w 3D znajdziesz w przykładowym pliku z dźwiękiem pozycjonowanym przestrzennie. Oprócz pozycji model dźwięku Web Audio może opcjonalnie uwzględniać prędkość dopplerowską. Ten przykład pokazuje efekt Dopplera w większym stopniu szczegółowości.
Więcej informacji na ten temat znajdziesz w szczegółowym samouczku [mixing positional audio and WebGL][webgl].
Efekty i filtry pokoju
W rzeczywistości sposób postrzegania dźwięku zależy w dużej mierze od pomieszczenia, w którym jest on słyszalny. Te same skrzypiące drzwi będą brzmieć zupełnie inaczej w piwnicy niż w dużym, otwartym holu. Twórcy gier o wysokiej jakości będą chcieli imitować te efekty, ponieważ tworzenie osobnego zestawu próbek dla każdego środowiska jest bardzo kosztowne i spowodowałoby jeszcze większą liczbę zasobów oraz większą ilość danych gry.
Ogólnie rzecz biorąc, różnica między dźwiękiem surowym a dźwiękiem w rzeczywistości to impuls. Te odpowiedzi impulsowej można skrupulatnie nagrać, a na dodatek istnieją strony, na których można znaleźć wiele takich nagranych wcześniej plików odpowiedzi impulsowej (przechowywanych jako pliki audio).
Więcej informacji o tym, jak tworzyć odpowiedzi impulsowe w danym środowisku, znajdziesz w sekcji „Konfiguracja nagrywania” w części Konwolucja specyfikacji Web Audio API.
Co ważniejsze w naszym przypadku, interfejs Web Audio API zapewnia łatwy sposób na zastosowanie tych odpowiedzi impulsowych do naszych dźwięków za pomocą węzła ConvolverNode.
// Make a source node for the sample.
var source = context.createBufferSource();
source.buffer = this.buffer;
// Make a convolver node for the impulse response.
var convolver = context.createConvolver();
convolver.buffer = this.impulseResponseBuffer;
// Connect the graph.
source.connect(convolver);
convolver.connect(context.destination);
Zobacz też demonstrację efektów pogłosowych na stronie specyfikacji interfejsu Web Audio API oraz ten przykład, który pozwala kontrolować miksowanie suchego (surowego) i mokrego (przetworzonego za pomocą konwulsora) dźwięku w ramach standardu jazzowego.
Ostatnie odliczanie
Stworzyliśmy grę, skonfigurowaliśmy dźwięk przestrzenny i mamy dużą liczbę węzłów AudioNode w grafie, które odtwarzają dźwięk jednocześnie. Świetnie, ale jest jeszcze jedna kwestia do rozważenia:
Ponieważ wiele dźwięków nakłada się na siebie bez normalizacji, możesz znaleźć się w sytuacji, w której przekroczysz możliwości głośnika. Podobnie jak obrazy, które wychodzą poza granice płótna, dźwięki mogą być przycinane, jeśli przebieg wykresu przekracza maksymalny próg, co powoduje wyraźne zniekształcenie. Fala wygląda mniej więcej tak:

Oto prawdziwy przykład działania funkcji przycinania. Sygnał wygląda źle:

Ważne jest, aby posłuchać zniekształconego dźwięku, takiego jak ten powyżej, lub odwrotnie – zbyt przytłumionych miksów, które zmuszają słuchaczy do zwiększenia głośności. Jeśli tak się dzieje, musisz to naprawić.
Wykrywanie przycinania
Z technicznego punktu widzenia przycięcie następuje, gdy wartość sygnału w dowolnym kanale przekracza prawidłowy zakres, czyli -1 i 1. Gdy wykryjesz takie zachowanie, warto wyświetlić użytkownikowi wizualną informację o tym, co się dzieje. Aby to zrobić, umieść w diagramie węzeł JavaScriptAudioNode. Graf dźwięku będzie wyglądał tak:
// Assume entire sound output is being piped through the mix node.
var meter = context.createJavaScriptNode(2048, 1, 1);
meter.onaudioprocess = processAudio;
mix.connect(meter);
meter.connect(context.destination);
Przycięcie może zostać wykryte w tym obiekcie processAudio
:
function processAudio(e) {
var buffer = e.inputBuffer.getChannelData(0);
var isClipping = false;
// Iterate through buffer to check if any of the |values| exceeds 1.
for (var i = 0; i < buffer.length; i++) {
var absValue = Math.abs(buffer[i]);
if (absValue >= 1) {
isClipping = true;
break;
}
}
}
Ogólnie staraj się nie nadużywać funkcji JavaScriptAudioNode
ze względu na wydajność. W tym przypadku alternatywna implementacja pomiaru mogłaby odczytywać wartość RealtimeAnalyserNode
w grafu audio dla getByteFrequencyData
w momencie renderowania, zgodnie z wartością określoną przez requestAnimationFrame
. To podejście jest bardziej efektywne, ale pomija większość sygnału (w tym miejsca, w których może dojść do przycięcia), ponieważ renderowanie odbywa się maksymalnie 60 razy na sekundę, podczas gdy sygnał audio zmienia się znacznie szybciej.
Wykrywanie klipów jest bardzo ważne, dlatego w przyszłości prawdopodobnie zobaczymy wbudowany węzeł MeterNode
interfejsu API Web Audio.
Zapobieganie przycinaniu
Dostosowując wzmocnienie w masterowym AudioGainNode, możesz stonować miks do poziomu, który zapobiega przycięciu. W praktyce jednak dźwięki odtwarzane w grze mogą zależeć od wielu czynników, więc trudno jest określić wartość głównego wzmocnienia, która zapobiega przycięciu w wszystkich stanach. Ogólnie rzecz biorąc, należy dostosować wzmocnienie, aby przewidzieć najgorszy scenariusz, ale jest to bardziej sztuka niż nauka.
Dodaj odrobinę cukru.
Kompresory są często używane w muzyce i produkcji gier, aby wygładzić sygnał i kontrolować jego szum. Ta funkcja jest dostępna w świecie Web Audio za pomocą elementu DynamicsCompressorNode
, który można wstawić do grafu audio, aby uzyskać głośniejszy, bogatszy i pełniejszy dźwięk, a także aby zapobiec przycięciu.
Cytowanie specyfikacji bezpośrednio w tym węźle
Kompresja dynamiki jest zwykle dobrym pomysłem, zwłaszcza w ustawieniach gry, w których, jak już wspomnieliśmy, nie wiesz dokładnie, jakie dźwięki będą odtwarzane i kiedy. Plink z DinahMoe Labs to świetny przykład, ponieważ dźwięki, które są odtwarzane, zależą od Ciebie i innych uczestników. Kompresor jest przydatny w większości przypadków, z wyjątkiem tych rzadkich, gdy masz do czynienia z dokładnie zmasterowanymi ścieżkami, które zostały już dostrojone tak, aby brzmiały „idealnie”.
Aby to zrobić, wystarczy umieścić w grafu audio węzeł DynamicCompressorNode, zazwyczaj jako ostatni węzeł przed węzłem docelowym:
// Assume the output is all going through the mix node.
var compressor = context.createDynamicsCompressor();
mix.connect(compressor);
compressor.connect(context.destination);
Więcej informacji o kompresji dynamicznej znajdziesz w tym artykule na Wikipedii.
Podsumowując, dokładnie posłuchaj, czy nie występuje przycięcie, i zapobiegaj mu, wstawiając węzeł wzmocnienia głównego. Następnie zaciśnij cały miks, używając węzła kompresora dynamiki. Wykres dźwięku może wyglądać tak:

Podsumowanie
To są moim zdaniem najważniejsze aspekty tworzenia ścieżki dźwiękowej do gry za pomocą interfejsu Web Audio API. Dzięki tym technikom możesz tworzyć naprawdę atrakcyjne treści audio bezpośrednio w przeglądarce. Zanim się pożegnam, chcę podzielić się z Tobą wskazówką dotyczącą przeglądarki: jeśli karta staje się nieaktywna, za pomocą interfejsu API widoczności strony zatrzymaj dźwięk, ponieważ w przeciwnym razie użytkownik może poczuć się sfrustrowany.
Więcej informacji o audio w internecie znajdziesz w artykule wprowadzającym. Jeśli masz pytania, sprawdź, czy nie ma na nie odpowiedzi w artykule z najczęstszymi pytaniami. Jeśli masz dodatkowe pytania, zadaj je na Stack Overflow, używając tagu web-audio.
Zanim się pożegnam, pokażę Ci kilka świetnych zastosowań interfejsu WebAudio API w rzeczywistych grach:
- Field Runners oraz opis niektórych szczegółów technicznych.
- Angry Birds, który niedawno przeszedł na Web Audio API. Więcej informacji znajdziesz w tym artykule.
- Skid Racer, który świetnie wykorzystuje dźwięk przestrzenny.