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>
251 lines
11 KiB
C
251 lines
11 KiB
C
// NAME: Signal Chain [CHAIN]
|
|
// PRIORITY: 0 (Special: always runs first, and is never in effect_chain)
|
|
// MIX: NONE // not an effect - it is the two ends of the chain
|
|
// POT: "Gate" LINEAR(-100.0 -40.0) = -70.0 dB
|
|
// INFO: Everything quieter than this is silenced. Set it just above the
|
|
// INFO: noise floor shown below. Fully down switches the gate off.
|
|
// POT: "Attack" LINEAR(0.0 10.0) = 1.5 ms
|
|
// INFO: How fast the gate opens once you play. Too slow eats the pick
|
|
// INFO: attack; too fast lets the noise through between notes.
|
|
// POT: "Release" LINEAR(50.0 500.0) = 150.0 ms
|
|
// INFO: How long it waits before closing again. Too short chops the
|
|
// INFO: tail off a chord as it decays.
|
|
// POT: "Trim" LINEAR(-20.0 20.0) = 0.0 dB
|
|
// INFO: Brings your pickup up to the level the rest of the chain is
|
|
// INFO: calibrated for, so every effect's dB markings mean what they
|
|
// INFO: say. Set it once, by the meters, and leave it.
|
|
// POT: "Volume" LINEAR(-40.0 20.0) = 0.0 dB
|
|
// INFO: How loud the pedal is, applied right at the end and changing
|
|
// INFO: nothing about the sound. Wanting more out of the pedal is not
|
|
// INFO: a reason to turn Trim up. Fully down is silence.
|
|
//
|
|
// The beginning and the end of the signal chain.
|
|
//
|
|
// This is not an effect and does not pretend to be one. It is never in
|
|
// effect_chain[], ROUTABLE_EFFECTS excludes it, single_sample() calls it
|
|
// directly rather than through do_effect_step(), and it has no wet and no
|
|
// dry - a gain stage cannot be blended against itself, because half of a
|
|
// gain plus half of no gain is a different gain rather than less of one.
|
|
// Hence 'MIX: NONE'.
|
|
//
|
|
// Trim and Volume are the two ends of it, and they are two controls
|
|
// rather than one because the chain is full of things that are not
|
|
// linear. Where you sit on a triode curve, where the boost starts
|
|
// folding, how far over the compressor's threshold you are - all of that
|
|
// depends on the absolute level going in, and the whole DSP is calibrated
|
|
// to a 1Vrms internal scale (see process.h) that any given guitar may
|
|
// miss by 20dB in either direction. Trim is what puts a pickup onto
|
|
// that scale, once, so that every effect's dB markings mean what their
|
|
// author intended. Volume is then what makes it as loud as you want
|
|
// without disturbing any of it - which is why it goes above unity too,
|
|
// and not only down: needing more out of the pedal is not a reason to
|
|
// drive the chain harder.
|
|
//
|
|
// Turn Trim up and Volume down by the same amount and you hear the
|
|
// non-linearity by itself with the loudness held still, which is the only
|
|
// honest way to judge it - louder always sounds better.
|
|
//
|
|
// The gate measures ahead of Trim deliberately - see chain_step()
|
|
// for why. Trim is about the chain that follows; the noise the gate
|
|
// exists to cut is a property of the room and the pickup, and does not
|
|
// move when trim does.
|
|
//
|
|
// Note that it can only be about noise arriving at the *input*. A gate
|
|
// at the front of the chain can do nothing about hiss produced further
|
|
// down it, which is why real rigs often put one in an amp's effects
|
|
// loop. That costs us nothing today, because the chain is float
|
|
// arithmetic and adds no noise of its own - the converter's floor is the
|
|
// only floor there is. If a preamp model ever gains modelled noise, the
|
|
// answer is a second gate at the far end rather than moving this one.
|
|
//
|
|
// This leaves 'mix', 'target', 'dry' and 'wet' unused on this effect.
|
|
// reset_effect() and the eeprom loader still set them; nothing ever reads
|
|
// them. Not worth special-casing the loaders over.
|
|
//
|
|
// Same envelope calculations as the compressor. I wonder if
|
|
// I should just have a combined noise gate / compressor thing?
|
|
//
|
|
// But the attack/release values are probably different.
|
|
//
|
|
// A bigger doubt about the same code, written down because it is the
|
|
// kind of thing that is otherwise only ever in somebody's head: the
|
|
// gate is a *binary* decision on the envelope, and the ramp in
|
|
// chain_step() exists to smooth that decision. It could instead have
|
|
// been a multiplier that follows the envelope directly and clamps at
|
|
// unity above the threshold, with no decision and no ramp in it - and
|
|
// then Attack and Release would genuinely be the fade times rather
|
|
// than the detector's.
|
|
//
|
|
// What has kept it binary is a worry about amplitude modulation: a
|
|
// gain that tracks the envelope closely moves at the signal's own
|
|
// frequency, and that is distortion. The compressor measures exactly
|
|
// that happening, and it falls about 11dB per octave, so it is a bass
|
|
// problem long before it is a guitar one. A fixed ramp cannot do it
|
|
// at all, because its shape does not depend on the level. Untested
|
|
// either way; this is the cautious side of the trade.
|
|
//
|
|
|
|
static struct {
|
|
struct envelope envelope;
|
|
float mult, level;
|
|
bool active;
|
|
|
|
//
|
|
// Both gains are slewed rather than applied as they arrive.
|
|
// Volume is CC 7, so it can be under a fader or an expression
|
|
// pedal and move a long way in a hurry, and a gain that steps
|
|
// instead of gliding clicks. Trim moves rarely and costs the
|
|
// same to do properly.
|
|
//
|
|
float trim, trim_target;
|
|
float volume, volume_target;
|
|
} chain;
|
|
|
|
// How fast the two gains chase their target: ~10ms at 48kHz, as MIX_SLEW
|
|
#define CHAIN_SLEW (1.0f / 512)
|
|
|
|
static inline void chain_init(unsigned char pot[10])
|
|
{
|
|
chain.trim_target = db_to_level(chain_trim_pot(pot));
|
|
|
|
//
|
|
// Gate fully down is off, and not merely very quiet.
|
|
//
|
|
// The bottom of the travel is -100dBV, which is 28uV peak to peak.
|
|
// No signal chain is that clean, so a threshold set there would
|
|
// never close the gate anyway and switching it off changes nothing
|
|
// you can hear. What it does change is what gets *reported*: the
|
|
// telemetry sends the gate multiplier, and a gate that is off has
|
|
// to say 1.0 rather than whatever it last ramped to. So make it
|
|
// an explicit off internally, and let the one control mean both
|
|
// things - a knob whose bottom end is "not at all" is what people
|
|
// expect, and it saves an on/off switch that only ever agreed with
|
|
// where the knob already was.
|
|
//
|
|
chain.active = pot[CHAIN_GATE] != 0;
|
|
|
|
float level_db = chain_gate_pot(pot);
|
|
chain.level = db_to_level(level_db);
|
|
|
|
float attack_ms = chain_attack_pot(pot);
|
|
float release_ms = chain_release_pot(pot);
|
|
envelope_init(&chain.envelope, attack_ms, release_ms);
|
|
|
|
//
|
|
// Volume reads in dB and goes above unity as well as below,
|
|
// because it is the output level and not an attenuator: wanting
|
|
// more out of the pedal should not mean driving the chain harder,
|
|
// which is Trim's job and changes the sound.
|
|
//
|
|
// The bottom of the travel is silence rather than -40dB, so that
|
|
// CC 7 at zero means what a MIDI host means by it. -40dB is a
|
|
// hundredth of an amplitude, so the step from there to nothing is
|
|
// inaudible - which is why the range goes that low rather than
|
|
// stopping at the -20dB that would otherwise be plenty.
|
|
//
|
|
chain.volume_target = pot[CHAIN_VOLUME] ?
|
|
db_to_level(chain_volume_pot(pot)) : 0.0f;
|
|
}
|
|
|
|
//
|
|
// The front of the chain: the gate, and the trim.
|
|
//
|
|
// The envelope is measured on the *untrimmed* input, which is the whole
|
|
// point of the ordering. Both trim and the gate's ramp are scalar
|
|
// multiplies, so they commute and the audio comes out identical either
|
|
// way - the only thing the order decides is what the threshold is
|
|
// compared against, and trim moves the signal and the noise together.
|
|
// Measured after trim, a threshold sitting correctly just above the hum
|
|
// would need raising by exactly however much trim was raised, every
|
|
// time, which is a control whose only job is to undo another one.
|
|
// Measured before, it depends on the room, the pickup and the converter,
|
|
// none of which trim can reach. Which is what a noise gate is about,
|
|
// and why 'Gate' reads in dBFS rather than as a fraction of anything.
|
|
//
|
|
// Also where Volume is slewed, even though single_sample() is what
|
|
// applies it at the far end. This runs once per sample and that is all a
|
|
// slew needs, and it beats teaching the tail of the chain how to chase a
|
|
// target of its own.
|
|
//
|
|
static inline sample_t chain_step(sample_t in)
|
|
{
|
|
//
|
|
// Mono in, two channels inside.
|
|
//
|
|
// The jacks on every board built so far are mono - only the left
|
|
// is wired to the codec - so the right arrives as silence. That
|
|
// is fine while the second channel is just the other half of a
|
|
// stereo pair nobody has, and useless the moment it becomes an
|
|
// internal path: a split has nothing to preserve if the thing it
|
|
// preserves is silence.
|
|
//
|
|
// So the front of the chain makes the input into both, before
|
|
// trim and the gate, so they apply to the two identically.
|
|
//
|
|
// A board with a stereo input will want to stop doing this, and
|
|
// the reserved CH_IN value for "both" is where that goes - see
|
|
// do_effect_step(). Nothing can currently tell the difference,
|
|
// which is issue 56.
|
|
//
|
|
in.right = in.left;
|
|
|
|
chain.trim += (chain.trim_target - chain.trim) * CHAIN_SLEW;
|
|
chain.volume += (chain.volume_target - chain.volume) * CHAIN_SLEW;
|
|
|
|
float gain = chain.trim;
|
|
|
|
//
|
|
// The envelope follows the left channel - the guitar - but the gate
|
|
// closes on both, so a stereo signal survives it.
|
|
//
|
|
// Run unconditionally, and only *use* it when the gate is on. It
|
|
// costs one multiply-add, and the alternative is that the reported
|
|
// noise floor freezes at whatever it was when the gate was switched
|
|
// off - the floor meter is derived from this and from nothing else,
|
|
// deliberately, so that the number on screen is the same quantity
|
|
// 'Gate' gets compared against. See single_sample().
|
|
//
|
|
float env = envelope_step(&chain.envelope, in.left);
|
|
|
|
if (chain.active) {
|
|
float mult = chain.mult;
|
|
|
|
//
|
|
// Ramp up fairly quickly, ramp down slowly.
|
|
//
|
|
// These two constants are the whole of the gate's fade and
|
|
// no control reaches them: 9.5ms to open and 95.9ms to
|
|
// close, at every setting of Attack and Release. Those two
|
|
// are the *detector's* time constants and decide when the
|
|
// comparison above flips, not how fast anything moves
|
|
// afterwards. Measured both ways in
|
|
// Documentation/effects/signal-chain.md.
|
|
//
|
|
// The ramp is here so the gate does not pop, and the
|
|
// asymmetry was set by ear. Open quickly, because losing
|
|
// the front of a note when you start playing is the thing
|
|
// you notice. Close ten times more slowly, because a long
|
|
// drawn-out decay suddenly going away is far more
|
|
// noticeable than the same time spent opening. Whether
|
|
// these are the *right* numbers has never been tested
|
|
// against anything; they are two values that sounded right.
|
|
//
|
|
if (env >= chain.level) {
|
|
mult = linear(0.01f, mult, 1.0f);
|
|
if (mult > 0.99f)
|
|
mult = 1.0f;
|
|
} else {
|
|
mult = linear(0.001f, mult, 0.0f);
|
|
if (mult < 0.01f)
|
|
mult = 0.0f;
|
|
chain_effect.intense = 1;
|
|
}
|
|
chain.mult = mult;
|
|
gain *= mult;
|
|
}
|
|
|
|
in.left *= gain;
|
|
in.right *= gain;
|
|
return in;
|
|
}
|