Skip to content

feat: M26a vinyl rip — record real records into the library#1

Open
kLOsk wants to merge 11 commits into
mainfrom
feat/vinyl-rip
Open

feat: M26a vinyl rip — record real records into the library#1
kLOsk wants to merge 11 commits into
mainfrom
feat/vinyl-rip

Conversation

@kLOsk

@kLOsk kLOsk commented Jul 6, 2026

Copy link
Copy Markdown
Owner

Summary

Drop the needle, get split, tagged, pre-analyzed FLAC tracks in the library. First increment of the M26 vinyl-rip milestone (PRD §5.2.7): capture + manual splits + encode + import, fully offline by design.

  • Engine: stereo record tap on ThruSource (pre-gain, underrun zero-fill, drop-on-overflow, alloc-free) + attach_thru_source_with_taps options-struct API (existing variants delegate to one shared core).
  • dub-rip (new): session state machine, crash-safe WAV spill + chunk-aligned live envelope, validated split plan, atomic rip.json manifest, idempotent commit (FLAC → Vorbis tags → import → pre-analysis), lossless side archive.
  • dub-encode (new): 24-bit FLAC (flacenc) + Picard-convention tagging (metaflac) — dep graph stays fully permissive; MP3/LAME evaluated and deferred. Includes the flacenc min/max-block-size workaround (symphonia rejects its default output as variable-blocksize).
  • dub-library: public import_file + analyze_compute_with_track.
  • CLI: dub rip headless dogfood.
  • FFI 52→54 + Swift UI: DubRipSession polling surface, start_thru_for_rip (tap wired at attach time), Preferences "Vinyl recording" toggle, PREP/PERF override switch (signed-off auto-detect exception), rip bar + live waveform + draggable split markers + review panel with per-segment progress. DEBUG-only built-in-soundcard fallback for no-rig dogfooding.
  • Docs: PRD §5.2.7 + M26 milestone row, LICENSE-DEPENDENCIES, LIBRARY-SCHEMA rip artifacts, SHIPPED/LESSONS/UI-BACKLOG §6; fixes stale fingerprint-algorithm and dub-stretch claims.

Deferred (tracked in UI-BACKLOG §6 + PRD): M26b auto gap detection / auto start-stop / recovery; M26c recognition (AcoustID + MusicBrainz + Discogs).

Test plan

  • cargo test --workspace: 1350 tests green (new: 9 record-tap + 5 with_taps engine tests incl. assert_no_alloc; 18 dub-rip incl. full capture→split→commit→retry pipeline + producer-drop salvage; 12 dub-encode symphonia round-trips + tag read-back; 6 dub-library import/analyze refactor tests; 13 FFI session tests incl. cancel-refusal-after-import).
  • cargo clippy --all-targets -- -D warnings + cargo fmt --check + ./scripts/check-docs.sh clean.
  • make app BUILD SUCCEEDED; make snapshot — 12 new RipSnapshotTests green.
  • Manually verified end-to-end on the built-in soundcard via the DEBUG dev-override (record → split → encode → import → tracks pre-analyzed in the library).
  • Note: base branch is being fast-forwarded to local main separately; once it lands, this PR's diff reduces to the four rip commits.

🤖 Generated with Claude Code

https://claude.ai/code/session_018eHhvnnd8i7C7jvK9Qu8pY

kLOsk and others added 11 commits June 27, 2026 18:44
…ter bar

User-owned organization (schema v8):
- Star ratings: `tracks.user_rating` (0–5), a clickable star column; overrides
  imported per-source ratings. iTunes `Rating` (0–100 → 0–5) now imported.
- Colour labels: `tracks.color`, a fixed 8-swatch palette; the chosen colour
  tints the track row and shows as a bordered box in the Color column.
- Favourites strip: 8 quick-access slots above the library; drag a Dub crate or
  imported playlist in, click to load, right-click to clear. Imported favourites
  are stored by `(source, name-path)` so they survive a re-scan.

