Buller Skapa en synthesizer för Retro Sound Effects - Core Engine

Det här är det andra i en serie av handledning där vi ska skapa en synthesizerbaserad ljudmotor som kan generera ljud för retroformade spel. Ljudmotorn genererar alla ljud vid körning utan att behöva några externa beroenden, som MP3-filer eller WAV-filer. Slutresultatet blir ett arbetsbibliotek som kan släppas enkelt i dina spel.

Om du inte redan har läst den första handledningen i denna serie borde du göra det innan du fortsätter.

Programmeringsspråket som används i denna handledning är ActionScript 3.0, men de använda teknikerna och koncepten kan enkelt översättas till något annat programmeringsspråk som tillhandahåller en låg ljud API.

Du bör se till att du har Flash Player 11.4 eller senare installerat för din webbläsare om du vill använda de interaktiva exemplen i denna handledning.


Audio Engine Demo

Vid slutet av denna handledning kommer all den kärnkod som krävs för ljudmotorn att ha slutförts. Följande är en enkel demonstration av ljudmotorn i åtgärd.

Endast ett ljud spelas i den demonstrationen, men frekvensen av ljudet blir randomiserad tillsammans med dess släpptid. Ljudet har också en modulator kopplad till den för att producera vibrato-effekten (modulera ljudets amplitud) och modulatorns frekvens är också randomiserad.


AudioWaveform Class

Den första klassen som vi kommer att skapa kommer helt enkelt att hålla konstanta värden för de vågformer som ljudmotorn kommer att använda för att generera ljudet.

Börja med att skapa ett nytt klasspaket som heter ljud, och lägg sedan till följande klass i det paketet:

paketljud public final class AudioWaveform static public const PULSE: int = 0; statisk offentlig konst SAWTOOTH: int = 1; statisk offentlig konst SINE: int = 2; statisk offentlig konst TRIANGEL: int = 3; 

Vi lägger också till en statisk offentlig metod för klassen som kan användas för att validera ett vågformvärde, metoden kommer tillbaka Sann eller falsk för att ange om vågformsvärdet är giltigt eller inte.

statisk offentlig funktion validera (vågform: int): Boolean if (waveform == PULSE) returnera true; om (vågform == SAWTOOTH) returnera sant; om (vågform == SINE) returneras sant; om (waveform == TRIANGLE) returnera true; returnera false; 

Slutligen bör vi förhindra att klassen blir instanserad eftersom det inte finns någon anledning för någon att skapa instanser av denna klass. Vi kan göra detta inom klasskonstruktören:

offentlig funktion AudioWaveform () kasta nytt fel ("AudioWaveform-klassen kan inte ordnas"); 

Den här klassen är nu klar.

Att förhindra enum-stilklasser, allstatiska klasser och singleton-klasser från att vara direkt instansierad är en bra sak att göra för att dessa typer av klass inte borde vara instansierade. det finns ingen anledning att inställa dem. Programmeringsspråk som Java gör det automatiskt för de flesta av dessa klasstyper men för närvarande i ActionScript 3.0 måste vi genomföra detta beteende manuellt inom klasskonstruktorn.


Ljudklass

Nästa på listan är Audio klass. Den här klassen är av samma natur som den ursprungliga ActionScript 3.0 Ljud klass: varje ljudmotor ljud kommer att representeras av en Audio klass förekomst.

Lägg till följande barebones klass till ljud paket:

paketljud public class Audio public function Audio () 

De första sakerna som måste läggas till i klassen är egenskaper som kommer att berätta för ljudmotorn hur man genererar ljudvågan när ljudet spelas. Dessa egenskaper inkluderar den typ av vågform som används av ljudet, frekvensen och amplituden för vågformen, ljudets varaktighet och dess frigivningstid (hur snabbt det försvinner). Alla dessa egenskaper kommer att vara privata och åtkomliga via getters / setters:

privat var m_waveform: int = AudioWaveform.PULSE; privat var m_frequency: Number = 100.0; privat varamöjlighet: nummer = 0,5; privat var m_duration: Number = 0.2; privat var m_release: Number = 0.2;

Som du kan se har vi satt ett förnuftigt standardvärde för varje egendom. De amplitud är ett värde inom intervallet 0,0 till 1,0, de frekvens är i Hertz, och varaktighet och släpp Tiderna är i sekunder.

Vi behöver också lägga till ytterligare två privata egenskaper för modulatorer som kan kopplas till ljudet. igen kommer dessa egenskaper att nås via getters / setters:

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

Slutligen, den Audio klassen innehåller några interna egenskaper som endast kommer att nås av Audioengine klass (vi kommer att skapa den klassen inom kort). Dessa egenskaper behöver inte döljas bakom getters / setters:

intern var position: Number = 0.0; interna var att spela: booleskt = false; intern var release: Boolean = false; interna varprover: vektor. = null;

De placera är i sekunder och det tillåter Audioengine klass för att hålla reda på ljudets position medan ljudet spelas krävs detta för att beräkna vågforms ljudproverna för ljudet. De spelar och frisättande egenskaper berättar Audioengine vilket tillstånd ljudet är i, och prover egendom är en referens till de cachade vågformsprover som ljudet använder. Användningen av dessa egenskaper kommer att bli tydlig när vi skapar Audioengine klass.

För att avsluta Audio klass måste vi lägga till getters / setters:

Audio.vågform

Offentlig slutfunktion få vågform (): int return m_waveform;  allmän slutlig funktionsuppsättning vågform (värde: int): void if (AudioWaveform.isValid (value) == false) return;  switch (värde) case AudioWaveform.PULSE: samples = AudioEngine.PULSE; ha sönder; fallet AudioWaveform.SAWTOOTH: samples = AudioEngine.SAWTOOTH; ha sönder; fallet AudioWaveform.SINE: samples = AudioEngine.SINE; ha sönder; fallet AudioWaveform.TRIANGLE: samples = AudioEngine.TRIANGLE; ha sönder;  m_waveform = värde; 

Audio.frekvens

[Inline] Offentlig slutfunktion få frekvens (): Nummer return m_frequency;  Offentlig slutlig funktionsinställd frekvens (värde: Nummer): void // klämma frekvensen till intervallet 1.0 - 14080.0 m_frequency = value < 1.0 ? 1.0 : value > 14080.0? 14080,0: värde; 

Audio.amplitud

[Inline] Offentlig slutfunktion få amplitud (): Number return m_amplitude;  Offentlig slutlig funktionsuppsättning amplitude (värde: Nummer): void // kläm amplituden till intervallet 0.0 - 1.0 m_amplitude = värde < 0.0 ? 0.0 : value > 1,0? 1,0: värde; 

Audio.varaktighet

[Inline] Offentlig slutfunktion får varaktighet (): Nummer return m_duration;  allmän slutlig funktionsinställd varaktighet (värde: antal): void // klämma längden till intervallet 0.0 - 60.0 m_duration = värde < 0.0 ? 0.0 : value > 60,0? 60,0: värde; 

Audio.släpp

[Inline] Offentlig slutfunktion få release (): Number return m_release;  public function set release (värde: Number): void // klämma ut släpptiden till intervallet 0.0 - 10.0 m_release = value < 0.0 ? 0.0 : value > 10,0? 10,0: värde; 

Audio.frequencyModulator

[Inline] Offentlig slutfunktion få frekvensModulator (): AudioModulator return m_frequencyModulator;  Offentlig slutfunktionsuppsättning frekvensModulator (värde: AudioModulator): void m_frequencyModulator = value; 

Audio.amplitudeModulator

[Inline] offentlig slutfunktion få amplitudeModulator (): AudioModulator return m_amplitudeModulator;  Offentlig slutfunktionsuppsättning Amplitudmodulator (värde: AudioModulator): void m_amplitudeModulator = value; 

