0
mirror of https://github.com/torvalds/GuitarPedal.git synced 2026-08-18 13:13:35 +00:00
Files
torvalds-GuitarPedal/Audio/biquad.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

229 lines
7.0 KiB
C

//
// Calculate biquad coefficients
//
struct biquad_coeff {
float b0, b1, b2;
float a1, a2;
};
struct biquad_state {
float x[2], y[2];
};
struct biquad {
struct biquad_coeff coeff;
struct biquad_state state;
};
// Direct form 1 may need more state than the "canonical" DF2,
// but gets noisy much quicker.
static inline float biquad_step_df1(const struct biquad_coeff *c, float in, float x[2], float y[2])
{
float out = c->b0*in + c->b1*x[0] + c->b2*x[1] - c->a1*y[0] - c->a2*y[1];
x[1] = x[0]; x[0] = in;
y[1] = y[0]; y[0] = out;
return out;
}
// A peaking biquad has a1==b1 and can avoid one multiply
static inline float biquad_peaking_step_df1(const struct biquad_coeff *c, float in, float x[2], float y[2])
{
float out = c->b0*in + c->b1*(x[0]-y[0]) + c->b2*x[1] - c->a2*y[1];
x[1] = x[0]; x[0] = in;
y[1] = y[0]; y[0] = out;
return out;
}
static inline float _biquad_peaking_step(const struct biquad_coeff *c, struct biquad_state *s, float x0)
{
return biquad_peaking_step_df1(c, x0, s->x, s->y);
}
static inline float _biquad_step(const struct biquad_coeff *c, struct biquad_state *s, float x0)
{
return biquad_step_df1(c, x0, s->x, s->y);
}
//
// Every constructor below places its poles and zeros with -2*cos(w0), so
// what it needs from the sine table is an *angle*, and cos is the worst
// possible way to ask for one down here.
//
// fastsincos() interpolates 256 entries a quarter linearly. Near zero
// phase the sine is straight and the chord is nearly exact; the cosine
// is at its maximum, where the chord sits furthest under the arc. And
// d(cos)/dw = -sin(w0), which vanishes exactly where that error is
// worst, so the angle recovered from the cosine is off by err/sin(w0).
// Asking for 20Hz got a filter at 30.7Hz, and 60Hz got 63.6Hz.
//
// So take the half angle, whose sine is the well-behaved one, and come
// back with the double-angle identities. Both come out of the one table
// lookup that was being done anyway. cos(w) is now built from a squared
// sine, which is the whole point: it carries the angle where cos does
// not. sin(w) is not fussy - it ends up in alpha, where it sets the
// bandwidth rather than the frequency - so the table's own cosine is
// good enough for that half.
//
// single-pole.h asks for the same half angle for its own reasons.
//
static inline struct sincos biquad_w0(float freq)
{
struct sincos h = fastsincos(0.5f * freq / SAMPLES_PER_SEC);
struct sincos w0;
w0.sin = 2 * h.sin * h.cos;
w0.cos = 1 - 2 * h.sin * h.sin;
return w0;
}
static inline void _biquad_lpf(struct biquad_coeff *res, float freq, float Q)
{
const struct sincos w0 = biquad_w0(freq);
float alpha = w0.sin/(2*Q);
float a0_inv = 1/(1 + alpha);
float b1 = (1 - w0.cos) * a0_inv;
res->b0 = b1 / 2;
res->b1 = b1;
res->b2 = b1 / 2;
res->a1 = -2*w0.cos * a0_inv;
res->a2 = (1 - alpha) * a0_inv;
}
static inline void _biquad_hpf(struct biquad_coeff *res, float freq, float Q)
{
const struct sincos w0 = biquad_w0(freq);
float alpha = w0.sin/(2*Q);
float a0_inv = 1/(1 + alpha);
float b1 = (1 + w0.cos) * a0_inv;
res->b0 = b1 / 2;
res->b1 = -b1;
res->b2 = b1 / 2;
res->a1 = -2*w0.cos * a0_inv;
res->a2 = (1 - alpha) * a0_inv;
}
static inline void _biquad_notch_filter(struct biquad_coeff *res, float freq, float Q)
{
const struct sincos w0 = biquad_w0(freq);
float alpha = w0.sin/(2*Q);
float a0_inv = 1/(1 + alpha);
res->b0 = 1 * a0_inv;
res->b1 = -2*w0.cos * a0_inv;
res->b2 = 1 * a0_inv;
res->a1 = -2*w0.cos * a0_inv;
res->a2 = (1 - alpha) * a0_inv;
}
static inline void _biquad_bpf_peak(struct biquad_coeff *res, float freq, float Q)
{
const struct sincos w0 = biquad_w0(freq);
float alpha = w0.sin/(2*Q);
float a0_inv = 1/(1 + alpha);
res->b0 = Q*alpha * a0_inv;
res->b1 = 0;
res->b2 = -Q*alpha * a0_inv;
res->a1 = -2*w0.cos * a0_inv;
res->a2 = (1 - alpha) * a0_inv;
}
static inline void _biquad_bpf(struct biquad_coeff *res, float freq, float Q)
{
const struct sincos w0 = biquad_w0(freq);
float alpha = w0.sin/(2*Q);
float a0_inv = 1/(1 + alpha);
res->b0 = alpha * a0_inv;
res->b1 = 0;
res->b2 = -alpha * a0_inv;
res->a1 = -2*w0.cos * a0_inv;
res->a2 = (1 - alpha) * a0_inv;
}
static inline void _biquad_allpass_filter(struct biquad_coeff *res, float freq, float Q)
{
const struct sincos w0 = biquad_w0(freq);
float alpha = w0.sin/(2*Q);
float a0_inv = 1/(1 + alpha);
res->b0 = (1 - alpha) * a0_inv;
res->b1 = (-2*w0.cos) * a0_inv;
res->b2 = 1; // Same as a0
res->a1 = res->b1;
res->a2 = res->b0;
}
//
// 'A' is the cookbook's A: the square root of the linear gain, so a
// peaking section boosts by A*A and a shelf reaches A*A. db_to_A() is
// where one comes from.
//
// These took the gain itself and square-rooted it here, which reads more
// naturally and was a mistake. The units of the argument were invisible
// - the app's copy of these same formulas takes A - and two call sites
// had grown fudge factors from guessing which, in opposite directions.
//
static inline void _biquad_peaking(struct biquad_coeff *res, float freq, float Q, float A)
{
const struct sincos w0 = biquad_w0(freq);
float alpha = w0.sin/(2*Q);
float a0_inv = 1 / (1 + alpha/A);
res->b0 = (1 + alpha*A) * a0_inv;
res->b1 = (-2*w0.cos) * a0_inv;
res->b2 = (1 - alpha*A) * a0_inv;
res->a1 = res->b1;
res->a2 = (1 - alpha/A) * a0_inv;
}
static inline void _biquad_loshelf(struct biquad_coeff *res, float freq, float Q, float A)
{
const struct sincos w0 = biquad_w0(freq);
float alpha = w0.sin/(2*Q);
float ap1 = A + 1;
float am1 = A - 1;
float sqAmin2 = 2 * sqrtf(A) * alpha;
float a0_inv = 1 / (ap1 + am1*w0.cos + sqAmin2);
res->b0 = A * (ap1 - am1*w0.cos + sqAmin2) * a0_inv;
res->b1 = 2*A* (am1 - ap1*w0.cos) * a0_inv;
res->b2 = A * (ap1 - am1*w0.cos - sqAmin2) * a0_inv;
res->a1 = -2*(am1 + ap1*w0.cos) * a0_inv;
res->a2 = (ap1 + am1*w0.cos - sqAmin2) * a0_inv;
}
static inline void _biquad_hishelf(struct biquad_coeff *res, float freq, float Q, float A)
{
const struct sincos w0 = biquad_w0(freq);
float alpha = w0.sin/(2*Q);
float ap1 = A + 1;
float am1 = A - 1;
float sqAmin2 = 2 * sqrtf(A) * alpha;
float a0_inv = 1 / (ap1 - am1*w0.cos + sqAmin2);
res->b0 = A * (ap1 + am1*w0.cos + sqAmin2) * a0_inv;
res->b1 =-2*A* (am1 + ap1*w0.cos) * a0_inv;
res->b2 = A * (ap1 + am1*w0.cos - sqAmin2) * a0_inv;
res->a1 = 2*(am1 - ap1*w0.cos) * a0_inv;
res->a2 = (ap1 - am1*w0.cos - sqAmin2) * a0_inv;
}
static inline float biquad_step(struct biquad *bq, float x0)
{ return _biquad_step(&bq->coeff, &bq->state, x0); }
#define biquad_lpf(bq,f,Q) _biquad_lpf(&(bq)->coeff,f,Q)
#define biquad_hpf(bq,f,Q) _biquad_hpf(&(bq)->coeff,f,Q)
#define biquad_notch_filter(bq,f,Q) _biquad_notch_filter(&(bq)->coeff,f,Q)
#define biquad_bpf_peak(bq,f,Q) _biquad_bpf_peak(&(bq)->coeff,f,Q)
#define biquad_bpf(bq,f,Q) _biquad_bpf(&(bq)->coeff,f,Q)
#define biquad_allpass_filter(bq,f,Q) _biquad_allpass_filter(&(bq)->coeff,f,Q)
#define biquad_peaking(bq,f,Q,A) _biquad_peaking(&(bq)->coeff,f,Q,A)
#define biquad_lowshelf(bq,f,Q,A) _biquad_loshelf(&(bq)->coeff,f,Q,A)
#define biquad_highshelf(bq,f,Q,A) _biquad_hishelf(&(bq)->coeff,f,Q,A)