RFC 0002: composable transition operators and the event timeline
Problem
A professional transition is not a tempo match and a bass swap. It is a
combination of operators applied on a beat grid — for example a long blend with
a low kill on the incoming, a midpoint bass swap, an outgoing high-pass sweep and
a one-beat echo out. Today the platform cannot represent that. TransitionRecipe
is a fixed struct — length, one fader curve, one bass-swap window, one optional
high-pass sweep — and the executor implements roughly four operators (fader,
three-band EQ with a low-EQ bass swap, and a high-pass filter). The learning loop
clusters that struct's six shape features, so it can only ever rank fader curves
and swap positions, never discover which combinations real DJs use.
The technique vocabulary already exists: crates/evaluation/resources/techniques-v1.json
(schema v2) lists 225 concepts across 15 dimensions — base, eq, filter, structure,
loop, tempo, harmony, vocal, stem, layering, sample, deck-motion, effect,
energy-intent, discovery. But 222 of them are live_autoplay_support: not_enabled
and only 3 are executable. The catalog names the goal; nothing executes or learns it.
Scope
This RFC defines the target model and a phased path. It does not itself add DSP. The engine gaining echo, reverb, loops, stems, deck-motion or sample playback is downstream work, gated per operator on real DSP plus validation, exactly as the current recipe language is bounded today.
Proposal
1. A transition is an event timeline, not a named shape
Represent a transition as an ordered list of beat-aligned operator events over a bar span, not a classification string. Discovery then clusters timelines into named styles rather than starting from hardcoded names.
{
"duration_bars": 32,
"events": [
{"bar": 1, "type": "deck_start", "deck": "b"},
{"bar": 1, "type": "low_kill", "deck": "b"},
{"bar": 9, "type": "fader_rise", "deck": "b", "beats": 32, "curve": "equal_power"},
{"bar": 17, "type": "bass_swap", "from": "a", "to": "b", "beats": 8},
{"bar": 25, "type": "hpf_sweep", "deck": "a", "from_hz": 20, "to_hz": 12000, "beats": 16},
{"bar": 31, "type": "echo", "deck": "a", "beat_length": 1.0, "feedback": 0.55},
{"bar": 32, "type": "deck_cut", "deck": "a"}
]
}
The existing TransitionRecipe becomes one compiler target: it lowers to this
timeline (fader ramp + low-band swap + optional HPF), so nothing already recorded
is lost and the current executor keeps working while the timeline is introduced.
2. Operators carry trajectories and parameters, never a boolean
An effect name alone is not executable evidence. Each operator is parameterized.
Filters carry a trajectory: HPF 20 Hz → 12 kHz over 16 beats is different from
LPF 200 Hz → 20 kHz over 32 beats, and the name does not say which.
Echo is not echo = true:
Echo { beat_length, wet, feedback, start_beat, end_beat, frequency_band }
so discovery can learn that a one-beat echo with rising feedback and a fader cut on the next downbeat is the pattern called Echo Out.
Loops are not one binary effect:
Loop { start_beat, length_beats, repetitions, source, slip, reverse, trajectory }
so an eight-beat outro loop, an 8→4→2→1 shortening, a half-beat roll, a vocal loop and a reverse roll are distinct rather than one flag.
3. Stems are operators too
vocal_remove / vocal_isolate, and the same for drums, bass and melody. This
lets the system represent transitions older fader/EQ datasets cannot: remove A's
bass under B's drums, then A's drums under B's bass while A's vocal rides over B,
then hand the floor to B. Requires source separation, which does not exist yet.
4. Three roles, kept separate
- Kimi chooses the musical intention and the combination of operators.
- The learned model proposes appropriate trajectories for that combination.
- Rust executes exact beat-aligned automation. Models never touch samples.
This is the existing split (Kimi selects, the executor runs a validated script) extended from one recipe shape to a composed operator set.
Current state vs target
| Now | Target | |
|---|---|---|
| Representation | fixed TransitionRecipe struct | ordered operator event timeline |
| Executable operators | fader, 3-band EQ, low-EQ bass swap, HPF sweep (~4) | the V1 core set below, then more |
| Vocabulary | techniques-v1.json v2, 225 concepts, 3 live | same catalog, operators promoted as DSP lands |
| Learning input | 6 recipe-shape features | operator presence, parameters and trajectories |
| Discovery | ranks recipe shape | clusters timelines into named styles |
V1 core operators
Roughly 25, chosen so combinations yield hundreds of styles without hundreds of hardcoded transitions. Each is promoted to live only with real DSP and validation.
fader, crossfade, cut; low_eq, mid_eq, high_eq, eq_kill;
hpf, lpf; delay, echo, ping_pong, reverb; roll, loop,
loop_halve, loop_double; vinyl_brake, backspin; flanger, phaser;
noise, crush; sample_trigger; vocal_stem, drum_stem, bass_stem,
melody_stem.
The first four (fader, crossfade, cut, low/high EQ and HPF) already exist and would be the first lowered onto the timeline with no new DSP.
Bridging large tempo gaps
Half and double-time mixing (implemented in playable_ratio) only bridges gaps
near a 2× ratio. A genuinely distant pair — say 95 into a 124 set — cannot be
beatmatched by stretch, and a slow crossfade there stops the beat. Real DJs bridge
such gaps with masking or ramping gestures rather than a tempo match, and these
are first-class operators in this model, not special cases:
- Tempo ramp. Over a breakdown or outro, slide the outgoing deck's tempo from its own toward the incoming's, so the incoming enters at its native tempo with no discontinuity. A continuous bridge; needs the executor to ramp deck tempo over a beat span.
- Echo / reverb tail out, then drop. Echo the outgoing into a decaying tail and drop the incoming on its own downbeat; the tail masks the tempo change.
- Deck-motion out. A spinback, backspin, tape-stop or vinyl brake ends the outgoing as a gesture, and the incoming drops at its own tempo. The brake is the transition; no beatmatch is required. This is how a set crosses genres — house into drum & bass, or into a half-time cut.
- Loop-and-ramp. Loop the outgoing's last phrase to buy time, ramp its tempo to the incoming's, then hand over.
- Drop mix on the phrase. A clean cut on a shared phrase boundary, used where the outgoing thins to a beatless section.
None of these are implemented; the audio engine has no echo, deck-motion or tempo ramp yet. Until they exist, a distant-tempo pair the stretch range cannot bridge falls back to the sequential fade — the least-bad available option, and explicitly not a professional transition. Building these is what makes "any two tracks, without stopping the beat" literally true; they belong to the same operator timeline and are sequenced after Phase 1.
Wire and state changes
- New
TransitionScript(operator timeline) alongsideTransitionRecipe; recipes lower to scripts. Immutable and versioned like recipes. technique_evidencealready stores per-frame control trajectories; extend its reader to derive operator events from them for clustering.- Learning features gain operator presence and trajectory summaries; a schema
bump (
dj-learning-3) freezing a new dataset, as before. - The virtual-booth compiler gains one lowering per operator; unknown operators are rejected, never silently dropped.
Failure behavior
An operator with no engine support is a catalog entry that cannot be scheduled; compilation rejects a script that names it, as recipe validation does today. A script that references a missing stem or an out-of-range trajectory is refused whole, never partially applied.
Migration
Additive. Existing recipes, attempts, evidence and the live sequential and structural paths are unchanged; the timeline is introduced as a second target and the executor keeps running recipes until every recipe path is lowered onto it.
Test plan
- A recipe lowered to a timeline compiles to byte-identical control commands.
- Each new operator has a lowering test and a rejection test for out-of-range
parameters, mirroring
TransitionRecipe::validate. - Discovery clusters a fixture of hand-authored timelines into their known styles.
Open questions
- Source separation for stems: offline pre-render, or a live separator?
- Which effects are honest on the current emulator (no key lock, no send FX bus) versus only on hardware, and how the catalog marks that per device profile.
- Whether Kimi selects operators freely or from a per-context feasible set, as it selects recipes today.
Disposition
Proposed. Phase 1 — the timeline representation with the existing ~4 operators lowered onto it, no new DSP — is the foundation everything else needs and is the recommended first build.