OSBYTE 158, 159; Sound and speech interrupt processing; ROM/PHROM read byte - 983 bytes (5.9%)


§1. Introduction.

 The sound chip has three channels that produce square waves tones, and one noise channel.
 The OS enhances this basic functionality with OSWORD 7 - Make a sound, and OSWORD 8 -
 Define an envelope. These correspond to the BASIC commands SOUND and ENVELOPE. The SOUND
 command simplifies the process of playing regular notes and noises, and adds the ability
 to store a queue of notes to play in advance. The ENVELOPE command allows a wide variety
 of sound effects to be played by varying the pitch and amplitude of a sound every 100th
 of a second as it plays.
 See .osword7EntryPoint.
 See .osword8EntryPoint.

 Sound durations are specified in 1/20ths of a second allowing a single byte to sustain a
 long note up to 12.75 seconds.

 In addition sound is produced by VDU 7 BELL - a surprisingly accurately pitched Treble C.
 See .vdu7EntryPoint.

 Finally, a percussive sound can (in extremis) be produced by setting and resetting the
 cassette relay. See .osbyte137EntryPoint. This is a mechanically generated sound, not
 produced via the internal loudspeaker.

 Many games such as Arcadians (Aardvark, 1983) and Zalaga (Acornsoft, 1982) that sought to
 emulate arcade games used the sound capabilities to create 'musical stings' and other
 effects.

§2. Silence a sound channel.

 On Entry:
       X = buffer number (4-7) for channel (0-3)
.silenceChannelX = $eb03
    LDA #4                                              mark end of release phase
    STA .channel0PhaseCounter - .bufferNumberSound0,X   to channel X
    LDA #$C0                                            code for zero volume
    fall through...

§3. Set volume for channel X.

 On Entry:
       A = volume, where $3F (loudest) to $C0 (silent) - see table below
       X = buffer number (4-7) for channel (0-3)
.setChannelXVolume = $eb0a
    STA .channel0Volume - .bufferNumberSound0,X         store A to give basic sound level
    LDY .soundDisableFlag                               get sound output/enable flag
    BEQ +                                               if (sound enabled) then branch
    LDA #$C0                                            code for zero volume
+
    we now convert the input volume into the value required to write to the sound chip
    SEC                                                 set carry
    SBC #$40                                            subtract $40
    LSR                                                 } divide by 8
    LSR                                                 }
    LSR                                                 }
    EOR #$0F                                            invert bits 0-3

    BASIC SOUND   volume on entry     accumulator
    volume        .channel0Volume     result now
     -15                $3F              $10          loudest
     -14                $37              $11
     -13                $2F              $12
     -12                $27              $13
     -11                $1F              $14
     -10                $17              $15
      -9                $0F              $16
      -8                $07              $17
      -7                $FF              $18
      -6                $F7              $19
      -5                $EF              $1A
      -4                $E7              $1B
      -3                $DF              $1C
      -2                $D7              $1D
      -1                $CF              $1E
       0                $C7              $1F          silent
       -                $C0              $1F          silent

    ORA .soundParameterTable - .bufferNumberSound0,X    get encoded value for channel into
                                                        top nybble
    ORA #$10                                            ensure bit 4 set (set volume)

.sendToSoundChip = $eb21
    PHP                                                 
.sendToSoundChipFlagsAreadyPushed = $eb22
    SEI                                                 disable interrupts
    LDY #$FF                                            }
    STY .systemVIADataDirectionRegisterA                } set data direction to all outputs
    STA .systemVIARegisterANoHandshake                  send data byte to sound chip
    INY                                                 Y=0
    STY .systemVIARegisterB                             set the write enable line low
                                                        (active)
                                                        to let the sound chip know there
                                                        is data
    LDY #2                                              Y = loop counter
-
    DEY                                                 execute a short delay (this is to
    BNE -                                               keep the write enable line low for
                                                        at least 8 us, 16 cycles. This is
                                                        required by the sound chip hardware)
                                                        
                                                        [An alternative to this loop that
                                                        takes the same number of cycles, but
                                                        is two bytes shorter would be to JSR
                                                        to an (existing) RTS instruction.]

    LDY #8                                              }
    STY .systemVIARegisterB                             } pull the write enable line high
                                                        } (inactive)

    LDY #4                                              Y = loop counter
-
    DEY                                                 execute another short delay loop
    BNE -                                               [it's longer this time for some
                                                        reason? It seems like this loop is
                                                        not needed at all to me.]

    PLP                                                 get back flags
    RTS                                                 

§4. soundParameterTable.

 Table to convert channel number to the bits required by the chip
.soundParameterTable = $eb40
    !byte $E0,$C0,$A0,$80

§5. skipToNextChannelLocal.

.skipToNextChannelLocal = $eb44
    JMP .skipToNextChannel                              go to next channel

§6. Update sounds in the 100Hz timer interrupt.

.processSoundInterrupt = $eb47
    LDA #0                                              
    STA .numberOfSoundChannelsOnHold                    zero number of channels on hold for
                                                        sync
    LDA .soundSyncCount                                 get sync count
    BNE +                                               if (this is not zero) then branch
    INC .numberOfSoundChannelsOnHold                    number of channels on hold for sync
                                                        = 1
    DEC .soundSyncCount                                 number of channels required for sync
                                                        = 255
