0
mirror of https://github.com/torvalds/GuitarPedal.git synced 2026-08-18 13:13:35 +00:00
Files
torvalds-GuitarPedal/Firmware/bindings.h
Linus Torvalds 0c1b9c3db3 Split Software/ into the four things it actually was
'Software' was the directory everything that was not KiCad ended up in,
which stopped describing anything a while ago - Validation and the web
app are software too.  Worse, it put the shared parts inside the
firmware, where they read as the firmware's own.

They are not.  Effects/ has three consumers built from it: the firmware,
Validation's bench, and the web app's controls, all generated from the
same POT: comments by gen_effects.py.  Audio/ has two - the bench
compiles the same biquads, the same envelope followers and the same
single_sample(), which is the whole reason a measurement on a
workstation says anything about the pedal.  Neither belongs under
Firmware/, so neither is under it any more:

  Effects/    one file per effect
  Audio/      the DSP they are built from, and the audio loop
  Firmware/   the rest of what runs on the pedal, and the submodules
  WebMIDI/    the web app
  scripts/    what the build runs
  Validation/ unchanged
  Hardware/, Documentation/, Images/

CMakeLists.txt and the wrapper Makefile move to the top with them,
because the build now consumes four of those directories and generates
into a fifth.  board.local and build/ come along; MIDI_CC_MAP.md is
generated into Documentation/ rather than into the old Software/ root.

scripts/ goes with the build rather than staying under the firmware,
because six of the ten had nothing to do with the firmware: gen_effects.py
reads Effects/ and writes to three different places, pow2/log2/quarter_sine
generate Audio/'s tables, check-readme.py compares Effects/ against the
README, and server.py serves the web app.  Four of them are invoked from
Validation, which was reaching into Firmware/ for tooling - the same
burying this commit is undoing.  The four that really are about the
firmware are ELF checks the top-level build drives anyway, and a second
scripts directory would only be a second place to look.

C includes say "Audio/foo.h" and the generated map says
"Effects/bar.h", with the repository root on the include path for both
the firmware and the bench.  Spelling the directory out rather than
relying on a bare name is what keeps Audio/cycles.h shimmable: a quoted
include searches the including file's own directory first.

The submodules are renamed as well as moved.  git mv updates their paths
but leaves the section names, and 'Software/pico-sdk' surviving in
.gitmodules would be the word this commit removes, still load-bearing.
That meant the nested modules under pico-sdk too - six .git files
pointing into .git/modules/Software - which is why 'git submodule update
--init --recursive' is worth running once after pulling this.

Verified rather than assumed: a clean configure and build, make check
(failing only on the missing-eeprom case it already failed on),
check-effects, all four analysis pages reproducing every series and
drawing every chart, and a flash to the board that still measures a
routed reverb where it did before.

One latent bug fell out of it.  bench/coeff declared only quarter_sine.h
of the three generated math tables, and Audio/util.h includes pow2.h and
log2.h as well - so building that target with an empty gen/ could never
have worked.  'make bench' builds bench/bench first, which generates all
three, so it stayed hidden until this rebuilt everything from nothing.

Signed-off-by: Linus Torvalds <torvalds@linux-foundation.org>
2026-08-11 13:48:26 -07:00

312 lines
10 KiB
C

#ifndef BINDINGS_H
#define BINDINGS_H
//
// What the physical controls actually do.
//
// There is one rotary and one footswitch, and what a gesture on either
// of them means is looked up here rather than compiled in. That is the
// whole point: a pedal with no screen cannot tell you what its knob is
// for, so the answer has to be set from something that can, and the
// WebMIDI app is that something.
//
// Nothing here is saved. The table is rebuilt from the defaults below
// on every boot, and those defaults are exactly what the pedal did when
// the gestures were hardcoded, so an unprogrammed pedal behaves as it
// always has. Persisting it waits for the storage rework, because the
// layout should not be guessed at before the shape of what is stored
// has settled - which is also what makes this testable on the old board
// with the 2kbit part, where there is nowhere to save it to anyway.
//
//
// The gestures - what a binding is *from*.
//
// This lists the gestures that exist, not the ones that are imaginable.
// The next board's jack carries either two more footswitches or an
// analog expression pedal, and whichever it turns out to be becomes
// more entries here; the wire format has a whole byte for the id, so
// growing the list costs nothing but the entries.
//
// Turning the rotary while pressing its shaft is deliberately not one
// of these. It used to select a pot, and it cannot coexist with a long
// press on the same shaft, because holding the shaft in order to turn
// it manufactures one every time.
//
enum control_id {
CTRL_ROTARY_TURN,
CTRL_ROTARY_TAP,
CTRL_ROTARY_HOLD,
CTRL_STOMP_TAP,
CTRL_STOMP_HOLD,
NR_CONTROLS,
};
//
// ...and what a binding is *to*.
//
// ACT_POT is the only one that means anything for a control that turns,
// and the others are the only ones that mean anything for one that
// clicks. Nothing here enforces that. Binding the footswitch to a pot
// is useless rather than dangerous, and the place to make a pointless
// choice hard to express is the app, not the wire.
//
enum bind_action {
ACT_NONE,
ACT_POT, // turn it: drive the target
ACT_NEXT_POT, // step the knob's pot to the next one
ACT_RESET_POT, // put the target back to its schema default
ACT_SET_POT, // set the target to val[0]
ACT_TOGGLE_POT, // flip the target between val[0] and val[1]
ACT_BYPASS,
ACT_TUNER,
ACT_SCENE, // effect = scene id
NR_ACTIONS,
};
//
// A target of BIND_FOLLOW means "whatever the knob is turning", rather
// than a particular pot.
//
// It exists for the way back. A press that resets the knob has to
// follow the knob when the knob is rebound, or it quietly goes on
// resetting the pot you moved away from - and the whole reason that
// press exists is to be the reliable way back when you cannot see
// anything. Naming the pot twice would work right up until it did not.
//
// It is deliberately not offered for the actions that carry a value.
// A value only means something once the pot is known: 80 is unity on
// the master volume and an arbitrary number of milliseconds on a delay
// time. If you want to name a value, name the pot it belongs to.
//
// 0x7f rather than 0xff because everything here goes out over SysEx,
// where the high bit is not ours to use.
//
#define BIND_FOLLOW 0x7f
//
// 'pot' numbers a parameter the way the SysEx parameter write already
// does: 0 is the mix, 1 to 10 are the effect's own pots. Two
// conventions for "which parameter of which effect" in one firmware
// would be one too many, and the mix is worth having - toggling it
// between nothing and everything is how a footswitch turns a single
// effect on and off.
//
// 'control' is which gesture fires it, and the table is flat: a gesture
// may appear in it more than once, so one press can do several things.
// Toggling one effect's mix up while taking another's down is how you
// switch between two effects without leaving the scene, and that is not
// expressible at all when a gesture gets exactly one action.
//
// Flat rather than a list hanging off each control, because it makes
// the natural write "here is the whole table" instead of "insert into
// row K's list" - and the app already draws only what the pedal echoes
// back, so that makes both directions the same message and leaves no
// insert, delete or reorder protocol to get wrong.
//
struct rule {
unsigned char control;
unsigned char action;
unsigned char effect;
unsigned char pot;
unsigned char val[2];
};
#define MAX_RULES 16
//
// Defaults for a pedal nobody has programmed.
//
// The rotary is the master volume, which is the only thing a single
// unlabelled knob can plausibly be. It does not change what any effect
// hears - Trim and Volume are the two ends of the chain and this is the
// far one - so getting it wrong costs loudness rather than tone, and it
// is audible, which matters when there is nothing to look at. It is
// also the parameter this firmware already treats as special: CC 7 goes
// straight to it.
//
// Both rotary presses reset it, and that is deliberate rather than a
// gesture going to waste. switch_irq() sets either the short bit or
// the long one and never both, so a press held a moment too long
// arrives only as a long press. If the two did different things, a
// slightly slow press would silently do the wrong one - and this action
// exists precisely to be the way back when you cannot see what you are
// doing, so it is the last thing that should be fussy about timing.
//
// The footswitch keeps what it always did.
//
static const struct rule default_rules[] = {
{ CTRL_ROTARY_TURN, ACT_POT, 0, CHAIN_VOLUME + 1 },
{ CTRL_ROTARY_TAP, ACT_RESET_POT, BIND_FOLLOW },
{ CTRL_ROTARY_HOLD, ACT_RESET_POT, BIND_FOLLOW },
{ CTRL_STOMP_TAP, ACT_BYPASS },
{ CTRL_STOMP_HOLD, ACT_TUNER },
};
//
// Three tables, and the one everything reads.
//
// A scene's rules, the pedal-wide ones, and the defaults above. For any
// one control the most specific level that has anything to say about it
// wins outright - see resolve_rules() - and what comes out is 'rules',
// which is the table it has always been and which nothing walking it had
// to learn about any of this.
//
static struct rule scene_rules[MAX_RULES];
static unsigned int nr_scene_rules;
static struct rule global_rules[MAX_RULES];
static unsigned int nr_global_rules;
//
// Bigger than a level, because it is a sum of them: sixteen rules on one
// control in a scene plus the globals' rules for the other four is more
// than sixteen, and quietly dropping the tail would be a miserable thing
// to debug. 192 bytes.
//
#define EFFECTIVE_RULES 32
static struct rule rules[EFFECTIVE_RULES];
static unsigned int nr_rules;
//
// Does this action point at a pot, and may it say "the knob's one"?
//
static bool action_has_target(unsigned int action)
{
return action == ACT_POT || action == ACT_RESET_POT ||
action == ACT_SET_POT || action == ACT_TOGGLE_POT;
}
static bool action_may_follow(unsigned int action)
{
return action == ACT_RESET_POT;
}
//
// Take the whole table off the wire.
//
// Everything is range checked here rather than where it is used, so
// that the table can be trusted by the things that walk it. An effect
// id out of range is an array read off the end, and the app is not the
// only thing that can send one of these.
//
// A rule that does not check out is dropped rather than the batch being
// rejected. The pedal answers with what it kept, so a dropped rule
// shows up at once as a row that did not come back - which is a better
// way to be told than an error the app would have to render, and it
// stops one bad rule from losing the other fifteen.
//
static bool rule_ok(const struct rule *r)
{
if (r->control >= NR_CONTROLS || r->action >= NR_ACTIONS)
return false;
if (r->action == ACT_SCENE)
return r->effect < MAX_SCENES;
if (!action_has_target(r->action))
return true;
if (r->effect == BIND_FOLLOW)
return action_may_follow(r->action);
if (r->effect >= ARRAY_SIZE(effects) || r->pot > 10)
return false;
//
// An effect with no wet and no dry has no mix to bind to, and a
// pot with no label does not exist on that effect.
//
if (!r->pot)
return !effects[r->effect]->no_mix;
return effects[r->effect]->pots[r->pot - 1].label != NULL;
}
//
// Work out what each control actually does.
//
// Per control, the most specific level that mentions it at all wins,
// and wins completely. Not a merge: every rule naming a gesture fires
// when that gesture happens - which is how one press takes one effect's
// mix up while taking another's down - so a union would mean a scene
// could never stop the footswitch being bypass. It could only ever add
// to it.
//
// Which makes ACT_NONE worth something. A scene rule that does nothing
// still counts as the scene having spoken for that control, so it is
// how you say "nothing happens here" as distinct from "this level is
// silent, ask the next one".
//
// And deleting every global rule falls back to the compiled-in
// defaults rather than to nothing at all, so a pedal cannot be
// configured into having no way back.
//
static void resolve_rules(void)
{
nr_rules = 0;
for (unsigned int ctrl = 0; ctrl < NR_CONTROLS; ctrl++) {
const struct rule *src;
unsigned int count, n = 0;
for (unsigned int i = 0; i < nr_scene_rules; i++)
n += scene_rules[i].control == ctrl;
if (n) {
src = scene_rules;
count = nr_scene_rules;
} else {
for (unsigned int i = 0; i < nr_global_rules; i++)
n += global_rules[i].control == ctrl;
if (n) {
src = global_rules;
count = nr_global_rules;
} else {
src = default_rules;
count = ARRAY_SIZE(default_rules);
}
}
for (unsigned int i = 0; i < count; i++) {
if (src[i].control != ctrl)
continue;
if (nr_rules >= EFFECTIVE_RULES)
return;
rules[nr_rules++] = src[i];
}
}
}
//
// Take one level's table off the wire.
//
// Everything is range checked here rather than where it is used, so
// that the resolved table can be trusted by the things that walk it.
//
static void set_rules(struct rule *dst, unsigned int *dst_count,
const uint8_t *buf, unsigned int count)
{
unsigned int kept = 0;
if (count > MAX_RULES)
count = MAX_RULES;
for (unsigned int i = 0; i < count; i++) {
const uint8_t *p = buf + i * 6;
struct rule r = { p[0], p[1], p[2], p[3], { p[4], p[5] } };
//
// Values are not checked. A pot's range is a property
// of the pot, and for a following target there is no pot
// yet to ask - so they are clamped where they are used,
// which is the only place that can always do it.
//
if (rule_ok(&r))
dst[kept++] = r;
}
*dst_count = kept;
resolve_rules();
}
#endif