rekordbox-bass-notes

rekordbox bass notes — POC Plan

Goal

A local web app that reads the Rekordbox library database with pyrekordbox, lists tracks in a React UI, and lets the user edit a single track’s Comment field and save it back to the database — with a confirmation step before every write.

The ultimate goal this POC builds toward: analyze the audio of a track and append a bass-profile token to its comment, e.g. B:L5M7H9, where:

This plan covers the full pipeline: comment read/write plumbing and the audio analysis stage that produces the token.


Architecture

┌─────────────────┐     HTTP/JSON      ┌──────────────────────┐    pyrekordbox      ┌─────────────────┐
│  React (Vite)   │ ◄───────────────►  │  FastAPI (Python)    │ ◄────────────────► │  master.db      │
│  - track list   │   GET  /tracks     │  - read via ORM      │   SQLCipher        │  (Rekordbox 6/7)│
│  - analyze      │   POST /analyze    │  - audio analysis    │                    │                 │
│  - edit comment │   PUT  /comment    │  - write + commit()  │                    └─────────────────┘
│  - confirm modal│                    │  - auto backup       │
└─────────────────┘                    └──────────────────────┘

Two dev processes: Vite dev server (frontend) proxying /api → Uvicorn (backend).

End-to-end analysis flow

select track ──▶ "Analyze audio" ──▶ backend locates file (DjmdContent.FolderPath)
                                          │
                                          ▼
                              decode → mono, resample to 2 kHz
                                          │
                                          ▼
                        3× zero-phase Butterworth band-pass (log-spaced)
                          Low 20–39 Hz · Med 39–77 Hz · High 77–150 Hz
                                          │
                                          ▼
                per band: RMS → dBFS → digit 0–9 (per-band absolute dBFS scale)
                                          │
                                          ▼
                     token "B:l{dL}m{dM}h{dH}"  +  merged-comment preview
                                          │
                     user reviews band table + diff ──▶ confirm modal ──▶ PUT ──▶ commit()

Tech stack

Layer Choice Why
DB access pyrekordbox (Rekordbox6Database) Only library (any language) with sync-safe write support: handles the SQLCipher key, ORM over DjmdContent, and the usn / rb_local_usn bump Rekordbox needs on commit().
Audio DSP numpy, scipy, soundfile soundfile/libsndfile decodes (WAV/AIFF/FLAC/MP3/OGG); scipy.signal for resample_poly + Butterworth band-pass + RMS. (librosa dropped for packaging — see DISTRIBUTION.md 0.1.)
Backend FastAPI + Uvicorn Minimal, typed, proxy-friendly.
Frontend React + Vite + TypeScript Fast POC, simple dev proxy.
HTTP fetch + small hooks No React Query needed for a POC.

System dependency: none for the supported formats (libsndfile is bundled in the soundfile wheel). ffmpeg will be added later for M4A/AAC/ALAC.

Why pyrekordbox and not another language

master.db is SQLCipher-encrypted SQLite with a static, community-known key, so any SQLCipher binding (Rust rusqlite + bundled-sqlcipher, Go go-sqlcipher, C/C++ SQLCipher, Node better-sqlite3-multiple-ciphers) can technically read and write it. What those don’t give you:

  1. Key acquisition — pyrekordbox download-key fetches/caches it.
  2. Schema knowledge — table/column names, relationships, enum columns.
  3. Sync bookkeeping — per-row usn, library-wide rb_local_usn, updated_at, and agentRegistry/sequence rows. Skip it and Rekordbox may ignore, revert, or mis-sync the edit (especially with Rekordbox cloud/library sync enabled).

Other projects (rekordcrate in Rust, crate-digger in Java) target the USB export.pdb format, not the desktop master.db, and are read-focused.

If a non-Python production version is wanted later: make the write in pyrekordbox, diff master.db before/after commit() to capture the exact write footprint, then port just that.


Locked-in decisions

