Files
tts/tts-studio/src/audio.rs
T
Renato PfeilerandClaude Opus 5.5 d12897a928 TTS Studio: Rust-GUI fuer Ansagen mit Qwen3-TTS
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>
2026-10-23 15:52:47 +02:00

153 lines
5.8 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 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();
}
}