You've already forked torvalds-GuitarPedal
mirror of
https://github.com/torvalds/GuitarPedal.git
synced 2026-08-18 13:13:35 +00:00
'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>
682 lines
21 KiB
C
682 lines
21 KiB
C
//
|
|
// This is the "ui" for now - really just for very random testing
|
|
//
|
|
#include "scene.h"
|
|
|
|
struct pot_range { int min, max; };
|
|
|
|
static const struct pot_range get_pot_range(const struct pot_descr *pot)
|
|
{
|
|
int min = 0, max = 120;
|
|
|
|
if (pot->enum_names) {
|
|
min = 0;
|
|
for (max = 0; pot->enum_names[max+1]; max++)
|
|
/* nothing */;
|
|
}
|
|
return (struct pot_range) { min, max };
|
|
}
|
|
|
|
//
|
|
// Which pot a binding acts on.
|
|
//
|
|
// BIND_FOLLOW means the knob's, which can only be answered at the
|
|
// moment the gesture happens - that is the entire point of it, and why
|
|
// it is resolved here rather than stored.
|
|
//
|
|
//
|
|
// The rule the knob is driving, for the things that follow it.
|
|
//
|
|
// The first one, when there are several: a knob bound to two parameters
|
|
// at once is a perfectly good macro control, but "reset the knob's
|
|
// parameter" has to mean one parameter, and the first is the only
|
|
// answer that does not depend on how the list happens to be ordered
|
|
// later.
|
|
//
|
|
static struct rule *knob_rule(void)
|
|
{
|
|
for (unsigned int i = 0; i < nr_rules; i++) {
|
|
if (rules[i].control == CTRL_ROTARY_TURN &&
|
|
rules[i].action == ACT_POT)
|
|
return &rules[i];
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
static struct effect *bind_target(const struct rule *b, unsigned int *pot,
|
|
unsigned int *eff_id)
|
|
{
|
|
unsigned int eff = b->effect, idx = b->pot;
|
|
|
|
if (eff == BIND_FOLLOW) {
|
|
const struct rule *knob = knob_rule();
|
|
|
|
if (!knob)
|
|
return NULL;
|
|
eff = knob->effect;
|
|
idx = knob->pot;
|
|
}
|
|
|
|
if (eff >= ARRAY_SIZE(effects) || idx > 10)
|
|
return NULL;
|
|
if (!idx) {
|
|
if (effects[eff]->no_mix)
|
|
return NULL;
|
|
} else if (!effects[eff]->pots[idx - 1].label) {
|
|
return NULL;
|
|
}
|
|
|
|
*pot = idx;
|
|
*eff_id = eff;
|
|
return effects[eff];
|
|
}
|
|
|
|
//
|
|
// A target's current value, default and range, with the mix folded in
|
|
// as parameter 0 the way the wire numbers it. The mix is a float on
|
|
// its own scale rather than a byte in pot_values[], which is the whole
|
|
// reason these three exist instead of the callers indexing directly.
|
|
//
|
|
static int target_value(struct effect *e, unsigned int pot)
|
|
{
|
|
switch (pot) {
|
|
case POT_MIX: return FLOAT_TO_POT(e->mix_pot);
|
|
case POT_CH_IN: return CH_IN(e->channels);
|
|
case POT_CH_OUT: return CH_OUT(e->channels);
|
|
case POT_MERGE: return FLOAT_TO_POT(e->merge);
|
|
}
|
|
return effect_pots(e)[pot - 1];
|
|
}
|
|
|
|
//
|
|
// The defaults are the ones reset_effect() hands out: read the left
|
|
// channel, write both, and keep all of whatever was not touched - which
|
|
// is what every effect did before there was a choice.
|
|
//
|
|
static int target_default(struct effect *e, unsigned int pot)
|
|
{
|
|
switch (pot) {
|
|
case POT_MIX: return FLOAT_TO_POT(e->def_mix);
|
|
case POT_CH_IN: return CH_IN_LEFT;
|
|
case POT_CH_OUT: return CH_OUT_BOTH;
|
|
case POT_MERGE: return 120;
|
|
}
|
|
return e->pots[pot - 1].def_val;
|
|
}
|
|
|
|
static struct pot_range target_range(struct effect *e, unsigned int pot)
|
|
{
|
|
switch (pot) {
|
|
case POT_MIX: return (struct pot_range){ 0, 120 };
|
|
case POT_CH_IN: return (struct pot_range){ 0, CH_IN_RIGHT };
|
|
case POT_CH_OUT: return (struct pot_range){ 0, CH_OUT_MERGE };
|
|
case POT_MERGE: return (struct pot_range){ 0, 120 };
|
|
}
|
|
return get_pot_range(e->pots + pot - 1);
|
|
}
|
|
|
|
//
|
|
// Put a value on a bound pot, and tell the app.
|
|
//
|
|
// Clamped here rather than where the binding was accepted, because this
|
|
// is the only place that always knows which pot it is: a following
|
|
// target does not have one until the gesture happens.
|
|
//
|
|
static void set_target(struct effect *effect, unsigned int eff_id,
|
|
unsigned int pot, int val)
|
|
{
|
|
const struct pot_range range = target_range(effect, pot);
|
|
|
|
if (val < range.min)
|
|
val = range.min;
|
|
else if (val > range.max)
|
|
val = range.max;
|
|
|
|
if (val == target_value(effect, pot))
|
|
return;
|
|
|
|
if (pot > POT_LAST)
|
|
set_effect_steering(effect, pot, val);
|
|
else if (pot)
|
|
set_effect_pot(effect, pot - 1, val);
|
|
else
|
|
set_effect_mix(effect, val);
|
|
|
|
sysex_echo_pot(eff_id, pot, val);
|
|
}
|
|
|
|
//
|
|
// The rotary, turned.
|
|
//
|
|
// Every rule the knob has gets the same movement, so a knob bound to
|
|
// two parameters moves both - which is what a macro control is. The
|
|
// accumulator is drained once, using the first rule's range to decide
|
|
// how coarse to be, because it is one physical movement and it cannot
|
|
// be spent twice.
|
|
//
|
|
// Note that the "__atomic" part isn't actually about SMP, just the
|
|
// interrupts. The low bits are deliberately left behind rather than
|
|
// drained, so that sub-detent movement on a coarse pot adds up instead
|
|
// of being thrown away a click at a time.
|
|
//
|
|
static void rotary_turned(void)
|
|
{
|
|
struct rule *first = knob_rule();
|
|
unsigned int pot, eff_id;
|
|
struct effect *effect;
|
|
int ignore_low_bits = 0;
|
|
|
|
if (!first || !(effect = bind_target(first, &pot, &eff_id))) {
|
|
//
|
|
// Nothing to drive. Drop what has accumulated rather
|
|
// than saving it up, or the first thing bound here
|
|
// would jump by however far the knob was fiddled with
|
|
// while it was pointing at nothing.
|
|
//
|
|
__atomic_store_n(&rotary_value, 0, __ATOMIC_RELAXED);
|
|
return;
|
|
}
|
|
|
|
// For small ranges, don't make the rotary so twitchy
|
|
const struct pot_range range = target_range(effect, pot);
|
|
if (range.max - range.min < 25)
|
|
ignore_low_bits = 2;
|
|
|
|
int mask = (1 << ignore_low_bits)-1;
|
|
int val = __atomic_fetch_and(&rotary_value, mask, __ATOMIC_RELAXED);
|
|
val >>= ignore_low_bits;
|
|
if (!val)
|
|
return;
|
|
|
|
for (unsigned int i = 0; i < nr_rules; i++) {
|
|
struct rule *r = &rules[i];
|
|
|
|
if (r->control != CTRL_ROTARY_TURN || r->action != ACT_POT)
|
|
continue;
|
|
effect = bind_target(r, &pot, &eff_id);
|
|
if (!effect)
|
|
continue;
|
|
//
|
|
// set_target() clamps and tells the app, and does
|
|
// nothing when the value did not move - which is what
|
|
// stops a knob held against an end from sending the
|
|
// same number for ever.
|
|
//
|
|
set_target(effect, eff_id, pot,
|
|
target_value(effect, pot) + val);
|
|
}
|
|
}
|
|
|
|
//
|
|
// Step the rotary's target to the next parameter that exists on the
|
|
// same effect, wrapping. Parameter 0 is the mix, so an effect that has
|
|
// one is stepped through as well.
|
|
//
|
|
// Only ever forwards: going backwards was what hold-and-turn did, and
|
|
// that gesture is gone.
|
|
//
|
|
// This is the one action whose effect is to move another binding, so it
|
|
// is also the one that has to say so afterwards.
|
|
//
|
|
static void next_bound_pot(void)
|
|
{
|
|
struct rule *b = knob_rule();
|
|
|
|
if (!b || b->effect >= ARRAY_SIZE(effects))
|
|
return;
|
|
|
|
struct effect *effect = effects[b->effect];
|
|
int idx = b->pot;
|
|
|
|
do {
|
|
if (++idx > 10)
|
|
idx = 0;
|
|
if (idx == b->pot)
|
|
return;
|
|
} while (idx ? !effect->pots[idx - 1].label : effect->no_mix);
|
|
|
|
//
|
|
// Deliberately on the resolved table rather than on whichever
|
|
// level supplied the rule. Stepping the knob to the next
|
|
// parameter is something you do mid-song to pick what it
|
|
// adjusts, not a change to how the pedal is set up - so it
|
|
// lasts until the scene is reloaded and is not written to
|
|
// flash, which is also what it did when there was only one
|
|
// table and nothing was saved at all.
|
|
//
|
|
b->pot = idx;
|
|
sysex_send_bindings(RULES_EFFECTIVE);
|
|
}
|
|
|
|
//
|
|
// The three press actions that put a value on a pot.
|
|
//
|
|
// The LED flashes whether or not anything moved. A press that changes
|
|
// nothing - because you were already at the default, or because the
|
|
// binding points at nothing - still has to be distinguishable from a
|
|
// press that did not register. "Did that do anything?" is the question
|
|
// these exist to stop you having to ask, so they cannot be silent.
|
|
//
|
|
static void do_pot_action(const struct rule *b)
|
|
{
|
|
unsigned int pot, eff_id;
|
|
struct effect *effect = bind_target(b, &pot, &eff_id);
|
|
|
|
if (effect) {
|
|
int val = -1;
|
|
|
|
switch (b->action) {
|
|
case ACT_RESET_POT:
|
|
val = target_default(effect, pot);
|
|
break;
|
|
case ACT_SET_POT:
|
|
val = b->val[0];
|
|
break;
|
|
case ACT_TOGGLE_POT:
|
|
//
|
|
// Stateless: whichever of the two it is not on
|
|
// right now. Remembering which one it went to
|
|
// last would be one more thing to fall out of
|
|
// step with a value the app has since changed.
|
|
//
|
|
val = target_value(effect, pot) == b->val[0]
|
|
? b->val[1] : b->val[0];
|
|
break;
|
|
}
|
|
|
|
if (val >= 0)
|
|
set_target(effect, eff_id, pot, val);
|
|
}
|
|
|
|
attention_preview = ATTENTION_PREVIEW_TICKS;
|
|
}
|
|
|
|
//
|
|
// A scene change asked for by a rule, and not yet done. -1 for none.
|
|
//
|
|
static int pending_scene = -1;
|
|
|
|
//
|
|
// The global-enable CC, remembered until it actually goes out. -1 for
|
|
// nothing owed.
|
|
//
|
|
// This one is an edge and has no repeat behind it: bypass and tuner mode
|
|
// are toggled by a foot, and if the message saying so is lost the app
|
|
// goes on drawing the old state until something else provokes a state
|
|
// dump. So it cannot simply be dropped the way the periodic status
|
|
// report can, and it must not block either - a foot going down while the
|
|
// host has stopped reading is a bad moment to spend 20ms.
|
|
//
|
|
// Held instead, and retried on the next tick. A second edge overwrites
|
|
// the first, which is right: what is owed is the current state, not the
|
|
// history of how it got there.
|
|
//
|
|
static int global_enable_owed = -1;
|
|
|
|
static void send_global_enable(int val)
|
|
{
|
|
global_enable_owed = send_midi_cc_nb(MIDI_CC_GLOBAL_ENABLE, val)
|
|
? -1 : val;
|
|
}
|
|
|
|
static void do_rule(const struct rule *b)
|
|
{
|
|
switch (b->action) {
|
|
case ACT_NEXT_POT:
|
|
next_bound_pot();
|
|
break;
|
|
|
|
case ACT_RESET_POT:
|
|
case ACT_SET_POT:
|
|
case ACT_TOGGLE_POT:
|
|
do_pot_action(b);
|
|
break;
|
|
|
|
case ACT_BYPASS:
|
|
disable_all = EFF_ENABLE_STEPS * !disable_all;
|
|
send_global_enable(disable_all ? 0 : 127);
|
|
break;
|
|
|
|
case ACT_TUNER:
|
|
tuner_mode = !tuner_mode;
|
|
send_global_enable(tuner_mode ? 68 : 69);
|
|
break;
|
|
|
|
case ACT_SCENE:
|
|
//
|
|
// Noted, not done. Loading a scene rebuilds the rule
|
|
// table - resolve_rules() - and 'b' points into that
|
|
// table, which fire_control() is in the middle of
|
|
// walking. Switching here would pull the list out from
|
|
// under the loop that is reading it, and the rest of
|
|
// the gesture's rules would come from whichever table
|
|
// replaced it.
|
|
//
|
|
// So the switch waits until nothing is iterating. It
|
|
// also means a gesture bound to two scene changes picks
|
|
// the last one rather than loading both, which is a more
|
|
// sensible answer than either.
|
|
//
|
|
// Checked again rather than trusted from the table.
|
|
// set_binding() saw the same MAX_SCENES, and this costs
|
|
// nothing.
|
|
//
|
|
if (b->effect < MAX_SCENES)
|
|
pending_scene = b->effect;
|
|
break;
|
|
}
|
|
}
|
|
|
|
//
|
|
// Every rule that names this gesture, in table order.
|
|
//
|
|
// They are not atomic against the audio core: each one publishes as it
|
|
// goes, so core 1 can see one parameter moved and the next not yet.
|
|
// That is a sample or two apart on a 25Hz tick, and every one of these
|
|
// ends up crossfaded by EFF_ENABLE_STEPS anyway, so it is not audible
|
|
// and not worth a second publishing mechanism to avoid.
|
|
//
|
|
static void fire_control(unsigned int ctrl)
|
|
{
|
|
for (unsigned int i = 0; i < nr_rules; i++) {
|
|
if (rules[i].control == ctrl)
|
|
do_rule(&rules[i]);
|
|
}
|
|
}
|
|
|
|
//
|
|
// Whatever the switches are bound to.
|
|
//
|
|
// Called from the main loop ahead of the tuner-mode check rather than
|
|
// from update_ui(), which does not run in tuner mode - something has to
|
|
// be able to turn it off again.
|
|
//
|
|
static void handle_switch_bindings(void)
|
|
{
|
|
static const struct {
|
|
unsigned char sw;
|
|
unsigned char ctrl;
|
|
} gestures[] = {
|
|
{ ROTARY_SWITCH, CTRL_ROTARY_TAP },
|
|
{ LONGPRESS(ROTARY_SWITCH), CTRL_ROTARY_HOLD },
|
|
{ STOMP_SWITCH, CTRL_STOMP_TAP },
|
|
{ LONGPRESS(STOMP_SWITCH), CTRL_STOMP_HOLD },
|
|
};
|
|
|
|
for (int i = 0; i < ARRAY_SIZE(gestures); i++) {
|
|
if (!switch_pressed(gestures[i].sw))
|
|
continue;
|
|
switch_clear(gestures[i].sw);
|
|
fire_control(gestures[i].ctrl);
|
|
}
|
|
|
|
//
|
|
// Out here, where the rule table is not being walked.
|
|
//
|
|
// Everything moves: pots, routing, and the rules themselves, so
|
|
// whatever the app has cached is wrong in every particular. Ask
|
|
// for the whole dump rather than working out what changed - this
|
|
// happens when somebody steps on a switch, not per sample.
|
|
//
|
|
if (pending_scene >= 0) {
|
|
//
|
|
// Only to a scene that has something in it. load_scene()
|
|
// on an empty one resets every effect and routes nothing,
|
|
// so a rule aimed at a scene nobody has saved would take
|
|
// a working pedal to silence at the touch of a switch -
|
|
// and the way back would be the switch that just did it,
|
|
// which now belongs to whatever the defaults say.
|
|
//
|
|
// Refusing leaves you where you were, which is wrong in a
|
|
// way you can hear and recover from.
|
|
//
|
|
if (populated_scenes() & (1u << pending_scene)) {
|
|
load_scene(pending_scene);
|
|
state_dump_tx = true;
|
|
}
|
|
pending_scene = -1;
|
|
}
|
|
}
|
|
|
|
|
|
// Human perception isn't linear, but neither
|
|
// is LED intensity, particularly since we're
|
|
// typically driving the LED at the lower range
|
|
// of the current range
|
|
//
|
|
//
|
|
// Random map from 0..1 to 0..full that works for the LED I have
|
|
// happened to pick.
|
|
//
|
|
// The full-scale value is a parameter because the smart LEDs want 255
|
|
// where the PWM one wants 4096, and the *curve* is the part that has to
|
|
// be shared. Applying the setting linearly instead makes 10% come out
|
|
// at 10% rather than at 3.2%, which is three times the light and looks
|
|
// like the driver is broken.
|
|
//
|
|
static int led_pwm_mapping(float pwm, int full)
|
|
{
|
|
return lrintf(pwm * sqrtf(pwm) * full);
|
|
}
|
|
|
|
//
|
|
// Drive the one LED from the same status the host is given.
|
|
//
|
|
// It takes the bits rather than a single 'intense' flag it could have
|
|
// been handed instead, and that is the whole point of the shape. This
|
|
// LED is a WS2812B on the next board, with colours to spend on telling a
|
|
// closed gate from a dropped sample, and this is the function that will
|
|
// spend them. Giving it everything now means that change is local to
|
|
// here rather than a new argument list and a new caller.
|
|
//
|
|
// What it can say today is bright or not, so:
|
|
//
|
|
// - faults always count. Clipping and a missed deadline are wrong
|
|
// whatever else is going on.
|
|
//
|
|
// - effect activity counts only while the pedal is in circuit. The
|
|
// chain still runs when bypassed - make_one_noise() keeps stepping it
|
|
// and crossfades the result away - so the compressor goes on
|
|
// compressing into an output nobody hears, and that is not news.
|
|
//
|
|
// - the attention preview counts because it *is* the thing being set;
|
|
// see status.h.
|
|
//
|
|
//
|
|
// No pin argument: which LED this drives is a property of the board and
|
|
// not of the caller, and on a board with smart LEDs there is no single
|
|
// pin to name. LED_GPIO does not even exist there.
|
|
//
|
|
static void set_led(bool on, uint8_t global, unsigned int chain)
|
|
{
|
|
bool fault = global & (STATUS_DROPPED_MASK | STATUS_CLIPPED);
|
|
bool activity = (global & STATUS_FRONT_ATTN) || chain;
|
|
|
|
bool intense = fault || attention_preview || (on && activity);
|
|
float pwm = intense ? settings.led_intense : settings.led_pwm;
|
|
|
|
#ifdef WS2812_GPIO
|
|
//
|
|
// A board with these has no plain LED to dim - the one it does
|
|
// have is across +5V and is a power indicator - so the PWM path
|
|
// below is not merely unnecessary here, it would drive a pin that
|
|
// is the stomp switch on this board.
|
|
//
|
|
// Same setting and same curve as the plain LED, because it is the
|
|
// same question and the pot for it already exists.
|
|
//
|
|
pixel_brightness = led_pwm_mapping(pwm, 255);
|
|
|
|
//
|
|
// Three LEDs, three jobs, chosen to match where they sit: the
|
|
// chain runs left, centre-back, right, and the centre one is
|
|
// directly behind the stomp switch.
|
|
//
|
|
// centre in circuit or bypassed - the pedal LED
|
|
// left something is wrong, and which
|
|
// right the chain is doing something
|
|
//
|
|
// The attention preview is the exception: it is not a status, it
|
|
// is the pedal showing you how bright "intense" is so you can set
|
|
// it. That needs one LED lit at that brightness and nothing more
|
|
// - three of them in white is all nine channels at once, which is
|
|
// three times the light and unpleasant to sit next to. So it is
|
|
// the centre one, in amber, and the others go dark.
|
|
//
|
|
unsigned int dropped = global & STATUS_DROPPED_MASK;
|
|
|
|
if (attention_preview) {
|
|
pixels_set(0, 0);
|
|
pixels_set(1, RGB(255, 120, 0));
|
|
pixels_set(2, 0);
|
|
} else {
|
|
pixels_set(0, global & STATUS_CLIPPED ? RGB(255, 0, 0)
|
|
: dropped ? RGB(255, 96, 0)
|
|
: 0);
|
|
pixels_set(1, on ? RGB(0, 255, 0) : 0);
|
|
pixels_set(2, on && activity ? RGB(0, 64, 255) : 0);
|
|
}
|
|
|
|
#else
|
|
int level = 0;
|
|
|
|
if (on || intense)
|
|
level = led_pwm_mapping(pwm, PWM_WRAP);
|
|
|
|
pwm_set_gpio_level(LED_GPIO, level);
|
|
#endif
|
|
}
|
|
|
|
_Static_assert(MAX_ROUTED_EFFECTS <= 2 * STATUS_CHAIN_BITS,
|
|
"a chain this long needs a third status CC");
|
|
|
|
//
|
|
// How often to say it again when nothing has changed.
|
|
//
|
|
// On change alone is not enough, for two reasons that have nothing to do
|
|
// with each other. A report can simply not go out - the endpoint is busy
|
|
// and this yields to anything that is not status - and a host that missed
|
|
// the one message would go on believing the old answer forever, because
|
|
// from here nothing has changed since. And an app that connects while
|
|
// something is already wrong never gets told at all, for the same reason:
|
|
// it wasn't listening when it changed.
|
|
//
|
|
// Every sixteenth tick is about five messages a second. It used to be
|
|
// every eighth, which was chosen against the cost of a SysEx state dump -
|
|
// and a dump is now 140 bytes rather than 1184, so the thing this was
|
|
// measured as "nothing next to" got twelve times smaller. Two thirds of
|
|
// a second to correct itself is still well inside the time it takes to
|
|
// look at an LED and wonder.
|
|
//
|
|
#define STATUS_REPEAT_TICKS 16
|
|
|
|
//
|
|
// Work out what the pedal is doing, and say so - to the LED and to the
|
|
// host, which are two renderings of the one answer. See midi.h for what
|
|
// goes in which bit.
|
|
//
|
|
// Both go out from here rather than the LED being driven separately,
|
|
// because the two used to disagree: the LED knew about clipping and lost
|
|
// samples while the effects' own activity went nowhere at all, having
|
|
// been aimed at a second LED that the current board does not have.
|
|
//
|
|
// Clearing 'intense' for every effect rather than just the one being
|
|
// edited is what makes the chain bits mean anything. The audio core
|
|
// sets them at 48kHz and this is the only thing that ever puts them
|
|
// back, so an effect that has stopped asking for attention would
|
|
// otherwise stay lit for good - which is exactly what boost, compressor
|
|
// and echo had been doing, unnoticed, because nothing read them.
|
|
//
|
|
static void show_status(void)
|
|
{
|
|
unsigned int dropped = __atomic_exchange_n(&samples_dropped, 0,
|
|
__ATOMIC_RELAXED);
|
|
uint8_t global = dropped > STATUS_DROPPED_MASK
|
|
? STATUS_DROPPED_MASK : dropped;
|
|
unsigned int attn = 0;
|
|
|
|
if (output_clipped)
|
|
global |= STATUS_CLIPPED;
|
|
if (effects[0]->intense)
|
|
global |= STATUS_FRONT_ATTN;
|
|
|
|
for (int i = 0; i < routed_effect_count; i++) {
|
|
if (effects[effect_chain[i]]->intense)
|
|
attn |= 1u << i;
|
|
}
|
|
|
|
set_led(!disable_all, global, attn);
|
|
|
|
// A CC value is seven bits, so the chain needs two of them
|
|
uint8_t chain[2] = { attn & ((1u << STATUS_CHAIN_BITS) - 1),
|
|
attn >> STATUS_CHAIN_BITS };
|
|
|
|
static uint8_t last_global = 0;
|
|
static uint8_t last_chain[2] = { 0, 0 };
|
|
static unsigned int tick;
|
|
|
|
bool again = (tick++ % STATUS_REPEAT_TICKS) == 0;
|
|
|
|
//
|
|
// Status yields to everything else, and never waits for the wire.
|
|
//
|
|
// Two rules, and they are the same idea twice. Nothing goes out
|
|
// while the pedal has a real reply in flight, because a schema or a
|
|
// state dump is something somebody asked for and this is not. And
|
|
// what does go out uses the non-blocking write, because the
|
|
// blocking one answers a full transmit fifo by spinning for 20ms -
|
|
// three of those, eight times a second, was the pedal stalling its
|
|
// own audio to talk to a host that had stopped listening. It
|
|
// measured as four to seven breaks in a two second capture, quite
|
|
// independently of any SysEx traffic.
|
|
//
|
|
// 'last' is only updated when the write is taken, so a report that
|
|
// does not go out is not remembered as sent and is tried again on
|
|
// the next tick, forty milliseconds later. That matters for a
|
|
// change: the repeat above covers a lost repeat, but only this
|
|
// covers a lost *edge*, which is the one nobody would notice going
|
|
// missing until the LED and the app disagreed.
|
|
//
|
|
bool quiet = !midi_tx_busy();
|
|
|
|
//
|
|
// Anything owed from a foot going down, first: it is an edge and
|
|
// the periodic report below is not.
|
|
//
|
|
if (quiet && global_enable_owed >= 0)
|
|
send_global_enable(global_enable_owed);
|
|
|
|
if (quiet && (again || global != last_global)) {
|
|
if (send_midi_cc_nb(MIDI_CC_STATUS_GLOBAL, global))
|
|
last_global = global;
|
|
}
|
|
if (quiet && (again || chain[0] != last_chain[0])) {
|
|
if (send_midi_cc_nb(MIDI_CC_STATUS_CHAIN_LO, chain[0]))
|
|
last_chain[0] = chain[0];
|
|
}
|
|
if (quiet && (again || chain[1] != last_chain[1])) {
|
|
if (send_midi_cc_nb(MIDI_CC_STATUS_CHAIN_HI, chain[1]))
|
|
last_chain[1] = chain[1];
|
|
}
|
|
|
|
for (int i = 0; i < ARRAY_SIZE(effects); i++)
|
|
effects[i]->intense = 0;
|
|
output_clipped = 0;
|
|
}
|
|
|
|
//
|
|
// 'update_ui()' is called every few ms to react to user events.
|
|
//
|
|
// The switches are not read here - see handle_switch_bindings(), which
|
|
// the main loop calls before deciding whether it is in tuner mode.
|
|
//
|
|
static void update_ui(void)
|
|
{
|
|
show_status();
|
|
|
|
if (attention_preview)
|
|
attention_preview--;
|
|
|
|
rotary_turned();
|
|
}
|