Ruis een synthesizer maken voor retro geluidseffecten - kernmotor

Dit is de tweede in een reeks tutorials waarin we een op synthesizer gebaseerde audiomachine zullen maken die geluiden voor retro-gestileerde games kan genereren. De audio-engine genereert alle geluiden tijdens de uitvoering zonder dat externe afhankelijkheden nodig zijn, zoals MP3-bestanden of WAV-bestanden. Het eindresultaat zal een werkbibliotheek zijn die moeiteloos in uw games kan worden geplaatst.

Als u de eerste zelfstudie in deze serie nog niet hebt gelezen, moet u dat doen voordat u doorgaat.

De programmeertaal die in deze zelfstudie wordt gebruikt, is ActionScript 3.0, maar de gebruikte technieken en concepten kunnen eenvoudig worden vertaald in elke andere programmeertaal die een low-levelgeluid-API biedt.

U moet ervoor zorgen dat Flash Player 11.4 of hoger is geïnstalleerd voor uw browser als u de interactieve voorbeelden in deze zelfstudie wilt gebruiken.


Demo audio-engine

Aan het einde van deze tutorial is alle kerncode die vereist is voor de audio-engine voltooid. Het volgende is een eenvoudige demonstratie van de audiomotor in actie.

Er wordt slechts één geluid in die demonstratie gespeeld, maar de frequentie van het geluid wordt willekeurig samen met de releasetijd. Het geluid heeft ook een modulator die eraan vastzit om het vibrato-effect te produceren (de amplitude van het geluid moduleren) en de frequentie van de modulator wordt ook willekeurig.


AudioWaveform-klasse

De eerste klasse die we zullen maken, houdt eenvoudig constante waarden vast voor de golfvormen die de audiomachine zal gebruiken om de hoorbare geluiden te genereren.

Begin met het maken van een nieuw klassepakket genaamd lawaai, en voeg dan de volgende klasse toe aan dat pakket:

package noise public final class AudioWaveform static public const PULSE: int = 0; statische openbare const SAWTOOTH: int = 1; statische openbare const SINE: int = 2; static public const DRIEHOEK: int = 3; 

We voegen ook een statische openbare methode toe aan de klasse die kan worden gebruikt om een ​​golfvormwaarde te valideren, de methode zal terugkeren waar of vals om aan te geven of de golfvormwaarde al dan niet geldig is.

static public function validate (waveform: int): Boolean if (waveform == PULSE) return true; if (golfvorm == SAWTOOTH) retourneren waar; if (golfvorm == SINE) return true; if (golfvorm == DRIEHOEK) geeft waar terug; return false; 

Ten slotte moeten we voorkomen dat de klasse wordt geïnstantieerd omdat er geen reden is dat iemand instanties van deze klasse maakt. We kunnen dit binnen de klassenbouwer doen:

public function AudioWaveform () throw new Error ("AudioWaveform class can not geinsiated"); 

Deze les is nu voltooid.

Voorkomen dat klassen in opsumstijl, all-statische klassen en singleton-klassen direct worden geïnstantieerd, is een goede zaak omdat deze klassen niet moeten worden geïnstantieerd; er is geen reden om ze te instantiëren. Programmeertalen zoals Java doen dit automatisch voor de meeste van deze klassen, maar momenteel moeten we dit gedrag in ActionScript 3.0 handmatig afdwingen binnen de klasseconstructor.


Audioklasse

Volgende op de lijst is de audio klasse. Deze klasse heeft een vergelijkbare aard als de oorspronkelijke ActionScript 3.0 Geluid klasse: elk geluid van de audiomotor wordt weergegeven met een audio klasse instantie.

Voeg de volgende barebones-klasse toe aan de lawaai pakket:

pakketruis public class Audio openbare functie Audio () 

De eerste dingen die aan de klasse moeten worden toegevoegd, zijn eigenschappen die de audiomachine vertellen hoe de geluidsgolf moet worden gegenereerd wanneer het geluid wordt afgespeeld. Deze eigenschappen omvatten het type golfvorm dat wordt gebruikt door het geluid, de frequentie en amplitude van de golfvorm, de duur van het geluid en de releasetijd (hoe snel deze uitfaden). Al deze eigenschappen zullen privé zijn en toegankelijk via getters / setters:

