1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
use std::path::PathBuf;
use thiserror::Error;
/// Errors produced by decibri operations.
///
/// This enum is `#[non_exhaustive]`: consumers pattern-matching on it must
/// include a `_ =>` catch-all arm so future variant additions are not
/// source-breaking.
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum DecibriError {
// ── Config validation ──────────────────────────────────────────────
#[error("sampleRate must be between 1000 and 384000")]
SampleRateOutOfRange,
#[error("channels must be between 1 and 32")]
ChannelsOutOfRange,
#[error("framesPerBuffer must be between 64 and 65536")]
FramesPerBufferOutOfRange,
#[error("format must be 'int16' or 'float32'")]
InvalidFormat,
// ── Device errors ──────────────────────────────────────────────────
#[error("No audio input device found matching \"{0}\"")]
DeviceNotFound(String),
/// No output device matched a `Name` or `Id` selector.
///
/// The split from [`Self::DeviceNotFound`] exists because the display
/// message needs to say "output" when the lookup was against output
/// devices, not "input". Issued by [`crate::device::resolve_output_device`]
/// via the internal direction-specific `not_found_error` hook.
#[error("No audio output device found matching \"{0}\"")]
OutputDeviceNotFound(String),
#[error("Multiple devices match \"{name}\":\n{matches}")]
MultipleDevicesMatch { name: String, matches: String },
#[error("device index out of range. Call Decibri.devices() to list available devices")]
DeviceIndexOutOfRange,
#[error("No microphone found. Check system audio input settings.")]
NoMicrophoneFound,
#[error("No audio output device found. Check system audio settings.")]
NoOutputDeviceFound,
#[error("Selected device is not a valid input device.")]
NotAnInputDevice,
#[error("Failed to enumerate devices: {0}")]
DeviceEnumerationFailed(String),
// ── Stream errors ──────────────────────────────────────────────────
#[error("Decibri 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. Enable in System Preferences > Security & Privacy.")]
PermissionDenied,
/// Capture stream has closed (stopped explicitly or by driver error).
/// Returned from `CaptureStream::try_next_chunk` / `next_chunk` when the
/// underlying channel has disconnected.
#[error("Capture stream is closed")]
CaptureStreamClosed,
/// Output stream has closed. Returned from `OutputStream::send` when the
/// underlying channel has disconnected.
#[error("Output stream closed")]
OutputStreamClosed,
// ── VAD config validation ──────────────────────────────────────────
/// Payload carries the offending sample rate; not formatted into the
/// Display string to preserve the 3.1.x message text.
#[error("Silero VAD only supports sample rates 8000 and 16000")]
VadSampleRateUnsupported(u32),
/// Payload carries the offending threshold; not formatted into the
/// Display string to preserve the 3.1.x message text.
#[error("VAD threshold must be between 0.0 and 1.0")]
VadThresholdOutOfRange(f32),
// ── ORT and VAD model errors ───────────────────────────────────────
//
// These variants carry a structured `ort::Error` via `#[source]` so
// downstream consumers can walk `error.source()` to the underlying
// ORT error for programmatic handling. Paths are `PathBuf` (not
// `String`) so consumers retain path semantics without re-parsing.
#[error(
"decibri: failed to initialize ONNX Runtime: {source}. \
Either pass ort_library_path in VadConfig, 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: ort::Error,
},
#[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 ORT may be missing \
from your platform package. Try reinstalling decibri.",
path.display()
)]
OrtLoadFailed {
path: PathBuf,
#[source]
source: ort::Error,
},
/// The `ort_library_path` in `VadConfig` failed a pre-check before it
/// could be handed to `ort::init_from`. Used only by the Windows
/// hang-prevention pre-check in `init_ort_once`.
///
/// Intentionally does *not* carry an `ort::Error` source, because
/// constructing an `ort::Error` under `ort-load-dynamic` calls into the
/// ORT C API (`ortsys![CreateStatus]`), which is exactly the hang this
/// pre-check is designed to prevent. Keeping this variant string-only
/// means the pre-check never touches ORT symbols.
///
/// Display message matches [`Self::OrtLoadFailed`] so Node, Python, and
/// future FFI consumers see the same actionable hint regardless of which
/// failure path was taken.
#[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 ORT may be missing \
from your platform package. Try reinstalling decibri.",
path.display()
)]
OrtPathInvalid { path: PathBuf, reason: &'static str },
#[error("Failed to create ort session builder: {0}")]
OrtSessionBuildFailed(#[source] ort::Error),
#[error("Failed to set ort threads: {0}")]
OrtThreadsConfigFailed(#[source] ort::Error),
#[error("Failed to load Silero VAD model from {}: {source}", path.display())]
VadModelLoadFailed {
path: PathBuf,
#[source]
source: ort::Error,
},
#[error("Silero VAD inference failed: {0}")]
OrtInferenceFailed(#[source] ort::Error),
#[error("Failed to create {kind} tensor: {source}")]
OrtTensorCreateFailed {
kind: &'static str,
#[source]
source: ort::Error,
},
#[error("Failed to extract {kind} tensor: {source}")]
OrtTensorExtractFailed {
kind: &'static str,
#[source]
source: ort::Error,
},
}
impl DecibriError {
/// Returns true if this error represents a failure to use a specific
/// ORT library path.
///
/// Consumers handling "the `ort_library_path` is wrong" logic should
/// match on this rather than enumerating [`Self::OrtLoadFailed`] and
/// [`Self::OrtPathInvalid`] separately:
///
/// - [`Self::OrtLoadFailed`] fires when ORT tried to load the path and
/// failed (e.g. wrong ORT version, corrupted dylib).
/// - [`Self::OrtPathInvalid`] fires when decibri's filesystem pre-check
/// rejected the path before ORT saw it (nonexistent, directory, etc).
///
/// Both represent the same user-facing failure mode: "this path cannot
/// be used to load ORT." The split is a mechanical necessity (see the
/// rustdoc on [`Self::OrtPathInvalid`]), not a categorization users
/// need to care about.
pub fn is_ort_path_error(&self) -> bool {
matches!(
self,
Self::OrtLoadFailed { .. } | Self::OrtPathInvalid { .. }
)
}
}