skillok meeting
393887285dfc8accae94edba2c7c575b098e705e
Перейти к файлу
Код
meeting cratesaudiosrcdecode.rs
anc Ядро: встреча = заметка Markdown, всё по кнопке - Убрана автоматика: слежение за Zoom, окно согласия, трей, уведомления, автозапуск, живая расшифровка, разбор по JSON-схеме. - Встреча — note.md и дорожки звука; правка, дописывание, переименование, удаление. - «Добавить аудио»: чтение mp3, m4a, wav, aiff, flac, ogg (symphonia), потоковое приведение к 16 кГц моно. - «Расшифровать»: нарезка по паузам с обрезкой тишины в начале куска, до 4 кусков сразу, повтор, пометка нераспознанного, блок «Расшифровка». - Команды ИИ над заметкой: «Конспект», «Задачи», «Ревью», свой запрос; промпты файлами в prompts/. - «Записать» по кнопке: дорожки Zoom и микрофона в WAV, пауза, стоп. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> ad47f89 30 сентября 2026 г.
161 строка 6.4 КБ
decode.rs
//! Чтение аудиофайла любого распространённого формата (mp3, m4a/aac, wav, aiff,
//! flac, ogg, opus в mkv/webm не поддерживается) в наш формат 16 кГц моно.
//! Декодер — `symphonia`, чистый Rust: одинаково работает в Windows и на Mac.

use std::fs::File;
use std::path::Path;

use symphonia::core::audio::SampleBuffer;
use symphonia::core::codecs::{DecoderOptions, CODEC_TYPE_NULL};
use symphonia::core::errors::Error as SymphoniaError;
use symphonia::core::formats::FormatOptions;
use symphonia::core::io::MediaSourceStream;
use symphonia::core::meta::MetadataOptions;
use symphonia::core::probe::Hint;

use crate::pcm::Resampler;

#[derive(Debug, thiserror::Error)]
pub enum DecodeError {
    #[error("файл не открывается: {0}")]
    Io(#[from] std::io::Error),
    #[error("формат не поддерживается — подойдут mp3, m4a, wav, aiff, flac, ogg")]
    Unsupported,
    #[error("в файле нет звуковой дорожки")]
    NoTrack,
    #[error("файл повреждён: {0}")]
    Broken(String),
}

/// Весь звук файла в формате [`crate::pcm`].
pub fn decode_file(path: &Path) -> Result<Vec<i16>, DecodeError> {
    let file = File::open(path)?;
    let stream = MediaSourceStream::new(Box::new(file), Default::default());
    let mut hint = Hint::new();
    if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
        hint.with_extension(ext);
    }
    let probed = symphonia::default::get_probe()
        .format(
            &hint,
            stream,
            &FormatOptions::default(),
            &MetadataOptions::default(),
        )
        .map_err(|_| DecodeError::Unsupported)?;
    let mut format = probed.format;
    let track = format
        .tracks()
        .iter()
        .find(|t| t.codec_params.codec != CODEC_TYPE_NULL)
        .ok_or(DecodeError::NoTrack)?;
    let track_id = track.id;
    let mut decoder = symphonia::default::get_codecs()
        .make(&track.codec_params, &DecoderOptions::default())
        .map_err(|_| DecodeError::Unsupported)?;

