Skip to content

Commit bb77301

Browse files
committed
feat(tts): add --tts-pad-silence-ms to circumvent VLC C2PA buffer drop
This introduces `tts_pad_silence_ms` to the TTS pipeline to inject pure silence frames at the head of the audio before writing to file or handing back via the ABI. VLC drops the initial ~1.5 seconds of streaming playback when reading a file containing a massive C2PA manifest (often 2-3KB appended at the end of a WAV file). Padding the audio stream directly ensures VLC only drops the padded silence, preserving the actual spoken speech while leaving the cryptographic provenance manifest intact. This is exposed to all ABI integrations via `crispasr_session_set_tts_pad_silence_ms(session, ms)` and the HTTP API as `"pad_silence_ms"`. For streaming ABI paths, the padding is only applied to the first chunk to avoid accumulating inter-sentence latency.
1 parent 42f56cf commit bb77301

16 files changed

Lines changed: 130 additions & 3 deletions

File tree

bindings/csharp/CrispASR/NativeMethods.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,10 @@ internal static extern int crispasr_session_set_instruct(
8888
internal static extern int crispasr_session_set_tts_phonemes(
8989
IntPtr s, [MarshalAs(UnmanagedType.LPUTF8Str)] string phonemes);
9090

91+
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
92+
internal static extern void crispasr_session_set_tts_pad_silence_ms(
93+
IntPtr s, int ms);
94+
9195
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
9296
internal static extern int crispasr_session_is_custom_voice(IntPtr s);
9397

bindings/csharp/CrispASR/Session.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,12 @@ public void SetTtsPhonemes(string phonemes)
177177
if (rc != 0) throw new InvalidOperationException($"set_tts_phonemes failed (rc={rc})");
178178
}
179179

180+
/// <summary>Pad N ms of silence at the beginning of TTS output. Useful to bypass VLC playback bugs where it drops the first ~1.5s of audio while parsing a large C2PA chunk.</summary>
181+
public void SetTtsPadSilenceMs(int ms)
182+
{
183+
NativeMethods.crispasr_session_set_tts_pad_silence_ms(Handle, ms);
184+
}
185+
180186
/// <summary>Whether the loaded model is a qwen3-tts CustomVoice variant.</summary>
181187
public bool IsCustomVoice => NativeMethods.crispasr_session_is_custom_voice(Handle) != 0;
182188

bindings/go/crispasr_session.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ int crispasr_session_n_speakers(CrispasrSession* s);
8383
const char* crispasr_session_get_speaker_name(CrispasrSession* s, int i);
8484
int crispasr_session_set_instruct(CrispasrSession* s, const char* instruct);
8585
int crispasr_session_set_tts_phonemes(CrispasrSession* s, const char* phonemes);
86+
void crispasr_session_set_tts_pad_silence_ms(CrispasrSession* s, int ms);
8687
int crispasr_session_is_custom_voice(CrispasrSession* s);
8788
int crispasr_session_is_voice_design(CrispasrSession* s);
8889
float* crispasr_session_synthesize(CrispasrSession* s, const char* text, int* out_n_samples);
@@ -1072,6 +1073,13 @@ func (s *CrispasrSession) SetTTSPhonemes(phonemes string) error {
10721073
}
10731074
}
10741075

