koan-core 0.31.1

Core library for koan — bit-perfect music player. Audio engine, player, database, format strings.
Documentation
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
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
use std::mem;
use std::os::raw::c_void;
use std::ptr;
use std::sync::mpsc;
use std::time::Duration;

use core_foundation::base::TCFType;
use core_foundation::string::CFString;
use coreaudio_sys::*;
use thiserror::Error;

#[derive(Debug, Error)]
pub enum DeviceError {
    #[error("CoreAudio error: {0}")]
    OSStatus(i32),
    #[error("no output devices found")]
    NoDevices,
    #[error("device not found: {0}")]
    NotFound(AudioDeviceID),
    #[error("device not found by name: {0}")]
    NotFoundByName(String),
}

type Result<T> = std::result::Result<T, DeviceError>;

fn check(status: OSStatus) -> Result<()> {
    if status == 0 {
        Ok(())
    } else {
        Err(DeviceError::OSStatus(status))
    }
}

#[derive(Debug, Clone)]
pub struct AudioDevice {
    pub id: AudioDeviceID,
    pub name: String,
    pub sample_rates: Vec<f64>,
}

/// Get the default output device ID.
pub fn default_output_device() -> Result<AudioDeviceID> {
    let property = AudioObjectPropertyAddress {
        mSelector: kAudioHardwarePropertyDefaultOutputDevice,
        mScope: kAudioObjectPropertyScopeGlobal,
        mElement: kAudioObjectPropertyElementMain,
    };

    let mut device_id: AudioDeviceID = 0;
    let mut size = mem::size_of::<AudioDeviceID>() as u32;

    // SAFETY: `device_id` is a stack-allocated AudioDeviceID with correct size.
    // CoreAudio writes a single u32 device ID into the provided buffer.
    check(unsafe {
        AudioObjectGetPropertyData(
            kAudioObjectSystemObject,
            &property,
            0,
            ptr::null(),
            &mut size,
            &mut device_id as *mut _ as *mut _,
        )
    })?;

    if device_id == 0 {
        return Err(DeviceError::NoDevices);
    }

    Ok(device_id)
}

/// List all output devices.
pub fn list_output_devices() -> Result<Vec<AudioDevice>> {
    let property = AudioObjectPropertyAddress {
        mSelector: kAudioHardwarePropertyDevices,
        mScope: kAudioObjectPropertyScopeGlobal,
        mElement: kAudioObjectPropertyElementMain,
    };

    let mut size: u32 = 0;
    // SAFETY: Querying data size only — no output buffer, just writes to `size`.
    check(unsafe {
        AudioObjectGetPropertyDataSize(
            kAudioObjectSystemObject,
            &property,
            0,
            ptr::null(),
            &mut size,
        )
    })?;

    let device_count = size as usize / mem::size_of::<AudioDeviceID>();
    let mut device_ids = vec![0u32; device_count];

    // SAFETY: `device_ids` is pre-allocated to exactly the size returned by
    // GetPropertyDataSize. CoreAudio fills it with `device_count` AudioDeviceIDs.
    check(unsafe {
        AudioObjectGetPropertyData(
            kAudioObjectSystemObject,
            &property,
            0,
            ptr::null(),
            &mut size,
            device_ids.as_mut_ptr() as *mut _,
        )
    })?;

    let mut devices = Vec::new();
    for id in device_ids {
        if !has_output_streams(id) {
            continue;
        }
        let name = device_name(id).unwrap_or_else(|_| format!("Unknown ({})", id));
        let sample_rates = available_sample_rates(id).unwrap_or_default();
        devices.push(AudioDevice {
            id,
            name,
            sample_rates,
        });
    }

    Ok(devices)
}

