Questo è il secondo di una serie di tutorial in cui creeremo un motore audio basato su un sintetizzatore in grado di generare suoni per giochi in stile retrò. Il motore audio genererà tutti i suoni in fase di esecuzione senza la necessità di alcuna dipendenza esterna come file MP3 o file WAV. Il risultato finale sarà una libreria funzionante che può essere facilmente spostata nei tuoi giochi.
Se non hai già letto il primo tutorial di questa serie, dovresti farlo prima di continuare.
Il linguaggio di programmazione utilizzato in questo tutorial è ActionScript 3.0 ma le tecniche e i concetti utilizzati possono essere facilmente tradotti in qualsiasi altro linguaggio di programmazione che fornisce un'API audio di basso livello.
Assicurati di aver installato Flash Player 11.4 o versioni successive per il tuo browser se desideri utilizzare gli esempi interattivi in questo tutorial.
Alla fine di questo tutorial, tutto il codice base richiesto per il motore audio sarà stato completato. Quanto segue è una semplice dimostrazione del motore audio in azione.
Solo un suono viene riprodotto in quella dimostrazione, ma la frequenza del suono viene randomizzata insieme al tempo di rilascio. Il suono ha anche un modulatore ad esso collegato per produrre l'effetto vibrato (modula l'ampiezza del suono) e la frequenza del modulatore viene anche randomizzata.
La prima classe che creeremo conserverà semplicemente i valori costanti per le forme d'onda che il motore audio utilizzerà per generare i suoni udibili.
Inizia creando un nuovo pacchetto di classe chiamato rumore
, e quindi aggiungere la seguente classe a quel pacchetto:
pacchetto noise public final class AudioWaveform static public const PULSE: int = 0; const pubblico statico SAWTOOTH: int = 1; public public const SINE: int = 2; const statico pubblico TRIANGLE: int = 3;
Aggiungeremo anche un metodo pubblico statico alla classe che può essere utilizzato per convalidare un valore di forma d'onda, il metodo restituirà vero
o falso
per indicare se il valore della forma d'onda è valido o meno.
funzione pubblica statica validate (waveform: int): Boolean if (waveform == PULSE) restituisce true; if (waveform == SAWTOOTH) restituisce true; if (waveform == SINE) restituisce true; if (waveform == TRIANGLE) restituisce true; restituisce falso;
Infine, dovremmo impedire l'istanziazione della classe perché non c'è motivo per nessuno di creare istanze di questa classe. Possiamo farlo nel costruttore della classe:
public function AudioWaveform () throw new Error ("La classe AudioWaveform non può essere istanziata");
Questa lezione è ora completa.
Evitare che le classi di stile enum, le classi tutte statiche e le classi di singleton vengano istanziate direttamente è una buona cosa da fare perché questi tipi di classi non dovrebbero essere istanziati; non c'è motivo di istanziarli. I linguaggi di programmazione come Java lo fanno automaticamente per la maggior parte di questi tipi di classi, ma attualmente in ActionScript 3.0 è necessario applicare manualmente questo comportamento all'interno del costruttore della classe.
Il prossimo sulla lista è il Audio
classe. Questa classe è simile in natura al nativo di ActionScript 3.0 Suono
classe: ogni suono del motore audio sarà rappresentato da un Audio
istanza di classe.
Aggiungi la seguente classe di barebone al rumore
pacchetto:
pacchetto noise public class Audio public function Audio ()
Le prime cose che devono essere aggiunte alla classe sono proprietà che diranno al motore audio come generare l'onda sonora ogni volta che viene riprodotto l'audio. Queste proprietà includono il tipo di forma d'onda utilizzata dal suono, la frequenza e l'ampiezza della forma d'onda, la durata del suono e il suo tempo di rilascio (quanto velocemente si attenua). Tutte queste proprietà saranno private e accessibili tramite getter / setter:
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;
Come puoi vedere, abbiamo impostato un valore predefinito ragionevole per ogni proprietà. Il ampiezza
è un valore nell'intervallo 0.0
a 1.0
, il frequenza
è in Hertz, e il durata
e pubblicazione
i tempi sono in secondi.
Abbiamo anche bisogno di aggiungere altre due proprietà private per i modulatori che possono essere collegati al suono; ancora queste proprietà saranno accessibili tramite getter / setter:
private var m_frequencyModulator: AudioModulator = null; private var m_amplitudeModulator: AudioModulator = null;
Finalmente, il Audio
la classe conterrà alcune proprietà interne a cui sarà possibile accedere solo AudioEngine
classe (creeremo questa classe a breve). Queste proprietà non devono essere nascoste dietro i getter / setter:
posizione var interna: Number = 0.0; riproduzione var interna: booleano = falso; rilascio var interno: booleano = falso; campioni interni var: Vector.= null;
Il posizione
è in secondi e permette il AudioEngine
classe per tenere traccia della posizione del suono durante la riproduzione del suono, questo è necessario per calcolare i campioni del suono della forma d'onda per il suono. Il giocando
e rilasciando
proprietà dicono al AudioEngine
in che stato è il suono e il campioni
proprietà è un riferimento ai campioni di forme d'onda memorizzate nella cache che l'audio sta utilizzando. L'uso di queste proprietà diventerà chiaro quando creeremo il AudioEngine
classe.
Per finire il Audio
classe abbiamo bisogno di aggiungere i getter / setter:
Audio.waveform
funzione pubblica finale get waveform (): int return m_waveform; public final function set waveform (value: int): void if (AudioWaveform.isValid (valore) == false) return; switch (value) case AudioWaveform.PULSE: samples = AudioEngine.PULSE; rompere; case AudioWaveform.SAWTOOTH: samples = AudioEngine.SAWTOOTH; rompere; case AudioWaveform.SINE: samples = AudioEngine.SINE; rompere; case AudioWaveform.TRIANGLE: samples = AudioEngine.TRIANGLE; rompere; m_waveform = value;
Audio.frequenza
[Inline] public final function get frequency (): Number return m_frequency; public final function set frequency (value: Number): void // blocca la frequenza nell'intervallo 1.0 - 14080.0 m_frequency = value < 1.0 ? 1.0 : value > 14080.0? 14080.0: valore;
Audio.ampiezza
[Inline] public final function get amplitude (): Number return m_amplitude; public final function set amplitude (value: Number): void // pinza l'ampiezza all'intervallo 0.0 - 1.0 m_amplitude = value < 0.0 ? 0.0 : value > 1,0? 1.0: valore;
Audio.durata
[Inline] public final function get duration (): Number return m_duration; public final function set duration (value: Number): void // limita la durata all'intervallo 0.0 - 60.0 m_duration = value < 0.0 ? 0.0 : value > 60.0? 60.0: valore;
Audio.pubblicazione
[Inline] public final function get release (): Number return m_release; public set set release (value: Number): void // blocca il tempo di rilascio nell'intervallo 0.0 - 10.0 m_release = value < 0.0 ? 0.0 : value > 10.0? 10.0: valore;
Audio.frequencyModulator
[Inline] public final function get frequencyModulator (): AudioModulator return m_frequencyModulator; public final function set frequencyModulator (valore: AudioModulator): void m_frequencyModulator = value;
Audio.amplitudeModulator
[Inline] public final function get amplitudeModulator (): AudioModulator return m_amplitudeModulator; public final function set amplitudeModulator (value: AudioModulator): void m_amplitudeModulator = value;
Hai senza dubbio notato il [In linea]
tag metadata associato ad alcune delle funzioni getter. Quel metadata tag è una nuova brillante funzione del più recente compilatore ActionScript 3.0 di Adobe e fa ciò che dice sullo stagno: esso inline (espande) il contenuto di una funzione. Questo è estremamente utile per l'ottimizzazione se usato in modo ragionevole e la generazione di audio dinamico in fase di runtime è sicuramente qualcosa che richiede un'ottimizzazione.
Lo scopo del AudioModulator
è quello di consentire l'ampiezza e la frequenza di Audio
istanze da modulare per creare effetti sonori utili e folli. I modulatori sono in realtà simili a Audio
le istanze, hanno una forma d'onda, un'ampiezza e una frequenza, ma in realtà non producono alcun suono udibile, modificano solo i suoni udibili.
Per prima cosa, crea la seguente classe di barebone in rumore
pacchetto:
pacchetto noise public class AudioModulator public function AudioModulator ()
Ora aggiungiamo le proprietà private private:
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;
Se stai pensando questo sembra molto simile al Audio
classe allora hai ragione: tutto tranne che per il cambio
la proprietà è la stessa.
Per capire cosa cambio
la proprietà fa, pensa a una delle forme d'onda di base che il motore audio sta usando (impulso, dente di sega, seno o triangolo) e quindi immagina una linea verticale che attraversi la forma d'onda in qualsiasi posizione desideri. La posizione orizzontale di quella linea verticale sarebbe la cambio
valore; è un valore nell'intervallo 0.0
a 1.0
che dice al modulatore da dove iniziare a leggere la sua forma d'onda e a sua volta può avere un profondo effetto sulle modifiche che il modulatore fa all'ampiezza o frequenza di un suono.
Ad esempio, se il modulatore stava usando una forma d'onda sinusoidale per modulare la frequenza di un suono, e il cambio
era fissato a 0.0
, la frequenza del suono salirà prima e poi cadrà a causa della curvatura dell'onda sinusoidale. Tuttavia, se il cambio
era fissato a 0.5
la frequenza del suono prima cadrà e poi aumenterà.
Comunque, torna al codice. Il AudioModulator
contiene un metodo interno che viene utilizzato solo da AudioEngine
; Il metodo è il seguente:
[Inline] processo della funzione finale interna (tempo: numero): Number 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;
Questa funzione è in linea perché è usata molto, e quando dico "molto" intendo 44100 volte al secondo per ogni suono che suona con un modulatore ad esso collegato (è qui che l'inlinazione diventa incredibilmente prezioso). La funzione acquisisce semplicemente un campione sonoro dalla forma d'onda utilizzata dal modulatore, regola l'ampiezza del campione e quindi restituisce il risultato.
Per finire il AudioModulator
classe abbiamo bisogno di aggiungere i getter / setter:
AudioModulator.waveform
funzione pubblica get waveform (): int return m_waveform; public function set waveform (value: int): void if (AudioWaveform.isValid (value) == false) return; switch (valore) case AudioWaveform.PULSE: m_samples = AudioEngine.PULSE; rompere; case AudioWaveform.SAWTOOTH: m_samples = AudioEngine.SAWTOOTH; rompere; case AudioWaveform.SINE: m_samples = AudioEngine.SINE; rompere; case AudioWaveform.TRIANGLE: m_samples = AudioEngine.TRIANGLE; rompere; m_waveform = value;
AudioModulator.frequenza
funzione pubblica get frequency (): Number return m_frequency; public set frequency frequency (value: Number): void // blocca la frequenza nell'intervallo 0,01 - 100,0 m_frequency = value < 0.01 ? 0.01 : value > 100.0? 100.0: valore;
AudioModulator.ampiezza
public function get amplitude (): Number return m_amplitude; public set set amplitude (value: Number): void // pinza l'ampiezza all'intervallo 0.0 - 8000.0 m_amplitude = value < 0.0 ? 0.0 : value > 8000.0? 8000.0: valore;
AudioModulator.cambio
funzione pubblica get shift (): Number return m_shift; public set set shift (value: Number): void // blocca lo spostamento nell'intervallo 0.0 - 1.0 m_shift = value < 0.0 ? 0.0 : value > 1,0? 1.0: valore;
E questo avvolge il AudioModulator
classe.
Ora per il grande: il AudioEngine
classe. Questa è una classe completamente statica e gestisce praticamente tutto ciò che riguarda Audio
istanze e generazione del suono.
Iniziamo con una lezione di barebone in rumore
pacchetto come al solito:
pacchetto noise import flash.events.SampleDataEvent; import flash.media.Sound; import flash.media.SoundChannel; import flash.utils.ByteArray; // public final class AudioEngine public function AudioEngine () throw new Error ("La classe AudioEngine non può essere istanziata");
Come accennato in precedenza, le classi all-static non dovrebbero essere istanziate, quindi l'eccezione che viene lanciata nel costruttore della classe se qualcuno tenta di creare un'istanza della classe. Anche la classe è finale
perché non c'è motivo di estendere una classe completamente statica.
Le prime cose che verranno aggiunte a questa classe sono le costanti interne. Queste costanti verranno utilizzate per memorizzare nella cache i campioni per ciascuna delle quattro forme d'onda utilizzate dal motore audio. Ogni cache contiene 44.100 campioni che equivalgono a una forma d'onda di hertz. Ciò consente al motore audio di produrre onde sonore a bassa frequenza veramente pulite.
Le costanti sono le seguenti:
static internal const PULSE: Vector.= nuovo vettore. (44100); const interno statico SAWTOOTH: Vector. = nuovo vettore. (44100); const interno statico SINE: Vector. = nuovo vettore. (44100); const interno statico TRIANGOLO: Vector. = nuovo vettore. (44100);
Ci sono anche due costanti private usate dalla classe:
const statico privato BUFFER_SIZE: int = 2048; const statico privato SAMPLE_TIME: Number = 1.0 / 44100.0;
Il DIMENSIONE BUFFER
è il numero di campioni audio che verranno passati all'API audio ActionScript 3.0 ogni volta che viene effettuata una richiesta di campioni audio. Questo è il numero minimo di campioni consentito e risulta nella minore latenza possibile del suono. Il numero di campioni potrebbe essere aumentato per ridurre l'utilizzo della CPU, ma ciò aumenterebbe la latenza del suono. Il SAMPLE_TIME
è la durata di un singolo campione sonoro, in secondi.
E ora per le variabili private:
statico privato 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; statico privato var m_audioList: Vector.
m_position
è usato per tenere traccia del tempo del flusso sonoro, in secondi.m_amplitude
è un'ampiezza secondaria globale per tutti i Audio
istanze che stanno giocando.m_soundStream
e m_soundChannel
non dovrebbe avere bisogno di alcuna spiegazione.m_audioList
contiene riferimenti a qualsiasi Audio
istanze che stanno giocando.m_sampleList
è un buffer temporaneo utilizzato per memorizzare campioni sonori quando richiesto dall'API audio ActionScript 3.0.Ora, abbiamo bisogno di inizializzare la classe. Ci sono molti modi per farlo ma preferisco qualcosa di bello e semplice, un costruttore di classi statiche:
funzione privata statica $ AudioEngine (): void var i: int = 0; var n: int = 44100; var p: Number = 0.0; // mentre io < 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();
Se hai letto il tutorial precedente in questa serie, probabilmente vedrai cosa sta succedendo in quel codice: i campioni per ciascuna delle quattro forme d'onda vengono generati e memorizzati nella cache, e ciò accade solo una volta. Anche il flusso audio viene istanziato e avviato e verrà eseguito continuamente fino alla sua chiusura.
Il AudioEngine
la classe ha tre metodi pubblici che sono usati per giocare e fermarsi Audio
casi:
AudioEngine.giocare()
gioco pubblico statico (audio: Audio): void if (audio.playing == false) m_audioList.push (audio); // questo ci permette di sapere esattamente quando è stato avviato il suono audio.position = m_position - (m_soundChannel.position * 0.001); audio.playing = true; audio.releasing = falso;
AudioEngine.Stop()
arresto statico della funzione pubblica (audio: Audio, allowRelease: Boolean = true): void if (audio.playing == false) // l'audio non riproduce il ritorno; if (allowRelease) // salta alla fine del suono e lo contrassegna come rilasciando audio.position = audio.duration; audio.releasing = true; ritorno; audio.playing = false; audio.releasing = falso;
AudioEngine.stopAll ()
static public function 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++;
E qui arrivano i principali metodi di elaborazione audio, entrambi privati:
AudioEngine.onSampleData ()
funzione privata statica 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;
Quindi, nel primo Se
dichiarazione stiamo controllando se il m_soundChannel
è ancora nullo, e dobbiamo farlo perché il SAMPLE_DATA
l'evento viene inviato non appena il m_soundStream.play ()
viene invocato il metodo e prima che il metodo abbia la possibilità di restituire a SoundChannel
esempio.
Il mentre
loop scorre attraverso i campioni sonori richiesti da m_soundStream
e li scrive al fornito ByteArray
esempio. I campioni sonori sono generati dal seguente metodo:
AudioEngine.generateSamples ()
funzione privata statica 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; // passa attraverso le istanze audio mentre (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) // l'istanza audio si è fermata o.playing = false; j ++; Continua; // l'istanza audio sta rilasciando o.releasing = true; // cattura la frequenza e l'ampiezza dell'istanza audio f = o.frequency; a = o.amplitude; // if (o.frequencyModulator! = null) // modula la frequenza f + = o.frequencyModulator.process (o.position); // if (o.amplitudeModulator! = null) // modula l'ampiezza a + = o.amplitudeModulator.process (o.position); // calcola la posizione all'interno della cache della forma d'onda p = (44100 * f * o.position)% 44100; // cattura il campione della forma d'onda s = o.samples [p]; // if (o.releasing) // calcola l'ampiezza di fade-out per il campione s * = 1.0 - ((o.position - o.duration) / o.release); // aggiungi il campione al buffer m_sampleList [j] + = s * a; // aggiorna la posizione dell'istanza audio o.position + = SAMPLE_TIME; j ++; i ++;
Infine, per finire, dobbiamo aggiungere il getter / setter per il privato m_amplitude
variabile:
funzione pubblica statica get amplitude (): Number return m_amplitude; ampiezza di set di funzioni pubbliche statiche (valore: Number): void // clamp l'ampiezza nell'intervallo 0.0 - 1.0 m_amplitude = value < 0.0 ? 0.0 : value > 1,0? 1.0: valore;
E ora ho bisogno di una pausa!
Nel terzo e ultimo tutorial della serie aggiungeremo processori audio il motore audio. Questi ci permetteranno di spingere tutti i campioni sonori generati attraverso unità di elaborazione come limitatori e ritardi. Daremo inoltre un'occhiata a tutto il codice per vedere se qualcosa può essere ottimizzato.
Tutto il codice sorgente per questa serie di tutorial sarà reso disponibile con il prossimo tutorial.
Seguici su Twitter, Facebook o Google+ per tenerti aggiornato con gli ultimi post.