- Python 82.4%
- JavaScript 13.3%
- HTML 2.3%
- CSS 2%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| .claude | ||
| .github/workflows | ||
| captures | ||
| config | ||
| patches | ||
| synths | ||
| web | ||
| .DS_Store | ||
| .gitattributes | ||
| .gitignore | ||
| app.py | ||
| app.spec | ||
| audio.py | ||
| audio_io.py | ||
| Behringer_UB-Xa.py | ||
| best_patch.json | ||
| BUILD.md | ||
| clip.py | ||
| coach.py | ||
| cz101_sysex.py | ||
| dsp.py | ||
| dx7_bringup.py | ||
| dx7_selfmatch.py | ||
| dx7_sysex.py | ||
| dx7_sysex_verify.py | ||
| features.py | ||
| library.py | ||
| main.py | ||
| match.py | ||
| match_cz_hw.py | ||
| match_dx7_algsweep.py | ||
| match_dx7_hw.py | ||
| matcher.py | ||
| optimizer.py | ||
| patch_model.py | ||
| PATH_B.md | ||
| pluginhost.py | ||
| port_cz_to_virtualcz.py | ||
| re_tools.py | ||
| README.md | ||
| requirements.txt | ||
| server.py | ||
| synth_interface.py | ||
| target.py | ||
| ubxa_sysex.py | ||
synth-matcher
Automatic patch programming for the Behringer UB-Xa by closed-loop optimization. Give it an isolated target sound; it searches the synth's parameter space (driving the real hardware over MIDI and recording the result back) until the synth produces something that matches.
This is a working scaffold, not a finished tool. It is structured so you can
hand it to an agent (Claude Code) and have it flesh out the TODOs, but every
file already has runnable bones.
Why the UB-Xa and not the Model D
The Model D (like a real Minimoog) has analog front-panel knobs that are not addressable over MIDI. The computer can play notes but cannot set the patch, so the optimization loop can't close. The UB-Xa exposes NRPN/CC control of all parameters plus bulk load/save, which is exactly what this needs.
The architecture is deliberately split so the only synth-specific file is
synth_interface.py. To target the DX7 / CZ-1000 / D-50 later, you swap that
file for a SysEx-based implementation and keep everything else.
How the loop works
target.wav ──► extract features ──┐
▼
┌─────────────────────────────────────────┐
│ optimizer proposes a parameter vector x │ ◄──┐
└─────────────────────────────────────────┘ │
│ │
decode x → patch (param dict) │
│ │
synth_interface: set patch over NRPN │
│ │
audio_io: note-on, record, note-off │
│ │
features: spectral distance(target, recorded) │
│ │
loss ─────────────────────────────┘
Each evaluation involves real-time audio capture, so it is slow (~2–4 s per candidate). Budget accordingly: a CMA-ES run of popsize 16 × 60 generations is ~1000 evals ≈ 45–70 min. This is normal. Optimize for few, informative evaluations, not a fast inner loop.
What you need
- A UB-Xa connected by USB (or DIN MIDI via an interface).
- An audio interface capturing the UB-Xa's output into the same machine.
- Python 3.10+.
pip install -r requirements.txt
First-run checklist (do not skip; these are the things that quietly ruin matches)
- Run the UB-Xa auto-calibration before a session. Analog drift means an un-calibrated synth won't reproduce a patch consistently.
- Fixed note & velocity. Pick one pitch (e.g. C3 / MIDI 48) and one velocity for both target capture and search. Polyphony and chords come later.
- Loudness-normalize target and recordings before comparing, or the search
matches volume instead of timbre. (Handled in
features.py.) - Single sustained note, no effects/reverb in the target. If the target was recorded with reverb or layering, there may be no patch that matches and the optimizer will chase ghosts.
- Expect a noise floor. The same patch never renders bit-identically twice on analog hardware, so the loss won't reach zero. Average 2 takes per candidate if the floor is noisy (costs 2× time).
Build order (suggested for the agent)
config/ubxa_params.py: complete the NRPN map from the UB-Xa manual's MIDI implementation chart. The starter map here is partial and unverified.synth_interface.py: confirm NRPN send works: set cutoff to 0 vs max by ear.audio_io.py: confirm a note triggers and records cleanly; check latency/trim.features.py: sanity-check the loss: identical files → ~0, different → larger.optimizer.py+main.py: run end to end on an easy target first (a patch you made on the UB-Xa yourself, so you know a perfect match exists).
Validate on a known-reachable target before trying real-world samples. If the optimizer can't recover a patch the synth itself just made, the bug is in the pipeline, not the search.
Known hard parts (flagged for honesty)
- Discrete parameters (waveform select, osc sync on/off, filter mode) are categorical, not continuous. The scaffold rounds them from the continuous vector, which is crude. Consider a mixed search (CMA-ES for continuous + occasional discrete mutation) if results plateau.
- Parameter interactions (resonance × cutoff × envelope) make the landscape non-convex. CMA-ES handles this better than naive hill-climbing; a genetic algorithm is a reasonable alternative.
- Recording alignment. Trim the attack consistently; a few ms of offset
changes the spectrogram.
audio_iotrims leading silence by threshold.