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
- Ejemplo de un efecto generado con BeepFX
- Modificación del reproductor de BeepFX
- Cómo usar el reproductor
- Conclusión
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

- 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:
Nota | Frecuencia | |
---|---|---|
DO | C | 261,626 |
DO# | C# | 277,183 |
RE | D | 293,665 |
RE# | D# | 311,127 |
MI | E | 329,628 |
F | F | 349,228 |
FA# | F# | 369,994 |
SOL | G | 391,995 |
SOL# | G# | 415,305 |
LA | A | 440,000 |
LA# | A# | 466,164 |
SI | B | 493,883 |
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

- 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:
Frame | Pitch |
---|---|
1 | 128 |
2 | 0 |
3 | 128 |
4 | 0 |
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

- 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ámetro | Valor | Observaciones |
---|---|---|
Frames | 1 | |
Frame lenght | 500 | |
Pitch | 440 | |
Pitch slide | 0 | |
Duty Duty slide | 128 | Este valor resulta de Duty slide * 256 + Duty |
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ámetro | Valor | Observaciones |
---|---|---|
Frames | 1 | |
Frame lenght | 500 | |
Pitch Pitch slide | 100 | Este valor resulta de Pitch slide * 256 + Pitch |
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 Assembly, quita 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 aquí.
También puedes visitar el resto de tutoriales:
Y recuerda, si lo usas no te limites a copiarlo, intenta entenderlo y adaptarlo a tus necesidades.