Tool: read an ear-tweaked patch back into its factory C++ source #99

Closed
opened 2026-09-22 23:30:59 +00:00 by bcox · 0 comments
Owner

Factory patches are source and blobs are outputs: patches/*.h is
authoritative, //patches:blobs bakes the .tymp files at build time, and
editing a factory patch in the editor and saving over its installed file
loses the edit at the next bazel run //patches:install. That is what
"factory" means and it should stay that way.

But tuning a patch by ear is the only way some of these get good, and the
editor is where that happens. We need a path from "I nudged this until it
sounded right"
back into the C++ that generates it.

The workflow

  1. bazel run //patches:install, open the patch in the editor, tweak by ear,
    save the result somewhere that is not the install directory.
  2. Run a tool that compares the saved .tymp against the factory patch it
    came from and reports what changed, in the vocabulary of the source
    rather than of the file format.
  3. Apply those changes to patches/<name>.h — by hand, or with the tool's
    help for the subset where that is safe.

Step 2 is the ticket. Step 3 may be manual forever and that is acceptable.

The tension that shapes the design

patches/*.h is documentation as much as it is code. These headers carry
the reasoning: why a clarinet's bore is sealed at the reed end, what the
Chowning index law is doing, which of crash.h's numbers came from a
listening spike and how they were measured. A tool that regenerates a patch
header from a blob would produce correct numbers and destroy everything that
makes the file worth reading.

So the deliverable is a diff reporter, not a code generator. Something
that says "decay_s moved from 3.0 to 3.4, set at crash.h:88" is worth far
more than something that emits a new crash.h. If a later version can
surgically rewrite a single literal in place, good — but regenerating the
file is the wrong goal, not a stretch goal.

What makes this harder than a field-by-field diff

The source's parameters sit upstream of the blob's. A patch header
authors a handful of numbers that materials/ expands into mode tables, that
SolveWaveguideConfig turns into line lengths, that the generators expand
into coefficients. That map is not invertible. If someone edits a mode
directly in the editor, there may be no change to a MaterialSpec that
produces it.

The tool must therefore be able to say "this change is not expressible in
the source's vocabulary"
rather than quietly writing a literal that
bypasses the generator. Three outcomes per changed field:

  • expressible — here is the authored value and where it is set;
  • derived — this field comes from a generator; here is the upstream input
    that would have to move, and by roughly how much, if that can be solved;
  • not expressible — this edit cannot be represented in the source as
    written, and either the patch needs a new authored parameter or the edit
    needs redoing upstream.

The third outcome is information, not a failure. Knowing that an ear-tweak
wanted something the patch's own vocabulary cannot say is a useful finding
about the patch.

Round-tripping must be idempotent. A value authored as a double is
quantized on the way into the blob; read back it is the quantized value.
Writing that back into the source and rebuilding must produce the same blob,
or every tweaking session ratchets the numbers. Whatever the tool reports has
to be a fixed point under apply-and-rebuild, and that is the property to
test first, by round-tripping every factory patch with no edits at all and
asserting that nothing is reported.

Goldens move. plugin/render_golden_test.cc pins 1024 samples and its
header records every re-blessing and why. A patch tweak that reaches the
golden patch is a deliberate re-blessing with a written reason, not a
by-product of an ear session.

Identifying which patch a file came from

project::Provenance already carries a factory patch's name and an FNV-1a
hash of its PTCH bytes, and provenance.h computes it (Fnv1a64,
PatchHash). A saved patch that started as a factory patch can therefore
name its origin, which is how the tool knows what to diff against without
being told. If an edited file carries no usable provenance, asking for the
name on the command line is a fine fallback.

Shape

A //tools: binary beside patch_editor and resynth, something like:

bazel run //tools:patch_diff -- --against crash ~/tweaks/crash.tymp

printing a human-readable report, with a machine-readable mode if it ever
needs one. Whether it can also --apply the expressible subset is a second
question, and a good reason to keep the report format stable and dull.

Open questions

  • Does the editor preserve everything the comparison needs — layer order,
    slot indices, asset identity — or does a save-and-reload already perturb
    things the diff would then report as changes?
  • Should the tool diff against the built blob (what the editor loaded) or
    against a freshly built one? They should be identical; if they are not,
    that is its own bug and the tool is a good place to notice it.
  • Is there value in the reverse direction — a mode that takes a factory patch
    and reports which of its authored numbers nothing downstream reads?
    Adjacent, cheap once the machinery exists, and it would find dead patch
    parameters.

Related: #97.

Factory patches are source and blobs are outputs: `patches/*.h` is authoritative, `//patches:blobs` bakes the `.tymp` files at build time, and editing a factory patch in the editor and saving over its installed file loses the edit at the next `bazel run //patches:install`. That is what "factory" means and it should stay that way. But tuning a patch by ear is the only way some of these get good, and the editor is where that happens. We need a path from *"I nudged this until it sounded right"* back into the C++ that generates it. ## The workflow 1. `bazel run //patches:install`, open the patch in the editor, tweak by ear, save the result somewhere that is not the install directory. 2. Run a tool that compares the saved `.tymp` against the factory patch it came from and reports what changed, in the vocabulary of the **source** rather than of the file format. 3. Apply those changes to `patches/<name>.h` — by hand, or with the tool's help for the subset where that is safe. Step 2 is the ticket. Step 3 may be manual forever and that is acceptable. ## The tension that shapes the design **`patches/*.h` is documentation as much as it is code.** These headers carry the reasoning: why a clarinet's bore is sealed at the reed end, what the Chowning index law is doing, which of `crash.h`'s numbers came from a listening spike and how they were measured. A tool that regenerates a patch header from a blob would produce correct numbers and destroy everything that makes the file worth reading. So the deliverable is **a diff reporter, not a code generator.** Something that says "`decay_s` moved from 3.0 to 3.4, set at `crash.h:88`" is worth far more than something that emits a new `crash.h`. If a later version can surgically rewrite a single literal in place, good — but regenerating the file is the wrong goal, not a stretch goal. ## What makes this harder than a field-by-field diff **The source's parameters sit upstream of the blob's.** A patch header authors a handful of numbers that `materials/` expands into mode tables, that `SolveWaveguideConfig` turns into line lengths, that the generators expand into coefficients. That map is not invertible. If someone edits a mode directly in the editor, there may be no change to a `MaterialSpec` that produces it. The tool must therefore be able to say **"this change is not expressible in the source's vocabulary"** rather than quietly writing a literal that bypasses the generator. Three outcomes per changed field: - *expressible* — here is the authored value and where it is set; - *derived* — this field comes from a generator; here is the upstream input that would have to move, and by roughly how much, if that can be solved; - *not expressible* — this edit cannot be represented in the source as written, and either the patch needs a new authored parameter or the edit needs redoing upstream. The third outcome is information, not a failure. Knowing that an ear-tweak wanted something the patch's own vocabulary cannot say is a useful finding about the patch. **Round-tripping must be idempotent.** A value authored as a `double` is quantized on the way into the blob; read back it is the quantized value. Writing that back into the source and rebuilding must produce the same blob, or every tweaking session ratchets the numbers. Whatever the tool reports has to be a fixed point under apply-and-rebuild, and that is the property to test first, by round-tripping every factory patch with no edits at all and asserting that nothing is reported. **Goldens move.** `plugin/render_golden_test.cc` pins 1024 samples and its header records every re-blessing and why. A patch tweak that reaches the golden patch is a deliberate re-blessing with a written reason, not a by-product of an ear session. ## Identifying which patch a file came from `project::Provenance` already carries a factory patch's name and an FNV-1a hash of its PTCH bytes, and `provenance.h` computes it (`Fnv1a64`, `PatchHash`). A saved patch that started as a factory patch can therefore name its origin, which is how the tool knows what to diff against without being told. If an edited file carries no usable provenance, asking for the name on the command line is a fine fallback. ## Shape A `//tools:` binary beside `patch_editor` and `resynth`, something like: ```sh bazel run //tools:patch_diff -- --against crash ~/tweaks/crash.tymp ``` printing a human-readable report, with a machine-readable mode if it ever needs one. Whether it can also `--apply` the expressible subset is a second question, and a good reason to keep the report format stable and dull. ## Open questions - Does the editor preserve everything the comparison needs — layer order, slot indices, asset identity — or does a save-and-reload already perturb things the diff would then report as changes? - Should the tool diff against the **built blob** (what the editor loaded) or against a freshly built one? They should be identical; if they are not, that is its own bug and the tool is a good place to notice it. - Is there value in the reverse direction — a mode that takes a factory patch and reports which of its authored numbers *nothing downstream reads*? Adjacent, cheap once the machinery exists, and it would find dead patch parameters. Related: #97.
bcox closed this issue 2026-09-29 23:41:23 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
bcox/tymbal#99
No description provided.