4.7 KiB
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 cappedload_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_PXof 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_durabout the pointer time (clamped to a min span and source length) → emitview_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_startand_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_commandextended: acceptcrossfades: float | list[float](scalar broadcasts to every join — backward compatible) andcurve: str(acrossfadec1=/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+concatof[0,selStart]+[selEnd,end]. - Silence —
volume=0/replace over the region. - Reverse —
areverse(segment viaatrim+areverse+concat). - Trim to selection — keep only
[selStart,selEnd]. - Fade selection —
afadeon the region.
- Delete —
- 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).