Skip to content

feat(sampler): Ultimate Audio FPGA PCM sampler as the default U64 video-audio backend#46

Merged
kfox merged 2 commits into
mainfrom
feat/ultimate-audio-sampler
Jun 26, 2026
Merged

feat(sampler): Ultimate Audio FPGA PCM sampler as the default U64 video-audio backend#46
kfox merged 2 commits into
mainfrom
feat/ultimate-audio-sampler

Conversation

@kfox

@kfox kfox commented Jun 26, 2026

Copy link
Copy Markdown
Owner

What

Adds the U64 firmware's "Ultimate Audio" FPGA PCM sampler ($DF20-$DFFF) as the high-fidelity, default video-audio backend on the Ultimate 64. It plays 8/16-bit PCM straight out of REU SDRAM with zero SID / $D418 / NMI / CPU / turbo involvement — so it's immune to the bus-halt/badline problems the 4-bit DAC fights, and sounds like an actual sound card instead of a digi-player. The 4-bit $D418 DAC stays for TeensyROM (no FPGA sampler) and as an opt-in lo-fi path; mic/webcam audio always uses the DAC.

How

  • c64cast/sampler.py (new): pure register helpers (divider / control byte / 8-/16-bit PCM pack / big-endian channel layout) + UltimateAudioSampler, a streaming REU ring. Channel 0 is programmed as an A↔B loop; a writer thread REUWRITEs decoded PCM ahead of a wall-clock-computed read head ((monotonic − gate_time) · rate), wrapping, NEUTRAL-padding only past a low watermark. The FPGA clock is crystal-exact, so the read head is computed (never read back) — open-loop and drift-free, no servo/governor/NMI.
  • Config: [audio].backend (auto/dac/sampler, default auto), sampler_sample_rate (44100), sampler_bits (16); resolve_audio_backend (mirrors resolve_use_reu_staged) + validate_sampler_cfg. VideoScene drives whichever audio object it's handed polymorphically.
  • Capability + provisioning: HardwareProfile.supports_sampler; doctor.provision_sampler/restore_sampler/sampler_is_available/_wants_sampler + a --doctor probe, all live + volatile + restored at teardown. The ring lives in REU SDRAM, so a sampler run also provisions the REU (16 MB) — which makes overlay-free bitmap video resolve to the tear-free REU bank-swap path (the sampler installs no $0314 IRQ, so video staging and the sampler coexist with no contention).
  • fps: sampler-audio bitmap video is off the bus, so it's not subject to the DAC's 20 fps cap — it defaults to the muted half-rate (30/25) pending an HW fps A/B (follow-up).
  • Wizard backend prompt, config/examples/scene-video-sampler.toml, regenerated schema, CLAUDE.md + docs/{caveats,usage}.md, and 51 new tests.

Verification

  • make check green: ruff + mypy --strict + pyright + 1657 tests (51 new).
  • HW-verified on a real Ultimate 64: the streaming ring was de-risked standalone first (gapless 440 Hz tone, correct pitch, no drift over a 3-minute clip), then the full production path was confirmed — --doctor reports the sampler mapped + audible and the REU enabled for it; a live backend = "sampler" mhires video run selected the sampler + REU-staged video, rendered, and played clean high-fidelity audio.

Follow-ups (deferred)

  • Raise the bitmap-video fps default for sampler audio via an HW fps A/B (the sampler frees the bus).
  • Add write-ahead-lead headroom for the case where the PyAV demuxer decodes video and pushes audio on the same thread.

kfox added 2 commits June 26, 2026 14:49
… ring

New c64cast/sampler.py: the U64 "Ultimate Audio" ($DF20-$DFFF) FPGA PCM
sampler as the high-fidelity video-audio path. Plays 8/16-bit PCM up to
48 kHz straight from REU SDRAM with zero SID/$D418/NMI/CPU/turbo, so it
is immune to the bus-halt/badline problems the 4-bit DAC fights.

Two halves:
- Pure register helpers (unit-testable): divider_for_rate, control_byte,
  pack_pcm (signed 8-bit / int16-LE), channel_register_writes (big-endian
  layout), program_channel/gate_off.
- UltimateAudioSampler: the scene-facing object (sample_rate,
  position_seconds, push_samples, get_recent_samples, stop) built on a
  streaming REU ring. Programs channel 0 as an A<->B loop over the ring;
  a writer thread REUWRITEs PCM ahead of a wall-clock-computed read head
  ((monotonic - gate_time) * rate), wrapping, NEUTRAL-padding only past a
  low watermark. Open-loop on the crystal-exact FPGA clock -> drift-free,
  no servo/governor.

De-risked on .64 + Cam Link before any integration: a 30 s 440 Hz tone
(zero-crossing 439.2 Hz = correct rate, 0 dropouts) and a 180 s music
clip (best_s 1.01 after correcting the lossy avfoundation capture clock;
write-ahead lead stayed bounded -> drift < 0.3%). User-confirmed: clear,
steady, high-quality, no wobble.

Not yet wired into config/scenes/cli/doctor -- next.
…udio backend

Integrate the HW-de-risked UltimateAudioSampler (b6f4e59) end to end so a
video scene on the U64 plays its soundtrack through the FPGA PCM sampler
(high fidelity, off the C64 bus) instead of the 4-bit $D418 DAC.

- config: [audio].backend ("auto"|"dac"|"sampler", default auto),
  sampler_sample_rate (44100), sampler_bits (16); resolve_audio_backend
  (mirrors resolve_use_reu_staged) + validate_sampler_cfg. build_scene
  swaps the DAC streamer for a per-scene UltimateAudioSampler when the
  backend resolves to "sampler". Bitmap-video fps default is no longer
  capped at 20 for sampler audio (it's off the bus) — defaults to the
  muted half-rate (30/25) pending an fps A/B.
- backend: HardwareProfile.supports_sampler (True Ultimate, False TR).
- doctor: sampler_is_available + provision_sampler/restore_sampler +
  _wants_sampler + a --doctor probe. The sampler's ring lives in REU
  SDRAM, so _wants_sampler also pulls the REU into _wants_reu — a sampler
  run provisions the REU (16 MB), which makes overlay-free bitmap video
  resolve to the tear-free REU bank-swap path (no $0314 contention since
  the sampler installs no IRQ).
- cli/ensemble: _resolve_sampler_available + provision/restore wired into
  build_stack/teardown_stack; SystemStack.sampler_available/sampler_restore
  threaded through scenes_from_config + reloads/ensemble followers.
- scenes: VideoScene drives the sampler polymorphically (isinstance branch
  → start() + push_samples + mark_eof); Scene.audio widened to a union;
  mic paths narrowed to AudioStreamer.
- wizard: prompts for the backend (+ bits/rate) when audio is on.
- example config scene-video-sampler.toml, schema regen, CLAUDE.md +
  docs/{caveats,usage}.md, 51 new tests (helpers/resolve/validate/streamer/
  provision/restore/_wants coupling). make check green.
@kfox kfox merged commit 9f34aba into main Jun 26, 2026
5 checks passed
@kfox kfox deleted the feat/ultimate-audio-sampler branch June 26, 2026 20:43
@codecov

codecov Bot commented Jun 26, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 71.18998% with 138 lines in your changes missing coverage. Please review.
✅ Project coverage is 79.49%. Comparing base (46cc06d) to head (2fa0164).
⚠️ Report is 3 commits behind head on main.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
c64cast/sampler.py 82.57% 31 Missing and 15 partials ⚠️
c64cast/doctor.py 68.33% 34 Missing and 4 partials ⚠️
c64cast/cli.py 5.00% 19 Missing ⚠️
c64cast/wizard.py 35.71% 14 Missing and 4 partials ⚠️
c64cast/scenes.py 13.33% 12 Missing and 1 partial ⚠️
c64cast/config.py 86.20% 3 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #46      +/-   ##
==========================================
- Coverage   79.76%   79.49%   -0.28%     
==========================================
  Files          68       69       +1     
  Lines       13020    13491     +471     
  Branches     1924     2013      +89     
==========================================
+ Hits        10385    10724     +339     
- Misses       2195     2303     +108     
- Partials      440      464      +24     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant