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)
- The TTS engine writes each line as a raw PCM WAV temp file (
app/tts.py, _save_wav → soundfile.write). This is lossless.
- 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.
- At merge time, each stored MP3 is loaded back (
AudioSegment.from_file), which decodes it to PCM.
- 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
- 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.
- 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().
- 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
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)
app/tts.py,_save_wav→soundfile.write). This is lossless.app/project.py→segment.export(mp3_filepath, format="mp3")export(..., format="mp3")call in the batch path.AudioSegment.from_file), which decodes it to PCM.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/.flacvoicelines:pcm_s16le / flac, 24000 Hz, 1 channels, s16). Encoding a 24 kHz mono source through ffmpeg's default MP3 path producesmp3, 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
Proposed direction
combine_audio_with_pauses().Notes
combine_audio_with_pauses()step inapp/tts.pydoesn't byte-concatenate files — it decodes segments to PCM and joinsAudioSegmentobjects 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.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