#!/usr/bin/env python3 """ check_melodies.py — prüft die Musik-Tabellen der Firmware, ohne zu flashen. python3 tools/check_melodies.py Hintergrund: die Frequenzen wurden ursprünglich mit Math.pow() gerechnet. Das Ergebnis war rechnerisch korrekt, kam aber als Kommazahl in music.playTone() an — der micro:bit machte daraus eine unsinnige Schwingungsdauer, krachte laut, blieb hängen und riss dabei die Bluetooth-Verbindung ab (Connection Timeout 0x08). Zu finden war das nur am Gerät. Damit so etwas künftig schon beim Bauen auffällt, rechnet dieses Skript die Ganzzahl-Tabelle aus main.ts gegen die exakte Formel nach und prüft die Notendaten auf Werte, die den Tonausgang durcheinanderbringen. Rückgabewert 0 = alles in Ordnung. """ import math import os import re import sys MAIN_TS = os.path.join(os.path.dirname(__file__), "..", "firmware", "main.ts") #: Ab hier wird es hörbar. 100 Cent sind ein Halbton. MAX_CENTS = 8.0 def strip_comments(src): """Kommentare entfernen — sonst schlägt die Suche nach Math.pow auf dem Kommentar an, der genau davor warnt.""" src = re.sub(r"/\*.*?\*/", "", src, flags=re.S) src = re.sub(r"//[^\n]*", "", src) return src def parse_main(path): src = open(path, encoding="utf-8").read() m = re.search(r"const NOTE_HZ16 = \[([0-9,\s]+)\]", src) if not m: sys.exit("NOTE_HZ16 nicht in main.ts gefunden") table = [int(x) for x in m.group(1).replace("\n", "").split(",") if x.strip()] melodies = re.findall(r'name == "([A-Z]+)"\) return hex`([0-9a-f]+)`', src) return src, table, melodies def midi_to_freq(table, midi): """Muss sich genauso verhalten wie midiToFreq() in main.ts.""" hz16 = table[midi % 12] octave = midi // 12 - 5 while octave > 0: hz16 *= 2 octave -= 1 while octave < 0: hz16 //= 2 octave += 1 return (hz16 + 8) // 16 def main(): src, table, melodies = parse_main(MAIN_TS) code = strip_comments(src) problems = [] if len(table) != 12: problems.append(f"NOTE_HZ16 hat {len(table)} Einträge, erwartet 12") # Kein Fließkomma im Tonpfad — das war die Ursache des Fehlers. for bad in ("Math.pow", "Math.round", "parseFloat"): if bad in code: problems.append(f"{bad} steht wieder in main.ts — Frequenzen müssen ganzzahlig bleiben") if not melodies: problems.append("keine Melodien gefunden") notes = set() for name, hexdata in melodies: data = bytes.fromhex(hexdata) if len(data) % 2: problems.append(f"{name}: ungerade Byte-Zahl, Noten sind Paare aus MIDI und Dauer") continue for i in range(0, len(data) - 1, 2): midi, dur = data[i], data[i + 1] if dur == 0: problems.append(f"{name}: Note {i//2} hat Dauer 0 — der Ton liefe endlos") if midi > 127: problems.append(f"{name}: Note {i//2} hat MIDI {midi} (>127)") if midi: notes.add(midi) worst = 0.0 worst_note = None for midi in sorted(notes): got = midi_to_freq(table, midi) if got <= 0: problems.append(f"MIDI {midi} ergibt {got} Hz — playTone braucht einen positiven Wert") continue exact = 440.0 * 2 ** ((midi - 69) / 12) cents = abs(1200 * math.log2(got / exact)) if cents > worst: worst, worst_note = cents, (midi, got, round(exact)) if cents > MAX_CENTS: problems.append(f"MIDI {midi}: {got} Hz statt {exact:.1f} Hz ({cents:.1f} Cent daneben)") print(f"{len(melodies)} Melodien, {len(notes)} verschiedene Noten " f"(MIDI {min(notes)}-{max(notes)})") if worst_note: midi, got, exact = worst_note print(f"größte Abweichung: {worst:.2f} Cent bei MIDI {midi} " f"({got} Hz statt {exact} Hz), erlaubt sind {MAX_CENTS}") if problems: print("\nFEHLER:", file=sys.stderr) for p in problems: print(f" - {p}", file=sys.stderr) return 1 print("alles in Ordnung") return 0 if __name__ == "__main__": sys.exit(main())