skillok meeting
ade9583c661cc1280b544b11b9e57c1c48bba461
Перейти к файлу
Код
meeting cratesaudiosrcwin.rs
anc Запись всего звука, папка хранения, точнее распознавание - Звук собеседников — весь звук компьютера, кроме самого Skillok Meet: Zoom, Google Meet, Телемост, что угодно. Windows — process loopback в режиме исключения своего процесса, macOS — ScreenCaptureKit без фильтра по приложению. Поиск Zoom и зависимость windows убраны. - Если звук компьютера недоступен (на Mac — нет разрешения), запись идёт с микрофона, а интерфейс говорит почему. - Настройки: папка хранения записей (выбрать, открыть, вернуть по умолчанию; записи переносятся целиком), точность распознавания — по умолчанию полная whisper-large-v3. - «Открыть папку с файлами» у записи; микрофон на Mac просит у CoreAudio сразу 16 кГц. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> 96a28a9 30 сентября 2026 г.
win.rs
//! Windows: звук компьютера и микрофон.
//!
//! - Звук компьютера — process loopback в режиме исключения (Windows 10 20438+):
//!   всё, что выводят программы, кроме самого Skillok Meet. Так пишутся
//!   собеседники в Zoom, Google Meet, Телемосте — где угодно.
//!   https://learn.microsoft.com/en-us/samples/microsoft/windows-classic-samples/applicationloopbackaudio-sample/
//!   Обёртка — крейт `wasapi` (`AudioClient::new_application_loopback_client`,
//!   `include_tree = false` — «все, кроме этого дерева процессов»).
//!
//! COM-объекты WASAPI не переносятся между потоками, поэтому каждый захват
//! живёт в своём потоке, а наружу отдаёт сэмплы через канал.

use std::collections::VecDeque;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::mpsc::{self, Receiver, RecvTimeoutError};
use std::sync::Arc;
use std::thread::JoinHandle;
use std::time::Duration;

use wasapi::{
    initialize_mta, AudioClient, DeviceEnumerator, Direction, Role, SampleType, StreamMode, WaveFormat,
};

use crate::pcm::{self, SAMPLE_RATE};
use crate::source::{Capture, CaptureError};

// ── Захват ──────────────────────────────────────────────────────────────────

/// Поток захвата и канал сэмплов из него.
struct Worker {
    rx: Receiver<Result<Vec<i16>, String>>,
    stop: Arc<AtomicBool>,
    thread: Option<JoinHandle<()>>,
}

impl Worker {
    fn spawn(open: impl FnOnce() -> Result<AudioClient, String> + Send + 'static) -> Result<Self, String> {
        let (tx, rx) = mpsc::channel();
        let (ready_tx, ready_rx) = mpsc::channel();
        let stop = Arc::new(AtomicBool::new(false));
        let stop_flag = stop.clone();
        let thread = std::thread::Builder::new()
            .name("meet-capture".into())
            .spawn(move || {
                let _ = initialize_mta();
                let started = open().and_then(|mut client| start(&mut client).map(|fmt| (client, fmt)));
                let (client, format) = match started {
                    Ok(v) => {
                        let _ = ready_tx.send(Ok(()));
                        v
                    }
                    Err(e) => {
                        let _ = ready_tx.send(Err(e));
                        return;
                    }
                };
                if let Err(e) = pump(&client, format, &tx, &stop_flag) {
                    let _ = tx.send(Err(e));
                }
                let _ = client.stop_stream();
            })
            .map_err(|e| e.to_string())?;
        ready_rx
            .recv()
            .map_err(|_| "поток захвата не запустился".to_string())??;
        Ok(Self {
            rx,
            stop,
            thread: Some(thread),
        })
    }

    fn read(&mut self) -> Result<Vec<i16>, String> {
        let mut out = Vec::new();
        match self.rx.recv_timeout(Duration::from_millis(50)) {
            Ok(block) => out.extend(block?),
            Err(RecvTimeoutError::Timeout) => return Ok(out),
            Err(RecvTimeoutError::Disconnected) => return Err("поток захвата остановился".into()),
        }
        while let Ok(block) = self.rx.try_recv() {
            out.extend(block?);
        }
        Ok(out)
    }
}

impl Drop for Worker {
    fn drop(&mut self) {
        self.stop.store(true, Ordering::Relaxed);
        if let Some(t) = self.thread.take() {
            let _ = t.join();
        }
    }
}

