Skip to main content

Set studio: discovery, editor, listening (2026-09-20, extended 2026-09-21)

A live set is now kept as an editable object, not only as a journal. Three modes share one page (/sets, Perform → Set studio):

  • Discovery is the Live club page as before: the autopilot picks tracks and moves with the model. Every session is autosaved as a recording — its steps (track, entry cue) and seams (the transition that left each track), appended as each handover completes. A recording is read-only; it is what the booth actually played.
  • Editor works on a mix: a copy of a recording (or a set built from library tracks). Reorder, remove and add tracks; for each seam ask for transitions — the local ranker lists every move it would offer the booth for that pair, and one button asks the DJ adviser to pick one from that list and explain it. Nothing else calls the model.
  • Listen renders a set offline and plays the WAV back. No planner, no model, no new evidence: the render is the stored steps and seams, so it sounds the same every time.

What a set stores

LiveSet { id, name, kind: recording|mix, session_id, derived_from, status,
pool, mix_bpm, energy, tempo_cap, track_seconds, outro_seconds,
steps: [ { track_id, title, cue_seconds, transition? } ], render? }
transition { option_id, technique, recipe, target_bpm, incoming/outgoing_ratio,
outgoing_exit_seconds, incoming_cue_seconds, mix, script,
rationale, attempt_id, source: live|editor|model, reason? }

Positions are on each recording's own timeline (where it was entered, where it was left), so the set does not depend on the tempo the booth happened to be at. Sets live as JSON under <data>/sets/ and are mirrored to the live_sets collection when MongoDB is configured. The live booth writes them best-effort off the control path; a storage problem is logged and never stops a set.

Autosave points

session_started creates rec-<session>; the first track adds step 1 (cue 0); each completed overlap adds the seam from the executed LearnedPlan (technique, recipe, operators, structural exit and entry, the live attempt id so the rating stays linked) and the next step at its planned cue; a beat-preserving cut records the cut recipe, the gesture (spinback, brake, impact) and the outgoing position at the cut; a manually loaded track adds a step with an open seam; stop marks the recording stopped. The Discovery list refreshes every five seconds while a set is live.

Editing rules

  • Reordering rebuilds the step list from the ordered track ids. A seam survives only when its pair (this track → the same next track) survived; every other seam is cleared and shows No transition yet. New tracks enter at their first beat.
  • Find transitions runs learning::load for the pair at the mix tempo with the set's own technique history, so the variety nudge sees edited seams as it sees played ones. The response is the shortlist the booth would be offered (base moves plus up to four variants each) with sections, energy delta, harmonic fit, vocal clash, evidence counts and rationale.
  • Ask the adviser sends the same prompt the live booth uses with exactly one candidate (the next track) and that option list, and returns the pick with its reason. The pick is revalidated against the list before it is saved; the model never proposes tracks, parameters or timing.
  • Auto-fill puts the ranker's first choice on every open seam, and an 8-beat cut on the downbeat where nothing beatmatches — the same fallback the booth uses.
  • Set by hand (seam_edit) puts a move on a seam without the ranker: one of the five structural recipes (blend, bassswap, filterfade, dropswap, cut) or a scripted gesture on the outgoing deck (echo_out, loop_out, sweep_out, brake, spinback), an overlap of 8–128 beats, and where the outgoing is left and the incoming starts, on their own timelines. With only positions, an existing move (including a ranked one) keeps its technique and script and just moves. Exits must fall after the track's cue and before its end; cues inside the track.
  • Cue (step_cue) moves where a step starts; the seam into it follows.
  • Reordering is drag-and-drop or arrows; a library track can be inserted after any row.
  • Paste (paste) copies a run of another set's steps — their seams included — after a row or at the end. The seam into the block and the block's last seam are new pairs and start open; a track the target already plays cannot be pasted again. The editor keeps one clipboard (a run of ticked rows) in the browser, so a block can be carried from a recording or a mix into any other mix.
  • A recording is never edited: the first edit in the editor copies it into a mix named “recording name · edit” and continues there.
  • Any edit clears the set's render record; a render always reflects the set as it was when it started.

Listening

The player draws the rendered mix as a waveform (peaks per column from render/peaks, computed once by the engine and cached beside mix.wav, so an eight-hour mix does not have to be decoded in the browser). Each track's span and every overlap are marked; clicking or dragging seeks, and any seam or 30-second span can be looped. The same player sits in the editor above the step list when the current version has a completed render, with an Audition button per seam that loops the overlap; an edit clears the render, so the player asks for a new render after changes.

Seam previews

POST /api/v1/live-sets/{id}/preview {index} renders one seam on its own: a lead-in of the outgoing (16 s by default, never shorter than the opening cut), the seam's own move, and a tail of the incoming (12 s). Pass option_id to hear a ranked move before it is saved; the set's chosen transition is left unchanged. It is a two-step schedule through the same planner, tempo and script as the full mix, so the clip is what the render will do, and it takes the lab's single job slot for a few seconds. GET …/preview/{session} reports progress and stale (the saved seam changed since); GET …/preview/{session}/mix.wav streams the clip. Clips of different moves on the same seam are kept so they can be compared; any edit clears them. The editor plays the clip in the same player, with peaks decoded in the browser (the clip is seconds long).

Continuing a recording

Any saved set — a stopped recording or a mix — can be continued on the Live club page (the Continue live picker on Discovery and the button in the editor open /clubs?continue=<set>): the autopilot starts a new session whose autosave appends to that set (Start.continue_set), the set's tracks count as already played, the first new track is chosen to follow the set's last one, and the set's pool is used unless a pool or pack is loaded on the page. The seam between the last track and the first new one is open — the booth did not play it — and is filled in the editor like any other. While the booth plays into a set its status is recording and the set cannot be edited or deleted; on stop a recording returns to stopped and a mix to saved.

Recording status

A recording says recording only while its session is the booth's live one. Sessions that end without a clean stop (an API restart, an interruption) used to keep the label forever, which blocked deleting them; the list, detail and edit routes now reconcile the status against the live session before answering, and a stale recording reads stopped and can be deleted.

Offline render

The mix is beat-locked at one mix tempo. Each track plays at the mix tempo, or at half or double time, whichever needs the smallest stretch within the tempo cap; a track that fits none is reported by name and the tempo (or the track) has to change. The live booth rides the outgoing's tempo into the next track; the offline engine pre-stretches each track to one tempo, so a ride cannot be reproduced and the recorded target_bpm per seam is kept only for the record.

Each step becomes a TransitionPlan: cue snapped to the declared beat grid, start frame from the previous step's exit on its own timeline divided by its playback ratio, overlap and bass-swap frames from the recipe at the incoming's tempo, the recorded operator script (loop-out, echo-out, sweeps, deck motion, one-shots, key sync) verbatim. An exit that leaves no room for the overlap is pulled back to the last beat that does, and a track is never left before its own incoming overlap has finished (the compiler's lease rule). The last track plays outro_seconds after its cue, or out. The manifest is diagnostic (grid approval is a live-execution gate, not a listening one) and carries source_set, which — like replay_of — stops the render from recording transition attempts: a listening copy adds no evidence.

The render runs through the lab's single job slot (Work::Planned) and exposes the usual session artifacts; the studio streams mix.wav through the workspace gateway.

Levels. A set render brings every track to −14 LUFS (EBU R128 integrated, from the library analysis) under a −1 dBTP true-peak ceiling, boosting a quiet track by at most 12 dB within its headroom — the same target the live booth's channel trims use, which can only attenuate. Tracks without a measurement play at unity. The gain applied to each track is recorded in the session's metrics.json (track_gains_db). Auditions and replays are not levelled, so their evidence stays comparable with earlier renders.

Since 2026-09-21 the engine prepares audio in a pipeline instead of all up front: a producer thread decodes and pre-stretches tracks a few at a time on the cores the process has (up to six), in the order the set needs them, and the mix loop pulls each track when its plan starts and drops it once it has stopped. A track is only stretched as far as it can be heard (until the next plan's overlap ends, plus a margin for loops and deck motion). Decoding and stretching were the whole cost — the mix loop itself runs at roughly 90× real time — so a set renders several times faster and memory stays at a handful of tracks however long the set is; before, every track of the set was held pre-stretched in memory for the whole render, which an eight-hour set could not fit.

API

RouteContract
GET/POST /api/v1/live-setsList summaries; create a mix {name, from} or {name, track_ids}
GET /api/v1/live-sets/{id}The set, its offline schedule (plan.timeline) or the problems that block it
POST /api/v1/live-sets/{id}rename, reorder, settings, seam_options (ask_model), seam_apply, seam_clear, seam_edit, step_cue, paste, auto_fill, delete
GET/POST /api/v1/live-sets/{id}/renderRender progress; start a render
GET /api/v1/live-sets/{id}/render/mix.wavThe rendered mix once complete
GET /api/v1/live-sets/{id}/render/peaks{columns, duration_seconds, peaks[0–100]} of the rendered mix (1500 columns, cached)
POST /api/v1/live-sets/{id}/previewRender one seam as a short clip: {index, option_id?, lead_seconds?, tail_seconds?}. option_id auditions a ranked move without saving it
GET /api/v1/live-sets/{id}/preview/{session}The preview's progress, record and stale flag
GET /api/v1/live-sets/{id}/preview/{session}/mix.wavThe clip once complete

The UI reaches these through /api/live-clubs/{club}/sets…; reads need booth access, writes need the owner. Recordings accept only rename and delete (and not delete while live); everything else needs a mix.

Not done

  • Hot cues and loops inside a track are not part of a set; only each step's start and each seam's exit and entry are editable.
  • No fade-out at the end of the render; the last track stops at its outro ceiling.
  • A recording made in guidance mode (human loading tracks) has open seams for the manual handovers; Auto-fill or Find transitions completes them in a mix.