Støy Lag en synthesizer for retro lydeffekter - Core Engine

Dette er det andre i en serie av opplæringsprogrammer der vi skal lage en syntetiseringsbasert lydmotor som kan generere lyder for retro-stilte spill. Lydmotoren vil generere alle lydene ved kjøretid uten behov for eksterne avhengigheter som MP3-filer eller WAV-filer. Sluttresultatet vil være et fungerende bibliotek som kan løses enkelt i spillene dine.

Hvis du ikke allerede har lest den første opplæringen i denne serien, bør du gjøre det før du fortsetter.

Programmeringsspråket som brukes i denne opplæringen, er ActionScript 3.0, men de teknikker og begreper som brukes kan enkelt oversettes til et hvilket som helst annet programmeringsspråk som gir en lavnivå lyd API.

Du bør sørge for at du har Flash Player 11.4 eller høyere installert for nettleseren din hvis du vil bruke de interaktive eksemplene i denne opplæringen.


Audio Engine Demo

Ved slutten av denne opplæringen vil all kjernekode som kreves for lydmotoren, være fullført. Følgende er en enkel demonstrasjon av lydmotor i aksjon.

Bare én lyd blir spilt i den demonstrasjonen, men frekvensen av lyden blir randomisert sammen med utgivelsestiden. Lyden har også en modulator festet til den for å produsere vibrato-effekten (modulere lydens amplitude) og frekvensen av modulatoren blir også randomisert.


AudioWaveform Class

Den første klassen som vi vil opprette vil bare holde konstante verdier for bølgeformene som lydmotoren vil bruke til å generere lydlydene.

Start med å opprette en ny klassepakke som heter bråk, og legg deretter til følgende klasse i den pakken:

pakkestøy offentlig endelig klasse AudioWaveform statisk offentlig const PULSE: int = 0; statisk offentlig const SAWTOOTH: int = 1; statisk offentlig konst SINE: int = 2; statisk offentlig const TRIANGLE: int = 3; 

Vi vil også legge til en statisk offentlig metode for klassen som kan brukes til å validere en bølgeform verdi, metoden kommer tilbake ekte eller falsk for å indikere om bølgeformverdien er gyldig eller ikke.

statisk offentlig funksjon validere (bølgeform: int): Boolsk hvis (bølgeform == PULSE) returnere sann; hvis (bølgeform == SAWTOOTH) returnere sann; hvis (bølgeform == SINE) returnere sann; hvis (bølgeform == TRIANGLE) returnere sann; returner falsk; 

Til slutt bør vi forhindre at klassen blir instantiated fordi det ikke er noen grunn til at noen kan lage forekomster av denne klassen. Vi kan gjøre dette innen klassekonstruktøren:

offentlig funksjon AudioWaveform () kaste ny feil ("AudioWaveform-klassen kan ikke bli instantiated"); 

Denne klassen er nå fullført.

Å forhindre enum-stil klasser, all-statiske klasser og singleton klasser fra å være direkte instantiated er en god ting å gjøre fordi disse typer klasser bør ikke bli instantiated; Det er ingen grunn til å instantiere dem. Programmeringsspråk som Java gjør dette automatisk for de fleste av disse klassetyper, men for øyeblikket i ActionScript 3.0 må vi håndheve denne oppførselen manuelt i klassekonstruktøren.


Lyd klasse

Neste på listen er Audio klasse. Denne klassen ligner i naturen til den native ActionScript 3.0 Lyd klasse: hver lydmotor lyd vil bli representert av en Audio klasse eksempel.

Legg til følgende barebones klasse til bråk pakke:

pakke støy offentlig klasse lyd offentlig funksjon lyd () 

De første tingene som må legges til i klassen, er egenskaper som forteller lydmotorens hvordan man genererer lydbølgen når lyden spilles. Disse egenskapene inkluderer typen av bølgeform som brukes av lyden, frekvensen og amplituden til bølgeformen, lydens varighet og dens frigivelsestid (hvor raskt det fades ut). Alle disse egenskapene vil være private og nås via getters / setters:

privat var m_waveform: int = AudioWaveform.PULSE; privat var m_frequency: Number = 100.0; privat var m_amplitude: tall = 0,5; privat var m_duration: tall = 0,2; privat var m_release: tall = 0,2;

Som du kan se, har vi satt en fornuftig standardverdi for hver eiendom. De amplitude er en verdi i området 0.0 til 1.0, de Frekvens er i Hertz, og varighet og utgivelse Tider er i sekunder.

Vi må også legge til to flere private eiendommer for modulatorer som kan festes til lyden; igjen disse egenskapene vil bli åpnet via getters / setters:

private var m_frequencyModulator: AudioModulator = null; privat var m_amplitudeModulator: AudioModulator = null;

Endelig, den Audio klassen vil inneholde noen interne egenskaper som kun vil bli åpnet av Audioengine klasse (vi vil lage den klassen kort tid). Disse egenskapene trenger ikke å være skjult bak getters / setters:

intern varposisjon: tall = 0,0; intern var å spille: boolsk = false; intern var release: Boolsk = false; interne varprøver: Vector. = null;

De stilling er i sekunder og det tillater det Audioengine klassen for å holde oversikt over lydens posisjon mens lyden spiller, er dette nødvendig for å beregne bølgeform lydprøver for lyden. De spiller og frigjøre egenskaper forteller Audioengine hva staten lyden er i, og prøver Egenskapen er en referanse til de hurtigbufrede kurvformene som lyden bruker. Bruken av disse egenskapene blir tydelig når vi lager Audioengine klasse.

For å fullføre Audio klasse vi må legge til getters / setters:

Audio.bølgeform

offentlig sluttfunksjon få bølgeform (): int return m_waveform;  Offentlig sluttfunksjonssett bølgeform (verdi: int): void if (AudioWaveform.isValid (verdi) == false) return;  bytte (verdi) tilfelle AudioWaveform.PULSE: samples = AudioEngine.PULSE; gå i stykker; tilfelle AudioWaveform.SAWTOOTH: samples = AudioEngine.SAWTOOTH; gå i stykker; tilfelle AudioWaveform.SINE: samples = AudioEngine.SINE; gå i stykker; tilfelle AudioWaveform.TRIANGLE: samples = AudioEngine.TRIANGLE; gå i stykker;  m_waveform = verdi; 

Audio.Frekvens

[Inline] offentlig sluttfunksjon få frekvens (): Nummer return m_frequency;  Offentlig sluttfunksjonssettfrekvens (verdi: Nummer): void // klemme frekvensen til intervallet 1,0 - 14080,0 m_frequency = verdi < 1.0 ? 1.0 : value > 14080.0? 14080.0: verdi; 

Audio.amplitude

[Inline] offentlig sluttfunksjon få amplitude (): Nummer return m_amplitude;  Offentlig sluttfunksjonssett amplitude (verdi: Nummer): void // klem amplituden til intervallet 0.0 - 1.0 m_amplitude = verdi < 0.0 ? 0.0 : value > 1,0? 1,0: verdi; 

Audio.varighet

[Inline] Offentlig sluttfunksjon få varighet (): Nummer return m_duration;  Offentlig sluttfunksjon satt varighet (verdi: Nummer): void // klemme varigheten til intervallet 0.0 - 60.0 m_duration = verdi < 0.0 ? 0.0 : value > 60,0? 60,0: verdi; 

Audio.utgivelse

[Inline] offentlig sluttfunksjon få utgivelse (): Nummer return m_release;  Offentlig funksjon satt utgivelse (verdi: Nummer): void // klemme utgivelsestiden til området 0,0 - 10,0 m_release = verdi < 0.0 ? 0.0 : value > 10,0? 10.0: verdi; 

Audio.frequencyModulator

[Inline] offentlig sluttfunksjon få frequencyModulator (): AudioModulator return m_frequencyModulator;  Offentlig sluttfunksjonssett frekvensModulator (verdi: AudioModulator): void m_frequencyModulator = value; 

Audio.amplitudeModulator

[Inline] offentlig sluttfunksjon få amplitudeModulator (): AudioModulator return m_amplitudeModulator;  Offentlig sluttfunksjonssett amplitudeModulator (verdi: AudioModulator): void m_amplitudeModulator = value; 

Du har uten tvil lagt merke til [På linje] metadata tag bundet til noen av getter-funksjonene. Den metadata-taggen er en skinnende ny funksjon av Adobes nyeste ActionScript 3.0-kompiler, og det gjør det som står på tinnet: det innbringer (utvider) innholdet i en funksjon. Dette er ekstremt nyttig for optimalisering når det brukes fornuftig, og generering av dynamisk lyd ved kjøring er absolutt noe som krever optimalisering.


AudioModulator Class

Formålet med AudioModulator er å tillate amplitude og frekvens av Audio eksempler på å bli modulert for å skape nyttige og galne lydeffekter. Modulatorer er faktisk lik Audio forekomster, de har en bølgeform, en amplitude og frekvens, men de produserer faktisk ingen lydlyd som de bare modiferer hørbare lyder.

Første ting først, opprett følgende barebones klasse i bråk pakke:

pakkestøy offentlig klasse AudioModulator offentlig funksjon AudioModulator () 

La oss nå legge til private private eiendommer:

privat var m_waveform: int = AudioWaveform.SINE; privat var m_frequency: tall = 4,0; privat var m_amplitude: tall = 1,0; privat var m_shift: tall = 0,0; private var m_samples: Vector. = null;

Hvis du tenker dette ser veldig ut som Audio så er du riktig: alt bortsett fra skifte Eiendommen er den samme.

For å forstå hva skifte Egenskapen, tenk på en av de grunnleggende bølgeformene som lydmotoren bruker (puls, savetann, sinus eller trekant) og så forestill deg en vertikal linje som går rett gjennom bølgeformen i en hvilken som helst posisjon du liker. Den horisontale posisjonen til den vertikale linjen vil være skifte verdi; det er en verdi i serien 0.0 til 1.0 som forteller modulatoren hvor du skal begynne å lese den er bølgeform fra og i sin tur kan ha en dyp innvirkning på modifikasjonene som modulatoren gjør til lydens amplitude eller frekvens.

Som et eksempel, hvis modulatoren brukte en sinusbølgeform for å modulere frekvensen av en lyd, og skifte ble satt til 0.0, lydens frekvens ville først stige og deretter falle på grunn av sinusbukkens krumning. Men hvis skifte ble satt til 0.5 lydens frekvens vil først falle og deretter stige.

Uansett, tilbake til koden. De AudioModulator inneholder en intern metode som bare brukes av Audioengine; metoden er som følger:

[Inline] intern sluttfunksjon prosess (tid: Nummer): Nummer var p: int = 0; var s: tall = 0,0; hvis (m_shift! = 0.0) time + = (1.0 / m_frequency) * m_shift;  p = (44100 * m_frequency * tid)% 44100; s = m_samples [p]; returner s * m_amplitude; 

Den funksjonen er innstilt fordi den brukes mye, og når jeg sier "mye", mener jeg 44100 ganger i sekundet for hver lyd som spiller, som har en modulator festet til den (dette er hvor inlining blir utrolig verdifullt). Funksjonen tar bare en lydprøve fra bølgeformen som modulatoren bruker, justerer prøvenes amplitude og returnerer deretter resultatet.

For å fullføre AudioModulator klasse vi må legge til getters / setters:

AudioModulator.bølgeform

offentlig funksjon få bølgeform (): int return m_waveform;  Offentlig funksjon satt bølgeform (verdi: int): void if (AudioWaveform.isValid (verdi) == false) return;  bytte (verdi) tilfelle AudioWaveform.PULSE: m_samples = AudioEngine.PULSE; gå i stykker; tilfelle AudioWaveform.SAWTOOTH: m_samples = AudioEngine.SAWTOOTH; gå i stykker; tilfelle AudioWaveform.SINE: m_samples = AudioEngine.SINE; gå i stykker; tilfelle AudioWaveform.TRIANGLE: m_samples = AudioEngine.TRIANGLE; gå i stykker;  m_waveform = verdi; 

AudioModulator.Frekvens

offentlig funksjon få frekvens (): tall return m_frequency;  offentlig funksjonsinnstilt frekvens (verdi: tall): void // klemme frekvensen til området 0,01 - 100,0 m_frequency = verdi < 0.01 ? 0.01 : value > 100,0? 100,0: verdi; 

AudioModulator.amplitude

offentlig funksjon få amplitude (): tall return m_amplitude;  offentlige funksjonsinnstilt amplitude (verdi: tall): void // klem amplituden til intervallet 0.0 - 8000.0 m_amplitude = verdi < 0.0 ? 0.0 : value > 8000.0? 8000.0: verdi; 

AudioModulator.skifte

offentlig funksjon få skift (): tall retur m_shift;  Offentlig funksjon sett skift (verdi: Nummer): void // klemme skiftet til intervallet 0.0 - 1.0 m_shift = verdi < 0.0 ? 0.0 : value > 1,0? 1,0: verdi; 

Og det bryter opp AudioModulator klasse.


AudioEngine Class

Nå for den store: den Audioengine klasse. Dette er en allstatisk klasse og administrerer stort sett alt relatert til Audio forekomster og lydgenerering.

La oss starte med en barebones klasse i bråk pakke som vanlig:

