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

4.7 KiB
Raw Blame History

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 iacrossfade=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):
    • Deleteatrim+concat of [0,selStart]+[selEnd,end].
    • Silencevolume=0/replace over the region.
    • Reverseareverse (segment via atrim+areverse+concat).
    • Trim to selection — keep only [selStart,selEnd].
    • Fade selectionafade 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).