Det här är det första 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.
Innan vi kan börja skapa ljudmotorn finns det några saker som vi behöver förstå; Dessa inkluderar de vågformar som ljudmotorn kommer att använda för att generera ljudljud och hur ljudvågor lagras och representeras i digital form.
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.
Den ljudmotor som vi kommer att skapa kommer att använda fyra grundläggande vågformer (även känd som periodisk vågformer, eftersom deras grundläggande former repeteras periodiskt), vilka alla är extremt vanliga i både analoga och digitala synthesizer. Varje vågform har sin egen unika hörbar egenskap.
Följande avsnitt ger en visuell återgivning av varje vågform, ett hörbart exempel på varje vågform och den kod som krävs för att generera varje vågform som en grupp av provdata.
Pulsvågan ger ett skarpt och harmoniskt ljud.
För att generera en uppsättning värden (i intervallet -1,0 till 1,0) som representerar en pulsvåg kan vi använda följande kod, var n
är antalet värden som krävs för att fylla arrayen, en
är arrayen och p
är normaliserad position inom vågformen:
var i: int = 0; var n: int = 100; var p: nummer; medan jag < n ) p = i / n; a[i] = p < 0.5 ? 1.0 : -1.0; i ++;
Sågtandvågen ger ett skarpt och starkt ljud.
För att generera en uppsättning värden (i intervallet -1,0 till 1,0) som representerar en sågtandvåg kan vi använda följande kod, där n
är antalet värden som krävs för att fylla arrayen, en
är arrayen och p
är normaliserad position inom vågformen:
var i: int = 0; var n: int = 100; var p: nummer; medan jag < n ) p = i / n; a[i] = p < 0.5 ? p * 2.0 : p * 2.0 - 2.0; i ++;
Sinusvågen ger ett jämnt och rent ljud.
För att generera en uppsättning värden (i intervallet -1,0 till 1,0) som representerar en sinusvåg kan vi använda följande kod, var n
är antalet värden som krävs för att fylla arrayen, en
är arrayen och p
är normaliserad position inom vågformen:
var i: int = 0; var n: int = 100; var p: nummer; medan jag < n ) p = i / n; a[i] = Math.sin( p * 2.0 * Math.PI ); i ++;
Triangeln våg ger ett jämnt och harmoniskt ljud.
För att generera en uppsättning värden (i intervallet -1,0 till 1,0) som representerar en triangelvåg kan vi använda följande kod, var n
är antalet värden som krävs för att fylla arrayen, en
är arrayen och p
är normaliserad position inom vågformen:
var i: int = 0; var n: int = 100; var p: nummer; medan jag < n ) p = i / n; a[i] = p < 0.25 ? p * 4.0 : p < 0.75 ? 2.0 - p * 4.0 : p * 4.0 - 4.0; i ++;
Här är en utökad version av rad 6:
om (s < 0.25) a[i] = p * 4.0; else if (p < 0.75) a[i] = 2.0 - (p * 4.0); else a[i] = (p * 4.0) - 4.0;
Två viktiga egenskaper hos en ljudvåg är amplitud och frekvens av vågformen: dessa dikterar volym och kasta av ljudet respektive. Amplituden är helt enkelt det absoluta toppvärdet för vågformen, och frekvensen är det antal gånger vågformen upprepas per sekund, vilken normalt mäts i hertz (Hz).
Följande bild är en 200 millisekund snapshot av en sågtandvågform med en amplitud på 0,5 och en frekvens på 20 hertz:
För att ge dig en uppfattning om hur frekvensen av en vågform direkt hänför sig till ljudets ljud, skulle en vågform med en frekvens av 440 hertz producera samma tonhöjd som standard A4-noten (mitten A) på ett modernt konsertpiano. Med den frekvensen i åtanke kan vi beräkna frekvensen av någon anteckning med hjälp av följande kod:
f = Math.pow (2, n / 12) * 440,0;
De n
variabel i den koden är antalet anteckningar från A4 (mitten A) till noten vi är intresserade av. Till exempel, för att hitta frekvensen av A5, en oktav över A4, skulle vi ange värdet av n
till 12
eftersom A5 är 12 noter över A4. För att hitta frekvensen av E2 skulle vi ange värdet på n
till -5
eftersom E2 är 5 anteckningar under A4. Vi kan också göra omvända och hitta en anteckning (i förhållande till A4) för en given frekvens:
n = Math.round (12.0 * Math.log (f / 440,0) * Math.LOG2E);
Anledningen till att dessa beräkningar fungerar är att notfrekvenserna är logaritmiska - multiplicera en frekvens med två flyttar en anteckning upp en enda oktav, medan en frekvens delas av två flyttar en anteckning ner en enda oktav.
I den digitala världen måste ljudvågor lagras som binär data, och det vanliga sättet att göra det är att ta periodiska ögonblicksbilder (eller prover) av en ljudvåg. Antalet vågprover som tas för varje sekund av ljudets längd är känd som samplingshastighet, så ett ljud med en samplingsfrekvens på 44100 kommer att innehålla 44100 vågprover (per kanal) för varje sekund av ljudets varaktighet.
Följande bild visar hur en ljudvåg kan samplas:
De vita blocken i den bilden representerar amplitudpunkterna för den våg som samplas och lagras i ett digitalt format. Du kan tänka på detta som en bitmapsbildsupplösning: ju fler pixlar som en bitmappsbild innehåller den mer visuella informationen som den kan hålla, och mer information resulterar i större filer (ignorera filkomprimering för nu). Detsamma gäller för digitala ljud: ju mer vågprover en ljudfil innehåller desto mer exakt kommer den rekonstruerade ljudvågen att vara.
Förutom att ha en samplingsfrekvens har digitala ljud också a bithastighet som mäts i bitar per sekund. Bithastigheten dikterar hur många binära bitar som används för att lagra varje vågprov. Detta liknar antalet bitar som används för att lagra ARGB-information för varje pixel i en bitmappsbild. Till exempel skulle ett ljud med en samplingsfrekvens på 44100 och en bithastighet på 705600 lagra vart och ett av sina vågprover som ett 16-bitars värde, och vi kan beräkna det tillräckligt enkelt med följande kod:
bitsPerSample = bitRate / sampleRate;
Här är ett fungerande exempel med ovanstående värden:
spår (705600/44100); // "16"
Att förstå vilka ljudprover är det viktigaste här; den ljudmotor som vi kommer att skapa måste generera och manipulera råa ljudprover.
En sak som vi borde vara medvetna om innan vi börjar programmera ljudmotorn är modulatorer, vilka är extremt vanliga i både analoga och digitala synthesizer. En modulator är i huvudsak bara en standardvågform, men i stället för att användas för att producera ett ljud används de vanligtvis för modulering av en eller flera egenskaper hos en hörbar vågform (t ex dess amplitud eller frekvens).
Ta vibrato, till exempel. Vibrato är en vanlig pulserande förändring av stigning. För att producera den effekten med en modulator kan du ställa modulatorens vågform till en sinusvåg och ställa modulatorens frekvens till någonstans runt 8 hertz. Om du sedan kopplade modulatorn till frekvensen för en hörbar vågform skulle resultatet vara en vibratoffekt - modulatorn skulle höja och sänka frekvensen (tonhöjden) av den hörbara vågformen åtta gånger per sekund.
Den ljudmotor som vi kommer att skapa kommer att låta dig koppla modulatorer till dina ljud så att du kan producera ett stort antal olika effekter.
I nästa handledning skapar vi kärnkoden för ljudmotorn och får allt igång. Följ oss på Twitter, Facebook eller Google+ för att hålla dig uppdaterad med de senaste inläggen.