pakkestøy import flash.events.SampleDataEvent; importere flash.media.Sound; importere flash.media.SoundChannel; importere flash.utils.ByteArray; // offentlig sluttklasse AudioEngine offentlig funksjon AudioEngine () kaste ny feil ("AudioEngine-klassen kan ikke bli instantiated"); 

Som nevnt før, bør ikke alle statiske klasser bli instantiated, derfor unntaket som kastes i klassekonstruktøren hvis noen prøver å ordne klassen. Klassen er også endelig fordi det ikke er grunn til å utvide en allstatisk klasse.

De første tingene som blir lagt til i denne klassen er interne konstanter. Disse konstantene vil bli brukt til å cache prøvene for hver av de fire bølgeformene som lydmotoren bruker. Hver cache inneholder 44,100 prøver som tilsvarer en hertz bølgeform. Dette gjør det mulig for lydmotor å produsere veldig rent lavfrekvent lydbølger.

Konstantene er som følger:

statisk intern const PULSE: Vector. = Ny Vector.(44100); statisk indre const SAWTOOTH: Vector. = Ny Vector.(44100); statisk indre const SINE: Vector. = Ny Vector.(44100); statisk intern const TRIANGLE: Vector. = Ny Vector.(44100);

Det er også to private konstanter som brukes av klassen:

statisk privat const BUFFER_SIZE: int = 2048; statisk privat const SAMPLE_TIME: Number = 1.0 / 44100.0;

De BUFFER STØRRELSE er antall lydprøver som skal sendes til ActionScript 3.0-lyd-API når en forespørsel om lydprøver blir gjort. Dette er det minste antall prøver tillatt og det resulterer i lavest mulig lydforsinkelse. Antallet prøver kunne økes for å redusere CPU-bruk, men det ville øke lydlatensen. De SAMPLE_TIME er varigheten av en enkelt lydprøve, i sekunder.

Og nå for de private variablene:

statisk privat varemisjon: tall = 0,0; statisk privat var m_amplitude: tall = 0,5; statisk privat var m_soundStream: Lyd = null; statisk privat var m_soundChannel: SoundChannel = null; statisk privat var m_audioList: Vector.
  • De m_position brukes til å holde oversikt over lydstrømtiden, i sekunder.
  • De m_amplitude er en global sekundær amplitude for alle Audio forekomster som spiller.
  • De m_soundStream og m_soundChannel bør ikke ha noen forklaring.
  • De m_audioList inneholder referanser til noen Audio forekomster som spiller.
  • De m_sampleList er en midlertidig buffer som brukes til å lagre lydprøver når de blir bedt om av ActionScript 3.0 lyd-API.

Nå må vi initialisere klassen. Det er mange måter å gjøre dette på, men jeg foretrekker noe fint og enkelt, en statisk klassekonstruktør:

statisk privat funksjon $ AudioEngine (): void var i: int = 0; var n: int = 44100; var p: tall = 0,0; // mens jeg < n )  p = i / n; SINE[i] = Math.sin( Math.PI * 2.0 * p ); PULSE[i] = p < 0.5 ? 1.0 : -1.0; SAWTOOTH[i] = p < 0.5 ? p * 2.0 : p * 2.0 - 2.0; TRIANGLE[i] = p < 0.25 ? p * 4.0 : p < 0.75 ? 2.0 - p * 4.0 : p * 4.0 - 4.0; i++;  // m_soundStream = new Sound(); m_soundStream.addEventListener( SampleDataEvent.SAMPLE_DATA, onSampleData ); m_soundChannel = m_soundStream.play();  $AudioEngine();

Hvis du har lest den forrige veiledningen i denne serien, vil du sannsynligvis se hva som skjer i den koden: prøvene for hver av de fire bølgeformene blir generert og bufret, og dette skjer bare en gang. Lydstrømmen blir også instantiated og startet og vil løpe kontinuerlig til appen er avsluttet.

De Audioengine klassen har tre offentlige metoder som brukes til å spille og stoppe Audio instanser:

Audioengine.spille()

statisk funksjon for offentlig funksjon (lyd: lyd): void if (audio.playing == false) m_audioList.push (lyd);  // dette lar oss vite nøyaktig når lyden ble startet audio.position = m_position - (m_soundChannel.position * 0.001); audio.playing = true; audio.releasing = false; 

Audioengine.Stoppe()

