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

Wprowadzenie

Dźwięk ma istotny wpływ na to, co sprawia, że korzystanie z multimediów jest 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. Moje najwspanialsze wspomnienia z gier wideo to muzyka 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 natychmiastowo 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ć przekonującą muzykę, projektanci muszą dostosować się do potencjalnie nieprzewidywalnego stanu gry. 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. W końcu 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 z gier w internecie

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 oczekiwania. 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ę jednak 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 szczegółowego harmonogramu.

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.

Jeśli Twoja pętla jest krótka i przewidywalna, bywa to denerwujące. 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.

Opaska garażowa

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.

Po wykonaniu tych czynności możesz odtwarzać w pętli wszystkie te źródła jednocześnie, a ponieważ mają one tę samą długość, interfejs Web Audio API zagwarantuje, że zachowają zgodność. W miarę jak postać jest zbliżona do ostatniej bitwy z bossem lub od niego oddalona, gra może zmieniać wartości wzmocnienia w każdym z węzłów w łańcuchu, korzystając z algorytmu dotyczącego wielkości zysku:

// 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 powyższym podejściu 2 źródła występują jednocześnie i przenikamy między nimi przy użyciu jednakowych krzywych mocy (jak opisano we wprowadzeniu).

Wielu deweloperów gier używa obecnie tagu <audio> do tworzenia muzyki w tle, ponieważ jest on dobrym rozwiązaniem 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 integrowaniu 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 dane wejściowe użytkownika lub zmiany stanu gry. Jednak podobnie jak muzyka w tle, efekty dźwiękowe mogą bardzo szybko stać się irytujące. Aby tego uniknąć, warto mieć pulę podobnych, ale różnych dźwięków. Mogą to być niewielkie, niewielkie odmiany kroków po gwałtowne różnice, jak widać w serii o Warcraft, w odpowiedzi na klikanie jednostek.

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 trakcie strzelaniny, a kilka aktorów strzela do 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. Jednoczesne odtwarzanie dźwięku z wielu źródeł o precyzyjnym czasie to jedna z najbardziej przydatnych funkcji Web Audio API.

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 różniłyby się one w zależności od dźwięku na podstawie odległości od celu i względnej pozycji (więcej na ten temat wkrótce), 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:

  1. z delikatną zmianą czasu między strzałami;
  2. 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.

Bardziej realistyczny przykład takich technik w praktyce znajdziesz w wersji demonstracyjnej stołu do puli, która wykorzystuje próbkowanie losowe i zróżnicowaną częstotliwość odtwarzania, aby uzyskać bardziej ciekawy dźwięk zderzeń piłki.

Dźwięk przestrzenny 3D

Gry są często osadzone w świecie z pewnymi właściwościami geometrycznymi, 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 ma wbudowane, wspierane sprzętowo funkcje pozycjonowania dźwięku, dzięki którym korzystanie z nich jest proste. Aby zrozumieć ten przykład, musisz mieć głośniki stereo (najlepiej słuchawki).

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 traktowaniu przestrzenności w Web Audio:

  • Odbiornik znajduje się domyślnie w początku układu współrzędnych (0, 0, 0).
  • Interfejsy API do pozycjonowania Web Audio nie mają jednostek, dlatego wprowadziłem mnożnik, aby poprawić brzmienie wersji demonstracyjnej.
  • 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 pozycjonowania jest bardzo zaawansowany i dość zaawansowany, głównie oparty na OpenAL. Więcej informacji znajdziesz w sekcji 3 i 4 specyfikacji, do której link znajduje się powyżej.

Model uwzględniający pozycję

Do kontekstu Web Audio API jest dołączony jeden 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 położenie 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 zbliżeniu.

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 w piwnicy będą brzmieć inaczej niż w dużym otwartym holu. W przypadku gier o wysokiej wartości produkcyjnej te efekty powinny być takie same, ponieważ utworzenie osobnego zestawu próbek dla każdego środowiska jest niezwykle kosztowne i prowadziłoby do powstania jeszcze większej ilości zasobów oraz danych w grze.

Ogólnie rzecz biorąc, różnica między dźwiękiem surowym a dźwiękiem w rzeczywistości to impuls. Te reakcje impulsów można żmudnie rejestrować, a dla wygody użytkowników istnieją witryny, które hostują wiele z tych wcześniej nagranych plików odpowiedzi impulsowych (zapisanych jako pliki audio).

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 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 przez konwolwer) dźwięku w ramach standardu jazzowego.

Końcowe odliczanie

Masz już grę, skonfigurujesz dźwięk pozycjonujący i masz na wykresie dużą liczbę węzłów AudioNodes, które są odtwarzane w tym samym czasie. Ś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:

Klips

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

Przycinanie

Ważne jest, aby posłuchać zniekształconego dźwięku, takiego jak ten powyżej, lub odwrotnie – zbyt przytłumionego miksu, który zmusza słuchaczy do zwiększenia głośności. Jeśli tak się dzieje, musisz to naprawić.

Wykrywanie przycinania

Z technicznego punktu widzenia przycinanie ma miejsce, gdy wartość sygnału w dowolnym kanale przekracza prawidłowy zakres, czyli od -1 do 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. Wykres audio 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ć elementu JavaScriptAudioNode ze względu na wydajność. W tym przypadku alternatywna implementacja pomiaru mogłaby odczytywać wartość RealtimeAnalyserNode w grafice 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ę, a sygnał audio zmienia się znacznie szybciej.

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

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 zapobiegnie 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ą powszechnie używane przy produkcji muzyki i gier, aby wygładzić i kontrolować skoki sygnału. Ta funkcja jest dostępna w środowisku Web Audio w narzędziu DynamicsCompressorNode, który można wstawić do wykresu audio, aby uzyskać głośniejszy, pełniejszy i pełniejszy dźwięk oraz ułatwić tworzenie klipów. 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 rzadkich przypadków, gdy mamy do czynienia z bardzo wymagającymi utworami, które zostały już tak dobrane, by brzmieć jak należy.

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 dynamiki znajdziesz w tym artykule w Wikipedii.

Podsumowując, uważnie poczekaj na przycięcie i zablokuj je, wstawiając węzeł nadrzędny. Następnie zaciśnij cały miks, używając węzła kompresora dynamiki. Wykres audio może wyglądać mniej więcej tak:

Wynik końcowy

Podsumowanie

Myślę, że są to najważniejsze aspekty tworzenia gier audio 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 Web Audio znajdziesz w tym artykule dla początkujących. Jeśli masz jakieś pytanie, poszukaj odpowiedzi w najczęstszych pytaniach na temat audio w internecie. 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: