#![allow(clippy::module_name_repetitions)]
#[cfg(feature = "mock_playback")]
use std::sync::atomic::AtomicBool;
use std::{
fs::File,
io::BufReader,
ops::Range,
sync::{
Arc,
mpsc::{Receiver, Sender},
},
time::Duration,
};
use log::{debug, error};
use rodio::{
Source,
decoder::DecoderBuilder,
source::{EmptyCallback, SeekError},
};
use tracing::instrument;
use crate::{
errors::LibraryError,
format_duration,
state::{Percent, SeekType, StateAudio, StateRuntime, Status},
udp::StateChange,
};
use mecomp_storage::db::schemas::song::SongBrief;
use one_or_many::OneOrMany;
pub mod commands;
pub mod queue;
use commands::{AudioCommand, QueueCommand, VolumeCommand};
use queue::Queue;
const MIN_VOLUME: f32 = 0.0;
const MAX_VOLUME: f32 = 10.0;
#[derive(Debug, Clone)]
pub struct AudioKernelSender {
tx: Sender<(AudioCommand, tracing::Span)>,
}
impl AudioKernelSender {
#[must_use]
#[inline]
pub fn start(event_tx: Sender<StateChange>) -> Arc<Self> {
let (command_tx, command_rx) = std::sync::mpsc::channel();
let tx_clone = command_tx.clone();
std::thread::Builder::new()
.name(String::from("Audio Kernel"))
.spawn(move || {
let kernel = AudioKernel::new(tx_clone, event_tx);
kernel.init(command_rx);
})
.unwrap();
Arc::new(Self::new(command_tx))
}
#[must_use]
#[inline]
pub(crate) const fn new(tx: Sender<(AudioCommand, tracing::Span)>) -> Self {
Self { tx }
}
#[instrument(skip(self))]
#[inline]
pub fn send(&self, command: AudioCommand) {
let ctx =
tracing::info_span!("Sending Audio Command to Kernel", command = ?command).or_current();
if let Err(e) = self.tx.send((command, ctx)) {
error!("Failed to send command to audio kernel: {e}");
panic!("Failed to send command to audio kernel: {e}");
}
}
#[instrument(skip(self))]
#[inline]
pub fn try_send(
&self,
command: AudioCommand,
) -> Result<(), std::sync::mpsc::SendError<(AudioCommand, tracing::Span)>> {
let ctx =
tracing::info_span!("Sending Audio Command to Kernel", command = ?command).or_current();
self.tx.send((command, ctx))
}
}
impl Drop for AudioKernelSender {
#[allow(clippy::missing_inline_in_public_items)]
fn drop(&mut self) {
let _ = self.try_send(AudioCommand::Exit);
}
}
pub(crate) struct AudioKernel {
#[cfg(not(feature = "mock_playback"))]
_music_output: rodio::OutputStream,
#[cfg(feature = "mock_playback")]
queue_rx_stop: Arc<AtomicBool>,
player: rodio::Sink,
queue: Queue,
volume: f32,
muted: bool,
status: Status,
command_tx: Sender<(AudioCommand, tracing::Span)>,
event_tx: Sender<StateChange>,
}
#[cfg(feature = "mock_playback")]
impl Drop for AudioKernel {
fn drop(&mut self) {
self.queue_rx_stop
.store(true, std::sync::atomic::Ordering::Relaxed);
}
}
impl AudioKernel {
#[must_use]
#[cfg(not(feature = "mock_playback"))]
pub fn new(
command_tx: Sender<(AudioCommand, tracing::Span)>,
event_tx: Sender<StateChange>,
) -> Self {
let stream = rodio::OutputStreamBuilder::open_default_stream().unwrap();
let sink = rodio::Sink::connect_new(stream.mixer());
sink.pause();
Self {
_music_output: stream,
player: sink,
queue: Queue::new(),
volume: 1.0,
muted: false,
status: Status::Stopped,
command_tx,
event_tx,
}
}
#[must_use]
#[cfg(feature = "mock_playback")]
pub fn new(
command_tx: Sender<(AudioCommand, tracing::Span)>,
event_tx: Sender<StateChange>,
) -> Self {
const QUEUE_POLLING_INTERVAL: Duration = Duration::from_micros(22);
let (sink, mut queue_rx) = rodio::Sink::new();
let queue_stop = Arc::new(AtomicBool::new(false));
let queue_stop_clone = queue_stop.clone();
std::thread::spawn(move || {
loop {
if queue_stop_clone.load(std::sync::atomic::Ordering::Relaxed) {
break;
}
queue_rx.next();
std::thread::sleep(QUEUE_POLLING_INTERVAL);
}
});
sink.pause();
Self {
player: sink,
queue_rx_stop: queue_stop,
queue: Queue::new(),
volume: 1.0,
muted: false,
status: Status::Stopped,
command_tx,
event_tx,
}
}
pub fn init(mut self, command_rx: Receiver<(AudioCommand, tracing::Span)>) {
for (command, ctx) in command_rx {
let _guard = ctx.enter();
let prev_status = self.status;
match command {
AudioCommand::Play => self.play(),
AudioCommand::Pause => self.pause(),
AudioCommand::TogglePlayback => self.toggle_playback(),
AudioCommand::RestartSong => {
self.restart_song();
let _ = self
.event_tx
.send(StateChange::Seeked(Duration::from_secs(0)));
}
AudioCommand::ClearPlayer => self.clear_player(),
AudioCommand::Queue(command) => self.queue_control(command),
AudioCommand::Exit => break,
AudioCommand::ReportStatus(tx) => {
let state = self.state();
if let Err(e) = tx.send(state) {
error!(
"Audio Kernel failed to send state to the receiver, state receiver likely has been dropped. State: {e}"
);
break;
}
}
AudioCommand::Volume(command) => self.volume_control(command),
AudioCommand::Seek(seek, duration) => {
self.seek(seek, duration);
let _ = self
.event_tx
.send(StateChange::Seeked(self.get_time_played()));
}
AudioCommand::Stop if prev_status != Status::Stopped => {
self.stop();
let _ = self
.event_tx
.send(StateChange::Seeked(Duration::from_secs(0)));
}
AudioCommand::Stop => {}
}
let new_status = self.status;
if prev_status != new_status {
let _ = self.event_tx.send(StateChange::StatusChanged(new_status));
}
}
}
#[instrument(skip(self))]
fn play(&mut self) {
if self.player.empty() {
return;
}
self.player.play();
self.status = Status::Playing;
}
#[instrument(skip(self))]
fn pause(&mut self) {
self.player.pause();
self.status = Status::Paused;
}
#[instrument(skip(self))]
fn stop(&mut self) {
self.player.pause();
self.seek(SeekType::Absolute, Duration::from_secs(0));
self.status = Status::Stopped;
}
#[instrument(skip(self))]
fn toggle_playback(&mut self) {
if self.player.is_paused() {
self.play();
} else {
self.pause();
}
}
#[instrument(skip(self))]
fn restart_song(&mut self) {
let status = self.status;
self.clear_player();
if let Some(song) = self.queue.current_song() {
if let Err(e) = self.append_song_to_player(song) {
error!("Failed to append song to player: {e}");
}
match status {
Status::Stopped => {}
Status::Paused => self.pause(),
Status::Playing => self.play(),
}
}
}
#[instrument(skip(self))]
fn clear(&mut self) {
self.clear_player();
self.queue.clear();
}
#[instrument(skip(self))]
fn clear_player(&mut self) {
self.player.clear();
self.status = Status::Stopped;
}
#[instrument(skip(self))]
fn queue_control(&mut self, command: QueueCommand) {
let prev_song = self.queue.current_song().cloned();
let prev_len = self.queue.len();
match command {
QueueCommand::Clear => self.clear(),
QueueCommand::PlayNextSong => self.start_next_song(),
QueueCommand::SkipForward(n) => self.skip_forward(n),
QueueCommand::SkipBackward(n) => self.skip_backward(n),
QueueCommand::SetPosition(n) => self.set_position(n),
QueueCommand::Shuffle => {
self.queue.shuffle();
let _ = self.event_tx.send(StateChange::QueueChanged);
}
QueueCommand::AddToQueue(song_box) => match song_box {
OneOrMany::None => {}
OneOrMany::One(song) => self.add_song_to_queue(*song),
OneOrMany::Many(songs) => self.add_songs_to_queue(songs),
},
QueueCommand::RemoveRange(range) => self.remove_range_from_queue(range),
QueueCommand::SetRepeatMode(mode) => {
self.queue.set_repeat_mode(mode);
let _ = self.event_tx.send(StateChange::RepeatModeChanged(mode));
}
}
let new_song = self.queue.current_song().cloned();
let new_len = self.queue.len();
if prev_len != new_len {
let _ = self.event_tx.send(StateChange::QueueChanged);
}
if prev_song != new_song {
let _ = self
.event_tx
.send(StateChange::TrackChanged(new_song.map(|s| s.id.into())));
}
}
#[instrument(skip(self))]
fn state(&self) -> StateAudio {
let queue_position = self.queue.current_index();
let current_song = self.queue.current_song().cloned();
let repeat_mode = self.queue.get_repeat_mode();
let runtime = current_song.as_ref().map(|song| {
let duration = song.runtime;
let seek_position = self.get_time_played();
let seek_percent =
Percent::new(seek_position.as_secs_f32() / duration.as_secs_f32() * 100.0);
StateRuntime {
seek_position,
seek_percent,
duration,
}
});
let status = self.status;
let status = if self.player.is_paused() {
debug_assert!(matches!(status, Status::Paused | Status::Stopped));
status
} else {
debug_assert_eq!(status, Status::Playing);
Status::Playing
};
let muted = self.muted;
let volume = self.volume;
let queued_songs = self.queue.queued_songs();
StateAudio {
queue: queued_songs,
queue_position,
current_song,
repeat_mode,
runtime,
status,
muted,
volume,
}
}
#[instrument(skip(self))]
fn start_next_song(&mut self) {
self.status = Status::Stopped;
self.player.pause();
let next_song = self.queue.next_song().cloned();
let repeat_mode = self.queue.get_repeat_mode();
let current_index = self.queue.current_index();
if let Some(song) = next_song {
if let Err(e) = self.append_song_to_player(&song) {
error!("Failed to append song to player: {e}");
}
if current_index.is_some() || repeat_mode.is_all() {
self.play();
}
}
}
#[instrument(skip(self))]
fn skip_forward(&mut self, n: usize) {
let status = self.status;
self.clear_player();
let next_song = self.queue.skip_forward(n).cloned();
if let Some(song) = next_song {
if let Err(e) = self.append_song_to_player(&song) {
error!("Failed to append song to player: {e}");
}
match status {
Status::Paused => self.pause(),
Status::Playing
if self.queue.get_repeat_mode().is_all()
|| self.queue.current_index().is_some() =>
{
self.play();
}
_ => {}
}
}
}
#[instrument(skip(self))]
fn skip_backward(&mut self, n: usize) {
let status = self.status;
self.clear_player();
let next_song = self.queue.skip_backward(n).cloned();
if let Some(song) = next_song {
if let Err(e) = self.append_song_to_player(&song) {
error!("Failed to append song to player: {e}");
}
match status {
Status::Stopped => {}
Status::Paused => self.pause(),
Status::Playing => self.play(),
}
}
}
#[instrument(skip(self))]
fn set_position(&mut self, n: usize) {
let status = self.status;
self.clear_player();
self.queue.set_current_index(n);
let next_song = self.queue.current_song().cloned();
if let Some(song) = next_song {
if let Err(e) = self.append_song_to_player(&song) {
error!("Failed to append song to player: {e}");
}
match status {
Status::Stopped => {}
Status::Paused => self.pause(),
Status::Playing => self.play(),
}
}
}
#[instrument(skip(self))]
fn add_song_to_queue(&mut self, song: SongBrief) {
self.queue.add_song(song);
if self.player.empty() {
let current_song = self.get_current_song();
if let Some(song) = current_song.or_else(|| self.get_next_song()) {
if let Err(e) = self.append_song_to_player(&song) {
error!("Failed to append song to player: {e}");
}
self.play();
}
}
}
#[instrument(skip(self))]
fn add_songs_to_queue(&mut self, songs: Vec<SongBrief>) {
self.queue.add_songs(songs);
if self.player.empty() {
let current_song = self.get_current_song();
if let Some(song) = current_song.or_else(|| self.get_next_song()) {
if let Err(e) = self.append_song_to_player(&song) {
error!("Failed to append song to player: {e}");
}
self.play();
}
}
}
#[instrument(skip(self))]
fn remove_range_from_queue(&mut self, range: Range<usize>) {
let current_to_be_removed = self
.queue
.current_index()
.is_some_and(|current_index| range.contains(¤t_index));
self.queue.remove_range(range);
if !current_to_be_removed {
return;
}
self.clear_player();
if let Some(song) = self.get_current_song()
&& let Err(e) = self.append_song_to_player(&song)
{
error!("Failed to append song to player: {e}");
}
}
#[instrument(skip(self))]
fn get_current_song(&self) -> Option<SongBrief> {
self.queue.current_song().cloned()
}
#[instrument(skip(self))]
fn get_next_song(&mut self) -> Option<SongBrief> {
self.queue.next_song().cloned()
}
fn get_time_played(&self) -> Duration {
self.player.get_pos()
}
#[instrument(skip(self, source))]
fn append_to_player<T>(&self, source: T)
where
T: Source<Item = f32> + Send + 'static,
{
self.player.append(source);
let command_tx = self.command_tx.clone();
self.player.append(EmptyCallback::new(Box::new(move || {
debug!("Song finished");
if let Err(e) = command_tx.send((
AudioCommand::Queue(QueueCommand::PlayNextSong),
tracing::Span::current(),
)) {
error!("Failed to send command to audio kernel: {e}");
} else {
debug!("Sent PlayNextSong command to audio kernel");
}
})));
}
#[instrument(skip(self))]
fn append_song_to_player(&self, song: &SongBrief) -> Result<(), LibraryError> {
let file = File::open(&song.path)?;
let byte_len = file.metadata()?.len();
let decoder = DecoderBuilder::new()
.with_data(BufReader::new(file))
.with_byte_len(byte_len)
.with_seekable(true)
.with_coarse_seek(true)
.with_gapless(true)
.build()?;
self.append_to_player(decoder);
Ok(())
}
#[instrument(skip(self))]
fn volume_control(&mut self, command: VolumeCommand) {
match command {
VolumeCommand::Up(change) => {
let volume = self.volume;
let updated = (volume + change).clamp(MIN_VOLUME, MAX_VOLUME);
if (volume - updated).abs() > 0.0001 {
self.volume = updated;
let _ = self.event_tx.send(StateChange::VolumeChanged(self.volume));
}
}
VolumeCommand::Down(change) => {
let volume = self.volume;
let updated = (volume - change).clamp(MIN_VOLUME, MAX_VOLUME);
if (volume - updated).abs() > 0.0001 {
self.volume = updated;
let _ = self.event_tx.send(StateChange::VolumeChanged(self.volume));
}
}
VolumeCommand::Set(updated) => {
let volume = self.volume;
let updated = updated.clamp(MIN_VOLUME, MAX_VOLUME);
if (volume - updated).abs() > 0.0001 {
self.volume = updated;
let _ = self.event_tx.send(StateChange::VolumeChanged(self.volume));
}
}
VolumeCommand::Mute => {
self.muted = true;
let _ = self.event_tx.send(StateChange::Muted);
}
VolumeCommand::Unmute => {
self.muted = false;
let _ = self.event_tx.send(StateChange::Unmuted);
}
VolumeCommand::ToggleMute => {
self.muted = !self.muted;
if self.muted {
let _ = self.event_tx.send(StateChange::Muted);
} else {
let _ = self.event_tx.send(StateChange::Unmuted);
}
}
}
if self.muted {
self.player.set_volume(0.0);
} else {
self.player.set_volume(self.volume.to_owned());
}
}
#[instrument(skip(self))]
fn seek(&mut self, seek: SeekType, duration: Duration) {
let new_time = match seek {
SeekType::Absolute => duration,
SeekType::RelativeForwards => self.get_time_played().saturating_add(duration),
SeekType::RelativeBackwards => self.get_time_played().saturating_sub(duration),
};
match self.player.try_seek(new_time) {
Ok(()) => {
debug!("Seek to {} successful", format_duration(&new_time));
if new_time > Duration::from_secs(0) && self.status == Status::Stopped {
self.status = Status::Paused;
}
}
Err(SeekError::NotSupported { underlying_source }) => {
error!("Seek not supported by source: {underlying_source}");
}
Err(err) => {
error!("Seeking failed with error: {err}");
}
}
}
}
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
use rstest::{fixture, rstest};
use crate::test_utils::init;
use super::*;
use std::sync::mpsc;
use std::time::Duration;
#[fixture]
fn audio_kernel() -> AudioKernel {
let (tx, _) = mpsc::channel();
let (event_tx, _) = mpsc::channel();
AudioKernel::new(tx, event_tx)
}
#[fixture]
fn audio_kernel_sender() -> Arc<AudioKernelSender> {
let (tx, _) = mpsc::channel();
AudioKernelSender::start(tx)
}
async fn get_state(sender: Arc<AudioKernelSender>) -> StateAudio {
let (tx, rx) = tokio::sync::oneshot::channel::<StateAudio>();
sender.send(AudioCommand::ReportStatus(tx));
rx.await.unwrap()
}
#[fixture]
fn sound() -> impl Source<Item = f32> + Send + 'static {
rodio::source::SineWave::new(440.0)
}
#[test]
fn test_audio_kernel_sender_send() {
let (tx, rx) = mpsc::channel();
let sender = AudioKernelSender::new(tx);
sender.send(AudioCommand::Play);
let (recv, _) = rx.recv().unwrap();
assert_eq!(recv, AudioCommand::Play);
}
#[test]
#[should_panic = "Failed to send command to audio kernel: sending on a closed channel"]
fn test_audio_kernel_send_closed_channel() {
let (tx, _) = mpsc::channel();
let sender = AudioKernelSender::new(tx);
sender.send(AudioCommand::Play);
}
#[test]
fn test_audio_kernel_try_send_closed_channel() {
let (tx, _) = mpsc::channel();
let sender = AudioKernelSender::new(tx);
assert!(sender.try_send(AudioCommand::Play).is_err());
}
#[rstest]
#[timeout(Duration::from_secs(3))] fn test_audio_player_kernel_spawn_and_exit(
#[from(audio_kernel_sender)] sender: Arc<AudioKernelSender>,
) {
init();
sender.send(AudioCommand::Exit);
}
#[rstest]
fn test_volume_control(mut audio_kernel: AudioKernel) {
audio_kernel.volume_control(VolumeCommand::Up(0.1));
let volume = audio_kernel.volume;
assert!(f32::EPSILON > (volume - 1.1).abs(), "{volume} != 1.1");
audio_kernel.volume_control(VolumeCommand::Down(0.1));
let volume = audio_kernel.volume;
assert!(f32::EPSILON > (volume - 1.0).abs(), "{volume} != 1.0");
audio_kernel.volume_control(VolumeCommand::Set(0.5));
let volume = audio_kernel.volume;
assert!(f32::EPSILON > (volume - 0.5).abs(), "{volume} != 0.5");
audio_kernel.volume_control(VolumeCommand::Mute);
assert_eq!(audio_kernel.muted, true);
audio_kernel.volume_control(VolumeCommand::Unmute);
assert_eq!(audio_kernel.muted, false);
audio_kernel.volume_control(VolumeCommand::ToggleMute);
assert_eq!(audio_kernel.muted, true);
audio_kernel.volume_control(VolumeCommand::ToggleMute);
assert_eq!(audio_kernel.muted, false);
}
mod playback_tests {
use mecomp_storage::{
db::schemas::song::Song,
test_utils::{arb_song_case, create_song_metadata, init_test_database},
};
use pretty_assertions::assert_eq;
use rstest::rstest;
use crate::test_utils::init;
use super::{super::*, audio_kernel, audio_kernel_sender, get_state, sound};
#[rstest]
fn test_audio_kernel_play_pause(
mut audio_kernel: AudioKernel,
sound: impl Source<Item = f32> + Send + 'static,
) {
init();
audio_kernel.player.append(sound);
audio_kernel.play();
assert!(!audio_kernel.player.is_paused());
audio_kernel.pause();
assert!(audio_kernel.player.is_paused());
}
#[rstest]
fn test_audio_kernel_toggle_playback(
mut audio_kernel: AudioKernel,
sound: impl Source<Item = f32> + Send + 'static,
) {
init();
audio_kernel.player.append(sound);
audio_kernel.play();
assert!(!audio_kernel.player.is_paused());
audio_kernel.toggle_playback();
assert!(audio_kernel.player.is_paused());
audio_kernel.toggle_playback();
assert!(!audio_kernel.player.is_paused());
}
#[rstest]
#[timeout(Duration::from_secs(10))] #[tokio::test]
async fn test_play_pause_toggle_restart(
#[from(audio_kernel_sender)] sender: Arc<AudioKernelSender>,
) {
init();
let db = init_test_database().await.unwrap();
let tempdir = tempfile::tempdir().unwrap();
let song = Song::try_load_into_db(
&db,
create_song_metadata(&tempdir, arb_song_case()()).unwrap(),
)
.await
.unwrap();
sender.send(AudioCommand::Queue(QueueCommand::AddToQueue(
song.brief().into(),
)));
let state = get_state(sender.clone()).await;
assert_eq!(state.queue_position, Some(0));
assert_eq!(state.status, Status::Playing);
sender.send(AudioCommand::Pause);
let state = get_state(sender.clone()).await;
assert_eq!(state.status, Status::Paused);
sender.send(AudioCommand::Play);
let state = get_state(sender.clone()).await;
assert_eq!(state.status, Status::Playing);
sender.send(AudioCommand::RestartSong);
let state = get_state(sender.clone()).await;
assert_eq!(state.status, Status::Playing);
sender.send(AudioCommand::TogglePlayback);
let state = get_state(sender.clone()).await;
assert_eq!(state.status, Status::Paused);
sender.send(AudioCommand::RestartSong);
let state = get_state(sender.clone()).await;
assert_eq!(state.status, Status::Paused);
sender.send(AudioCommand::Exit);
}
#[rstest]
fn test_audio_kernel_stop(mut audio_kernel: AudioKernel) {
init();
audio_kernel.player.append(sound());
audio_kernel.play();
assert!(!audio_kernel.player.is_paused());
audio_kernel.stop();
assert!(audio_kernel.player.is_paused());
assert_eq!(audio_kernel.player.get_pos(), Duration::from_secs(0));
assert_eq!(audio_kernel.status, Status::Stopped);
}
#[rstest]
#[timeout(Duration::from_secs(10))] #[tokio::test]
async fn test_audio_kernel_skip_forward(mut audio_kernel: AudioKernel) {
init();
let db = init_test_database().await.unwrap();
let tempdir = tempfile::tempdir().unwrap();
let state = audio_kernel.state();
assert_eq!(state.queue_position, None);
assert!(state.paused());
assert_eq!(state.status, Status::Stopped);
audio_kernel.queue_control(QueueCommand::AddToQueue(OneOrMany::Many(vec![
Song::try_load_into_db(
&db,
create_song_metadata(&tempdir, arb_song_case()()).unwrap(),
)
.await
.unwrap()
.into(),
Song::try_load_into_db(
&db,
create_song_metadata(&tempdir, arb_song_case()()).unwrap(),
)
.await
.unwrap()
.into(),
Song::try_load_into_db(
&db,
create_song_metadata(&tempdir, arb_song_case()()).unwrap(),
)
.await
.unwrap()
.into(),
])));
let state = audio_kernel.state();
assert_eq!(state.queue_position, Some(0));
assert!(!state.paused());
assert_eq!(state.status, Status::Playing);
audio_kernel.queue_control(QueueCommand::SkipForward(1));
let state = audio_kernel.state();
assert_eq!(state.queue_position, Some(1));
assert!(!state.paused());
assert_eq!(state.status, Status::Playing);
audio_kernel.queue_control(QueueCommand::SkipForward(1));
let state = audio_kernel.state();
assert_eq!(state.queue_position, Some(2));
assert!(!state.paused());
assert_eq!(state.status, Status::Playing);
audio_kernel.queue_control(QueueCommand::SkipForward(1));
let state = audio_kernel.state();
assert_eq!(state.queue_position, None);
assert!(state.paused());
assert_eq!(state.status, Status::Stopped);
}
#[rstest]
#[timeout(Duration::from_secs(10))] #[tokio::test]
async fn test_audio_kernel_skip_forward_sender(
#[from(audio_kernel_sender)] sender: Arc<AudioKernelSender>,
) {
init();
let db = init_test_database().await.unwrap();
let tempdir = tempfile::tempdir().unwrap();
let state = get_state(sender.clone()).await;
assert_eq!(state.queue_position, None);
assert!(state.paused());
assert_eq!(state.status, Status::Stopped);
sender.send(AudioCommand::Queue(QueueCommand::AddToQueue(
OneOrMany::Many(vec![
Song::try_load_into_db(
&db,
create_song_metadata(&tempdir, arb_song_case()()).unwrap(),
)
.await
.unwrap()
.into(),
Song::try_load_into_db(
&db,
create_song_metadata(&tempdir, arb_song_case()()).unwrap(),
)
.await
.unwrap()
.into(),
Song::try_load_into_db(
&db,
create_song_metadata(&tempdir, arb_song_case()()).unwrap(),
)
.await
.unwrap()
.into(),
]),
)));
let state = get_state(sender.clone()).await;
assert_eq!(state.queue_position, Some(0));
assert!(!state.paused());
assert_eq!(state.status, Status::Playing);
sender.send(AudioCommand::Queue(QueueCommand::SkipForward(1)));
let state = get_state(sender.clone()).await;
assert_eq!(state.queue_position, Some(1));
assert!(!state.paused());
assert_eq!(state.status, Status::Playing);
sender.send(AudioCommand::Queue(QueueCommand::SkipForward(1)));
let state = get_state(sender.clone()).await;
assert_eq!(state.queue_position, Some(2));
assert!(!state.paused());
assert_eq!(state.status, Status::Playing);
sender.send(AudioCommand::Queue(QueueCommand::SkipForward(1)));
let state = get_state(sender.clone()).await;
assert_eq!(state.queue_position, None);
assert!(state.paused());
assert_eq!(state.status, Status::Stopped);
sender.send(AudioCommand::Exit);
}
#[rstest]
#[timeout(Duration::from_secs(10))] #[tokio::test]
async fn test_remove_range_from_queue(
#[from(audio_kernel_sender)] sender: Arc<AudioKernelSender>,
) {
init();
let db = init_test_database().await.unwrap();
let tempdir = tempfile::tempdir().unwrap();
let song1 = Song::try_load_into_db(
&db,
create_song_metadata(&tempdir, arb_song_case()()).unwrap(),
)
.await
.unwrap();
let song2 = Song::try_load_into_db(
&db,
create_song_metadata(&tempdir, arb_song_case()()).unwrap(),
)
.await
.unwrap();
sender.send(AudioCommand::Queue(QueueCommand::AddToQueue(
OneOrMany::Many(vec![song1.clone().into(), song2.clone().into()]),
)));
let state = get_state(sender.clone()).await;
assert_eq!(state.queue_position, Some(0));
assert!(!state.paused());
assert_eq!(state.status, Status::Playing);
sender.send(AudioCommand::Pause);
sender.send(AudioCommand::Queue(QueueCommand::RemoveRange(0..1)));
let state = get_state(sender.clone()).await;
assert_eq!(state.queue_position, Some(0));
assert!(state.paused());
assert_eq!(state.status, Status::Stopped);
assert_eq!(state.queue.len(), 1);
assert_eq!(state.queue[0], song2.clone().into());
sender.send(AudioCommand::Play);
sender.send(AudioCommand::Queue(QueueCommand::AddToQueue(
song1.clone().brief().into(),
)));
let state = get_state(sender.clone()).await;
assert_eq!(state.queue_position, Some(0));
assert!(!state.paused());
assert_eq!(state.status, Status::Playing);
assert_eq!(state.queue.len(), 2);
assert_eq!(state.queue[0], song2.clone().into());
assert_eq!(state.queue[1], song1.into());
sender.send(AudioCommand::Queue(QueueCommand::RemoveRange(1..2)));
let state = get_state(sender.clone()).await;
assert_eq!(state.queue_position, Some(0));
assert!(!state.paused());
assert_eq!(state.status, Status::Playing);
assert_eq!(state.queue.len(), 1);
assert_eq!(state.queue[0], song2.into());
sender.send(AudioCommand::Exit);
}
#[rstest]
#[timeout(Duration::from_secs(10))] #[tokio::test]
async fn test_audio_kernel_skip_backward(
#[from(audio_kernel_sender)] sender: Arc<AudioKernelSender>,
) {
init();
let db = init_test_database().await.unwrap();
let tempdir = tempfile::tempdir().unwrap();
let state = get_state(sender.clone()).await;
assert_eq!(state.queue_position, None);
assert!(state.paused());
assert_eq!(state.status, Status::Stopped);
sender.send(AudioCommand::Queue(QueueCommand::AddToQueue(
OneOrMany::Many(vec![
Song::try_load_into_db(
&db,
create_song_metadata(&tempdir, arb_song_case()()).unwrap(),
)
.await
.unwrap()
.into(),
Song::try_load_into_db(
&db,
create_song_metadata(&tempdir, arb_song_case()()).unwrap(),
)
.await
.unwrap()
.into(),
Song::try_load_into_db(
&db,
create_song_metadata(&tempdir, arb_song_case()()).unwrap(),
)
.await
.unwrap()
.into(),
]),
)));
let state = get_state(sender.clone()).await;
assert_eq!(state.queue_position, Some(0));
assert!(!state.paused());
assert_eq!(state.status, Status::Playing);
sender.send(AudioCommand::Queue(QueueCommand::SkipForward(2)));
let state = get_state(sender.clone()).await;
assert_eq!(state.queue_position, Some(2));
assert!(!state.paused());
assert_eq!(state.status, Status::Playing);
sender.send(AudioCommand::Queue(QueueCommand::SkipBackward(1)));
let state = get_state(sender.clone()).await;
assert_eq!(state.queue_position, Some(1));
assert!(!state.paused());
assert_eq!(state.status, Status::Playing);
sender.send(AudioCommand::Queue(QueueCommand::SkipBackward(1)));
let state = get_state(sender.clone()).await;
assert_eq!(state.queue_position, Some(0));
assert!(!state.paused());
sender.send(AudioCommand::Queue(QueueCommand::SkipBackward(1)));
let state = get_state(sender.clone()).await;
assert_eq!(state.queue_position, None);
assert!(state.paused());
assert_eq!(state.status, Status::Stopped);
sender.send(AudioCommand::Exit);
}
#[rstest]
#[timeout(Duration::from_secs(10))] #[tokio::test]
async fn test_audio_kernel_set_position(
#[from(audio_kernel_sender)] sender: Arc<AudioKernelSender>,
) {
init();
let db = init_test_database().await.unwrap();
let tempdir = tempfile::tempdir().unwrap();
let state = get_state(sender.clone()).await;
assert_eq!(state.queue_position, None);
assert!(state.paused());
assert_eq!(state.status, Status::Stopped);
sender.send(AudioCommand::Queue(QueueCommand::AddToQueue(
OneOrMany::Many(vec![
Song::try_load_into_db(
&db,
create_song_metadata(&tempdir, arb_song_case()()).unwrap(),
)
.await
.unwrap()
.into(),
Song::try_load_into_db(
&db,
create_song_metadata(&tempdir, arb_song_case()()).unwrap(),
)
.await
.unwrap()
.into(),
Song::try_load_into_db(
&db,
create_song_metadata(&tempdir, arb_song_case()()).unwrap(),
)
.await
.unwrap()
.into(),
]),
)));
let state = get_state(sender.clone()).await;
assert_eq!(state.queue_position, Some(0));
assert!(!state.paused());
assert_eq!(state.status, Status::Playing);
sender.send(AudioCommand::Queue(QueueCommand::SetPosition(1)));
let state = get_state(sender.clone()).await;
assert_eq!(state.queue_position, Some(1));
assert!(!state.paused());
assert_eq!(state.status, Status::Playing);
sender.send(AudioCommand::Queue(QueueCommand::SetPosition(2)));
let state = get_state(sender.clone()).await;
assert_eq!(state.queue_position, Some(2));
assert!(!state.paused());
assert_eq!(state.status, Status::Playing);
sender.send(AudioCommand::Queue(QueueCommand::SetPosition(0)));
let state = get_state(sender.clone()).await;
assert_eq!(state.queue_position, Some(0));
assert!(!state.paused());
assert_eq!(state.status, Status::Playing);
sender.send(AudioCommand::Queue(QueueCommand::SetPosition(3)));
let state = get_state(sender.clone()).await;
assert_eq!(state.queue_position, Some(2));
assert!(!state.paused());
assert_eq!(state.status, Status::Playing);
sender.send(AudioCommand::Exit);
}
#[rstest]
#[timeout(Duration::from_secs(10))] #[tokio::test]
async fn test_audio_kernel_clear(
#[from(audio_kernel_sender)] sender: Arc<AudioKernelSender>,
) {
init();
let db = init_test_database().await.unwrap();
let tempdir = tempfile::tempdir().unwrap();
let state = get_state(sender.clone()).await;
assert_eq!(state.queue_position, None);
assert!(state.paused());
assert_eq!(state.status, Status::Stopped);
sender.send(AudioCommand::Queue(QueueCommand::AddToQueue(
OneOrMany::Many(vec![
Song::try_load_into_db(
&db,
create_song_metadata(&tempdir, arb_song_case()()).unwrap(),
)
.await
.unwrap()
.into(),
Song::try_load_into_db(
&db,
create_song_metadata(&tempdir, arb_song_case()()).unwrap(),
)
.await
.unwrap()
.into(),
Song::try_load_into_db(
&db,
create_song_metadata(&tempdir, arb_song_case()()).unwrap(),
)
.await
.unwrap()
.into(),
]),
)));
let state = get_state(sender.clone()).await;
assert_eq!(state.queue_position, Some(0));
assert_eq!(state.queue.len(), 3);
assert!(!state.paused());
assert_eq!(state.status, Status::Playing);
sender.send(AudioCommand::ClearPlayer);
let state = get_state(sender.clone()).await;
assert_eq!(state.queue_position, Some(0));
assert_eq!(state.queue.len(), 3);
assert!(state.paused());
assert_eq!(state.status, Status::Stopped);
sender.send(AudioCommand::Queue(QueueCommand::Clear));
let state = get_state(sender.clone()).await;
assert_eq!(state.queue_position, None);
assert_eq!(state.queue.len(), 0);
assert!(state.paused());
assert_eq!(state.status, Status::Stopped);
sender.send(AudioCommand::Exit);
}
#[rstest]
#[timeout(Duration::from_secs(10))] #[tokio::test]
async fn test_audio_kernel_shuffle(
#[from(audio_kernel_sender)] sender: Arc<AudioKernelSender>,
) {
init();
let db = init_test_database().await.unwrap();
let tempdir = tempfile::tempdir().unwrap();
let state = get_state(sender.clone()).await;
assert_eq!(state.queue_position, None);
assert!(state.paused());
assert_eq!(state.status, Status::Stopped);
sender.send(AudioCommand::Queue(QueueCommand::AddToQueue(
OneOrMany::Many(vec![
Song::try_load_into_db(
&db,
create_song_metadata(&tempdir, arb_song_case()()).unwrap(),
)
.await
.unwrap()
.into(),
Song::try_load_into_db(
&db,
create_song_metadata(&tempdir, arb_song_case()()).unwrap(),
)
.await
.unwrap()
.into(),
Song::try_load_into_db(
&db,
create_song_metadata(&tempdir, arb_song_case()()).unwrap(),
)
.await
.unwrap()
.into(),
]),
)));
let state = get_state(sender.clone()).await;
assert_eq!(state.queue_position, Some(0));
assert_eq!(state.queue.len(), 3);
assert!(!state.paused());
assert_eq!(state.status, Status::Playing);
sender.send(AudioCommand::Queue(QueueCommand::SkipForward(1)));
let state = get_state(sender.clone()).await;
assert_eq!(state.queue_position, Some(1));
assert_eq!(state.queue.len(), 3);
assert!(!state.paused());
assert_eq!(state.status, Status::Playing);
sender.send(AudioCommand::Queue(QueueCommand::Shuffle));
let state = get_state(sender.clone()).await;
assert_eq!(state.queue_position, Some(0));
assert_eq!(state.queue.len(), 3);
assert!(!state.paused());
assert_eq!(state.status, Status::Playing);
sender.send(AudioCommand::Exit);
}
#[rstest]
#[timeout(Duration::from_secs(5))] #[tokio::test]
async fn test_volume_commands(#[from(audio_kernel_sender)] sender: Arc<AudioKernelSender>) {
init();
let state = get_state(sender.clone()).await;
assert!(
f32::EPSILON > (state.volume - 1.0).abs(),
"{} != 1.0",
state.volume
);
assert!(!state.muted);
sender.send(AudioCommand::Volume(VolumeCommand::Up(0.1)));
let state = get_state(sender.clone()).await;
assert!(
f32::EPSILON > (state.volume - 1.1).abs(),
"{} != 1.1",
state.volume
);
assert!(!state.muted);
sender.send(AudioCommand::Volume(VolumeCommand::Down(0.1)));
let state = get_state(sender.clone()).await;
assert!(
f32::EPSILON > (state.volume - 1.0).abs(),
"{} != 1.0",
state.volume
);
assert!(!state.muted);
sender.send(AudioCommand::Volume(VolumeCommand::Set(0.5)));
let state = get_state(sender.clone()).await;
assert!(
f32::EPSILON > (state.volume - 0.5).abs(),
"{} != 0.5",
state.volume
);
assert!(!state.muted);
sender.send(AudioCommand::Volume(VolumeCommand::Mute));
let state = get_state(sender.clone()).await;
assert!(
f32::EPSILON > (state.volume - 0.5).abs(),
"{} != 0.5",
state.volume
); assert!(state.muted);
sender.send(AudioCommand::Volume(VolumeCommand::Unmute));
let state = get_state(sender.clone()).await;
assert!(
f32::EPSILON > (state.volume - 0.5).abs(),
"{} != 0.5",
state.volume
);
assert!(!state.muted);
sender.send(AudioCommand::Volume(VolumeCommand::ToggleMute));
let state = get_state(sender.clone()).await;
assert!(
f32::EPSILON > (state.volume - 0.5).abs(),
"{} != 0.5",
state.volume
);
assert!(state.muted);
sender.send(AudioCommand::Volume(VolumeCommand::ToggleMute));
let state = get_state(sender.clone()).await;
assert!(
f32::EPSILON > (state.volume - 0.5).abs(),
"{} != 0.5",
state.volume
);
assert!(!state.muted);
sender.send(AudioCommand::Exit);
}
#[rstest]
#[timeout(Duration::from_secs(5))] #[tokio::test]
async fn test_volume_out_of_bounds(
#[from(audio_kernel_sender)] sender: Arc<AudioKernelSender>,
) {
init();
sender.send(AudioCommand::Volume(VolumeCommand::Up(MAX_VOLUME + 0.5)));
let state = get_state(sender.clone()).await;
assert!(
f32::EPSILON > (state.volume - MAX_VOLUME).abs(),
"{} != {}",
state.volume,
MAX_VOLUME
);
assert!(!state.muted);
sender.send(AudioCommand::Volume(VolumeCommand::Down(
MAX_VOLUME + 0.5 - MIN_VOLUME,
)));
let state = get_state(sender.clone()).await;
assert!(
f32::EPSILON > (state.volume - MIN_VOLUME).abs(),
"{} != {}",
state.volume,
MIN_VOLUME
);
assert!(!state.muted);
sender.send(AudioCommand::Volume(VolumeCommand::Set(MAX_VOLUME + 0.5)));
let state = get_state(sender.clone()).await;
assert!(
f32::EPSILON > (state.volume - MAX_VOLUME).abs(),
"{} != {}",
state.volume,
MAX_VOLUME
);
assert!(!state.muted);
sender.send(AudioCommand::Volume(VolumeCommand::Set(MIN_VOLUME - 0.5)));
let state = get_state(sender.clone()).await;
assert!(
f32::EPSILON > (state.volume - MIN_VOLUME).abs(),
"{} != {}",
state.volume,
MIN_VOLUME
);
assert!(!state.muted);
sender.send(AudioCommand::Exit);
}
#[rstest]
#[timeout(Duration::from_secs(9))] #[tokio::test]
async fn test_seek_commands(#[from(audio_kernel_sender)] sender: Arc<AudioKernelSender>) {
init();
let db = init_test_database().await.unwrap();
let tempdir = tempfile::tempdir().unwrap();
let song = Song::try_load_into_db(
&db,
create_song_metadata(&tempdir, arb_song_case()()).unwrap(),
)
.await
.unwrap();
sender.send(AudioCommand::Queue(QueueCommand::AddToQueue(
song.clone().brief().into(),
)));
sender.send(AudioCommand::Stop);
sender.send(AudioCommand::Seek(
SeekType::Absolute,
Duration::from_secs(0),
));
let state: StateAudio = get_state(sender.clone()).await;
assert_eq!(state.queue_position, Some(0));
assert_eq!(state.status, Status::Stopped);
assert_eq!(
state.runtime.unwrap().duration,
Duration::from_secs(10) + Duration::from_millis(188)
);
assert_eq!(state.runtime.unwrap().seek_position, Duration::from_secs(0));
sender.send(AudioCommand::Seek(
SeekType::RelativeForwards,
Duration::from_secs(2),
));
let state = get_state(sender.clone()).await;
assert_eq!(state.runtime.unwrap().seek_position, Duration::from_secs(2));
assert_eq!(state.current_song, Some(song.clone().into()));
assert_eq!(state.status, Status::Paused);
sender.send(AudioCommand::Seek(
SeekType::RelativeBackwards,
Duration::from_secs(1),
));
let state = get_state(sender.clone()).await;
assert_eq!(state.runtime.unwrap().seek_position, Duration::from_secs(1));
assert_eq!(state.current_song, Some(song.clone().into()));
assert_eq!(state.status, Status::Paused);
sender.send(AudioCommand::Seek(
SeekType::Absolute,
Duration::from_secs(10),
));
let state = get_state(sender.clone()).await;
assert_eq!(
state.runtime.unwrap().seek_position,
Duration::from_secs(10)
);
assert_eq!(state.current_song, Some(song.clone().into()));
assert_eq!(state.status, Status::Paused);
sender.send(AudioCommand::Play);
sender.send(AudioCommand::Seek(
SeekType::RelativeForwards,
Duration::from_secs(1),
));
tokio::time::sleep(Duration::from_millis(500)).await;
let state = get_state(sender.clone()).await;
assert_eq!(
state.queue_position, None,
"Song did not end as expected, queue position: {:?}. runtime info: {:?}",
state.queue_position, state.runtime,
);
assert_eq!(state.status, Status::Stopped);
sender.send(AudioCommand::Exit);
}
}
}