Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 24 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,15 @@ Use it at your own risk, and back up your rekordbox library before importing any
- **Browse tracks** on CDJs by artist, album, genre, BPM, key, label, year, remixer, folder
- **Import your library** — from **rekordbox** (`rekordbox.xml`, an encrypted `master.db`, or a full library-backup `.zip`) or **Traktor** (`collection.nml`). A rekordbox import brings tracks, MyTags (with categories), track colors, (smart) playlists, cue points (hot + memory, with colors/loops), ANLZ analysis (waveforms/beat grids/phrases), and artwork; a Traktor import brings tracks, playlists, and cues
- **Smart playlists** — rekordbox rule sets imported and evaluated live (BPM/key/genre/date/tag/… conditions)
- **Serve a rekordbox USB directly** — point `--music-dir` at an exported stick: its playlists appear on the decks and in the web UI, tracks keep rekordbox's own beat grids, waveforms and hot/memory cues, and the stick's database replaces the tag scan (web UI, decks and stick share one track-ID space)
- **DJM mixer awareness** — surfaces DJM channel/master state on the link
- **Native FLAC/WAV/AIFF playback** — CDJ decodes lossless formats directly over NFS
- **Color waveforms** on the CDJ (PWV4 overview + PWV5 scrolling, generated via FFT spectral analysis); honors the CDJ "waveform color" setting (blue / RGB / 3-band) in both the deck and the web UI
- **BPM detection** with dynamic programming beat tracker and multi-ratio correction
- **Color waveforms** on the CDJ (PWV4 overview + PWV5 scrolling, generated with time-domain Butterworth filter banks calibrated against rekordbox's rendering); honors the CDJ "waveform color" setting (blue / RGB / 3-band) in both the deck and the web UI
- **BPM detection** with autocorrelation + a perceptual tempo prior, coherence-verified integer/half-integer snap, and a DP beat tracker
- **Key detection** using chromagram analysis (octaves 4-7, Krumhansl-Kessler profiles)
- **Beat grid** generation with QM-DSP-inspired DP tracking, zero-phase scoring, and BPM rounding
- **Beat grid** generation phase-locked via a multiband onset tempogram (modelled on rekordbox's reverse-engineered 25-band novelty), windowed clarity-weighted phase, and a gated half-beat correction
- **Phrase/song structure** detection (PSSI format)
- **Cue point management** — save/load via CDJ + add/edit/delete via HTTP API/web UI with color support
- **Cue point management** — hot and memory cues: save/load from the CDJ, create either kind in the web UI (+ CUE / + MEM), add/edit/delete via HTTP API, with color support
- **NFS v2 file server** streams audio to CDJs (with optional FLAC/WAV/AIFF to MP3 transcoding)
- **Lazy analysis** mode for instant startup (tracks analyzed on-demand when CDJs request them); artwork is also extracted lazily
- **Library mode** with no music directory required (add tracks dynamically via HTTP API)
Expand Down Expand Up @@ -177,15 +178,30 @@ Scan a music directory at startup:
./vynull --interface eth1 --music-dir /path/to/music --lazy-analysis
```

### Rekordbox USB Mode
### Serve a rekordbox USB

If you have an existing rekordbox-exported USB drive:
Point `--music-dir` at an existing rekordbox-exported USB drive and Vynull
serves the stick's own database instead of scanning tags:

```bash
# CDJ mode needs UDP 111 — see "Port 111 (CDJ mode only)" for sudo-free options
sudo ./vynull --interface eth1 --music-dir /media/usb --mode cdj
./vynull --interface eth1 --music-dir /media/usb
```

Decks and the web UI get the stick's playlists, rekordbox's own beat
grids, waveforms and hot/memory cues (our analyzer only runs for tracks
with missing analysis), and everything shares the stick's track IDs. The
stick's content is treated as read-only — its playlists can't be edited
from the web UI.

OneLibrary sticks work when exported with older-player compatibility:
Vynull reads the classic `PIONEER/rekordbox/export.pdb` half of the
dual-format layout (the encrypted OneLibrary database itself is not yet
supported).

The same works in CDJ mode (`--mode cdj`, appears as a CDJ-USB source
instead of a rekordbox laptop) — that mode needs UDP 111; see "Port 111
(CDJ mode only)" for sudo-free options.

### Generate USB Structure

Create a rekordbox-compatible USB structure from a music directory:
Expand Down
120 changes: 120 additions & 0 deletions analysis/realaudio_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
// SPDX-License-Identifier: GPL-3.0-or-later

package analysis

import (
"fmt"
"os"
"path/filepath"
"testing"
)

// Real-audio diagnostic harnesses for beat/phrase detection, env-gated so
// machines without (private, copyrighted) audio still pass. Moved here from
// link/prolink, where the part-3c encoder relocation had stranded them —
// their subject is the detector, not the wire encoders (those dumps live in
// link/prolink/encoders_realaudio_test.go).

// TestDetectBeats runs beat detection across every audio file in a
// directory and sanity-checks that a BPM comes out.
//
// TEST_MUSIC_DIR=/path/to/tracks go test ./analysis/ -run TestDetectBeats -v
func TestDetectBeats(t *testing.T) {
dir := os.Getenv("TEST_MUSIC_DIR")
if dir == "" {
t.Skip("Set TEST_MUSIC_DIR to run BPM detection tests")
}

entries, err := os.ReadDir(dir)
if err != nil {
t.Fatalf("read dir: %v", err)
}

for _, e := range entries {
if e.IsDir() {
continue
}
switch filepath.Ext(e.Name()) {
case ".mp3", ".m4a", ".flac", ".wav", ".aiff", ".aif":
default:
continue
}

path := filepath.Join(dir, e.Name())
t.Run(e.Name(), func(t *testing.T) {
samples, err := DecodePCM(path, AnalysisRate)
if err != nil {
t.Fatalf("decode: %v", err)
}

result := DetectBeatsWithEncoderDelay(samples, AnalysisRate, EncoderDelayMs(path))
dur := float64(len(samples)) / float64(AnalysisRate)

t.Logf("BPM=%.2f beats=%d downbeat=%.1fms duration=%.1fs",
result.BPM, len(result.Beats), result.Downbeat, dur)

if result.BPM == 0 {
t.Error("BPM detection failed (returned 0)")
}
})
}
}

// TestDetectBeatsSingle analyzes one file (TEST_AUDIO_FILE) and prints the
// detection + phrase breakdown. Uses the format-aware encoder delay — the
// old harness called DetectBeats (the lossy default), silently shifting
// lossless files' grids by the 25ms MP3 encoder-delay compensation.
func TestDetectBeatsSingle(t *testing.T) {
path := os.Getenv("TEST_AUDIO_FILE")
if path == "" {
t.Skip("Set TEST_AUDIO_FILE to run single-file BPM test")
}

samples, err := DecodePCM(path, AnalysisRate)
if err != nil {
t.Fatalf("decode: %v", err)
}

result := DetectBeatsWithEncoderDelay(samples, AnalysisRate, EncoderDelayMs(path))
dur := float64(len(samples)) / float64(AnalysisRate)

fmt.Printf("File: %s\n", filepath.Base(path))
fmt.Printf("Duration: %.1fs\n", dur)
fmt.Printf("BPM: %.2f\n", result.BPM)
fmt.Printf("Beats: %d\n", len(result.Beats))
fmt.Printf("Downbeat: %.1fms\n", result.Downbeat)

if len(result.Beats) > 8 {
fmt.Printf("First 8 beats (ms): ")
for i := 0; i < 8; i++ {
fmt.Printf("%.1f ", result.Beats[i])
}
fmt.Println()

fmt.Printf("IBIs (ms): ")
for i := 1; i < 8 && i < len(result.Beats); i++ {
fmt.Printf("%.1f ", result.Beats[i]-result.Beats[i-1])
}
fmt.Println()
}

phrases := DetectPhrases(samples, AnalysisRate, result.BPM, result.Downbeat)
fmt.Printf("\nPhrases: %d\n", len(phrases))
phraseNames := map[uint16]string{
1: "Intro", 2: "Up", 3: "Down", 5: "Chorus", 6: "Outro",
}
for i, p := range phrases {
name := phraseNames[p.Kind]
if name == "" {
name = fmt.Sprintf("Unknown(%d)", p.Kind)
}
startSec := float64(p.StartBeat-1) * 60.0 / result.BPM
endSec := float64(p.EndBeat) * 60.0 / result.BPM
fmt.Printf(" %2d. %-8s beats %4d-%4d (%.1fs - %.1fs) energy=%.3f\n",
i+1, name, p.StartBeat, p.EndBeat, startSec, endSec, p.Energy)
}

if result.BPM == 0 {
t.Error("BPM detection failed")
}
}
189 changes: 0 additions & 189 deletions link/prolink/bpm_test.go

This file was deleted.

Loading
Loading