Modell- und Stimmenauswahl, neue Klonstimme per Datei oder Mikrofon (automatisch aufbereitet, an Satzpause gekuerzt, per Whisper transkribiert), Texteingabe mit Pausen, Tempo, Satz-/Kommapausen mit Raumklang, Hintergrundmusik, Ziel Asterisk (WAV/G.722) oder WhatsApp (MP3), Vorhoeren und Speichern. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
153 lines
5.8 KiB
Rust
153 lines
5.8 KiB
Rust
//! 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<MixerDeviceSink>,
|
||
current: Option<rodio::Player>,
|
||
error: Option<String>,
|
||
}
|
||
|
||
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<String> {
|
||
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<AtomicBool>,
|
||
level: Arc<AtomicU32>,
|
||
samples: Arc<Mutex<Vec<f32>>>,
|
||
sample_rate: u32,
|
||
channels: u16,
|
||
started: Instant,
|
||
}
|
||
|
||
impl Recording {
|
||
pub fn start() -> Result<Self, String> {
|
||
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::<f32>() / 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<f32> = interleaved.chunks(ch).map(|c| c.iter().sum::<f32>() / 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();
|
||
}
|
||
}
|