Curso Programación ZX SpectrumCursos

Ensamblador para ZX Spectrum – Pong: $0C sonido y optimización

Llegamos a la recta final, estamos acabando.

5
(5)

Ensamblador para ZX Spectrum – Pong: Paso 10, sonido y optimización

Sí, como dijimos en el entrega anterior, la bola va algo lenta. Esto es debido, en gran parte, a que el marcador se repinta en cada iteración del bucle principal, lo cual no es necesario.

Optimización

El marcador solo se debería repintar cuando es borrado por la bola. Modificando este aspecto, vamos a ganar velocidad en la bola, ya que el tiempo de proceso en cada iteración del bucle principal se va a reducir.

Como es costumbre, creamos la carpeta Paso10 y copiamos los archivos Controls.asm, Game.asm, Main.asm, Sprite.asm y Video.asm desde la carpeta Paso09.

Lo primero es localizar el área de la pantalla dónde la bola borra el marcador, definiendo una serie de constantes en el archivo Sprite.asm, justo debajo de la constante POINTS_P2:

POINTS_X1_L:    EQU $0c
POINTS_X1_R:    EQU $0f
POINTS_X2_L:    EQU $10
POINTS_X2_R:    EQU $13
POINTS_Y_B:     EQU $14

El significado de estas constantes, en orden de aparición, es:

  • Columna en la que la bola empieza a borrar el marcador del jugador 1 por la izquierda.
  • Columna en la que la bola empieza a borrar el marcador del jugador 1 por la derecha.
  • Columna en la que la bola empieza a borrar el marcador del jugador 2 por la izquierda.
  • Columna en la que la bola empieza a borrar el marcador del jugador 2 por la derecha.
  • Tercio, línea y scanline en la que la bola empieza a borrar el marcador por la parte de abajo.

Una vez que hemos definido estas constantes, vamos a modificar las rutinas PrintPoints y ReprintPoints del archivo Video.asm, empezando por localizar la etiqueta printPoint_print, que vamos a sustituir por PrintPoint.

Dentro de la rutina PrintPoints, hay tres llamadas a printPoint_print, que vamos a sustituir por PrintPoint.

Compilamos, cargamos en el emulador y comprobamos que no hemos roto nada.

El siguiente paso es modificar la rutina ReprintPoints. En realidad, no lo vamos a modificar, la vamos a borrar y a volver a implementar:

ReprintPoints:
ld      hl, (ballPos)
call    GetPtrY	
cp      POINTS_Y_B
ret     nc

Cargamos la posición de la bola en HL, LD HL, (ballPos), obtenemos tercio, línea y scanline de la posición de la bola, CALL GetPtrY, y lo comparamos con la posición donde la bola empieza a borrar el marcador desde abajo, CP POINTS_Y_B. Si no hay acarreo, la bola pasa por debajo del marcador y sale, RET NC.

Si hay acarreo, según la coordenada Y de la bola, esta podría borrar el marcador.

ld      a, l
and     $1f
cp      POINTS_X1_L
ret     c
jr      z, reprintPoint_1_print

Cargamos la línea y columna de la posición de la bola en A, LD A, L, nos quedamos con la columna, AND $1F, y lo comparamos con la coordenada X en la que empieza a borrar el marcador del jugador 1 por la izquierda, CP POINTS_X1_L. Si hay acarreo, la bola pasa por la izquierda del marcador y sale, REC C. Si las dos coordenadas coinciden, la bola va a borrar el marcador del jugador 1, y salta para repintarlo, JR Z, reprintPoint_1_print.

Si no hemos salido, ni saltado, seguimos con las comprobaciones:

cp      POINTS_X2_R
jr      z, reprintPoint_2_print
ret     nc

Comparamos la coordenada X donde está la bola con la coordenada donde se empieza a borrar el marcador del jugador 2 por la derecha, CP POINTS_X2_R. Si son iguales, salta a repintar el marcador del jugador 2, JR Z, reprintPoint_2_print. Si no salta y no hay acarreo, la bola pasa por la derecha y sale, RET NC.

Si no hemos saltado, ni hemos salido, seguimos con las comprobaciones:

reprintPoint_1:
cp      POINTS_X1_R
jr      c, reprintPoint_1_print
jr      nz, reprintPoint_2

Comparamos la coordenada X de la bola con la coordenada donde la bola empieza a borrar el marcador del jugador 1 por la derecha, CP POINTS_X1_R. Si hay acarreo, está borrando el marcador del jugador 1 y salta para repintarlo, JR C, reprintPoint_1_print. Si no son la misma coordenada, pasa por la derecha del marcador del jugador 1 y salta para comprobar si borra el marcador del jugador 2, JR NZ, reprintPoint_2.

Si está borrando el marcador del jugador 1, lo repinta:

reprintPoint_1_print:
ld      a, (p1points)
call    GetPointSprite
push    hl

Cargamos los puntos del jugador 1 en A, LD A, (p1points), obtenemos la dirección del sprite a pintar, CALL GetPointSprite, y preservamos el valor, PUSH HL.

Empezamos pintando el primer dígito, las decenas:

ld      e, (hl)
inc     hl
ld      d, (hl)
ld      hl, POINTS_P1
call    PrintPoint
pop     hl

Cargamos en E la parte baja de la dirección de memoria del sprite del primer dígito, LD E, (HL), apuntamos HL a la parte alta de la dirección, INC HL, cargamos la parte alta de la dirección en D, LD D, (HL), cargamos en HL la dirección de memoria dónde se pinta el marcador del jugador 1, LD HL, POINTS_P1, pintamos el primer dígito, CALL PrintPoint, y recuperamos el valor de HL, POP HL.

Terminamos pintando el segundo dígito:

inc     hl			
inc     hl
ld      e, (hl)
inc     hl
ld      d, (hl)
ld      hl, POINTS_P1
inc     l
jr      PrintPoint

Apuntamos HL a la dirección de memoria del sprite del segundo dígito, INC HL, INC HL, cargamos la parte baja de la dirección en E, LD E, (HL), apuntamos HL a la parte alta de la dirección, INC HL, y la cargamos en D, LD D, (HL), cargamos en HL la dirección de memoria dónde se pinta el marcador del jugador 1, LD HL, POINTS_P1, apuntamos HL a la dirección dónde se pinta el segundo dígito, INC L, y pintamos el dígito y salimos, JR PrintPoint.

Posiblemente os estaréis preguntando, ¿Cómo salimos? ¡Si no hay ningún RET!

