Chiptune
The same program in MIPS, RISC-V, Z80.
This program plays a short tune on the Synth, the sound chip this editor adds at $E00100;
EASy68K does not have it. Three of its eight channels play together, the way a game console's music
did: a pulse wave for the melody, a triangle wave for the bass, and noise for the drums. Choose
Build, then Run, with your sound on.
Each channel has the same controls in a block of $20 bytes. With a0 pointing at channel 0,
channel 1's controls are $20 bytes farther on and channel 2's are $40 bytes farther on. For
example, the waveform is at offset $11 within a channel, so channel 1's waveform is at
$20 + $11 = $31 from a0.
The envelope shapes a note's level: attack rises to full, decay falls to the sustain level, and release fades to silence when the gate turns off. Sustain holds while the gate stays on and ranges from 0 to 127; the separate volume control scales the whole note. Attack, decay and release use ticks of 1/1024 second. Attack gives the time for a full rise from silence; decay and release give the time for a full fall to silence, so a smaller change takes less time. A time of 0 is immediate. The controls start at 0, which supplies the bass's instant attack and the drum's silent sustain here.
The lead uses a pulse wave. Its pulse width sets how much of each cycle is high, in parts of 4096: 1024 is a quarter of the cycle; 2048 gives equal high and low halves, a square wave.
SYNTH equ $E00100 ; channel 0; channel 1 is at +$20 and channel 2 at +$40
lea SYNTH, a0
; channel 0, the lead: a narrow pulse, struck, then held a little quieter
move.b #0, $11(a0) ; waveform 0: pulse
move.w #1024, $02(a0) ; pulse width: high a quarter of the time
move.w #4, $04(a0) ; attack
move.w #200, $06(a0) ; decay
move.w #60, $08(a0) ; release
move.b #80, $12(a0) ; volume
move.b #70, $13(a0) ; sustain
; channel 1, the bass: a triangle, held at full level
move.b #1, $31(a0) ; waveform 1: triangle
move.w #40, $28(a0) ; release
move.b #127, $32(a0) ; volume
move.b #127, $33(a0) ; sustain
; channel 2, the drums: noise with a short decay, its sustain left at 0
move.b #4, $51(a0) ; waveform 4: noise
move.w #90, $46(a0) ; decay
move.b #100, $52(a0) ; volume
lea song, a1
step:
move.w (a1)+, d2 ; the lead
cmp.w #$FFFF, d2
beq done
lea SYNTH, a2
bsr strike
move.w (a1)+, d2 ; the bass
lea SYNTH+$20, a2
bsr strike
move.w (a1)+, d2 ; the drum
lea SYNTH+$40, a2
bsr strike
move.l #15, d1 ; wait 15 hundredths of a second per step
move.b #23, d0 ; task 23: wait
trap #15
bra step
; Plays d2 Hz on the channel at a2; 0 leaves it alone. Leaves a1 unchanged.
strike:
tst.w d2
beq held
move.b #0, $10(a2) ; gate off, then on: restart the envelope
move.w d2, (a2)
move.b #1, $10(a2)
held:
rts
done:
move.l #60, d1 ; let the last chord ring
move.b #23, d0
trap #15
move.b #9, d0 ; the program's end releases every gate
trap #15
; Three words per step: lead Hz, bass Hz, drum noise Hz. Each long line holds four steps.
; 0 leaves that channel as it is; a lead of $FFFF ends the song.
song:
dc.w 523, 131, 1200, 659, 0, 0, 784, 0, 24000, 659, 0, 0
dc.w 523, 131, 7000, 659, 0, 0, 784, 0, 24000, 1047, 0, 0
dc.w 440, 110, 1200, 523, 0, 0, 659, 0, 24000, 523, 0, 0
dc.w 440, 110, 7000, 523, 0, 0, 659, 0, 24000, 880, 0, 0
dc.w 349, 87, 1200, 440, 0, 0, 523, 0, 24000, 440, 0, 0
dc.w 349, 87, 7000, 440, 0, 0, 523, 0, 24000, 698, 0, 0
dc.w 392, 98, 1200, 494, 0, 0, 587, 0, 24000, 494, 0, 0
dc.w 392, 98, 7000, 494, 0, 0, 587, 0, 24000, 784, 0, 0
dc.w 523, 131, 1200
dc.w $FFFF
The song is a table in memory, with three words per step: lead, bass and drum frequencies in Hz.
Each long dc.w line contains four of these triples. On the first step, the three
move.w (a1)+, d2 instructions read 523, 131 and 1200. Each read advances a1 by two
bytes, so after all three it points at 659, the start of the next step. strike leaves a1
unchanged, letting the loop continue through the table.
The next step reads 659, 0, 0: the lead changes pitch, while the bass and drum get no register
writes. A table value of 0 leaves all channel controls unchanged. The bass keeps sounding;
the drum gets no new hit.
Task 23 waits 15 hundredths of a second after each step. The eight long lines contain 32 steps,
grouped musically into four bars of eight steps over the chords C, A minor, F and G. The
bass gets a frequency on the first and fifth step of each bar. A closing triple adds step 33,
then the single word $FFFF ends the loop before it reads another bass or drum word. done
lets that closing chord ring for another 60 hundredths of a second before ending the program.
Each channel is set up once. The lead decays to a quieter level; the bass holds at full level; the drum's sustain is 0, so each hit decays to silence even though its gate stays on. The noise frequency sets how bright a drum sounds: 1200 gives a low thud, 7000 a snare-like sound and 24000 a hi-hat-like sound.
strike plays one frequency on one channel. Storing the new frequency would only change the
pitch of the note already sounding, so it turns the gate off and on around it to restart the
attack from the current level. With the bass's instant attack and full sustain, a repeated pitch
may still sound continuous; restarting the envelope does not necessarily make an audible gap.
Try it
Change the lead's waveform store to move.b #2, $11(a0) for a brighter, brassier sawtooth melody.
Or keep waveform 0 and change its pulse-width store to move.w #2048, $02(a0) for a hollower
square wave.
Then make the bass shorter: add move.w #150, $26(a0) to its setup and change its sustain store
to move.b #0, $33(a0). Decay is at channel offset $06, so $20 + $06 = $26; sustain is at
$13, so $20 + $13 = $33. Leave its volume at 127. Each bass note now falls to silence in
150 ticks, about 0.146 seconds. The Synth chapter has the other
registers.