Summary
Multiplayer model A (independent b2b decks) just shipped: each player drives their own session/song locally, and the co-DJ stream now reflects the armed/launched pattern (emitLivePlayback) rather than the edit rack, so a collaborator hears your launches on your deck lane and the crossfader blends them.
Model B is the next step: a shared session where launches and song position are synchronized across players — when the host (or a master lane) launches a pattern/scene, everyone follows one playhead. This is necessary for the session system to work "properly" in a true collaborative jam (not just two independent decks).
Why A is not enough
The live-performance layer is local runtime state (rt.*), never streamed as control state:
| State |
Where |
Synced today |
Song position (rt.songScene, songSceneEndTick) |
runtime |
❌ |
Launched pattern (rt.sessionArmedClipIds) |
runtime |
❌ (only the resulting audio/pattern streams via A) |
Per-track overrides (rt.trackOverrides) |
runtime |
❌ |
| Patterns / clips / session grid / scenes |
project → TPL |
✅ (master-gated, Merge.tish) |
In A we stream the realized pattern (the notes), so the other side hears the result but doesn't share the intent — they can't follow your song, and there's no single shared arrangement/playhead. The song arrangement is also still implicit (saved scenes in grid order); there's no explicit, editable, portable arrangement.
Proposed architecture for B
-
Explicit song arrangement block in TPL — make the arrangement data, not derived:
Emit + parse + round-trip + tests. The song-chain in playbackTick reads this instead of deriving grid order. (Foundation: already discussed as a standalone improvement.)
-
Launch / transport control messages on the hub — a new message type alongside tpl.block:
launch.scene { sceneIdx, effectivePerfStep }
launch.clip { trackId, clipId, effectivePerfStep } (per-track override)
transport { play: "song"|"sequence"|"stop", songScene, effectivePerfStep }
Gated by master rights (laneCanMaster in Merge.tish); per-track overrides scoped to the track owner.
-
Shared transport clock / playhead — a master broadcasts song position (scene + bar) on a quantized boundary so followers align; or an Ableton-Link-style beat clock. Receivers apply launches to the shared arming (sessionArmedClipIds / songScene) at the target perfStep, rather than only merging track bodies.
-
Authority & conflict model — who may launch (master vs per-lane ownership), how a follower's local launches reconcile with the master's, and how per-track overrides interact with ownership (actorMayEditTrack).
-
UI — indicate "following host" vs "free" per player; show the shared playhead; a follow/independent toggle so a player can break off into their own deck (back to model A behaviour) and rejoin.
Tasks
Acceptance criteria
- Host launches P3 → all following players switch to P3 on the same bar.
- A player can take a per-track clip (their owned track) without desyncing the rest.
- A player can toggle "independent" (model A) and rejoin "follow" cleanly.
- Song arrangement is explicit in TPL and round-trips.
References
Summary
Multiplayer model A (independent b2b decks) just shipped: each player drives their own session/song locally, and the co-DJ stream now reflects the armed/launched pattern (
emitLivePlayback) rather than the edit rack, so a collaborator hears your launches on your deck lane and the crossfader blends them.Model B is the next step: a shared session where launches and song position are synchronized across players — when the host (or a master lane) launches a pattern/scene, everyone follows one playhead. This is necessary for the session system to work "properly" in a true collaborative jam (not just two independent decks).
Why A is not enough
The live-performance layer is local runtime state (
rt.*), never streamed as control state:rt.songScene,songSceneEndTick)rt.sessionArmedClipIds)rt.trackOverrides)project→ TPLMerge.tish)In A we stream the realized pattern (the notes), so the other side hears the result but doesn't share the intent — they can't follow your song, and there's no single shared arrangement/playhead. The song arrangement is also still implicit (saved scenes in grid order); there's no explicit, editable, portable arrangement.
Proposed architecture for B
Explicit
songarrangement block in TPL — make the arrangement data, not derived:Emit + parse + round-trip + tests. The song-chain in
playbackTickreads this instead of deriving grid order. (Foundation: already discussed as a standalone improvement.)Launch / transport control messages on the hub — a new message type alongside
tpl.block:launch.scene { sceneIdx, effectivePerfStep }launch.clip { trackId, clipId, effectivePerfStep }(per-track override)transport { play: "song"|"sequence"|"stop", songScene, effectivePerfStep }Gated by master rights (
laneCanMasterinMerge.tish); per-track overrides scoped to the track owner.Shared transport clock / playhead — a master broadcasts song position (scene + bar) on a quantized boundary so followers align; or an Ableton-Link-style beat clock. Receivers apply launches to the shared arming (
sessionArmedClipIds/songScene) at the targetperfStep, rather than only merging track bodies.Authority & conflict model — who may launch (master vs per-lane ownership), how a follower's local launches reconcile with the master's, and how per-track overrides interact with ownership (
actorMayEditTrack).UI — indicate "following host" vs "free" per player; show the shared playhead; a follow/independent toggle so a player can break off into their own deck (back to model A behaviour) and rejoin.
Tasks
songarrangement block: TPL emit/parse, playback reads it, round-trip testssessionArmedClipIds/songSceneateffectivePerfStepAcceptance criteria
References
emitLivePlayback(src/tpl/Emit.tish), broadcast wiring in src/ui/App.tish (coDjPushLive), tests intest/smoke.tish(mp A:checks).firstSongScene,nextSongScene,armSceneWithOverrides,trackOverrides).