Skip to content
RetrogamingDeep Dive Published Updated 7 min readViews unavailable

SNES S-DSP Echo: ESA, EDL, FIR History, and Shared ARAM Writes

Trace SNES echo from voice routing through the ESA/EDL ring buffer, eight-tap FIR, signed feedback, write inhibition, and DSP-to-ARAM timing.

The SNES S-DSP echo path is a sample-synchronous feedback system backed by the same 64 KiB ARAM address space used by the SPC700. It is not a generic host reverb effect. The DSP selects which voices feed the echo input, reads left and right history from a programmable buffer, applies an eight-tap FIR filter, scales the echo return, mixes signed feedback, and optionally writes the resulting pair back to ARAM. The register state and order of those operations determine both sound and visible memory contents.

That ordering explains many “close but wrong” audio implementations. A host reverb plug-in may produce a similar tail while ignoring ESA base movement, EDL wrap, FIR coefficient changes, echo-write inhibition, and interactions with the SPC700’s own memory writes. A cycle-accurate emulator must treat the DSP as a timed ARAM client, not as an audio post-processing effect attached after voice mixing.

Echo is a ring in ARAM, not a hidden buffer

ESA selects the echo region’s base page in ARAM. EDL selects a delay length in units of 2 KiB, corresponding to roughly 16 ms per nonzero step at the DSP’s sample cadence; the maximum programmed delay is approximately 240 ms. The zero setting has a minimum small ring behavior rather than an unbounded or absent buffer. Echo address arithmetic wraps within the DSP’s 16-bit ARAM address space, and the active delay controls where the ring pointer returns to its beginning.

Each stereo sample frame consumes four bytes of echo history: a signed 16-bit left sample and a signed 16-bit right sample. Those values are read from ARAM, passed into the filter pipeline, and eventually used in the left/right output mix. The DSP also constructs new echo input from selected voices and feedback. When echo writes are enabled, the new left/right pair replaces the data at the current ring position; when the echo-write-disable bit is set, the read and output path still operate but the writeback is suppressed.

Because the SPC700 and S-DSP share ARAM, software can inspect or modify the echo area. The exact interaction depends on the hardware’s access schedule and must be modeled in the shared memory arbiter. If an SPC700 store targets an address that the DSP reads or writes in the same timing window, the winner and resulting bytes are observable to software. A separate host allocation for “reverb RAM” cannot reproduce this behavior.

EON and EVOL shape the input and return independently

The echo-on register EON is a per-voice bitmask. It determines which voice outputs contribute to the echo input, not which voices are audible in the main output. A voice can be routed to the main speakers while excluded from echo, or be included in echo while its direct contribution is muted by separate routing and volume controls. Preserve the per-voice direct and echo paths independently.

EVOL left and right scale the filtered echo return into the stereo output. EFB controls signed feedback: the previous echo contribution is combined with the current echo input and fed back into future ring entries. A positive feedback amount can extend a tail, while negative values invert the contribution. The hardware clips and shifts values at defined fixed-point stages; using floating point with a final clamp may produce slightly different rounding and overflow behavior.

The echo filter is an eight-tap finite impulse response (FIR) path. Eight signed coefficients, held in FIR registers, weight delayed samples from the left and right history. The filter has internal sample history separate from the ring buffer: changing coefficients does not mean the previous buffer bytes disappear, and reading the ring does not by itself define the FIR delay line. A faithful DSP keeps the circular history, current tap position, register values, and fixed-point accumulator width explicit.

The order of channels and taps matters. On each output sample, the DSP advances its echo history, reads the next stereo pair, calculates the FIR results using signed coefficient multiplication, shifts and accumulates intermediate values, and then combines filtered return and feedback. A simplified symmetric filter or a host biquad is not equivalent. Unit tests should use isolated impulse inputs so the exact eight output coefficients can be observed independently of a musical track.

Echo registers have timing and write side effects