/// Check if a device has output streams.
fn has_output_streams(device_id: AudioDeviceID) -> bool {
    let property = AudioObjectPropertyAddress {
        mSelector: kAudioDevicePropertyStreams,
        mScope: kAudioObjectPropertyScopeOutput,
        mElement: kAudioObjectPropertyElementMain,
    };

    let mut size: u32 = 0;
    // SAFETY: Querying data size only — no output buffer, just writes to `size`.
    let status =
        unsafe { AudioObjectGetPropertyDataSize(device_id, &property, 0, ptr::null(), &mut size) };

    status == 0 && size > 0
}

/// Get a device's name via CFString.
fn device_name(device_id: AudioDeviceID) -> Result<String> {
    let property = AudioObjectPropertyAddress {
        mSelector: kAudioObjectPropertyName,
        mScope: kAudioObjectPropertyScopeGlobal,
        mElement: kAudioObjectPropertyElementMain,
    };

    let mut name_ref: CFStringRef = ptr::null();
    let mut size = mem::size_of::<CFStringRef>() as u32;

    // SAFETY: Standard CoreAudio property query. `name_ref` is a correctly-sized
    // output buffer for a single CFStringRef. CoreAudio writes the pointer and
    // transfers ownership to the caller (Create Rule).
    check(unsafe {
        AudioObjectGetPropertyData(
            device_id,
            &property,
            0,
            ptr::null(),
            &mut size,
            &mut name_ref as *mut _ as *mut _,
        )
    })?;

    if name_ref.is_null() {
        return Ok(String::new());
    }

    // SAFETY: `name_ref` was returned by a CoreAudio Create Rule API — the caller
    // owns the reference. `wrap_under_create_rule` takes ownership and will
    // CFRelease on drop, so no manual release is needed. The pointer cast bridges
    // coreaudio-sys's CFStringRef and core-foundation's CFStringRef which are
    // identical C types from different bindgen runs.
    let cf_string: CFString = unsafe {
        CFString::wrap_under_create_rule(name_ref as core_foundation::string::CFStringRef)
    };
    Ok(cf_string.to_string())
}

/// Get available sample rates for a device.
pub fn available_sample_rates(device_id: AudioDeviceID) -> Result<Vec<f64>> {
    let property = AudioObjectPropertyAddress {
        mSelector: kAudioDevicePropertyAvailableNominalSampleRates,
        mScope: kAudioObjectPropertyScopeOutput,
        mElement: kAudioObjectPropertyElementMain,
    };

    let mut size: u32 = 0;
    // SAFETY: Querying data size only — no output buffer, just writes to `size`.
    check(unsafe {
        AudioObjectGetPropertyDataSize(device_id, &property, 0, ptr::null(), &mut size)
    })?;

    let count = size as usize / mem::size_of::<AudioValueRange>();
    let mut ranges = vec![
        AudioValueRange {
            mMinimum: 0.0,
            mMaximum: 0.0,
        };
        count
    ];

    // SAFETY: `ranges` is pre-allocated to exactly the size returned by
    // GetPropertyDataSize. CoreAudio fills it with `count` AudioValueRange structs.
    check(unsafe {
        AudioObjectGetPropertyData(
            device_id,
            &property,
            0,
            ptr::null(),
            &mut size,
            ranges.as_mut_ptr() as *mut _,
        )
    })?;

    let mut rates: Vec<f64> = ranges.iter().map(|r| r.mMaximum).collect();
    rates.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
    rates.dedup();

    Ok(rates)
}

/// Get the current nominal sample rate of a device.
pub fn get_device_sample_rate(device_id: AudioDeviceID) -> Result<f64> {
    let property = AudioObjectPropertyAddress {
        mSelector: kAudioDevicePropertyNominalSampleRate,
        mScope: kAudioObjectPropertyScopeGlobal,
        mElement: kAudioObjectPropertyElementMain,
    };

    let mut rate: f64 = 0.0;
    let mut size = mem::size_of::<f64>() as u32;

    // SAFETY: `rate` is a stack-allocated f64 with correct size for the property.
    check(unsafe {
        AudioObjectGetPropertyData(
            device_id,
            &property,
            0,
            ptr::null(),
            &mut size,
            &mut rate as *mut _ as *mut _,
        )
    })?;

    Ok(rate)
}

