Listening beats reading when you're away from the screen. OmniBrief lets you play any brief as audio - and because the browser already ships a speech engine, that costs nothing: no audio files, no cloud TTS API, no per-character billing.

The engine is the Web Speech API's SpeechSynthesis interface. It's delightfully simple to start with and has two sharp edges worth knowing: the voice list loads asynchronously, and the pause/resume state is unreliable to read back. Here's how OmniBrief handles both.

Key Technical Concepts Covered

  • SpeechSynthesis basics: speak an utterance with a chosen voice and rate.
  • The async voice list: getVoices() is often empty on first call.
  • Voice ranking: prefer higher-quality voices per language.
  • Tracking state yourself: don't trust speechSynthesis.paused.

Part of the engineering behind OmniBrief.

Tested with Chrome 149 (July 2026). Available speech voices vary by operating system and browser; not all are guaranteed to run offline.

The Async Voice List

The first surprise: speechSynthesis.getVoices() frequently returns an empty array on the first call because voices load asynchronously. You must also listen for the voiceschanged event and re-read the list. OmniBrief sorts the result so higher-quality voices surface first:

export function getVoices() {
  if (typeof speechSynthesis === 'undefined') return [];
  return [...speechSynthesis.getVoices()].sort((a, b) => {
    // Prefer Google / natural voices, then alphabetical
    const score = (v) => (/google|natural|premium/i.test(v.name) ? 0 : 1);
    return score(a) - score(b) || a.name.localeCompare(b.name);
  });
}

export function onVoicesChanged(fn) {
  if (typeof speechSynthesis === 'undefined') return () => {};
  speechSynthesis.addEventListener('voiceschanged', fn);
  return () => speechSynthesis.removeEventListener('voiceschanged', fn);
}

Speaking an Utterance

With a voice chosen, playback is a SpeechSynthesisUtterance handed to speechSynthesis.speak(). Rate, pitch, and voice are all set on the utterance:

function speak(text, voice, rate = 1) {
  speechSynthesis.cancel();               // clear any queued speech
  const u = new SpeechSynthesisUtterance(text);
  if (voice) u.voice = voice;
  u.rate = rate;
  u.onend = () => setPlaying(false);
  speechSynthesis.speak(u);
}

Track Pause State Yourself

The second sharp edge: speechSynthesis.paused and .speaking are inconsistent across engines, so driving a play/pause button off them produces flicker and wrong states. OmniBrief keeps its own boolean and toggles it alongside the API calls:

let isPaused = false;

function togglePause() {
  if (isPaused) {
    speechSynthesis.resume();
    isPaused = false;
  } else {
    speechSynthesis.pause();
    isPaused = true;
  }
  return isPaused;   // source of truth for the UI
}

Conclusion: Free, Local, and Instant

The Web Speech API turns any brief into audio with a few lines and no infrastructure. Handle the asynchronous voice list, rank voices for quality, and own your pause state rather than reading it back - and you get a reliable read-aloud feature that costs nothing and works offline.