private var m_waveform: int = AudioWaveform.PULSE; private var m_frequency: Number = 100.0; private var m_amplitude: Number = 0.5; private var m_duration: Number = 0.2; private var m_release: Number = 0.2;

Zoals u kunt zien, hebben we voor elke eigenschap een redelijke standaardwaarde ingesteld. De amplitude is een waarde in het bereik 0.0 naar 1.0, de frequentie is in Hertz, en de looptijd en vrijlating tijden zijn in seconden.

We moeten ook nog twee private eigenschappen toevoegen voor de modulators die aan het geluid kunnen worden gekoppeld; opnieuw zijn deze eigenschappen toegankelijk via getters / setters:

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

eindelijk, de audio klasse zal enkele interne eigenschappen bevatten die alleen door de AudioEngine klasse (we zullen die klasse binnenkort aanmaken). Deze eigenschappen hoeven niet verborgen te zijn achter getters / setters:

interne var-positie: aantal = 0,0; interne var-weergave: Boolean = false; interne var releasing: Boolean = false; interne var-monsters: Vector. = null;

De positie is in seconden en het staat de AudioEngine klasse om de positie van het geluid bij te houden terwijl het geluid wordt afgespeeld, dit is nodig om de geluidsgolven van de golfvorm voor het geluid te berekenen. De spelen en vrijgeven eigenschappen vertellen het AudioEngine in welke staat het geluid is, en de samples eigenschap is een verwijzing naar de in cache opgeslagen golfvormmonsters die door het geluid worden gebruikt. Het gebruik van deze eigenschappen zal duidelijk worden wanneer we de AudioEngine klasse.

Om het te voltooien audio klasse moeten we de getters / setters toevoegen:

audio.golfvorm

public final function get waveform (): int return m_waveform;  publieke laatste functie set golfvorm (waarde: int): void if (AudioWaveform.isValid (waarde) == false) return;  switch (waarde) case AudioWaveform.PULSE: samples = AudioEngine.PULSE; breken; case AudioWaveform.SAWTOOTH: samples = AudioEngine.SAWTOOTH; breken; case AudioWaveform.SINE: samples = AudioEngine.SINE; breken; case AudioWaveform.TRIANGLE: samples = AudioEngine.TRIANGLE; breken;  m_waveform = waarde; 

audio.frequentie

[Inline] public final function get frequency (): Number return m_frequency;  public final function set frequency (waarde: Number): void // clamp the frequency to the range 1.0 - 14080.0 m_frequency = value < 1.0 ? 1.0 : value > 14080.0? 14080.0: waarde; 

audio.amplitude

[Inline] public final function get amplitude (): Number return m_amplitude;  public final function set amplitude (waarde: Number): void // clamp the amplitude to the range 0.0 - 1.0 m_amplitude = value < 0.0 ? 0.0 : value > 1.0? 1.0: waarde; 

audio.looptijd

[Inline] public final function get duration (): Number return m_duration;  public final function set duration (value: Number): void // clamp the duration to the range 0.0 - 60.0 m_duration = value < 0.0 ? 0.0 : value > 60.0? 60.0: waarde; 

audio.vrijlating

[Inline] public final function get release (): Number return m_release;  public function set release (value: Number): void // klem de releasetijd op het bereik 0.0 - 10.0 m_release = waarde < 0.0 ? 0.0 : value > 10.0? 10.0: waarde; 

audio.frequencyModulator

[Inline] public final function get frequencyModulator (): AudioModulator return m_frequencyModulator;  public final function set frequencyModulator (waarde: AudioModulator): void m_frequencyModulator = value; 

audio.amplitudeModulator

[Inline] public final function krijgt amplitudeModulator (): AudioModulator return m_amplitudeModulator;  public final function set amplitudeModulator (waarde: AudioModulator): void m_amplitudeModulator = value; 

U hebt ongetwijfeld de [In lijn] metadata tag gebonden aan enkele van de getter-functies. Die metagegevenstag is een glimmende nieuwe functie van Adobe's nieuwste ActionScript 3.0-compileerprogramma en doet wat op het etiket staat: het lijnt de inhoud van een functie in (breidt) uit. Dit is uiterst handig voor optimalisatie bij verstandig gebruik, en het genereren van dynamische audio tijdens runtime is zeker iets dat moet worden geoptimaliseerd.


