Chapter 12: Sound Programming¶
The SGU-1 synthesis model and the audio plumbing are covered in Chapter 5; the full register map lives in Appendix A, and Appendix G documents what every field means numerically. This chapter is the programming-side counterpart — the idiomatic sequences for picking a channel, setting up a voice, running an envelope, sweeping a parameter, playing back PCM, and beeping the buzzer.
The SGU-1 Register Window¶
The whole of the SGU-1 is reached through a 64-byte window at $FEC0–$FEFF. That window is channel-switched: the byte at offset $3F (i.e. $FEFF) is a selector that remaps the other 63 bytes to the registers of one specific channel. Writing a channel number $00-$08 there switches the window, and $FF selects the service bank instead — the sample port and the controls for the CODEC/DSP downstream. Values $09-$FE are reserved – write only the defined ones.
Bringing the Chip Up¶
Two things have to happen before any note will sound, and both are easy to forget because neither produces an error — just silence.
Reset the chip. A program that starts warm inherits whatever the previous one left running: held gates, armed sweeps, a looping PCM voice, a filter with its cutoff closed. The simplest reset is to wipe every channel’s registers to zero, which is precisely what a hardware reset does to them.
; 8-bit A/X/Y (SEP #$30), data bank $00
ldy #8 ; channels 8 down to 0
reset_ch:
sty $FEFF ; select the channel
ldx #$3E ; window offsets $3E..$00
lda #$00
reset_reg:
sta $FEC0,x ; $FEFF, the selector itself, is left alone
dex
bpl reset_reg
dey
bpl reset_ch
Unmute. The chip comes up silent by design: the service bank’s master volume gates the entire mix and resets to zero, so that a reset can never blast whatever the register file happened to be holding. Select the service bank with $FF and raise it.
lda #$FF
sta $FEFF ; select the service bank
lda #$FF
sta $FEE0 ; master volume -- unmute
The full cycle is therefore reset, unmute, select a channel, configure it, set its volume, key on. To configure a different channel, select it and configure again; changes to one channel do not touch the others’ state.
Channel volume deserves its own step in that list because it is zero after a reset, and a channel with VOL = 0 is silent no matter how carefully its operators are programmed.
Selecting a Channel¶
lda #0 ; channel 0
sta $FEFF ; SGU channel-select
After this, all reads and writes to $FEC0–$FEFE apply to channel 0. The selector itself is stateful on the audio side — the next write to $FEFF switches; no other operation invalidates the selection.
The Channel Layout, At a Glance¶
The 63 programmable bytes fall into two halves:
$FEC0–$FEDF— four 8-byte operators (numbered 0..3).$FEE0–$FEFE— channel-wide controls: frequency, volume, pan, filter, sweeps, PCM pointers, flags.
Per-operator registers (R0..R7) hold everything needed for one FM operator: waveform, multiplier, detune, envelope rates and levels, routing, and the small handful of ESFM-style extras (fixed-frequency flag, delayed key-on, ring mod, hard sync). See Appendix A for bit-level layout.
Programming a Voice¶
The order of operations matters less than you might expect: registers take effect as they are written, and the envelope only starts on the key-on. The canonical setup sequence, assuming the chip has already been reset and unmuted:
Select the channel.
Set per-operator fields: waveform,
MUL,TL, envelope (AR/DR/SL/RR/SR), routing (OUT= this operator’s level into the channel mix,MOD= how hard the previous operator modulates it – feedback on operator 0).Set channel fields: pitch (
FREQ), volume (VOL) — zero after a reset, so a channel left at the default is silent — pan (PAN), filter taps (FLAGS0), any sweep enables (FLAGS1).Key on: set
GATEandTRIGtogether inFLAGS0.
A simple two-operator FM setup — one carrier, one modulator — at middle A, with a pluck-like envelope:
; Channel 0
lda #0
sta $FEFF
; Operator 0 = modulator
lda #%00000000 ; R0: no TRM/VIB/KSR; MUL = 1
sta $FEC0
lda #%00011000 ; R1: TL = 24 (moderate modulation depth)
sta $FEC1
lda #$E1 ; R2: AR=14 (fast), DR=1 (long decay)
sta $FEC2
lda #$07 ; R3: SL=0, RR=7 (medium release)
sta $FEC3
lda #0
sta $FEC4 ; R4: DT=0, SR=0
sta $FEC5 ; R5: no delay / FIX / WPAR
lda #%00000000 ; R6: no feedback (MOD on operator 0 is feedback gain)
sta $FEC6
lda #%00000000 ; R7: OUT=0 (internal only), WAVE=0 (SINE)
sta $FEC7
; Operator 1 = carrier
lda #%00000001 ; R0: MUL = 1
sta $FEC8
lda #0 ; R1: TL = 0 (loud)
sta $FEC9
lda #$F1 ; R2: AR=15, DR=1
sta $FECA
lda #$07 ; R3: SL=0, RR=7
sta $FECB
lda #0
sta $FECC ; R4: DT=0, SR=0
sta $FECD ; R5: no delay / FIX / WPAR
lda #%00001110 ; R6: MOD=7 -- take full modulation from operator 0
sta $FECE
lda #%11100000 ; R7: OUT=7 (max), WAVE=0 (SINE)
sta $FECF
; Channel: pitch, volume, gate
lda #<7382 ; A4 (440 Hz); FREQ = Hz * 2^24 / 1000000
sta $FEE0
lda #>7382
sta $FEE1
lda #$40 ; VOL = 64
sta $FEE2
lda #0 ; PAN centre
sta $FEE3
lda #%00000011 ; FLAGS0: GATE + TRIG (note-on from silence)
sta $FEE4
The two bits do different jobs. GATE (bit 0) is the key level, not an edge. Raising it on a released or idle voice starts the note — the envelope enters attack from whatever attenuation it currently sits at — but writing it to a voice that is already sounding leaves the envelope entirely alone, which is why a new FREQ under a held gate slurs instead of retriggering. TRIG (bit 1) is the one-shot, self-clearing bit that restarts the envelope from silence: it is the only way to retrigger a note that is still sounding, and leaving it clear is the only way to avoid restarting one. The two together spell out the four note events – %11 note-on, %01 legato key-down, %00 note-off, %10 note-cut – each a single write. Appendix G has the full table.
Changing a Voice While It Sounds¶
The SGU-1 has no shadow registers and no double-buffering: the synthesis engine reads each register as it renders each sample, so a write takes effect on the next one, mid-note. Rewriting a sounding voice is therefore normal practice, and for legato, slides, swells and filter motion it is the only way to get the effect at all: the alternatives (a TRIG, or a GATE cycle) both disturb the envelope you are trying to keep.
Legato is a pitch change with the gate left up. Nothing else changes; the envelope carries on from wherever it is:
lda #0
sta $FEFF ; select channel 0
lda #<8286 ; new pitch (B4, 493.9 Hz)
sta $FEE0
lda #>8286
sta $FEE1 ; FLAGS0 untouched -- the note slurs
A slide is the same write repeated, a step at a time, from the driver’s per-frame tick:
; once per frame while the note is held; freq_lo/hi is the driver's shadow
lda #0
sta $FEFF
lda freq_lo
clc
adc #<SLIDE_STEP
sta freq_lo
sta $FEE0
lda freq_hi
adc #>SLIDE_STEP
sta freq_hi
sta $FEE1
Or hand the whole gesture to the hardware and let the frequency sweep run it, which costs one setup and no per-frame work at all (see Sweeps below). What you must not do is both: an armed sweep writes back into FREQ itself, so a CPU-driven slide and a hardware sweep on the same parameter will fight over the register.
The same applies to every other channel and operator parameter. VOL under a held gate is a swell or a fade, CUTOFF is filter motion, DUTY is PWM, and operator TL / MOD / OUT morph the timbre the way a filter would on a subtractive synth.
One register needs care: FLAGS0 holds TRIG next to the filter taps and the PCM bit. TRIG self-clears, so reading the register back will not show it — but a shadow byte or an instrument image with the bit still set will re-fire the retrigger the next time anything writes that register, resetting the envelope. Keep GATE and TRIG out of stored patches and out of the driver’s shadow, and add them explicitly at the note event: ORA #$01 for a key-down, ORA #$03 for a note-on.
Envelope Shapes¶
AR, DR, SR, and RR are 5-bit rates (AR and DR are split across R2 low nibble and R7 high bits — see Appendix A for the exact packing). SL is a 4-bit level (the sustain plateau height, not a rate); TL is a 7-bit total level attenuation split across R1 low bits and R6[0].
A few practical envelopes:
Sound |
|
|
|
|
|
|
|---|---|---|---|---|---|---|
Percussion |
31 |
14 |
0 |
0 |
15 |
0 |
Pad |
10 |
4 |
12 |
2 |
8 |
0 |
Pluck |
31 |
4 |
0 |
0 |
7 |
0 |
Bell |
31 |
2 |
0 |
4 |
10 |
0 |
These are starting points — FM’s timbre depends heavily on per-operator TL and routing. Use a short test harness that key-ons on every VBI to iterate fast.
Filter¶
Each channel has a per-channel multimode filter shared by all of its operators. Cutoff is 16 bits (CUTOFF_L / CUTOFF_H), resonance is 8 bits (RESON, 0 = no feedback, 255 = maximal resonance). The output taps are selected by FLAGS0 bits 5 (low), 6 (high) and 7 (band); selected taps are summed, so a compound mode is just two bits set. With all three clear the filter is bypassed entirely.
lda #<4000 ; cutoff value
sta $FEE6
lda #>4000
sta $FEE7
lda #120 ; RESON: moderate resonance
sta $FEE9
lda #%00100001 ; FLAGS0: low-pass tap (bit 5) + GATE
sta $FEE4
Reset the filter state on a new note by setting the “filter reset” bit in FLAGS1 — useful for percussion sounds that should start cleanly without the prior note’s trail.
Sweeps¶
Three per-channel hardware sweeps — frequency, volume, cutoff — each described by a 16-bit SPEED, an 8-bit AMOUNT (direction + magnitude), and an 8-bit BOUND (travel limit + wrap / bounce behaviour). Enable them with the matching bits in FLAGS1.
Canonical uses:
Frequency sweep: vibrato, pitch bends, arpeggios. Fast small amplitude = vibrato; slow large amplitude = glide.
Volume sweep: tremolo, fades, and attack/decay shaping outside the operator envelope.
Cutoff sweep: filter motion that follows the note instead of a global LFO.
Example — a slow upward cutoff sweep that opens the filter after key-on:
lda #<200 ; SWCUT_SPEED
sta $FEF8
lda #>200
sta $FEF9
lda #%10001000 ; SWCUT_AMT: bit 7 = up, step = 8
sta $FEFA
lda #$E0 ; SWCUT_BOUND
sta $FEFB
lda #%01000000 ; FLAGS1: cutoff-sweep enable
sta $FEE5
(Exact AMOUNT / BOUND / FLAGS1 bit positions are in Appendix A.)
PCM Playback¶
Any channel can switch from FM synthesis to PCM playback by setting the PCM-enable bit in FLAGS0 (bit 3). The channel’s three 16-bit pointers then drive playback:
PCM_POS— the current play position (initialized to where you want playback to start).PCM_END— the end boundary. WhenPCM_POSreaches this, the channel either stops or loops.PCM_RST— the loop restart position (used when thePCM_LOOPbit is set inFLAGS1).
PCM sample data lives in the audio chip’s internal 64 KB sample RAM, addressable by these 16-bit pointers. Loading a sample into that memory is done through a separate firmware path (asset loader); from the CPU side, a program treats the sample region as opaque once loaded.
A looping one-shot:
lda #<sample_start
sta $FEEA ; PCM_POS_L
lda #>sample_start
sta $FEEB ; PCM_POS_H
lda #<sample_end
sta $FEEC ; PCM_END_L
lda #>sample_end
sta $FEED ; PCM_END_H
lda #<sample_loop
sta $FEEE ; PCM_RST_L
lda #>sample_loop
sta $FEEF ; PCM_RST_H
lda #%00000100 ; FLAGS1: PCM_LOOP
sta $FEE5
lda #%00001001 ; FLAGS0: PCM + GATE
sta $FEE4
Pitch of the playback is driven by the channel’s FREQ register as with FM channels, so the same sample can be re-pitched across the keyboard for sampler-style instruments.
PCM as a wavetable operator¶
A subtler use of the sample region: any operator can pick WAVE = 7 (SAMPLE), which treats a 1024-sample slice of the sample region as a wavetable, with PCM_RST pointing at the slice base. This lets a channel mix a sampled voice as the modulator or carrier in an otherwise-FM patch — the defining flexibility of the SGU-1 over pure-FM chips.
System Buzzer¶
The buzzer is a separate, simpler subsystem at $FFA8–$FFAB (see Chapter 6), intended for the sort of blip-and-beep the OS or an application wants without spinning up a full SGU voice.
; ~1 kHz beep at 50% duty for a short time
lda #$FF ; FREQ_L
sta $FFA8
lda #$8C ; FREQ_H (together encodes ~1 kHz — see Chapter 6)
sta $FFA9
lda #$80 ; DUTY = 50%
sta $FFAA
; ... wait ...
lda #$00
sta $FFAA ; DUTY = 0 silences
The frequency is encoded logarithmically, so a byte-table lookup for common pitches (note names to FREQ values) is a practical way to “play notes” on the buzzer. For any real music, use SGU-1 instead.
Summary¶
Programming the SGU-1 is register-driven and mostly stateless: select the channel, write its registers, key it on. The channel-switched window keeps the per-channel programming compact; hardware envelopes, sweeps, and PCM mode keep the CPU burden low even for rich patches. The tracker plus the shipped player routines handle the song-engine layer; the buzzer covers the system-beep use case without touching the synth. Full register / bit layouts are in Appendix A.