Kasza Róbert informatika oldala



Publikációk

ARDUINO

Fm szintetizátor Arduino Due-val.(magyar nyelv)

Fm synthesizer with Arduino Due. (in English)

Polifónikus Fm szintetizátor Arduino Due-val.

A projekt leírása

Az Arduino Due rendelkezik két darab 12 bites DAC-al (digitál-analóg konverterrel). Ezt használjuk ki ahhoz, hogy audio kimenetet kapjunk. A program futási időben kiszámolja egy 1024 elemű pufferbe az audió értékeket, majd az audio.h osztály függvényeivel a kimenetre írja. A mintavételezési frekvencia 44100Hz. Az audio.h 16 bitesen kezeli a jelet, viszont az Arduino Due DAC-ja csak 12bites. A beépített függvényekkel nem megvalósítható egy szinusz-jel előállítása, mivel rengeteg törtműveletet igényel, sőt a projekt során alapvetően kerülöm a tört számok használatát. A szinusz és egyéb hullámformákat egy tömbbe számolja ki az eszköz az induláskor. Hat generátor áll rendelkezésre egy hang előállításához. Egy hangszín 6 operátort használhat. (2xegy vivő+2 modulátor fm modulációnál) Például az op1-es és az op4-es a vivő. Az op2,op3 az op1 modulátora, az op5, op6 az op4 modulátora. Mind a hat generátor változtatható hullámformájú, ezek jelalakjával még kísérletezem. Várható fejlesztés a közeljövőben a vegyes mód: tehát három operátor az fm modulációért felelős, illetve három alapvető jelalakot valósít meg (szinusz, négyszög, háromszög, fűrész). A generátorok frekvenciája lehet fix illetve a leütött billentyűtől függő. Minden operátornak saját burkológörbéje van. Ezek paraméterei a Yamaha DX hangszerektől megszokott módon AR, AL, D1L,D1R,D2L,D2R,RL,RR. Egy hangszín tényleges hosszát, az op1 generátor AR+D1R+D2R+RR értékének összege adja. Ha ez az idő eltelt, akkor a hangszín lefutottnak minősül. A többi operátor értékei ezen időtartamon belül érvényesek. E mellett rendelkezésre áll még egy pich görbe is, aminek értéke operátoronként +, 0, vagy - előjellel adódik hozzá azok frekvenciájához. Az eszköz dinamikaérzékeny, az oszcillátorok hangereje változhat a dinamika arányában. A kész jel sztereó vagy monó módban írható a kimenetre. Jelenlegi fejlesztés egy egyszerű reverb effekt előállítása. Ennek jelerőssége, illetve ReberbTime paramétere állítható. Az eszköz 6 hangig polifonikus! Egyszerű midi billentyűzetről, vagy számítógépről vezérelhető, 8 előre beállított hangszínt, azaz prestetet tartalmaz.

Videók

Képek

Letölthető hanganyag:

Hang1 Hang2 Hang3 Hang4 Hang5 Hang6 Hang7 Hang8 Hang9 Hang10 Hang11 Hang12 Hang13

A programról:

Az fm moduláció:

A freq-mutató 22 bites eltolásával valósul meg a szinusz-jel előállítása, egy a színuszjel értékeivel előre feltöltött 1024 elemű tömbben. Így nagy pontossággal előállítható a kívánt frekvencia:

buffer=sinusfg[freqmutato1>> 22];
freqmutato1 += sinewave1freq;

Az fm moduláció megvalósításában egy Yamaha YM2151-es chip dokumentációjában talált képlet volt a segítségemre:

Ugye itt látszik, hogy a moduláció ha a B Level értéke nulla, akkor csak egy egyszerű szinuszjelet ad vissza. Amennyiben a modulátor hangereje nem nulla, így annak hangerőfüggő értéke hozzáadódik, vagy kivonódik a vivő frekvenciájából, és így modulálja annak frekvenciáját. Természetesen közben mindig az aktuális frekvenciáit is figyelni kell az oszcillátoroknak, hisz azok a pichgörbe függvényében változhatnak! Mindez előállítása egy egyszerű színusz függvénnyel nagyon egyszerű lenne, viszont ezt az Arduino Due nem bírja másodpercenként 44100x2-szer kiszámítani. Ezért egy 1024 elemű tömbbe kiszámoljuk induláskor a szinusz értékeket, és ezek értékeit adjuk vissza a frekvenciának megfelelően.

Fm moduláció lelke két operátorral:

sinusfg[((freqmutato1 >> 22) + fmsinusfg[freqmutato2 >> 22] * op2gorbelevel / op2level) % 1024] * op1gorbelevel / op1level

Az fm moduláció három operátorral:

generator1[((freqmutato1 >> 22) + generator2[((freqmutato2 >> 22)+generator3[freqmutato3 >> 22]* op3level/4096)% 1024] * op2level/4096) % 1024] *op1level>>12

Az fm moduláció négy operátorral:

generator1[((freqmutato1 >> 22) + generator2[((freqmutato2 >> 22)+generator3[((freqmutato3 >> 22)+generator4[freqmutato4 >> 22]*op4level/4096)%FG_SIZE ] * op3level/4096)% FG_SIZE ] * op2level/4096) % FG_SIZE ] *op1level>>12

Ezen algoritmusok a szintetizátor lelkei. Ezek sebessége kulcsfontosságú. Sajnos a maradékos osztást nem tudtam kihagyni, mert akkor előbb utóbb történt egy túlindexelés a tömbökben, és megállt a hangszer. Illetve nem nagyon volt ötletem a visszacsatolás, FeedBack megvalósítására. Ez utóbbi kiküszöbölése miatt nem csak színuszjelleket, hanem háromszög, négyszög illetve fűrészfog jeleket islehet használni, mind vivőként, mint modulátorként, így összetettebb jelalakok is előállíthatóak. Ha van bármiféle ötleted a modulációs függvények optimalizálására, írj nyugodtan! A további fejlesztésekben felmerült, egy PWM szintézis alapú algoritmus megvalósítása, ez még csak elvi szinten létezik.

A program felépítését a következő ábra szemlélteti:

Az Audio.h függvénytár pufferjébe állítjuk elő az audió jelet. Jelenleg még megoldásra szorul, a hangerőléptetések miatti extrém görbeátmenet. Ezek a felharmónikusok sajnos hallatszanak a kimeneten. Ez a probléma látható az alábbi ábrán. Csökkentése egyébként megoldható átlagszámítással, illetve a tvagörbe paraméterének megfelelő beállításával.

Az összeállított puffer kiírása a DAC-ra a következő függvényekkel történik.

Audio.prepare(buffer, audiobuffersize, volume);
Audio.write(buffer, audiobuffersize);

A burkológörbék

A tva burkológörbék előállítása már a hangszín betöltésekor, vagy bármely tvagörbe paraméterének állítása után megvalósul. A burkológörbe értékeiből (Attack, Decay, Decay2, Released) legenerál egy tömbböt, mely értékei tartalmazzák a hangerőértékeket a görbefelbontásnak megfelelően. A legkisebb felbontás a 2 lépés. Az aktuális görbeidő a gorbetime[0] változóba tárolódik. Ez az érték mutatja, hol járunk az adott billentyűnek megfelelő hang Tva görbéjében. Bufferperiódusonként valósul meg a görbék léptetése. Amennyiben a billentyű lenyomva marad, úgy a léptetés gorbetime[0] != maxrelease0 - 1-ig megy. Elengedés esetén, az adott hanghoz tartozó NoteOff paraméternél, gorbetime[0] = maxrelease0-al tőrténik meg a released (lecsengés) állapotba váltás, ami maxtime0-ig megy, majd -1-re vált a gorbetime[0] értéke. Ezzel jelezzük, hogy a folyamat lezajlott, a görbe alapállapotban van. Ebből az is következik, hogy egy adott hangmagasságú hang egy időben csak egyszer szólalhat meg. Számomra ez abszolut nem volt zavaró, sőt kifejezetten életszerű hangzást ad, és így elkerültem, hogy bonyolultabb számításokkal vezéreljem a polyfóniát. Mivel egy hanghoz 6 operátor tartozik így görbeléptetéskor mindegyiknek megfelelő modulációs szintet be kell állítani! A "/op1veloc" osztással állítható a velocity paraméter, ami annyira életszerűvé tette az előadásmódot az első kisérletre, hogy nem, hogy nem is variáltam vele. Ugye az is természetes, hogy mivel FM modulációról beszélünk, itt nincs szükség TVF burkolóra. Hiszen a görbe szerepét az algoritmus határozza meg, ettől függ, hogy vivő vagy modulátorként viselkedik, az adott operátor. Az alábbi kódban látszik egy példa a TVA görbeléptetés megvalósítására:

if (gorbetime[0] == maxtime0) {
gorbetime[0] = -1;
} else {
if (gorbetime[0] >= 0) {
if (gorbetime[0] == 0) {
ptrnullaz(0);
}
if (gorbetime[0] != maxrelease0 - 1)
gorbetime[0]++;
op1level[0] = op1gorbe[gorbetime[0]] * op1volume * (waveveloc[0] / op1veloc);
op2level[0] = op2gorbe[gorbetime[0]] * op2volume * (waveveloc[0] / op2veloc);
op3level[0] = op3gorbe[gorbetime[0]] * op3volume * (waveveloc[0] / op3veloc);
op4level[0] = op4gorbe[gorbetime[0]] * op4volume * (waveveloc[0] / op4veloc);
op5level[0] = op5gorbe[gorbetime[0]] * op5volume * (waveveloc[0] / op5veloc);
op6level[0] = op6gorbe[gorbetime[0]] * op6volume * (waveveloc[0] / op6veloc);
}
}

if (op1ar > 1) {
for (int i = 0; i <= op1ar; i++)
{
op1gorbe[i] = op1al / op1ar * i;
}
}
else {
op1gorbe[0] = op1al;
}
for (int i = 1; i <= op1d1r; i++)
{
op1gorbe[op1ar + i] = (op1al - (op1al - op1d1l) * i / op1d1r);
}
for (int i = 0; i <= op1d2r; i++)
{
op1gorbe[op1ar + op1d1r + i] = (op1d1l - (op1d1l - op1d2l) * i / op1d2r);
}
for (int i = 0; i <= op1rr; i++)
{
op1gorbe[op1ar + op1d1r + op1d2r + i] = (op1d2l - (op1d2l - op1rl) * i / op1rr);
}

A tva gorbék léptetése ill leállítasa, ha az adott hang véget ért:

if ((gorbetime[0] >= 0) && (gorbetime[0] < hangmintahossz)) {
if (gorbetime[0] == 0) {
ptrnullaz(0);
}
if (gorbetime0lehet) {
if(gorbetime[0]!=releasetime-1)
gorbetime[0]++;
op1level[0] = op1gorbe[gorbetime[0]] * op1volume * (wave0veloc / op1veloc);
op2level[0] = op2gorbe[gorbetime[0]] * op2volume * (wave0veloc / op2veloc);
op3level[0] = op3gorbe[gorbetime[0]] * op3volume * (wave0veloc / op3veloc);
op4level[0] = op4gorbe[gorbetime[0]] * op4volume * (wave0veloc / op4veloc);
op5level[0] = op5gorbe[gorbetime[0]] * op5volume * (wave0veloc / op5veloc);
op6level[0] = op6gorbe[gorbetime[0]] * op6volume * (wave0veloc / op6veloc);
} else {
gorbetime0lehet = true;
}
} else {
gorbetime[0] = -1;
}

A reverb:

A reverb motor:

bufferbe=(level-2)*bufferbe/level;
bufferbe += delaybuffer[delaybufferindex];
buffer[ bufferindex] = bufferbe;
delaybuffer[delaybufferindex] = (reverblevel-1)*bufferbe/reverblevel;
delaybufferindex++;
if (delaybufferindex > reverbtime) {
delaybufferindex = 0;
}

A MIDI:

A MIDI események feldolgozása: Ha midi esemény érkezik, akkor annak típusa szerint dolgozom fel. Ha NOTE-On azaz billentyűlenyomás történt, akkor ugye az első adat bájt a billentyű kódja, a második a dinamika értéke. A beérkező billentyűlenyomáshoz 6 generátort léptetek. a polifóbia miatt. Ha egyesnél tartunk akkor az első generátor frekvenciája(wavefreq1)átveszi a noteertek tömbben meghatározott értéket, a wave0veloc pedig a dinamika értékét. Emellett az adott hanggenerátor tva görbéjét le kell nulláznom, hogy elölről játszódjon le a hang. Mivel számon kell tartanom, mely hangok vannak lenyomva így a kapott billentyűértéket eltárolom az oldnoteByte1 változóban. Ezt használom a Note-off ágban az elengedésekhez. Itt a tva görbéket a release (lecsengés) időpontra állítom. Program change üzenet esetén a programchange eljárást hívom meg, ami meghívja a megfelelő hangszínt, illetve AfterTouchPoly üzenetnél a paraméterállító eljárásomat, mivel ezzel kommunikál valós időben az eszköz.

