;** MIDI data stream interpreter, for inclusion in MIDIsrc file **;

MinMIDIPitch * 12   ; midi pitch value for sound system pitch of zero
MaxMIDIPitch * 96   ; midi pitch value for (nearly) max sound system pitch
MaxSoundPitch * &7000  ; max sound sys pitch (almost). Above this it sounds bad
MinSoundPitch *  &100  ; below this it becomes bbc emulation mode
MaxChannelNumber * 16    ; maximum MIDI channel number
; duration values for note on and note off:
OnDuration * &FF   ; = ever
OffDuration * &1    ; = stop as quickly as possible
ImmediateScheduleTime * 0
Semitone  &  &10000/12   ; = 5461.3333, but actually stored and used as 5461.
; This value is a sound system semitone, left-shifted 4 places to reduce the
; worst-case incremental error when added 11 times to C to reach B.
; This means that the semitone between B and C is approximately 0.07% wider
; (instead of 1.2% wider) than all the other semitones. (This error only
; affects the internal sound system, not the MIDI data). 

GatedZeroAmplitude  * &100
GatedOffAmplitude   * &104 ;zero amplitude doesnt work for wavesynth voice off
SmoothZeroAmplitude * &180

  ALIGN

SoundInterpreter ROUT
;Get byte from receive buffer fifo where the irq routine put it, and
; translate it and control the sound system
; R12 is pointer to workspace
     STMFD   sp!, {r3-r11,lr}
     LDR     r6, [r12, #ModeFlagBits]
; r6 = mode flags
; ensure necessary bits of sound system are present
     TST     r6, #Sound0Present
     TSTNE   r6, #Sound1Present
     LDMEQFD sp!, {r3-r11,pc}^ ; return if not     
     TEQP    pc, #3      ; enable irq
     LDR     r4, [r12, #BufferSoundOutPntr]
; refer to in or out buffer pointer depending on state of connection bit
     LDRB    r9, [r12, #InterpretChannel] ; which buffer is it looking at?
; ensure Port connection number is legal
     MOV     r8, r6, LSR#NExtraPortsShift
     AND     r8, r8, #3
     CMP     r9, r8
     MOVGT   r9, #0
     ADD     r3, r12, r9, LSL#2        ; buffer offset
     TST     R6, #ConnectionBit
     LDRNE   r5, [r3, #TxBufferPntrs]
     LDREQ   r5, [r3, #RxBufferPntrs]
     BIC     r5, r5, #&FF000000   ; buffer in pointer
     BIC     r5, r5, #&00FF0000
     ADD     r3, r12, r9, LSL#2        ; buffer offset
     LDRNE   r11, [r3, #TxBufferStarts]
     LDREQ   r11, [r3, #RxBufferStarts]
; r4 = buffer out pointer
; r5 = buffer in pointer
; r11 = buffer start
GetMore
     CMP     r4, r5   ; Check if buffer has data and get it
     BEQ     BufferEmpty
     LDRB    R2, [r11, R4]    ; Load data from buffer
; r2 = data byte
     ADD     R4, R4, #1      ; increment buffer pointer
     TST     R6, #ConnectionBit
     LDREQ   r8, [r12, #RxBufferLen]
     MOVNE   r8, #TxBufferLen
     CMP     R4, r8
     MOVGE   R4, #0        ; circular buffer, reset pointer
  ; R2 contains byte
  ; R4 and R5 contain buffer in and out pointers
; Intercept system exclusive data quickly, and empty it from buffer.
; start of exclusive is &F0, end of exclusive "EOX" is &F7 or any other
; status byte
     CMP     R2, #&F0   ; system exclusive
     STREQB  R2, [R12, #RunningStatus]
     BEQ     GetMore
     LDRB    R0, [R12, #RunningStatus]  ;get previous status byte
     CMP     R0, #&F0   ; was it system exclusive?
     TSTEQ   R2, #&80   ; if so, is it now a status byte?
     BEQ     GetMore ; if not then empty system excl data quickly from buffer
;NotExclusive
     TST     R2, #&80    ; Status byte ?  Test msb
     BEQ     ReadDataByte  ; if not then read data byte
     AND     R0, R2, #&F8
     CMP     R0, #&F8     ; is it a real time message?
     BEQ     GetMore   ; I dont do anything with system real time here
     AND     R0, R2, #&F0
     CMP     R0, #&F0    ; dont update RS with system common status
     STRNEB  R2, [R12, #RunningStatus]  ; store status byte in runningstatus
     BNE     NotSysMsg
;System common
     MOV     R3, #&1             ; pend for 1 data byte
     CMP     R2, #SongPosPointerVal ; song position pointer status
     MOVEQ   R3, #&2             ; pend for 2 bytes
     CMPNE   R2, #SongSelectVal     ; song select status
     STREQB  r3, [r12, #PendSysMsg] ; pending RS for system message
     CMP     R2, #TuneRequestVal    ; tune request status
     BEQ     TuneRequest  ; ignore "end of system exclusive" flag
     B       GetMore
NotSysMsg
     MOV     r3, #0
     STRB    r3, [r12, #PendSysMsg] ; clear possible sys common pend state
     MOV     R3, #&20    ; default expect 2 bytes
     CMP     R0, #ChannelPressureVal  ; IF Channel pressure
     CMPNE   R0, #ProgramChangeVal    ; OR Program change
     MOVEQ   R3, #&10    ; THEN expect 1 byte
ExpectBytes
     STRB    R3, [R12, #ByteCounter]  ; number of bytes for this status
     B       GetMore
ReadDataByte
     LDRB    r0, [r12, #PendSysMsg] ; check if pending RS for system message
     ANDS    r0, r0, #3
     SUBNE   r0, r0, #1
     STRNEB  r0, [r12, #PendSysMsg]
     BNE     GetMore                ; if pending then ignore this data byte
;If expected byte number has elapsed since status or cleared then do something
  ; R2 contains data byte
     LDRB    R0, [R12, #ByteCounter]  ;bits 3-0 is current byte number
                                      ;bits 7-4 is expected byte number
     ADD     R0, R0, #1   ; increment bytes-received counter
     MOV     R1, R0, LSR#4  ; expected bytes
     AND     R1, R1, #&3
     AND     R3, R0, #&3
     CMP     R1, R3   ; have I got enough data bytes yet?
     BICEQ   R0, R0, #&F ; reset byte counter
     STRB    R0, [R12, #ByteCounter]
     STRNEB  R2, [R12, #FirstDataByte]
     BNE     GetMore
; Now a complete set of bytes should have been got. Translate and output
     CMP     R1, #1
     MOVEQ   R1, R2   ; for 1-byte commands move the byte to R1
     LDRB    R0, [R12, #RunningStatus]
     LDRNEB  R1, [R12, #FirstDataByte]
     AND     R3, R0, #&F0
     CMP     R3, #&F0    ; System message
     BEQ     GetMore   ; ignored
DecodeMode
     AND     R3, R0, #&0F ;channel number; bottom 4 bits of status  =N
     TST     R6, #OmniBit  ; test omni off/on
     BNE     InterpretCommand  ; omni on
     LDRB    R7, [R12, #CurrentChannel]
     TST     R6, #PolyBit  ; Omni off. Test poly/mono
     BEQ     OmniOffMono
     CMP     R3, R7   ; compare command channel with currently set midi channel
     BNE     GetMore     ; if omni off then ignore data not on current channel
     B       InterpretCommand
OmniOffMono
     CMP     R3, R7  ; compare current channel
     BLT     GetMore ; ignore data if channel no. < N
     LDRB    r8, [r12, #MonoChannels] ; no. of channels in mono mode = M-1
     ADD     R9, R7, R8
     CMP     R3, R9  ; compare top of channel range (omni off / mono)
     BGT     GetMore  ;ignore data if channel not in range N..M-1
     SUB     R9, R3, R7            ; voice number-1
     CMP     R9, #MaxVoices
     BGE     GetMore              ; ignore attempts to control voices>8

InterpretCommand
; r1 = data byte 1
; r2 = data byte 2
; r3 = channel num
     MOV     R0, R0, LSR #4    ; top 4bits of status
     AND     R0, R0, #7         ; take 3 bits of status in LSB
     ADD     PC, PC, R0, LSL #2
     B       GetMore    ; dummy instruction
     B       NoteOff
     B       NoteOn              ; channel voice jump table
     B       GetMore    ; PolyKeyPressure is ignored
     B       ChannelMode
     B       ProgramChange
     B       GetMore    ; ChannelPressure is ignored
     B       PitchWheel
     B       GetMore

TuneRequest
     LDR     R0, [R12, #Tuning]
     SWI     XSound_Tuning
     BLVS    SoundSWIError ; abort if v set
     LDMVSFD sp!, {r3-r11,pc}^
     B       GetMore

NoteOff   ;Ignores note off velocity. Labels within NoteOff are prefixed "NOf"
     LDRB    R9, [R12, #VoiceStates]
     MOV     R0, #0
     MOV     R10, #1
     MOV     R8, R1       ; midi pitch
     TST     R6, #PolyBit ; sort out which voice assignment is needed from mode
     BNE     NOfFindVoice ;mode 1 and 3 are same. chnl# has been tested 
; Mono
     TST     R6, #OmniBit
; omni off
     LDREQB  R7, [R12, #CurrentChannel]   ; this is 'N' in MIDI spec 1.0 p5
     SUBEQ   R0, R3, R7            ; voice number-1
NOfFindVoice      ; poly entry
     TST     R9, R10, LSL R0    ; find an ON voice
     BNE     NOfFoundVoice
     ADD     R0, R0, #1
     B       NOfLoopBack
NOfFoundVoice
     ADD     R7, R12, #VoiceMPitches
     LDRB    R7, [R7, R0]
     CMP     R7, R8        ; match pitch of note
     ADDNE   R0, R0, #1
     BNE     NOfLoopBack
     BIC     R9, R9, R10, LSL R0   ; clear voice bit in voice states byte
     ADD     R7, R12, #VoiceSPitches
     LDR     R3, [R7, R0, LSL#2] ; load VoiceSPitches word (16b)
     ADD     R7, R0, #1   ; channel number range 1 to 8
     ORR     R2, R7, #GatedOffAmplitude:SHL:16  ; amplitude / channel
     ORR     R3, R3, #OffDuration:SHL:16  ; duration/pitch
     MOV     r0, r2
     MOV     r1, r3
     SWI     XSound_ControlPacked
     BLVS    SoundSWIError ; abort if v set
     LDMVSFD sp!, {r3-r11,pc}^

     TST     R6, #PolyBit
     BEQ     GetMore         ; exit immediately if mono mode
     MOV     R0, R7           ; restore R0 state
NOfLoopBack
     CMP     R0, #MaxVoices  ; else look for more notes to switch off
     BLT     NOfFindVoice
     STRB    R9, [R12, #VoiceStates]    ; this should be clear now
     B       GetMore

NoteOn                     ; labels within NoteOn are prefixed "NOn"
     CMP     R2, #0
     BEQ     NoteOff    ; Note on with velocity of zero = note off
     MOV     R10, #1
     MOV     R0, #0
     LDRB    R9, [R12, #VoiceStates]  ; byte containing on/off states of voices
     TST     R6, #PolyBit ; sort out which voice assignment is needed from mode
     BNE     NOnFindFreeVoice  ;No need to test omni. channel# is OK
; Mono
     TST     R6, #OmniBit
     BNE     NOnMonoVoiceAssign
     LDRB    R7, [R12, #CurrentChannel]   ; this is 'N' in MIDI spec 1.0 p5
     SUB     R0, R3, R7           ; voice number-1. Range has been tested
     B       NOnMonoVoiceAssign
NOnFindFreeVoice        ;Poly Voice Assign
     TST     R9, R10, LSL R0   ; find free voice (bit=0)
     ADDNE   R0, R0, #1
     BNE     NOnFindFreeVoice ;bit 8 upwards are 0, else possible infinite loop
     CMP     R0, #MaxVoices
     BGE     NoFreeVoice    ; no free voice found
NOnMonoVoiceAssign                      ; mono assign
     CMP     R1, #MaxMIDIPitch
     BGT     NoteRangeError
     CMP     R1, #MinMIDIPitch
     BLE     NoteRangeError ; below &100 it reverts to bbc em. mode
     ORR     R9, R9, R10, LSL R0  ; set bit for chosen voice
     STRB    R9, [R12, #VoiceStates]
     ADD     R7, R12, #VoiceMPitches
     STRB    R1, [R7, R0] ;  store (MIDI) note value in #VoiceMPitches
     SUB     R1, R1, #MinMIDIPitch  ; sound pitch of 0 is midi value 12
     MOV     R3, #0     ; to reduce additive errors and increase accuracy,
NOnFindOctave           ; do calcs left-shifted 4 places
     SUBS    R7, R1, #12       ; MIDI octave = 12
     ADDPL   R3, R3, #&1000:SHL:4   ; sound system octave = &1000
     MOVPL   R1, R7
     BEQ     NOnFoundNote      ; takes branch if note is c
     BPL     NOnFindOctave
     LDR     R7, Semitone
NOnFindNote
     SUBS    R1, R1, #1
     ADDPL   R3, R3, R7
     BPL     NOnFindNote
NOnFoundNote
     MOV     R3, R3, LSR#4    ; shift back 4 places to get correct value
     ADD     R7, R12, #VoiceSPitches
     STR     R3, [R7, R0, LSL#2] ; Store pitch in VoiceSPitches
     LDR     r7, [R12, #ModeFlagBits]
     TST     r7, #TouchSenseOff
     MOVNE   r1, #&7f       ; only touch sensitive if mode bit set
     MOVEQ   R2, R2, LSR#2    ; Ignore bottom 2 bits of velocity
     ANDEQ   R2, R2, #&1F     ; top 5 bits range 0-31
     ADREQ   R7, LogTable
     LDREQB  R1, [R7, R2]     ; log of top 5 bits
     ADD     R7, R12, #VoiceAmps
     STRB    R1, [R7, R0]     ; store note-on amplitude in VoiceAmps
     ORR     R1, R1, #GatedZeroAmplitude  ; gate on. Amp 00 to 7F
     ADD     R0, R0, #1           ; channel range 1 to 8
     ORR     R3, R3, #OnDuration:SHL:16  ; duration/pitch
     ORR     R2, R0, R1, LSL#16   ; amplitude/channel
     MOV     r0, r2
     MOV     r1, r3
     SWI     XSound_ControlPacked
     BLVS    SoundSWIError ; abort if v set
     LDMVSFD sp!, {r3-r11,pc}^
     B       GetMore
ChannelMode
     LDRB    R7, [R12, #CurrentChannel]
     CMP     R3, R7   ; compare command channel with currently set midi channel
     BNE     GetMore  ; if not same channel then ignore mode message
     SUBS    R1, R1, #&7A
     BMI     GetMore
     AND     R1, R1, #7   ; make sure it cant exceed jump table
     ADD     PC, PC, R1, LSL #2
     B       GetMore    ; dummy instruction
     B       AllNotesOff    ; LocalControl does nothing now
     B       AllNotesOff
     B       OmniModeOff
     B       OmniModeOn
     B       MonoModeOn
     B       PolyModeOn
     B       GetMore

OmniModeOff
     TEQP    PC, #I_bit:OR:3  ; disable interrupts for consistent mode state
     LDR     R6, [R12, #ModeFlagBits]
     BIC     R6, R6, #OmniBit   ;   clear bit 1
     STR     R6, [R12, #ModeFlagBits]
     TEQP    PC, #3    ; re-enable interrupts
     B       AllNotesOff

OmniModeOn
     TEQP    PC, #I_bit:OR:3  ; disable interrupts for consistent mode state
     LDR     R6, [R12, #ModeFlagBits]
     ORR     R6, R6, #OmniBit   ;   set bit 1
     STR     R6, [R12, #ModeFlagBits]
     TEQP    PC, #3    ; re-enable interrupts
     B       AllNotesOff

MonoModeOn
     AND     R2, R2, #&F  ; number of channels in mono mode
     CMP     R2, #0
     MOVEQ   R2, #MaxVoices   ; default number of channels = 8
     BEQ     ChannelSet
     CMP     R2, #MaxVoices
     MOVGT   R2, #MaxVoices
     STRB    R2, [R12, #MonoChannels]
ChannelSet
     TEQP    PC, #I_bit:OR:3  ; disable interrupts for consistent mode state
     LDR     R6, [R12, #ModeFlagBits]
     BIC     R6, R6, #PolyBit   ;   clear poly bit
     STR     R6, [R12, #ModeFlagBits]
     TEQP    PC, #3    ; re-enable interrupts
     B       AllNotesOff

PolyModeOn
     TEQP    PC, #I_bit:OR:3  ; disable interrupts for consistent mode state
     LDR     R6, [R12, #ModeFlagBits]
     ORR     R6, R6, #PolyBit   ;   set bit 0
     STR     R6, [R12, #ModeFlagBits]
     TEQP    PC, #3    ; re-enable interrupts

AllNotesOff
     BL      DoAllNotesOff
     B       GetMore

ProgramChange
     ADD     R1, R1, #1
     MOV     r0, #1      ; sound channel number (scans 1-8)
     MOV     R7, #MaxVoices ; in poly mode all voices are changed
     TST     R6, #PolyBit    ;  voice assignment from mode
     BNE     %FT14       ;mode 1 and 3 are same. chnl# has been tested
; Mono mode; only 1 voice is changed
     TST     R6, #OmniBit  ; only voice 1 is changed in omni on / mono mode
     LDREQB  R7, [R12, #CurrentChannel]   ; this is 'N' in MIDI spec 1.0 p5
     SUBEQ   r0, r3, r7       ; voice number-1
     ADDEQ   r0, r0, #1
     MOV     r7, r0
14   MOV     r2, r1
15   MOV     r1, r2
     SWI     XSound_AttachVoice
     ADD     r0, r0, #1
     CMP     r0, r7
     BLE     %BT15
     B       GetMore

PitchWheel  ; Range of pitch wheel is &0000 to &3FFF. Centre position is &2000
     LDRB    R9, [R12, #VoiceStates]
     MOV     R0, #0         ; channel
     AND     R1, R1, #&7F   ; range limit
     AND     R2, R2, #&7F
     ORR     R1, R1, R2, LSL #7 ; combine lsb and msb
     SUB     R10, R1, #&2000
     MOV     R8, #MaxVoices
     TST     R6, #PolyBit ; sort out which voice assignment is needed from mode
     BNE     PWFindVoice   ;No need to test omni. channel# is OK
     TST     R6, #OmniBit  ; Mono
     MOVNE   R8, #1
     LDREQB  R7, [R12, #CurrentChannel]   ; this is 'N' in MIDI spec 1.0 p5
     SUBEQ   R0, R3, R7           ; voice number-1. Range has been tested
     MOVEQ   R8, R0
     B       PWFoundVoice
PWFindVoice  ; poly entry: check each of the 8 voices, if on then change pitch
     MOV     R7, #1
     TST     R9, R7, LSL R0
     BNE     PWFoundVoice
     ADD     R0, R0, #1
     CMP     R0, #MaxVoices
     BLT     PWFindVoice
     B       GetMore     
PWFoundVoice
     ADD     R7, R12, #VoiceAmps
     LDRB    R1, [R7, R0] ; load VoiceAmps byte
     ADD     R7, R12, #VoiceSPitches
     LDR     R2, [R7, R0, LSL#2] ; load VoiceSPitches word (32b)
     ADD     R2, R2, R10, ASR#1  ; change pitch value      :PITCH
     CMP     R2, #MaxSoundPitch  ; switch off note if it exceeds pitch range
     MOVGT   R2, #MaxSoundPitch
     MOVGT   R1, #1              ; reduce to a whisper
     CMP     R2, #MinSoundPitch
     MOVLT   R2, #MinSoundPitch
     MOVLT   R1, #1              ; reduce to a whisper
     ADD     R0, R0, #1      ;                            :CHANNEL
     MOV     R7, R0          ; copy channel number
     ORR     R1, R1, #SmoothZeroAmplitude ;               :AMPLITUDE
     ORR     R3, R2, #OnDuration:SHL:16     ; duration/pitch
     ORR     R2, R0, R1, LSL#16           ; amp/channel

     MOV     r0, r2
     MOV     r1, r3
     SWI     XSound_ControlPacked

     MOV     R0, R7
     CMP     R0, R8
     BLT     PWFindVoice
     B       GetMore     

NoteRangeError
     MOV     R0, #"L"
     STRB    R0, [R12, #ErrorFlag]
     B       GetMore
NoFreeVoice
     MOV     R0, #"V"
     STRB    R0, [R12, #ErrorFlag]
     B       GetMore
BufferEmpty
     STR     R4, [R12, #BufferSoundOutPntr]
     LDMFD   sp!, {r3-r11,pc}^

; conversion table to convert velocity and pressure values into amplitudes for soundsys
LogTable ; NB linear now rather than logarithmic, seems to give a better result with Casio CPS-101
;     = &20,&28,&30,&37, &3e,&45,&4b,&40,  &42,&44,&46,&50, &56,&5b,&61,&65
;     = &5a,&60,&64,&69, &6c,&70,&73,&75,  &77,&79,&7a,&7b, &7c,&7d,&7e,&7f
     = &40,&42,&44,&46, &48,&4A,&4C,&4E,  &50,&52,&54,&56, &58,&5A,&5C,&5E
     = &60,&62,&64,&66, &68,&6A,&6C,&6E,  &70,&72,&74,&76, &78,&7A,&7C,&7E

     ALIGN

  END
