Add crossfade + mixed overlap between tracks
Adjacent tracks now blend for a configurable window. Two modes:
- Crossfade: cosine/sine curves (outgoing^2 + incoming^2 = 1) — perceived
loudness stays constant across the overlap.
- Mixed: no curves, both tracks at full volume — sums additively for a
louder overlap window (DJ-style mix).
The SymphoniaPlayer worker now runs two decoders in parallel during an
overlap. Each track has its own cpal Stream + ring buffer; the OS mixes
them at the audio layer. Fade envelopes are applied per-frame in the
sample push path (using this track's produced_output_frames as the
progress clock).
Data
- fin-config: CrossfadeMode (Off/Crossfade/Mixed) + CrossfadeSettings
(mode + duration_secs, default 5s). #[serde(default)] on the Config
field keeps older configs loading.
- fin-player: crossfade module with fade_at() + FadePair, 6 unit tests
covering endpoint values, midpoint equal-power, sum-of-squares
invariant, and clamping.
Audio path
- Track gets `ended: bool`, `overlap_incoming/outgoing: Option<OverlapContext>`.
On EOF the current track marks itself ended but doesn't advance the queue
— that's the promotion step's job.
- Two ways an overlap starts:
1. End-approach: worker sees decoder-position + duration <= xf duration,
peeks the next queue item, loads it as `next` with fade_in, marks
current with fade_out.
2. User Play/enqueue-jump: while a track's playing and crossfade is on,
the incoming track loads as `next` instead of hard-cutting.
- Promotion fires when either the fade-in completes OR the current track's
fade-out reaches full length OR current ended prematurely.
- Manual Next snap-promotes any pending crossfade for instant response.
- SetReplayGain propagates to both tracks so a mid-overlap RG change hits
both sides.
Renderer + config
- PlaybackState.crossfade mirrored so the TUI can render without a
separate query path.
- Renderer::set_crossfade with a no-op default (Chromecast/UPnP each
handle their own transitions).
- main.rs applies cfg.crossfade on startup before playback begins.
TUI
- `f` cycles Off → Crossfade → Mixed → Off, persisted to config.toml.
Duration edit stays in config.toml.
- Player bar shows ⋈ Ns (Crossfade) or ≈ Ns (Mixed) when active.
- Settings screen row: mode + duration + hint.
- Help text updated.
Verified live: debug log confirms two crossfades executed with
length_frames=240000 (5s at 48 kHz), promoted after exactly 5 s;
Mixed mode swaps to the ≈ glyph. mpv still never spawns for audio.
82 workspace tests pass.