Noise Erstellen eines Synthesizers für Retro-Soundeffekte - Core Engine

Dies ist das zweite einer Reihe von Tutorials, in denen wir eine auf Synthesizer basierende Audio-Engine erstellen werden, die Sounds für Spiele im Retro-Stil erzeugen kann. Die Audio-Engine erzeugt zur Laufzeit alle Sounds, ohne dass externe Abhängigkeiten wie MP3-Dateien oder WAV-Dateien erforderlich sind. Das Endergebnis wird eine funktionierende Bibliothek sein, die mühelos in Ihre Spiele eingefügt werden kann.

Wenn Sie das erste Tutorial dieser Serie noch nicht gelesen haben, sollten Sie dies tun, bevor Sie fortfahren.

Die in diesem Lernprogramm verwendete Programmiersprache ist ActionScript 3.0. Die verwendeten Techniken und Konzepte können jedoch problemlos in jede andere Programmiersprache übersetzt werden, die eine Low-Level-Sound-API bietet.

Sie sollten sicherstellen, dass Flash Player 11.4 oder höher für Ihren Browser installiert ist, wenn Sie die interaktiven Beispiele in diesem Lernprogramm verwenden möchten.


Audio Engine Demo

Am Ende dieses Tutorials ist der gesamte für die Audio-Engine erforderliche Kerncode abgeschlossen. Das Folgende ist eine einfache Demonstration der Audio-Engine in Aktion.

In dieser Demonstration wird nur ein Sound gespielt, aber die Frequenz des Sounds wird zusammen mit der Veröffentlichungszeit randomisiert. An den Sound ist auch ein Modulator angeschlossen, um den Vibrato-Effekt zu erzeugen (die Amplitude des Sounds zu modulieren), und die Frequenz des Modulators wird ebenfalls randomisiert.


AudioWaveform-Klasse

Die erste Klasse, die wir erstellen werden, enthält einfach konstante Werte für die Wellenformen, die die Audio-Engine zum Erzeugen der hörbaren Sounds verwendet.

Erstellen Sie zunächst ein neues Klassenpaket namens Lärm, Fügen Sie dem Paket dann die folgende Klasse hinzu:

Paketrauschen public final class AudioWaveform statisch public const PULSE: int = 0; statische public const SAWTOOTH: int = 1; statische öffentliche const SINE: int = 2; statisches öffentliches const TRIANGLE: int = 3; 

Wir fügen der Klasse auch eine statische öffentliche Methode hinzu, die zum Validieren eines Wellenformwerts verwendet werden kann. Die Methode wird zurückgegeben wahr oder falsch um anzuzeigen, ob der Wellenformwert gültig ist.

statische öffentliche Funktion validate (Wellenform: int): Boolean if (Wellenform == PULSE) gibt true zurück. if (Wellenform == SAWTOOTH) gibt true zurück; if (Wellenform == SINE) gibt true zurück; if (Wellenform == DREIECK) gibt true zurück; falsch zurückgeben; 

Schließlich sollten wir verhindern, dass die Klasse instanziiert wird, da für niemanden Grund besteht, Instanzen dieser Klasse zu erstellen. Wir können dies innerhalb des Klassenkonstruktors tun:

public function AudioWaveform () werfen neuen Fehler ("AudioWaveform-Klasse kann nicht instanziiert werden"); 

Diese Klasse ist jetzt abgeschlossen.

Die direkte Instanziierung von Klassen mit Klassenumgebungen, statischen Klassen und Singleton-Klassen zu verhindern, ist eine gute Sache, da diese Arten von Klassen nicht instanziiert werden sollten. Es gibt keinen Grund, sie zu instanziieren. Programmiersprachen wie Java führen dies für die meisten dieser Klassentypen automatisch durch. In ActionScript 3.0 müssen wir dieses Verhalten jedoch manuell im Klassenkonstruktor erzwingen.


Audioklasse

Weiter auf der Liste ist das Audio Klasse. Diese Klasse ähnelt dem nativen ActionScript 3.0 Klingen Klasse: Jeder Sound einer Audio-Engine wird durch eine Audio Klasseninstanz.

Fügen Sie der folgenden Barebones-Klasse hinzu Lärm Paket:

Paketrauschen public class Audio öffentliche Funktion Audio () 

