0
mirror of https://github.com/torvalds/GuitarPedal.git synced 2026-08-19 05:33:46 +00:00
Files
torvalds-GuitarPedal/Audio/process.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

216 lines
7.5 KiB
C

//
// Do the 'sample to float' and 'float to sample' processing
// together with basic noise gating
//
#define FLOAT_TO_SAMPLE_MULTIPLIER (0x80000000 / 1.0)
// Random buffer size. Note that we only expose
// half the data in the buffer so that we don't
// need to worry about new input overwriting
// the part of the buffer we're looking at.
//
// 256 samples is about 5ms worth of data at 48kHz
#define USB_OUTPUT_SIZE 512
#define USB_OUTPUT_MASK (USB_OUTPUT_SIZE-1)
static struct {
unsigned phase;
unsigned head, tail;
raw_sample_t buf[USB_OUTPUT_SIZE];
} usb_output;
//
// The internal scale: 1.0 is one volt RMS of sine.
//
// The codec's full scale is what sets this, and it is the same at
// both ends. tac5112.h puts VREF at 2.75V (register 0x4d) and
// runs everything single-ended, and at that VREF the TAC5242 is
// 2Vrms differential, so 1Vrms single-ended - in and out alike.
//
// So a full-scale sample is a 1Vrms sine, which peaks at 1.4142V,
// and 'raw / 2^31' is already the internal float we want: its peak
// equals the RMS volts of a sine. A 1Vrms sine peaks at 1.0
// internally, which is what "0dBFS = 1Vrms" means and what
// level_to_dbfs() reports against. No correction factor.
//
// It used to have one:
//
// #define SAMPLE_TO_FLOAT_MULTIPLIER (3.45 / 2.82843 / 0x80000000)
//
// on the grounds that the input reached full scale at 3.45Vpp while
// the output produced 2.828Vpp, and the comment here said outright
// that it existed "to correct for whatever I'm doing wrong". The
// two ends really are symmetric, so there was nothing to correct
// and the correction was itself the error: it put 1.725dB of gain
// between the input and the output, which is a pedal that is not
// unity gain even bypassed.
//
// Measured, output patched back to input on one board: the round
// trip read +1.499dB before this and -0.226dB after, and the
// remainder is real analog loss rather than arithmetic. See
// Validation/test-analog.py.
//
// The observation that constant was justified with is still true
// and is worth keeping: a 90mVpp 110Hz sine is 31.8mVrms and reads
// -30dBFS, on an early TAC5112 board and a current TAC5242 one
// alike. It was the *inference* that was wrong. That reading says
// the internal peak equals the input's RMS volts, which is one
// equation in two unknowns - it is satisfied by 3.45Vpp full scale
// with a 1.2198 multiplier and by 2.828Vpp with no multiplier at
// all, since 2.828 x 1.2198 is 3.45. The datasheet picks between
// them, and picks the second.
//
// Measured directly once the multiplier was gone, which is when the
// question stops being degenerate: with this at 1.0 the captured
// value *is* Vpeak/Vfs_peak, so a known input reads the full scale
// off. A 500mVpp 440Hz sine from a signal generator:
//
// reads -15.22 dBFS
// implied full scale 2.8855Vpp = 1.0202Vrms
// datasheet 1.0000Vrms (+0.17dB)
//
// and the old 3.45Vpp figure would have put that same input at
// -16.78 dBFS, which is 1.55dB from where it actually landed. The
// remaining 2% is inside a bench generator's own accuracy and is
// not worth chasing further.
//
// Swept with the same generator, 500mVpp, and the whole input is one
// high-pass and nothing else:
//
// 20 Hz -2.73 dB 4000 Hz -0.04 dB
// 40 Hz -0.93 dB 10000 Hz -0.09 dB
// 160 Hz -0.08 dB 16000 Hz -0.05 dB
// 440 Hz 0.00 dB
// 1000 Hz +0.02 dB
//
// A single pole at 18.6Hz fits all eight points with an rms error of
// 0.036dB, and a two-pole fit is twice as bad and biased - so whichever
// of the board's coupling cap and the codec's own AC coupling is
// higher, the other is far enough down to be invisible. From 160Hz to
// 16kHz the spread is 0.109dB peak to peak, which is the repeatability
// of the measurement rather than anything the pedal is doing: there is
// no high-frequency rolloff below Nyquist at all.
//
// What that says about the low end, which was the open question: -0.22dB
// at a guitar's low E, -0.81dB at a bass low E, -1.35dB at a five-string
// low B. The corner is below the bottom of hearing.
//
// The scale holds at a second amplitude: 2Vpp at 440Hz implies 1.0218
// Vrms against 500mVpp's 1.0202, the same to 0.014dB across a 4x change.
// That rules out a generator offset and any nonlinearity, and does not
// rule out a proportional generator error - a bench source reading 2%
// low everywhere looks exactly like this, and separating the two wants a
// meter on the generator rather than more captures.
//
// It also accounts for the round trip. The DAC and the ADC are
// specified alike but measure 2% apart, so sending a signal out and
// reading it back loses that 2% - which is the -0.226dB
// test-analog.py reports, leaving the cable itself essentially
// lossless rather than the -0.23dB being a cable at all.
//
#define SAMPLE_TO_FLOAT_MULTIPLIER (1.0 / 0x80000000)
static inline sample_t process_input(raw_sample_t sample)
{
sample_t val = {
.left = sample.left * SAMPLE_TO_FLOAT_MULTIPLIER,
.right = sample.right * SAMPLE_TO_FLOAT_MULTIPLIER
};
if (tuner_mode) {
analyze_process_sample(val);
val.left = val.right = 0.0;
}
return val;
}
//
// Convert a nominal -1.0..1.0 signal to a full-scale s32 sample.
//
// Anything at or past full scale gets pinned to the end of the range.
// The test has to be on the *input*: the FP->int conversion is only
// defined for values that already fit, and testing the result can't
// work anyway, since it comes back around into range at +-2.0, +-4.0...
//
// Below full scale there's nothing to worry about: the largest float
// under 1.0 is (1 - 2^-24), so the scaled value tops out at 2^31 - 128
// and the conversion cannot overflow.
//
static inline s32 convert_output(float out)
{
if (fabsf(out) >= 1.0f) {
output_clipped = 1;
return out > 0.0f ? INT32_MAX : INT32_MIN;
}
return lrintf(out * FLOAT_TO_SAMPLE_MULTIPLIER);
}
static inline raw_sample_t process_output(sample_t out, raw_sample_t dry)
{
raw_sample_t wet = {
.left = convert_output(out.left),
.right = convert_output(out.right)
};
raw_sample_t usb;
switch (settings.usb_output) {
case LR_None: return wet;
case LR_Wet: usb = wet; break;
case LR_Dry: usb = dry; break;
default: usb.left = wet.left; usb.right = dry.left; break;
}
unsigned head = usb_output.head;
// Store the sample, *then* publish it. The other way round - which
// is what this used to do - lets cpu0 see the new head and read the
// slot before cpu1 has written it.
usb_output.buf[head & USB_OUTPUT_MASK] = usb;
smp_store_release(&usb_output.head, head + 1);
return wet;
}
static inline unsigned output_buffer_size(void)
{
unsigned nr = usb_output.head - usb_output.tail;
if (nr > USB_OUTPUT_SIZE/2)
nr = USB_OUTPUT_SIZE/2;
return nr;
}
static inline unsigned get_output_samples(s32 *buffer, unsigned nr)
{
unsigned head = smp_load_acquire(&usb_output.head);
unsigned tail = usb_output.tail;
// If more than 75% of the buffer is filled, we
// have lost sync, and we will just restart at
// the half buffer mark.
unsigned max = head - tail;
if (max > 3 * USB_OUTPUT_SIZE / 4) {
max = USB_OUTPUT_SIZE / 2;
tail = head - max;
}
// This is the max we'll copy
//
// Note that we keep 'output.tail' as
// the full 32-bit value so that we can
// tell if the head has gone way past.
if (nr > max)
nr = max;
usb_output.tail = tail + nr;
tail &= USB_OUTPUT_MASK;
unsigned batch = nr;
if (tail + batch > USB_OUTPUT_SIZE) {
batch = USB_OUTPUT_SIZE - tail;
memcpy(buffer, usb_output.buf + tail, batch * sizeof(raw_sample_t));
buffer += batch * 2;
batch = nr - batch;
tail = 0;
}
memcpy(buffer, usb_output.buf + tail, batch * sizeof(raw_sample_t));
return nr;
}