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
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
//! Root failure kinds and safe default error messages.
use crate::error_context::{ErrorContext, ErrorGroup};
use crate::types::Permission;
use crate::VadErrorCode;
use std::fmt;
/// Stable root failure classification, preserved through context and related errors.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum ErrorKind {
/// Invalid argument.
InvalidArg,
/// Invalid lifecycle state.
InvalidState,
/// Device lookup completed without a match.
DeviceNotFound,
/// Recording permission denied.
PermissionDenied,
/// Unsupported OS version.
UnsupportedOsVersion,
/// Active device lost.
DeviceLost,
/// Backend failure.
Backend,
/// Unsupported PCM format.
UnsupportedFormat,
/// Negotiated native format changed.
NativeFormatChanged,
/// Unsupported operation.
Unsupported,
/// Device selection is ambiguous.
AmbiguousDeviceName,
}
/// Typed failure with library-authored explanations and redacted private fields.
///
/// Message-bearing variants must contain safe, library-authored explanations, which may
/// include OS error text, never device names, paths, or raw environment diagnostics.
/// Display retains those explanations. Permission details and context call labels remain
/// available only through structured access.
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum Error {
/// Invalid argument with a safe validation explanation.
InvalidArg(String),
/// Invalid state with a safe lifecycle explanation.
InvalidState(String),
/// Device lookup completed without a match.
DeviceNotFound,
/// Permission denied with explicit structured detail.
PermissionDenied {
/// Denied permission.
permission: Permission,
/// Raw diagnostic detail, opt-in only.
detail: String,
},
/// Unsupported OS version.
UnsupportedOsVersion,
/// Capture device lost.
DeviceLost,
/// Backend failure with a safe explanation.
Backend(String),
/// Addon failure with its stable identity and opt-in diagnostic message.
Addon {
/// Stable addon cause.
kind: crate::AddonErrorKind,
/// Original diagnostic, available through structured access only.
message: String,
},
/// Optional VAD processor failure; capture remains usable.
Vad {
/// Stable typed processor cause.
code: VadErrorCode,
},
/// Unsupported format with a safe validation explanation.
UnsupportedFormat(String),
/// Native format mismatch, retaining numeric pairs.
NativeFormatChanged {
/// Advertised rate/channels.
advertised: (u32, u16),
/// Actual rate/channels.
actual: (u32, u16),
},
/// Unsupported operation.
Unsupported,
/// Selection matched multiple devices.
AmbiguousDeviceName,
/// Opt-in native diagnostic detail, excluded from safe display.
Diagnostic {
/// Typed underlying failure.
source: Box<Error>,
/// Native diagnostic text, available only through structured access.
detail: String,
},
/// Context preserving the source kind.
Context {
/// Underlying failure.
source: Box<Error>,
/// Structured operation/lane/native status.
context: ErrorContext,
},
/// Primary with a nonempty list of related failures.
Multiple(ErrorGroup),
}
impl Error {
/// Root kind, recursively preserving the primary cause.
pub fn kind(&self) -> ErrorKind {
match self {
Self::InvalidArg(_) => ErrorKind::InvalidArg,
Self::InvalidState(_) => ErrorKind::InvalidState,
Self::DeviceNotFound => ErrorKind::DeviceNotFound,
Self::PermissionDenied { .. } => ErrorKind::PermissionDenied,
Self::UnsupportedOsVersion => ErrorKind::UnsupportedOsVersion,
Self::DeviceLost => ErrorKind::DeviceLost,
Self::Backend(_) => ErrorKind::Backend,
Self::Addon { kind, .. } => kind.error_kind(),
Self::Vad { .. } => ErrorKind::Backend,
Self::UnsupportedFormat(_) => ErrorKind::UnsupportedFormat,
Self::NativeFormatChanged { .. } => ErrorKind::NativeFormatChanged,
Self::Unsupported => ErrorKind::Unsupported,
Self::AmbiguousDeviceName => ErrorKind::AmbiguousDeviceName,
Self::Context { source, .. } | Self::Diagnostic { source, .. } => source.kind(),
Self::Multiple(group) => group.primary().kind(),
}
}
/// Add context using one typed construction path.
pub fn with_context(self, context: ErrorContext) -> Self {
Self::Context {
source: Box::new(self),
context,
}
}
/// Retain opt-in native text without exposing it in the default message.
pub fn with_diagnostic_detail(self, detail: String) -> Self {
Self::Diagnostic {
source: Box::new(self),
detail,
}
}
/// Native diagnostic text along the primary cause chain, outermost first.
pub fn diagnostic_details(&self) -> Vec<&str> {
match self {
Self::Diagnostic { source, detail } => {
let mut details = vec![detail.as_str()];
details.extend(source.diagnostic_details());
details
}
Self::Context { source, .. } => source.diagnostic_details(),
Self::Multiple(group) => group.primary().diagnostic_details(),
_ => Vec::new(),
}
}
/// Primary root for explicit structured projections and permission checks.
pub fn root(&self) -> &Self {
match self {
Self::Context { source, .. } | Self::Diagnostic { source, .. } => source.root(),
Self::Multiple(group) => group.primary().root(),
error => error,
}
}
/// Recording permission represented by the primary cause.
pub fn permission(&self) -> Option<Permission> {
match self.root() {
Self::PermissionDenied { permission, .. } => Some(*permission),
_ => None,
}
}
/// Addon cause retained through primary context wrappers.
pub fn addon_kind(&self) -> Option<crate::AddonErrorKind> {
match self.root() {
Self::Addon { kind, .. } => Some(*kind),
_ => None,
}
}
/// Optional VAD cause retained through primary context wrappers.
pub fn vad_code(&self) -> Option<VadErrorCode> {
match self.root() {
Self::Vad { code } => Some(*code),
_ => None,
}
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidArg(message) => write!(f, "invalid argument: {message}"),
Self::InvalidState(message) => write!(f, "invalid state: {message}"),
Self::DeviceNotFound => f.write_str("device not found"),
Self::PermissionDenied { permission, .. } => write!(f, "{permission} recording permission denied or not granted. {}", permission.guidance()),
Self::UnsupportedOsVersion => f.write_str("unsupported OS version"),
Self::DeviceLost => f.write_str("device lost"),
Self::Backend(message) => write!(f, "backend error: {message}"),
Self::Addon { kind, .. } => f.write_str(kind.explanation()),
Self::Vad { code } => write!(f, "VAD failed ({}): {}", code.as_str(), code.explanation()),
Self::UnsupportedFormat(message) => write!(f, "unsupported output format: {message}"),
Self::NativeFormatChanged { advertised, actual } => write!(f, "native input format changed from {} Hz/{} channels to {} Hz/{} channels; recreate the stream", advertised.0, advertised.1, actual.0, actual.1),
Self::Unsupported => f.write_str("unsupported"),
Self::AmbiguousDeviceName => f.write_str("device selection is ambiguous; select a unique device"),
Self::Diagnostic { source, .. } => source.fmt(f),
Self::Context { source, context } => write!(f, "{source} {context}"),
Self::Multiple(group) => {
write!(f, "{}", group.primary())?;
// Isolated VAD causes remain in the structured group and shutdown cleanup
// report. They do not change the capture primary's diagnostic message.
for error in group.secondary().filter(|error| error.vad_code().is_none()) { write!(f, "; related failure: {error}")?; }
Ok(())
}
}
}
}
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Context { source, .. } | Self::Diagnostic { source, .. } => Some(source.as_ref()),
Self::Multiple(group) => Some(group.primary()),
_ => None,
}
}
}