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.
|
||||
|
||||
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
|
||||
an fest. Sobald beide Teile ihre Endposition erreicht haben, spielt ein
|
||||
kurzer Sound-Sting (TITEL2.TFX/.SAM -- das kuerzeste der 3 Musikstuecke,
|
||||
genau ein "Bling"-Jingle, kein Loop). Danach faedet der WEISSE
|
||||
Hintergrund kurz zu GRAU um (NICHT weiss -- war ein Fehler in v1!), haelt
|
||||
kurz, dann schwarze Pause. WICHTIG: alle Rauser-Assets werden nur bei
|
||||
**halber Pixel-Groesse** (0.5x) ihrer PCC-Nativaufloesung gezeichnet --
|
||||
bei 1:1 sind sie in der echten DOSBox-Aufnahme klar zu groß (per
|
||||
Pixel-Vermessung bestaetigt, siehe NOTES.md "Rauser/Titelbild-Korrektur").
|
||||
ein "Bling"-Jingle von ~2s natuerlicher Laenge, kein Loop). Danach faedet
|
||||
der WEISSE Hintergrund kurz zu GRAU um (NICHT weiss -- war ein Fehler in
|
||||
v1!), haelt kurz, dann schwarze Pause. WICHTIG: alle Rauser-Assets werden
|
||||
nur bei **halber Pixel-Groesse** (0.5x) ihrer PCC-Nativaufloesung
|
||||
gezeichnet -- bei 1:1 sind sie in der echten DOSBox-Aufnahme klar zu groß
|
||||
(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
|
||||
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
|
||||
5. Kellogg's-Markenlogo (KELLOGGS.PCC) erscheint SOFORT komplett; darunter
|
||||
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 os
|
||||
import time
|
||||
from collections import deque
|
||||
|
||||
sys.path.insert(0, os.path.dirname(__file__))
|
||||
from kellogg_formats import DATContainer, load_pcc
|
||||
@@ -109,6 +122,67 @@ def indices_to_surface(width, height, indices, palette):
|
||||
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:
|
||||
def __init__(self, dat_path):
|
||||
self.container = DATContainer(dat_path)
|
||||
@@ -121,10 +195,16 @@ class Assets:
|
||||
w, h, indices, palette = self.pcc(name)
|
||||
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
|
||||
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)
|
||||
if cut_border_bg:
|
||||
surf = make_border_bg_transparent(surf)
|
||||
w, h = surf.get_size()
|
||||
new_size = (max(1, round(w * scale)), max(1, round(h * scale)))
|
||||
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
|
||||
320x240-Leinwand (RAUSER1 links, RAUSER3 rechts daneben, RAUSER2 darunter
|
||||
mittig -- alle bei ASSET_DISPLAY_SCALE, siehe Docstring).'''
|
||||
r1 = assets.pcc_surface_scaled('RAUSER1') # "RAUSER"-Box
|
||||
r2 = assets.pcc_surface_scaled('RAUSER2') # "ADVERTAINMENT"
|
||||
r3 = assets.pcc_surface_scaled('RAUSER3') # "!" auf blauer Box
|
||||
# cut_border_bg=True (Fix 22.07.2026 abends): macht den papierweissen
|
||||
# Hintergrund um jede Box transparent (nur die vom Bildrand aus
|
||||
# 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_h = r1.get_height() + r2.get_height()
|
||||
x0 = (320 - total_w) // 2
|
||||
@@ -267,7 +343,12 @@ def run_rauser(screen, clock, assets):
|
||||
sting = None
|
||||
try:
|
||||
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)
|
||||
except Exception as exc:
|
||||
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:
|
||||
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_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):
|
||||
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:
|
||||
return True
|
||||
|
||||
@@ -436,9 +535,14 @@ def run_once(screen, clock, assets):
|
||||
# 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).
|
||||
# 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:
|
||||
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.play(-1)
|
||||
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)
|
||||
|
||||
|
||||
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).
|
||||
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)
|
||||
out_path = os.path.join(_CACHE_DIR, f'{name}.wav')
|
||||
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) {
|
||||
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;
|
||||
}
|
||||
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;
|
||||
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);
|
||||
|
||||
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) {
|
||||
fprintf(stderr, "tfmx_api.init failed (module not recognized)\n");
|
||||
fprintf(stderr, "tfmx_init failed (module not recognized)\n");
|
||||
return 2;
|
||||
}
|
||||
|
||||
int32_t total_frames = (int32_t)(seconds * sample_rate);
|
||||
float *scratch = malloc(sizeof(float) * total_frames * 2);
|
||||
int16_t *pcm = malloc(sizeof(int16_t) * total_frames * 2);
|
||||
player_get_audio_s16(&tfmx_api, s, pcm, scratch, total_frames);
|
||||
int32_t max_frames = (int32_t)(max_seconds * sample_rate);
|
||||
// Chunk klein genug waehlen, dass real_song_end nicht durch einen
|
||||
// zweiten Tick INNERHALB desselben Chunks schon wieder auf 0 zurueck-
|
||||
// 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);
|
||||
fprintf(stderr, "wrote %s: %d frames @ %d Hz (%.1fs)\n", argv[3], total_frames, sample_rate, seconds);
|
||||
int32_t total = 0;
|
||||
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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user