Chiptune
The same program in M68K, MIPS, Z80.
This program plays a short tune on the Synth, this editor's sound chip at 0xffff0100;
RARS does not have it. Three channels play together: 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 repeats the same register layout every 0x20 bytes. Channels 0, 1 and 2 start at
s0, s0 + 0x20 and s0 + 0x40. Add a setting's offset: decay is at 0x06, so bass decay is
at 0x26(s0). sh writes two-byte settings; sb writes one-byte settings. Pulse width
counts the high part of a cycle out of 4096: 1024 gives a quarter; 2048 gives half, a square wave.
An envelope shapes a note's volume. Gate-on starts the attack, which rises to full
volume; decay falls to the sustain level and holds there while the gate stays on.
Gate-off starts the release, which fades to silence. Sustain ranges from 0 (silence) to 127
(the channel's full volume). Attack, decay and release use ticks of 1/1024 second, about one
millisecond. Decay and release values describe a fall across the whole volume range; smaller
drops take proportionally less time. A zero time makes that phase immediate. Registers start at
zero, so the bass's unset attack and decay are immediate, and the drum's unset sustain is silent.
.data
# Each triple is a step, an eighth of a bar: the frequencies in Hz of the lead, bass and drum's
# noise. 0 leaves that channel as it is; a lead of 0xffff ends the song.
song:
.half 523, 131, 1200, 659, 0, 0, 784, 0, 24000, 659, 0, 0
.half 523, 131, 7000, 659, 0, 0, 784, 0, 24000, 1047, 0, 0
.half 440, 110, 1200, 523, 0, 0, 659, 0, 24000, 523, 0, 0
.half 440, 110, 7000, 523, 0, 0, 659, 0, 24000, 880, 0, 0
.half 349, 87, 1200, 440, 0, 0, 523, 0, 24000, 440, 0, 0
.half 349, 87, 7000, 440, 0, 0, 523, 0, 24000, 698, 0, 0
.half 392, 98, 1200, 494, 0, 0, 587, 0, 24000, 494, 0, 0
.half 392, 98, 7000, 494, 0, 0, 587, 0, 24000, 784, 0, 0
.half 523, 131, 1200
.half -1 # stored as 0xffff: the end of the song
.text
.globl main
main:
li s0, 0xffff0100 # the Synth: channel 0 at +0x00, 1 at +0x20, 2 at +0x40
# channel 0, the lead: a narrow pulse, struck, then held a little quieter
sb zero, 0x11(s0) # waveform 0: pulse
li t0, 1024
sh t0, 0x02(s0) # pulse width: high a quarter of the time
li t0, 4
sh t0, 0x04(s0) # attack
li t0, 200
sh t0, 0x06(s0) # decay
li t0, 60
sh t0, 0x08(s0) # release
li t0, 80
sb t0, 0x12(s0) # volume
li t0, 70
sb t0, 0x13(s0) # sustain
# channel 1, the bass: a triangle, held at full level
li t0, 1
sb t0, 0x31(s0) # waveform 1: triangle
li t0, 40
sh t0, 0x28(s0) # release
li t0, 127
sb t0, 0x32(s0) # volume
sb t0, 0x33(s0) # sustain
# channel 2, the drums: noise that decays to silence, its sustain left at 0
li t0, 4
sb t0, 0x51(s0) # waveform 4: noise
li t0, 90
sh t0, 0x46(s0) # decay
li t0, 100
sb t0, 0x52(s0) # volume
la s1, song
li s2, 1 # the gate bit
li s3, 0xffff # the end of the song
step:
lhu t0, 0(s1) # the lead
beq t0, s3, done
mv a1, s0 # channel 0's registers
jal strike
lhu t0, 2(s1) # the bass
addi a1, s0, 0x20
jal strike
lhu t0, 4(s1) # the drum
addi a1, s0, 0x40
jal strike
li a7, 32
li a0, 150 # an eighth of a bar: 150 milliseconds
ecall
addi s1, s1, 6 # the next three-halfword step
j step
# Plays the frequency in t0 on the channel whose registers start at a1; 0 leaves it alone.
strike:
beqz t0, held
sb zero, 0x10(a1) # gate off, then on: restart the attack from its current level
sh t0, 0(a1)
sb s2, 0x10(a1)
held:
jr ra
done:
li a7, 32
li a0, 600 # let the last chord ring
ecall
li a7, 10 # the program's end releases every gate
ecall
Each table step is three halfwords: lead, bass and drum frequencies in Hz. 523, 131, 1200 is
the first step; the first eight data lines pack four steps each. lhu reads their two-byte
values at offsets 0, 2 and 4; adding 6 to s1 reaches the next triple. A zero tells strike to
leave that channel alone.
Service 32 waits 150 milliseconds per step. The first 32 steps make four bars of eight; the
separate 33rd triple plays the closing chord. The following .half -1 stores 0xffff, the end
marker checked in the lead position. done lets that chord ring for another 600 milliseconds
before exiting. Noise frequency controls the drum's brightness: 1200 gives a low thud, 7000 a
snare-like sound and 24000 a hi-hat-like hiss.
strike has a local register contract: its caller supplies the frequency in t0 and the
channel's starting address in a1. It reads both and uses s2 for gate-on, 1. jal strike
saves the return address in ra; jr ra jumps back to it, just like ret. For a nonzero
frequency, the routine turns the gate off, stores the frequency, then turns the gate on. This
restarts attack from the current envelope level. The bass's immediate attack and full sustain
may therefore make repeated notes sound continuous.
Try it
For a brighter, brassier lead, replace sb zero, 0x11(s0) with li t0, 2 followed by
sb t0, 0x11(s0) to select a sawtooth. Run and listen. Restore the original waveform store,
then change li t0, 1024 before the pulse-width store to li t0, 2048. Predict how the
half-width pulse will sound, then run it to compare the hollower square wave with the original.
Then make the bass shorter. After its waveform store, add li t0, 150 followed by
sh t0, 0x26(s0) for decay, and replace sb t0, 0x33(s0) with sb zero, 0x33(s0) for zero
sustain. Predict which bass notes will now have silence between them, then run and listen:
each dies away in about 146 milliseconds, before the next 150-millisecond step. The
Synth chapter has the other registers.