Espamática
RetroZX Spectrum

Uso de BeepFX

El objetivo de esta entrada es dar unas pequeñas nociones para ayudar a sacar el mayor provecho posible a BeepFX.

Toda esto surge por la colaboración con Juntelart y su ZX-Game Maker para modificar la rutina de reproducción que genera BeepFX, y así evitar que los juegos que queden parados.

Una vez terminada esta rutina, me encontré con la circunstancia de que diseñaba efectos un tanto aleatoriamente, pero sin tener muy claro lo que hacía. Como no sé si alguien más se encuentra en esta situación, he decidido escribir este artículo; espero que os sea de provecho.

Antes de comenzar quiero agradecer los acertados comentarios que me han hecho mis compañeros del grupo Ensamblador Z80 de Telegram: Conrado Badenas Mengod y Pedro Picapiedra.

Tabla de contenidos

Cómo configurar los sonidos con BeepFX

Cada sonido se configura de manera distinta, aunque semejante, por lo que os muestro por separado tanto sus parámetros como su uso.

Parámetros del tono

BeepFX, tono
BeepFX, tono
  • Frames: número de fracciones, no confundir con los típicos cincuenta frames por segundo (o sesenta) del ZX-Spectrum. Admite valores de 1 a 65536.
  • Frame length: longitud de cada uno de los frames. Admite valores de 1 a 65536. 1 segundo es aproximadamente 32768.
  • Pitch: frecuencia, este valor indica la nota. Admite valores de 1 a 65536.
  • Pitch slide: deslizamiento de la frecuencia. Se aplica a cada frame, por lo que no tiene efecto si sólo hay un frame; es el valor que se suma a la frecuencia en cada frame. Admite valores de -32767 a 32768.
  • Duty: ciclo de trabajo. En base a este valor se va a activar o desactivar el EAR y el altavoz interno. El valor medio es 128, valor con el que las notas suenan más claras. Cambiando este valor se distorsiona el sonido. Admite valores de 0 a 255. Con 0 no se escucha nada.
  • Duty slide: igual que Pitch Slide pero para Duty. Admite valores de -127 a 128.

Ahora veamos como funcionan estos parámetros.

Si configuras un bloque con 1 frame, 32768 de longitud, 440 de pitch, 0 de pitch slide, 128 de duty y 0 de duty slide, oirás el LA siguiente a DO central. Si ahora pones 10 frames con una longitud de 3276 y pitch slide de 100, verás como el efecto dura el mismo tiempo, pero se escuchan diez tonos distintos. El primer frame empieza en la frecuencia 440 y va sumando 100 a la frecuencia en los siguientes, resultando 540, 640, 740, etc.

Las frecuencias del DO central y los once semitonos siguientes son:

NotaFrecuencia
DOC261,626
DO#C#277,183
RED293,665
RE#D#311,127
MIE329,628
FF349,228
FA#F#369,994
SOLG391,995
SOL#G#415,305
LAA440,000
LA#A#466,164
SIB493,883
BeepFX, frecuencias de las notas

Aunque las frecuencias tienen decimales, tú siempre especificarás los valores enteros. Para subir una octava debes multiplicar el valor de la frecuencia por dos y para bajar una octava dividirlo entre dos, de ahí que se reflejen los valores decimales. Si subes tres octavas (2^3), al multiplicar por ocho el SI sin tener en cuenta los decimales da tres mil novecientos cuarenta y cuatro, pero teniendo en cuenta los decimales da tres mil novecientos cincuenta y uno sin tener en cuenta los decimales del resultado, una diferencia de siete, aunque no creo que se aprecie en demasía.

Si te interesa saber más sobre esta frecuencias, Conrado me ha proporcionado este enlace.

Parámetros del ruido

BeepFX, ruído
BeepFX, ruido
  • Frames: igual que Frames en los parámetros del tono.
  • Frame length: igual que Frame length en los parámetros del tono.
  • Pitch: parecido a Pitch en los parámetros del tono, pero admite valores de 1 a 256. Usa los valores que hay en los primeros 8Kb de la ROM para generar el ruido.
  • Pitch slide: igual que Pitch slide en los parámetros del tono.

Fijaos que en Pitch indico que es semejante a Pitch en los parámetros del tono. Probad a configurar un ruido con Frames = 10, Frame length = 30000, Pitch = 128 y Pitch slide = 128. Al reproducir este bloque apreciaréis que hay una variación de sonido, digamos, entre los frames pares y los impares. Esto se debe a que en los ruidos, Pith y Pitch Slide son bytes, resultando que los valores de Pitch en cada frame son:

FramePitch
1128
20
3128
40
BeepFX, efecto de Pitch Slide sobre Pitch en los ruidos

Y así hasta llegar al décimo frame.

Para conseguir mayor variación de tonos pon un valor más bajo en Pitch Slide, por ejemplo treinta y dos.

Parámetros de la pausa

BeepFX, pausa
BeepFX, pausa
  • Frames: igual que Frames en los parámetros del tono.
  • Frame length: igual que Frame length en los parámetros del tono.

Si te fijas en el código ensamblador que genera BeepFX, la pausa es idéntica al tono (incluso se identifica como tal), pero con los parámetros Pitch, Pitch slide, Duty y Duty slide a cero.

Esto es algo que tienes que tener muy en cuenta. Si tu sonido (o quizá música) va a reproducir un bloque en cada interrupción, pero para conseguir el compás correcto necesitas incluir pausas (notas que no suenan), estas pausas debes configurarlas con Frames = 1 y Frame length = 1. Durante esa interrupción, o en el bucle del programa, lo único que interesa es que la nota que suena no suene, así que cuánto menos dure mucho mejor.

Ejemplo de un efecto generado con BeepFX

A continuación, un ejemplo de un efecto generado con BeepFX que contiene los tres tipos de bloques especificados anteriormente (se muestra el código ensamblador que genera el programa).

SoundEffectsData:
  defw SoundEffect0Data

SoundEffect0Data:
  defb 1 ;tone
  defw 1,500,440,0,128
  defb 1 ;tone
  defw 1,500,440,0,128
  defb 1 ;pause
  defw 1,500,0,0,0
  defb 1 ;pause
  defw 1,500,0,0,0
  defb 2 ;noise
  defw 1,500,100
  defb 2 ;noise
  defw 1,500,100
  defb 0

En los bloques de tono y pausa, en las líneas que empiezan por defw, los valores son los siguientes (son valores de 16 bits):

ParámetroValorObservaciones
Frames1
Frame lenght500
Pitch440
Pitch slide0
Duty
Duty slide
128Este valor resulta de Duty slide * 256 + Duty
BeepFX, tono

En los bloques de ruido, en las líneas que empiezan por defw, los valores son los siguientes (vuelven a ser valores de 16 bits):

ParámetroValorObservaciones
Frames1
Frame lenght500
Pitch
Pitch slide
100Este valor resulta de Pitch slide * 256 + Pitch
BeepFX, ruido

Los bloques de Tono y Pausa tienen una longitud de once bytes, desde el byte que indica el tipo de bloque, hasta el último byte del bloque. Los bloques de Ruido tienen una longitud de siete bytes desde el byte que indica el tipo de bloque hasta el último byte del bloque.

En ensamblador, los valores separados por coma que se encuentran después de defb o db son de tipo byte (8 bits). Por el contrario, los valores que se encuentran después de defw o dw son de tipo word (palabra) o dos bytes (16 bits).

Cada bloque está compuesto por una primera línea defb en la que se indica el tipo de bloque, y una segunda línea defw en la que se indican los parámetros del bloque.

Modificación del reproductor de BeepFX

Con BeepFX puedes crear efectos realmente sorprendentes, incluso proporciona un reproductor para que no tengas que preocuparte de nada, pero aquí hay un gran problema: el reproductor reproduce todo el efecto, lo cual puede provocar, y provoca, que tus juegos se paren. En cuanto quieras tener un efecto elaborado, seguro que tu juego se detiene mientras se reproduce.

Ya comenté al principio que este artículo surge a consecuencia de una conversación que tuve con Junterlart. He realizado una modificación sobre el reproductor de Shiru, para que se pueda reducir bloque a bloque en lugar de reproducir el efecto completo.

Para comprender lo que hace la rutina de Shiru la he comentado línea a línea, lo cual me ha ayudado también a entender como funciona BeepFX. Esta es la rutina tal y como ha quedado.

; BeepFX player by Shiru
; You are free to do whatever you want with this code
; Modificado por Juan Antonio Rubio García

; Si se va a usar desde fuera de ASM, antes de Play poner ORG Dirección para que calcule las
; direcciones al ensamblar
; Cambios realizados para su uso en ZX-Game Maker