Estaréis pensando que en lugar de JR PrintPoint, tendríamos que haber puesto:

call    PrintPoint
ret

Y efectivamente esto funciona, pero no es necesario. Además, de la forma que lo hemos implementado, ahorramos tiempo de proceso y bytes.

La última instrucción de PrintPoint es un RET, y este es el RET que utilizamos para salir, por eso podemos poner JR en lugar de CALL y RET.

Por eso, y porque no tenemos nada que tengamos que recuperar de la pila. Si hubiéramos dejado algo en la pila, los resultados serían impredecibles.

A continuación, podemos ver la diferencia de ciclos de reloj y bytes entre hacerlo de una manera o de otra:

InstrucciónCiclos de relojBytes
CALL PrintProint173
RET101
JR PrintPoint122
Ensamblador para ZX Spectrum, diferencia de ciclos y bytes

Nos hemos ahorrado 15 ciclos de reloj y 2 bytes.

También hemos cambiado la forma de repintar. Antes repintábamos los marcadores haciendo OR con lo que hubiera pintado en esa zona, y ahora directamente pintamos el marcador. El resultado es que al pintar el marcador borramos la bola, lo que puede producir algún parpadeo. Como estos parpadeos también existen en el arcade original, lo dejamos así…o podéis cambiarlo.

Vamos ahora a ver como repintamos el marcador del jugador 2:

reprintPoint_2:
cp      POINTS_X2_L
ret     c

En este punto, solo hay que comprobar que la bola no esté pasando entre los marcadores sin borrarlos. Comparamos con el límite izquierdo del marcador 2, CP POINTS_X2_L, y si hay acarreo sale pues pasa por la izquierda, RET C.

Si no ha salido, hay que repintar el marcador del jugador 2, lo cual es casi idéntico a lo que hacemos con el marcador del jugador 1, por lo que se marcan las diferencias sin entrar en el detalle:

reprintPoint_2_print:       ; # Cambio #
ld      a, (p2points)       ; # Cambio #
call    GetPointSprite
push    hl
; 1er dígito
ld      e, (hl)
inc     hl
ld      d, (hl)
ld      hl, POINTS_P2       ; # Cambio #
call    PrintPoint
pop     hl
; 2º dígito
inc     hl			
inc     hl
ld      e, (hl)
inc     hl
ld      d, (hl)
ld      hl, POINTS_P2       ; # Cambio #
inc     l
jr      PrintPoint

Siendo el aspecto final de la rutina el siguiente:

; – ---------------------------------------------------------------------------
; Repinta el marcador.
; Cada número consta de 1 byte de ancho por 16 de alto.
; Altera el valor de los registros AF, BC, DE y HL.
; – ---------------------------------------------------------------------------
ReprintPoints:
ld      hl, (ballPos)           ; Carga la posición de la bola en HL
call    GetPtrY                 ; Obtiene tercio, línea y scanline de esta posición
cp      POINTS_Y_B              ; Compara con la posición Y donde 
                                ; empieza a borrar el marcador
ret     nc                      ; Si no hay acarreo, paso por debajo y sale
; Si llega aquí la bola podría borrar el marcador, según su posición Y
ld      a, l                    ; Carga línea y columna de la posición 
                                ; de la bola en A
and     $1f                     ; Se queda con la columna
cp      POINTS_X1_L             ; Compara con la posición donde la bola borrar el
                                ; marcador del jugador 1 por la izquierda
ret     c                       ; Si hay acarreo pasa por la izquierda y sale
jr      z, reprintPoint_1_print ; Si coinciden, la bola va a borrar el marcador
                                ; y repinta
; Sigue con las comprobaciones
cp      POINTS_X2_R             ; Compara la coordenada X de la bola con la
                                ; posición donde borra el marcador 2 por la derecha
jr      z, reprintPoint_2_print ; Si son iguales, repinta el marcador
ret     nc                      ; Si no hay acarreo, pasa por la derecha y sale
; Resto de comprobaciones para averiguar si borra el marcador 1
reprintPoint_1:
cp      POINTS_X1_R             ; Compara la coordenada X de la bola con la
                                ; posición donde borra el marcador 1 por la derecha
jr      c, reprintPoint_1_print ; Si hay acarreo, borra el marcador y repinta
jr      nz, reprintPoint_2      ; Si no es 0 para por la derecha del marcador 1
                                ; y salta
; Repinta el marcador del jugador 1
reprintPoint_1_print:
ld      a, (p1points)           ; Carga en A la puntuación del jugador 1
call    GetPointSprite          ; Obtiene la dirección del sprite a pintar
push    hl                      ; Preserva el valor de HL
ld      e, (hl)                 ; Carga en E la parte baja de la dirección 
                                ; del sprite
inc     hl                      ; Apunta HL a la parte alta de la dirección
ld      d, (hl)                 ; La carga en D
ld      hl, POINTS_P1           ; Carga en HL la dirección dónde se pinta el 
                                ; marcador 1
call    PrintPoint              ; Pinta el primer dígito
pop     hl                      ; Recupera el valor de HL
inc     hl			
inc     hl                      ; Apunta HL al sprite del segundo dígito
ld      e, (hl)                 ; Carga la parte baja de la dirección en E
inc     hl                      ; A punta HL a la parte alta de la dirección
ld      d, (hl)                 ; La carga en D
ld      hl, POINTS_P1           ; Carga en HL la dirección dónde se pinta el 
                                ; marcador 1
inc     l                       ; Apunta a la dirección dónde se pinta el segundo 
                                ; dígito
jr      PrintPoint              ; Pinta el dígito y sale
; Resto de comprobaciones para averiguar si borra el marcador 2
reprintPoint_2:
cp      POINTS_X2_L             ; Compara la coordenada X de la bola con la 
                                ; posición donde borra el marcador 2 por la 
                                ; izquierda
ret     c                       ; Si hay acarreo, pasa por la izquierda y sale
; Repinta el marcador del jugador 2
reprintPoint_2_print:
ld      a, (p2points)           ; Carga en A la puntuación del jugador 2
call    GetPointSprite          ; Obtiene la dirección del sprite a pintar
push    hl                      ; Preserva el valor de HL
ld      e, (hl)                 ; Carga en E la parte baja de la dirección del 
                                ; sprite
inc     hl                      ; Apunta HL a la parte alta de la dirección
ld      d, (hl)                 ; La carga en D
ld      hl, POINTS_P2           ; Carga en HL la dirección dónde se pinta el 
                                ; marcador 2