/// Формат, который получился у клиента.
#[derive(Clone, Copy)]
enum Format {
    /// Наш формат: WASAPI сам привёл к 16 кГц моно.
    Native,
    /// Запасной: float, как попросили, с частотой и числом каналов.
    Float { rate: u32, channels: usize },
}

/// Просим 16 кГц моно 16 бит с автоконвертацией. Если устройство отказало —
/// float 48 кГц стерео и приводим сами.
fn start(client: &mut AudioClient) -> Result<Format, String> {
    let mode = StreamMode::EventsShared {
        autoconvert: true,
        buffer_duration_hns: 2_000_000,
    };
    let native = WaveFormat::new(16, 16, &SampleType::Int, SAMPLE_RATE as usize, 1, None);
    let format = match client.initialize_client(&native, &Direction::Capture, &mode) {
        Ok(()) => Format::Native,
        Err(first) => {
            let float = WaveFormat::new(32, 32, &SampleType::Float, 48_000, 2, None);
            client
                .initialize_client(&float, &Direction::Capture, &mode)
                .map_err(|second| format!("{first}; запасной формат: {second}"))?;
            Format::Float {
                rate: 48_000,
                channels: 2,
            }
        }
    };
    Ok(format)
}

fn pump(
    client: &AudioClient,
    format: Format,
    tx: &mpsc::Sender<Result<Vec<i16>, String>>,
    stop: &AtomicBool,
) -> Result<(), String> {
    let event = client.set_get_eventhandle().map_err(|e| e.to_string())?;
    let capture = client.get_audiocaptureclient().map_err(|e| e.to_string())?;
    client.start_stream().map_err(|e| e.to_string())?;
    let mut raw: VecDeque<u8> = VecDeque::new();
    while !stop.load(Ordering::Relaxed) {
        // Захват звука не сигналит, пока все молчат: таймаут — не ошибка.
        let _ = event.wait_for_event(100);
        loop {
            let frames = capture
                .get_next_packet_size()
                .map_err(|e| e.to_string())?
                .unwrap_or(0);
            if frames == 0 {
                break;
            }
            let before = raw.len();
            let info = capture
                .read_from_device_to_deque(&mut raw)
                .map_err(|e| e.to_string())?;
            if info.flags.silent {
                for b in raw.iter_mut().skip(before) {
                    *b = 0;
                }
            }
        }
        if raw.is_empty() {
            continue;
        }
        let bytes: Vec<u8> = raw.drain(..).collect();
        let samples = match format {
            Format::Native => bytes
                .as_chunks::<2>()
                .0
                .iter()
                .map(|c| i16::from_le_bytes(*c))
                .collect(),
            Format::Float { rate, channels } => {
                let floats: Vec<f32> = bytes
                    .as_chunks::<4>()
                    .0
                    .iter()
                    .map(|c| f32::from_le_bytes(*c))
                    .collect();
                pcm::to_native(&floats, channels, rate)
            }
        };
        if tx.send(Ok(samples)).is_err() {
            break;
        }
    }
    Ok(())
}

/// Весь звук компьютера, кроме нашего процесса.
pub struct SystemAudio(Worker);

impl SystemAudio {
    pub fn open() -> Result<Self, CaptureError> {
        let pid = std::process::id();
        Worker::spawn(move || {
            AudioClient::new_application_loopback_client(pid, false).map_err(|e| e.to_string())
        })
        .map(Self)
        .map_err(CaptureError::Loopback)
    }
}

impl Capture for SystemAudio {
    fn read(&mut self) -> Result<Vec<i16>, CaptureError> {
        self.0.read().map_err(CaptureError::Loopback)
    }
}

/// Микрофон по умолчанию для связи — тот же, что обычно выбран в программе встреч.
pub struct Microphone(Worker);

impl Microphone {
    pub fn open() -> Result<Self, CaptureError> {
        Worker::spawn(|| {
            let enumerator = DeviceEnumerator::new().map_err(|e| e.to_string())?;
            let device = enumerator
                .get_default_device_for_role(&Direction::Capture, &Role::Communications)
                .or_else(|_| enumerator.get_default_device(&Direction::Capture))
                .map_err(|e| e.to_string())?;
            device.get_iaudioclient().map_err(|e| e.to_string())
        })
        .map(Self)
        .map_err(CaptureError::Microphone)
    }
}

impl Capture for Microphone {
    fn read(&mut self) -> Result<Vec<i16>, CaptureError> {
        self.0.read().map_err(CaptureError::Microphone)
    }
}