You've already forked torvalds-GuitarPedal
mirror of
https://github.com/torvalds/GuitarPedal.git
synced 2026-08-14 04:43:53 +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>
148 lines
5.4 KiB
Python
Executable File
148 lines
5.4 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
#
|
|
# Turn "-35 dB" into the 0..120 the firmware stores.
|
|
#
|
|
# Every analysis script needs this and the obvious way to do it is to
|
|
# write the arithmetic inline against the range in the header. That is
|
|
# how three separate measurements got taken at the wrong settings in one
|
|
# afternoon: a range moved, the script did not, and nothing said so -
|
|
# the numbers came out plausible and wrong, which is the worst kind.
|
|
#
|
|
# So this reads the POT: line rather than being told, and then checks
|
|
# itself. gen_effects.py has already converted each declared default
|
|
# into the raw 0..120 that goes in effect_map.h, so converting the
|
|
# header's default here and comparing against that is a test of this
|
|
# file's arithmetic against the generator's, on every pot, every run.
|
|
# If a curve is handled wrongly the mismatch shows up immediately
|
|
# instead of as a strange measurement a week later.
|
|
#
|
|
# It deliberately does not implement every curve. FREQUENCY is a cubic
|
|
# and SQUARED is its own thing; neither is needed yet, and guessing at
|
|
# them to be complete would be inventing an authority this file does not
|
|
# have. Ask for one and it says so.
|
|
#
|
|
import math
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
HERE = Path(__file__).resolve().parent
|
|
EFFECTS = HERE.parent / "Effects"
|
|
MAP = HERE / "bench" / "gen" / "effect_map.h"
|
|
|
|
POT_RE = re.compile(
|
|
r'//[ \t]*POT:[ \t]*"([^"]+)"[ \t]+(LINEAR|EXPONENTIAL|FREQUENCY|SQUARED|RAW|ENUM)'
|
|
r'(?:\(([^)]+)\))?(?:[ \t]*=[ \t]*(\S+))?')
|
|
|
|
|
|
def _declared():
|
|
"""Every POT: line in the tree, by effect display name and label."""
|
|
out = {}
|
|
for path in sorted(EFFECTS.glob("*.h")):
|
|
text = path.read_text()
|
|
m = re.search(r"^// NAME:\s*(.+?)\s*\[(\w+)\]\s*$", text, re.M)
|
|
if not m:
|
|
continue
|
|
for label, curve, rng, default in POT_RE.findall(text):
|
|
lo, hi = (None, None)
|
|
if rng:
|
|
parts = rng.split()
|
|
if len(parts) == 2:
|
|
try:
|
|
lo, hi = float(parts[0]), float(parts[1])
|
|
except ValueError:
|
|
pass
|
|
out[(m.group(1), label)] = (curve, lo, hi, default)
|
|
return out
|
|
|
|
|
|
def _raw_defaults():
|
|
"""What the generator turned each declared default into."""
|
|
out = {}
|
|
text = MAP.read_text() if MAP.exists() else ""
|
|
for name, body in re.findall(r'\.name = "([^"]*)",.*?\.pots = \{(.*?)\n\t\}',
|
|
text, re.S):
|
|
base = re.sub(r" \d+$", "", name) # "Tone 1" -> "Tone"
|
|
for label, _unit, dv in re.findall(
|
|
r'EFFECT_POT\("([^"]*)",\s*([^,]*),\s*(\d+)', body):
|
|
out.setdefault((base, label), int(dv))
|
|
return out
|
|
|
|
|
|
def to_pot(effect, label, value):
|
|
"""The 0..120 the firmware stores for an engineering value."""
|
|
spec = _DECLARED.get((effect, label))
|
|
if spec is None:
|
|
raise KeyError(f"no POT: line for {effect}:{label}")
|
|
curve, lo, hi, _ = spec
|
|
if curve == "LINEAR":
|
|
p = (value - lo) / (hi - lo)
|
|
elif curve == "EXPONENTIAL":
|
|
p = math.log2(value / lo) / math.log2(hi / lo)
|
|
else:
|
|
raise NotImplementedError(
|
|
f"{effect}:{label} is {curve}; pots.py only does LINEAR and "
|
|
f"EXPONENTIAL, and guessing at the rest would be inventing one")
|
|
return max(0, min(120, round(p * 120)))
|
|
|
|
|
|
def value(effect, label, pot):
|
|
"""The other way: what a raw 0..120 setting reads as on the knob.
|
|
|
|
A sweep is written in raw steps, because that is the thing with a
|
|
hundred and twenty-one of them and no rounding in it, and then has
|
|
to say in its table what each step meant. Doing that by hand is the
|
|
same mistake as doing to_pot() by hand, from the same direction.
|
|
"""
|
|
spec = _DECLARED.get((effect, label))
|
|
if spec is None:
|
|
raise KeyError(f"no POT: line for {effect}:{label}")
|
|
curve, lo, hi, _ = spec
|
|
p = pot / 120.0
|
|
if curve == "LINEAR":
|
|
return lo + p * (hi - lo)
|
|
if curve == "EXPONENTIAL":
|
|
return lo * (hi / lo) ** p
|
|
raise NotImplementedError(
|
|
f"{effect}:{label} is {curve}; pots.py only does LINEAR and "
|
|
f"EXPONENTIAL, and guessing at the rest would be inventing one")
|
|
|
|
|
|
def arg(effect, label, value):
|
|
"""...as the --pot argument the bench wants."""
|
|
return ["--pot", f"{effect}:{label}={to_pot(effect, label, value)}"]
|
|
|
|
|
|
def selfcheck():
|
|
"""Does this file's arithmetic agree with the generator's?
|
|
|
|
Returns the list of disagreements, empty when all is well.
|
|
"""
|
|
bad, raws = [], _raw_defaults()
|
|
for (effect, label), (curve, lo, hi, default) in sorted(_DECLARED.items()):
|
|
if curve not in ("LINEAR", "EXPONENTIAL") or default is None:
|
|
continue
|
|
want = raws.get((effect, label))
|
|
if want is None:
|
|
continue
|
|
try:
|
|
got = to_pot(effect, label, float(default))
|
|
except (ValueError, TypeError):
|
|
continue
|
|
if got != want:
|
|
bad.append(f"{effect}:{label} default {default} -> {got}, "
|
|
f"generator says {want}")
|
|
return bad
|
|
|
|
|
|
_DECLARED = _declared()
|
|
|
|
if __name__ == "__main__":
|
|
problems = selfcheck()
|
|
for p in problems:
|
|
print("pots: MISMATCH " + p)
|
|
n = sum(1 for v in _DECLARED.values() if v[0] in ("LINEAR", "EXPONENTIAL"))
|
|
print(f"pots: {n} linear/exponential pots, "
|
|
f"{'all agree with the generator' if not problems else 'DISAGREEMENTS ABOVE'}")
|
|
sys.exit(1 if problems else 0)
|