Skip to content
RetrogamingDeep Dive Published Updated 8 min readViews unavailable

Game Boy Wave Channel: CH3 Sample Sequencing and Wave RAM Access Windows

Model Game Boy channel 3 as a 32-nibble sequencer with model-specific wave RAM arbitration, DAC level, trigger state, and testable sample timing.

Game Boy audio channel 3 is a programmable waveform player backed by 16 bytes of wave RAM. It reads two four-bit samples from each byte and steps through 32 nibbles at a frequency-controlled rate. The channel’s behavior is not equivalent to a generic wavetable oscillator that continuously reads a host buffer: its sample index, output latch, DAC enable, length timer, trigger behavior, and CPU-visible wave RAM arbitration are part of the hardware contract.

The sharpest compatibility differences appear when software rewrites wave RAM while the channel is playing. On classic DMG behavior, channel 3 has priority for the current wave-RAM byte, and CPU reads can expose the same currently selected byte regardless of the address requested. Writes and access windows have additional model-specific quirks. Color hardware and later revisions differ, and emulators have accumulated cycle-level workarounds based on hardware tests. A single unrestricted 16-byte array can therefore produce audible differences and break games that stream samples live.

Sixteen bytes, thirty-two samples

Wave RAM occupies $FF30-$FF3F, a 16-byte region. Each byte contains two samples, high nibble first followed by low nibble in the ordinary playback sequence. The channel’s internal sample index spans 0 through 31. At each channel timer event, it selects the next nibble, updates its output state, and advances the index with wraparound. The waveform storage is small but can encode arbitrary 4-bit shapes, not merely a fixed square or triangle.

The frequency timer determines how quickly the sample index advances. Channel 3’s hardware timer derives from the 11-bit frequency value, so a lower frequency register value produces a longer interval between sample steps. A full 32-nibble waveform cycle contains 32 sample steps; it is important to distinguish sample-step rate from complete waveform repetition rate. If a mixer asks for output at 48 kHz, that does not mean the APU itself advances samples at 48 kHz. The emulator must resample the discrete channel output from emulated timer events onto the host audio clock.

The wave channel has control and output-level registers, a length register, and frequency registers. The DAC-enable bit is independent from whether the channel’s current output sample is zero. The output-level field selects mute or a shifted/scaled digital sample, but a model’s analog mixer and high-pass filter shape what reaches the final signal. Preserve the raw digital nibble, the DAC state, and the mixed analog contribution separately so tests can distinguish sequencer errors from volume or filtering errors.

Triggering channel 3 is a state transition. It enables the channel if its DAC is available, reloads its length state according to the APU rules, initializes or advances timing state, and interacts with the sample index and output buffer. Pan Docs notes that channel 3’s sample index increments at a rate 32 times the channel frequency and that the first sample behavior is not a simple “start at nibble zero and emit it immediately.” Track the sequencer state and sample latch explicitly; a trigger should not be implemented as index = 0; output = wave_ram[0] unless a tested model requires exactly that timing.

Wave RAM is arbitrated with playback

When the channel is running, wave RAM reads are coupled to playback. On DMG-class behavior, the channel’s internal sample fetch selects which byte the memory port can expose. As a result, a CPU read from wave RAM can return the currently active byte even if the guest used another address. The CPU is not necessarily looking at an independent memory port. This can be used by drivers that update the current sample, but it can also make a naive debugger display surprising values.

Access behavior changes across hardware generations and phases. Some models permit CPU wave-RAM access only during a narrow interval around the channel’s sample fetch; others expose different restrictions or quirks. SameBoy models delayed wave reads and uses model-specific timing state rather than returning the backing array unconditionally. Do not generalize one emulator’s exact access window to every Game Boy without checking the target revision. If behavior is only measured on certain models, identify those models and isolate the compatibility policy.

Memory ownership has two observable directions. A CPU read can return the device-selected byte, while a CPU write may update the byte selected by the channel or be accepted only in a particular access window. It is unsafe to implement reads and writes using different guessed rules. Build a matrix keyed by hardware model, channel enabled state, access address, elapsed cycles from sample fetch, and read versus write. Test both the externally visible result and the playback sample that follows the access.

This access arbitration also affects audio. If a CPU write changes the currently fetched byte before the second nibble is consumed, playback may use a mixture of the old high nibble and new low nibble depending on exact timing. A host-side waveform copy that is refreshed only at frame boundaries cannot reproduce this. Store wave RAM as device-visible state and have the sequencer fetch the currently selected nibble at the defined emulated cycle.

