Repository navigation
Sesión 4 Videojuegos1
- Sesion V1-4.1: Motivación, esfuerzo y tiempo para crear Videojuegos en ensamblador (I)
- Sesión V1-4.2: Reproduciendo música en ensamblador Z80 con CPCtelera
- Sesión V1-4.3: Música por interrupciones en ensamblador Z80 con CPCtelera
La música en el amstrad se hace mediante el chip AY-8912: es un sintetizador de onda cuadrada. La hoja de datos está disponible aquí: https://www.cpcwiki.eu/index.php/Datasheet_AY-8913
Será necesario llamar a la función de reproducción de sonido desde el bucle principal, que se ejecuta a 50Hz. Esto depende de cómo se haya generado la música. Otras requiren de 25Hz. En ese caso habrá que modificar el bucle principal para llamar a la función a estos 25Hz
Para generar las canciones utilizamos el programa Arkos Tracker 2, que está disponible para Linux
He hecho esta mini-melodía (parte inicial del himno de la alegría) de sólo 1 canal. Lo he exportado al fichero Alegria.wav

Igual que hemos hecho con los sprites, usamos una macro de conversión de música del formato Arkos (.aks) a C y ensamblador para incluir en nuestros programas. Es la que se encuentra en cfg/music_conversion.mk
## Convert music/song.aks to src/music/song.s and src/music/song.h
## This file contains a music created with Arkos Tracker. This macro
## will convert the music into a data array called g_mysong that will be
## placed at the 0x42A0 memory address in an absolue way.
##
$(eval $(call AKS2C,assets/alegria.aks,song_alegria,src/music/,0x0040))El último parámetro es la dirección donde queremos que se almacene la canción. Se utilizan posiciones de memoria absolutas por un tema de optimizaciones. Lo ponemos en la dirección más baja posible (y que sea segura): la dirección 0x0040
Al probarlo con la canción mía, da un error de conversión:

Tengo que averiguar por qué...
Voy a probar con la música que usa en el vídeo: princess7.aks. OK! la compilación es correcta:

