Skip to content

[Bug]: MP3 Non-Transparent Bitrate #73

Description

@ChristianMHolt

What happened?

Title: Segment and final audiobook audio uses lossy MP3 at ffmpeg's default low bitrate, re-encoded twice (generation loss)

Summary

The current pipeline stores each per-line voiceline as MP3 and the final merged audiobook as MP3, both with no bitrate specified — so ffmpeg uses its default, which for this project's 24000 Hz mono TTS output is only ~32 kb/s (not 128k as one might assume for music; ffmpeg's default MP3 bitrate scales with sample rate and channel count). On its own that's already well below transparent, but the larger problem is that the final merge step decodes each stored MP3 segment back to PCM and re-encodes the entire result to MP3 a second time, compounding lossy artifacts. This generation loss noticeably degrades the final audiobook relative to the WAV voice-reference samples used as model input.

How the pipeline works today (where the loss happens)

  1. The TTS engine writes each line as a raw PCM WAV temp file (app/tts.py, _save_wav → soundfile.write). This is lossless.
  2. That WAV is re-encoded and stored as MP3 with no bitrate argument:
    • Single-chunk: app/project.py → segment.export(mp3_filepath, format="mp3")
    • Batch: same export(..., format="mp3") call in the batch path.
  3. At merge time, each stored MP3 is loaded back (AudioSegment.from_file), which decodes it to PCM.
  4. The segments are joined in memory with pauses, and the whole result is re-encoded to MP3 again (app/project.py, merge_audio() → final_audio.export(output_path, format="mp3")), again at the default bitrate.

So the audio is MP3-encoded twice before it reaches you — once at segment save, once at final merge. Lossy→lossy is generation loss: each pass adds and compounds artifacts that the previous pass introduced.

What the default bitrate actually is

ffmpeg's default MP3 bitrate is not a fixed 128 kb/s — it is chosen by libmp3lame from the input's sample rate and channel count. This project's TTS model outputs 24000 Hz mono (confirmed by probing temp_batch_*.wav/.flac voicelines: pcm_s16le / flac, 24000 Hz, 1 channels, s16). Encoding a 24 kHz mono source through ffmpeg's default MP3 path produces mp3, 24000 Hz, mono, fltp, 32 kb/s. For comparison, the same default applied to 44.1 kHz stereo would be 128 kb/s. So the segments here were being stored at roughly a quarter of what someone assuming "128k default" would expect.

Why this matters

  • Double lossy encoding. Even a single 32 kb/s MP3 pass on 24 kHz mono is audibly limited; the second encode at the merge step compounds it. The final file has passed through lossy compression twice, neither pass configurable.
  • No control over quality. No bitrate, mode, or format option is exposed, so even users who'd accept lossy output can't request a sane one.
  • Wasted intermediates. Storing segments as MP3 saves disk but costs fidelity, and that "saving" is paid for twice downstream.

Proposed direction

  1. Don't use lossy codecs as intermediates. Store per-line voicelines losslessly (WAV or FLAC) so that the only lossy pass — if any — happens exactly once, at final export. This is the single most impactful change.
  2. Offer a lossless output option (e.g., FLAC). Disk impact is modest: at this project's 24 kHz mono rate, real FLAC voicelines measure ~25 KB/s (~90 MB/hour), so even a ~30-hour book is on the order of a couple of gigabytes. FLAC decodes back to bit-identical PCM, so it's safe to concatenate via the existing decode-and-join step in combine_audio_with_pauses().
  3. If a lossy final is still desired, at minimum encode it once from the lossless intermediate at a sample-rate-appropriate high bitrate, rather than twice at ffmpeg's default.

Notes

  • The combine_audio_with_pauses() step in app/tts.py doesn't byte-concatenate files — it decodes segments to PCM and joins AudioSegment objects in memory — so the segment storage format isn't constrained by any concatenation requirement; any format ffmpeg can read (MP3, WAV, FLAC, …) already works there.
  • I arrived at the 32 kb/s figure empirically (generated test tones at 24 kHz mono, encoded with ffmpeg's default MP3 settings, and probed the output), and confirmed the model's native output rate by probing the actual temp_batch_*.wav / voiceline files in this project.

Steps to reproduce

Generate voicelines.

Terminal / log output

GPU

NVIDIA (Windows)

GPU model and VRAM

RTX 5080

TTS Mode

Local (built-in)

Voice type (if relevant)

Custom Voice

Additional context

No response

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions