;; -------------------------------------------------------------------
;;                        ZX81 MUSIC INTERPRETER
;;                         KELLY ABRANTES MURTA
;;                              APRIL 2005
;;                         kmurta@yahoo.com.br
;;                           http://zx-81.tk
;; -------------------------------------------------------------------
;;
;; This program implement in the ZX81 a macro musical language
;; similar to the "PLAY" command in MSX computers. See the zx81mi.txt 
;; file for instructions of how to use it.
;;
;;                  "This is a FREEWARE program"
;;            "Fell free to do with it what you want"
;;
;;
;; Compile with "tasm -80 -b zx81mi.asm zx81mi.p"
;;

#define db .byte      ; TASM cross-assembler definitions
#define dw .word
#define ds .block
#define org .org
#define end .end

        org     $4009

;= System variables ============================================

        db      0               ;VERSN
        dw      0               ;E_PPC
        dw      dfile           ;D_FILE
        dw      dfile+1         ;DF_CC
        dw      var             ;VARS
        dw      0               ;DEST
        dw      var+1           ;E_LINE
        dw      last-1          ;CH_ADD
        dw      0               ;X_PTR
        dw      last            ;STKBOT
        dw      last            ;STKEND
        db      0               ;BERG
        dw      membot          ;MEM
        db      0               ;not used
        db      2               ;DF_SZ
        dw      1               ;S_TOP
        db      $FF,$FF,$FF     ;LAST_K
        db      55              ;MARGIN
        dw      dfile           ;NXTLIN
        dw      0               ;OLDPPC
        db      0               ;FLAGX
        dw      0               ;STRLEN
        dw      $0C8D           ;T_ADDR
        dw      0               ;SEED
        dw      $FFFF           ;FRAMES
        db      0,0             ;COORDS
        db      $BC             ;PR_CC
        db      33,24           ;S_POSN
        db      01000000B       ;CDFLAG
        ds      33              ;Print buffer
membot: ds      30              ;Calculatorīs memory area
        ds      2               ;not used

;= First BASIC line, asm code ==================================

line0:  db      0,0
        dw      dfile-$-2
        db      $ea             ; REM

        db      $bf,$bd,$a4,$9d,$80,$b2,$ba,$b8
        db      $ae,$a8,$80,$ae,$b3,$b9,$aa,$b7
        db      $b5,$b7,$aa,$b9,$aa,$b7,$00,$00
        db      $00,$00,$00,$00,$00,$00,$00,$00
        db      $90,$a8,$91,$9e,$9c,$9c,$a1,$80
        db      $b0,$aa,$b1,$b1,$be,$80,$a6,$9b
        db      $80,$b2,$ba,$b7,$b9,$a6,$76,$76

;; -------------------------------------------------------------
;;  Play a music mainloop
;; -------------------------------------------------------------
;; Play a music stored in RAM and ended with a '$ff' byte
;; The routine gets two arguments (in HL and DE) for produce a
;; sound: the tone period and the note duration, both measured
;; in T states. For pauses, the tone period is zero.
;; A direct call to the routine is possible if the music is 
;; already decoded in memory.
;;
 
play:   call    $0f23           ; set FAST mode
        ld      bc,$0000        ; start address of music

