""" Duenner Python-Wrapper um das MIT-lizenzierte `tfmx_player/render_tfmx` (siehe tools/tfmx_player/LICENSE, Copyright Peter Fors) -- rendert ein TFMX-Musikstueck (.TFX + .SAM aus PCKELL.DAT) einmalig zu einer WAV-Datei und cached das Ergebnis, damit die Intro-Sequenz die Musik per pygame.mixer abspielen kann. Warum Subprocess statt reinem Python-Reimplement: TFMX ist ein komplexes Amiga-Tracker-Format (siehe tfmx.h, ~95KB reine Player-Logik). Das MIT-lizenzierte C-Referenztool duerfen wir direkt nutzen (im Gegensatz zum lizenzlosen C#-Referenzprojekt fuer PCX/BOB/MAP, das wir bewusst nur als Format-Wissen genommen und selbst neu geschrieben haben). Fuer Musik- Wiedergabe ist der Sound selbst das Ziel, nicht der Decoder-Code -- ein funktionierender, korrekt lizenzierter Renderer ist hier der pragmatische Weg statt TFMX in Python neu zu erfinden. Gerenderte WAVs sind urheberrechtlich das Original-Spielmaterial (Musik), werden daher NICHT ins Git-Repo committed (wie raw/ und extracted_dat/) -- liegen nur lokal im Cache-Verzeichnis. """ import os import subprocess import sys _HERE = os.path.dirname(os.path.abspath(__file__)) _PLAYER_DIR = os.path.join(_HERE, 'tfmx_player') _BINARY = os.path.join(_PLAYER_DIR, 'render_tfmx') _PROJECT_ROOT = os.path.dirname(_HERE) _CACHE_DIR = os.environ.get('KELLOGG_AUDIO_CACHE', os.path.join(_PROJECT_ROOT, 'audio_cache')) def _ensure_binary(): if os.path.exists(_BINARY): return subprocess.run(['bash', os.path.join(_PLAYER_DIR, 'build.sh')], check=True) def render_wav(container, name, seconds=180, force=False): """container: kellogg_formats.DATContainer; name: z.B. 'TITEL' (ohne Extension). Gibt den Pfad zur gerenderten (gecachten) WAV-Datei zurueck. WICHTIG (Fix 22.07.2026 abends): `seconds` ist nur noch eine Sicherheits- OBERGRENZE, keine Ziel-Laenge! render_tfmx erkennt das natuerliche Song- Ende/den Loop-Punkt selbst (tfmx.h real_song_end) und schneidet dort ab -- z.B. ist TITEL tatsaechlich ~109s lang, ONGAME2 ~123s, TITEL2 (der Rauser- Sting) nur ~2s. Vorher wurde hart bei `seconds` abgeschnitten, was TITEL (mit frueher genutzten 60s) VOR dem echten Songende kappte -- das war der Bug hinter "Musik ist zu kurz, da fehlt der Rest". Der Default hier (180s) ist bewusst grosszuegig ueber die laengsten bekannten Stuecke hinaus gewaehlt, damit nichts abgeschnitten wird; im Normalfall stoppt render_tfmx laengst vorher von selbst. ACHTUNG Cache: der Dateiname ist rein `.wav` OHNE den seconds-Wert. Wird `seconds` also mal kleiner gewaehlt als die echte Songlaenge (z.B. fuer einen bewusst kurzen Teaser), MUSS `force=True` gesetzt werden, sonst liefert diese Funktion den ggf. laenger/kuerzer gerenderten alten Cache- Treffer zurueck, egal was `seconds` diesmal sagt -- genau das hat den urspruenglichen Bug mit ausgeloest (ein alter 8s-Debug-Render von TITEL blieb liegen und wurde stillschweigend weiterverwendet).""" os.makedirs(_CACHE_DIR, exist_ok=True) out_path = os.path.join(_CACHE_DIR, f'{name}.wav') if os.path.exists(out_path) and not force: return out_path _ensure_binary() tfx_data = container.entries[name + '.TFX'] sam_data = container.entries[name + '.SAM'] tfx_path = os.path.join(_CACHE_DIR, f'{name}.TFX') sam_path = os.path.join(_CACHE_DIR, f'{name}.SAM') with open(tfx_path, 'wb') as f: f.write(tfx_data) with open(sam_path, 'wb') as f: f.write(sam_data) result = subprocess.run( [_BINARY, tfx_path, sam_path, out_path, str(seconds)], capture_output=True, text=True ) if result.returncode != 0: raise RuntimeError(f'render_tfmx failed for {name}: {result.stderr}') return out_path if __name__ == '__main__': # Kleiner CLI-Test: python3 tfmx_audio.py [seconds] sys.path.insert(0, _HERE) from kellogg_formats import DATContainer dat_path = sys.argv[1] name = sys.argv[2] seconds = int(sys.argv[3]) if len(sys.argv) > 3 else 20 c = DATContainer(dat_path) path = render_wav(c, name, seconds, force=True) print(f'rendered: {path}')