statisk offentlig funksjonstopp (lyd: Lyd, allowRelease: Boolean = true): void if (audio.playing == false) // lyden spilles ikke tilbake;  hvis (allowRelease) // hopp over til slutten av lyden og merk det som å frigjøre audio.position = audio.duration; audio.releasing = true; komme tilbake;  audio.playing = false; audio.releasing = false; 

Audioengine.STOPALL ()

statisk offentlig funksjon stopAll (allowRelease: Boolean = true): void var i: int = 0; var n: int = m_audioList.length; var o: Lyd = null; // hvis (allowRelease) mens (i < n )  o = m_audioList[i]; o.position = o.duration; o.releasing = true; i++;  return;  while( i < n )  o = m_audioList[i]; o.playing = false; o.releasing = false; i++;  

Og her kommer de viktigste lydbehandlingsmetodene, som begge er private:

Audioengine.onSampleData ()

statisk privat funksjon onSampleData (hendelse: SampleDataEvent): void var i: int = 0; var n: int = BUFFER_SIZE; var s: tall = 0,0; var b: ByteArray = event.data; // hvis (m_soundChannel == null) mens (i < n )  b.writeFloat( 0.0 ); b.writeFloat( 0.0 ); i++;  return;  // generateSamples(); // while( i < n )  s = m_sampleList[i] * m_amplitude; b.writeFloat( s ); b.writeFloat( s ); m_sampleList[i] = 0.0; i++;  // m_position = m_soundChannel.position * 0.001; 

Så i den første hvis erklæring vi sjekker om m_soundChannel er fortsatt null, og vi må gjøre det fordi EKSEMPELDATA Hendelsen sendes så snart som m_soundStream.play () Metoden er påkalt, og før metoden får en sjanse til å returnere a SoundChannel forekomst.

De samtidig som loop ruller gjennom lydprøver som er forespurt av m_soundStream og skriver dem til den oppgitte ByteArray forekomst. Lydprøver genereres ved hjelp av følgende metode:

Audioengine.generateSamples ()

statisk privat funksjon generereSamples (): void var i: int = 0; var n: int = m_audioList.length; var j: int = 0; var k: int = BUFFER_SIZE; var p: int = 0; var f: tall = 0,0; var a: tall = 0,0; var s: tall = 0,0; var o: Lyd = null; // rulle gjennom lydinstansene mens (i < n )  o = m_audioList[i]; // if( o.playing == false )  // the audio instance has stopped completely m_audioList.splice( i, 1 ); n--; continue;  // j = 0; // generate and buffer the sound samples while( j < k )  if( o.position < 0.0 )  // the audio instance hasn't started playing yet o.position += SAMPLE_TIME; j++; continue;  if( o.position >= o.duration) hvis (o.position> = o.duration + o.release) // lyd forekomsten har stoppet o.playing = false; j ++; Fortsette;  // lyden forekommer slipper o.releasing = true;  // Ta tak i lydinstansens frekvens og amplitude f = o.frequency; a = o.amplitude; // hvis (o.frequencyModulator! = null) // moduler frekvensen f + = o.frequencyModulator.process (o.position);  // hvis (o.amplitudeModulator! = null) // modulere amplituden a + = o.amplitudeModulator.process (o.position);  // beregne posisjonen i bølgeformbufferen p = (44100 * f * o.position)% 44100; // ta tak i bølgeformprøven s = o.samples [p]; // hvis (o.releasing) // beregne fade-out amplitude for prøven s * = 1.0 - ((o.position - o.duration) / o.release);  // legge prøven til bufferen m_sampleList [j] + = s * a; // Oppdater lydinstansens posisjon o.position + = SAMPLE_TIME; j ++;  i ++; 

Til slutt, for å fullføre ting, må vi legge til getteren / setteren for den private m_amplitude variabel:

statisk offentlig funksjon få amplitude (): tall return m_amplitude;  statisk offentlig funksjonssett amplitude (verdi: tall): void // klem amplituden til intervallet 0.0 - 1.0 m_amplitude = verdi < 0.0 ? 0.0 : value > 1,0? 1,0: verdi; 

Og nå trenger jeg en pause!


Kommer opp…

I den tredje og siste opplæringen i serien vil vi legge til lydprosessorer til lydmotor. Disse vil tillate oss å skyve alle de genererte lydprøverne selv om prosesseringsenheter som harde begrensere og forsinkelser. Vi vil også ta en titt på hele koden for å se om noe kan optimaliseres.

All kildekoden for denne opplæringsserien vil bli gjort tilgjengelig med neste opplæring.

Følg oss på Twitter, Facebook eller Google+ for å holde deg oppdatert med de siste innleggene.