Kuberwastaken/claurst · error · anyhow::Error

Voice unavailable

Error message

Voice unavailable

What it means

start_recording checks availability() first; if voice is unavailable it sends a VoiceEvent::Error and returns Err using the availability's error message, falling back to the generic 'Voice unavailable'. It signals the failure both through the event channel and the Result.

Solutions

  1. Grant microphone access in OS privacy/mic settings.
  2. Check availability() before starting and surface the specific error_message() to the user.
  3. Connect a microphone / run in an environment with an audio input device.

Example fix

// before
voice.start_recording(&event_tx).await?;
// after
let avail = voice.check_availability();
if !avail.is_available() {
    eprintln!("voice unavailable: {:?}", avail.error_message());
    return Ok(());
}
voice.start_recording(&event_tx).await?;
Defensive patterns

Strategy: validation

Validate before calling

let availability = voice.check_availability();
if !availability.is_available() {
    return Err(anyhow::anyhow!(
        availability.error_message().unwrap_or_default()
    ));
}

Prevention

When it happens

Trigger: Calling start_recording when check_availability() reports unavailable — no microphone permissions, no audio input subsystem, or platform limitations.

Common situations: App lacks microphone permission (OS privacy settings); headless/SSH environment without audio devices; voice feature disabled at compile time; no input device present.

Understand the failure class

Background: "unsupported platform" / "not supported on this platform" errors: what they mean and how to fix them — this error's family across 47 libraries.

Related errors


AI-assisted analysis of Kuberwastaken/claurst@b0637c97ec (2026-09-10). Data as JSON: /api/errors/e270bf12f0cb11e0. Report an issue: GitHub.

Appendix: source

Thrown at src-rust/crates/core/src/voice.rs:236

    ///
    /// This is a non-blocking call: audio capture and transcription run on
    /// Tokio tasks that stay alive until `stop_recording` is called (or the
    /// recorder is dropped).
    pub async fn start_recording(
        &mut self,
        event_tx: mpsc::Sender<VoiceEvent>,
    ) -> anyhow::Result<()> {
        if self.is_recording.load(Ordering::SeqCst) {
            return Ok(());
        }

        let availability = self.check_availability();
        if !availability.is_available() {
            let msg = availability
                .error_message()
                .unwrap_or_else(|| "Voice unavailable".to_string());
            let _ = event_tx.send(VoiceEvent::Error(msg.clone())).await;
            return Err(anyhow::anyhow!(msg));
        }

        self.is_recording.store(true, Ordering::SeqCst);

        let is_recording = self.is_recording.clone();
        let config = self.config.clone();

        // cpal::Stream is !Send, so we can't use tokio::spawn (which requires Send).
        // Instead, spin up a dedicated OS thread with its own single-threaded tokio
        // runtime so the stream stays local to that thread throughout its lifetime.
        std::thread::spawn(move || {
            let rt = tokio::runtime::Builder::new_current_thread()
                .enable_all()
                .build()
                .expect("voice thread runtime");
            rt.block_on(async move {
                match record_and_transcribe(is_recording, event_tx.clone(), config).await {
                    Ok(()) => {}

View on GitHub (pinned to b0637c97ec)