AudioModulator-klasse

Het doel van de AudioModulator is om de amplitude en frequentie van te laten audio instanties die moeten worden gemoduleerd om nuttige en gekke geluidseffecten te creëren. Modulators zijn eigenlijk vergelijkbaar met audio instanties, ze hebben een golfvorm, een amplitude en frequentie, maar produceren geen echt hoorbaar geluid, ze passen alleen hoorbare geluiden aan.

Eerste ding eerst, maak de volgende barebones klasse in de lawaai pakket:

pakketruis public class AudioModulator public function AudioModulator () 

Laten we nu de privé-privé-eigenschappen toevoegen:

private var m_waveform: int = AudioWaveform.SINE; private var m_frequency: Number = 4.0; private var m_amplitude: Number = 1.0; private var m_shift: Number = 0.0; private var m_samples: Vector. = null;

Als je denkt dat dit erg lijkt op de audio klas dan heb je gelijk: alles behalve het verschuiving eigendom is hetzelfde.

Om te begrijpen wat de verschuiving property doet denken aan een van de basisgolfvormen die de audio-engine gebruikt (puls, zaagtand, sinus of driehoek) en stel je dan een verticale lijn voor die recht door de golfvorm loopt op elke positie die jij wilt. De horizontale positie van die verticale lijn zou de verschuiving waarde; het is een waarde in het bereik 0.0 naar 1.0 die de modulator vertelt waar de golfvorm van begint te lezen en die op zijn beurt een diepgaand effect kan hebben op de modificaties die de modulator maakt op de amplitude of frequentie van een geluid.

Als de modulator bijvoorbeeld een sinusgolfvorm gebruikte om de frequentie van een geluid te moduleren, en de verschuiving was ingesteld op 0.0, de frequentie van het geluid zou eerst stijgen en vervolgens dalen als gevolg van de kromming van de sinusgolf. Echter, als het verschuiving was ingesteld op 0.5 de frequentie van het geluid zou eerst dalen en dan stijgen.

Hoe dan ook, terug naar de code. De AudioModulator bevat een interne methode die alleen wordt gebruikt door de AudioEngine; de methode is als volgt:

[Inline] intern definitief functieproces (tijd: Getal): Getal var p: int = 0; var s: Number = 0.0; if (m_shift! = 0.0) time + = (1.0 / m_frequency) * m_shift;  p = (44100 * m_frequency * time)% 44100; s = m_samples [p]; return s * m_amplitude; 

Die functie is inline omdat het veel wordt gebruikt, en wanneer ik "veel" zeg, bedoel ik 44100 keer per seconde voor elk geluid dat speelt met een modulator eraan (dit is waar inlining ongelooflijk waardevol wordt). De functie pakt eenvoudig een geluidsvoorbeeld uit de golfvorm die de modulator gebruikt, past de amplitude van dat sample aan en retourneert vervolgens het resultaat.

Om het te voltooien AudioModulator klasse moeten we de getters / setters toevoegen:

AudioModulator.golfvorm

public function get waveform (): int return m_waveform;  openbare functieset golfvorm (waarde: int): void if (AudioWaveform.isValid (waarde) == false) return;  switch (waarde) case AudioWaveform.PULSE: m_samples = AudioEngine.PULSE; breken; case AudioWaveform.SAWTOOTH: m_samples = AudioEngine.SAWTOOTH; breken; case AudioWaveform.SINE: m_samples = AudioEngine.SINE; breken; case AudioWaveform.TRIANGLE: m_samples = AudioEngine.TRIANGLE; breken;  m_waveform = waarde; 

AudioModulator.frequentie

public function get frequency (): Number return m_frequency;  public function set frequency (waarde: Number): void // clamp the frequency to the range 0.01 - 100.0 m_frequency = value < 0.01 ? 0.01 : value > 100.0? 100.0: waarde; 

AudioModulator.amplitude

