use std::io::{Read, Write};
use std::process::{Child, Command, Stdio};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use symphonia::core::audio::sample::Sample;
use symphonia::core::codecs::audio::AudioDecoderOptions;
use symphonia::core::formats::probe::Hint;
use symphonia::core::formats::{FormatOptions, TrackType};
use symphonia::core::io::{MediaSourceStream, ReadOnlySource};
use symphonia::core::meta::MetadataOptions;
pub const STATION: &str = "SomaFM ยท The Trip";
const STREAM: &str = "https://ice2.somafm.com/thetrip-128-mp3";
#[derive(Debug)]
pub struct Sound {
stop: Arc<AtomicBool>,
player: Arc<Mutex<Option<Child>>>,
}
impl Sound {
pub fn start() -> Option<Self> {
if cfg!(test) {
return None;
}
let sound = Self {
stop: Arc::new(AtomicBool::new(false)),
player: Arc::new(Mutex::new(None)),
};
let (stop, player) = (sound.stop.clone(), sound.player.clone());
std::thread::Builder::new()
.name("rave-sound".into())
.spawn(move || {
let _ = play(&stop, &player);
})
.ok()?;
Some(sound)
}
}
impl Drop for Sound {
fn drop(&mut self) {
self.stop.store(true, Ordering::Relaxed);
if let Some(mut child) = self.player.lock().ok().and_then(|mut p| p.take()) {
let _ = child.kill();
let _ = child.wait();
}
}
}
fn play(stop: &AtomicBool, player: &Mutex<Option<Child>>) -> Option<()> {
let agent: ureq::Agent = ureq::Agent::config_builder()
.timeout_connect(Some(std::time::Duration::from_secs(5)))
.user_agent(concat!("cctop/", env!("CARGO_PKG_VERSION")))
.build()
.into();
let body = agent.get(STREAM).call().ok()?.into_body().into_reader();
let source = ReadOnlySource::new(Shared(Mutex::new(body)));
let stream = MediaSourceStream::new(Box::new(source), Default::default());
let mut hint = Hint::new();
hint.with_extension("mp3");
let mut format = symphonia::default::get_probe()
.probe(
&hint,
stream,
FormatOptions::default(),
MetadataOptions::default(),
)
.ok()?;
let track = format.default_track(TrackType::Audio)?;
let track_id = track.id;
let mut decoder = symphonia::default::get_codecs()
.make_audio_decoder(
track.codec_params.as_ref()?.audio()?,
&AudioDecoderOptions::default(),
)
.ok()?;
let mut pipe = None;
let mut samples: Vec<i16> = Vec::new();
let mut bytes: Vec<u8> = Vec::new();
while !stop.load(Ordering::Relaxed) {
let packet = format.next_packet().ok()??;
if packet.track_id != track_id {
continue;
}
let Ok(audio) = decoder.decode(&packet) else {
continue;
};
if pipe.is_none() {
let spec = audio.spec();
let mut child = spawn_player(spec.rate(), spec.channels().count())?;
pipe = child.stdin.take();
let mut slot = player.lock().ok()?;
if stop.load(Ordering::Relaxed) {
let _ = child.kill();
let _ = child.wait();
return None;
}
*slot = Some(child);
}
samples.resize(audio.samples_interleaved(), i16::MID);
audio.copy_to_slice_interleaved(&mut samples);
bytes.clear();
bytes.extend(samples.iter().flat_map(|s| s.to_le_bytes()));
pipe.as_mut()?.write_all(&bytes).ok()?;
}
Some(())
}
fn spawn_player(rate: u32, channels: usize) -> Option<Child> {
let (rate, channels) = (rate.to_string(), channels.to_string());
let players: [(&str, Vec<String>); 2] = [
(
"pacat",
vec![
"--playback".into(),
"--raw".into(),
"--format=s16le".into(),
format!("--rate={rate}"),
format!("--channels={channels}"),
"--latency-msec=1500".into(),
"--client-name=cctop".into(),
format!("--stream-name={STATION}"),
],
),
(
"aplay",
vec![
"-q".into(),
"-t".into(),
"raw".into(),
"-f".into(),
"S16_LE".into(),
"-r".into(),
rate,
"-c".into(),
channels,
"--buffer-time=1500000".into(),
],
),
];
players.iter().find_map(|(name, args)| {
Command::new(name)
.args(args)
.stdin(Stdio::piped())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.ok()
})
}
struct Shared<R>(Mutex<R>);
impl<R: Read> Read for Shared<R> {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
match self.0.get_mut() {
Ok(inner) => inner.read(buf),
Err(_) => Err(std::io::Error::other("poisoned")),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn no_music_under_test() {
assert!(Sound::start().is_none());
}
}