druppin/set-builder-companion

Python

0

0 commits

updated Oct 9, 2026

See the code

README

Harmonic Set Builder

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.

Run

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).

Using it

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.

Set Builder

  • Sources (right): click Library, a crate or a playlist to focus it. Right-click → Open as set loads a Mixxx playlist/crate as a new set (also works as an analyzer for existing sets).
  • Track table (middle): Mixxx's columns plus Move / Mood / Energy Δ / BPM Δ / Tier / ½·2× against the reference track (selected setlist row, else the last track). Default order is the suggestion ranking; Suggested order returns to it. Right-click the header to choose columns.
  • Filters (above the track table): ★ To be added only shows just the tracks you want to play; Filters ▾ adds In key with the reference track, BPM from the reference track (Safe only / Safe or Caution) and Only tracks with a key and BPM. They combine with search and "Hide tracks already in the set", the count shows how many tracks are visible, and the choice is remembered.
  • Setlist (left): drag tracks in, drag rows to reorder, Delete to remove, double-click a track to append. Red = key clash with the previous track, amber = duplicate. Undo/redo covers everything.
  • To be added: tracks you want to play later. Click a column header to sort it like the track table (right-click the header → Unsort to go back to the order you added them). A track leaves the list as soon as it's added to the set (by drag, double-click, filling a transition or a finished route) and comes back if it's removed from the set again, unless another copy is still in the set. Right-click (or ⋯) → Suggest where to place it or Show me how to get here, which opens a route of dashed transitional entries. Click the first one to see its candidates; drop or double-click a track to fill it.
  • Help me fix key mixing mistakes: shows + on red rows; click it to bridge the clash.
  • Energy graph (top): energy from Camelot moves, breaking at clashes; click a point to select the row.
  • Preview (bottom bar): click ▶ on any row, press Space in a table, or right-click → Preview. Click the same track again to pause; click the seek bar to jump, or use −10s / +10s. Tracks on an unmounted drive report "file not found". Uses Qt Multimedia (bundled with PySide6).
  • Cover art: the Art column shows each track's cover; hover it for a larger view. The preview bar and the "Compared with" line show it too. Covers come from Mixxx's recorded cover file, art embedded in the audio (MP3/ID3, FLAC, M4A) or a 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.
  • Set length: under the setlist, e.g. "14 tracks · ≈ 43:11 mixed (49:41 back to back)". The mixed estimate subtracts a 16-bar overlap per transition at the incoming tempo (Settings → Mixing → Mix overlap; 0 = back to back). With a route open it also shows the length including the transitions still to fill.
  • Sources: clicking the Crates / Playlists headings folds them; the focused source stays highlighted.
  • Structure column (set, track table, pool): each analyzed track's sections, e.g. I16 B8 D32 Br16 B8 D32 O16 (Intro, Build, Drop, Breakdown, Groove, Outro, lengths in bars). Fills in once tracks are analyzed in Phrases.

Phrases

Track structure: where the intro, builds, drops, breakdowns and outro are, from audio analysis.

  • The left-hand tree works like the Set Builder's Sources: Current set, Library, and foldable Crates and Playlists, each with "tracks · analyzed ✓" counts (right-click → Open as set works here too). Click one to list its tracks; search and the status filter (Not analyzed, Analyzed, File changed, Failed) narrow the list, and right-clicking the header hides columns. Then Analyze selected or Analyze all new (everything listed that isn't analyzed yet). Results are kept and only redone when a file changes (mtime + size) or on Re-analyze (right-click). Analysis runs in background processes at low priority; it's meant to run while Mixxx is closed, and asks first if Mixxx is open.
  • The plot shows the per-bar energy curve (loudness, kick/bass, highs, brightness and onset density, each normalized per track) and the kick/bass line, with the sections shaded. Click anywhere (or double-click a section) to play from there.
  • Transitions: for the selected track and its neighbours in the set — e.g. "Start B's 16-bar intro at A's outro (bar 97)" or "Drop B's drop on A's last breakdown" — plus phrase-compatibility warnings (intro shorter than the outro, phrase grids that won't line up).
  • Set energy flow: the set's tracks' energy curves laid end to end, overlapping at those mix points.
  • Analyzers: Built-in (default; a few seconds per track, tuned for dance music) or allin1 (All-In-One Music Structure Analyzer, a neural network; about 10 minutes per 3-minute track on this laptop's CPU). allin1 can't run on the app's Python, so it runs in its own environment (see below). Its raw output is cached, so labeling can be re-tuned without re-running it (hsb analyze --relabel).
  • Bars come from Mixxx's own beat grid when the track has one (decoded from library.beats), so section starts, transition points and exported cues sit on Mixxx's beats. Otherwise allin1's beats or detected beats.
  • Export cues to Mixxx…: writes each section start as a labeled, colored hot cue (◆ 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.

Learn

  • Explore & listen: click a key on the Camelot wheel; every key you can mix to lights up (green smooth, blue energy move). Click a move (or a second key) for why it works: which notes the two scales share and which change (e.g. "6 of 7 notes shared … Changes: F→F#"), how it feels, its energy, and a DJ tip. Hear this key plays its scale, home chord and a typical dance-music chord loop; Hear the move plays one key's chords then the other's, and Hear them blended plays both at once, so smooth moves sound consonant and clashes sound sour. Below, your own tracks in those keys: double-click to hear real music in the key.
  • Quiz ("guess first"): name the move, in key or clash, energy up or down, find the key (answer on the wheel), and ear training (listen to a blend, judge it). Questions can use pairs of your own tracks (preview them before answering). Every answer is explained the same way as Explore. Your per-move accuracy is kept, and Practise my weak moves more asks more about the moves you miss. Number keys answer, Enter goes on.

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.

Phrase analysis from the command line

.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.

Setting up allin1 (optional)

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).

Benchmark: which analyzer works on your music

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.

Comparing analyzers on a track

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.

Moving results between computers

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.

Running the heavy models on a Windows PC with an NVIDIA GPU (WSL2)

  1. Windows: Windows 10 21H2 or newer (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.
  2. Ubuntu: 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).
  3. Music: plug in the music drive. With Mixxx closed on the laptop, copy ~/.mixxx/mixxxdb.sqlite to the top of the drive first. In Ubuntu, ls /mnt shows the drive letters.
  4. Run: 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.
  5. Back on the laptop: Phrases view → Import… → hsb-analysis.json.gz on the drive.

Cue export safety

The only code that writes to Mixxx's database is data/mixxx_cues.py, and only when you confirm a write:

  1. It refuses while Mixxx is running (Mixxx caches tracks and would overwrite the cues).
  2. It backs up mixxxdb.sqlite first, as mixxxdb.sqlite.hsb-backup-<timestamp> next to it. To undo, close Mixxx and copy the backup back.
  3. Everything is written in one transaction and rolled back on any error (e.g. a slot that's no longer empty).
  4. Your cues are never changed: hot cues only go into empty slots (default slots 1–8, settable), a section that already has one of your hot cues within 0.25 s is skipped, and only cues this tool made (label ◆ …, 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.
  5. The dry run reads and computes only.

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.

Hard guarantees

  • Mixxx's database is opened with 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).
  • Phrase analysis results live in the app's own analysis.sqlite, never in Mixxx's database.
  • Pure logic (core/) has no Qt imports and is fully unit-tested.

Layout

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/

Decisions and deviations from the brief

Defaults from section 9 of the brief are used unless noted.

  • Energy deltas are calibrated to camelotwheel.org (read from its 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.
  • Diagonals count in all four directions: besides the brief's A n→B n+1 and B n→A n−1, the A n→B n−1 (Diagonal down (to major), "Settle + brighten") and B n→A n+1 (Diagonal up (to minor), "Lift + darken") moves share six of seven notes too and are Smooth, energy 0 as on camelotwheel.org.
  • Moves the site allows but the brief's table does not (±3 "parallel", +4 "related", A n→B n−3 "compatible tone") are off-key clashes here, per the brief.
  • Mixxx specifics confirmed against a real library: 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).
  • Crates have no order; imported crates use artist, title (Mixxx's default sort).
  • Fix mode shows the + in a narrow column on the red row itself rather than in the gap above it.
  • In slot view, a setlist row dragged onto the slot is always moved; the brief's exception ("unless Hide tracks already in the set is checked") was ambiguous and is not implemented.
  • Undo uses whole-set snapshots per command (sets are small), so every mutation is undoable uniformly.
  • A route that becomes unreachable stays open showing only the target placeholder, with a hint of which rule to relax in the status bar.
  • Not in v1 (per the brief): set-length target, live OSC link. The "guess first" learning mode is now the Learn view's quiz.

Phrase analysis spec: decisions

  • Module layout follows this repo: cue export is data/mixxx_cues.py (not mixxx/cues.py), and Mixxx reads reuse data/mixxx_db.py (no separate mixxx/db.py).
  • Two analyzers instead of allin1 only, since allin1 can't install on Python 3.14 and is slow on CPU. Both produce allin1's label vocabulary so one mapping turns them into DJ labels.
  • Label mapping tuned on allin1's real output for a synthetic dance track (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.
  • Neither analyzer labels build-ups, so they're derived. First choice: the 4–16-bar section right before a drop (its boundary comes from allin1 or the built-in novelty), if it's clearly quieter than the drop and something rises in it (onsets, highs or brightness). At the very start of a track it must also lose the kick/bass, so a kick intro that adds hats isn't mistaken for a build. Otherwise a build is looked for in the last 16/8/4 bars before the drop: two of the three rises, a falling or absent low end, a change where it starts (something comes in or the kick drops out), and a jump in energy or low end at the drop.
  • Drops are judged against the track itself: a section counts as a Drop only within 0.12 of the track's second-loudest section (so one peak section can't demote the rest). A loud opening groove is a Groove, not Drop 1 (JOYRYDE – GOT REAL). A big jump from a quieter section, measured on its loud bars because a drop can open with a few sparse ones, is a Drop (ACRAZE – Do It To It, second drop).
  • Fake drops: when the drop hits on a phrase line, pauses a bar or two and then really drops, the drop starts on the phrase line (My Nu Leng – Worldwide). When the tease is longer, a short "drop" between two builds, it's labeled a build too (kept visible as a part of the build).
  • Cooldown at the end: a final section that steps clearly down from the drop before it is a Groove (KRAZKA – ПЫХ ПЫХ); silence after the music ends doesn't count against the last section (Ship Wrek – Shake That).
  • A near-silent stretch before a drop is a breakdown, not a build; only the rising end is the build (K Motionz – Only You). A build may start a bar or two before the riser, when the kick fades early.
  • These cases are regression tests (tests/fixtures/real_tracks.json: per-bar features and allin1 output, no audio), with the DJ's verdicts in the test names.
  • Sections shorter than 4 bars (except builds) merge into a neighbour.
  • Boundaries the model finds inside a section (a 32-bar drop changing at bar 16) are kept as parts ("Drop 1", "Drop 1 b") instead of being merged away: on 10 hand-cued tracks, merging them dropped the share of the DJ's own hot cues found from 72% (allin1's raw boundaries) to 56%. Parts get hot cues after whole sections; the Structure column, intro/outro markers and transition tips use whole sections.
  • Boundaries snap to the nearest downbeat, then onto the 8-bar grid when within 1 bar (spec §3.5). The grid starts where the track's phrases do: a pickup bar before the first phrase (e.g. AC Slater – Bass Face) puts every phrase one bar after Mixxx's first downbeat, found from where the section changes fall.
  • Results record the labeling-rules version. When the rules change, Analyze all new re-labels older results from the saved model output in seconds (shown as outdated labels); allin1 isn't re-run, and an allin1 result isn't replaced just because Built-in is selected. Right-click → Re-label does it on demand, Re-analyze from scratch re-runs the selected analyzer.
  • "Nothing runs while Mixxx is open": cue export refuses outright; analysis asks (GUI) or needs --allow-while-mixxx-runs (CLI), and always runs at nice 10.
  • Hot cue colors come from Mixxx's default palette: Intro green, Build yellow, Drop red, Breakdown blue, Groove purple, Outro celeste. Default cap 8 slots; with more sections than free slots, Drops, Breakdowns and the Outro win.
  • The spec's milestone 1 (validate ~20 tracks by ear) is hsb validate; label thresholds are constants at the top of analysis/labels.py for tuning afterwards.

druppin/set-builder-companion

Python

0

0 commits

updated Oct 9, 2026

See the code

README

Harmonic Set Builder

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.

Run

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).

Using it

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.

Set Builder

  • Sources (right): click Library, a crate or a playlist to focus it. Right-click → Open as set loads a Mixxx playlist/crate as a new set (also works as an analyzer for existing sets).
  • Track table (middle): Mixxx's columns plus Move / Mood / Energy Δ / BPM Δ / Tier / ½·2× against the reference track (selected setlist row, else the last track). Default order is the suggestion ranking; Suggested order returns to it. Right-click the header to choose columns.
  • Filters (above the track table): ★ To be added only shows just the tracks you want to play; Filters ▾ adds In key with the reference track, BPM from the reference track (Safe only / Safe or Caution) and Only tracks with a key and BPM. They combine with search and "Hide tracks already in the set", the count shows how many tracks are visible, and the choice is remembered.
  • Setlist (left): drag tracks in, drag rows to reorder, Delete to remove, double-click a track to append. Red = key clash with the previous track, amber = duplicate. Undo/redo covers everything.
  • To be added: tracks you want to play later. Click a column header to sort it like the track table (right-click the header → Unsort to go back to the order you added them). A track leaves the list as soon as it's added to the set (by drag, double-click, filling a transition or a finished route) and comes back if it's removed from the set again, unless another copy is still in the set. Right-click (or ⋯) → Suggest where to place it or Show me how to get here, which opens a route of dashed transitional entries. Click the first one to see its candidates; drop or double-click a track to fill it.
  • Help me fix key mixing mistakes: shows + on red rows; click it to bridge the clash.
  • Energy graph (top): energy from Camelot moves, breaking at clashes; click a point to select the row.
  • Preview (bottom bar): click ▶ on any row, press Space in a table, or right-click → Preview. Click the same track again to pause; click the seek bar to jump, or use −10s / +10s. Tracks on an unmounted drive report "file not found". Uses Qt Multimedia (bundled with PySide6).
  • Cover art: the Art column shows each track's cover; hover it for a larger view. The preview bar and the "Compared with" line show it too. Covers come from Mixxx's recorded cover file, art embedded in the audio (MP3/ID3, FLAC, M4A) or a 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.
  • Set length: under the setlist, e.g. "14 tracks · ≈ 43:11 mixed (49:41 back to back)". The mixed estimate subtracts a 16-bar overlap per transition at the incoming tempo (Settings → Mixing → Mix overlap; 0 = back to back). With a route open it also shows the length including the transitions still to fill.
  • Sources: clicking the Crates / Playlists headings folds them; the focused source stays highlighted.
  • Structure column (set, track table, pool): each analyzed track's sections, e.g. I16 B8 D32 Br16 B8 D32 O16 (Intro, Build, Drop, Breakdown, Groove, Outro, lengths in bars). Fills in once tracks are analyzed in Phrases.

Phrases

Track structure: where the intro, builds, drops, breakdowns and outro are, from audio analysis.

  • The left-hand tree works like the Set Builder's Sources: Current set, Library, and foldable Crates and Playlists, each with "tracks · analyzed ✓" counts (right-click → Open as set works here too). Click one to list its tracks; search and the status filter (Not analyzed, Analyzed, File changed, Failed) narrow the list, and right-clicking the header hides columns. Then Analyze selected or Analyze all new (everything listed that isn't analyzed yet). Results are kept and only redone when a file changes (mtime + size) or on Re-analyze (right-click). Analysis runs in background processes at low priority; it's meant to run while Mixxx is closed, and asks first if Mixxx is open.
  • The plot shows the per-bar energy curve (loudness, kick/bass, highs, brightness and onset density, each normalized per track) and the kick/bass line, with the sections shaded. Click anywhere (or double-click a section) to play from there.
  • Transitions: for the selected track and its neighbours in the set — e.g. "Start B's 16-bar intro at A's outro (bar 97)" or "Drop B's drop on A's last breakdown" — plus phrase-compatibility warnings (intro shorter than the outro, phrase grids that won't line up).
  • Set energy flow: the set's tracks' energy curves laid end to end, overlapping at those mix points.
  • Analyzers: Built-in (default; a few seconds per track, tuned for dance music) or allin1 (All-In-One Music Structure Analyzer, a neural network; about 10 minutes per 3-minute track on this laptop's CPU). allin1 can't run on the app's Python, so it runs in its own environment (see below). Its raw output is cached, so labeling can be re-tuned without re-running it (hsb analyze --relabel).
  • Bars come from Mixxx's own beat grid when the track has one (decoded from library.beats), so section starts, transition points and exported cues sit on Mixxx's beats. Otherwise allin1's beats or detected beats.
  • Export cues to Mixxx…: writes each section start as a labeled, colored hot cue (◆ 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.

Learn

  • Explore & listen: click a key on the Camelot wheel; every key you can mix to lights up (green smooth, blue energy move). Click a move (or a second key) for why it works: which notes the two scales share and which change (e.g. "6 of 7 notes shared … Changes: F→F#"), how it feels, its energy, and a DJ tip. Hear this key plays its scale, home chord and a typical dance-music chord loop; Hear the move plays one key's chords then the other's, and Hear them blended plays both at once, so smooth moves sound consonant and clashes sound sour. Below, your own tracks in those keys: double-click to hear real music in the key.
  • Quiz ("guess first"): name the move, in key or clash, energy up or down, find the key (answer on the wheel), and ear training (listen to a blend, judge it). Questions can use pairs of your own tracks (preview them before answering). Every answer is explained the same way as Explore. Your per-move accuracy is kept, and Practise my weak moves more asks more about the moves you miss. Number keys answer, Enter goes on.

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.

Phrase analysis from the command line

.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.

Setting up allin1 (optional)

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).

Benchmark: which analyzer works on your music

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.

Comparing analyzers on a track

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.

Moving results between computers

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.

Running the heavy models on a Windows PC with an NVIDIA GPU (WSL2)

  1. Windows: Windows 10 21H2 or newer (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.
  2. Ubuntu: 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).
  3. Music: plug in the music drive. With Mixxx closed on the laptop, copy ~/.mixxx/mixxxdb.sqlite to the top of the drive first. In Ubuntu, ls /mnt shows the drive letters.
  4. Run: 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.
  5. Back on the laptop: Phrases view → Import… → hsb-analysis.json.gz on the drive.

Cue export safety

The only code that writes to Mixxx's database is data/mixxx_cues.py, and only when you confirm a write:

  1. It refuses while Mixxx is running (Mixxx caches tracks and would overwrite the cues).
  2. It backs up mixxxdb.sqlite first, as mixxxdb.sqlite.hsb-backup-<timestamp> next to it. To undo, close Mixxx and copy the backup back.
  3. Everything is written in one transaction and rolled back on any error (e.g. a slot that's no longer empty).
  4. Your cues are never changed: hot cues only go into empty slots (default slots 1–8, settable), a section that already has one of your hot cues within 0.25 s is skipped, and only cues this tool made (label ◆ …, 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.
  5. The dry run reads and computes only.

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.

Hard guarantees

  • Mixxx's database is opened with 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).
  • Phrase analysis results live in the app's own analysis.sqlite, never in Mixxx's database.
  • Pure logic (core/) has no Qt imports and is fully unit-tested.

Layout

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/

Decisions and deviations from the brief

Defaults from section 9 of the brief are used unless noted.

  • Energy deltas are calibrated to camelotwheel.org (read from its 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.
  • Diagonals count in all four directions: besides the brief's A n→B n+1 and B n→A n−1, the A n→B n−1 (Diagonal down (to major), "Settle + brighten") and B n→A n+1 (Diagonal up (to minor), "Lift + darken") moves share six of seven notes too and are Smooth, energy 0 as on camelotwheel.org.
  • Moves the site allows but the brief's table does not (±3 "parallel", +4 "related", A n→B n−3 "compatible tone") are off-key clashes here, per the brief.
  • Mixxx specifics confirmed against a real library: 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).
  • Crates have no order; imported crates use artist, title (Mixxx's default sort).
  • Fix mode shows the + in a narrow column on the red row itself rather than in the gap above it.
  • In slot view, a setlist row dragged onto the slot is always moved; the brief's exception ("unless Hide tracks already in the set is checked") was ambiguous and is not implemented.
  • Undo uses whole-set snapshots per command (sets are small), so every mutation is undoable uniformly.
  • A route that becomes unreachable stays open showing only the target placeholder, with a hint of which rule to relax in the status bar.
  • Not in v1 (per the brief): set-length target, live OSC link. The "guess first" learning mode is now the Learn view's quiz.

Phrase analysis spec: decisions

  • Module layout follows this repo: cue export is data/mixxx_cues.py (not mixxx/cues.py), and Mixxx reads reuse data/mixxx_db.py (no separate mixxx/db.py).
  • Two analyzers instead of allin1 only, since allin1 can't install on Python 3.14 and is slow on CPU. Both produce allin1's label vocabulary so one mapping turns them into DJ labels.
  • Label mapping tuned on allin1's real output for a synthetic dance track (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.
  • Neither analyzer labels build-ups, so they're derived. First choice: the 4–16-bar section right before a drop (its boundary comes from allin1 or the built-in novelty), if it's clearly quieter than the drop and something rises in it (onsets, highs or brightness). At the very start of a track it must also lose the kick/bass, so a kick intro that adds hats isn't mistaken for a build. Otherwise a build is looked for in the last 16/8/4 bars before the drop: two of the three rises, a falling or absent low end, a change where it starts (something comes in or the kick drops out), and a jump in energy or low end at the drop.
  • Drops are judged against the track itself: a section counts as a Drop only within 0.12 of the track's second-loudest section (so one peak section can't demote the rest). A loud opening groove is a Groove, not Drop 1 (JOYRYDE – GOT REAL). A big jump from a quieter section, measured on its loud bars because a drop can open with a few sparse ones, is a Drop (ACRAZE – Do It To It, second drop).
  • Fake drops: when the drop hits on a phrase line, pauses a bar or two and then really drops, the drop starts on the phrase line (My Nu Leng – Worldwide). When the tease is longer, a short "drop" between two builds, it's labeled a build too (kept visible as a part of the build).
  • Cooldown at the end: a final section that steps clearly down from the drop before it is a Groove (KRAZKA – ПЫХ ПЫХ); silence after the music ends doesn't count against the last section (Ship Wrek – Shake That).
  • A near-silent stretch before a drop is a breakdown, not a build; only the rising end is the build (K Motionz – Only You). A build may start a bar or two before the riser, when the kick fades early.
  • These cases are regression tests (tests/fixtures/real_tracks.json: per-bar features and allin1 output, no audio), with the DJ's verdicts in the test names.
  • Sections shorter than 4 bars (except builds) merge into a neighbour.
  • Boundaries the model finds inside a section (a 32-bar drop changing at bar 16) are kept as parts ("Drop 1", "Drop 1 b") instead of being merged away: on 10 hand-cued tracks, merging them dropped the share of the DJ's own hot cues found from 72% (allin1's raw boundaries) to 56%. Parts get hot cues after whole sections; the Structure column, intro/outro markers and transition tips use whole sections.
  • Boundaries snap to the nearest downbeat, then onto the 8-bar grid when within 1 bar (spec §3.5). The grid starts where the track's phrases do: a pickup bar before the first phrase (e.g. AC Slater – Bass Face) puts every phrase one bar after Mixxx's first downbeat, found from where the section changes fall.
  • Results record the labeling-rules version. When the rules change, Analyze all new re-labels older results from the saved model output in seconds (shown as outdated labels); allin1 isn't re-run, and an allin1 result isn't replaced just because Built-in is selected. Right-click → Re-label does it on demand, Re-analyze from scratch re-runs the selected analyzer.
  • "Nothing runs while Mixxx is open": cue export refuses outright; analysis asks (GUI) or needs --allow-while-mixxx-runs (CLI), and always runs at nice 10.
  • Hot cue colors come from Mixxx's default palette: Intro green, Build yellow, Drop red, Breakdown blue, Groove purple, Outro celeste. Default cap 8 slots; with more sections than free slots, Drops, Breakdowns and the Outro win.
  • The spec's milestone 1 (validate ~20 tracks by ear) is hsb validate; label thresholds are constants at the top of analysis/labels.py for tuning afterwards.