Decision Value
Band edges Log-spaced thirds of 20–150 Hz → 20.0 / 39.1 / 76.6 / 150.0 Hz (displayed rounded: 20 / 39 / 77 / 150). Configurable.
Per-band metric Zero-phase Butterworth band-pass (order 8, SOS) → RMS in dBFS (0 dBFS = full-scale).
dBFS → digit Per-band linear map, absolute (referenced to digital full scale, not track loudness). settings.dbfs_scale[band] = (min → digit 0, max → digit 9), clamped: digit = clamp(floor((dbfs − min) / (max − min) * 10), 0, 9). Calibrated from p5/p95 of a sample of ~117 local tracks: L −46→−18, M −23→−7, H −20→−9 (backend/calibrate.py). Frozen once tokens are written to the real DB — a recalibration must bump the preset letter (B → C).
Preset prefix B: — constant, configurable PRESET_LETTER.
Analysis window Whole track (RMS over full duration).
Token format B:l6m9h7 — uppercase preset prefix, lowercase band letters so the digits stand out.
Comment merge Prepend. Strip every existing B:l#m#h# token (matched in either letter case), collapse leftover whitespace, then put the new token at the front of the remaining comment (joined by comment_sep). action = replaced if any existed, else prepended.

Prerequisites (one-time)

Already set up in this repo: .venv/ (Python 3.11) with pyrekordbox 0.4.4, sqlcipher3-wheels 0.5.7, SQLAlchemy 2.0.52. The app opens your real Rekordbox library by default (auto-located). (Early development used a sample/master.db copy; that has since been removed — see the note under Build order.)

  1. Python env: use .venv (.venv/bin/python, .venv/bin/pip). Core deps: pip install pyrekordbox sqlcipher3-wheels. pyrekordbox needs an SQLCipher driver; sqlcipher3-wheels is a prebuilt one, so import sqlcipher3 works with no compiler. Only if that import fails, build from source: python -m pyrekordbox install-sqlcipher (clones into ./.tmp/; a trailing Could not remove temporary directory '.tmp' is harmless — rm -rf .tmp after).
  2. Database key: no manual step. pyrekordbox 0.4.4 resolves the Rekordbox 6/7 key automatically. This version has no download-key subcommand — python -m pyrekordbox --help shows only install-sqlcipher. If opening a DB ever raises a key/decrypt error, upgrade pyrekordbox or pass key=....
  3. Quit Rekordbox fully whenever the backend writes — the DB is otherwise locked; the write fails or leaves inconsistent state. The backend enforces this (409).
  4. Rekordbox version: pyrekordbox auto-locates master.db for v6 (~/Library/Pioneer/rekordbox/) or v7 (rekordbox7). /api/health reports the resolved path. To use a different master.db, browse to it in the header (or set REKORDBOX_DB_PATH).
  5. Audio decoding is soundfile/libsndfile (bundled) — no ffmpeg needed for WAV/AIFF/FLAC/MP3/OGG; M4A/AAC/ALAC not supported yet.

Backend

backend/config.py

Env-configurable: REKORDBOX_DB_PATH / USE_LIVE_LIBRARY, RESULT_LIMIT, AUDIO_SR (500), band_edges_hz (20/39.15/76.63/150), dbfs_scale per band via DBFS_{MIN,MAX}_{L,M,H} (see Locked-in decisions), PRESET_LETTER (B), FILTER_ORDER (8), COMMENT_SEP (" "), BACKUP_DIR, BACKUP_KEEP (20).

backend/db.py — pyrekordbox wrapper

backend/restore.py — CLI (no HTTP surface)

backend/analysis.py — audio analysis

backend/main.py — routes

Method Path Body Returns
GET /api/health – { version, db_path, db_kind (live|custom|none), detected_library_path, rekordbox_running, track_count, local_track_count } — none + nulls when no DB is open; polled by the UI every 5 s
GET /api/tracks?search=&limit= – [{ id, title, artist, album, genre, comment, folder_path, has_file }] — local-file tracks only
GET /api/tracks/{id} – single track (any id, incl. streaming)
GET /api/tracks/{id}/audio (Range) streams the local audio file (FileResponse, Accept-Ranges: bytes → seeking); 404 / 422 no local file
POST /api/tracks/{id}/analyze – { id, title, artist, audio_path, sample_rate, duration_sec, bands:[…], token, current_comment, proposed_comment, merge_action, existing_tokens } — no write; 404 / 422 (no local file) / 500
POST /api/tracks/analyze { ids: [...] } NDJSON stream, one line per track: {id, index, total, ok, …} (ok adds token/bands/proposed_comment/…; not-ok adds error). No write.
PUT /api/tracks/{id}/comment { token } xor { comment } { id, old_comment, new_comment, backup_path } — token → merge_token (prepend); guard (409) + WAL-checkpoint backup + commit()
PUT /api/tracks/comments { items: [{id, token\|comment}] } { backup_path, count, results:[{id, old_comment, new_comment}] } — atomic: one backup, one commit(), one quick_check; any unknown id → 404, nothing written; dup ids → 422
POST /api/db/switch { target: "live" } or { target: "custom", path } Reopen the shared RekordboxDB (auto-locate, or a chosen master.db) and persist to config.json. Returns fresh /api/health.
GET /api/backups – { backup_dir, live:{db_path,usn,track_count,tagged_count}, backups:[{name,taken,size,wal_size,shm_size,usn,track_count,tagged_count,recent,is_prerestore,error}] }
POST /api/backups/{name}/restore – Guarded (409 if Rekordbox open) → snapshot current DB → apply_restore → reopen. Returns {restored_from, prerestore_snapshot, …health}.

