Rauser-Fade-Bug (Alpha-Cutout), Musik-Laenge-Bug (real_song_end-Erkennung) und Factor5-Melodie gefixt
This commit is contained in:
@@ -1051,3 +1051,70 @@ Original aus. Screenshots liegen als Beleg bei:
|
|||||||
`title_check_frames/`) nach der Analyse wieder aufgeraeumt, nicht im Repo.
|
`title_check_frames/`) nach der Analyse wieder aufgeraeumt, nicht im Repo.
|
||||||
|
|
||||||
Laeuft selbstaendig weiter, keine Rueckfrage noetig.
|
Laeuft selbstaendig weiter, keine Rueckfrage noetig.
|
||||||
|
|
||||||
|
## 22.07.2026, spaeter Abend (2): drei Feedback-Bugs von Stefan gefixt
|
||||||
|
|
||||||
|
Stefan hatte nach dem letzten Pfade-relativ/Rauser/Titelbild-Fix drei neue
|
||||||
|
Beobachtungen gemeldet, alle drei bestaetigt und behoben:
|
||||||
|
|
||||||
|
**1. "Rauser-Text fadet nicht mit aus, bleibt hart umrandet stehen."**
|
||||||
|
Ursache endlich richtig verstanden (der oben als "kosmetischer Rest, low
|
||||||
|
priority" dokumentierte Kasten): RAUSER1/2/3.PCC haben keinen Alphakanal, ihr
|
||||||
|
papierweisser Hintergrund sitzt als hartes Rechteck um die Box-Grafik. Fix:
|
||||||
|
neue Funktion `make_border_bg_transparent()` in `intro_sequence.py` -- Flood-
|
||||||
|
Fill vom Bildrand aus ueber alle "papierweissen" Pixel (Schwelle 235/255 je
|
||||||
|
Kanal), NUR die vom Rand aus zusammenhaengende Flaeche wird transparent.
|
||||||
|
Isolierte weisse Pixel MITTEN im Motiv (z.B. die weissen Buchstaben in
|
||||||
|
RAUSER1) beruehren den Rand nicht und bleiben unangetastet -- genau das
|
||||||
|
Problem, an dem der fruehere `set_colorkey()`-Versuch gescheitert war.
|
||||||
|
Verifiziert per direktem Pixel-Test (nicht nur behauptet): Eckpixel neben der
|
||||||
|
Box liegt nach dem Fix exakt bei RAUSER_GREY (73,73,73) statt weiss, und im
|
||||||
|
Bild bleiben klar sichtbare, fast voll opake Buchstaben-Pixel (Alpha bis 254)
|
||||||
|
erhalten. Screenshot der Grau-Phase zum Beleg:
|
||||||
|
[FILE: /shared/uploads/aria_rauser_fade_grey_check.png]
|
||||||
|
|
||||||
|
**2. "Musik ist zu kurz, da fehlt der Rest" + Cache-Bug gefunden.**
|
||||||
|
Zwei uebereinanderliegende Ursachen:
|
||||||
|
a) `render_wav()` cached per Dateiname OHNE den `seconds`-Parameter zu
|
||||||
|
beruecksichtigen -- ein alter Test-Render (TITEL mit 8s, TITEL2 mit 15s
|
||||||
|
aus einer frueheren Debug-Session) blieb im `audio_cache/` liegen und
|
||||||
|
wurde bei jedem Aufruf stillschweigend weiterverwendet, egal welche
|
||||||
|
`seconds` der Aufrufer diesmal wollte.
|
||||||
|
b) selbst ein frischer Render haette bei den frueher verwendeten festen
|
||||||
|
Werten (TITEL: 60s) abgeschnitten -- **TITEL ist tatsaechlich ~109s
|
||||||
|
lang, ONGAME2 ~123s**, beides deutlich laenger als angenommen.
|
||||||
|
Echter Fix in `render_tfmx.c`: statt stur `seconds` lang zu rendern, wird
|
||||||
|
jetzt in kleinen 64-Frame-Haeppchen gerendert und tfmx.h's eigenes
|
||||||
|
`real_song_end`-Flag beobachtet (wird pro Tick gesetzt, sobald die
|
||||||
|
Track-Sequenz einmal komplett durchgelaufen ist und intern neu startet) --
|
||||||
|
sobald es feuert, ist GENAU eine vollstaendige natuerliche Song-Schleife im
|
||||||
|
Kasten, egal wie lang das Stueck wirklich ist. `seconds`/`max_seconds` ist
|
||||||
|
jetzt nur noch eine Sicherheits-Obergrenze (Default 180s, grosszuegig ueber
|
||||||
|
allen 3 bekannten Stuecken). Getestet und verifiziert:
|
||||||
|
- TITEL: 109.44s (natuerliches Ende erkannt)
|
||||||
|
- TITEL2 (Rauser-Sting): 2.04s (natuerliches Ende erkannt)
|
||||||
|
- ONGAME2: 122.88s (natuerliches Ende erkannt)
|
||||||
|
Alte falschen Cache-Dateien geloescht, mit dem gefixten Tool neu gerendert
|
||||||
|
und die Laengen direkt per `pygame.mixer.Sound(...).get_length()` verifiziert
|
||||||
|
(nicht nur die WAV-Header-Bytes ausgerechnet).
|
||||||
|
|
||||||
|
**3. "Bei Factor5 gibt's auch noch ne Melodie, solange das Lied ist Factor5
|
||||||
|
anzeigen und dann zum Kellogg's-praesentiert."**
|
||||||
|
`ONGAME2` war bis jetzt der einzige der 3 TFMX-Tracks ohne zugeordneten
|
||||||
|
Screen (TITEL2 = Rauser-Sting, TITEL = Kellogg's-Logo/Titelbild-Loop) --
|
||||||
|
genau das ist offenbar die Factor5-Melodie. `run_once()` in
|
||||||
|
`intro_sequence.py` spielt ONGAME2 jetzt beim Erscheinen des Factor5-Logos
|
||||||
|
einmal komplett ab und haelt den Screen exakt so lange (`sound.get_length()`,
|
||||||
|
~123s statt der alten festen 3.0s), bevor es zur schwarzen Pause + Kellogg's-
|
||||||
|
Logo weitergeht.
|
||||||
|
|
||||||
|
**Alle 3 Fixes verifiziert, nicht nur behauptet:**
|
||||||
|
- Rauser-Alpha-Fix: direkter Pixel-Test + Screenshot (oben verlinkt).
|
||||||
|
- Musik-Laengen: `render_tfmx`-Stderr-Ausgabe UND `pygame.mixer.Sound.get_length()`
|
||||||
|
geben identische Werte.
|
||||||
|
- Kompletter Smoke-Test (`xvfb-run python3 tools/intro_sequence.py --once`)
|
||||||
|
laeuft ohne Traceback durch (headless ohne Audio-Geraet, laeuft dann
|
||||||
|
erwartungsgemaess stumm mit Fallback-Dauern).
|
||||||
|
|
||||||
|
Committet + gepusht. Naechster Schritt weiterhin offen: echtes Gameplay
|
||||||
|
(Tony auf `W1L0` bewegen).
|
||||||
|
|||||||
+129
-25
@@ -11,15 +11,27 @@ Serie vermessen, siehe NOTES.md Abschnitt "Boot-Sequenz vermessen". Kurzfassung:
|
|||||||
mit "!") faehrt von OBEN ein, RAUSER2 ("ADVERTAINMENT") steht von Anfang
|
mit "!") faehrt von OBEN ein, RAUSER2 ("ADVERTAINMENT") steht von Anfang
|
||||||
an fest. Sobald beide Teile ihre Endposition erreicht haben, spielt ein
|
an fest. Sobald beide Teile ihre Endposition erreicht haben, spielt ein
|
||||||
kurzer Sound-Sting (TITEL2.TFX/.SAM -- das kuerzeste der 3 Musikstuecke,
|
kurzer Sound-Sting (TITEL2.TFX/.SAM -- das kuerzeste der 3 Musikstuecke,
|
||||||
genau ein "Bling"-Jingle, kein Loop). Danach faedet der WEISSE
|
ein "Bling"-Jingle von ~2s natuerlicher Laenge, kein Loop). Danach faedet
|
||||||
Hintergrund kurz zu GRAU um (NICHT weiss -- war ein Fehler in v1!), haelt
|
der WEISSE Hintergrund kurz zu GRAU um (NICHT weiss -- war ein Fehler in
|
||||||
kurz, dann schwarze Pause. WICHTIG: alle Rauser-Assets werden nur bei
|
v1!), haelt kurz, dann schwarze Pause. WICHTIG: alle Rauser-Assets werden
|
||||||
**halber Pixel-Groesse** (0.5x) ihrer PCC-Nativaufloesung gezeichnet --
|
nur bei **halber Pixel-Groesse** (0.5x) ihrer PCC-Nativaufloesung
|
||||||
bei 1:1 sind sie in der echten DOSBox-Aufnahme klar zu groß (per
|
gezeichnet -- bei 1:1 sind sie in der echten DOSBox-Aufnahme klar zu groß
|
||||||
Pixel-Vermessung bestaetigt, siehe NOTES.md "Rauser/Titelbild-Korrektur").
|
(per Pixel-Vermessung bestaetigt, siehe NOTES.md "Rauser/Titelbild-
|
||||||
|
Korrektur"). KORREKTUR (22.07.2026 abends): die Assets haben keinen
|
||||||
|
Alphakanal, ihr papierweisser Hintergrund sass als harter Kasten um die
|
||||||
|
Box-Grafik und fadete beim Uebergang zu Grau NICHT mit (Stefans Meldung:
|
||||||
|
"Text bleibt hart umrandet stehen"). Fix: make_border_bg_transparent()
|
||||||
|
macht per Flood-Fill vom Bildrand aus genau diesen Aussenbereich
|
||||||
|
transparent, isolierte weisse Buchstaben MITTEN im Motiv bleiben
|
||||||
|
unangetastet.
|
||||||
2. kurze schwarze Pause
|
2. kurze schwarze Pause
|
||||||
3. Factor5-Studio-Logo (FACTOR5.PCC, STATISCHES Bild -- keine Animation,
|
3. Factor5-Studio-Logo (FACTOR5.PCC, STATISCHES Bild -- keine Animation,
|
||||||
das "Geister"-Doppellogo ist Teil des Assets selbst), ~6s
|
das "Geister"-Doppellogo ist Teil des Assets selbst) mit einer EIGENEN
|
||||||
|
Melodie (ONGAME2.TFX/.SAM, der bis 22.07.2026 abends noch keinem Screen
|
||||||
|
zugeordnete dritte TFMX-Track -- Stefans Hinweis: "bei factor 5 gibts
|
||||||
|
auch noch ne melodie"). Der Screen bleibt exakt so lange stehen wie das
|
||||||
|
Stueck einmal komplett durchspielt (natuerliche Laenge per render_tfmx
|
||||||
|
erkannt, ~123s), nicht mehr eine feste Dauer.
|
||||||
4. kurze schwarze Pause
|
4. kurze schwarze Pause
|
||||||
5. Kellogg's-Markenlogo (KELLOGGS.PCC) erscheint SOFORT komplett; darunter
|
5. Kellogg's-Markenlogo (KELLOGGS.PCC) erscheint SOFORT komplett; darunter
|
||||||
faedet der Text "praesentiert" per Palette-Fade von blass nach saettigt-
|
faedet der Text "praesentiert" per Palette-Fade von blass nach saettigt-
|
||||||
@@ -58,6 +70,7 @@ sonst loopt die Sequenz weiter wie im Original-Titelbildschirm)
|
|||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
|
from collections import deque
|
||||||
|
|
||||||
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
|
||||||
@@ -109,6 +122,67 @@ def indices_to_surface(width, height, indices, palette):
|
|||||||
return surf.convert()
|
return surf.convert()
|
||||||
|
|
||||||
|
|
||||||
|
def make_border_bg_transparent(surface, threshold=235):
|
||||||
|
'''Fix fuer Stefans Meldung (22.07.2026 abends): "der Rauser-Text fadet
|
||||||
|
nicht mit aus, bleibt hart umrandet". Ursache: die Rauser-PCC-Assets haben
|
||||||
|
KEINEN Alphakanal -- ihr papierweisser Hintergrund sitzt als hartes
|
||||||
|
Rechteck um die eigentliche Box-Grafik (RAUSER-Schriftzug/ADVERTAINMENT/
|
||||||
|
"!"-Box). Auf weissem Bildhintergrund faellt das nicht auf, aber sobald
|
||||||
|
die Szene zu Grau ueberblendet, bleibt dieses Rechteck stur weiss stehen
|
||||||
|
statt mit auszufaden. Ein v1-Versuch mit set_colorkey() wurde verworfen,
|
||||||
|
weil er auch die WEISSEN BUCHSTABEN in RAUSER1 durchsichtig machte.
|
||||||
|
|
||||||
|
Fix: Flood-Fill vom Bildrand aus ueber alle "papierweissen" Pixel (alle
|
||||||
|
Kanaele >= threshold) -- NUR die vom Rand aus zusammenhaengende Flaeche
|
||||||
|
wird transparent gemacht. Isolierte weisse Pixel MITTEN im Motiv (z.B.
|
||||||
|
weisse Buchstaben im RAUSER-Schriftzug) beruehren den Bildrand nicht und
|
||||||
|
bleiben deshalb unangetastet. Ergebnis: eine Surface mit echtem
|
||||||
|
Alphakanal, die beim Blit auf einen sich veraendernden Hintergrund
|
||||||
|
(weiss->grau) sauber durchscheint, statt eine harte Kante zu zeigen.'''
|
||||||
|
w, h = surface.get_size()
|
||||||
|
surf = surface.convert_alpha()
|
||||||
|
px = pygame.PixelArray(surf)
|
||||||
|
|
||||||
|
def is_bg(x, y):
|
||||||
|
color = surf.unmap_rgb(px[x, y])
|
||||||
|
return color.r >= threshold and color.g >= threshold and color.b >= threshold
|
||||||
|
|
||||||
|
seen = bytearray(w * h)
|
||||||
|
dq = deque()
|
||||||
|
|
||||||
|
def consider(x, y):
|
||||||
|
if not seen[y * w + x] and is_bg(x, y):
|
||||||
|
seen[y * w + x] = 1
|
||||||
|
dq.append((x, y))
|
||||||
|
|
||||||
|
for x in range(w):
|
||||||
|
consider(x, 0)
|
||||||
|
consider(x, h - 1)
|
||||||
|
for y in range(h):
|
||||||
|
consider(0, y)
|
||||||
|
consider(w - 1, y)
|
||||||
|
|
||||||
|
while dq:
|
||||||
|
cx, cy = dq.popleft()
|
||||||
|
if cx + 1 < w:
|
||||||
|
consider(cx + 1, cy)
|
||||||
|
if cx - 1 >= 0:
|
||||||
|
consider(cx - 1, cy)
|
||||||
|
if cy + 1 < h:
|
||||||
|
consider(cx, cy + 1)
|
||||||
|
if cy - 1 >= 0:
|
||||||
|
consider(cx, cy - 1)
|
||||||
|
del px
|
||||||
|
|
||||||
|
alpha = pygame.surfarray.pixels_alpha(surf)
|
||||||
|
for x in range(w):
|
||||||
|
for y in range(h):
|
||||||
|
if seen[y * w + x]:
|
||||||
|
alpha[x, y] = 0
|
||||||
|
del alpha
|
||||||
|
return surf
|
||||||
|
|
||||||
|
|
||||||
class Assets:
|
class Assets:
|
||||||
def __init__(self, dat_path):
|
def __init__(self, dat_path):
|
||||||
self.container = DATContainer(dat_path)
|
self.container = DATContainer(dat_path)
|
||||||
@@ -121,10 +195,16 @@ class Assets:
|
|||||||
w, h, indices, palette = self.pcc(name)
|
w, h, indices, palette = self.pcc(name)
|
||||||
return indices_to_surface(w, h, indices, palette)
|
return indices_to_surface(w, h, indices, palette)
|
||||||
|
|
||||||
def pcc_surface_scaled(self, name, scale=ASSET_DISPLAY_SCALE):
|
def pcc_surface_scaled(self, name, scale=ASSET_DISPLAY_SCALE, cut_border_bg=False):
|
||||||
'''Skaliert BEIDE Achsen um scale -- fuer die Rauser-Assets (dort per
|
'''Skaliert BEIDE Achsen um scale -- fuer die Rauser-Assets (dort per
|
||||||
Pixelvermessung als gleichmaessige 0.5x-Verkleinerung bestaetigt).'''
|
Pixelvermessung als gleichmaessige 0.5x-Verkleinerung bestaetigt).
|
||||||
|
cut_border_bg=True macht den vom Bildrand aus erreichbaren papierweissen
|
||||||
|
Hintergrund transparent (siehe make_border_bg_transparent) -- fuer die
|
||||||
|
Rauser-Teile, damit sie beim Fade zu Grau nicht als harte weisse
|
||||||
|
Kaesten stehen bleiben.'''
|
||||||
surf = self.pcc_surface(name)
|
surf = self.pcc_surface(name)
|
||||||
|
if cut_border_bg:
|
||||||
|
surf = make_border_bg_transparent(surf)
|
||||||
w, h = surf.get_size()
|
w, h = surf.get_size()
|
||||||
new_size = (max(1, round(w * scale)), max(1, round(h * scale)))
|
new_size = (max(1, round(w * scale)), max(1, round(h * scale)))
|
||||||
return pygame.transform.smoothscale(surf, new_size)
|
return pygame.transform.smoothscale(surf, new_size)
|
||||||
@@ -213,19 +293,15 @@ def build_rauser_parts(assets):
|
|||||||
'''Liefert die drei skalierten Rauser-Teile + ihre Endpositionen auf einer
|
'''Liefert die drei skalierten Rauser-Teile + ihre Endpositionen auf einer
|
||||||
320x240-Leinwand (RAUSER1 links, RAUSER3 rechts daneben, RAUSER2 darunter
|
320x240-Leinwand (RAUSER1 links, RAUSER3 rechts daneben, RAUSER2 darunter
|
||||||
mittig -- alle bei ASSET_DISPLAY_SCALE, siehe Docstring).'''
|
mittig -- alle bei ASSET_DISPLAY_SCALE, siehe Docstring).'''
|
||||||
r1 = assets.pcc_surface_scaled('RAUSER1') # "RAUSER"-Box
|
# cut_border_bg=True (Fix 22.07.2026 abends): macht den papierweissen
|
||||||
r2 = assets.pcc_surface_scaled('RAUSER2') # "ADVERTAINMENT"
|
# Hintergrund um jede Box transparent (nur die vom Bildrand aus
|
||||||
r3 = assets.pcc_surface_scaled('RAUSER3') # "!" auf blauer Box
|
# zusammenhaengende Flaeche, siehe make_border_bg_transparent) -- vorher
|
||||||
|
# blieb hier ein harter weisser Kasten stehen, der beim Fade zu Grau nicht
|
||||||
|
# mitgefadet ist (Stefans Meldung "Text bleibt hart umrandet stehen").
|
||||||
|
r1 = assets.pcc_surface_scaled('RAUSER1', cut_border_bg=True) # "RAUSER"-Box
|
||||||
|
r2 = assets.pcc_surface_scaled('RAUSER2', cut_border_bg=True) # "ADVERTAINMENT"
|
||||||
|
r3 = assets.pcc_surface_scaled('RAUSER3', cut_border_bg=True) # "!" auf blauer Box
|
||||||
|
|
||||||
# BEKANNTE UNSCHOENHEIT (bewusst NICHT "gefixt"): die PCC-Assets haben
|
|
||||||
# weissen Hintergrund in ihrer eigenen Bounding-Box (kein Alpha-Kanal).
|
|
||||||
# Auf weissem Grund faellt das nicht auf, nach dem Fade zu Grau sieht man
|
|
||||||
# einen schwachen weissen Kasten um RAUSER2/RAUSER3. Ein Versuch, Weiss
|
|
||||||
# per set_colorkey() durchsichtig zu machen, wurde wieder verworfen --
|
|
||||||
# dadurch wurden auch die WEISSEN BUCHSTABEN in RAUSER1 durchsichtig
|
|
||||||
# (schlimmer als der Kasten). Sauberer Fix braucht eine echte
|
|
||||||
# Alpha-Maske pro Asset statt eines einzelnen Colorkeys; low priority,
|
|
||||||
# siehe "Noch offen" unten.
|
|
||||||
total_w = r1.get_width() + r3.get_width()
|
total_w = r1.get_width() + r3.get_width()
|
||||||
total_h = r1.get_height() + r2.get_height()
|
total_h = r1.get_height() + r2.get_height()
|
||||||
x0 = (320 - total_w) // 2
|
x0 = (320 - total_w) // 2
|
||||||
@@ -267,7 +343,12 @@ def run_rauser(screen, clock, assets):
|
|||||||
sting = None
|
sting = None
|
||||||
try:
|
try:
|
||||||
if pygame.mixer.get_init():
|
if pygame.mixer.get_init():
|
||||||
sting_wav = tfmx_audio.render_wav(assets.container, 'TITEL2', seconds=1.2)
|
# KEIN fester seconds-Cap mehr (Fix 22.07.2026 abends, siehe
|
||||||
|
# tfmx_audio.render_wav-Docstring) -- render_tfmx erkennt das
|
||||||
|
# natuerliche Ende von TITEL2 selbst (~2s Jingle) und schneidet
|
||||||
|
# dort ab. Der alte harte 1.2s-Cap konnte den Sting theoretisch
|
||||||
|
# noch VOR seinem echten Ende kappen.
|
||||||
|
sting_wav = tfmx_audio.render_wav(assets.container, 'TITEL2')
|
||||||
sting = pygame.mixer.Sound(sting_wav)
|
sting = pygame.mixer.Sound(sting_wav)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
print(f'[intro_sequence] Rauser-Sound-Sting konnte nicht geladen werden: {exc}', file=sys.stderr)
|
print(f'[intro_sequence] Rauser-Sound-Sting konnte nicht geladen werden: {exc}', file=sys.stderr)
|
||||||
@@ -417,12 +498,30 @@ def run_once(screen, clock, assets):
|
|||||||
if quit_requested:
|
if quit_requested:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# 3) Factor5 (statisch)
|
# 3) Factor5 (statisches Bild, ABER mit eigener Melodie -- ONGAME2, der
|
||||||
|
# dritte/letzte bisher nicht zugeordnete TFMX-Track, siehe Stefans
|
||||||
|
# Meldung 22.07.2026 abends: "bei factor 5 gibts auch noch ne melodie
|
||||||
|
# solange das lied ist factor 5 anzeigen und dann zum kellogs
|
||||||
|
# praesentiert". Der Screen bleibt jetzt exakt so lange stehen wie das
|
||||||
|
# Stueck einmal komplett durchspielt (~123s natuerliche Laenge, siehe
|
||||||
|
# tfmx_audio.render_wav-Docstring), statt einer festen 3.0s-Dauer.
|
||||||
factor5 = build_letterboxed(assets.pcc_surface('FACTOR5'))
|
factor5 = build_letterboxed(assets.pcc_surface('FACTOR5'))
|
||||||
|
factor5_duration = 3.0 # Fallback falls kein Audio verfuegbar
|
||||||
|
factor5_sound = None
|
||||||
|
try:
|
||||||
|
if pygame.mixer.get_init():
|
||||||
|
factor5_wav = tfmx_audio.render_wav(assets.container, 'ONGAME2')
|
||||||
|
factor5_sound = pygame.mixer.Sound(factor5_wav)
|
||||||
|
factor5_duration = factor5_sound.get_length()
|
||||||
|
factor5_sound.play()
|
||||||
|
except Exception as exc:
|
||||||
|
print(f'[intro_sequence] Factor5-Melodie konnte nicht geladen werden: {exc}', file=sys.stderr)
|
||||||
|
|
||||||
def show_factor5(_t):
|
def show_factor5(_t):
|
||||||
blit_scaled_centered(screen, factor5, WINDOW_W, WINDOW_H)
|
blit_scaled_centered(screen, factor5, WINDOW_W, WINDOW_H)
|
||||||
quit_requested = wait_or_skip(clock, 3.0, show_factor5)
|
quit_requested = wait_or_skip(clock, factor5_duration, show_factor5)
|
||||||
|
if factor5_sound is not None:
|
||||||
|
factor5_sound.stop()
|
||||||
if quit_requested:
|
if quit_requested:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -436,9 +535,14 @@ def run_once(screen, clock, assets):
|
|||||||
# bis zum Ende der Sequenz (Original hat vermutlich durchgehende
|
# bis zum Ende der Sequenz (Original hat vermutlich durchgehende
|
||||||
# Titelmusik ueber Logo+Titelbild+Karte -- noch nicht per Referenz-
|
# Titelmusik ueber Logo+Titelbild+Karte -- noch nicht per Referenz-
|
||||||
# Sichtung bestaetigt, aber plausibelste Annahme fuers erste v1).
|
# Sichtung bestaetigt, aber plausibelste Annahme fuers erste v1).
|
||||||
|
# KEIN fester seconds-Cap mehr (Fix 22.07.2026 abends): TITEL ist
|
||||||
|
# tatsaechlich ~109s lang -- der alte 60s-Cap (plus ein liegen
|
||||||
|
# gebliebener 8s-Debug-Cache) hat das Stueck weit vor seinem echten Ende
|
||||||
|
# abgeschnitten. Das war Stefans "Musik ist zu kurz, da fehlt der
|
||||||
|
# Rest"-Bug. render_tfmx erkennt den natuerlichen Loop-Punkt jetzt selbst.
|
||||||
try:
|
try:
|
||||||
if pygame.mixer.get_init():
|
if pygame.mixer.get_init():
|
||||||
titel_wav = tfmx_audio.render_wav(assets.container, 'TITEL', seconds=60)
|
titel_wav = tfmx_audio.render_wav(assets.container, 'TITEL')
|
||||||
pygame.mixer.music.load(titel_wav)
|
pygame.mixer.music.load(titel_wav)
|
||||||
pygame.mixer.music.play(-1)
|
pygame.mixer.music.play(-1)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
|
|||||||
+21
-2
@@ -35,9 +35,28 @@ def _ensure_binary():
|
|||||||
subprocess.run(['bash', os.path.join(_PLAYER_DIR, 'build.sh')], check=True)
|
subprocess.run(['bash', os.path.join(_PLAYER_DIR, 'build.sh')], check=True)
|
||||||
|
|
||||||
|
|
||||||
def render_wav(container, name, seconds=20, force=False):
|
def render_wav(container, name, seconds=180, force=False):
|
||||||
"""container: kellogg_formats.DATContainer; name: z.B. 'TITEL' (ohne Extension).
|
"""container: kellogg_formats.DATContainer; name: z.B. 'TITEL' (ohne Extension).
|
||||||
Gibt den Pfad zur gerenderten (gecachten) WAV-Datei zurueck."""
|
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)
|
os.makedirs(_CACHE_DIR, exist_ok=True)
|
||||||
out_path = os.path.join(_CACHE_DIR, f'{name}.wav')
|
out_path = os.path.join(_CACHE_DIR, f'{name}.wav')
|
||||||
if os.path.exists(out_path) and not force:
|
if os.path.exists(out_path) and not force:
|
||||||
|
|||||||
@@ -62,10 +62,23 @@ static void write_wav(const char *path, int16_t *pcm, int32_t frames, int32_t sa
|
|||||||
|
|
||||||
int main(int argc, char **argv) {
|
int main(int argc, char **argv) {
|
||||||
if (argc < 4) {
|
if (argc < 4) {
|
||||||
fprintf(stderr, "usage: %s <mdat.tfx> <smpl.sam> <out.wav> [seconds]\n", argv[0]);
|
fprintf(stderr, "usage: %s <mdat.tfx> <smpl.sam> <out.wav> [max_seconds]\n", argv[0]);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
double seconds = argc > 4 ? atof(argv[4]) : 15.0;
|
// WICHTIG (Fix 22.07.2026 abends): [max_seconds] ist NUR noch eine
|
||||||
|
// Sicherheits-Obergrenze, keine Ziel-Laenge mehr! Vorher wurde exakt
|
||||||
|
// `seconds` lang gerendert -- war der Wert kleiner als die echte
|
||||||
|
// Songlaenge, wurde mitten im Stueck abgeschnitten (Stefans Meldung:
|
||||||
|
// "die Musik ist zu kurz, da fehlt der Rest", verursacht durch TITEL mit
|
||||||
|
// festen 60s bzw. einen stehengebliebenen 8s-Cache-Rest). Stattdessen
|
||||||
|
// rendern wir jetzt in kleinen Haeppchen und beobachten tfmx.h's eigenes
|
||||||
|
// `real_song_end`-Flag (siehe tfmx.h: wird pro Tick in tfmx_run()
|
||||||
|
// gesetzt, sobald die Track-Sequenz einmal komplett durchgelaufen ist
|
||||||
|
// und intern neu startet) -- sobald es feuert, haben wir GENAU eine
|
||||||
|
// vollstaendige natuerliche Song-Schleife im Kasten und brechen ab,
|
||||||
|
// egal wie lang das Stueck wirklich ist. max_seconds greift nur als
|
||||||
|
// Notbremse, falls ein Modul (defekt/kaputt) nie ein Ende meldet.
|
||||||
|
double max_seconds = argc > 4 ? atof(argv[4]) : 180.0;
|
||||||
|
|
||||||
uint32_t mdat_len = 0, smpl_len = 0;
|
uint32_t mdat_len = 0, smpl_len = 0;
|
||||||
uint8_t *mdat = load_file(argv[1], &mdat_len);
|
uint8_t *mdat = load_file(argv[1], &mdat_len);
|
||||||
@@ -88,20 +101,61 @@ int main(int argc, char **argv) {
|
|||||||
memcpy(bundle + hdr_off + mdat_len, smpl, smpl_len);
|
memcpy(bundle + hdr_off + mdat_len, smpl, smpl_len);
|
||||||
|
|
||||||
int32_t sample_rate = 44100;
|
int32_t sample_rate = 44100;
|
||||||
void *s = tfmx_api.init(bundle, bundle_len, sample_rate);
|
// Direkt tfmx_init/tfmx_get_audio statt der generischen player_api-
|
||||||
|
// Indirektion nutzen -- wir brauchen Zugriff auf state->real_song_end,
|
||||||
|
// das die generische Schnittstelle (nur get_audio(state, out, frames))
|
||||||
|
// nicht durchreicht.
|
||||||
|
struct tfmx_state *s = tfmx_init(bundle, bundle_len, sample_rate);
|
||||||
if (!s) {
|
if (!s) {
|
||||||
fprintf(stderr, "tfmx_api.init failed (module not recognized)\n");
|
fprintf(stderr, "tfmx_init failed (module not recognized)\n");
|
||||||
return 2;
|
return 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
int32_t total_frames = (int32_t)(seconds * sample_rate);
|
int32_t max_frames = (int32_t)(max_seconds * sample_rate);
|
||||||
float *scratch = malloc(sizeof(float) * total_frames * 2);
|
// Chunk klein genug waehlen, dass real_song_end nicht durch einen
|
||||||
int16_t *pcm = malloc(sizeof(int16_t) * total_frames * 2);
|
// zweiten Tick INNERHALB desselben Chunks schon wieder auf 0 zurueck-
|
||||||
player_get_audio_s16(&tfmx_api, s, pcm, scratch, total_frames);
|
// gesetzt wurde, bevor wir nachsehen (ein Tick liegt typischerweise bei
|
||||||
|
// ~20ms/882 Frames bei 44.1kHz -- 64 Frames sind davon weit entfernt).
|
||||||
|
const int32_t CHUNK = 64;
|
||||||
|
float *scratch = malloc(sizeof(float) * CHUNK * 2);
|
||||||
|
int16_t *pcm = malloc(sizeof(int16_t) * (size_t)max_frames * 2);
|
||||||
|
if (!scratch || !pcm) {
|
||||||
|
fprintf(stderr, "out of memory\n");
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
|
||||||
write_wav(argv[3], pcm, total_frames, sample_rate);
|
int32_t total = 0;
|
||||||
fprintf(stderr, "wrote %s: %d frames @ %d Hz (%.1fs)\n", argv[3], total_frames, sample_rate, seconds);
|
int natural_end = 0;
|
||||||
|
while (total < max_frames) {
|
||||||
|
int32_t this_chunk = CHUNK;
|
||||||
|
if (total + this_chunk > max_frames) {
|
||||||
|
this_chunk = max_frames - total;
|
||||||
|
}
|
||||||
|
|
||||||
tfmx_api.free(s);
|
memset(scratch, 0, sizeof(float) * (size_t)this_chunk * 2);
|
||||||
|
tfmx_get_audio(s, scratch, this_chunk);
|
||||||
|
|
||||||
|
int16_t *out_ptr = pcm + (size_t)total * 2;
|
||||||
|
for (int32_t i = 0; i < this_chunk * 2; ++i) {
|
||||||
|
float v = scratch[i] * 32767.0f;
|
||||||
|
if (v > 32767.0f) v = 32767.0f;
|
||||||
|
if (v < -32768.0f) v = -32768.0f;
|
||||||
|
out_ptr[i] = (int16_t)v;
|
||||||
|
}
|
||||||
|
total += this_chunk;
|
||||||
|
|
||||||
|
if (s->real_song_end) {
|
||||||
|
natural_end = 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
write_wav(argv[3], pcm, total, sample_rate);
|
||||||
|
fprintf(stderr, "wrote %s: %d frames @ %d Hz (%.2fs)%s\n",
|
||||||
|
argv[3], total, sample_rate, (double)total / sample_rate,
|
||||||
|
natural_end ? " [natuerliches Songende/Loop-Punkt erkannt]"
|
||||||
|
: " [Sicherheits-Obergrenze erreicht, kein Songende gefunden -- Modul pruefen]");
|
||||||
|
|
||||||
|
tfmx_free(s);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user