Tworzenie ścieżki dźwiękowej gry za pomocą interfejsu Web Audio API

Wstęp

Ważną częścią tego, co sprawia, że multimedia są tak atrakcyjne, jest dźwięk. Jeśli zdarzyło Ci się oglądać film z wyłączonym dźwiękiem, pewnie udało Ci się to zauważyć.

Gry nie są wyjątkiem. Moje najwspanialsze wspomnienia z gier wideo to muzyka i efekty dźwiękowe. Obecnie, po niemal 20 latach od zagrania w ulubione utwory, nadal nie potrafię przyzwyczaić się do kompozycji zespołu Koji Kondo Zeldy i nastrojowej ścieżki dźwiękowej Matta Uelmena. Tak samo chwytliwe jest to, co grają efekty dźwiękowe, takie jak natychmiastowo rozpoznawalne odpowiedzi na kliknięcia jednostek z Warcrafta czy fragmenty z klasycznych gier Nintendo.

Dźwięk z gry niesie ze sobą ciekawe wyzwania. Aby stworzyć przekonującą muzykę w grze, projektanci muszą dostosować się do potencjalnie nieprzewidywalnego stanu gry, w którym się znajdują. W praktyce fragmenty gry mogą trwać przez nieokreślony czas, dźwięki mogą wchodzić w interakcje z otoczeniem i mieszać je w złożone sposoby, takie jak efekty otoczenia czy względne położenie dźwięku. Jednocześnie może być odtwarzana duża liczba dźwięków, z których wszystkie muszą brzmieć dobrze razem i wyrenderować bez ponoszenia konsekwencji za wydajność.

Dźwięk z gry w internecie

W prostych grach wystarczy użyć tagu <audio>. Wiele przeglądarek ma jednak słabe implementacje, co powoduje zakłócenia w dźwięku i duże opóźnienia. Być może jest to chwilowy problem, ponieważ dostawcy intensywnie pracują nad usprawnieniem swoich implementacji. Stan tagu <audio> znajdziesz w przydatnym pakiecie testowym na stronie areweplayingyet.org.

Bardziej szczegółowo przyjrzymy się jednak specyfikacji tagu <audio>, ale okazuje się, że jest wiele rzeczy, których po prostu nie da się za jego pomocą zrobić, co nie jest zaskakujące, ponieważ został on zaprojektowany z myślą o odtwarzaniu multimediów. Oto niektóre ograniczenia:

  • Brak możliwości zastosowania filtrów do sygnału dźwiękowego
  • Brak możliwości dostępu do nieprzetworzonych danych PCM
  • Brak informacji o położeniu i kierunku źródeł i słuchaczy
  • Bez precyzyjnego harmonogramu.

W pozostałej części artykułu omówię niektóre z nich w kontekście materiałów dźwiękowych z gry napisanych za pomocą interfejsu Web Audio API. Krótkie wprowadzenie do tego interfejsu API znajdziesz w samouczku dla początkujących.

Podkład muzyczny

Gry często mają w tle muzykę w pętli.

Krótka i przewidywalna pętla może być bardzo irytująca. Jeśli odtwarzacz utknie w jakimś obszarze lub na określonym poziomie, a ta sama próbka będzie ciągle odtwarzana w tle, warto stopniowo ściemnić ścieżkę, aby zapobiec dalszemu zniechęcaniu użytkownika. Inna strategia to stosowanie miksów o różnej intensywności, które stopniowo ze sobą przenikają się w zależności od kontekstu gry.

Jeśli np. Twój gracz znajduje się w strefie, w której toczy się epicka bitwa z bossem, możesz mieć do wyboru kilka miksów o różnym poziomie emocji – od klimatycznych, przez zapowiedź i intensywnych. Oprogramowanie do syntezy muzyki często pozwala wyeksportować kilka miksów (o tej samej długości) na podstawie utworu, wybierając zestaw ścieżek do użycia w eksporcie. W ten sposób uzyskasz wewnętrzną spójność i unikniesz niepotrzebnych przejść przy przechodzeniu z jednej ścieżki do drugiej.

Zespół garażowy

Następnie, korzystając z interfejsu Web Audio API, możesz zaimportować wszystkie te próbki za pomocą np. klasy BufferLoader w XHR. Szczegółowo opisujemy to we wprowadzeniu do interfejsu Web Audio API. Wczytywanie dźwięków wymaga czasu, więc zasoby używane w grze powinny być wczytywane podczas wczytywania strony, na początku poziomu lub stopniowo.