public function get amplitude (): Number return m_amplitude;  public function set amplitude (waarde: Number): void // clamp the amplitude to the range 0.0 - 8000.0 m_amplitude = value < 0.0 ? 0.0 : value > 8000.0? 8000.0: waarde; 

AudioModulator.verschuiving

public function get shift (): Number return m_shift;  publieke functie set shift (waarde: Number): void // klem de shift naar het bereik 0.0 - 1.0 m_shift = waarde < 0.0 ? 0.0 : value > 1.0? 1.0: waarde; 

En dat wikkelt de AudioModulator klasse.


AudioEngine-klasse

Nu voor de grote: de AudioEngine klasse. Dit is een all-static klasse en beheert vrijwel alles met betrekking tot audio instanties en geluidsgeneratie.

Laten we beginnen met een barebones-klasse in de lawaai pakket zoals gebruikelijk:

pakketruis import flash.events.SampleDataEvent; import flash.media.Sound; import flash.media.SoundChannel; import flash.utils.ByteArray; // openbare afsluitende klasse AudioEngine openbare functie AudioEngine () nieuwe fout toevoegen ("AudioEngine-klasse kan niet worden geïnstantieerd"); 

Zoals eerder vermeld, mogen allstatische klassen niet worden geïnstantieerd, vandaar de uitzondering die in de klasse-constructor wordt gegooid als iemand de klasse probeert te instantiëren. De klas is ook laatste omdat er geen reden is om een ​​all-static klasse uit te breiden.

De eerste dingen die aan deze klasse worden toegevoegd, zijn interne constanten. Deze constanten worden gebruikt om de samples te cachen voor elk van de vier golfvormen die de audio-engine gebruikt. Elke cache bevat 44.100 samples, wat overeenkomt met één hertz-golfvorm. Hierdoor kan de audiomachine echt schone, lage frequentie geluidsgolven produceren.

De constanten zijn als volgt:

static internal const PULSE: Vector. = nieuwe Vector.(44100); static internal const SAWTOOTH: Vector. = nieuwe Vector.(44100); statische interne const SINE: Vector. = nieuwe Vector.(44100); static internal const DRIEHOEK: Vector. = nieuwe Vector.(44100);

Er zijn ook twee privéconstanten die door de klas worden gebruikt:

static private const BUFFER_SIZE: int = 2048; static private const SAMPLE_TIME: Number = 1.0 / 44100.0;

De BUFFER GROOTTE is het aantal geluidsfragmenten dat wordt doorgegeven aan de ActionScript 3.0-geluid-API wanneer er een verzoek voor geluidssamples wordt gemaakt. Dit is het kleinste aantal toegestane samples en resulteert in de laagst mogelijke latentie van het geluid. Het aantal samples kan worden verhoogd om het CPU-gebruik te verminderen, maar dat zou de geluidswachttijd verhogen. De PROEFTIJD is de duur van een enkel geluidsvoorbeeld, in seconden.

En nu voor de privévariabelen:

static private var m_position: Number = 0.0; static private var m_amplitude: Number = 0.5; static private var m_soundStream: Sound = null; static private var m_soundChannel: SoundChannel = null; static private var m_audioList: Vector.
  • De m_position wordt gebruikt om de tijd van de geluidstream in seconden bij te houden.
  • De m_amplitude is een globale secundaire amplitude voor alle audio instanties die worden afgespeeld.
  • De m_soundStream en m_soundChannel geen uitleg nodig.
  • De m_audioList bevat verwijzingen naar welke dan ook audio instanties die worden afgespeeld.
  • De m_sampleList is een tijdelijke buffer die wordt gebruikt om geluidssamples op te slaan wanneer deze worden aangevraagd door de Sound API van ActionScript 3.0.

Nu moeten we de klas initialiseren. Er zijn verschillende manieren om dit te doen, maar ik geef de voorkeur aan iets leuks en eenvoudigs, een statische klassenbouwer:

statische persoonlijke functie $ AudioEngine (): void var i: int = 0; var n: int = 44100; var p: Number = 0.0; // terwijl ik < 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();

Als je de vorige tutorial in deze serie hebt gelezen, zul je waarschijnlijk zien wat er in die code gebeurt: de samples voor elk van de vier waveforms worden gegenereerd en in de cache opgeslagen, en dit gebeurt maar één keer. De geluidsstroom wordt ook geïnstantieerd en gestart en zal continu worden uitgevoerd totdat de app wordt beëindigd.

