Sound (TFMX) geloest: MIT-lizenzierter Player eingebunden, Titelmusik in Intro-Sequenz
This commit is contained in:
@@ -3,3 +3,9 @@ raw/
|
|||||||
extracted_pre/
|
extracted_pre/
|
||||||
extracted_dat/
|
extracted_dat/
|
||||||
*.ppm
|
*.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
|
||||||
|
|||||||
@@ -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).
|
Level W1L0 als naechster grosser Meilenstein (MAP+ICO+BOB sind fertig).
|
||||||
|
|
||||||
Laeuft selbstaendig weiter, keine Rueckfrage noetig.
|
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 <in.tfx> <in.sam>
|
||||||
|
<out.wav> [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.
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ import time
|
|||||||
|
|
||||||
sys.path.insert(0, os.path.dirname(__file__))
|
sys.path.insert(0, os.path.dirname(__file__))
|
||||||
from kellogg_formats import DATContainer, load_pcc
|
from kellogg_formats import DATContainer, load_pcc
|
||||||
|
import tfmx_audio
|
||||||
|
|
||||||
import pygame
|
import pygame
|
||||||
|
|
||||||
@@ -214,6 +215,18 @@ def run_once(screen, clock, assets):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
# 5) Kellogg's-Logo + "praesentiert"-Palettenfade
|
# 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')
|
kw, kh, kidx, kpal = assets.pcc('KELLOGGS')
|
||||||
|
|
||||||
def show_kelloggs(t):
|
def show_kelloggs(t):
|
||||||
@@ -260,7 +273,14 @@ def run_once(screen, clock, assets):
|
|||||||
|
|
||||||
def main():
|
def main():
|
||||||
once = '--once' in sys.argv
|
once = '--once' in sys.argv
|
||||||
|
pygame.mixer.pre_init(44100, -16, 2, 512)
|
||||||
pygame.init()
|
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))
|
screen = pygame.display.set_mode((WINDOW_W, WINDOW_H))
|
||||||
pygame.display.set_caption("Tony & Friends in Kellogg's Land -- Intro (Remake)")
|
pygame.display.set_caption("Tony & Friends in Kellogg's Land -- Intro (Remake)")
|
||||||
clock = pygame.time.Clock()
|
clock = pygame.time.Clock()
|
||||||
@@ -271,6 +291,8 @@ def main():
|
|||||||
while not quit_requested and not once:
|
while not quit_requested and not once:
|
||||||
quit_requested = run_once(screen, clock, assets)
|
quit_requested = run_once(screen, clock, assets)
|
||||||
|
|
||||||
|
if pygame.mixer.get_init():
|
||||||
|
pygame.mixer.music.stop()
|
||||||
pygame.quit()
|
pygame.quit()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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 (<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')
|
||||||
|
_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 <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}')
|
||||||
Reference in New Issue
Block a user