Die ersten Dinge, die der Klasse hinzugefügt werden müssen, sind Eigenschaften, die der Audio-Engine sagen, wie die Schallwelle erzeugt wird, wenn der Sound abgespielt wird. Diese Eigenschaften umfassen den Typ des Signals, das vom Sound verwendet wird, die Frequenz und Amplitude des Signals, die Dauer des Sounds und seine Release-Zeit (wie schnell sie ausgeblendet wird). Alle diese Eigenschaften sind privat und werden über Getter / Setter abgerufen:

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

Wie Sie sehen, haben wir für jede Eigenschaft einen sinnvollen Standardwert festgelegt. Das Amplitude ist ein Wert im Bereich 0,0 zu 1,0, das Frequenz ist in Hertz, und die Dauer und Veröffentlichung Zeiten sind in Sekunden.

Wir müssen auch zwei weitere private Eigenschaften für die Modulatoren hinzufügen, die an den Sound angehängt werden können. Diese Eigenschaften werden wieder über Getter / Setter abgerufen:

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

Endlich, das Audio Diese Klasse enthält einige interne Eigenschaften, auf die nur die Klasse zugreifen kann AudioEngine Klasse (wir werden diese Klasse in Kürze erstellen). Diese Eigenschaften müssen nicht hinter Getter / Setter verborgen werden:

interne var Position: Anzahl = 0.0; interne Var-Wiedergabe: Boolean = false; interne var-Freigabe: Boolean = false; interne var-proben: Vektor. = null;

Das Position ist in Sekunden und erlaubt das AudioEngine Um die Position des Sounds während der Wiedergabe zu verfolgen, ist dies erforderlich, um die Wellenform-Sound-Samples für den Sound zu berechnen. Das spielen und freigeben Eigenschaften sagen der AudioEngine In welchem ​​Zustand ist der Ton und Proben Diese Eigenschaft ist eine Referenz auf die zwischengespeicherten Wellenform-Samples, die der Sound verwendet. Die Verwendung dieser Eigenschaften wird klar, wenn wir das erstellen AudioEngine Klasse.

Um das zu beenden Audio Klasse müssen wir die Getter / Setter hinzufügen:

Audio.Wellenform

öffentliche letzte Funktion get wave (): int return m_waveform;  public final function set wave (value: int): void if (AudioWaveform.isValid (value) == false) return;  switch (Wert) case AudioWaveform.PULSE: samples = AudioEngine.PULSE; brechen; case AudioWaveform.SAWTOOTH: Samples = AudioEngine.SAWTOOTH; brechen; case AudioWaveform.SINE: Samples = AudioEngine.SINE; brechen; case AudioWaveform.TRIANGLE: Samples = AudioEngine.TRIANGLE; brechen;  m_waveform = Wert; 

Audio.Frequenz

[Inline] öffentliche letzte Funktion get frequency (): Number return m_frequency;  public final function set frequency (Wert: Number): void // klemmt die Frequenz auf den Bereich 1.0 - 14080.0 m_frequency = value < 1.0 ? 1.0 : value > 14080.0? 14080.0: Wert; 

Audio.Amplitude

[Inline] öffentliche letzte Funktion get amplitude (): Number return m_amplitude;  public final function set amplitude (value: Number): void // klemmt die Amplitude auf den Bereich 0,0 - 1,0 m_amplitude = value < 0.0 ? 0.0 : value > 1,0? 1,0: Wert; 

Audio.Dauer

[Inline] öffentliche letzte Funktion get duration (): Number return m_duration;  public final function set duration (value: Number): void // klemmt die Dauer auf den Bereich 0.0 - 60.0 m_duration = value < 0.0 ? 0.0 : value > 60,0? 60,0: Wert; 

Audio.Veröffentlichung

[Inline] öffentliche abschließende Funktion get release (): Number return m_release;  public function set release (value: Number): void // klemmt die Releasezeit auf den Bereich 0,0 - 10,0 m_release = value < 0.0 ? 0.0 : value > 10,0? 10,0: Wert; 

Audio.frequencyModulator

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

Audio.Amplitudenmodulator

[Inline] öffentliche letzte Funktion get amplitudeModulator (): AudioModulator return m_amplitudeModulator;  public final function set amplitudeModulator (Wert: AudioModulator): void m_amplitudeModulator = Wert; 

Sie haben das zweifellos bemerkt [In der Reihe] Metadaten-Tag, das an einige Getter-Funktionen gebunden ist. Dieses Metadaten-Tag ist eine glänzende neue Funktion des neuesten ActionScript 3.0-Compilers von Adobe und funktioniert wie es aussieht: Es wird der Inhalt einer Funktion eingebettet (erweitert). Dies ist äußerst nützlich für die Optimierung, wenn es sinnvoll eingesetzt wird. Die Erzeugung von dynamischem Audio zur Laufzeit ist sicherlich etwas, das optimiert werden muss.


AudioModulator-Klasse

Der Zweck der AudioModulator ist die Amplitude und Frequenz von Audio Instanzen, die moduliert werden müssen, um nützliche und verrückte Soundeffekte zu erzeugen. Modulatoren sind eigentlich ähnlich Audio Sie haben zwar eine Wellenform, eine Amplitude und eine Frequenz, erzeugen jedoch keinen hörbaren Klang, sondern modifizieren nur hörbare Geräusche.

Zuerst erstellen Sie die folgende Barebones-Klasse in der Lärm Paket:

Paketrauschen public class AudioModulator öffentliche Funktion AudioModulator () 

Nun fügen wir die privaten privaten Eigenschaften hinzu:

private var m_waveform: int = AudioWaveform.SINE; private var m_frequency: Anzahl = 4,0; private var m_amplitude: Anzahl = 1,0; private var m_shift: Number = 0.0; private var m_samples: Vektor. = null;

Wenn Sie denken, sieht das sehr ähnlich aus Audio klasse dann bist du richtig: alles außer dem Verschiebung Eigenschaft ist das gleiche.

Um zu verstehen, was die Verschiebung Denken Sie an eine der grundlegenden Wellenformen, die die Audio-Engine verwendet (Impuls, Sägezahn, Sinus oder Dreieck), und stellen Sie sich eine vertikale Linie vor, die an jeder beliebigen Position durch die Wellenform verläuft. Die horizontale Position dieser vertikalen Linie wäre die Verschiebung Wert; Es ist ein Wert im Bereich 0,0 zu 1,0 Dies gibt dem Modulator an, wo er mit dem Lesen seiner Wellenform beginnen soll, und kann wiederum tiefgreifende Auswirkungen auf die Änderungen haben, die der Modulator an der Amplitude oder Frequenz eines Sounds vornimmt.

Wenn der Modulator beispielsweise eine Sinus-Wellenform verwendet, um die Frequenz eines Tons zu modulieren, und die Verschiebung wurde an eingestellt 0,0, Die Frequenz des Klanges würde zuerst steigen und dann aufgrund der Krümmung der Sinuswelle abfallen. Wenn jedoch die Verschiebung wurde an eingestellt 0,5 Die Klangfrequenz würde zuerst fallen und dann steigen.

Wie auch immer, zurück zum Code. Das AudioModulator enthält eine interne Methode, die nur vom verwendet wird AudioEngine; Die Methode ist wie folgt:

[Inline] interner abschließender Funktionsprozess (time: Number): Number var p: int = 0; var s: Anzahl = 0,0; if (m_shift! = 0,0) Zeit + = (1,0 / m_Frequenz) * m_shift;  p = (44100 * m_Frequenz * Zeit)% 44100; s = m_samples [p]; return s * m_amplitude; 

Diese Funktion ist inline, weil sie häufig verwendet wird. Wenn ich "viel" sage, dann meine ich 44100 Mal pro Sekunde für jeden Sound, an dem ein Modulator angeschlossen ist (hier wird Inlining unglaublich wertvoll). Die Funktion erfasst einfach ein Sound-Sample aus der vom Modulator verwendeten Wellenform, passt die Amplitude dieses Samples an und gibt das Ergebnis zurück.

Um das zu beenden AudioModulator Klasse müssen wir die Getter / Setter hinzufügen:

AudioModulator.Wellenform

public function get wave (): int return m_waveform;  public function set wave (value: int): void if (AudioWaveform.isValid (value) == false) return;  switch (value) case AudioWaveform.PULSE: m_samples = AudioEngine.PULSE; brechen; case AudioWaveform.SAWTOOTH: m_samples = AudioEngine.SAWTOOTH; brechen; case AudioWaveform.SINE: m_samples = AudioEngine.SINE; brechen; case AudioWaveform.TRIANGLE: m_samples = AudioEngine.TRIANGLE; brechen;  m_waveform = Wert; 

AudioModulator.Frequenz

öffentliche Funktion get frequency (): Number return m_frequency;  public function set frequency (value: number): void // klemmt die Frequenz auf den Bereich 0,01 - 100,0 m_frequency = value < 0.01 ? 0.01 : value > 100,0? 100,0: Wert; 