call    PrintPoint              ; Pinta el primer dígito
pop     hl                      ; Recupera el valor de HL
inc     hl			
inc     hl                      ; Apunta HL al sprite del segundo dígito
ld      e, (hl)                 ; Carga la parte baja de la dirección en E
inc     hl                      ; A punta HL a la parte alta de la dirección
ld      d, (hl)                 ; La carga en D
ld      hl, POINTS_P2           ; Carga en HL la dirección dónde se pinta el 
                                ; marcador 2
inc     l                       ; Apunta a la dirección dónde se pinta el segundo 
                                ; dígito
jr      PrintPoint              ; Pinta el dígito y sale

Compilamos, cargamos en el emulador, y vemos el resultado.

Podemos ver que la bola ahora va más rápida, incluso cuándo tiene que ir lento. También, si nos fijamos cuando es el jugador 2 el que marca el tanto y la bola debe salir por la derecha, parte de la misma se ve durante un corto espacio de tiempo en la izquierda.

Si hacemos memoria, cuando marcamos un punto la pelota sale desde el campo del jugador que ha ganado el punto. Eso nos lleva a la conclusión de que el problema está en la rutina SetBallRight, y más concretamente, en la primera línea:

ld      hl, $4d7f

Según esta línea, posicionamos la pelota en el tercio 1, scanline 5, línea 3, columna 31.

Además, dos líneas más abajo, cambiamos la rotación de la bola, poniéndola a -1:

ld      a, $ff
ld      (ballRotation), a

Ahora, si buscamos el sprite correspondiente a esta rotación, vemos que es el siguiente:

db      $00, $78    ; +7/$07    00000000 01111000   -1/$ff

Por lo que la columna 31 la pintamos en blanco, y en la 32 pintamos $78. Pero es que columna 32 no existe: las columnas en total son 32, pero van de la 0 a la 31. Al pintar en la 32, estamos pintando en la columna 0.

Una vez visto esto, la solución es sencilla. Editamos la primera línea de la rutina SetBallRight, para posicionar la bola en la columna 30:

ld      hl, $4d7e

Compilamos, cargamos en el emulador, y vemos que este problema ha quedado resuelto.

Y ahora vamos a cambiar la velocidad de la bola, para que no vaya tan rápida.

La configuración de la bola la tenemos guardada en ballSetting, en el archivo Sprite.asm:

; Velocidad y dirección de la bola.
; bits 0 a 3:   movimientos de la bola para que cambie la posición Y. 
;               Valores f = semiplano, 2 = semi diagonal, 1 = diagonal
; bits 4 y 5:   velocidad de la bola: 1 muy rápido, 2 rápido, 3 lento
; bit 6: dirección X:   0 derecha / 1 izquierda
; bit 7: dirección Y:   0 arriba / 1 abajo
ballSetting:
db      $31     ; 0011 0001

Según vemos en los comentarios, la velocidad de la bola se configura en los bits 4 y 5. Sería tan sencillo como que la velocidad 2 sea muy rápido, la 3 rápido, y la… En 2 bits solo podemos especificar valores del 0 a 3, y el resto de bits los tenemos ocupados.

Vamos a «robar» un bit a la inclinación de la bola. Como resultado, podremos reducir la velocidad de la bola, y como contraprestación, cuando la bola vaya plana, va a ir un poco más inclinada:

; Velocidad y dirección de la bola.
; bits 0 a 2:   Movimientos de la bola para que cambie la posición Y. 
;               Valores 7 = semiplano, 2 = semi diagonal, 1 = diagonal
; bits 3 y 5:   velocidad de la bola: 2 muy rápido, 3 rápido, 4 lento
; bit 6: dirección X:   0 derecha / 1 izquierda
; bit 7: dirección Y:   0 arriba / 1 abajo
ballSetting:
db      $21     ; 0010 0001

Y ahora hay tres rutinas que tenemos que cambiar:

  • CheckCrossY en Game.asm: en esta rutina asignamos inclinación y velocidad de la bola, dependiendo de en qué parte de la pala golpea.
  • MoveBallY en Game.asm: en esta rutina evaluamos si los movimientos acumulados de la bola han alcanzado los necesarios para cambiar la coordenada Y de la misma.
  • SetBallLeft y SetBallRight en Game.asm: en estas rutinas reiniciamos la configuración de la bola.
  • Loop en Main.asm: al inicio de esta rutina, verificamos si se ha llegado al número de iteraciones del bucle, necesarias para mover la bola.

Empezamos por CheckCrossY en Game.asm. Localizamos la etiqueta checkCrossY_1_5, y después la línea OR $31:

or      $31     ; Hacia arriba, velocidad 3 e inclinación diagonal

Según la nueva definición, vamos a poner velocidad 4 e inclinación diagonal:

00 100 001

Los bits 3 a 5 (100) especifican la velocidad, y los bits 0 a 2 (001) la inclinación. La línea OR $31 debe quedar de la siguiente manera:

or      $21

Localizamos la etiqueta checkCrossY_2_5 y ponemos velocidad 3, inclinación semi diagonal:

00 011 010

Modificamos la línea:

or      $22     ; Hacia arriba, velocidad 2 e inclinación semi diagonal

Y la dejamos como:

or      $1a

Localizamos la etiqueta checkCrossY_3_5 y ponemos velocidad 2, inclinación semi plana:

00 010 111

Modificamos la línea:

or      $1f     ; Hacia arriba/abajo, velocidad 1 e inclinación semi plana

Y la dejamos como:

or      $17

Localizamos la etiqueta checkCrossY_4_5 y ponemos velocidad 3, inclinación semi diagonal:

10 011 010

Modificamos la línea:

or      $a2     ; Hacia abajo, velocidad 2 e inclinación semi diagonal

Y la dejamos como:

or      $9a

Localizamos la etiqueta checkCrossY_5_5 y ponemos velocidad 4, inclinación diagonal:

10 100 001

Modificamos la línea:

or      $b1         ; Hacia abajo, velocidad 3 e inclinación diagonal

Y la dejamos como:

or      $a1

Con esto hemos acabado con la parte más laboriosa de la modificación.

Localizamos la etiqueta MoveBallY, y modificamos la segunda línea:

and     $0f

Y la dejamos como:

and     $07

Con $0f ahora cogeríamos la inclinación y el primer bit de la velocidad. Con $07 solo cogemos la inclinación.

Modificamos el reinicio de la configuración de la bola en las rutinas SetBallLeft y SetBallRight.

En SetBallLeft modificamos la línea:

