/library/decisions/dj-agent
ADR 0007 — Phrase-derived crossfade length
The Mixer (dj/mixer.py) crossfades out of one track's section into the next. The open question (docs/phases.md) was whether the crossfade length should be a fixed number of bars (e.g. always 8) or …
mirrored from ~/dev/dj-agent/docs/adr/0007-phrase-derived-crossfades.md · commit 52419d2 · synced 2026.07.09Accepted
Status: Accepted
Date: 2026-06-04
Relates to: 0004-structure-aware-sections.md, 0005-calibrated-beats-and-energy.md
Context
The Mixer (dj/mixer.py) crossfades out of one track's section into the next.
The open question (docs/phases.md) was whether the crossfade length should be a
fixed number of bars (e.g. always 8) or computed from the section's phrase
grid.
A fixed length is wrong at both ends: an 8-bar crossfade swallows a 4-bar outro
(you're fading into the next track before the current section even establishes),
and it underuses a 32-bar breakdown (a long, luxurious blend was available and
we cut it short). Because Phase 1 already detects sections with downbeat-anchored
bounds (ADR 0004/0005), the bar count of each section is known for free.
Decision
Derive the crossfade length from the outgoing section's bar grid. The overlap
is half the outgoing section's bars, clamped to [1, 8] bars (crossfade_bars),
then converted to seconds at the transition's target tempo (crossfade_seconds):
bars = section_bars(outgoing_len_s, bpm) # length in bars (4/4)
xfade = clamp(bars // 2, 1, 8) # half the section, capped
seconds = xfade * 4 * 60 / target_bpm # bars → seconds at tempo
The cap (8 bars) keeps even a long breakdown from dragging; the floor (1 bar) keeps a tiny section from producing a click-cut. When the section length is unknown (no cue points), it falls back to a safe 4-bar default.
Rationale
- A blend should live inside one phrase. Halving the section keeps the crossfade proportional to the musical unit it sits in, so the mix lands on the phrase regardless of whether the part is 4 or 32 bars.
- The data is already there. Section bounds are downbeat-anchored at ingest, so "how many bars is this part" is a column lookup, not new analysis.
- Tempo-correct by construction. Converting bars → seconds at the target tempo (the tempo B is beatmatched to) means the overlap is exactly N bars of the playing track, not N bars of B's original tempo.
Trade-offs accepted
- Half-the-section is a heuristic, not a musical truth. Some transitions want a
quick cut, others a long roll; "half, capped at 8" is a sane default, not the
optimum. A future per-transition policy (driven by section labels — quick out of
a
drop, long out of abreak) can refine it. The constantMAX_XFADE_BARScentralizes the cap for tuning. - Ignores B's section length. We size the crossfade from A's outgoing section
only. If B's mix-in section is shorter than the overlap, the render clamps to the
available audio (
min(cross, len(mix), len(seg))), so it's safe but can be shorter than planned.
Consequences
dj/mixer.py:crossfade_bars/crossfade_seconds/section_barsare pure and unit-tested (test_mixer.py);plan_transitionsuses them per adjacent pair, sizing each crossfade from the outgoing slot's cue length and the arc's target tempo.- The fixed-vs-phrase open question in
docs/phases.mdis resolved (phrase-derived).