audiofox 1.0.0

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

// macOS uses platform-native types, so while it does not need the standard library, Audiofox still
// must link to a fully functional system to capture audio from it.
#![cfg_attr(target_os = "macos", no_std)]
cfg_if::cfg_if! {
    if #[cfg(target_os = "macos")] {
        use hashbrown::{HashMap, HashSet};
    } else {
        use std::collections::{HashMap, HashSet};
    }
}

extern crate alloc;
extern crate core;

#[macro_use]
mod common;
mod platform;

use alloc::borrow::Cow;
use core::cell::RefCell;

use anyhow::bail;
use log::{info, warn};
use uuid::Uuid;

use crate::common::audiofox::Audiofox;
use crate::platform::{PlatformHandle, PlatformManager};

pub use crate::common::chunk::Chunk;
pub use crate::common::result::{Error, Result};
pub use crate::common::source::{AudioSource, AudioSources, SourceId, SourceName};

/// Initialize and receive a new [`AudioManager`].
///
/// # Platforms
/// This command is infallible on macOS.
///
/// # Errors
/// Will return [`Error`] if startup fails. Should this occur, it can be expected that any subsequent
/// `AudioManager` will also fail to start.
pub fn start() -> Result<AudioManager> {
    Ok(AudioManager {
        manager: Audiofox::start()?,
        streams: RefCell::new(HashMap::new()),
    })
}

/// A cross-platform interface for listing and capturing audio sources.
///
/// An `AudioManager` can be used to perform three commands, which behave consistently across all
/// platforms: [`list()`](Self::list), [`link()`](Self::link), and [`stop()`](Self::stop).
///
/// Any number of `AudioManager` may exist at a given time, and each may call `list()` or `link()`
/// any number of times. However, creating more than one `AudioManager` is often unnecessary, and
/// more than one per thread is not recommended.
///
/// See the documentation for each command for more information.
#[must_use = "the manager must be stored to be kept alive"]
pub struct AudioManager {
    manager: PlatformManager,
    streams: RefCell<HashMap<Uuid, PlatformHandle>>,
}

impl AudioManager {
    /// Get a list of [`AudioSource`].
    ///
    /// These sources represent the current sources of audio that can be linked to by Audiofox to get
    /// a stream of audio data.
    ///
    /// # Platforms
    /// This command is infallible on Linux.
    ///
    /// # Errors
    /// Will return [`Error`] if one is encountered while building the list. This will not occur
    /// if there are simply no `AudioSource` to return.
    ///
    /// See also: [`link()`](Self::link) | [`stop()`](Self::stop)
    pub fn list(&self) -> Result<AudioSources> {
        self.manager.list()
    }

    /// Link to one or more [`AudioSource`]. If successful, start streaming audio.
    ///
    /// When all links are established, this method returns a [`LinkHandle`], bound to the lifetime
    /// of the `AudioManager`. So long as this handle exists, and there is audio to be captured,
    /// you will receive a continuous stream of [`Chunk`] containing the captured audio data.
    ///
    /// # Refresh behavior
    /// `link()` will automatically rerun [`list()`](Self::list) to refresh the [`SourceId`] of the
    /// given sources. Thus, linking will succeed on any app that is closed and reopened before
    /// `link()` is called, unless its [`SourceName`] has changed, or if it no longer represents a
    /// valid source of audio. See [`AudioSource`] for the definition of such a source.
    ///
    /// # Reconnect behavior
    /// On macOS and Linux, should the target of an active stream be closed (or otherwise deregistered
    /// with the system's audio API), the stream will automatically reconnect to it as soon as it becomes
    /// available again. Investigating support for this feature on Windows is an inaugural release priority.
    ///
    /// # Errors
    /// Will return [`Error`] if one is encountered during the linking process. Once streaming, the state
    /// of the stream can be monitored by its logs.
    ///
    /// See also: [`list()`](Self::list) | [`stop()`](Self::stop)
    pub fn link<F>(&self, sources: &[AudioSource], callback: F) -> Result<LinkHandle<'_>>
    where
        F: Fn(Chunk) + Send + Sync + 'static,
    {
        if sources.is_empty() {
            bail!("No sources provided.");
        }

        // Get the most recent source information
        let new = self.list()?;

        if new.is_empty() {
            bail!("All provided sources are no longer valid.")
        }

        // If the target sources are stale, refresh the ID fields of all extant sources
        let targets: Cow<[AudioSource]> = if *sources == *new {
            Cow::Borrowed(sources)
        } else {
            let old: HashSet<&SourceName> = sources.iter().map(|node| &node.name).collect();
            Cow::Owned(new
                .iter()
                .filter_map(|new| {
                    if old.contains(&new.name) {
                        Some(new.clone())
                    } else {
                        warn!("{new} is no longer valid, skipping...");
                        None
                    }
                })
                .collect())
        };

        if targets.is_empty() {
            bail!("All provided sources are no longer valid.")
        }

        // Link to the new sources
        let uuid = Uuid::new_v4();
        let handle = self.manager.link(uuid, &targets, callback)?;
        self.streams.borrow_mut().insert(uuid, handle);
        Ok(LinkHandle { uuid, manager: self })
    }

    /// Drop the `AudioManager`, cleaning up all active links and manager resources.
    ///
    /// See also: [`list()`](Self::list) | [`link()`](Self::link)
    pub fn stop(self) {
        drop(self);
    }
}

impl Drop for AudioManager {
    fn drop(&mut self) {
        info!("manager stopped, cleaning up...");
    }
}

/// A handle to manage the lifetime of a link, received by an [`AudioManager`].
///
/// The associated stream will end once this handle is dropped. You may also call
/// [`unlink()`](Self::unlink) to explicitly drop it.
#[must_use = "the stream will end immediately if the handle is not stored"]
pub struct LinkHandle<'a> {
    manager: &'a AudioManager,
    uuid: Uuid,
}

impl LinkHandle<'_> {
    /// Stop the stream and clean up its resources.
    ///
    /// This behavior also occurs on drop.
    pub fn unlink(self) {
        drop(self);
    }
}

impl Drop for LinkHandle<'_> {
    fn drop(&mut self) {
        info!("handle dropped, ending stream...");
        self.manager.streams.borrow_mut().remove(&self.uuid);
    }
}