#[cfg(any(feature = "vad", feature = "denoise", feature = "capture"))]
use std::path::PathBuf;
use thiserror::Error;
#[cfg(target_os = "macos")]
const PERMISSION_HINT: &str = "Enable in System Settings > Privacy & Security > Microphone.";
#[cfg(target_os = "windows")]
const PERMISSION_HINT: &str = "Enable in Settings > Privacy & Security > Microphone.";
#[cfg(target_os = "linux")]
const PERMISSION_HINT: &str =
"Check your distribution's audio permissions (PulseAudio / PipeWire).";
#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
const PERMISSION_HINT: &str = "Check your system audio permissions.";
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum DecibriError {
#[error("sample rate must be between 1000 and 384000")]
SampleRateOutOfRange,
#[error("channels must be between 1 and 32")]
ChannelsOutOfRange,
#[error("multichannel capture is not supported; channels must be 1 (mono)")]
MultichannelNotSupported,
#[error("frames per buffer must be between 64 and 65536")]
FramesPerBufferOutOfRange,
#[error("agc target level must be between -40 and -3")]
AgcTargetOutOfRange,
#[error("limiter ceiling must be between -3.0 and 0.0")]
LimiterCeilingOutOfRange,
#[error("format must be 'int16' or 'float32'")]
InvalidFormat,
#[error("No microphone found matching \"{0}\"")]
MicrophoneNotFound(String),
#[error("No speaker found matching \"{0}\"")]
SpeakerNotFound(String),
#[error("Multiple devices match \"{name}\":\n{matches}")]
MultipleDevicesMatch { name: String, matches: String },
#[error("device index out of range. Call devices() to list available devices")]
DeviceIndexOutOfRange,
#[error("No microphone found. Check system audio input settings.")]
NoMicrophoneFound,
#[error("No speaker found. Check system audio settings.")]
NoSpeakerFound,
#[error("Selected device is not a valid microphone.")]
NotAnInputDevice,
#[error("Failed to enumerate devices: {0}")]
DeviceEnumerationFailed(String),
#[error("audio stream is already running. Call stop() first.")]
AlreadyRunning,
#[error("Failed to open audio stream: {0}")]
StreamOpenFailed(String),
#[error("Failed to start audio stream: {0}")]
StreamStartFailed(String),
#[error("Microphone permission denied. {}", PERMISSION_HINT)]
PermissionDenied,
#[error("Microphone stream is closed")]
MicrophoneStreamClosed,
#[error("Speaker stream is closed")]
SpeakerStreamClosed,
#[error("decibri: audio device error: {source}")]
DeviceFailed {
#[source]
source: Box<dyn std::error::Error + Send + Sync>,
},
#[cfg(feature = "capture")]
#[error("the requested sample rate conversion is not supported by the resampler")]
ResampleConfigInvalid { in_rate: u32, out_rate: u32 },
#[error("Silero VAD only supports sample rates 8000 and 16000")]
VadSampleRateUnsupported(u32),
#[error("VAD threshold must be between 0.0 and 1.0")]
VadThresholdOutOfRange(f32),
#[cfg(feature = "capture")]
#[error("Failed to read audio file {}: {source}", path.display())]
FileReadFailed {
path: PathBuf,
#[source]
source: std::io::Error,
},
#[cfg(feature = "capture")]
#[error("invalid WAV file: {reason}")]
WavInvalid { reason: &'static str },
#[error("analysis requires VAD; construct the File with a vad configuration")]
VadNotConfigured,
#[error("File iteration has begun; construct a new File to analyze the whole recording")]
FileEngaged,
#[cfg(any(feature = "vad", feature = "denoise"))]
#[error(
"decibri: failed to initialize ONNX Runtime: {source}. \
Either pass ort_library_path when constructing the VAD, set \
ORT_DYLIB_PATH to point to a valid ONNX Runtime library, or \
enable the `ort-download-binaries` feature for zero-config builds."
)]
OrtInitFailed {
#[source]
source: Box<dyn std::error::Error + Send + Sync>,
},
#[cfg(any(feature = "vad", feature = "denoise"))]
#[error(
"decibri: failed to load ONNX Runtime from {}: {source}. \
If ORT_DYLIB_PATH is set, verify it points to a valid ONNX Runtime \
library for your platform. Otherwise the bundled ONNX Runtime may \
be missing from your installation. Try reinstalling decibri.",
path.display()
)]
OrtLoadFailed {
path: PathBuf,
#[source]
source: Box<dyn std::error::Error + Send + Sync>,
},
#[cfg(any(feature = "vad", feature = "denoise"))]
#[error(
"decibri: failed to load ONNX Runtime from {}: {reason}. \
If ORT_DYLIB_PATH is set, verify it points to a valid ONNX Runtime \
library for your platform. Otherwise the bundled ONNX Runtime may \
be missing from your installation. Try reinstalling decibri.",
path.display()
)]
OrtPathInvalid { path: PathBuf, reason: &'static str },
#[cfg(any(feature = "vad", feature = "denoise"))]
#[error("Failed to create ort session builder: {0}")]
OrtSessionBuildFailed(#[source] Box<dyn std::error::Error + Send + Sync>),
#[cfg(any(feature = "vad", feature = "denoise"))]
#[error("Failed to set ort threads: {0}")]
OrtThreadsConfigFailed(#[source] Box<dyn std::error::Error + Send + Sync>),
#[cfg(any(feature = "vad", feature = "denoise"))]
#[error("Failed to load Silero VAD model from {}: {source}", path.display())]
VadModelLoadFailed {
path: PathBuf,
#[source]
source: Box<dyn std::error::Error + Send + Sync>,
},
#[cfg(any(feature = "vad", feature = "denoise"))]
#[error("Failed to load model from {}: {source}", path.display())]
ModelLoadFailed {
path: PathBuf,
#[source]
source: Box<dyn std::error::Error + Send + Sync>,
},
#[cfg(any(feature = "vad", feature = "denoise"))]
#[error("Silero VAD inference failed: {0}")]
OrtInferenceFailed(#[source] Box<dyn std::error::Error + Send + Sync>),
#[cfg(any(feature = "vad", feature = "denoise"))]
#[error("Failed to create {kind} tensor: {source}")]
OrtTensorCreateFailed {
kind: &'static str,
#[source]
source: Box<dyn std::error::Error + Send + Sync>,
},
#[cfg(any(feature = "vad", feature = "denoise"))]
#[error("Failed to extract {kind} tensor: {source}")]
OrtTensorExtractFailed {
kind: &'static str,
#[source]
source: Box<dyn std::error::Error + Send + Sync>,
},
#[error(
"ONNX Runtime was initialized in pid {init_pid} but is being used in pid {current_pid}. \
Python's fork() start method is incompatible with ORT initialization. Use \
multiprocessing.set_start_method('spawn') or construct Microphone(vad='silero') \
inside each child process."
)]
ForkAfterOrtInit { init_pid: u32, current_pid: u32 },
#[error("ONNX backend error from {backend}: {source}")]
OnnxBackendFailed {
backend: &'static str,
#[source]
source: Box<dyn std::error::Error + Send + Sync>,
},
}
fn sample_source() -> Box<dyn std::error::Error + Send + Sync> {
Box::new(std::io::Error::other("sample"))
}
macro_rules! error_identity {
($(
$( #[$attr:meta] )*
$pattern:pat => $name:literal, $code:literal, $sample:expr ;
)*) => {
impl DecibriError {
pub fn variant_name(&self) -> &'static str {
match self {
$( $( #[$attr] )* $pattern => $name, )*
}
}
pub fn code(&self) -> &'static str {
match self {
$( $( #[$attr] )* $pattern => $code, )*
}
}
}
fn fill_error_catalog(catalog: &mut Vec<DecibriError>) {
$( $( #[$attr] )* catalog.push($sample); )*
}
#[doc(hidden)]
pub fn error_catalog() -> Vec<DecibriError> {
let mut catalog = Vec::new();
fill_error_catalog(&mut catalog);
catalog
}
};
}
error_identity! {
DecibriError::SampleRateOutOfRange => "SampleRateOutOfRange", "SAMPLE_RATE_OUT_OF_RANGE",
DecibriError::SampleRateOutOfRange;
DecibriError::ChannelsOutOfRange => "ChannelsOutOfRange", "CHANNELS_OUT_OF_RANGE",
DecibriError::ChannelsOutOfRange;
DecibriError::MultichannelNotSupported => "MultichannelNotSupported", "MULTICHANNEL_NOT_SUPPORTED",
DecibriError::MultichannelNotSupported;
DecibriError::FramesPerBufferOutOfRange => "FramesPerBufferOutOfRange", "FRAMES_PER_BUFFER_OUT_OF_RANGE",
DecibriError::FramesPerBufferOutOfRange;
DecibriError::AgcTargetOutOfRange => "AgcTargetOutOfRange", "AGC_TARGET_OUT_OF_RANGE",
DecibriError::AgcTargetOutOfRange;
DecibriError::LimiterCeilingOutOfRange => "LimiterCeilingOutOfRange", "LIMITER_CEILING_OUT_OF_RANGE",
DecibriError::LimiterCeilingOutOfRange;
DecibriError::InvalidFormat => "InvalidFormat", "INVALID_FORMAT",
DecibriError::InvalidFormat;
DecibriError::MicrophoneNotFound(_) => "MicrophoneNotFound", "MICROPHONE_NOT_FOUND",
DecibriError::MicrophoneNotFound("sample".to_string());
DecibriError::SpeakerNotFound(_) => "SpeakerNotFound", "SPEAKER_NOT_FOUND",
DecibriError::SpeakerNotFound("sample".to_string());
DecibriError::MultipleDevicesMatch { .. } => "MultipleDevicesMatch", "MULTIPLE_DEVICES_MATCH",
DecibriError::MultipleDevicesMatch {
name: "sample".to_string(),
matches: "sample".to_string(),
};
DecibriError::DeviceIndexOutOfRange => "DeviceIndexOutOfRange", "DEVICE_INDEX_OUT_OF_RANGE",
DecibriError::DeviceIndexOutOfRange;
DecibriError::NoMicrophoneFound => "NoMicrophoneFound", "NO_MICROPHONE_FOUND",
DecibriError::NoMicrophoneFound;
DecibriError::NoSpeakerFound => "NoSpeakerFound", "NO_SPEAKER_FOUND",
DecibriError::NoSpeakerFound;
DecibriError::NotAnInputDevice => "NotAnInputDevice", "NOT_AN_INPUT_DEVICE",
DecibriError::NotAnInputDevice;
DecibriError::DeviceEnumerationFailed(_) => "DeviceEnumerationFailed", "DEVICE_ENUMERATION_FAILED",
DecibriError::DeviceEnumerationFailed("sample".to_string());
DecibriError::AlreadyRunning => "AlreadyRunning", "ALREADY_RUNNING",
DecibriError::AlreadyRunning;
DecibriError::StreamOpenFailed(_) => "StreamOpenFailed", "STREAM_OPEN_FAILED",
DecibriError::StreamOpenFailed("sample".to_string());
DecibriError::StreamStartFailed(_) => "StreamStartFailed", "STREAM_START_FAILED",
DecibriError::StreamStartFailed("sample".to_string());
DecibriError::PermissionDenied => "PermissionDenied", "PERMISSION_DENIED",
DecibriError::PermissionDenied;
DecibriError::MicrophoneStreamClosed => "MicrophoneStreamClosed", "MICROPHONE_STREAM_CLOSED",
DecibriError::MicrophoneStreamClosed;
DecibriError::SpeakerStreamClosed => "SpeakerStreamClosed", "SPEAKER_STREAM_CLOSED",
DecibriError::SpeakerStreamClosed;
DecibriError::DeviceFailed { .. } => "DeviceFailed", "DEVICE_FAILED",
DecibriError::DeviceFailed { source: sample_source() };
#[cfg(feature = "capture")]
DecibriError::ResampleConfigInvalid { .. } => "ResampleConfigInvalid", "RESAMPLE_CONFIG_INVALID",
DecibriError::ResampleConfigInvalid { in_rate: 44100, out_rate: 16000 };
DecibriError::VadSampleRateUnsupported(_) => "VadSampleRateUnsupported", "VAD_SAMPLE_RATE_UNSUPPORTED",
DecibriError::VadSampleRateUnsupported(44100);
DecibriError::VadThresholdOutOfRange(_) => "VadThresholdOutOfRange", "VAD_THRESHOLD_OUT_OF_RANGE",
DecibriError::VadThresholdOutOfRange(1.5);
#[cfg(feature = "capture")]
DecibriError::FileReadFailed { .. } => "FileReadFailed", "FILE_READ_FAILED",
DecibriError::FileReadFailed {
path: PathBuf::from("sample.wav"),
source: std::io::Error::other("sample"),
};
#[cfg(feature = "capture")]
DecibriError::WavInvalid { .. } => "WavInvalid", "WAV_INVALID",
DecibriError::WavInvalid { reason: "sample" };
DecibriError::VadNotConfigured => "VadNotConfigured", "VAD_NOT_CONFIGURED",
DecibriError::VadNotConfigured;
DecibriError::FileEngaged => "FileEngaged", "FILE_ENGAGED",
DecibriError::FileEngaged;
#[cfg(any(feature = "vad", feature = "denoise"))]
DecibriError::OrtInitFailed { .. } => "OrtInitFailed", "ORT_INIT_FAILED",
DecibriError::OrtInitFailed { source: sample_source() };
#[cfg(any(feature = "vad", feature = "denoise"))]
DecibriError::OrtLoadFailed { .. } => "OrtLoadFailed", "ORT_LOAD_FAILED",
DecibriError::OrtLoadFailed {
path: PathBuf::from("sample"),
source: sample_source(),
};
#[cfg(any(feature = "vad", feature = "denoise"))]
DecibriError::OrtPathInvalid { .. } => "OrtPathInvalid", "ORT_LOAD_FAILED",
DecibriError::OrtPathInvalid {
path: PathBuf::from("sample"),
reason: "sample",
};
#[cfg(any(feature = "vad", feature = "denoise"))]
DecibriError::OrtSessionBuildFailed(_) => "OrtSessionBuildFailed", "ORT_SESSION_BUILD_FAILED",
DecibriError::OrtSessionBuildFailed(sample_source());
#[cfg(any(feature = "vad", feature = "denoise"))]
DecibriError::OrtThreadsConfigFailed(_) => "OrtThreadsConfigFailed", "ORT_THREADS_CONFIG_FAILED",
DecibriError::OrtThreadsConfigFailed(sample_source());
#[cfg(any(feature = "vad", feature = "denoise"))]
DecibriError::VadModelLoadFailed { .. } => "VadModelLoadFailed", "VAD_MODEL_LOAD_FAILED",
DecibriError::VadModelLoadFailed {
path: PathBuf::from("sample"),
source: sample_source(),
};
#[cfg(any(feature = "vad", feature = "denoise"))]
DecibriError::ModelLoadFailed { .. } => "ModelLoadFailed", "MODEL_LOAD_FAILED",
DecibriError::ModelLoadFailed {
path: PathBuf::from("sample"),
source: sample_source(),
};
#[cfg(any(feature = "vad", feature = "denoise"))]
DecibriError::OrtInferenceFailed(_) => "OrtInferenceFailed", "ORT_INFERENCE_FAILED",
DecibriError::OrtInferenceFailed(sample_source());
#[cfg(any(feature = "vad", feature = "denoise"))]
DecibriError::OrtTensorCreateFailed { .. } => "OrtTensorCreateFailed", "ORT_TENSOR_CREATE_FAILED",
DecibriError::OrtTensorCreateFailed {
kind: "sample",
source: sample_source(),
};
#[cfg(any(feature = "vad", feature = "denoise"))]
DecibriError::OrtTensorExtractFailed { .. } => "OrtTensorExtractFailed", "ORT_TENSOR_EXTRACT_FAILED",
DecibriError::OrtTensorExtractFailed {
kind: "sample",
source: sample_source(),
};
DecibriError::ForkAfterOrtInit { .. } => "ForkAfterOrtInit", "FORK_AFTER_ORT_INIT",
DecibriError::ForkAfterOrtInit { init_pid: 1, current_pid: 2 };
DecibriError::OnnxBackendFailed { .. } => "OnnxBackendFailed", "ONNX_BACKEND_FAILED",
DecibriError::OnnxBackendFailed {
backend: "sample",
source: sample_source(),
};
}
#[cfg(feature = "capture")]
impl From<decibri_resampler::ResamplerError> for DecibriError {
fn from(e: decibri_resampler::ResamplerError) -> Self {
use decibri_resampler::ResamplerError;
match e {
ResamplerError::RatePairUnsupported { in_rate, out_rate } => {
DecibriError::ResampleConfigInvalid { in_rate, out_rate }
}
_ => DecibriError::ResampleConfigInvalid {
in_rate: 0,
out_rate: 0,
},
}
}
}
impl DecibriError {
pub fn is_ort_path_error(&self) -> bool {
#[cfg(any(feature = "vad", feature = "denoise"))]
{
matches!(
self,
Self::OrtLoadFailed { .. } | Self::OrtPathInvalid { .. }
)
}
#[cfg(not(any(feature = "vad", feature = "denoise")))]
{
false
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::error::Error as _;
#[test]
fn device_failed_carries_structured_source() {
let inner = std::io::Error::other("device gone");
let err = DecibriError::DeviceFailed {
source: Box::new(inner),
};
assert_eq!(err.to_string(), "decibri: audio device error: device gone");
assert!(err.source().is_some());
}
#[cfg(target_os = "macos")]
#[test]
fn macos_permission_hint_uses_modern_wording() {
assert_eq!(
PERMISSION_HINT,
"Enable in System Settings > Privacy & Security > Microphone."
);
}
#[test]
fn permission_denied_message_prefix_is_frozen() {
let msg = DecibriError::PermissionDenied.to_string();
assert!(
msg.starts_with("Microphone permission denied. "),
"frozen prefix must not drift: {msg}"
);
}
fn screaming_snake(name: &str) -> String {
let mut out = String::new();
for (i, ch) in name.char_indices() {
if i > 0 && ch.is_ascii_uppercase() {
out.push('_');
}
out.push(ch.to_ascii_uppercase());
}
out
}
#[test]
fn codes_follow_the_naming_rule() {
for err in error_catalog() {
let name = err.variant_name();
let expected = if name == "OrtPathInvalid" {
"ORT_LOAD_FAILED".to_string()
} else {
screaming_snake(name)
};
assert_eq!(err.code(), expected, "code drifted for {name}");
}
}
#[test]
fn catalog_names_are_unique() {
let catalog = error_catalog();
let mut names: Vec<&str> = catalog.iter().map(|e| e.variant_name()).collect();
let total = names.len();
names.sort_unstable();
names.dedup();
assert_eq!(names.len(), total, "catalog carries a variant twice");
}
#[cfg(any(feature = "vad", feature = "denoise"))]
#[test]
fn only_the_ort_path_pair_shares_a_code() {
let catalog = error_catalog();
let shared: Vec<&str> = catalog
.iter()
.filter(|e| {
catalog
.iter()
.filter(|other| other.code() == e.code())
.count()
> 1
})
.map(|e| e.variant_name())
.collect();
assert_eq!(shared, ["OrtLoadFailed", "OrtPathInvalid"]);
}
#[test]
fn catalog_instances_render() {
for err in error_catalog() {
assert!(
!err.to_string().is_empty(),
"empty Display for {}",
err.variant_name()
);
}
}
#[test]
fn file_engaged_message_is_frozen() {
assert_eq!(
DecibriError::FileEngaged.to_string(),
"File iteration has begun; construct a new File to analyze the whole recording"
);
}
}