again:  ld      a,(bc)
        cp      $ff             ; check for end of music
        ret     z
        ld      l,a             ; get note(tone period) in HL
        srl     l               ; 
        inc     bc
        ld      a,(bc)
        ld      h,a
        inc     bc
        ld      a,(bc)
        ld      e,a             ; get duration(#cycles) in DE
        inc     bc
        ld      a,(bc)
        ld      d,a
        inc     bc              ; increment and save the music pointer
        push    bc
        ld      a,h             ; if note is zero, then it is a pause
        or      l
        jr      z,pause
 
 
;; -------------------------------------------------------------
;;  Routine to control sound
;;  (Based on BEEPER routine of ZX Spectrum)
;; -------------------------------------------------------------
;
; Outputs a sound of given duration and frequency
; to the EAR OUT and TV OUT.
;   Enter with: DE = #cycles
;               HL = tone period
;
; The tone period is measured in T states:
;
; duration of low = 68 + 2048*H + 8*L
; duration of hi  = 68 + 2048*H + 8*L
; Tp = Tone period = 136 + 4096*H + 16*L
;                  = 136 + 16*HL
;
; As an example, to output five seconds of middle C (261.624 Hz):
;   (a) Tone period = 1/261.624 = 3.822ms
;   (b) Tone period in T-States = 3.822ms*fCPU = 12422
;         where fCPU = clock frequency of the CPU = 3.25MHz
;   (c) Find H and L for desired tone period:
;         HL = (Tp - 136) / 16 = (12422 - 136) / 16 = 768 = $0300
;   (d) Tone duration in cycles = 5s/3.822ms = 1308 cycles
;         DE = 1308 = $051C
;
; The resulting waveform has a duty ratio of exactly 50%.
;

ncycle: out     ($ff),a         ;(11)
        ld      b,h             ;(4)
        ld      c,l             ;(4)
        inc     b               ;(4)
        inc     c               ;(4)
lowp:   dec     c               ;(4)
        jr      nz,lowp         ;(12/7)
        ld      c,$7e           ;(7)
        dec     b               ;(4)
        jp      nz,lowp         ;(10)
        dec     de              ;(6)
        jp      np              ;(10)
np:     nop                     ;(4)

        in      a,($fe)         ;(11)
        ld      b,h             ;(4)
        ld      c,l             ;(4)
        inc     b               ;(4)
        inc     c               ;(4)
hip:    dec     c               ;(4)
        jr      nz,hip          ;(12/7)
        ld      c,$7e           ;(7)
        dec     b               ;(4)
        jp      nz,hip          ;(10)
        ld      a,d             ;(4)
        or      e               ;(4)
        jr      nz,ncycle       ;(12/7)


;; -------------------------------------------------------------
;;  Check BREAK key 
;; -------------------------------------------------------------
;

tst_brk:
        pop     bc              ; restore music pointer
        call    $0f46           ; check BREAK key
        jr      c,again
        rst     08H
        db      $0c             ; Error Report: BREAK - CONT repeats


;; -------------------------------------------------------------
;;  PAUSE routine
;; -------------------------------------------------------------
;; Implement a pause of t seconds
;;
;; Tp = 3356*DE-3338
;;
;; DE = (t*3.25MHz+3338)/3356 = 968.4*t + 0.995
;;
;; t  = 240/(T*P)
;;
;; DE = 232416/(T*P)
;;

pause:  ld      b,$00           ;(7)
dly:    djnz    dly             ;(13/8)
        dec     de              ;(6)
        ld      a,d             ;(4)
        or      e               ;(4)
        jr      nz,pause        ;(12/7)
        jr      tst_brk


;; -------------------------------------------------------------
;;  The music macro language handler
;; -------------------------------------------------------------
;; Interpret a musical string and convert it into a chain of
;; tones and durations stored in RAM, 256 bytes above the STKEND.
;; 

start:  call    $0f23           ; set FAST mode
        ld      ix,($401c)      ; the music data is stored 256 bytes above the STKEND
        ld      de,$0100
        add     ix,de
        ld      (play+4),ix     ; store the start address of music
        ld      a,$78           ; set default parameters: T=120, O=4, L=4
        ld      (tempo),a
        ld      a,$04
        ld      (octave),a
        ld      (lenght),a
        rst     20H             ; advance through ','

nxt_stt:
        ld      bc,$0004        ; check for free space in the RAM for four bytes
        push    ix
        pop     hl
        call    $0ec8           ; TEST-ROOM routine. If no free space, a Report 4 error is generated
        rst     20H             ; NXT-CHAR
        cp      $0b             ; cote? end of command
        jr      nz,cnl
        ld      (ix+$00),$ff    ; mark the end of music
        call    play            ; play the music
        ld      sp,($4002)      ; set the stack pointer from ERR_SP
        call    $0f2b           ; set SLOW mode and return to BASIC
        ret

cnl:    cp      $76             ; check for NEWLINE that mark the end of a 'REM' line
        jr      nz,cstt
        pop     hl              ; restore CH-ADD e go to check for more statements
        ld      ($4016),hl
        jr      nxt_stt
 
cstt:   cp      $39             ; check for statement 'T'
        jr      z,stt_t
        cp      $34             ; check for statement 'O'
        jr      z,stt_o
        cp      $31             ; check for statement 'L'
        jr      z,stt_l
        cp      $35            ; check for statement 'P'
        jr      z,stt_p
        cp      $3d             ; check for statement 'X'
        jr      z,stt_x
        cp      $25             ; check for statements A to G
        jr      nc,stt_g
 
sintax_err:
        rst     08H
        db      $0b             ; Error Report: Nonsense in BASIC
 
;; 'T' statement handler
stt_t:  call    get_n_par
        ld      a,e
        and     a
        jr      z,sintax_err
        ld      (tempo),a
        jr      nxt_stt

;; 'O' statement handler
stt_o:  call    get_n_par
        ld      a,e
        and     a
        jr      z,sintax_err
        ld      (octave),a
        jr      nxt_stt

;; 'L' statement handler
stt_l:  call    get_n_par
        ld      a,e
        and     a
        jr      z,sintax_err
        ld      (lenght),a
        jr      nxt_stt

;; 'X' statement handler
stt_x:  call    get_n_par
        push    hl              ; save CH-ADD for next character after statement 'Xn'
        ex      de,hl           ; number line in HL
        call    $09d8           ; LINE-ADDR; return line address in HL
        ld      de,$0004
        add     hl,de
        ld      ($4016),hl      ; now, the CH-ADD point to start-1 of the musical data in a REM line
        jr      nxt_stt         ; go to interpret the string in a 'REM' line

;; 'P' statement handler
stt_p:  call    get_n_par
        ld      a,e
        and     a
        jr      z,sintax_err
                                ;                               calc stack
        call    $151d           ; STACK-A                       P
        ld      a,(tempo)
        call    $151d           ; STACK-A                       P, T
        rst     28H             ; FP-CALC
        db      $04             ;; multiply                     P*T
        db      $30             ;; STK-DATA
        db      $40             ;; Bytes: 2
        db      $42             ;; Exponent: $42
        db      $35,$9c         ;; (+00, +00)                   P*T, 232416
        db      $01             ;; exchange                     232416, P*T
        db      $05             ;; division                     232416/P*T
        db      $34             ;; END-CALC
        call    $158A           ; FP-TO-BC
        ld      d,b
        ld      e,c
        ld      hl,$0000        ; a note period of zero indicate a pause
        push    hl
        jp      dot


;; -------------------------------------------------------------      
;;  Statements A-G handler
;; -------------------------------------------------------------
;; The note letter is converted into a number from zero to 
;; fourteen and the table 'notes' is used to obtain the offset
;; into the tones table. The tone is loaded in 'HL', octave is
;; checked and the tone constant halved until the correct octave
;; is reached. The note duration is obtained from Note_Period,
;; 'T' and 'L'. If a '.' is trailing the note or the pause
;; command, then note duration is multiplied by one and a half.
;; Finally, the note period and note duration are stored in
;; memory, returning to mainloop for check next statemant.
;; 
;; The note duration is obtained from:
;;
;;   Note_Duration = Freq*240/(T*L)
;;
;;   Note_Period = ((3250000/Freq)-136)/Freq = 203125/Freq - 8.5
;;
;;   Obs.: Note_Duration and Note_Period are boths measureds in T states
;;

stt_g:  cp      $2d
        jr      nc,sintax_err
        ld      d,a             ; save note
        sub     $25
        add     a,a
        ld      e,a
        rst     20H             ; NEXT-CHAR
        cp      $15             ; '+' (check for sustain signal)
        jr      z,sust
        cp      $16             ; '-' (check for bemol signal)
        jr      z,bemol
        call prv_char
        jr tom
bemol:  dec e
        ld      a,d
        cp      $28             ; check if it is note C
        jr      z,tom-1
        cp      $2b             ; check if it is note F
        jr      nz,tom 
        dec     e
tom:    dec     e
sust:   ld      d,$00 
        ld      hl,notes 
        add     hl,de 
        ld      a,(hl) 
        ld      e,a 
        ld      hl,tones 
        add     hl,de 
        ld      de,($401c)       ; load destination from STKEND system variable.
        push    de
        call    $19f6            ; routine MOVE-FP: read FP value (freq) from semitone table (HL) and push onto calc stack
        pop     hl               ; HL point to first byte(exponent) of number
        ld      ($401c),de       ; update the STKEND variable
        ld      a,(octave)       ; adjust octave 
        dec     a 
        add     a,(hl)           ; increase exponent by 'A' (equivalent to multiplying by 2^A) 
        ld      (hl),a
                                ;                               calc stack 
        rst     28H             ; FP-CALC                       freq 
        db      $2d             ; duplicate                     freq, freq 
        db      $30             ;; stk-data                     freq, freq, 203125 
        db      $80             ;; Bytes: 3 
        db      $42             ;; Exponent: $92 
        db      $46, $5d, $40   ;; (+00) 
        db      $01             ;; exchange                     freq, 203125, freq 
        db      $05             ;; division                     freq, 203125/freq 
        db      $30             ;; stk-data                     freq, 203125/freq, 8.5 
        db      $34             ;; Exponent: $88, Bytes: 1 
        db      $08             ;; (+00, +00, +00) 
        db      $03             ;; subtract                     freq, 203125/freq - 8.5 = Tp
        db      $34             ;; end-calc                     freq, Tp 
        call    $158A           ; FP-TO-BC                      freq 
        push    bc              ; save the tone period

;; Calculate the note duration:
        ld      a,$f0
                                ;                               calc stack
        call    $151d           ;; STACK-A                      freq, 240
        ld      a,(tempo)       ;; T
        call    $151d           ;; STACK-A                      freq, 240, T
        ld      a,(lenght)      ;; L
        call    $151d           ;; STACK-A                      freq, 240, T, L
        rst     28H             ;; FP-CALC
        db      $04             ;; multiply                     freq, 240, T*L
        db      $05             ;; division                     freq, 240/(T*L)
        db      $04             ;; multiply                     freq*240/(T*L)
        db      $34             ;; end-calc                     freq*240/(T*L)

        call    $158A           ; FP-TO-BC
        ld      d,b             ; save note duration in 'DE'
        ld      e,c
dot:    rst     20H             ; NXT-CHAR
        cp      $1b             ; if a '.' , the duration is multiplied by one and a half
        jr      z,one_half
        call    prv_char
        jr      str_nt
 
one_half:
        ld      h,d             ; 'multiply' the note duration by one and a half
        ld      l,e
        srl     d
        rr      e
        add     hl,de
        ex      de,hl
        jr      dot             ; look for more dots

str_nt: pop     hl              ; restore the tone period
        ld      (ix+$00),l      ; save note period
        ld      (ix+$01),h
        ld      (ix+$02),e      ; save note/pause duration
        ld      (ix+$03),d
        ld      de,$0004
        add     ix,de
        jp      nxt_stt
 
;; -------------------------------------------------------------
;;  Get a numeric parameter routine
;; -------------------------------------------------------------
;; A numeric parameter is evaluated e returned in 'DE'
;;
 
get_n_par:
        ld      de,$0000
nxt_d:  rst     20H             ; NEXT-CHAR

        cp      $1c             ; check if it is a numeric character
        jr      c,prv_char      ; adjust CH-ADD and return
        cp      $26
        jr      nc,prv_char     ; adjust CH-ADD and return
 
        ld      hl,$0000
        ld      b,$0a
x_10:   add     hl,de
        jr      c,ior_err
        djnz    x_10
        sub     $1c
        ld      e,a
        ld      d,$00
        add     hl,de
        jr      c,ior_err
        ex      de,hl
        jr      nxt_d
 
ior_err:
        rst     08H
        db      $0a             ; Error Report: integer out of range
 

;; Adjust the CH-ADD to pointer the previous character 
prv_char:
        dec     hl
        ld      ($4016),hl
        ret

;; ------------------------------------------------------------- 
;;  Note offset table 
;; ------------------------------------------------------------- 
;; This table is used to translate a note number from zero to 
;; fourteen to an offset into the semi-tone table at 'tones'. 
;; The note itself, rather than the note number, is shown below with 
;; each offset value: 
;; 

notes:  db      40      ; A- 
        db      45      ; A 
        db      50      ; A+ or B- 
        db      55      ; B or C- 
        db      00      ; B+ 
        db      00      ; C 
        db      05      ; C+ or D- 
        db      10      ; D 
        db      15      ; D+ or E- 
        db      20      ; E or F- 
        db      25      ; E+ 
        db      25      ; F 
        db      30      ; F+ or G- 
        db      35      ; G 
        db      40      ; G+


;; ------------------------------------------------------------- 
;;  The tone period table 
;; ------------------------------------------------------------- 
;; This table contains the twelve constants required to produce 
;; the tones of octave 1. To move n octaves higher, frequencies 
;; are multiplied by 2^n. For each constant the corresponding 
;; note and frequency are shown: 
;;
;; semi-tone         five byte fp         decimal freq     note 
tones:  db      $86, $02, $D0, $12, $86;  32.7031956613     C 
        db      $86, $0A, $97, $60, $75;  34.6478288919     C# 
        db      $86, $12, $D5, $17, $1F;  36.7080960125     D 
        db      $86, $1B, $90, $41, $02;  38.8908729851     D# 
        db      $86, $24, $D0, $53, $CA;  41.2034446299     E 
        db      $86, $2E, $9D, $36, $B1;  43.6535289436     F 
        db      $86, $38, $FF, $49, $3E;  46.2493028343     F# 
        db      $86, $43, $FF, $6A, $73;  48.9994295090     G 
        db      $86, $4F, $A7, $00, $54;  51.9130871891     G# 
        db      $86, $5C, $00, $00, $00;  55.0000000000     A 
        db      $86, $69, $14, $F6, $24;  58.2704702020     A# 
        db      $86, $76, $F1, $10, $05;  61.7354126723     B


;; -------------------------------------------------------------
;;  Default parameters for 'T', 'O' and 'L' commands
;; -------------------------------------------------------------
;;

tempo:  db      120
octave: db      4
lenght: db      4

        db      $76             ;N/L

;- Display file --------------------------------------------

dfile:  db      $76
        db      $76,$76,$76,$76,$76,$76,$76,$76
        db      $76,$76,$76,$76,$76,$76,$76,$76
        db      $76,$76,$76,$76,$76,$76,$76,$76

;- BASIC-Variables ----------------------------------------

var:    db      $80

;- End of program area ----------------------------

last:

        end
