You've already forked torvalds-GuitarPedal
mirror of
https://github.com/torvalds/GuitarPedal.git
synced 2026-08-14 12:44:08 +00:00
The effects have never been measured. The plumbing around them has -
boot, link gain, ring latency, the MIDI parser - but nothing has ever
asked an effect what it does to a signal, and the only DSP ever executed
off-target is the FFT in test-fft.c.
bench/ closes that. It builds audio/effect.h, effects/*.h and
single_sample() for the host and replaces the only two things the audio
core touches - the DMA read and write pointers, and the free-running
timer - with ordinary memory. So what runs is the signal path rather
than a model of it, which matters more than it sounds: 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.
It needed no change to anything under Software/. That is worth
recording as a fact about the architecture: apart from __not_in_flash()
in reverb.h, the effect headers have no hardware in them at all.
Measured on a transparent chain, the bench's own floor is a gain error
of -0.000265 dB and noise 144.7 dB down. The gain error is the
firmware's, not the bench's - it is eps(1.0)/2 * 512, where a 1/512 slew
stalls against float32 rounding.
bench.py drives it. The stimulus is 440 Hz because 48000/440 is 1200/11
exactly, so 12000 samples hold 110 whole cycles and every harmonic lands
on a bin with no leakage - and because 48000/440 is *not* an integer,
which is what lets aliased harmonics fall off the harmonic grid where
they can be told from honest distortion. A frequency that divides the
sample rate hides its own aliasing inside its harmonic series.
--map asks a single function out of audio/util.h directly, with no audio
path around it, so the fast approximations can be characterised against
double precision instead of being inferred through an effect that is
already distorting.
Three targets:
check-effects the two controls that say whether the instrument
works - a transparent chain has to come out
transparent, and boost's fold(), which is sharp on
purpose, has to read as sharp. Host only.
check-bench whether the bench agrees with a real pedal. Test
tone into boost's wavefolder, captured over USB, so
the path is digital end to end and a disagreement is
a real one. Every harmonic agrees to 0.00 dB and the
aliasing to 0.04 dB.
check-analog the other half: a patch cable from the output back to
the input, one board and one codec so there is no
second clock to chase. The converter pair is a gain
constant to 0.001 dB over 58 dB of level, 1.08 ms of
delay, and a residual 84.9 dB down once those two are
removed.
All three skip rather than fail when what they need is not plugged in.
Signed-off-by: Linus Torvalds <torvalds@linux-foundation.org>
101 lines
3.7 KiB
Python
Executable File
101 lines
3.7 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
#
|
|
# Whether the effect bench is working, which is not the same question as
|
|
# whether the effects are.
|
|
#
|
|
# What an effect does to a signal is a conversation - see
|
|
# Documentation/effect-analysis - and almost none of it has a right
|
|
# answer that can be asserted. What does have a right answer is the
|
|
# instrument: a transparent chain has to come out transparent, and a
|
|
# waveshaper that is deliberately sharp has to read as sharp. Those two
|
|
# are the whole of this file.
|
|
#
|
|
# A bench that fails either makes every number taken with it worthless,
|
|
# which is why this is in 'check' and the rest of it is not.
|
|
#
|
|
# Skips rather than fails without numpy, the same bargain the node check
|
|
# in the Makefile makes: this is the only thing in Validation/ that needs
|
|
# more than a C compiler and a python, and 'make check' should still be
|
|
# worth running without it.
|
|
#
|
|
import sys
|
|
|
|
try:
|
|
import numpy as np
|
|
except ImportError:
|
|
print("test-effects: SKIPPED - no numpy")
|
|
sys.exit(0)
|
|
|
|
import bench as B
|
|
|
|
FAILED = []
|
|
|
|
|
|
def check(name, got, lo=None, hi=None, unit=""):
|
|
ok = True
|
|
if lo is not None and got < lo:
|
|
ok = False
|
|
if hi is not None and got > hi:
|
|
ok = False
|
|
want = "%s..%s" % ("" if lo is None else "%g" % lo,
|
|
"" if hi is None else "%g" % hi)
|
|
print(" %-44s %9.3f %-4s want %-14s %s"
|
|
% (name, got, unit, want, "ok" if ok else "FAIL"))
|
|
if not ok:
|
|
FAILED.append(name)
|
|
|
|
|
|
#
|
|
# The negative control. [CHAIN] with the gate off and both gains at 0dB
|
|
# is a wire, and every number here is the bench measuring itself.
|
|
#
|
|
# The gain is allowed to be a whisker under unity rather than exactly
|
|
# unity: the pedal's slewed gains converge to within eps(1.0)/2 * 512 of
|
|
# their target and stop there, which is -0.000265 dB. That is the
|
|
# firmware's arithmetic and not an error in the measurement, so the
|
|
# bound admits it and nothing more.
|
|
#
|
|
print("negative control - [CHAIN] at unity, gate off")
|
|
for dbfs in (-40.0, -18.0, -6.0):
|
|
m = B.measure(["--pot", "Signal Chain:Gate=0"], dbfs=dbfs)
|
|
tag = "%.0f dBFS" % dbfs
|
|
check("transparent gain (%s)" % tag, m["gain_db"], -0.001, 0.001, "dB")
|
|
check("transparent thd (%s)" % tag, m["thd_db"], None, -120.0, "dB")
|
|
check("transparent alias (%s)" % tag, m["alias_db"], None, -120.0, "dB")
|
|
#
|
|
# sqrt(2) is the crest factor of a sine, so that is what the second
|
|
# difference of an undistorted one comes out at. Anything with a
|
|
# bend in it reads higher.
|
|
#
|
|
check("no corner (%s)" % tag, m["corner_sharpness"], 1.35, 1.50)
|
|
check("no clipping (%s)" % tag, m["clipped"], None, 0.0)
|
|
check("no dropped samples (%s)" % tag, m["dropped"], None, 0.0)
|
|
|
|
#
|
|
# The positive control. boost.h's fold() reflects everything past the
|
|
# level about the level, so the transfer curve reverses slope there.
|
|
# That is a slope discontinuity on purpose, and an instrument that
|
|
# cannot see it cannot see any of the accidental ones either.
|
|
#
|
|
print()
|
|
print("positive control - [BOOST] folding on purpose")
|
|
folding = ["--pot", "Signal Chain:Gate=0", "--route", "Boost",
|
|
"--mix", "Boost=120",
|
|
"--pot", "Boost:Boost=90", "--pot", "Boost:Level=40",
|
|
"--pot", "Boost:Basscut=120", "--pot", "Boost:Highcut=120"]
|
|
|
|
clean = B.measure(folding, dbfs=-60.0)
|
|
check("below the fold, no corner", clean["corner_sharpness"], 1.35, 3.0)
|
|
|
|
for dbfs in (-24.0, -12.0):
|
|
m = B.measure(folding, dbfs=dbfs)
|
|
tag = "%.0f dBFS" % dbfs
|
|
check("folding reads as a corner (%s)" % tag, m["corner_sharpness"], 8.0, None)
|
|
check("folding aliases audibly (%s)" % tag, m["alias_db"], -30.0, None, "dB")
|
|
|
|
print()
|
|
if FAILED:
|
|
print("test-effects: FAILED - %s" % ", ".join(FAILED))
|
|
sys.exit(1)
|
|
print("test-effects: ok")
|