AudioModulator.Amplitude

public function get amplitude (): Number return m_amplitude;  public function set amplitude (value: number): void // klemmt die Amplitude auf den Bereich 0.0 - 8000.0 m_amplitude = value < 0.0 ? 0.0 : value > 8000,0? 8000.0: Wert; 

AudioModulator.Verschiebung

public function get shift (): Number return m_shift;  public function set shift (value: Number): void // klemmt die Verschiebung auf den Bereich 0.0 - 1.0 m_shift = value < 0.0 ? 0.0 : value > 1,0? 1,0: Wert; 

Und das schließt das ein AudioModulator Klasse.


AudioEngine-Klasse

Nun zu den Großen: der AudioEngine Klasse. Dies ist eine rein statische Klasse und verwaltet so ziemlich alles, was damit zusammenhängt Audio Instanzen und Tonerzeugung.

Beginnen wir mit einer Barebones-Klasse im Lärm paket wie gewohnt:

Paketrauschen import flash.events.SampleDataEvent; import flash.media.Sound; import flash.media.SoundChannel; import flash.utils.ByteArray; // public final class AudioEngine public function AudioEngine () werfen neuen Fehler ("AudioEngine-Klasse kann nicht instanziiert werden"); 

Wie bereits erwähnt, sollten statische Klassen nicht instantiiert werden. Daher wird die Ausnahme im Klassenkonstruktor ausgelöst, wenn jemand versucht, die Klasse zu instanziieren. Die Klasse ist auch Finale weil es keinen Grund gibt, eine rein statische Klasse zu erweitern.

Die ersten Dinge, die dieser Klasse hinzugefügt werden, sind interne Konstanten. Diese Konstanten werden verwendet, um die Samples für jede der vier von der Audio-Engine verwendeten Wellenformen zwischenzuspeichern. Jeder Cache enthält 44.100 Samples, was einem Hertz-Signal entspricht. Dadurch kann die Audio-Engine wirklich saubere niederfrequente Schallwellen erzeugen.

Die Konstanten lauten wie folgt:

statische interne const PULSE: Vector. = neuer Vektor.(44100); statische interne const SAWTOOTH: Vektor. = neuer Vektor.(44100); statische interne const SINE: Vector. = neuer Vektor.(44100); statisches internes const DREIECK: Vektor. = neuer Vektor.(44100);

Es gibt auch zwei private Konstanten, die von der Klasse verwendet werden:

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

Das PUFFERGRÖSSE Die Anzahl der Sound-Samples, die an die ActionScript 3.0-Sound-API übergeben werden, wenn eine Anforderung für Sound-Samples gestellt wird. Dies ist die kleinste zulässige Anzahl von Samples und führt zu einer möglichst geringen Klanglatenz. Die Anzahl der Samples könnte erhöht werden, um die CPU-Auslastung zu reduzieren, dies würde jedoch die Klanglatenz erhöhen. Das BEISPIELZEIT ist die Dauer eines einzelnen Klangbeispiels in Sekunden.

Und nun zu den privaten Variablen:

statische private var m_position: Number = 0.0; statische private var m_amplitude: Anzahl = 0,5; statische private var m_soundStream: Sound = null; statisch privat var m_soundChannel: SoundChannel = null; statische private var m_audioList: Vektor.
  • Das m_position wird verwendet, um die Tonstromzeit in Sekunden zu verfolgen.
  • Das m_amplitude ist eine globale sekundäre Amplitude für alle Audio Instanzen, die spielen.
  • Das m_soundStream und m_soundChannel sollte keine Erklärung brauchen.
  • Das m_audioList enthält Verweise auf alle Audio Instanzen, die spielen.
  • Das m_sampleList ist ein temporärer Puffer zum Speichern von Sound-Samples, wenn diese von der ActionScript 3.0-Sound-API angefordert werden.

Nun müssen wir die Klasse initialisieren. Es gibt zahlreiche Möglichkeiten, dies zu tun, aber ich bevorzuge etwas Schönes und Einfaches, einen statischen Klassenkonstruktor:

statische private Funktion $ AudioEngine (): void var i: int = 0; var n: int = 44100; var p: Anzahl = 0,0; // während ich < 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();