Frontend

Views

Two-column workspace: the track list at 70% width, a sticky 30% detail column on the right (stacks below on narrow screens).

  1. Track list (TrackTable.tsx) — a play/pause button then Title / Artist / Album / Genre / Comment, with a client-side search box and a checkbox per row (row click also toggles) plus a select-all-shown header box. Only tracks with a real local audio file are listed (list_tracks filters on has_file). 1a. Player (player.tsx context + PlayerPanel.tsx) — always shown at the top of the right column (idle state when nothing is playing): one shared <audio> streaming GET /api/tracks/{id}/audio (Range-enabled), a Web Audio AnalyserNode spectrum analyser on a <canvas> — log frequency axis, first three bars pinned to the colour-coded L/M/H sub-bass bands (fftSize 4096), the rest a log-spaced spectrum; the rAF loop reads the node through a ref — a seek bar with m:ss labels, play/pause, stop. Analysis cache (analysisCache.tsx): results are kept per track id for the life of the current library (cleared on DB switch / restore), so deselecting and reselecting a track shows its result immediately, and the single / batch flows share results both ways. AnalysisDetail.tsx renders the band table + token + CommentDiff.tsx, shared by both panels.
  2. Single panel (AnalyzePanel.tsx) — right column when exactly 1 row selected. Shows the cached AnalysisDetail if present (button reads Re-analyze), otherwise Analyze audio. Save to Rekordbox → ConfirmDialog.tsx (old → new) → PUT /…/comment (disabled when the comment already carries the token).
  3. Batch panel (BatchPanel.tsx) — right column when ≥2 rows selected. On selection it seeds the view from the cache; Analyze N streams only the not-yet-analyzed ids (done/total progress). Per-track accordion rows — click to expand the full AnalysisDetail. Save M (M = tracks whose comment changes) → BatchConfirmDialog.tsx → PUT /api/tracks/comments (one atomic write).
  4. Save buttons disable while Rekordbox is running; every write path shows the backend error (esp. “quit Rekordbox”). On success the panels call refetch so the table’s Comment column updates.

State

Dev proxy

vite.config.ts proxies /api → http://localhost:8000.


Project structure

writertest/
├── PLAN.md
├── README.md                 # setup + run steps
├── .gitignore
├── backend/
│   ├── requirements.txt       # fastapi, uvicorn, pyrekordbox, sqlcipher3-wheels,
│   │                          #   psutil, numpy, scipy, soundfile
│   ├── config.py              # env-configurable params
│   ├── db.py                  # pyrekordbox wrapper: reads, backup(), set_comment()
│   ├── analysis.py            # decode + band-pass + dBFS + token + merge (+ CLI)
│   ├── calibrate.py           # batch dBFS distribution report (CLI)
│   ├── restore.py             # list / restore master.db backups (CLI)
│   ├── inspect_db.py          # sanity-check a master.db copy (CLI)
│   ├── main.py                # FastAPI app + routes
│   └── backups/               # auto-written master.db backups (gitignored)
└── frontend/
    ├── package.json
    ├── vite.config.ts
    └── src/
        ├── App.tsx
        ├── api.ts   types.ts
        ├── analysisCache.tsx (Context: results by track id)
        ├── player.tsx        (Context: one <audio> + Web Audio AnalyserNode)
        ├── hooks/
        │   ├── useHealth.ts (5 s poll)   useTracks.ts   useBackups.ts
        │   ├── useAnalyze.ts             useUpdateComment.ts
        │   └── useBatchAnalyze.ts        useBatchUpdate.ts
        └── components/
            ├── TrackTable.tsx        AnalysisDetail.tsx (shared)
            ├── PlayerPanel.tsx       AnalyzePanel.tsx     ConfirmDialog.tsx
            ├── BatchPanel.tsx        BatchConfirmDialog.tsx
            ├── CommentDiff.tsx       DbSwitcher.tsx
            └── NoLibrary.tsx  RekordboxBanner.tsx  RestorePanel.tsx

(No CommentEditor.tsx — free-text comment editing was in the original sketch but the core loop is analyze → confirm → save; add it later if wanted.)


Build order

Steps 1–5 were developed and tested against a sample/master.db — a copy of the library. That copy was removed once the write path was proven; the app now defaults to the real library (auto-located), with a “use a different database…” control for a copy elsewhere. Historical references below are left as-is.

  1. Backend read-only — scaffold FastAPI, open_db(), GET /api/tracks + /api/health. Verify against a copy of master.db; confirm titles / artists / comments come through.
  2. Frontend read-only — Vite app, track table + search, wired to /api/tracks.
  3. Analysis module standalone ✅ — backend/analysis.py + CLI. Verified on synthetic tones and 6 real tracks. backend/calibrate.py added. Calibration done here (ahead of step 6): per-band dbfs_scale set from the p5/p95 of a 117-track batch — L −46→−18, M −23→−7, H −20→−9 — giving a full 0–9 digit spread on L and H (M stays top-heavy, matching the real distribution).
  4. POST /analyze + AnalyzePanel ✅ — full analyze-and-preview loop, no writes.
  5. Write path ✅ — PUT /api/tracks/{id}/comment (token merges via merge_token, comment replaces outright). RekordboxDB.set_comment: pre-flight rekordbox_running() guard (409) → WAL-checkpoint + .db/-wal/-shm backup to backend/backups/ → capture old Commnt → mutate → commit() (which re-checks Rekordbox itself; caught as a second 409 path). Confirmed by reading pyrekordbox’s source: plain attribute assignment is queued by Base.__setattr__ and commit() bumps the row’s rb_local_usn + the global agentRegistry USN before the SQL commit — no manual bookkeeping needed. Verified end-to-end: 422 (missing/both body fields), 404 (unknown id), 200 write with correct old/new/backup_path, persistence on a freshly reopened connection, agentRegistry.localUpdateCount +1 and rb_local_usn/updated_at bumped on the row, 409 via a monkeypatched guard (both at the RekordboxDB and FastAPI-route layers) with the comment left untouched. Tested against sample/master.db; a real pre-existing comment (a YouTube link) was incidentally overwritten mid-test and restored from the run’s own backup.
  6. Go live ✅ (mechanism) — dbfs_scale calibrated (step 3); the app now defaults to the real library (auto-located), so “go live” is just: quit Rekordbox, do one real edit, reopen Rekordbox, verify. The first real write on a user’s machine still hasn’t been done here.
  7. README ✅ — written; see also DISTRIBUTION.md.

Since step 7 the project has grown well past the original plan: batch analyze/write, runtime DB switching, backup/restore API + panel, human errors, a two-column layout, an analysis cache, an audio player, and Phase 0 of the macOS packaging effort (DISTRIBUTION.md).


Execution notes (model & reasoning)

Recommended settings for a Claude Code session executing this plan. Thinking keywords (think → think hard → think harder → ultrathink) raise the per-turn reasoning budget; /model switches model.

Phase / step Model Reasoning Why
Default for the project Sonnet 5 think (medium) Well-trodden FastAPI + React patterns.
Step 1 — backend read-only scaffold Sonnet 5 low–think Boilerplate.
Step 2 — frontend read-only Sonnet 5 low–think Component skeletons, dev proxy.
Step 3 — DSP module (analysis.py) Opus 5 or Sonnet 5 think hard Filter design, sosfiltfilt, RMS→dBFS reference, digit-clamp math — bugs are silent. Drop back to think once the standalone CLI produces sane values.
Step 4 — POST /analyze + AnalyzePanel Sonnet 5 think HTTP wiring around a proven module.
Step 5 — pyrekordbox write path Opus 5 or Sonnet 5 think hard Experimental commit(), usn / rb_local_usn bookkeeping, diff-inspecting the write footprint, verifying persistence across a DB reopen.
Step 6 — calibration Sonnet 5 think Iterative judgment on real audio; human in the loop.
Step 7 — README Sonnet 5 low Documentation.

Pattern: start each phase at the higher setting to establish structure, then lower it for fill-in. Opus fast mode is a good fit for steps 3 and 5 if available.


Database safety (implemented)

Still not covered: same-disk backups only (no off-machine copy); no cross-process lock (two writers, or two concurrent PUTs on the shared session, are unguarded — single-user POC assumption); Rekordbox cloud/library sync can touch master.db independently and isn’t detected (pause it during use).

Risks & caveats