vdj-waveshare

Architecture

 VirtualDJ (Mac)                         same Mac                     LAN
┌─────────────────────────┐   UDP    ┌──────────────┐   HTTP + SSE   ┌──────────────────┐
│ deck 1  WaveShare plugin │ ───────▶ │  server.js   │ ─────────────▶ │ browser / tablet │
│ deck 2  WaveShare plugin │  :7778   │  (Node 18+)  │     :7777      │ (lighting op)    │
└─────────────────────────┘          └──────────────┘                └──────────────────┘

Audio never leaves VirtualDJ. Only the envelope goes out: 3 bytes per 10 ms.

Plugin: plugin/WaveShare.cpp

A VirtualDJ 8 Buffer DSP effect (IVdjPluginBufferDsp8). Only this plugin type receives the decoded track and reads samples at any position, which makes it work with streaming (TIDAL) where no file exists on disk.

One instance per deck. Each instance:

  1. Passes audio through untouched in OnGetSongBuffer.
  2. Scans the track in 8192-sample chunks, starting at the playhead, then wraps to the start. A 4-minute track takes about 15 s.
  3. Collects metadata (title, BPM, key, loop) 4 times per second through VirtualDJ script queries.
  4. Runs a sender thread. The thread reads plugin memory only and never calls VirtualDJ.

Pre-load

VirtualDJ calls the plugin only while its deck plays. To scan a track on a paused deck, the playing deck’s audio thread also works for the paused deck’s instance.

Lessons from testing:

Beat grid

OnGetSongBuffer reads SongPosBeats and SongBpm (samples per beat), which VirtualDJ fills before each call of the deck’s own audio thread. beatAnchor() turns them into the sample position of beat 0. Two atomics carry it to the sender thread, which appends 8 bytes to the metadata packet. A new track resets the grid.

Pre-load and the master clock (tickRemote) call the plugin from another deck’s or the master’s thread, where these fields belong to the caller. So they never set the grid. A paused deck driven by the master clock shows its waveform, but no beat lines until it plays once.

Master clock: plugin/WaveShareClock.cpp

A second, tiny plugin (IVdjPluginDsp8) for the master effect slot. VirtualDJ calls master effects about 100 times per second even while every deck is paused (measured with a probe on 14 Oct 2026, deck N queries returned the paused deck’s length and BPM). Confirmed working on real VirtualDJ the same day.

Server: server/server.js

Node 18+, no npm packages.

Dashboard: server/index.html

Single file, no build step, no libraries.

Known issues

Issue Status
Network Control plugin crashes VirtualDJ on quit (std::terminate in a NetworkSocket thread, 3 crash reports on 13 Oct 2026) Server no longer uses it
get_loaded_song 'filepath' crashes VirtualDJ on a TIDAL track Never used
VirtualDJ wiki lists Documents/VirtualDJ/PluginsArm as plugin folder Wrong on current versions. Real path: ~/Library/Application Support/VirtualDJ/PluginsMacArm/SoundEffect
Units of get_loop_in_time Assumed ms, not verified with pitch ≠ 0
SCALE in the plugin Set by eye
Beat grid units (SongBpm as track samples per beat) Assumed, not verified with pitch ≠ 0
Cable detection on macOS (--iface auto) By port name. A dock, club Ethernet or a phone tethered over USB counts as cable
A track loaded while no deck plays (typical first track of a set) stayed empty until a deck played. VirtualDJ never calls a paused deck’s effect Fixed by WaveShare Clock on the master (confirmed on real VirtualDJ, 14 Oct 2026). Without it: play any deck, even in the headphones
Windows No build yet