zeroclaw-labs/zeroclaw · error · anyhow::Error
VoiceWake: audio stream ended unexpectedly
Error message
VoiceWake: audio stream ended unexpectedly
What it means
VoiceWakeChannel::listen runs a state machine over chunks received from an mpsc channel fed by the cpal input-stream callback; the loop only exits when audio_rx.recv() yields None, i.e. every sender was dropped. Since the stream is intentionally leaked, this bail means the capture callback stopped sending — the microphone stream terminated underneath the channel (device removed, host error, or the stream was dropped by the OS/driver). It is a liveness error: the wake-word listener is dead until listen() is restarted.
Source
Thrown at crates/zeroclaw-channels/src/voice_wake.rs:477
.with_outcome(::zeroclaw_log::EventOutcome::Unknown)
.with_attrs(::serde_json::json!({"error": format!("{}", e)})),
"VoiceWake: transcription error for utterance"
);
}
}
state = WakeState::Listening;
capture_buf.clear();
}
}
WakeState::Processing => {
// Should not receive chunks while processing, but just buffer them.
// State transitions happen above synchronously after transcription.
}
}
}
bail!("VoiceWake: audio stream ended unexpectedly");
}
}
// ── Audio utilities ────────────────────────────────────────────
/// Compute RMS (root-mean-square) energy of an audio chunk.
pub fn compute_rms_energy(samples: &[f32]) -> f32 {
if samples.is_empty() {
return 0.0;
}
let sum_sq: f32 = samples.iter().map(|s| s * s).sum();
(sum_sq / samples.len() as f32).sqrt()
}
/// Encode raw f32 PCM samples as a WAV byte buffer (16-bit PCM).
/// This produces a minimal valid WAV file that Whisper-compatible APIs accept.
pub fn encode_wav_from_f32(samples: &[f32], sample_rate: u32, channels: u16) -> Vec<u8> {
let bits_per_sample: u16 = 16;View on GitHub (pinned to 88bb9c8533)
Solutions
- Check physical/default device state: is the mic still connected and still the system default input?
- Restart the daemon (or re-invoke listen) — startup re-acquires default_input_device and rebuilds the stream.
- Use a stable, always-present input device (built-in mic or virtual device) instead of hot-pluggable hardware for an always-on bot.
- Inspect preceding WARN 'VoiceWake: audio stream error' log entries — they carry the cpal error that preceded the stream death.
Example fix
// before — one-shot listen dies with the device
voice_wake.listen(tx).await?; // someday: "audio stream ended unexpectedly"
// after — supervise and rebuild the stream on failure
loop {
if let Err(e) = voice_wake.listen(tx.clone()).await {
::zeroclaw_log::record!(WARN, ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note)
.with_attrs(::serde_json::json!({"error": format!("{e}")})),
"voice_wake listener stopped; restarting in 5s");
}
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
} Defensive patterns
Strategy: retry
Validate before calling
use cpal::traits::{DeviceTrait, HostTrait};
// Verify an input device still exists before (re)starting the listener.
let device_available = cpal::default_host().default_input_device()
.and_then(|d| d.default_input_config().ok().is_some());
anyhow::ensure!(device_available, "no usable default input device; skipping voice_wake restart"); Try / catch
match voice_wake.listen(tx.clone()).await {
Ok(()) => {}
Err(e) if e.to_string().contains("audio stream ended unexpectedly") => {
// Device-level failure: wait, re-probe the default input device, then
// call listen() again. Back off between restarts to avoid hot-looping
// on a permanently missing device.
}
Err(e) => return Err(e),
} Prevention
- Supervise listen() in a restart loop with backoff instead of treating it as run-forever.
- Run always-on bots with a non-hot-pluggable default input (built-in mic or persistent virtual device).
- Alert on the preceding 'VoiceWake: audio stream error' WARN logs — they usually precede stream death.
- Disable the voice_wake channel on hosts without a microphone so startup fails predictably.
When it happens
Trigger: listen() is running and (1) a USB microphone or Bluetooth headset is unplugged/disconnected mid-session; (2) the OS suspends or reclaims the audio device (system sleep/resume, another app takes exclusive control); (3) an ALSA/JACK error kills the stream and the callback's sender is dropped; (4) the daemon's audio thread panics. The error appears asynchronously, long after startup.
Common situations: Long-running bot on a laptop: closing the lid or unplugging the USB mic kills the listener; Bluetooth headset battery dying; PulseAudio/PipeWire restarting; headless box losing the default input after an OS update.
Related errors
- Unsupported input sample format: {format}
- audio download exceeds {} byte limit
- audio download failed after token refresh: {}
- audio download failed: {}
- audio download failed ({status}) for message {message_id}
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/2c9ff09cee67a584.
Report an issue: GitHub.