include "inc/cmdsys.plh"
include "inc/fileio.plh"
include "inc/args.plh"
//
// Usage is documented following the source in this file...
//
const rndseed        = $004E
const LSB            = 0
const MSB            = 1
const MB_ARPEGGIO    = 4 // In 16ths of a second
const MAX_MBCH_NOTES = 9
const SPKR_ARPEGGIO  = 2 // In 16ths of a second
const DUR16TH        = 8
const MAX_SPKR_NOTES = 4
const NOTEDIV        = 4
//
// 6522 VIA registers
//
struc t_VIA
    byte IORB       // I/O Register B
    byte IORA       // I/O Register A
    byte DDRB       // Data Direction Register B
    byte DDRA       // Data Direction Register A
    word T1C        // Timer 1 Count
    word T1L        // Timer 1 Latch
    word T2C        // Timer 2 Count
    byte SR         // Shift Register
    byte ACR        // Aux Control Register
    byte PCR        // Peripheral Control Register
    byte IFR        // Interrupt Flag Register
    byte IER        // Interrupt Enable Register
    byte IOA_noHS   // I/O Register A - no HandShake
end
const T1CH = T1C+1
//
// AY-3-8910 PSG registers
//
struc t_PSG
    word AFREQ      // A Frequency Period
    word BFREQ      // B Frequency Period
    word CFREQ      // C Frequency Period
    byte NGFREQ     // Noise Generator Frequency Period
    byte MIXER      // Enable=0/Disable=1 NG C(5) B(4) A(3) Tone C(2) B(1) A(0)
    byte AENVAMP    // A Envelope/Amplitude
    byte BENVAMP    // B Envelope/Amplitude
    byte CENVAMP    // C Envelope/Amplitude
    word ENVPERIOD  // Envelope Period
    byte ENVSHAPE   // Envelope Shape
end
//
// Apple III hardware constants.
//
const ENV_REG      = $FFDF
//
// Sequence event
//
struc t_event
    byte deltatime  // Event delta time in 4.4 seconds
    byte percnote   // Percussion:7==0 ? Pitch:4-0 : Octave:6-4,Note:3-0
    byte perchanvol // Percussion      ? EnvDur:7-0           : Channel:7,Volume:3-0
end
//
// Predef routines
//
predef musicPlay(track, rept)#0
predef musicStop#0
predef spkrSequence(yield, func)#0
predef a2spkrTone(pitch, duration)#0
predef a2spkrPCM(samples, pagecnt)#0
predef a2keypressed
//
// Static sequencer values
//
export word musicSequence = @spkrSequence
export word tonePlay      = @a2spkrTone
export word pcmPlay       = @a2spkrPCM
word        keypressed    = @a2keypressed

word instr[] // Overlay with other variables
word seqTrack, seqEvent, seqTime, eventTime, updateTime
byte numNotes, seqRepeat
byte indexA[2], indexB[2], indexC[2]
byte noteA[2], noteB[2], noteC[2]
word notes1[MAX_MBCH_NOTES], notes2[MAX_MBCH_NOTES]
word notes[2] = @notes1, @notes2
word periods1[MAX_MBCH_NOTES], periods2[MAX_MBCH_NOTES]
word periods[2] = @periods1, @periods2
//
// MockingBoard data.
//
word[] mbVIAs      // Treat this as an array of VIA ptrs
word   mbVIA1, mbVIA2
word   mbSlot = -1
//
// Octave basis frequency periods (starting at MIDI note #12)
// Notes will be encoded as basis note (LSNibble) and octave (MSNibble))
//
word[] spkrOctave0 // Overlay and scale mbOctave0 for speaker version
word[12] mbOctave0 = 3900, 3681, 3474, 3279, 3095, 2922, 2758, 2603, 2457, 2319, 2189, 2066
word[5] arpeggioDuration = DUR16TH, DUR16TH, DUR16TH/2, DUR16TH/3, DUR16TH/4
//
// Emulators are broken - they only activate the MockingBoard's 6522 Timer1
// functionality when interrupts are enabled. This music sequencer is run
// in polling mode without the use of MockingBoard interrupts. To work around
// the emulators, MockingBoard interrupts are enabled, but the 6502 IRQs are
// disabled. NO INTERRUPTS ARE HANDLED WHEN PLAYING MUSIC! The previous state
// is restored between playing sequences.
//
asm vmincs
        !SOURCE "vmsrc/plvmzp.inc"
