You've already forked torvalds-GuitarPedal
mirror of
https://github.com/torvalds/GuitarPedal.git
synced 2026-06-25 06:07:56 +00:00
It transfers the tuning data as actual MIDI notes with the cent data using the "pitch bend" range. The tuner on/off is just another GLOBAL_ENABLE_CC byte. Because pitch bending is per-channel - and we want to send multiple notes for the polyphonic case - this sends the tuning data on separate channels, with channel #0 for the chromatic tuning data. This is all possibly horribly wrong. And I think my recent simplifications of the tuning code may have over-simplified things a bit, and it got a bit less reliable and the chromatic one now finds bogus over-tones much too easily. So this all will need tweaking a lot to be useful, but the code *looks* clean. It just doesn't work so well in practice yet. But the MIDI part seems solid (famous last words). Small details. Signed-off-by: Linus Torvalds <torvalds@linux-foundation.org>
303 lines
12 KiB
Python
Executable File
303 lines
12 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
import sys
|
|
import os
|
|
import re
|
|
import json
|
|
import math
|
|
|
|
def generate(audio_dir, out_h, out_js, out_md):
|
|
cc_counter = 40
|
|
midi_map = [] # List of dicts for C output
|
|
ui_effects = [] # List of dicts for JS output
|
|
effects_data = []
|
|
|
|
for filename in os.listdir(audio_dir):
|
|
if not filename.endswith('.h'):
|
|
continue
|
|
base = filename[:-2]
|
|
header_path = os.path.join(audio_dir, filename)
|
|
|
|
with open(header_path, 'r') as f:
|
|
content = f.read()
|
|
|
|
name_match = re.search(r'//\s*NAME:\s*(.*?)\s*\[(.*?)\]', content)
|
|
priority_match = re.search(r'//\s*PRIORITY:\s*(\d+)', content)
|
|
graph_match = re.search(r'//\s*GRAPH:\s*(\w+)', content)
|
|
|
|
if not name_match:
|
|
continue
|
|
|
|
full_name = name_match.group(1).strip()
|
|
short_name = name_match.group(2).strip()
|
|
priority = int(priority_match.group(1)) if priority_match else 100
|
|
graph_fn = graph_match.group(1) if graph_match else None
|
|
|
|
pots = []
|
|
# Match: // POT: "Name" CURVE(a b c) = 1.0 Unit
|
|
pot_lines = re.findall(r'//[ \t]*POT:[ \t]*"([^"]+)"[ \t]+(LINEAR|FREQUENCY|SQUARED|EXPONENTIAL|RAW|ENUM)(?:\(([^)]+)\))?(?:[ \t]*=[ \t]*(\S+))?(?:[ \t]+(\S+))?[ \t]*\n?', content)
|
|
|
|
for p_label, p_curve, p_args, p_def, p_unit in pot_lines:
|
|
enum_list = None
|
|
if p_curve == 'ENUM' and p_args:
|
|
enum_list = p_args.split()
|
|
|
|
default_val = 0.0
|
|
if p_def:
|
|
if enum_list and p_def in enum_list:
|
|
default_val = float(enum_list.index(p_def))
|
|
else:
|
|
try:
|
|
default_val = float(p_def)
|
|
except ValueError:
|
|
default_val = 0.0
|
|
|
|
pots.append({
|
|
'label': p_label,
|
|
'unit': p_unit if p_unit else "none",
|
|
'curve': p_curve,
|
|
'args': p_args.split() if p_args else [],
|
|
'enum': enum_list,
|
|
'default': default_val
|
|
})
|
|
|
|
effects_data.append({
|
|
'base': base,
|
|
'full_name': full_name,
|
|
'short_name': short_name,
|
|
'priority': priority,
|
|
'graph': graph_fn,
|
|
'pots': pots,
|
|
'header_path': header_path
|
|
})
|
|
|
|
# Sort by priority
|
|
effects_data.sort(key=lambda x: x['priority'])
|
|
|
|
# Build maps
|
|
for e_idx, e_data in enumerate(effects_data):
|
|
for p_idx, pot in enumerate(e_data['pots']):
|
|
midi_map.append({
|
|
"cc": cc_counter,
|
|
"type": 1, # POT
|
|
"effect_idx": e_idx,
|
|
"pot_idx": p_idx
|
|
})
|
|
pot['cc'] = cc_counter
|
|
cc_counter += 1
|
|
|
|
enable_cc = cc_counter
|
|
midi_map.append({
|
|
"cc": cc_counter,
|
|
"type": 2, # ENABLE
|
|
"effect_idx": e_idx,
|
|
"pot_idx": 0
|
|
})
|
|
e_data['enable_cc'] = enable_cc
|
|
cc_counter += 1
|
|
|
|
ui_pots = []
|
|
for pot in e_data['pots']:
|
|
min_v = 0.0
|
|
max_v = 1.0
|
|
if pot['curve'] != 'ENUM' and len(pot['args']) >= 2:
|
|
min_v = float(pot['args'][0])
|
|
max_v = float(pot['args'][1])
|
|
elif pot['curve'] == 'ENUM' and pot['enum']:
|
|
max_v = float(len(pot['enum']) - 1)
|
|
|
|
ui_pots.append({
|
|
"name": pot['label'],
|
|
"unit": pot['unit'],
|
|
"curve": pot['curve'],
|
|
"min": min_v,
|
|
"max": max_v,
|
|
"default": pot['default'],
|
|
"cc": pot['cc'],
|
|
"enum": pot['enum']
|
|
})
|
|
|
|
ui_effects.append({
|
|
"id": e_data['base'],
|
|
"name": e_data['full_name'],
|
|
"shortName": e_data['short_name'],
|
|
"enable_cc": enable_cc,
|
|
"pots": ui_pots
|
|
})
|
|
|
|
# Generate effect_map.h
|
|
with open(out_h, 'w') as f:
|
|
f.write("// Auto-generated by gen_effects.py\n")
|
|
|
|
for e_data in effects_data:
|
|
base = e_data['base']
|
|
struct_name = f"{base}_effect" if base != "eq" else "EQ"
|
|
e_data['struct_name'] = struct_name
|
|
|
|
f.write(f"static struct effect {struct_name};\n")
|
|
|
|
for p_idx, pot in enumerate(e_data['pots']):
|
|
fn_name = f"{base}_pot{p_idx}"
|
|
pot['fn_name'] = fn_name
|
|
|
|
if pot['curve'] == 'ENUM' and pot['enum']:
|
|
enum_name = f"{base}_pot{p_idx}_enum"
|
|
pot['enum_name'] = enum_name
|
|
f.write(f"static const char *const {enum_name}[] = {{ ")
|
|
for val in pot['enum']:
|
|
f.write(f'"{val}", ')
|
|
f.write("NULL };\n")
|
|
|
|
args_str = ", ".join(pot['args'])
|
|
if pot['curve'] == 'RAW' or pot['curve'] == 'ENUM':
|
|
f.write(f"static float {fn_name}(unsigned char pot) {{ return pot; }}\n")
|
|
elif pot['curve'] == 'LINEAR':
|
|
f.write(f"static float {fn_name}(unsigned char pot) {{ return linear_pot(pot, {args_str}); }}\n")
|
|
elif pot['curve'] == 'FREQUENCY':
|
|
f.write(f"static float {fn_name}(unsigned char pot) {{ return frequency_pot(pot, {args_str}); }}\n")
|
|
elif pot['curve'] == 'SQUARED':
|
|
f.write(f"static float {fn_name}(unsigned char pot) {{ float p = POT_TO_FLOAT(pot); return linear(p*p, {args_str}); }}\n")
|
|
elif pot['curve'] == 'EXPONENTIAL':
|
|
a_val = float(pot['args'][0])
|
|
b_val = float(pot['args'][1])
|
|
log2_ratio = math.log2(b_val / a_val) if a_val != 0 else 0
|
|
f.write(f"static float {fn_name}(unsigned char pot) {{ float p = POT_TO_FLOAT(pot); return {a_val}f * pow2(p * {log2_ratio}f); }}\n")
|
|
|
|
f.write(f"#include \"../effects/{base}.h\"\n")
|
|
|
|
f.write(f"static struct effect {struct_name} = {{\n")
|
|
f.write(f"\t.name = \"{e_data['full_name']}\",\n")
|
|
f.write(f"\t.short_name = \"{e_data['short_name']}\",\n")
|
|
if e_data['graph']:
|
|
f.write(f"\t.graph = {e_data['graph']},\n")
|
|
f.write(f"\t.init = {base}_init,\n")
|
|
f.write(f"\t.step = {base}_step,\n")
|
|
f.write(f"\t.pots = {{\n")
|
|
|
|
for p_idx, pot in enumerate(e_data['pots']):
|
|
y = pot['default']
|
|
curve = pot['curve']
|
|
|
|
a = 0.0
|
|
b = 1.0
|
|
if curve != 'ENUM' and len(pot['args']) >= 2:
|
|
a = float(pot['args'][0])
|
|
b = float(pot['args'][1])
|
|
|
|
if curve == 'RAW' or curve == 'ENUM':
|
|
p = y
|
|
elif curve == 'LINEAR':
|
|
p = (y - a) / (b - a) if b != a else 0
|
|
elif curve == 'FREQUENCY':
|
|
p = ((y - a) / (b - a)) ** (1/3.0) if b != a else 0
|
|
elif curve == 'SQUARED':
|
|
p = ((y - a) / (b - a)) ** 0.5 if b != a else 0
|
|
elif curve == 'EXPONENTIAL':
|
|
p = math.log2(y / a) / math.log2(b / a) if (b != a and a != 0 and y != 0) else 0
|
|
|
|
if curve == 'RAW' or curve == 'ENUM':
|
|
pot_val = int(round(y))
|
|
else:
|
|
pot_val = int(round(p * 120))
|
|
if pot_val < 0: pot_val = 0
|
|
if pot_val > 120: pot_val = 120
|
|
|
|
unit_str = f"\"{pot['unit']}\"" if pot['unit'] and pot['unit'] != "none" else "NULL"
|
|
enum_str = f", {pot['enum_name']}" if 'enum_name' in pot else ""
|
|
f.write(f"\t\tEFFECT_POT(\"{pot['label']}\", {unit_str}, {pot['fn_name']}, {pot_val}{enum_str}),\n")
|
|
|
|
f.write("\t}\n")
|
|
f.write("};\n\n")
|
|
|
|
f.write("static struct effect *const effects[] = {\n")
|
|
for e_data in effects_data:
|
|
f.write(f"\t&{e_data['struct_name']},\n")
|
|
f.write("};\n\n")
|
|
|
|
f.write("struct midi_cc_mapping {\n\tuint8_t type; // 0=none, 1=pot, 2=enable\n\tuint8_t effect_idx;\n\tuint8_t pot_idx;\n};\n\n")
|
|
f.write("static const struct midi_cc_mapping dense_midi_map[128] = {\n")
|
|
|
|
map_dict = {m["cc"]: m for m in midi_map}
|
|
for cc in range(128):
|
|
if cc in map_dict:
|
|
m = map_dict[cc]
|
|
f.write(f"\t[{cc}] = {{ .type = {m['type']}, .effect_idx = {m['effect_idx']}, .pot_idx = {m['pot_idx']} }},\n")
|
|
|
|
f.write("};\n\n")
|
|
|
|
f.write("static const uint8_t effect_enable_to_cc[] = {\n")
|
|
for e_idx in range(len(effects_data)):
|
|
enables = [m["cc"] for m in midi_map if m["effect_idx"] == e_idx and m["type"] == 2]
|
|
if enables:
|
|
f.write(f"\t[{e_idx}] = {enables[0]},\n")
|
|
f.write("};\n\n")
|
|
|
|
f.write("static const uint8_t effect_pot_to_cc[][10] = {\n")
|
|
for e_idx in range(len(effects_data)):
|
|
f.write(f"\t[{e_idx}] = {{ ")
|
|
for p_idx in range(10):
|
|
pots = [m["cc"] for m in midi_map if m["effect_idx"] == e_idx and m["type"] == 1 and m["pot_idx"] == p_idx]
|
|
if pots:
|
|
f.write(f"{pots[0]}, ")
|
|
else:
|
|
f.write("0, ")
|
|
f.write("},\n")
|
|
f.write("};\n\n")
|
|
|
|
f.write(f"#define GLOBAL_ENABLE_CC 20\n")
|
|
f.write(f"#define STATE_DUMP_CC 119\n")
|
|
f.write(f"#define EFFECT_COUNT {len(effects_data)}\n\n")
|
|
|
|
with open(out_js, 'w') as f:
|
|
f.write("// Auto-generated by gen_effects.py\n")
|
|
f.write("const PEDAL_EFFECTS = " + json.dumps(ui_effects, indent=2) + ";\n")
|
|
f.write("const GLOBAL_ENABLE_CC = 20;\n")
|
|
f.write("const STATE_DUMP_CC = 119;\n")
|
|
|
|
with open(out_md, 'w') as f:
|
|
f.write("# MIDI CC Mapping\n\n")
|
|
f.write("**MIDI Channel:** The pedal responds on **all channels** (Omni Mode). You can set your controller to any channel (1-16).\n\n")
|
|
f.write("## Global Controls\n\n")
|
|
f.write("- **Global Enable/Bypass:** CC 20\n")
|
|
f.write(" - `0` = Bypass\n")
|
|
f.write(" - `64` = Restore All Defaults\n")
|
|
f.write(" - `65` = Save All Effects State\n")
|
|
f.write(" - `66` = Load All Effects State\n")
|
|
f.write(" - `67` = Disable All Effects (Global Enable)\n")
|
|
f.write(" - `68` = Enter Tuner Mode\n")
|
|
f.write(" - `69` = Exit Tuner Mode\n")
|
|
f.write(" - `126` = Reboot to USB Bootloader\n")
|
|
f.write(" - `Other` = Enable\n")
|
|
f.write("\n## Effects\n\n")
|
|
for e_idx, e_data in enumerate(effects_data):
|
|
f.write(f"### {e_data['full_name']} ({e_data['short_name']})\n\n")
|
|
f.write(f"- **Program Change (PC):** {e_idx} (Selects this effect as active in UI)\n")
|
|
f.write(f"- **Enable/Bypass Effect:** CC {e_data['enable_cc']}\n")
|
|
f.write(" - Values: `0`=Bypass, `64`=Restore Defaults, `65`=Save State, `66`=Load State, `Other`=Enable\n")
|
|
for pot in e_data['pots']:
|
|
if pot['curve'] == 'ENUM' and pot['enum']:
|
|
range_str = ", ".join([f"{i}={v}" for i, v in enumerate(pot['enum'])])
|
|
input_str = f"Input: 0-{len(pot['enum'])-1} (maps to: {range_str})"
|
|
elif pot['curve'] == 'RAW':
|
|
input_str = "Input: 0-127 (Raw value)"
|
|
else:
|
|
if len(pot['args']) >= 2:
|
|
min_val = pot['args'][0]
|
|
max_val = pot['args'][1]
|
|
unit = pot['unit']
|
|
if unit and unit != "none":
|
|
range_str = f"{min_val} to {max_val} {unit}"
|
|
else:
|
|
range_str = f"{min_val} to {max_val}"
|
|
else:
|
|
range_str = "0.0 to 1.0"
|
|
input_str = f"Input: 0-120 (maps to: {range_str})"
|
|
f.write(f"- **{pot['label']}:** CC {pot['cc']} - {input_str}\n")
|
|
f.write("\n")
|
|
|
|
if __name__ == "__main__":
|
|
if len(sys.argv) < 5:
|
|
print("Usage: gen_effects.py <audio_dir> <out_h> <out_js> <out_md>")
|
|
sys.exit(1)
|
|
|
|
generate(sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4])
|