Colonne sonore dinamiche e sequenziali per i giochi

In questo tutorial daremo un'occhiata a una tecnica per costruire e sequenziare la musica dinamica per i giochi. La costruzione e il sequenziamento avvengono in fase di esecuzione, consentendo agli sviluppatori di giochi di modificare la struttura della musica per riflettere ciò che accade nel mondo di gioco.

Prima di entrare nei dettagli tecnici, potresti dare un'occhiata a una dimostrazione pratica di questa tecnica in azione. La musica nella dimostrazione è costruita da una collezione di singoli blocchi di audio che vengono sequenziati e mixati in runtime per formare la traccia musicale completa.

Clicca per vedere la demo.

Questa dimostrazione richiede un browser Web che supporti l'API Web Audio W3C e l'audio OGG. Google Chrome è il miglior browser da utilizzare per visualizzare questa dimostrazione, ma è anche possibile utilizzare Firefox Aurora.

Se non riesci a visualizzare la demo sopra nel tuo browser, puoi invece guardare questo video di YouTube:



Panoramica

Il modo in cui questa tecnica funziona è abbastanza semplice, ma ha il potenziale di aggiungere musica dinamica ai giochi se è usata in modo creativo. Consente inoltre di creare brani musicali infinitamente lunghi da un file audio relativamente piccolo.

La musica originale viene essenzialmente decostruita in una serie di blocchi, ognuno dei quali è lungo una barra, e tali blocchi sono memorizzati in un singolo file audio. Il sequencer musicale carica il file audio ed estrae i campioni audio grezzi necessari per ricostruire la musica. La struttura della musica è dettata da una serie di array mutabili che dicono al sequencer quando suonare i blocchi di musica.

Puoi pensare a questa tecnica come una versione semplificata del software di sequencing come Reason, FL Studio o Dance EJay. Puoi anche pensare a questa tecnica come l'equivalente musicale dei mattoncini Lego.


Struttura del file audio

Come accennato in precedenza, il sequencer musicale richiede che la musica originale sia decostruita in una serie di blocchi, e tali blocchi devono essere memorizzati in un file audio.

Questa immagine mostra come i blocchi possono essere memorizzati in un file audio.

In quell'immagine puoi vedere che ci sono cinque blocchi singoli memorizzati nel file audio e tutti i blocchi hanno la stessa lunghezza. Per semplificare le cose per questo tutorial, i blocchi sono tutti una barra lunga.

L'ordine dei blocchi nel file audio è importante perché determina quali canali sequencer sono assegnati ai blocchi. Il primo blocco (ad es. Drums) sarà assegnato al primo canale sequencer, il secondo blocco (ad esempio percussioni) sarà assegnato al secondo canale sequencer, e così via.


Canali del sequencer

Un canale sequencer rappresenta una fila di blocchi e contiene flag (uno per ogni barra della musica) che indicano se il blocco assegnato al canale deve essere riprodotto. Ogni flag è un valore numerico ed è zero (non suonare il blocco) o uno (gioca il blocco).

Questa immagine mostra la relazione tra i blocchi e i canali del sequencer.

I numeri allineati orizzontalmente lungo la parte inferiore dell'immagine sopra rappresentano i numeri delle barre. Come puoi vedere, nella prima battuta della musica (01) verrà riprodotto solo il blocco Guitar, ma nella quinta battuta (05) Verranno riprodotti i blocchi di batteria, percussione, basso e chitarra.


Programmazione

In questo tutorial non passeremo attraverso il codice di un sequencer musicale funzionante; invece, vedremo il codice base richiesto per far girare un semplice sequencer musicale. Il codice verrà presentato come pseudo-codice per mantenere le cose il più possibile indipendenti dalla lingua.

Prima di iniziare, è necessario tenere presente che il linguaggio di programmazione che alla fine decidi di utilizzare richiederà un'API che ti consenta di manipolare l'audio a un livello basso. Un buon esempio di questo è l'API Web Audio disponibile in JavaScript.

Puoi anche scaricare i file sorgente allegati a questo tutorial per studiare un'implementazione JavaScript di un sequencer musicale di base creato come dimostrazione per questo tutorial.

Ricapitolazione rapida

Abbiamo un singolo file audio che contiene blocchi di musica. Ogni blocco musicale è lungo una barra e l'ordine dei blocchi nel file audio determina il canale sequencer a cui i blocchi sono assegnati.

