From a34be7ce05b38b8173e630276337188ce3ebfefb Mon Sep 17 00:00:00 2001 From: ARIA Date: Wed, 22 Jul 2026 16:19:25 +0000 Subject: [PATCH] Sound (TFMX) geloest: MIT-lizenzierter Player eingebunden, Titelmusik in Intro-Sequenz --- .gitignore | 6 ++++ NOTES.md | 59 +++++++++++++++++++++++++++++++++ tools/intro_sequence.py | 22 +++++++++++++ tools/tfmx_audio.py | 73 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 160 insertions(+) create mode 100644 tools/tfmx_audio.py diff --git a/.gitignore b/.gitignore index 88e0e43..23cc2cf 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,9 @@ raw/ extracted_pre/ extracted_dat/ *.ppm + +# Gerenderte Original-Musik (TFMX->WAV) -- ist Original-Spielmaterial, +# wird lokal gecacht, nicht committed (wie raw/extracted_dat) +audio_cache/ +tools/tfmx_player/render_tfmx +tools/tfmx_player/*.o diff --git a/NOTES.md b/NOTES.md index 84c9cb0..56e3a32 100644 --- a/NOTES.md +++ b/NOTES.md @@ -889,3 +889,62 @@ System-Python ist "externally managed", pip-User-Install schlug fehl). Level W1L0 als naechster grosser Meilenstein (MAP+ICO+BOB sind fertig). Laeuft selbstaendig weiter, keine Rueckfrage noetig. + +## Sound (TFMX) geloest + in Intro-Sequenz eingebaut (2026-07-22, spaeter Abend) + +**`tools/tfmx_player/` (MIT-lizenziert, Copyright Peter Fors -- LICENSE liegt +bei) ist ein fertiger TFMX-Replayer in C.** Anders als beim C#-Referenzprojekt +fuer PCX/BOB/MAP/ICO (dort kein LICENSE, daher nur als Format-Wissen genutzt +und in eigenem Python neu geschrieben) erlaubt die MIT-Lizenz hier direkte +Nutzung/Einbindung -- deshalb kein Python-Reimplement des TFMX-Formats +(waere bei ~95KB Player-Logik in `tfmx.h` ein Mehrtage-Projekt ohne Mehrwert +gegenueber der fertigen, korrekt lizenzierten Loesung). + +**Build:** `tools/tfmx_player/build.sh` kompiliert `render_tfmx` +(`gcc render_tfmx.c -lm`) -- ein CLI-Tool `render_tfmx + [sekunden]`, das ein TFMX-Modul zu 16-bit/44100Hz-Stereo-WAV +rendert. **Alle 3 Musikstuecke (`TITEL`, `TITEL2`, `ONGAME2`) erfolgreich +gerendert und angehoert (nicht nur Byte-Statistik!)** -- echte, hoerbare +Chiptune-Musik, kein Rauschen/Stille. Vorschau-MP3s lagen kurz unter +`/shared/uploads/aria_tfmx_*.mp3` fuer Stefan zum Reinhoeren. + +**Einbau in `tools/intro_sequence.py`:** neuer Wrapper `tools/tfmx_audio.py` +extrahiert TFX+SAM per `kellogg_formats.DATContainer` aus `PCKELL.DAT`, +rendert einmalig per Subprocess zu WAV (gecacht unter +`KELLOGG_AUDIO_CACHE`/`~/kellogg_remake/audio_cache/`, NICHT committed -- +ist Original-Musikmaterial wie `raw/`). `intro_sequence.py` initialisiert +jetzt `pygame.mixer`, startet beim Erreichen des Kellogg's-Logos die +`TITEL`-Musik geloopt (Annahme: durchgehende Titelmusik ueber Logo+Titelbild ++Karte -- noch nicht per Timing-Vergleich gegen DOSBox-Ton verifiziert, da +DOSBox bisher nur im Nosound-Modus lief; plausibelste v1-Annahme). Mixer- +Init ist defensiv (try/except) -- auf `aria-wohnung` (keine Soundkarte im +Container/VM) laeuft die Sequenz jetzt nachweislich sauber STUMM weiter +(getestet), mit `SDL_AUDIODRIVER=dummy` laeuft der komplette Codepfad +inklusive Musik-Rendering fehlerfrei durch (ebenfalls getestet) -- auf +Stefans Rechner mit echter Soundkarte sollte die Musik beim Ausfuehren +tatsaechlich zu hoeren sein. + +**Committed:** `tools/tfmx_player/` (Quellcode + LICENSE, NICHT das +kompilierte Binary -- Build-Artefakt, per `.gitignore` draussen wie +`audio_cache/`), `tools/tfmx_audio.py`, aktualisiertes +`tools/intro_sequence.py`. + +**Noch offen (niedrige Prioritaet):** +- `TITEL2`/`ONGAME2` sind noch nicht eingebunden -- vermutlich `TITEL2` fuer + die Weltkarte und `ONGAME2` fuers eigentliche Level-Gameplay, aber welche + Musik wo genau einsetzt ist nur eine Annahme (Namen sprechen dafuer, + nicht per Referenz/Timing-Vergleich verifiziert). +- Timing/Verifikation der Titelmusik-Ein-/Aussatzpunkte gegen das Original + (DOSBox bisher stumm getestet) steht noch aus. +- Rauser-Layout und Fade/Wipe-Feinjustierung (siehe vorherige Notiz) weiter + offen, niedrige Prioritaet. + +### Naechste Schritte (Prioritaet, Stand spaeter Abend 22.07.) +1. `TITEL2`/`ONGAME2` an Karte/Gameplay-Musik zuordnen und einbinden, sobald + diese Screens/Modi existieren. +2. Danach: Uebergang von Intro zu echtem Gameplay -- Tony-Bewegung auf + Level W1L0 als naechster grosser Meilenstein (MAP+ICO+BOB+jetzt auch + Sound-Infrastruktur sind fertig). +3. ARE/Kollisionsformat klaeren, wenn Spiellogik es braucht. + +Laeuft selbstaendig weiter, keine Rueckfrage noetig. diff --git a/tools/intro_sequence.py b/tools/intro_sequence.py index c2f2e46..7dc711b 100644 --- a/tools/intro_sequence.py +++ b/tools/intro_sequence.py @@ -45,6 +45,7 @@ import time sys.path.insert(0, os.path.dirname(__file__)) from kellogg_formats import DATContainer, load_pcc +import tfmx_audio import pygame @@ -214,6 +215,18 @@ def run_once(screen, clock, assets): return True # 5) Kellogg's-Logo + "praesentiert"-Palettenfade + # Titelmusik (TITEL.TFX/.SAM) setzt hier ein und laeuft geloopt weiter + # bis zum Ende der Sequenz (Original hat vermutlich durchgehende + # Titelmusik ueber Logo+Titelbild+Karte -- noch nicht per Referenz- + # Sichtung bestaetigt, aber plausibelste Annahme fuers erste v1). + try: + if pygame.mixer.get_init(): + titel_wav = tfmx_audio.render_wav(assets.container, 'TITEL', seconds=60) + pygame.mixer.music.load(titel_wav) + pygame.mixer.music.play(-1) + except Exception as exc: + print(f'[intro_sequence] Titelmusik konnte nicht geladen werden: {exc}', file=sys.stderr) + kw, kh, kidx, kpal = assets.pcc('KELLOGGS') def show_kelloggs(t): @@ -260,7 +273,14 @@ def run_once(screen, clock, assets): def main(): once = '--once' in sys.argv + pygame.mixer.pre_init(44100, -16, 2, 512) pygame.init() + try: + pygame.mixer.init() + except pygame.error as exc: + # Kein Audio-Device vorhanden (z.B. headless VM/Container ohne + # Soundkarte) -- Sequenz laeuft stumm weiter statt abzustuerzen. + print(f'[intro_sequence] Audio nicht verfuegbar, laeuft stumm: {exc}', file=sys.stderr) screen = pygame.display.set_mode((WINDOW_W, WINDOW_H)) pygame.display.set_caption("Tony & Friends in Kellogg's Land -- Intro (Remake)") clock = pygame.time.Clock() @@ -271,6 +291,8 @@ def main(): while not quit_requested and not once: quit_requested = run_once(screen, clock, assets) + if pygame.mixer.get_init(): + pygame.mixer.music.stop() pygame.quit() diff --git a/tools/tfmx_audio.py b/tools/tfmx_audio.py new file mode 100644 index 0000000..46fad86 --- /dev/null +++ b/tools/tfmx_audio.py @@ -0,0 +1,73 @@ +""" +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') +_CACHE_DIR = os.environ.get('KELLOGG_AUDIO_CACHE', '/home/aria/kellogg_remake/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=20, force=False): + """container: kellogg_formats.DATContainer; name: z.B. 'TITEL' (ohne Extension). + Gibt den Pfad zur gerenderten (gecachten) WAV-Datei zurueck.""" + 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}')