Acesta este al doilea dintr-o serie de tutoriale în care vom crea un motor audio bazat pe sintetizator care poate genera sunete pentru jocuri retro-stil. Motorul audio va genera toate sunetele în timpul rulării, fără a fi nevoie de alte dependențe externe, cum ar fi fișiere MP3 sau fișiere WAV. Rezultatul final va fi o bibliotecă de lucru care poate fi abandonată fără efort în jocurile tale.
Dacă nu ați citit deja primul tutorial din această serie, ar trebui să faceți acest lucru înainte de a continua.
Limba de programare folosită în acest tutorial este ActionScript 3.0, dar tehnicile și conceptele folosite pot fi traduse cu ușurință în orice alt limbaj de programare care oferă un API de sunet de nivel scăzut.
Ar trebui să vă asigurați că aveți instalat browserul dvs. Flash Player 11.4 sau superior dacă doriți să utilizați exemplele interactive din acest tutorial.
Până la sfârșitul acestui tutorial, tot codul de bază necesar pentru motorul audio va fi fost finalizat. Următoarea este o simplă demonstrație a motorului audio în acțiune.
Doar un singur sunet se joacă în acea demonstrație, dar frecvența sunetului este randomizată împreună cu timpul de eliberare. Sunetul are, de asemenea, un modulator atașat la acesta pentru a produce efectul vibrato (modularea amplitudinii sunetului), iar frecventa modulatorului este, de asemenea, randomizată.
Prima clasă pe care o vom crea va conține pur și simplu valori constante pentru formele de undă pe care motorul audio le va folosi pentru a genera sunetele audibile.
Începeți prin crearea unui nou pachet de clasă numit zgomot
, și apoi adăugați următoarea clasă la acel pachet:
pachetul de zgomot public final clasa AudioWaveform static public const PULSE: int = 0; static public const SAWTOOTH: int = 1; public static const SINE: int = 2; public static const TRIANGLE: int = 3;
Vom adăuga, de asemenea, o metodă publică statică la clasa care poate fi utilizată pentru a valida o valoare a formei de undă, metoda va reveni Adevărat
sau fals
pentru a indica dacă valoarea formei de undă este validă sau nu.
funcția publică statică validată (forma de undă: int): Boolean if (waveform == PULSE) return true; dacă (forma de undă == SAWTOOTH) returnează adevărat; dacă (forma de undă == SINE) returnează adevărat; dacă (forma de undă == TRIANGLE) returnează adevărat; return false;
În cele din urmă, ar trebui să împiedicăm clasarea să fie instanțiată, deoarece nu există niciun motiv pentru cineva să creeze instanțe din această clasă. Putem face acest lucru în cadrul constructorului de clasă:
funcția publică AudioWaveform () aruncă o nouă eroare ("Clasa AudioWaveform nu poate fi instanțiată");
Această clasă este acum completă.
Împiedicarea claselor de tip enum, a claselor statice și a claselor singleton de a fi instanțiate direct este un lucru bun, deoarece aceste tipuri de clasă nu ar trebui să fie instanțiate; nu există niciun motiv pentru a le instanțiate. Limbile de programare, cum ar fi Java, fac acest lucru automat pentru majoritatea acestor tipuri de clase, dar în prezent în ActionScript 3.0 trebuie să impunem acest comportament manual în cadrul constructorului de clasă.
Următorul pe listă este Audio
clasă. Această clasă are o natură similară naturii ActionScript 3.0 Sunet
clasa: fiecare sunet al unui motor audio va fi reprezentat de un Audio
clasă instanță.
Adăugați următoarea clasă barebones la zgomot
pachet:
pachet de zgomot clasa publica Audio functie publica Audio ()
Primele lucruri care trebuie adăugate la clasă sunt proprietăți care vor spune motorului audio cum să genereze undele sonore ori de câte ori sunetul este redat. Aceste proprietăți includ tipul de undă utilizat de sunet, frecvența și amplitudinea formei de undă, durata sunetului și timpul de eliberare (cât de repede se estompează). Toate aceste proprietăți vor fi private și vor fi accesate prin intermediul getters / setters:
privat var m_waveform: int = AudioWaveform.PULSE; privat var m_frequency: Număr = 100.0; privat var m_amplitude: Number = 0.5; privat var m_duration: Number = 0.2; privat var m_release: Number = 0.2;
După cum puteți vedea, am stabilit o valoare implicită sensibilă pentru fiecare proprietate. amplitudine
este o valoare în domeniu 0.0
la 1.0
, frecvență
este în hertz, și durată
și eliberare
ori sunt în secunde.
De asemenea, trebuie să adăugăm două proprietăți private pentru modulatorii care pot fi atașați la sunet; din nou aceste proprietăți vor fi accesate prin intermediul getters / setters:
privat var m_frequencyModulator: AudioModulator = null; privat var m_amplitudeModulator: AudioModulator = null;
În cele din urmă, Audio
clasa va conține câteva proprietăți interne care vor fi accesate numai de către AudioEngine
(vom crea această clasă în curând). Aceste proprietăți nu trebuie ascunse în spatele getters / setters:
poziția internă var: Număr = 0,0; jocul intern var: Boolean = false; eliberare internă var: Boolean = false; probe interne var: Vector.= null;
poziţie
este în câteva secunde și permite AudioEngine
pentru a urmări poziția sunetului în timpul redării sunetului, acest lucru este necesar pentru a calcula eșantioanele de sunet pentru forma de undă pentru sunet. joc
și eliberare
proprietățile spun AudioEngine
în care starea sunetului este, și eșantioane
proprietatea este o referință la probele de undă memorate în cache, pe care le utilizează sunetul. Utilizarea acestor proprietăți va deveni clară când vom crea AudioEngine
clasă.
Pentru a termina Audio
clasa trebuie să adăugăm pe getters / setters:
Audio.forme de undă
funcția publică finală obține forma de undă (): int return m_waveform; forma finală a funcției publice setată (valoare: int): void if (AudioWaveform.isValid (valoare) == false) return; comutator (valoare) caz AudioWaveform.PULSE: samples = AudioEngine.PULSE; pauză; cazul AudioWaveform.SAWTOOTH: samples = AudioEngine.SAWTOOTH; pauză; cazul AudioWaveform.SINE: samples = AudioEngine.SINE; pauză; caz AudioWaveform.TRIANGLE: samples = AudioEngine.TRIANGLE; pauză; m_waveform = valoare;
Audio.frecvență
[Inline] funcția finală publică obține frecvență (): Număr return m_frequency; frecvența setului final al funcției publice (valoare: Număr): void // fixarea frecvenței la intervalul 1.0 - 14080.0 m_frequency = valoare < 1.0 ? 1.0 : value > 14080,0? 14080.0: valoare;
Audio.amplitudine
[Inline] funcția finală publică obține amplitudine (): Numărul return m_amplitude; setarea funcției finale publice amplitudine (valoare: Număr): void // fixați amplitudinea la intervalul 0.0 - 1.0 m_amplitude = value < 0.0 ? 0.0 : value > 1,0? 1.0: valoare;
Audio.durată
[Inline] funcția finală publică obține durata (): Numărul return m_duration; durata setului funcției finale publice (valoare: Număr): void // fixarea duratei la intervalul 0.0 - 60.0 m_duration = valoare < 0.0 ? 0.0 : value > 60,0? 60,0: valoare;
Audio.eliberare
[Inline] funcția publică finală obține eliberare (): Număr return m_release; eliberare set de funcții publice (valoare: Număr): void // fixarea timpului de eliberare în intervalul 0.0 - 10.0 m_release = valoare < 0.0 ? 0.0 : value > 10,0? 10,0: valoare;
Audio.frequencyModulator
[Inline] funcția finală publică get frequencyModulator (): AudioModulator return m_frequencyModulator; funcția finală publică setată frecvențăModulator (valoare: AudioModulator): void m_frequencyModulator = value;
Audio.amplitudeModulator
[Inline] funcția finală publică obține amplitudineaModulator (): AudioModulator return m_amplitudeModulator; setarea funcției finale publice amplitudeModulator (valoare: AudioModulator): void m_amplitudeModulator = value;
Fără îndoială, ați observat [In linie]
eticheta de metadate legată de câteva dintre funcțiile getter. Eticheta de metadate este o nouă caracteristică strălucitoare a celui mai recent compilator Adobe ActionScript 3.0 și face ceea ce spune pe suport: el inlinează (extinde) conținutul unei funcții. Acest lucru este extrem de util pentru optimizare atunci când este utilizat în mod sensibil, iar generarea de sunet dinamic în timpul rulării este cu siguranță ceva care necesită optimizare.
Scopul AudioModulator
este de a permite amplitudinea și frecvența lui Audio
instanțe care trebuie modulate pentru a crea efecte sonore utile și nebune. Modulatoarele sunt de fapt similare cu Audio
de exemplu, ele au o formă de undă, o amplitudine și o frecvență, dar de fapt nu produc niciun sunet audibil, modifică doar sunete audibile.
Mai întâi, mai întâi, creați următoarea clasă barebones în zgomot
pachet:
pachetul de zgomot public class AudioModulator funcția publică AudioModulator ()
Acum, să adăugăm proprietățile private private:
privat var m_waveform: int = AudioWaveform.SINE; privat var m_frequency: Number = 4.0; privat var m_amplitude: Number = 1.0; privat var m_shift: Number = 0.0; privat var m_samples: Vector.= null;
Dacă vă gândiți că acest lucru pare foarte asemănător cu Audio
atunci ai dreptate: totul, cu excepția schimb
proprietatea este aceeași.
Pentru a înțelege ce schimb
proprietatea, gândiți-vă la una dintre formele de undă de bază pe care motorul audio le utilizează (puls, fierăstrău, sinus sau triunghi) și apoi imaginați o linie verticală care rulează direct prin forma de undă în orice poziție doriți. Poziția orizontală a acestei linii verticale ar fi schimb
valoare; este o valoare în domeniu 0.0
la 1.0
care le spune modulatorului de unde să înceapă să citească forma de undă a acesteia și, la rândul său, poate avea un efect profund asupra modificărilor pe care le face modulatorul la amplitudinea sau frecvența sunetului.
De exemplu, dacă modulatorul folosea o formă de undă sinusoidală pentru a modula frecvența unui sunet și schimb
a fost stabilit la 0.0
, frecvența sunetului ar crește mai întâi și apoi va cădea datorită curburii valului sinusoidal. Cu toate acestea, dacă schimb
a fost stabilit la 0.5
frecvența sunetului ar cădea mai întâi și apoi va crește.
Oricum, înapoi la cod. AudioModulator
conține o metodă internă utilizată numai de către AudioEngine
; metoda este după cum urmează:
[Inline] proces de funcționare finală internă (timp: Număr): Număr var p: int = 0; var s: Număr = 0,0; dacă (m_shift! = 0.0) timp + = (1.0 / m_frequency) * m_shift; p = (44100 * m_frecvență * timp)% 44100; s = m_samples [p]; întoarcere s * m_amplitude;
Această funcție este înclinată deoarece este folosită foarte mult și când spun "o mulțime" vreau să spun 44100 de ori o secundă pentru fiecare sunet care joacă și care are un modulator atașat la acesta (aici este locul în care inlinierea devine incredibil de valoroasă). Funcția captează pur și simplu o probă de sunet din forma de undă utilizată de modulator, ajustează amplitudinea eșantionului și apoi returnează rezultatul.
Pentru a termina AudioModulator
clasa trebuie să adăugăm pe getters / setters:
AudioModulator.forme de undă
funcția publică obține forma de undă (): int return m_waveform; funcția publică setată forma de undă (valoare: int): void if (AudioWaveform.isValid (valoare) == false) return; comutare (valoare) caz AudioWaveform.PULSE: m_samples = AudioEngine.PULSE; pauză; caz AudioWaveform.SAWTOOTH: m_samples = AudioEngine.SAWTOOTH; pauză; caz AudioWaveform.SINE: m_samples = AudioEngine.SINE; pauză; caz AudioWaveform.TRIANGLE: m_samples = AudioEngine.TRIANGLE; pauză; m_waveform = valoare;
AudioModulator.frecvență
funcția publică obține frecvența (): Număr return m_frequency; funcția publică setată frecvență (valoare: număr): void // fixați frecvența la intervalul 0.01 - 100.0 m_frequency = valoare < 0.01 ? 0.01 : value > 100,0? 100,0: valoare;
AudioModulator.amplitudine
funcția publică obține amplitudine (): Număr return m_amplitude; setarea funcției publice amplitudine (valoare: Număr): void // fixarea amplitudinii la intervalul 0.0 - 8000.0 m_amplitude = value < 0.0 ? 0.0 : value > 8000,0? 8000.0: valoare;
AudioModulator.schimb
funcția publică se schimbă (): Numărul return m_shift; funcția publică set shift (valoare: Număr): void // clampă trecerea la intervalul 0.0 - 1.0 m_shift = valoare < 0.0 ? 0.0 : value > 1,0? 1.0: valoare;
Și asta îl înfășoară AudioModulator
clasă.
Acum, pentru cel mare: AudioEngine
clasă. Aceasta este o clasă all-statică și gestionează destul de mult tot ce este legat Audio
instanțe și generarea sunetului.
Să începem cu o clasă barebones în zgomot
pachet ca de obicei:
pachetul de zgomot import flash.events.SampleDataEvent; import flash.media.Sound; import flash.media.SoundChannel; import flash.utils.ByteArray; // clasa publică finală AudioEngine funcția publică AudioEngine () arunca o nouă eroare ("Clasa AudioEngine nu poate fi instanțiată");
După cum sa menționat mai sus, clasele statice nu ar trebui să fie instanțiate, de aceea excepția care este aruncată în constructorul de clasă dacă cineva încearcă să instanțizeze clasa. Clasa este de asemenea final
pentru că nu există niciun motiv pentru a extinde o clasă all-statică.
Primele lucruri care vor fi adăugate la această clasă sunt constante interne. Aceste constante vor fi folosite pentru stocarea în cache a eșantioanelor pentru fiecare din cele patru forme de undă pe care le utilizează motorul audio. Fiecare memorie cache conține 44.100 de eșantioane care se echivalează cu o singură formă de undă hertz. Acest lucru permite motorului audio să producă unde de sunet cu frecvențe foarte scăzute.
Constantele sunt după cum urmează:
statică internă const PULSE: Vector.= Vector nou. (44100); statică internă const SAWTOOTH: Vector. = Vector nou. (44100); static intern const SINE: Vector. = Vector nou. (44100); static intern const TRIANGLE: Vector. = Vector nou. (44100);
Există, de asemenea, două constante private utilizate de clasă:
static privat const BUFFER_SIZE: int = 2048; statică privată const SAMPLE_TIME: Number = 1.0 / 44100.0;
DIMENSIUNEA MEMORIEI TAMPON
este numărul de eșantioane de sunet care vor fi transmise la API-ul de sunet ActionScript 3.0 ori de câte ori este făcută o solicitare de eșantioane de sunet. Acesta este cel mai mic număr de eșantioane permise și are ca rezultat cea mai mică posibilă latență sonoră. Numărul de eșantioane ar putea fi mărit pentru a reduce utilizarea procesorului, dar care ar spori latența sunetului. TIMP DE PROBĂ
este durata unui singur eșantion de sunet, în câteva secunde.
Și acum pentru variabilele private:
statică privată var m_position: Number = 0.0; statică privată var m_amplitude: Number = 0.5; statică privată var m_soundStream: Sound = null; static privat var m_soundChannel: SoundChannel = null; static privat var m_audioList: Vector.
m_position
este folosit pentru a ține evidența timpului fluxului de sunet, în câteva secunde.m_amplitude
este o amplitudine globală secundară pentru toate Audio
instanțele care se joacă.m_soundStream
și m_soundChannel
nu ar trebui să aibă nevoie de explicații.m_audioList
conține referințe la orice Audio
instanțele care se joacă.m_sampleList
este un tampon temporar utilizat pentru a stoca eșantioane de sunet atunci când acestea sunt solicitate de API-ul de sunet ActionScript 3.0.Acum, trebuie să inițializăm clasa. Există numeroase moduri de a face acest lucru, dar prefer ceva frumos și simplu, constructor de clasă statică:
funcția privată statică $ AudioEngine (): void var i: int = 0; var n: int = 44100; var p: Număr = 0,0; // in timp ce eu < 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();
Dacă ați citit tutorialul anterior din această serie, probabil veți vedea ce se întâmplă în acest cod: mostrele pentru fiecare dintre cele patru forme de undă sunt generate și stocate în memoria cache, iar acest lucru se întâmplă doar o singură dată. Fluxul de sunet este, de asemenea, instanțiat și pornit și va funcționa continuu până când aplicația va fi terminată.
AudioEngine
clasa are trei metode publice care sunt folosite pentru a juca și a opri Audio
instanțe:
AudioEngine.Joaca()
funcția publică funcțională statică (audio: audio): void if (audio.playing == false) m_audioList.push (audio); // aceasta ne permite să știm exact când sunetul a fost pornit audio.position = m_position - (m_soundChannel.position * 0.001); audio.playing = adevărat; audio.releasing = false;
AudioEngine.Stop()
oprirea funcției publice statice (audio: Audio, allowRelease: Boolean = true): void if (audio.playing == false) // sunetul nu se redă; if (allowRelease) // sări la sfârșitul sunetului și semnalizează-l ca eliberând audio.position = audio.duration; audio.releasing = adevărat; întoarcere; audio.playing = false; audio.releasing = false;
AudioEngine.stopAll ()
funcția statică publică stopAll (allowRelease: Boolean = true): void var i: int = 0; var n: int = m_audioList.length; var o: Audio = null; // if (allowRelease) în timp ce (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++;
Și aici vin principalele metode de procesare audio, ambele fiind private:
AudioEngine.onSampleData ()
funcția statică privată onSampleData (eveniment: SampleDataEvent): void var i: int = 0; var n: int = BUFFER_SIZE; var s: Număr = 0,0; var b: ByteArray = eveniment.data; // if (m_soundChannel == null) în timp ce (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;
Deci, în primul dacă
declarație verificăm dacă m_soundChannel
este încă nulă și trebuie să facem acest lucru pentru că SAMPLE_DATA
evenimentul este expediat imediat ce m_soundStream.play ()
metoda este invocată și înainte ca metoda să aibă șansa de a reveni SoundChannel
instanță.
in timp ce
buclă rulou prin probele de sunet care au fost solicitate de m_soundStream
și le scrie la cele furnizate ByteArray
instanță. Probele de sunet sunt generate prin următoarea metodă:
AudioEngine.generateSamples ()
funcția statică privată 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: număr = 0,0; var a: Număr = 0,0; var s: Număr = 0,0; var o: Audio = null; // rotiți prin instanțele audio în timp ce (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) // instanta audio sa oprit o.playing = false; j ++; continua; // instanța audio eliberează o.releasing = true; / / apuca frecvența și amplitudinea instanței audio f = o.frequency; a = o.amplitudine; // dacă (o.frequencyModulator! = null) // modulați frecvența f + = o.frequencyModulator.process (o.position); // dacă (o.amplitudeModulator! = null) // modula amplitudinea a + = o.amplitudeModulator.process (o.position); // calcula poziția în cache-ul de undă p = (44100 * f * o.position)% 44100; // apuca proba de undă s = o.samples [p]; // if (o.releasing) // calcula amplitudinea fade-out pentru proba s * = 1.0 - ((o.position - o.duration) / o.release); // adăugați eșantionul la tampon m_sampleList [j] + = s * a; // actualizați poziția instanței audio o.position + = SAMPLE_TIME; j ++; i ++;
În cele din urmă, pentru a termina lucrurile, trebuie să adăugăm pickerul / setter-ul privat m_amplitude
variabil:
funcția publică statică obține amplitudine (): Număr return m_amplitude; set de funcții publice statice amplitudine (valoare: Număr): void // fixați amplitudinea la intervalul 0.0 - 1.0 m_amplitude = value < 0.0 ? 0.0 : value > 1,0? 1.0: valoare;
Și acum am nevoie de o pauză!
În cel de-al treilea și ultimul tutorial din serie vom adăuga procesoare audio la motorul audio. Acestea ne vor permite să împingem toate probele de sunet generate, deși unitățile de procesare, cum ar fi limitatoarele grele și întârzierile. De asemenea, vom examina întregul cod pentru a vedea dacă ceva poate fi optimizat.
Tot codul sursă pentru această serie de tutori va fi disponibil cu tutorialul următor.
Urmăriți-ne pe Twitter, Facebook sau Google+ pentru a fi la curent cu cele mai recente postări.