1076+
// SetTTSPadSilenceMs pads N ms of silence at the beginning of TTS output.
1077+
// Useful to bypass VLC playback bugs where it drops the first ~1.5s of audio
1078+
// while parsing a large C2PA chunk.
1079+
func (s *CrispasrSession) SetTTSPadSilenceMs(ms int) {
1080+
C.crispasr_session_set_tts_pad_silence_ms(s.handle, C.int(ms))
1081+
}
1082+
10751083
// IsCustomVoice reports whether the loaded model is a qwen3-tts
10761084
// CustomVoice variant (use SetSpeakerName for it).
10771085
func (s *CrispasrSession) IsCustomVoice() bool {

bindings/java/src/main/java/io/github/ggerganov/whispercpp/CrispasrSession.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ public interface Lib extends Library {
4646
int crispasr_session_set_instruct(Pointer session, String instruct);
4747
// #316: synthesize these phonemes verbatim, skipping the G2P. Empty clears. kokoro and piper only (rc=-2 otherwise).
4848
int crispasr_session_set_tts_phonemes(Pointer session, String phonemes);
49+
void crispasr_session_set_tts_pad_silence_ms(Pointer session, int ms);
4950
int crispasr_session_is_custom_voice(Pointer session);
5051
int crispasr_session_is_voice_design(Pointer session);
5152
Pointer crispasr_session_synthesize(Pointer session, String text, IntByReference outNSamples);
@@ -737,6 +738,11 @@ public void setTtsPhonemes(String phonemes) {
737738
if (rc != 0) throw new RuntimeException("set_tts_phonemes failed (rc=" + rc + ")");
738739
}
739740

741+
/** Pad N ms of silence at the beginning of TTS output. Useful to bypass VLC playback bugs where it drops the first ~1.5s of audio while parsing a large C2PA chunk. */
742+
public void setTtsPadSilenceMs(int ms) {
743+
Lib.INSTANCE.crispasr_session_set_tts_pad_silence_ms(handle, ms);
744+
}
745+
740746
/**
741747
* Whether the loaded model is a qwen3-tts CustomVoice variant
742748
* (use {@link #setSpeakerName(String)} for it).

bindings/javascript/emscripten.cpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ const char* crispasr_session_get_speaker_name(CrispasrSession* s, int i);
4545
int crispasr_session_set_instruct(CrispasrSession* s, const char* instruct);
4646
// #316: synthesize these phonemes verbatim, skipping the G2P. Empty clears. kokoro and piper only (rc=-2).
4747
int crispasr_session_set_tts_phonemes(struct crispasr_session* s, const char* phonemes);
48+
void crispasr_session_set_tts_pad_silence_ms(struct crispasr_session* s, int ms);
4849
int crispasr_session_is_custom_voice(CrispasrSession* s);
4950
int crispasr_session_is_voice_design(CrispasrSession* s);
5051
float* crispasr_session_synthesize(CrispasrSession* s, const char* text, int* out_n_samples);
@@ -642,6 +643,13 @@ EMSCRIPTEN_BINDINGS(whisper) {
642643
return crispasr_session_set_tts_phonemes(g_tts_session, phonemes.c_str());
643644
}));
644645

646+
// Pad N ms of silence at the beginning of TTS output. Useful to bypass VLC playback bugs.
647+
emscripten::function("ttsSetPadSilenceMs", emscripten::optional_override([](int ms) {
648+
if (!g_tts_session)
649+
return;
650+
crispasr_session_set_tts_pad_silence_ms(g_tts_session, ms);
651+
}));
652+
645653
// qwen3-tts variant detection (returns false also when the active
646654
// backend isn't qwen3-tts).
647655
emscripten::function("ttsIsCustomVoice", emscripten::optional_override([]() -> bool {

bindings/ruby/ext/ruby_crispasr_session.c

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ extern const char* crispasr_session_get_speaker_name(struct CrispasrSession* s,
4545
extern int crispasr_session_set_instruct(struct CrispasrSession* s, const char* instruct);
4646
/* #316: synthesize these phonemes verbatim, skipping the G2P. Empty clears. kokoro and piper only (rc=-2 otherwise). */
4747
extern int crispasr_session_set_tts_phonemes(struct CrispasrSession* s, const char* phonemes);
48+
extern void crispasr_session_set_tts_pad_silence_ms(struct CrispasrSession* s, int ms);
4849
extern int crispasr_session_is_custom_voice(struct CrispasrSession* s);
4950
extern int crispasr_session_is_voice_design(struct CrispasrSession* s);
5051
extern float* crispasr_session_synthesize(struct CrispasrSession* s, const char* text, int* out_n_samples);
@@ -771,6 +772,12 @@ static VALUE rb_session_set_tts_phonemes(VALUE self, VALUE handle, VALUE phoneme
771772
return Qnil;
772773
}
773774

775+
static VALUE rb_session_set_tts_pad_silence_ms(VALUE self, VALUE handle, VALUE ms) {
776+
struct CrispasrSession* s = (struct CrispasrSession*)NUM2ULL(handle);
777+
crispasr_session_set_tts_pad_silence_ms(s, NUM2INT(ms));
778+
return Qnil;
779+
}
780+
774781
static VALUE rb_session_is_custom_voice(VALUE self, VALUE handle) {
775782
struct CrispasrSession* s = (struct CrispasrSession*)NUM2ULL(handle);
776783
return crispasr_session_is_custom_voice(s) ? Qtrue : Qfalse;
@@ -1879,6 +1886,7 @@ void init_ruby_crispasr_session(VALUE* mWhisper) {
18791886
rb_define_singleton_method(mSession, "speakers", rb_session_speakers, 1);
18801887
rb_define_singleton_method(mSession, "set_instruct", rb_session_set_instruct, 2);
18811888
rb_define_singleton_method(mSession, "set_tts_phonemes", rb_session_set_tts_phonemes, 2);
1889+
rb_define_singleton_method(mSession, "set_tts_pad_silence_ms", rb_session_set_tts_pad_silence_ms, 2);
18821890
rb_define_singleton_method(mSession, "is_custom_voice", rb_session_is_custom_voice, 1);
18831891
rb_define_singleton_method(mSession, "is_voice_design", rb_session_is_voice_design, 1);
18841892
rb_define_singleton_method(mSession, "synthesize", rb_session_synthesize, 2);

crispasr-sys/src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -582,6 +582,10 @@ extern "C" {
582582
s: *mut CrispasrSession,
583583
phonemes: *const c_char,
584584
) -> c_int;
585+
pub fn crispasr_session_set_tts_pad_silence_ms(
586+
s: *mut CrispasrSession,
587+
ms: c_int,
588+
);
585589
// qwen3-tts variant detection (returns 0/1; 0 also covers "not qwen3-tts").
586590
pub fn crispasr_session_is_custom_voice(s: *mut CrispasrSession) -> c_int;
587591
pub fn crispasr_session_is_voice_design(s: *mut CrispasrSession) -> c_int;

crispasr/src/lib.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1683,6 +1683,12 @@ impl Session {
16831683
Ok(())
16841684
}
16851685

1686+
/// Pad N ms of silence at the beginning of TTS output. Useful to bypass VLC playback bugs
1687+
/// where it drops the first ~1.5s of audio while parsing a large C2PA chunk.
1688+
pub fn set_tts_pad_silence_ms(&self, ms: i32) {
1689+
unsafe { crispasr_sys::crispasr_session_set_tts_pad_silence_ms(self.handle, ms as std::os::raw::c_int) };
1690+
}
1691+
16861692
/// Select + load a punctuation-restoration model (`auto`/`firered`/`fullstop`/
16871693
/// `punctuate-all`/`pcs`/path; `"none"`/`""` unloads). Auto-downloads on first use.
16881694
pub fn set_punc_model(&self, punc_model: &str) -> Result<(), String> {

docs/tts.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1843,6 +1843,12 @@ Build the library for the target with `-DCRISPASR_C2PA_FETCH=ON`:
18431843
(all modern browsers, 2023+). Only needed for `c2paSign(bytes, "audio/mpeg")`
18441844
and other non-WAV containers in the browser.
18451845

1846+
**VLC Playback Bug (Silence Padding).** Because the C2PA JUMBF metadata chunk is extremely large (often 2-3 KB), some audio players (notably **VLC**) stall their playback thread for ~1.5 seconds while parsing it from the end of the file before playing the stream. For short TTS clips, this causes the first 1-2 seconds of the generated speech to be silently dropped during playback.
1847+
To work around this while keeping the C2PA metadata intact, you can explicitly pad silence frames at the beginning of the audio. By the time the player unblocks, the silence has played out and the speech begins unharmed.
1848+
- **CLI:** `--tts-pad-silence-ms 1500`
1849+
- **C ABI:** `crispasr_session_set_tts_pad_silence_ms(session, 1500)`
1850+
- **HTTP Server:** Add `"pad_silence_ms": 1500` to the JSON request body.
1851+
18461852
### Voice cloning consent gate
18471853

18481854
Voice cloning (`.wav` reference files) requires explicit consent:

examples/cli/cli.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -730,6 +730,8 @@ static bool whisper_params_parse_arg_streaming_tts(int argc, char** argv, int& i
730730
params.g2p_dict = ARGV_NEXT;
731731
} else if (arg == "--tts-trim-silence") {
732732
params.tts_trim_silence = true;
733+
} else if (arg == "--tts-pad-silence-ms") {
734+
params.tts_pad_silence_ms = std::stoi(ARGV_NEXT);
733735
} else if (arg == "--tts-play") {
734736
params.tts_play = true;
735737
} else if (arg == "--tts-play-device") {
@@ -1468,6 +1470,9 @@ static void whisper_print_usage(int /*argc*/, char** argv, const whisper_params&
14681470
params.tts_speed);
14691471
fprintf(stderr, " --tts-trim-silence [%-7s] trim leading silence from TTS output\n",
14701472
params.tts_trim_silence ? "true" : "false");
1473+
fprintf(stderr,
1474+
" --tts-pad-silence-ms N [%-7d] prepend N ms of silence (useful for VLC C2PA buffer drop)\n",
1475+
params.tts_pad_silence_ms);
14711476
fprintf(stderr, " --tts-play [%-7s] play synthesised audio on the local speaker\n",
14721477
params.tts_play ? "true" : "false");
14731478
fprintf(stderr, " --tts-play-device N [%-7d] speaker device index (-1 = default)\n",

0 commit comments

Comments
 (0)