Sinua
Connect your appVoice

Voice button

A microphone button bound to a voice source: connect, mute, push-to-talk and end the session, with the view following along.

The voice button is the control that goes with a visual: it connects the voice source, mutes and unmutes it, and ends the session, and it shows where the session stands. The agent's own states (thinking, speaking) stay on the visual.

idle
A simulated conversation: no microphone, no audio, no network.

This demo runs on a simulated conversation: press the button to start it. No microphone is used.

voice-button-web.ts
import "@sinua/web/element"; // registers <sinua-view>
import type { SinuaViewElement } from "@sinua/web/element";
import { defineSinuaVoiceButtonElement, type SinuaVoiceButtonElement } from "@sinua/web/voice-button";
import { LocalMicVoiceSource } from "@sinua/voice/mic";

// <sinua-view pattern="glowing"></sinua-view>
// <sinua-voice-button mode="push-to-talk"></sinua-voice-button>   (or mode="toggle", the default)
defineSinuaVoiceButtonElement();

const source = new LocalMicVoiceSource(); // any VoiceSource: a vendor source, the mic, a simulation
document.querySelector<SinuaViewElement>("sinua-view")!.voice = source;
const button = document.querySelector<SinuaVoiceButtonElement>("sinua-voice-button")!;
button.source = source; // the button connects, mutes and ends the session; the view shows it

button.addEventListener("voicebuttonchange", (e) => {
  console.log("voice button:", e.detail.state); // ready, connecting, listening, muted, error
});
voice-button-react.tsx
import { useState } from "react";
import { SinuaView, SinuaVoiceButton } from "@sinua/web/react";
import { LocalMicVoiceSource } from "@sinua/voice/mic";

export function Assistant() {
  const [source] = useState(() => new LocalMicVoiceSource());
  return (
    <>
      <SinuaView pattern="glowing" voice={source} style={{ width: 160, height: 160 }} />
      {/* Press to connect; then press to mute and unmute. Long-press ends the session. */}
      <SinuaVoiceButton source={source} mode="toggle" />
    </>
  );
}

States

StateWhenLooks like
readynot connecteda microphone
connectinga connection is startinga spinning ring
listeningconnected, not muteda ring on the edge that swells with the level
mutedconnected, mutedthe microphone struck through, grey
errorthe connection faileda warning; the accessible name says why

Modes

  • toggle (the default): a press connects, then mutes and unmutes.
  • Push-to-talk (mode="push-to-talk" on the web element, "pushToTalk" in code): pressing connects the first time and unmutes; releasing mutes. The session stays up between presses, so there's no reconnect delay.
  • In both modes, a long press (0.6 s), the small ✕, or Escape on the web ends the session.
  • A source that can't mute ends the session on a press while listening, and push-to-talk behaves like toggle.

Mute

Muting sends silence and keeps the session up: no reconnect and no lost context. Every built-in source supports it, and a view bound to the source shows a muted cue. onConnectionChange says when a session starts and ends, which the agent's state can't: an agent can be idle while connected.

Sharing a source

A voice source holds one callback of each kind, so a second listener would silently replace the first. Views and the button share a source through SharedVoiceSource automatically. To listen yourself, subscribe through it too, never on the raw source:

sharing-web.ts
import { SharedVoiceSource, type VoiceSource } from "@sinua/core";

// A source holds one callback of each kind. To listen yourself next to a view or a
// button, subscribe through the shared fan-out, never on the raw source.
export function watch(source: VoiceSource, pill: HTMLElement) {
  const shared = SharedVoiceSource.of(source); // the same instance every time for this source
  const offState = shared.onStateChange((state) => (pill.textContent = state));
  const offLink = shared.onConnectionChange((connected) => pill.toggleAttribute("data-live", connected));
  return () => {
    offState();
    offLink();
  };
}

// Mute without ending the session: silence goes out, the agent keeps its context,
// and every view bound to the source shows the muted cue.
export const mute = (source: VoiceSource, muted: boolean) => SharedVoiceSource.of(source).setMuted(muted);

On iOS and Android the same fan-out has listenState, listenConnection, listenMute and friends, each returning a cancel.

Accessibility

On the web it's a real button: its name follows the state ("Start voice", "Microphone on", "Microphone muted"…), its hint says what a press does, and muted is aria-pressed. VoiceOver and TalkBack get a button role and an "End voice session" action. A screen reader can't hold a press, so in push-to-talk its activation toggles. Every word can be replaced with labels.

On this page