SONORA LIVE

API & protocol

REST for sessions and discovery, Socket.IO for state and audio. Machine-readable: /api/v1/openapi.json. Full realtime reference: PROTOCOL.md in the server's folder.

Authentication

SONORA signs in with POST /api/v1/auth/login and gets an access token (1 hour) and a refresh token (30 days, rotated on every use). It sends Authorization: Bearer <accessToken>. The website uses its sign-in cookie plus an X-CSRF-Token header. Each session hands out short-lived scoped tokens from POST /sessions/{id}/join: listener (15 min, anyone allowed to see it) or publisher (2 h, its DJs). Ending a session voids all its tokens.

REST endpoints /api/v1

GET/healthService health
GET/timeServer time in ms (for clock offset estimates)
POST/auth/registerCreate an account (desktop)
POST/auth/loginSign in (desktop)
POST/auth/refreshExchange a refresh token (it is rotated: the old one stops working)
POST/auth/logoutRevoke a refresh token
GET/meThe signed-in account
GET/sessionsPublic broadcasts that are starting, live or reconnecting
POST/sessionsCreate a broadcast (or carry on the host's open one: `resumed: true`)
GET/sessions/{sid}A session the caller may see
PATCH/sessions/{sid}Change title, genres, visibility, allow list, privacy or quality (host)
POST/sessions/{sid}/joinA scoped token: `listener` (anyone who may see the session) or `publisher` (its DJs)
POST/sessions/{sid}/endEnd the broadcast (host). Voids every token of the session.
GET/sessions/{sid}/historyTracks that were audible, oldest first
GET/sessions/{sid}/chatThe last 100 chat messages
POST/sessions/{sid}/moderationDelete a message, mute/unmute a listener, add/remove a moderator
PUT/sessions/{sid}/assets/{asset}Upload a loaded track's waveform (publisher token)
GET/sessions/{sid}/assets/{asset}/manifestWaveform levels, chunk counts and a 1024-point overview (listener or publisher token)
GET/sessions/{sid}/assets/{asset}/chunks/{level}/{index}4096 points of one level
GET/sessions/{sid}/fallbackMP3 (or Ogg Opus) stream for browsers without WebCodecs; estimated timing only (listener token as `t`)
GET/users/{handle}A DJ's public profile
POST/users/{handle}/followFollow or unfollow
POST/nettestUpload test: post up to 4 MB, time it on the client
GET/b2bBack-to-back requests and sessions of the caller
POST/b2bAsk another DJ to go back to back
POST/b2b/{code}/{action}accept | decline | end
PUT/b2b/{code}/tracks/{key}Pass a loaded track to the other DJ (binary body, ≤ 120 MB, deleted when the B2B ends)
GET/b2b/{code}/tracks/{key}Fetch the other DJ's track

Errors are {"error": {"code", "message"}} with a matching status. Lists use cursor and limit; nextCursor is null on the last page.

Realtime (Socket.IO on /socket.io/)

Listening

sub.join {token}            → {session, epoch, snapshot, deltas[], telemetry[], seq, chat[], participants, me}
sub.audio {sessionId, on}   start/stop receiving audio packets
sub.resync {sessionId}      → a fresh {epoch, snapshot, deltas, telemetry, seq}
chat.send {sessionId, text} → {ok, id} | {error}

events: state.snapshot · state.delta · state.telemetry · audio (binary) · stream.epoch ·
        session.status · session.ended · participants · chat · chat.delete · chat.moderation · history

Publishing (SONORA)

pub.hello {token, protocolVersion: 1, publisherEpoch, engineSampleRate, audio: {codec, channels, bitrate, frameMs}}
                              → {ok, streamEpoch}
pub.snapshot {streamEpoch, seq, revision, t, state}
pub.delta {streamEpoch, seq, baseRevision, revision, t, changes: [[key, value], …]}   → {ok} | {resync: true}
pub.telemetry {streamEpoch, t, d: [[pos, rate, playing] | null, …], m: {ch[], pk[L,R], rms[L,R], clip}}
pub.audio  binary: header + 1–16 Opus packets, each stamped with its engine frame
pub.played {streamEpoch, deck, track: {title, artist, bpm, key}}
pub.stop {end}

t is SONORA's audio clock in engine frames (a safe integer; divide by engineSampleRate for seconds). Track positions (pos, cue points, loops) are seconds of the source file, a different timeline: pitch, scratching and loops make them run at other speeds. A delta whose seq isn't the next one, or whose baseRevision doesn't match, is refused with resync and SONORA sends a new snapshot; listeners that see a gap ask for sub.resync.

State keys

layout.decks                         2 | 4
deckN.track                          {asset, gen, title, artist, duration, bpm, key, artwork} | {masked, gen} | null
deckN.status / .load                 empty | loading | ready | error · 0–1
deckN.playing .looping .quantize .slip .sync .keyLock .reverse .vinyl .master   booleans
deckN.loop                           {start, end} seconds of the track | null
deckN.loopBeats .semitones .tempo (0–1 fader) .tempoRange (fraction) .bpm (track tempo here)
deckN.cue .hotcues[8] {at, color, name, loop}       only when the DJ shares cues
deckN.grid {bpm, offset, markers}                   only when the DJ shares waveforms
chN.trim .eqHigh .eqMid .eqLow .filter .fader       0–1 positions (units below)
chN.killHigh .killMid .killLow .quickOn             booleans · chN.quickType · chN.xf A|thru|B · chN.cue
mixer.crossfader .master (0–1) · mixer.curve · mixer.limiter
fxU.assign[4] .master .mode (drywet|dryplus) .mix .super · fxU.slotS {type, on, level, params[]}

Units, as SONORA's engine applies them: trim ±12 dB linear in dB around the centre; EQ: centre unity, right +12 dB, left falls to a full kill; filter: centre is off, left low-pass, right high-pass; tempo: 1 + (tempo − 0.5) × 2 × tempoRange; master level gain v^1.6.