A desktop companion for planning DJ sets in Mixxx. It suggests which track mixes well next — by Camelot move, mood and energy change, and BPM distance — and routes the set toward tracks you want to reach. Every suggestion shows why (the move, its mood, energy Δ, BPM Δ), because the point is to build harmonic-mixing intuition.
It is a set-preparation tool, not a live controller: it reads a read-only snapshot of
Mixxx's library and exports finished sets as .m3u8 for Mixxx's Import Playlist.
python -m venv .venv
.venv/bin/pip install -e '.[test]'
.venv/bin/harmonic-set-builder # or: .venv/bin/python -m harmonic_set_builder
.venv/bin/harmonic-set-builder --db /path/to/mixxxdb.sqlite # override (saved to config)
Windows: .venv\Scripts\harmonic-set-builder.exe. The library is auto-detected in
~/.mixxx/, ~/.local/share/Mixxx/ (Linux), %LOCALAPPDATA%\Mixxx\ (Windows) or Mixxx's
macOS container; File → Choose Mixxx library… overrides it.
Tests: .venv/bin/pytest (UI tests run offscreen).
The window has three views (toolbar or View menu, Ctrl+1 / 2 / 3): Set Builder, Phrases and Learn. The preview bar at the bottom is shared by all three.
cover/folder/front image in the album folder. They load in the
background and are cached as small PNGs in the app data directory (covers/), so the drive isn't
re-read each launch. After mounting a drive, Refresh library (Ctrl+R) picks up covers that were missing.I16 B8 D32 Br16 B8 D32 O16
(Intro, Build, Drop, Breakdown, Groove, Outro, lengths in bars). Fills in once tracks are analyzed in Phrases.Track structure: where the intro, builds, drops, breakdowns and outro are, from audio analysis.
hsb analyze --relabel).library.beats), so section
starts, transition points and exported cues sit on Mixxx's beats. Otherwise allin1's beats or detected beats.◆ Drop 1, …) and sets
Mixxx's intro/outro markers from the Intro/Outro sections. A dry run is shown first; writing needs Mixxx
closed. See Cue export safety below.Sets autosave (debounced 0.5 s, atomic, last 20 versions) to the app data directory
(~/.local/share/HarmonicSetBuilder/Harmonic Set Builder/sets on Linux). Config is one JSON file in
the app config directory.
.venv/bin/hsb analyze # every library track not analyzed yet (built-in analyzer)
.venv/bin/hsb analyze --backend allin1 --limit 5
.venv/bin/hsb analyze --path ~/Music/new # a folder instead of the Mixxx library
.venv/bin/hsb show "Chicken Soup" # sections + an energy sparkline
.venv/bin/hsb validate --sample 20 # analyze 20 tracks across genres → CSV to check by ear
.venv/bin/hsb export-cues --dry-run # what cue export would do; touches nothing
.venv/bin/hsb export-cues --tracks "Chicken Soup" --replace-own
analyze/validate refuse to run while Mixxx is open unless given --allow-while-mixxx-runs.
allin1 needs Python ≤ 3.11 (its madmom dependency doesn't build on 3.14) and a natten build matching
PyTorch. The app looks for it in <app data>/allin1-venv (override in Settings → Phrase analysis):
D="$HOME/.local/share/HarmonicSetBuilder/Harmonic Set Builder/allin1-venv"
python3.11 -m venv "$D"
"$D/bin/pip" install cython numpy==1.26.4 wheel setuptools
"$D/bin/pip" install torch==2.4.1 torchaudio==2.4.1 --index-url https://download.pytorch.org/whl/cpu
"$D/bin/pip" install --no-build-isolation git+https://github.com/CPJKU/madmom
NATTEN_CUDA_ARCH="" "$D/bin/pip" install --no-build-isolation natten==0.17.1 # CPU build, a few minutes
"$D/bin/pip" install allin1==1.1.0 demucs
On Windows use py -3.11 and Scripts\pip.exe; the same pins apply (not yet tried there).
hsb benchmark --tracks-file benchmark/testset.txt runs the DJ's test set (21 tracks they know well; the desktop
script uses it by default). hsb benchmark --sample 10 instead picks 10 tracks (across genres) on which you've placed at least 3 hot cues in
Mixxx, runs every analyzer on them, and reports how many of your cues each one finds within 0.5 s and within
one bar: the built-in rules, allin1's pop model (raw and with the rules), allin1's unlisted EDM checkpoint
Raveform v1 (raw and with the rules) and CUE-DETR (ETH Zurich, ISMIR 2024; the authors' threshold and
clustered candidates). Model outputs are cached in analysis.sqlite, so re-scoring is instant. Details go to
<app data>/benchmark/latest.json. On a laptop CPU it takes about 15–20 minutes per track (allin1's source
separation and 8-model pop ensemble dominate); on an NVIDIA GPU, well under a minute.
The Phrases view's Show: menu switches the plot and section list between everything saved for the selected
track: the saved analysis, each model's raw output (allin1's pop labels, Raveform's unnamed EDM labels r0–r10
with the model's confidence), each model run through the app's rules (rebuilt from the stored per-bar features,
no audio needed) and CUE-DETR's cue points. Your Mixxx hot cues stay drawn on top.
hsb export-analysis FILE.json.gz writes every analyzed track with all saved model outputs; Import… in the
Phrases view (or hsb import-analysis FILE) loads it. Tracks match by path (--map FROM=TO rewrites prefixes);
the file size must match, and results take on this computer's file timestamps the first time the file is seen,
so a drive moved between Windows and Linux doesn't trigger re-analysis. A more recent local result is kept.
Both desktop scripts write hsb-analysis.json.gz to the music drive: desktop_analyze.sh for the library,
desktop_benchmark.sh for the benchmark tracks.
winver), a current NVIDIA driver, then in an administrator
PowerShell: wsl --install and reboot (Ubuntu 22.04 or 24.04 both work). Open Ubuntu from the Start menu and create a user.git clone https://github.com/druppin/set-builder-companion && cd set-builder-companion
then bash scripts/setup_wsl_gpu.sh (once; 30–60 min, mostly compiling NATTEN for the GPU).~/.mixxx/mixxxdb.sqlite to the
top of the drive first. In Ubuntu, ls /mnt shows the drive letters.bash scripts/desktop_analyze.sh d (use the drive's letter; add --limit 20 for a first try)
analyzes the library on the GPU with allin1 (plus Raveform and CUE-DETR for comparison);
bash scripts/desktop_benchmark.sh d runs the benchmark. Both point the laptop's paths at the drive and write
hsb-analysis.json.gz to it; the benchmark also writes hsb-benchmark-desktop.html (timelines) and .json.
A drive plugged in after Ubuntu started isn't in /mnt: sudo mkdir -p /mnt/e && sudo mount -t drvfs E: /mnt/e.hsb-analysis.json.gz on the drive.The only code that writes to Mixxx's database is data/mixxx_cues.py, and only when you confirm a write:
mixxxdb.sqlite first, as mixxxdb.sqlite.hsb-backup-<timestamp> next to it. To undo, close
Mixxx and copy the backup back.◆ …, ids
remembered in the analysis DB) are ever replaced, and only with Replace cues from my earlier exports.
Mixxx's analyzer sets only an intro start and an outro end; by default the export fills in the missing
intro end / outro start and keeps yours (Settings → Phrase analysis turns that off). Complete markers are
left alone.Verified against this library (Mixxx 2.4, 2026-10-08): cues(id, track_id, type, position, length, hotcue, label, color); type 1 hot cue, 2 main cue, 4 loop, 6 intro, 7 outro, 8 audible sound; positions in stereo
samples (seconds × samplerate × 2); hotcue 0-based; colors 0xRRGGBB. On a copy of the library, detected drops
landed within 0.25 s of hot cues placed by hand.
mode=ro once per snapshot, copied via SQLite's backup API and
closed; all queries run on the copy. A test asserts the original file is byte-identical afterwards.
The one exception is cue export, which you ask for explicitly (see Cue export safety).analysis.sqlite, never in Mixxx's database.core/) has no Qt imports and is fully unit-tested.harmonic_set_builder/
core/ camelot, bpm, energy, ranking, route, setlist, settings, track,
theory (scales, shared notes, explanations), synth (reference audio), quiz (no Qt)
analysis/ grid (Mixxx beat grids), energy (per-bar features, builds), structure (built-in + allin1),
labels (DJ labels, cleanup), store (analysis.sqlite), pipeline, batch, transitions (no Qt)
data/ mixxx_db (locate/snapshot/read), mixxx_cues (cue export), store (autosave), export, config, paths
ui/ PySide6 models, views, energy graph, sources tree, controller, main window,
phrases (Phrases view), learn + wheel (Learn view)
cli.py the `hsb` command
tests/
Defaults from section 9 of the brief are used unless noted.
app.js, 2026-10-07; recorded in
tests/fixtures/camelotwheel_energy.json). That site scores relative major/minor and diagonals as 0,
±2 as ±3 and a semitone up (+7 wheel steps) as +2, so those replace the brief's table values.
Semitone down (not offered on the site) mirrors it at −2. All deltas are editable in Settings → Energy & moves.
Note: the site labels +7 wheel steps "Tritone Jump", but it is musically a semitone; this app's
Tritone is the real one (±6 steps) and is a clash.key_id 1–12 = C…B major, 13–24 = C…B minor;
library.location is a track_locations.id; Playlists.hidden 0 = normal, 1 = Auto DJ,
2 = history, −1 = internal placeholder (never shown).data/mixxx_cues.py (not mixxx/cues.py), and Mixxx reads
reuse data/mixxx_db.py (no separate mixxx/db.py).tests/test_analysis.py): allin1
found the boundaries but called the first half "intro", so intro only counts for a leading run that ends at
the first drop-level section, and outro only after the last drop; elsewhere energy and the kick decide.
A quiet section that ends the track after its last drop is the Outro even without a kick.tests/fixtures/real_tracks.json: per-bar features and allin1 output,
no audio), with the DJ's verdicts in the test names.--allow-while-mixxx-runs (CLI), and always runs at nice 10.hsb validate; label thresholds are constants at the
top of analysis/labels.py for tuning afterwards.A desktop companion for planning DJ sets in Mixxx. It suggests which track mixes well next — by Camelot move, mood and energy change, and BPM distance — and routes the set toward tracks you want to reach. Every suggestion shows why (the move, its mood, energy Δ, BPM Δ), because the point is to build harmonic-mixing intuition.
It is a set-preparation tool, not a live controller: it reads a read-only snapshot of
Mixxx's library and exports finished sets as .m3u8 for Mixxx's Import Playlist.
python -m venv .venv
.venv/bin/pip install -e '.[test]'
.venv/bin/harmonic-set-builder # or: .venv/bin/python -m harmonic_set_builder
.venv/bin/harmonic-set-builder --db /path/to/mixxxdb.sqlite # override (saved to config)
Windows: .venv\Scripts\harmonic-set-builder.exe. The library is auto-detected in
~/.mixxx/, ~/.local/share/Mixxx/ (Linux), %LOCALAPPDATA%\Mixxx\ (Windows) or Mixxx's
macOS container; File → Choose Mixxx library… overrides it.
Tests: .venv/bin/pytest (UI tests run offscreen).
The window has three views (toolbar or View menu, Ctrl+1 / 2 / 3): Set Builder, Phrases and Learn. The preview bar at the bottom is shared by all three.
cover/folder/front image in the album folder. They load in the
background and are cached as small PNGs in the app data directory (covers/), so the drive isn't
re-read each launch. After mounting a drive, Refresh library (Ctrl+R) picks up covers that were missing.I16 B8 D32 Br16 B8 D32 O16
(Intro, Build, Drop, Breakdown, Groove, Outro, lengths in bars). Fills in once tracks are analyzed in Phrases.Track structure: where the intro, builds, drops, breakdowns and outro are, from audio analysis.
hsb analyze --relabel).library.beats), so section
starts, transition points and exported cues sit on Mixxx's beats. Otherwise allin1's beats or detected beats.◆ Drop 1, …) and sets
Mixxx's intro/outro markers from the Intro/Outro sections. A dry run is shown first; writing needs Mixxx
closed. See Cue export safety below.Sets autosave (debounced 0.5 s, atomic, last 20 versions) to the app data directory
(~/.local/share/HarmonicSetBuilder/Harmonic Set Builder/sets on Linux). Config is one JSON file in
the app config directory.
.venv/bin/hsb analyze # every library track not analyzed yet (built-in analyzer)
.venv/bin/hsb analyze --backend allin1 --limit 5
.venv/bin/hsb analyze --path ~/Music/new # a folder instead of the Mixxx library
.venv/bin/hsb show "Chicken Soup" # sections + an energy sparkline
.venv/bin/hsb validate --sample 20 # analyze 20 tracks across genres → CSV to check by ear
.venv/bin/hsb export-cues --dry-run # what cue export would do; touches nothing
.venv/bin/hsb export-cues --tracks "Chicken Soup" --replace-own
analyze/validate refuse to run while Mixxx is open unless given --allow-while-mixxx-runs.
allin1 needs Python ≤ 3.11 (its madmom dependency doesn't build on 3.14) and a natten build matching
PyTorch. The app looks for it in <app data>/allin1-venv (override in Settings → Phrase analysis):
D="$HOME/.local/share/HarmonicSetBuilder/Harmonic Set Builder/allin1-venv"
python3.11 -m venv "$D"
"$D/bin/pip" install cython numpy==1.26.4 wheel setuptools
"$D/bin/pip" install torch==2.4.1 torchaudio==2.4.1 --index-url https://download.pytorch.org/whl/cpu
"$D/bin/pip" install --no-build-isolation git+https://github.com/CPJKU/madmom
NATTEN_CUDA_ARCH="" "$D/bin/pip" install --no-build-isolation natten==0.17.1 # CPU build, a few minutes
"$D/bin/pip" install allin1==1.1.0 demucs
On Windows use py -3.11 and Scripts\pip.exe; the same pins apply (not yet tried there).
hsb benchmark --tracks-file benchmark/testset.txt runs the DJ's test set (21 tracks they know well; the desktop
script uses it by default). hsb benchmark --sample 10 instead picks 10 tracks (across genres) on which you've placed at least 3 hot cues in
Mixxx, runs every analyzer on them, and reports how many of your cues each one finds within 0.5 s and within
one bar: the built-in rules, allin1's pop model (raw and with the rules), allin1's unlisted EDM checkpoint
Raveform v1 (raw and with the rules) and CUE-DETR (ETH Zurich, ISMIR 2024; the authors' threshold and
clustered candidates). Model outputs are cached in analysis.sqlite, so re-scoring is instant. Details go to
<app data>/benchmark/latest.json. On a laptop CPU it takes about 15–20 minutes per track (allin1's source
separation and 8-model pop ensemble dominate); on an NVIDIA GPU, well under a minute.
The Phrases view's Show: menu switches the plot and section list between everything saved for the selected
track: the saved analysis, each model's raw output (allin1's pop labels, Raveform's unnamed EDM labels r0–r10
with the model's confidence), each model run through the app's rules (rebuilt from the stored per-bar features,
no audio needed) and CUE-DETR's cue points. Your Mixxx hot cues stay drawn on top.
hsb export-analysis FILE.json.gz writes every analyzed track with all saved model outputs; Import… in the
Phrases view (or hsb import-analysis FILE) loads it. Tracks match by path (--map FROM=TO rewrites prefixes);
the file size must match, and results take on this computer's file timestamps the first time the file is seen,
so a drive moved between Windows and Linux doesn't trigger re-analysis. A more recent local result is kept.
Both desktop scripts write hsb-analysis.json.gz to the music drive: desktop_analyze.sh for the library,
desktop_benchmark.sh for the benchmark tracks.
winver), a current NVIDIA driver, then in an administrator
PowerShell: wsl --install and reboot (Ubuntu 22.04 or 24.04 both work). Open Ubuntu from the Start menu and create a user.git clone https://github.com/druppin/set-builder-companion && cd set-builder-companion
then bash scripts/setup_wsl_gpu.sh (once; 30–60 min, mostly compiling NATTEN for the GPU).~/.mixxx/mixxxdb.sqlite to the
top of the drive first. In Ubuntu, ls /mnt shows the drive letters.bash scripts/desktop_analyze.sh d (use the drive's letter; add --limit 20 for a first try)
analyzes the library on the GPU with allin1 (plus Raveform and CUE-DETR for comparison);
bash scripts/desktop_benchmark.sh d runs the benchmark. Both point the laptop's paths at the drive and write
hsb-analysis.json.gz to it; the benchmark also writes hsb-benchmark-desktop.html (timelines) and .json.
A drive plugged in after Ubuntu started isn't in /mnt: sudo mkdir -p /mnt/e && sudo mount -t drvfs E: /mnt/e.hsb-analysis.json.gz on the drive.The only code that writes to Mixxx's database is data/mixxx_cues.py, and only when you confirm a write:
mixxxdb.sqlite first, as mixxxdb.sqlite.hsb-backup-<timestamp> next to it. To undo, close
Mixxx and copy the backup back.◆ …, ids
remembered in the analysis DB) are ever replaced, and only with Replace cues from my earlier exports.
Mixxx's analyzer sets only an intro start and an outro end; by default the export fills in the missing
intro end / outro start and keeps yours (Settings → Phrase analysis turns that off). Complete markers are
left alone.Verified against this library (Mixxx 2.4, 2026-10-08): cues(id, track_id, type, position, length, hotcue, label, color); type 1 hot cue, 2 main cue, 4 loop, 6 intro, 7 outro, 8 audible sound; positions in stereo
samples (seconds × samplerate × 2); hotcue 0-based; colors 0xRRGGBB. On a copy of the library, detected drops
landed within 0.25 s of hot cues placed by hand.
mode=ro once per snapshot, copied via SQLite's backup API and
closed; all queries run on the copy. A test asserts the original file is byte-identical afterwards.
The one exception is cue export, which you ask for explicitly (see Cue export safety).analysis.sqlite, never in Mixxx's database.core/) has no Qt imports and is fully unit-tested.harmonic_set_builder/
core/ camelot, bpm, energy, ranking, route, setlist, settings, track,
theory (scales, shared notes, explanations), synth (reference audio), quiz (no Qt)
analysis/ grid (Mixxx beat grids), energy (per-bar features, builds), structure (built-in + allin1),
labels (DJ labels, cleanup), store (analysis.sqlite), pipeline, batch, transitions (no Qt)
data/ mixxx_db (locate/snapshot/read), mixxx_cues (cue export), store (autosave), export, config, paths
ui/ PySide6 models, views, energy graph, sources tree, controller, main window,
phrases (Phrases view), learn + wheel (Learn view)
cli.py the `hsb` command
tests/
Defaults from section 9 of the brief are used unless noted.
app.js, 2026-10-07; recorded in
tests/fixtures/camelotwheel_energy.json). That site scores relative major/minor and diagonals as 0,
±2 as ±3 and a semitone up (+7 wheel steps) as +2, so those replace the brief's table values.
Semitone down (not offered on the site) mirrors it at −2. All deltas are editable in Settings → Energy & moves.
Note: the site labels +7 wheel steps "Tritone Jump", but it is musically a semitone; this app's
Tritone is the real one (±6 steps) and is a clash.key_id 1–12 = C…B major, 13–24 = C…B minor;
library.location is a track_locations.id; Playlists.hidden 0 = normal, 1 = Auto DJ,
2 = history, −1 = internal placeholder (never shown).data/mixxx_cues.py (not mixxx/cues.py), and Mixxx reads
reuse data/mixxx_db.py (no separate mixxx/db.py).tests/test_analysis.py): allin1
found the boundaries but called the first half "intro", so intro only counts for a leading run that ends at
the first drop-level section, and outro only after the last drop; elsewhere energy and the kick decide.
A quiet section that ends the track after its last drop is the Outro even without a kick.tests/fixtures/real_tracks.json: per-bar features and allin1 output,
no audio), with the DJ's verdicts in the test names.--allow-while-mixxx-runs (CLI), and always runs at nice 10.hsb validate; label thresholds are constants at the
top of analysis/labels.py for tuning afterwards.