De AudioEngine klasse heeft drie openbare methoden die worden gebruikt om te spelen en te stoppen audio voorbeelden:

AudioEngine.spelen()

statische openbare functie afspelen (audio: audio): void if (audio.playing == false) m_audioList.push (audio);  // dit stelt ons in staat precies te weten wanneer het geluid is gestart. audio.position = m_position - (m_soundChannel.position * 0.001); audio.playing = waar; audio.releasing = false; 

AudioEngine.hou op()

statische openbare functie stop (audio: Audio, allowRelease: Boolean = true): void if (audio.playing == false) // het geluid speelt geen return af;  if (allowRelease) // ga naar het einde van het geluid en markeer het als het vrijgeven van audio.position = audio.duration; audio.releasing = true; terug te keren;  audio.playing = false; audio.releasing = false; 

AudioEngine.stop alles()

statische openbare functie stopAll (allowRelease: Boolean = true): void var i: int = 0; var n: int = m_audioList.length; var o: Audio = null; // if (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++;  

En hier komen de belangrijkste audiobewerkingsmethoden, die beide privé zijn:

AudioEngine.onSampleData ()

static private function onSampleData (event: SampleDataEvent): void var i: int = 0; var n: int = BUFFER_SIZE; var s: Number = 0.0; var b: ByteArray = event.data; // if (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; 

Dus in de eerste als verklaring we controleren of het m_soundChannel is nog steeds null, en we moeten dat doen omdat het SAMPLE_DATA evenement wordt verzonden zodra de m_soundStream.play () methode wordt aangeroepen, en voordat de methode de kans krijgt om een SoundChannel aanleg.

De terwijl loop rolt door de geluidsbestanden die zijn aangevraagd door m_soundStream en schrijft ze naar de voorziene ByteArray aanleg. De geluidsfragmenten worden op de volgende manier gegenereerd:

AudioEngine.generateSamples ()

static private function generateSamples (): 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: Number = 0.0; var a: Number = 0.0; var s: Number = 0.0; var o: Audio = null; // rol door de audio-instanties terwijl (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) if (o.position> = o.duration + o.release) // de audio-instantie is gestopt o.playing = false; j ++; doorgaan met;  // de audio-instantie komt vrij o.releasing = true;  // grijp de frequentie en de amplitude van de audio-instantie f = o.frequency; a = o.amplitude; // if (o.frequencyModulator! = null) // moduleer de frequentie f + = o.frequencyModulator.process (o.position);  // if (o.amplitudeModulator! = null) // moduleer de amplitude a + = o.amplitudeModulator.process (o.position);  // bereken de positie binnen de golfvormcache p = (44100 * f * o.positie)% 44100; // pak het golfvormmonster s = o.samples [p]; // if (o.releasing) // bereken de fade-out-amplitude voor het monster s * = 1.0 - ((o.position - o.duration) / o.release);  // voeg het voorbeeld toe aan de buffer m_sampleList [j] + = s * a; // update de positie van de audio-instantie o.position + = SAMPLE_TIME; j ++;  i ++; 

Tot slot, om dingen af ​​te maken, moeten we de getter / setter voor privé toevoegen m_amplitude variabele:

statische openbare functie krijgt amplitude (): Number return m_amplitude;  statische public function set amplitude (waarde: Number): void // clamp the amplitude to the range 0.0 - 1.0 m_amplitude = value < 0.0 ? 0.0 : value > 1.0? 1.0: waarde; 

En nu heb ik een pauze nodig!


Binnenkort…

In de derde en laatste tutorial in de serie zullen we toevoegen audio-processors de naar audio-engine. Hiermee kunnen we alle gegenereerde geluidssamples pushen via verwerkingseenheden zoals harde limiters en vertragingen. We zullen ook de hele code bekijken om te zien of iets kan worden geoptimaliseerd.

Alle broncode voor deze zelfstudiereeks wordt beschikbaar gemaakt bij de volgende zelfstudie.

Volg ons op Twitter, Facebook of Google+ om op de hoogte te blijven van de laatste berichten.