; Primero: en la dirección Play+$01 cargar el sonido a reproducir
;     LD   Play+$01,$03   POKE Play+1,3           Indica cargar el sonido 3
; Segundo: llamar a Play
;     CALL Play           RANDOMIZE USR Play      Pone en memoria la dirección del sonido 3
; Tercero: llamar a NextNote cuando sea preciso
;     CALL NextNote       RANDOMIZE USR Play+17   Reproduce un bloque del efecto y sale
;                                                 Si no hay ningún efecto cargado sale
Play:
  ld   c, $00           ; En $00 se indicará el sonido a reproducir
  ld   b, $00           ; BC = A, efecto a reproducir
  ld   hl,FXAddress     ; Dirección en la que están las direcciones de los efectos
  add  hl,bc
  add  hl,bc            ; HL = dirección donde está la dirección del sonido a reproducir
  ld   c, (hl)
  inc  hl
  ld   b, (hl)          ; BC = dirección del sonido a reproducir
  ld   (FX),bc          ; Guarda en memoria la dirección del sonido a reproducir
  ret

NextNote:
  ld   hl,(FX)          ; HL = dirección del siguiente bloque a reproducir
  ld   a, (hl)          ; Carga el tipo de efecto en A (es el primer byte del bloque)
  or   a                ; Comprueba si es 0
  ret  z                ; Si es 0 sale, nada que reproducir

  di                    ; Desactiva las interrupciones
  push ix
  push iy               ; Preserva los registros índice
  push hl
  pop  ix               ; Carga en IX el valor de HL (dirección del efecto/bloque), ver línea 29

  ld   a, ($5C48)       ; Obtiene los atributos del borde
  rra
  rra
  rra                   ; Los pone en los bits 0 a 2
  and  $07              ; Se queda con los atributos del borde
  ld (sfxRoutineToneBorder  +$01),a
  ld (sfxRoutineNoiseBorder +$01),a ; Modifica las rutinas con el valor del borde
                                    ; OR $00, sustituye 0 por el valor de A, en líneas 102 y 150

readData:
  ld   a, (ix+$00)      ; A = tipo de bloque
  ld   c, (ix+$01)
  ld   b, (ix+$02)      ; BC = Frames
  ld   e, (ix+$03)
  ld   d, (ix+$04)      ; DE = Frame lenght, estos tres parámetros son comunes en tono y ruido
  push de
  pop  iy               ; Carga en IY el valor de DE, Frame lenght

  dec  a                ; Decrementa A (Tipo de bloque)
  jr   z,sfxRoutineTone ; Si es 0, es tono, salta
  dec  a                ; Decrementa A
  jr   z,sfxRoutineNoise; Si es 0, es ruido, salta
endData:
  pop  iy
  pop  ix               ; Recupera los registros índice
  ei                    ; Reactiva las interrupciones
  ret                   ; Sale de la rutina
   
nextData:
  add  ix,bc            ; Apunta IX al bloque siguiente, ver líneas 129 y 183 para BC, líneas 37 y 38 para IX
  ld   (FX),ix          ; Guarda en memoria la dirección del siguiente bloque 
  jr   endData          ; Salta para salir de la rutina

; Genera tono con seis parámetros, 11 bytes por bloque. Frame length 32768 = 1 segundo aprox.
; Tono
;       0
;	      Type = Tone
;  defb 1
;       1 - 2 	3 - 4 	      5 - 6           7 - 8 	  9 - 10
;	Frames	Frame lenght  Pitch	      Pith slide  Duty y Duty slide
;                             Frecuencia
;  defw 65536,	65536,	      65535,	      32768,	  33023
sfxRoutineTone:
  ld   e, (ix+$05)      ; IX = dirección el bloque, ver líneas 37 y 38
  ld   d, (ix+$06)      ; DE = Pitch
  ld   a, (ix+$09)      ; A = Duty
  ld   (sfxRoutineToneDuty+1),a ; Modifica la rutina con el valor de Duty
                                ; CP $00, sustituye 0 por el valor de A, ver línea 98
  ld   hl,$00           ; HL = 0. Para reproducir el sonido va sumando Pitch (frecuencia)
                        ; y usa el byte alto para activar/desactivar EAR y altavoz interno
sfxRT0:
  push bc               ; Preserva BC (Frames), ver líneas 51 y 52
  push iy
  pop  bc               ; Carga en BC el valor de IY (Frame lenght), ver líneas 55 y 58
sfxRT1: ; Desde aquí la rutina tarda 88 t-sates hasta la línea 108 (sincronización).
  add  hl,de            ; Suma DE (Pitch) a HL (11 t-states)
  ld   a, h             ; Carga el byte alto en A (4 t-sates)