costanti

Ci sono due informazioni che ci serviranno prima di poter procedere. Abbiamo bisogno di conoscere il tempo della musica, in battiti al minuto, e il numero di battiti in ogni battuta. Quest'ultimo può essere pensato come la firma del tempo della musica. Questa informazione dovrebbe essere memorizzata come valori costanti perché non cambia mentre il sequencer musicale è in esecuzione.

 TEMPO = 100 // beats al minuto SIGNATURE = 4 // beats per bar

Dobbiamo anche conoscere la frequenza di campionamento utilizzata dall'API audio. Normalmente questo sarà 44100 Hz, perché è perfettamente adatto per l'audio, ma alcune persone hanno il loro hardware configurato per utilizzare una frequenza di campionamento più elevata. L'API audio che scegli di utilizzare dovrebbe fornire queste informazioni, ma per lo scopo di questo tutorial assumeremo che la frequenza di campionamento sia 44100 Hz.

 SAMPLE_RATE = 44100 // Hertz

Ora possiamo calcolare la lunghezza del campione di una barra di musica, ovvero il numero di campioni audio in un blocco di musica. Questo valore è importante perché consente al sequencer musicale di individuare i singoli blocchi di musica e i campioni audio all'interno di ciascun blocco, nei dati del file audio.

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

Stream audio

L'API audio che decidi di utilizzare stabilirà il modo in cui gli stream audio (array di campioni audio) sono rappresentati nel tuo codice. Ad esempio, l'API Web Audio utilizza oggetti AudioBuffer.

Per questo tutorial ci saranno due flussi audio. Il primo stream audio sarà di sola lettura e conterrà tutti i campioni audio caricati dal file audio contenente i blocchi musicali, questo è il flusso audio "input".

Il secondo stream audio sarà di sola scrittura e verrà utilizzato per inviare campioni audio all'hardware; questo è il flusso audio "output". Ciascuno di questi flussi sarà rappresentato come una matrice monodimensionale.

 input = [...] output = [...]

Il processo esatto richiesto per caricare il file audio ed estrarre i campioni audio dal file sarà dettato dal linguaggio di programmazione che si utilizza. Con questo in mente, assumeremo il ingresso l'array audio stream contiene già i campioni audio estratti dal file audio.

Il produzione il flusso audio sarà di solito una lunghezza fissa perché la maggior parte delle API audio ti consentiranno di scegliere la frequenza con cui i campioni audio devono essere elaborati e inviati all'hardware - cioè, quanto spesso un aggiornare la funzione è invocata. La frequenza è normalmente legata direttamente alla latenza dell'audio, le alte frequenze richiedono più potenza del processore ma risultano in latenze più basse e viceversa.

Dati del sequenziatore

I dati del sequencer sono una matrice multidimensionale; ogni sotto-matrice rappresenta un canale sequencer e contiene flag (uno per ogni barra della musica) che indicano se il blocco musicale assegnato al canale deve essere riprodotto o meno. La lunghezza degli array di canali determina anche la durata della musica.

 canali = [[0,0,0,0, 0,0,0,0, 1,1,1,1, 1,1,1,1], // tamburi [0,0,0,0, 1 , 1,1,1, 1,1,1,1, 1,1,1,1], // percussione [0,0,0,0, 0,0,0,0, 1,1,1, 1, 1,1,1,1], // basso [1,1,1,1, 1,1,1,1, 1,1,1,1, 1,1,1,1], // chitarra [0,0,0,0, 0,0,1,1, 0,0,0,0, 0,0,1,1] // archi]

I dati che vedi qui rappresentano una struttura musicale lunga sedici bar. Contiene cinque canali, uno per ogni blocco di musica nel file audio, e i canali sono nello stesso ordine dei blocchi di musica nel file audio. I flag negli array di canali ci permettono di sapere se il blocco assegnato ai canali deve essere riprodotto o meno: il valore 0 significa che un blocco non verrà riprodotto; il valore 1 significa che verrà riprodotto un blocco.

Questa struttura dati è mutevole, può essere modificata in qualsiasi momento anche quando il sequencer musicale è in esecuzione, e ciò consente di modificare le bandiere e la struttura della musica per riflettere ciò che sta accadendo in un gioco.

Elaborazione audio

La maggior parte delle API audio trasmetteranno un evento a una funzione di gestore eventi o invocheranno direttamente una funzione quando è necessario inviare più campioni audio all'hardware. Questa funzione viene solitamente invocata costantemente come il ciclo di aggiornamento principale di un gioco, ma non così frequentemente, quindi occorre dedicare del tempo a ottimizzarlo.

Fondamentalmente ciò che accade in questa funzione è:

  1. Più campioni audio vengono estratti da ingresso flusso audio.
  2. Questi campioni vengono sommati per formare un singolo campione audio.
  3. Questo campione audio viene inserito nel produzione flusso audio.

Prima di arrivare al coraggio della funzione, dobbiamo definire un paio di altre variabili nel codice:

 playing = true // indica se la musica (il sequencer) sta riproducendo position = 0 // la posizione dell'header del sequencer, in campioni

Il giocando Boolean ci fa semplicemente sapere se la musica sta suonando; se non sta suonando, dobbiamo inserire campioni audio silenziosi nel produzione flusso audio. Il posizione tiene traccia di dove si trova la testina all'interno della musica, quindi è un po 'come uno scrubber su un tipico lettore di musica o video.

Ora per il coraggio della funzione:

 function update () outputIndex = 0 outputCount = output.length if (playing == false) // I campioni silenziosi devono essere inviati al flusso di output mentre (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) // reimposta la posizione della testina per eseguire il loop della posizione musicale = 0 outputIndex ++

Come puoi vedere, il codice richiesto per elaborare i campioni audio è abbastanza semplice, ma poiché questo codice verrà eseguito numerose volte al secondo, dovresti esaminare i modi per ottimizzare il codice all'interno della funzione e precalcolare quanti più valori possibili. Le ottimizzazioni che è possibile applicare al codice dipendono esclusivamente dal linguaggio di programmazione che si utilizza.

Non dimenticare che puoi scaricare i file sorgente allegati a questo tutorial se vuoi vedere un modo per implementare un sequencer musicale di base in JavaScript usando l'API Web Audio.


Gli appunti

Il formato del file audio che utilizzi deve consentire all'audio di scorrere senza interruzioni. In altre parole, il codificatore utilizzato per generare il file audio non deve iniettare alcun padding (frammenti silenziosi di audio) nel file audio. Purtroppo i file MP3 e MP4 non possono essere utilizzati per questo motivo. È possibile utilizzare i file OGG (utilizzati dalla dimostrazione JavaScript). Puoi anche utilizzare i file WAV se lo desideri, ma non sono una scelta sensata per giochi o applicazioni basati sul web a causa delle loro dimensioni.

Se si sta programmando un gioco e se il linguaggio di programmazione che si sta utilizzando per il gioco supporta la concorrenza (thread o worker), si consiglia di prendere in considerazione l'esecuzione del codice di elaborazione audio nella propria discussione o worker se è possibile farlo. In questo modo si allevierà il ciclo di aggiornamento principale del gioco di qualsiasi overhead di elaborazione audio che potrebbe verificarsi.


Musica dinamica nei giochi popolari

Di seguito è riportata una piccola selezione di giochi popolari che sfruttano la musica dinamica in un modo o nell'altro. L'implementazione che questi giochi utilizzano per la loro musica dinamica può variare, ma il risultato finale è lo stesso: i giocatori del gioco hanno un'esperienza di gioco più coinvolgente.

  • Viaggio: thatgamecompany.com
  • Fiore: thatgamecompany.com
  • LittleBigPlanet: littlebigplanet.com
  • Portal 2: thinkwithportals.com
  • PixelJunk Shooter: pixeljunk.jp
  • Red Dead Redemption: rockstargames.com
  • Uncharted: naughtydog.com

Conclusione

Ecco, ecco una semplice implementazione di musica sequenziale dinamica che può davvero migliorare la natura emotiva di un gioco. Il modo in cui decidi di usare questa tecnica e quanto complessa diventa il sequencer, dipende interamente da te. Ci sono molte indicazioni che questa semplice implementazione può prendere e copriremo alcune di quelle direzioni in un futuro tutorial.

Se avete domande, non esitate a postarli nei commenti qui sotto e vi ricontatteremo al più presto possibile.