Dynamic filter bar (client-side, over the in-memory current view):
- A configurable, collapsible row of Serato-style filter boxes below the
  favourites: categorical value lists with counts (Genre/Key/Color/Source/
  Artist/Album/Year/Version/Composer), auto BPM tempo buckets, and a ≥N-star
  Rating threshold. Multi-select = OR within a box, AND across boxes; value
  lists cascade (counts narrow as other boxes are picked). Boxes derive from the
  current view and rebuild on source switch; the enabled-set + collapsed flag
  persist, selections are transient. `recomputeSortedTracks()` now sorts the
  filtered subset, so the AppKit table inherits filtering at one choke point.

Fixes:
- Apple Music tracks rendered untitled: the browse query joined iTunes only for
  ratings, not the §8.1 display chain. iTunes is now a first-class display source
  (title/artist/album/genre/year/comment/composer/track#, source label, sort
  keys). Regression test added.
- Colour-column swatch was invisible: a Shape inside a `.borderlessButton` menu
  label is dropped by that style. The swatch now renders as normal cell content
  with the palette menu as a clear overlay.

FFI: rating/color on `LibraryTrack`; `set_user_rating` / `set_track_color`;
favourite slot list/set/clear/tracks + `LibraryFavoriteSlot`. The filter bar is
Swift-only.

Tests: dub-library 223, dub-ffi 60, dub-cli 150 green; clippy clean. macOS app
builds; dogfooded on the rig.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014AqZ6CVZ2wXvRisoTg7vWg
- Introduced key lock feature for decks, allowing pitch to be held while tempo follows the platter. This includes a toggle for enabling/disabling key lock and selecting the time-stretch engine (resampler or WSOLA).
- Added UI components for key lock control, including live A/B selection and pitch adjustment for testing.
- Updated the engine's command structure to support key lock operations and integrated telemetry for key lock state.
- Enhanced the library to include new dependencies for time-stretching and key lock functionality.

Tests: Verified functionality across the engine and UI components, ensuring key lock engages correctly and the UI reflects the current state.
- Introduced a new echo-out feature that allows users to engage a 1-beat, 100% wet echo effect on decks, enhancing performance capabilities.
- Updated the engine to support echo-out commands, including engaging, releasing, and setting parameters for the echo effect.
- Enhanced the UI to include an echo-out toggle button, which is conditionally displayed based on user preferences.
- Added necessary telemetry to track the echo state, ensuring accurate feedback in the UI and engine.

Tests: Verified the functionality of the echo-out feature across the engine and UI components, ensuring correct engagement and disengagement behavior.
Adds the dub siren, the app's signature FX (PRD §6.3). Simple mode is a
grid of classic preset one-shots (siren, alarm, laser, bomb, machine gun,
lickshot, UFO, riser) fired by pad or keyboard (Z X C V B N M ,).

- dub-dsp: SirenVoice — an additive generator (oscillator + pitch LFO +
  one-shot pitch sweep + filtered noise + exp-decay envelope + percussive
  retrigger + slap-back delay + lo-fi sample-rate/bit crush). Preset bank
  resolved off-RT; RT-safe (no alloc/locks, denormal-protected, xorshift
  noise). SN76477/GS1-inspired; a faithful chip port follows for Expert mode.
- dub-engine: per-deck siren rendered after echo-out (so echo's dry-mute
  can't swallow it); fire-by-id command + precomputed patch bank;
  siren_state telemetry; optional beat-matched echo override.
- dub-ffi: fire_siren_preset / release_siren / siren_preset_names (FFI 47).
- apple: SIREN preset grid (Performance + Prep), keyboard triggers,
  Preferences toggles (enable + beat-match echo), siren colour token.

Test plan:
- cargo test -p dub-dsp / -p dub-engine / -p dub-ffi (pass; incl. RT-safety
  under assert_no_alloc, audibility + crush regression tests).
- cargo clippy --workspace --all-targets -D warnings (clean); cargo fmt.
- make rt-audit (no RT alloc); make app (BUILD SUCCEEDED).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014AqZ6CVZ2wXvRisoTg7vWg
…-FX DSP

The dub siren is now a self-contained instrument (voice + its own PT2399
echo + volume) rendered after the deck echo-out and the FX rack, following
the device-independent three-mode contract (Simple tap / Advanced DUB
super-knob / Expert individual controls + unit selector).

Units:
- GS1  — Rigsmith/Honsitak-style 8-shot ROM chip (dub-dsp::Hk628, +um3561)
- DS01E — Benidub analog-osc siren: 4 MODE tones on the generic SirenVoice,
          PITCH/RATE/TRIGGER mirroring the real unit
- SN76477 — MAME-faithful complex-sound-generator port

Shared PT2399 dub-echo chip (dub-dsp::Pt2399): variable-delay pitch-dive,
degrading feedback, companding hiss, delay-dependent grit, resonant TPT-SVF
FILTER (dark LP -> open -> thin HP, log-mapped LUT) and momentary ECHO CUT.

Vintage-FX DSP chain also landed (spring Schroeder reverb / RE-201 tape /
Big Knob HPF / Mu-Tron phaser) with RT-safe macro->coefficient LUTs, parked
behind the deferred deck-role FX channel (ControlMode::Fx lands dormant).

All siren/FX levels trimmed to the -14 LUFS track target; every macro
precomputes its transcendentals off-RT so apply_command stays pure.

Engine: siren_unit/benidub_patches/sn76477_presets state, siren render pass,
command + handle trios. FFI 47->52: SirenUnit, set_siren_unit/_voice/
_controls/_dub_macro, siren_unit_preset_names, ControlMode::Fx. Swift:
per-unit pad relabel, DUB super-knob, Prep-surface Expert panel.

Docs: SHIPPED (M14/M15/M16), UI-BACKLOG §5 (deferred siren/FX work),
LESSONS (DSP/FX RT-safety + render-order), roadmap dashboard.
Removed accidentally-committed insta .pending-snap review artifacts and
gitignored them.

Deferred: siren-sound fine-tuning -> polish phase; Performance-surface /
in-deck Expert panel -> later; FX rack as a deck-role FX channel -> post-release.

Test plan:
- cargo test -p dub-dsp -p dub-engine -p dub-ffi (green)
- cargo clippy -p dub-dsp -p dub-engine -p dub-ffi --all-targets -D warnings (clean)
- cargo fmt
- make app (BUILD SUCCEEDED earlier this session; no code change since)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0187AfZpbDCNe7mKYfrK979r
- Implemented a streaming decode-ahead mechanism for track loading, allowing playback to start while the tail of the track continues to decode in the background. This change significantly reduces the time before playback begins, especially for longer tracks.
- Updated the `load_track` function to decode a few seconds of audio synchronously, while the full buffer is pre-allocated, improving responsiveness.
- Introduced a new `SampleStore` structure to manage interleaved sample storage, supporting both one-shot and streaming decodes without compromising safety or performance.
- Enhanced the analysis pipeline to operate in a lock-free manner, allowing for concurrent track analysis without blocking library access, thus improving overall application performance.
- Added tests to ensure the new streaming functionality and analysis improvements work as expected without introducing regressions.

This refactor aligns with the PRD principles of maintaining performance while ensuring reliability and responsiveness during track loading and analysis.
- Updated the README to reflect the new schema version (v9) and the addition of 18 tables.
- Revised the PRD to clarify the status of customizable columns and the data plumbing for per-source disagreement views.
- Adjusted the roadmap to indicate the re-scoped M11d-columns milestone, focusing on the remaining data plumbing tasks.
- Corrected the current applied schema version in the LIBRARY-SCHEMA documentation to v9.
- Enhanced documentation for the new features including hot cues, reverse loops, and external library importers.
…ttach

M26a. ThruSource gains with_record_tap(): full-quality interleaved
stereo, pre-gain (a deck-gain ride must not print into the rip
archive), carrying the underrun zero-fill so the recorded timeline
stays continuous; drop-on-overflow like the M8/M9 mono taps, one
push_slice memcpy per block, alloc-free under assert_no_alloc.

EngineHandle::attach_thru_source_with_taps(ThruTaps) replaces the
combinatorial attach-variant growth: the four existing variants now
delegate to one shared trunk (deck check, config validate, reclaim,
source build, command push), so a new tap can never fork the attach
semantics. Returns the raw record consumer (ThruTapHandles) — the
engine knows nothing about ripping. Record ring sized 4 s stereo to
absorb capture-worker file-I/O stalls.

Test plan: cargo test -p dub-engine (266 tests: 9 new record-tap
unit tests incl. alloc-free with all three taps, 5 new with_taps
handle tests incl. full input-ring-to-record-ring path; all existing
attach-variant tests unchanged and green); clippy -D warnings clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018eHhvnnd8i7C7jvK9Qu8pY
…ctors, dub rip CLI

M26a: record a real record side, split it, and land tagged,
pre-analyzed FLAC tracks in the library. Fully offline by design
(recognition arrives with M26c in a separate leaf crate).

dub-rip (new): RipSession state machine (Armed -> Recording ->
Stopped{Manual|MaxDuration|InputLost}); off-RT capture worker
draining the engine record tap into a crash-safe 32-bit-float WAV
spill while building a chunk-to-frame-aligned live peak envelope
(dub-peaks Decimator); validated manual split plan (min segment 5 s,
proptest-tiled); atomic rip.json manifest (tmp+fsync+rename on every
mutation); idempotent commit — per segment: slice PCM -> FLAC ->
Vorbis tags -> import_file -> pre-analyze via
analyze_compute_with_track (fingerprint/grid/key/LUFS/waveform
populated at import); lossless side.flac archive; spill deleted only
after full success. Producer loss mid-rip fail-safe-stops with the
captured audio intact.

dub-encode (new): 24-bit FLAC via flacenc (pure Rust, Apache-2.0) +
Picard-convention Vorbis/PICTURE tagging via metaflac (MIT) — the
dep graph stays fully permissive; MP3/LAME (LGPL) evaluated and
deferred. Works around flacenc 0.5.1 shrinking StreamInfo
min_block_size on the final short frame (symphonia rejects the file
as variable-blocksize; forced min == max per libFLAC convention).

dub-library: public import_file() extracted from the folder importer
(single per-file path; UnsupportedExtension/ImportFailed variants)
and analyze_compute_with_track() so analysis can run on in-RAM PCM.

dub-cli: `dub rip` — headless dogfood (Enter starts/stops or
--duration; --splits/--titles/--artist/--album/--genre/--year;
--library; session dirs under ~/Music/Dub/Rips/<timestamp>).

Test plan: cargo test --workspace green (dub-rip: 15 unit + 3
integration incl. full capture->split->commit->retry pipeline and
producer-drop salvage; dub-encode: 12 incl. symphonia round-trips +
tag read-back; dub-library: 235 incl. new import_file/
analyze_compute_with_track suites); clippy -D warnings + fmt clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018eHhvnnd8i7C7jvK9Qu8pY
… -> 54)

