OSBYTE 158, 159; Sound and speech interrupt processing; ROM/PHROM read byte - 983 bytes (5.9%)
- §1. Introduction
- §2. Silence a sound channel
- §3. Set volume for channel X
- §4. soundParameterTable
- §5. skipToNextChannelLocal
- §6. Update sounds in the 100Hz timer interrupt
- §7. clearSoundChannels
- §8. checkForNextSoundToPlay
- §9. clearSoundChannelBuffer
- §10. syncSoundBufferEmpty
- §11. silenceNonEnvelopeSound
- §12. syncSounds
- §13. setPitch
- §14. Set the pitch of a sound
- §15. Read the next sound from the SOUND buffer and act on it
- §16. Pitch tables
- §17. pitchLookupTableHigh
- §18. PHROM numbers are $F0-$FF
- §19. readByteFromROMOrPHROM
- §20. readByteFromROMFSorPHROM
- §21. readByteFromPHROM
- §22. OSBYTE 158 - Read byte from speech processor
- §23. writeLoadAddressCommandToSpeechProcessor
- §24. OSBYTE 159 - Write to speech processor
- §25. Read or write to the Speech processor
- §26. setROMOrPHROMAddress
- §27. Send the given address to the Speech Processor
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.
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...
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
Table to convert channel number to the bits required by the chip
.soundParameterTable = $eb40 !byte $E0,$C0,$A0,$80
.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
.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
.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
.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
.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...
.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
.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
.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
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#
.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
.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
.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
.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