Se han generado correctamente los fichero src/music/princess7.h y src/music/princess7.s con la información de la canción.
Ahora voy a abrir el fichero princess7.aks con el Arkos 2 y a grabarlo con otro nombre, a ver si el formato es el mismo. Comprueba que son diferentes:
obijuan@Hoth:~/Develop/DEZ80/RA2017/practica/assets$ ls -l princess*.aks
-rw-r--r-- 1 obijuan obijuan 6328 oct 21 15:43 princess7_2.aks
-rw-rw-r-- 1 obijuan obijuan 51272 oct 20 21:53 princess7.aks
obijuan@Hoth:~/Develop/DEZ80/RA2017/practica/assets$
El que he generado nuevo, con el Arkos2 parece que está comprimido. Al compilar sale este error:
[practica] Converting music in assets/princess7_2.aks into data arrays...
cpct_aks2c -m "0x0040" -od src/music/ -id song_princess7 assets/princess7_2.aks
Converting 'assets/princess7_2.aks' to binary...
#########################
## UNRECOVERABLE ERROR ##
#########################
##> There was an error exporting song 'assets/princess7_2.aks'. Please check that 'assets/princess7_2.aks' is a valid AKS song file and run this script again.
El archivo original está en xml. Esta es la parte inicial:
<?xml version="1.0"?>
<Song xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Version>Arkos Tracker 1.0</Version>
<FileName>G:\arkosTracker-1.0\princess7.aks</FileName>
<Name>CodePrincess</Name>
<Author>Joe McAlby</Author>
<Comments>Qué sueño!</Comments>
<DOSName />
<Length>4</Length>
<LoopStart>0</LoopStart>
<LoopEnd>3</LoopEnd>
<Frequency>freq25hz</Frequency>
<MasterFrequency>1000000</MasterFrequency>
<SamplesChannel>1</SamplesChannel>
<BeginningSpeed>3</BeginningSpeed>
<Highlight>8</Highlight>
...Al descomprimirlo se obtiene el archivo xml, pero el formato lo han cambiado (han añadido esquemas) y el script de conversión de cpctelera no lo reconoce:
Esta es la nueva cabecera del xml (princess7_2.aks)
<?xml version="1.0" encoding="UTF-8"?>
<aks:song xmlns:aks="http://www.julien-nevo.com/ArkosTrackerSong" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.julien-nevo.com/ArkosTrackerSong.xsd">
<aks:formatVersion>1.0</aks:formatVersion>
<aks:title>CodePrincess</aks:title>
<aks:author>Joe McAlby</aks:author>
<aks:composer>Joe McAlby</aks:composer>
<aks:comment>Qué sueño!</aks:comment>
<aks:creationDate>2021-10-21</aks:creationDate>
<aks:modificationDate>2021-10-21</aks:modificationDate>
<aks:fmInstruments>
<aks:fmInstrument version="1.0">
<aks:number>1</aks:number>
<aks:title>Pluck bass</aks:title>
<aks:speed>0</aks:speed>
<aks:isLooping>false</aks:isLooping>
<aks:loopStartIndex>0</aks:loopStartIndex>
<aks:endIndex>14</aks:endIndex>
<aks:isRetrig>false</aks:isRetrig>
<aks:colorArgb>4292927712</aks:colorArgb>
<aks:fmInstrumentCell>
...He encontrado más información. En este fork de cpctelera han añadido macros para convertir desde arkos 2: https://github.com/cmp102/cpctelera/tree/arkostracker2
De momento, para las pruebas voy a usar la música generada con Arkos 1...
El fichero binario que se genera obj/practica.bin ocupa 19K!!!
$ ls -lh obj/practica.bin
-rw-rw-r-- 1 obijuan obijuan 19K oct 21 16:12 obj/practica.bin
Pero la canción no ocupa tanto, ni el juego tampoco. La información de la canción la vemos en el fichero src/music/princess7.h:
/// Song 'song_princess7' metadata macros
#define song_princess7_address 0x0040
#define song_princess7_size 1854
#define song_princess7_end 0x77E
/// Song 'song_princess7' array declaration
extern void song_princess7[song_princess7_size];Ocupa 1854 bytes
El tamaño tan grande del fichero practica.bin es debido a que es una imagen exacta de lo que hay en memoria. Al principio están los datos de la canción. Y luego, desde la dirección 0x77E hasta que empieza el programa en la 0x4000, se meten todos 0's en el binario
Como la canción termina en la dirección 0x77E, podemos hacer que nuestro programa comience en la 0x77F (justo a continuación). Esto lo hacemos editando el fichero cfg/build_config.cfg
[...]
# Name of the project (without spaces, as it will be used as filename)
# and Z80 memory location where code will start in the generated binary
PROJNAME := practica
Z80CODELOC := 0x4000
[...]
En Z80CODELOC ponemos la dirección 0x077F
Ahora al compilar vemos que el programa comienza en la dirección 0x0040 y termina en la 0x12A6
[...]
Binary file start = 00000040
Records start = 00000040
Highest address = 000012A6
[...]
El nuevo binario ocupa 4.7K
$ ls -lh obj/practica.bin
-rw-rw-r-- 1 obijuan obijuan 4,7K oct 21 16:38 obj/practica.bin
Para reproducir la música tenemos usamos las siguientes funciones de cpctelera
Lo ponemos en el main.s. Primero inicializamos la música:
[...]
;;-- Change the color palette
ld hl, #_sprite_palette
ld de, #16
call cpct_setPalette_asm
;;-- Initialize
ld DE, #_song_princess7
call cpct_akp_musicInit_asm
;;-- Draw the map
;;-- Configure the tileset
ld hl,#_g_tileset
call cpct_etm_setTileset2x4_asm
[...]Y ahora en el bucle principal añadimos la llamada para el refresco de la música:
[...]
;;-- Wait for the raster to finish
call cpct_waitVSYNC_asm
;;-- Update the music!
call cpct_akp_musicPlay_asm
jr main_loopEl ponerlo en el bucle principal nos garantiza que se llama a la función de música a la frecuencia de 50Hz (justo después de que el raster haya terminado de dibujar)
si lo probamos veremos que funciona... se escucha la música... pero va ¡MUY RÁPIDO!
Esto es porque esa música se ha creado a 25Hz, por lo que hay que llamar desde el bucle principal a la frecuencia de 25hz, en vez de 50Hz
[...]
;;-- Wait for the raster to finish
call cpct_waitVSYNC_asm
;;-- Toggle the play_music variable
ld a,(play_music)
xor #1
ld (play_music), a
jr z, main_loop
;;-- Update the music!
;;-- It is refresh at the rate speed of 25Hz
call cpct_akp_musicPlay_asm
jr main_loop
; -- Aux variable for playing the music
; -- at the rate of 25Hz
play_music: .db 0Este sistema de reproducir música está funcionando porque el bucle principal que tenemos es pequeño, y podemos garantizar que se ejecuta a 50Hz. Pero si tenemos un bucle principal que haga más operaciones, ocurrirá que al llamar a VSYNC el raster ya habrá pasado y se quedará esperando al siguietne. Resultado: las cosas irán lentas, y el bucle principal YA NO irá a 50Hz. La música NO sonará bien
Esto lo podemos comprobar hacendo copy&paste de algunas operaciones, simulando que en el bucle se hacen más cosas:
[...]
call hero_draw ;;-- Draw the hero
call obstacle_draw ;;-- Draw the bullet
call ball_draw ;;-- Draw the ball
call hero_draw ;;-- Draw the hero
call obstacle_draw ;;-- Draw the bullet
call ball_draw ;;-- Draw the ball
call hero_draw ;;-- Draw the hero
call obstacle_draw ;;-- Draw the bullet
call ball_draw ;;-- Draw the ball
call hero_draw ;;-- Draw the hero
call obstacle_draw ;;-- Draw the bullet
call ball_draw ;;-- Draw the ball
call hero_draw ;;-- Draw the hero
call obstacle_draw ;;-- Draw the bullet
call ball_draw ;;-- Draw the ball
call hero_draw ;;-- Draw the hero
call obstacle_draw ;;-- Draw the bullet
call ball_draw ;;-- Draw the ball
call hero_draw ;;-- Draw the hero
call obstacle_draw ;;-- Draw the bullet
call ball_draw ;;-- Draw the ball
call hero_draw ;;-- Draw the hero
call obstacle_draw ;;-- Draw the bullet
call ball_draw ;;-- Draw the ball
call hero_draw ;;-- Draw the hero
call obstacle_draw ;;-- Draw the bullet
call ball_draw ;;-- Draw the ball
call hero_draw ;;-- Draw the hero
call obstacle_draw ;;-- Draw the bullet
call ball_draw ;;-- Draw the ball
[...]Al ejecutarlo va todo más lento, y también podrían aparecer parpadeos en el sprite. Para solucionar esto usamos las interrupciones
Empezamos por una rutina de atención a las interrupciones nula, que no hace nada. Declaramos la etiqueta isr como global para poder poner puntos de ruptura:
isr::
retPara establecer esta función como rutina de atención a las interrupciones llamamos a la función cpct_setInterruptHandler.html
[...]
;;-- Set ISR
ld hl, #isr
call cpct_setInterruptHandler_asm
[...]Si ejecutamos ahora, veremos el juego (sin sonido todavía). Se está llamando a la rutina de atención a la interrupción, pero no lo vemos. Para comprobarlo vamos a poner un punto de ruptura
Buscamos en el fichero practica.map el símbolo isr.Vemos que la rutina está en la dirección 0x0E97
[...]
00000E71 bullet_draw
00000E77 bullet_erase
00000E97 isr
00000ECF _main
00000F67 ball_draw
00000F6F ball_update
[...]
Ejecutamos el juego y antes de que arranque lo ponemos en pausa. Vamos a la dirección 0x0E97 y ponemos un punto de ruptura

