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>
This commit is contained in:
1 parent
264a6239e1
commit
d12897a928
11 files changed
+7048
No files matched your search
@@ -0,0 +1,152 @@
|
||||
//! 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();
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user