мне кажется это очень геморно.
возможно проще самому слать данные в порт AYшки
Вид для печати
мне кажется это очень геморно.
возможно проще самому слать данные в порт AYшки
Так, отсюда - http://www.matthew-wilson.net/spectr.../128_ROM0.html
Оператор PLAY находится по адресу 0985h, я так понимаю.
- - - Добавлено - - -
К этому придем, я пока пытаюсь понять как и что. В идеале нужно миди файлы закидывать через последовательный порт AY-MIDI интерфейса.
http://www.fruitcake.plus.com/Sincla...isassembly.htm
ищи именно midi в дизасме rom0
(чуток опоздал с советом, вижу что уже копаешь в этом направлении)
Перевел по старинке бейсиковский файл Bach.tap в trd
загружать из TR-DOS
RUN "MIDIBACH"
Вложение Вложение 75443
Проверил, вроде играет.
Небольшое видео с примером звучания Баха
https://vk.com/micklab?z=video-19161...3ac079b886751b
Продолжаем попытки исследования оператора PLAY
Исходя из исходников Basic-128 оператор PLAY располагается в адресах ПЗУ 0985h...11ECh
Непосредственно с миди каналом работают следующие процедуры
0CDDh - Play Command 'Y' (MIDI Channel)
0CEEh - Play Command 'Z' (MIDI Programming Code)
116Eh - Play Note on MIDI Channel
118Dh - Turn MIDI Channel Off
11A3h - Send Byte to MIDI Device
Собственно подпрограмму отправки байта в канал Миди можно использовать в своих программах, ибо она уже расчитана на скорость 31250
Код:; =====================
; PLAY COMMAND ROUTINES
; =====================
; Up to 3 channels of music/noise are supported by the AY-3-8912 sound generator.
; Up to 8 channels of music can be sent to support synthesisers, drum machines or sequencers via the MIDI interface,
; with the first 3 channels also played by the AY-3-8912 sound generator. For each channel of music, a MIDI channel
; can be assigned to it using the 'Y' command.
;
; The PLAY command reserves and initialises space for the PLAY command. This comprises a block of $003C bytes
; used to manage the PLAY command (IY points to this command data block) and a block of $0037 bytes for each
; channel string (IX is used to point to the channel data block for the current channel). [Note that the command
; data block is $04 bytes larger than it needs to be, and each channel data block is $11 bytes larger than it
; needs to be]
;
; Entry: B=The number of strings in the PLAY command (1..8).
; -------------------------
; Command Data Block Format
; -------------------------
; IY+$00 / IY+$01 = Channel 0 data block pointer. Points to the data for channel 0 (string 1).
; IY+$02 / IY+$03 = Channel 1 data block pointer. Points to the data for channel 1 (string 2).
; IY+$04 / IY+$05 = Channel 2 data block pointer. Points to the data for channel 2 (string 3).
; IY+$06 / IY+$07 = Channel 3 data block pointer. Points to the data for channel 3 (string 4).
; IY+$08 / IY+$09 = Channel 4 data block pointer. Points to the data for channel 4 (string 5).
; IY+$0A / IY+$0B = Channel 5 data block pointer. Points to the data for channel 5 (string 6).
; IY+$0C / IY+$0D = Channel 6 data block pointer. Points to the data for channel 6 (string 7).
; IY+$0E / IY+$0F = Channel 7 data block pointer. Points to the data for channel 7 (string 8).
; IY+$10 = Channel bitmap. Initialised to $FF and a 0 rotated in to the left for each string parameters
; of the PLAY command, thereby indicating the channels in use.
; IY+$11 / IY+$12 = Channel data block duration pointer. Points to duration length store in channel 0 data block (string 1).
; IY+$13 / IY+$14 = Channel data block duration pointer. Points to duration length store in channel 1 data block (string 2).
; IY+$15 / IY+$16 = Channel data block duration pointer. Points to duration length store in channel 2 data block (string 3).
; IY+$17 / IY+$18 = Channel data block duration pointer. Points to duration length store in channel 3 data block (string 4).
; IY+$19 / IY+$1A = Channel data block duration pointer. Points to duration length store in channel 4 data block (string 5).
; IY+$1B / IY+$1C = Channel data block duration pointer. Points to duration length store in channel 5 data block (string 6).
; IY+$1D / IY+$1E = Channel data block duration pointer. Points to duration length store in channel 6 data block (string 7).
; IY+$1F / IY+$20 = Channel data block duration pointer. Points to duration length store in channel 7 data block (string 8).
; IY+$21 = Channel selector. It is used as a shift register with bit 0 initially set and then shift to the left
; until a carry occurs, thereby indicating all 8 possible channels have been processed.
; IY+$22 = Temporary channel bitmap, used to hold a working copy of the channel bitmap at IY+$10.
; IY+$23 / IY+$24 = Address of the channel data block pointers, or address of the channel data block duration pointers
; (allows the routine at $0A6E (ROM 0) to be used with both set of pointers).
; IY+$25 / IY+$26 = Stores the smallest duration length of all currently playing channel notes.
; IY+$27 / IY+$28 = The current tempo timing value (derived from the tempo parameter 60..240 beats per second).
; IY+$29 = The current effect waveform value.
; IY+$2A = Temporary string counter selector.
; IY+$2B..IY+$37 = Holds a floating point calculator routine.
; IY+$38..IY+$3B = Not used.
; -------------------------
; Channel Data Block Format
; -------------------------
; IX+$00 = The note number being played on this channel (equivalent to index offset into the note table).
; IX+$01 = MIDI channel assigned to this string (range 0 to 15).
; IX+$02 = Channel number (range 0 to 7), i.e. index position of the string within the PLAY command.
; IX+$03 = 12*Octave number (0, 12, 24, 36, 48, 60, 72, 84 or 96).
; IX+$04 = Current volume (range 0 to 15, or if bit 4 set then using envelope).
; IX+$05 = Last note duration value as specified in the string (range 1 to 9).
; IX+$06 / IX+$07 = Address of current position in the string.
; IX+$08 / IX+$09 = Address of byte after the end of the string.
; IX+$0A = Flags:
; Bit 0 : 1=Single closing bracket found (repeat string indefinitely).
; Bits 1-7: Not used (always 0).
; IX+$0B = Open bracket nesting level (range $00 to $04).
; IX+$0C / IX+$0D = Return address for opening bracket nesting level 0 (points to character after the bracket).
; IX+$0E / IX+$0F = Return address for opening bracket nesting level 1 (points to character after the bracket).
; IX+$10 / IX+$11 = Return address for opening bracket nesting level 2 (points to character after the bracket).
; IX+$12 / IX+$13 = Return address for opening bracket nesting level 3 (points to character after the bracket).
; IX+$14 / IX+$15 = Return address for opening bracket nesting level 4 (points to character after the bracket).
; IX+$16 = Closing bracket nesting level (range $FF to $04).
; IX+$17...IX+$18 = Return address for closing bracket nesting level 0 (points to character after the bracket).
; IX+$19...IX+$1A = Return address for closing bracket nesting level 1 (points to character after the bracket).
; IX+$1B...IX+$1C = Return address for closing bracket nesting level 2 (points to character after the bracket).
; IX+$1D...IX+$1E = Return address for closing bracket nesting level 3 (points to character after the bracket).
; IX+$1F...IX+$20 = Return address for closing bracket nesting level 4 (points to character after the bracket).
; IX+$21 = Tied notes counter (for a single note the value is 1).
; IX+$22 / IX+$23 = Duration length, specified in 96ths of a note.
; IX+$24...IX+$25 = Subsequent note duration length (used only with triplets), specified in 96ths of a note.
; IX+$26...IX+$36 = Not used.
L0985: DI ; Disable interrupts to ensure accurate timing.
;Create a workspace for the play channel command strings
PUSH BC ; B=Number of channel string (range 1 to 8). Also used as string index number in the following loop.
LD DE,$0037 ;
LD HL,$003C ;
L098D: ADD HL,DE ; Calculate HL=$003C + ($0037 * B).
DJNZ L098D ;
LD C,L ;
LD B,H ; BC=Space required (maximum = $01F4).
RST 28H ;
DEFW BC_SPACES ; $0030. Make BC bytes of space in the workspace.
DI ; Interrupts get re-enabled by the call mechanism to ROM 1 so disable them again.
PUSH DE ;
POP IY ; IY=Points at first new byte - the command data block.
PUSH HL ;
POP IX ; IX=Points at last new byte - byte after all channel information blocks.
LD (IY+$10),$FF ; Initial channel bitmap with value meaning 'zero strings'
;Loop over each string to be played
L09A0: LD BC,$FFC9 ; $-37 ($37 bytes is the size of a play channel string information block).
ADD IX,BC ; IX points to start of space for the last channel.
LD (IX+$03),$3C ; Default octave is 5.
LD (IX+$01),$FF ; No MIDI channel assigned.
LD (IX+$04),$0F ; Default volume is 15.
LD (IX+$05),$05 ; Default note duration.
LD (IX+$21),$00 ; Count of the number of tied notes.
LD (IX+$0A),$00 ; Signal not to repeat the string indefinitely.
LD (IX+$0B),$00 ; No opening bracket nesting level.
LD (IX+$16),$FF ; No closing bracket nesting level.
LD (IX+$17),$00 ; Return address for closing bracket nesting level 0.
LD (IX+$18),$00 ; [No need to initialise this since it is written to before it is ever tested]
; [*BUG* - At this point interrupts are disabled and IY is now being used as a pointer to the master
; PLAY information block. Unfortunately, interrupts are enabled during the STK_FETCH call and
; IY is left containing the wrong value. This means that if an interrupt were to occur during
; execution of the subroutine then there would be a one in 65536 chance that (IY+$40) will be
; corrupted - this corresponds to the volume setting for music channel A.
; Rewriting the SWAP routine to only re-enable interrupts if they were originally enabled
; would cure this bug (see end of file for description of her suggested fix). Credit: Toni Baker, ZX Computing Monthly]
; [An alternative and simpler solution to the fix Toni Baker describes would be to stack IY, set IY to point
; to the system variables at $5C3A, call STK_FETCH, disable interrupts, then pop the stacked value back to IY. Credit: Paul Farrow]
RST 28H ; Get the details of the string from the stack.
DEFW STK_FETCH ; $2BF1.
DI ; Interrupts get re-enabled by the call mechanism to ROM 1 so disable them again.
LD (IX+$06),E ; Store the current position within in the string, i.e. the beginning of it.
LD (IX+$07),D ;
LD (IX+$0C),E ; Store the return position within the string for a closing bracket,
LD (IX+$0D),D ; which is initially the start of the string in case a single closing bracket is found.
EX DE,HL ; HL=Points to start of string. BC=Length of string.
ADD HL,BC ; HL=Points to address of byte after the string.
LD (IX+$08),L ; Store the address of the character just
LD (IX+$09),H ; after the string.
POP BC ; B=String index number (range 1 to 8).
PUSH BC ; Save it on the stack again.
DEC B ; Reduce the index so it ranges from 0 to 7.
LD C,B ;
LD B,$00 ;
SLA C ; BC=String index*2.
PUSH IY ;
POP HL ; HL=Address of the command data block.
ADD HL,BC ; Skip 8 channel data pointer words.
PUSH IX ;
POP BC ; BC=Address of current channel information block.
LD (HL),C ; Store the pointer to the channel information block.
INC HL ;
LD (HL),B ;
OR A ; Clear the carry flag.
RL (IY+$10) ; Rotate one zero-bit into the least significant bit of the channel bitmap.
; This initially holds $FF but once this loop is over, this byte has
; a zero bit for each string parameter of the PLAY command.
POP BC ; B=Current string index.
DEC B ; Decrement string index so it ranges from 0 to 7.
PUSH BC ; Save it for future use on the next iteration.
LD (IX+$02),B ; Store the channel number.
JR NZ,L09A0 ; Jump back while more channel strings to process.
POP BC ; Drop item left on the stack.
;Entry point here from the vector table at $011B
L0A05: LD (IY+$27),$1A ; Set the initial tempo timing value.
LD (IY+$28),$0B ; Corresponds to a 'T' command value of 120, and gives two crotchets per second.
PUSH IY ;
POP HL ; HL=Points to the command data block.
LD BC,$002B ;
ADD HL,BC ;
EX DE,HL ; DE=Address to store RAM routine.
LD HL,L0A31 ; HL=Address of the RAM routine bytes.
LD BC,$000D ;
LDIR ; Copy the calculator routine to RAM.
LD D,$07 ; Register 7 - Mixer.
LD E,$F8 ; I/O ports are inputs, noise output off, tone output on.
CALL L0E7C ; Write to sound generator register.
LD D,$0B ; Register 11 - Envelope Period (Fine).
LD E,$FF ; Set period to maximum.
CALL L0E7C ; Write to sound generator register.
INC D ; Register 12 - Envelope Period (Coarse).
CALL L0E7C ; Write to sound generator register.
JR L0A7D ; Jump ahead to continue.
; [Could have saved these 2 bytes by having the code at $0A7D (ROM 0) immediately follow]
; -------------------------------------------------
; Calculate Timing Loop Counter <<< RAM Routine >>>
; -------------------------------------------------
; This routine is copied into the command data block (offset $2B..$37) by
; the routine at $0A05 (ROM 0).
; It uses the floating point calculator found in ROM 1, which is usually
; invoked via a RST $28 instruction. Since ROM 0 uses RST $28 to call a
; routine in ROM 1, it is unable to invoke the floating point calculator
; this way. It therefore copies the following routine to RAM and calls it
; with ROM 1 paged in.
;
; The routine calculates (10/x)/7.33e-6, where x is the tempo 'T' parameter value
; multiplied by 4. The result is used an inner loop counter in the wait routine at $0F76 (ROM 0).
; Each iteration of this loop takes 26 T-states. The time taken by 26 T-states
; is 7.33e-6 seconds. So the total time for the loop to execute is 2.5/TEMPO seconds.
;
; Entry: The value 4*TEMPO exists on the calculator stack (where TEMPO is in the range 60..240).
; Exit : The calculator stack holds the result.
L0A31: RST 28H ; Invoke the floating point calculator.
DEFB $A4 ; stk-ten. = x, 10
DEFB $01 ; exchange. = 10, x
DEFB $05 ; division. = 10/x
DEFB $34 ; stk-data. = 10/x, 7.33e-6
DEFB $DF ; - exponent $6F (floating point number 7.33e-6).
DEFB $75 ; - mantissa byte 1
DEFB $F4 ; - mantissa byte 2
DEFB $38 ; - mantissa byte 3
DEFB $75 ; - mantissa byte 4
DEFB $05 ; division. = (10/x)/7.33e-6
DEFB $38 ; end-calc.
RET ;
; --------------
; Test BREAK Key
; --------------
; Test for BREAK being pressed.
; Exit: Carry flag reset if BREAK is being pressed.
L0A3E: LD A,$7F ;
IN A,($FE) ;
RRA ;
RET C ; Return with carry flag set if SPACE not pressed.
LD A,$FE ;
IN A,($FE) ;
RRA ;
RET ; Return with carry flag set if CAPS not pressed.
; -------------------------------------------
; Select Channel Data Block Duration Pointers
; -------------------------------------------
; Point to the start of the channel data block duration pointers within the command data block.
; Entry: IY=Address of the command data block.
; Exit : HL=Address of current channel pointer.
L0A4A: LD BC,$0011 ; Offset to the channel data block duration pointers table.
JR L0A52 ; Jump ahead to continue.
; ----------------------------------
; Select Channel Data Block Pointers
; ----------------------------------
; Point to the start of the channel data block pointers within the command data block.
; Entry: IY=Address of the command data block.
; Exit : HL=Address of current channel pointer.
L0A4F: LD BC,$0000 ; Offset to the channel data block pointers table.
L0A52: PUSH IY ;
POP HL ; HL=Point to the command data block.
ADD HL,BC ; Point to the desired channel pointers table.
LD (IY+$23),L ;
LD (IY+$24),H ; Store the start address of channels pointer table.
LD A,(IY+$10) ; Fetch the channel bitmap.
LD (IY+$22),A ; Initialise the working copy.
LD (IY+$21),$01 ; Channel selector. Set the shift register to indicate the first channel.
RET ;
; -------------------------------------------------
; Get Channel Data Block Address for Current String
; -------------------------------------------------
; Entry: HL=Address of channel data block pointer.
; Exit : IX=Address of current channel data block.
L0A67: LD E,(HL) ;
INC HL ;
LD D,(HL) ; Fetch the address of the current channel data block.
PUSH DE ;
POP IX ; Return it in IX.
RET ;
; -------------------------
; Next Channel Data Pointer
; -------------------------
L0A6E: LD L,(IY+$23) ; The address of current channel data pointer.
LD H,(IY+$24) ;
INC HL ;
INC HL ; Advance to the next channel data pointer.
LD (IY+$23),L ;
LD (IY+$24),H ; The address of new channel data pointer.
RET ;
; ---------------------------
; PLAY Command (Continuation)
; ---------------------------
; This section is responsible for processing the PLAY command and is a continuation of the routine
; at $0985 (ROM 0). It begins by determining the first note to play on each channel and then enters
; a loop to play these notes, fetching the subsequent notes to play at the appropriate times.
L0A7D: CALL L0A4F ; Select channel data block pointers.
L0A80: RR (IY+$22) ; Working copy of channel bitmap. Test if next string present.
JR C,L0A8C ; Jump ahead if there is no string for this channel.
;HL=Address of channel data pointer.
CALL L0A67 ; Get address of channel data block for the current string into IX.
CALL L0B5C ; Find the first note to play for this channel from its play string.
L0A8C: SLA (IY+$21) ; Have all channels been processed?
JR C,L0A97 ; Jump ahead if so.
CALL L0A6E ; Advance to the next channel data block pointer.
JR L0A80 ; Jump back to process the next channel.
;The first notes to play for each channel have now been determined. A loop is entered that coordinates playing
;the notes and fetching subsequent notes when required. Notes across channels may be of different lengths and
;so the shortest one is determined, the tones for all channels set and then a waiting delay entered for the shortest
;note delay. This delay length is then subtracted from all channel note lengths to leave the remaining lengths that
;each note needs to be played for. For the channel with the smallest note length, this will now have completely played
;and so a new note is fetched for it. The smallest length of the current notes is then determined again and the process
;described above repeated. A test is made on each iteration to see if all channels have run out of data to play, and if
;so this ends the PLAY command.
L0A97: CALL L0F91 ; Find smallest duration length of the current notes across all channels.
PUSH DE ; Save the smallest duration length.
CALL L0F42 ; Play a note on each channel.
POP DE ; DE=The smallest duration length.
L0A9F: LD A,(IY+$10) ; Channel bitmap.
CP $FF ; Is there anything to play?
JR NZ,L0AAB ; Jump if there is.
CALL L0E93 ; Turn off all sound and restore IY.
EI ; Re-enable interrupts.
RET ; End of play command.
L0AAB: DEC DE ; DE=Smallest channel duration length, i.e. duration until the next channel state change.
CALL L0F76 ; Perform a wait.
CALL L0FC1 ; Play a note on each channel and update the channel duration lengths.
CALL L0F91 ; Find smallest duration length of the current notes across all channels.
JR L0A9F ; Jump back to see if there is more to process.
; ----------------------------
; PLAY Command Character Table
; ----------------------------
; Recognised characters in PLAY commands.
L0AB7: DEFM "HZYXWUVMT)(NO!"
; ------------------
; Get Play Character
; ------------------
; Get the current character from the PLAY string and then increment the
; character pointer within the string.
; Exit: Carry flag set if string has been fully processed.
; Carry flag reset if character is available.
; A=Character available.
L0AC5: CALL L0EE3 ; Get the current character from the play string for this channel.
RET C ; Return if no more characters.
INC (IX+$06) ; Increment the low byte of the string pointer.
RET NZ ; Return if it has not overflowed.
INC (IX+$07) ; Else increment the high byte of the string pointer.
RET ; Returns with carry flag reset.
; --------------------------
; Get Next Note in Semitones
; --------------------------
; Finds the number of semitones above C for the next note in the string,
; Entry: IX=Address of the channel data block.
; Exit : A=Number of semitones above C, or $80 for a rest.
L0AD1: PUSH HL ; Save HL.
LD C,$00 ; Default is for a 'natural' note, i.e. no adjustment.
L0AD4: CALL L0AC5 ; Get the current character from the PLAY string, and advance the position pointer.
JR C,L0AE1 ; Jump if at the end of the string.
CP '&' ; $26. Is it a rest?
JR NZ,L0AEC ; Jump ahead if not.
LD A,$80 ; Signal that it is a rest.
L0ADF: POP HL ; Restore HL.
RET ;
L0AE1: LD A,(IY+$21) ; Fetch the channel selector.
OR (IY+$10) ; Clear the channel flag for this string.
LD (IY+$10),A ; Store the new channel bitmap.
JR L0ADF ; Jump back to return.
L0AEC: CP '#' ; $23. Is it a sharpen?
JR NZ,L0AF3 ; Jump ahead if not.
INC C ; Increment by a semitone.
JR L0AD4 ; Jump back to get the next character.
L0AF3: CP '$' ; $24. Is it a flatten?
JR NZ,L0AFA ; Jump ahead if not.
DEC C ; Decrement by a semitone.
JR L0AD4 ; Jump back to get the next character.
L0AFA: BIT 5,A ; Is it a lower case letter?
JR NZ,L0B04 ; Jump ahead if lower case.
PUSH AF ; It is an upper case letter so
LD A,$0C ; increase an octave
ADD A,C ; by adding 12 semitones.
LD C,A ;
POP AF ;
L0B04: AND $DF ; Convert to upper case.
SUB $41 ; Reduce to range 'A'->0 .. 'G'->6.
JP C,L0F22 ; Jump if below 'A' to produce error report "k Invalid note name".
CP $07 ; Is it 7 or above?
JP NC,L0F22 ; Jump if so to produce error report "k Invalid note name".
PUSH BC ; C=Number of semitones.
LD B,$00 ;
LD C,A ; BC holds 0..6 for 'a'..'g'.
LD HL,L0DF9 ; Look up the number of semitones above note C for the note.
ADD HL,BC ;
LD A,(HL) ; A=Number of semitones above note C.
POP BC ; C=Number of semitones due to sharpen/flatten characters.
ADD A,C ; Adjust number of semitones above note C for the sharpen/flatten characters.
POP HL ; Restore HL.
RET ;
; ----------------------------------
; Get Numeric Value from Play String
; ----------------------------------
; Get a numeric value from a PLAY string, returning 0 if no numeric value present.
; Entry: IX=Address of the channel data block.
; Exit : BC=Numeric value, or 0 if no numeric value found.
L0B1D: PUSH HL ; Save registers.
PUSH DE ;
LD L,(IX+$06) ; Get the pointer into the PLAY string.
LD H,(IX+$07) ;
LD DE,$0000 ; Initialise result to 0.
L0B28: LD A,(HL) ;
CP '0' ; $30. Is character numeric?
JR C,L0B45 ; Jump ahead if not.
CP ':' ; $3A. Is character numeric?
JR NC,L0B45 ; Jump ahead if not.
INC HL ; Advance to the next character.
PUSH HL ; Save the pointer into the string.
CALL L0B50 ; Multiply result so far by 10.
SUB '0' ; $30. Convert ASCII digit to numeric value.
LD H,$00 ;
LD L,A ; HL=Numeric digit value.
ADD HL,DE ; Add the numeric value to the result so far.
JR C,L0B42 ; Jump ahead if an overflow to produce error report "l number too big".
EX DE,HL ; Transfer the result into DE.
POP HL ; Retrieve the pointer into the string.
JR L0B28 ; Loop back to handle any further numeric digits.
L0B42: JP L0F1A ; Jump to produce error report "l number too big".
; [Could have saved 1 byte by directly using JP C,L0F1A (ROM 0) instead of using this JP and
; the two JR C,L0B42 (ROM 0) instructions that come here]
;The end of the numeric value was reached
L0B45: LD (IX+$06),L ; Store the new pointer position into the string.
LD (IX+$07),H ;
PUSH DE ;
POP BC ; Return the result in BC.
POP DE ; Restore registers.
POP HL ;
RET ;
; -----------------
; Multiply DE by 10
; -----------------
; Entry: DE=Value to multiple by 10.
; Exit : DE=Value*10.
L0B50: LD HL,$0000 ;
LD B,$0A ; Add DE to HL ten times.
L0B55: ADD HL,DE ;
JR C,L0B42 ; Jump ahead if an overflow to produce error report "l number too big".
DJNZ L0B55 ;
EX DE,HL ; Transfer the result into DE.
RET ;
; ----------------------------------
; Find Next Note from Channel String
; ----------------------------------
; Entry: IX=Address of channel data block.
L0B5C: CALL L0A3E ; Test for BREAK being pressed.
JR C,L0B69 ; Jump ahead if not pressed.
CALL L0E93 ; Turn off all sound and restore IY.
EI ; Re-enable interrupts.
CALL L05AC ; Produce error report. [Could have saved 1 byte by using JP L05D6 (ROM 0)]
DEFB $14 ; "L Break into program"
L0B69: CALL L0AC5 ; Get the current character from the PLAY string, and advance the position pointer.
JP C,L0DA2 ; Jump if at the end of the string.
CALL L0DF0 ; Find the handler routine for the PLAY command character.
LD B,$00 ;
SLA C ; Generate the offset into the
LD HL,L0DCA ; command vector table.
ADD HL,BC ; HL points to handler routine for this command character.
LD E,(HL) ;
INC HL ;
LD D,(HL) ; Fetch the handler routine address.
EX DE,HL ; HL=Handler routine address for this command character.
CALL L0B84 ; Make an indirect call to the handler routine.
JR L0B5C ; Jump back to handle the next character in the string.
;Comes here after processing a non-numeric digit that does not have a specific command routine handler
;Hence the next note to play has been determined and so a return is made to process the other channels.
L0B83: RET ; Just make a return.
L0B84: JP (HL) ; Jump to the command handler routine.
; --------------------------
; Play Command '!' (Comment)
; --------------------------
; A comment is enclosed within exclamation marks, e.g. "! A comment !".
; Entry: IX=Address of the channel data block.
L0B85: CALL L0AC5 ; Get the current character from the PLAY string, and advance the position pointer.
JP C,L0DA1 ; Jump if at the end of the string.
CP '!' ; $21. Is it the end-of-comment character?
RET Z ; Return if it is.
JR L0B85 ; Jump back to test the next character.
; -------------------------
; Play Command 'O' (Octave)
; -------------------------
; The 'O' command is followed by a numeric value within the range 0 to 8,
; although due to loose range checking the value MOD 256 only needs to be
; within 0 to 8. Hence O256 operates the same as O0.
; Entry: IX=Address of the channel data block.
L0B90: CALL L0B1D ; Get following numeric value from the string into BC.
LD A,C ; Is it between 0 and 8?
CP $09 ;
JP NC,L0F12 ; Jump if above 8 to produce error report "n Out of range".
SLA A ; Multiply A by 12.
SLA A ;
LD B,A ;
SLA A ;
ADD A,B ;
LD (IX+$03),A ; Store the octave value.
RET ;
; ----------------------------
; Play Command 'N' (Separator)
; ----------------------------
; The 'N' command is simply a separator marker and so is ignored.
; Entry: IX=Address of the channel data block.
L0BA5: RET ; Nothing to do so make an immediate return.
; ----------------------------------
; Play Command '(' (Start of Repeat)
; ----------------------------------
; A phrase can be enclosed within brackets causing it to be repeated, i.e. played twice.
; Entry: IX=Address of the channel data block.
L0BA6: LD A,(IX+$0B) ; A=Current level of open bracket nesting.
INC A ; Increment the count.
CP $05 ; Only 4 levels supported.
JP Z,L0F2A ; Jump if this is the fifth to produce error report "d Too many brackets".
LD (IX+$0B),A ; Store the new open bracket nesting level.
LD DE,$000C ; Offset to the bracket level return position stores.
CALL L0C27 ; HL=Address of the pointer in which to store the return location of the bracket.
LD A,(IX+$06) ; Store the current string position as the return address of the open bracket.
LD (HL),A ;
INC HL ;
LD A,(IX+$07) ;
LD (HL),A ;
RET ;
; --------------------------------
; Play Command ')' (End of Repeat)
; --------------------------------
; A phrase can be enclosed within brackets causing it to be repeated, i.e. played twice.
; Brackets can also be nested within each other, to 4 levels deep.
; If a closing bracket if used without a matching opening bracket then the whole string up
; until that point is repeated indefinitely.
; Entry: IX=Address of the channel data block.
L0BC2: LD A,(IX+$16) ; Fetch the nesting level of closing brackets.
LD DE,$0017 ; Offset to the closing bracket return address store.
OR A ; Is there any bracket nesting so far?
JP M,L0BF0 ; Jump if none. [Could have been faster by jumping to L0BF3 (ROM 0)]
;Has the bracket level been repeated, i.e. re-reached the same position in the string as the closing bracket return address?
CALL L0C27 ; HL=Address of the pointer to the corresponding closing bracket return address store.
LD A,(IX+$06) ; Fetch the low byte of the current address.
CP (HL) ; Re-reached the closing bracket?
JR NZ,L0BF0 ; Jump ahead if not.
INC HL ; Point to the high byte.
LD A,(IX+$07) ; Fetch the high byte address of the current address.
CP (HL) ; Re-reached the closing bracket?
JR NZ,L0BF0 ; Jump ahead if not.
;The bracket level has been repeated. Now check whether this was the outer bracket level.
DEC (IX+$16) ; Decrement the closing bracket nesting level since this level has been repeated.
LD A,(IX+$16) ; [There is no need for the LD A,(IX+$16) and OR A instructions since the DEC (IX+$16) already set the flags]
OR A ; Reached the outer bracket nesting level?
RET P ; Return if not the outer bracket nesting level such that the character
; after the closing bracket is processed next.
;The outer bracket level has been repeated
BIT 0,(IX+$0A) ; Was this a single closing bracket?
RET Z ; Return if it was not.
;The repeat was caused by a single closing bracket so re-initialise the repeat
LD (IX+$16),$00 ; Restore one level of closing bracket nesting.
XOR A ; Select closing bracket nesting level 0.
JR L0C0B ; Jump ahead to continue.
;A new level of closing bracket nesting
L0BF0: LD A,(IX+$16) ; Fetch the nesting level of closing brackets.
INC A ; Increment the count.
CP $05 ; Only 5 levels supported (4 to match up with opening brackets and a 5th to repeat indefinitely).
JP Z,L0F2A ; Jump if this is the fifth to produce error report "d Too many brackets".
LD (IX+$16),A ; Store the new closing bracket nesting level.
CALL L0C27 ; HL=Address of the pointer to the appropriate closing bracket return address store.
LD A,(IX+$06) ; Store the current string position as the return address for the closing bracket.
LD (HL),A ;
INC HL ;
LD A,(IX+$07) ;
LD (HL),A ;
LD A,(IX+$0B) ; Fetch the nesting level of opening brackets.
L0C0B: LD DE,$000C ;
CALL L0C27 ; HL=Address of the pointer to the opening bracket nesting level return address store.
LD A,(HL) ; Set the return address of the nesting level's opening bracket
LD (IX+$06),A ; as new current position within the string.
INC HL ;
LD A,(HL) ; For a single closing bracket only, this will be the start address of the string.
LD (IX+$07),A ;
DEC (IX+$0B) ; Decrement level of open bracket nesting.
RET P ; Return if the closing bracket matched an open bracket.
;There is one more closing bracket then opening brackets, i.e. repeat string indefinitely
LD (IX+$0B),$00 ; Set the opening brackets nesting level to 0.
SET 0,(IX+$0A) ; Signal a single closing bracket only, i.e. to repeat the string indefinitely.
RET ;
; ------------------------------------
; Get Address of Bracket Pointer Store
; ------------------------------------
; Entry: IX=Address of the channel data block.
; DE=Offset to the bracket pointer stores.
; A=Index into the bracket pointer stores.
; Exit : HL=Address of the specified pointer store.
L0C27: PUSH IX ;
POP HL ; HL=IX.
ADD HL,DE ; HL=IX+DE.
LD B,$00 ;
LD C,A ;
SLA C ;
ADD HL,BC ; HL=IX+DE+2*A.
RET ;
; ------------------------
; Play Command 'T' (Tempo)
; ------------------------
; A temp command must be specified in the first play string and is followed by a numeric
; value in the range 60 to 240 representing the number of beats (crotchets) per minute.
; Entry: IX=Address of the channel data block.
L0C32: CALL L0B1D ; Get following numeric value from the string into BC.
LD A,B ;
OR A ;
JP NZ,L0F12 ; Jump if 256 or above to produce error report "n Out of range".
LD A,C ;
CP $3C ;
JP C,L0F12 ; Jump if 59 or below to produce error report "n Out of range".
CP $F1 ;
JP NC,L0F12 ; Jump if 241 or above to produce error report "n Out of range".
;A holds a value in the range 60 to 240
LD A,(IX+$02) ; Fetch the channel number.
OR A ; Tempo 'T' commands have to be specified in the first string.
RET NZ ; If it is in a later string then ignore it.
LD B,$00 ; [Redundant instruction - B is already zero]
PUSH BC ; C=Tempo value.
POP HL ;
ADD HL,HL ;
ADD HL,HL ; HL=Tempo*4.
PUSH HL ;
POP BC ; BC=Tempo*4. [Would have been quicker to use the combination LD B,H and LD C,L]
PUSH IY ; Save the pointer to the play command data block.
RST 28H ;
DEFW STACK_BC ; $2D2B. Place the contents of BC onto the stack. The call restores IY to $5C3A.
DI ; Interrupts get re-enabled by the call mechanism to ROM 1 so disable them again.
POP IY ; Restore IY to point at the play command data block.
PUSH IY ; Save the pointer to the play command data block.
PUSH IY ;
POP HL ; HL=pointer to the play command data block.
LD BC,$002B ;
ADD HL,BC ; HL =IY+$002B.
LD IY,$5C3A ; Reset IY to $5C3A since this is required by the floating point calculator.
PUSH HL ; HL=Points to the calculator RAM routine.
LD HL,L0C76 ;
LD (RETADDR),HL ; $5B5A. Set up the return address.
LD HL,YOUNGER ;
EX (SP),HL ; Stack the address of the swap routine used when returning to this ROM.
PUSH HL ; Re-stack the address of the calculator RAM routine.
JP SWAP ; $5B00. Toggle to other ROM and make a return to the calculator RAM routine.
; --------------------
; Tempo Command Return
; --------------------
; The calculator stack now holds the value (10/(Tempo*4))/7.33e-6 and this is stored as the tempo value.
; The result is used an inner loop counter in the wait routine at $0F76 (ROM 0). Each iteration of this loop
; takes 26 T-states. The time taken by 26 T-states is 7.33e-6 seconds. So the total time for the loop
; to execute is 2.5/TEMPO seconds.
L0C76: DI ; Interrupts get re-enabled by the call mechanism to ROM 1 so disable them again.
RST 28H ;
DEFW FP_TO_BC ; $2DA2. Fetch the value on the top of the calculator stack.
DI ; Interrupts get re-enabled by the call mechanism to ROM 1 so disable them again.
POP IY ; Restore IY to point at the play command data block.
LD (IY+$27),C ; Store tempo timing value.
LD (IY+$28),B ;
RET ;
; ------------------------
; Play Command 'M' (Mixer)
; ------------------------
; This command is used to select whether to use tone and/or noise on each of the 3 channels.
; It is followed by a numeric value in the range 1 to 63, although due to loose range checking the
; value MOD 256 only needs to be within 0 to 63. Hence M256 operates the same as M0.
; Entry: IX=Address of the channel data block.
L0C84: CALL L0B1D ; Get following numeric value from the string into BC.
LD A,C ; A=Mixer value.
CP $40 ; Is it 64 or above?
JP NC,L0F12 ; Jump if so to produce error report "n Out of range".
;Bit 0: 1=Enable channel A tone.
;Bit 1: 1=Enable channel B tone.
;Bit 2: 1=Enable channel C tone.
;Bit 3: 1=Enable channel A noise.
;Bit 4: 1=Enable channel B noise.
;Bit 5: 1=Enable channel C noise.
CPL ; Invert the bits since the sound generator's mixer register uses active low enable.
; This also sets bit 6 1, which selects the I/O port as an output.
LD E,A ; E=Mixer value.
LD D,$07 ; D=Register 7 - Mixer.
CALL L0E7C ; Write to sound generator register to set the mixer.
RET ; [Could have saved 1 byte by using JP L0E7C (ROM 0)]
; -------------------------
; Play Command 'V' (Volume)
; -------------------------
; This sets the volume of a channel and is followed by a numeric value in the range
; 0 (minimum) to 15 (maximum), although due to loose range checking the value MOD 256
; only needs to be within 0 to 15. Hence V256 operates the same as V0.
; Entry: IX=Address of the channel data block.
L0C95: CALL L0B1D ; Get following numeric value from the string into BC.
LD A,C ;
CP $10 ; Is it 16 or above?
JP NC,L0F12 ; Jump if so to produce error report "n Out of range".
LD (IX+$04),A ; Store the volume level.
; [*BUG* - An attempt to set the volume for a sound chip channel is now made. However, this routine fails to take into account
; that it is also called to set the volume for a MIDI only channel, i.e. play strings 4 to 8. As a result, corruption
; occurs to various sound generator registers, causing spurious sound output. There is in fact no need for this routine
; to set the volume for any channels since this is done every time a new note is played - see routine at $0A97 (ROM 0).
; the bug fix is to simply to make a return at this point. This routine therefore contains 11 surplus bytes. Credit: Ian Collier (+3), Paul Farrow (128)]
LD E,(IX+$02) ; E=Channel number.
LD A,$08 ; Offset by 8.
ADD A,E ; A=8+index.
LD D,A ; D=Sound generator register number for the channel.
LD E,C ; E=Volume level.
CALL L0E7C ; Write to sound generator register to set the volume for the channel.
RET ; [Could have saved 1 byte by using JP L0E7C (ROM 0)]
; ------------------------------------
; Play Command 'U' (Use Volume Effect)
; ------------------------------------
; This command turns on envelope waveform effects for a particular sound chip channel. The volume level is now controlled by
; the selected envelope waveform for the channel, as defined by the 'W' command. MIDI channels do not support envelope waveforms
; and so the routine has the effect of setting the volume of a MIDI channel to maximum, i.e. 15. It might seem odd that the volume
; for MIDI channels is set to 15 rather than just filtered out. However, the three sound chip channels can also drive three MIDI
; channels and so it would be inconsistent for these MIDI channels to have their volume set to 15 but have the other MIDI channels
; behave differently. However, it could be argued that all MIDI channels should be unaffected by the 'U' command.
; There are no parameters to this command.
; Entry: IX=Address of the channel data block.
L0CAD: LD E,(IX+$02) ; Get the channel number.
LD A,$08 ; Offset by 8.
ADD A,E ; A=8+index.
LD D,A ; D=Sound generator register number for the channel. [This is not used and so there is no need to generate it. It was probably a left
; over from copying and modifying the 'V' command routine. Deleting it would save 7 bytes. Credit: Ian Collier (+3), Paul Farrow (128)]
LD E,$1F ; E=Select envelope defined by register 13, and reset volume bits to maximum (though these are not used with the envelope).
LD (IX+$04),E ; Store that the envelope is being used (along with the reset volume level).
RET ;
; ------------------------------------------
; Play command 'W' (Volume Effect Specifier)
; ------------------------------------------
; This command selects the envelope waveform to use and is followed by a numeric value in the range
; 0 to 7, although due to loose range checking the value MOD 256 only needs to be within 0 to 7.
; Hence W256 operates the same as W0.
; Entry: IX=Address of the channel data block.
L0CBA: CALL L0B1D ; Get following numeric value from the string into BC.
LD A,C ;
CP $08 ; Is it 8 or above?
JP NC,L0F12 ; Jump if so to produce error report "n Out of range".
LD B,$00 ;
LD HL,L0DE8 ; Envelope waveform lookup table.
ADD HL,BC ; HL points to the corresponding value in the table.
LD A,(HL) ;
LD (IY+$29),A ; Store new effect waveform value.
RET ;
; -----------------------------------------
; Play Command 'X' (Volume Effect Duration)
; -----------------------------------------
; This command allows the duration of a waveform effect to be specified, and is followed by a numeric
; value in the range 0 to 65535. A value of 1 corresponds to the minimum duration, increasing up to 65535
; and then maximum duration for a value of 0. If no numeric value is specified then the maximum duration is used.
; Entry: IX=Address of the channel data block.
L0CCE: CALL L0B1D ; Get following numeric value from the string into BC.
LD D,$0B ; Register 11 - Envelope Period Fine.
LD E,C ;
CALL L0E7C ; Write to sound generator register to set the envelope period (low byte).
INC D ; Register 12 - Envelope Period Coarse.
LD E,B ;
CALL L0E7C ; Write to sound generator register to set the envelope period (high byte).
RET ; [Could have saved 1 byte by using JP L0E7C (ROM 0)]
; -------------------------------
; Play Command 'Y' (MIDI Channel)
; -------------------------------
; This command sets the MIDI channel number that the string is assigned to and is followed by a numeric
; value in the range 1 to 16, although due to loose range checking the value MOD 256 only needs to be within 1 to 16.
; Hence Y257 operates the same as Y1.
; Entry: IX=Address of the channel data block.
L0CDD: CALL L0B1D ; Get following numeric value from the string into BC.
LD A,C ;
DEC A ; Is it 0?
JP M,L0F12 ; Jump if so to produce error report "n Out of range".
CP $10 ; Is it 10 or above?
JP NC,L0F12 ; Jump if so to produce error report "n Out of range".
LD (IX+$01),A ; Store MIDI channel number that this string is assigned to.
RET ;
; ----------------------------------------
; Play Command 'Z' (MIDI Programming Code)
; ----------------------------------------
; This command is used to send a programming code to the MIDI port. It is followed by a numeric
; value in the range 0 to 255, although due to loose range checking the value MOD 256 only needs
; to be within 0 to 255. Hence Z256 operates the same as Z0.
; Entry: IX=Address of the channel data block.
L0CEE: CALL L0B1D ; Get following numeric value from the string into BC.
LD A,C ; A=(low byte of) the value.
CALL L11A3 ; Write byte to MIDI device.
RET ; [Could have saved 1 byte by using JP L0E7C (ROM 0)]
; -----------------------
; Play Command 'H' (Stop)
; -----------------------
; This command stops further processing of a play command. It has no parameters.
; Entry: IX=Address of the channel data block.
L0CF6: LD (IY+$10),$FF ; Indicate no channels to play, thereby causing
RET ; the play command to terminate.
; --------------------------------------------------------
; Play Commands 'a'..'g', 'A'..'G', '1'.."12", '&' and '_'
; --------------------------------------------------------
; This handler routine processes commands 'a'..'g', 'A'..'G', '1'.."12", '&' and '_',
; and determines the length of the next note to play. It provides the handling of triplet and tied notes.
; It stores the note duration in the channel data block's duration length entry, and sets a pointer in the command
; data block's duration lengths pointer table to point at it. A single note letter is deemed to be a tied
; note count of 1. Triplets are deemed a tied note count of at least 2.
; Entry: IX=Address of the channel data block.
; A=Current character from play string.
L0CFB: CALL L0E19 ; Is the current character a number?
JP C,L0D81 ; Jump if not number digit.
;The character is a number digit
CALL L0DAC ; HL=Address of the duration length within the channel data block.
CALL L0DB4 ; Store address of duration length in command data block's channel duration length pointer table.
XOR A ;
LD (IX+$21),A ; Set no tied notes.
CALL L0EC8 ; Get the previous character in the string, the note duration.
CALL L0B1D ; Get following numeric value from the string into BC.
LD A,C ;
OR A ; Is the value 0?
JP Z,L0F12 ; Jump if so to produce error report "n Out of range".
CP $0D ; Is it 13 or above?
JP NC,L0F12 ; Jump if so to produce error report "n Out of range".
CP $0A ; Is it below 10?
JR C,L0D32 ; Jump if so.
;It is a triplet semi-quaver (10), triplet quaver (11) or triplet crotchet (12)
CALL L0E00 ; DE=Note duration length for the duration value.
CALL L0D74 ; Increment the tied notes counter.
LD (HL),E ; HL=Address of the duration length within the channel data block.
INC HL ;
LD (HL),D ; Store the duration length.
L0D28: CALL L0D74 ; Increment the counter of tied notes.
INC HL ;
LD (HL),E ;
INC HL ; Store the subsequent note duration length in the channel data block.
LD (HL),D ;
INC HL ;
JR L0D38 ; Jump ahead to continue.
;The note duration was in the range 1 to 9
L0D32: LD (IX+$05),C ; C=Note duration value (1..9).
CALL L0E00 ; DE=Duration length for this duration value.
L0D38: CALL L0D74 ; Increment the tied notes counter.
L0D3B: CALL L0EE3 ; Get the current character from the play string for this channel.
CP '_' ; $5F. Is it a tied note?
JR NZ,L0D6E ; Jump ahead if not.
CALL L0AC5 ; Get the current character from the PLAY string, and advance the position pointer.
CALL L0B1D ; Get following numeric value from the string into BC.
LD A,C ; Place the value into A.
CP $0A ; Is it below 10?
JR C,L0D5F ; Jump ahead for 1 to 9 (semiquaver ... semibreve).
;A triplet note was found as part of a tied note
PUSH HL ; HL=Address of the duration length within the channel data block.
PUSH DE ; DE=First tied note duration length.
CALL L0E00 ; DE=Note duration length for this new duration value.
POP HL ; HL=Current tied note duration length.
ADD HL,DE ; HL=Current+new tied note duration lengths.
LD C,E ;
LD B,D ; BC=Note duration length for the duration value.
EX DE,HL ; DE=Current+new tied note duration lengths.
POP HL ; HL=Address of the duration length within the channel data block.
LD (HL),E ;
INC HL ;
LD (HL),D ; Store the combined note duration length in the channel data block.
LD E,C ;
LD D,B ; DE=Note duration length for the second duration value.
JR L0D28 ; Jump back.
;A non-triplet tied note
L0D5F: LD (IX+$05),C ; Store the note duration value.
PUSH HL ; HL=Address of the duration length within the channel data block.
PUSH DE ; DE=First tied note duration length.
CALL L0E00 ; DE=Note duration length for this new duration value.
POP HL ; HL=Current tied note duration length.
ADD HL,DE ; HL=Current+new tied not duration lengths.
EX DE,HL ; DE=Current+new tied not duration lengths.
POP HL ; HL=Address of the duration length within the channel data block.
JP L0D3B ; Jump back to process the next character in case it is also part of a tied note.
;The number found was not part of a tied note, so store the duration value
L0D6E: LD (HL),E ; HL=Address of the duration length within the channel data block.
INC HL ; (For triplet notes this could be the address of the subsequent note duration length)
LD (HL),D ; Store the duration length.
JP L0D9C ; Jump forward to make a return.
; This subroutine is called to increment the tied notes counter
L0D74: LD A,(IX+$21) ; Increment counter of tied notes.
INC A ;
CP $0B ; Has it reached 11?
JP Z,L0F3A ; Jump if so to produce to error report "o too many tied notes".
LD (IX+$21),A ; Store the new tied notes counter.
RET ;
;The character is not a number digit so is 'A'..'G', '&' or '_'
L0D81: CALL L0EC8 ; Get the previous character from the string.
LD (IX+$21),$01 ; Set the number of tied notes to 1.
;Store a pointer to the channel data block's duration length into the command data block
CALL L0DAC ; HL=Address of the duration length within the channel data block.
CALL L0DB4 ; Store address of duration length in command data block's channel duration length pointer table.
LD C,(IX+$05) ; C=The duration value of the note (1 to 9).
PUSH HL ; [Not necessary]
CALL L0E00 ; Find the duration length for the note duration value.
POP HL ; [Not necessary]
LD (HL),E ; Store it in the channel data block.
INC HL ;
LD (HL),D ;
JP L0D9C ; Jump to the instruction below. [Redundant instruction]
L0D9C: POP HL ;
INC HL ;
INC HL ; Modify the return address to point to the RET instruction at $0B83 (ROM 0).
PUSH HL ;
RET ; [Over elaborate when a simple POP followed by RET would have sufficed, saving 3 bytes]
; -------------------
; End of String Found
; -------------------
;This routine is called when the end of string is found within a comment. It marks the
;string as having been processed and then returns to the main loop to process the next string.
L0DA1: POP HL ; Drop the return address of the call to the comment command.
;Enter here if the end of the string is found whilst processing a string.
L0DA2: LD A,(IY+$21) ; Fetch the channel selector.
OR (IY+$10) ; Clear the channel flag for this string.
LD (IY+$10),A ; Store the new channel bitmap.
RET ;
; --------------------------------------------------
; Point to Duration Length within Channel Data Block
; --------------------------------------------------
; Entry: IX=Address of the channel data block.
; Exit : HL=Address of the duration length within the channel data block.
L0DAC: PUSH IX ;
POP HL ; HL=Address of the channel data block.
LD BC,$0022 ;
ADD HL,BC ; HL=Address of the store for the duration length.
RET ;
; -------------------------------------------------------------------------
; Store Entry in Command Data Block's Channel Duration Length Pointer Table
; -------------------------------------------------------------------------
; Entry: IY=Address of the command data block.
; IX=Address of the channel data block for the current string.
; HL=Address of the duration length store within the channel data block.
; Exit : HL=Address of the duration length store within the channel data block.
; DE=Channel duration.
L0DB4: PUSH HL ; Save the address of the duration length within the channel data block.
PUSH IY ;
POP HL ; HL=Address of the command data block.
LD BC,$0011 ;
ADD HL,BC ; HL=Address within the command data block of the channel duration length pointer table.
LD B,$00 ;
LD C,(IX+$02) ; BC=Channel number.
SLA C ; BC=2*Index number.
ADD HL,BC ; HL=Address within the command data block of the pointer to the current channel's data block duration length.
POP DE ; DE=Address of the duration length within the channel data block.
LD (HL),E ; Store the pointer to the channel duration length in the command data block's channel duration pointer table.
INC HL ;
LD (HL),D ;
EX DE,HL ;
RET ;
; -----------------------
; PLAY Command Jump Table
; -----------------------
; Handler routine jump table for all PLAY commands.
L0DCA: DEFW L0CFB ; Command handler routine for all other characters.
DEFW L0B85 ; '!' command handler routine.
DEFW L0B90 ; 'O' command handler routine.
DEFW L0BA5 ; 'N' command handler routine.
DEFW L0BA6 ; '(' command handler routine.
DEFW L0BC2 ; ')' command handler routine.
DEFW L0C32 ; 'T' command handler routine.
DEFW L0C84 ; 'M' command handler routine.
DEFW L0C95 ; 'V' command handler routine.
DEFW L0CAD ; 'U' command handler routine.
DEFW L0CBA ; 'W' command handler routine.
DEFW L0CCE ; 'X' command handler routine.
DEFW L0CDD ; 'Y' command handler routine.
DEFW L0CEE ; 'Z' command handler routine.
DEFW L0CF6 ; 'H' command handler routine.
; ------------------------------
; Envelope Waveform Lookup Table
; ------------------------------
; Table used by the play 'W' command to find the corresponding envelope value
; to write to the sound generator envelope shape register (register 13). This
; filters out the two duplicate waveforms possible from the sound generator and
; allows the order of the waveforms to be arranged in a more logical fashion.
L0DE8: DEFB $00 ; W0 - Single decay then off. (Continue off, attack off, alternate off, hold off)
DEFB $04 ; W1 - Single attack then off. (Continue off, attack on, alternate off, hold off)
DEFB $0B ; W2 - Single decay then hold. (Continue on, attack off, alternate on, hold on)
DEFB $0D ; W3 - Single attack then hold. (Continue on, attack on, alternate off, hold on)
DEFB $08 ; W4 - Repeated decay. (Continue on, attack off, alternate off, hold off)
DEFB $0C ; W5 - Repeated attack. (Continue on, attack on, alternate off, hold off)
DEFB $0E ; W6 - Repeated attack-decay. (Continue on, attack on, alternate on, hold off)
DEFB $0A ; W7 - Repeated decay-attack. (Continue on, attack off, alternate on, hold off)
; --------------------------
; Identify Command Character
; --------------------------
; This routines attempts to match the command character to those in a table.
; The index position of the match indicates which command handler routine is required
; to process the character. Note that commands are case sensitive.
; Entry: A=Command character.
; Exit : Zero flag set if a match was found.
; BC=Indentifying the character matched, 1 to 15 for match and 0 for no match.
L0DF0: LD BC,$000F ; Number of characters + 1 in command table.
LD HL,L0AB7 ; Start of command table.
CPIR ; Search for a match.
RET ;
; ---------------
; Semitones Table
; ---------------
; This table contains an entry for each note of the scale, A to G,
; and is the number of semitones above the note C.
L0DF9: DEFB $09 ; 'A'
DEFB $0B ; 'B'
DEFB $00 ; 'C'
DEFB $02 ; 'D'
DEFB $04 ; 'E'
DEFB $05 ; 'F'
DEFB $07 ; 'G'
; -------------------------
; Find Note Duration Length
; -------------------------
; Entry: C=Duration value (0 to 12, although a value of 0 is never used).
; Exit : DE=Note duration length.
L0E00: PUSH HL ; Save HL.
LD B,$00 ;
LD HL,L0E0C ; Note duration table.
ADD HL,BC ; Index into the table.
LD D,$00 ;
LD E,(HL) ; Fetch the length from the table.
POP HL ; Restore HL.
RET ;
; -------------------
; Note Duration Table
; -------------------
; A whole note is given by a value of 96d and other notes defined in relation to this.
; The value of 96d is the lowest common denominator from which all note durations
; can be defined.
L0E0C: DEFB $80 ; Rest [Not used since table is always indexed into with a value of 1 or more]
DEFB $06 ; Semi-quaver (sixteenth note).
DEFB $09 ; Dotted semi-quaver (3/32th note).
DEFB $0C ; Quaver (eighth note).
DEFB $12 ; Dotted quaver (3/16th note).
DEFB $18 ; Crotchet (quarter note).
DEFB $24 ; Dotted crotchet (3/8th note).
DEFB $30 ; Minim (half note).
DEFB $48 ; Dotted minim (3/4th note).
DEFB $60 ; Semi-breve (whole note).
DEFB $04 ; Triplet semi-quaver (1/24th note).
DEFB $08 ; Triplet quaver (1/12th note).
DEFB $10 ; Triplet crochet (1/6th note).
; -----------------
; Is Numeric Digit?
; -----------------
; Tests whether a character is a number digit.
; Entry: A=Character.
; Exit : Carry flag reset if a number digit.
L0E19: CP '0' ; $30. Is it '0' or less?
RET C ; Return with carry flag set if so.
CP ':' ; $3A. Is it more than '9'?
CCF ;
RET ; Return with carry flag set if so.
; -----------------------------------
; Play a Note On a Sound Chip Channel
; -----------------------------------
; This routine plays the note at the current octave and current volume on a sound chip channel. For play strings 4 to 8,
; it simply stores the note number and this is subsequently played later.
; Entry: IX=Address of the channel data block.
; A=Note value as number of semitones above C (0..11).
L0E20: LD C,A ; C=The note value.
LD A,(IX+$03) ; Octave number * 12.
ADD A,C ; Add the octave number and the note value to form the note number.
CP $80 ; Is note within range?
JP NC,L0F32 ; Jump if not to produce error report "m Note out of range".
LD C,A ; C=Note number.
LD A,(IX+$02) ; Get the channel number.
OR A ; Is it the first channel?
JR NZ,L0E3F ; Jump ahead if not.
;Only set the noise generator frequency on the first channel
LD A,C ; A=Note number (0..107), in ascending audio frequency.
CPL ; Invert since noise register value is in descending audio frequency.
AND $7F ; Mask off bit 7.
SRL A ;
SRL A ; Divide by 4 to reduce range to 0..31.
LD D,$06 ; Register 6 - Noise pitch.
LD E,A ;
CALL L0E7C ; Write to sound generator register.
L0E3F: LD (IX+$00),C ; Store the note number.
LD A,(IX+$02) ; Get the channel number.
CP $03 ; Is it channel 0, 1 or 2, i.e. a sound chip channel?
RET NC ; Do not output anything for play strings 4 to 8.
;Channel 0, 1 or 2
LD HL,L1096 ; Start of note lookup table.
LD B,$00 ; BC=Note number.
LD A,C ; A=Note number.
SUB $15 ; A=Note number - 21.
JR NC,L0E57 ; Jump if note number was 21 or above.
LD DE,$0FBF ; Note numbers $00 to $14 use the lowest note value.
JR L0E5E ; [Could have saved 4 bytes by using XOR A and dropping through to L0E57 (ROM 0)]
;Note number 21 to 107 (range 0 to 86)
L0E57: LD C,A ;
SLA C ; Generate offset into the table.
ADD HL,BC ; Point to the entry in the table.
LD E,(HL) ;
INC HL ;
LD D,(HL) ; DE=Word to write to the sound chip registers to produce this note.
L0E5E: EX DE,HL ; HL=Register word value to produce the note.
LD D,(IX+$02) ; Get the channel number.
SLA D ; D=2*Channel number, to give the tone channel register (fine control) number 0, 2, or 4.
LD E,L ; E=The low value byte.
CALL L0E7C ; Write to sound generator register.
INC D ; D=Tone channel register (coarse control) number 1, 3, or 5.
LD E,H ; E=The high value byte.
CALL L0E7C ; Write to sound generator register.
BIT 4,(IX+$04) ; Is the envelope waveform being used?
RET Z ; Return if it is not.
LD D,$0D ; Register 13 - Envelope Shape.
LD A,(IY+$29) ; Get the effect waveform value.
LD E,A ;
CALL L0E7C ; Write to sound generator register.
RET ; [Could have saved 4 bytes by dropping down into the routine below.]
; ----------------------------
; Set Sound Generator Register
; ----------------------------
; Entry: D=Register to write.
; E=Value to set register to.
L0E7C: PUSH BC ;
LD BC,$FFFD ;
OUT (C),D ; Select the register.
LD BC,$BFFD ;
OUT (C),E ; Write out the value.
POP BC ;
RET ;
; -----------------------------
; Read Sound Generator Register
; -----------------------------
; Entry: A=Register to read.
; Exit : A=Value of currently selected sound generator register.
L0E89: PUSH BC ;
LD BC,$FFFD ;
OUT (C),A ; Select the register.
IN A,(C) ; Read the register's value.
POP BC ;
RET ;
; ------------------
; Turn Off All Sound
; ------------------
L0E93: LD D,$07 ; Register 7 - Mixer.
LD E,$FF ; I/O ports are inputs, noise output off, tone output off.
CALL L0E7C ; Write to sound generator register.
;Turn off the sound from the AY-3-8912
LD D,$08 ; Register 8 - Channel A volume.
LD E,$00 ; Volume of 0.
CALL L0E7C ; Write to sound generator register to set the volume to 0.
INC D ; Register 9 - Channel B volume.
CALL L0E7C ; Write to sound generator register to set the volume to 0.
INC D ; Register 10 - Channel C volume.
CALL L0E7C ; Write to sound generator register to set the volume to 0.
CALL L0A4F ; Select channel data block pointers.
;Now reset all MIDI channels in use
L0EAC: RR (IY+$22) ; Working copy of channel bitmap. Test if next string present.
JR C,L0EB8 ; Jump ahead if there is no string for this channel.
CALL L0A67 ; Get address of channel data block for the current string into IX.
CALL L118D ; Turn off the MIDI channel sound assigned to this play string.
L0EB8: SLA (IY+$21) ; Have all channels been processed?
JR C,L0EC3 ; Jump ahead if so.
CALL L0A6E ; Advance to the next channel data block pointer.
JR L0EAC ; Jump back to process the next channel.
L0EC3: LD IY,$5C3A ; Restore IY.
RET ;
; ---------------------------------------
; Get Previous Character from Play String
; ---------------------------------------
; Get the previous character from the PLAY string, skipping over spaces and 'Enter' characters.
; Entry: IX=Address of the channel data block.
L0EC8: PUSH HL ; Save registers.
PUSH DE ;
LD L,(IX+$06) ; Get the current pointer into the PLAY string.
LD H,(IX+$07) ;
L0ED0: DEC HL ; Point to previous character.
LD A,(HL) ; Fetch the character.
CP ' ' ; $20. Is it a space?
JR Z,L0ED0 ; Jump back if a space.
CP $0D ; Is it an 'Enter'?
JR Z,L0ED0 ; Jump back if an 'Enter'.
LD (IX+$06),L ; Store this as the new current pointer into the PLAY string.
LD (IX+$07),H ;
POP DE ; Restore registers.
POP HL ;
RET ;
; --------------------------------------
; Get Current Character from Play String
; --------------------------------------
; Get the current character from the PLAY string, skipping over spaces and 'Enter' characters.
; Exit: Carry flag set if string has been fully processed.
; Carry flag reset if character is available.
; A=Character available.
L0EE3: PUSH HL ; Save registers.
PUSH DE ;
PUSH BC ;
LD L,(IX+$06) ; HL=Pointer to next character to process within the PLAY string.
LD H,(IX+$07) ;
L0EEC: LD A,H ;
CP (IX+$09) ; Reached end-of-string address high byte?
JR NZ,L0EFB ; Jump forward if not.
LD A,L ;
CP (IX+$08) ; Reached end-of-string address low byte?
JR NZ,L0EFB ; Jump forward if not.
SCF ; Indicate string all processed.
JR L0F05 ; Jump forward to return.
L0EFB: LD A,(HL) ; Get the next play character.
CP ' ' ; $20. Is it a space?
JR Z,L0F09 ; Ignore the space by jumping ahead to process the next character.
CP $0D ; Is it 'Enter'?
JR Z,L0F09 ; Ignore the 'Enter' by jumping ahead to process the next character.
OR A ; Clear the carry flag to indicate a new character has been returned.
L0F05: POP BC ; Restore registers.
POP DE ;
POP HL ;
RET ;
L0F09: INC HL ; Point to the next character.
LD (IX+$06),L ;
LD (IX+$07),H ; Update the pointer to the next character to process with the PLAY string.
JR L0EEC ; Jump back to get the next character.
; --------------------------
; Produce Play Error Reports
; --------------------------
L0F12: CALL L0E93 ; Turn off all sound and restore IY.
EI ;
CALL L05AC ; Produce error report.
DEFB $29 ; "n Out of range"
L0F1A: CALL L0E93 ; Turn off all sound and restore IY.
EI ;
CALL L05AC ; Produce error report.
DEFB $27 ; "l Number too big"
L0F22: CALL L0E93 ; Turn off all sound and restore IY.
EI ;
CALL L05AC ; Produce error report.
DEFB $26 ; "k Invalid note name"
L0F2A: CALL L0E93 ; Turn off all sound and restore IY.
EI ;
CALL L05AC ; Produce error report.
DEFB $1F ; "d Too many brackets"
L0F32: CALL L0E93 ; Turn off all sound and restore IY.
EI ;
CALL L05AC ; Produce error report.
DEFB $28 ; "m Note out of range"
L0F3A: CALL L0E93 ; Turn off all sound and restore IY.
EI ;
CALL L05AC ; Produce error report.
DEFB $2A ; "o Too many tied notes"
; -------------------------
; Play Note on Each Channel
; -------------------------
; Play a note and set the volume on each channel for which a play string exists.
L0F42: CALL L0A4F ; Select channel data block pointers.
L0F45: RR (IY+$22) ; Working copy of channel bitmap. Test if next string present.
JR C,L0F6C ; Jump ahead if there is no string for this channel.
CALL L0A67 ; Get address of channel data block for the current string into IX.
CALL L0AD1 ; Get the next note in the string as number of semitones above note C.
CP $80 ; Is it a rest?
JR Z,L0F6C ; Jump ahead if so and do nothing to the channel.
CALL L0E20 ; Play the note if a sound chip channel.
LD A,(IX+$02) ; Get channel number.
CP $03 ; Is it channel 0, 1 or 2, i.e. a sound chip channel?
JR NC,L0F69 ; Jump if not to skip setting the volume.
;One of the 3 sound chip generator channels so set the channel's volume for the new note
LD D,$08 ;
ADD A,D ; A=0 to 2.
LD D,A ; D=Register (8 + string index), i.e. channel A, B or C volume register.
LD E,(IX+$04) ; E=Volume for the current channel.
CALL L0E7C ; Write to sound generator register to set the output volume.
L0F69: CALL L116E ; Play a note and set the volume on the assigned MIDI channel.
L0F6C: SLA (IY+$21) ; Have all channels been processed?
RET C ; Return if so.
CALL L0A6E ; Advance to the next channel data block pointer.
JR L0F45 ; Jump back to process the next channel.
; ------------------
; Wait Note Duration
; ------------------
; This routine is the main timing control of the PLAY command.
; It waits for the specified length of time, which will be the
; lowest note duration of all active channels.
; The actual duration of the wait is dictated by the current tempo.
; Entry: DE=Note duration, where 96d represents a whole note.
;Enter a loop waiting for (135+ ((26*(tempo-100))-5) )*DE+5 T-states
L0F76: PUSH HL ; (11) Save HL.
LD L,(IY+$27) ; (19) Get the tempo timing value.
LD H,(IY+$28) ; (19)
LD BC,$0064 ; (10) BC=100
OR A ; (4)
SBC HL,BC ; (15) HL=tempo timing value - 100.
PUSH HL ; (11)
POP BC ; (10) BC=tempo timing value - 100.
POP HL ; (10) Restore HL.
;Tempo timing value = (10/(TEMPO*4))/7.33e-6, where 7.33e-6 is the time for 26 T-states.
;The loop below takes 26 T-states per iteration, where the number of iterations is given by the tempo timing value.
;So the time for the loop to execute is 2.5/TEMPO seconds.
;
;For a TEMPO of 60 beats (crotchets) per second, the time per crotchet is 1/24 second.
;The duration of a crotchet is defined as 24 from the table at $0E0C, therefore the loop will get executed 24 times
;and hence the total time taken will be 1 second.
;
;The tempo timing value above has 100 subtracted from it, presumably to approximately compensate for the overhead time
;previously taken to prepare the notes for playing. This reduces the total time by 2600 T-states, or 733us.
L0F86: DEC BC ; (6) Wait for tempo-100 loops.
LD A,B ; (4)
OR C ; (4)
JR NZ,L0F86 ; (12/7)
DEC DE ; (6) Repeat DE times
LD A,D ; (4)
OR E ; (4)
JR NZ,L0F76 ; (12/7)
RET ; (10)
; -----------------------------
; Find Smallest Duration Length
; -----------------------------
; This routine finds the smallest duration length for all current notes
; being played across all channels.
; Exit: DE=Smallest duration length.
L0F91: LD DE,$FFFF ; Set smallest duration length to 'maximum'.
CALL L0A4A ; Select channel data block duration pointers.
L0F97: RR (IY+$22) ; Working copy of channel bitmap. Test if next string present.
JR C,L0FAF ; Jump ahead if there is no string for this channel.
;HL=Address of channel data pointer. DE holds the smallest duration length found so far.
PUSH DE ; Save the smallest duration length.
LD E,(HL) ;
INC HL ;
LD D,(HL) ;
EX DE,HL ; DE=Channel data block duration length.
LD E,(HL) ;
INC HL ;
LD D,(HL) ; DE=Channel duration length.
PUSH DE ;
POP HL ; HL=Channel duration length.
POP BC ; Last channel duration length.
OR A ;
SBC HL,BC ; Is current channel's duration length smaller than the smallest so far?
JR C,L0FAF ; Jump ahead if so, with the new smallest value in DE.
;The current channel's duration was not smaller so restore the last smallest into DE.
PUSH BC ;
POP DE ; DE=Smallest duration length.
L0FAF: SLA (IY+$21) ; Have all channel strings been processed?
JR C,L0FBA ; Jump ahead if so.
CALL L0A6E ; Advance to the next channel data block duration pointer.
JR L0F97 ; Jump back to process the next channel.
L0FBA: LD (IY+$25),E ;
LD (IY+$26),D ; Store the smallest channel duration length.
RET ;
; ---------------------------------------------------------------
; Play a Note on Each Channel and Update Channel Duration Lengths
; ---------------------------------------------------------------
; This routine is used to play a note and set the volume on all channels.
; It subtracts an amount of time from the duration lengths of all currently
; playing channel note durations. The amount subtracted is equivalent to the
; smallest note duration length currently being played, and as determined earlier.
; Hence one channel's duration will go to 0 on each call of this routine, and the
; others will show the remaining lengths of their corresponding notes.
; Entry: IY=Address of the command data block.
L0FC1: XOR A ;
LD (IY+$2A),A ; Holds a temporary channel bitmap.
CALL L0A4F ; Select channel data block pointers.
L0FC8: RR (IY+$22) ; Working copy of channel bitmap. Test if next string present.
JP C,L105A ; Jump ahead if there is no string for this channel.
CALL L0A67 ; Get address of channel data block for the current string into IX.
PUSH IY ;
POP HL ; HL=Address of the command data block.
LD BC,$0011 ;
ADD HL,BC ; HL=Address of channel data block duration pointers.
LD B,$00 ;
LD C,(IX+$02) ; BC=Channel number.
SLA C ; BC=2*Channel number.
ADD HL,BC ; HL=Address of channel data block duration pointer for this channel.
LD E,(HL) ;
INC HL ;
LD D,(HL) ; DE=Address of duration length within the channel data block.
EX DE,HL ; HL=Address of duration length within the channel data block.
PUSH HL ; Save it.
LD E,(HL) ;
INC HL ;
LD D,(HL) ; DE=Duration length for this channel.
EX DE,HL ; HL=Duration length for this channel.
LD E,(IY+$25) ;
LD D,(IY+$26) ; DE=Smallest duration length of all current channel notes.
OR A ;
SBC HL,DE ; HL=Duration length - smallest duration length.
EX DE,HL ; DE=Duration length - smallest duration length.
POP HL ; HL=Address of duration length within the channel data block.
JR Z,L0FFC ; Jump if this channel uses the smallest found duration length.
LD (HL),E ;
INC HL ; Update the duration length for this channel with the remaining length.
LD (HL),D ;
JR L105A ; Jump ahead to update the next channel.
;The current channel uses the smallest found duration length
; [A note has been completed and so the channel volume is set to 0 prior to the next note being played.
; This occurs on both sound chip channels and MIDI channels. When a MIDI channel is assigned to more than
; one play string and a rest is used in one of those strings. As soon as the end of the rest period is
; encountered, the channel's volume is set to off even though one of the other play strings controlling
; the MIDI channel may still be playing. This can be seen using the command PLAY "Y1a&", "Y1N9a". Here,
; string 1 starts playing 'a' for the period of a crotchet (1/4 of a note), where as string 2 starts playing
; 'a' for nine periods of a crotchet (9/4 of a note). When string 1 completes its crotchet, it requests
; to play a period of silence via the rest '&'. This turns the volume of the MIDI channel off even though
; string 2 is still timing its way through its nine crotchets. The play command will therefore continue for
; a further seven crotchets but in silence. This is because the volume for note is set only at its start
; and no coordination occurs between strings to turn the volume back on for the second string. It is arguably
; what the correct behaviour should be in such a circumstance where the strings are providing conflicting instructions,
; but having the latest command or note take precedence seems a logical approach. Credit: Ian Collier (+3), Paul Farrow (128)]
L0FFC: LD A,(IX+$02) ; Get the channel number.
CP $03 ; Is it channel 0, 1 or 2, i.e. a sound chip channel?
JR NC,L100C ; Jump ahead if not a sound generator channel.
LD D,$08 ;
ADD A,D ;
LD D,A ; D=Register (8+channel number) - Channel volume.
LD E,$00 ; E=Volume level of 0.
CALL L0E7C ; Write to sound generator register to turn the volume off.
L100C: CALL L118D ; Turn off the assigned MIDI channel sound.
PUSH IX ;
POP HL ; HL=Address of channel data block.
LD BC,$0021 ;
ADD HL,BC ; HL=Points to the tied notes counter.
DEC (HL) ; Decrement the tied notes counter. [This contains a value of 1 for a single note]
JR NZ,L1026 ; Jump ahead if there are more tied notes.
CALL L0B5C ; Find the next note to play for this channel from its play string.
LD A,(IY+$21) ; Fetch the channel selector.
AND (IY+$10) ; Test whether this channel has further data in its play string.
JR NZ,L105A ; Jump to process the next channel if this channel does not have a play string.
JR L103D ; The channel has more data in its play string so jump ahead.
;The channel has more tied notes
L1026: PUSH IY ;
POP HL ; HL=Address of the command data block.
LD BC,$0011 ;
ADD HL,BC ; HL=Address of channel data block duration pointers.
LD B,$00 ;
LD C,(IX+$02) ; BC=Channel number.
SLA C ; BC=2*Channel number.
ADD HL,BC ; HL=Address of channel data block duration pointer for this channel.
LD E,(HL) ;
INC HL ;
LD D,(HL) ; DE=Address of duration length within the channel data block.
INC DE ;
INC DE ; Point to the subsequent note duration length.
LD (HL),D ;
DEC HL ;
LD (HL),E ; Store the new duration length.
L103D: CALL L0AD1 ; Get next note in the string as number of semitones above note C.
LD C,A ; C=Number of semitones.
LD A,(IY+$21) ; Fetch the channel selector.
AND (IY+$10) ; Test whether this channel has a play string.
JR NZ,L105A ; Jump to process the next channel if this channel does not have a play string.
LD A,C ; A=Number of semitones.
CP $80 ; Is it a rest?
JR Z,L105A ; Jump to process the next channel if it is.
CALL L0E20 ; Play the new note on this channel at the current volume if a sound chip channel, or simply store the note for play strings 4 to 8.
LD A,(IY+$21) ; Fetch the channel selector.
OR (IY+$2A) ; Insert a bit in the temporary channel bitmap to indicate this channel has more to play.
LD (IY+$2A),A ; Store it.
;Check whether another channel needs its duration length updated
L105A: SLA (IY+$21) ; Have all channel strings been processed?
JR C,L1066 ; Jump ahead if so.
CALL L0A6E ; Advance to the next channel data pointer.
JP L0FC8 ; Jump back to update the duration length for the next channel.
; [*BUG* - By this point, the volume for both sound chip and MIDI channels has been set to 0, i.e. off. So although the new notes have been
; set playing on the sound chip channels, no sound is audible. For MIDI channels, no new notes have yet been output and hence these
; are also silent. If the time from turning the volume off for the current note to the time to turn the volume on for the next note
; is short enough, then it will not be noticeable. However, the code at $1066 (ROM 0) introduces a 1/96th of a note delay and as a result a
; 1/96th of a note period of silence between notes. The bug can be resolved by simply deleting the two instructions below that introduce
; the delay. A positive side effect of the bug in the 'V' volume command at $0C95 (ROM 0) is that it can be used to overcome the gaps of silence
; between notes for sound chip channels. By interspersing volume commands between notes, a new volume level is immediately set before
; the 1/96th of a note delay is introduced for the new note. Therefore, the delay occurs when the new note is audible instead of when it
; is silent. For example, PLAY "cV15cV15c" instead of PLAY "ccc". The note durations are still 1/96th of a note longer than they should
; be though. This technique will only work on the sound chip channels and not for any MIDI channels. Credit: Ian Collier (+3), Paul Farrow (128)]
L1066: LD DE,$0001 ; Delay for 1/96th of a note.
CALL L0F76 ;
CALL L0A4F ; Select channel data block pointers.
;All channel durations have been updated. Update the volume on each sound chip channel, and the volume and note on each MIDI channel
L106F: RR (IY+$2A) ; Temporary channel bitmap. Test if next string present.
JR NC,L108C ; Jump ahead if there is no string for this channel.
CALL L0A67 ; Get address of channel data block for the current string into IX.
LD A,(IX+$02) ; Get the channel number.
CP $03 ; Is it channel 0, 1 or 2, i.e. a sound chip channel?
JR NC,L1089 ; Jump ahead if so to process the next channel.
LD D,$08 ;
ADD A,D ;
LD D,A ; D=Register (8+channel number) - Channel volume.
LD E,(IX+$04) ; Get the current volume.
CALL L0E7C ; Write to sound generator register to set the volume of the channel.
L1089: CALL L116E ; Play a note and set the volume on the assigned MIDI channel.
L108C: SLA (IY+$21) ; Have all channels been processed?
RET C ; Return if so.
CALL L0A6E ; Advance to the next channel data pointer.
JR L106F ; Jump back to process the next channel.
; -----------------
; Note Lookup Table
; -----------------
; Each word gives the value of the sound generator tone registers for a given note.
; There are 9 octaves, containing a total of 108 notes. These represent notes 21 to
; 128. Notes 0 to 20 cannot be reproduced on the sound chip and so note 21 will be
; used for all of these (they will however be sent to a MIDI device if one is assigned
; to a channel). [Note that both the sound chip and the MIDI port can not play note 128
; and so its inclusion in the table is a waste of 2 bytes]. The PLAY command does not allow
; octaves higher than 8 to be selected directly. Using PLAY "O8G" will select note 115. To
; select higher notes, sharps must be included, e.g. PLAY "O8#G" for note 116, PLAY "O8##G"
; for note 117, etc, up to PLAY "O8############G" for note 127. Attempting to access note
; 128 using PLAY "O8#############G" will lead to error report "m Note out of range".
L1096: DEFW $0FBF ; Octave 1, Note 21 - A (27.50 Hz, Ideal=27.50 Hz, Error=-0.01%) C0
DEFW $0EDC ; Octave 1, Note 22 - A# (29.14 Hz, Ideal=29.16 Hz, Error=-0.08%)
DEFW $0E07 ; Octave 1, Note 23 - B (30.87 Hz, Ideal=30.87 Hz, Error=-0.00%)
DEFW $0D3D ; Octave 2, Note 24 - C (32.71 Hz, Ideal=32.70 Hz, Error=+0.01%) C1
DEFW $0C7F ; Octave 2, Note 25 - C# (34.65 Hz, Ideal=34.65 Hz, Error=-0.00%)
DEFW $0BCC ; Octave 2, Note 26 - D (36.70 Hz, Ideal=36.71 Hz, Error=-0.01%)
DEFW $0B22 ; Octave 2, Note 27 - D# (38.89 Hz, Ideal=38.89 Hz, Error=+0.01%)
DEFW $0A82 ; Octave 2, Note 28 - E (41.20 Hz, Ideal=41.20 Hz, Error=+0.00%)
DEFW $09EB ; Octave 2, Note 29 - F (43.66 Hz, Ideal=43.65 Hz, Error=+0.00%)
DEFW $095D ; Octave 2, Note 30 - F# (46.24 Hz, Ideal=46.25 Hz, Error=-0.02%)
DEFW $08D6 ; Octave 2, Note 31 - G (49.00 Hz, Ideal=49.00 Hz, Error=+0.00%)
DEFW $0857 ; Octave 2, Note 32 - G# (51.92 Hz, Ideal=51.91 Hz, Error=+0.01%)
DEFW $07DF ; Octave 2, Note 33 - A (55.01 Hz, Ideal=55.00 Hz, Error=+0.01%)
DEFW $076E ; Octave 2, Note 34 - A# (58.28 Hz, Ideal=58.33 Hz, Error=-0.08%)
DEFW $0703 ; Octave 2, Note 35 - B (61.75 Hz, Ideal=61.74 Hz, Error=+0.02%)
DEFW $069F ; Octave 3, Note 36 - C ( 65.39 Hz, Ideal= 65.41 Hz, Error=-0.02%) C2
DEFW $0640 ; Octave 3, Note 37 - C# ( 69.28 Hz, Ideal= 69.30 Hz, Error=-0.04%)
DEFW $05E6 ; Octave 3, Note 38 - D ( 73.40 Hz, Ideal= 73.42 Hz, Error=-0.01%)
DEFW $0591 ; Octave 3, Note 39 - D# ( 77.78 Hz, Ideal= 77.78 Hz, Error=+0.01%)
DEFW $0541 ; Octave 3, Note 40 - E ( 82.41 Hz, Ideal= 82.41 Hz, Error=+0.00%)
DEFW $04F6 ; Octave 3, Note 41 - F ( 87.28 Hz, Ideal= 87.31 Hz, Error=-0.04%)
DEFW $04AE ; Octave 3, Note 42 - F# ( 92.52 Hz, Ideal= 92.50 Hz, Error=+0.02%)
DEFW $046B ; Octave 3, Note 43 - G ( 98.00 Hz, Ideal= 98.00 Hz, Error=+0.00%)
DEFW $042C ; Octave 3, Note 44 - G# (103.78 Hz, Ideal=103.83 Hz, Error=-0.04%)
DEFW $03F0 ; Octave 3, Note 45 - A (109.96 Hz, Ideal=110.00 Hz, Error=-0.04%)
DEFW $03B7 ; Octave 3, Note 46 - A# (116.55 Hz, Ideal=116.65 Hz, Error=-0.08%)
DEFW $0382 ; Octave 3, Note 47 - B (123.43 Hz, Ideal=123.47 Hz, Error=-0.03%)
DEFW $034F ; Octave 4, Note 48 - C (130.86 Hz, Ideal=130.82 Hz, Error=+0.04%) C3
DEFW $0320 ; Octave 4, Note 49 - C# (138.55 Hz, Ideal=138.60 Hz, Error=-0.04%)
DEFW $02F3 ; Octave 4, Note 50 - D (146.81 Hz, Ideal=146.83 Hz, Error=-0.01%)
DEFW $02C8 ; Octave 4, Note 51 - D# (155.68 Hz, Ideal=155.55 Hz, Error=+0.08%)
DEFW $02A1 ; Octave 4, Note 52 - E (164.70 Hz, Ideal=164.82 Hz, Error=-0.07%)
DEFW $027B ; Octave 4, Note 53 - F (174.55 Hz, Ideal=174.62 Hz, Error=-0.04%)
DEFW $0257 ; Octave 4, Note 54 - F# (185.04 Hz, Ideal=185.00 Hz, Error=+0.02%)
DEFW $0236 ; Octave 4, Note 55 - G (195.83 Hz, Ideal=196.00 Hz, Error=-0.09%)
DEFW $0216 ; Octave 4, Note 56 - G# (207.57 Hz, Ideal=207.65 Hz, Error=-0.04%)
DEFW $01F8 ; Octave 4, Note 57 - A (219.92 Hz, Ideal=220.00 Hz, Error=-0.04%)
DEFW $01DC ; Octave 4, Note 58 - A# (232.86 Hz, Ideal=233.30 Hz, Error=-0.19%)
DEFW $01C1 ; Octave 4, Note 59 - B (246.86 Hz, Ideal=246.94 Hz, Error=-0.03%)
DEFW $01A8 ; Octave 5, Note 60 - C (261.42 Hz, Ideal=261.63 Hz, Error=-0.08%) C4 Middle C
DEFW $0190 ; Octave 5, Note 61 - C# (277.10 Hz, Ideal=277.20 Hz, Error=-0.04%)
DEFW $0179 ; Octave 5, Note 62 - D (294.01 Hz, Ideal=293.66 Hz, Error=+0.12%)
DEFW $0164 ; Octave 5, Note 63 - D# (311.35 Hz, Ideal=311.10 Hz, Error=+0.08%)
DEFW $0150 ; Octave 5, Note 64 - E (329.88 Hz, Ideal=329.63 Hz, Error=+0.08%)
DEFW $013D ; Octave 5, Note 65 - F (349.65 Hz, Ideal=349.23 Hz, Error=+0.12%)
DEFW $012C ; Octave 5, Note 66 - F# (369.47 Hz, Ideal=370.00 Hz, Error=-0.14%)
DEFW $011B ; Octave 5, Note 67 - G (391.66 Hz, Ideal=392.00 Hz, Error=-0.09%)
DEFW $010B ; Octave 5, Note 68 - G# (415.13 Hz, Ideal=415.30 Hz, Error=-0.04%)
DEFW $00FC ; Octave 5, Note 69 - A (439.84 Hz, Ideal=440.00 Hz, Error=-0.04%)
DEFW $00EE ; Octave 5, Note 70 - A# (465.72 Hz, Ideal=466.60 Hz, Error=-0.19%)
DEFW $00E0 ; Octave 5, Note 71 - B (494.82 Hz, Ideal=493.88 Hz, Error=+0.19%)
DEFW $00D4 ; Octave 6, Note 72 - C (522.83 Hz, Ideal=523.26 Hz, Error=-0.08%) C5
DEFW $00C8 ; Octave 6, Note 73 - C# (554.20 Hz, Ideal=554.40 Hz, Error=-0.04%)
DEFW $00BD ; Octave 6, Note 74 - D (586.46 Hz, Ideal=587.32 Hz, Error=-0.15%)
DEFW $00B2 ; Octave 6, Note 75 - D# (622.70 Hz, Ideal=622.20 Hz, Error=+0.08%)
DEFW $00A8 ; Octave 6, Note 76 - E (659.77 Hz, Ideal=659.26 Hz, Error=+0.08%)
DEFW $009F ; Octave 6, Note 77 - F (697.11 Hz, Ideal=698.46 Hz, Error=-0.19%)
DEFW $0096 ; Octave 6, Note 78 - F# (738.94 Hz, Ideal=740.00 Hz, Error=-0.14%)
DEFW $008D ; Octave 6, Note 79 - G (786.10 Hz, Ideal=784.00 Hz, Error=+0.27%)
DEFW $0085 ; Octave 6, Note 80 - G# (833.39 Hz, Ideal=830.60 Hz, Error=+0.34%)
DEFW $007E ; Octave 6, Note 81 - A (879.69 Hz, Ideal=880.00 Hz, Error=-0.04%)
DEFW $0077 ; Octave 6, Note 82 - A# (931.43 Hz, Ideal=933.20 Hz, Error=-0.19%)
DEFW $0070 ; Octave 6, Note 83 - B (989.65 Hz, Ideal=987.76 Hz, Error=+0.19%)
DEFW $006A ; Octave 7, Note 84 - C (1045.67 Hz, Ideal=1046.52 Hz, Error=-0.08%) C6
DEFW $0064 ; Octave 7, Note 85 - C# (1108.41 Hz, Ideal=1108.80 Hz, Error=-0.04%)
DEFW $005E ; Octave 7, Note 86 - D (1179.16 Hz, Ideal=1174.64 Hz, Error=+0.38%)
DEFW $0059 ; Octave 7, Note 87 - D# (1245.40 Hz, Ideal=1244.40 Hz, Error=+0.08%)
DEFW $0054 ; Octave 7, Note 88 - E (1319.53 Hz, Ideal=1318.52 Hz, Error=+0.08%)
DEFW $004F ; Octave 7, Note 89 - F (1403.05 Hz, Ideal=1396.92 Hz, Error=+0.44%)
DEFW $004B ; Octave 7, Note 90 - F# (1477.88 Hz, Ideal=1480.00 Hz, Error=-0.14%)
DEFW $0047 ; Octave 7, Note 91 - G (1561.14 Hz, Ideal=1568.00 Hz, Error=-0.44%)
DEFW $0043 ; Octave 7, Note 92 - G# (1654.34 Hz, Ideal=1661.20 Hz, Error=-0.41%)
DEFW $003F ; Octave 7, Note 93 - A (1759.38 Hz, Ideal=1760.00 Hz, Error=-0.04%)
DEFW $003B ; Octave 7, Note 94 - A# (1878.65 Hz, Ideal=1866.40 Hz, Error=+0.66%)
DEFW $0038 ; Octave 7, Note 95 - B (1979.30 Hz, Ideal=1975.52 Hz, Error=+0.19%)
DEFW $0035 ; Octave 8, Note 96 - C (2091.33 Hz, Ideal=2093.04 Hz, Error=-0.08%) C7
DEFW $0032 ; Octave 8, Note 97 - C# (2216.81 Hz, Ideal=2217.60 Hz, Error=-0.04%)
DEFW $002F ; Octave 8, Note 98 - D (2358.31 Hz, Ideal=2349.28 Hz, Error=+0.38%)
DEFW $002D ; Octave 8, Note 99 - D# (2463.13 Hz, Ideal=2488.80 Hz, Error=-1.03%)
DEFW $002A ; Octave 8, Note 100 - E (2639.06 Hz, Ideal=2637.04 Hz, Error=+0.08%)
DEFW $0028 ; Octave 8, Note 101 - F (2771.02 Hz, Ideal=2793.84 Hz, Error=-0.82%)
DEFW $0025 ; Octave 8, Note 102 - F# (2995.69 Hz, Ideal=2960.00 Hz, Error=+1.21%)
DEFW $0023 ; Octave 8, Note 103 - G (3166.88 Hz, Ideal=3136.00 Hz, Error=+0.98%)
DEFW $0021 ; Octave 8, Note 104 - G# (3358.81 Hz, Ideal=3322.40 Hz, Error=+1.10%)
DEFW $001F ; Octave 8, Note 105 - A (3575.50 Hz, Ideal=3520.00 Hz, Error=+1.58%)
DEFW $001E ; Octave 8, Note 106 - A# (3694.69 Hz, Ideal=3732.80 Hz, Error=-1.02%)
DEFW $001C ; Octave 8, Note 107 - B (3958.59 Hz, Ideal=3951.04 Hz, Error=+0.19%)
DEFW $001A ; Octave 9, Note 108 - C (4263.10 Hz, Ideal=4186.08 Hz, Error=+1.84%) C8
DEFW $0019 ; Octave 9, Note 109 - C# (4433.63 Hz, Ideal=4435.20 Hz, Error=-0.04%)
DEFW $0018 ; Octave 9, Note 110 - D (4618.36 Hz, Ideal=4698.56 Hz, Error=-1.71%)
DEFW $0016 ; Octave 9, Note 111 - D# (5038.21 Hz, Ideal=4977.60 Hz, Error=+1.22%)
DEFW $0015 ; Octave 9, Note 112 - E (5278.13 Hz, Ideal=5274.08 Hz, Error=+0.08%)
DEFW $0014 ; Octave 9, Note 113 - F (5542.03 Hz, Ideal=5587.68 Hz, Error=-0.82%)
DEFW $0013 ; Octave 9, Note 114 - F# (5833.72 Hz, Ideal=5920.00 Hz, Error=-1.46%)
DEFW $0012 ; Octave 9, Note 115 - G (6157.81 Hz, Ideal=6272.00 Hz, Error=-1.82%)
DEFW $0011 ; Octave 9, Note 116 - G# (6520.04 Hz, Ideal=6644.80 Hz, Error=-1.88%)
DEFW $0010 ; Octave 9, Note 117 - A (6927.54 Hz, Ideal=7040.00 Hz, Error=-1.60%)
DEFW $000F ; Octave 9, Note 118 - A# (7389.38 Hz, Ideal=7465.60 Hz, Error=-1.02%)
DEFW $000E ; Octave 9, Note 119 - B (7917.19 Hz, Ideal=7902.08 Hz, Error=+0.19%)
DEFW $000D ; Octave 10, Note 120 - C ( 8526.20 Hz, Ideal= 8372.16 Hz, Error=+1.84%) C9
DEFW $000C ; Octave 10, Note 121 - C# ( 9236.72 Hz, Ideal= 8870.40 Hz, Error=+4.13%)
DEFW $000C ; Octave 10, Note 122 - D ( 9236.72 Hz, Ideal= 9397.12 Hz, Error=-1.71%)
DEFW $000B ; Octave 10, Note 123 - D# (10076.42 Hz, Ideal= 9955.20 Hz, Error=+1.22%)
DEFW $000B ; Octave 10, Note 124 - E (10076.42 Hz, Ideal=10548.16 Hz, Error=-4.47%)
DEFW $000A ; Octave 10, Note 125 - F (11084.06 Hz, Ideal=11175.36 Hz, Error=-0.82%)
DEFW $0009 ; Octave 10, Note 126 - F# (12315.63 Hz, Ideal=11840.00 Hz, Error=+4.02%)
DEFW $0009 ; Octave 10, Note 127 - G (12315.63 Hz, Ideal=12544.00 Hz, Error=-1.82%)
DEFW $0008 ; Octave 10, Note 128 - G# (13855.08 Hz, Ideal=13289.60 Hz, Error=+4.26%)
; -------------------------
; Play Note on MIDI Channel
; -------------------------
; This routine turns on a note on the MIDI channel and sets its volume, if MIDI channel is assigned to the current string.
; Three bytes are sent, and have the following meaning:
; Byte 1: Channel number $00..$0F, with bits 4 and 7 set.
; Byte 2: Note number $00..$7F.
; Byte 3: Note velocity $00..$78.
; Entry: IX=Address of the channel data block.
L116E: LD A,(IX+$01) ; Is a MIDI channel assigned to this string?
OR A ;
RET M ; Return if not.
;A holds the assigned channel number ($00..$0F)
OR $90 ; Set bits 4 and 7 of the channel number. A=$90..$9F.
CALL L11A3 ; Write byte to MIDI device.
LD A,(IX+$00) ; The note number.
CALL L11A3 ; Write byte to MIDI device.
LD A,(IX+$04) ; Fetch the channel's volume.
RES 4,A ; Ensure the 'using envelope' bit is reset so
SLA A ; that A holds a value between $00 and $0F.
SLA A ; Multiply by 8 to increase the range to $00..$78.
SLA A ; A=Note velocity.
CALL L11A3 ; Write byte to MIDI device.
RET ; [Could have saved 1 byte by using JP L11A3 (ROM 0)]
; ---------------------
; Turn MIDI Channel Off
; ---------------------
; This routine turns off a note on the MIDI channel, if a MIDI channel is assigned to the current string.
; Three bytes are sent, and have the following meaning:
; Byte 1: Channel number $00..$0F, with bit 7 set.
; Byte 2: Note number $00..$7F.
; Byte 3: Note velocity $40.
; Entry: IX=Address of the channel data block.
L118D: LD A,(IX+$01) ; Is a MIDI channel assigned to this string?
OR A ;
RET M ; Return if not.
;A holds the assigned channel number ($00..$0F)
OR $80 ; Set bit 7 of the channel number. A=$80..$8F.
CALL L11A3 ; Write byte to MIDI device.
LD A,(IX+$00) ; The note number.
CALL L11A3 ; Write byte to MIDI device.
LD A,$40 ; The note velocity.
CALL L11A3 ; Write byte to MIDI device.
RET ; [Could have saved 1 byte by using JP L11A3 (ROM 0)]
; ------------------------
; Send Byte to MIDI Device
; ------------------------
; This routine sends a byte to the MIDI port. MIDI devices communicate at 31250 baud,
; although this routine actually generates a baud rate of 31388, which is within the 1%
; tolerance supported by MIDI devices.
; Entry: A=Byte to send.
L11A3: LD L,A ; Store the byte to send.
LD BC,$FFFD ;
LD A,$0E ;
OUT (C),A ; Select register 14 - I/O port.
LD BC,$BFFD ;
LD A,$FA ; Set RS232 'RXD' transmit line to 0. (Keep KEYPAD 'CTS' output line low to prevent the keypad resetting)
OUT (C),A ; Send out the START bit.
LD E,$03 ; (7) Introduce delays such that the next bit is output 113 T-states from now.
L11B4: DEC E ; (4)
JR NZ,L11B4 ; (12/7)
NOP ; (4)
NOP ; (4)
NOP ; (4)
NOP ; (4)
LD A,L ; (4) Retrieve the byte to send.
LD D,$08 ; (7) There are 8 bits to send.
L11BE: RRA ; (4) Rotate the next bit to send into the carry.
LD L,A ; (4) Store the remaining bits.
JP NC,L11C9 ; (10) Jump if it is a 0 bit.
LD A,$FE ; (7) Set RS232 'RXD' transmit line to 1. (Keep KEYPAD 'CTS' output line low to prevent the keypad resetting)
OUT (C),A ; (11)
JR L11CF ; (12) Jump forward to process the next bit.
L11C9: LD A,$FA ; (7) Set RS232 'RXD' transmit line to 0. (Keep KEYPAD 'CTS' output line low to prevent the keypad resetting)
OUT (C),A ; (11)
JR L11CF ; (12) Jump forward to process the next bit.
L11CF: LD E,$02 ; (7) Introduce delays such that the next data bit is output 113 T-states from now.
L11D1: DEC E ; (4)
JR NZ,L11D1 ; (12/7)
NOP ; (4)
ADD A,$00 ; (7)
LD A,L ; (4) Retrieve the remaining bits to send.
DEC D ; (4) Decrement the bit counter.
JR NZ,L11BE ; (12/7) Jump back if there are further bits to send.
NOP ; (4) Introduce delays such that the stop bit is output 113 T-states from now.
NOP ; (4)
ADD A,$00 ; (7)
NOP ; (4)
NOP ; (4)
LD A,$FE ; (7) Set RS232 'RXD' transmit line to 0. (Keep KEYPAD 'CTS' output line low to prevent the keypad resetting)
OUT (C),A ; (11) Send out the STOP bit.
LD E,$06 ; (7) Delay for 101 T-states (28.5us).
L11E7: DEC E ; (4)
JR NZ,L11E7 ; (12/7)
RET ; (10)
Идем дальше, попытка распарсить параметры оператора PLAY
Имеем пример - PLAY "T160","","","Y1Z192Z0V15O5cdefgabC"
T160 - установка темпа 160 (по умолчанию 120)
Y1 - установка канала 1 миди (собственно Y дает понять что будет играть миди, а не AY), диапазон 1...16. Внутри оператора уменьшается на 1, чтобы передавалось в канал миди значение 0...F.
Z192, Z0 - непосредственная отправка команд в канал миди, в данном случае 0С0h и 00h - выбираем инструмент (акустический рояль) для канала 1
V15 - установка громкости звучания (15)
O5 - установка октавы (5)
cdefgabC - ноты
Продолжаем исследования. Теперь наберем строчку в эмуляторе Unreal
PLAY "T160","","","Y1Z192Z0V15O5cdefgabC"
И поставим в дебаггере точку останова на процедуру вывода байта в порт миди (11A3h - Send Byte to MIDI Device)
Вот что пересылается в порт миди в соответствии с параметрами сторки:
Z192Z0 - C0h, 00h - выбираем инструмент, в данном случае акустический рояль
c - 90h, 3Ch, 78h, 80h, 3Ch, 40h - включаем и выключаем ноту До
d - 90h, 3Eh, 78h, 80h, 3Eh, 40h - включаем и выключаем ноту Ре
e - 90h, 40h, 78h, 80h, 40h, 40h - включаем и выключаем ноту Ми
f - 90h, 41h, 78h, 80h, 41h, 40h - включаем и выключаем ноту Фа
g - 90h, 43h, 78h, 80h, 43h, 40h - включаем и выключаем ноту Соль
a - 90h, 45h, 78h, 80h, 45h, 40h - включаем и выключаем ноту Ля
b - 90h, 47h, 78h, 80h, 47h, 40h - включаем и выключаем ноту Си
C - 90h, 48h, 78h, 80h, 48h, 40h - включаем и выключаем ноту До следующей октавы
Также следует помнить, когда в строке задавали параметр O5 - выбор октавы 5,
то в соответствии с таблицей 5 октава соответствует Первой октаве в реальности
Также стоит отметить что между включением и выключением нот существует некоторая пауза, по всей видимости соответствующая выбранному темпу.Код:
; -----------------
; Note Lookup Table
; -----------------
; Each word gives the value of the sound generator tone registers for a given note.
; There are 9 octaves, containing a total of 108 notes. These represent notes 21 to
; 128. Notes 0 to 20 cannot be reproduced on the sound chip and so note 21 will be
; used for all of these (they will however be sent to a MIDI device if one is assigned
; to a channel). [Note that both the sound chip and the MIDI port can not play note 128
; and so its inclusion in the table is a waste of 2 bytes]. The PLAY command does not allow
; octaves higher than 8 to be selected directly. Using PLAY "O8G" will select note 115. To
; select higher notes, sharps must be included, e.g. PLAY "O8#G" for note 116, PLAY "O8##G"
; for note 117, etc, up to PLAY "O8############G" for note 127. Attempting to access note
; 128 using PLAY "O8#############G" will lead to error report "m Note out of range".
L1096: DEFW $0FBF ; Octave 1, Note 21 - A (27.50 Hz, Ideal=27.50 Hz, Error=-0.01%) C0
DEFW $0EDC ; Octave 1, Note 22 - A# (29.14 Hz, Ideal=29.16 Hz, Error=-0.08%)
DEFW $0E07 ; Octave 1, Note 23 - B (30.87 Hz, Ideal=30.87 Hz, Error=-0.00%)
DEFW $0D3D ; Octave 2, Note 24 - C (32.71 Hz, Ideal=32.70 Hz, Error=+0.01%) C1
DEFW $0C7F ; Octave 2, Note 25 - C# (34.65 Hz, Ideal=34.65 Hz, Error=-0.00%)
DEFW $0BCC ; Octave 2, Note 26 - D (36.70 Hz, Ideal=36.71 Hz, Error=-0.01%)
DEFW $0B22 ; Octave 2, Note 27 - D# (38.89 Hz, Ideal=38.89 Hz, Error=+0.01%)
DEFW $0A82 ; Octave 2, Note 28 - E (41.20 Hz, Ideal=41.20 Hz, Error=+0.00%)
DEFW $09EB ; Octave 2, Note 29 - F (43.66 Hz, Ideal=43.65 Hz, Error=+0.00%)
DEFW $095D ; Octave 2, Note 30 - F# (46.24 Hz, Ideal=46.25 Hz, Error=-0.02%)
DEFW $08D6 ; Octave 2, Note 31 - G (49.00 Hz, Ideal=49.00 Hz, Error=+0.00%)
DEFW $0857 ; Octave 2, Note 32 - G# (51.92 Hz, Ideal=51.91 Hz, Error=+0.01%)
DEFW $07DF ; Octave 2, Note 33 - A (55.01 Hz, Ideal=55.00 Hz, Error=+0.01%)
DEFW $076E ; Octave 2, Note 34 - A# (58.28 Hz, Ideal=58.33 Hz, Error=-0.08%)
DEFW $0703 ; Octave 2, Note 35 - B (61.75 Hz, Ideal=61.74 Hz, Error=+0.02%)
DEFW $069F ; Octave 3, Note 36 - C ( 65.39 Hz, Ideal= 65.41 Hz, Error=-0.02%) C2
DEFW $0640 ; Octave 3, Note 37 - C# ( 69.28 Hz, Ideal= 69.30 Hz, Error=-0.04%)
DEFW $05E6 ; Octave 3, Note 38 - D ( 73.40 Hz, Ideal= 73.42 Hz, Error=-0.01%)
DEFW $0591 ; Octave 3, Note 39 - D# ( 77.78 Hz, Ideal= 77.78 Hz, Error=+0.01%)
DEFW $0541 ; Octave 3, Note 40 - E ( 82.41 Hz, Ideal= 82.41 Hz, Error=+0.00%)
DEFW $04F6 ; Octave 3, Note 41 - F ( 87.28 Hz, Ideal= 87.31 Hz, Error=-0.04%)
DEFW $04AE ; Octave 3, Note 42 - F# ( 92.52 Hz, Ideal= 92.50 Hz, Error=+0.02%)
DEFW $046B ; Octave 3, Note 43 - G ( 98.00 Hz, Ideal= 98.00 Hz, Error=+0.00%)
DEFW $042C ; Octave 3, Note 44 - G# (103.78 Hz, Ideal=103.83 Hz, Error=-0.04%)
DEFW $03F0 ; Octave 3, Note 45 - A (109.96 Hz, Ideal=110.00 Hz, Error=-0.04%)
DEFW $03B7 ; Octave 3, Note 46 - A# (116.55 Hz, Ideal=116.65 Hz, Error=-0.08%)
DEFW $0382 ; Octave 3, Note 47 - B (123.43 Hz, Ideal=123.47 Hz, Error=-0.03%)
DEFW $034F ; Octave 4, Note 48 - C (130.86 Hz, Ideal=130.82 Hz, Error=+0.04%) C3
DEFW $0320 ; Octave 4, Note 49 - C# (138.55 Hz, Ideal=138.60 Hz, Error=-0.04%)
DEFW $02F3 ; Octave 4, Note 50 - D (146.81 Hz, Ideal=146.83 Hz, Error=-0.01%)
DEFW $02C8 ; Octave 4, Note 51 - D# (155.68 Hz, Ideal=155.55 Hz, Error=+0.08%)
DEFW $02A1 ; Octave 4, Note 52 - E (164.70 Hz, Ideal=164.82 Hz, Error=-0.07%)
DEFW $027B ; Octave 4, Note 53 - F (174.55 Hz, Ideal=174.62 Hz, Error=-0.04%)
DEFW $0257 ; Octave 4, Note 54 - F# (185.04 Hz, Ideal=185.00 Hz, Error=+0.02%)
DEFW $0236 ; Octave 4, Note 55 - G (195.83 Hz, Ideal=196.00 Hz, Error=-0.09%)
DEFW $0216 ; Octave 4, Note 56 - G# (207.57 Hz, Ideal=207.65 Hz, Error=-0.04%)
DEFW $01F8 ; Octave 4, Note 57 - A (219.92 Hz, Ideal=220.00 Hz, Error=-0.04%)
DEFW $01DC ; Octave 4, Note 58 - A# (232.86 Hz, Ideal=233.30 Hz, Error=-0.19%)
DEFW $01C1 ; Octave 4, Note 59 - B (246.86 Hz, Ideal=246.94 Hz, Error=-0.03%)
DEFW $01A8 ; Octave 5, Note 60 - C (261.42 Hz, Ideal=261.63 Hz, Error=-0.08%) C4 Middle C
DEFW $0190 ; Octave 5, Note 61 - C# (277.10 Hz, Ideal=277.20 Hz, Error=-0.04%)
DEFW $0179 ; Octave 5, Note 62 - D (294.01 Hz, Ideal=293.66 Hz, Error=+0.12%)
DEFW $0164 ; Octave 5, Note 63 - D# (311.35 Hz, Ideal=311.10 Hz, Error=+0.08%)
DEFW $0150 ; Octave 5, Note 64 - E (329.88 Hz, Ideal=329.63 Hz, Error=+0.08%)
DEFW $013D ; Octave 5, Note 65 - F (349.65 Hz, Ideal=349.23 Hz, Error=+0.12%)
DEFW $012C ; Octave 5, Note 66 - F# (369.47 Hz, Ideal=370.00 Hz, Error=-0.14%)
DEFW $011B ; Octave 5, Note 67 - G (391.66 Hz, Ideal=392.00 Hz, Error=-0.09%)
DEFW $010B ; Octave 5, Note 68 - G# (415.13 Hz, Ideal=415.30 Hz, Error=-0.04%)
DEFW $00FC ; Octave 5, Note 69 - A (439.84 Hz, Ideal=440.00 Hz, Error=-0.04%)
DEFW $00EE ; Octave 5, Note 70 - A# (465.72 Hz, Ideal=466.60 Hz, Error=-0.19%)
DEFW $00E0 ; Octave 5, Note 71 - B (494.82 Hz, Ideal=493.88 Hz, Error=+0.19%)
DEFW $00D4 ; Octave 6, Note 72 - C (522.83 Hz, Ideal=523.26 Hz, Error=-0.08%) C5
DEFW $00C8 ; Octave 6, Note 73 - C# (554.20 Hz, Ideal=554.40 Hz, Error=-0.04%)
DEFW $00BD ; Octave 6, Note 74 - D (586.46 Hz, Ideal=587.32 Hz, Error=-0.15%)
DEFW $00B2 ; Octave 6, Note 75 - D# (622.70 Hz, Ideal=622.20 Hz, Error=+0.08%)
DEFW $00A8 ; Octave 6, Note 76 - E (659.77 Hz, Ideal=659.26 Hz, Error=+0.08%)
DEFW $009F ; Octave 6, Note 77 - F (697.11 Hz, Ideal=698.46 Hz, Error=-0.19%)
DEFW $0096 ; Octave 6, Note 78 - F# (738.94 Hz, Ideal=740.00 Hz, Error=-0.14%)
DEFW $008D ; Octave 6, Note 79 - G (786.10 Hz, Ideal=784.00 Hz, Error=+0.27%)
DEFW $0085 ; Octave 6, Note 80 - G# (833.39 Hz, Ideal=830.60 Hz, Error=+0.34%)
DEFW $007E ; Octave 6, Note 81 - A (879.69 Hz, Ideal=880.00 Hz, Error=-0.04%)
DEFW $0077 ; Octave 6, Note 82 - A# (931.43 Hz, Ideal=933.20 Hz, Error=-0.19%)
DEFW $0070 ; Octave 6, Note 83 - B (989.65 Hz, Ideal=987.76 Hz, Error=+0.19%)
DEFW $006A ; Octave 7, Note 84 - C (1045.67 Hz, Ideal=1046.52 Hz, Error=-0.08%) C6
DEFW $0064 ; Octave 7, Note 85 - C# (1108.41 Hz, Ideal=1108.80 Hz, Error=-0.04%)
DEFW $005E ; Octave 7, Note 86 - D (1179.16 Hz, Ideal=1174.64 Hz, Error=+0.38%)
DEFW $0059 ; Octave 7, Note 87 - D# (1245.40 Hz, Ideal=1244.40 Hz, Error=+0.08%)
DEFW $0054 ; Octave 7, Note 88 - E (1319.53 Hz, Ideal=1318.52 Hz, Error=+0.08%)
DEFW $004F ; Octave 7, Note 89 - F (1403.05 Hz, Ideal=1396.92 Hz, Error=+0.44%)
DEFW $004B ; Octave 7, Note 90 - F# (1477.88 Hz, Ideal=1480.00 Hz, Error=-0.14%)
DEFW $0047 ; Octave 7, Note 91 - G (1561.14 Hz, Ideal=1568.00 Hz, Error=-0.44%)
DEFW $0043 ; Octave 7, Note 92 - G# (1654.34 Hz, Ideal=1661.20 Hz, Error=-0.41%)
DEFW $003F ; Octave 7, Note 93 - A (1759.38 Hz, Ideal=1760.00 Hz, Error=-0.04%)
DEFW $003B ; Octave 7, Note 94 - A# (1878.65 Hz, Ideal=1866.40 Hz, Error=+0.66%)
DEFW $0038 ; Octave 7, Note 95 - B (1979.30 Hz, Ideal=1975.52 Hz, Error=+0.19%)
DEFW $0035 ; Octave 8, Note 96 - C (2091.33 Hz, Ideal=2093.04 Hz, Error=-0.08%) C7
DEFW $0032 ; Octave 8, Note 97 - C# (2216.81 Hz, Ideal=2217.60 Hz, Error=-0.04%)
DEFW $002F ; Octave 8, Note 98 - D (2358.31 Hz, Ideal=2349.28 Hz, Error=+0.38%)
DEFW $002D ; Octave 8, Note 99 - D# (2463.13 Hz, Ideal=2488.80 Hz, Error=-1.03%)
DEFW $002A ; Octave 8, Note 100 - E (2639.06 Hz, Ideal=2637.04 Hz, Error=+0.08%)
DEFW $0028 ; Octave 8, Note 101 - F (2771.02 Hz, Ideal=2793.84 Hz, Error=-0.82%)
DEFW $0025 ; Octave 8, Note 102 - F# (2995.69 Hz, Ideal=2960.00 Hz, Error=+1.21%)
DEFW $0023 ; Octave 8, Note 103 - G (3166.88 Hz, Ideal=3136.00 Hz, Error=+0.98%)
DEFW $0021 ; Octave 8, Note 104 - G# (3358.81 Hz, Ideal=3322.40 Hz, Error=+1.10%)
DEFW $001F ; Octave 8, Note 105 - A (3575.50 Hz, Ideal=3520.00 Hz, Error=+1.58%)
DEFW $001E ; Octave 8, Note 106 - A# (3694.69 Hz, Ideal=3732.80 Hz, Error=-1.02%)
DEFW $001C ; Octave 8, Note 107 - B (3958.59 Hz, Ideal=3951.04 Hz, Error=+0.19%)
DEFW $001A ; Octave 9, Note 108 - C (4263.10 Hz, Ideal=4186.08 Hz, Error=+1.84%) C8
DEFW $0019 ; Octave 9, Note 109 - C# (4433.63 Hz, Ideal=4435.20 Hz, Error=-0.04%)
DEFW $0018 ; Octave 9, Note 110 - D (4618.36 Hz, Ideal=4698.56 Hz, Error=-1.71%)
DEFW $0016 ; Octave 9, Note 111 - D# (5038.21 Hz, Ideal=4977.60 Hz, Error=+1.22%)
DEFW $0015 ; Octave 9, Note 112 - E (5278.13 Hz, Ideal=5274.08 Hz, Error=+0.08%)
DEFW $0014 ; Octave 9, Note 113 - F (5542.03 Hz, Ideal=5587.68 Hz, Error=-0.82%)
DEFW $0013 ; Octave 9, Note 114 - F# (5833.72 Hz, Ideal=5920.00 Hz, Error=-1.46%)
DEFW $0012 ; Octave 9, Note 115 - G (6157.81 Hz, Ideal=6272.00 Hz, Error=-1.82%)
DEFW $0011 ; Octave 9, Note 116 - G# (6520.04 Hz, Ideal=6644.80 Hz, Error=-1.88%)
DEFW $0010 ; Octave 9, Note 117 - A (6927.54 Hz, Ideal=7040.00 Hz, Error=-1.60%)
DEFW $000F ; Octave 9, Note 118 - A# (7389.38 Hz, Ideal=7465.60 Hz, Error=-1.02%)
DEFW $000E ; Octave 9, Note 119 - B (7917.19 Hz, Ideal=7902.08 Hz, Error=+0.19%)
DEFW $000D ; Octave 10, Note 120 - C ( 8526.20 Hz, Ideal= 8372.16 Hz, Error=+1.84%) C9
DEFW $000C ; Octave 10, Note 121 - C# ( 9236.72 Hz, Ideal= 8870.40 Hz, Error=+4.13%)
DEFW $000C ; Octave 10, Note 122 - D ( 9236.72 Hz, Ideal= 9397.12 Hz, Error=-1.71%)
DEFW $000B ; Octave 10, Note 123 - D# (10076.42 Hz, Ideal= 9955.20 Hz, Error=+1.22%)
DEFW $000B ; Octave 10, Note 124 - E (10076.42 Hz, Ideal=10548.16 Hz, Error=-4.47%)
DEFW $000A ; Octave 10, Note 125 - F (11084.06 Hz, Ideal=11175.36 Hz, Error=-0.82%)
DEFW $0009 ; Octave 10, Note 126 - F# (12315.63 Hz, Ideal=11840.00 Hz, Error=+4.02%)
DEFW $0009 ; Octave 10, Note 127 - G (12315.63 Hz, Ideal=12544.00 Hz, Error=-1.82%)
DEFW $0008 ; Octave 10, Note 128 - G# (13855.08 Hz, Ideal=13289.60 Hz, Error=+4.26%)
Громкость, которая указывалась как параметр V15, включается в параметр динамики воспроизведении ноты, в данном случаем значение байта равно 78h.
Диапазон громкости 0...15, но бейсик ее преобразует в значения 00...78h