clubtagger is a low-latency recorder and song tagger for clubs and venues.
It captures live audio from ALSA or SLink (Allen & Heath SQ network audio), generates acoustic fingerprints locally using [vibra],
identifies songs via Shazam-compatible lookup, and integrates with Pioneer CDJ/XDJ equipment via Pro DJ Link.
- 🎧 Live audio capture — ALSA (Linux) or SLink (Allen & Heath SQ, 24-bit)
- 🔎 Local fingerprinting via
libvibra(no audio leaves the system) - 🎛️ Pro DJ Link integration — reads track metadata directly from Pioneer CDJs/XDJs
- 📚 OneLibrary support — decrypts and queries Rekordbox 6+ exportLibrary.db (CDJ-3000X)
- 🧠 Confidence model — weighted signal accumulation from CDJ + Shazam + on-air status
- 🔤 Fuzzy matching — Levenshtein distance handles typos and encoding differences
- 🎵 Vinyl-friendly — tolerates pitch variations from turntables
- 💾 WAV/FLAC recording with seamless file splitting
- 🗄️ SQLite logging — track plays with timestamps, ISRC codes
- 🌐 Web UI — real-time VU meters, deck status, beat/BPM/key via WebSocket
- ⚙️ Lightweight C implementation with modular architecture
sudo apt-get install libasound2-dev libcurl4-openssl-dev libsqlite3-dev libpcap-dev libflac-dev
makebrew install curl sqlite libpcap flac openssl
make # builds without ALSA supportlibcurl— HTTP communicationlibvibra— local acoustic fingerprinting (optional, enables--audio-tag)libsqlite3— track databaselibpcap— network packet capture (SLink, Pro DJ Link)libcrypto(OpenSSL) — OneLibrary decryption (SQLCipher 4)libFLAC— FLAC encoding (optional)libasound2— ALSA audio capture (Linux only)
The build auto-detects available libraries. Without libvibra, only --record and --cdj-tag modes are available.
clubtagger has three main modes that can be combined:
| Mode | Flag | Description |
|---|---|---|
| Recording | --record |
Capture audio to WAV/FLAC files |
| Audio tagging | --audio-tag |
Identify songs via Shazam fingerprinting |
| CDJ tagging | --cdj-tag |
Read track metadata from Pioneer CDJs |
./clubtagger --record --audio-tag \
--source alsa --device hw:2,0 \
--db tracks.db --verbose./clubtagger --record --audio-tag \
--source slink --device en0 --rate 96000 \
--format flac --db tracks.db./clubtagger --cdj-tag \
--prolink-interface en7 \
--db tracks.db --verbose./clubtagger --record --audio-tag --cdj-tag \
--source slink --device en7 \
--prolink-interface en7 \
--db tracks.db --ws-socket /run/clubtagger.sock./clubtagger --cdj-tag \
--prolink-interface eth1 --prolink-passive \
--db tracks.db| Option | Description |
|---|---|
--record |
Enable audio recording to WAV/FLAC |
--audio-tag |
Enable Shazam fingerprint identification (requires libvibra) |
--cdj-tag |
Enable CDJ/Pro DJ Link track reading |
| Option | Description | Default |
|---|---|---|
--source |
Audio source: alsa or slink |
(required for audio) |
--device |
ALSA device or network interface | default |
--rate |
Sample rate (Hz) | 48000 |
--channels |
Audio channels | 2 |
--bits |
Bit depth (16 or 24) | 16 |
| Option | Description | Default |
|---|---|---|
--format |
Output format: wav or flac |
wav |
--prefix |
Filename prefix | capture |
--outdir |
Output directory | . |
--max-file-sec |
Max seconds per file | 600 |
--ring-sec |
Ring buffer size | max-file-sec + 60 |
--threshold |
RMS threshold for music detection | 50 |
--sustain-sec |
Seconds above threshold to start | 1.0 |
--silence-sec |
Silence duration to stop | 15 |
The --threshold value is used for both recording triggers and Shazam fingerprinting.
| Option | Description | Default |
|---|---|---|
--fingerprint-sec |
Fingerprint length | 12 |
--interval |
Seconds between checks | 2 |
--shazam-gap-sec |
Min seconds between lookups | 10 |
--same-track-hold-sec |
Skip lookups for same track | 90 |
| Option | Description | Default |
|---|---|---|
--prolink-interface |
Network interface for CDJ traffic | (required) |
--prolink-passive |
SPAN/mirror port mode (eavesdrop only, no registration) | Off |
--olib-key KEY |
OneLibrary (exportLibrary.db) decryption passphrase | (none) |
| Option | Description | Default |
|---|---|---|
--match-threshold |
Fuzzy match similarity % (0-100) | 60 |
| Option | Description | Default |
|---|---|---|
--db |
SQLite database path | (none) |
--ws-socket |
WebSocket server: Unix socket path or TCP port number | (none) |
--timezone |
Override timezone | Europe/Amsterdam |
--verbose |
Enable detailed logging | Off |
clubtagger supports two modes for Pro DJ Link integration:
On startup, clubtagger observes the network for 10 seconds without sending anything. If status/beat packets are already flowing (because 2+ CDJs or a CDJ + DJM are already communicating), it stays passive — no player slot consumed, completely invisible to the DJ network.
If no status packets are seen during observation (single CDJ with no peers, or CDJs waiting for a peer before broadcasting), clubtagger registers as a virtual CDJ (active mode), occupying one player slot.
| Situation | Auto-detected mode | Slot used? |
|---|---|---|
| SPAN/mirror port | Passive | No |
| 2+ CDJs on switch | Passive | No |
| CDJ + DJM on switch | Passive | No |
| Single CDJ, no other peers | Active | Yes (1 slot) |
In both modes, clubtagger can:
- Receive status packets from CDJs (rekordbox ID, BPM, play state, on-air)
- Receive beat/position packets with real-time playback position (CDJ-3000: ~30ms)
- Passively capture databases (PDB and OneLibrary) from NFS traffic between CDJs
- Correlate with fingerprints for higher confidence matches
Active mode additionally enables: 5. Fetch databases (OneLibrary + PDB) directly from CDJs via NFS 6. Query DBServer (port 1051) for track title/artist as a fallback
If a track can't be resolved passively, clubtagger can temporarily re-activate to query DBServer, then return to passive.
Forces passive mode regardless of auto-detection. Use this when you know you're on a SPAN port and want to guarantee zero network footprint.
./clubtagger --cdj-tag --prolink-interface eth1 --prolink-passive --db tracks.dbLimitation: Passive mode (both auto and forced) requires at least two devices on the DJ network. A single CDJ with no peers won't broadcast status or beat packets. Use active mode (omit --prolink-passive) for single-CDJ setups — auto-detection handles this automatically.
CDJ Status Packet → rekordbox_id → OneLibrary lookup (SQLite)
↘ PDB lookup (fallback)
↘ DBServer query (last resort)
↘ Fuzzy match with Shazam result
CDJ-3000X and newer hardware export databases in the OneLibrary format — a SQLCipher 4 encrypted SQLite database (PIONEER/rekordbox/exportLibrary.db). clubtagger:
- Fetches the encrypted database via NFSv2
- Derives the decryption key using PBKDF2-HMAC-SHA512 (256,000 iterations)
- Decrypts all pages with AES-256-CBC
- Loads the result as an in-memory SQLite database
- Queries tracks by content_id with artist JOINs
OneLibrary provides richer metadata than the legacy PDB format (26 tables including playlists, cue points, history, and more). If OneLibrary is not available (older USB sticks without Rekordbox 6+ export), clubtagger falls back to the PDB parser.
| Device | Database | Position Packets | Max Players |
|---|---|---|---|
| CDJ-2000NXS2 | PDB only | No | 4 |
| CDJ-3000 | PDB + OneLibrary | Yes (~30ms) | 6 |
| CDJ-3000X | PDB + OneLibrary | Yes (~30ms) | 6 |
| DJM-900NXS2 | — | — | (mixer) |
| DJM-V10 | — | — | (6ch mixer) |
When comparing CDJ metadata with Shazam results, clubtagger uses:
- Substring containment — "One More Time" matches "One More Time (Original Mix)"
- Levenshtein similarity — "Tiësto" matches "Tiesto" (86% similarity)
Configure with --match-threshold (default 60%).
Enable the WebSocket server for a real-time web interface:
./clubtagger --cdj-tag --prolink-interface en7 \
--ws-socket /run/clubtagger.sockFor development/testing, use a TCP port instead of a Unix socket:
./clubtagger --cdj-tag --prolink-interface en7 \
--ws-socket 9090See nginx.conf.example for a full configuration with HTTPS and basic auth.
upstream clubtagger {
server unix:/run/clubtagger.sock;
}
location = /ws {
auth_basic off;
proxy_pass http://clubtagger;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_buffering off;
proxy_read_timeout 24h;
}
location / {
root /var/www/clubtagger;
}- VU meters — 60 Hz audio levels with peak hold and decay
- CDJ deck status — real-time from raw Pro DJ Link packets:
- Playing/paused, ON AIR, BPM with pitch offset
- Musical key (A-based mapping from CDJ-3000)
- Beat position (4-dot indicator updated per-beat)
- Loop state and beat count
- Master, Sync, Master Tempo badges
- Track position / duration (CDJ-3000: 30ms updates)
- Media source (USB/SD/Link) and database source (OneLibrary/PDB/DBServer)
- Track identification — confidence bars with CDJ + Shazam signals
- Track history — recent plays from database
- Activity log — live system messages
- System stats — CPU load, memory, disk space
Raw Pro DJ Link packets are forwarded as binary WebSocket frames directly to the browser. JavaScript parses packet bytes using known offsets (BPM, pitch, beat, key, loop, position). This provides sub-millisecond UI updates without C-side JSON serialization overhead.
Metadata that requires C-side logic (track title, artist, confidence, ISRC, database source) is sent as JSON text frames at 1 Hz.
[cap] started: rate=96000 ch=2 (SLink source, 24-bit)
[cdj] CDJ-2000NXS2 #1 online @ 192.168.1.101
[cdj] 📥 Fetching database from 192.168.1.101 (USB)...
[cdj] ✅ Loaded 847 tracks from database
[wrt] TRIGGER avg=142 (prebuffer 480000 frames)
[id] 2026-02-08 00:15:23 MATCH: Daft Punk — One More Time [ISRC GBDUW0000059] (85%, both)
[cdj] Fuzzy title match: 92% ("One More Time" vs "One More Time (Radio Edit)")
[wrt] SPLIT at 57600000 frames (10.0 min)
SELECT timestamp, artist, title, confidence, source FROM plays ORDER BY timestamp DESC LIMIT 5;| timestamp | artist | title | confidence | source |
|---|---|---|---|---|
| 2026-02-08 00:15:23 | Daft Punk | One More Time | 85 | both |
| 2026-02-08 00:11:45 | Kraftwerk | The Model | 75 | audio |
| 2026-02-08 00:08:12 | Aphex Twin | Windowlicker | 70 | cdj/on-air |
clubtagger/
├── audio/ # Audio capture (ALSA, SLink, AF_XDP)
├── prolink/ # Pro DJ Link protocol implementation
│ ├── prolink.c # Packet parsing (keepalive, status, beat, position)
│ ├── registration.c # Virtual CDJ registration and slot management
│ ├── dbserver.c # DBServer queries (port 1051)
│ ├── nfs_client.c # NFS v2 client for database fetching
│ ├── pdb.c # Rekordbox export.pdb fetch + parser
│ ├── onelibrary.c # OneLibrary exportLibrary.db decrypt + SQLite query
│ └── track_cache.c # In-memory metadata cache
├── shazam/ # Audio fingerprinting
├── writer/ # Async WAV/FLAC writing
├── server/ # WebSocket server (binary packet relay + JSON events)
├── db/ # SQLite integration
└── www/ # Web UI (HTML/JS)
Audio is captured into a fixed-size ring buffer. Oldest samples are automatically overwritten. When recording triggers, all buffered audio becomes the "prebuffer". This provides:
- Constant memory usage regardless of silence duration
- Gapless recording when music briefly dips below threshold
- No lost samples as long as gaps are shorter than the buffer
- clubtagger never sends raw audio — only fingerprint hashes
- CDJ integration auto-detects whether to register or stay passive; SPAN ports and multi-CDJ setups consume zero player slots
- UTF-8 safe throughout: handles accented characters, emoji, CJK
- Supports streaming tracks (Beatport LINK, etc.) via status packet detection
- Intended for licensed environments to log playback for rights reporting
- Respect third-party service terms and copyright laws
MIT — see LICENSE
- BayernMuller/vibra
- Deep Symmetry — Pro DJ Link protocol documentation
- alphatheta-connect — CDJ-3000 protocol details
- pyrekordbox — OneLibrary format research
- ALSA Project
- libcurl
- SQLite
- OpenSSL — SQLCipher 4 decryption