audiofox 1.0.3

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

use std::cell::OnceCell;
use std::sync::mpsc;
use std::thread;

use anyhow::{Context, anyhow};
use log::{error, info};
use uuid::Uuid;

use crate::common::audiofox::Audiofox;
use crate::common::callback::{AudioCallback, AudioCallbackFn};
use crate::common::result::Result;
use crate::common::source::{AudioSource, AudioSources};
use crate::platform::linux::command::Response;
use crate::platform::linux::pipewire::pw_thread;

/// Valid commands to send to the PipeWire loop.
#[derive(Debug)]
pub enum Command {
    Stop,
    List,
    Init(Uuid, AudioCallback),
    Link(Uuid, AudioSources),
    End(Uuid),
}

/// Implementation of [`AudioManager`](crate::AudioManager) for PipeWire.
pub struct PlatformManager {
    sender: pw::channel::Sender<Command>,
    receiver: mpsc::Receiver<Response>,
    handle: OnceCell<thread::JoinHandle<()>>,
}

/// Implementation of [`LinkHandle`](crate::LinkHandle) for PipeWire.
pub struct PlatformHandle {
    sender: pw::channel::Sender<Command>,
    uuid: Uuid,
}

/// Implementation of [`Audiofox`] for PipeWire.
impl Audiofox for PlatformManager {
    type LinkHandle = PlatformHandle;

    fn start() -> Result<Self> {
        pw::init();

        // Initialize the three main objects
        let (pw_sender, receiver) = mpsc::channel::<Response>();
        let (sender, pw_receiver) = pw::channel::channel();
        let handle = thread::spawn(move || pw_thread(&pw_sender, pw_receiver)).into();

        // Wait for the ready response
        receive!(receiver, Response::Ready => {
            info!("pipewire thread successfully started");
            Self { sender, receiver, handle } }
        ).context("Failed to initialize PipeWire thread")
    }

    fn list(&self) -> Result<AudioSources> {
        // Forward to the pipewire thread
        roundtrip!(self.sender, self.receiver,
            Command::List,
            Response::List(sources) => sources
        ).context("List command failed")
    }

    fn link<F>(&self, uuid: Uuid, sources: &[AudioSource], callback: F) -> Result<Self::LinkHandle>
    where
        F: AudioCallbackFn,
    {
        // Set the UUID for the handle
        info!("initializing stream node...");
        roundtrip!(self.sender, self.receiver,
            Command::Init(uuid, callback.into()),
            Response::Init => ()
        ).context("Failed to initialize stream")?;

        // Create a new handle with the UUID
        info!("stream node initialized, linking targets...");
        roundtrip!(self.sender, self.receiver,
            Command::Link(uuid, sources.into()),
            Response::Link => PlatformHandle { sender: self.sender.clone(), uuid }
        ).context("Failed to create LinkHandle")
    }
}

impl Drop for PlatformManager {
    fn drop(&mut self) {
        if let Err(e) = || -> Result<()> {
            // Quit the pipewire loop
            send!(self.sender, Command::Stop).context("PipeWire thread already stopped")?;

            // Join the pipewire thread
            self.handle
                .take().context("PipeWire thread handle already taken")?
                .join().map_err(|_| anyhow!("Failed to join Pipewire thread"))
        }() {
            // If shutdown fails, report the error
            error!("cleanup failed: {e}");
        }
    }
}

impl Drop for PlatformHandle {
    fn drop(&mut self) {
        // This is a safe no-op if the manager has already shut down
        send_logged!(self.sender, Command::End(self.uuid));
    }
}