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

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

use anyhow::{anyhow, Context};
use log::{error, info};
use uuid::Uuid;
use wasapi::initialize_mta;
use windows::Win32::Foundation::RPC_E_CHANGED_MODE;

use crate::common::audiofox::{Audiofox, AUDIOFOX};
use crate::common::callback::AudioCallbackFn;
use crate::common::result::Result;
use crate::common::source::{AudioSource, AudioSources};
use crate::platform::windows::list::list;
use crate::platform::windows::stream::{start_stream, Response};

#[derive(Debug)]
pub enum Command {
    Unlink,
}

/// Implementation of [`AudioManager`](crate::AudioManager) for WASAPI.
pub struct PlatformManager;

/// Implementation of [`LinkHandle`](crate::LinkHandle) for WASAPI.
pub struct PlatformHandle {
    sender: mpsc::Sender<Command>,
    handle: OnceCell<thread::JoinHandle<()>>,
}

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

    fn start() -> Result<Self> {
        // If Audiofox is running standalone, the MTA must first be initialized. If it is running
        // in an embedded manner (e.g., node), the thread mode may already be set. Thus, if the
        // HRESULT is RPC_E_CHANGED_MODE, we can ignore it; the COM APIs will work just fine.
        // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-erref/705fb797-2175-4a90-b5a3-3918024b10b8
        match initialize_mta() {
            RPC_E_CHANGED_MODE => {
                info!("thread mode already set, skipping MTA initialization");
            }
            result => {
                result.ok()?;
                info!("successfully initialized MTA");
            }
        }
        Ok(Self)
    }

    fn list(&self) -> Result<AudioSources> {
        list()
    }

    fn link<F>(&self, uuid: Uuid, sources: &[AudioSource], callback: F) -> Result<Self::LinkHandle>
    where
        F: AudioCallbackFn,
    {
        let callback = callback.into();
        let sources = sources.to_vec();

        // Establish channels
        let (sender, stream_receiver) = mpsc::channel::<Command>();
        let (stream_sender, receiver) = mpsc::channel::<Response>();

        // Spawn the stream thread
        let handle = OnceCell::from(
            thread::Builder::new()
                .name(format!("{AUDIOFOX}-{uuid}"))
                .spawn(move || start_stream(&stream_sender, &stream_receiver, &sources, &callback))
                .context("Failed to spawn stream thread")?
        );
        
        // Wait for the ready response
        receive!(receiver, Response::Ready => {
            info!("stream thread successfully started");
            PlatformHandle { sender, handle } }
        ).context("Failed to initialize stream thread")
    }
}

impl Drop for PlatformHandle {
    fn drop(&mut self) {
        if let Err(e) = || -> Result<()> {
            info!("manager stopped, cleaning up...");

            // Stop playback
            send!(self.sender, Command::Unlink).context("Stream already stopped")?;

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