/// Find an output device by name. Returns the device ID if found.
pub fn find_output_device_by_name(name: &str) -> Result<AudioDeviceID> {
    let devices = list_output_devices()?;
    devices
        .iter()
        .find(|d| d.name == name)
        .map(|d| d.id)
        .ok_or_else(|| DeviceError::NotFoundByName(name.to_string()))
}

/// RAII guard that removes a CoreAudio property listener on drop.
/// Prevents listener registration leaks even on early returns or panics.
struct ListenerGuard {
    device_id: AudioDeviceID,
    property: AudioObjectPropertyAddress,
    callback: AudioObjectPropertyListenerProc,
    client_data: *mut c_void,
}

impl Drop for ListenerGuard {
    fn drop(&mut self) {
        // SAFETY: Removing the same listener+client_data pair that was registered.
        // Must happen exactly once — guaranteed by the Drop trait.
        unsafe {
            AudioObjectRemovePropertyListener(
                self.device_id,
                &self.property,
                self.callback,
                self.client_data,
            );
        }
    }
}

/// CoreAudio property listener callback. Fires on the HAL I/O thread when the
/// nominal sample rate changes. Zero work — just signals the waiting thread.
///
/// # Safety
/// `in_client_data` must point to a valid `mpsc::SyncSender<()>`. The pointer
/// is valid for the lifetime of the `ListenerGuard` in `set_device_sample_rate`.
unsafe extern "C" fn rate_change_callback(
    _in_object_id: AudioObjectID,
    _in_number_addresses: UInt32,
    _in_addresses: *const AudioObjectPropertyAddress,
    in_client_data: *mut c_void,
) -> OSStatus {
    // SAFETY: `in_client_data` points to a valid `SyncSender<()>` on the calling
    // thread's stack, kept alive by the `ListenerGuard` which removes this listener
    // before the sender is dropped.
    let tx = unsafe { &*(in_client_data as *const mpsc::SyncSender<()>) };
    // Ignore send errors — receiver may have already been dropped (timeout).
    let _ = tx.try_send(());
    0
}

/// Set the nominal sample rate of a device (for bit-perfect matching).
///
/// CoreAudio rate changes are asynchronous — the device needs time to physically
/// reclock. This function registers a property listener on
/// `kAudioDevicePropertyNominalSampleRate` and waits for the callback instead of
/// polling. Falls back on timeout (5s, covers slow USB Class 1 DACs doing PLL
/// relock). Returns the actual device rate after the attempt.
pub fn set_device_sample_rate(device_id: AudioDeviceID, rate: f64) -> Result<f64> {
    let property = AudioObjectPropertyAddress {
        mSelector: kAudioDevicePropertyNominalSampleRate,
        mScope: kAudioObjectPropertyScopeGlobal,
        mElement: kAudioObjectPropertyElementMain,
    };

    // Early-out: if the device already reports the target rate, skip the switch.
    let current = get_device_sample_rate(device_id)?;
    if (current - rate).abs() < 0.1 {
        return Ok(current);
    }

    // Set up a oneshot channel for the property listener callback.
    let (tx, rx) = mpsc::sync_channel::<()>(1);

    // SAFETY: `tx` lives on the stack for the duration of this function.
    // The ListenerGuard removes the listener before `tx` is dropped, so the
    // callback never sees a dangling pointer.
    let client_data = &tx as *const mpsc::SyncSender<()> as *mut c_void;

    check(unsafe {
        AudioObjectAddPropertyListener(
            device_id,
            &property,
            Some(rate_change_callback),
            client_data,
        )
    })?;

    // RAII: ensures RemovePropertyListener runs even on early return/panic.
    let _guard = ListenerGuard {
        device_id,
        property,
        callback: Some(rate_change_callback),
        client_data,
    };

    // Request the rate change.
    // SAFETY: `rate` is a stack-allocated f64 with correct size for the property.
    check(unsafe {
        AudioObjectSetPropertyData(
            device_id,
            &property,
            0,
            ptr::null(),
            mem::size_of::<f64>() as u32,
            &rate as *const _ as *const _,
        )
    })?;

    // Wait for the listener callback or timeout.
    // 5s covers USB Class 1 DACs that need PLL relock (3-5s typical).
    const TIMEOUT: Duration = Duration::from_secs(5);

    match rx.recv_timeout(TIMEOUT) {
        Ok(()) => {
            // Callback fired — verify the rate actually matches (spurious callbacks
            // can fire for reasons other than our change).
            let actual = get_device_sample_rate(device_id)?;
            if (actual - rate).abs() < 0.1 {
                return Ok(actual);
            }
            log::warn!(
                "sample rate listener fired but rate mismatch: requested {rate}Hz, device reports {actual}Hz"
            );
            Ok(actual)
        }
        Err(_) => {
            let actual = get_device_sample_rate(device_id)?;
            log::warn!(
                "device sample rate switch timed out after {TIMEOUT:?}: requested {rate}Hz, device reports {actual}Hz"
            );
            Ok(actual)
        }
    }
    // _guard drops here → AudioObjectRemovePropertyListener called before tx is dropped
}

