Звуковая карта "ZXM-CrystalMIDI" - Обсуждение

Все вопросы, связанные с ресурсом micklab.ru

Модератор: Mick

Аватара пользователя
idxi
Сообщения: 179
Зарегистрирован: Пн, 03.07.2017 16:54:54

Re: Звуковая карта "ZXM-CrystalMIDI" - Обсуждение

Сообщение idxi »

Вообще, я за возможную идею миксовать каналы через какую нить управляемую микруху - так чтобы любой канал можно было сделать левый/правый/моно/... + громкость аппаратно, только вопрос, есть ли такие чип'чики )
ну ессесно по дефольту если ничего не изменялось - классические стерео ABC/ACB/... для совместимости воспроизведения )
еще куда интереснее AYX32 - там виртуально 4-е чипа AY с одним аудиовыходом на микрухе, tsl давно, вроде, в раздумьях по этому стерео :/
Еще бы ethernet-чип какой нибудь разместить на платке.. :/
Аватара пользователя
idxi
Сообщения: 179
Зарегистрирован: Пн, 03.07.2017 16:54:54

Re: Звуковая карта "ZXM-CrystalMIDI" - Обсуждение

Сообщение idxi »

kgmcneil писал(а):Is this project similar to the midi project that was done for the ZX Uno recently, that joined a midi chip to a Spectrum clone, and allowed it to use PLAY commands to control the midi chip?...
That project can be found here: http://www.zxuno.com/forum/viewtopic.php?f=25&t=3963
I own a ZX Evo. I am curious about whether this project could give similar capabilities??
Would your device require the removal of the AY chip from the mother board, or does it simply sit on top of the AY chip? - would it have to be soldered on?...
Very curious about where this project will take you,
Many Regards and lots of respect to you,
Karl
:)


Karl,
I think that soon we will see and find out everything about this project :)
We are waiting for the completion of the implementation and adjustment of the ZXM-CrystalMIDI project.
Аватара пользователя
Mick
Сообщения: 1150
Зарегистрирован: Чт, 19.06.2014 11:25:00

Re: Звуковая карта "ZXM-CrystalMIDI" - Обсуждение

Сообщение Mick »

Гы гы, не прошло и года - я все таки собрал сей девайс.

Изображение

Изображение

AY часть попробовал, ее не сломал :)
Надо теперь миди часть попробовать

Забыл написать. Подключил когда платку в комп, слышу AY играет тихо. Начал смотреть, а у него питание 3В. Потом допер, у меня стоит стабилизатор, который должен питаться от 12В чтобы получить аналоговые 5В. Зато опытным путем доказано, что YM2149F работает от 3В (только немного тише).

Заодно дополню, то что я штыри напаял снизу, это просто один из вариантов подключения платы, можно шлейфом или чем то другим.
Если ставить бутерброд непосредственно на место AY в компьютер, то лучше вместо панельки использовать типа таких разъемов - https://www.chipdip.ru/product/pbs20

- - - Добавлено - - -

Нашел у поляков строчку из бейсика для проверки миди

PLAY "T160","","","Y1Z192Z0V15O5cdefgabC"

Набрал, надо же услышал пианино
Аватара пользователя
TS-Labs
Сообщения: 5381
Зарегистрирован: Чт, 26.07.2012 01:29:56

Re: Звуковая карта "ZXM-CrystalMIDI" - Обсуждение

Сообщение TS-Labs »

Mick писал(а):что YM2149F работает от 3В

Не работает. Те что у меня, даже при 4.5В уходят в нирвану.
Аватара пользователя
Mick
Сообщения: 1150
Зарегистрирован: Чт, 19.06.2014 11:25:00

Re: Звуковая карта "ZXM-CrystalMIDI" - Обсуждение

Сообщение Mick »

TS-Labs писал(а):Не работает. Те что у меня, даже при 4.5В уходят в нирвану.


Тот что на фото - он работал от 3В. Не знаю может это подделка, а может в 2000 году уже другой тех процесс был. У меня на фото дата 0045
Даже стало интересно. У тебя какой свежести?
Аватара пользователя
Mick
Сообщения: 1150
Зарегистрирован: Чт, 19.06.2014 11:25:00

Re: Звуковая карта "ZXM-CrystalMIDI" - Обсуждение

Сообщение Mick »

Вот небольшое видео, где Ямаха играет на 3 вольтах

https://disk.yandex.ru/i/_KyI3dkAbYImRw

А это фото показывает как не надо включать иначе будет 3В на AY

Изображение

Лучше вот так

Изображение
Аватара пользователя
TS-Labs
Сообщения: 5381
Зарегистрирован: Чт, 26.07.2012 01:29:56

Re: Звуковая карта "ZXM-CrystalMIDI" - Обсуждение

Сообщение TS-Labs »

Mick писал(а):а может в 2000 году уже другой тех процесс был

100%.

Походу, у меня старые.
photo_2021-05-19_04-54-53.jpg
Аватара пользователя
TS-Labs
Сообщения: 5381
Зарегистрирован: Чт, 26.07.2012 01:29:56

Re: Звуковая карта "ZXM-CrystalMIDI" - Обсуждение

Сообщение TS-Labs »

Mick писал(а):https://disk.yandex.ru/i/_KyI3dkAbYImRw

В/на Украине яндекс забанен.
Mick писал(а):А это фото показывает как не надо включать иначе будет 3В на AY

А почему так схема сделана? На панельке же есть 5В.
Аватара пользователя
Mick
Сообщения: 1150
Зарегистрирован: Чт, 19.06.2014 11:25:00

Re: Звуковая карта "ZXM-CrystalMIDI" - Обсуждение

Сообщение Mick »

TS-Labs писал(а):А почему так схема сделана? На панельке же есть 5В.


Я разделил питание звуковой части. Можно питаться от отдельного источника 12В через стабилизатор, но если не нужно, то не впаиваем стабилизатор, ставим пару перемычек и питаемся от 5В компьютера. А 3В получилось, что я запитался от 5В компьютера при установленном стабилизаторе. :)
Аватара пользователя
N_S
Сообщения: 289
Зарегистрирован: Вс, 05.08.2012 22:39:51

Re: Звуковая карта "ZXM-CrystalMIDI" - Обсуждение

Сообщение N_S »

Mick писал(а): Пт, 03.04.2020 19:31:52так называемый AY-RS232/MIDI интерфейс.
так же эта приблуда эмулируетсо в zxspin :ura:
конечно играет с жутким спотыком...

Изображение
еще бы найти процедуры в ПЗУ-шке
которые шлют на этот рс232
тк там посылание\прием выполняетсо по образу и подобию пищания на бипере (насколько я понел)
и лишний раз изобретать уже отлаженный на реале велосипед как то не хочется... :vodka:

вот теперь думаю где бы выкопать этот бгмерский хрензнает какой BT631A штекер :bang:
чтоб подключить какую нибудь миди к своему серому +2
и чего низя было поставить какой нить нормальный дин5 и ниипать людям моск своими британскими телефонными разъемами?
...хотя еще надо ткнуть какой нить RJ25 6P6C без защелки
мож влезет с легкой обработкой напильником
Аватара пользователя
Mick
Сообщения: 1150
Зарегистрирован: Чт, 19.06.2014 11:25:00

Re: Звуковая карта "ZXM-CrystalMIDI" - Обсуждение

Сообщение Mick »

N_S писал(а): Пт, 18.06.2021 23:14:50 еще бы найти процедуры в ПЗУ-шке
которые шлют на этот рс232
Пожалуйста

Исходя из исходников 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)
Ответить