use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::thread::JoinHandle;
use std::time::{Duration, Instant};
use meet_audio::pcm::{self, WavWriter};
use meet_audio::source::{Capture, CaptureError};
use crate::transcript::Speaker;
pub struct Track {
pub speaker: Speaker,
pub capture: Box<dyn Capture>,
pub path: PathBuf,
}
#[derive(Debug, Clone, PartialEq, serde::Serialize)]
#[serde(tag = "kind", content = "data", rename_all = "snake_case")]
pub enum Event {
Level { speaker: Speaker, dbfs: f32 },
TrackFailed { speaker: Speaker, error: String },
}
pub type Emit = Arc<dyn Fn(Event) + Send + Sync>;
#[derive(Debug, Clone, PartialEq)]
pub struct Written {
pub speaker: Speaker,
pub duration_ms: u64,
}
pub struct Session {
stop: Arc<AtomicBool>,
paused: Arc<AtomicBool>,
threads: Vec<JoinHandle<Option<Written>>>,
pub started: Instant,
}
impl Session {
pub fn start(tracks: Vec<Track>, emit: Emit) -> Result<Self, String> {
let stop = Arc::new(AtomicBool::new(false));
let paused = Arc::new(AtomicBool::new(false));
let mut threads = Vec::new();
for track in tracks {
let writer =
WavWriter::create(&track.path).map_err(|e| format!("{}: {e}", track.path.display()))?;
let (stop, paused, emit) = (stop.clone(), paused.clone(), emit.clone());
threads.push(
std::thread::Builder::new()
.name(format!("meet-track-{}", track.speaker.track_file()))
.spawn(move || run_track(track, writer, stop, paused, emit))
.map_err(|e| e.to_string())?,
);
}
Ok(Self {
stop,
paused,
threads,
started: Instant::now(),
})
}
pub fn set_paused(&self, paused: bool) {
self.paused.store(paused, Ordering::Relaxed);
}
pub fn is_paused(&self) -> bool {
self.paused.load(Ordering::Relaxed)
}
pub fn stop(self) -> Vec<Written> {
self.stop.store(true, Ordering::Relaxed);
self.threads
.into_iter()
.filter_map(|t| t.join().ok().flatten())
.collect()
}
}
fn run_track(
mut track: Track,
mut writer: WavWriter,
stop: Arc<AtomicBool>,
paused: Arc<AtomicBool>,
emit: Emit,
) -> Option<Written> {
let speaker = track.speaker.clone();
let mut level_buf: Vec<i16> = Vec::new();
let mut last_level = Instant::now();
let mut last_flush = Instant::now();
while !stop.load(Ordering::Relaxed) {
let samples = match track.capture.read() {
Ok(s) => s,
Err(CaptureError::Ended) => break,
Err(e) => {
emit(Event::TrackFailed {
speaker: speaker.clone(),
error: e.to_string(),
});
break;
}
};
if samples.is_empty() || paused.load(Ordering::Relaxed) {
continue;
}
if writer.write(&samples).is_err() {
emit(Event::TrackFailed {
speaker: speaker.clone(),
error: "не удалось записать на диск".into(),
});
break;
}
if last_flush.elapsed() >= Duration::from_secs(5) {
let _ = writer.flush();
last_flush = Instant::now();
}
level_buf.extend_from_slice(&samples);
if last_level.elapsed() >= Duration::from_millis(200) {
emit(Event::Level {
speaker: speaker.clone(),
dbfs: pcm::level_dbfs(&level_buf),
});
level_buf.clear();
last_level = Instant::now();
}
}
let _ = writer.flush();
emit(Event::Level {
speaker: speaker.clone(),
dbfs: -90.0,
});
Some(Written {
speaker,
duration_ms: writer.duration_ms(),
})
}
#[cfg(test)]
mod tests {
use super::*;
use meet_audio::pcm::{decode_wav, ms_to_samples};
use meet_audio::source::Replay;
use std::sync::Mutex;
struct Broken;
impl Capture for Broken {
fn read(&mut self) -> Result<Vec<i16>, CaptureError> {
Err(CaptureError::Loopback("устройство пропало".into()))
}
}
#[test]
fn tracks_are_written_to_their_files() {
let dir = tempfile::tempdir().unwrap();
let events = Arc::new(Mutex::new(Vec::new()));
let sink = events.clone();
let me = dir.path().join("me.wav");
let session = Session::start(
vec![
Track {
speaker: Speaker::Me,
capture: Box::new(Replay::new(vec![500; ms_to_samples(2_000)], 40.0)),
path: me.clone(),
},
Track {
speaker: Speaker::Others,
capture: Box::new(Broken),
path: dir.path().join("others.wav"),
},
],
Arc::new(move |e| sink.lock().unwrap().push(e)),
)
.unwrap();
std::thread::sleep(Duration::from_millis(400));
let written = session.stop();
let me_written = written.iter().find(|w| w.speaker == Speaker::Me).unwrap();
assert_eq!(me_written.duration_ms, 2_000);
assert_eq!(
decode_wav(&std::fs::read(&me).unwrap()).unwrap().len(),
ms_to_samples(2_000)
);
let events = events.lock().unwrap();
assert!(events.iter().any(|e| matches!(
e,
Event::TrackFailed {
speaker: Speaker::Others,
..
}
)));
assert!(events.iter().any(|e| matches!(
e,
Event::Level {
speaker: Speaker::Me,
..
}
)));
}
#[test]
fn paused_audio_is_not_written() {
let dir = tempfile::tempdir().unwrap();
let session = Session::start(
vec![Track {
speaker: Speaker::Me,
capture: Box::new(Replay::new(vec![500; ms_to_samples(60_000)], 1.0)),
path: dir.path().join("me.wav"),
}],
Arc::new(|_| {}),
)
.unwrap();
session.set_paused(true);
std::thread::sleep(Duration::from_millis(300));
assert_eq!(session.stop()[0].duration_ms, 0, "на паузе ничего не записано");
}
}