audiofox 1.1.0

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 anyhow::Context;
use log::{debug, error};
use wasapi::{AudioClient, Direction, SampleType, StreamMode, WaveFormat};

use crate::common::result::Result;
use crate::common::source::AudioSource;
use crate::common::stream::{CHANNELS, SAMPLE_RATE};

pub const STORE_BITS: usize = 32; // 4 bytes per sample
pub const VALID_BITS: usize = 32; // no padding
pub const BLOCKALIGN: usize = CHANNELS * size_of::<f32>(); // 8
pub const SAMPLE_TYPE: SampleType = SampleType::Float; // F32LE

/// Per-link values used to initialize and maintain a link. A stream may consist of one or more
/// such links.
pub struct LinkState {
    pub audio_client: AudioClient,
    pub capture_client: wasapi::AudioCaptureClient,
    pub handle: wasapi::Handle,
    pub buffer: Box<[u8]>,
}

impl LinkState {
    /// Initialize capture on to a single process.
    pub fn link(source: &AudioSource, _frames: usize) -> Result<Self> {
        // Set the audio format
        let format = &WaveFormat::new(STORE_BITS, VALID_BITS, &SAMPLE_TYPE, SAMPLE_RATE, CHANNELS, None);

        // Initialize a new loopback client
        let mut audio_client = AudioClient::new_application_loopback_client(source.id[0], true)?;
        let mode = StreamMode::EventsShared {
            autoconvert: true,
            buffer_duration_hns: 0, // no-op in loopback mode
        };
        audio_client.initialize_client(format, &Direction::Capture, &mode)?;

        // Initialize the capture client
        let capture_client = audio_client.get_audiocaptureclient()
            .context("Failed to get audio capture client")?;

        // Initialize the event handle
        let handle = audio_client.set_get_eventhandle()
            .context("Failed to get audio event handle")?;

        // Allocate a buffer of the provided size
        let buffer_size = audio_client.get_buffer_size()
            .context("Failed to get buffer size")? as usize;

        // Allocate a buffer of this size to store the audio data
        let buffer = vec![0u8; buffer_size * BLOCKALIGN].into_boxed_slice();

        // Start the audio stream
        audio_client.start_stream()
            .context("Failed to start stream")?;

        Ok(Self { audio_client, capture_client, handle, buffer })
    }
}

impl Drop for LinkState {
    fn drop(&mut self) {
        if let Err(e) = self.audio_client.stop_stream() {
            error!("cleanup failed: {e}");
        }
    }
}