fix(audio): Wiedergabe-MP3 als CBR ohne Xing-Tabelle -- Wortmarkierung war 28 s voraus

Die Markierung im Transkript stand auf einem Wort, gesprochen wurde eins vier
Zeilen weiter oben (gemessen an #85: 24 s Vorlauf).

Nicht die Ursache -- jeweils per Gegenprobe ausgeschlossen: die Wortzeiten
(Ausschnitte bei 600/1240/3400 s einzeln transkribiert, enthalten exakt die
Wörter der DB), MP3- vs WAV-Zeitachse (identisch), ffmpeg-Seek (trifft),
ontimeupdate-Logik (korrekt).

Ursache war der Sprung im Browser: in einer VBR-Datei gibt es keine feste
Byte-pro-Sekunde-Regel, also nutzt er die Xing-Tabelle -- 100 Stützstellen,
bei 63 Minuten ein 38-s-Raster. Er landet auf der Stützstelle davor, meldet
über currentTime aber die angefragte Zeit. Nachgerechnet: Ziel 20:40 spielt
ab 20:11,5 (-28,5 s).

Fix: -b:a 64k -ar 48000 -ac 1 -write_xing 0 statt -qscale:a 3. Byte-Position
proportional zur Zeit. Die Tabelle muss auch bei CBR weg, sie rastert auf
1/256 der Dateigröße (~15 s pro Stunde, Restfehler bis -6,4 s gemessen). Ohne
sie rechnet der Decoder linear: Restfehler 0,02 s an neun Stichpunkten.

lab/recode-cbr.sh schreibt den Altbestand einmalig um (Quelle WAV, sonst die
MP3 selbst), lab/tocerr.py ist das Messwerkzeug dahinter.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
schakal00783
2026-08-02 23:40:29 +02:00
co-authored by Claude Opus 5
parent 3496b4a2df
commit d9554c590c
5 changed files with 239 additions and 3 deletions
+79
View File
@@ -0,0 +1,79 @@
#!/usr/bin/env python3
"""Wie weit daneben landet ein Browser-Seek in dieser MP3?
Ein Browser springt in einer VBR-Datei über die Xing-TOC: 100 Stützstellen,
linear interpoliert -> Byte-Position. Er meldet danach die ANGEFRAGTE Zeit,
dekodiert aber ab dem gefundenen Byte. Dieses Skript rechnet beides nach:
Byte-Position wie der Browser, echte Zeit per Frame-Zählung.
"""
import sys, struct
path = sys.argv[1]
targets = [float(x) for x in sys.argv[2].split(',')]
data = open(path, 'rb').read()
BITRATES = [0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 0] # MPEG1 Layer3
SRATES = [44100, 48000, 32000]
def frame_len(hdr):
if hdr[0] != 0xFF or (hdr[1] & 0xE0) != 0xE0:
return None, None, None
ver = (hdr[1] >> 3) & 3 # 3 = MPEG1
layer = (hdr[1] >> 1) & 3 # 1 = Layer III
if ver != 3 or layer != 1:
return None, None, None
br = BITRATES[(hdr[2] >> 4) & 0xF] * 1000
sr = SRATES[(hdr[2] >> 2) & 3]
pad = (hdr[2] >> 1) & 1
if br == 0 or sr == 0:
return None, None, None
return int(144 * br / sr) + pad, sr, 1152
# --- alle Frames einmal durchzählen: Byte -> exakte Zeit -------------------
pos, table = 0, [] # (byte_offset, start_time)
t = 0.0
while pos < len(data) - 4:
ln, sr, spf = frame_len(data[pos:pos + 4])
if not ln:
pos += 1
continue
table.append((pos, t))
t += spf / sr
pos += ln
print(f'{len(table)} frames, echte Dauer {t:.1f} s, Dateigroesse {len(data)} bytes')
# --- Xing-TOC ---------------------------------------------------------------
i = data.find(b'Xing')
if i < 0:
i = data.find(b'Info')
flags = struct.unpack('>I', data[i + 4:i + 8])[0]
p = i + 8
frames = struct.unpack('>I', data[p:p + 4])[0] if flags & 1 else None
if flags & 1: p += 4
bytes_total = struct.unpack('>I', data[p:p + 4])[0] if flags & 2 else len(data)
if flags & 2: p += 4
toc = list(data[p:p + 100]) if flags & 4 else None
print(f'Xing: frames={frames}, bytes={bytes_total}, TOC={"ja" if toc else "nein"}')
if not toc:
sys.exit('kein TOC -> Browser extrapoliert rein linear')
def real_time_at(byte):
lo, best = 0, table[0][1]
for off, tt in table:
if off <= byte: best = tt
else: break
return best
print(f'\n{"Ziel":>8} {"Byte(TOC)":>12} {"echte Zeit":>12} {"Fehler":>10}')
for target in targets:
pct = target / t * 100.0
a = int(pct)
fa = toc[a]
fb = toc[a + 1] if a < 99 else 256.0
frac = pct - a
byte = int((fa + (fb - fa) * frac) / 256.0 * bytes_total)
real = real_time_at(byte)
lin_byte = int(target / t * len(data))
lin_real = real_time_at(lin_byte)
print(f'{target:8.1f} {byte:12d} {real:12.1f} {real - target:+10.1f} s'
f' | linear: {lin_real:8.1f} {lin_real - target:+8.1f} s')