In deze tutorial zullen we een techniek bekijken voor het construeren en sequencen van dynamische muziek voor games. De constructie en sequencing vindt tijdens runtime plaats, waardoor game-ontwikkelaars de structuur van de muziek kunnen aanpassen aan wat er in de gamewereld gebeurt.
Voordat we ingaan op de technische details, wil je misschien een werkende demonstratie van deze techniek in actie bekijken. De muziek in de demonstratie is opgebouwd uit een verzameling afzonderlijke audioblokken die in runtime worden gesequenced en gemixt tot het volledige muzieknummer.
Klik om de demo te bekijken.Voor deze demonstratie is een webbrowser vereist die de W3C Web Audio API en OGG-audio ondersteunt. Google Chrome is de beste browser om deze demonstratie mee te bekijken, maar Firefox Aurora kan ook worden gebruikt.
Als je de bovenstaande demo niet in je browser kunt bekijken, kun je in plaats daarvan deze YouTube-video bekijken:
De manier waarop deze techniek werkt is redelijk eenvoudig, maar het heeft de potentie om een aantal hele mooie dynamische muziek toe te voegen aan games als het op een creatieve manier wordt gebruikt. Het maakt het ook mogelijk om oneindig lange muziektracks te maken met een relatief klein audiobestand.
De originele muziek wordt in wezen gedeconstrueerd tot een verzameling blokken, die elk één staaflengte hebben, en die blokken worden opgeslagen in een enkel audiobestand. De muzieksequencer laadt het audiobestand en extraheert de onbewerkte audiosamples die het nodig heeft om de muziek te reconstrueren. De structuur van de muziek wordt gedicteerd door een verzameling veranderbare matrices die de sequencer vertellen wanneer de blokken muziek moeten worden gespeeld.
Je kunt deze techniek zien als een vereenvoudigde versie van sequencing-software zoals Reason, FL Studio of Dance EJay. Je kunt deze techniek ook zien als het muzikale equivalent van Legoblokjes.
Zoals eerder vermeld, vereist de muzieksequencer dat de originele muziek wordt gedeconstrueerd in een verzameling blokken en die blokken moeten worden opgeslagen in een audiobestand.
Deze afbeelding laat zien hoe de blokken kunnen worden opgeslagen in een audiobestand.In die afbeelding kunt u zien dat er vijf afzonderlijke blokken zijn opgeslagen in het audiobestand en dat alle blokken even lang zijn. Om de dingen eenvoudig te houden voor deze tutorial, zijn de blokken allemaal één balk lang.
De volgorde van de blokken in het audiobestand is belangrijk omdat het dicteert aan welke sequencerkanalen de blokken zijn toegewezen. Het eerste blok (bijvoorbeeld drums) zal worden toegewezen aan het eerste sequencerkanaal, het tweede blok (bijvoorbeeld percussie) zal worden toegewezen aan het tweede sequencerkanaal, enzovoort.
Een sequencer-kanaal vertegenwoordigt een rij blokken en bevat vlaggen (één voor elke muziekbalk) die aangeven of het blok dat aan het kanaal is toegewezen, moet worden afgespeeld. Elke vlag is een numerieke waarde en is ofwel nul (speelt het blok niet) of één (speelt het blok).
Deze afbeelding demonstreert de relatie tussen de blokken en de sequencer-kanalen.De cijfers die horizontaal langs de onderkant van de bovenstaande afbeelding zijn uitgelijnd, geven barnummers weer. Zoals je kunt zien, in de eerste balk met muziek (01) alleen het gitaarblok wordt gespeeld, maar in de vijfde balk (05) de Drums, Percussion, Bass en Guitar-blokken worden gespeeld.
In deze tutorial zullen we niet de code van een volledig werkende muzieksequencer doorlopen; in plaats daarvan kijken we naar de kerncode die nodig is om een eenvoudige muzieksequencer te laten draaien. De code zal worden gepresenteerd als pseudo-code om de dingen als taal-agnostisch mogelijk te houden.
Voordat we beginnen, moet u in gedachten houden dat de programmeertaal die u uiteindelijk wilt gebruiken, een API vereist die u in staat stelt om audio op een laag niveau te manipuleren. Een goed voorbeeld hiervan is de Web Audio API die beschikbaar is in JavaScript.
Je kunt ook de bronbestanden downloaden die bij deze tutorial horen om een JavaScript-implementatie van een standaard muzieksequencer te bestuderen die is gemaakt als een demonstratie voor deze tutorial.
We hebben een enkel audiobestand met blokken muziek. Elk muziekblok is één balk lang en de volgorde van de blokken in het audiobestand bepaalt het sequencerkanaal waaraan de blokken zijn toegewezen.
Er zijn twee soorten informatie die we nodig hebben voordat we verder kunnen gaan. We moeten het tempo van de muziek kennen, in beats per minuut, en het aantal beats in elke balk. Dit laatste kan worden gezien als de maatsoort van de muziek. Deze informatie moet worden opgeslagen als constante waarden, omdat deze niet veranderen terwijl de muzieksequencer actief is.
TEMPO = 100 // slagen per minuut HANDTEKENING = 4 // slagen per staaf
We moeten ook de samplefrequentie weten die de audio-API gebruikt. Meestal zal dit 44100 Hz zijn, omdat het prima is voor audio, maar sommige mensen hebben hun hardware geconfigureerd om een hogere samplefrequentie te gebruiken. De audio-API die u wilt gebruiken, moet deze informatie bevatten, maar voor het doel van deze zelfstudie gaan we ervan uit dat de samplefrequentie 44100 Hz is.
SAMPLE_RATE = 44100 // Hertz
We kunnen nu de sample-lengte van één muziekstuk berekenen - dat wil zeggen, het aantal audiomonsters in één muziekblok. Deze waarde is belangrijk omdat het de muzieksequencer toestaat om de individuele muziekblokken en de audiomonsters binnen elk blok in de audiobestandgegevens te lokaliseren.
BLOCK_SIZE = verdieping (SAMPLE_RATE * (60 / (TEMPO / SIGNATURE)))
De audio-API die u wilt gebruiken, bepaalt hoe audiostreams (arrays van audiomonsters) in uw code worden weergegeven. De Web Audio API gebruikt bijvoorbeeld AudioBuffer-objecten.
Voor deze zelfstudie zijn er twee audiostreams. De eerste audiostream is alleen-lezen en bevat alle audiomonsters geladen uit het audiobestand met de muziekblokken, dit is de "invoer" audiostream.
De tweede audiostream is alleen-schrijven en wordt gebruikt om audiomonsters naar de hardware te pushen; dit is de "uitvoer" audiostream. Elk van deze streams wordt weergegeven als een eendimensionale array.
input = [...] output = [...]
Het exacte proces dat vereist is om het audiobestand te laden en de audiofragmenten uit het bestand te extraheren, wordt bepaald door de programmeertaal die u gebruikt. Met dat in gedachten zullen we het invoer
audiostreamarray bevat al de audiosamples die uit het audiobestand zijn geëxtraheerd.
De uitgang
audiostream heeft meestal een vaste lengte omdat de meeste audio-API's u in staat stellen om de frequentie te kiezen waarop de audiomonsters moeten worden verwerkt en naar de hardware moeten worden verzonden - dat is, hoe vaak een bijwerken
functie wordt opgeroepen. De frequentie is normaal gesproken rechtstreeks gekoppeld aan de latentie van de audio, hoge frequenties vereisen meer processorkracht maar ze resulteren in lagere latencies en vice versa.
De sequencer-gegevens zijn een meerdimensionale array; elke subarray representeert een sequencer-kanaal en bevat vlaggen (één voor elke muziekbar) die aangeven of het muziekblok dat aan het kanaal is toegewezen al dan niet moet worden afgespeeld. De lengte van de kanaalmatrices bepaalt ook de lengte van de muziek.
kanalen = [[0,0,0,0, 0,0,0,0, 1,1,1,1, 1,1,1,1], // drums [0,0,0,0, 1 , 1,1,1, 1,1,1,1, 1,1,1,1], // percussie [0,0,0,0, 0,0,0,0, 1,1,1, 1, 1,1,1,1], // bas [1,1,1,1, 1,1,1,1, 1,1,1,1, 1,1,1,1], // gitaar [0,0,0,0, 0,0,1,1, 0,0,0,0, 0,0,1,1] // snaren]
De gegevens die je daar ziet, vertegenwoordigen een muziekstructuur die zestien maten lang is. Het bevat vijf kanalen, één voor elk muziekblok in het audiobestand en de kanalen zijn in dezelfde volgorde als de muziekblokken in het audiobestand. De vlaggen in de kanaalmatrices laten ons weten of het blok toegewezen aan de kanalen al dan niet moet worden gespeeld: de waarde 0
betekent dat een blok niet wordt gespeeld; de waarde 1
betekent dat er een blok wordt gespeeld.
Deze datastructuur kan worden gewijzigd, het kan op elk moment worden gewijzigd, zelfs wanneer de muzieksequencer actief is, en dit stelt je in staat om de vlaggen en de structuur van de muziek aan te passen om weer te geven wat er in een game gebeurt.
De meeste audio-API's zenden een gebeurtenis uit naar een gebeurtenishandlerfunctie of roepen een functie rechtstreeks aan, wanneer ze meer audiomonsters naar de hardware moeten pushen. Deze functie wordt meestal constant aangeroepen zoals de hoofdupdate van een game, maar niet zo vaak, dus tijd moet besteed worden aan het optimaliseren ervan.
Wat er in deze functie gebeurt, is eigenlijk:
invoer
audiostream.uitgang
audiostream.Voordat we het lef van de functie raken, moeten we nog een paar variabelen in de code definiëren:
spelen = waar // geeft aan of de muziek (de sequencer) de positie speelt = 0 // de positie van de afspeelkop van de sequencer, in voorbeelden
De spelen
Boolean laat ons eenvoudig weten of de muziek wordt afgespeeld; als het niet speelt, moeten we stille audiosamples in de uitgang
audiostream. De positie
houdt bij waar de afspeelkop zich binnen de muziek bevindt, dus het is een beetje zoals een scrubber op een typische muziek- of videospeler.
Nu voor het lef van de functie:
functie update () outputIndex = 0 outputCount = output.length if (spelen == false) // silent samples moeten naar de outputstream worden geduwd terwijl (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) // reset de positie van de afspeelkop om de muziekpositie te herhalen = 0 outputIndex ++
Zoals u ziet, is de code die vereist is om de audiomonsters te verwerken vrij eenvoudig, maar aangezien deze code meerdere keren per seconde wordt uitgevoerd, moet u manieren bekijken om de code binnen de functie te optimaliseren en zoveel mogelijk waarden vooraf te berekenen. De optimalisaties die u op de code kunt toepassen, zijn uitsluitend afhankelijk van de programmeertaal die u gebruikt.
Vergeet niet dat je de bronbestanden van deze tutorial kunt downloaden als je een manier wilt bekijken om een standaard muzieksequencer in JavaScript te implementeren met behulp van de Web Audio API.
Het formaat van het audiobestand dat u gebruikt, moet toestaan dat de audio naadloos aansluit. Met andere woorden, de encoder die wordt gebruikt om het audiobestand te genereren, mag geen opvulling (stille stukken audio) in het audiobestand injecteren. Helaas kunnen MP3- en MP4-bestanden om die reden niet worden gebruikt. OGG-bestanden (gebruikt door de JavaScript-demonstratie) kunnen worden gebruikt. Je zou ook WAV-bestanden kunnen gebruiken als je dat wilt, maar ze zijn geen verstandige keuze voor webgebaseerde games of applicaties vanwege hun grootte.
Als u een game programmeert en als de programmeertaal die u gebruikt voor de game gelijktijdigheid ondersteunt (threads of werknemers), dan kunt u overwegen om de audioverwerkingscode in zijn eigen thread of werker te gebruiken als dit mogelijk is. Als u dat doet, wordt de belangrijkste updatelus van het spel van eventuele overhead voor audiobewerking verlicht.
Het volgende is een kleine selectie van populaire spellen die op de een of andere manier gebruik maken van dynamische muziek. De implementatie die deze games gebruiken voor hun dynamische muziek kan variëren, maar het eindresultaat is hetzelfde: de spelers van de game hebben een meer meeslepende game-ervaring.
Dus, daar ga je - een eenvoudige implementatie van dynamische sequentiële muziek die de emotionele aard van een game echt kan verbeteren. Hoe u besluit om deze techniek te gebruiken en hoe complex de sequencer wordt, is geheel aan u. Er zijn veel aanwijzingen die deze eenvoudige implementatie kan aannemen en we zullen enkele van die aanwijzingen in een toekomstige zelfstudie bespreken.
Als u vragen heeft, kunt u deze in de reacties hieronder plaatsen en ik zal zo snel mogelijk contact met u opnemen.