Skip to content

Fix audio crackle at the start of every synthesized utterance#137

Open
7MS8 wants to merge 1 commit into
woheller69:masterfrom
7MS8:fix/startup-crackle
Open

Fix audio crackle at the start of every synthesized utterance#137
7MS8 wants to merge 1 commit into
woheller69:masterfrom
7MS8:fix/startup-crackle

Conversation

@7MS8

@7MS8 7MS8 commented Jul 14, 2026

Copy link
Copy Markdown

Summary

  • TtsService.onSynthesizeText called callback.start() before any audio had been generated, so the system AudioTrack began draining an empty buffer while the model computed its first inference chunk (slower than steady-state, since onnxruntime warms up its execution graph on first use). This race causes an audible crackle/underrun at the start of every utterance played through the system TTS API.
  • Confirmed via AudioTrack: restartIfDisabled(...): disabled due to previous underrun, restarting in logcat, reliably reproducing on every utterance.
  • Likely the same root cause as Needs (possibly adjustable) delay before sound production #90 ("Needs (possibly adjustable) delay before sound production"), which reports it as a clipped first word instead of an audible crackle — same race, different symptom depending on how the underrun manifests.
  • Fix: buffer ~300ms of audio before calling callback.start(), giving the AudioTrack a pre-roll cushion so inference no longer races playback. Short utterances that finish generating before the threshold is reached flush whatever was buffered once generation completes, so added latency stays proportional to utterance length rather than a flat per-utterance cost.

Test plan

  • Built and installed on a Pixel 7a (Android 17) via a side-by-side debug build (applicationIdSuffix) to avoid disturbing the existing production install.
  • Confirmed via adb logcat that before the fix, every utterance played through Settings → Text-to-speech output → "Play" (which exercises the real onSynthesizeText path) produced an AudioTrack underrun warning; after the fix it does not, across repeated tests in different languages (including a cold model load for a not-yet-cached language).
  • ./gradlew :app:compileDebugKotlin succeeds with no new warnings.

🤖 Generated with Claude Code

TtsService called callback.start() before any audio had been
generated, so the system AudioTrack began draining an empty buffer
while the model computed its first inference chunk (slower than
steady-state, since onnxruntime warms up its execution graph on first
use). The race caused an audible crackle/underrun at the start of
every utterance played through the system TTS API (confirmed via
AudioTrack "disabled due to previous underrun, restarting" in
logcat). Likely the same root cause as woheller69#90, which reports it as a
clipped first word instead of a crackle.

Buffer ~300ms of audio before calling callback.start(), giving the
AudioTrack a pre-roll cushion so inference no longer races playback.
Short utterances that finish generating before the threshold is
reached flush whatever was buffered once generation completes, so
added latency stays proportional to utterance length rather than a
flat per-utterance cost.

Tested on a Pixel 7a: before the fix, every utterance played through
the system TTS API produced an audible crackle and an AudioTrack
underrun warning in logcat; after the fix neither reproduces across
repeated test utterances in different languages.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@woheller69

Copy link
Copy Markdown
Owner

if there is an issue, which I never noticed, it should be fixed in the upsteam project

https://github.com/k2-fsa/sherpa-onnx

@7MS8

7MS8 commented Jul 15, 2026

Copy link
Copy Markdown
Author

Thanks for taking a look. To be precise about where the race actually lives: sherpa-onnx's generateWithConfigAndCallback only invokes a per-chunk audio callback (ttsCallback in TtsService.kt) as it generates samples — it has no knowledge of Android's TextToSpeechService/SynthesisCallback API at all. The AudioTrack that plays the audio is started by this app's TtsService.onSynthesizeText, via callback.start(), which is entirely application code, not sherpa-onnx.

The race: before this fix, callback.start() was called before any audio had been generated, so Android's AudioTrack began draining an empty buffer while the model computed its first chunk (slower than steady-state, since onnxruntime warms up its execution graph on first use). Confirmed via AudioTrack: restartIfDisabled(...): disabled due to previous underrun, restarting in logcat on every utterance, reliably, on my Pixel 7a (GrapheneOS).

Even a faster sherpa-onnx wouldn't remove this race, only shrink the window — any time synthesis is momentarily slower than real-time playback (cold start, background CPU contention, a slower device), the same underrun would recur unless the app buffers ahead before starting playback. That's why the fix is a small pre-roll buffer (~300ms) in TtsService.kt before calling callback.start() — standard practice for this class of producer/consumer race, independent of how fast the model itself runs.

Issue #90 describes the same symptom (clipped first word instead of an audible crackle) — I believe it's the same underlying race, just manifesting slightly differently depending on exactly how the underrun lands.

Happy to answer questions if useful — otherwise I'll keep the fix in my fork since it resolves the issue for my install either way.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants