Què és el MSX-Music?
L'MSX-Music és un sistema de so basat en el xip de Yamaha YM2413 (OPLL - FM Operator Type-L Light) i que és una simplificació del YM3812 (OPL2).
No s'ha de confondre amb el MSX-Audio que utilitza el Y8950 (OPL1) i que com a diferència important, conté un canal ADPCM que no es troba a l'OPLL. La part FM és la mateixa.
El cartutxo de Panasonic FS-CA1 és l'únic que complia les especificacions del MSX-Audio. Les altres implementacions de l'època comercial es troba als cartutxos Philips NMS 1205 (Music Module) i el Toshiba HX-MU900 (FM-Synthesizer Unit) que es poden ampliar per ser totalment compatibles amb el format. L'última implementació d'aquest estàndard és l'AudioWave.
El MSX-Music es troba en els 2+ i en els Turbo-R. A l'època comercial es podia també aconseguir amb el Panasoft FM-Pak. Després van sortir molts més cartutxos compatibles amb el format, com s'indica al wiki del msx.org. Aquest sistema de so permet generar 9 canals independents de so amb la tècnica de síntesis FM o 6 canals FM i 5 percussions diferents. També permet escollir l'instrument diferent per cada canal, tenint-ne 15 de definits i un instrument que es pot crear per l'usuari. Les especificacions del xip es poden trobar aquí.
Què farem en aquest post?
Primer de tot crearem una funció en ensamblador que escrigui els registres de l'OPLL per poder-los configurar des del MSX. Amb aquesta funció escriurem una nota diferent a cada canal. També configurarem un instrument propi i canviarem els instruments dels canals.
Com configurem l'OPLL des del MSX?
L'MSX configura el MSX-Music accedint al port 0x7C i el 0x7D. En el primer s'ha d'escriure el número de registre del YM2413 que volem accedir i al segon escrivim el valor que volguem que prengui aquest registre. Aquests valors els vaig trobar a uns fitxers que em van passar en un grup de Telegram quan vaig demanar informació. He intentat esbrinar si en el MSX Data Pack sortia aquesta informació i no ho he trobat, però mirant la pàgina del Grauw he vist que hi ha un extracte del Datapack del MSX-Music a on hi apareix.
Com escrivim aquests codis en C?
Les comandes en ensamblador per fer aquests outs és amb out(adreça,A). Per fer-ho amb SDCC el primer que se'm va ocórrer és utilitzar les funcions en assemblador que permet el SDCC tal i com explica l'Eric Boez al seu llibre FUSION-C: MSX C Library complete journey o el Konamiman en el seu github.
Nosaltres necessitem passar dos paràmetres que són de tipus char, en el primer passem el registre del YM2413 i en el segon paràmetre indiquem el valor que prendrà aquest registre. Si mirem els exemples del Konamiman, coincideix amb la funció SumTwoChars(char x, char y) del seu exemple. La funció per escriure paràmetres quedaria com:
L'apèndix __naked en la definició de la funció serveix per indicar (tal i com s'explica a la documentació del SDCC 3.11.3 Naked Functions) que el compilador no modifica cap registre i que és el programador qui s'encarrega de preservar els diferents registres. Amb els indicadors __asm __endasm marquem que tot el codi entre aquests dos serà en assemblador SDCC. Aquest assemblador té les seves particularitats sintàctiques, com per exemple que per fer (IY)+1 com es fa en la majoria aquí ha de ser 1(IY).
El primer que fem és carregar els paràmetres als registres D i E amb l'adreça que hem preparat abans en el registre IY. Esperem uns cicles perquè els regsitres estiguin estables amb la comanda ex af,af' i carreguem el registre A amb el paràmetre que havíem guardat al registre D i fem l'out. Esperem que el valor d'out s'estabilitzi i fem una altra sortida a l'adreça 0x7D amb el valor del segon paràmetre de la funció que havíem guardat en E. Ens esperem uns cicles de rellotge perquè el YM2413 pugui llegir els valors i acabem la funció.
En escriure aquest article, he vist que el SDCC permet crear una variable i associar-la a un port (explicat a l'apartat 3.5.2 Z80/Z180/eZ80 intrinsic named address spaces del manual) usant la sintaxi __sfr __at (adreça_port) nom_variable. Així en el nostre cas si creésim __sfr __at(0x7C) num_registre_YM2413 cada cop que féssim num_registre_YM2413 = 0x10 el compilador ho traduiria amb les comandes
ld A,#0x10
out (#0x7C),A
L'únic que faltaria és una comanda en C que tardés els mateixos cicles que ex af,af' i podríem tenir tot el codi sense usar assemblador. Però de moment continuem amb el que ja he pogut comprovar que funciona.
Haureu notat que esperem temps diferents a que l'OPLL llegeixi cada port. Això és així per especificació de Yamaha. Tal i com està documentat en el datapack a la taula 7.22.
Tampoc se us haurà escapat que la funció es diu Z80, i és que per R800, a l'executar les comandes més ràpid, n'hem d'executar més per tenir el mateix temps d'espera.
Funcionament del Yamaha YM2413
Tal i com s'indica en el datasheet del xip, a la Table II-7 tenim un mapa dels registres que copio aquí per facilitar la lectura:
Els primers set registres són els utilitzats per crear el nostre propi instrument. Hi ha tot d'articles que expliquen com crear instruments a partir de la síntesi de FM i és un tema que no he provat mai. Si algú està interessat el Moonblaster també tenia una part de síntesi i et permetia trastejar amb els que ja portava. He mirat el furnace i també té una part de síntesi. He buscat per la web si hi havia instruments creats per la gent i no n'he sapigut trobar. El que sí que sé és que pots carregar els que hi ha en el directori d'instruments per estudiar els seus presets.
Després saltem al registre 0x0E que és el que configura el funcionament del xip si el volem en mode rítmic, bit 5 = 1, o en mode melòdic, bit 5=0. En mode rítmic, els últims 3 canals no s'utilitzen com a instrument sinó que configuren 5 instruments de percussió: Bass drum, Snare drum, Tom, Cymbal and Hi-Hat.
Els registres del 10 al 18 contenen els 8 bits més baixos del número F. Aquest número F està molt relacionat amb la freqüència, to, nota, del canal.
Els nou registres següents, 20-28, contenen el bit més significatiu del número F, 3 bits per indicar el bloc/octava, 1 bit per si el canal està actiu i un altre per indicar si ha de mantenir la nota del canal.
Els nou registres últims, 30-38, conté cadascú el tipus d'instrument a tocar per aquell canal i el seu volum. Els instruments són: 0-Original, 1-Violí, 2-Guitarra, 3-Piano, 4-Flauta, 5-Clarinet, 6-Oboè, 7-Trompeta, 8-Òrgan, 9-Corn, 10-Sintetitzador, 11-Clavicordi, 12-Vibràfon, 13-Baix sintètic, 14-Baix acústic i 15-Guitarra elèctrica. Sobre el volum s'ha de tenir en compte que 0 és màxim volum i 15 és mínim volum.
Com indiquem la nota que ha de sonar per un dels canals?
Per indicar la nota que volem que soni per un dels canals, s'ha de modificar el F-number i el Block d'aquell canal. Cada canal està format per 3 registres que el defineixen. Per exemple, pel canal 0, es programen els registres 10, 20 i 30; pel canal 1 els registres 11, 21 i 31; i així anar fent fins al canal 8 que és definit per 18, 28 i 38.
Per traduir la freqüència de la nota que volem que soni al doblet F-number i Block, Yamaha indica en el seu manual (pàgina 15) que F=(fmus*2^(18)/fsam)/2^(b-1) a on F és el F-number, fmus és la freqüència que nosaltres volem que soni i b és el bloc data (octava). També indiquen dues taules amb exemples de freqüència (Table III-8-1 i Table III-8-2) a on es pot veure que per diferents octaves la mateixa nota va canviant de F-Number. Hi ha un post al msx.org que tracta aquest tema i que en un comentari diu que una solució fàcil és posar tots els F-Num segons la freqüència de la taula que indica Table III-8-1 i després anar modificant el bloc com si es tractés de l'octava. Aquesta és la solució que es va aplicar al driver de moonblaster fet en C. Però si mirem altres implementacions, com ara el driver de moonblaster de bifi o el player del mbm del roboplay, veiem que tots dos utilitzen una taula amb les següents freqüències:
static const uint16_t g_frequency_table[] = { 0x00AD, 0x00B7, 0x00C2, 0x00CD, 0x00D9, 0x00E6, 0x00F4, 0x0103, 0x0112, 0x0122, 0x0134, 0x0146, 0x02AD, 0x02B7, 0x02C2, 0x02CD, 0x02D9, 0x02E6, 0x02F4, 0x0303, 0x0312, 0x0322, 0x0334, 0x0346, 0x04AD, 0x04B7, 0x04C2, 0x04CD, 0x04D9, 0x04E6, 0x04F4, 0x0503, 0x0512, 0x0522, 0x0534, 0x0546, 0x06AD, 0x06B7, 0x06C2, 0x06CD, 0x06D9, 0x06E6, 0x06F4, 0x0703, 0x0712, 0x0722, 0x0734, 0x0746, 0x08AD, 0x08B7, 0x08C2, 0x08CD, 0x08D9, 0x08E6, 0x08F4, 0x0903, 0x0912, 0x0922, 0x0934, 0x0946, 0x0AAD, 0x0AB7, 0x0AC2, 0x0ACD, 0x0AD9, 0x0AE6, 0x0AF4, 0x0B03, 0x0B12, 0x0B22, 0x0B34, 0x0B46, 0x0CAD, 0x0CB7, 0x0CC2, 0x0CCD, 0x0CD9, 0x0CE6, 0x0CF4, 0x0D03, 0x0D12, 0x0D22, 0x0D34, 0x0D46, 0x0EAD, 0x0EB7, 0x0EC2, 0x0ECD, 0x0ED9, 0x0EE6, 0x0EF4, 0x0F03, 0x0F12, 0x0F22, 0x0F34, 0x0F46 };
Aquesta taula conté 8x12 elements que són totes les freqüències de les 12 notes per les 8 octaves. Primer em pensava que aquests valors eren les freqüències de les notes musicals, vaig reobrir el post comentat més amunt i em van aclarir que ja són els valors directament pel registre 10 i els 4 bits més baixos del 20 (he posat 10 i 20 però serveixen per tots els canals, des del 0 fins al 8). Després he pogut comprovar amb diferents càlculs utilitzant els valors anteriors que realment utilitzen l'octava com a número de block i que els F-Number s'anaven repetint. De les dades anteriors he deduït que han utilitzat un fsam de 83340.57803 que no sé exactament d'on surt aquest valor. Un exemple de la taula per dues octaves és:
Nota | Frequency of the note | octave | Fnum | Fnum in hex |
A1 | 55.0 | 1 | 173 | AD |
A#/Bb1 | 58.27047018976124 | 1 | 183.287115324158 | B7 |
B1 | 61.735412657015516 | 1 | 194.185934357522 | C2 |
C1 | 65.40639132514966 | 1 | 205.732830895471 | CD |
C#/Db1 | 69.29565774421802 | 1 | 217.966341631813 | D9 |
D1 | 73.4161919793519 | 1 | 230.927294771416 | E6 |
D#/Eb1 | 77.78174593052023 | 1 | 244.658946290545 | F4 |
E1 | 82.4068892282175 | 1 | 259.207124299666 | 103 |
F1 | 87.30705785825097 | 1 | 274.620381990499 | 112 |
F#/Gb1 | 92.4986056779086 | 1 | 290.950159677785 | 122 |
G1 | 97.99885899543733 | 1 | 308.250956476557 | 134 |
G#/Ab1 | 103.82617439498628 | 1 | 326.580512187866 | 146 |
A2 | 110.0 | 2 | 173 | AD |
A#/Bb2 | 116.54094037952248 | 2 | 183.287115324158 | B7 |
B2 | 123.470825314031 | 2 | 194.185934357522 | C2 |
C2 | 130.8127826502993 | 2 | 205.732830895471 | CD |
C#/Db2 | 138.59131548843604 | 2 | 217.966341631813 | D9 |
D2 | 146.83238395870376 | 2 | 230.927294771416 | E6 |
D#/Eb2 | 155.56349186104046 | 2 | 244.658946290545 | F4 |
E2 | 164.813778456435 | 2 | 259.207124299666 | 103 |
F2 | 174.61411571650194 | 2 | 274.620381990499 | 112 |
F#/Gb2 | 184.9972113558172 | 2 | 290.950159677785 | 122 |
G2 | 195.99771799087466 | 2 | 308.250956476557 | 134 |
G#/Ab2 | 207.65234878997256 | 2 | 326.580512187866 | 146 |
A3 | 220.0 | 3 | 173 | AD |
Per tant, per escriure la nota, mirarem quina nota és, el seu corresponent F-Number i l'octava que volem que soni. Si el F-Number és de 9 bits, haurem d'utilitzar el del registre 20.
Programa que toca una nota
Anem a fer sonar aquest xip amb un senzill programa:
Primer de tot creem les funcions en assemblador, si l'ordinador és un Turbo-R afegim més comandes per esperar a que estiguin els registres estables per a ser llegits, que és la funció WriteOPLLreg_TR.
I comencem ja la funció main. Aquest cop utilitzarem l'screen 0 per anar escrivint missatges per pantalla i esperant que una tecla sigui premuda per continuar avançant amb el test.
A la línia 59 escollim l'instrument 15 (guitarra elèctrica, el 15 és el 0xF i el posem a la part més alta del registre 0x30 que escull l'instrument i el volum, en aquest cas 0 que és el màxim) amb el que ja tenim instrument i volum pel canal 0. Ara queda posar la nota i octava a sonar pel canal 0. La nota és un La que segons la taula anterior té valor AD. Escollim l'octava 4 (b100), que si la desplacem un lloc a l'esquerra ja que el bit menys significatiu és el bit novè del F-num tenim 0x8 i si diem que aquell canal està sonant (bit Key on del registre 0x20) li posem el valor 0x18 al registre 20. Si volguèssim que fes un sustain (que quan diem que apaguem la tecla fa una reducció gradual del volum), escriuríem el valor 0x38.
Indiquem que el so està sonant per pantalla i esperem que s'apreti una tecla per continuar.
Un cop apretada la tecla canviem la nota fent-la més greu dues octaves. Per això apaguem la nota posant el bit Key a 0 del registre 0x20 i escrivim el F-num que serà el mateix i tornem activar el Key i l'octava en el registre 0x20. I a la línia 69 tornem a esperar una tecla.
Ara farem un canvi d'instrument i tornarem a la nota La (A4). Per això apaguem la nota, configurem ara el violí i tornem a posar el F-num, l'octava i activem el canal (bit Key). Tot això entre les línies 72 i 75. Indiquem per pantalla la nova configuració i esperem una tecla.
Entre la 79 i la 91 tornem a canviar l'octava de la nota i l'apaguem sense que faci sustain. Després configurem el canal amb sustain a la línia 93 i esperem una tecla per apagar-lo. Quan l'apretem veurem que el so no s'acaba immediatament com abans que no tenia sustain, sinó que es va esmorteint amb el temps. Depèn de l'instrument escollit, l'efecte del sustain es nota més o menys, hi ha instruments que no mantenen la nota sinó que és com una percussió, aquests amb prou feines es nota el sustain.
Un cop hem apretat una tecla per continuar, farem sonar l'acord de do major que són les notes g3, e3 i c3, tocada cadascuna amb un instrument diferent. Entre les línies 101 i 103 configurem els instruments: clarinet pel canal 0, Oboè pel canal 1 i el corn pel canal 2. Després configurem el F-num per les corresponents notes que són: 0x134, 0xCD i 0x103. Hi ha dues notes que tenen un F-num de 9 bits, per tant quan configurem els registres 0x30 corresponents haurem de sumar 1 a la part de l'octava tal i com es veu a les línies 109, 110 i 111. El canal 1 que no té el novè bit, té un 0x36 al registre 0x21, mentre que els altres tenen el valor 0x37.
Un cop fet l'acord, anem a canviar el volum del canal 1, el de l'oboè, posant un número més gran que 0 a la part baixa del byte del registre 0x31 (línia 114). Esperem una tecla i apaguem tots els canals i sortim de l'aplicació amb Exit(0) per finalitzar el nostre test.
Conclusions
En aquest programa hem vist com tocar diferents notes amb l'OPLL (MSX-Music) del FM-PAC que ja ve de sèrie en els MSX2+ i Turbo-R. Un següent pas seria crear els nostres instruments, utilitzant per exemple el Frunace i mirar els instruments que porta de sèrie per veure el seu funcionament.
Un altre tema seria el tempo, aquí hem fet el test de forma seqüencial i sense moure cap sprite. Si volguéssim fer-ho, hauríem d'utilitzar les interrupcions. En aquest cas, com que l'OPLL no genera cap interrupció, hem d'utilitzar la del VDP que és sempre a 50Hz ó 60Hz (depèn de si és PAL o NTSC) i a partir d'aquí poder controlar el tempo. Això és el que s'utilitza en aquesta implementació de reproductor de Moonblaster feta en C, que utilitza la comanda InitVDPInterruptHandler del Fusion-C per sobreescriure el Hook del VDP i que cada 50 (ó 60) vegades per segon executi la nostra funció, que en aquest cas és mirar quina nota s'ha de tocar i configurar els registres corresponents per a que soni.
Clica aquí per a veure l'exemple funcionant.
Comments