• Python 82.4%
  • JavaScript 13.3%
  • HTML 2.3%
  • CSS 2%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Casuallynoted df723913ef oij
2026-06-18 21:50:50 -04:00
.claude 489 2026-06-18 11:16:41 -04:00
.github/workflows 486 2026-06-18 11:53:40 -04:00
captures Synth Patch Matcher — multi-synth closed-loop patch matcher 2026-06-18 10:15:26 -04:00
config asdf 2026-06-18 20:51:56 -04:00
patches oij 2026-06-18 21:50:50 -04:00
synths poiu 2026-06-18 21:44:58 -04:00
web asdf 2026-06-18 20:51:56 -04:00
.DS_Store oij 2026-06-18 21:50:50 -04:00
.gitattributes Synth Patch Matcher — multi-synth closed-loop patch matcher 2026-06-18 10:15:26 -04:00
.gitignore 486 2026-06-18 11:53:40 -04:00
app.py 486 2026-06-18 11:53:40 -04:00
app.spec 486 2026-06-18 11:53:40 -04:00
audio.py asdf 2026-06-18 20:51:56 -04:00
audio_io.py Synth Patch Matcher — multi-synth closed-loop patch matcher 2026-06-18 10:15:26 -04:00
Behringer_UB-Xa.py Synth Patch Matcher — multi-synth closed-loop patch matcher 2026-06-18 10:15:26 -04:00
best_patch.json Synth Patch Matcher — multi-synth closed-loop patch matcher 2026-06-18 10:15:26 -04:00
BUILD.md 456 2026-06-18 11:38:35 -04:00
clip.py 464 2026-06-18 14:08:54 -04:00
coach.py Adaptive floor 2026-06-18 12:30:44 -04:00
cz101_sysex.py asdf 2026-06-18 20:51:56 -04:00
dsp.py Synth Patch Matcher — multi-synth closed-loop patch matcher 2026-06-18 10:15:26 -04:00
dx7_bringup.py asdf 2026-06-18 20:51:56 -04:00
dx7_selfmatch.py asdf 2026-06-18 20:51:56 -04:00
dx7_sysex.py asdf 2026-06-18 20:51:56 -04:00
dx7_sysex_verify.py asdf 2026-06-18 20:51:56 -04:00
features.py 454 2026-06-18 13:27:51 -04:00
library.py 474 2026-06-18 14:38:59 -04:00
main.py 456 2026-06-18 11:38:35 -04:00
match.py asdf 2026-06-18 20:51:56 -04:00
match_cz_hw.py asdf 2026-06-18 20:51:56 -04:00
match_dx7_algsweep.py asdf 2026-06-18 20:51:56 -04:00
match_dx7_hw.py asdf 2026-06-18 20:51:56 -04:00
matcher.py 454 2026-06-18 13:23:34 -04:00
optimizer.py Synth Patch Matcher — multi-synth closed-loop patch matcher 2026-06-18 10:15:26 -04:00
patch_model.py Synth Patch Matcher — multi-synth closed-loop patch matcher 2026-06-18 10:15:26 -04:00
PATH_B.md 456 2026-06-18 11:38:35 -04:00
pluginhost.py 486 2026-06-18 11:53:40 -04:00
port_cz_to_virtualcz.py asdf 2026-06-18 20:51:56 -04:00
re_tools.py Synth Patch Matcher — multi-synth closed-loop patch matcher 2026-06-18 10:15:26 -04:00
README.md 456 2026-06-18 11:38:35 -04:00
requirements.txt Synth Patch Matcher — multi-synth closed-loop patch matcher 2026-06-18 10:15:26 -04:00
server.py asdf 2026-06-18 20:51:56 -04:00
synth_interface.py Synth Patch Matcher — multi-synth closed-loop patch matcher 2026-06-18 10:15:26 -04:00
target.py Synth Patch Matcher — multi-synth closed-loop patch matcher 2026-06-18 10:15:26 -04:00
ubxa_sysex.py Synth Patch Matcher — multi-synth closed-loop patch matcher 2026-06-18 10:15:26 -04:00

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 (~24 s per candidate). Budget accordingly: a CMA-ES run of popsize 16 × 60 generations is ~1000 evals ≈ 4570 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)

  1. Run the UB-Xa auto-calibration before a session. Analog drift means an un-calibrated synth won't reproduce a patch consistently.
  2. 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.
  3. Loudness-normalize target and recordings before comparing, or the search matches volume instead of timbre. (Handled in features.py.)
  4. 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.
  5. 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)

  1. 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.
  2. synth_interface.py: confirm NRPN send works: set cutoff to 0 vs max by ear.
  3. audio_io.py: confirm a note triggers and records cleanly; check latency/trim.
  4. features.py: sanity-check the loss: identical files → ~0, different → larger.
  5. 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_io trims leading silence by threshold.

About

Semi-automated synth patch generation, using sample matching

Releases

Topics

Metadata

230 KiB

Languages

  • Python 82.4%
  • JavaScript 13.3%
  • HTML 2.3%
  • CSS 2%