Files
8-cut/docs/plans/2026-07-02-audio-editor-fancier-design.md
T

73 lines
4.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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_endsel_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).