M26a Swift increment A. FFI (crates/dub-ffi/src/rip.rs, polling-only
per house style): DubRipSession (start/stop/cancel, 10 Hz status,
generation counter for callback-free refetch, packed 12-byte
envelope chunks wire-compatible with peaks_extend, split CRUD with
stable ids, segments + metadata, confirm_encode_and_import on a
named dub-rip-commit worker with coarse job_progress, session_dir);
start_thru_for_rip wires the deck-0 record tap at Thru-attach time
(the input consumer is take-once — a tap cannot be bolted on later);
cancel refuses once any segment imported (the FLACs live in the
session dir and the library references them by path — retry is the
recovery path); list_raw_input_devices exposes the pre-classifier
HAL input list for the DEBUG built-in-soundcard fallback.

Swift: Preferences "Vinyl recording" toggle; PREP/PERF override
switch in the status strip (signed-off exception to hardware
auto-detect — override clears on interface unplug); PrepRipBar
(record button, elapsed, level meter with clip flash, stop);
RipLiveOverview (incremental growing waveform, shared
OverviewDecimator extracted from TrackOverviewView);
RipSplitMarkerOverlay (drag with local echo + 30 Hz throttle,
double-click add, context-click remove, arrow-key nudge, boundary
audition); RipReviewPanel (editable metadata cards, per-segment
encode/import progress dots, idempotent retry, two-step discard).
DEBUG builds record from the built-in mic when the Developer mode
override is engaged (mono duplicated to both slots via channel map);
Release stays DJ-interface-only. Rip availability check kept cheap —
HAL enumeration deferred to the record click (SwiftUI re-evaluates
at deck-poll rate).

