skillok meeting
778ade180d60c9f7692423132486bd257954c517
Перейти к файлу
Код
meeting cratesaudiosrcmac.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 г.
mac.rs
//! macOS: звук компьютера и микрофон.
//!
//! - Звук компьютера — ScreenCaptureKit: всё, что выводят программы, кроме самого
//!   Skillok Meet. Так пишутся собеседники в Zoom, Google Meet, Телемосте — где
//!   угодно. Нужно разрешение «Запись экрана и системного звука» (Системные
//!   настройки → Конфиденциальность и безопасность).
//!   https://developer.apple.com/documentation/screencapturekit/scstreamconfiguration/capturesaudio
//! - Микрофон — CoreAudio через `cpal`. Нужно разрешение «Микрофон».
//!
//! Потоки CoreAudio и ScreenCaptureKit живут в своих потоках системы; наружу
//! сэмплы идут через канал, как в Windows.

use std::sync::mpsc::{self, Receiver, RecvTimeoutError, Sender};
use std::time::Duration;

use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
use screencapturekit::cm::CMTime;
use screencapturekit::prelude::*;

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

fn drain(rx: &Receiver<Vec<i16>>) -> Result<Vec<i16>, ()> {
    let mut out = Vec::new();
    match rx.recv_timeout(Duration::from_millis(50)) {
        Ok(block) => out.extend(block),
        Err(RecvTimeoutError::Timeout) => return Ok(out),
        Err(RecvTimeoutError::Disconnected) => return Err(()),
    }
    while let Ok(block) = rx.try_recv() {
        out.extend(block);
    }
    Ok(out)
}

// ── Звук компьютера ─────────────────────────────────────────────────────────

struct AudioOut {
    tx: Sender<Vec<i16>>,
}

impl SCStreamOutputTrait for AudioOut {
    fn did_output_sample_buffer(&self, sample: CMSampleBuffer, of_type: SCStreamOutputType) {
        if !matches!(of_type, SCStreamOutputType::Audio) {
            return;
        }
        let Ok(list) = sample.audio_buffer_list() else {
            return;
        };
        // Поток настроен на 16 кГц моно float: один буфер, сэмплы f32 подряд.
        let Some(buffer) = list.get(0) else { return };
        let floats: Vec<f32> = buffer
            .data()
            .as_chunks::<4>()
            .0
            .iter()
            .map(|c| f32::from_le_bytes(*c))
            .collect();
        let _ = self.tx.send(pcm::f32_to_i16(&floats));
    }
}

/// Весь звук, который выводит компьютер, кроме самого Skillok Meet.
pub struct SystemAudio {
    stream: SCStream,
    rx: Receiver<Vec<i16>>,
}

impl SystemAudio {
    pub fn open() -> Result<Self, CaptureError> {
        let err = |e: &dyn std::fmt::Display| {
            CaptureError::Loopback(format!(
                "{e}. Разрешите запись звука: Системные настройки → Конфиденциальность и безопасность → \
                 Запись экрана и системного звука"
            ))
        };
        let content = SCShareableContent::get().map_err(|e| err(&e))?;
        let displays = content.displays();
        let display = displays.first().ok_or_else(|| err(&"нет экрана"))?;
        // Фильтр по экрану без исключений: звук берётся от всех программ;
        // свой звук исключает настройка потока ниже.
        let filter = SCContentFilter::create()
            .with_display(display)
            .with_excluding_windows(&[])
            .build()
            .map_err(|e| err(&e))?;
        // Видео не нужно, но поток без него не бывает: самый маленький кадр раз в секунду.
        let config = SCStreamConfiguration::new()
            .with_width(2)
            .with_height(2)
            .with_minimum_frame_interval(&CMTime::new(1, 1))
            .with_captures_audio(true)
            .with_excludes_current_process_audio(true)
            .with_sample_rate(SAMPLE_RATE as i32)
            .with_channel_count(1);
        let (tx, rx) = mpsc::channel();
        let mut stream = SCStream::new(&filter, &config).map_err(|e| err(&e))?;
        stream
            .add_output_handler(AudioOut { tx }, SCStreamOutputType::Audio)
            .map_err(|e| err(&e))?;
        stream.start_capture().map_err(|e| err(&e))?;
        Ok(Self { stream, rx })
    }
}

