//! Vorhoeren (Wiedergabe) und Mikrofonaufnahme ueber rodio. use crate::media; use rodio::microphone::MicrophoneBuilder; use rodio::{Decoder, DeviceSinkBuilder, MixerDeviceSink}; use std::path::Path; use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; use std::sync::{Arc, Mutex, mpsc}; use std::time::{Duration, Instant}; #[derive(Default)] pub struct Player { // beide muessen leben, solange etwas spielt sink: Option, current: Option, error: Option, } impl Player { pub fn play(&mut self, path: &Path) { self.stop(); if let Err(e) = self.try_play(path) { self.error = Some(format!("Wiedergabe nicht möglich: {e}")); } } fn try_play(&mut self, path: &Path) -> Result<(), String> { if self.sink.is_none() { let mut sink = DeviceSinkBuilder::open_default_sink().map_err(|e| e.to_string())?; sink.log_on_drop(false); self.sink = Some(sink); } let sink = self.sink.as_ref().expect("sink"); let file = std::fs::File::open(path).map_err(|e| format!("{}: {e}", path.display()))?; let player = rodio::Player::connect_new(sink.mixer()); player.append(Decoder::try_from(file).map_err(|e| e.to_string())?); self.current = Some(player); Ok(()) } pub fn stop(&mut self) { if let Some(p) = self.current.take() { p.stop(); } } pub fn is_playing(&self) -> bool { self.current.as_ref().is_some_and(|p| !p.empty()) } pub fn take_error(&mut self) -> Option { self.error.take() } } /// Laufende Mikrofonaufnahme. Das Mikrofon puffert nur ~100 ms, deshalb liest ein /// eigener Thread laufend mit und sammelt die Samples hier ein. pub struct Recording { stop: Arc, level: Arc, samples: Arc>>, sample_rate: u32, channels: u16, started: Instant, } impl Recording { pub fn start() -> Result { let stop = Arc::new(AtomicBool::new(false)); let level = Arc::new(AtomicU32::new(0)); let samples = Arc::new(Mutex::new(Vec::new())); let (tx, rx) = mpsc::channel(); let (stop_t, level_t, samples_t) = (stop.clone(), level.clone(), samples.clone()); std::thread::spawn(move || { let opened = MicrophoneBuilder::new() .default_device() .and_then(|b| b.default_config()) .map_err(|e| e.to_string()) .and_then(|b| b.open_stream().map_err(|e| e.to_string())) .map_err(|e| format!("Mikrofon nicht verfügbar: {e}")); let mut mic = match opened { Ok(m) => m, Err(e) => { let _ = tx.send(Err(e)); return; } }; let cfg = *mic.config(); let (sr, ch) = (cfg.sample_rate.get(), cfg.channel_count.get()); let _ = tx.send(Ok((sr, ch))); let block = (sr as usize * ch as usize / 20).max(1); // 50 ms let mut buf = Vec::with_capacity(block); // next() blockiert, bis Samples da sind; Stopp wird beim naechsten Sample bemerkt while !stop_t.load(Ordering::Relaxed) { let Some(s) = mic.next() else { break }; buf.push(s); if buf.len() >= block { let rms = (buf.iter().map(|s| s * s).sum::() / buf.len() as f32).sqrt(); level_t.store(rms.to_bits(), Ordering::Relaxed); samples_t.lock().expect("samples").append(&mut buf); } } samples_t.lock().expect("samples").append(&mut buf); }); let (sample_rate, channels) = rx.recv_timeout(Duration::from_secs(5)).map_err(|_| "Mikrofon antwortet nicht.".to_string())??; Ok(Recording { stop, level, samples, sample_rate, channels, started: Instant::now() }) } pub fn elapsed(&self) -> f32 { self.started.elapsed().as_secs_f32() } /// Pegel fuer die Anzeige, 0..1 ueber -60..0 dBFS pub fn level(&self) -> f32 { let rms = f32::from_bits(self.level.load(Ordering::Relaxed)); ((20.0 * (rms + 1e-9).log10() + 60.0) / 60.0).clamp(0.0, 1.0) } /// Aufnahme beenden und als Mono-WAV speichern. pub fn stop(self, file: &Path) -> Result<(), String> { self.stop.store(true, Ordering::Relaxed); std::thread::sleep(Duration::from_millis(100)); // letzten Block einsammeln lassen let interleaved = std::mem::take(&mut *self.samples.lock().map_err(|e| e.to_string())?); let ch = self.channels.max(1) as usize; let mono: Vec = interleaved.chunks(ch).map(|c| c.iter().sum::() / ch as f32).collect(); if media::seconds(mono.len(), self.sample_rate) < 1.0 { return Err("Aufnahme ist leer oder zu kurz – Mikrofon prüfen.".into()); } media::write_wav(file, &mono, self.sample_rate) } } #[cfg(test)] mod tests { use super::*; /// Echte Hardware: cargo test -- --ignored audio #[test] #[ignore] fn audio_geraete() { let mut sink = DeviceSinkBuilder::open_default_sink().expect("Ausgabegerät"); sink.log_on_drop(false); let rec = Recording::start().expect("Mikrofon"); std::thread::sleep(Duration::from_millis(1500)); println!("Mikrofon: {} Hz, {} Kanal/Kanäle, Pegel {:.2}", rec.sample_rate, rec.channels, rec.level()); let file = std::env::temp_dir().join("tts-studio-mic-test.wav"); rec.stop(&file).expect("Aufnahme"); let (s, sr) = media::read_wav_bytes(&std::fs::read(&file).unwrap()).unwrap(); println!("aufgenommen: {:.2} s", media::seconds(s.len(), sr)); std::fs::remove_file(&file).unwrap(); } }