/// Callback data for a live rate subscription. Boxed so the pointer handed to
/// CoreAudio stays put for the life of the watch.
struct RateSink {
    device_id: AudioDeviceID,
    on_change: Box<dyn Fn(f64) + Send + Sync>,
}

/// A live subscription to a device's nominal sample rate.
///
/// The device rate is shared: koan sets it, but Audio MIDI Setup, a vendor
/// control panel or any other client can move it back at any moment, and the
/// HAL then resamples koan to reach it. A rate read once at engine creation
/// goes stale the instant that happens — and the claim it feeds is the one
/// this player exists to make. Dropping the watch unsubscribes.
pub struct RateWatch {
    _guard: ListenerGuard,
    _sink: Box<RateSink>,
}

// SAFETY: the raw pointer in `_guard` addresses `_sink`'s heap allocation,
// which the watch owns. Fields drop in declaration order, so the listener is
// removed before the sink it points at. Nothing else observes either.
unsafe impl Send for RateWatch {}
unsafe impl Sync for RateWatch {}

/// Fires on the HAL notification thread whenever the nominal rate moves.
///
/// # Safety
/// `in_client_data` must point to a `RateSink` kept alive by the `RateWatch`
/// that registered this listener.
unsafe extern "C" fn rate_watch_callback(
    _in_object_id: AudioObjectID,
    _in_number_addresses: UInt32,
    _in_addresses: *const AudioObjectPropertyAddress,
    in_client_data: *mut c_void,
) -> OSStatus {
    // SAFETY: the registering `RateWatch` owns the sink and removes this
    // listener on drop, so the pointer is live for every call that can reach here.
    let sink = unsafe { &*(in_client_data as *const RateSink) };
    if let Ok(rate) = get_device_sample_rate(sink.device_id) {
        (sink.on_change)(rate);
    }
    0
}

/// Subscribe to nominal sample rate changes on a device.
pub fn watch_device_sample_rate(
    device_id: AudioDeviceID,
    on_change: Box<dyn Fn(f64) + Send + Sync>,
) -> Result<RateWatch> {
    let property = AudioObjectPropertyAddress {
        mSelector: kAudioDevicePropertyNominalSampleRate,
        mScope: kAudioObjectPropertyScopeGlobal,
        mElement: kAudioObjectPropertyElementMain,
    };

    let sink = Box::new(RateSink {
        device_id,
        on_change,
    });
    let client_data = &*sink as *const RateSink as *mut c_void;

    check(unsafe {
        AudioObjectAddPropertyListener(device_id, &property, Some(rate_watch_callback), client_data)
    })?;

    Ok(RateWatch {
        _guard: ListenerGuard {
            device_id,
            property,
            callback: Some(rate_watch_callback),
            client_data,
        },
        _sink: sink,
    })
}