Skip to content

feat: file recording & improved waveform - #31

Merged
Horuse merged 42 commits into
mainfrom
feat/file-recording-append-waveform
Sep 3, 2026
Merged

Horuse merged 42 commits into
mainfrom
feat/file-recording-append-waveform

Conversation

@Horuse

@Horuse Horuse commented Aug 23, 2026

Copy link
Copy Markdown
Owner

What does this PR do?

Three things that hang together around the File Recording node:

1. Append mode for WAV/AIFF. The recording node now has New / Overwrite / Append modes. Append reopens an existing WAV/AIFF and extends it (compressed formats forbid append, and the encoder enforces that). Overwrite-restart is detected via the recorder's stopped event and re-anchors the view at frame 0.

2. Shared live canvas waveform (src/lib/components/waveform_scope.svelte), used by both the Waveform node and the File Recording node. It renders on a single retina-scaled Canvas 2D surface with:

  • block-aligned min/max binning (stable under streaming), right-anchored grid so the live edge is always filled;
  • a grid-aligned realtime ring for the recording tail, overlaid on lazily loaded disk-file peak history (WAV/AIFF only), with no seam at the realtime↔file boundary;
  • pan/zoom with a 1:1 scrollbar, jump-to-live, loader during restart gaps, multi-channel lane clamping (maxChannels), and a per-lane minimum height;
  • disk history is warmed even while following so it survives the live-ring teardown on stop, and the ring stays bounded in file mode (history comes from disk).

3. Audio File node playback reliability (playback, not recording):

  • total frames are derived from the file itself (container metadata → end-seek → full decode scan), so the scrubber range and duration are real, not zeroed or grown from play time;
  • the scrubber holds the drag position locally instead of being yanked back by 100 ms progress events;
  • loop/stop-rewind restart by reopening the decoder — symphonia's isomp4 reader cannot rewind an already-read stream ("no atom pending read"), so restarting via a fresh probe is the reliable path across formats;
  • Opus playback via symphonia-adapter-libopus (symphonia has no first-party Opus codec); the adapter registers a libopus decoder into the symphonia registry, so Opus flows through the same demux/decode path as every other format;
  • a WAV whose data-chunk size exceeds the actual bytes now ends as a clean end-of-stream instead of killing the reader thread ("unexpected end of file").

Why is this the right approach?

  • Symphonia for all decoding, custom encoders for recording. Symphonia is decode-only; the encoders/ folder exists because recording needs writing, which symphonia can't do. Playback of WAV/AIFF/FLAC/MP3/AAC/Opus all routes through symphonia. Opus specifically has no first-party symphonia codec, so the third-party symphonia-adapter-libopus is the least custom code: it plugs into the same registry and gives us seek/duration/EOF for free. The alternative — a hand-written Ogg Opus decoder — was built first and then replaced because it duplicated seek/duration logic the standard path already provides.
  • Restart-by-reopen over seek-to-zero. Symphonia's MP4 reader can't rewind a consumed stream; reopening the file is deterministic and correct for every format, at the cost of a one-time probe per loop wrap.
  • Custom peak reader, not in-memory history. File recordings can be huge; loading min/max bins lazily for the visible range keeps RAM bounded.

Checklist

  • Diff is limited to the change — no unrelated edits
  • bun run check passes
  • cargo check --manifest-path src-tauri/Cargo.toml passes
  • bun run format leaves the tree clean
  • Generated TS types are committed with the Rust change (File Recording data/mode types regenerated)
  • New dependency (symphonia-adapter-libopus) justified above
  • No allocations/locks/syscalls added to cpal / SCK callbacks or DspWorker::run — all work is in the file-reader thread and the frontend

Platform coverage

  • Developed on: macOS 14
  • Tested on: macOS 14, Windows 11, Fedora 44
  • What I did to test: full Rust test suite (114 passed), including format roundtrip (WAV i16/i24/f32, AIFF i16/i24, FLAC i16/i24, MP3, Opus, and AAC on macOS), loop/seek-after-EOF restart, and a truncated-WAV clean-EOF regression test. Played an Audio File and a File Recording through the node graph.
  • Not tested: Linux

Per-OS impact: the reader (file_reader.rs), decoder registry, and the new format tests are shared across all three backends. The AAC encoder and its test are macOS-only and gated behind #[cfg(target_os = "macos")]; Linux/Windows skip them. Opus, WAV/AIFF/FLAC/MP3 decoding and the recording-modes logic are platform-independent.

The CLAP/VST3 plugin render tests were made resilient: a plugin that refuses to load, activate, or pass audio is reported and skipped rather than failing the suite, and the tests skip entirely when no plugins are installed — so CI (which has none) stays green.

Horuse added 30 commits August 16, 2026 20:45
@Horuse
Horuse merged commit 63d28a8 into main Sep 3, 2026
9 checks passed
@Horuse
Horuse deleted the feat/file-recording-append-waveform branch September 3, 2026 22:22
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