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}
28
29pub trait AudioBackend: Send + Sync {
35 fn list_devices(&self) -> Result<Vec<DeviceInfo>, BackendError>;
37
38 fn default_device(&self) -> Result<DeviceInfo, BackendError>;
40
41 fn supported_sample_rates(&self, device: &DeviceInfo) -> Result<Vec<f64>, BackendError>;
43
44 fn get_device_sample_rate(&self, device: &DeviceInfo) -> Result<f64, BackendError>;
46
47 fn set_device_sample_rate(&self, device: &DeviceInfo, rate: f64) -> Result<f64, BackendError>;
51
52 fn watch_device_sample_rate(
60 &self,
61 _device: &DeviceInfo,
62 _on_change: Box<dyn Fn(f64) + Send + Sync>,
63 ) -> Option<Box<dyn SampleRateWatch>> {
64 None
65 }
66
67 fn create_engine(
70 &self,
71 device: &DeviceInfo,
72 sample_rate: f64,
73 channels: u32,
74 consumer: rtrb::Consumer<f32>,
75 samples_played: Arc<AtomicU64>,
76 ) -> Result<Box<dyn AudioEngineHandle>, BackendError>;
77}
78
79pub trait SampleRateWatch: Send + Sync {}
81
82pub trait AudioEngineHandle: Send {
88 fn start(&self) -> Result<(), BackendError>;
89 fn stop(&self) -> Result<(), BackendError>;
90 fn is_running(&self) -> bool;
91 fn fade_out(&self);
92 fn fade_in(&self) -> Result<(), BackendError>;
94 fn is_silent(&self) -> bool;
95 fn lead_in(&self, _frames: u64) {}
101}
102
103#[cfg(test)]
104mod tests {
105 use super::*;
106
107 #[test]
108 fn device_info_construction() {
109 let info = DeviceInfo {
110 name: "Test DAC".into(),
111 sample_rates: vec![44100.0, 48000.0, 96000.0],
112 platform_id: 42,
113 };
114 assert_eq!(info.name, "Test DAC");
115 assert_eq!(info.sample_rates.len(), 3);
116 assert_eq!(info.platform_id, 42);
117 }
118
119 #[test]
120 fn backend_error_formatting() {
121 let err = BackendError::NoDevices;
122 assert_eq!(err.to_string(), "no output devices found");
123
124 let err = BackendError::DeviceNotFound("Missing".into());
125 assert!(err.to_string().contains("Missing"));
126
127 let err = BackendError::UnsupportedSampleRate(192000.0);
128 assert!(err.to_string().contains("192000"));
129 }
130
131 #[test]
132 fn platform_backend_constructs() {
133 let _backend = super::super::platform_backend();
135 }
136
137 #[test]
138 fn platform_backend_lists_devices() {
139 let backend = super::super::platform_backend();
140 let result = backend.list_devices();
142 assert!(result.is_ok());
143 }
144
145 #[test]
146 fn platform_backend_has_default_device() {
147 let backend = super::super::platform_backend();
148 let _ = backend.default_device();
151 }
152
153 #[test]
154 fn engine_create_with_ring_buffer() {
155 let backend = super::super::platform_backend();
156 let device = match backend.default_device() {
157 Ok(d) => d,
158 Err(_) => return, };
160
161 let (producer, consumer) = rtrb::RingBuffer::new(4096);
162 let samples_played = Arc::new(AtomicU64::new(0));
163
164 let rate = device.sample_rates.first().copied().unwrap_or(44100.0);
165
166 let engine = backend.create_engine(&device, rate, 2, consumer, samples_played);
167 if let Ok(engine) = engine {
170 assert!(!engine.is_running());
171 drop(engine);
173 }
174 drop(producer); }
176}