use rodio::buffer::SamplesBuffer;
use rodio::{OutputStream, Sink};
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
use std::sync::mpsc::{Receiver, Sender};
use std::sync::Arc;
use std::time::Duration;
use symphonia::core::audio::{SampleBuffer, SignalSpec};
use symphonia::core::codecs::DecoderOptions;
use symphonia::core::errors::Error as SymphoniaError;
use symphonia::core::formats::FormatOptions;
use symphonia::core::io::{MediaSource, MediaSourceStream};
use symphonia::core::meta::MetadataOptions;
use symphonia::core::probe::Hint;
fn level_bits(chunk: &[f32]) -> u32 {
if chunk.is_empty() {
return 0f32.to_bits();
}
let sum: f32 = chunk.iter().map(|s| s * s).sum();
let rms = (sum / chunk.len() as f32).sqrt();
let normalized = if rms < 0.01 { 0.0 } else { rms.min(1.0) };
normalized.to_bits()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn level_is_zero_for_silence_and_rises_with_amplitude() {
assert_eq!(f32::from_bits(level_bits(&[0.0; 64])), 0.0);
assert_eq!(f32::from_bits(level_bits(&[0.005; 64])), 0.0);
let quiet = f32::from_bits(level_bits(&[0.1; 64]));
let loud = f32::from_bits(level_bits(&[0.8; 64]));
assert!(quiet > 0.0 && quiet < loud, "{quiet} !< {loud}");
assert!((f32::from_bits(level_bits(&[1.0; 64])) - 1.0).abs() < 1e-6);
}
#[test]
fn level_of_an_empty_chunk_is_silence() {
assert_eq!(f32::from_bits(level_bits(&[])), 0.0);
}
}
struct ChunkSource {
rx: Arc<std::sync::Mutex<Receiver<Vec<u8>>>>,
current: Vec<u8>,
pos: usize,
finished: bool,
}
impl ChunkSource {
fn new(rx: Receiver<Vec<u8>>) -> Self {
Self {
rx: Arc::new(std::sync::Mutex::new(rx)),
current: Vec::new(),
pos: 0,
finished: false,
}
}
}
impl std::io::Read for ChunkSource {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
loop {
if self.pos < self.current.len() {
let n = (self.current.len() - self.pos).min(buf.len());
buf[..n].copy_from_slice(&self.current[self.pos..self.pos + n]);
self.pos += n;
return Ok(n);
}
if self.finished {
return Ok(0);
}
let next = self
.rx
.lock()
.map_err(|_| std::io::Error::other("chunk channel poisoned"))?
.recv();
match next {
Ok(chunk) => {
self.current = chunk;
self.pos = 0;
}
Err(_) => {
self.finished = true;
return Ok(0);
}
}
}
}
}
impl std::io::Seek for ChunkSource {
fn seek(&mut self, _pos: std::io::SeekFrom) -> std::io::Result<u64> {
Err(std::io::Error::other(
"Edge TTS audio is a network stream and cannot be seeked",
))
}
}
impl MediaSource for ChunkSource {
fn is_seekable(&self) -> bool {
false
}
fn byte_len(&self) -> Option<u64> {
None
}
}
pub fn play_mp3_stream(
on_chunk: impl FnOnce(&mut dyn FnMut(&[u8]) -> Result<(), String>) -> Result<(), String>
+ Send
+ 'static,
stop: Arc<AtomicBool>,
level: &AtomicU32,
) -> Result<bool, String> {
let (tx, rx): (Sender<Vec<u8>>, Receiver<Vec<u8>>) = std::sync::mpsc::channel();
let producer_stop = stop.clone();
let produced = std::thread::spawn(move || -> Result<(), String> {
let result = on_chunk(&mut |chunk: &[u8]| {
if producer_stop.load(Ordering::Relaxed) {
return Err(CANCELLED.to_string());
}
tx.send(chunk.to_vec())
.map_err(|_| "playback consumer went away".to_string())
});
drop(tx);
result
});
let outcome = decode_and_play(rx, &stop, level);
let produce_result = produced
.join()
.map_err(|_| "TTS producer thread panicked".to_string())?;
match produce_result {
Err(e) if e == CANCELLED => {}
other => other?,
}
outcome
}
pub(crate) const CANCELLED: &str = "__rpi_voice_cancelled";
fn decode_and_play(
rx: Receiver<Vec<u8>>,
stop: &AtomicBool,
level: &AtomicU32,
) -> Result<bool, String> {
let (_stream, stream_handle) =
OutputStream::try_default().map_err(|e| format!("Audio output error: {e}"))?;
let sink = Sink::try_new(&stream_handle).map_err(|e| format!("Sink error: {e}"))?;
let source = ChunkSource::new(rx);
let mss = MediaSourceStream::new(Box::new(source), Default::default());
let probed = symphonia::default::get_probe()
.format(
&Hint::new(),
mss,
&FormatOptions::default(),
&MetadataOptions::default(),
)
.map_err(|e| format!("MP3 probe error: {e}"))?;
let mut format = probed.format;
let track = format
.default_track()
.ok_or_else(|| "MP3 stream has no track".to_string())?;
let track_id = track.id;
let params = track.codec_params.clone();
let mut decoder = symphonia::default::get_codecs()
.make(¶ms, &DecoderOptions::default())
.map_err(|e| format!("MP3 decoder error: {e}"))?;
let mut spec: Option<SignalSpec> = None;
let mut buffer: Option<SampleBuffer<f32>> = None;
let mut cancelled = false;
loop {
if stop.load(Ordering::Relaxed) {
cancelled = true;
break;
}
let packet = match format.next_packet() {
Ok(p) => p,
Err(SymphoniaError::IoError(_)) => break,
Err(SymphoniaError::ResetRequired) => break,
Err(e) => return Err(format!("MP3 read error: {e}")),
};
if packet.track_id() != track_id {
continue;
}
let decoded = match decoder.decode(&packet) {
Ok(d) => d,
Err(SymphoniaError::IoError(_)) => break,
Err(SymphoniaError::DecodeError(_)) => continue,
Err(e) => return Err(format!("MP3 decode error: {e}")),
};
let decoded_spec = *decoded.spec();
if spec != Some(decoded_spec) {
spec = Some(decoded_spec);
buffer = Some(SampleBuffer::<f32>::new(decoded.capacity() as u64, decoded_spec));
}
let Some(buf) = buffer.as_mut() else {
continue;
};
buf.copy_interleaved_ref(decoded);
let samples = buf.samples();
if samples.is_empty() {
continue;
}
let channels = decoded_spec.channels.count();
if channels == 0 {
continue;
}
level.store(level_bits(samples), Ordering::Relaxed);
let frames = samples.len() / channels;
sink.append(SamplesBuffer::new(
channels as u16,
decoded_spec.rate,
samples.to_vec(),
));
let ms = (frames as u64 * 1000) / decoded_spec.rate.max(1) as u64;
let mut slept = 0u64;
while slept < ms {
if stop.load(Ordering::Relaxed) {
cancelled = true;
break;
}
let step = (ms - slept).min(20);
std::thread::sleep(Duration::from_millis(step));
slept += step;
}
if cancelled {
break;
}
}
if cancelled || stop.load(Ordering::Relaxed) {
sink.stop();
level.store(0, Ordering::Relaxed);
return Ok(false);
}
while !sink.empty() {
if stop.load(Ordering::Relaxed) {
sink.stop();
level.store(0, Ordering::Relaxed);
return Ok(false);
}
std::thread::sleep(Duration::from_millis(20));
}
level.store(0, Ordering::Relaxed);
Ok(true)
}