¿Qué es el MSX-Music?
El MSX-Music es un sistema de sonido basado en el chip Yamaha YM2413 (OPLL - FM Operator Type-L Light), que es una simplificación del YM3812 (OPL2).
No se debe confundir con el MSX-Audio que utiliza el Y8950 (OPL1) y que, como diferencia importante, contiene un canal ADPCM que no se encuentra en el OPLL. La parte de FM es la misma.
El cartucho de Panasonic FS-CA1 es el único que cumplía las especificaciones del MSX-Audio. Las otras implementaciones de la época comercial se encuentran en los cartuchos Philips NMS 1205 (Music Module) y Toshiba HX-MU900 (FM-Synthesizer Unit), que se pueden ampliar para ser totalmente compatibles con el formato. La última implementación de este estándar es el AudioWave.
El MSX-Music se encuentra en los 2+ y en los Turbo-R. En la época comercial también se podía conseguir con el Panasoft FM-Pak. Luego salieron muchos más cartuchos compatibles con el formato, como se indica en el wiki de msx.org. Este sistema de sonido permite generar 9 canales independientes de sonido con la técnica de síntesis FM o 6 canales FM y 5 percusiones diferentes. También permite elegir el instrumento diferente para cada canal, teniendo 15 definidos y un instrumento que se puede crear por el usuario. Las especificaciones del chip se pueden encontrar aquí.
¿Qué haremos en este post?
Primero, crearemos una función en ensamblador que escriba los registros del OPLL para poder configurarlos desde el MSX. Con esta función, escribiremos una nota diferente en cada canal. También configuraremos un instrumento propio y cambiaremos los instrumentos de los canales.
¿Cómo configuramos el OPLL desde el MSX?
El MSX configura el MSX-Music accediendo al puerto 0x7C y el 0x7D. En el primero se debe escribir el número de registro del YM2413 al que queremos acceder y en el segundo escribimos el valor que queremos que tome este registro. Estos valores los encontré en unos archivos que me pasaron en un grupo de Telegram cuando pedí información. He intentado averiguar si en el MSX Data Pack aparecía esta información y no lo he encontrado, pero mirando la página de Grauw he visto que hay un extracto del Datapack del MSX-Music donde aparece.
¿Cómo escribimos estos códigos en C?
Los comandos en ensamblador para hacer estos outs son con out(dirección,A). Para hacerlo con SDCC lo primero que se me ocurrió es utilizar las funciones en ensamblador que permite el SDCC tal y como explica Eric Boez en su libro FUSION-C: MSX C Library complete journey o Konamiman en su github
.
Necesitamos pasar dos parámetros que son de tipo char, en el primero pasamos el registro del YM2413 y en el segundo parámetro indicamos el valor que tomará este registro. Si miramos los ejemplos de Konamiman, coincide con la función SumTwoChars(char x, char y) de su ejemplo. La función para escribir parámetros quedaría como:
El apéndice __naked en la definición de la función sirve para indicar (tal y como se explica en la documentación del SDCC 3.11.3 Naked Functions) que el compilador no modifica ningún registro y que es el programador quien se encarga de preservar los diferentes registros. Con los indicadores asm __endasm marcamos que todo el código entre estos dos será en ensamblador SDCC. Este ensamblador tiene sus particularidades sintácticas, como por ejemplo que para hacer (IY)+1 como se hace en la mayoría aquí debe ser 1(IY).
Lo primero que hacemos es cargar los parámetros en los registros D y E con la dirección que hemos preparado antes en el registro IY. Esperamos unos ciclos para que los registros estén estables con la comando ex af,af' y cargamos el registro A con el parámetro que habíamos guardado en el registro D y hacemos el out. Esperamos que el valor de out se estabilice y hacemos otra salida a la dirección 0x7D con el valor del segundo parámetro de la función que habíamos guardado en E. Nos esperamos unos ciclos de reloj para que el YM2413 pueda leer los valores y terminamos la función.
Al escribir este artículo, he visto que el SDCC permite crear una variable y asociarla a un puerto (explicado en la sección 3.5.2 Z80/Z180/eZ80 intrinsic named address spaces del manual) usando la sintaxis __sfr__at (dirección_puerto) nombre_variable. Así en nuestro caso si creáramos __sfr__at(0x7C) num_registro_YM2413 cada vez que hiciéramos num_registro_YM2413 = 0x10 el compilador lo traduciría con los comandos
ld A,#0x10
out (#0x7C),A
Lo único que faltaría es una comando en C que tardara los mismos ciclos que ex af,af' y podríamos tener todo el código sin usar ensamblador. Pero de momento continuamos con lo que ya he podido comprobar que funciona.
Habrán notado que esperamos tiempos diferentes para que el OPLL lea cada puerto. Esto es así por especificación de Yamaha. Tal y como está documentado en el datapack en la tabla 7.22.
Tampoco se les habrá escapado que la función se llama Z80, y es que para R800, al ejecutar los comandos más rápido, tenemos que ejecutar más para tener el mismo tiempo de espera.
Funcionamento del Yamaha YM2413
Tal y como se indica en el datasheet del chip, en la Table II-7 tenemos un mapa de los registros que copio aquí para facilitar la lectura:
Los primeros siete registros son los utilizados para crear nuestro propio instrumento. Hay muchos artículos que explican cómo crear instrumentos a partir de la síntesis FM y es un tema que nunca he probado. Si alguien está interesado, el Moonblaster también tenía una parte de síntesis y te permitía experimentar con los que ya traía. He mirado el furnace y también tiene una parte de síntesis. He buscado por la web si había instrumentos creados por la gente y no he sabido encontrar ninguno. Lo que sí sé es que puedes cargar los que hay en el directorio de instrumentos para estudiar sus presets.
Después saltamos al registro 0x0E, que es el que configura el funcionamiento del chip si lo queremos en modo rítmico, bit 5 = 1, o en modo melódico, bit 5=0. En modo rítmico, los últimos 3 canales no se utilizan como instrumento sino que configuran 5 instrumentos de percusión: Bass drum, Snare drum, Tom, Cymbal y Hi-Hat.
Los registros del 10 al 18 contienen los 8 bits más bajos del número F. Este número F está muy relacionado con la frecuencia, tono, nota, del canal.
Los nueve registros siguientes, 20-28, contienen el bit más significativo del número F, 3 bits para indicar el bloque/octava, 1 bit para si el canal está activo y otro para indicar si debe mantener la nota del canal.
Los nueve registros últimos, 30-38, contienen cada uno el tipo de instrumento a tocar por ese canal y su volumen. Los instrumentos son: 0-Original, 1-Violín, 2-Guitarra, 3-Piano, 4-Flauta, 5-Clarinet, 6-Oboe, 7-Trompeta, 8-Órgano, 9-Cuerno, 10-Sintetizador, 11-Clavicordio, 12-Vibráfono, 13-Bajo sintético, 14-Bajo acústico y 15-Guitarra eléctrica. Sobre el volumen se debe tener en cuenta que 0 es máximo volumen y 15 es mínimo volumen.
¿Cómo indicamos la nota que debe sonar para uno de los canales?
Para indicar la nota que queremos que suene para uno de los canales, se debe modificar el F-number y el Block de ese canal. Cada canal está compuesto por 3 registros que lo definen. Por ejemplo, para el canal 0, se programan los registros 10, 20 y 30; para el canal 1 los registros 11, 21 y 31; y así sucesivamente hasta el canal 8 que está definido por 18, 28 y 38.
Para traducir la frecuencia de la nota que queremos que suene al doblete F-number y Block, Yamaha indica en su manual (página 15) que F=(fmus*2^(18)/fsam)/2^(b-1) donde F es el F-number, fmus es la frecuencia que nosotros queremos que suene y b es el bloque data (octava). También indican dos tablas con ejemplos de frecuencia (Table III-8-1 y Table III-8-2) donde se puede ver que para diferentes octavas la misma nota va cambiando de F-Number. Hay un post en msx.org que trata este tema y que en un comentario dice que una solución fácil es poner todos los F-Num según la frecuencia de la tabla que indica Table III-8-1 y luego ir modificando el bloque como si se tratara de la octava. Esta es la solución que se aplicó al driver de moonblaster hecho en C. Pero si miramos otras implementaciones, como el driver de moonblaster de bifi o el player del mbm del roboplay, vemos que ambos utilizan una tabla con las siguientes frecuencias:
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 };
Esta tabla contiene 8x12 elementos que son todas las frecuencias de las 12 notas por las 8 octavas. Al principio pensaba que estos valores eran las frecuencias de las notas musicales, pero luego, al volver a abrir el post mencionado arriba, me aclararon que ya son los valores directamente para el registro 10 y los 4 bits más bajos del 20 (he puesto 10 y 20 pero sirven para todos los canales, desde el 0 hasta el 8). Después he podido comprobar con diferentes cálculos utilizando los valores anteriores que realmente utilizan la octava como número de bloque y que los F-Number se iban repitiendo. De los datos anteriores he deducido que han utilizado un fsam de 83340.57803 que no sé exactamente de dónde sale este valor. Un ejemplo de la tabla para dos octavas es:
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 |
Por lo tanto, para escribir la nota, observaremos qué nota es, su correspondiente F-Number y la octava en la que queremos que suene. Si el F-Number es de 9 bits, tendremos que usar el del registro 20.
Programa que reproduce una nota
Vamos a hacer que este chip suene con un programa sencillo:
En primer lugar, creamos las funciones en lenguaje ensamblador. Si la computadora es un Turbo-R, agregamos más comandos para esperar a que los registros se estabilicen antes de ser leídos, lo cual es la función WriteOPLLreg_TR.
Y comenzamos la función principal (main). En esta ocasión, utilizaremos la pantalla 0 para mostrar mensajes en pantalla y esperar a que se presione una tecla para continuar con la prueba.
En la línea 59, elegimos el instrumento 15 (guitarra eléctrica, donde 15 corresponde a 0xF, y lo colocamos en la parte más alta del registro 0x30, que selecciona el instrumento y el volumen, en este caso, 0 que es el máximo). Ahora solo falta configurar la nota y la octava para que suene en el canal 0. La nota es un La, que según la tabla anterior tiene un valor de AD. Elegimos la octava 4 (b100), y si la desplazamos un lugar a la izquierda, dado que el bit menos significativo es el noveno bit del F-number, obtenemos 0x8. Si indicamos que ese canal está sonando (bit Key on del registro 0x20), entonces le asignamos el valor 0x18 al registro 20. Si quisiéramos que se mantuviera la nota durante un tiempo (sustain) cuando dejamos de presionar la tecla, escribiríamos el valor 0x38.
Mostramos en pantalla que el sonido está sonando y esperamos a que se presione una tecla para continuar.
Una vez presionada la tecla, cambiamos la nota para que sea dos octavas más grave. Para hacerlo, apagamos la nota estableciendo el bit Key en 0 en el registro 0x20 y escribimos el mismo F-number. Luego, volvemos a activar el Key y la octava en el registro 0x20. Entre las líneas 69 y 72, esperamos nuevamente a que se presione una tecla.
Ahora cambiaremos el instrumento y volveremos a la nota La (A4). Para esto, apagamos la nota, configuramos el violín y restablecemos el F-number, la octava y activamos el canal (bit Key) entre las líneas 72 y 75. Mostramos la nueva configuración en pantalla y esperamos una tecla.
Entre las líneas 79 y 91, cambiamos nuevamente la octava de la nota y la apagamos sin que tenga sustain. Luego, configuramos el canal con sustain en la línea 93 y esperamos una tecla para apagarlo. Cuando lo hacemos, notaremos que el sonido no se detiene inmediatamente como antes sin sustain, sino que se desvanece con el tiempo. El efecto del sustain varía según el instrumento elegido; algunos mantienen la nota, mientras que otros actúan como una percusión y apenas se nota el sustain.
Una vez que presionamos una tecla para continuar, reproduciremos el acorde de do mayor, que consta de las notas g3, e3 y c3, cada una tocada con un instrumento diferente. Entre las líneas 101 y 103, configuramos los instrumentos: clarinete para el canal 0, oboe para el canal 1 y corneta para el canal 2. Luego configuramos el F-number para las notas correspondientes, que son: 0x134, 0xCD y 0x103. Hay dos notas que tienen un F-number de 9 bits, por lo que cuando configuramos los registros 0x30 correspondientes, debemos sumar 1 a la parte de la octava, como se ve en las líneas 109, 110 y 111. El canal 1, que no tiene el noveno bit, tiene un valor de 0x36 en el registro 0x21, mientras que los otros tienen el valor de 0x37.
Una vez que se ha ejecutado el acorde, vamos a cambiar el volumen del canal 1, el del oboe, estableciendo un número mayor que 0 en la parte baja del byte del registro 0x31 (línea 114). Esperamos a que se presione una tecla y luego apagamos todos los canales y salimos de la aplicación con la función Exit(0) para finalizar nuestra prueba.
Conclusiones
En este programa hemos visto cómo tocar diferentes notas con el OPLL (MSX-Music) del FM-PAC que viene de serie en los MSX2+ y Turbo-R. El siguiente paso podría ser crear nuestros propios instrumentos, utilizando, por ejemplo, el Frunace y examinando los instrumentos que trae de serie para comprender su funcionamiento.
Otro tema importante sería el tempo. En este caso, hemos realizado la prueba de manera secuencial y sin mover ningún sprite. Si quisiéramos hacerlo, deberíamos utilizar interrupciones. Dado que el OPLL no genera interrupciones, tendríamos que utilizar las del VDP, que se producen a 50 Hz o 60 Hz (dependiendo de si es PAL o NTSC), para controlar el tempo. Esto es lo que se utiliza en esta implementación del reproductor de Moonblaster hecho en C, que utiliza el comando InitVDPInterruptHandler de Fusion-C para sobrescribir el Hook del VDP y ejecutar nuestra función cada 50 (o 60) veces por segundo, que en este caso sería para determinar qué nota tocar y configurar los registros correspondientes para que suene.
Haz clic aquí para ver el ejemplo funcionando.
Comments