Tone Sear
Drop in a song. It isolates the guitar, finds the sections where the guitar is playing, groups those sections by tone, records an excerpt of each one, measures what that tone is actually doing, and writes a NUX MG-300 MKII preset recipe for each distinct tone.
Runs entirely on your machine. Nothing is uploaded anywhere.
Run it with Docker (recommended)
cp .env.example .env
# put a secret in it: openssl rand -base64 32
docker compose up -d --build
Then open http://127.0.0.1:8000 and create an account.
That brings up two containers: the app and Postgres. Three named volumes keep
state across restarts and rebuilds β db-data (accounts and job records),
job-data (uploads, stems, clips, reports) and model-cache (the Demucs
weights, ~250 MB, downloaded on the first analysis rather than baked into the
image).
Torch is installed from the CPU-only wheel index, which keeps the image around 2 GB instead of 6+ GB. Analysis is CPU-bound either way.
SESSION_SECRET is the one required setting; compose refuses to start without
it, rather than booting with a random secret that would silently sign everyone
out on the next restart.
docker compose logs -f app # watch it work
docker compose down # stop, keep data
docker compose down -v # stop and delete accounts, jobs and weights
Or run it directly
./run.sh
First run creates a virtualenv and installs everything (a few minutes; Demucs pulls in PyTorch, around 2 GB). Then open http://127.0.0.1:8000.
With no DATABASE_URL set it uses a SQLite file in data/, so there is no
database server to install. Set SESSION_SECRET to keep sessions across
restarts.
To skip Demucs for a quick trial, install everything else and the app falls back to analysing the mix directly. It will say so, and it lowers its own confidence scores, because tone estimates taken through drums and vocals are worth less.
Requires Python 3.10+, ffmpeg on PATH (brew install ffmpeg). run.sh picks
the newest qualifying interpreter it can find, since a bare python3 is often
older than 3.10.
Accounts and sign-in
The app is behind a login. Two ways in:
- Google β one button, no password. Off until you configure it (below).
- Email and password β always available, so a fresh deployment is usable before you have an OAuth client, and stays usable if Google is unreachable.
Signing in with Google using an email that already has a password account links the two rather than creating a second account you cannot tell apart.
Each analysis belongs to the account that started it. Job ids are short and sit in URLs, so every job route is scoped to its owner; somebody else's job is a 404, not a 403. Your past analyses are listed on the front page and can be reopened or deleted.
Once your own account exists, set ALLOW_REGISTRATION=0 to close signups.
Turning on Google sign-in
Google Cloud Console β APIs & Services β Credentials β Create credentials β OAuth client ID β Web application.
Under Authorised redirect URIs add exactly:
http://127.0.0.1:8000/auth/google/callbackfor local usehttps://your-domain/auth/google/callbackin production
Put the client id and secret in
.env:GOOGLE_CLIENT_ID=...apps.googleusercontent.com GOOGLE_CLIENT_SECRET=...docker compose up -dagain.
The redirect URI has to match what Google has on file character for character.
Behind a proxy or a real domain, set BASE_URL=https://your-domain so the app
sends that instead of guessing from the request.
When Google refuses
| What Google says | What it means |
|---|---|
| 401: invalid_client β "The OAuth client was not found" | The client id reaching Google is not a real one. Either GOOGLE_CLIENT_ID is a placeholder, the variable never made it into the process, or the client was deleted. Check with curl -s localhost:8000/api/auth/config β {"google":true} only means something is set, not that it is valid. |
| 401: invalid_client β "Unauthorized" | The id is real but GOOGLE_CLIENT_SECRET is wrong. |
| 400: redirect_uri_mismatch | The callback the app sent is not on the client's authorised list. The error page shows the URI it tried; paste that exact string into the Google console, or set BASE_URL. |
| 403: access_denied | The client is in "Testing" mode and the account is not on the test-user list. Add it, or publish the client. |
| Button does not appear at all | Both GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET must be non-empty. Restart after editing .env β the values are read at startup. |
The credentials are read once when the process starts, so docker compose up -d
again (or restart run.sh) after changing them.
Configuration
| Variable | Default | Meaning |
|---|---|---|
SESSION_SECRET |
required in compose | Signs the session cookie. Changing it logs everyone out. |
DATABASE_URL |
SQLite in data/ |
e.g. postgresql+psycopg://user:pw@host/db |
GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET |
empty | Enables the Google button when both are set. |
BASE_URL |
guessed from request | Public origin, for the OAuth callback behind a proxy. |
COOKIE_SECURE |
0 |
Set to 1 when serving over HTTPS. |
ALLOW_REGISTRATION |
1 |
0 closes signups. |
JOBS_DIR / DATA_DIR |
jobs/, data/ |
Where audio and the SQLite file live. |
PORT |
8000 |
Published port. |
Serving this on the open internet: put it behind TLS and set
COOKIE_SECURE=1. Uploading audio and running Demucs is expensive and
unauthenticated users cannot do it, but any registered user can, so close
registration or keep it on a private network.
What it does, in order
- Separate β Demucs
htdemucs_6s, which has a dedicated guitar stem (most models only give you a catch-all "other"). - Find the guitar β an adaptive level threshold on the guitar stem, with short gaps closed and blips discarded.
- Group by tone β every 2 s window gets a timbre fingerprint (MFCCs plus centroid, flatness, bandwidth, zero-crossing rate, crest factor). Agglomerative clustering, with the number of tones chosen by silhouette score. One tone is a valid answer; it will not invent a second one to look busy. Fragments under 6 s or 5 % of the guitar time are dropped as transitions.
- Record β a representative excerpt of each tone group is written to
jobs/<id>/clips/tone_N.wavand is playable in the browser. - Measure β see below.
- Map β measurements become MG-300 MKII blocks, each with a stated reason and a confidence.
What it measures, and how
| Quantity | Method |
|---|---|
| Drive (0-100) | Crest factor, plus spectral valley depth on chords / high-frequency sustain on single notes, plus note decay rate. Calibrated against a physical string model rendered through a cab at five known gain settings. |
| Tone shape | Band energies in seven bands, expressed as dB deviation from a reference mic'd-cab curve, then centred so only the shape matters. |
| Compression | Crest factor, envelope spread, attack sharpness. Skipped when the amp is already saturating. |
| Delay | Periodic bumps in decay regions, i.e. after a struck note where nothing new is played. Cross-checked with autocorrelation inside the same region. Repeats that do not get quieter are rejected as the song's rhythm rather than an echo. |
| Reverb | RT60 from a fit to the late part of the decay, plus a diffuseness test: a reverb tail turns noise-like, a long sustaining note stays harmonic. Both are required. |
| Modulation | Amplitude modulation for tremolo, spectral-centroid sweep for phaser/wah, comb structure via cepstrum for flanger, pitch wobble for chorus/vibrato, stereo width for detune. Every one of these must be sinusoidal β harmonic content in the modulation spectrum means it is strumming, not an LFO. |
| Tempo / division | Beat tracking on the full mix; delay times are reported in ms and as a note division. |
Whole-mix autocorrelation is deliberately never trusted for delay: a delay synced to the tempo is mathematically indistinguishable from the rhythm of the part. The app would rather say nothing than guess.
Tweaking it: the radar
Every tone card carries a nine-axis radar. It starts where the analysis put it, and you drag it from there.
| Axis | In plain terms | Measured from | Moves |
|---|---|---|---|
| Drive | distortion, saturation | crest factor + spectral valley depth | AMP Gain, drive pedal choice |
| Low End | bass weight | 80-250 Hz balance | AMP Bass, EQ lows, cab |
| Brightness | presence, pick attack | 1.6-8 kHz balance + centroid | AMP Treble/Presence, cab |
| Mids | scooped to mid-forward | mid scoop in dB | AMP Middle, pedal voicing |
| Compression | sustain, evenness | crest factor + envelope spread | COMP |
| Reverb | dry to wet | RT60 + late tail energy | RVB |
| Delay | echo amount | repeat level + feedback | DLY |
| Modulation | chorus, phaser, tremolo | modulation depth | MOD |
| Wah | envelope filter | not measured - a taste control | EFX slot |
An axis does not turn a knob. It edits the measurement the knob came from, and the whole chain re-solves. Pull Drive down on a metal tone and you don't just get less gain - the amp becomes a Super Rvb, the drive pedal disappears, and the compressor switches on because the amp is no longer doing the squashing. Every block that moved gets outlined and tagged.
Consequences worth knowing:
- Leave the radar alone and you get the analysed preset byte for byte. The axes apply deltas, so an untouched axis changes nothing. Tested.
- The radar can never contradict the chain. Whether Reverb, Delay and Modulation read above zero is answered by asking the mapper, not by re-implementing its thresholds - an earlier version showed Delay at 95 next to a DLY block that was correctly switched off.
- Wah and the drive pedal are the same physical slot. Raise Wah past 10 and the overdrive is displaced; the UI says so rather than quietly dropping it.
- Drag with the mouse, or tab to a point and use arrow keys (shift for bigger steps, Home to reset that axis).
Validating against the actual pedal
The app can tell you what it measured. Only the MG-300 can tell you whether the preset gets there. Dial the preset in, reamp the excerpt through the pedal over USB, record the output, and drop it into the Validate panel: the capture goes through the same measurement pipeline, and you get a per-axis difference, a match score, and an instruction per block - "Bass is 14 too low", not "sounds thin". The captured tone is drawn on the same radar in green, so the shape of the mismatch is visible at a glance.
docs/VALIDATION.md has the wiring, and 16 test cases in three tiers: prove the loop is honest (null loop, level independence, repeatability), prove each axis moves the right thing and nothing else, then the real question - does it match the record, and does it converge when you follow the fixes.
What you get
- In the browser β one card per tone, the recorded excerpt, a timeline of where that tone appears, every block with its settings, and the reason each choice was made.
- HTML sheet β the same thing, printable, self-contained.
- Dial-in text β plain text, one line per block, for reading off a phone while you set the pedal up.
- presets.json β machine-readable, ready to become real preset files (below).
- Raw measurements β every number the decisions were made from.
Knob values are 0-100 to match the MG's display. Where a physical quantity was measured (delay ms, RT60, LFO Hz) the target is printed too β set the knob until the unit shows that value, because the knob-to-value curve differs per model.
QuickTone preset files
Each tone card has a QuickTone preset file button, which follows the radar if you have moved it. In QuickTone, click Import, pick the file and choose a slot β that slot is overwritten.
The file sets the amp, cab, drive, compressor, modulation, delay and reverb models, which of those blocks are on, all six amp knobs and the reverb level. The other effect knobs, the EQ and the gate keep the template's values, so set those from the settings sheet.
NUX does not publish the format. The layout in app/preset_format.py was worked
out by exporting one preset from QuickTone with a single control changed per file
and diffing the files. tests/test_preset_format.py checks that the encoder
reproduces those exports byte for byte; the exports live in
samples/reference_presets/, which is not committed, so those checks skip on a
fresh clone. Every file starts from app/catalog/mg300mk2_template.mg300MK2patch,
a genuine export, and only the bytes whose meaning is known are changed.
Model names and menu numbers in app/catalog/mg300mk2.json follow QuickTone
2.6.7's dropdowns. Six models on the pedal have no tonal profile yet β MLD,
I DELUXE RVB, I CLASS A30, I FIREMAN, MODERN DIST and RED COMP β and are never
picked automatically.
Honest limits
- Separation is the ceiling. Demucs guitar stems are good, not perfect. Doubled guitars panned hard, or a guitar sitting under a loud synth, will bleed. The confidence score already accounts for this; the raw measurements let you check.
- Two guitars playing simultaneously get analysed as one tone. Clustering splits by time, not by source. Overlapping parts average together.
- Drive on solo single notes is less certain than on chords β fewer partials means fewer places for saturation to show up. Reflected in the confidence.
- Modulation locked to the strumming rhythm (a tremolo at exactly twice the strum rate) is not separable from picking dynamics. The app stays silent rather than guessing.
- Long, high-feedback delays in dense playing are approximate; 100-500 ms measures well.
- The amp catalogue in
app/catalog/mg300mk2.jsonis an assumption. Model names follow NUX's MG-series naming; verify against your firmware's list and edit that file if anything differs. Voicing descriptors (brightness, mid shape, gain window) are hand-authored judgements β tune them and the amp choices move with them. Nothing is hard-coded.
Tests
python3 tests/test_detectors.py
python3 tests/test_perceptual.py
python3 tests/test_auth.py
136 checks in total. The first suite synthesises guitar with known effect settings and checks the detectors recover them: five gain stages ranked correctly, tremolo rates within 0.4 Hz, delay times within 12 %, RT60 within 45 %, and β the part that matters most β dry signals must not produce phantom effects, swept across every gain stage rather than a couple of samples.
The second suite covers the radar and the validation maths: untouched axes reproduce the preset exactly, each axis moves in the direction it claims, no axis disturbs a block it has no business touching (the software rehearsal of hardware tests T4-T12), and a deliberately wrong capture is scored down and diagnosed correctly β so the scoring is proven before anything is plugged in.
The third suite is about access, not audio. The interesting property is not that login works but that a signed-in user cannot reach somebody else's analysis: every job route β status, clips, downloads, radar adjust, validate, delete β is checked from a second account and has to answer 404. It runs against a throwaway SQLite database and needs no server.
Layout
app/dsp.py spectra, dynamics, drive scoring
app/timefx.py delay, reverb, modulation detection
app/sections.py guitar activity, tone clustering, clip rendering
app/mapper.py measurements -> MG-300 MKII blocks (all the rules live here)
app/perceptual.py the nine radar axes, forward and inverse
app/validate.py compare a capture of the pedal against the target
app/catalog/ the device model list - edit freely
app/pipeline.py orchestration
app/report.py HTML and text output
app/main.py web server, job ownership
app/auth.py Google OAuth + password accounts, sessions
app/db.py engine and session handling
app/models.py User and Job tables
web/index.html the whole UI
web/login.html the login / sign-up page
Dockerfile CPU-only torch image
docker-compose.yml app + Postgres + volumes
.env.example every setting, commented
docs/VALIDATION.md hardware wiring + 16 test cases
tests/ synthetic ground truth


