- Jukebox user interface.
A simple user interface was chosen with an introduction screen, a basic selection screen and an in play screen.
- Introduction screen.
A simple screen that is displayed once on initialization of program.
Midi Juke Box
Press any key view selection
- Selection screen.
Allows for selection of a song.
1-Courting 2-Ghosts'n'Goblins 3-Dixie
4-Music Box Dancer 5-BubbleBobble
- In play screen.
Describes current state of system and allows canceling of song in progress.
Playing song - press any key to stop
- Primary sound buffer.
All samples are rendered into a 64 word wrap around primary sound buffer located at 0x800. An 11KHz interrupt is setup (using output compare 7), to select the next sound sample from the buffer and place it on PORT-T.
Interrupt initialization code:
clr OC7M
clr OC7D ; all timers disconnected from PORTT
clr TCTL1
clr TCTL2
clr TCTL3
clr TCTL4 ; all timers disconnected from PORTT
movb #%10000000,TMSK1 ; enable IOC channel 7 interrupt
movb #%10000000,TIOS ; enable IOC channel 7 to output
movb #%00001000, TMSK2 ;
bset TFLG1, #%10000000 ; reset interrupt tc07
movb #%11100000, TSCR ; enable timer, fast flag clear
movw #sampleRate, TC7 ; X Khz interrupt
cli ; enable interrupts
The buffer has an insert pointer for new samples and a remove pointer for samples to play. The insert pointer never passes the remove pointer. If there are no samples to play the play code stalls until there is.
playNextSoundSample interrupt
ldx bufferRemovePtr
cpx bufferInsertPtr
beq playNextSoundSample_noSampleReady
ldd 2,x+ ; load next sample to play
addd #($7FFF-256) ; convert from 16bits signed to 8bits
lsrd ; unsigned for output
stab PORTT ; store low byte of sample on PORT-T
tfr x,d ; make sure buffer doesn't exceed
andb #%1111111 ; it's bounds
std bufferRemovePtr
playNextSoundSample_noSampleReady:
bset TFLG1, #%10000000 ; reset interrupt tc07
rti
Note that the buffer is in 16 bit for mixing purposes and there is no reason given a 16 bit parallel DAC that it couldn't be played in 16 bit with minuscule extra overhead.
- Midi decoder.
Midi is a delta-time encoded event format. It comes in three formats Format 0, 1 and 2.
- Format 0 is a single track format where all midi channels are stored in one track.
- Format 1 is a multiple track format where each midi channel gets its own track and all tracks are played simultaneously based on track 0s delta time events.
- Format 2 is a multiple track format with multiple sequences (songs) each with their own delta time events.
For the sake of simplicity I chose to decode format 0, also format 0 is the most common format.
The Midi specification required 16 channels of sound maximum to be played simultaneously during a tune. I limited this to 12 as the CPU would not manage 16.
Each sound channel has the following parameters:
Parameter | Size(Bytes) | Description |
state | 1 | 0 available, 1 playing, 2 decaying |
midiChannel | 1 | Current midi channel assigned to this sample channel. |
note | 1 | Note being played. |
envVolume | 2 | Current envelope volume. |
envDelta | 2 | Delta for envelope per sample. |
samplePtr | 2 | Current pointer to sample being played. |
sampleLoopStart | 2 | Pointer to beginning of sample. |
sampleLoopEnd | 2 | Pointer to end of sample. |
sampleSubPos | 2 | Used to play sample at varying speeds. |
sampleSubPosAdd | 2 | Rate to play sample at. 256 = 1:1 |
sampleLoopStartEndDiff | 2 | Difference between sample loop start and end. |
The basic midi rendering loops is as follows.

The fill buffer loop basically works out how much of the buffer is empty and then calls the sound renderer to render the empty buffer space.
- 12 channel sound renderer
The midi specification requires a minimum of 16 channels of simultaneous samples to be played at once, typically in greater than 16 bit in greater than 44 KHz. Due to limitations of the MCU speed I limited this to 12 channels of 8 bit sound at 11 KHz. This roughly gave me:
CPU Cycles per channel per Hz = CPU MHz / channels / sample rate
= 8 MHz / 12 / 11 KHz
= 60 cycles
Some quick pseudo-code showed that this was feasible.
The sound render renders 12 channels maximum to the primary sound buffer.
It consists of the following parts:
- Sound Samples
The midi samples were chosen to be basic sine waves due to lack of memory. The samples can be found in the file samples.asm. They are signed 8 bit 11 KHz samples.
- Sound Envelopes
Typical midi samples have Attack Decay Sustain Release envelope which further modifies the sampled sound to be more like original instrument without using excessive amounts of memory. Due to limits of the MCU I chose to only implement a Sustain and Release for my sound samples.
Sustain is required to differentiate between multiple notes played in sequence with the same note. And the decay is added to prevent notes from clipping when they are turned off.
- Sample frequency modification
The samples are re-sampled at a new frequency based off a 8:8 fixed point lookup table basically generated from the equation
n = number of semitones above the base sample you want to resample to.
Step = 2n/12
e.g. If we have a note at 440Hz that we wanted to resample up 2 semitones:
step = 2n/12
= 1.1225(4sf)
440 Hz x step = 493.9 Hz
There is no antialiasing in the resampling which causes some erroneous noise to be present in the output. But no more CPU cycles are available to implement resampling.
An 8:8 fixed point value is allows for stepping through the sample at anywhere from 1/256th speed to 256 times speed.
The basic sound rendering loop:
; // envelopeVolume -= envelopeDecay;
ldd chan_envelopeVolume
subd chan_envelopeDelta
bmi ENDSAMPLE ; if envelope reachs 0 then the sample
std chan_envelopeVolume ; is dead
tfr d,y ; save envelope volume in y
; // sampleSubPos += sampleSubPosAdd; sampleSub pos is in 8:8 fixed point
ldd chan_sampleSubPos ; update pointer to current sample index
addd chan_sampleSubPosAdd; just storing the low byte effectively
stab chan_sampleSubPos+1 ; rounds the high byte off
; // v = samplePos+(int)SampleSubPos
ldx chan_samplePtr ; increment the real sample position
ldab x ; by the whole amount of the sampleSubPos
leax a,x ; and load the new sample
cpx chan_sampleLoopEnd ;
bge RESET_SAMPLEPTR ; have we reached the end of the loop?
BACK_FROM_RESET_SAMPLE_PTR:
stx chan\1_samplePtr ; if so reset to start of loop
; // [bufferInsertPos++] = v * envelopeVolume;
sex b,d ; convert 8bit signed sample to 16bit signed
emuls ; sample
tfr y,d ; scale sample to envlope
addd SP ; add sample to other channels samples
std SP ; and store
bra SKIP ;
RESET_SAMPLEPTR:
exg d,x ;
subd chan\1_sampleLoopStartEndDiff ;
exg d,x ;
bra BACK_FROM_RESET_SAMPLE_PTR
ENDSAMPLE:
clr chan\1_state
SKIP:
- Sample mixing
All 12 channels are mixed together with simple addition. This has the benefit of no loss in quality initially from the mixing, but requires a final stage to convert the 16 bit sample to a more manageable quantity i.e. 8 bits.