end
asm getStatusReg#1
        PHP
        PLA
        DEX
        STA     ESTKL,X
        LDA     #$00
        STA     ESTKH,X
        RTS
end
asm setStatusReg(stat)#0
        LDA     ESTKL,X
        INX
        PHA
        PLP
        RTS
end
asm disableInts#0
        SEI
        RTS
end
asm enableInts#0
        CLI
        RTS
end
//
// Write Programmable Sound Generator Registers
//
asm psgWriteTone(pVIA, reg, freq, vol)#0
        LDA     ESTKL+3,X
        STA     TMPL
        LDA     ESTKH+3,X
        STA     TMPH
        LDY     #$01
        LDA     ESTKL+2,X
        LSR
        ADC     #$08
        STA     (TMP),Y
        DEY
        LDA     #$07
        STA     (TMP),Y
        LDA     #$04
        STA     (TMP),Y
        LDA     ESTKL,X
        INY
        STA     (TMP),Y
        DEY
        LDA     #$06
        STA     (TMP),Y
        LDA     #$04
        STA     (TMP),Y
        INX
        BNE     +
end
asm psgWriteWord(pVIA, reg, val)#0
        LDA     ESTKL+2,X
        STA     TMPL
        LDA     ESTKH+2,X
        STA     TMPH
+       LDY     #$01
        TYA
        CLC
        ADC     ESTKL+1,X
        STA     (TMP),Y
        DEY
        LDA     #$07
        STA     (TMP),Y
        LDA     #$04
        STA     (TMP),Y
        LDA     ESTKH,X
        INY
        STA     (TMP),Y
        DEY
        LDA     #$06
        STA     (TMP),Y
        LDA     #$04
        STA     (TMP),Y
        BNE     +
end
asm psgWrite(pVIA, reg, val)#0
        LDA     ESTKL+2,X
        STA     TMPL
        LDA     ESTKH+2,X
        STA     TMPH
+       LDY     #$01
        LDA     ESTKL+1,X
        STA     (TMP),Y
        DEY
        LDA     #$07
        STA     (TMP),Y
        LDA     #$04
        STA     (TMP),Y
        LDA     ESTKL,X
        INY
        STA     (TMP),Y
        DEY
        LDA     #$06
        STA     (TMP),Y
        LDA     #$04
        STA     (TMP),Y
        INX
        INX
        INX
        RTS
end
asm viaCheck(pVIA)#1
        PHP
        SEI
        LDA     ESTKL,X
        STA     TMPL
        LDA     ESTKH,X
        STA     TMPH
        STX     ESP
        LDX     #$80
        LDY     #$04
        SEC
-       LDA     (TMP),Y
        BMI     +
        DEX
        BNE     -
        TXA                 ; TIMEOUT
        BEQ     ++
+       SBC     (TMP),Y
++      LDX     ESP
        STA     ESTKL,X
        LDA     #$00
        STA     ESTKH,X
        PLP
        RTS
end
//
// Apple II speaker tone generator routines
//
asm a2spkrTone(pitch, duration)#0
        STX     ESP
        LDY     ESTKH,X
        LDA     ESTKL,X
        BEQ     +
        INY
+       STA     DSTL
        STY     DSTH
        LDY     ESTKH+1,X
        LDA     ESTKL+1,X
        BEQ     +
        INY
+       STA     TMPL
        STY     TMPH
        TAX
        LDA     #$FF
        PHP
        SEI
;
; Total loop count is 32 cycles, regardless of path taken
;
-       NOP                 ; 2
        NOP                 ; 2
        BCS     +           ; 3
                            ;---
                            ;+7 = 12 (from BCS below)
+
--      SEC                 ; 2
        DEX                 ; 2
        BNE     ++          ; 2/3
                            ;----
                            ; 6/7

        DEY                 ; 2
        BNE     +++         ; 2/3
                            ;----
                            ;+4/5 = 10/11

        BIT     $C030       ; 4
        LDX     TMPL        ; 3
        LDY     TMPH        ; 3
                            ;---
                            ;+10 = 20

TONELP  SBC     #$01        ; 2
        BCS     -           ; 2/3
                            ;----
                            ; 4/5

        DEC     DSTL        ; 5
        BNE     --          ; 3
                            ;----
                            ;+8 = 12

        DEC     DSTH        ; This sequence isn't accounted for
        BNE     --          ; since it is taken only in extreme cases
        BEQ     TONEXIT

++      NOP                 ; 2
        NOP                 ; 2
                            ;---
                            ;+4 = 11 (from BNE above)

+++     BIT     $C000       ; 4
        NOP                 ; 2
        BPL     TONELP      ; 3
                            ;---
                            ;+9 = 20
TONEXIT PLP
        LDX     ESP
        INX
        INX
        RTS
end
asm a2spkrPCM(rlepcm, pgcnt)#0
        LDA     ESTKL,X
        STA     TMP
        LDA     ESTKL+1,X
        STA     SRCL
        LDA     ESTKH+1,X
        STA     SRCH
        STX     ESP
        PHP
        SEI
        LDY     #$00
PWM_OUT LDA     (SRC),Y     ; 5
        LSR                 ; 2
        LSR                 ; 2
        LSR                 ; 2
        LSR                 ; 2
        TAX                 ; 2
PWM_REP BIT     $C030       ; 4
        LDA     (SRC),Y     ; 5
        AND     #$0F        ; 2
end
asm pwm_on
        STA     ON_BR+1     ; 4
ON_BR   BNE     OFF_BR      ; 3
        !FILL   15, $EA     ; 15 * 2 NOPs
        BIT     $C030       ; 4
        EOR     #$0F        ; 2
end
asm pwm_off
        STA     OFF_BR+1    ; 4
OFF_BR  BNE     ON_BR       ; 3
        !FILL   15, $EA     ; 15 * 2 NOPs
        DEX                 ; 2
        BPL     PWM_REP     ; 3
        INY                 ; 2
        BNE     PWM_OUT     ; 3
        INC     SRCH
        DEC     TMP
        BNE     PWM_OUT
PCMEX   PLP
        LDX     ESP
        INX
        INX
        RTS
end
asm a2keypressed
        INC     $004E       ; rndseed
        BNE     +
        INC     $004F
+       DEX
        LDY     #$00
        BIT     $C000
        BPL     +
        DEY
+       STY     ESTKL,X
        STY     ESTKH,X
end
asm nopBackground#0
        RTS
end
def a3keypressed
    byte count
    byte params[5]

    params.0 = 3
    params.1 = cmdsys.devcons
    params.2 = 5
    params:3 = @count
    syscall($82, @params)
    return count
end
def a3spkrTone(pitch, duration)#0
    byte env

    env = ^ENV_REG
    ^ENV_REG = env | $C0
    a2spkrTone(pitch, duration)
    ^ENV_REG = env
end
def a3spkrPCM(samples, pagecnt)#0
    byte env

    env = ^ENV_REG
    ^ENV_REG = env | $C0
    a2spkrPCM(samples, pagecnt)
    ^ENV_REG = env
end
//
// Search slots for MockingBoard
//
def mbTicklePSG(pVIA)
    //puts("VIA address: $"); puth(pVIA); puts(" Timer Diff = "); puti(viaCheck(pVIA)); putln
    if viaCheck(pVIA) == 8 and viaCheck(pVIA) == 8 // Check twice
        pVIA->IER  = $7F  // Mask all interrupts
        //pVIA->ACR  = $00  // Stop T1 countdown
        pVIA->DDRB = $FF  // Output enable port A and B
        pVIA->DDRA = $FF
        pVIA->IORA = $00 // Reset MockingBoard
        //if pVIA->IORA == $00
        pVIA->IORA = $04 // Inactive MockingBoard control lines
        //if pVIA->IORA == $04
            //
            // At least we know we have some sort of R/W in the ROM
            // address space. Most likely a MockingBoard or John Bell
            // 6522 board. We will assume its a MockingBoard because
            // emulators fail the following PSG read test.
            //
            //psgWriteWord(pVIA, 2, $DA7E)
            //if mbReadP(pVIA, 2) == $7E and mbReadP(pVIA, 3) == $0A
                return pVIA
            //fin
        //fin
    fin
    return 0
