From 799b0647036c4d0f3341adeff5169f124a4c112d Mon Sep 17 00:00:00 2001 From: Ethanfel Date: Thu, 2 Jul 2026 16:51:29 +0200 Subject: [PATCH] =?UTF-8?q?docs:=20Phase=203=20design=20=E2=80=94=20intera?= =?UTF-8?q?ctive=20waveform=20+=20per-join=20crossfade/curves=20+=20destru?= =?UTF-8?q?ctive=20editor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 --- .../2026-07-02-audio-editor-fancier-design.md | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 docs/plans/2026-07-02-audio-editor-fancier-design.md diff --git a/docs/plans/2026-07-02-audio-editor-fancier-design.md b/docs/plans/2026-07-02-audio-editor-fancier-design.md new file mode 100644 index 0000000..c73a792 --- /dev/null +++ b/docs/plans/2026-07-02-audio-editor-fancier-design.md @@ -0,0 +1,72 @@ +# Audio editor — "fancier" features (Phase 3) — Design + +**Goal:** Three deferred editing capabilities on the Audio tab, phased: +- **3a — Interactive waveform selection** (foundation): drag in/out handles, zoom, moving playhead. +- **3b — Per-join crossfade + curves** in the Merge pane. +- **3c — Destructive clip editor**: cut/delete/silence/reverse a region with undo/redo. + +**Builds on:** Phase 1/2 — `AudioWaveform` (read-only strip), `load_region_samples`/`peaks`, extract/audition via `_current_edit_filters`, the Merge pane + `build_crossfade_merge_command`. + +**Runs in:** Python/Qt client + `core/`. No server/DB change. + +**Order:** 3a first (unlocks precise selection everywhere), then 3b (small engine extension), then 3c (largest; reuses 3a's widget). Same `audio-tab` branch, reviewed per task. + +--- + +## Phase 3a — Interactive waveform selection + +Turn `AudioWaveform` into a selection surface that drives the extract region. Extract/audition/add-to-merge already use `_cursor` + `_spn_audio_len`, so they honor the dragged selection with no handler change. + +**View + selection model (all seconds, absolute in source):** +- `_view_start`, `_view_dur` — the decoded, zoomable visible window (peaks are for this window, via the existing capped `load_region_samples`). +- `_sel_start`, `_sel_end` — the selection within the window. +- Pixel↔time helpers (pure, unit-tested): `t = _view_start + (x/width)·_view_dur`; `x = (t−_view_start)/_view_dur·width`. + +**Interaction:** +- Press within `HANDLE_PX` of an in/out handle → drag that edge; press elsewhere → start a new selection at that time, drag to extend. +- Move → update the active edge, clamp to the window, keep `start < end`, repaint. +- Release → emit `selection_changed(sel_start, sel_end)`. +- Wheel → zoom `_view_dur` about the pointer time (clamped to a min span and source length) → emit `view_changed(view_start, view_dur)`. +- Height grows 48→~96 for usable dragging. + +**Painting (extends current):** peak bars + a translucent selection band + two bright handle lines + an optional playhead line. + +**Signals:** `selection_changed(float,float)`, `view_changed(float,float)`. + +**MainWindow wiring:** +- Refresh (↻ / on load): decode `[_cursor, _cursor+view_dur]` (view_dur defaults to max(length, a few s)); `set_view(...)`, `set_peaks(...)`, selection = `[_cursor, _cursor+len]`. +- `selection_changed` → set `_cursor = sel_start` and `_spn_audio_len = sel_end−sel_start` (which already refreshes the timeline band); block signals while syncing to avoid feedback. +- Typing `_spn_audio_len` → update the waveform selection (two-way sync). +- `view_changed` → re-decode peaks for the new window (throttled, capped decode). + +**Playhead (optional within 3a):** during audition a QTimer sets `_playhead = sel_start + elapsed` (ffplay gives no position; elapsed-based is close enough), cleared on stop. Deferrable if it complicates. + +**Tests:** unit-test the pixel↔time round-trip + clamping (pure); smoke-test that a simulated drag emits `selection_changed` and syncs `_cursor`/length, and that typing a length moves the selection. + +--- + +## Phase 3b — Per-join crossfade + curves + +- Merge items gain a per-join crossfade + curve (stored `UserRole+2` = float, `UserRole+3` = curve name). New joins default from the global `_spn_crossfade` + a new curve combo; double-click a row to override that join. +- `build_crossfade_merge_command` extended: accept `crossfades: float | list[float]` (scalar broadcasts to every join — backward compatible) and `curve: str` (acrossfade `c1=/c2=`; e.g. `tri`/`exp`/`log`/`qsin`). Join *i* → `acrossfade=d={xf[i]}:c1={curve}:c2={curve}`. +- The duration guard checks each clip against its own join's crossfade. + +--- + +## Phase 3c — Destructive clip editor (largest) + +A modal editor over ONE file (the current selection rendered to a temp, a merge output, or an opened file). Detailed plan authored after 3a/3b land. +- Full-file interactive waveform (reuse the 3a widget) + a selection. +- Region ops, each rendering a NEW temp version (non-in-place → clean undo): + - **Delete** — `atrim`+`concat` of `[0,selStart]`+`[selEnd,end]`. + - **Silence** — `volume=0`/replace over the region. + - **Reverse** — `areverse` (segment via `atrim`+`areverse`+`concat`). + - **Trim to selection** — keep only `[selStart,selEnd]`. + - **Fade selection** — `afade` on the region. +- **Undo/redo:** a stack of rendered version files; undo pops to the prior file, redo re-applies. Robust and simple. +- **Save / Save-as.** Pure per-op command builders in `core/ffmpeg.py` (TDD); the dialog wires them. + +--- + +## What this does NOT do +- No multi-track timeline / overlap mixing; no spectral editing; no saved editor projects (session/file-based only).