+
    LDX #8                                              set loop counter (X = buffer number
                                                        4-7, corresponding to the channel
                                                        number 0-3)
.processSoundChannelLoop = $eb59
    DEX                                                 loop counter (X=7,6,5,4)
    LDA .channel0Occupancy - .bufferNumberSound0,X      get (sound queue occupancy)
    BEQ .skipToNextChannelLocal                         if (nothing playing on this channel)
                                                        then branch (check next channel)
    LDA .bufferEmptyFlags,X                             get buffer empty flag
    BMI .chooseNextSound                                if (negative, i.e. buffer empty)
                                                        then branch (choose the next sound
                                                        to play)
    LDA .channel0Duration - .bufferNumberSound0,X       get duration remaining
    BNE .continueExistingSound                          if (duration remaining > 0) then
                                                        branch (continue processing current
                                                        sound)
.chooseNextSound = $eb69
    JSR .checkForNextSoundToPlay                        check and pick up new sound if
                                                        required
.continueExistingSound = $eb6c
    LDA .channel0Duration - .bufferNumberSound0,X       get duration of current sound
    BEQ .skipUpdateOfCurrentSound                       if (duration is zero) then branch
                                                        (skip update)
    CMP #$FF                                            check for $FF duration (infinite
                                                        duration)
    BEQ .notAtEndOfCurrentSoundDuration                 if (infinite duration) then branch
                                                        (not at end of current sound)

    decrement 20Hz counter
    DEC .channel0Countdown20Hz - .bufferNumberSound0,X  decrement 10 mS count
    BNE .notAtEndOfCurrentSoundDuration                 if (counter not reached zero) then
                                                        branch (not at end of current sound)

    reset 20Hz counter
    LDA #5                                              reset to 5
    STA .channel0Countdown20Hz - .bufferNumberSound0,X  to give 50 mSec delay

    decrement duration at 20Hz
    DEC .channel0Duration - .bufferNumberSound0,X       and decrement main counter
    BNE .notAtEndOfCurrentSoundDuration                 if (duration is non-zero) then
                                                        branch (not at end of current sound)

.skipUpdateOfCurrentSound = $eb84
    JSR .checkForNextSoundToPlay                        check and get new sound

.notAtEndOfCurrentSoundDuration = $eb87
    LDA .channel0StepCountdownProgress - .bufferNumberSound0,X  check the step progress
    BEQ +                                                       if (step progress counter is
                                                                zero) then branch (don't
                                                                decrement step progress)
    DEC .channel0StepCountdownProgress - .bufferNumberSound0,X  decrement step progress
    BNE .skipToNextChannelLocal                                 if (step progress counter is
                                                                zero) then branch (check
                                                                next channel)

+
    LDY .channel0EnvelopeOffset - .bufferNumberSound0,X get envelope data offset
    CPY #$FF                                            check for no envelope active on this
                                                        channel
    BEQ .skipToNextChannelLocal                         if (no envelope active) then branch
                                                        (check next channel)

    LDA .envelopeBuffer,Y                               get step length (first byte of
                                                        envelope data)
    AND #%01111111                                      clear the repeat bit
    STA .channel0StepCountdownProgress - .bufferNumberSound0,X  store it
    LDA .channel0PhaseCounter - .bufferNumberSound0,X   get current phase in ADSR
    CMP #4                                              check for release phase complete
    BEQ .adjustEnvelopePitch                            if (release phase completed) then
                                                        branch

    set target amplitude for new phase
    LDA .channel0PhaseCounter - .bufferNumberSound0,X   start new step by getting current
                                                        phase
    CLC                                                 
    ADC .channel0EnvelopeOffset - .bufferNumberSound0,X add it to envelope offset
    TAY                                                 transfer to Y
    LDA .envelopeBuffer + 11,Y                          and get target value at end of
                                                        current phase
    SEC                                                 
    SBC #$3F                                            
    STA .targetAmplitude                                store modified number as current
                                                        target amplitude

    set amplitude step for new phase
    LDA .envelopeBuffer + 7,Y                           get change of amplitude for current
                                                        phase
    STA .currentAmplitudeStep                           store as current amplitude step
                                                        change

    calculate updated volume
    LDA .channel0Volume - .bufferNumberSound0,X         get current volume level
    PHA                                                 save it
    CLC                                                 clear carry
    ADC .currentAmplitudeStep                           add current amplitude step change
    BVC .skipToggleBits                                 if (no overflow) then branch
    ROL                                                 carry = bit 7. If new amplitude is
                                                        negative (relatively quiet) then set
                                                        the maximum volume
    LDA #$3F                                            A=$3F (loudest volume)
    BCS .skipToggleBits                                 if (carry set) then branch
    EOR #$FF                                            toggle bits (A=$C0, silent)