or      $31     ; Pone dirección X a derecha, velocidad 3, 
                ; inclinación diagonal

Y la dejamos como:

or      $21

En SetBallRight modificamos la línea:

or      $71     ; Pone dirección X a izquierda, velocidad 3,
                ; inclinación diagonal

Y la dejamos como:

or      $61

Vamos a terminar modificando el código de la etiqueta Loop de Main.asm.

A partir de la segunda línea, nos encontramos 4 instrucciones RRCA. Quitamos una, para rotar solo 3 veces y dejar en los bits 0, 1 y 2, la velocidad de la bola.

;rrca       ; # Eliminar línea #
rrca
rrca
rrca

Como ahora tenemos 3 bits para la velocidad, en lugar de dos, modificamos la línea siguiente, que es:

and     $03

Y la dejamos como:

and     $07

Compilamos, cargamos en el emulador, y comprobamos que la velocidad de la bola es ahora más llevadera, en detrimento de la inclinación.

Optimizamos ScanKeys

Vamos a optimizar la rutina ScanKeys, tal y como anunciamos en Ensamblador para ZX Spectrum – Pong: $04 Teclas de control.

En la rutina ScanKeys hay cuatro instrucciones BIT, dos BIT $00, A, y otras dos BIT $01, A. Con estas instrucciones BIT comprobamos el estado de un bit en concreto de un registro, sin alterar el valor de dicho registro; cada instrucción BIT ocupa 2 bytes y tarda 8 ciclos de reloj.

Vamos a sustituir las instrucciones BIT por AND, ahorrándonos un ciclo de reloj en cada una. Sustituimos las instrucciones BIT $00, A por AND $01, y las instrucciones BIT $01, A, por AND $02. Con esta modificación vamos a ahorrar 4 ciclos de reloj, aunque vamos a alterar el valor del registro A que, en este caso, no importa.

Optimizamos Cls

Vamos a optimizar la rutina Cls, tal y como anunciamos en Ensamblador para ZX Spectrum – Pong: $05 Palas y línea central.

Vamos a recordar como es la rutina actualmente:

; – ---------------------------------------------------------------------------
; Limpia la pantalla, tinta 7, fondo 0.
; Altera el valor de los registros AF, BC, DE y HL.
; – ---------------------------------------------------------------------------
Cls:
; Limpia los píxeles de la pantalla
ld      hl, $4000   ; Carga en HL el inicio de la VideoRAM
ld      (hl), $00   ; Limpia los píxeles de esa dirección
ld      de, $4001   ; Carga en DE la siguiente posición de la VideoRAM
ld      bc, $17ff   ; 6143 repeticiones
ldir                ; Limpia todos los píxeles de la VideoRAM

; Pone la tinta en blanco y el fondo en negro
ld      hl, $5800   ; Carga en HL el inicio del área de atributos
ld      (hl), $07   ; Lo pone con la tinta en blanco y el fondo en negro
ld      de, $5801   ; Carga en DE la siguiente posición del área de atributos
ld      bc, $2ff    ; 767 repeticiones
ldir                ; Asigna el valor a toda el área de atributos

ret

La primera parte de la rutina limpia los píxeles, y la segunda asigna los colores a la pantalla. Es en esta segunda parte donde vamos a realizar la optimización.

Una vez ejecutado el primer LDIR, HL vale $57FF y DE vale $5800. Cargar un valor de 16 bits en un registro de 16 bits consume 10 ciclos de reloj y 3 bytes, por lo que haciendo LD HL, $5800 y LD DE, $5801, consumimos 20 ciclos de reloj y 6 bytes.

Como podemos ver, HL y DE valen uno menos de lo que necesitamos para asignar los atributos a la pantalla, por lo que lo único que necesitamos es incrementar su valor en uno, y es ahí donde vamos a conseguir la optimización; vamos a sustituir LD HL, $5800 y LD HL, $5801 por INC HL e INC DE. Incrementar un registro de 16 bits consume 6 ciclos de reloj y ocupa un byte, por lo que el coste total será de 12 ciclos de reloj y 2 bytes, frente a los 20 ciclos de reloj y 6 bytes actuales, logrando un ahorro de 8 ciclos de reloj y 4 bytes.

El aspecto final de la rutina es:

; – ---------------------------------------------------------------------------
; Limpia la pantalla, tinta 7, fondo 0.
; Altera el valor de los registros AF, BC, DE y HL.
; – ---------------------------------------------------------------------------
Cls:
; Limpia los píxeles de la pantalla
ld      hl, $4000   ; Carga en HL el inicio de la VideoRAM
ld      (hl), $00   ; Limpia los píxeles de esa dirección
ld      de, $4001   ; Carga en DE la siguiente posición de la VideoRAM
ld      bc, $17ff   ; 6143 repeticiones
ldir                ; Limpia todos los píxeles de la VideoRAM

; Pone la tinta en blanco y el fondo en negro
inc     hl          ; Apunta HL al inicio del área de atributos
ld      (hl), $07   ; Lo pone con la tinta en blanco y el fondo en negro
inc     de          ; Apunta DE a la siguiente posición del área de atributos
ld      bc, $2ff    ; 767 repeticiones
ldir                ; Asigna el valor a toda el área de atributos

ret

Optimizamos MoveBall

Vamos a optimizar la rutina MoveBall, tal y como anunciamos en Ensamblador para ZX Spectrum – Pong: $07 Movemos la bola por la pantalla.

Comentamos que se podían ahorrar 5 bytes y 2 ciclos de reloj, lo cual vamos a conseguir modificando 5 líneas del conjunto de rutinas MoveBall, que se encuentran en el archivo Game.asm. En concreto vamos a sustituir las 5 líneas JR moveBall_end por RET; JR ocupa 2 bytes y tarda 12 ciclos de reloj, mientras que RET ocupa 1 byte y tarda 10 ciclos de reloj.

Como podemos observar, en la etiqueta MoveBall_end solo hay una instrucción, RET, de ahí que podamos sustituir todos los JR moveBall_end por RET.

Hemos dicho que solo ahorramos 2 ciclos de reloj, lo cual es debido a que cada vez que se llama a MoveBall, solo se ejecuta uno de los JR, por eso solo se ahorran 2 ciclos y no 10, aunque sí se ahorran 5 bytes.

Los JR que vamos a sustituir, los encontramos como última línea de las etiquetas:

  • moveBall_right.
  • moveBall_rightLast.
  • moveBall_rightChg.
  • moveBall_left.
  • moveBall_leftLast.

