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 alloc::string::{String, ToString};
use alloc::vec;
use alloc::vec::Vec;
use core::mem::size_of;
use core::ptr::addr_of_mut;

use anyhow::Context;
use log::warn;
use objc2_app_kit::{NSApplicationActivationPolicy, NSWorkspace};
use objc2_core_audio::{
    AudioObjectID, kAudioHardwarePropertyProcessObjectList, kAudioObjectSystemObject,
    kAudioProcessPropertyPID,
};

use crate::common::result::Result;
use crate::common::source::{AudioSource, AudioSources};
use crate::platform::mac::wrapper::{get_prop_data, get_prop_size};

/// List applications capable of streaming audio from.
pub fn list() -> Result<AudioSources> {
    let mut sources = vec![];
    for object_id in list_audio_objects()? {
        match get_pid(object_id) {
            Ok(id) => {
                if let Some(name) = get_friendly_name(id) {
                    sources.push(AudioSource { id: id.into(), name: name.into() });
                }
            }
            Err(e) => warn!("failed to get PID from audio source: {e:?}"),
        }
    }
    Ok(sources.into())
}

/// List all audio process objects by their [`AudioObjectID`].
fn list_audio_objects() -> Result<Vec<AudioObjectID>> {
    // Get the system object's size
    let system_object = kAudioObjectSystemObject as AudioObjectID;
    let query = query!(kAudioHardwarePropertyProcessObjectList);
    let data_size = get_prop_size(system_object, &query)
        .context("Failed to get system object property size")?;

    // Calculate the number of audio objects contained in the system object
    let num_objects = data_size / size_of::<AudioObjectID>() as u32;

    // If none, return an empty vec
    if num_objects == 0 {
        return Ok(vec![]);
    }

    // Else, populate a vec with their AudioObjectIDs
    let mut object_ids = vec![0; num_objects as usize];
    get_prop_data(
        system_object,
        &query,
        None,
        data_size,
        object_ids.as_mut_ptr(),
    ).context("Failed to get system object property data")?;

    Ok(object_ids)
}

/// Get a PID from an [`AudioObjectID`].
fn get_pid(object_id: AudioObjectID) -> Result<u32> {
    let mut pid = 0;
    get_prop_data(
        object_id,
        &query!(kAudioProcessPropertyPID),
        None,
        size_of::<AudioObjectID>() as u32,
        addr_of_mut!(pid),
    ).context("Failed to get PID from AudioObjectID")?;

    Ok(pid)
}

/// Get a PID's friendly name if it represents an application that appears in the Dock.
fn get_friendly_name(pid: u32) -> Option<String> {
    let workspace = NSWorkspace::sharedWorkspace();
    let apps = workspace.runningApplications();
    for app in &apps {
        // Filter out system daemons via activation policy
        if app.activationPolicy() == NSApplicationActivationPolicy::Regular
            && app.processIdentifier() == pid.cast_signed() {
            return Some(app.localizedName()?.to_string());
        }
    }
    None
}