Dynamiske, sekvensielle lydspor for spill

I denne opplæringen tar vi en titt på en teknikk for konstruksjon og sekvensering av dynamisk musikk for spill. Konstruksjonen og sekvenseringen skjer ved brukstid, slik at spillutviklere kan endre strukturen til musikken for å reflektere hva som skjer i spillverdenen.

Før vi hopper inn i de tekniske detaljene, vil du kanskje se på en fungerende demonstrasjon av denne teknikken i aksjon. Musikken i demonstrasjonen er konstruert fra en samling av individuelle lydblokker som er sekvensert og blandet sammen på kjøretid for å danne hele musikksporet.

Klikk for å vise demoen.

Denne demonstrasjonen krever en nettleser som støtter W3C Web Audio API og OGG lyd. Google Chrome er den beste nettleseren som skal bruke for å vise denne demonstrasjonen, men Firefox Aurora kan også brukes.

Hvis du ikke kan se demonstrasjonen ovenfor i nettleseren din, kan du se på denne YouTube-videoen i stedet:



Oversikt

Måten denne teknikken virker på, er ganske enkel, men det har potensial til å legge til noen veldig fin dynamisk musikk til spill hvis den brukes kreativt. Det lar også uendelig lange musikkspor bli opprettet fra en relativt liten lydfil.

Den opprinnelige musikken er i hovedsak dekonstruert i en samling blokker, hver av dem er en linje i lengden, og disse blokkene lagres i en enkelt lydfil. Musikk-sequencer laster lydfilen og trekker ut de rå lydprøvene som trengs for å rekonstruere musikken. Musikkens struktur dikteres av en samling av mutable arrays som forteller sequencer når man skal spille musikkblokkene.

Du kan tenke på denne teknikken som en forenklet versjon av sekvenseringsprogramvare som Reason, FL Studio eller Dance EJay. Du kan også tenke på denne teknikken som den musikalske ekvivalenten av Lego murstein.


Lydfilstruktur

Som nevnt tidligere krever musikk-sequencer den opprinnelige musikken som dekonstrueres i en samling blokker, og disse blokkene må lagres i en lydfil.

Dette bildet viser hvordan blokkene kan lagres i en lydfil.

I det bildet kan du se at det er fem individuelle blokker lagret i lydfilen, og alle blokkene er like lange. For å holde ting enkelt for denne opplæringen er blokkene alle en linje lang.

Plasseringen av blokkene i lydfilen er viktig fordi den dikterer hvilke sequencer-kanaler blokkene er tildelt. Den første blokk (for eksempel trommer) vil bli tilordnet den første sekvenseringskanalen, den andre blokken (for eksempel perkusjon) vil bli tilordnet den andre sekvenser-kanalen og så videre.


Sequencer kanaler

En sequencer-kanal representerer en rekke blokker og inneholder flagg (en for hver musikklinje) som angir hvorvidt blokken som er tilordnet kanalen skal spilles. Hvert flagg er en numerisk verdi og er enten null (ikke spill blokken) eller en (spille blokken).

Dette bildet demonstrerer forholdet mellom blokkene og sequencer-kanalene.

Tallene justeres horisontalt langs bunnen av bildet ovenfor representerer stangnumre. Som du kan se, i den første baren av musikk (01) bare gitarblokken vil bli spilt, men i den femte baren (05) Trommer, Percussion, Bass og Guitar blokker vil bli spilt.


programmering

I denne opplæringen vil vi ikke gå gjennom koden til en full arbeidsmusikk-sequencer; I stedet vil vi se på kjerne koden som kreves for å få en enkel musikk sequencer kjører. Koden vil bli presentert som pseudo-kode for å holde ting som språk-agnostisk som mulig.

Før vi begynner, må du huske at programmeringsspråket du endelig bestemmer deg for å bruke, krever en API som lar deg manipulere lyd på lavt nivå. Et godt eksempel på dette er Web Audio API tilgjengelig i JavaScript.

Du kan også laste ned kildefilene som er vedlagt denne opplæringen, for å studere en JavaScript-implementering av en grunnleggende musikk-sequencer som ble opprettet som en demonstrasjon for denne opplæringen.

Quick Recap