void serialEvent3() {
if (MIDI2.read()) {
if (MIDI2.getChannel() == midichan) {
switch (MIDI2.getType())
{
case midi::NoteOn:
noteByte = MIDI2.getData1();
velocityByte = MIDI2.getData2();
gorbetime[generatornumber] = 0;
wavefreq[generatornumber] = noteertek[noteByte];
waveveloc[generatornumber] = velocityByte;
oldnoteByte[generatornumber] = noteByte;
generatornumber++;
if (generatornumber == 6) {
generatornumber = 0;
}
break;
case midi::NoteOff:
noteByte = MIDI2.getData1();
// velocityByte = MIDI2.getData2();
if (noteByte == oldnoteByte[0]) {
gorbetime[0] = maxrelease0;
}
if (noteByte == oldnoteByte[1]) {
gorbetime[1] = maxrelease1;
}
if (noteByte == oldnoteByte[2]) {
gorbetime[2] = maxrelease2;
}
if (noteByte == oldnoteByte[3]) {
gorbetime[3] = maxrelease3;
}
if (noteByte == oldnoteByte[4]) {
gorbetime[4] = maxrelease4;
}
if (noteByte == oldnoteByte[5]) {
gorbetime[5] = maxrelease5;
}
break;
case midi:: ProgramChange:
noteByte = MIDI2.getData1();
velocityByte = MIDI2.getData2();
programchange(noteByte);
break;
case midi:: AfterTouchPoly:
noteByte = MIDI2.getData1();
velocityByte = MIDI2.getData2();
parameterchange(noteByte, velocityByte);
break;
case midi:: ControlChange:
noteByte = MIDI2.getData1();
velocityByte = MIDI2.getData2();
pichband(noteByte, velocityByte);
break;
case midi:: Clock:
sendmidiclock();
break;
}
}
}
}

A fejlesztés során alapvető problémát jelentett a tömbhasználat a paramétereknél, mivel a tömbindexelés használata lelassította a loop-ot. Ezért van sok paraméterhez külön név létrehozva. Az elnevezések sem minidg konzekvensek, mert sokszor 0-tól, sokszor 1-től kezdődik a számozás. Ezt a későbbiekben még lehet kijavítom. Ugyanúgy kerülendőnek bizonyultak a beépített függvények, illetve loop-ok. Tehát nem lehet valós időben használni sem a sin sem a rand függvényeket. Az eszköz alapvetően egy while (true) {} loopban működik. A tva-görbék léptetése a millis() függvénnyel van szinkronizálva. Sajnos a léptetésnél előfordul, hogy hallatszik ropogás amennyiben hirtelen nagyot lép a hangerőgörbe, de erről már fentebb volt szó, kijavítása folyamatban. A készülék alapból a Note-On, Note-Off, Program Change paramétereket tudja fogadni. A hangszín szerkesztéséhez pedig a polyfonia aftertouch üzeneteket használja. Itt az átküldött billentyűkód adja a paramétert, és a következő bájt értéke adja a paraméterhez tartozó értéket. .

Az állítható paraméterek listája és értékei:

LINK

A szintetizátor alpha verziójánal forráskódja:

LINK

A szintetizátorunkat midi utasításokkal pc-ről is vezérelhetjük: A következő programból indultam ki:
C# MIDI toolkit
Ezzel a programmal elmenthetjük, illetve áttölthetjük az épp aktuális hangszínt, viszont várhatóan a későbbiekben egy Arduino Nanoval lesz megoldva a paraméterek vezérlése, illetve a kijelzés.

A PC szoftver alpha verziója letölthető:

LINK

Köszönet:

Mindenképpen köszönöm a Hobbielektronika fórum Arduinós tagjainak: kapu48, illetve Benjami-nak a segítséget! Családtagjaimnak, illetve a szomszédoknak, hogy kibírták a mindennapos zajokat, különösképpen, ha egy hibás paraméterérték miatt telítésbe vágtam a bufferértékeket. Meg mindenkinek aki hagyott dolgozni. Sok sikert a teszteléshez!

Kapcsolat:

kaszarobert@gmail.com


Az utolsó módosítás: 2023 September 19 13:49:18.