Następnie utworzysz źródło dla każdego węzła i węzeł wzmocnienia dla każdego źródła, a następnie połączysz wykres.

Po wykonaniu tej czynności możesz odtwarzać wszystkie te źródła jednocześnie w pętli, a ponieważ są one takiej samej długości, interfejs Web Audio API zagwarantuje, że pozostaną one wyrównane. W miarę zbliżania się do ostatecznej bitwy z bossem wartości zysku w poszczególnych węzłach w łańcuchu może być różna wartość zysku w grze, korzystając z algorytmu o wartości zdobycia podobnego do tego:

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

Przy zastosowaniu powyższego podejścia 2 źródła grają równocześnie i przenikamy między nimi, używając krzywych mocy jednakowych (jak opisano we wprowadzeniu).

Obecnie wielu deweloperów gier używa tagu <audio> jako muzyki w tle, ponieważ nadaje się on do strumieniowego przesyłania treści. Teraz możesz przenieść treści z tagu <audio> do kontekstu Web Audio.

Ta technika może być przydatna, ponieważ tag <audio> działa ze strumieniowym przesyłaniem treści, co pozwala od razu odtworzyć muzykę w tle, zamiast czekać, aż zostanie pobrana. Przesyłając strumień do interfejsu Web Audio API, możesz nim modyfikować i analizować. 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 szczegółów na temat 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 odpowiedzi na działania użytkownika lub zmiany stanu gry. Podobnie jak muzyka w tle, efekty dźwiękowe szybko stają się irytujące. Aby tego uniknąć, warto mieć pulę podobnych, ale odmiennych dźwięków. Może to być drobna różnica w przykładach kroków lub gwałtowne zmiany, jak w serii Warcraft w odpowiedzi na kliknięcie jednostki.

Inną kluczową cechą efektów dźwiękowych w grach jest to, że może być ich wiele jednocześnie. Wyobraź sobie, że jesteś w trakcie strzelaniny, w której wielu aktorów strzela z karabinów maszynowych. Każdy karabin maszynowy strzela kilka razy na sekundę, co powoduje odtwarzanie dziesiątek efektów dźwiękowych w tym samym czasie. Odtwarzanie dźwięku z wielu, precyzyjnie kierowanych źródeł jednocześnie to jedno z największych zalet Web Audio API.

W poniższym przykładzie stworzyliśmy pocisk z kilku próbek pocisków, tworząc wiele źródeł dźwięku, których odtwarzanie jest rozłożone 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 Twojej grze brzmiały dokładnie tak, to byłoby nudne. Oczywiście dźwięki te będą się różnić w zależności od odległości od miejsca docelowego i położenia względnego (więcej o tym później), ale nawet to może nie wystarczyć. Na szczęście interfejs Web Audio API pozwala łatwo poprawić ten przykład na 2 sposoby:

  1. Z subtelną zmianą czasu między wystrzeliwaniem pocisków
  2. Zmieniając szybkość odtwarzania każdej próbki (i zmieniając tonację), by lepiej symulować losowość w świecie rzeczywistym.

Aby zobaczyć bardziej autentyczny przykład zastosowania tych technik w praktyce, obejrzyj prezentację tabeli bilardowej, która wykorzystuje próbkowanie losowe i zmienia współczynnik odtwarzania, aby uzyskać ciekawszy dźwięk stuknięcia piłki.

Dźwięk pozycyjny 3D

Gry tego typu często są osadzone w świecie z pewnymi właściwościami geometrycznymi, czy to w 2D, czy w 3D. W takim przypadku umiejscowienie dźwięku stereo może znacznie zwiększyć wrażenia z oglądania. Na szczęście interfejs Web Audio API ma wbudowane funkcje dźwięku pozycyjnego z akceleracją sprzętową, których używanie jest całkiem łatwe. Przy okazji: sprawdź, czy masz głośniki stereo (najlepiej słuchawki), żeby poniższy przykład miał sens.

W przykładzie powyżej na środku obszaru roboczego znajduje się słuchacz (ikona osoby), a mysz określa pozycję źródła (ikony głośnika). To jest prosty przykład użycia AudioPannerNode do uzyskania tego typu efektu. Podstawowy założenie przykładu powyżej polega na reagowaniu na ruch kursora myszy przez ustawienie pozycji źródła dźwięku w następujący 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 przestrzennym traktowaniu przez Web Audio:

  • Detektor znajduje się domyślnie w punkcie początkowym (0, 0, 0).
  • Interfejsy API pozycjonowania Web Audio nie mają jednostek, więc wprowadziłem mnożnik, który sprawia, że prezentacja brzmi lepiej.
  • Web Audio wykorzystuje współrzędne kartezjańskie „y-is-up” (odwrotność niż w przypadku większości komputerowych systemów graficznych). Dlatego w powyższym fragmencie zamieniam oś Y.

