Skip to content

Sesión 4 Videojuegos1

Juan Gonzalez-Gomez edited this page Oct 21, 2021 · 53 revisions

Contenidos

Sesión V1-4.1: Motivación, esfuerzo y tiempo para crear Videojuegos en ensamblador (I)

Click to see the youtube video

Sesión V1-4.2: Reproduciendo música en ensamblador Z80 con CPCtelera

Click to see the youtube video

Sintetizador de onda cuadrada: AY-8912

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

El programa Arkos Tracker 2

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

Conversion de musica

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&#233; sue&#241;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...

Tamaño del binario y dirección de comienzo

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

Reproduciendo la música

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_loop

El 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 0

Sesión V1-4.3: Música por interrupciones en ensamblador Z80 con CPCtelera

Click to see the youtube video

Problema: Bucle principal grande

Este 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

Rutina de atención a la interrupción inicial

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:: 
  ret

Para 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

Cambiando el color del borde

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 

  ret

Esto es lo que vemos:

Cada una de las franjas de color representa una de las 6 interrupciones

Tocando música

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'
  ret

Al 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

Clone this wiki locally