impl Capture for SystemAudio {
    fn read(&mut self) -> Result<Vec<i16>, CaptureError> {
        drain(&self.rx).map_err(|_| CaptureError::Loopback("поток звука компьютера остановился".into()))
    }
}

impl Drop for SystemAudio {
    fn drop(&mut self) {
        let _ = self.stream.stop_capture();
    }
}

// ── Микрофон ────────────────────────────────────────────────────────────────

/// Микрофон по умолчанию. Поток `cpal` нельзя передавать между потоками,
/// поэтому он живёт в своём потоке до сигнала остановки.
pub struct Microphone {
    rx: Receiver<Vec<i16>>,
    stop: Option<Sender<()>>,
    thread: Option<std::thread::JoinHandle<()>>,
}

impl Microphone {
    pub fn open() -> Result<Self, CaptureError> {
        let (tx, rx) = mpsc::channel::<Vec<i16>>();
        let (stop_tx, stop_rx) = mpsc::channel::<()>();
        let (ready_tx, ready_rx) = mpsc::channel::<Result<(), String>>();
        let thread = std::thread::Builder::new()
            .name("meet-mic".into())
            .spawn(move || {
                let started = (|| -> Result<cpal::Stream, String> {
                    let device = cpal::default_host()
                        .default_input_device()
                        .ok_or("микрофон не найден")?;
                    // Сначала просим 16 кГц у самой системы: CoreAudio приводит частоту
                    // качественнее простого усреднения. Не выйдет — берём родную частоту.
                    let native = device.supported_input_configs().ok().and_then(|mut all| {
                        all.find(|c| c.sample_format() == cpal::SampleFormat::F32)
                            .and_then(|c| c.try_with_sample_rate(SAMPLE_RATE))
                    });
                    let config = match native {
                        Some(c) => c,
                        None => device.default_input_config().map_err(|e| e.to_string())?,
                    };
                    let channels = config.channels() as usize;
                    let rate = config.sample_rate();
                    let mut resampler = Resampler::new(channels, rate);
                    let stream = match config.sample_format() {
                        cpal::SampleFormat::F32 => device.build_input_stream(
                            config.into(),
                            move |data: &[f32], _: &_| {
                                let _ = tx.send(resampler.push(data));
                            },
                            |e| eprintln!("микрофон: {e}"),
                            None,
                        ),
                        cpal::SampleFormat::I16 => device.build_input_stream(
                            config.into(),
                            move |data: &[i16], _: &_| {
                                let floats: Vec<f32> =
                                    data.iter().map(|&s| s as f32 / i16::MAX as f32).collect();
                                let _ = tx.send(resampler.push(&floats));
                            },
                            |e| eprintln!("микрофон: {e}"),
                            None,
                        ),
                        other => return Err(format!("формат {other:?} не поддерживается")),
                    }
                    .map_err(|e| e.to_string())?;
                    stream.play().map_err(|e| e.to_string())?;
                    Ok(stream)
                })();
                match started {
                    Ok(stream) => {
                        let _ = ready_tx.send(Ok(()));
                        let _ = stop_rx.recv();
                        drop(stream);
                    }
                    Err(e) => {
                        let _ = ready_tx.send(Err(e));
                    }
                }
            })
            .map_err(|e| CaptureError::Microphone(e.to_string()))?;
        ready_rx
            .recv()
            .map_err(|_| CaptureError::Microphone("поток микрофона не запустился".into()))?
            .map_err(|e| {
                CaptureError::Microphone(format!(
                    "{e}. Проверьте доступ: Системные настройки → Конфиденциальность и безопасность → Микрофон"
                ))
            })?;
        Ok(Self {
            rx,
            stop: Some(stop_tx),
            thread: Some(thread),
        })
    }
}

impl Capture for Microphone {
    fn read(&mut self) -> Result<Vec<i16>, CaptureError> {
        drain(&self.rx).map_err(|_| CaptureError::Microphone("поток микрофона остановился".into()))
    }
}

impl Drop for Microphone {
    fn drop(&mut self) {
        drop(self.stop.take());
        if let Some(t) = self.thread.take() {
            let _ = t.join();
        }
    }
}