end
def mbSearch(slot)
    if slot >= 0 and slot <= 7
        if slot
            mbVIA1 = mbTicklePSG($C000 + (slot << 8))
            if mbVIA1
                mbVIA2 = mbTicklePSG(mbVIA1 + $80)
                return slot
            fin
        else
            for slot = 1 to 7
                if slot == 3 or slot == 6
                    continue
                fin
                mbVIA1 = mbTicklePSG($C000 + (slot << 8))
                if mbVIA1
                    mbVIA2 = mbTicklePSG(mbVIA1 + $80)
                    return slot
                fin
            next
        fin
    fin
    return -1
end
def psgSetup(pVIA)#0
    psgWrite(pVIA,     MIXER,     $3F)   // Turn everything off
    psgWrite(pVIA,     AENVAMP,   $00)
    psgWrite(pVIA,     BENVAMP,   $00)
    psgWrite(pVIA,     CENVAMP,   $10)
    psgWrite(pVIA,     NGFREQ,    $01)
    psgWriteWord(pVIA, ENVPERIOD, $0001)
    psgWrite(pVIA,     ENVSHAPE,  $00)   // Single decay
    psgWriteWord(pVIA, AFREQ,     $0000) // Fast response to update
    psgWriteWord(pVIA, BFREQ,     $0000)
    psgWriteWord(pVIA, CFREQ,     $0000)
    psgWrite(pVIA,     MIXER,     $38)   // Tone on C, B, A
end
//
// Sequence notes through MockingBoard
//
def mbSequence(yield, func)#0
    word period, n, yieldTime
    byte note, volume, channel, i, overflow, status, quit

    //
    // Reset oscillator table
    //
    indexA[0] = 0; indexA[1] = 0
    indexB[0] = 1; indexB[1] = 1
    indexC[0] = 2; indexC[1] = 2
    noteA[0]  = 0; noteA[1]  = 0
    noteB[0]  = 0; noteB[1]  = 0
    noteC[0]  = 0; noteC[1]  = 0
    //
    // Get the PSGs ready
    //
    status = getStatusReg
    disableInts
    mbVIA1->ACR = $40       // Continuous T1 interrupts
    mbVIA1=>T1L = $F9C2     // 16 Ints/sec
    mbVIA1=>T1C = $F9C2     // 16 Ints/sec
    mbVIA1->IFR = $40       // Clear interrupt
    mbVIA1->IER = $C0       // Enable Timer1 interrupt
    psgSetup(mbVIA1)
    if mbVIA2; psgSetup(mbVIA2); fin
    overflow    = 0
    if yield and func
        yieldTime = seqTime + yield
    else
        yieldTime = $7FFF
    fin
    updateTime = seqTime
    quit = FALSE
    repeat
        while eventTime == seqTime
            note = seqEvent->percnote
            if note & $80
                //
                // Note event
                //
                volume  = seqEvent->perchanvol
                channel = (volume & mbVIA2.LSB) >> 7 // Clever - mbVIA2.0 will be $80 if it exists
                if volume & $0F
                    //
                    // Note on
                    //
                    for i = 0 to MAX_MBCH_NOTES-1
                        //
                        // Look for available slot in active note table
                        //
                        if !notes[channel, i].LSB //or notes[channel, i] == note
                            break
                        fin
                    next
                    //
                    // Full note table, kick one out
                    //
                    if i == MAX_MBCH_NOTES
                        i = overflow
                        overflow = (overflow + 1) % MAX_MBCH_NOTES
                    else
                        numNotes++
                    fin
                    notes[channel, i] = note | (volume << 8)
                    periods[channel, i] = mbOctave0[note & $0F] >> ((note >> 4) & $07)
                else
                    //
                    // Note off
                    //
                    for i = 0 to MAX_MBCH_NOTES-1
                        //
                        // Remove from active note table
                        //
                        if notes[channel, i].LSB == note
                            notes[channel, i] = 0
                            numNotes--
                            break
                        fin
                    next
                fin
                updateTime = seqTime
            else
                //
                // Percussion event
                //
                period = seqEvent->perchanvol
                if period
                    if (period & $80)
                        psgWrite(mbVIA1, MIXER,       $1C)   // NG on C, Tone on B, A
                        psgWrite(mbVIA1, CENVAMP,     $10)
                        psgWrite(mbVIA1, ENVSHAPE,   (note >> 4) & $04)
                        psgWrite(mbVIA1, NGFREQ,     (note >> 1) & $1F)
                        psgWrite(mbVIA1, ENVPERIOD+1, period & $7F)
                    elsif mbVIA2
                        psgWrite(mbVIA2, MIXER,       $1C)   // NG on C, Tone on B, A
                        psgWrite(mbVIA2, CENVAMP,     $10)
                        psgWrite(mbVIA2, ENVSHAPE,   (note >> 4) & $04)
                        psgWrite(mbVIA2, NGFREQ,     (note >> 1) & $1F)
                        psgWrite(mbVIA2, ENVPERIOD+1, period)
                    fin
                else
                    if seqRepeat
                        //
                        // Reset sequence
                        //
                        musicPlay(seqTrack, TRUE)
                        seqTime = -1 // Offset seqTime++ later
                    else
                        musicStop
                    fin
                    quit = TRUE // Exit out
                    break
                fin
            fin
            //
            // Next event
            //
            seqEvent  = seqEvent + t_event
            eventTime = seqEvent->deltatime + eventTime
        loop
        if updateTime <= seqTime
            //
            // Time slice active note tables (arpeggio)
            //
            for channel = 0 to 1
                //
                // Multiplex oscillator A
                //
                i = indexA[channel]
                repeat
                    i = (i + 3) % MAX_MBCH_NOTES
                    n = notes[channel, i]
                    if n // Non-zero volume
                        break
                    fin
                until i == indexA[channel]
                if n.LSB <> noteA[channel]
                    psgWriteTone(mbVIAs[channel], AFREQ, periods[channel, i], n.MSB)
                    noteA[channel]  = n.LSB
                    indexA[channel] = i
                fin
                //
                // Multiplex oscillator B
                //
                i = indexB[channel]
                repeat
                    i = (i + 3) % MAX_MBCH_NOTES
                    n = notes[channel, i]
                    if n // Non-zero volume
                        break
                    fin
                until i == indexB[channel]
                if n.LSB <> noteB[channel]
                    psgWriteTone(mbVIAs[channel], BFREQ, periods[channel, i], n.MSB)
                    noteB[channel]  = n.LSB
                    indexB[channel] = i
                fin
                //
                // Multiplex oscillator C
                //
                i = indexC[channel]
                repeat
                    i = (i + 3) % MAX_MBCH_NOTES
                    n = notes[channel, i]
                    if n // Non-zero volume
                        break
                    fin
                until i == indexC[channel]
                if n.LSB <> noteC[channel]
                    psgWrite(mbVIAs[channel],     MIXER,   $38)   // Tone on C, B, A
                    psgWriteTone(mbVIAs[channel], CFREQ,   periods[channel, i], n.MSB)
                    noteC[channel]  = n.LSB
                    indexC[channel] = i
                fin
            next
            updateTime = seqTime + MB_ARPEGGIO - (numNotes >> 2)
        fin
        //
        // Increment time tick
        //
        seqTime++
        while !(mbVIA1->IFR & $40) // Wait for T1 interrupt
            if a2keypressed(); quit = TRUE; break; fin
        loop
        mbVIA1->IFR = $40 // Clear interrupt
        if yieldTime <= seqTime; func()#0; yieldTime = seqTime + yield; fin
    until quit
    psgWrite(mbVIA1, MIXER,   $FF) // Turn everything off
    psgWrite(mbVIA1, AENVAMP, $00)
    psgWrite(mbVIA1, BENVAMP, $00)
    psgWrite(mbVIA1, CENVAMP, $00)
    if mbVIA2
        psgWrite(mbVIA2, MIXER,   $FF)
        psgWrite(mbVIA2, AENVAMP, $00)
        psgWrite(mbVIA2, BENVAMP, $00)
        psgWrite(mbVIA2, CENVAMP, $00)
    fin
    //mbVIA1->ACR = $00 // Stop T1 countdown
    mbVIA1->IER = $7F // Mask all interrupts
    mbVIA1->IFR = $40 // Clear interrupt
    setStatusReg(status)