Du märkte utan tvekan [I kö] metadatagången är bunden till några av getterfunktionerna. Den metadata-taggen är en glänsande ny egenskap hos Adobes senaste ActionScript 3.0-kompilator och det gör det som står på tennet: det inlines (expanderar) innehållet i en funktion. Detta är extremt användbart för optimering när det används förnuftigt, och generering av dynamiskt ljud vid körning är säkert något som kräver optimering.


AudioModulator Class

Syftet med AudioModulator är att tillåta amplituden och frekvensen av Audio instanser att moduleras för att skapa användbara och galen ljudeffekter. Modulatorer motsvarar faktiskt Audio instanser har de en vågform, en amplitud och en frekvens, men de producerar inte något ljud som de bara ändrar ljud.

Först första, skapa följande barebones klass i ljud paket:

paketljud public class AudioModulator public function AudioModulator () 

Låt oss nu lägga till privata privata egenskaper:

privat var m_waveform: int = AudioWaveform.SINE; privat var m_frequency: Number = 4.0; privat varamöjlighet: nummer = 1,0; privat var m_shift: Number = 0.0; privat var m_samples: Vector. = null;

Om du tänker ser det mycket ut som Audio klass då är du rätt: allt utom för flytta egendom är densamma.

För att förstå vad flytta egenskapen, tänk på en av de grundläggande vågformer som ljudmotorn använder (puls, sågtand, sinus eller triangel) och föreställ dig sedan en vertikal linje som löper rakt igenom vågformen i vilken position som helst. Den vertikala linjens horisontella position skulle vara flytta värde; det är ett värde inom intervallet 0,0 till 1,0 som berättar modulatorn där man börjar läsa sin vågform från och i sin tur kan ha en djup påverkan på modifikationerna som modulatorn gör till ljudets amplitud eller frekvens.

Som ett exempel, om modulatorn använde en sinusvågform för att modulera frekvensen för ett ljud och flytta satt till 0,0, ljudets frekvens skulle först stiga och då falla på grund av sinusvågens krökning. Men om flytta satt till 0,5 ljudets frekvens skulle först falla och sedan stiga.

Hur som helst, tillbaka till koden. De AudioModulator innehåller en intern metod som endast används av Audioengine; metoden är enligt följande:

[Inline] intern slutfunktionsprocess (tid: Nummer): Nummer var p: int = 0; var s: nummer = 0,0; om (m_shift! = 0.0) tid + = (1,0 / m_frekvens) * m_shift;  p = (44100 * m_frequency * tid)% 44100; s = m_samples [p]; returnera s * m_amplitude; 

Den funktionen är inline eftersom den används mycket, och när jag säger "mycket" menar jag 44100 gånger i sekund för varje ljud som spelar som har en modulator kopplad till den (det här är inlining blir otroligt värdefullt). Funktionen tar enkelt ett ljudprov från den vågform som modulatorn använder, justerar provets amplitud och returnerar sedan resultatet.

För att avsluta AudioModulator klass måste vi lägga till getters / setters:

AudioModulator.vågform

allmän funktion få vågform (): int return m_waveform;  public function set waveform (värde: int): void if (AudioWaveform.isValid (value) == false) return;  switch (värde) case AudioWaveform.PULSE: m_samples = AudioEngine.PULSE; ha sönder; fallet AudioWaveform.SAWTOOTH: m_samples = AudioEngine.SAWTOOTH; ha sönder; fallet AudioWaveform.SINE: m_samples = AudioEngine.SINE; ha sönder; fallet AudioWaveform.TRIANGLE: m_samples = AudioEngine.TRIANGLE; ha sönder;  m_waveform = värde; 

AudioModulator.frekvens

allmän funktion få frekvens (): Nummer return m_frequency;  public function set frequency (värde: Number): void // klämma frekvensen till intervallet 0.01 - 100.0 m_frequency = value < 0.01 ? 0.01 : value > 100,0? 100,0: värde; 

