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:
co-authored by
Claude Opus 5
parent
3496b4a2df
commit
d9554c590c
+18
-3
@@ -334,14 +334,29 @@ export async function stopAndSave() {
|
||||
// lame's psymodel asserts (`calc_energy: el >= 0`) on pure digital silence —
|
||||
// real since the gated mic path: nobody spoke = all-zero samples. Fall back
|
||||
// to a filterless encode instead of failing the whole stop.
|
||||
// KONSTANTE BITRATE, kein VBR (früher `-qscale:a 3`). Grund ist die
|
||||
// Wort-Markierung im Transkript: der Browser springt in einer VBR-MP3 über
|
||||
// die Xing-Tabelle, und die hat nur 100 Stützstellen — bei einer 63-Minuten-
|
||||
// Aufnahme also ein Raster von 38 s. Er landet auf der Stützstelle davor,
|
||||
// meldet aber die angefragte Zeit zurück. Gemessen an #85: Sprung auf 20:40
|
||||
// spielte in Wahrheit ab 20:11,5 (−28,5 s), die Markierung lief entsprechend
|
||||
// dem Ton voraus. Bei CBR ist die Byte-Position proportional zur Zeit, jeder
|
||||
// Sprung trifft damit auf Frame-Genauigkeit (~26 ms). 64 kbit ≈ die bisherige
|
||||
// Durchschnittsrate (68 kbit), die Dateigröße bleibt also praktisch gleich.
|
||||
// `-ar 48000` fest, damit der Fallback-Zweig ohne loudnorm nicht bei 16 kHz
|
||||
// landet (= MPEG-2.5, andere Framelänge).
|
||||
// `-write_xing 0` muss dazu: die Xing-Tabelle speichert Byte-Positionen nur
|
||||
// in 1/256 der Dateigröße, bei einer Stunde Audio also ~15 s grob — ein
|
||||
// Browser, der sie benutzt, wäre trotz CBR wieder daneben. Ohne sie rechnet
|
||||
// er linear, was bei CBR exakt ist (nachgemessen: 0,02 s = ein halber Frame).
|
||||
const MP3_ARGS = ['-codec:a', 'libmp3lame', '-b:a', '64k', '-ar', '48000', '-ac', '1', '-write_xing', '0'];
|
||||
try {
|
||||
await pexec('ffmpeg', ['-y', '-i', c.wavPath,
|
||||
'-af', 'highpass=f=80,loudnorm=I=-16:TP=-1.5:LRA=11',
|
||||
'-codec:a', 'libmp3lame', '-qscale:a', '3', mp3Path]);
|
||||
...MP3_ARGS, mp3Path]);
|
||||
} catch (e) {
|
||||
console.warn('mp3 encode with filters failed (silence?), retrying plain:', String(e.message).slice(0, 200));
|
||||
await pexec('ffmpeg', ['-y', '-i', c.wavPath,
|
||||
'-codec:a', 'libmp3lame', '-qscale:a', '3', mp3Path]);
|
||||
await pexec('ffmpeg', ['-y', '-i', c.wavPath, ...MP3_ARGS, mp3Path]);
|
||||
}
|
||||
|
||||
// Move the wav into REC_DIR (rootfs — survives a reboot); it is NOT left in
|
||||
|
||||
Reference in New Issue
Block a user