La etiqueta moveBall_end se puede eliminar, pero no el RET que la sigue, aunque la etiqueta en si no ocupa nada.

Optimizamos ReprintLine

Vamos a optimizar la rutina ReprintLine, tal y como anunciamos en Ensamblador para ZX Spectrum – Pong: $08 Campo, palas, bola y temporización.

Comentamos que se podían ahorrar 5 bytes y 22 ciclos de reloj, lo cual vamos a conseguir modificando 8 líneas de la rutina ReprintLine del archivo Video.asm.

Lo primero que vamos a hacer es localizar la etiqueta reprintLine_loopCont y la vamos a mover tres líneas más abajo, justo encima de CALL NextScan.

El siguiente paso es localizar la línea LD C, LINE y borrar las tres líneas siguientes:

jr      ReprintLine_loopCont
ReprintLine_00:
ld      c, ZERO

El siguiente paso es localizar las líneas JR C, reprintLine_00 y JR Z, reprintLine_00 y sustituimos reprintLine_00 por reprintLine_loopCont.

El último paso nos lleva al primero. Localizamos la nueva ubicación de la etiqueta reprintLine_loopCont, y 4 líneas más arriba eliminamos LD C, LINE. Dos líneas más abajo de la línea eliminada, sustituimos OR C por OR LINE.

¿Qué hemos hecho?

El objetivo final de la rutina es repintar la parte de la línea central que se ha borrado, sin borrar la parte de la bola que hay donde se tiene que repintar, para lo cual obtenemos los píxeles que hay en pantalla y los mezclamos con la parte de la línea que hay que repintar, y ahí está la cuestión; si lo que hay que repintar de la línea es la parte que va a ZERO (blanco), no es necesario repintarla.

El aspecto final de la rutina es el siguiente:

; – ---------------------------------------------------------------------------
; Repinta la línea central.
; Altera el valor de los registros AF, B y HL.
; – ---------------------------------------------------------------------------
ReprintLine:
ld      hl, (ballPos)               ; Carga en HL la posición de la bola
ld      a, l                        ; Carga la línea y columna en A
and     $e0                         ; Se queda con la línea
or      $10                         ; Pone la columna a 16 ($10)
ld      l, a                        ; Carga el valor en L. HL = Posición inicial

ld      b, $06                      ; Se repintan 6 scanlines
reprintLine_loop:
ld      a, h                        ; Carga tercio y scanline en A
and     $07                         ; Se queda con el scanline
; Si está en los scanlines 0 o 7 pinta ZERO
; Si está en los scanlines 1, 2, 3, 4, 5 o 6 pinta LINE
cp      $01                         ; Comprueba si está en scanline 1 o superior
jr      c, reprintLine_loopCont     ; Si está por debajo, salta
cp      $07                         ; Comprueba si está en scanline 7
jr      z, reprintLine_loopCont     ; Si es así, salta

ld      a, (hl)                     ; Obtiene los pixeles de la posición actual
or      LINE                        ; Los mezcla con C
ld      (hl), a                     ; Pinta el resultado en la posición actual
reprintLine_loopCont:
call    NextScan                    ; Obtiene el scanline siguiente
djnz    reprintLine_loop            ; Hasta que B = 0

ret

Optimizamos PrintPoints y ReprintPoints

Vamos a optimizar la rutina PrintPoints, tal y como anunciamos en Ensamblador para ZX Spectrum – Pong: $0A Partida a dos jugadores y cambio de velocidad de la bola.

Comentamos que podríamos ahorrar 2 bytes y 12 ciclos de reloj haciendo una pequeña modificación en la rutina PrintPoints. Pues bien, estamos de enhorabuena ya que en realidad nos vamos a ahorrar 4 bytes y 24 ciclos de reloj; lo cambios que vamos a realizar en PrintPoints, los vamos a realizar también en ReprintPoints.

En la tercera línea de PrintPoints encontramos PUSH HL, y esta es la primera línea que vamos a cambiar de lugar, ya que preservamos el valor del registro HL antes de tiempo. Cortamos esta línea y la pegamos tres líneas más abajo, justo antes de cargar la dirección de memoria donde se pintan los puntos del jugador 1 en HL, LD HL, POINTS_P1. El motivo de preservar el valor del registro HL es justamente esta instrucción.

Una vez que llamamos a pintar el punto, recuperamos el valor de HL, POP HL, e incrementamos HL dos veces para apuntarlo a la parte baja de la dirección donde está el segundo dígito. Pues bien, como hemos preservado HL después de posicionarnos en la parte alta de la dirección del primer dígito, ahora vamos a quitar uno de estos dos INC HL: nos acabamos de ahorrar 1 byte y 6 ciclos de reloj.

Esta misma modificación tenemos que hacerla al pintar el marcador del jugador 2 y en la rutina ReprintPoints. En total ahorramos 4 bytes y 24 ciclos de reloj.

El aspecto final de PrintPoints es el siguiente:

; – ---------------------------------------------------------------------------
; Pinta el marcador.
; Cada número consta de 1 byte de ancho por 16 de alto.
; Altera el valor de los registros AF, BC, DE y HL.
; – ---------------------------------------------------------------------------
PrintPoints:
ld      a, (p1points)       ; Carga en A los puntos del jugador 1
call    GetPointSprite      ; Obtiene el sprite a pintar en el marcador
; 1er dígito del jugador 1
ld      e, (hl)             ; Carga en E la parte baja de la dirección 
                            ; donde está el primer dígito
inc     hl                  ; Apunta HL a la parte alta de la dirección 
                            ; donde está el primer dígito
ld      d, (hl)             ; y la carga en D
push    hl                  ; Preserva el valor de HL
ld      hl, POINTS_P1       ; Carga en HL la dirección de memoria donde se pintan
                            ; los puntos del jugador 1
call    PrintPoint          ; Pinta el primer dígito del marcador del jugador 1

pop     hl                  ; Recupera el valor de HL
; 2º dígito del jugador 1
inc     hl                  ; Apunta HL a la parte baja de la dirección 
                            ; donde está el segundo dígito
ld      e, (hl)             ; y la carga en E
inc     hl                  ; Apunta HL a la parte alta de la dirección 
                            ; donde está el segundo dígito
ld      d, (hl)             ; y la carga en D
ld      hl, POINTS_P1       ; Carga en HL la dirección de memoria donde se pintan
                            ; los puntos del jugador 1
inc     l                   ; Apunta HL a la dirección donde se pinta el segundo dígito
call    PrintPoint          ; Pinta el segundo dígito del marcador del jugador 1

