Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Toccata and the AD1848 model

Copperline’s Toccata is a hardware model of the MacroSystem Zorro II sound board, not an AHI-aware audio API: the guest enumerates the real manufacturer/product identity, programs the AD1848 codec’s indexed registers, pushes/pulls bytes through the board’s own 1024-byte FIFO, and polls the board’s status/control register exactly as the stock toccata.audio AHI driver does. See Writing Zorro board plugins for the autoconfig window and Audio sink service (src/audio/) for how the board’s output joins the mixer and stem capture.

Modelled against WinUAE/amiberry’s sndboard.cpp (byte-identical in both trees) as a behavioural oracle: register offsets, bit semantics, the FIFO’s threshold and byte-order quirks, and the interrupt condition are transcribed from what that reference does, not from the AD1848 datasheet alone -- several of the board’s most consequential behaviours (reg 12 pinned to plain-AD1848 mode, the FIFO’s little-endian-vs-big-endian byte order, underrun repeating rather than silencing) are MacroSystem-specific or emulator-specific choices a datasheet alone would not predict.

Implemented controller surface

Ad1848 (src/toccata/ad1848.rs) owns all chip state: the 16 reachable indexed registers, the 1024-byte play FIFO, the board’s own status/control register, the auto-calibration countdown, and DAC output volume. It has no Zorro/bus coupling -- Toccata (src/toccata.rs) is the board wrapper: autoconfig identity (BoardSpec::toccata), the 64 KB window’s four-port address decode, and the mixer-rate cadence.

Mixer cadence and resampling

The codec’s own programmed rate is independent of Copperline’s fixed 44.1 kHz mixer rate (established by the audio sink service, Audio sink service (src/audio/)). Toccata::tick runs two independent exact-ratio accumulators -- each the same shape as Paula::advance_audio’s own -- rather than one: advance_codec at the AD1848’s own active rate, and advance_mixer at the mixer’s fixed rate. Splitting them is deliberate, not incidental: a polyphase windowed-sinc resampler (src/audio/resample.rs, shared with the MT-32 engine) is inherently non-causal, since it needs taps on both sides of the output instant and primes by pulling a full window’s worth of input before its first output. If the resampler pulled straight from the chip (calling Ad1848::produce_one_sample directly from inside its own refill closure), its first use after startup or a rate change would drain dozens of FIFO bytes and evaluate the half-empty/interrupt condition all at once, decades ahead of when a real codec would reach them -- the resampler’s own lookahead would leak into hardware-state timing correctness.

advance_codec is what actually drains the FIFO and evaluates the half-empty/interrupt condition (the reference’s own audio_state_sndboard_toccata per-sample body, transplanted verbatim), paced causally by cck at the codec’s own rate; each produced sample is queued into decoded, a plain FIFO of raw pre-resample frames. advance_mixer pulls from that passive queue through the resampler -- never from the chip directly -- repeating the chip’s last known sample (via the side-effect-free Ad1848::peek_last_sample) if decoded is momentarily empty. This keeps the resampler’s non-causal lookahead confined to shaping the interpolated waveform; it can never reorder when a FIFO byte drains or an interrupt raises. Resamplers are cached per codec rate (Toccata’s resamplers map, at most 14 entries, the AD1848’s legal rate count) so returning to an already-programmed rate never rebuilds its kernel table.

Produced frames push into ToccataAudioRing (src/chipset/paula.rs, alongside CdAudioRing), which push_mixed_frame pops one frame from per mixer tick -- a plain per-frame pop, not a rate conversion, since the board already resampled before pushing. Unlike CD-DA’s bursty per-sector delivery, the board’s own tick cadence matches the mixer’s, so both the decoded queue and the ring stay near-empty in steady state; their fixed capacities are a safety margin against a stalled consumer, not a buffering requirement.

A cached resampler’s history buffer holds the last ~64 input frames it convolves over, so Toccata::reset() (a guest CPU reset, matching every other in-tree board’s ZorroDevice::reset() -- a full hardware reinit) zeroes both accumulators, clears decoded, and clears the whole resampler cache, not just Ad1848’s own registers/FIFO. Without that, a few dozen milliseconds of pre-reset audio would bleed through the stale kernel window into what should be post-reset silence -- covered by reset_clears_stale_resampler_history_so_silence_follows_immediately in src/toccata.rs.

Interrupt

INT6/EXTER, level-sensitive (Toccata::int6_line reads Ad1848::int6_pending). The condition requires the codec to have been started (reg 9’s playback/ record enable bits), the board’s own STATUS_FIFO_CODEC gate, the relevant direction’s FIFO-enable bit, that direction’s INTENA bit, and the edge-latched half-empty/half-full flag -- all evaluated once per produced sample. Reading the status register acknowledges (clears) pending interrupt bits; the half-empty/half-full latch itself is cleared by FIFO port access instead, so a status-read ack with the latch still set re-raises the interrupt on the next produced sample.

Determinism

Every board-side computation -- register writes, FIFO drains, interrupt evaluation, the resampler’s phase -- is driven purely by tick’s cck argument or by CPU register accesses, both already deterministic inputs. Nothing reads wall-clock time, so a Toccata-fitted machine is warp-safe and reproducible exactly like the rest of the emulated audio path (see Audio sink service (src/audio/)'s determinism section) -- two runs of the same scripted scenario produce byte-identical toccata.wav stem captures.

Savestates

Toccata derives Serialize/Deserialize directly (Boxed as BoardDevice::Toccata, like Picasso2) with no #[serde(skip)] fields: codec_acc/decoded are genuine machine state for the reason given above, and the resamplers cache -- despite only shaping the waveform, never FIFO/IRQ timing -- is serialized too, via Resampler’s own manual Serialize/Deserialize in src/audio/resample.rs (its derived kernels table is rebuilt from l/m on load rather than stored, since it is a pure function of the reduced rate ratio). This makes a save-state load reproduce an uninterrupted run’s output exactly, not just its FIFO/IRQ timing -- a resumed run’s toccata.wav stem is byte-identical to what an uninterrupted run would have produced at the same point, covered by savestate_round_trip_reproduces_an_uninterrupted_runs_output in src/toccata.rs.

What’s out of scope for M1-M3