Zaawansowane: pachołki dźwięku

Model pozycjonujący jest bardzo zaawansowany i dobrze zaawansowany, głównie oparty na OpenAL. Więcej informacji znajdziesz w sekcjach 3 i 4 specyfikacji, do której prowadzi link powyżej.

Model pozycji

Do kontekstu interfejsu Web Audio API jest dołączony pojedynczy element AudioListener, który można skonfigurować w przestrzeni według pozycji i orientacji. Każde źródło można przekazać przez węzeł AudioPannerNode, który przestrzenny dźwięk wejściowy. Węzeł panoramiczny ma położenie i orientację, a także model odległości i kierunku.

Model odległości określa poziom wzmocnienia w zależności od odległości od źródła, natomiast model kierunkowy można skonfigurować, określając stożek wewnętrzny i zewnętrzny, które określają wielkość (zwykle ujemnego) wzmocnienia, jeśli słuchacz znajduje się w wewnętrznym stożku, między stożkiem zewnętrznym lub na zewnątrz stożka.

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 uogólnia do trzeciego wymiaru. Przykład dźwięku przestrzennego w 3D znajdziesz w tej sekcji. Oprócz położenia model dźwięku Web Audio może też uwzględniać prędkość w przypadku przesunięć dopplera. Ten przykład bardziej szczegółowo pokazuje efekt Dopplera.

Więcej informacji na ten temat znajdziesz w tym szczegółowym samouczku [łączenie pozycyjnego dźwięku i Webgl].

Efekty dla pokoi i filtry

W rzeczywistości sposób, w jaki dźwięk jest odbierany, w dużej mierze zależy od pomieszczenia, w którym się on pojawia. W piwnicy skrzypienie drzwi będzie brzmieć zupełnie inaczej niż w dużym, otwartym korytarzu. W grach o dużej wartości produkcyjnej trzeba będzie naśladować te efekty, ponieważ stworzenie osobnego zestawu próbek dla każdego środowiska jest bardzo kosztowne i prowadzi do zbudowania jeszcze większej ilości zasobów i ilości danych gry.

Ogólnie rzecz biorąc, termin audio oznaczający różnicę między nieprzetworzonym dźwiękiem a tym, jak brzmi w rzeczywistości, to reakcja na impulsy. Takie spontaniczne reakcje mogą być trudne do rejestrowania i dla Twojej wygody istnieją witryny, w których dla Twojej wygody przechowywane są nagrane wcześniej pliki odpowiedzi impulsowej (zapisane jako dźwięk).

Więcej informacji o tworzeniu odpowiedzi impulsowych w danym środowisku znajdziesz w sekcji „Konfiguracja nagrywania” w części Convolution specyfikacji interfejsu Web Audio API.

Co ważniejsze do naszych celów, interfejs Web Audio API zapewnia prosty sposób stosowania impulsowych reakcji do dźwięków za pomocą 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);

Zapoznaj się też z prezentacją efektów pomieszczeń na stronie specyfikacji interfejsu Web Audio API oraz tym przykładem, który pokazuje, jak mieszać na sucho (surowo) i mokry (przetworzone za pomocą konwolwersu) świetnego miksu jazzowego.

Ostatnie odliczanie

Masz już grę, masz skonfigurowany dźwięk pozycyjny, a na wykresie jest wiele węzłów audio, które są odtwarzane w tym samym czasie. Świetnie, ale musisz jeszcze rozważyć jedną rzecz:

Kilka dźwięków po prostu nakłada się na siebie i nie ma normalizacji. Może się zdarzyć, że przekroczysz próg możliwości głośnika. Podobnie jak obrazy przekraczające granice obszaru roboczego, dźwięki również mogą być przycinane, jeśli fala przekracza maksymalny próg, co powoduje wyraźne zniekształcenie. Fala wygląda mniej więcej tak:

Klip

Oto rzeczywisty przykład klipów w praktyce. Krzywa wygląda źle:

Klip

Ważne, by wsłuchiwać się w ostre zniekształcenia, takie jak te powyżej, lub na odwrót: miksy stonowane, które zmuszają słuchaczy do zwiększenia głośności. Jeśli jesteś w takiej sytuacji, naprawdę musisz rozwiązać ten problem.