The DSP registers include ESA (echo start address), EDL (echo delay), EFB (echo feedback), EON (echo-enabled voice mask), EVOL L/R, FIR coefficient registers, and FLG control bits. These are written through the SPC700’s DSP address/data ports. The SPC700’s register write is not a direct C++ member assignment; it traverses the memory-mapped communication path and takes effect at an emulated time. If software changes ESA or EDL during playback, the DSP must apply those values at the documented register pipeline boundary.

EDL changes can interact with the ring pointer. Some reference implementations note that a newly written delay value may not affect the active wrap boundary until the DSP reaches a particular echo-index phase. This can make it appear as if the old delay remained in effect for part of a period. Do not apply an EDL change retroactively to every past sample. Preserve the index and the change timing, and verify the behavior against a low-level DSP test or hardware-derived reference.

FLG contains global DSP controls including reset, mute, noise clock, and echo-write disable. Muting the output is not the same as stopping channel timers or disabling echo writes. Likewise, echo-write-disable is not a “turn echo off” switch: the old ARAM history can continue to be read and mixed while new samples are not written. Tests must vary these controls separately and assert both output samples and ARAM bytes.

A fixed-point FIR reference fixture

This small Python fixture isolates an eight-tap signed FIR operation. It does not implement DSP cycle order, register latency, ring management, saturation, or echo feedback. It exists to make coefficient sign, sample history ordering, and accumulator shifts explicit before the memory and timing pipeline is added.

from dataclasses import dataclass, field


@dataclass
class EchoFir:
    coefficients: list[int]
    history: list[int] = field(default_factory=lambda: [0] * 8)
    cursor: int = 0

    def __post_init__(self):
        if len(self.coefficients) != 8:
            raise ValueError("S-DSP echo FIR has eight taps")
        if any(not -128 <= value <= 127 for value in self.coefficients):
            raise ValueError("FIR coefficients must be signed 8-bit values")

    def push_and_filter(self, sample):
        if not -32768 <= sample <= 32767:
            raise ValueError("echo sample must be signed 16-bit")
        self.history[self.cursor] = sample
        total = sum(
            self.history[(self.cursor - tap) & 7] * coefficient
            for tap, coefficient in enumerate(self.coefficients)
        )
        self.cursor = (self.cursor + 1) & 7
        return total >> 6


fir = EchoFir([64, 0, 0, 0, 0, 0, 0, 0])
assert fir.push_and_filter(1200) == 1200

The fixture uses a simple circular history convention and arithmetic shift. Confirm tap ordering, product shifts, overflow, and saturation against a hardware-derived reference before treating it as a complete S-DSP model. Keep left and right histories separate, since cross-channel mixing is not implied by a stereo echo buffer.

Validation with impulses and memory traces

Start with all voices excluded from EON and a silent echo buffer. Then enable exactly one voice, hold its amplitude constant, and capture direct output, echo return, and new ARAM writes. Use a one-sample impulse to reveal the FIR tap sequence. Change one FIR coefficient at a time and verify the expected signed contribution at the corresponding delayed sample. Set EVOL to zero while retaining echo writes; the ring should continue changing even though no echo return is audible.

Next test feedback using a known initial ring pair and a controlled EFB value. Verify positive, zero, and negative feedback independently for left and right channels. Disable echo writes and confirm that output continues from the existing buffer while the target ARAM bytes remain unchanged. Move ESA to a new page and observe the first read and write addresses. Change EDL around the wrap boundary and record when the new ring length takes effect.

Finally schedule SPC700 reads and writes to the echo address near the DSP read/write point. Record CPU cycle, DSP sample phase, address, old byte, winning access, and new byte. Run the tests across save-state restoration in the middle of a ring frame; the echo offset, history buffer, filter cursor, and delayed register effects must all resume identically. Compare decoded samples numerically before listening, because subjective similarity does not prove address or timing accuracy.

Acceptance criteria

A reliable S-DSP echo implementation preserves the ARAM ring, EON routing, signed EVOL/EFB math, eight-tap left/right FIR history, register write phase, and echo-write inhibition as separate state. It shares ARAM arbitration with the SPC700 and can reproduce the same sample and memory trace after a save-state restore. With that structure, a wrong tail can be localized to routing, history order, coefficient arithmetic, ring geometry, or a bus collision.

Related:

Sources:

Comments