skillok meeting
1b62ba24a18c2b97ace8dd04ee8ccbcfafcb5b37
Перейти к файлу
Код
meeting cratesaudiosrcsource.rs
anc macOS: запись звука Zoom и микрофона - Звук Zoom — ScreenCaptureKit с фильтром по приложению us.zoom.xos, сразу 16 кГц моно; микрофон — CoreAudio через cpal. - Info.plist с объяснениями для запросов доступа (микрофон, системный звук), minimumSystemVersion 13.0, сборка .app. - Swift-мост ScreenCaptureKit при одних Command Line Tools: путь к библиотекам совместимости Swift и rpath /usr/lib/swift в build.rs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> 1b62ba2 30 сентября 2026 г.
source.rs
//! Источник звука: дорожка «я» (микрофон) или «собеседники» (звук Zoom).
//!
//! Источник отдаёт то, что накопилось с прошлого вызова, в формате
//! [`crate::pcm`]. Время звуку ставит тот, кто читает: конец блока — момент
//! чтения. Источник может молчать (захват процесса не шлёт ничего, пока Zoom
//! тих) — это не ошибка, дыру заполнит [`crate::chunker::Chunker`].

use std::time::{Duration, Instant};

use crate::pcm::SAMPLES_PER_MS;

#[derive(Debug, thiserror::Error)]
pub enum CaptureError {
    #[error("Звук Zoom недоступен: {0}")]
    Loopback(String),
    #[error("Микрофон недоступен: {0}")]
    Microphone(String),
    #[error("Запись встреч работает только в Windows и macOS")]
    Unsupported,
    #[error("Источник звука закончился")]
    Ended,
}

pub trait Capture: Send {
    /// Ждёт не дольше ~50 мс и отдаёт накопленные сэмплы (может — ни одного).
    fn read(&mut self) -> Result<Vec<i16>, CaptureError>;
}

/// Звук из памяти в реальном времени (со множителем скорости). Для тестов и
/// для проверки цепочки на машине без Windows.
pub struct Replay {
    samples: Vec<i16>,
    pos: usize,
    started: Option<Instant>,
    speed: f64,
}

impl Replay {
    pub fn new(samples: Vec<i16>, speed: f64) -> Self {
        Self {
            samples,
            pos: 0,
            started: None,
            speed,
        }
    }
}

impl Capture for Replay {
    fn read(&mut self) -> Result<Vec<i16>, CaptureError> {
        if self.pos >= self.samples.len() {
            return Err(CaptureError::Ended);
        }
        let started = *self.started.get_or_insert_with(Instant::now);
        std::thread::sleep(Duration::from_millis(20));
        let due = (started.elapsed().as_secs_f64() * 1000.0 * self.speed) as u64 * SAMPLES_PER_MS;
        let end = (due as usize).clamp(self.pos, self.samples.len());
        let out = self.samples[self.pos..end].to_vec();
        self.pos = end;
        Ok(out)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::pcm::ms_to_samples;

    #[test]
    fn replay_delivers_everything_then_ends() {
        let mut r = Replay::new(vec![1; ms_to_samples(300)], 10.0);
        let mut got = 0;
        loop {
            match r.read() {
                Ok(s) => got += s.len(),
                Err(CaptureError::Ended) => break,
                Err(e) => panic!("{e}"),
            }
        }
        assert_eq!(got, ms_to_samples(300));
    }
}