Solitito — guitar chord and note recognition
Recognition model for
Solitito, a
real-time guitar trainer written in Rust. Code, training scripts and
documentation are in the GitHub repository — this one holds the two binary
artifacts the app needs.
| file | |
|---|
best_model_v2_take6_onset.onnx | the model, four heads, 29 MB — what the app loads if it is there |
best_model_v2_take6.onnx | the same three-head model as before, kept because it still works |
dsp_weights.json | pseudo-CQT kernel in sparse format, 2 MB |
The two models share the first three outputs, name for name and weight for
weight: the fourth head was trained with the rest of the network frozen. An app
built against the older file reads the newer one without noticing.
What it does
Takes 48 frames of features (144 CQT bins + 12 chroma + 12 bass energy, 16 kHz,
256-sample hop — 0.77 s of audio) and returns four heads:
| output | shape | meaning |
|---|
root_logits | 13 | 12 pitch classes + "Noise" |
quality_logits | 11 | maj, min, maj7, dom7, min7, m7b5, dim7, aug, sus, note, N |
pitch_logits | 12 | sigmoid — which pitch classes are sounding |
onset_logits | 12 | sigmoid — which pitch classes were STRUCK in the last 6 frames |
The fourth head answers a question the other three cannot. "Sounding" includes a
string ringing on, one resonating in sympathy, and the note before — all of them
true and none of them played just now. It is a separate head rather than a
threshold on the pitch output because the two disagree on purpose: measured on a
recording of single notes, two thirds of the frames where the pitch head reports
a class other than the one being played carry no attack at all.
CNN with Squeeze-and-Excitation blocks, then a Transformer encoder with a CLS
token. 7.3M parameters, CPU inference.
Results
Measured on a validation split grouped by source recording, with solo tracks
excluded:
| metric | |
|---|
| root accuracy | 98.1% |
| pitch F1 | 0.909 |
| exact match (root and quality) | 92.4% |
| onset F1 | 0.812 |
The first three are unchanged between the two files — the onset head was trained
on its own with everything else frozen, so they are bit-for-bit what they were.
The onset figure is at the threshold that maximised F1 on the validation split;
it is honest about WHEN (202 ms after the strike against 676 ms for the other
paths) and less so about WHICH string, since an attack spreads onto its
neighbours.
Training data
Two sources: a synthetic set rendered through
NAM
amp models with exact labels, and
GuitarSet for
real playing.
Getting GuitarSet right took four runs. Half of it is _solo — monophonic
improvisation carrying the accompaniment's chord annotation — and its
instructed chord labels contain no maj7 or min7 at all, while calling 500
segments m that were played as m7. Fixing those two moved exact match from
44.8% to 92.4%. The details, with numbers, are in the GitHub README.
Using it
Put the model and dsp_weights.json next to the Solitito binary. The app
prefers best_model_v2_take6_onset.onnx and falls back to the three-head file,
so either one on its own is enough; SOLITITO_MODEL overrides the choice.
./solitito --check verifies that what is there loads.
Feeding this model your own features requires matching the DSP exactly — the
input is not raw audio. dist/gen_weights.py in the GitHub repository produces
the kernel, and dsp_weights.json here is its output.
License
MIT. GuitarSet is CC BY 4.0 — Qingyang Xi, Rachel M. Bittner, Johan Pauwels,
Xuzhou Ye & Juan P. Bello,
https://guitarset.weebly.com/.