    let mut out = Vec::new();
    let mut resampler: Option<Resampler> = None;
    let mut buffer: Option<SampleBuffer<f32>> = None;
    loop {
        let packet = match format.next_packet() {
            Ok(p) => p,
            Err(SymphoniaError::IoError(e)) if e.kind() == std::io::ErrorKind::UnexpectedEof => break,
            Err(SymphoniaError::ResetRequired) => break,
            Err(e) => return Err(DecodeError::Broken(e.to_string())),
        };
        if packet.track_id() != track_id {
            continue;
        }
        let decoded = match decoder.decode(&packet) {
            Ok(d) => d,
            // Битый пакет в середине — пропускаем, как делают плееры.
            Err(SymphoniaError::DecodeError(_)) => continue,
            Err(e) => return Err(DecodeError::Broken(e.to_string())),
        };
        let spec = *decoded.spec();
        let r = resampler.get_or_insert_with(|| Resampler::new(spec.channels.count(), spec.rate));
        let b = match buffer.as_mut() {
            Some(b) if b.capacity() >= decoded.capacity() * spec.channels.count() => b,
            _ => buffer.insert(SampleBuffer::new(decoded.capacity() as u64, spec)),
        };
        b.copy_interleaved_ref(decoded);
        out.extend(r.push(b.samples()));
    }
    Ok(out)
}

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

    #[test]
    fn reads_our_own_wav() {
        let dir = std::env::temp_dir().join(format!("meet-decode-{}", std::process::id()));
        std::fs::create_dir_all(&dir).unwrap();
        let path = dir.join("t.wav");
        let samples: Vec<i16> = (0..ms_to_samples(500))
            .map(|i| ((i % 64) as i16 - 32) * 300)
            .collect();
        std::fs::write(&path, encode_wav(&samples)).unwrap();
        let back = decode_file(&path).unwrap();
        assert_eq!(back.len(), samples.len());
        let worst = back
            .iter()
            .zip(&samples)
            .map(|(a, b)| (*a as i32 - *b as i32).abs())
            .max()
            .unwrap();
        assert!(worst <= 1, "{worst}");
        std::fs::remove_dir_all(dir).unwrap();
    }

    #[test]
    fn stereo_44k_wav_is_converted() {
        // WAV 44,1 кГц стерео, 16 бит — как у диктофона.
        let rate = 44_100u32;
        let frames = rate as usize / 2;
        let mut data = Vec::new();
        for i in 0..frames {
            let v = ((i as f32 / rate as f32 * std::f32::consts::TAU * 200.0).sin() * 10_000.0) as i16;
            data.extend_from_slice(&v.to_le_bytes());
            data.extend_from_slice(&v.to_le_bytes());
        }
        let mut wav = Vec::new();
        wav.extend_from_slice(b"RIFF");
        wav.extend_from_slice(&(36 + data.len() as u32).to_le_bytes());
        wav.extend_from_slice(b"WAVEfmt ");
        wav.extend_from_slice(&16u32.to_le_bytes());
        wav.extend_from_slice(&1u16.to_le_bytes());
        wav.extend_from_slice(&2u16.to_le_bytes());
        wav.extend_from_slice(&rate.to_le_bytes());
        wav.extend_from_slice(&(rate * 4).to_le_bytes());
        wav.extend_from_slice(&4u16.to_le_bytes());
        wav.extend_from_slice(&16u16.to_le_bytes());
        wav.extend_from_slice(b"data");
        wav.extend_from_slice(&(data.len() as u32).to_le_bytes());
        wav.extend_from_slice(&data);
        let dir = std::env::temp_dir().join(format!("meet-decode44-{}", std::process::id()));
        std::fs::create_dir_all(&dir).unwrap();
        let path = dir.join("rec.wav");
        std::fs::write(&path, wav).unwrap();
        let out = decode_file(&path).unwrap();
        assert!(
            (out.len() as i64 - 8_000).abs() <= 1,
            "полсекунды в 16 кГц: {}",
            out.len()
        );
        std::fs::remove_dir_all(dir).unwrap();
    }

    #[test]
    fn garbage_is_unsupported() {
        let dir = std::env::temp_dir().join(format!("meet-decode-bad-{}", std::process::id()));
        std::fs::create_dir_all(&dir).unwrap();
        let path = dir.join("notes.txt");
        std::fs::write(&path, "это не звук").unwrap();
        assert!(matches!(decode_file(&path), Err(DecodeError::Unsupported)));
        std::fs::remove_dir_all(dir).unwrap();
    }
}