Smart Crossfade
The beat-matched, phrase-aligned transition engine: confidence tiers, overlap selection, the discarded-music budget, and offline overlap rendering.
Smart Crossfade is Orchard’s transition engine. Where an ordinary crossfade fades one track down while the next fades up on a fixed timer, Smart Crossfade analyzes both tracks ahead of time, picks where the mix belongs, aligns the two grids to a shared downbeat, and degrades to something simpler whenever the evidence for a more ambitious mix is missing.
The design is inspired by Apple Music’s AutoMix.
Turning it on
Settings → Playback → Crossfade mode → Smart. Crossfade itself must be enabled, which it is by default. The Crossfade length slider still applies to Standard mode and acts as a rail on Smart mode rather than as its primary control.
The two stages
Every shipped auto-mix system converges on the same split, and Orchard follows it:
- Analysis runs ahead of playback and writes down what it measured, along with how confident it is.
- The runtime reads the stored analysis and decides how ambitious a transition that evidence can support. Nothing at runtime touches raw audio to make policy decisions.
The practical effect is that a bad analysis cannot quietly produce a confident-sounding mix. Confidence is carried through the whole pipeline and checked at every gate.
What analysis measures
Analysis is driven by src/audio/crossfade/smartCrossfadeAnalysis.js and executes in the
native analyzer where available, with a JavaScript fallback. For each track it produces:
- Tempo (BPM) and a beat grid with downbeat positions.
- Beat confidence, the number that governs everything downstream.
- Musical key and mode.
- An energy curve sampled across the track, used to locate intros, outros, and silence.
- A vocal activity mask, aligned to the energy curve samples, marking where singing is.
Work is prioritized so that effort concentrates near an upcoming transition. Three
priority levels exist: current, next, and background, with a preparation concurrency
of four. The track playing and the track after it get analyzed first; everything else fills
in behind them.
Catalog tempo lookups from the BPM worker can merge into an analysis, but they arrive with a beat confidence of zero. A metadata BPM alone can therefore never authorize beat-matching, only inform the softer tiers.
The three tiers
src/audio/crossfade/transitionPolicy.js grades every transition into one of three tiers
and states its reasons. Ambition falls in explicit steps as certainty falls.
| Tier | What happens | Requires |
|---|---|---|
beatmatched | Full beat-matched, phrase-aligned overlap with time-stretching and a bass handover | Beat confidence at or above 0.55 on both tracks, both tempi inside 40 to 220 BPM, and a stretch ratio within 4% of 1.0 |
dj_assisted | Beat-quantized anchors and an EQ handoff, with no time-stretching | At least one track at or above 0.2 beat confidence, both tempi in range |
plain_crossfade | Ordinary equal-power fade | Everything else |
The stretch limit mirrors kMaxTransparentRatioDeviation in
native/transition/wsola.h. Beyond about four percent, time-stretching stops being
transparent and starts being audible, so Orchard refuses to do it rather than shipping a
warbling mix.
Tempo comparison happens on the octave, the way a DJ counts a 63 BPM track against a
126 BPM one. alignTempoOctave halves or doubles the incoming tempo until it sits closest
to the outgoing one.
Where the mix goes
Choosing the overlap window is a separate problem from choosing the tier.
Overlaps are measured in beats, because beats are what the ear hears. The ceiling is 16 beats, four bars. Seconds-based rails exist for tempi where four bars would be absurd: a minimum of 4 seconds, 6 seconds for fast tracks, a maximum of 12 seconds, and an 8-second fallback when no beat-derived answer is available.
Two further constraints shape the window:
The discarded-music budget. A transition may leave a short tail of the outgoing track unplayed, up to 12 seconds of audible music. This is the difference between mixing an outro and skipping one. The analyzer will mark an outro up to 48 seconds before content ends, and the silence-cliff detector will call any large gap past the halfway mark a mix-out point. Anchoring to either without a budget would throw away a minute of music the listener can plainly hear is still coming. Whether a stretch counts against the budget is judged against the track’s own loud-end reference, so genuine silence costs nothing.
Vocal clash. If both the outgoing and incoming windows measurably contain singing at once, above a mask threshold of 0.6, the planner avoids overlapping them. Absence of a mask never blocks anything, since the JavaScript fallback analyzer emits a flat 0.5 mask and punishing every track it touched would be wrong.
Same-album consecutive tracks are detected and treated differently, so albums meant to play gapless are not mixed into each other.
Rendering a beat-matched transition
Beat-matched transitions do not do DSP during playback. The overlap is rendered offline by the native transition renderer into a single finished stereo buffer: time-stretch, beat alignment, filter sweep, and bass handover all baked in. Playback then becomes a scheduling problem.
The sequence in src/audio/crossfade/wsolaCrossfade.js:
- Preparation begins up to 30 seconds before the transition. Decoding two tracks and rendering the overlap takes a few seconds, and the lead time leaves slack for slow networks without holding whole tracks of PCM in memory.
- Scheduling wants about 1.2 seconds of runway. The playback clock ticks every 120 ms, so a lead over roughly half a second guarantees at least one tick lands in time.
- During the overlap the rendered buffer plays on the sample-accurate audio context clock while both media elements run muted underneath. Cancelling at any point restores ordinary playback without re-buffering.
- Handoff passes audible playback to the incoming element the instant the buffer ends.
Two details matter for sound quality:
- Handoff fades are 10 ms. Both sides carry the same audio at the handoff, but a media element’s position is only accurate to tens of milliseconds. Any window where both are audible is two near-copies of one signal offset in time, which comb-filters into a metallic rasp. Ten milliseconds is long enough to avoid a click and short enough that what remains is a single transient.
- Drift tolerance is 40 ms. The muted incoming element is corrected when it drifts past that, and corrections are kept well clear of the handoff, since each one is a seek the element needs time to settle after.
If the scheduler ends up more than 350 ms late relative to the planned downbeat, starting would audibly clip the front of the overlap, so the transition falls back to the ordinary crossfade.
Reading what happened
Orchard surfaces the planned transition in the player, including the tier it chose. When a
mix sounds plainer than expected, the reason is almost always one of the tier reasons:
outgoing-tempo, incoming-tempo, tempo-distance, or beat-confidence.
Relationship to Best Mix
Smart Crossfade decides how to join two given tracks. Best Mix decides which tracks to put next to each other. They share the same analysis data and the same tempo and key math, and they work well together, though either works alone.
Mobile
Orchard Mobile implements the same architecture natively on Android, with beat and downbeat tracking from a quantized Beat This! model, vocal presence from open-unmix, and tempo, key, and energy from the native C++ analyzer. See Orchard Mobile.
Source map
| Path | Role |
|---|---|
src/audio/crossfade/smartCrossfadeAnalysis.js | Analysis orchestration and priorities |
src/audio/crossfade/transitionPolicy.js | Confidence tiers, budgets, vocal clash |
src/audio/crossfade/transitionPlanner.js | Overlap window selection |
src/audio/crossfade/wsolaPlanner.js | Beat-matched plan construction |
src/audio/crossfade/wsolaCrossfade.js | Runtime scheduling of the rendered overlap |
src/audio/crossfade/crossfadeMixer.js | Standard equal-power mixing |
src/audio/crossfade/autoCrossfade.js | Mode defaults and entry point |
native/transition/ | Offline overlap renderer and time-stretch |