docs: Phase 3 design — interactive waveform + per-join crossfade/curves + destructive editor
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -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).
|
||||
Reference in New Issue
Block a user