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

> Learn how voice actions trigger radio playback and deferred confirmation in God's Eye View using the control_radio tool. Discover how audio stream status ensures accurate user feedback.

- Repository: [Bilawal Sidhu/gods-eye-view](https://github.com/bilawalsidhu/gods-eye-view)
- Tags: how-to-guide
- Published: 2026-08-29

---

**Voice actions trigger radio playback through the `control_radio` tool in [`src/voice/gevRealtime.js`](https://github.com/bilawalsidhu/gods-eye-view/blob/main/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`](https://github.com/bilawalsidhu/gods-eye-view/blob/main/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`](https://github.com/bilawalsidhu/gods-eye-view/blob/main/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).

```javascript
// 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.

```javascript
// 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`](https://github.com/bilawalsidhu/gods-eye-view/blob/main/src/voice/gevActions.js) (lines 95‑97).

```javascript
// 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).

```javascript
// 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).

```javascript
// 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.

```javascript
// 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`](https://github.com/bilawalsidhu/gods-eye-view/blob/main/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

```javascript
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:

```javascript
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`](https://github.com/bilawalsidhu/gods-eye-view/blob/main/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`](https://github.com/bilawalsidhu/gods-eye-view/blob/main/src/voice/gevActions.js)** | Concrete tool functions including `control_radio` that interact with the Radio layer and resolve station data. |
| **[`src/voice/voiceCost.js`](https://github.com/bilawalsidhu/gods-eye-view/blob/main/src/voice/voiceCost.js)** | Tier-based cost model determining when voice sessions terminate, affecting radio re-enable timing. |
| **[`src/data/radioBrowser.js`](https://github.com/bilawalsidhu/gods-eye-view/blob/main/src/data/radioBrowser.js)** | Fetches the Radio Browser directory used by `control_radio` to resolve station URLs. |
| **[`src/data/radioCountry.js`](https://github.com/bilawalsidhu/gods-eye-view/blob/main/src/data/radioCountry.js)** | Normalizes station data for the radio layer. |
| **[`src/ui.js`](https://github.com/bilawalsidhu/gods-eye-view/blob/main/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`](https://github.com/bilawalsidhu/gods-eye-view/blob/main/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`](https://github.com/bilawalsidhu/gods-eye-view/blob/main/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`](https://github.com/bilawalsidhu/gods-eye-view/blob/main/src/voice/gevActions.js)**, which implements the `control_radio` function. This module uses **[`src/data/radioBrowser.js`](https://github.com/bilawalsidhu/gods-eye-view/blob/main/src/data/radioBrowser.js)** to query the Radio Browser directory and **[`src/data/radioCountry.js`](https://github.com/bilawalsidhu/gods-eye-view/blob/main/src/data/radioCountry.js)** to normalize station metadata before returning the stream URL to the voice controller.