.skipToggleBits = $ebcf
    STA .channel0Volume - .bufferNumberSound0,X         store in current volume
    ROL                                                 }
    EOR .channel0Volume - .bufferNumberSound0,X         } take the EOR of bits 6 and 7
    BPL .updateAmplitude                                if (bits 6 and 7 of volume are
                                                        equal) then branch (to update
                                                        amplitude)
    LDA #$3F                                            }
    BCC +                                               } if (carry clear) then branch
                                                        } (A=$3F); otherwise A=$C0
    EOR #$FF                                            }
+
    STA .channel0Volume - .bufferNumberSound0,X         this is stored in current volume

.updateAmplitude = $ebe1
    DEC .currentAmplitudeStep                           decrement amplitude change per step
    LDA .channel0Volume - .bufferNumberSound0,X         get volume again
    SEC                                                 set carry
    SBC .targetAmplitude                                subtract target value
    EOR .currentAmplitudeStep                           check against step value: a negative
                                                        result indicates correct trend
    BMI +                                               so jump to next part

    enter new amplitude phase
    LDA .targetAmplitude                                
    STA .channel0Volume - .bufferNumberSound0,X         set target amplitude
    INC .channel0PhaseCounter - .bufferNumberSound0,X   increment phase counter
+
    PLA                                                 get the old volume level
    EOR .channel0Volume - .bufferNumberSound0,X         and compare with the new volume
    AND #$F8                                            mask out lower bits
    BEQ .adjustEnvelopePitch                            if (they are the same) then branch
                                                        (volume doesn't need changing)
    LDA .channel0Volume - .bufferNumberSound0,X         }
    JSR .setChannelXVolume                              } set new volume level

.adjustEnvelopePitch = $ec07
    LDA .channel0Section - .bufferNumberSound0,X        get current pitch section (0-2)
    CMP #3                                              check if we have reached section 3
    BEQ .skipToNextChannel                              if (current section is 3) then
                                                        branch (skip rest of loop as all
                                                        sections are finished)
    LDA .channel0SectionCountdownProgress - .bufferNumberSound0,X   check progress within
                                                                    current pitch section
    BNE .countdownTimerNotFinished                      if (countdown timer is not yet zero)
                                                        then branch

    increment pitch section
    INC .channel0Section - .bufferNumberSound0,X        implement a section change
    LDA .channel0Section - .bufferNumberSound0,X        check if it's complete
    CMP #3                                              
    BNE +                                               if (not complete) then branch

    reached end of all three pitch sections
    LDY .channel0EnvelopeOffset - .bufferNumberSound0,X }
    LDA .envelopeBuffer,Y                               } get first envelope byte (top bit =
                                                        } repeat pitch envelope)
    BMI .skipToNextChannel                              if (negative, i.e. don't repeat
                                                        pitch envelope) then branch
    LDA #0                                              }
    STA .channel0PitchOffset - .bufferNumberSound0,X    } reset to first pitch section
    STA .channel0Section - .bufferNumberSound0,X        }

+
    LDA .channel0Section - .bufferNumberSound0,X        get number of steps in new section
    CLC                                                 
    ADC .channel0EnvelopeOffset - .bufferNumberSound0,X 
    TAY                                                 
    LDA .envelopeBuffer + 4,Y                           get envelope byte at offset 4
                                                        (number of steps in first pitch
                                                        section)
    STA .channel0SectionCountdownProgress - .bufferNumberSound0,X   
    BEQ .skipToNextChannel                              if (zero) then branch (skip forwards)

.countdownTimerNotFinished = $ec3d
    DEC .channel0SectionCountdownProgress - .bufferNumberSound0,X   decrement countdown timer
    LDA .channel0EnvelopeOffset - .bufferNumberSound0,X get rate of pitch change
    CLC                                                 
    ADC .channel0Section - .bufferNumberSound0,X        add section number
    TAY                                                 
    LDA .envelopeBuffer + 1,Y                           get envelope byte 1+section (current
                                                        change of pitch)
    CLC                                                 
    ADC .channel0PitchOffset - .bufferNumberSound0,X    add to current pitch offset
    STA .channel0PitchOffset - .bufferNumberSound0,X    save updated pitch offset
    CLC                                                 
    ADC .channel0BasePitch - .bufferNumberSound0,X      add base pitch
    JSR .setPitch                                       set new pitch

.skipToNextChannel = $ec59
    CPX #4                                              
    BEQ +                                               if (X=4, i.e. last channel) then
                                                        branch (return)
    JMP .processSoundChannelLoop                        do loop again

§7. clearSoundChannels.

.clearSoundChannels = $ec60
    LDX #8                                              X=8, loop counter
-
    DEX                                                 } Loop from X=7 to X=4 inclusive
    JSR .clearSoundChannelBuffer                        } clearing each sound buffer
    CPX #4                                              }
    BNE -                                               }
+
    RTS                                                 

§8. checkForNextSoundToPlay.

.checkForNextSoundToPlay = $ec6b
    LDA .channel0PhaseCounter - .bufferNumberSound0,X   get current phase counter
    CMP #4                                              is it 'release complete' state
    BEQ +                                               if (release complete) then branch
    LDA #3                                              mark release in progress
    STA .channel0PhaseCounter - .bufferNumberSound0,X   store it
+
    LDA .bufferEmptyFlags,X                             check buffer empty flag
    BEQ .skipResetAsBufferIsNotEmpty                    if (buffer not empty) then branch
    LDA #0                                              mark buffer not empty
    STA .bufferEmptyFlags,X                             an store it

    LDY #4                                              Y = loop counter
-
    STA .channel0SyncFlag - 1,Y                         zero sync bytes
    DEY                                                 
    BNE -                                               until Y=0

    STA .channel0Duration - .bufferNumberSound0,X       zero duration count
    DEY                                                 
    STY .soundSyncCount                                 reset sync count to $FF (no sync)

.skipResetAsBufferIsNotEmpty = $ec90
    LDA .channel0SyncFlag - .bufferNumberSound0,X       get synchronising flag
    BEQ .syncSounds                                     if (it's zero) then branch
    LDA .numberOfSoundChannelsOnHold                    get number of channels on hold
    BEQ .silenceNonEnvelopeSound                        if (it's zero) then branch (silence
                                                        sound)
    LDA #0                                              
    STA .channel0SyncFlag - .bufferNumberSound0,X       zero synchronising flag
.readNextSoundFromBufferAndProcessLocal = $ec9f
    JMP .readNextSoundFromBufferAndProcess              

§9. clearSoundChannelBuffer.

.clearSoundChannelBuffer = $eca2
    JSR .silenceChannelX                                silence the channel
    TYA                                                 Y=0 A=0
    STA .channel0Duration - .bufferNumberSound0,X       zero main count
    STA .bufferEmptyFlags,X                             mark buffer not empty
    STA .channel0Occupancy - .bufferNumberSound0,X      mark channel dormant
    LDY #3                                              Y=loop counter
-
    STA .channel0SyncFlag,Y                             }
    DEY                                                 } zero sync flags
    BPL -                                               }

    STY .soundSyncCount                                 reset sync to $FF (no sync)
    BMI .setPitchChanged                                ALWAYS branch

§10. syncSoundBufferEmpty.

.syncSoundBufferEmpty = $ecbc
    PHP                                                 save flags
    SEI                                                 and disable interrupts
    LDA .channel0PhaseCounter - .bufferNumberSound0,X   get ADSR phase counter
    CMP #4                                              check for end of release phase
    BNE +                                               if (not end of release phase) then
                                                        branch
    JSR .osbyte152EntryPoint                            examine buffer
    BCC +                                               if (not empty, i.e. there's another
                                                        sound to play) then branch
    LDA #0                                              }
    STA .channel0Occupancy - .bufferNumberSound0,X      } mark channel as silent
+
    PLP                                                 get back flags
    fall through...

§11. silenceNonEnvelopeSound.

.silenceNonEnvelopeSound = $ecd0
    LDY .channel0EnvelopeOffset - .bufferNumberSound0,X this value is $FF if no envelope is
                                                        defined
    CPY #$FF                                            
    BNE .exit25                                         if (envelope defined) then branch
    JSR .silenceChannelX                                silence channel
.exit25 = $ecda
    RTS                                                 

§12. syncSounds.

.syncSounds = $ecdb
    JSR .osbyte152EntryPoint                            examine next byte in buffer (sets
                                                        carry if buffer empty)
    BCS .syncSoundBufferEmpty                           if (buffer empty) then branch
    AND #3                                              get just bits 0 and 1
    BEQ .readNextSoundFromBufferAndProcessLocal         if (zero) then branch
    LDA .soundSyncCount                                 get synchronising count
    BEQ +                                               if (zero, i.e. nothing to sync) then
                                                        branch

    INC .channel0SyncFlag - .bufferNumberSound0,X       set sync flag
    BIT .soundSyncCount                                 check bit 7 is clear
    BPL ++                                              if (sync required) then branch
    JSR .osbyte152EntryPoint                            get first byte from buffer
    AND #3                                              get just bits 0 and 1
    STA .soundSyncCount                                 store result
    BPL +                                               ALWAYS branch

++
    DEC .soundSyncCount                                 
+
    JMP .silenceNonEnvelopeSound                        silence the channel if envelope not
                                                        in use

§13. setPitch.

.setPitch = $ed01
    CMP .channel0Pitch - .bufferNumberSound0,X          check against pitch value
    BEQ .exit25                                         if (pitch is equal) then branch
                                                        (exit)
.setPitchChanged = $ed06
    STA .channel0Pitch - .bufferNumberSound0,X          store new pitch
    CPX #.bufferNumberSound0                            check for noise channel
    BNE .setPitchNotNoise                               if (X is not noise channel) then
                                                        branch (not noise)

    setting noise
    AND #$0F                                            noise value
    ORA .soundParameterTable - .bufferNumberSound0,X    convert to chip format, by adding
                                                        the value required for channel
    PHP                                                 save flags
    JMP .localSendToSoundChipFlagsAreadyPushed          and pass to chip control routine

§14. Set the pitch of a sound.

 On Entry:
       A = pitch (0-255).
