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

128 lines
4.5 KiB
Python

#
# Building a scene payload, from the host.
#
# There is no way to set an effect's channel routing over MIDI - that
# wants a helper UI rather than a raw byte and is deferred - so a test
# that needs one writes the scene directly and plants it with picotool.
#
# The identities come out of the *built* effect_map.h rather than being
# recomputed here. gen_effects.py hashes a canonical description of
# each effect, and reimplementing that hash in a second language is how
# the two would come to disagree about which effect is which, which is
# the one thing a stored scene must not be wrong about.
#
# The layout is the other half of the _Static_asserts in
# Firmware/scene.h. Those exist because two of the offsets here are not
# visible in the C declaration: a scene_effect is 22 bytes of fields in
# a 24-byte slot, and the array of them is 4-aligned so it starts at 24
# rather than 22. Both were wrong here first time round.
#
import hashlib
import re
import struct
import sys
SLOT_SIZE = 4096
TAIL_SIZE = 64
PAYLOAD_SIZE = SLOT_SIZE - TAIL_SIZE
MARKER = b"SAVEAREA"
CONTAINER_VERSION = 1
SCENE_VERSION = 3
SCENE_MAX_EFFECTS = 32
MAX_ROUTED = 14
MAX_RULES = 16
EFFECT_SIZE = 24
EFFECTS_AT = 24
SCENE_SIZE = EFFECTS_AT + SCENE_MAX_EFFECTS * EFFECT_SIZE + MAX_RULES * 6
XIP_BASE = 0x10000000
AREA_OFFSET = 0x1C0000
# do_effect_step()'s two 2-bit fields, and zero is what an effect did
# before there was a choice: read the left channel, write both.
IN_LEFT, IN_RIGHT = 0, 1
OUT_BOTH, OUT_LEFT, OUT_RIGHT, OUT_MERGE = 0, 1, 2, 3
def channels(ch_in=IN_LEFT, ch_out=OUT_BOTH):
return ch_in | (ch_out << 2)
def effects_from_map(path="../build/effect_map.h"):
"""Every effect in the built firmware, in id order."""
text = open(path).read()
out = []
#
# The short name is not here to be matched on: it stopped being
# stored on struct effect once nothing read it at run time. It was
# never read here either - by_name() below deliberately matches the
# display name, because copies share a short one.
#
for m in re.finditer(
r'\.name = "([^"]*)",\s*\n'
r'\s*\.id_hash = (0x[0-9a-f]+),\s*\n\s*\.pot_hash = (0x[0-9a-f]+),',
text):
out.append({"name": m.group(1),
"id": int(m.group(2), 16), "pot": int(m.group(3), 16),
"pots": [0] * 10, "mix": 120, "channels": 0, "merge": 120})
if not out:
sys.exit(f"scene: no effects in {path} - built?")
# The schema defaults, so a scene says what the pedal would have said
# rather than zeroing every knob it does not mention.
for eff, block in zip(out, text.split(".pots = {")[1:]):
# EFFECT_POT(label, unit, def_val[, enum]) - the third field
# used to be a converting accessor nothing ever called through.
vals = re.findall(r'EFFECT_POT\("[^"]*",[^,]*,\s*(\d+)', block)
for i, v in enumerate(vals[:10]):
eff["pots"][i] = int(v)
return out
def by_name(effects, name):
"""By full name, because the copies share a short one.
'Tone 1' and 'Tone 2' are both [TONE] - that is what COPIES: means -
so the short name cannot pick between them and the display name can.
"""
for e in effects:
if e["name"].lower() == name.lower():
return e
known = ", ".join(e["name"] for e in effects)
sys.exit(f"scene: no effect named '{name}'. Have: {known}")
def build(effects, routed):
"""The payload. 'routed' is a list of indices into 'effects'."""
body = struct.pack("<HBB", SCENE_VERSION, len(effects), len(routed))
body += bytes(routed) + bytes(MAX_ROUTED - len(routed))
body += struct.pack("<B", 0) + bytes(3)
body = body.ljust(EFFECTS_AT, b"\0")
for e in effects:
body += struct.pack("<II", e["id"], e["pot"])
body += bytes(e["pots"])
body += struct.pack("<BBB", e["mix"], e["channels"], e["merge"])
body += bytes(EFFECT_SIZE - 21)
body += bytes(EFFECT_SIZE * (SCENE_MAX_EFFECTS - len(effects)))
body += bytes(6 * MAX_RULES)
if len(body) != SCENE_SIZE:
sys.exit(f"scene: built {len(body)} bytes, "
f"struct scene_payload is {SCENE_SIZE}")
return body
def slot_image(payload, key, seq):
"""Wrap a payload in the container flash_store.h expects."""
body = payload.ljust(PAYLOAD_SIZE, b"\0")
body += MARKER + struct.pack("<IHH", seq, CONTAINER_VERSION, key)
body += b"\0" * 16
return body + hashlib.sha256(body).digest()
def slot_address(n):
return XIP_BASE + AREA_OFFSET + n * SLOT_SIZE