0
mirror of https://github.com/torvalds/GuitarPedal.git synced 2026-08-14 04:43:53 +00:00
Files
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

567 lines
16 KiB
C

//
// The pedal's audio core, on a workstation.
//
// This is not a model of the signal path. It is the signal path: the
// same audio/effect.h, the same effects/*.h, the same single_sample(),
// compiled with the same flags for a different instruction set. The
// only things replaced are the two register blocks the audio core
// touches, and they are replaced with ordinary memory - see shim/.
//
// That distinction is the whole reason this exists. A harness that
// re-implemented the middle of single_sample() would be a second
// opinion about what the pedal does, and a second opinion is exactly
// what you cannot check an effect against.
//
// Raw float32 stereo frames in on stdin, the same out on stdout, so
// every signal and every measurement belongs to the python on the other
// end. Effects are named, pots are named, and nothing here is
// addressed by a number that could quietly come to mean something else.
//
// Build: see ../Makefile. It needs effect_map.h and the three math
// tables generated first, which the Makefile does into bench/gen/.
//
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <stdbool.h>
#include <stdarg.h>
#include <string.h>
#include <strings.h>
#ifdef __SSE__
#include <xmmintrin.h>
#endif
//
// The two fake register blocks the shim headers declare. Defined here
// rather than in a header of their own because they are storage, and
// this is the only translation unit there is - the same bargain the
// firmware makes with pedal.c.
//
#include "pico/stdlib.h"
#include "hardware/dma.h"
#include "hardware/timer.h"
struct bench_dma bench_dma_regs;
struct bench_timer bench_timer_regs;
//
// Everything below is pedal.c's include list, in pedal.c's order,
// stopping at the point where the pedal starts being a pedal. Include
// order is program order in this codebase, so the order matters and the
// omissions are deliberate: no tinyusb, no i2c, no PIO, no scene
// storage, no UI. None of them is between an input sample and an
// output one.
//
#include "status.h"
#include "Audio/types.h"
#include "Audio/util.h"
#include "Audio/envelope.h"
#include "Audio/single-pole.h"
#include "Audio/biquad.h"
#include "Audio/fft.h"
#include "Audio/analyze.h"
//
// Two of the things audio/effect.h expects to already exist - see the
// contract at the top of it. Only tuner_mode is reachable from the
// audio path, and process_input() diverts the whole signal into the
// tuner when it is set, so it is held at zero here for the same reason
// the tuner is not built: a bench measuring an effect is not measuring
// the tuner. user_interaction is written by hardware.h and read by
// nothing on this side.
//
static int tuner_mode = 0;
static volatile int user_interaction = 0;
#include "Audio/effect.h"
uint8_t effect_chain[MAX_ROUTED_EFFECTS];
uint8_t routed_effect_count = 0;
#include "effect-state.h"
//
// Declared by audio/effect.h and defined by usb-audio.h on the pedal.
// There is no USB here, and USB audio input is a separate question from
// what an effect does to a signal, so it is silence.
//
sample_t get_usb_audio_input(void)
{
sample_t zero = { 0.0f, 0.0f };
return zero;
}
static void die(const char *fmt, ...)
__attribute__((format(printf, 1, 2), noreturn));
static void die(const char *fmt, ...)
{
va_list ap;
va_start(ap, fmt);
vfprintf(stderr, fmt, ap);
va_end(ap);
exit(1);
}
//
// Naming things
//
// Effects are addressed by their display name and pots by their label,
// because those are the two strings that actually exist at runtime.
// The short name - [ECHO], [CHAIN] - is deliberately not in struct
// effect: it is the effect's name in the generated C, and
// audio/effect.h says outright that storing it "cost a pointer per
// effect for nobody". Adding one so a test could say ECHO would be
// paying that cost after all, to spell a name the display string
// already spells.
//
// Matching is case-insensitive, exact first and then unique substring,
// so 'echo' finds "Tape Echo" while an ambiguous fragment is an error
// with the candidates in it rather than a guess.
//
static bool name_hit(const char *haystack, const char *needle, bool *exact)
{
*exact = !strcasecmp(haystack, needle);
if (*exact)
return true;
for (const char *p = haystack; *p; p++) {
if (!strncasecmp(p, needle, strlen(needle)))
return true;
}
return false;
}
//
// Labels may carry leading spaces for indentation in the app -
// settings.h has " ATTN" - so a name on a command line should not have
// to reproduce the layout.
//
static const char *unpad(const char *s)
{
while (*s == ' ')
s++;
return s;
}
static struct effect *find_effect(const char *name)
{
struct effect *hit = NULL;
int hits = 0;
for (int i = 0; i < (int)ARRAY_SIZE(effects); i++) {
bool exact;
if (!name_hit(effects[i]->name, name, &exact))
continue;
if (exact)
return effects[i];
hit = effects[i];
hits++;
}
if (hits == 1)
return hit;
fprintf(stderr, hits ? "'%s' is ambiguous. It could be:\n"
: "There is no effect called '%s'. There is:\n",
name);
for (int i = 0; i < (int)ARRAY_SIZE(effects); i++) {
bool exact;
if (!hits || name_hit(effects[i]->name, name, &exact))
fprintf(stderr, " %s\n", effects[i]->name);
}
exit(1);
}
static int find_pot(struct effect *e, const char *label)
{
int hit = -1, hits = 0;
for (int i = 0; i < 10; i++) {
bool exact;
if (!e->pots[i].label)
continue;
if (!name_hit(unpad(e->pots[i].label), label, &exact))
continue;
if (exact)
return i;
hit = i;
hits++;
}
if (hits == 1)
return hit;
fprintf(stderr, hits ? "'%s' is ambiguous on %s. Its pots are:\n"
: "%s has no pot called '%s'. It has:\n",
hits ? label : e->name, hits ? e->name : label);
for (int i = 0; i < 10; i++) {
if (e->pots[i].label)
fprintf(stderr, " %s\n", unpad(e->pots[i].label));
}
exit(1);
}
static unsigned char pot_value(struct effect *e, int idx, const char *text)
{
char *end;
long v = strtol(text, &end, 0);
if (end == text || *end)
die("'%s' is not a number\n", text);
if (v < 0 || v > max_pot_val(e, idx))
die("%s / %s takes 0..%d, not %ld\n",
e->name, unpad(e->pots[idx].label), max_pot_val(e, idx), v);
return (unsigned char) v;
}
//
// --list, which is how you find out what the other options accept.
//
static void list_effects(void)
{
for (int i = 0; i < (int)ARRAY_SIZE(effects); i++) {
struct effect *e = effects[i];
printf("%s%s [mix: %s]\n", e->name,
effect_always_runs(i) ? " (always runs)" : "",
e->no_mix ? "none" :
e->mix_law == MIX_POWER ? "equal power" : "linear");
for (int p = 0; p < 10; p++) {
if (!e->pots[p].label)
continue;
printf(" %-14s 0..%-4d default %-4d %s\n",
unpad(e->pots[p].label), max_pot_val(e, p),
e->pots[p].def_val,
e->pots[p].unit ? e->pots[p].unit : "");
}
}
}
//
// The two ends of the converter, run backwards and forwards.
//
// process_input() and convert_output() are deliberately not inverses of
// each other - the 1.2198 asymmetry in audio/process.h is exactly what
// makes the peak of the internal float equal the RMS volts of a sine -
// so undoing one with the other would put a silent 1.7dB in the middle
// of every measurement. Each is undone with itself.
//
// So stdin and stdout are both in the pedal's *internal* float scale,
// where 1.0 is one volt RMS of sine and every dB an effect is marked in
// is measured. A transparent chain is then a bit-exact pipe, which is
// the property the negative control checks.
//
// The input can carry a little over full scale (2^31 x the multiplier
// is 1.2198) and the output cannot: convert_output() pins anything at
// or past 1.0 and says so in 'output_clipped'. That asymmetry is the
// pedal's, not the bench's, and the summary on stderr is how a
// measurement finds out it happened.
//
#define RAW_FULL_SCALE 2147483647.0f
static s32 float_to_raw(float v)
{
float scaled = v / (float)SAMPLE_TO_FLOAT_MULTIPLIER;
if (scaled >= RAW_FULL_SCALE)
return INT32_MAX;
if (scaled <= -RAW_FULL_SCALE)
return INT32_MIN;
return (s32) scaled;
}
static float raw_to_float(s32 v)
{
return v * (float)(1.0 / 2147483648.0);
}
//
// Asking a primitive directly.
//
// Everything above runs a signal through the audio path. This does not:
// it maps one float to one float through a single function out of
// audio/util.h, so that the fast approximations can be characterised
// against double-precision arithmetic without an effect's own
// distortion sitting on top of the answer.
//
// It is deliberately a separate mode with a separate wire format - mono
// floats rather than stereo frames - because it is not audio and should
// not be mistaken for it in a capture file.
//
// Note that the sine here is fastsincos(), which is not the same code as
// the sine the test tone generates: lfo_step() interpolates the quarter
// table itself. Two implementations of one idea, so both are worth
// asking, and the tone generator is asked by running [TESTTONE].
//
static float map_tanh(float x) { return tanhf(x); }
static float map_sin(float x) { return fastsincos(x).sin; }
static float map_cos(float x) { return fastsincos(x).cos; }
static float map_pow2(float x) { return pow2(x); }
static float map_log2(float x) { return log2f(x); }
static float map_exp(float x) { return expf(x); }
static float map_db(float x) { return db_to_level(x); }
static float map_dbA(float x) { return db_to_A(x); }
static float map_tc(float x) { return time_constant(x); }
//
// The two single-pole helpers hand back a one-field struct, so the
// field is what there is to look at: 'alpha' is the whole filter.
//
static float map_sp_freq(float x) { return single_pole_freq(x).alpha; }
static float map_sp_time(float x) { return single_pole_time(x).alpha; }
//
// Not an approximation at all, and here because the pot curves are the
// one place where being exactly right at the ends matters - a pot that
// cannot reach its own declared maximum is a different bug from an
// inaccurate one.
//
static float map_pot_linear(float x) { return POT_TO_FLOAT(x); }
static float map_pot_cubic(float x) { return cubic(POT_TO_FLOAT(x), 0.0f, 1.0f); }
static const struct {
const char *name;
float (*fn)(float);
} maps[] = {
{ "tanh", map_tanh },
{ "sin", map_sin },
{ "cos", map_cos },
{ "pow2", map_pow2 },
{ "log2", map_log2 },
{ "exp", map_exp },
{ "db_to_level", map_db },
{ "db_to_A", map_dbA },
{ "time_constant", map_tc },
{ "single_pole_freq", map_sp_freq },
{ "single_pole_time", map_sp_time },
{ "pot_to_float", map_pot_linear },
{ "pot_cubic", map_pot_cubic },
};
static int run_map(const char *name)
{
float (*fn)(float) = NULL;
float x;
for (int i = 0; i < (int)ARRAY_SIZE(maps); i++) {
if (!strcasecmp(maps[i].name, name))
fn = maps[i].fn;
}
if (!fn) {
fprintf(stderr, "no such primitive '%s'. There is:\n", name);
for (int i = 0; i < (int)ARRAY_SIZE(maps); i++)
fprintf(stderr, " %s\n", maps[i].name);
return 1;
}
while (fread(&x, sizeof(x), 1, stdin) == 1) {
float y = fn(x);
if (fwrite(&y, sizeof(y), 1, stdout) != 1)
die("short write\n");
}
fflush(stdout);
return 0;
}
//
// One "cpu" sample period.
//
// Everything here is what the DMA engine would have done, and nothing
// here is what single_sample() does - that is the point. The receive
// pointer goes one slot ahead of the cpu so the spin exits immediately,
// and the transmit pointer stays half a ring behind so the deadline
// check in single_sample() stays false and samples_dropped keeps
// meaning something.
//
static void bench_sample(raw_sample_t in, raw_sample_t *out)
{
unsigned slot = cpu_idx;
i2s_dma_buf[slot] = in;
bench_dma_regs.ch[dma_rx].write_addr = (uintptr_t)&i2s_dma_buf[(slot + 1) & 15];
bench_dma_regs.ch[dma_tx].read_addr = (uintptr_t)&i2s_dma_buf[(slot + 8) & 15];
//
// 20.83us a sample at 48kHz. The load meter reads the difference
// across its own spin, which is zero here, so it will report a
// fully loaded core. That is honest - this "core" never waits -
// and it is not audio either way.
//
bench_timer_regs.timerawl += 21;
single_sample(1.0f);
*out = i2s_dma_buf[slot];
}
int main(int argc, char **argv)
{
struct { struct effect *e; int pot; unsigned char val; } pots[64];
struct { struct effect *e; unsigned char val; } mixes[32];
struct effect *route[MAX_ROUTED_EFFECTS];
int nr_pots = 0, nr_mixes = 0, nr_route = 0;
routing_bitmap_t routable;
unsigned long frames = 0;
#ifdef __SSE__
//
// The pedal sets FZ in FPSCR on both cores - see enable_ftz() in
// pedal.c - so subnormal results flush to zero. Do the same here,
// or a reverb tail decaying past 1e-38 takes a different path on
// the two machines and the bench stops being the pedal.
//
_mm_setcsr(_mm_getcsr() | 0x8040); /* FTZ | DAZ */
#endif
for (int i = 1; i < argc; i++) {
const char *arg = argv[i];
char buf[256], *eq, *colon;
if (!strcmp(arg, "--list")) {
list_effects();
return 0;
}
if (i + 1 >= argc)
die("%s wants a value\n", arg);
snprintf(buf, sizeof(buf), "%s", argv[++i]);
//
// Before any of the effect setup, because it does not use
// any of it - see run_map().
//
if (!strcmp(arg, "--map"))
return run_map(buf);
if (!strcmp(arg, "--route")) {
if (nr_route >= MAX_ROUTED_EFFECTS)
die("a chain holds %d effects\n", MAX_ROUTED_EFFECTS);
route[nr_route++] = find_effect(buf);
continue;
}
eq = strchr(buf, '=');
if (!eq)
die("%s wants a NAME=VALUE\n", arg);
*eq++ = 0;
if (!strcmp(arg, "--mix")) {
if (nr_mixes >= (int)ARRAY_SIZE(mixes))
die("too many --mix\n");
mixes[nr_mixes].e = find_effect(buf);
mixes[nr_mixes].val = pot_value(mixes[nr_mixes].e, 0, eq);
nr_mixes++;
continue;
}
if (!strcmp(arg, "--pot")) {
if (nr_pots >= (int)ARRAY_SIZE(pots))
die("too many --pot\n");
colon = strrchr(buf, ':');
if (!colon)
die("--pot wants EFFECT:POT=VALUE\n");
*colon++ = 0;
pots[nr_pots].e = find_effect(buf);
pots[nr_pots].pot = find_pot(pots[nr_pots].e, colon);
pots[nr_pots].val = pot_value(pots[nr_pots].e,
pots[nr_pots].pot, eq);
nr_pots++;
continue;
}
die("unknown option %s\n", arg);
}
//
// Bring the effects up the way pedal.c's init_effects() does:
// every effect reset to its declared defaults, the routing, the
// pots, then one init() per effect. What is skipped is
// load_globals() and load_scene(), which read an eeprom that is
// not here - so the starting point is a pedal that has never been
// saved to, which is exactly the reproducible one.
//
for (int i = 0; i < (int)ARRAY_SIZE(effects); i++)
reset_effect(effects[i]);
routable = routing_start();
for (int i = 0; i < nr_route; i++) {
uint8_t id = 0;
while (id < EFFECT_COUNT && effects[id] != route[i])
id++;
if (!routing_add(&routable, id))
die("cannot route %s: already routed, or it is not routable\n",
route[i]->name);
}
routing_end(routable);
//
// After the routing, because routing_end() unroutes everything
// that was not asked for and unroute_effect() puts every pot back
// to its default on the way past.
//
for (int i = 0; i < nr_pots; i++)
set_effect_pot(pots[i].e, pots[i].pot, pots[i].val);
for (int i = 0; i < nr_mixes; i++)
set_effect_mix(mixes[i].e, mixes[i].val);
for (int i = 0; i < (int)ARRAY_SIZE(effects); i++) {
struct effect *e = effects[i];
e->last = e->seq;
e->init(effect_pots(e));
}
init_meters();
//
// And then it is just a pipe. A frame at a time rather than a
// buffer, because the ring is sixteen slots deep and the point is
// to stay inside it; the cost is irrelevant next to what python
// does with the answer.
//
for (;;) {
float frame[2];
raw_sample_t in, out;
if (fread(frame, sizeof(frame), 1, stdin) != 1)
break;
in.left = float_to_raw(frame[0]);
in.right = float_to_raw(frame[1]);
bench_sample(in, &out);
frames++;
frame[0] = raw_to_float(out.left);
frame[1] = raw_to_float(out.right);
if (fwrite(frame, sizeof(frame), 1, stdout) != 1)
die("short write\n");
}
fflush(stdout);
//
// The conditions the numbers were taken under, on stderr where
// they cannot be mistaken for signal. 'clipped' and 'dropped'
// are the two that invalidate a measurement rather than merely
// describing it, and 'fade' is how many frames at the front of the
// capture are an effect ramping in rather than an effect.
//
fprintf(stderr,
"frames %lu clipped %u dropped %u fade %d\n"
"in %.6f floor %.6f out %.6f\n",
frames, output_clipped, samples_dropped, EFF_ENABLE_STEPS,
meter_in, meter_floor, meter_out);
return 0;
}