Rauser-Fade-Bug (Alpha-Cutout), Musik-Laenge-Bug (real_song_end-Erkennung) und Factor5-Melodie gefixt

This commit is contained in:
ARIA
2026-07-22 21:31:33 +00:00
parent 6d196020db
commit 094aca0e1e
4 changed files with 282 additions and 38 deletions
+67
View File
@@ -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
View File
@@ -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
View File
@@ -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:
+65 -11
View File
@@ -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;
}