0
mirror of https://github.com/torvalds/GuitarPedal.git synced 2026-08-14 04:43:53 +00:00
Files
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

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)