audiofox 1.0.1

Cross platform audio capture made easy!
// SPDX-License-Identifier: GPL-2.0-or-later WITH Classpath-exception-2.0
// Audiofox, the audio capturer!
// Copyright (c) 2026 foxberries

use core::array;
use core::ffi::{CStr, c_void};
use core::mem::MaybeUninit;
use core::ptr::{NonNull, null};

use anyhow::Context;
use objc2_core_audio::{
    AudioHardwareCreateAggregateDevice, AudioObjectGetPropertyData, AudioObjectGetPropertyDataSize,
    AudioObjectID, AudioObjectPropertyAddress,
};
use objc2_core_foundation::{
    CFArray, CFBoolean, CFDictionary, CFNumber, CFRetained, CFString,
    kCFTypeDictionaryKeyCallBacks, kCFTypeDictionaryValueCallBacks,
};

use crate::common::result::Result;

/// Wrapper for [`AudioObjectGetPropertyDataSize`].
pub fn get_prop_size(
    audio_object: AudioObjectID,
    address: &AudioObjectPropertyAddress,
) -> Result<u32> {
    let mut data_size = 0;

    // SAFETY: All provided pointers are valid.
    check_status!(unsafe {
        AudioObjectGetPropertyDataSize(
            audio_object,
            NonNull::from(address),
            0,      // Can be 0
            null(), // Can be null
            NonNull::from(&mut data_size),
        )
    })?;

    Ok(data_size)
}

/// Wrapper for [`AudioObjectGetPropertyData`].
pub fn get_prop_data<T>(
    audio_object: AudioObjectID,
    property: &AudioObjectPropertyAddress,
    qualifier: Option<(u32, *const c_void)>,
    mut data_size: u32,
    data: *mut T,
) -> Result<()> {
    // Use safe defaults if qualifier is None
    let (qualifier_size, qualifier_data) = match qualifier {
        Some((size, qual_data)) => (size, qual_data),
        None => (0, null()),
    };

    // SAFETY: All provided pointers are valid.
    check_status!(unsafe {
        AudioObjectGetPropertyData(
            audio_object,
            NonNull::from(property),
            qualifier_size,
            qualifier_data,
            NonNull::from(&mut data_size),
            NonNull::new(data.cast::<c_void>()).unwrap(),
        )
    })
}

/// Wrapper for [`AudioHardwareCreateAggregateDevice`].
pub fn create_aggregate(dict: &CFDictionary) -> Result<AudioObjectID> {
    let mut aggregate_id: AudioObjectID = 0;

    check_status!(unsafe {
        AudioHardwareCreateAggregateDevice(dict, NonNull::from(&mut aggregate_id))
    })?;

    Ok(aggregate_id)
}

/// Builder pattern for [`CFDictionary`].
pub struct DictBuilder<const N: usize> {
    keys: [MaybeUninit<CFRetained<CFString>>; N],
    vals: [MaybeUninit<DictValue>; N],
    count: usize
}

impl<const N: usize> DictBuilder<N> {
    /// Create a new `DictBuilder` of capacity `N`.
    #[must_use]
    pub const fn new() -> Self {
        Self {
            keys: [const { MaybeUninit::uninit() }; N],
            vals: [const { MaybeUninit::uninit() }; N],
            count: 0,
        }
    }

    /// Add a key-value pair to the `DictBuilder` with automatic type conversion.
    pub fn insert(mut self, key: &'static CStr, value: impl Into<DictValue>) -> Self {
        self.keys[self.count] = MaybeUninit::new(CFString::from_static_str(
            key.to_str().expect("Key should never be invalid UTF-8")
        ));
        self.vals[self.count] = MaybeUninit::new(value.into());
        self.count += 1;
        self
    }

    /// Finalize the `DictBuilder` and receive a [`CFDictionary`].
    pub fn build(self) -> Result<CFRetained<CFDictionary>> {
        // SAFETY: build() will only be called after insert()
        let mut key_ptrs: [*const c_void; N] = unsafe { *self.keys.as_ptr().cast() };
        let mut val_ptrs: [*const c_void; N] = array::from_fn(|i| {
            unsafe { self.vals[i].assume_init_ref().as_ptr() }
        });

        // SAFETY: All provided pointers are valid.
        unsafe {
            CFDictionary::new(
                None,
                key_ptrs.as_mut_ptr(),
                val_ptrs.as_mut_ptr(),
                N.cast_signed(),
                &raw const kCFTypeDictionaryKeyCallBacks,
                &raw const kCFTypeDictionaryValueCallBacks,
            )
        }.context("Failed to create dictionary")
    }
}

/// Types supported by [`DictBuilder`].
pub enum DictValue {
    String(CFRetained<CFString>),
    Boolean(CFRetained<CFBoolean>),
    Array(CFRetained<CFArray<CFDictionary>>),
    Integer(CFRetained<CFNumber>),
}

impl DictValue {
    /// Convert a `DictValue` to a raw pointer.
    fn as_ptr(&self) -> *const c_void {
        match self {
            Self::String(val) => Self::ptr(val),
            Self::Boolean(val) => Self::ptr(val),
            Self::Array(val) => Self::ptr(val),
            Self::Integer(val) => Self::ptr(val),
        }
    }

    /// Convert a CoreFoundation value to a raw pointer.
    fn ptr<T: objc2_core_foundation::Type>(val: &CFRetained<T>) -> *const c_void {
        CFRetained::as_ptr(val).as_ptr().cast::<c_void>()
    }
}

impl From<bool> for DictValue {
    fn from(b: bool) -> Self {
        Self::Boolean(CFRetained::from(CFBoolean::new(b)))
    }
}

impl From<CFRetained<CFDictionary>> for DictValue {
    fn from(dict: CFRetained<CFDictionary>) -> Self {
        Self::Array(CFArray::from_retained_objects(&[dict]))
    }
}

impl From<&str> for DictValue {
    fn from(s: &str) -> Self {
        Self::String(CFString::from_str(s))
    }
}

impl From<u32> for DictValue {
    fn from(t: u32) -> Self {
        Self::Integer(CFNumber::new_i64(i64::from(t)))
    }
}