.setPitchNotNoise = $ed16
    PHA                                                 remember A = pitch (0-252)
    AND #3                                              
    STA .fractionalSemitones                            store the fractional amount (0-3)
                                                        between semitones

    calculate the octave
    LDA #0                                              
    STA .soundPitchLow                                  octave
    PLA                                                 get back A = original pitch value
    LSR                                                 }
    LSR                                                 } divide by 4 (to get semitone count
                                                        } 0-63)
-
    CMP #12                                             
    BCC .finishedGettingOctave                          if (remainder is less than 12) then
                                                        branch (we have found the right
                                                        octave)
    INC .soundPitchLow                                  increment octave
    SBC #12                                             subtract 12 for each octave
    BNE -                                               if (not zero) then branch (loop back)

    at this point .soundPitchLow defines the Octave (0-5)
    A = the semitone within the octave

    get the 10 bit pitch value for the note within the octave from the pitch lookup tables
.finishedGettingOctave = $ed2f
    TAY                                                 Y = semitone within octave
    LDA .soundPitchLow                                  get octave number into A
    PHA                                                 push it
    LDA .pitchLookupTableLow,Y                          get semitone byte from first pitch
                                                        table
    STA .soundPitchLow                                  store it
    LDA .pitchLookupTableHigh,Y                         get semitone byte from second pitch
                                                        table
    PHA                                                 push second table byte
    AND #3                                              keep two least significant bits only
    STA .soundPitchHigh                                 save them
    PLA                                                 pull original second table byte
    LSR                                                 }
    LSR                                                 }
    LSR                                                 } shift high nybble down into low
                                                        } nybble
    LSR                                                 }
    STA .soundFractional                                store it (amount to subtract for
                                                        each fraction (0-3) between
                                                        semitones)

    adjust for any eighth tones (0-3) between semitones
    LDA .soundPitchLow                                  
    LDY .fractionalSemitones                            loop counter (0-3)
    BEQ ++                                              

-
    SEC                                                 }
    SBC .soundFractional                                } soundFractional is subtracted from
                                                        } A each time around the loop
    BCS +                                               
    DEC .soundPitchHigh                                 decrement high byte of pitch as
                                                        needed
+
    DEY                                                 decrement loop counter
    BNE -                                               if (not done) then branch (loop back)

++
    STA .soundPitchLow                                  store result (low byte of pitch)

    halve the 10-bit pitch value (.soundPitchLow/High) for each octave
    PLA                                                 recall octave number
    TAY                                                 Y = loop counter = octave number
    BEQ +                                               
-
    LSR .soundPitchHigh                                 }
    ROR .soundPitchLow                                  } halve soundVariableA/B
    DEY                                                 
    BNE -                                               if (not done yet) then branch (loop
                                                        back)

+
    LDA .soundPitchLow                                  

    Offset the pitch slightly depending on the channel. Perhaps to avoid
    phase shift effects when two or more channels play the same pitch?
    CLC                                                 
    ADC .soundPitchOffsetByChannelTable - .bufferNumberSound0,X 
    STA .soundPitchLow                                  
    BCC +                                               
    INC .soundPitchHigh                                 

    .soundPitchLow/High now holds the 10 bit pitch we want
    we adjust to get the exact bytes to send to the sound chip
+
    AND #$0F                                            frequency value (low four bits)
    ORA .soundParameterTable - .bufferNumberSound0,X    convert to chip format, by adding
                                                        the value required for channel

    PHP                                                 push flags
    SEI                                                 disable interrupts
    JSR .sendToSoundChip                                send first byte to sound chip

    LDA .soundPitchLow                                  calculate the second byte to send
                                                        to the sound chip
    LSR .soundPitchHigh                                 }
    ROR                                                 }
    LSR .soundPitchHigh                                 } divide by 4
    ROR                                                 }

    LSR                                                 }
    LSR                                                 } shift byte right twice

    now A holds the second byte we want to send to the sound chip
.localSendToSoundChipFlagsAreadyPushed = $ed95
    JMP .sendToSoundChipFlagsAreadyPushed               send second byte to sound chip

§15. Read the next sound from the SOUND buffer and act on it.

 Each entry in the sound buffer is three bytes:

   byte 0: envelope-volume-hold-sync byte:
               bit 7     = 0 for an envelope number
               bits 3-6  = envelope number - 1 (0-3), or volume in range (15 to 0)
               bit 2     = h, the hold bit
               bits 0-1  = ss, the sync bits (0-3)
   byte 1: pitch
   byte 2: duration

 See .osword7EntryPoint where these bytes are added to the buffer.
.readNextSoundFromBufferAndProcess = $ed98
    PHP                                                 push flags
    SEI                                                 disable interrupts
    JSR .osbyte145EntryPoint                            read first byte from buffer
                                                        (envelope-volume-hold-sync byte)

    check hold bit
    PHA                                                 push A
    AND #4                                              isolate the hold bit
    BEQ .playNewSound                                   if (hold not set) then branch
    PLA                                                 get back A

    hold required
    LDY .channel0EnvelopeOffset - .bufferNumberSound0,X get envelope offset
    CPY #$FF                                            check if envelope is in use
    BNE +                                               if (envelope in use) then branch

    no envelope in use, silence channel
    JSR .silenceChannelX                                so call to silence channel