Wenn Sie das vorherige Tutorial dieser Serie gelesen haben, werden Sie wahrscheinlich sehen, was in diesem Code passiert: Die Samples für jede der vier Wellenformen werden generiert und zwischengespeichert. Dies geschieht nur einmal. Der Sound-Stream wird ebenfalls instanziiert und gestartet und läuft kontinuierlich, bis die App beendet wird.

Das AudioEngine Die Klasse verfügt über drei öffentliche Methoden, die zum Spielen und Stoppen verwendet werden Audio Instanzen:

AudioEngine.abspielen()

statische öffentliche Funktionswiedergabe (Audio: Audio): void if (audio.playing == false) m_audioList.push (audio);  // Dadurch können wir genau wissen, wann der Sound gestartet wurde. audio.position = m_position - (m_soundChannel.position * 0.001); audio.playing = true; audio.releasing = false; 

AudioEngine.halt()

statischer öffentlicher Funktionsstopp (Audio: Audio, allowRelease: Boolean = true): void if (audio.playing == false) // der Sound wird nicht wiedergegeben return;  if (allowRelease) // bis zum Ende des Sounds springen und als freizugeben markieren. audio.position = audio.duration; audio.releasing = true; Rückkehr;  audio.playing = false; audio.releasing = false; 

AudioEngine.stopAll ()

statische öffentliche Funktion 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++;  

Und hier kommen die wichtigsten Audioverarbeitungsmethoden, die beide privat sind:

AudioEngine.onSampleData ()

statische private Funktion onSampleData (Ereignis: SampleDataEvent): void var i: int = 0; var n: int = BUFFER_SIZE; var s: Anzahl = 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; 

Also im ersten ob Aussage wir überprüfen, ob die m_soundChannel ist immer noch null, und wir müssen das tun, weil die BEISPIELDATEN Ereignis wird abgesandt, sobald die m_soundStream.play () Die Methode wird aufgerufen, und bevor die Methode die Möglichkeit hat, a zurückzugeben SoundChannel Beispiel.

Das während Schleife rollt durch die Sound-Samples, die von angefordert wurden m_soundStream und schreibt sie dem bereitgestellten ByteArray Beispiel. Die Klangbeispiele werden auf folgende Weise erzeugt:

AudioEngine.generateSamples ()

statische private Funktion 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: Anzahl = 0,0; var a: Zahl = 0,0; var s: Anzahl = 0,0; var o: Audio = null; // durch die Audio-Instanzen rollen, während (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) // die Audioinstanz wurde beendet o.playing = false; j ++; fortsetzen;  // die Audioinstanz gibt o.releasing = true frei;  // greife die Frequenz und Amplitude der Audio-Instanz f = o.Frequenz; a = o.amplitude; // if (o.frequencyModulator! = null) // moduliere die Frequenz f + = o.frequencyModulator.process (o.position);  // if (o.amplitudeModulator! = null) // moduliere die Amplitude a + = o.amplitudeModulator.process (o.position);  // Berechne die Position innerhalb des Wellenformcaches p = (44100 * f * o.position)% 44100; // greife die Wellenformprobe s = o.samples [p]; // if (o.release) // Berechne die Ausblende-Amplitude für die Probe s * = 1.0 - ((o.position - o.duration) / o.release);  // füge das Sample zum Puffer hinzu m_sampleList [j] + = s * a; // Aktualisiere die Position der Audio-Instanz o.position + = SAMPLE_TIME; j ++;  i ++; 

Zum Abschluss müssen wir den Getter / Setter für den Privaten hinzufügen m_amplitude Variable:

statische öffentliche Funktion get amplitude (): Number return m_amplitude;  statische öffentliche Funktionssollamplitude (value: Number): void // klemmt die Amplitude auf den Bereich 0,0 - 1,0 m_amplitude = value < 0.0 ? 0.0 : value > 1,0? 1,0: Wert; 

Und jetzt brauche ich eine Pause!


Kommt…

Im dritten und letzten Tutorial der Serie werden wir hinzufügen Audioprozessoren die Audio Engine. Dadurch können wir alle erzeugten Soundsamples durch Verarbeitungseinheiten wie harte Begrenzer und Verzögerungen pushen. Wir werden uns auch den gesamten Code ansehen, um zu sehen, ob alles optimiert werden kann.

Der gesamte Quellcode für diese Tutorialserie wird im nächsten Tutorial verfügbar gemacht.

Folgen Sie uns auf Twitter, Facebook oder Google+, um über die neuesten Beiträge auf dem Laufenden zu bleiben.