skillok meeting
main
Перейти к файлу
Код
meeting cratesaudiosrcsource.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 г.
source.rs
//! Источник звука: дорожка «я» (микрофон) или «собеседники» (звук компьютера).
//!
//! Источник отдаёт то, что накопилось с прошлого вызова, в формате
//! [`crate::pcm`]. Время звуку ставит тот, кто читает: конец блока — момент
//! чтения. Источник может молчать (захват звука не шлёт ничего, пока
//! все молчат) — это не ошибка, дыру заполнит [`crate::chunker::Chunker`].

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

use crate::pcm::SAMPLES_PER_MS;

#[derive(Debug, thiserror::Error)]
pub enum CaptureError {
    #[error("Звук компьютера недоступен: {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));
    }
}