asteroid/docs/DJ-CONSOLE.org

27 KiB
Raw Blame History

DJ Console — Design Document

Overview

The DJ Console is a new feature for Asteroid Radio that allows DJs to mix tracks live from the station's music library, or connect external audio equipment (turntables, CDJs, mixers, etc.) to broadcast live sets.

The console is a dedicated web page at /asteroid/dj, accessible to users with the :dj or :admin role. It provides a dual-deck interface with crossfader, per-deck volume, library search, and external audio input — all feeding into the existing Harmony mixer and streaming pipeline.

Why This Works

The current CL-Streamer architecture makes this straightforward:

  • Harmony's mixer already sums all active voices. The streaming drain, encoders (LAME, FDK-AAC), and broadcast buffers don't need to change. A DJ session is just manual control over which voices are playing and at what volume.
  • play-file already creates Harmony voices. Each deck loads a track the same way the auto-playlist does — the difference is that the DJ controls when to start, stop, and crossfade rather than the play-list loop.
  • External audio input is another voice on the mixer. Whether it comes from a local sound card (ALSA/PulseAudio/JACK) or a network audio stream, once it's a Harmony source, it's mixed identically to library tracks.

Architecture

System Diagram

┌──────────────────────────────────────────────────────────────┐
│                       SBCL Process                            │
│                                                               │
│  ┌─────────────────────────────────────────────────────┐     │
│  │              DJ Session (dj-session.lisp)            │     │
│  │                                                      │     │
│  │  ┌──────────┐        ┌──────────┐                   │     │
│  │  │ DECK A   │        │ DECK B   │                   │     │
│  │  │ voice-a  │        │ voice-b  │                   │     │
│  │  │ vol: 0.8 │        │ vol: 0.6 │                   │     │
│  │  │ state:   │        │ state:   │                   │     │
│  │  │ playing  │        │ cued     │                   │     │
│  │  └────┬─────┘        └────┬─────┘                   │     │
│  │       │                   │                          │     │
│  │       │  ┌──────────────┐ │                          │     │
│  │       │  │ EXTERNAL IN  │ │                          │     │
│  │       │  │ (optional)   │ │                          │     │
│  │       │  │ voice-ext    │ │                          │     │
│  │       │  └──────┬───────┘ │                          │     │
│  │       │         │         │                          │     │
│  │  ┌────▼─────────▼─────────▼────┐                    │     │
│  │  │     CROSSFADER (0.0 — 1.0)  │                    │     │
│  │  │     Adjusts A/B balance     │                    │     │
│  │  └─────────────┬───────────────┘                    │     │
│  └────────────────│────────────────────────────────────┘     │
│                   │                                           │
│              ┌────▼────┐                                     │
│              │ Harmony  │                                     │
│              │ Mixer    │  (sums all active voices)           │
│              └────┬────┘                                     │
│                   │                                           │
│  ┌────────────────▼──────────────────────────────────┐       │
│  │            streaming-drain (unchanged)              │       │
│  │  float→s16 → LAME (MP3) + FDK-AAC (AAC)           │       │
│  │  → broadcast buffers → HTTP clients                │       │
│  └────────────────────────────────────────────────────┘       │
│                                                               │
│  ┌───────────────────────┐  ┌────────────────────────┐       │
│  │ Auto-Playlist          │  │ Radiance Web Server    │       │
│  │ (paused while DJ live) │  │ (port 8080)            │       │
│  └───────────────────────┘  └────────────────────────┘       │
└──────────────────────────────────────────────────────────────┘

Audio Flow

When a DJ session is active:

  1. The auto-playlist (play-list loop) is paused — its current position is saved for seamless resume later.
  2. The DJ loads tracks onto Deck A and/or Deck B via the library search.
  3. Each deck creates a Harmony voice. play-file is called with :on-end :disconnect so the voice cleans up when the track ends.
  4. The crossfader adjusts the (mixed:volume) of each deck's voice:

    • Position 0.0: Deck A at full volume, Deck B silent
    • Position 0.5: Both at equal volume
    • Position 1.0: Deck B at full volume, Deck A silent
  5. Per-deck volume knobs provide independent gain before the crossfader.
  6. Harmony's mixer sums all active voices (both decks + external input if connected).
  7. The streaming drain captures the mixed output — identical to auto-playlist mode. Encoders and listeners are unaware of the source change.

When the DJ ends the session:

  1. Active decks fade out over 3 seconds (volume-ramp).
  2. The auto-playlist resumes from the saved position.
  3. ICY metadata updates to show the resumed track.

External Audio Input

Use Cases

  • Turntable/CDJ setup: DJ connects external hardware to the server's sound card (line-in, USB audio interface)
  • Remote DJ: DJ streams audio over the network from their own equipment/software (e.g., Traktor, Serato, Ableton, BUTT)
  • Hybrid set: DJ mixes between library tracks on the decks and external audio input

Option 1: Local Sound Card Input (ALSA/PulseAudio/JACK)

For DJs with physical access to the server (or a connected audio interface), cl-mixed can capture from ALSA, PulseAudio, or JACK:

  • cl-mixed-pulse — PulseAudio source capture
  • cl-mixed-alsa — ALSA device capture
  • cl-mixed-jack — JACK audio connection

The captured audio becomes a Harmony source that feeds into the mixer like any other voice. This is the lowest-latency option.

┌──────────────┐    ┌───────────┐    ┌─────────────┐
│ Turntable /  │───▶│ USB Audio │───▶│ ALSA/Pulse  │
│ CDJ / Mixer  │    │ Interface │    │ Capture     │
└──────────────┘    └───────────┘    └──────┬──────┘
                                            │
                                    ┌───────▼───────┐
                                    │ Harmony Voice  │
                                    │ (ext-input)    │
                                    └───────┬───────┘
                                            │
                                    ┌───────▼───────┐
                                    │ Harmony Mixer  │
                                    │ → encoders →   │
                                    │   listeners    │
                                    └───────────────┘

Option 2: Network Audio Input (Remote DJ)

For remote DJs, the server accepts an incoming audio stream. The DJ sends audio from their software (BUTT, Traktor, etc.) over HTTP or Icecast protocol to a receive endpoint on the CL-Streamer server.

The incoming compressed audio (MP3 or Ogg) is decoded and fed into Harmony's mixer as another voice.

┌──────────────┐         ┌──────────────────┐
│ DJ Software  │ ──HTTP──▶│ CL-Streamer      │
│ (BUTT,       │  or SRT  │ /dj-input mount  │
│  Traktor,    │         │                  │
│  etc.)       │         │ decode → Harmony │
└──────────────┘         │ voice (ext-input)│
                          └────────┬─────────┘
                                   │
                          ┌────────▼─────────┐
                          │ Harmony Mixer     │
                          │ → encoders →      │
                          │   listeners       │
                          └──────────────────┘

Implementation options:

  • Icecast source protocol: DJ software connects as an Icecast source client (SOURCE /dj-input HTTP/1.0). CL-Streamer accepts the connection, decodes the incoming MP3/Ogg stream via cl-mixed, and routes it to the mixer. Most DJ software already supports this.
  • SRT (Secure Reliable Transport): Lower latency for remote DJs. Would require SRT library integration (future).
  • WebRTC: Browser-based DJ could stream from their mic/mixer via WebRTC. Complex but enables browser-only DJ workflow (future).

Option 3: Hybrid

Both options can be active simultaneously. The DJ console UI shows an "External Input" channel alongside Deck A and Deck B, with its own volume control and mute button. The crossfader operates on Deck A/B; the external input has independent volume.

Access Control

Route and Roles

  • URL: /asteroid/dj
  • Required role: :dj (which also grants access to :admin users, since admin inherits all lower roles)
  • Single session: Only one DJ session can be active at a time. The session is owned by the user who started it.
  • Admin override: Admin users can end any DJ session (e.g., if a DJ disconnects without ending cleanly)

Session Lifecycle

  1. DJ navigates to /asteroid/dj and clicks "Go Live"
  2. Server pauses the auto-playlist and creates a dj-session struct
  3. DJ loads tracks, mixes, broadcasts
  4. DJ clicks "End Session" — decks fade out, auto-playlist resumes
  5. If DJ disconnects without ending: a watchdog timer (configurable, default 60s of no API polls) auto-ends the session

Backend Design

New file: dj-session.lisp

DJ Session State

(defclass dj-session ()
  ((owner :initarg :owner :accessor session-owner
          :documentation "Username of the DJ who owns this session")
   (started-at :initarg :started-at :accessor session-started-at
               :initform (get-universal-time))
   (deck-a :initform (make-instance 'dj-deck :name :a) :accessor session-deck-a)
   (deck-b :initform (make-instance 'dj-deck :name :b) :accessor session-deck-b)
   (crossfader :initform 0.5 :accessor session-crossfader
               :documentation "0.0 = all A, 1.0 = all B")
   (external-input :initform nil :accessor session-external-input
                   :documentation "External audio voice, or NIL if not connected")
   (external-volume :initform 1.0 :accessor session-external-volume)
   (last-poll :initform (get-universal-time) :accessor session-last-poll
              :documentation "Timestamp of last UI poll, for watchdog")
   (saved-playlist-state :initform nil :accessor session-saved-playlist-state
                         :documentation "Auto-playlist state saved on session start")))

(defclass dj-deck ()
  ((name :initarg :name :accessor deck-name :documentation ":a or :b")
   (file-path :initform nil :accessor deck-file-path)
   (voice :initform nil :accessor deck-voice
          :documentation "Harmony voice, or NIL if not loaded/playing")
   (volume :initform 1.0 :accessor deck-volume
           :documentation "Per-deck volume before crossfader")
   (state :initform :empty :accessor deck-state
          :documentation ":empty, :loaded, :playing, :paused")
   (track-info :initform nil :accessor deck-track-info
               :documentation "Plist: (:artist :title :album :file :display-title)")))

(defvar *dj-session* nil
  "The currently active DJ session, or NIL if no DJ is live.")

Key Functions

Function Description
start-dj-session (user) Pause auto-playlist, create session
end-dj-session () Fade out decks, resume auto-playlist
load-deck (deck file-path) Load a track onto deck (reads metadata, creates voice)
play-deck (deck) Start playback on a loaded/paused deck
pause-deck (deck) Pause a playing deck
stop-deck (deck) Stop and unload a deck
seek-deck (deck position) Seek to position (seconds)
set-crossfader (position) Set crossfader 0.01.0, updates deck volumes
set-deck-volume (deck vol) Set per-deck volume 0.01.0
connect-external-input (src) Connect external audio source to mixer
disconnect-external-input () Disconnect external audio
set-external-volume (vol) Set external input volume
dj-session-status () Returns full state for UI polling
dj-watchdog-check () Called by cl-cron; ends stale sessions

Crossfader Volume Calculation

The crossfader applies a constant-power curve so the perceived volume stays consistent across the sweep:

(defun crossfader-volumes (position)
  "Return (values vol-a vol-b) for crossfader position 0.01.0.
   Uses constant-power (equal-power) curve."
  (let ((angle (* position (/ pi 2.0))))
    (values (cos angle)    ;; Deck A: 1.0 at 0.0, 0.0 at 1.0
            (sin angle)))) ;; Deck B: 0.0 at 0.0, 1.0 at 1.0

The effective volume for each deck is: effective-volume = deck-volume * crossfader-component

This is applied by setting (mixed:volume voice) on each deck's Harmony voice.

ICY Metadata During DJ Session

When the DJ is live, ICY metadata updates to reflect the currently audible track. Options:

  • Auto-detect: Whichever deck is louder (based on crossfader position) determines the displayed track
  • Manual override: DJ can set custom metadata text (e.g., "DJ Fade — Live Set")
  • Session default: Show "DJ <username> — Live" when the session starts

API Endpoints

All endpoints require :dj role.

Session Management

Endpoint Method Description
asteroid/dj/session/start POST Start DJ session
asteroid/dj/session/end POST End session, resume auto-playlist
asteroid/dj/session/status GET Full state (polled by UI every 500ms)
asteroid/dj/session/metadata POST Set custom ICY metadata text

Deck Control

Endpoint Method Params Description
asteroid/dj/deck/load POST deck (a/b), track-id Load track onto deck
asteroid/dj/deck/play POST deck Play
asteroid/dj/deck/pause POST deck Pause
asteroid/dj/deck/stop POST deck Stop and unload
asteroid/dj/deck/seek POST deck, position (seconds) Seek
asteroid/dj/deck/volume POST deck, volume (0.01.0) Set deck volume

Crossfader

Endpoint Method Params Description
asteroid/dj/crossfader POST position (0.01.0) Set crossfader

External Input

Endpoint Method Params Description
asteroid/dj/external/connect POST source (alsa/pulse) Connect local audio input
asteroid/dj/external/disconnect POST Disconnect external input
asteroid/dj/external/volume POST volume (0.01.0) Set external input volume

Library Search

Endpoint Method Params Description
asteroid/dj/library/search GET q (search query) Search library for tracks

Frontend Design

Template: template/dj-console.ctml

Layout

┌─────────────────────────────────────────────────────────────┐
│  🎛️  DJ CONSOLE                    DJ: fade    [End Session]│
├────────────────────────┬────────────────────────────────────┤
│                        │                                     │
│   ◉ DECK A             │   ◎ DECK B                         │
│   ─────────────────    │   ─────────────────                │
│   Orbital              │   (empty)                          │
│   Halcyon + On + On    │                                     │
│   Brown Album          │                                     │
│                        │                                     │
│   ▶  ⏸  ■             │   ▶  ⏸  ■                          │
│   [========|------]    │   [-------------]                   │
│   2:14 / 9:27          │   0:00 / 0:00                      │
│                        │                                     │
│   VOL [=========-]     │   VOL [=========-]                  │
│                        │                                     │
├────────────────────────┴────────────────────────────────────┤
│                                                              │
│      A [===================|===================] B           │
│                     CROSSFADER                               │
│                                                              │
├──────────────────────────────────────────────────────────────┤
│  🎤 EXTERNAL INPUT          [Connect]  Vol [=========-]      │
│     Status: Not connected                                    │
├──────────────────────────────────────────────────────────────┤
│  📚 LIBRARY                                                  │
│  [____________________________] [Search]                     │
│                                                              │
│  Artist              Title                  Album     Action │
│  ─────────────────── ───────────────────── ───────── ─────── │
│  Orbital             Halcyon + On + On     Brown..   [A] [B] │
│  Underworld          Born Slippy (Nuxx)    STITI     [A] [B] │
│  Boards of Canada    Kid for Today         In a..   [A] [B] │
│  Drexciya            Bubble Chamber        Journey  [A] [B] │
│  Model 500           Digital Solutions     Digital  [A] [B] │
│  ...                                                         │
│                                                              │
│  ◀ Page 1 of 12 ▶                                            │
└──────────────────────────────────────────────────────────────┘

UI Behaviour

  • Status polling: ParenScript polls asteroid/dj/session/status every 500ms for deck positions, crossfader state, and track info. This keeps the progress bars and time displays in sync.
  • Crossfader: An HTML range input (slider). On change, sends the new position to asteroid/dj/crossfader.
  • Deck volume: Vertical or horizontal sliders per deck.
  • Library search: Text input with debounced search (300ms delay). Results show artist, title, album, and [Load A] / [Load B] buttons.
  • Deck controls: Play/Pause/Stop buttons. Seek by clicking on the progress bar.
  • External input: Connect button opens a dropdown to select audio source (ALSA device, PulseAudio). Volume slider.
  • Session indicator: Shows who is currently DJing and session duration. Visible to admin users from the admin dashboard as well.

ParenScript: parenscript/dj-console.lisp

Key functions:

Function Description
poll-session-status GET status, update all UI elements
load-track-to-deck POST load, refresh deck display
toggle-deck-playback Play/pause deck
stop-deck Stop and unload
update-crossfader POST new crossfader position
update-deck-volume POST new volume
search-library GET search results, render table
start-session POST session start
end-session POST session end, redirect to front page
connect-external POST external input connection

Navigation

Add "DJ Console" link to template/partial/navbar-admin.ctml, visible only to users with :dj or :admin role:

<c:unless test='(asteroid::member-string "dj" (** :navbar-exclude))'>
  <a href="/asteroid/dj"
     lquery='(attr :target (when framesetp "_self"))'>
    🎛️ DJ
  </a>
</c:unless>

The DJ link appears between "Profile" and "Admin" in the navbar. For :dj users who don't have :admin access, they won't see the Admin or Users links but will see the DJ Console link.

Files to Create / Modify

New Files

File Description
dj-session.lisp DJ session state, deck management, auto-fallback
parenscript/dj-console.lisp DJ console UI logic (ParenScript)
template/dj-console.ctml DJ console HTML template

Modified Files

File Change
asteroid.lisp Add define-page dj-console, DJ API endpoints, serve dj-console.js
asteroid.asd Add dj-session to :components
template/partial/navbar-admin.ctml Add DJ Console nav link
stream-harmony.lisp Add pause/resume hooks for auto-playlist
cl-streamer/harmony-backend.lisp Export deck-level play/pause/seek if needed
cl-streamer/package.lisp Export new symbols

Implementation Plan

Phase 1: Library Mixing (Core)

  1. dj-session.lisp — session lifecycle, deck state, crossfader math
  2. API endpoints in asteroid.lisp — session/deck/crossfader control
  3. stream-harmony.lisp — pause/resume auto-playlist on session start/end
  4. template/dj-console.ctml — dual-deck UI with library browser
  5. parenscript/dj-console.lisp — polling, controls, search
  6. Navigation and ASDF updates

Phase 2: External Audio Input

  1. Local audio capture via cl-mixed-pulse or cl-mixed-alsa
  2. Network audio input — accept Icecast source protocol connections
  3. External input UI in the DJ console
  4. Hybrid mixing (decks + external simultaneously)

Phase 3: Polish

  1. Waveform display (decode audio ahead of time, render waveform in canvas)
  2. Cue points / hot cues
  3. BPM detection and sync
  4. Effects (EQ, filter, reverb via cl-mixed effects chain)
  5. Session recording (save the mixed output to a file)
  6. Chat / talkback between DJ and listeners

Open Questions

  1. Monitoring / cue preview: Should the DJ be able to preview a track in headphones before pushing it live? This would require a second audio output (local sound card) separate from the streaming drain. Possible with Harmony but adds complexity.
  2. Concurrent sessions: Do we ever want multiple DJs? (e.g., back- to-back sets with handover). For now, single session is simpler.
  3. Network input protocol: Icecast source protocol is the most compatible with existing DJ software. Should we also support SRT for lower latency?
  4. Latency: The current pipeline has ~50ms latency (Harmony's buffer) plus encoding time. For remote DJ monitoring this is fine. For local DJ monitoring with headphones, we may want a direct monitor path that bypasses encoding.
  5. Recording: Should DJ sets be recorded to disk automatically? Could write the raw PCM or a high-quality FLAC alongside the encoded streams.