Length, power, and channel status are separate

Channel 3 has a length counter clocked by the APU frame sequencer. The length load is based on an 8-bit register and the channel length-enable flag. Length expiration can disable output independently of the timer and wave contents. Tests should vary length enable and trigger timing around frame-sequencer edges because a write near a length clock may have a different effect than a write in the middle of the interval.

The global APU power bit and channel DAC are not interchangeable. Turning off the APU affects channel registers and waveform state; disabling the channel DAC mutes or powers down the channel path but does not mean wave RAM bytes should be discarded. A save state should retain the global power state, channel active bit, wave bytes, current sample index, timer remainder, length, level, and output sample latch.

On later Game Boy Color hardware, read-only PCM output registers provide a way to inspect digital channel outputs before the final analog-like mixer. Use them in CGB-mode test ROMs to separate the channel sequencer from the stereo mixer and high-pass filter. DMG hardware has no equivalent software-readable output register, so use a core trace or external test harness there. The documented distinction keeps a good audio waveform from masking an incorrect CPU-visible wave RAM behavior.

A deterministic playback index fixture

This Python example expresses the basic nibble order and frequency-timer reload as a small test helper. It is not a complete Game Boy model: the divider phase, register write timing, length clock, trigger quirks, wave-RAM bus arbitration, and analog mixer still belong in the hardware implementation. The function is useful for proving that byte order and sample count do not drift.

from dataclasses import dataclass


def wave_nibble(wave_ram, sample_index):
    if len(wave_ram) != 16:
        raise ValueError("channel 3 wave RAM must contain 16 bytes")
    if not 0 <= sample_index < 32:
        raise ValueError("sample index must be 0..31")
    byte = wave_ram[sample_index >> 1]
    return (byte >> 4) & 0x0F if sample_index % 2 == 0 else byte & 0x0F


@dataclass
class WaveSequencer:
    frequency: int
    sample_index: int = 0
    timer: int = 0

    def __post_init__(self):
        if not 0 <= self.frequency <= 0x7FF:
            raise ValueError("frequency must fit the 11-bit register")

    def tick(self):
        if self.timer == 0:
            self.timer = 2 * (2048 - self.frequency)
            self.sample_index = (self.sample_index + 1) & 0x1F
            return True
        self.timer -= 1
        return False


wave = bytes((0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0) * 2)
assert [wave_nibble(wave, i) for i in range(4)] == [1, 2, 3, 4]

The timer formula shown is the documented base period in the Game Boy clock domain; a complete core must account for the machine’s APU clock and model-specific phase. The initial sample-index convention is intentionally kept in a separate trigger test because hardware sample-zero behavior has subtleties beyond a steady-state wraparound test.

Test the access window, not just the sound

Begin with a fixed waveform containing distinct nibbles, such as 1, 2, 3, ... F, 0. Verify sample order, timer period, level selection, and wraparound. Then read each wave-RAM address while the channel is disabled and while it is playing; log the requested address, returned byte, sample index, and time since the last fetch. Repeat immediately before and after the sequencer changes byte. On hardware with a narrow access window, test one cycle on both sides of the boundary.

Writes need an equivalent matrix. Write a different pattern during playback and check which physical byte changed, whether the write was ignored or redirected, and which subsequent samples hear the modification. Run the same program on DMG, CGB, and GBA-compatible models. Mark behaviors as confirmed, inferred from implementation, or still unverified so an emulator does not promote a model-specific quirk to a universal claim.

Use an audio capture with sample-index telemetry, not only listening tests. A note that sounds close can still have a one-nibble phase shift or incorrect access behavior. Save state after a partial timer interval and after a CPU wave-RAM write, restore it, and compare the following sample stream and CPU reads bit for bit. Also run channel 3 while the other three channels are active to ensure global mixing and output routing do not alter its timer schedule.

Acceptance criteria

A faithful wave channel exposes the 16-byte memory, 32-nibble sequencer, frequency timer, sample latch, DAC and output level, length state, and model-specific access arbitration as separate components. It produces reproducible playback before and after save-state restore and explains every CPU wave-RAM result from the model, phase, and selected byte. That precision is necessary for both accurate audio and games that stream waveforms at runtime.

Related:

Sources:

Comments