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

use crate::common::builder::ChunkBuilder;
use crate::common::result::Result;
use crate::common::source::AudioSource;

pub const SAMPLE_RATE: usize = 48000; // 48kHz
pub const STORE_BITS: usize = 32; // 4 bytes per sample
pub const VALID_BITS: usize = 32; // no padding
pub const CHANNELS: usize = 2; // 2 channels
pub const SAMPLE_TYPE: SampleType = SampleType::Float; // F32LE
pub const BLOCKALIGN: usize = CHANNELS * size_of::<f32>(); // 8
pub const PACKET_SAMPLES: usize = 480; // samples per WASAPI audio packet
pub const PACKET_FRAMES: usize = PACKET_SAMPLES * 2; // frames per WASAPI packet
pub const PACKET_BYTES: usize = BLOCKALIGN * PACKET_SAMPLES; // 3840
pub const TIMEOUT_MS: u32 = 1000; // handle timeout duration

/// 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 event_handle: wasapi::Handle,
    pub buffer: ChunkBuilder<u8, PACKET_BYTES>,
}

impl LinkState {
    /// Initialize capture on to a single process.
    pub fn link(source: &AudioSource) -> 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,
        };
        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 event_handle = audio_client.set_get_eventhandle()
            .context("Failed to get audio event handle")?;

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

        Ok(Self { audio_client, capture_client, event_handle, buffer: ChunkBuilder::default() })
    }
}

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