Files
tony-remake/tools/tfmx_audio.py
T

94 lines
4.1 KiB
Python

"""
Duenner Python-Wrapper um das MIT-lizenzierte `tfmx_player/render_tfmx`
(siehe tools/tfmx_player/LICENSE, Copyright Peter Fors) -- rendert ein
TFMX-Musikstueck (<NAME>.TFX + <NAME>.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 `<name>.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 <DAT_PATH> <NAME> [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}')