sfxRoutineToneDuty:
  cp   $00              ; Lo compara con el valor de Duty ($00 se modifica en las líneas 110 a 112, se carga la primera vez en línea 86) (7 t-sates) 
  sbc  a, a             ; Resta A - A teniendo en cuenta el acarreo (4 t-states)
  and  $10              ; Se queda con el bit 4, EAR y altavoz interno activado/desactivado (7 t-states)
sfxRoutineToneBorder:
  or   $00              ; Añade el color del borde (se modificó en las línea 45) (7 t-states)
  out  ($fe),a          ; Manda el valor al puerto 254 para activar/desactivar EAR y altavoz interno (11 t-sates)
  ld   a,(0)            ; (13 t-states)
  dec  bc               ; Decrementa BC (6 t-sates)
  ld   a, b             ; (4 t-states)
  or   c                ; Comprueba si BC = 0 (4 t-sates)
  jp  nz, sfxRT1        ; Si no es 0, bucle hasta que Frame lenght = 0 (10 t-states, Total 88 t-states)

  ld   a,(sfxRoutineToneDuty+1) ; Cambio de Duty en línea 98
  add  a,(ix+$0a)               ; Añade el valor de Duty slide
  ld   (sfxRoutineToneDuty+1),a ; Cambia el valor de la línea 98

  ld   c, (ix+$07)
  ld   b, (ix+$08)      ; BC = Pitch slide
  ex   de,hl            ; Intercambia los valores de DE y HL
  add  hl,bc            ; Suma Pitch slide a Pitch, Pitch se carga en líneas 83 y 84
  ex   de,hl            ; Intercambia los valores de DE y HL (DE = Pitch + Pitch slide)

  pop  bc               ; Recupera BC (Frames)
  dec  bc               ; Decrementa BC
  ld   a, b
  or   c                ; Comprueba si BC = 0
  jr   nz,sfxRT0        ; Si no es 0, bucle hasta que Frames = 0

  ld   c, $0b           ; C = bytes a los que está el siguiente bloque, BC viene a 0
  jp   nextData

; Genera ruido con dos parámetros, 7 bytes por bloque. Frame length 32768 = 1 segundo +/-.
;       0
;       Type = Noise
; defb  2 ;noise
;       1 - 2   3 - 4         5 - 6
;       Frames  Frame lenght  Pitch y Pitch slide
; defw  65535,  65535,        33023
sfxRoutineNoise:
  ld   e, (ix+$05)      ; E = Pitch, IX = dirección el bloque, ver líneas 37 y 38
  ld   d, $01
  ld   h, d
  ld   l, d             ; HL = $11 (Dirección 17 de la ROM)
sfxRN0:
  push bc               ; Preserva BC (Frames)
  push iy
  pop  bc               ; Carga en BC el valor de IY (Frame lenght), ver líneas 55 y 56
sfxRN1: 
; Desde aquí, la rutina tarda 112 t-states hasta la línea 168 si en la línea 153 D=0 y 88 si D<>0
  ld   a, (hl)          ; A = Valor de la dirección a la que apunta HL (en los primeros 8Kb de la ROM) (7 t-states)
  and  $10              ; Se queda con el bit 4, activa o desactiva EAR y altavoz interno (7 t-states)
sfxRoutineNoiseBorder:
  or   $00              ; Añade el color del borde ($00 se modificó en la línea 46) (7 t-states)
  out  ($fe),a          ; Manda el valor al puerto 254 para activar/desactivar EAR y altavoz interno (11 t-sates)
  dec  d                ; Decrementa D (4 t-states)
  jp   z, sfxRN2        ; Si es 0 salta (10 t-states)
  nop                   ; (4 t-states)
  jp   sfxRN3           ; Salta (10 t-states)
sfxRN2:
  ld   d, e             ; D = Pitch (4 t-states)
  inc  hl               ; HL += 1 (6 t-states)
  ld   a, h             ; A = H (4 t-states)
  and  $1F              ; Se queda con el valor de los bits 0 a 4 (7 t-states)
  ld   h, a             ; H = A, dejando HL apuntando a alguna dirección de los primeros 8Kb de la ROM (4 t-states)
  ld   a, ($00)         ; (13 t-sates)