+
    JSR .osbyte145EntryPoint                            get (and ignore) pitch byte
    JSR .osbyte145EntryPoint                            get the duration
    PLP                                                 get back flags
    JMP .setDurationAndExit                             set duration, exit

.playNewSound = $edb7
    PLA                                                 get back A
    AND #%11111000                                      zero bits 0-2
    ASL                                                 put bit 7 into carry
    BCC +                                               if (zero, envelope) then branch

    extract and set volume
    EOR #$FF                                            }
    LSR                                                 } convert volume number into
    SEC                                                 } an expected value for the
    SBC #$40                                            } volume routine
    JSR .setChannelXVolume                              and set volume

    set no envelope in use
    LDA #$FF                                            A=$FF
+
    set envelope offset = envelope number * 16 (or $FF for no envelope)
    STA .channel0EnvelopeOffset - .bufferNumberSound0,X get envelope number-1 *16 into A

    restart the channel's 20Hz timer
    LDA #5                                              set duration sub-counter
    STA .channel0Countdown20Hz - .bufferNumberSound0,X  

    set phase 1, the start of the sound
    LDA #1                                                      set phase counter
    STA .channel0StepCountdownProgress - .bufferNumberSound0,X  

    reset section countdown, phase counter, and pitch offset
    LDA #0                                                          
    STA .channel0SectionCountdownProgress - .bufferNumberSound0,X   reset pitch section
                                                                    countdown progress
    STA .channel0PhaseCounter - .bufferNumberSound0,X               reset pitch phase
    STA .channel0PitchOffset - .bufferNumberSound0,X                reset pitch offset

    set section to $FF
    LDA #$FF                                            
    STA .channel0Section - .bufferNumberSound0,X        

    store pitch
    JSR .osbyte145EntryPoint                            read pitch
    STA .channel0BasePitch - .bufferNumberSound0,X      set it

    read duration
    JSR .osbyte145EntryPoint                            read duration
    PLP                                                 
    PHA                                                 save duration

    set pitch
    LDA .channel0BasePitch - .bufferNumberSound0,X      get back pitch value
    JSR .setPitch                                       and set it

    set duration
    PLA                                                 get back duration
.setDurationAndExit = $edf7
    STA .channel0Duration - .bufferNumberSound0,X       set duration
    RTS                                                 

§16. Pitch tables.

 These next two tables are used to convert the 8 bit pitch (0-255) of a sound value passed
 to the OS (e.g. via OSWORD 7, see .osword7EntryPoint) into the 10-bit integer pitch value
 required by the sound chip.

 When given a 10 bit pitch value, the chip outputs a tone with frequency:

       frequency (in Hz) = 4,000,000 / (32 * pitch)

 The first table provides the lower 8 bits and the second table (bits 0-1) provide the two
 high bits:

        low byte      10 bit
        +bits 0,1     chip      actual      ideal
 note   high byte     pitch     frequency   frequency
 -------------------------------------------------------------------------
   B    $3F0       =  1008      124.01Hz    123.47Hz
   C    $3B7       =   951      131.44Hz    130.81Hz
   C#   $382       =   898      139.20Hz    138.59Hz
   D    $34F       =   847      147.58Hz    146.83Hz
   D#   $320       =   800      156.25Hz    155.56Hz
   E    $2F3       =   755      165.56Hz    164.81Hz
   F    $2C8       =   712      175.56Hz    174.61Hz
   F#   $2A0       =   672      186.01Hz    185.00Hz
   G    $27B       =   635      196.85Hz    196.00Hz
   G#   $257       =   599      208.68Hz    207.65Hz
   A    $235       =   565      221.24Hz    220.00Hz
   A#   $216       =   534      234.08Hz    233.08Hz

 The upper nybble (bits 4-7) of the .pitchLookupTableHigh stores the amount to reduce the
 10 bit pitch value for each quarter (0-3) between two adjacent semitones. Bits 2-3 are
 unused.
.pitchLookupTableLow = $edfb
    !byte $F0                                           B
    !byte $B7                                           C
    !byte $82                                           C#
    !byte $4F                                           D
    !byte $20                                           D#
    !byte $F3                                           E
    !byte $C8                                           F
    !byte $A0                                           F#
    !byte $7B                                           G
    !byte $57                                           G#
    !byte $35                                           A
    !byte $16                                           A#

§17. pitchLookupTableHigh.

.pitchLookupTableHigh = $ee07
    !byte $E7                                           B
    !byte $D7                                           C
    !byte $CB                                           C#
    !byte $C3                                           D
    !byte $B7                                           D#
    !byte $AA                                           E
    !byte $A2                                           F
    !byte $9A                                           F#
    !byte $92                                           G
    !byte $8A                                           G#
    !byte $82                                           A
    !byte $7A                                           A#

