Text-to-Speech Is Not a speak() Call
The challenge ๐งช
If you have ever assumed Text-to-Speech on Android is straightforward, this article is for you.
But first, let us test your skills.
Think you can make this speak on Android?
๐ เค เคตเฅเคฏเคเฅเคคเฅเคฝเคฏเคฎเคเคฟเคจเฅเคคเฅเคฏเฅเคฝเคฏเคฎเคตเคฟเคเคพเคฐเฅเคฏเฅเคฝเคฏเคฎเฅเคเฅเคฏเคคเฅ เฅค
"Invisible, beyond thought, unchanging." โ Krishna describing the nature of the self.
Today is World Sanskrit Day, so the timing is fitting. ๐๏ธ
Try playing it on the plain TextToSpeech API that Google provides โ but specifically with a Sanskrit voice.
Build a minimal Android app, initialize the TTS engine, set the language to Sanskrit, and call speak() on this string.
Chances are it will not speak anything. Not even a single letter would be uttered. ๐
That is the moment when a developer realizes that TTS is not a simple API call.
The twist ๐
Use a Marathi or Hindi voice instead. Same engine. Same text. Same API call.
It plays perfectly. ๐ฃ๏ธ
Same engine. Same verse. Different voice. Completely different result.
The boundary between "speakable" and "not speakable" is not at the engine level. It is at the voice level within the engine.
The Sanskrit voice within Google's TTS engine cannot handle this verse. But the Marathi voice โ which shares much of the same Devanagari character set โ handles it without issue.
This changes how you think about TTS integration.
What happened in production ๐ญ
This is not a theoretical exercise. This is what we actually hit.
In Bhagavad Gita, the player screen uses TTS to read verses aloud. The experience is designed to feel like playing a media file: continuous, flowing, uninterrupted.
But certain words โ especially compound words and special conjunct characters โ were being silently skipped. Not errored. Not logged. Just... silent.
The engine would skip the entire word if it couldn't speak something in it.
So a verse that should take 15 seconds to read would finish in 8. The user would hear a flowing recitation with missing pieces and never know what was lost. ๐ถ
The worst kind of bug. The system appears to work. The output is incomplete. Users never know.
This is NOT just a Sanskrit problem ๐
Sanskrit is the most visible example, but the problem shows up everywhere.
Take two Unicode characters that look nearly identical:
- เฅ โ vocalic r, short โ (speakable by Google TTS)
- เฅ โ vocalic r, long โ (silence)
They differ by a single Unicode code point, but one produces audio and the other produces silence. And the engine does not tell you โ it just stays quiet.
Different TTS engines handle complex characters differently. But even within a single engine, different voices handle the same characters differently. A voice that speaks Tamil conjuncts correctly may skip Burmese tone marks. A voice that handles Thai stacking may fail on Tibetan.
And it gets worse. ๐ฑ
Voice compatibility varies across Android versions, TTS engine versions, and device manufacturers. A voice that works perfectly on Android 13 may fail silently on Android 14. An engine update can break character handling that previously worked. A Samsung device may ship a different engine version than a Pixel on the same Android version.
None of this is documented in a way that developers can rely on. There is no voice capability matrix. There is no API to ask an engine "can you speak this character correctly on this device?"
You discover the problem when users report missing audio on a device you never tested. ๐
Even within widely-supported language families, edge cases exist. A Marathi verse containing Sanskrit compound words can fail silently on one voice but work on a different voice. A Hindi sentence with certain Devanagari conjuncts can produce gaps on one voice and read correctly on another.
The line between "supported" and "unsupported" is not clean. It is a gradient. And the standard API gives you no tools to navigate it.
The assumption that breaks everything โ ๏ธ
The standard Android TextToSpeech API makes a quiet assumption: the engine you select will handle the language you request.
For English, French, German, or Mandarin, that assumption holds well enough. The engines have been tuned for these languages, the character sets are well-supported, and the output is predictable.
For languages with complex scripts โ Sanskrit, Tamil, Tibetan, Thai, Khmer, or many others โ that assumption collapses. And as the Sanskrit test showed, even when the engine nominally supports the language, the specific voice may not handle all characters.
Adding the version dimension makes it worse. Even the voice that worked last month may not work after an engine update. The API gives you no tools to discover this at runtime. There is no method to ask an engine whether it can speak a specific character correctly. There is no voice capability profile. The engine accepts the input, and you discover the problem only when the output is silent or incomplete.
If you have only integrated TTS for well-supported languages on a single test device, you may never encounter this. The moment you need a complex script across a range of devices and OS versions, the entire integration model breaks.
The problem gets worse with continuous playback ๐ต
Now add another requirement: the app does not just need to speak one sentence. It needs to play a continuous sequence of content โ verse after verse, section after section โ with smooth transitions, role-appropriate voices, and uninterrupted flow.
In Bhagavad Gita, the player screen plays TTS in a continuous loop. It behaves like a media file: the user starts playback, and the audio flows without manual intervention. Transitions between verses are smooth. Voices can shift by role. The experience is designed to feel like listening to a recording.
Except there are no pre-recorded files for most of the content. The audio is generated at runtime from text, voices, and a sequence-driven orchestration layer.
Combine that with the voice-level and version-level variability problems and you have a genuinely hard engineering challenge:
- โ The content is continuous and sequenced
- โ ๏ธ Some voices can't speak certain characters in the content
- ๐ The engine does not reject bad input โ it just stays silent
- ๐ฑ Different devices have different engines with different voices
- ๐ Even within the same engine, different voices produce different results
- ๐ข Voice behavior can change across Android versions and engine updates
- ๐ The same content needs to work across many languages
- ๐ง The experience must feel like media playback, not a series of disjointed TTS calls
Standard TextToSpeech does not solve any of those problems. It gives you a sentence-level API and assumes the engine handles the rest.
The multi-engine, multi-voice approach ๐
SskCore's TTS architecture addresses this by treating engine AND voice selection as first-class problems.
Instead of relying on a single TTS engine with a default voice, the platform manages multiple engines and multiple voices, and selects the appropriate combination based on the language, the script, and the actual speaking capability of each voice on the specific device.
This is not a simple wrapper around TextToSpeech. It is a multi-engine, multi-voice orchestration layer that:
Evaluates voice capabilities per language and script. ๐ฏ Not all voices support all scripts. Not all voices within the same engine handle the same characters. The platform needs to know, or discover, which voice-engine combinations actually speak the text correctly.
Selects the voice that works, not just the voice that claims to. ๐ฃ๏ธ The platform does not blindly pick the "Sanskrit voice" for Sanskrit text. It evaluates which voice actually handles the characters in the content โ which may be a Marathi voice, a Hindi voice, or another voice that shares the relevant character set.
Falls back gracefully across voices and engines. ๐ก๏ธ If the selected voice produces silence or garbled output for a specific character, the platform can route that segment to a different voice or engine that handles it correctly. The fallback is per-segment, not per-app.
Adapts across versions. ๐ฑ Because voice behavior can shift with Android or engine updates, the platform does not hardcode a single voice as "the solution." It discovers and re-evaluates capabilities, so a voice that stops working after an update can be replaced without an app update.
Maintains sequencing across voice switches. ๐ Switching voices mid-sequence is risky โ the voice quality, pacing, and tone may change. The platform must manage these transitions so the user experience remains coherent.
Handles the silence problem. ๐ The hardest bug in complex-script TTS is silent failure. The engine does not error. It does not throw an exception. It just does not speak. Detecting and recovering from this requires the platform to understand what "no output" means and route the content accordingly.
Once this infrastructure is built, it works for any language. You do not need a special solution for Sanskrit, a different solution for Thai, and another for Tibetan. The same orchestration layer handles them all. The language and content determine which voices to try. The platform handles the rest.
Sequence-driven, role-oriented playback ๐ญ
The multi-voice approach solves the character and version problems. But continuous playback requires more.
When content flows as a sequence โ verse after verse in a long reading session โ the platform needs to own:
- Segment boundaries. ๐ Where does one verse end and the next begin?
- Transition timing. โฑ๏ธ How much pause between segments? How does the audio feel continuous rather than choppy?
- Role-based voice assignment. ๐ญ Not every segment should use the same voice. Narration, dialogue, attributed text โ these may need different voice profiles.
- Interruption and resumption. โธ๏ธ If the user pauses, leaves the app, or receives a notification, the platform must resume from the correct position without restarting the sequence.
- Looping. ๐ In Bhagavad Gita's player, the sequence can loop continuously. The platform must handle the transition from the last segment back to the first smoothly.
These are not sentence-level problems. They are systems-level problems. And they are the reason TTS is not a speak() call โ it is a product capability that requires orchestration infrastructure.
What the UI actually needs: highlight and progress โจ
Media-like playback is not enough. A reading app needs the UI to track the audio.
SskCore's TTS layer exposes two first-class signals that most wrappers miss:
Highlight โ where are we speaking right now? A real-time character range
[start, end)in the original text, updated as each word is spoken. The range stays accurate even when the text is normalized before speaking (for example, a display form replaced with a speakable form) and even when long text is chunked for the engine. Per-role and global streams are available, so a Sanskrit step and a translation step each highlight independently.Progress โ how far are we through? A
0.0 โ 1.0fraction derived from the last spoken character, again per-role and global. This drives seek bars, verse progress, and precise pause/resume.
Two details make this hard and why it belongs in the platform:
Transformation-aware mapping. If the spoken text differs from the displayed text, the highlight must map back to the original character positions. Otherwise the UI highlights the wrong word.
Network-voice timing. Network voices (like Google WaveNet) have a 200โ600ms gap between
onRangeStartand audible audio. Without compensation the highlight runs ahead of the sound. The platform applies a tunable delay so highlight and audio stay in sync โ zero for local voices, compensated for network voices.
On top of that, the platform owns the surrounding lifecycle that apps otherwise reimplement badly: chunking long text at sentence boundaries, computing a precise resume offset so pause/resume never repeats or skips a word, handling audio focus and headphone-unplug (becoming noisy) by pausing gracefully, and detecting stalls where an engine accepts an utterance but never produces audio (watchdog โ clean failure instead of infinite silence). Background cues like a looping conch can be attached to the sequence with declarative lifecycles โ the platform guarantees they never outlive the sequence.
Once, trial and error. Afterwards, every screen reuses it.
Why this is a platform problem ๐๏ธ
It is tempting to solve the TTS problem at the application level. The app needs audio, so the app manages engines, voices, sequences, and fallbacks.
But when TTS is needed across multiple content domains โ not just one screen in one app โ it becomes infrastructure. And infrastructure has different design requirements:
- Scaling across languages is automatic. ๐ Once the multi-voice approach and sequence-driven playback are configured, they work across every supported language. Adding a new language does not require rewriting the playback logic. You configure the content and the language. The platform handles voice selection, fallback, and sequencing.
- Content grows without asset multiplication. ๐ Adding a new verse does not require recording new audio. The text is the source of truth. Every new piece of content is playable in every supported language without additional audio assets.
- Engine and voice improvements flow to all consumers. โฌ๏ธ When the platform improves voice selection or fallback logic, every application that uses the platform benefits automatically.
- Version changes are absorbed once. ๐ข When a new Android version or engine update shifts voice behavior, the platform adapts โ and every consumer app inherits the fix without code changes.
- UI stays honest. โจ Because highlight and progress are part of the platform contract, every consumer gets word-accurate highlighting and reliable progress without rebuilding the mapping, chunking, and timing logic.
The scaling argument is worth emphasizing: a TTS integration scales with the number of languages times the number of content pieces times the number of device versions. A TTS capability scales with the content only. The language and version dimensions become configurations, not multiplications.
The recording trap ๐ชค
The assumption that "serious audio requires pre-recorded files" is the most common limitation in mobile audio design.
Recorded audio is consistent. It sounds the same on every device. For premium audiobooks or studio-produced content, recording is the right choice.
But for applications where content volume is high, languages are numerous, and the content itself is text โ scripture, literature, educational material โ recorded audio creates a maintenance problem that grows faster than the content.
If your app supports multiple languages and each language requires a full set of recorded audio files, you have multiplied your content pipeline by the number of languages. Every correction or addition requires re-recording in every language.
A sequence-driven, multi-voice TTS approach sidesteps that entirely. The text is the source. The device provides the voice. The platform handles orchestration. The result scales with content volume, not with the number of audio engineers available.
For Sanskrit specifically, this is especially powerful. Recording Sanskrit audio correctly requires a native speaker with precise pronunciation knowledge. A TTS-based approach can leverage engine and voice improvements over time without re-recording everything.
The trade-offs โ๏ธ
This approach is not without costs.
Voice quality varies by device, engine, voice selection, and version. ๐ฑ Pre-recorded audio sounds the same everywhere. Device TTS voices vary in naturalness, pronunciation, and prosody. Multi-voice selection and version adaptation add more variables.
Voice capability is opaque and shifting. ๐ There is no standard API to ask a voice "can you speak this character correctly?" and no guarantee the answer will be the same after the next OS update. The platform may need to discover capabilities empirically and re-evaluate them over time.
Sequencing complexity is real. ๐งฉ Managing a continuous queue of segments with multi-voice fallback, role-based voice assignment, version adaptation, and smooth transitions is genuinely complex infrastructure.
Content must be structured. ๐ Sequence-driven TTS works best when content has clear segment boundaries and role metadata.
These are the trade-offs the design accepts. The alternative โ pre-recorded audio โ trades scalability and language coverage for voice consistency. SskCore's approach trades some voice consistency for massive scalability, automatic language coverage, version resilience, and the ability to handle complex scripts that standard voices refuse to speak.
A safe thought experiment ๐งช
If you maintain an Android application that uses TTS, try this:
- Pick a language with a complex script โ Sanskrit, Tamil, Tibetan, Thai, or any language with conjunct characters or stacked vowel marks.
-
Initialize the standard
TextToSpeechAPI and attempt to speak a sentence containing edge-case Unicode characters. - Try it with the "native" voice first. Then try it with a related language's voice (Hindi for Sanskrit, Malayalam for Tamil, etc.).
- Observe the difference. You may find that the "wrong" voice speaks the text better than the "right" one.
- Now test on a different Android version or device manufacturer. You may find that a voice that worked on one device fails silently on another.
- Ask yourself: if this language were required for your app, how would you decide which voice to use? And what happens when the voice you chose silently skips a word โ or stops working after an OS update?
The question is not "does my app need Sanskrit?" The question is "what happens when a user speaks a language your chosen TTS voice handles poorly โ on a device you never tested, running an Android version you did not anticipate?" For most apps, the answer is silence. For a platform that treats TTS as infrastructure, the answer is a fallback that keeps the audio flowing.
TTS is infrastructure ๐๏ธ
Most mobile applications treat text-to-speech as a feature they integrate. A few lines of code, a voice selection, a speak() call.
SskCore treats TTS as infrastructure: a sequence-driven, multi-engine, multi-voice, role-oriented, version-adaptive capability โ with word-accurate highlight and reliable progress as part of the contract โ that can serve any content domain, including languages where the standard API's own voices fail silently, and where voice behavior shifts across devices and OS versions.
The platform owns the hard problems โ engine selection, voice selection, character handling, version adaptation, sequencing, transitions, continuity, highlight mapping, progress, focus handling, and stall detection โ so consumer applications can offer rich audio experiences without rebuilding the orchestration layer.
That is why the architecture is reusable. It does not solve "speak this sentence." It solves "play this entire sequence correctly, in any language, with the right voice, on any device, without silently skipping words โ and keep the UI perfectly in sync."
When the engineering problem shifts from a single API call to a systems-level capability, the design has to follow. ๐ฏ
Try the production result: Bhagavad Gita: The Song of God on Google Play โ 4.6โ , 10K+ downloads, 24 languages, built on this TTS platform.
SskCore is an internal Android engineering platform built from recurring production lessons across multiple applications. This article discusses the general engineering principle of multi-voice TTS orchestration and sequence-driven playback as reusable platform capabilities; it does not disclose internal class names, module structures, engine-selection algorithms, voice-selection logic, character-mapping configuration, or repository locations.
Top comments (0)