use std::{
collections::HashMap,
fmt,
sync::{
atomic::{AtomicBool, Ordering},
mpsc, Arc,
},
thread,
};
use tracing::{info, span, Level};
use crate::songs::Song;
#[derive(Clone)]
pub struct Device {
name: String,
is_playing: Arc<AtomicBool>,
}
impl Device {
pub fn get(name: &str) -> Device {
Device {
name: name.to_string(),
is_playing: Arc::new(AtomicBool::new(false)),
}
}
#[cfg(test)]
pub fn is_playing(&self) -> bool {
self.is_playing.load(Ordering::Relaxed)
}
}
impl crate::audio::Device for Device {
fn play_from(
&self,
song: Arc<Song>,
_: &HashMap<String, Vec<u16>>,
sync: crate::playsync::PlaybackSync,
) -> Result<(), crate::audio::AudioError> {
let crate::playsync::PlaybackSync {
cancel_handle,
ready_tx,
clock,
start_time,
..
} = sync;
let span = span!(Level::INFO, "play song (mock)");
let _enter = span.enter();
let remaining_duration = song.duration().saturating_sub(start_time);
info!(
device = self.name,
song = song.name(),
duration = song.duration_string(),
start_time = format!("{:?}", start_time),
"Playing song."
);
let (sleep_tx, sleep_rx) = mpsc::channel::<()>();
self.is_playing.store(true, Ordering::Relaxed);
let finished = Arc::new(AtomicBool::new(false));
let join_handle = {
let cancel_handle = cancel_handle.clone();
let finished = finished.clone();
thread::spawn(move || {
let mut ready_tx = ready_tx;
ready_tx.send();
clock.wait_for_start_or_cancel(&cancel_handle);
if cancel_handle.is_cancelled() {
finished.store(true, Ordering::Relaxed);
cancel_handle.notify();
return;
}
let _ = sleep_rx.recv_timeout(remaining_duration);
finished.store(true, Ordering::Relaxed);
cancel_handle.notify();
})
};
cancel_handle.wait(finished);
self.is_playing.store(false, Ordering::Relaxed);
sleep_tx
.send(())
.map_err(|e| crate::audio::AudioError::Playback(e.to_string()))?;
let join_result = join_handle.join();
if join_result.is_err() {
return Err(crate::audio::AudioError::Playback(
"Error while joining thread!".to_string(),
));
}
Ok(())
}
#[cfg(test)]
fn to_mock(&self) -> Result<Arc<Device>, crate::audio::AudioError> {
Ok(Arc::new(self.clone()))
}
}
impl fmt::Display for Device {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} (Mock)", self.name,)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn display_includes_name_and_mock() {
let device = Device::get("TestDevice");
let display = format!("{}", device);
assert_eq!(display, "TestDevice (Mock)");
}
#[test]
fn get_creates_device_not_playing() {
let device = Device::get("test");
assert!(!device.is_playing());
}
#[test]
fn clone_shares_is_playing_state() {
let device = Device::get("test");
let cloned = device.clone();
assert!(!cloned.is_playing());
}
#[test]
fn play_from_zero_duration_completes() {
use crate::audio::Device as DeviceTrait;
use crate::clock::PlaybackClock;
use crate::playsync::{CancelHandle, PlaybackSync};
use crate::songs::Song;
use std::time::Duration;
let device = Device::get("mock-zero");
let song = Arc::new(Song::new_for_test("zero-song", &["t1"]));
let mappings = std::collections::HashMap::new();
let cancel_handle = CancelHandle::new();
let (ready_tx, ready_rx) = std::sync::mpsc::channel();
let clock = PlaybackClock::wall();
let device_clone = device.clone();
let cancel_clone = cancel_handle.clone();
let clock_clone = clock.clone();
let handle = thread::spawn(move || {
let _ = device_clone.play_from(
song,
&mappings,
PlaybackSync {
cancel_handle: cancel_clone,
ready_tx: crate::playsync::ReadyGuard::new(ready_tx),
clock: clock_clone,
start_time: Duration::from_millis(0),
loop_control: crate::playsync::LoopControl::new(),
},
);
});
ready_rx.recv().expect("ready signal");
clock.start();
handle.join().expect("thread should not panic");
assert!(
!device.is_playing(),
"device should not be playing after zero-duration song"
);
}
#[test]
fn play_from_with_start_time_offset() {
use crate::audio::Device as DeviceTrait;
use crate::clock::PlaybackClock;
use crate::playsync::{CancelHandle, PlaybackSync};
use crate::songs::Song;
use std::time::Duration;
let device = Device::get("mock-offset");
let song = Arc::new(Song::new_for_test("offset-song", &["t1"]));
let mappings = std::collections::HashMap::new();
let cancel_handle = CancelHandle::new();
let (ready_tx, ready_rx) = std::sync::mpsc::channel();
let clock = PlaybackClock::wall();
let device_clone = device.clone();
let cancel_clone = cancel_handle.clone();
let clock_clone = clock.clone();
let handle = thread::spawn(move || {
let _ = device_clone.play_from(
song,
&mappings,
PlaybackSync {
cancel_handle: cancel_clone,
ready_tx: crate::playsync::ReadyGuard::new(ready_tx),
clock: clock_clone,
start_time: Duration::from_secs(1), loop_control: crate::playsync::LoopControl::new(),
},
);
});
ready_rx.recv().expect("ready signal");
clock.start();
handle.join().expect("thread should not panic");
assert!(!device.is_playing());
}
#[test]
fn play_from_cancel_before_barrier() {
use crate::audio::Device as DeviceTrait;
use crate::clock::PlaybackClock;
use crate::playsync::{CancelHandle, PlaybackSync};
use crate::songs::Song;
use std::time::Duration;
let device = Device::get("mock-precancel");
let song = Arc::new(Song::new_for_test("song", &["t1"]));
let mappings = std::collections::HashMap::new();
let cancel_handle = CancelHandle::new();
let (ready_tx, ready_rx) = std::sync::mpsc::channel();
let clock = PlaybackClock::wall();
cancel_handle.cancel();
let device_clone = device.clone();
let cancel_clone = cancel_handle.clone();
let clock_clone = clock.clone();
let handle = thread::spawn(move || {
let _ = device_clone.play_from(
song,
&mappings,
PlaybackSync {
cancel_handle: cancel_clone,
ready_tx: crate::playsync::ReadyGuard::new(ready_tx),
clock: clock_clone,
start_time: Duration::from_millis(0),
loop_control: crate::playsync::LoopControl::new(),
},
);
});
ready_rx.recv().expect("ready signal");
cancel_handle.notify();
handle.join().expect("thread should not panic");
assert!(!device.is_playing());
}
}