Vi har en enkelt lydfil som inneholder musikkblokker. Hver blokk med musikk er en linje i lengden, og rekkefølgen av blokkene i lydfilen dikterer sekvensererkanalen blokkene er tildelt til.

konstanter

Det er to deler av informasjonen vi trenger før vi kan fortsette. Vi trenger å vite tempoet til musikken, i slag per minutt, og antall beats i hver bar. Sistnevnte kan betraktes som musikkens tids signatur. Denne informasjonen skal lagres som konstante verdier fordi den ikke endres mens musikk-sequencer kjører.

 TEMPO = 100 // slag per minutt SIGNATURE = 4 // slag per bar

Vi trenger også å vite samplingsfrekvensen som lyd-API-en bruker. Vanligvis vil dette være 44100 Hz, fordi det er helt greit for lyd, men noen har harddisken konfigurert til å bruke en høyere samplingsfrekvens. Lyd-API-en du velger å bruke, bør gi denne informasjonen, men i form av denne opplæringen vil vi anta at samplefrekvensen er 44100 Hz.

 SAMPLE_RATE = 44100 // Hertz

Vi kan nå beregne prøve lengden på en musikklinje - det vil si antall lydprøver i ett blokk med musikk. Denne verdien er viktig fordi det tillater at musikk-sequenceren finner de individuelle blokkene av musikk og lydprøver i hver blokk i lydfildataene.

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

Lydstrømmer

Lyd-API-en du velger å bruke, vil diktere hvordan lydstrømmer (arrayer av lydprøver) er representert i koden din. For eksempel bruker Web Audio API-en AudioBuffer-objekter.

For denne opplæringen vil det være to lydstrømmer. Den første lydstrømmen vil være skrivebeskyttet og vil inneholde alle lydprøver lastet fra lydfilen som inneholder musikkblokkene, dette er lydinngangen "Input".

Den andre lydstrømmen vil være skrivebeskyttet og vil bli brukt til å skyve lydprøver til maskinvaren; dette er lydutgangen "utgang". Hver av disse strømmene vil bli representert som et endimensjonalt utvalg.

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

Den nøyaktige prosessen som kreves for å laste lydfilen og trekke ut lydprøver fra filen, vil bli diktert av programmeringsspråket du bruker. Med det i tankene vil vi anta inngang audio stream array inneholder allerede lydprøver hentet fra lydfilen.

De produksjon lydstrøm vil vanligvis være en fast lengde fordi de fleste lyd-APIer vil tillate deg å velge frekvensen der lydprøver må behandles og sendes til maskinvaren - det vil si hvor ofte en Oppdater funksjonen er påkalt. Frekvensen er vanligvis bundet direkte til lydens latens, høye frekvenser vil kreve mer prosessorkraft, men de resulterer i lavere latenser og omvendt.

Sequencer Data

Sekvensatordataene er et flerdimensjonalt array; Hver undergruppe representerer en sequencer-kanal og inneholder flagg (en for hver musikklinje) som angir om musikkblokken som er tilordnet kanalen skal spilles av eller ikke. Lengden på kanalarrayene dikterer også lengden på musikken.

 kanaler = [[0,0,0,0, 0,0,0,0, 1,1,1,1,1,1,1,1], // trommer [0,0,0,0, 1 , 1,1,1, 1,1,1,1, 1,1,1,1] / perkusjon [0,0,0,0, 0,0,0,0, 1,1,1, 1, 1,1,1,1] // bass [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1] // gitar [0,0,0,0, 0,0,1,1, 0,0,0,0, 0,0,1,1] // strenger]

Dataene du ser der representerer en musikkstruktur som er seksten barer lang. Den inneholder fem kanaler, en for hver musikkblokk i lydfilen, og kanalene er i samme rekkefølge som musikkblokkene i lydfilen. Flaggene i kanalradiusene gir oss beskjed om blokken som er tilordnet kanalene, skal spilles eller ikke: verdien 0 betyr at en blokk ikke vil bli spilt; verdien 1 betyr at en blokk vil bli spilt.

Denne datastrukturen er mutabel, den kan endres til enhver tid, selv når musikk-sequencer kjører, og dette gjør at du kan endre flagg og struktur av musikken for å gjenspeile hva som skjer i et spill.

Lydbehandling

De fleste lyd-APIer sender enten en hendelse til en hendelseshåndteringsfunksjon, eller påkaller en funksjon direkte, når den må trykke flere lydprøver til maskinvaren. Denne funksjonen er vanligvis påkrevd som den viktigste oppdateringssløyfen til et spill, men ikke så ofte, slik at tiden bør brukes til å optimalisere den.

I utgangspunktet hva som skjer i denne funksjonen er:

  1. Flere lydprøver blir trukket fra inngang lyd stream.
  2. Disse prøvene blir lagt sammen for å danne en enkelt lydprøve.
  3. At lydprøven skyves inn i produksjon lyd stream.

Før vi kommer til funksjonens tarm, må vi definere et par flere variabler i koden:

 spiller = true // angir om musikken (sequencer) spiller posisjon = 0 // posisjonen til sequencer playhead, i prøver

De spiller Boolean lar oss bare vite om musikken spiller; Hvis det ikke spiller, må vi skyve lydløse lydprøver inn i produksjon lyd stream. De stilling holder styr på hvor stykkhodet er innenfor musikken, så det er litt som en skrubber på en typisk musikk- eller videospiller.

Nå for funksjonens tarm:

 funksjonoppdatering () outputIndex = 0 outputCount = output.length hvis (spiller == false) // stille prøver må skyves til utgangsstrømmen mens (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) // tilbakestill stillingshodeposisjonen for å sløyfe musikkposisjonen = 0 outputIndex ++

Som du kan se, er koden som kreves for å behandle lydprøver, ganske enkel, men da denne koden skal kjøres mange ganger i sekundet, bør du se på måter å optimalisere koden i funksjonen og forhåndsberegne så mange verdier som mulig. Optimeringene du kan søke på koden, er bare avhengig av hvilket programmeringsspråk du bruker.

Ikke glem at du kan laste ned kildefilene som er vedlagt denne opplæringen, hvis du vil se på en måte å implementere en grunnleggende musikk-sequencer på i JavaScript ved hjelp av Web Audio API.


Merknader

Formatet til lydfilen du bruker, må tillate lyden til sløyfe sømløst. Med andre ord, koderen som brukes til å generere lydfilen, bør ikke injisere noen polstring (lydbiter av lyd) i lydfilen. Dessverre kan ikke MP3- og MP4-filer brukes av den grunn. OGG-filer (brukt av JavaScript-demonstrasjonen) kan brukes. Du kan også bruke WAV-filer hvis du vil, men de er ikke et fornuftig valg for nettbaserte spill eller applikasjoner på grunn av deres størrelse.

Hvis du programmerer et spill, og hvis programmeringsspråket du bruker til spillet støtter samtidighet (tråder eller arbeidstakere), vil du kanskje vurdere å kjøre lydbehandlings-koden i sin egen tråd eller arbeider hvis det er mulig å gjøre det. Å gjøre det vil lette spillets viktigste oppdateringssløyfe for eventuelle lydbehandlingsutgifter som kan oppstå.


Dynamisk musikk i populære spill

Følgende er et lite utvalg av populære spill som utnytter dynamisk musikk på en eller annen måte. Gjennomførelsen disse spillene bruker for sin dynamiske musikk, kan variere, men sluttresultatet er det samme: spillets spillere har en mer fordybende spillopplevelse.

  • Reisen: thatgamecompany.com
  • Blomst: thatgamecompany.com
  • LittleBigPlanet: littlebigplanet.com
  • Portal 2: thinkwithportals.com
  • PixelJunk Shooter: pixeljunk.jp
  • Red Dead Redemption: rockstargames.com
  • Ukjent: naughtydog.com

Konklusjon

Så, der går du - en enkel implementering av dynamisk sekvensiell musikk som virkelig kan forbedre følelsesmessig karakter av et spill. Hvordan du bestemmer deg for å bruke denne teknikken, og hvor komplisert sequencer blir, er helt opp til deg. Det er mange retninger som denne enkle gjennomføringen kan ta, og vi vil dekke noen av disse retningene i en fremtidig veiledning.

Hvis du har noen spørsmål, vær så snill å poste dem i kommentarene nedenfor, så vil jeg komme tilbake til deg så snart som mulig.