How Voice Actions Trigger Radio Playback and Deferred Confirmation in God's Eye View

Voice actions trigger radio playback through the control_radio tool in src/voice/gevRealtime.js, which buffers results in pendingRadioPlaybackResult and defers the "Turning on the radio" confirmation until startPreparedRadioAfterPlaybackReady verifies the audio stream is actually playing.

The gods-eye-view repository implements a sophisticated voice-controlled Internet Radio subsystem where spoken commands can start, stop, or change stations without conflicting with ongoing voice conversations. When a user says "play news near Austin," the system does not confirm immediately; instead, it waits until the broadcaster successfully connects before emitting a deferred confirmation. This article examines the complete mechanism by which voice actions trigger radio playback and deferred confirmation, based on the implementation in src/voice/gevRealtime.js and its supporting modules.

Voice-to-Radio Handoff Architecture

The voice subsystem centers on the GevRealtimeController class, which orchestrates a tight hand-off between the OpenAI Realtime microphone session and the built-in Internet Radio layer. When a spoken command reaches the assistant, the controller decides whether to pause the radio while the voice conversation is active, whether a control_radio tool call should start playback, and how to provide a deferred confirmation once the broadcaster is actually playing.

The architecture relies on three core policy helpers defined in src/voice/gevRealtime.js:

  • shouldPauseRadioForVoice (lines 15‑26) — Determines if the radio should be ducked or paused during voice states like "connecting", "executing", or speaking.
  • shouldStopVoiceAfterRadioTool (lines 28‑34) — Recognizes successful radio-control actions (play, resume, select, next, previous) that merit handing the speaker back to the broadcaster.
  • silenceRadioForVoice (lines 85‑88) — Ducks or pauses the radio during speech synthesis to prevent audio overlap.

Step-by-Step Execution Flow

The end-to-end flow from voice input to radio playback involves eight distinct steps coordinated by the realtime controller.

1. Detect Voice Input and Initialize Session

Voice input originates from a UI button or hold-Space shortcut that starts a GevRealtimeController. During the session, any spoken utterance is sent as a conversation.item.create event of role user. The initialization logic resides in initGevVoiceCommands → controller.buttonHandler (lines 12‑16).

// gevRealtime.js#L12-L16
controller.buttonHandler = () => {
  // Initializes WebRTC connection and microphone session
  this.connect();
};

2. Pause Radio While Voice Owns the Speaker

Before establishing the WebRTC connection, the controller calls pauseRadioForVoice(). The helper shouldPauseRadioForVoice returns true for any "connecting", "executing", or user/AI speaking state, causing the radio to be ducked or fully paused to prevent interference.

// gevRealtime.js#L15-L26
function shouldPauseRadioForVoice({ status, speaker }) {
  return ['connecting', 'executing'].includes(status) || 
         ['user', 'ai'].includes(speaker);
}

3. Dispatch the Voice-Derived Tool Call

When the Realtime server returns a function call (e.g., control_radio), handleRealtimeEvent extracts it using extractFunctionCalls and routes it to the action runner (this.runner). The runner executes the Radio API implementation located in src/voice/gevActions.js (lines 95‑97).

// gevRealtime.js#L95-L97
const calls = extractFunctionCalls(payload);
calls.forEach(call => this.runner.execute(call));

4. Determine Radio Hand-Off Policy

The helper shouldStopVoiceAfterRadioTool recognizes successful radio-control actions that merit handing the speaker back to the broadcaster. It returns true for actions like play, resume, select, next, and previous (lines 28‑34).

// gevRealtime.js#L28-L34
function shouldStopVoiceAfterRadioTool(action) {
  return ['play', 'resume', 'select', 'next', 'previous'].includes(action);
}

5. Reserve Radio Hand-Off for Disabling Actions

For actions that disable, pause, or stop playback, the controller reserves the radio hand-off so that any in-flight playback is cancelled before the new command proceeds. The reservation token is stored in radioReservationToken (lines 140‑150).

6. Prepare the Broadcaster After Tool Completion

When the tool result indicates pending playback (result.radioPlaybackRequested), the controller stores the result in pendingRadioPlaybackResult. Once the surrounding response finishes (response.done), startPreparedRadioAfterPlaybackReady is invoked to actually start the stream and then close the voice channel (lines 37‑70).

// gevRealtime.js#L37-L70
async function startPreparedRadioAfterPlaybackReady(result, callbacks) {
  // Verify playback started, then close voice channel
  await callbacks.prepareRadio();
  if (result.audioState === 'playing') {
    callbacks.stopVoice();
  }
}

7. Close Voice and Emit Deferred Confirmation

After the radio has successfully started, the controller calls the optional stopVoice callback (which removes the microphone UI) and returns { handled: true, result: { audioState: 'playing' } }. The UI layer then emits a short spoken confirmation ("Turning on the radio") after the playback is verified, guaranteeing the user never hears "already playing" when the stream failed to load (lines 61‑69).

8. Silence Radio During Voice Synthesis

If the voice session needs to duck the radio during speech synthesis, silenceRadioForVoice calls the radio layer's duckRadio and optionally pauseRadio (lines 85‑88).

The Deferred Confirmation Mechanism

The deferred confirmation relies on two critical safety mechanisms to prevent race conditions and false confirmations.

Epoch-Based Hand-Off Tracking: The controller tracks a radioHandoffEpoch. Each tool call captures the epoch at start; only the most recent epoch may actually trigger playback. This prevents a newer "stop" command from being overwritten by an older "play" that arrives later (lines 158‑166).

Result Buffering: pendingRadioPlaybackResult stores the tool's result until the surrounding response is fully completed. The response.done listener (lines 75‑93) then calls startPreparedRadioAfterPlaybackReady. This buffering ensures the spoken "Turning on the radio" is only emitted after the broadcaster is verified to be playing, not when the command is merely received.

Practical Implementation Examples

Example 1: Triggering Playback via Voice Command

When a user says "play news near Austin," the system resolves the command to a specific station and defers confirmation until the stream connects.

// Simulate a spoken command
controller.sendTextCommand('play news near Austin');

The text becomes a conversation.item.create → Realtime → control_radio tool call with parameters { action: 'play', category: 'news', locationId: 'austin' }. The tool implementation in src/voice/gevActions.js resolves the nearest station, sets result.radioPlaybackRequested = true, and returns { ok: true, action: 'control_radio', radioAction: 'play' }. The handleRealtimeEvent buffers this result, then after the response finishes calls startPreparedRadioAfterPlaybackReady, which starts the stream and says "Turning on the radio".

Example 2: Stopping Radio via Voice

controller.sendTextCommand('stop the radio');

This generates control_radio with action: 'stop'. Inside the tool-dispatch loop, the controller detects radioOwnershipClaimed (action === 'stop') and immediately calls radioLayer.stopPlayback (via the reservation token) to cancel any pending play. Because shouldStopVoiceAfterRadioTool returns false for a stop action, the voice session remains active.

Example 3: Manual Integration with Helper APIs

For advanced custom integrations, you can use the internal helpers directly:

import { shouldPauseRadioForVoice, startPreparedRadioAfterPlaybackReady } from './voice/gevRealtime.js';

// Pause radio while voice is listening
if (shouldPauseRadioForVoice({ status: controller.status, speaker: 'user' })) {
  controller.radioLayer?.pause?.({ origin: 'voice-duck' });
}

// Hand off to radio after tool indicates playback
await startPreparedRadioAfterPlaybackReady(pendingResult, {
  prepareRadio: () => controller.radioLayer?.play?.({ origin: 'voice-start' }),
  stopVoice:   () => controller.stop({ preserveRadioPlayback: true }),
  cancelRadio: () => controller.radioLayer?.stopPlayback?.({ origin: 'voice-cancel' }),
});

Critical Source Files

File Purpose
src/voice/gevRealtime.js Central voice controller implementing pause logic, radio hand-off, deferred confirmation, and the main Realtime event loop (lines 12‑166).
src/voice/gevActions.js Concrete tool functions including control_radio that interact with the Radio layer and resolve station data.
src/voice/voiceCost.js Tier-based cost model determining when voice sessions terminate, affecting radio re-enable timing.
src/data/radioBrowser.js Fetches the Radio Browser directory used by control_radio to resolve station URLs.
src/data/radioCountry.js Normalizes station data for the radio layer.
src/ui.js Contains deferred navigation logic patterns similar to the radio hand-off mechanism.

Summary

  • Voice actions trigger radio playback through the control_radio tool dispatched via handleRealtimeEvent in src/voice/gevRealtime.js.
  • The system uses shouldPauseRadioForVoice to duck or pause the radio during active voice conversations, preventing audio collision.
  • shouldStopVoiceAfterRadioTool determines when control should return to the radio after successful play, resume, or select actions.
  • A deferred confirmation pattern buffers results in pendingRadioPlaybackResult and only confirms "Turning on the radio" after startPreparedRadioAfterPlaybackReady verifies the stream is playing.
  • Epoch tracking (radioHandoffEpoch) prevents race conditions where older commands might override newer stop requests.
  • The reservation token system ensures that stop commands immediately cancel any in-flight play requests.

Frequently Asked Questions

How does the system prevent race conditions between consecutive voice commands?

The controller implements a radioHandoffEpoch counter that increments with each command. When a tool call begins, it captures the current epoch; only the most recent epoch is allowed to trigger playback. If a user says "play" followed quickly by "stop," the stop command increments the epoch, causing the pending play operation to be discarded when it eventually resolves (lines 158‑166 in gevRealtime.js).

Why is the radio confirmation deferred rather than immediate?

The confirmation is deferred to avoid false positives. If the system confirmed immediately when receiving the command, the user might hear "Turning on the radio" followed by silence if the station fails to load. By buffering the result in pendingRadioPlaybackResult and waiting for response.done, the system verifies the audio stream is actually playing before emitting the confirmation (implemented in startPreparedRadioAfterPlaybackReady, lines 37‑70).

What happens if a user interrupts a "play" command with "stop"?

When a "stop" command is detected (radioOwnershipClaimed), the controller immediately calls radioLayer.stopPlayback using the radioReservationToken. This cancels any pending playback before it can start. The reservation logic in the tool-dispatch loop (lines 140‑150) ensures that disabling actions take precedence over enabling ones.

Which file contains the logic for resolving specific radio stations?

The actual station resolution logic resides in src/voice/gevActions.js, which implements the control_radio function. This module uses src/data/radioBrowser.js to query the Radio Browser directory and src/data/radioCountry.js to normalize station metadata before returning the stream URL to the voice controller.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →