Dinamice, coloane sonore secvențiale pentru jocuri

În acest tutorial vom analiza o tehnică de construire și secvențiere a muzicii dinamice pentru jocuri. Construcția și secvențierea se întâmplă la timpul de execuție, permițând dezvoltatorilor de jocuri să modifice structura muzicii pentru a reflecta ceea ce se întâmplă în lumea jocurilor.

Înainte de a sari în detaliile tehnice, poate doriți să aruncați o privire la o demonstrație de lucru a acestei tehnici în acțiune. Muzica din demonstrație este construită dintr-o colecție de blocuri individuale de audio care sunt secvențiate și amestecate împreună la timpul de execuție pentru a forma piesa muzicală completă.

Faceți clic pentru a vizualiza demonstrația.

Această demonstrație necesită un browser web care acceptă W3C Web Audio API și audio OGG. Google Chrome este cel mai bun browser pe care îl puteți utiliza pentru a vedea această demonstrație, dar poate fi folosit și Firefox Aurora.

Dacă nu puteți vedea demo-ul de mai sus în browserul dvs., puteți urmări acest videoclip YouTube:



Prezentare generală

Modul în care această tehnică funcționează este destul de simplă, dar are potențialul de a adăuga o muzică dinamică foarte drăguță jocurilor dacă este folosită creativ. De asemenea, permite crearea de piese muzicale de lungă durată dintr-un fișier audio relativ mic.

Muzica originală este, în esență, deconstruită într-o colecție de blocuri, fiecare dintre ele având o singură bară, iar blocurile respective sunt stocate într-un singur fișier audio. Sequencerul de muzică încarcă fișierul audio și extrage mostrele audio brute necesare pentru a reconstrui muzica. Structura muzicii este dictată de o colecție de tablouri mutabile care îi spun secvențiatorului când să joace blocurile de muzică.

Vă puteți gândi la această tehnică ca o versiune simplificată a software-ului de secvențiere, cum ar fi Reason, FL Studio sau Dance EJay. Vă puteți gândi și la această tehnică ca echivalent muzical al cărămizilor Lego.


Structura fișierului audio

După cum am menționat anterior, sequencerul muzicii necesită deconstrucția muzicii originale într-o colecție de blocuri, iar aceste blocuri trebuie să fie stocate într-un fișier audio.

Această imagine demonstrează cum ar putea fi stocate blocurile într-un fișier audio.

În această imagine puteți vedea că există cinci blocuri individuale stocate în fișierul audio, iar toate blocurile sunt de aceeași lungime. Pentru a păstra lucrurile simple pentru acest tutorial, blocurile sunt cu totul lungime.

Ordinea blocurilor din fișierul audio este importantă deoarece dictează ce canale de secvențiere sunt atribuite blocurilor. Primul bloc (de exemplu, tobe) va fi alocat primului canal de secvență, cel de-al doilea bloc (de exemplu percuția) va fi alocat celui de-al doilea canal de secvență și așa mai departe.


Canale de secvență

Un canal de secvență reprezintă un rând de blocuri și conține steaguri (câte unul pentru fiecare bara de muzică) care indică dacă trebuie să fie redat blocul atribuit canalului. Fiecare pavilion este o valoare numerică și este fie zero (nu joacă blocul), fie unul (joacă blocul).

Această imagine demonstrează relația dintre blocuri și canalele secvențiatorului.

Numerele aliniate orizontal de-a lungul fundului imaginii de mai sus reprezintă numerele de bare. După cum puteți vedea, în prima bară de muzică (01) se va juca numai blocul de chitară, dar în a cincea bară (05) se vor juca blocurile de Tobe, Percuție, Bas și Chitară.


Programare

În acest tutorial nu vom trece prin codul unui sequencer complet de muzică; în schimb, vom examina codul de bază necesar pentru a obține un simplu sequencer de muzică. Codul va fi prezentat ca pseudo-cod pentru a păstra lucrurile cât mai agnostic.

Înainte de a începe, trebuie să țineți cont de limbajul de programare pe care decideți în cele din urmă să îl folosiți va necesita un API care vă permite să manipulați audio la un nivel scăzut. Un bun exemplu este API-ul Web Audio disponibil în JavaScript.

De asemenea, puteți descărca fișierele sursă atașate acestui tutorial pentru a studia o implementare JavaScript a unui sequencer muzical de bază care a fost creat ca o demonstrație pentru acest tutorial.

Recapitulare rapidă

Avem un singur fișier audio care conține blocuri de muzică. Fiecare bloc de muzică are o bară în lungime, iar ordinea blocurilor din fișierul audio dictează canalul de secvență pe care sunt alocate blocurile.

constante

Există două informații pe care le vom avea nevoie înainte de a le putea continua. Trebuie să știm tempo-ul muzicii, în bătăi pe minut, și numărul de bătăi în fiecare bar. Acesta din urmă poate fi considerat ca semn de timp al muzicii. Această informație ar trebui să fie stocată ca valori constante, deoarece nu se modifică în timp ce secvențele de muzică rulează.

 TEMPO = 100 // bătăi pe minut SIGNATURE = 4 // bătăi pe bar

De asemenea, trebuie să cunoaștem rata de eșantionare utilizată de API-ul audio. În mod obișnuit, acest lucru va fi de 44100 Hz, deoarece este perfect pentru audio, dar unii oameni au hardware-ul configurat să utilizeze o rată de eșantionare mai mare. API-ul audio pe care îl alegeți să îl utilizați ar trebui să furnizeze aceste informații, dar în scopul acestui tutorial vom presupune că rata de eșantionare este de 44100 Hz.

 SAMPLE_RATE = 44100 // Hertz

Acum putem calcula lungimea eșantionului unei bare de muzică - adică numărul de eșantioane audio dintr-un bloc de muzică. Această valoare este importantă deoarece permite sequencerului de muzică să găsească blocurile individuale de muzică și probele audio din fiecare bloc în fișierele audio.

 BLOCK_SIZE = etaj (SAMPLE_RATE * (60 / (TEMPO / SIGNATURE)))

Fluxuri audio

API-ul audio pe care îl alegeți va dicta modul în care vor fi reprezentate în codul dvs. fluxurile audio (matrice de eșantioane audio). De exemplu, API-ul Web Audio utilizează obiecte AudioBuffer.

Pentru acest tutorial vor exista două fluxuri audio. Primul flux audio va fi citit și va conține toate probele audio încărcate din fișierul audio care conține blocurile muzicale, acesta fiind fluxul audio "de intrare".

Al doilea flux audio va fi numai pentru scriere și va fi folosit pentru a împinge materialele audio la hardware; acesta este fluxul audio "ieșire". Fiecare dintre aceste fluxuri va fi reprezentată ca o matrice unidimensională.

 intrare = [...] ieșire = [...]

Procesul exact necesar pentru încărcarea fișierului audio și extragerea probelor audio din fișier va fi dictat de limba de programare pe care o utilizați. În acest sens, vom presupune intrare setul de flux audio conține deja mostre audio extrase din fișierul audio.

producție fluxul audio va avea, de obicei, o lungime fixă, deoarece majoritatea API-urilor audio vă vor permite să alegeți frecvența la care probele audio trebuie procesate și trimise pe hardware - adică, cât de des Actualizați funcția este invocată. Frecvența este în mod normal legată direct de latența audio-ului, frecvențele înalte vor necesita mai multă putere procesoarelor, dar vor duce la latențe mai mici și viceversa.

Date secvențiale

Datele secvențiatorului sunt o matrice multidimensională; fiecare sub-matrice reprezintă un canal de secvențiator și conține steaguri (câte unul pentru fiecare bara de muzică) care indică dacă ar trebui să fie jucat sau nu blocul muzical atribuit canalului. Lungimea magistralelor de canale dictează și lungimea muzicii.

 canale = [[0,0,0,0, 0,0,0,0, 1,1,1,1, 1,1,1,1], // tobe [0,0,0,0, 1 , 1,1,1, 1,1,1,1, 1,1,1,1], // percuție [0,0,0,0, 0,0,0,0, 1,1,1, 1,1,1,1], // bass [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1], // chitara [0,0,0,0, 0,0,1,1, 0,0,0,0, 0,0,1,1] // siruri de caractere]

Datele pe care le vedeți acolo reprezintă o structură de muzică lungă de șaisprezece bară. Conține cinci canale, câte unul pentru fiecare bloc de muzică din fișierul audio, iar canalele sunt în aceeași ordine ca și blocurile de muzică din fișierul audio. Steagurile din rețelele de canale ne informează dacă blocul alocat canalelor ar trebui să fie jucat sau nu: valoarea 0 înseamnă că un bloc nu va fi redat; valoarea 1 înseamnă că un bloc va fi redat.

Această structură de date este mutabilă, poate fi modificată oricând, chiar și atunci când se execută secvențele de muzică, ceea ce vă permite să modificați drapelele și structura muzicii pentru a reflecta ce se întâmplă într-un joc.

Prelucrarea audio

Cele mai multe aplicații API audio vor difuza un eveniment la o funcție de gestionare a evenimentelor sau vor invoca o funcție direct, atunci când trebuie să împingă mai multe probe audio pe hardware. Această funcție este, de obicei, invocată în mod constant ca bucla principală de actualizare a unui joc, dar nu la fel de frecvent, astfel încât timpul ar trebui să fie cheltuit pentru optimizarea acestuia.

Practic, ceea ce se întâmplă în această funcție este:

  1. Probele audio multiple sunt scoase din intrare fluxul audio.
  2. Aceste probe sunt adăugate împreună pentru a forma o singură probă audio.
  3. Eșantionul audio este împins în producție fluxul audio.

Înainte de a ajunge la curajul funcției, trebuie să definim câteva variabile în cod:

 play = true // indică dacă muzica (secvențiatorul) joacă poziția = 0 // poziția capului de redare a secvențiatorului, în eșantioane

joc Boolean ne permite să știm dacă se joacă muzica; dacă nu se joacă, trebuie să împingem eșantioane audio silențioase în producție fluxul audio. poziţie ține evidența locului unde se află piesa de joacă, așa că este un pic cam ca un scruber pe o muzică sau un player video tipic.

Acum, pentru curajul funcției:

 function () outputIndex = 0 outputCount = output.length daca (play == false) // Probele silentioase trebuie sa fie impinse in fluxul de iesire in timp ce (outputIndex < outputCount )  output[ outputIndex++ ] = 0.0  // the remainder of the function should not be executed return  chnCount = channels.length // the length of the music, in samples musicLength = BLOCK_SIZE * channels[ 0 ].length while( outputIndex < outputCount )  chnIndex = 0 // the bar of music that the sequencer playhead is pointing at barIndex = floor( position / BLOCK_SIZE ) // set the output sample value to zero (silent) output[ outputIndex ] = 0.0 while( chnIndex < chnCount )  // check the channel flag to see if the block should be played if( channels[ chnIndex ][ barIndex ] == 1 )  // the position of the block in the "input" stream inputOffset = BLOCK_SIZE * chnIndex // index into the "input" stream inputIndex = inputOffset + ( position % BLOCK_SIZE ) // add the block sample to the output sample output[ outputIndex ] += input[ inputIndex ]  chnIndex++  // advance the playhead position position++ if( position >= musicLength) // resetați poziția capului de joc pentru a încrucișa poziția muzicii = 0 outputIndex ++

După cum puteți vedea, codul necesar pentru procesarea probelor audio este destul de simplu, dar deoarece acest cod va fi rulat de mai multe ori pe secundă, ar trebui să căutați modalități de a optimiza codul în cadrul funcției și de a calcula cât mai multe valori posibile. Optimizările pe care le puteți aplica codului depind exclusiv de limba de programare pe care o utilizați.

Nu uitați că puteți descărca fișierele sursă atașate acestui tutorial dacă doriți să vă uitați la o modalitate de a implementa un sequencer de bază de muzică în JavaScript utilizând Web Audio API.


notițe

Formatul fișierului audio pe care îl utilizați trebuie să permită ca audio să se încrucișeze fără probleme. Cu alte cuvinte, codificatorul folosit pentru a genera fișierul audio nu ar trebui să injecteze nici un fel de umplutură (bucăți silențioase de sunet) în fișierul audio. Din păcate, fișierele MP3 și MP4 nu pot fi folosite din acest motiv. Fișierele OGG (utilizate de demonstrația JavaScript) pot fi utilizate. De asemenea, puteți utiliza fișiere WAV dacă doriți, dar acestea nu sunt o alegere sensibilă pentru jocuri sau aplicații web bazate pe dimensiunea lor.

Dacă programați un joc și dacă limba de programare pe care o utilizați pentru joc acceptă concurrency (fire sau lucrători), atunci este posibil să doriți să luați în considerare rularea codului de procesare audio în firul sau lucrătorul propriu dacă este posibil. Acest lucru va ușura bucla de actualizare principală a jocului de orice tip de procesare audio care ar putea apărea.


Muzică dinamică în jocuri populare

Următoarea este o mică selecție de jocuri populare care profită de muzica dinamică într-un fel sau altul. Implementarea acestor jocuri pentru muzica lor dinamică poate varia, dar rezultatul final este același: jucătorii jocului au o experiență de joc mai plină de experiență.

  • Călătorie: thatgamecompany.com
  • Flori: thatgamecompany.com
  • LittleBigPlanet: littlebigplanet.com
  • Portal 2: thinkwithportals.com
  • PixelJunk Shooter: pixeljunk.jp
  • Red Dead Redemption: rockstargames.com
  • Necunoscut: naughtydog.com

Concluzie

Deci, te duci - o implementare simplă a muzicii secvențiale dinamice care poate spori natura emotivă a unui joc. Cum decideți să utilizați această tehnică și cât de complexă devine secvențiatorul, depinde în întregime de dvs. Există o mulțime de direcții pe care această implementare simplă le poate lua și vom acoperi câteva din aceste direcții într-un tutorial viitor.

Dacă aveți întrebări, nu ezitați să le postați în comentariile de mai jos și vă voi întoarce cât mai curând posibil.