User Tag List

Страница 2 из 2 ПерваяПервая 12
Показано с 11 по 19 из 19

Тема: Проигрывание миди через бейсик

Комбинированный просмотр

Предыдущее сообщение Предыдущее сообщение   Следующее сообщение Следующее сообщение
  1. #1

    Регистрация
    03.03.2008
    Адрес
    Петербург
    Сообщений
    279
    Спасибо Благодарностей отдано 
    69
    Спасибо Благодарностей получено 
    22
    Поблагодарили
    16 сообщений
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)

    По умолчанию

    Цитата Сообщение от Mick Посмотреть сообщение
    PLAY "T160","","","Y1Z192Z0V15O5cdefgabC"

    Пианино проигрывает ноты.

    Как тоже самое сделать из ассемблера. То есть, как вызвать оператор PLAY из программы на ассемблере и подсунуть ему некий массив данных - параметры оператора?
    Может быть как-то типа того, как вызываются команды для TR-DOS в главе "Командный процессор" ("ZX-Spectrum & TR-DOS Для пользователей и программистов"; страницы 185-189).

  2. #1
    С любовью к вам, Yandex.Direct
    Размещение рекламы на форуме способствует его дальнейшему развитию

  3. #2

    Регистрация
    27.02.2005
    Адрес
    москва
    Сообщений
    14,292
    Записей в дневнике
    1
    Спасибо Благодарностей отдано 
    203
    Спасибо Благодарностей получено 
    1,456
    Поблагодарили
    946 сообщений
    Mentioned
    18 Post(s)
    Tagged
    0 Thread(s)

    По умолчанию

    мне кажется это очень геморно.
    возможно проще самому слать данные в порт AYшки

  4. #3

    Регистрация
    14.06.2005
    Адрес
    г. Калуга
    Сообщений
    10,141
    Спасибо Благодарностей отдано 
    216
    Спасибо Благодарностей получено 
    769
    Поблагодарили
    417 сообщений
    Mentioned
    23 Post(s)
    Tagged
    0 Thread(s)

    По умолчанию

    Так, отсюда - http://www.matthew-wilson.net/spectr.../128_ROM0.html
    Оператор PLAY находится по адресу 0985h, я так понимаю.

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

    Цитата Сообщение от goodboy Посмотреть сообщение
    возможно проще самому слать данные в порт AYшки
    К этому придем, я пока пытаюсь понять как и что. В идеале нужно миди файлы закидывать через последовательный порт AY-MIDI интерфейса.
    Сайт поддержки моих изделий - http://micklab.ru/
    Группа ВКонтакте - https://vk.com/micklab

  5. #4

    Регистрация
    27.02.2005
    Адрес
    москва
    Сообщений
    14,292
    Записей в дневнике
    1
    Спасибо Благодарностей отдано 
    203
    Спасибо Благодарностей получено 
    1,456
    Поблагодарили
    946 сообщений
    Mentioned
    18 Post(s)
    Tagged
    0 Thread(s)

    По умолчанию

    http://www.fruitcake.plus.com/Sincla...isassembly.htm
    ищи именно midi в дизасме rom0
    (чуток опоздал с советом, вижу что уже копаешь в этом направлении)

  6. #5

    Регистрация
    14.06.2005
    Адрес
    г. Калуга
    Сообщений
    10,141
    Спасибо Благодарностей отдано 
    216
    Спасибо Благодарностей получено 
    769
    Поблагодарили
    417 сообщений
    Mentioned
    23 Post(s)
    Tagged
    0 Thread(s)

    По умолчанию

    Перевел по старинке бейсиковский файл Bach.tap в trd

    загружать из TR-DOS

    RUN "MIDIBACH"

    Вложение MidiBach_trd.rar

    Проверил, вроде играет.
    Сайт поддержки моих изделий - http://micklab.ru/
    Группа ВКонтакте - https://vk.com/micklab

    Этот пользователь поблагодарил Mick за это полезное сообщение:

    nimdasys_inbox_ru(29.05.2021)

  7. #6

    Регистрация
    14.06.2005
    Адрес
    г. Калуга
    Сообщений
    10,141
    Спасибо Благодарностей отдано 
    216
    Спасибо Благодарностей получено 
    769
    Поблагодарили
    417 сообщений
    Mentioned
    23 Post(s)
    Tagged
    0 Thread(s)

    По умолчанию

    Небольшое видео с примером звучания Баха

    https://vk.com/micklab?z=video-19161...3ac079b886751b
    Сайт поддержки моих изделий - http://micklab.ru/
    Группа ВКонтакте - https://vk.com/micklab

  8. #7

    Регистрация
    14.06.2005
    Адрес
    г. Калуга
    Сообщений
    10,141
    Спасибо Благодарностей отдано 
    216
    Спасибо Благодарностей получено 
    769
    Поблагодарили
    417 сообщений
    Mentioned
    23 Post(s)
    Tagged
    0 Thread(s)

    По умолчанию

    Продолжаем попытки исследования оператора PLAY

    Исходя из исходников Basic-128 оператор PLAY располагается в адресах ПЗУ 0985h...11ECh

    Непосредственно с миди каналом работают следующие процедуры

    0CDDh - Play Command 'Y' (MIDI Channel)
    0CEEh - Play Command 'Z' (MIDI Programming Code)
    116Eh - Play Note on MIDI Channel
    118Dh - Turn MIDI Channel Off
    11A3h - Send Byte to MIDI Device

    Собственно подпрограмму отправки байта в канал Миди можно использовать в своих программах, ибо она уже расчитана на скорость 31250

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

    Этот пользователь поблагодарил Mick за это полезное сообщение:

    nimdasys_inbox_ru(29.05.2021)

  9. #8

    Регистрация
    14.06.2005
    Адрес
    г. Калуга
    Сообщений
    10,141
    Спасибо Благодарностей отдано 
    216
    Спасибо Благодарностей получено 
    769
    Поблагодарили
    417 сообщений
    Mentioned
    23 Post(s)
    Tagged
    0 Thread(s)

    По умолчанию

    Идем дальше, попытка распарсить параметры оператора PLAY

    Имеем пример - PLAY "T160","","","Y1Z192Z0V15O5cdefgabC"

    T160 - установка темпа 160 (по умолчанию 120)
    Y1 - установка канала 1 миди (собственно Y дает понять что будет играть миди, а не AY), диапазон 1...16. Внутри оператора уменьшается на 1, чтобы передавалось в канал миди значение 0...F.
    Z192, Z0 - непосредственная отправка команд в канал миди, в данном случае 0С0h и 00h - выбираем инструмент (акустический рояль) для канала 1
    V15 - установка громкости звучания (15)
    O5 - установка октавы (5)
    cdefgabC - ноты
    Сайт поддержки моих изделий - http://micklab.ru/
    Группа ВКонтакте - https://vk.com/micklab

    Эти 3 пользователя(ей) поблагодарили Mick за это полезное сообщение:

    Djoni(25.05.2021), nimdasys_inbox_ru(29.05.2021), Oleg N. Cher(27.05.2021)

  10. #9

    Регистрация
    14.06.2005
    Адрес
    г. Калуга
    Сообщений
    10,141
    Спасибо Благодарностей отдано 
    216
    Спасибо Благодарностей получено 
    769
    Поблагодарили
    417 сообщений
    Mentioned
    23 Post(s)
    Tagged
    0 Thread(s)

    По умолчанию

    Продолжаем исследования. Теперь наберем строчку в эмуляторе Unreal

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

    И поставим в дебаггере точку останова на процедуру вывода байта в порт миди (11A3h - Send Byte to MIDI Device)

    Вот что пересылается в порт миди в соответствии с параметрами сторки:

    Z192Z0 - C0h, 00h - выбираем инструмент, в данном случае акустический рояль
    c - 90h, 3Ch, 78h, 80h, 3Ch, 40h - включаем и выключаем ноту До
    d - 90h, 3Eh, 78h, 80h, 3Eh, 40h - включаем и выключаем ноту Ре
    e - 90h, 40h, 78h, 80h, 40h, 40h - включаем и выключаем ноту Ми
    f - 90h, 41h, 78h, 80h, 41h, 40h - включаем и выключаем ноту Фа
    g - 90h, 43h, 78h, 80h, 43h, 40h - включаем и выключаем ноту Соль
    a - 90h, 45h, 78h, 80h, 45h, 40h - включаем и выключаем ноту Ля
    b - 90h, 47h, 78h, 80h, 47h, 40h - включаем и выключаем ноту Си
    C - 90h, 48h, 78h, 80h, 48h, 40h - включаем и выключаем ноту До следующей октавы

    Также следует помнить, когда в строке задавали параметр O5 - выбор октавы 5,
    то в соответствии с таблицей 5 октава соответствует Первой октаве в реальности

    Код:
     
    ; -----------------
    ; Note Lookup Table
    ; -----------------
    ; Each word gives the value of the sound generator tone registers for a given note.
    ; There are 9 octaves, containing a total of 108 notes. These represent notes 21 to
    ; 128. Notes 0 to 20 cannot be reproduced on the sound chip and so note 21 will be
    ; used for all of these (they will however be sent to a MIDI device if one is assigned
    ; to a channel). [Note that both the sound chip and the MIDI port can not play note 128
    ; and so its inclusion in the table is a waste of 2 bytes]. The PLAY command does not allow
    ; octaves higher than 8 to be selected directly. Using PLAY "O8G" will select note 115. To
    ; select higher notes, sharps must be included, e.g. PLAY "O8#G" for note 116, PLAY "O8##G"
    ; for note 117, etc, up to PLAY "O8############G" for note 127. Attempting to access note
    ; 128 using PLAY "O8#############G" will lead to error report "m Note out of range".
    
    L1096:  DEFW $0FBF        ; Octave  1, Note  21 - A  (27.50 Hz, Ideal=27.50 Hz, Error=-0.01%) C0
            DEFW $0EDC        ; Octave  1, Note  22 - A# (29.14 Hz, Ideal=29.16 Hz, Error=-0.08%)
            DEFW $0E07        ; Octave  1, Note  23 - B  (30.87 Hz, Ideal=30.87 Hz, Error=-0.00%)
    
            DEFW $0D3D        ; Octave  2, Note  24 - C  (32.71 Hz, Ideal=32.70 Hz, Error=+0.01%) C1
            DEFW $0C7F        ; Octave  2, Note  25 - C# (34.65 Hz, Ideal=34.65 Hz, Error=-0.00%)
            DEFW $0BCC        ; Octave  2, Note  26 - D  (36.70 Hz, Ideal=36.71 Hz, Error=-0.01%)
            DEFW $0B22        ; Octave  2, Note  27 - D# (38.89 Hz, Ideal=38.89 Hz, Error=+0.01%)
            DEFW $0A82        ; Octave  2, Note  28 - E  (41.20 Hz, Ideal=41.20 Hz, Error=+0.00%)
            DEFW $09EB        ; Octave  2, Note  29 - F  (43.66 Hz, Ideal=43.65 Hz, Error=+0.00%)
            DEFW $095D        ; Octave  2, Note  30 - F# (46.24 Hz, Ideal=46.25 Hz, Error=-0.02%)
            DEFW $08D6        ; Octave  2, Note  31 - G  (49.00 Hz, Ideal=49.00 Hz, Error=+0.00%)
            DEFW $0857        ; Octave  2, Note  32 - G# (51.92 Hz, Ideal=51.91 Hz, Error=+0.01%)
            DEFW $07DF        ; Octave  2, Note  33 - A  (55.01 Hz, Ideal=55.00 Hz, Error=+0.01%)
            DEFW $076E        ; Octave  2, Note  34 - A# (58.28 Hz, Ideal=58.33 Hz, Error=-0.08%)
            DEFW $0703        ; Octave  2, Note  35 - B  (61.75 Hz, Ideal=61.74 Hz, Error=+0.02%)
    
            DEFW $069F        ; Octave  3, Note  36 - C  ( 65.39 Hz, Ideal= 65.41 Hz, Error=-0.02%) C2
            DEFW $0640        ; Octave  3, Note  37 - C# ( 69.28 Hz, Ideal= 69.30 Hz, Error=-0.04%)
            DEFW $05E6        ; Octave  3, Note  38 - D  ( 73.40 Hz, Ideal= 73.42 Hz, Error=-0.01%)
            DEFW $0591        ; Octave  3, Note  39 - D# ( 77.78 Hz, Ideal= 77.78 Hz, Error=+0.01%)
            DEFW $0541        ; Octave  3, Note  40 - E  ( 82.41 Hz, Ideal= 82.41 Hz, Error=+0.00%)
            DEFW $04F6        ; Octave  3, Note  41 - F  ( 87.28 Hz, Ideal= 87.31 Hz, Error=-0.04%)
            DEFW $04AE        ; Octave  3, Note  42 - F# ( 92.52 Hz, Ideal= 92.50 Hz, Error=+0.02%)
            DEFW $046B        ; Octave  3, Note  43 - G  ( 98.00 Hz, Ideal= 98.00 Hz, Error=+0.00%)
            DEFW $042C        ; Octave  3, Note  44 - G# (103.78 Hz, Ideal=103.83 Hz, Error=-0.04%)
            DEFW $03F0        ; Octave  3, Note  45 - A  (109.96 Hz, Ideal=110.00 Hz, Error=-0.04%)
            DEFW $03B7        ; Octave  3, Note  46 - A# (116.55 Hz, Ideal=116.65 Hz, Error=-0.08%)
            DEFW $0382        ; Octave  3, Note  47 - B  (123.43 Hz, Ideal=123.47 Hz, Error=-0.03%)
    
            DEFW $034F        ; Octave  4, Note  48 - C  (130.86 Hz, Ideal=130.82 Hz, Error=+0.04%) C3
            DEFW $0320        ; Octave  4, Note  49 - C# (138.55 Hz, Ideal=138.60 Hz, Error=-0.04%)
            DEFW $02F3        ; Octave  4, Note  50 - D  (146.81 Hz, Ideal=146.83 Hz, Error=-0.01%)
            DEFW $02C8        ; Octave  4, Note  51 - D# (155.68 Hz, Ideal=155.55 Hz, Error=+0.08%)
            DEFW $02A1        ; Octave  4, Note  52 - E  (164.70 Hz, Ideal=164.82 Hz, Error=-0.07%)
            DEFW $027B        ; Octave  4, Note  53 - F  (174.55 Hz, Ideal=174.62 Hz, Error=-0.04%)
            DEFW $0257        ; Octave  4, Note  54 - F# (185.04 Hz, Ideal=185.00 Hz, Error=+0.02%)
            DEFW $0236        ; Octave  4, Note  55 - G  (195.83 Hz, Ideal=196.00 Hz, Error=-0.09%)
            DEFW $0216        ; Octave  4, Note  56 - G# (207.57 Hz, Ideal=207.65 Hz, Error=-0.04%)
            DEFW $01F8        ; Octave  4, Note  57 - A  (219.92 Hz, Ideal=220.00 Hz, Error=-0.04%)
            DEFW $01DC        ; Octave  4, Note  58 - A# (232.86 Hz, Ideal=233.30 Hz, Error=-0.19%)
            DEFW $01C1        ; Octave  4, Note  59 - B  (246.86 Hz, Ideal=246.94 Hz, Error=-0.03%)
    
            DEFW $01A8        ; Octave  5, Note  60 - C  (261.42 Hz, Ideal=261.63 Hz, Error=-0.08%) C4 Middle C
            DEFW $0190        ; Octave  5, Note  61 - C# (277.10 Hz, Ideal=277.20 Hz, Error=-0.04%)
            DEFW $0179        ; Octave  5, Note  62 - D  (294.01 Hz, Ideal=293.66 Hz, Error=+0.12%)
            DEFW $0164        ; Octave  5, Note  63 - D# (311.35 Hz, Ideal=311.10 Hz, Error=+0.08%)
            DEFW $0150        ; Octave  5, Note  64 - E  (329.88 Hz, Ideal=329.63 Hz, Error=+0.08%)
            DEFW $013D        ; Octave  5, Note  65 - F  (349.65 Hz, Ideal=349.23 Hz, Error=+0.12%)
            DEFW $012C        ; Octave  5, Note  66 - F# (369.47 Hz, Ideal=370.00 Hz, Error=-0.14%)
            DEFW $011B        ; Octave  5, Note  67 - G  (391.66 Hz, Ideal=392.00 Hz, Error=-0.09%)
            DEFW $010B        ; Octave  5, Note  68 - G# (415.13 Hz, Ideal=415.30 Hz, Error=-0.04%)
            DEFW $00FC        ; Octave  5, Note  69 - A  (439.84 Hz, Ideal=440.00 Hz, Error=-0.04%)
            DEFW $00EE        ; Octave  5, Note  70 - A# (465.72 Hz, Ideal=466.60 Hz, Error=-0.19%)
            DEFW $00E0        ; Octave  5, Note  71 - B  (494.82 Hz, Ideal=493.88 Hz, Error=+0.19%)
    
            DEFW $00D4        ; Octave  6, Note  72 - C  (522.83 Hz, Ideal=523.26 Hz, Error=-0.08%) C5
            DEFW $00C8        ; Octave  6, Note  73 - C# (554.20 Hz, Ideal=554.40 Hz, Error=-0.04%)
            DEFW $00BD        ; Octave  6, Note  74 - D  (586.46 Hz, Ideal=587.32 Hz, Error=-0.15%)
            DEFW $00B2        ; Octave  6, Note  75 - D# (622.70 Hz, Ideal=622.20 Hz, Error=+0.08%)
            DEFW $00A8        ; Octave  6, Note  76 - E  (659.77 Hz, Ideal=659.26 Hz, Error=+0.08%)
            DEFW $009F        ; Octave  6, Note  77 - F  (697.11 Hz, Ideal=698.46 Hz, Error=-0.19%)
            DEFW $0096        ; Octave  6, Note  78 - F# (738.94 Hz, Ideal=740.00 Hz, Error=-0.14%)
            DEFW $008D        ; Octave  6, Note  79 - G  (786.10 Hz, Ideal=784.00 Hz, Error=+0.27%)
            DEFW $0085        ; Octave  6, Note  80 - G# (833.39 Hz, Ideal=830.60 Hz, Error=+0.34%)
            DEFW $007E        ; Octave  6, Note  81 - A  (879.69 Hz, Ideal=880.00 Hz, Error=-0.04%)
            DEFW $0077        ; Octave  6, Note  82 - A# (931.43 Hz, Ideal=933.20 Hz, Error=-0.19%)
            DEFW $0070        ; Octave  6, Note  83 - B  (989.65 Hz, Ideal=987.76 Hz, Error=+0.19%)
    
            DEFW $006A        ; Octave  7, Note  84 - C  (1045.67 Hz, Ideal=1046.52 Hz, Error=-0.08%) C6
            DEFW $0064        ; Octave  7, Note  85 - C# (1108.41 Hz, Ideal=1108.80 Hz, Error=-0.04%)
            DEFW $005E        ; Octave  7, Note  86 - D  (1179.16 Hz, Ideal=1174.64 Hz, Error=+0.38%)
            DEFW $0059        ; Octave  7, Note  87 - D# (1245.40 Hz, Ideal=1244.40 Hz, Error=+0.08%)
            DEFW $0054        ; Octave  7, Note  88 - E  (1319.53 Hz, Ideal=1318.52 Hz, Error=+0.08%)
            DEFW $004F        ; Octave  7, Note  89 - F  (1403.05 Hz, Ideal=1396.92 Hz, Error=+0.44%)
            DEFW $004B        ; Octave  7, Note  90 - F# (1477.88 Hz, Ideal=1480.00 Hz, Error=-0.14%)
            DEFW $0047        ; Octave  7, Note  91 - G  (1561.14 Hz, Ideal=1568.00 Hz, Error=-0.44%)
            DEFW $0043        ; Octave  7, Note  92 - G# (1654.34 Hz, Ideal=1661.20 Hz, Error=-0.41%)
            DEFW $003F        ; Octave  7, Note  93 - A  (1759.38 Hz, Ideal=1760.00 Hz, Error=-0.04%)
            DEFW $003B        ; Octave  7, Note  94 - A# (1878.65 Hz, Ideal=1866.40 Hz, Error=+0.66%)
            DEFW $0038        ; Octave  7, Note  95 - B  (1979.30 Hz, Ideal=1975.52 Hz, Error=+0.19%)
    
            DEFW $0035        ; Octave  8, Note  96 - C  (2091.33 Hz, Ideal=2093.04 Hz, Error=-0.08%) C7
            DEFW $0032        ; Octave  8, Note  97 - C# (2216.81 Hz, Ideal=2217.60 Hz, Error=-0.04%)
            DEFW $002F        ; Octave  8, Note  98 - D  (2358.31 Hz, Ideal=2349.28 Hz, Error=+0.38%)
            DEFW $002D        ; Octave  8, Note  99 - D# (2463.13 Hz, Ideal=2488.80 Hz, Error=-1.03%)
            DEFW $002A        ; Octave  8, Note 100 - E  (2639.06 Hz, Ideal=2637.04 Hz, Error=+0.08%)
            DEFW $0028        ; Octave  8, Note 101 - F  (2771.02 Hz, Ideal=2793.84 Hz, Error=-0.82%)
            DEFW $0025        ; Octave  8, Note 102 - F# (2995.69 Hz, Ideal=2960.00 Hz, Error=+1.21%)
            DEFW $0023        ; Octave  8, Note 103 - G  (3166.88 Hz, Ideal=3136.00 Hz, Error=+0.98%)
            DEFW $0021        ; Octave  8, Note 104 - G# (3358.81 Hz, Ideal=3322.40 Hz, Error=+1.10%)
            DEFW $001F        ; Octave  8, Note 105 - A  (3575.50 Hz, Ideal=3520.00 Hz, Error=+1.58%)
            DEFW $001E        ; Octave  8, Note 106 - A# (3694.69 Hz, Ideal=3732.80 Hz, Error=-1.02%)
            DEFW $001C        ; Octave  8, Note 107 - B  (3958.59 Hz, Ideal=3951.04 Hz, Error=+0.19%)
    
            DEFW $001A        ; Octave  9, Note 108 - C  (4263.10 Hz, Ideal=4186.08 Hz, Error=+1.84%) C8
            DEFW $0019        ; Octave  9, Note 109 - C# (4433.63 Hz, Ideal=4435.20 Hz, Error=-0.04%)
            DEFW $0018        ; Octave  9, Note 110 - D  (4618.36 Hz, Ideal=4698.56 Hz, Error=-1.71%)
            DEFW $0016        ; Octave  9, Note 111 - D# (5038.21 Hz, Ideal=4977.60 Hz, Error=+1.22%)
            DEFW $0015        ; Octave  9, Note 112 - E  (5278.13 Hz, Ideal=5274.08 Hz, Error=+0.08%)
            DEFW $0014        ; Octave  9, Note 113 - F  (5542.03 Hz, Ideal=5587.68 Hz, Error=-0.82%)
            DEFW $0013        ; Octave  9, Note 114 - F# (5833.72 Hz, Ideal=5920.00 Hz, Error=-1.46%)
            DEFW $0012        ; Octave  9, Note 115 - G  (6157.81 Hz, Ideal=6272.00 Hz, Error=-1.82%)
            DEFW $0011        ; Octave  9, Note 116 - G# (6520.04 Hz, Ideal=6644.80 Hz, Error=-1.88%)
            DEFW $0010        ; Octave  9, Note 117 - A  (6927.54 Hz, Ideal=7040.00 Hz, Error=-1.60%)
            DEFW $000F        ; Octave  9, Note 118 - A# (7389.38 Hz, Ideal=7465.60 Hz, Error=-1.02%)
            DEFW $000E        ; Octave  9, Note 119 - B  (7917.19 Hz, Ideal=7902.08 Hz, Error=+0.19%)
    
            DEFW $000D        ; Octave 10, Note 120 - C  ( 8526.20 Hz, Ideal= 8372.16 Hz, Error=+1.84%) C9
            DEFW $000C        ; Octave 10, Note 121 - C# ( 9236.72 Hz, Ideal= 8870.40 Hz, Error=+4.13%)
            DEFW $000C        ; Octave 10, Note 122 - D  ( 9236.72 Hz, Ideal= 9397.12 Hz, Error=-1.71%)
            DEFW $000B        ; Octave 10, Note 123 - D# (10076.42 Hz, Ideal= 9955.20 Hz, Error=+1.22%)
            DEFW $000B        ; Octave 10, Note 124 - E  (10076.42 Hz, Ideal=10548.16 Hz, Error=-4.47%)
            DEFW $000A        ; Octave 10, Note 125 - F  (11084.06 Hz, Ideal=11175.36 Hz, Error=-0.82%)
            DEFW $0009        ; Octave 10, Note 126 - F# (12315.63 Hz, Ideal=11840.00 Hz, Error=+4.02%)
            DEFW $0009        ; Octave 10, Note 127 - G  (12315.63 Hz, Ideal=12544.00 Hz, Error=-1.82%)
            DEFW $0008        ; Octave 10, Note 128 - G# (13855.08 Hz, Ideal=13289.60 Hz, Error=+4.26%)
    Также стоит отметить что между включением и выключением нот существует некоторая пауза, по всей видимости соответствующая выбранному темпу.
    Громкость, которая указывалась как параметр V15, включается в параметр динамики воспроизведении ноты, в данном случаем значение байта равно 78h.
    Диапазон громкости 0...15, но бейсик ее преобразует в значения 00...78h
    Последний раз редактировалось Mick; 26.05.2021 в 10:20.
    Сайт поддержки моих изделий - http://micklab.ru/
    Группа ВКонтакте - https://vk.com/micklab

    Эти 3 пользователя(ей) поблагодарили Mick за это полезное сообщение:

    CLR(19.05.2024), nimdasys_inbox_ru(29.05.2021), Oleg N. Cher(27.05.2021)

Страница 2 из 2 ПерваяПервая 12

Информация о теме

Пользователи, просматривающие эту тему

Эту тему просматривают: 1 (пользователей: 0 , гостей: 1)

Похожие темы

  1. Ответов: 2
    Последнее: 24.01.2015, 20:35
  2. Проигрывание TS в AYEmul или Wortex Tracker
    от GM BIT в разделе Музыка
    Ответов: 5
    Последнее: 13.11.2010, 06:19
  3. Фоновое проигрывание AY/YM музыки в меню Aeon
    от ILoveSpeccy в разделе Музыка
    Ответов: 9
    Последнее: 06.05.2009, 14:48
  4. Проигрывание SoundTrackerPro музыки
    от Jukov в разделе Программирование
    Ответов: 5
    Последнее: 26.02.2006, 05:49

Ваши права

  • Вы не можете создавать новые темы
  • Вы не можете отвечать в темах
  • Вы не можете прикреплять вложения
  • Вы не можете редактировать свои сообщения
  •