Factor5-Sound vollstaendig: Ausklang-Tail im TFMX-Renderer (loop_mode-Stopp + Fade)

Stefans Meldung "Factor5-Zeit zu kurz, Soundfile nicht ganz abgespielt" -- die
Ursache lag im Renderer: real_song_end markiert nur das Ende der TRACK-SEQUENZ
(2.4s), die dann noch klingenden Samples wurden hart abgeschnitten.

- render_tfmx.c: neues optionales 6. CLI-Arg tail_max_seconds. Im Tail-Modus
  wird loop_mode=0 gesetzt -- tfmx.h laesst den Sequencer bei song_end dann
  STEHEN (kein Restart, keine neuen Noten), waehrend die Paula-Voices weiter
  ausklingen. (Erster Versuch ohne loop_mode-Stopp nahm stattdessen die
  naechste Loop-Iteration auf -- per RMS-Selbstaehnlichkeit nachgewiesen.)
  Tail endet bei ~300ms Stille oder tail-Limit; bei Limit-Ende (loopende
  Samples ohne natuerliches Ende) wird ein 600ms-Fade-out angewendet.
- Factor5 ("Brausen") loopt auf Paula-Ebene endlos -> Tail 2.6s => Gesamt
  exakt 5.0s (DOSBox-Messung ~4.8s Screendauer, Stefans "ca. 5 Sekunden"),
  sauber ausgeblendet. Rauser-Sting endet natuerlich bei 2.34s.
- audio.py: tail_seconds-Parameter, Cache-Suffix _tail (alte abgeschnittene
  Caches greifen nicht mehr). intro.py: Jingles mit Tail, Titelmusik bewusst
  ohne (exakter Loop-Punkt-Schnitt fuers nahtlose pygame-Loopen).

Screen haelt weiterhin exakt Songlaenge (jetzt 5.0s), verifiziert: WAV klingt
aus (RMS 2065->261), --once headless exit 0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
duffyduck
2026-07-24 22:22:31 +02:00
co-authored by Claude Fable 5
parent ccad606a43
commit 949a7eeda2
3 changed files with 114 additions and 19 deletions
+22 -7
View File
@@ -62,7 +62,7 @@ def _ensure_binary():
subprocess.run(['bash', os.path.join(_PLAYER_DIR, 'build.sh')], check=True)
def render_wav(container, name, seconds=180, force=False, song_index=None):
def render_wav(container, name, seconds=180, force=False, song_index=None, tail_seconds=None):
"""container: kellogg_formats.DATContainer; name: z.B. 'TITEL' (ohne Extension).
song_index: optional -- welcher der ggf. mehreren in <name>.TFX gebuendelten
Songs gerendert werden soll (siehe Modul-Docstring). None/0 = Default-
@@ -78,13 +78,24 @@ def render_wav(container, name, seconds=180, force=False, song_index=None):
laengsten bekannten Stuecke hinaus gewaehlt, damit nichts abgeschnitten
wird; im Normalfall stoppt render_tfmx laengst vorher von selbst.
tail_seconds (24.07.2026, Factor5-"Soundfile nicht ganz abgespielt"-Fix):
fuer kurze Jingles/Stinger. real_song_end markiert nur das Ende der
TRACK-SEQUENZ -- die dann noch klingenden Samples (Ausklang) wuerden ohne
Tail hart abgeschnitten. Mit tail_seconds > 0 rendert render_tfmx nach dem
Sequenz-Ende weiter, bis ~300ms Stille erreicht sind (max. tail_seconds).
Fuer geloopte lange Tracks (Titelmusik) NICHT setzen -- dort ist der
exakte Loop-Punkt-Schnitt fuers nahtlose Loopen der richtige.
ACHTUNG Cache: der Dateiname ist `<name>.wav` (song_index None/0) bzw.
`<name>_song<N>.wav` fuer N>0. Wird `seconds` kleiner gewaehlt als die
echte Songlaenge, MUSS `force=True` gesetzt werden, sonst liefert diese
Funktion einen ggf. laenger/kuerzer gerenderten alten Cache-Treffer
zurueck, egal was `seconds` diesmal sagt."""
`<name>_song<N>.wav` fuer N>0; mit tail_seconds kommt `_tail` dazu (damit
alte, noch abgeschnittene Cache-Dateien nicht faelschlich weiterverwendet
werden). Wird `seconds` kleiner gewaehlt als die echte Songlaenge, MUSS
`force=True` gesetzt werden, sonst liefert diese Funktion einen ggf.
laenger/kuerzer gerenderten alten Cache-Treffer zurueck."""
os.makedirs(_CACHE_DIR, exist_ok=True)
suffix = f'_song{song_index}' if song_index else ''
if tail_seconds:
suffix += '_tail'
out_path = os.path.join(_CACHE_DIR, f'{name}{suffix}.wav')
if os.path.exists(out_path) and not force:
return out_path
@@ -100,8 +111,12 @@ def render_wav(container, name, seconds=180, force=False, song_index=None):
f.write(sam_data)
cmd = [_BINARY, tfx_path, sam_path, out_path, str(seconds)]
if song_index:
cmd.append(str(song_index))
if song_index or tail_seconds:
# song_index ist Positionsarg vor tail_seconds -- bei tail ohne
# expliziten Song -1 uebergeben (= Default-Song, siehe render_tfmx.c)
cmd.append(str(song_index if song_index else -1))
if tail_seconds:
cmd.append(str(tail_seconds))
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode != 0:
raise RuntimeError(f'render_tfmx failed for {name} (song_index={song_index}): {result.stderr}')