Wykryj przycinanie

Z punktu widzenia kwestii technicznych, przycinanie ma miejsce, gdy wartość sygnału w dowolnym kanale przekracza prawidłowy zakres, czyli od -1 do 1. Gdy zostanie wykryty, warto poinformować nas w sposób graficzny o problemie. Aby to zrobić, umieść w wykresie JavaScriptAudioNode. Wykres audio będzie miał taką konfigurację:

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

Przycinanie można wykryć w tym module obsługi 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 nie należy nadużywać polecenia JavaScriptAudioNode ze względu na wydajność. W tym przypadku alternatywna implementacja pomiaru danych może sondować RealtimeAnalyserNode na wykresie audio dla danych getByteFrequencyData w czasie renderowania, zgodnie z wartością requestAnimationFrame. Ta metoda jest bardziej wydajna, ale nie zawiera większości sygnału (w tym miejsc, w których może być klip), ponieważ renderowanie odbywa się najwyżej 60 razy na sekundę, a sygnał audio zmienia się znacznie szybciej.

Wykrywanie klipów jest bardzo ważne, więc w przyszłości prawdopodobnie zobaczymy wbudowany węzeł MeterNode Web Audio API.

Zapobiegaj przycinaniu

Dostosowując wzmocnienie w głównym węźle AudioGainNode, możesz dostosować swoją składankę do poziomu, który nie pozwoli na przycinanie. W praktyce jednak dźwięk odtwarzany w grze może zależeć od wielu czynników, dlatego trudno jest określić wartość wzmocnienia głównego, która uniemożliwi przycinanie we wszystkich stanach. Zwykle należy modyfikować zyski, aby przewidzieć najgorszy przypadek, ale to więcej niż nauka.

Dodaj trochę cukru

Kompresory są powszechnie stosowane w produkcji muzyki i gier, aby wygładzić sygnał i kontrolować jego skokowe skoki. Ta funkcja jest dostępna w świecie Web Audio za pomocą interfejsu DynamicsCompressorNode, który możesz wstawić do wykresu audio, aby uzyskać głośniejszy, głębszy i bardziej pełnowymiarowy dźwięk oraz pomóc w przycinaniu. Bezpośrednio cytując specyfikację, ten węzeł

Kompresja dynamiki jest zwykle dobrym pomysłem, zwłaszcza w przypadku gier, w których, jak wspominaliśmy wcześniej, nie wiadomo dokładnie, jakie dźwięki i kiedy będą odtwarzane. Świetnym przykładem jest Plink z modułów DinahMoe, ponieważ odtwarzane dźwięki w pełni zależą od Ciebie i innych uczestników. Kompresor przydaje się w większości przypadków, z wyjątkiem tych rzadkich, gdy masz do czynienia z misternie zdolnymi utworami, które zostały już dostrojone.

Aby to zrobić, wystarczy umieścić na wykresie audio element DynamicsCompressorNode, zwykle jako ostatni węzeł przed miejscem 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 na temat kompresji dynamiki znajdziesz w tym artykule w Wikipedii.

Podsumowując, zachowaj ostrożność podczas przycinania i zapobiegaj mu, wstawiając główny węzeł wzmocnienia. Następnie doprecyzuj cały miks, korzystając z węzła sprężarki dynamicznej. Wykres audio może wyglądać mniej więcej tak:

Wynik końcowy

Podsumowanie

Omawiamy tu, jak moim zdaniem, najważniejsze aspekty tworzenia dźwięku w grze z użyciem interfejsu Web Audio API. Dzięki tym technikom możesz tworzyć naprawdę atrakcyjne ścieżki dźwiękowe bezpośrednio w przeglądarce. Zanim się wyniosę, przyda Ci się wskazówka dotycząca konkretnej przeglądarki: pamiętaj, aby wstrzymać odtwarzanie dźwięku, jeśli karta pracuje w tle, korzystając z interfejsu page visibility API. W przeciwnym razie będzie to frustrujące dla użytkowników.

Dodatkowe informacje o Web Audio znajdziesz w tym artykule wprowadzającym. Jeśli masz jakieś pytania, sprawdź, czy nie ma na nie odpowiedzi w Najczęstszych pytaniach dotyczących audio w internecie. Jeśli masz dodatkowe pytania, zadaj je na Stack Overflow, używając tagu web-audio.

Zanim się podpisze, zachęcam do zapoznania się z wykorzystaniem interfejsu WebAudio API w prawdziwych grach.