AudioModulator.amplitud

allmän funktion få amplitud (): nummer return m_amplitude;  public function set amplitud (värde: Number): void // kläm amplituden till intervallet 0.0 - 8000.0 m_amplitude = value < 0.0 ? 0.0 : value > 8000.0? 8000.0: värde; 

AudioModulator.flytta

allmän funktion få skift (): nummer return m_shift;  public function set shift (värde: Number): void // klämma överföringen till intervallet 0.0 - 1.0 m_shift = värde < 0.0 ? 0.0 : value > 1,0? 1,0: värde; 

Och det sveper upp AudioModulator klass.


AudioEngine Class

Nu för den stora: den Audioengine klass. Detta är en allstatisk klass och hanterar nästan allt relaterat till Audio instanser och ljudgenerering.

Låt oss börja med en barebones klass i ljud paket som vanligt:

paketbuller import flash.events.SampleDataEvent; importera flash.media.Sound; importera flash.media.SoundChannel; importera flash.utils.ByteArray; // offentliga slutklassen AudioEngine public function AudioEngine () kasta nytt fel ("AudioEngine-klassen kan inte instanseras"); 

Som tidigare nämnts bör allstatiska klasser inte ordnas, det undantag som kastas i klasskonstruktören om någon försöker att instansera klassen. Klassen är också slutlig eftersom det finns ingen anledning att förlänga en allstatisk klass.

De första sakerna som läggs till i denna klass är interna konstanter. Dessa konstanter kommer att användas för att cache proverna för var och en av de fyra vågformer som ljudmotorn använder. Varje cache innehåller 44 100 prov som motsvarar en hertz vågform. Detta gör det möjligt för ljudmotorn att producera riktigt rena lågfrekventa ljudvågor.

Konstanterna är följande:

statisk inre const PULSE: Vector. = ny vektor.(44100); statisk inre const SAWTOOTH: Vector. = ny vektor.(44100); statisk inre const SINE: Vector. = ny vektor.(44100); statisk inre const TRIANGLE: Vector. = ny vektor.(44100);

Det finns också två privata konstanter som används av klassen:

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

De BUFFER_SIZE är antalet ljudprover som skickas till ActionScript 3.0-ljud API närhelst en begäran om ljudprover görs. Detta är det minsta antalet prover tillåtna och det resulterar i lägsta möjliga ljudfördröjning. Antalet prover kan ökas för att minska CPU-användningen, men det skulle öka ljudfördröjningen. De SAMPLE_TIME är varaktigheten för ett enda ljudprov, i sekunder.

Och nu för de privata variablerna:

statisk privat varamposition: Number = 0.0; statisk privat varamamplitude: Number = 0.5; statisk privat var m_soundStream: Ljud = null; statisk privat var m_soundChannel: SoundChannel = null; statisk privat var m_audioList: Vector.
  • De m_position används för att hålla reda på ljudströmmen, i sekunder.
  • De m_amplitude är en global sekundär amplitud för alla Audio instanser som spelar.
  • De m_soundStream och m_soundChannel bör inte behöva någon förklaring.
  • De m_audioList innehåller referenser till någon Audio instanser som spelar.
  • De m_sampleList är en tillfällig buffert som används för att lagra ljudprover när de begärs av ActionScript 3.0-ljud API.

Nu måste vi initiera klassen. Det finns många sätt att göra detta men jag föredrar något snyggt och enkelt, en statisk klasskonstruktör:

statisk privat funktion $ AudioEngine (): void var i: int = 0; var n: int = 44100; var p: Nummer = 0,0; // medan jag < 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();

Om du har läst den föregående handledningen i den här serien ser du förmodligen vad som händer i den koden: proverna för var och en av de fyra vågformerna genereras och cachas, och detta händer bara en gång. Ljudströmmen blir också instansierad och startad och körs kontinuerligt tills appen avslutas.

De Audioengine klassen har tre offentliga metoder som används för att spela och sluta Audio instanser:

Audioengine.spela()

statisk public function play (ljud: ljud): void if (audio.playing == false) m_audioList.push (ljud);  // det här låter oss veta exakt när ljudet startades audio.position = m_position - (m_soundChannel.position * 0.001); audio.playing = true; audio.releasing = false; 

Audioengine.sluta()

statisk allmän funktion stopp (ljud: Ljud, allowRelease: Boolean = true): void if (audio.playing == false) // ljudet spelas inte tillbaka;  om (allowRelease) // hoppa till slutet av ljudet och flagga det som att släppa ljud.position = audio.duration; audio.releasing = true; lämna tillbaka;  audio.playing = false; audio.releasing = false; 

Audioengine.stopAll ()

statisk allmän funktion stopAll (allowRelease: Boolean = true): void var i: int = 0; var n: int = m_audioList.length; var o: Ljud = null; // om (allowRelease) while (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++;  

Och här kommer de viktigaste ljudbehandlingsmetoderna, som båda är privata:

Audioengine.onSampleData ()

statisk privat funktion onSampleData (händelse: SampleDataEvent): void var i: int = 0; var n: int = BUFFER_SIZE; var s: nummer = 0,0; var b: ByteArray = event.data; // om (m_soundChannel == null) while (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örsta om uttalande vi kontrollerar om m_soundChannel är fortfarande noll, och vi måste göra det för att STICKPROV händelsen skickas så snart som m_soundStream.play () Metoden åberopas, och innan metoden får en chans att returnera a Soundchannel exempel.

De medan loop rullar genom de ljudprover som har begärts av m_soundStream och skriver dem till den tillhandahållna Bytearray exempel. Ljudproverna genereras enligt följande metod:

Audioengine.generateSamples ()

statisk privat funktion genereraSamples (): 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: nummer = 0,0; var a: nummer = 0,0; var s: nummer = 0,0; var o: Ljud = null; // rulla igenom ljudinstanserna medan (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) om (o.position> = o.duration + o.release) // ljudinstansen har slutat o.playing = false; j ++; Fortsätta;  // ljudinstansen släpper o.releasing = true;  // Hämta ljudinstansens frekvens och amplitud f = o.frequency; a = o.amplitude; // om (o.frequencyModulator! = null) // modulera frekvensen f + = o.frequencyModulator.process (o.position);  // om (o.amplitudeModulator! = null) // modulera amplituden a + = o.amplitudeModulator.process (o.position);  // beräkna läget inom vågform cachen p = (44100 * f * o.position)% 44100; // ta vågformprovet s = o.samples [p]; // om (o.releasing) // beräkna fade-out amplituden för provet s * = 1.0 - ((o.position - o.duration) / o.release);  // lägg provet till bufferten m_sampleList [j] + = s * a; // Uppdatera ljudinstansens position o.position + = SAMPLE_TIME; j ++;  i ++; 

Slutligen, för att avsluta saker, måste vi lägga till getter / setter för den privata m_amplitude variabel:

statisk offentlig funktion få amplitud (): nummer return m_amplitude;  statisk allmän funktionsuppsättning amplitud (värde: antal): void // klämma amplituden till intervallet 0,0-1,0 m_amplitude = värde < 0.0 ? 0.0 : value > 1,0? 1,0: värde; 

Och nu behöver jag en paus!


Kommer upp…

I den tredje och sista handledningen i serien lägger vi till ljudprocessorer den till ljudmotor. Dessa kommer att tillåta oss att trycka alla genererade ljudproverna, trots bearbetningsenheter som hårda begränsningar och förseningar. Vi kommer också att titta på hela koden för att se om något kan optimeras.

All källkod för denna handledningsserie kommer att bli tillgänglig med nästa handledning.

Följ oss på Twitter, Facebook eller Google+ för att hålla dig uppdaterad med de senaste inläggen.