Es importante marcar la opción write para acceder a la ram (ram y rom comparten las partes bajas de la memoria)

En la animación vemos que efectivamente la rutina de atención a la interrupcion se está ejecutando
Vamos a cambiar el color del borde durante la rutina de atencion a la interrupción para poder "verla". El color se cambia llamando a la rutina cpct_setPALColour-asm
Cada interrupción se produce 6 veces por fotograma. En cada una de ellas usaremos un color, y lo iteraremos 6 veces, desde el valor 0x10 al 0x16
color: .db 0x10
isr::
ld l, #16 ;;-- Establecer color del borde
ld a, (color)
ld h, a
call cpct_setPALColour_asm
ld a, (color)
inc a
cp #0x16
jr nz, continue
;;-- Reset color
ld a, #0x10
continue:
ld (color),a
retEsto es lo que vemos:

Cada una de las franjas de color representa una de las 6 interrupciones
Para reproducir la música hay que llamar a la funcion cpct_akp_musicPlay_asm desde la rutina de atención a la interrupción. Pero ahora hay que tener en cuenta que se ejecuta 6 veces por fotograma. Como la frecuencia de refresco de la música debe ser de 25Hz, llamaremos a esta función 1 vez de cada 12. Para ello usamos la variable contador play_music inicializada a 12 y la vamos descontando. Al llegar a 0 llamamos a la reproducción de música
play_music: .db #12
isr:
ex af, af'
exx
push af
push iy
push bc
push de
push hl
;;-- Toggle the play_music variable
ld a,(play_music)
dec a
ld (play_music), a
jr nz, volver
;;-- Update the music!
;;-- It is refresh at the rate speed of 25Hz
call cpct_akp_musicPlay_asm
ld a, #12
ld (play_music), a
volver:
pop hl
pop de
pop bc
pop iy
pop af
exx
ex af, af'
retAl configurar la isr con la función cpct_setInterruptHandler_asm ya se guardan en la pila los registros normales. Sin embargo no se guardan los alternativos ni los registros iy,ix. La función de reproducción de música utilia los registros alternativos y el iy por lo que tenemos que guardarlos en la pila y recuperarlos al terminar