sfxRN3:
  nop                   ; (4 t-states)
  dec  bc               ; Decrementa BC (6 t-states)
  ld   a, b             ; (4 t-states)
  or   c                ; Comprueba si BC = 0 (4 t-states)
  jp   nz, sfxRN1       ; Si no es 0, bucle hasta que Frame lenght = 0 (10 t-states, Total 88 o 112 t-states)

  ld   a, e             ; A = Pitch
  add  a, (ix+$06)      ; A += Pitch slide
  ld   e, a             ; Actualiza Pitch

  pop  bc               ; Recupera BC (Frames)
  dec  bc               ; BC -= 1
  ld   a, b
  or   c                ; Comprueba si BC = 0
  jr   nz,sfxRN0        ; Si no es 0, bucle hasta que Frame = 0

  ld   c, $07           ; C = bytes a los que está el siguiente bloque, BC viene a 0
  jp   nextData

FX:
  dw   $001E            ; Se inicializa a $001E para que desde la primera vez salga de NextNote
                        ; si no tiene que reproducir nada (líneas 29 a 32)

FXAddress:
; Archivo asm generado por BeepFX sin reproductor
include "fx.asm"

Cómo usar el reproductor

El reproductor tiene dos puntos de entrada, Play y NextNote. Lo primero que tienes que hacer es poner en la dirección de memoria Play+1 el número de efecto que quieres reproducir, y acto seguido llamar a Play. Posteriormente, cada vez que quieras reproducir un bloque, en cada interrupción, en cada iteración del bucle de tu juego, debes llamar a NextNote.

En ensamblador sería algo así:

ld   (Play+$01), $01
call Play
...
...
...
call NextNote

En BASIC se haría de la siguiente manera, suponiendo que cargas el reproductor y los efectos a partir de la línea 50000:

POKE 50001,1
RANDOMIZE USR 50000:REM Llama a PLAY
...
...
...

RANDOMIZE USR 50017

No obstante, próximamente voy a publicar en El taller de Juanan en la web de Play On Retro, una variación de la rutina que creo que simplifica su uso desde BASIC.

Ten en cuenta que los efectos que diseñes en BeepFX no van a sonar igual en tus juegos. La explicación a esto es que en tu juego vas a reproducir un bloque, por ejemplo, por cada interrupción, mientras que en BeepFX se reproduce todo el efecto completo. Para simular esto en BeepFX debes meter pausas ficticias de una interrupción; hay cincuenta interrupciones por segundo, por lo que una aproximación sería entre cada tono o ruido poner una pausa de un frame con una longitud de seiscientos cincuenta y cinco. Estas pausas ficticias se verán fácilmente identificadas como:

defb 1
defw 1,655,0,0,0

No olvides borrarlas del archivo generado por BeepFX.

También es posible que tu efecto tenga pausas necesarias, es decir, no quieres que suene un bloque en cada interrupción, si no que quieres que haya más espacio. Para esto último incluye pausas de un frame y una longitud de uno, así las podrás distinguir fácilmente de las pausas para simular el modo de reproducción y no entorpecerán el flujo del juego.

Una vez que tengas todos los efectos diseñados, en el menú File, haz clic sobre Compile y en la ventana emergente selecciona Assemblyquita el check a Include player code (no vamos a usar el reproductor de BeepFX) y haz clic sobre Compile (el resto de parámetros no son importantes en nuestro caso). En el diálogo Save compiled data, graba el archivo como fx.asm en el directorio dónde tengas los fuentes para que el ensamblador que uses pueda localizarlo en la línea include que hay al final del reproductor.

Conclusión

Espero que todo lo aquí descrito os sirva para que vuestros juegos no se paren cuándo se reproduce algún efecto de sonido creado con BeepFX y en consecuencia fluyan mejor.

La idea de esta modificación surgió cuándo Juntelart me comentó el problema al reproducir los efectos de sonido, que paraban los juegos, y creo que hemos dado con una solución decente.

La colaboración hace la fuerza.

Puedes descargar el proyecto de pruebas, con el código fuente desde aq.

Y recuerda, si lo usas no te limites a copiarlo, intenta entenderlo y adaptarlo a tus necesidades.

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.

Este sitio web utiliza cookies para que usted tenga la mejor experiencia de usuario. Si continúa navegando está dando su consentimiento para la aceptación de las mencionadas cookies y la aceptación de nuestra política de cookies, pinche el enlace para mayor información.plugin cookies

ACEPTAR
Aviso de cookies

Descubre más desde Espamática

Suscríbete ahora para seguir leyendo y obtener acceso al archivo completo.

Seguir leyendo