ld      a, (p2points)       ; Carga en A los puntos del jugador 2
call    GetPointSprite      ; Obtiene el sprite a pintar en el marcador
; 1er dígito del jugador 2
ld      e, (hl)             ; Carga en E la parte baja de la dirección
                            ; donde está el primer dígito
inc     hl                  ; Apunta HL a la parte alta de la dirección
                            ; donde está el primer dígito
ld      d, (hl)             ; y la carga en D
push    hl                  ; Preserva el valor de HL
ld      hl, POINTS_P2       ; Carga en HL la dirección de memoria donde se pintan
                            ; los puntos del jugador 2
call    PrintPoint          ; Pinta el primer dígito del marcador del jugador 2

pop     hl                  ; Recupera el valor de HL
; 2º dígito	del jugador 2	
inc     hl                  ; Apunta HL a la parte baja de la dirección
                            ; donde está el segundo dígito
ld      e, (hl)             ; y la carga en E
inc     hl                  ; Apunta HL a la parte alta de la dirección
                            ; donde está el segundo dígito
ld      d, (hl)             ; y la carga en D
ld      hl, POINTS_P2       ; Carga en HL la dirección de memoria donde se pintan
                            ; los puntos del jugador 2
inc     l                   ; Apunta HL a la dirección donde se pinta el segundo dígito
; Pinta el segundo dígito del marcador del jugador 2

PrintPoint:
ld      b, $10              ; Cada dígito son 2 bytes por 16 (scanlines)
push    de                  ; Preserva el valor de DE
push    hl                  ; Preserva el valor de HL
printPoint_printLoop:
ld      a, (de)             ; Carga en A el byte a pintar
ld      (hl), a             ; Pinta el byte
inc     de                  ; Apunta DE al siguiente byte
call    NextScan            ; Apunta HL al siguiente scanline
djnz    printPoint_printLoop    ; Hasta que B = 0

pop     hl                  ; Recupera el valor de HL
pop     de                  ; Recupera el valor de DE

ret

Y el de ReprintPoints:

; – ---------------------------------------------------------------------------
; Repinta el marcador.
; Cada número consta de 1 byte de ancho por 16 de alto.
; Altera el valor de los registros AF, BC, DE y HL.
; – ---------------------------------------------------------------------------
ReprintPoints:
ld      hl, (ballPos)               ; Carga la posición de la bola en HL
call    GetPtrY                     ; Obtiene tercio, línea y scanline de esta posición
cp      POINTS_Y_B                  ; Compara con la posición Y donde empieza a borrar el marcador
ret     nc                          ; Si no hay acarreo, paso por debajo y sale
; Si llega aquí la bola podría borrar el marcador, según su posición Y
ld      a, l                        ; Carga línea y columna de la posición de la bola en A
and     $1f                         ; Se queda con la columna
cp      POINTS_X1_L                 ; Compara con la posición donde la bola borrar el marcador
                                    ; del jugador 1 por la izquierda
ret     c                           ; Si hay acarreo pasa por la izquierda y sale
jr      z, reprintPoint_1_print     ; Si coinciden, la bola va a borrar el marcador y repinta
; Sigue con las comprobaciones
cp      POINTS_X2_R                 ; Compara la coordenada X de la bola con la posición donde borra el 
                                    ; marcador 2 por la derecha
jr      z, reprintPoint_2_print     ; Si son iguales, repinta el marcador
ret     nc                          ; Si no hay acarreo, pasa por la derecha y sale
; Resto de comprobaciones para averiguar si borra el marcador 1
reprintPoint_1:
cp      POINTS_X1_R                 ; Compara la coordenada X de la bola con la posición donde borra el
                                    ; marcador 1 por la derecha
jr      c, reprintPoint_1_print     ; Si hay acarreo, borra el marcador y repinta
jr      nz, reprintPoint_2          ; Si no es 0 para por la derecha del marcador 1 y salta
; Repinta el marcador del jugador 1
reprintPoint_1_print:
ld      a, (p1points)               ; Carga en A la puntuación del jugador 1
call    GetPointSprite              ; Obtiene la dirección del sprite a pintar
ld      e, (hl)                     ; Carga en E la parte baja de la dirección del sprite
inc     hl                          ; Apunta HL a la parte alta de la dirección
ld      d, (hl)                     ; La carga en D
push    hl                          ; Preserva el valor de HL
ld      hl, POINTS_P1               ; Carga en HL la dirección dónde se pinta el marcador 1
call    PrintPoint                  ; Pinta el primer dígito
pop     hl                          ; Recupera el valor de HL		
inc     hl                          ; Apunta HL al sprite del segundo dígito
ld      e, (hl)                     ; Carga la parte baja de la dirección en E
inc     hl                          ; A punta HL a la parte alta de la dirección
ld      d, (hl)                     ; La carga en D
ld      hl, POINTS_P1               ; Carga en HL la dirección dónde se pinta el marcador 1
inc     l                           ; Apunta a la dirección dónde se pinta el segundo dígito
jr      PrintPoint                  ; Pinta el dígito y sale
; Resto de comprobaciones para averiguar si borra el marcador 2
reprintPoint_2:
cp      POINTS_X2_L                 ; Compara la coordenada X de la bola con la posición donde borra el
                                    ; marcador 2 por la izquierda
ret     c                           ; Si hay acarreo, pasa por la izquierda y sale
; Repinta el marcador del jugador 2
reprintPoint_2_print:
ld      a, (p2points)               ; Carga en A la puntuación del jugador 2
call    GetPointSprite              ; Obtiene la dirección del sprite a pintar
ld      e, (hl)                     ; Carga en E la parte baja de la dirección del sprite
inc     hl                          ; Apunta HL a la parte alta de la dirección
ld      d, (hl)                     ; La carga en D
push    hl                          ; Preserva el valor de HL
ld      hl, POINTS_P2               ; Carga en HL la dirección dónde se pinta el marcador 2
call    PrintPoint                  ; Pinta el primer dígito
pop     hl                          ; Recupera el valor de HL
inc     hl                          ; Apunta HL al sprite del segundo dígito
ld      e, (hl)                     ; Carga la parte baja de la dirección en E
inc     hl                          ; A punta HL a la parte alta de la dirección
ld      d, (hl)                     ; La carga en D
ld      hl, POINTS_P2               ; Carga en HL la dirección dónde se pinta el marcador 2
inc     l                           ; Apunta a la dirección dónde se pinta el segundo dígito
jr      PrintPoint                  ; Pinta el dígito y sale