end
//
// Sequence notes through Apple II speaker
//
def spkrSequence(yield, func)#0
    word period, duration, yieldTime
    byte note, i, n, overflow

    //
    // Start sequencing
    //
    overflow = 0
    if yield and func
        yieldTime = seqTime + yield
    else
        yieldTime = $7FFF
    fin
    updateTime = seqTime
    repeat
        while eventTime == seqTime
            note = seqEvent->percnote
            if note & $80
                //
                // Note event
                //
                if seqEvent->perchanvol & $0F
                    //
                    // Note on
                    //
                    for i = 0 to MAX_SPKR_NOTES-1
                        //
                        // Look for available slot in active note table
                        //
                        if !notes1[i] or note == notes1[i]
                            break
                        fin
                    next
                    if i == MAX_SPKR_NOTES
                        //
                        // Full note table, kick one out
                        //
                        overflow = (overflow + 1) & (MAX_SPKR_NOTES-1)
                        i = overflow
                    elsif !notes1[i]
                        //
                        // Add new note
                        //
                        numNotes++
                    fin
                    notes1[i]   = note
                    periods1[i] = spkrOctave0[note & $0F] >> ((note >> 4) & $07)
                else
                    //
                    // Note off
                    //
                    for i = 0 to MAX_SPKR_NOTES-1
                        //
                        // Remove from active note table
                        //
                        if notes1[i] == note
                            notes1[i] = 0
                            numNotes--
                            break
                        fin
                    next
                fin
            else
                //
                // Percussion event
                //
                if seqEvent->perchanvol
                    //pcmPlay($D000, 0, 64) // Play some random sample as percussion
                else
                    if seqRepeat
                        musicPlay(seqTrack, TRUE)
                    else
                        musicStop
                    fin
                    return
                fin
            fin
            //
            // Next event
            //
            seqEvent  = seqEvent  + t_event
            eventTime = eventTime + seqEvent->deltatime
        loop
        if numNotes > 1
            for i = 0 to MAX_SPKR_NOTES-1
                if notes1[i]
                    tonePlay(periods1[i], arpeggioDuration[numNotes])#0
                fin
            next
            seqTime++
        else
            period = 0
            for i = 0 to MAX_SPKR_NOTES-1
                if notes1[i]
                    period = periods1[i]
                    break;
                fin
            next
            duration = eventTime - seqTime
            seqTime  = duration + seqTime
            tonePlay(period, DUR16TH * duration)#0
        fin
        if yieldTime <= seqTime; func()#0; yieldTime = seqTime + yield; fin
    until keypressed()
end
//
// No sequence, just waste time and yield
//
def noSequence(yield, func)#0
    //
    // Start wasting time
    //
    if !yield or !func
        yield = 0
    fin
    seqTime = 0
    repeat
        seqTime++
        if seqTime < 0; seqTime = 1; fin // Capture wrap-around
        tonePlay(0, DUR16TH) // Waste 16th of a second playing silence
        if keypressed(); return; fin
        if yield == seqTime; func()#0; seqTime = 0; fin
    until FALSE
end
//
// Start sequencing music track
//
export def musicPlay(track, rept)#0
    byte i

    //
    // Select proper sequencer based on hardware
    //
    if mbSlot > 0
        musicSequence = @mbSequence
    else
        musicSequence = @spkrSequence
    fin
    //
    // Zero out active notes
    //
    for i = 0 to MAX_MBCH_NOTES-1; notes1[i] = 0;   notes2[i] = 0;   next
    for i = 0 to MAX_MBCH_NOTES-1; periods1[i] = 0; periods2[i] = 0; next
    //
    // Start sequencing
    //
    seqRepeat  = rept
    seqTrack   = track
    seqEvent   = seqTrack
    seqTime    = 0
    eventTime  = seqEvent->deltatime
    numNotes   = 0
end
//
// Stop sequencing music track
//
export def musicStop#0
    musicSequence = @noSequence
end
//
// Play until keystroke
//
export def musicGetKey(yield, backgroundProc)#1
    char key

    key = 0
    if yield < 1; yield = 1; fin
    if backgroundProc == NULL; backgroundProc = @nopBackground; fin
    if seqTrack
        repeat
            musicSequence(yield, backgroundProc)#0 // Call background proc every half second
            if keypressed()
                key       = getc
                seqRepeat = FALSE
            fin
        until not seqRepeat
    fin
    return key
end
//
// Read sequence onto heap
//
export def musicRead(filename)#1
    byte ref
    word seq, len

    seq = NULL
    ref = fileio:open(filename)
    if ref
        seq = heapalloc(heapavail - 256)
        len = fileio:read(ref, seq, heapmark - seq)
        fileio:close(ref)
        heaprelease(seq + len)
    fin
    return seq
end

when MACHID & MACHID_MODEL
    is MACHID_III
        tonePlay   = @a3spkrTone
        pcmPlay    = @a3spkrPCM
        keypressed = @a3keypressed
        break
    is MACHID_I
        puts("Sound unsupported.\n")
        return -1
        break
    otherwise
        //puts("MockingBoard Slot:\n")
        //puts("ENTER = None\n")
        //puts("0     = Scan\n")
        //puts("1-7   = Slot #\n")
        //instr = gets('>'|$80)
        //if ^instr
        //    mbSlot = mbSearch(^(instr + 1) - '0')
        //fin
        mbSlot = mbSearch(0)
        break
wend
if mbSlot < 0
    //
    // No MockingBoard - scale octave0 for speaker
    //
    for instr = 0 to 11
        spkrOctave0[instr] = mbOctave0[instr]/NOTEDIV
    next
fin
//
// Fixups
//
pwm_on:1  = @pwm_on+4
pwm_off:1 = @pwm_off+4
//
// Keep module in memory
//
done

////////////////////////////////////////////////////////////////////////////////

There are three main externally callable routines in this module:

musicPlay(trackPtr, trackRepeat)
    Start playing a track sequence in the getUpperKey routine
    Params:
        Pointer to a track sequence created from the cvtmidi.py tool
        Repeat flag - TRUE or FALSE.
    The first time its is called, it will try and search for a MockingBoard.
    However, it is noted that this can cause problems if a Z-80 card is installed.
    The scanning routine might cause a hang if it encounters a Z-80 card before
    it finds a MockingBoard. In order to make this robust, it might be best to
    prompt the user to search for the MockingBoard, enter the actual MockingBoard
    slot, or skip the MockingBoard and use the internal speaker.

musicStop()
    Stop playing a track sequence in the getUpperKey routine
        The getUpperKey routine will call a dummy sequence routine that will
        keep the correct timing for any background processing

musicGetKey(yieldtime, yieldproc)
    Wait for a keypress and return the character
        While waiting for the keypress, the track sequence will be played though
        either the MockingBoard (if present) or the internal speaker. Optionally,
        a background function can be called periodically based on the sequencer
        timing, so its pretty accurate.

The low level internal speaker routines used to generate tones and waveforms
can be called for warnings, sound effects, etc:

spkrTone(period, duration)#0
    Play a tone
    Params:
        (1020000 / 64 / period) Hz
        (duration * 32 * 256 / 1020000) seconds

spkrPWM(samples, speed, len)#0
    Play a Pulse Width Modulated waveform
    Params:
        Pointer to 8 bit pulse width samples
        Speed to play through samples
        Length of sample

The main routines for sequencing music are:

mbSequence(yield, func)
spkrSequence(yield, func)
noSequence(yield, func)

    All three try and provide more functionality than would be present in
    previous music sequencers. The MockingBoard sequencer will attempt to play up
    to 9 tones per sound generator (18 if a MockingBoard II is found). Up to
    four notes will be played simultaneously on the internal speaker. In order
    to play more notes than the hardware normally supports, a technique using
    arpeggio (playing multiple notes in a quick sequence rather than concurrently)
    pulls off this feat. The sequencers will immediately return if a keypress is
    detected. Finally, during the sequencing, a background function can be periodically
    called every 'yield' time which has a resolution of a 16th of a second. Pass
    in zero for 'yield' and/or 'func' to disable any background calls.