Test plan: cargo test -p dub-ffi (76 tests incl. 13 rip-session
tests: state mapping, split CRUD rollback, envelope packing, full
commit -> Done with library assertions, cancel-refusal after import,
closed-library failure); clippy -D warnings clean; make app BUILD
SUCCEEDED; make snapshot — 12 new RipSnapshotTests green
(PrepRipBar/RipReviewPanel/RipSegmentCard/marker-overlay states);
manually verified end-to-end on the built-in soundcard (record ->
split -> encode -> import).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018eHhvnnd8i7C7jvK9Qu8pY
PRD: new §5.2.7 (vinyl rip feature spec incl. the Preferences-gated
PREP/PERF override as a documented auto-detect exception, the
FLAC-not-MP3 rationale, review-always flow, and the DEBUG built-in
fallback status) + the M26 row in §12.2 with a/b/c sub-milestones
and the M21 seeding synergy.

LICENSE-DEPENDENCIES: flacenc (Apache-2.0, incl. the min/max
block-size symphonia workaround), metaflac (MIT), serde/serde_json
promoted workspace-wide; explicit statement that MP3/LAME (LGPL) was
evaluated and deferred so the fully-permissive posture is unchanged;
ureq noted as the planned M26c addition.

LIBRARY-SCHEMA: rip session artifacts (rip.json manifest v1, session
dir layout, atomic write + idempotent-retry semantics, Picard-style
Vorbis tagging — schema stays v9); fingerprint algorithm claim
corrected (dedupe uses the chromaprint TEST1 preset, not AcoustID's
TEST2 — M26c computes TEST2 transiently).

AGENTS: dub-encode + dub-rip in the repo layout, dep-list additions,
`rip` in the dub-cli command list; stale dub-stretch "Rubber Band
FFI" description fixed (M14 shipped pure-Rust WSOLA); same TEST1
fingerprint correction.

SHIPPED: M26a entry + refreshed currently-shipped header (was stuck
at M11d.8). LESSONS: new Vinyl rip section (tap-at-attach-time,
flacenc block-size gotcha, classifier hides the built-in mic,
spill-to-disk rationale, cancel-refusal after import) + three stale
bullets corrected (device-picker invariant gains the signed-off M26
exception; GPLv3 bullet now reflects the permissive-graph reality;
snapshot tests exist). UI-BACKLOG: new §6 with the deliberate M26a
gaps (R-39 live encode progress, R-40 recovery banner, R-41 capture
deck pane, R-42 Keychain token, R-43 Performance-mode rip +
monitor mute).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018eHhvnnd8i7C7jvK9Qu8pY
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