Sonido

Y abordamos el penúltimo paso: vamos a implementar efectos de sonido cuando la bola golpea con los laterales, las palas, o cuando se marque algún punto.

Añadimos el archivo Sound.asm, y añadimos las constantes y rutinas necesarias para nuestros efectos de sonido, que van a ser los sonidos a reproducir cuando la bola rebota contra los distintos elementos.

Vamos a definir 3 sonidos distintos:

  • Cuando se marca un punto.
  • Cuando la bola choca con una pala.
  • Cuando la bola choca con el borde.

Para cada sonido tenemos que definir la nota y la frecuencia. La frecuencia es el tiempo que va a durar la nota, y la vamos a identificar con el sufijo FQ.

; Punto
C_3:        EQU $0D07
C_3_FQ:     EQU $0082 / $10

; Pala
C_4:        EQU $066E
C_4_FQ:     EQU $0105 / $10

; Rebote
C_5:        EQU $0326
C_5_FQ:     EQU $020B / $10

Todos los sonidos que vamos a usar son DO, aunque en distintas escalas. A mayor escala, el sonido es más agudo.

Las frecuencias especificadas son las que hacen que la nota dure un segundo, es por eso que las dividimos por 16. Si las multiplicáramos por 2, la nota duraría 2 segundos.

A cada nota, en cada escala, le corresponde una frecuencia propia. En la sección de ficheros os podéis descargar sendas tablas con frecuencias y notas, en decimal, hexadecimal, y código ensamblador.

La siguiente constante que vamos a ver, es la dirección de memoria donde está alojada la rutina BEEPER de la ROM:

BEEPER:     EQU $03B5

Esta rutina recibe en HL la nota y en DE la duración, y altera el valor de los registros AF, BC, DE, HL e IX, además de otro aspecto que veremos más adelante.

Debido a que la rutina BEEPER de la ROM altera tantos registros, es recomendable no llamarla directamente; vamos a implementar una rutina que lo haga.

La rutina que vamos a implementar, recibe en A el tipo de sonido a emitir, 1 = punto, 2 = pala, 3 = borde, y no altera el valor de ningún registro:

PlaySound:
push    de
push    hl

Preservamos el valor de los registros DE, PUSH DE, y HL, PUSH HL.

cp      $01
jr      z, playSound_point

Comprobamos si el sonido a reproducir es de tipo 1 (punto), CP $01, y de ser así saltamos, JR Z, playSound_point.

cp      $02
jr      z, playSound_paddle

Si el sonido no es de tipo 1, comprobamos si es de tipo 2 (pala), CP $02, y de ser así saltamos, JR Z, playSound_paddle.

Si el sonido no es de tipo 1, ni de tipo 2, es de tipo 3 (borde):

ld      hl, C_5
ld      de, C_5_FQ
jr      beep

Cargamos en HL la nota, LD HL, C_5, cargamos en DE la frecuencia (duración), LD DE, C_5_FQ, y saltamos a reproducir el sonido, JR beep.

Si el sonido es de tipo 1 o 2, hacemos los mismo, pero con los valores de cada sonido:

playSound_point:
ld      hl, C_3
ld      de, C_3_FQ
jr      beep

playSound_paddle:
ld      hl, C_4
ld      de, C_4_FQ

Nos ahorramos el último JR, ya que justo después viene la rutina que reproduce el sonido:

beep:
push    af
push    bc
push    ix
call    BEEPER
pop     ix
pop     bc
pop     af

pop     hl
pop     de

ret

Preservamos los valores de AF, PUSH AF, de BC, PUSH BC, y de IX, PUSH IX. Llamamos a la rutina de la ROM, CALL BEEPER, y recuperamos los valores de IX, POP IX, de BC, POP BC, de AF, POP AF, de HL, POP HL, y de DE, POP DE. Los valores de HL y DE los preservamos al principio de la rutina PlaySound. Por último, salimos, RET.

El aspecto final del archivo Sound.asm, es el siguiente:

; – ---------------------------------------------------------------------------
; Sound
; Fichero con los sonidos
; – ---------------------------------------------------------------------------
; Punto
C_3:    EQU $0D07
C_3_FQ: EQU $0082 / $10

; Pala
C_4:    EQU $066E
C_4_FQ: EQU $0105 / $10

; Rebote
C_5:    EQU $0326
C_5_FQ: EQU $020B / $10

; – ---------------------------------------------------------------------------
; Rutina beeper de la ROM.
;
; Entrada:  HL -> Nota.
;           DE -> Duración.
;
; Altera el valor de los registros AF, BC, DE, HL e IX.
; – ---------------------------------------------------------------------------
BEEPER: EQU $03B5

; – ---------------------------------------------------------------------------
; Reproduce el sonido de los rebotes.
; Entrada:  A -> Tipo de rebote.    1. Punto
;                                   2. Pala
;                                   3. Borde
; – ---------------------------------------------------------------------------
PlaySound:
; Preserva el valor de los registros
push    de
push    hl

cp      $01                 ; Evalúa si se emite el sonido de Punto
jr      z, playSound_point  ; Si es así salta

cp      $02                 ; Evalúa si se emite el sonido de Pala
jr      z, playSound_paddle ; Si es así salta

; Se emite el sonido de Borde
ld      hl, C_5             ; Carga en HL la nota
ld      de, C_5_FQ          ; Carga en DE la duración (frecuencia)
jr      beep                ; Salta a emitir el sonido

; Se emite el sonido de Punto
playSound_point:
ld      hl, C_3             ; Carga en HL la nota
ld      de, C_3_FQ          ; Carga en DE la duración (frecuencia)
jr      beep                ; Salta a emitir el sonido

; Se emite el sonido de Pala
playSound_paddle:
ld      hl, C_4             ; Carga en HL la nota
ld      de, C_4_FQ          ; Carga en DE la duración (frecuencia)

; Hace sonar la nota
beep:
; Preserva el valor de los registros ya que la rutina BEEPER de la ROM los altera
push    af
push    bc
push    ix

call    BEEPER              ; Llama a la rutina BEEPER de la ROM

; Recupera el valor de los registros
pop     ix
pop     bc
pop     af

pop     hl
pop     de

ret

Para acabar, tenemos que llamar a nuestra nueva rutina para emitir los sonidos de los rebotes de la bola.

Abrimos el archivo Game.asm y localizamos la etiqueta checkBallCross_right. Vamos a añadir dos líneas entre la línea RET NZ, y la línea LD A, (ballSetting):

ld      a, $02
call    PlaySound