[ More accurate values for these two tables would be as follows:

.pitchLookupTableLow
   !byte $F3                                           ; B
   !byte $BB                                           ; C
   !byte $85                                           ; C#
   !byte $52                                           ; D
   !byte $23                                           ; D#
   !byte $F5                                           ; E
   !byte $CB                                           ; F
   !byte $A3                                           ; F#
   !byte $7D                                           ; G
   !byte $59                                           ; G#
   !byte $37                                           ; A
   !byte $17                                           ; A#

.pitchLookupTableHigh
   !byte $E7                                           ; B
   !byte $D7                                           ; C
   !byte $DB                                           ; C#   <-- this is the only
   !byte $C3                                           ; D        change to this table
   !byte $B7                                           ; D#
   !byte $AA                                           ; E
   !byte $A2                                           ; F
   !byte $9A                                           ; F#
   !byte $92                                           ; G
   !byte $8A                                           ; G#
   !byte $82                                           ; A
   !byte $7A                                           ; A#
]

§18. PHROM numbers are $F0-$FF.

.setInitialSpeechPHROMNumber = $ee13
    LDA #$EF                                            first PHROM number is $F0 (A=$EF is
                                                        one less since it will be
                                                        incremented before being accessed)
    STA .currentSpeechPHROMOrROMNumber                  store it
    RTS                                                 

§19. readByteFromROMOrPHROM.

.readByteFromROMOrPHROM = $ee18
    LDX #.romServiceCallROMFilingSystemInitialize       X=paged ROM service call for ROM
                                                        filing system initialise
    INC .currentSpeechPHROMOrROMNumber                  
    LDY .currentSpeechPHROMOrROMNumber                  get ROM number
    BPL .romServiceCall                                 if (non-negative, i.e. it's a Paged
                                                        ROM) then branch

    PHROM found (top bit of ROM number set)
    LDX #0                                              }
    STX .romAddressHigh                                 }
    INX                                                 } set address pointer in PHROM to
                                                        } $0001
    STX .romAddressLow                                  }
    JSR .writeAddressAndROMNumberToSpeechProcessor      pass info to speech processor

    Check that we have a copyright string "(C)" in the PHROM
    LDX #3                                              X = loop counter
-
    JSR .readByteFromPHROM                              read byte from PHROM
    CMP .copyrightString,X                              check if it's the copyright string
    BNE .readByteFromROMOrPHROM                         if (no match) then branch (try next
                                                        ROM)
    DEX                                                 decrement loop counter
    BPL -                                               loop back until done

    LDA #$3E                                            ROM address is $003E, to read the
                                                        'end of speech data' address (See
                                                        Speech User Guide, Page 27)
    STA .romAddressLow                                  set low byte of address

.readFromPHROM = $ee3b
    JSR .writeAddressAndROMNumberToSpeechProcessor      pass info to speech processor

    LDX #$FF                                            X = loop counter (to read two bytes)
.loopNextByte = $ee40
    JSR .readByteFromPHROM                              check for speech proc. etc.

    Store the byte read into .romAddressLow/High, reversing all the bits
    LDY #8                                              Y = loop counter Y (to read 8 bits)
-
    ASL                                                 shift byte from PHROM
    ROR .romAddressHigh,X                               rotate into memory, reversing order
                                                        of the bits
    DEY                                                 into .romAddressLow (X=255) and
                                                        .romAddressHigh (X=0)
    BNE -                                               loop eight times

    INX                                                 increment X to zero (first loop
                                                        iteration) or one (second loop
                                                        iteration)
    BEQ .loopNextByte                                   if (X=0, first time around loop)
                                                        then branch (loop back to read
                                                        another byte)

    CLC                                                 
    BCC .writeAddressAndROMNumberToSpeechProcessor      ALWAYS branch

§20. readByteFromROMFSorPHROM.

 Read from ROM Filing System or Speech PHrase ROM
.readByteFromROMFSorPHROM = $ee51
    LDX #.romServiceCallROMFilingSystemByteGet          X=paged ROM service call to get a
                                                        byte from the ROM filing system
    LDY .currentSpeechPHROMOrROMNumber                  check ROM / PHROM number
    BMI .readByteFromPHROM                              if (ROM number is negative, i.e. a
                                                        PHROM) then branch
    LDY #$FF                                            Y=255
.romServiceCall = $ee59
    PHP                                                 store flags
    JSR .osbyte143EntryPoint                            paged ROM service call
    PLP                                                 restore flags
    CMP #1                                              if (A >= 1, i.e. ROM service call
                                                        was not claimed) then set carry
                                                        [but carry seems to be unused]
    TYA                                                 A = Y = byte read
    RTS                                                 

§21. readByteFromPHROM.

.readByteFromPHROM = $ee62
    PHP                                                 push flags
    SEI                                                 disable interrupts
    LDY #$10                                            Command code to ask speech processor
                                                        to read a byte
    JSR .osbyte159EntryPoint                            call OSBYTE 159 - write command to
                                                        speech processor
    LDY #0                                              Y=0
    BEQ .readWriteSpeechProcessorPushedFlags            ALWAYS branch (read speech processor)

§22. OSBYTE 158 - Read byte from speech processor.

.osbyte158EntryPoint = $ee6d
    LDY #0                                              Y=0 to set speech proc to read
    BEQ .readWriteSpeechProcessor                       ALWAYS branch

write A (a byte of an address) to speech processor as two nybbles
.writeTwoNybblesOfAddressToSpeechProcessor = $ee71
    PHA                                                 push A
    JSR .writeLoadAddressCommandToSpeechProcessor       to write to speech processor
    PLA                                                 get back A
    ROR                                                 } bring upper nybble to lower nybble
    ROR                                                 } by rotate right
    ROR                                                 } 4 times
    ROR                                                 }
    fall through...

§23. writeLoadAddressCommandToSpeechProcessor.

 Write 'Load address' command to speech processor
 See Speech System Users Guide (Page 21)
.writeLoadAddressCommandToSpeechProcessor = $ee7a
    AND #%00001111                                      mask out top nybble
    ORA #%01000000                                      set top bits: 'Load address' command
    TAY                                                 forming command for speech processor
    fall through...

§24. OSBYTE 159 - Write to speech processor.

 On Entry:
       Y = data or command
.osbyte159EntryPoint = $ee7f
    TYA                                                 transfer data or command from Y to A
    LDY #1                                              to set speech proc to write
    fall through...

§25. Read or write to the Speech processor.

 See 'Speech System Users Guide' for details, and data sheets for the chips:

 Speech processor is 'Texas Instruments TMS 5220 Voice Synthesis Processor'
 Speech data ROM  is 'Texas Instruments 16K TMS 6100'

 On Entry:
       Y = 0 to read from speech processor
       Y = 1 to write to speech processor
       A = value to write (speech op-code or data) if writing

 On Exit:
       Y = byte read (when reading)
       N flag set if top bit of Y is set
.readWriteSpeechProcessor = $ee82
    PHP                                                 remember flags
    SEI                                                 disable interrupts
.readWriteSpeechProcessorPushedFlags = $ee84
    BIT .speechSystemPresentFlag                        check for speech processor
    BPL .speechProcessingDone                           if (speech processor not found)
                                                        then branch

    read or write to the speech processor
    PHA                                                 remember A (value to write)
    LDA .speechDirectionTable,Y                         } set data direction to give 8
    STA .systemVIADataDirectionRegisterA                } bit input (Y=0) or output (Y=1)
    PLA                                                 recall A (value to write)

    write byte (only has an effect if the data direction register is set for writing)
    STA .systemVIARegisterANoHandshake                  send value to speech chip

    LDA .speechEnableTable,Y                            } enable speech read (Y=0)
    STA .systemVIARegisterB                             } or write (Y=1)

    loop to wait for enable read / write to take effect
-
    BIT .systemVIARegisterB                             check result
    BMI -                                               loop back until speech processor
                                                        reports ready (when bit 7 of Port
                                                        B is clear)

    read byte (only reads a valid value if the data direction register is set for reading)
    LDA .systemVIARegisterANoHandshake                  read speech processor data

    disable reading / writing speech
    PHA                                                 remember A
    LDA .speechDisableTable,Y                           } disable speech read (Y=0)
    STA .systemVIARegisterB                             } or write (Y=1)
    PLA                                                 recall A

.speechProcessingDone = $eeaa
    PLP                                                 get back flags (including restoring
                                                        interrupts to what they were before)
    TAY                                                 Y = value read from speech processor
    RTS                                                 

§26. setROMOrPHROMAddress.

.setROMOrPHROMAddress = $eead
    LDA .fsSpareByteA                                   set ROM displacement pointer
    STA .romAddressLow                                  in .romAddressLow
    LDA .fsSpareByteB                                   
    STA .romAddressHigh                                 And .romAddressHigh
    LDA .currentSpeechPHROMOrROMNumber                  check PHROM / ROMFS number
    BPL .exit26                                         if (ROM is selected, not PHROM)
                                                        then branch (return)
    fall through...

§27. Send the given address to the Speech Processor.

 The speech processor has a command where the CPU can send an address for the speech
 processor together with a ROM/PHROM number. Data is written to the speech processor a
 nybble at a time. Five nybbles are needed in total.

 On Entry:
       .romAddressLow/High contains the 14 bit address (to access anywhere in a 16K ROM)
       to send to the speech processor.
       .currentSpeechPHROMOrROMNumber contains the ROM/PHROM number (6 bits) to the speech
       processor.
.writeAddressAndROMNumberToSpeechProcessor = $eebb
    PHP                                                 push flags
    SEI                                                 disable interrupts
    LDA .romAddressLow                                  get ROM address low
    JSR .writeTwoNybblesOfAddressToSpeechProcessor      pass it to speech processor
    LDA .currentSpeechPHROMOrROMNumber                  }
    STA .tempStoreFA                                    } .tempStoreFA = PHROM / ROM number
    LDA .romAddressHigh                                 get ROM address high
    ROL                                                 }
    ROL                                                 }
    LSR .tempStoreFA                                    } replace the top two bits with the
    ROR                                                 } lower two bits of the ROM/PHROM
    LSR .tempStoreFA                                    } number
    ROR                                                 }
    JSR .writeTwoNybblesOfAddressToSpeechProcessor      pass byte to speech processor
    LDA .tempStoreFA                                    .tempStoreFA has the remaining bits
                                                        of the ROM/PHROM number
    JSR .writeLoadAddressCommandToSpeechProcessor       pass lower nybble to speech processor
    PLP                                                 restore flags
.exit26 = $eed9
    RTS