koan_core/audio/
backend.rs1use std::sync::Arc;
2use std::sync::atomic::AtomicU64;
3
4use thiserror::Error;
5
6#[derive(Debug, Error)]
7pub enum BackendError {
8 #[error("no output devices found")]
9 NoDevices,
10 #[error("device not found: {0}")]
11 DeviceNotFound(String),
12 #[error("unsupported sample rate: {0}")]
13 UnsupportedSampleRate(f64),
14 #[error("platform error: {0}")]
15 Platform(String),
16 #[error("stream creation failed: {0}")]
17 StreamCreation(String),
18}
19
20#[derive(Debug, Clone)]
22pub struct DeviceInfo {
23 pub name: String,
24 pub sample_rates: Vec<f64>,
25 pub platform_id: u64,
27 pub kind: OutputKind,
28}
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
33pub enum OutputKind {
34 BuiltIn,
35 Usb,
36 Bluetooth,
37 AirPlay,
38 Display,
40 Virtual,
41 #[default]
42 Other,
43}
44
45impl OutputKind {
46 pub fn as_str(self) -> &'static str {
47 match self {
48 Self::BuiltIn => "builtin",
49 Self::Usb => "usb",
50 Self::Bluetooth => "bluetooth",
51 Self::AirPlay => "airplay",
52 Self::Display => "display",
53 Self::Virtual => "virtual",
54 Self::Other => "other",
55 }
56 }
57}
58
59pub trait AudioBackend: Send + Sync {
65 fn list_devices(&self) -> Result<Vec<DeviceInfo>, BackendError>;
67
68 fn default_device(&self) -> Result<DeviceInfo, BackendError>;
70
71 fn supported_sample_rates(&self, device: &DeviceInfo) -> Result<Vec<f64>, BackendError>;
73
74 fn get_device_sample_rate(&self, device: &DeviceInfo) -> Result<f64, BackendError>;
76
77 fn set_device_sample_rate(&self, device: &DeviceInfo, rate: f64) -> Result<f64, BackendError>;
81
82 fn watch_device_sample_rate(
90 &self,
91 _device: &DeviceInfo,
92 _on_change: Box<dyn Fn(f64) + Send + Sync>,
93 ) -> Option<Box<dyn SampleRateWatch>> {
94 None
95 }
96
97 fn create_engine(
100 &self,
101 device: &DeviceInfo,
102 sample_rate: f64,
103 channels: u32,
104 consumer: rtrb::Consumer<f32>,
105 samples_played: Arc<AtomicU64>,
106 ) -> Result<Box<dyn AudioEngineHandle>, BackendError>;
107}
108
109pub trait SampleRateWatch: Send + Sync {}
111
112pub trait AudioEngineHandle: Send {
118 fn start(&self) -> Result<(), BackendError>;
119 fn stop(&self) -> Result<(), BackendError>;
120 fn is_running(&self) -> bool;
121 fn fade_out(&self);
122 fn set_sleep_gain(&self, _gain: f32, _snap: bool) {}
125 fn fade_in(&self) -> Result<(), BackendError>;
127 fn fade_out_quickly(&self) {
130 self.fade_out()
131 }
132 fn fade_in_quickly(&self) -> Result<(), BackendError> {
133 self.fade_in()
134 }
135 fn period(&self) -> std::time::Duration {
138 std::time::Duration::ZERO
139 }
140 fn is_silent(&self) -> bool;
141 fn lead_in(&self, _frames: u64) {}
147}
148
149#[cfg(test)]
150mod tests {
151 use super::*;
152
153 #[test]
154 fn device_info_construction() {
155 let info = DeviceInfo {
156 name: "Test DAC".into(),
157 sample_rates: vec![44100.0, 48000.0, 96000.0],
158 platform_id: 42,
159 kind: Default::default(),
160 };
161 assert_eq!(info.name, "Test DAC");
162 assert_eq!(info.sample_rates.len(), 3);
163 assert_eq!(info.platform_id, 42);
164 }
165
166 #[test]
167 fn backend_error_formatting() {
168 let err = BackendError::NoDevices;
169 assert_eq!(err.to_string(), "no output devices found");
170
171 let err = BackendError::DeviceNotFound("Missing".into());
172 assert!(err.to_string().contains("Missing"));
173
174 let err = BackendError::UnsupportedSampleRate(192000.0);
175 assert!(err.to_string().contains("192000"));
176 }
177
178 #[test]
179 fn platform_backend_constructs() {
180 let _backend = super::super::platform_backend();
182 }
183
184 #[test]
185 fn platform_backend_lists_devices() {
186 let backend = super::super::platform_backend();
187 let result = backend.list_devices();
189 assert!(result.is_ok());
190 }
191
192 #[test]
193 fn platform_backend_has_default_device() {
194 let backend = super::super::platform_backend();
195 let _ = backend.default_device();
198 }
199
200 #[test]
201 fn engine_create_with_ring_buffer() {
202 let backend = super::super::platform_backend();
203 let device = match backend.default_device() {
204 Ok(d) => d,
205 Err(_) => return, };
207
208 let (producer, consumer) = rtrb::RingBuffer::new(4096);
209 let samples_played = Arc::new(AtomicU64::new(0));
210
211 let rate = device.sample_rates.first().copied().unwrap_or(44100.0);
212
213 let engine = backend.create_engine(&device, rate, 2, consumer, samples_played);
214 if let Ok(engine) = engine {
217 assert!(!engine.is_running());
218 drop(engine);
220 }
221 drop(producer); }
223}