You've already forked torvalds-GuitarPedal
mirror of
https://github.com/torvalds/GuitarPedal.git
synced 2026-08-14 12:44:08 +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>
959 lines
45 KiB
Plaintext
959 lines
45 KiB
Plaintext
Reading the effects
|
|
===================
|
|
|
|
A pass over every file in Effects/, plus the primitives in
|
|
Audio/ that they share. Two things are wanted from it: defects
|
|
found by reading, and - for each effect - the pot settings that are
|
|
worth putting a signal through. The second half is the input to the
|
|
measurement work.
|
|
|
|
Pot values below are the raw 0..120 the firmware stores, because that is
|
|
what a scene holds and what set_effect_pot() takes. The engineering
|
|
value each one works out to is in brackets.
|
|
|
|
The settings are prose here and ought not to stay that way: every other
|
|
fact about an effect lives in the effect's own header, parsed out of a
|
|
structured comment by gen_effects.py, and a table of pot values kept
|
|
somewhere else will drift the first time a range moves. They want to be
|
|
TEST: lines. That has not been done - see 129.
|
|
|
|
**Sections marked MEASURED have been through the bench; the rest is
|
|
still reading.** That distinction is the whole point of the file, and
|
|
two of the things reading was confident about turned out to be wrong
|
|
when measured - the compressor's release and the tanh clamp - so it is
|
|
kept visible rather than tidied away once a thing is settled.
|
|
|
|
|
|
The shared primitives
|
|
---------------------
|
|
|
|
Three findings live below the effects, in code that many of them call.
|
|
They are first because they explain things that would otherwise look
|
|
like separate bugs in five different files.
|
|
|
|
1. single_pole_freq() is a long way off above a few kHz.
|
|
|
|
audio/single-pole.h maps a corner frequency to a coefficient with
|
|
|
|
omega = 2*pi*freq/fs; alpha = omega / (1 + omega)
|
|
|
|
which is the first term of the series for the exact 1 - exp(-omega).
|
|
The file says it is a "standard fast approximation when far away from
|
|
Nyquist", and that is true; the trouble is what counts as far away.
|
|
Solving for where the resulting one-pole is actually -3dB:
|
|
|
|
asked alpha real -3dB error
|
|
30 0.00391 29.9 Hz -0.2%
|
|
150 0.01926 148.6 Hz -1.0%
|
|
1000 0.11575 940.9 Hz -5.9%
|
|
3000 0.28197 2554.0 Hz -14.9%
|
|
5000 0.39559 3930.4 Hz -21.4%
|
|
9000 0.54088 6273.0 Hz -30.3%
|
|
15000 0.66256 9272.2 Hz -38.2%
|
|
20000 0.72360 11596.4 Hz -42.0%
|
|
|
|
Two call sites are well inside the bad region. klon.h asks for a
|
|
15kHz input bandwidth and gets 9.3kHz. frenchie.h's tone control
|
|
sweeps 1500..20000Hz and actually sweeps 1373..11596Hz, so the top
|
|
half of the knob's travel is spent on the difference between two dull
|
|
settings. cabsim.h's only use is at 150Hz and is fine.
|
|
|
|
The exact form costs one pow2() at init and is already written down
|
|
three times elsewhere in the tree: echo.h's echo_onepole_set_cutoff(),
|
|
preamp.h's preamp_onepole_set(), and inline in tremolo.h. All three
|
|
are the same line - pow2(-9.06472028f * fc / fs) - and none of them
|
|
is single-pole.h. So there are two conventions and four copies.
|
|
|
|
2. tanhf()'s clamp is a repair, not a defect. MEASURED.
|
|
|
|
audio/util.h is a [5/4] Pade approximant with
|
|
|
|
if (d < abs_n) d = abs_n;
|
|
|
|
to hold the result inside +-1. The Pade overshoots: it reaches 1.0
|
|
at x = 3.6467 and keeps climbing (1.00165 at x=4, 1.00744 at x=5), so
|
|
from the knee upwards the output is exactly 1.0 and flat while true
|
|
tanh still has a slope of 0.0027. That is a slope discontinuity, and
|
|
the first version of this document called it a corner and left it at
|
|
that. Measuring it says something better.
|
|
|
|
A sine of amplitude A through three curves - true tanh in float64,
|
|
the bare rational with no clamp, and what the pedal ships - taken
|
|
through 'bench --map tanh', so the middle column is the
|
|
approximation's own error and the right-hand one is the clamp's:
|
|
|
|
A THD(true) pade-true pedal-pade pedal-true
|
|
0.10 -61.6 dB -319.7 dB -147.0 dB -147.0 dB
|
|
0.30 -42.7 dB -249.6 dB -146.6 dB -146.6 dB
|
|
1.00 -23.5 dB -149.2 dB -145.0 dB -143.3 dB
|
|
2.00 -15.2 dB -98.6 dB -144.6 dB -98.6 dB
|
|
3.00 -12.0 dB -73.6 dB -144.3 dB -73.6 dB
|
|
3.65 -10.9 dB -62.9 dB -113.4 dB -62.9 dB
|
|
4.00 -10.4 dB -58.3 dB -63.3 dB -63.5 dB
|
|
6.00 -8.9 dB -40.5 dB -40.6 dB -68.4 dB
|
|
10.00 -7.8 dB -24.0 dB -24.0 dB -71.5 dB
|
|
25.00 -6.9 dB -5.3 dB -5.3 dB -75.9 dB
|
|
|
|
Three things fall out of that table.
|
|
|
|
**In the range a guitar actually lives in the question is empty.**
|
|
The pedal's internal scale runs -1.0..1.0 and a played note sits
|
|
around 0.1, where tanh's *intended* distortion is -61.6 dB and the
|
|
approximation contributes -147 dB - and -147 dB is not the
|
|
approximation, it is the float32 round trip, since the Pade there is
|
|
exact to -320 dB and the clamp cannot engage at all. At A=0.03 the
|
|
figures are -82.5 dB intended and -147.6 dB from everything else.
|
|
|
|
**Below the knee the approximation error is the larger of the two,
|
|
by 40 to 70 dB.** At A=3 the rational is 73.6 dB off true tanh and
|
|
the clamp contributes nothing. Anyone worrying about the corner is
|
|
worrying about the smaller term.
|
|
|
|
**Above the knee the clamp makes the answer better, not worse.** The
|
|
last column is what matters and it stays around -70 dB while the
|
|
unclamped rational runs away - at A=25 the bare Pade is 5.3 dB from
|
|
the truth and the shipped function is 75.9 dB from it. That is the
|
|
whole point of the clamp: the [5/4] Pade diverges like x/15, so
|
|
without it "tanh" would grow without bound instead of saturating.
|
|
|
|
And reaching the knee at all means |x| >= 3.65 on a scale where full
|
|
scale is 1.0, so anything that gets there is already square: THD at
|
|
the knee is -10.9 dB. klon at full gain does get there - its op-amp
|
|
stage reaches 56x - and measured across -30 to -6 dBFS in, nothing at
|
|
the -17 dBFS where the clamp starts biting separates from the
|
|
saturation around it.
|
|
|
|
So: not a bug, and worth writing down precisely because it looked
|
|
like one. What the table does suggest is that anyone wanting a
|
|
cleaner saturator should improve the rational rather than the clamp.
|
|
|
|
2a. The sine table is better than it needs to be. MEASURED.
|
|
|
|
Every LFO, every biquad coefficient and the test tone come from a
|
|
256-entry quarter table (SINE_STEP_SHIFT 8, so 1024 points a cycle)
|
|
with linear interpolation. testtone.h says "the tone is exactly as
|
|
clean as the vibrato is", which is honest and is not a number.
|
|
|
|
fastsincos() over one cycle of phase, against float64:
|
|
|
|
peak absolute error 4.687e-06 (-106.6 dBFS)
|
|
THD of the sine it makes -141.9 dB
|
|
total non-fundamental -109.3 dB
|
|
|
|
The interesting part is that those last two disagree by 32 dB, and
|
|
the reason is that **the error is almost entirely not distortion**.
|
|
The loudest error component sits at the fundamental itself, -110 dB
|
|
down: linear interpolation puts the chord under the arc, everywhere
|
|
and always, so the reconstructed sine is uniformly a hair small.
|
|
That is a gain error of about 3 parts per million, not a harmonic.
|
|
What is left is a pair of inharmonic spurs near 18.1 and 19.0 kHz at
|
|
-120 dB, which are the table-rate ripple folded back.
|
|
|
|
The tone the pedal actually generates, [TESTTONE] measured through
|
|
the whole audio path rather than the function alone, agrees: -141.6
|
|
dB THD. Its frequency is right to **60 parts per billion** - the
|
|
accumulator step for 440 Hz works out to 439.999969 Hz - and the
|
|
+-4 Hz skirts in the capture are that offset leaking in the window,
|
|
not anything the generator did.
|
|
|
|
For scale: the quietest noise floor either board has measured is
|
|
-102.4 dBFS (see 95), and the codec is 24-bit. So the generator's
|
|
worst artefact is around 20 dB under the noise it is measured
|
|
against, and its harmonic distortion is 40 dB under that again. It
|
|
is not the limiting factor in anything, and now there is a number
|
|
saying so instead of a comparison to the vibrato.
|
|
|
|
3. Reverb's LFOs wind down and stop.
|
|
|
|
reverb.h rotates a quadrature pair (s,c) by a fixed (ds,dc) each
|
|
sample instead of calling sincos, and never renormalises. The
|
|
rotation constants come from fastsincos(), whose linear interpolation
|
|
between table entries always lands slightly inside the unit circle, so
|
|
the magnitude is under 1 and the pair spirals in:
|
|
|
|
rate per-sample after 1s after 60s after 10min
|
|
0.21 Hz 0.999999916 0.9960 0.7852 0.0891
|
|
0.31 Hz 0.999999876 0.9941 0.7004 0.0284
|
|
0.46 Hz 0.999999817 0.9913 0.5905 0.0052
|
|
0.67 Hz 0.999999735 0.9874 0.4659 0.0005
|
|
|
|
The modulation is the Lexicon trick for breaking up fixed resonant
|
|
peaks in a long tail, so what this costs is that the reverb slowly
|
|
turns into the unmodulated version of itself over a few minutes of
|
|
being switched on, and comes back when a pot is touched (which calls
|
|
reverb_init() and re-seeds the rotators). A cheap fix is one
|
|
reciprocal-square-root every few thousand samples, or just calling
|
|
fastsincos() on a phase accumulator like every other LFO here does.
|
|
|
|
|
|
2b. The rest of the helpers. MEASURED.
|
|
|
|
Asked through 'bench --map', which maps one float to one float
|
|
through a single function with no audio path around it. The accuracy
|
|
question turns out to be settled and the domain question does not.
|
|
|
|
Accuracy, over the ranges these are actually used across:
|
|
|
|
pow2 -31..+31 1.66e-06 rel 0.000014 dB
|
|
db_to_level -100..+40 dB 1.99e-06 rel 0.000017 dB
|
|
db_to_A -100..+40 dB 1.47e-06 rel 0.000013 dB
|
|
expf -20..+20 2.77e-06 rel
|
|
log2f 1e-30..1e30 9.55e-06 absolute
|
|
time_constant 0.1..5000 ms 0.15% worst, on the resulting
|
|
time constant rather than the coefficient
|
|
|
|
That is a hair inside single precision everywhere, which is what the
|
|
author expected and had checked by comparing absolute errors. The
|
|
one systematic effect worth naming is that pow2() reads 6.1e-07 high
|
|
on average, because linear interpolation of a convex curve always
|
|
overshoots - five parts in ten million, mentioned so nobody
|
|
rediscovers it and thinks it is a bug.
|
|
|
|
The domains are the other story, and there are three findings there:
|
|
|
|
- **time_constant(0) was undefined behaviour and [CHAIN]'s Attack pot
|
|
could ask for it** - LINEAR(0.0 10.0), so pot 0 is exactly zero
|
|
milliseconds. It divided by zero, handed pow2() an infinity, and
|
|
the result then depended on which way the float-to-int conversion
|
|
saturates. ARM saturates toward the sign, so the pedal got
|
|
INT_MIN, the 'exp < -31' guard fired, and an attack coefficient of
|
|
zero meant an instantaneous attack - the right answer. x86
|
|
saturates the other way, missed the guard, and segfaulted. So no
|
|
pedal ever crashed on it and one might have. FIXED: zero is now
|
|
answered directly, which is the value ARM was arriving at anyway.
|
|
Issue 133.
|
|
|
|
- **pow2() was guarded below -31 and not above +31**, where the shift
|
|
wraps: pow2(32) came back as 1 and pow2(40) as 256, a tiny number
|
|
for a huge one. The source said "we'll return random values, don't
|
|
do it", which was true and was not a guard. FIXED: it saturates at
|
|
both ends now, to 2^31 rather than to FLT_MAX because FLT_MAX times
|
|
almost anything is the infinity the guard is there to avoid.
|
|
Issue 134.
|
|
|
|
- **log2f() did not mask the sign bit out of the exponent**, so
|
|
log2f(-1) was +256 rather than NaN. A large finite positive is the
|
|
worst possible wrong answer, because the one place the two compose
|
|
- the compressor's mypow() - would have fed it straight into
|
|
pow2()'s unguarded end, and two plausible-looking numbers make a
|
|
plausible-looking result. FIXED: x <= 0 returns -127.0f, which is
|
|
what zero already returned by accident, so only the negative case
|
|
changes. Issue 134.
|
|
|
|
None of the three was reachable with a bad argument except the first,
|
|
and all three are one comparison in functions that are on the audio
|
|
core. check-audio still reports no calls out and the accuracy table
|
|
above is unchanged to every digit.
|
|
|
|
Two smaller things, recorded so they are not rediscovered:
|
|
single_pole_time() is +10% at 0.1 ms and +1% at 1 ms, the same
|
|
approximation family as single_pole_freq() above, but its only
|
|
pot-fed use is klon's 100 ms coupling cap where it is 0.01% out; and
|
|
u32_to_fraction() converts through float32, so an LFO's phase
|
|
resolution is 2^-24 of a cycle rather than the 2^-32 the accumulator
|
|
carries. Still 16.7 million points per cycle against a 1024-point
|
|
table, so it is not the limit on anything.
|
|
|
|
2c. FREQUENCY() is a cubic, and the bottom of it is dead. MEASURED.
|
|
|
|
frequency_pot() is linear(p*p*p, a, b). Across the 121 pot steps the
|
|
output step size runs 5.79e-07 at the bottom to 2.48e-02 at the top,
|
|
a ratio of 42841:1. Pot 60 - the middle of the travel - is 12.5% of
|
|
the way up the range, and the first 26 steps cover the bottom 1%.
|
|
|
|
On vibrato's Rate FREQUENCY(0.1 8.0) that means the first quarter of
|
|
the knob moves the rate from 0.100 to about 0.16 Hz while the last
|
|
few steps jump by 0.2 Hz each.
|
|
|
|
parametric_eq.h already worked this out and moved to EXPONENTIAL,
|
|
with a comment explaining why and a range of 20480 chosen so that ten
|
|
octaves over 120 steps is exactly a semitone a step. The same
|
|
argument applies unchanged to every FREQUENCY() pot left: boost's
|
|
Basscut and Highcut, phaser's LFO and Freq, tremolo's Rate and
|
|
vibrato's Rate. Issue 135.
|
|
|
|
|
|
The effects, one at a time
|
|
--------------------------
|
|
|
|
[CHAIN] - signal_chain.h - trim, gate, master volume
|
|
|
|
Not an effect: it is the two ends of the chain, called by name from
|
|
single_sample(). Trim and Volume are slewed gains, the gate is an
|
|
envelope follower against a threshold in dBFS.
|
|
|
|
The gate multiplier snaps rather than arriving:
|
|
|
|
if (mult > 0.99f) mult = 1.0f;
|
|
...
|
|
if (mult < 0.01f) mult = 0.0f;
|
|
|
|
so a gate opening steps the last 1% of its gain in one sample, and a
|
|
gate closing steps the last 1% likewise. A 1% step in gain is about
|
|
-40dB of click relative to the signal, once per gate transition.
|
|
Whether that is audible is exactly what the next pass is for.
|
|
|
|
The envelope is deliberately measured before trim, and the comment
|
|
explaining why is correct and worth keeping.
|
|
|
|
Worth testing:
|
|
- Trim=60 [0dB] Volume=80 [0dB] Gate=0 [off] - the transparent
|
|
case, and the negative control for the whole bench. Anything
|
|
other than "identical to the input" here is the instrument's
|
|
fault, not the effect's.
|
|
- Trim=120 [+20dB] Volume=80 [0dB] Gate=0 - gain accuracy
|
|
- Trim=0 [-20dB] Volume=120 [+20dB] Gate=0 - and back again
|
|
- Gate=60 [-70dB] Attack=18 [1.5ms] Release=27 [151ms] with a tone
|
|
stepped either side of the threshold - where the knee actually is
|
|
|
|
MEASURED, on the Attack pot, and worth recording because the obvious
|
|
test of it gives the wrong impression. With a tone 6 dB above a
|
|
-76 dBFS threshold, where the envelope's rise time is what decides
|
|
when the threshold gets crossed:
|
|
|
|
Attack pot declared gate opens after
|
|
0 0.00 ms 3.50 ms
|
|
18 1.50 ms 6.00 ms
|
|
60 5.00 ms 10.50 ms
|
|
120 10.00 ms 17.50 ms
|
|
|
|
The 3.5 ms floor is chain_step()'s own linear(0.01f, mult, 1.0f)
|
|
opening ramp, which is fixed and is not the pot's business. Run the
|
|
same test with the tone 64 dB above the threshold and all four
|
|
settings collapse onto that floor and the pot looks dead - which is
|
|
what an envelope follower against a threshold is supposed to do, and
|
|
is a badly chosen stimulus rather than a defect.
|
|
|
|
[TONE] - tone.h - low shelf, peaking mid, high shelf (two copies)
|
|
|
|
Clean. Three RBJ sections in series, Q from the GRAPH: declaration so
|
|
the app draws what the filter is. A shelf at 0dB is exactly
|
|
transparent rather than approximately so, which the header says and
|
|
the algebra confirms. This is one of the two effects whose expected
|
|
answer can be computed rather than recorded, so it is what validates
|
|
the frequency-response half of the bench.
|
|
|
|
Worth testing:
|
|
- all flat (Bass=60, Mid=60, Treble=60) [0dB each] - must be a
|
|
bit-exact no-op
|
|
- Bass=120 [+15dB] alone, then Treble=120, then Mid=120 - one
|
|
section at a time against the biquad computed in numpy
|
|
- Mid=120 [+15dB] Mid Q=120 [4.0] - the narrow case, where a
|
|
numerical error in the coefficients would show first
|
|
- Bass=0 [-15dB] Treble=0 [-15dB] - shelves down as well as up
|
|
|
|
[EQ] - parametric_eq.h - five bands
|
|
|
|
Clean, and the same story as [TONE] with more sections. Uses
|
|
_biquad_peaking_step() for the three peaks, which is valid exactly
|
|
because _biquad_peaking() guarantees a1 == b1; the pairing is right.
|
|
|
|
Worth testing:
|
|
- all gains at 60 [0dB] - transparent
|
|
- P2 Gain=120 [+20dB] alone at P2 Freq=68 [1016Hz] - one peak
|
|
- LS Gain=0 [-20dB] and HS Gain=120 [+20dB] - a tilt
|
|
- all five bands at +20dB - the cascade, where clipping would show
|
|
|
|
[BOOST] - boost.h - gain, bandpass, wavefolder
|
|
|
|
The reference for "deliberately not smooth". fold() reflects
|
|
everything past the level about the level, repeatedly, so the transfer
|
|
curve is continuous in value and reverses slope at the fold point.
|
|
That is a textbook slope discontinuity and it is the point of the
|
|
effect. It is the positive control for the smoothness metric: if the
|
|
measurement does not flag this, the measurement is broken.
|
|
|
|
Worth testing:
|
|
- Boost=0 [0dB] Level=120 [0dB] - clean, nothing folds
|
|
- Boost=90 [+30dB] Level=40 [-26.7dB] - folding hard
|
|
- Boost=60 [+20dB] Level=80 [-13.3dB] - folding on peaks only,
|
|
which is where the level sweep should find the knee
|
|
|
|
[PREAMP] - preamp.h - two-stage triode or JFET waveshaper
|
|
|
|
Uses the correct exponential one-pole rather than single_pole_freq(),
|
|
so its stated corners are its real corners. DC blockers between
|
|
stages, precomputed tanhf() of the bias offsets so the static DC is
|
|
removed rather than accumulating. The normalisation constants
|
|
(PREAMP_TUBE_NORM, PREAMP_JFET_NORM) are hand-fitted to give about
|
|
1.05 small-signal gain, which is a claim worth checking with a very
|
|
quiet tone.
|
|
|
|
Worth testing:
|
|
- Sat=24 [1.2x] Level=60 [0dB] Voice=0 [Tube] at -60dBFS - the
|
|
small-signal gain, against the claimed 1.05
|
|
- the same at -6dBFS - where the tanh actually bends
|
|
- Sat=120 [4.0x] Level=60 Voice=0 - past the tanhf knee
|
|
- Voice=1 [JFET] at the same three, since it is a different path
|
|
|
|
[COMPRESSOR] - compressor.h
|
|
|
|
A reading here was wrong, and the measurement is why it is written
|
|
down rather than quietly deleted.
|
|
|
|
The claim was that the Attack and Release pots do not control the
|
|
attack and release of the gain, because the gain is slewed by a
|
|
hard-coded coefficient after the envelope has already decided:
|
|
|
|
compressor.compression = linear(0.01f, compressor.compression, target);
|
|
|
|
0.01 a sample at 48kHz is a time constant of 100 samples, 2.08ms.
|
|
That much is true; the inference from it was backwards. 2ms is much
|
|
*faster* than anything the Release pot asks for (50..500ms), so it is
|
|
not a limit on the release at all - it just smooths the detector's
|
|
decision. It would only swamp the pot if it were slower than the pot's
|
|
range, and it is nowhere near.
|
|
|
|
Measured, with a -6dBFS tone dropped to -46dBFS and the recovery timed
|
|
to within 1dB of its final value:
|
|
|
|
Release pot declared measured recovery
|
|
0 50 ms 135 ms
|
|
60 275 ms 418 ms
|
|
120 500 ms 388 ms
|
|
|
|
So the knob works. What it does not do is mean what it says: three
|
|
times the declared time at the bottom of the travel, and the top two
|
|
settings land the wrong way round. Some of that last part is the
|
|
crudeness of a "within 1dB" threshold against an exponential tail, and
|
|
the honest summary is that the pot is real, monotonic over most of its
|
|
range and not calibrated in milliseconds.
|
|
|
|
mypow() is pow2(log2f(a)*b) over the fast table pair, so the knee
|
|
shape carries whatever those two tables' interpolation error is. pow2
|
|
measures good to 0.000008 dB across 2^-3..2^3, so this is not it.
|
|
|
|
Worth testing:
|
|
- Level=60 [-20dB] Ratio=60 [10.5x] Attack=0 [2ms] Release=0 [50ms]
|
|
- the same with Release=120 [500ms] - these two should differ and
|
|
the claim here is that they barely will
|
|
- Ratio=0 [1.0x] - ratio 1 is no compression; compressor.ratio
|
|
becomes 0 and target becomes pow(x,0)=1, so this should be a clean
|
|
pass-through at Boost gain
|
|
- Boost=120 [24dB] with Level=0 - makeup gain alone
|
|
|
|
[KLON] - klon.h
|
|
|
|
Two things. The 15kHz input bandwidth is really 9.3kHz, per the
|
|
single_pole_freq() finding above. And the clean half of the
|
|
clean/dirty blend is taken from the raw input:
|
|
|
|
mixed = in * clean_amt + ge_clip * dirty_amt;
|
|
|
|
where ge_clip descends from `pre`, which has been through the DC
|
|
blocker, the 30Hz coupling filter and the input bandwidth filter, and
|
|
`in` has been through none of them. So the two halves of the blend
|
|
are not phase-aligned, and at intermediate Gain settings they will
|
|
partially cancel somewhere in the top octave. It may well sound fine
|
|
- the dirty path dominates wherever it matters - but it is not what
|
|
the comment describes, and the fix is one character.
|
|
|
|
Also: single_pole_freq() and single_pole_rc() are called inside
|
|
klon_step(), once per sample, on constant arguments. single-pole.h
|
|
explicitly invites this ("it's much better to just do them as part of
|
|
the step function, since that will allow the compiler to just fold all
|
|
the computations"), and single_pole_freq is simple enough to fold.
|
|
single_pole_rc() ends in a divide; worth checking the disassembly
|
|
rather than assuming.
|
|
|
|
Worth testing:
|
|
- Gain=0 [0.0] Output=48 [0.4] - fully clean, where the raw-`in`
|
|
path is the *whole* signal and the filters are bypassed entirely
|
|
- Gain=120 [1.0] Output=48 - fully dirty, no clean path at all
|
|
- Gain=60 [0.5] Output=48 - the blend, where the phase mismatch
|
|
lives. Sweep frequency here rather than level.
|
|
- Treble=0 and Treble=120 at Gain=24 - the shelf and presence peak
|
|
|
|
[FRENCHIE] - frenchie.h - Champ-style amp
|
|
|
|
The most code and the most to say about.
|
|
|
|
Four parameters of frenchie_triode_step() are dead. It takes
|
|
pos_hard, neg_hard, pos_ceil and neg_ceil and uses none of them; the
|
|
body was replaced with a single offset tanh ("Replaced piecewise tanh
|
|
with a single offset tanh...") and the parameters were left in the
|
|
signature. The call site still passes values for them, and one of
|
|
those values carries a comment claiming it was tuned:
|
|
|
|
1.1f, 0.935f /* FIX: was 0.9f, 0.935 matches positive slope
|
|
(1.1 * 0.85) */, 0.85f, 1.0f,
|
|
|
|
That FIX changed nothing. Worth deleting the parameters, and worth
|
|
remembering as a case of a comment that documents an effect the code
|
|
does not have.
|
|
|
|
The tone control asks for up to 20kHz through single_pole_freq() and
|
|
therefore tops out at 11.6kHz - the same primitive bug as klon, and
|
|
more visible here because it is a knob the player turns.
|
|
|
|
Three places step discontinuously:
|
|
|
|
- the output is clamp(mono * out_level, -0.95f, 0.95f), a hard
|
|
clamp, and out_level goes to 2.0. A hard clamp is a slope
|
|
discontinuity, unlike every other limiter in this codebase.
|
|
- grid conduction, in both the input stage and the triode, is
|
|
continuous in value at the threshold and not in slope: below it
|
|
the slope is 1, above it is (1 + intensity*3) * (0.05 +
|
|
intensity*0.15), which at the default intensity is 0.31. A
|
|
three-to-one slope change is a corner.
|
|
- the transformer saturation block is skipped entirely when
|
|
xfmr_sat <= 0.05 and applied at full strength when it crosses,
|
|
and the one-pole inside it only integrates while the branch is
|
|
taken, so its state is stale each time the branch re-enters.
|
|
|
|
frenchie_gate_step() is the counter-example and is done properly:
|
|
smoothstep t*t*(3-2t) between the thresholds, which has zero slope at
|
|
both ends and so joins the flat regions smoothly. Whoever wrote that
|
|
one knew exactly what the other three are missing.
|
|
|
|
Worth testing:
|
|
- Gain=60 Tone=60 Input=60 Sag=60 Level=15 [0.25] - the defaults
|
|
- Level=120 [2.0] with a loud tone - the output clamp, guaranteed
|
|
- Input=0 [0.0] vs Input=120 [1.0] at -6dBFS - the grid-conduction
|
|
corner, which should appear and disappear with intensity
|
|
- Tone=0 and Tone=120 - the frequency response the knob claims
|
|
- Gain=120 Sag=120 with a tone burst - the PSU sag envelope
|
|
|
|
[CAB] - cabsim.h
|
|
|
|
A cascade of biquads with two envelope-driven modulations on top.
|
|
The filters are fine. Two things to look at:
|
|
|
|
- the breakup waveshaper is skipped entirely when Breakup is 0 and
|
|
applied with drive_gain = 1 + amt*40 as soon as it is not, so the
|
|
bottom of that knob's travel steps in. Value-continuous (at
|
|
amt=0 the branch is a no-op and the expression reduces to
|
|
identity) but the *derivative with respect to the knob* jumps.
|
|
That is a control smoothness question rather than a waveform one.
|
|
- the Chug compression multiplies the envelope by a hard-coded 25.0
|
|
"since guitar signals are typically ~0.1 peak", then clamps the
|
|
result at 0. A hard clamp again, and a magic constant that
|
|
assumes a signal level.
|
|
|
|
Worth testing:
|
|
- Resonance=60 Presence=60 Axis=60 Breakup=0 Chug=0 - the plain
|
|
filter cascade, computable against biquad.h
|
|
- Axis=0 vs Axis=120 - the 3.5kHz/6kHz high cut
|
|
- Breakup=120 at -6dBFS - the waveshaper, past the tanhf knee
|
|
- Chug=120 with a tone burst - the envelope-driven resonance duck
|
|
|
|
[ECHO] - echo.h - Echoplex model
|
|
|
|
The most carefully written file here, and the one place where an
|
|
effect having its own internal wet/dry blend is clearly deliberate
|
|
rather than accidental: Blend is the Echoplex's own control, the dry
|
|
it blends is the *preamp output* rather than the input, and the
|
|
header explains why. Compare phaser and flanger below.
|
|
|
|
The delay line is int16_t, so the wet path carries about -96dB of
|
|
quantisation floor. Presumably a tape-character decision; nothing
|
|
says so, and it is worth a sentence in the header either way.
|
|
|
|
echo_init() sets last_blend = -1.0f to force the blend gains to be
|
|
recomputed on the first step, and does not do the same for last_tone.
|
|
It works out - tone glides up from 0 and crosses the 0.001 threshold
|
|
within a few samples - but the asymmetry is the kind that survives
|
|
until someone changes the glide.
|
|
|
|
Sustain goes to 1.1 and is multiplied by max_feedback 1.05, so the
|
|
loop gain reaches 1.155 and is bounded only by the tanhf on the write.
|
|
That is self-oscillation and appears intentional.
|
|
|
|
Note for whoever measures this: delay_smooth is 0.00012, a time
|
|
constant of 8333 samples, and echo.delay_s starts at zero. The delay
|
|
takes roughly a second to glide to its setting after an init, so a
|
|
capture taken too early measures the glide.
|
|
|
|
Worth testing:
|
|
- Blend=120 [1.0] Sustain=0 [0.0] Time=60 [183ms] Record=25
|
|
[0.5] Tone=120 WowFlut=0 - one clean repeat, no feedback, no
|
|
modulation: the delay time is then measurable by correlation
|
|
- the same at Time=0 [50ms] and Time=120 [672ms] - does the pot
|
|
mean what it says
|
|
- Sustain=90 [0.825] - the tail, and how fast it darkens
|
|
- WowFlut=120 [1.0] - the modulation depth, and whether the
|
|
interpolated read is smooth while it moves
|
|
- Mode=1 [SOS] - the indefinite loop
|
|
|
|
[REVERB] - reverb.h - Freeverb FIXED, and it works
|
|
|
|
This is now repaired in the tree. The history is kept because the
|
|
shape of the mistake is the useful part.
|
|
|
|
reverb_state.wet_level was declared, read once, and never written
|
|
anywhere in the tree:
|
|
|
|
float w = reverb_state.wet_level;
|
|
return w * REVERB_SCALEWET * wet + (1.0f - w) * REVERB_SCALEDRY * in;
|
|
|
|
With w = 0 that is `return in`. The eight comb filters and four
|
|
allpasses run every sample and their output is multiplied by zero.
|
|
So the reverb is a pass-through that costs about a hundred operations
|
|
a sample, and the DEFAULT_MIX of 0.18 mixes 18% of the dry signal
|
|
with 82% of the dry signal.
|
|
|
|
The header's own comment said "scalewet (1.5) and scaledry (1.0) give
|
|
unity dry gain at 0% wet", which describes a control that was never
|
|
connected. Freeverb has a wet/dry of its own and this pedal has
|
|
do_effect_step(); the wet_level line was the former surviving a
|
|
conversion to the latter.
|
|
|
|
The fix is to delete the internal blend and return
|
|
`wet * REVERB_SCALEWET`, letting MIX: POWER do the job it was declared
|
|
for. Measured after that change, an impulse sent once the enable fade
|
|
has finished:
|
|
|
|
response peak 0.0169 from an impulse of 0.5, 54 ms in
|
|
-10 dB after 199 ms
|
|
-20 dB after 438 ms
|
|
-30 dB after 695 ms
|
|
-60 dB after 1565 ms
|
|
|
|
So it is a reverb: a dense build to a peak 54 ms after the strike and
|
|
a 1.6-second RT60 at Room=0.84. Sustained level against the dry it
|
|
replaces is -4.24 dB at full wet.
|
|
|
|
Two things worth knowing now that there is something to hear. The
|
|
mix sweep dips - 0.00, -5.70, -4.24 dB at mix 0, 60 and 120 - which is
|
|
not the equal-power law failing but the wet simply being 4 dB quieter
|
|
than the dry, so filling the hole between two unequal levels leaves
|
|
one. And this changes what every stored scene sounds like, from no
|
|
reverb to reverb, which is a thing to say out loud rather than let
|
|
people discover.
|
|
|
|
Separately, the comb read pointers are modulated and then truncated:
|
|
|
|
unsigned id = (unsigned)((float)c->delay + mod * REVERB_MOD_DEPTH);
|
|
|
|
No interpolation. The read position jumps a whole sample at a time
|
|
as the LFO sweeps +-6 samples, so each comb emits a step
|
|
discontinuity on every crossing. A step is the worst kind of corner
|
|
- broadband, harmonics falling off only as 1/n - and there are eight
|
|
of them. This is a strong candidate for an audible defect once the
|
|
wet path is connected at all.
|
|
|
|
And the LFOs decay to nothing over minutes, per the primitives
|
|
section above.
|
|
|
|
Worth testing (all of which currently measure a wire):
|
|
- Room=60 [0.84] Damp=60 [0.3] with an impulse - the tail, its
|
|
length, and whether it is a tail at all
|
|
- Room=120 [0.98] - the longest tail, where the comb modulation
|
|
has the most time to be heard
|
|
- a steady tone for several minutes - the LFO decay
|
|
|
|
[PITCH] - pitch.h
|
|
|
|
Two delay taps walking at different rates, crossfaded by sin and cos
|
|
so the discontinuities land where each tap's weight is zero. The
|
|
header argues that sin^2 + cos^2 = 1 makes it unity power gain, and
|
|
that argument holds only while the two taps are uncorrelated.
|
|
|
|
At Octave = 60 the pot maps to exactly 0.0, pow2(0) is exactly 1.0
|
|
and pitch.step is exactly 0.0. Then delay is 1 for both taps, both
|
|
reads return the same sample, and the output is
|
|
|
|
tap * (sin + cos) = tap * sqrt(2) * sin(theta + pi/4)
|
|
|
|
which is the input multiplied by a factor sweeping between -1.414 and
|
|
+1.414 at 5.86Hz (the 8192-sample phase period). So the setting that
|
|
reads as "no shift" is a ring modulator that inverts twice a cycle,
|
|
not a bypass. Every other setting decorrelates the taps and the
|
|
header's argument applies.
|
|
|
|
Worth testing:
|
|
- Octave=60 [0.0] - the degenerate case above
|
|
- Octave=90 [1.0, the default] Feedback=0 - one octave up, measured
|
|
as the ratio of output to input frequency
|
|
- Octave=30 [-1.0] Feedback=0 - one octave down
|
|
- Octave=90 Feedback=120 [1.0] - the regeneration
|
|
|
|
[VIB] - vibrato.h
|
|
|
|
Small and correct. A single modulated tap around a 6ms centre, pure
|
|
wet out, MIX: POWER to blend it - which is exactly right for a
|
|
modulated delay and is the shape phaser and flanger should have had.
|
|
Buffer arithmetic checks out: 6ms + 5ms is 528 samples into a 1024
|
|
buffer.
|
|
|
|
The one thing to measure is the interpolated read. sample_array_read()
|
|
is linear, so as the fractional delay sweeps it introduces a
|
|
frequency-dependent amplitude ripple and a slope discontinuity every
|
|
time it crosses an integer sample. Vibrato sweeps continuously and so
|
|
crosses constantly. This is the cleanest place to measure that
|
|
artefact, because there is nothing else in the effect.
|
|
|
|
Worth testing:
|
|
- Rate=75 [2.03Hz] Depth=21 [0.875ms] - the default
|
|
- Rate=0 [0.1Hz] Depth=120 [5ms] - slow and deep, so the
|
|
interpolation artefact is spread out and easy to localise
|
|
- Rate=120 [8Hz] Depth=0 [0ms] - zero depth, which should be a
|
|
fixed 6ms delay and nothing else
|
|
|
|
[FLANGER] - flanger.h
|
|
|
|
Returns (in + out) / 2 - it mixes its own dry in, and halves
|
|
everything. Then do_effect_step() mixes dry again, on a LINEAR law,
|
|
because the header carries no MIX: line. So at the default mix the
|
|
dry signal arrives twice by two different routes and the wet arrives
|
|
6dB down.
|
|
|
|
Compare vibrato, which is the same kind of effect written the other
|
|
way and is right.
|
|
|
|
Worth testing:
|
|
- Freq=60 [2.5Hz] Delay=60 [2ms] Depth=60 [0.5] Feedback=60 [0.5]
|
|
at mix 120 and at mix 60 - the double-dry, which shows as the
|
|
notches not reaching as deep as they should
|
|
- Depth=0 - a fixed comb filter, whose notch spacing is computable
|
|
- Feedback=120 [1.0] - where tanhf is in the loop
|
|
|
|
[PHASER] - phaser.h
|
|
|
|
Returns tanhf(in + out) where out is the allpass cascade - so the dry
|
|
is inside the wet, and then mixed again by do_effect_step() on the
|
|
default LINEAR law. At mix 0.5 that is 1.5 dry. Same defect as
|
|
flanger, differently spelled.
|
|
|
|
The tanhf() on the output is applied to a signal that is nominally
|
|
twice the input amplitude, which puts it closer to the Pade knee than
|
|
anything else that is not a distortion effect.
|
|
|
|
_biquad_allpass_filter() is recomputed every sample - four
|
|
multiplies, a divide, and a fastsincos() through _w0() - because the
|
|
centre frequency is swept by the LFO. That is the honest way to
|
|
sweep a filter and it is also the most expensive per-sample thing in
|
|
the tree. Worth a load measurement rather than a rewrite.
|
|
|
|
Worth testing:
|
|
- LFO=60 Feedback=60 Freq=60 Q=60 at mix 120 and mix 60 - the
|
|
double-dry
|
|
- Feedback=0 - the plain four-stage allpass, whose notches are
|
|
computable
|
|
- Feedback=120 [0.75] - resonant, and where the output tanh bites
|
|
|
|
[TREM] - tremolo.h
|
|
|
|
The one MIX: STEREO effect. NORM is a complex multiply - a rotation
|
|
of (l + i*r) - which is equal-power by construction and needs no mix
|
|
law to arrange it, and the header explains that properly. HARM is
|
|
two one-pole branches modulated in anti-phase, using the correct
|
|
exponential coefficient rather than single_pole_freq().
|
|
|
|
Nothing looks wrong. What is worth measuring is the claim: at
|
|
Depth=120 the magnitude |out| should equal |in| exactly for a mono
|
|
input, and the two channels should be in quadrature.
|
|
|
|
Worth testing:
|
|
- Rate=75 [2.52Hz] Depth=60 [0.5] Mode=0 [NORM] - the default
|
|
- Depth=120 [1.0] Mode=0 - the equal-power claim, and the phase
|
|
inversion at the half-cycle the header admits to
|
|
- Depth=0 - must be exactly transparent
|
|
- Mode=1 [HARM] at Depth=120 - the anti-phase branches, and the
|
|
claimed unity gain at zero depth
|
|
|
|
[TESTTONE] - testtone.h
|
|
|
|
The generator, and therefore the first thing to measure rather than
|
|
the last. Its sine is lfo_step()'s quarter table: 256 entries per
|
|
quarter, so 1024 points per cycle, linearly interpolated. The header
|
|
says "the tone is exactly as clean as the vibrato is", which is
|
|
honest and is not a number. Chord-versus-arc arithmetic puts the
|
|
interpolation error near -106dB, and that wants confirming before any
|
|
measurement is quoted against it.
|
|
|
|
Freq is one semitone per pot step - 13.75 * 2^(p/12) - so pot 60 is
|
|
exactly 440Hz and the generator can only produce equal-tempered
|
|
frequencies. That is worth knowing before designing a test around a
|
|
round number.
|
|
|
|
Worth testing:
|
|
- Level=96 [-18dBFS] Freq=60 [440Hz] Shape=0 [Sine] - the
|
|
calibration case, against a numpy sine
|
|
- Freq=0 [13.75Hz] and Freq=120 [14080Hz] - the ends, where table
|
|
interpolation error is worst relative to the period
|
|
- Shape=1,2 [Triangle, Saw] - which are corners on purpose
|
|
- Level=0 - must be exact digital silence, not a very quiet tone
|
|
|
|
[SETTINGS] - settings.h
|
|
|
|
MIX: NONE, no step function, not routable. Nothing to measure.
|
|
|
|
|
|
Does the pedal agree?
|
|
---------------------
|
|
|
|
All of the above is a workstation doing arithmetic. Two runs on the
|
|
real board say whether that was worth anything, and both are in
|
|
Validation/ as targets rather than as a story: 'make check-bench' and
|
|
'make check-analog'.
|
|
|
|
THE DIGITAL PATH. [TESTTONE] at 440 Hz -18 dBFS into [BOOST] with
|
|
fold() engaged, captured over USB - no generator, no cable, no second
|
|
board, so the whole path is digital and a disagreement would be a real
|
|
one. fold() is the sharpest thing in the tree, which is the point: two
|
|
implementations of one arithmetic diverge there if they diverge at all.
|
|
|
|
harmonic 1 pedal -32.91 bench -32.91 diff +0.00 dB
|
|
harmonic 3 pedal -50.09 bench -50.09 diff -0.00 dB
|
|
harmonic 5 pedal -34.37 bench -34.37 diff +0.00 dB
|
|
harmonic 7 pedal -57.29 bench -57.29 diff +0.00 dB
|
|
harmonic 9 pedal -38.44 bench -38.44 diff -0.00 dB
|
|
non-harmonic pedal -42.46 bench -42.43 diff -0.04 dB
|
|
|
|
Every harmonic a symmetric wavefolder makes, to 0.00 dB, and the
|
|
aliasing - the hardest thing to get to agree, since it is what came back
|
|
over Nyquist - to 0.04 dB. The even harmonics sit at -135 dBFS and are
|
|
skipped: they are two roundings of zero.
|
|
|
|
The clean control is not a null test either, which makes it worth more
|
|
than one: the 200 Hz basscut takes 0.18 dB out of a 440 Hz tone and both
|
|
sides agree about that too.
|
|
|
|
THE ANALOG PATH. A patch cable from the output back to the input, so
|
|
the DAC and the ADC are the same codec on the same clock and there is no
|
|
drift to chase. 'Wet/Dry' puts what was sent and what came back side by
|
|
side in one capture.
|
|
|
|
sent returned gain THD noise
|
|
-63.0 -61.50 1.50 -73.8 -103.0 dBFS
|
|
-45.0 -43.50 1.50 -93.4 -103.0 dBFS
|
|
-27.0 -25.50 1.50 -82.5 -102.4 dBFS
|
|
-18.0 -16.50 1.50 -90.1 -100.0 dBFS
|
|
-9.0 -7.50 1.50 -89.2 -93.2 dBFS
|
|
-4.5 -3.00 1.50 -84.7 -89.0 dBFS
|
|
|
|
**The gain is constant to 0.001 dB across 58 dB of level.** That is the
|
|
headline: the converter pair is a straight line, and the +1.50 dB is a
|
|
fixed property of the analog design rather than anything that varies.
|
|
It is also the same +1.5 dB of link gain per pedal that 95 records from
|
|
the two-board ring, arrived at independently.
|
|
|
|
Nulled at -18 dBFS, after removing that gain and the delay:
|
|
|
|
delay 52.0645 samples = 1.0847 ms
|
|
gain +1.4991 dB
|
|
residual -84.9 dB below the signal = 0.0057%
|
|
harmonics 2..12 -90.2 dB converter distortion
|
|
everything else -86.4 dB noise, -102.9 dBFS absolute
|
|
|
|
So the analog round trip reproduces the signal to about six parts in a
|
|
hundred thousand, and what is left is second-harmonic-dominated
|
|
converter distortion at -90 dB over a -102.9 dBFS noise floor. That
|
|
floor is the one 95 has been quoting at -102.4 dBFS, measured a
|
|
different way and landing in the same place.
|
|
|
|
Two things worth knowing before reading those numbers again.
|
|
|
|
**The delay must be solved, not searched.** At 440 Hz one sample is 3.3
|
|
degrees, so a null that hunts for the lag on a 0.01-sample grid leaves
|
|
0.03 degrees of phase behind - which is -65 dB of residual fundamental,
|
|
larger than the distortion being measured. The first run here did
|
|
exactly that and reported -71.0 dB, with the residual's loudest
|
|
component sitting at the fundamental, which is the giveaway. Taking the
|
|
lag from the phase of the fundamental instead drops the residual
|
|
fundamental to -304 dB and the answer to -84.9.
|
|
|
|
**The two delay measurements disagree by 0.9 samples** - 52.06 from the
|
|
phase at 440 Hz, 52.96 from correlating broadband noise. They are not
|
|
the same quantity: one is the phase delay at a single frequency and the
|
|
other is a broadband group delay, and a converter's decimation and
|
|
interpolation filters have no reason to make those equal. Neither has
|
|
been checked against the other and the difference is not yet explained.
|
|
|
|
WHAT THIS DOES NOT COVER. One board, one cable, one afternoon, one
|
|
temperature. 95 is the issue about a bench measurement carrying no
|
|
record of the bench, and it applies here: the 1.50 dB and the -102.9
|
|
dBFS belong to this board with this cable, and stay hypotheses about
|
|
anything else until the cable has been moved and the number followed it.
|
|
|
|
|
|
What the bench then said
|
|
------------------------
|
|
|
|
Validation/bench builds the pedal's own audio core for the host and
|
|
Validation/bench.py drives it - see the header of each. A 440Hz sine at
|
|
an exact 110 cycles in a 12000-sample window, so every harmonic lands on
|
|
a bin with no leakage, and 48000/440 is deliberately not an integer so
|
|
that aliased harmonics fall off the harmonic grid and can be told apart
|
|
from honest distortion.
|
|
|
|
The instrument's own floor, measured on a transparent chain: a gain
|
|
error of -0.000265 dB and noise 144.7 dB down. The gain error is not
|
|
the bench's - it is exactly eps(1.0)/2 * 512, which is where a 1/512
|
|
slew stalls against float32 rounding, so every slewed gain in the pedal
|
|
lands that far short of its target and stops. Inaudible, and worth
|
|
knowing before reading anything finer.
|
|
|
|
CONFIRMED, AND FIXED - reverb was a wire. Routed at full mix, the
|
|
output differed from no reverb at all by 1.9e-6, which is that same
|
|
slew residual. An impulse produced a tail of exactly zero energy.
|
|
Eight combs and four allpasses ran every sample into a multiply by an
|
|
uninitialised zero. Repaired by deleting the internal blend; it now
|
|
measures a 1.57-second RT60 peaking 54 ms after the strike. Verified
|
|
on the board after reflashing: routed at full mix it is no longer
|
|
bit-identical to no reverb at all, energy away from the 440 Hz grid
|
|
goes from -120 to -50 dBFS, and switching the tone off mid-capture
|
|
leaves a tail that decays 30 dB in 930 ms.
|
|
|
|
CONFIRMED - pitch at Octave=60 is a ring modulator. The pot reads as
|
|
no shift; the output peak is 1.414 times the input peak, to three
|
|
figures, and the envelope swings 0.078..0.178 at 5.86Hz, inverting
|
|
twice a cycle. Every other Octave setting decorrelates the two taps
|
|
and comes out within 0.6 dB of unity, so the header's equal-power
|
|
argument is right everywhere except at the one setting that looks
|
|
like a bypass.
|
|
|
|
CONFIRMED - the flanger's mix knob cannot reach the effect. With
|
|
Depth and Feedback at zero it is a fixed comb, so its notch should
|
|
null. Measured at 248Hz:
|
|
|
|
mix 30 60 90 120
|
|
notch -2.5 -6.0 -12.0 -34.0 dB
|
|
|
|
Three quarters of the travel does almost nothing and the notch only
|
|
arrives at the very top, because the effect returns (in+out)/2 and
|
|
then do_effect_step() mixes dry in again on a LINEAR law. Vibrato,
|
|
the same primitive written the other way round - pure wet, MIX: POWER
|
|
- sweeps properly: -25.5 dB at mix 60 and 0.0 dB at mix 120, dry
|
|
through cancellation to the effect alone.
|
|
|
|
NOT CONFIRMED - the compressor's release. See above; the reading was
|
|
wrong and the pot works.
|
|
|
|
NOT A DEFECT - the tanhf() clamp, and not observable either. Asked
|
|
directly through 'bench --map tanh' rather than through an effect, it
|
|
turns out to be the thing that keeps the approximation bounded, and to
|
|
be the smaller of the two error terms everywhere below the knee. See
|
|
the primitives section above for the table. Klon at full gain drives
|
|
the waveshaper past the knee at every input level from -30dBFS up and
|
|
the corner never separates from the saturation around it: sharpness
|
|
15..21, THD -22..-7 dB, no feature at the -17dBFS where the clamp
|
|
starts biting.
|
|
|
|
NOT A DEFECT - the sine table, measured for the first time at -141.9
|
|
dB THD, with its real error being a -110 dB gain reduction rather than
|
|
distortion. Again see above. Both of these were on the suspect list
|
|
from reading and both come off it; that is as useful an outcome as
|
|
finding a bug, and cheaper to act on.
|
|
|
|
Still unmeasured, and each one wants a stimulus this pass did not build:
|
|
|
|
- single_pole_freq() at 5kHz and above (klon, frenchie) - a sweep,
|
|
which is the frequency-response pass rather than this one
|
|
- reverb's uninterpolated comb modulation, which cannot be heard
|
|
until the wet path is connected at all
|
|
- reverb's LFOs decaying over minutes - a very long capture
|
|
- frenchie's output clamp, grid conduction and transformer branch,
|
|
which showed as a corner (sharpness 4.8 at -12dBFS) without
|
|
saying which of the three it was
|
|
- the gate's 1% snap, which needs a burst across the threshold
|
|
- linear interpolation in the modulated delay lines, which the
|
|
cycle-fold metric cannot separate from the modulation itself
|
|
|
|
The last of those is a limit of the instrument rather than of the
|
|
effect. Folding a capture onto its repeat assumes the output is
|
|
periodic, and a swept delay is not; every modulated effect therefore
|
|
reads as "corner" and "alias" on this metric whatever it is doing. They
|
|
want the LFO held still, or a metric that does not assume periodicity.
|