Carga el tipo de sonido (pala) en A, LD A, $02, y emite el sonido, CALL PlaySound.

Localizamos la etiqueta checkBallCross_left. Vamos añadir las mismas dos líneas de antes entre la línea RET NZ, y la línea LD A, (ballSetting):

ld      a, $02
call    PlaySound

Localizamos la etiqueta moveBall_upChg. Justo debajo de la misma, añadimos dos líneas casi iguales a las anteriores:

ld      a, $03
call    PlaySound

Localizamos la etiqueta moveBall_downChg. Justo debajo de la misma, añadimos las dos líneas anteriores:

ld      a, $03
call    PlaySound

Localizamos la etiqueta moveBall_rightChg, y justo debajo añadimos:

ld      a, $01
call    PlaySound

Cinco líneas más abajo localizamos CALL SetBallLeft. Debajo añadimos:

ld      a, $03
call    PlaySound

Localizamos la etiqueta moveBall_leftChg, y justo debajo añadimos:

ld      a, $01
call    PlaySound

Cinco líneas más abajo localizamos CALL SetBallRight. Debajo añadimos:

ld      a, $03
call    PlaySound

Por último, abrimos el archivo Main.asm, localizamos la rutina Loop y justo encima añadimos las siguientes líneas:

ld      a, $03
call    PlaySound

Nos vamos al final del fichero, y en la parte de los «includes», incluimos el archivo Sound.asm:

include "Sound.asm"

Si todo ha ido bien, hemos llegado al final. Compilamos, cargamos en el emulador y…

Ensamblador para ZX Spectrum, "Y el borde se pone blanco"
Ensamblador para ZX Spectrum, «Y el borde se pone blanco»

¿Qué le pasa al borde? ¿Por qué es blanco? Bueno, ya advertimos que la rutina BEEPER de la ROM altera muchas cosas, y una de ellas es el color del borde, aunque tiene fácil solución.

Por suerte, tenemos una variable de sistema dónde podemos guardar el color del borde. En esta variable se guardan también los atributos de la pantalla inferior. El fondo de dicha pantalla es el color del borde.

Abrimos el archivo Video.asm y al inicio del mismo declaramos una constante con la dirección de memoria de dicha variable del sistema:

BORDCR: EQU $5c48

Localizamos las rutina Cls, y antes de la línea LD HL, $5800, añadimos:

ld      a, $07      ; Fondo negro, tinta blanca

Modificamos la línea LD (HL), $07 dejándola así:

ld      (hl), a

Por último, antes de RET, añadimos:

ld      (BORDCR), a

Compilamos, cargamos en el emulador, y ahora sí. ¿Hemos terminado nuestro PorompomPong?

Ensamblador para ZX Spectrum, ¿Hemos terminado?
Ensamblador para ZX Spectrum. ¿Hemos terminado?

Compatibilidad con 16K

Todavía nos falta una última cosa por hacer. ¿Es compatible nuestro programa con el modelo de 16K? Pues todavía no, pero como no trabajamos con interrupciones, es muy sencillo hacer que sea compatible.

Vamos a abrir el archivo Main.asm, vamos a localizar las directivas ORG y END, y vamos a sustituir $8000 por $5dad en el caso de ORG. En el caso de END, vamos a sustituir $8000 por Main, que es la etiqueta de entrada al programa.

Si ahora compilamos y cargamos en el emulador con el modelo de 16K, nuestro programa es compatible.

Si nos fijamos bien, podemos observar que se ha perdido algo de velocidad. Esta perdida es debida a que los segundos 16KB del ZX Spectrum, que es donde ahora cargamos el programa, es lo que se llama memoria contenida, y está compartida con la ULA. Cuando la ULA trabaja, todo se para.

Vamos a volver a cambiar la velocidad a la que va la bola.

Abrimos el archivo Sprite.asm, localizamos la etiqueta ballSetting, comentamos la línea OR $21 y escribimos justo debajo:

; or    $21
or      $19

Ahora la bola se inicia a velocidad 3, que va a ser la más lenta.

Abrimos el archivo Game.asm, localizamos la etiqueta SetBallLeft, comentamos la línea 7 y escribimos justo debajo:

; or    $21
or      $19

Ahora, cuando reiniciamos la bola para que salga por la izquierda de la pantalla, se inicia a velocidad 3.

Localizamos la etiqueta SetBallRight, comentamos la línea 7, y escribimos justo debajo:

; db    $61
db      $59

Ahora, cuando reiniciamos la bola para que salga por la derecha de la pantalla, se inicia a velocidad 3.

Localizamos la etiqueta checkCrossY_1_5, comentamos la línea 7, y escribimos justo debajo:

; or    $21
or      $19

Ahora la velocidad de la bola es 3 en lugar de 4.

Localizamos la etiqueta checkCrossY_2_5, comentamos la línea 7, y escribimos justo debajo:

; or    $1a
or      $12

Ahora la velocidad de la bola es 2 en lugar de 3.

Localizamos la etiqueta checkCrossY_3_5, comentamos la línea 7, y escribimos justo debajo:

; or    $17
or      $0f

Ahora la velocidad de la bola es 1 en lugar de 2.

Localizamos la etiqueta checkCrossY_4_5, comentamos la línea 7, y escribimos justo debajo:

; or    $9a
or      $92

Ahora la velocidad de la bola es 2 en lugar de 3.

Localizamos la etiqueta checkCrossY_5_5, comentamos la línea 3, y escribimos justo debajo:

; or    $a1
or      $99

Ahora la velocidad de la bola es 3 en lugar de 4.

Compilamos, probamos en el emulador y… ¡Hemos terminado!

Frecuencias y notas

En el fichero «Frecuencias y Notas», se muestran los valores para las frecuencias y las notas de 8 escalas distintas. Cuanto menor es la escala, más grave, y cuanto mayor, más aguda.

Los valores de las frecuencias se han obtenido de Dilwyn Jones Sinclair QL Pages.

Estas frecuencias indican la duración de la nota, que en estos casos son de un segundo.

Para calcular cada nota, la fórmula es la siguiente:

(437500/frecuencia)-30125

Esta fórmula ha sido tomada del libro Programación Avanzada del ZX Spectrum de Steve Kramer. Podéis encontrar dicha fórmula en la página 18.

Enlaces de interés

Ficheros

¿De cuánta utilidad te ha parecido este contenido?

¡Haz clic en una estrella para puntuar!

Mostrar más

Deja una respuesta

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

Publicaciones relacionadas

Botón volver arriba