#![allow(clippy::module_name_repetitions)]
use std::fmt;
use thiserror::Error;
#[derive(Debug, Clone)]
pub enum AudioCommand {
SetVolume(f64),
Pause,
Resume,
Seek(f64),
Flush,
Reset,
}
#[derive(Debug, Clone)]
pub enum AudioResponse {
Success,
Error(String),
}
#[derive(Debug)]
pub struct CommandMessage {
pub command: AudioCommand,
pub response_sender: Option<flume::Sender<AudioResponse>>,
}
#[derive(Debug, Error)]
pub enum AudioError {
#[error("Command error: {0}")]
Command(String),
#[error("Channel send error")]
ChannelSend,
#[error("Channel receive error")]
ChannelReceive,
#[error("Unexpected response type")]
UnexpectedResponse,
#[error("Handle not available")]
HandleNotAvailable,
}
impl From<flume::SendError<CommandMessage>> for AudioError {
fn from(_: flume::SendError<CommandMessage>) -> Self {
Self::ChannelSend
}
}
impl From<flume::TrySendError<CommandMessage>> for AudioError {
fn from(_: flume::TrySendError<CommandMessage>) -> Self {
Self::ChannelSend
}
}
impl From<flume::RecvError> for AudioError {
fn from(_: flume::RecvError) -> Self {
Self::ChannelReceive
}
}
pub struct AudioHandle {
command_sender: flume::Sender<CommandMessage>,
}
impl fmt::Debug for AudioHandle {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("AudioHandle")
.field("command_sender", &"<flume::Sender>")
.finish()
}
}
impl Clone for AudioHandle {
fn clone(&self) -> Self {
Self {
command_sender: self.command_sender.clone(),
}
}
}
impl AudioHandle {
#[must_use]
pub const fn new(command_sender: flume::Sender<CommandMessage>) -> Self {
Self { command_sender }
}
pub async fn set_volume(&self, volume: f64) -> Result<(), AudioError> {
self.send_command_with_response(AudioCommand::SetVolume(volume))
.await?;
Ok(())
}
pub async fn pause(&self) -> Result<(), AudioError> {
self.send_command_with_response(AudioCommand::Pause).await?;
Ok(())
}
pub async fn resume(&self) -> Result<(), AudioError> {
self.send_command_with_response(AudioCommand::Resume)
.await?;
Ok(())
}
pub async fn seek(&self, position: f64) -> Result<(), AudioError> {
self.send_command_with_response(AudioCommand::Seek(position))
.await?;
Ok(())
}
pub async fn flush(&self) -> Result<(), AudioError> {
self.send_command_with_response(AudioCommand::Flush).await?;
Ok(())
}
pub async fn reset(&self) -> Result<(), AudioError> {
self.send_command_with_response(AudioCommand::Reset).await?;
Ok(())
}
pub fn set_volume_immediate(&self, volume: f64) -> Result<(), AudioError> {
self.send_command_fire_and_forget(AudioCommand::SetVolume(volume))
}
pub fn pause_immediate(&self) -> Result<(), AudioError> {
self.send_command_fire_and_forget(AudioCommand::Pause)
}
pub fn resume_immediate(&self) -> Result<(), AudioError> {
self.send_command_fire_and_forget(AudioCommand::Resume)
}
async fn send_command_with_response(
&self,
command: AudioCommand,
) -> Result<AudioResponse, AudioError> {
let (response_tx, response_rx) = flume::bounded(1);
self.command_sender
.send_async(CommandMessage {
command,
response_sender: Some(response_tx),
})
.await?;
response_rx.recv_async().await.map_err(AudioError::from)
}
fn send_command_fire_and_forget(&self, command: AudioCommand) -> Result<(), AudioError> {
self.command_sender
.try_send(CommandMessage {
command,
response_sender: None,
})
.map_err(AudioError::from)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test_log::test]
fn test_audio_handle_fire_and_forget_success() {
let (tx, rx) = flume::bounded(10);
let handle = AudioHandle::new(tx);
let result = handle.set_volume_immediate(0.5);
assert!(result.is_ok());
let msg = rx.try_recv().unwrap();
assert!(matches!(msg.command, AudioCommand::SetVolume(v) if (v - 0.5).abs() < 0.001));
assert!(msg.response_sender.is_none());
}
#[test_log::test]
fn test_audio_handle_pause_immediate() {
let (tx, rx) = flume::bounded(10);
let handle = AudioHandle::new(tx);
let result = handle.pause_immediate();
assert!(result.is_ok());
let msg = rx.try_recv().unwrap();
assert!(matches!(msg.command, AudioCommand::Pause));
assert!(msg.response_sender.is_none());
}
#[test_log::test]
fn test_audio_handle_resume_immediate() {
let (tx, rx) = flume::bounded(10);
let handle = AudioHandle::new(tx);
let result = handle.resume_immediate();
assert!(result.is_ok());
let msg = rx.try_recv().unwrap();
assert!(matches!(msg.command, AudioCommand::Resume));
assert!(msg.response_sender.is_none());
}
#[test_log::test]
fn test_audio_handle_fire_and_forget_channel_full() {
let (tx, _rx) = flume::bounded(0);
let handle = AudioHandle::new(tx);
let result = handle.set_volume_immediate(0.5);
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), AudioError::ChannelSend));
}
#[test_log::test(switchy_async::test)]
async fn test_audio_handle_set_volume() {
let (tx, rx) = flume::bounded(10);
let handle = AudioHandle::new(tx);
switchy_async::task::spawn(async move {
let msg = rx.recv_async().await.unwrap();
if let Some(resp_tx) = msg.response_sender {
resp_tx.send_async(AudioResponse::Success).await.unwrap();
}
});
let result = handle.set_volume(0.5).await;
assert!(result.is_ok());
}
#[test_log::test(switchy_async::test)]
async fn test_audio_handle_pause() {
let (tx, rx) = flume::bounded(10);
let handle = AudioHandle::new(tx);
switchy_async::task::spawn(async move {
let msg = rx.recv_async().await.unwrap();
if let Some(resp_tx) = msg.response_sender {
resp_tx.send_async(AudioResponse::Success).await.unwrap();
}
});
let result = handle.pause().await;
assert!(result.is_ok());
}
#[test_log::test(switchy_async::test)]
async fn test_audio_handle_resume() {
let (tx, rx) = flume::bounded(10);
let handle = AudioHandle::new(tx);
switchy_async::task::spawn(async move {
let msg = rx.recv_async().await.unwrap();
if let Some(resp_tx) = msg.response_sender {
resp_tx.send_async(AudioResponse::Success).await.unwrap();
}
});
let result = handle.resume().await;
assert!(result.is_ok());
}
#[test_log::test(switchy_async::test)]
async fn test_audio_handle_seek() {
let (tx, rx) = flume::bounded(10);
let handle = AudioHandle::new(tx);
switchy_async::task::spawn(async move {
let msg = rx.recv_async().await.unwrap();
assert!(matches!(msg.command, AudioCommand::Seek(pos) if (pos - 10.5).abs() < 0.001));
if let Some(resp_tx) = msg.response_sender {
resp_tx.send_async(AudioResponse::Success).await.unwrap();
}
});
let result = handle.seek(10.5).await;
assert!(result.is_ok());
}
#[test_log::test(switchy_async::test)]
async fn test_audio_handle_flush() {
let (tx, rx) = flume::bounded(10);
let handle = AudioHandle::new(tx);
switchy_async::task::spawn(async move {
let msg = rx.recv_async().await.unwrap();
assert!(matches!(msg.command, AudioCommand::Flush));
if let Some(resp_tx) = msg.response_sender {
resp_tx.send_async(AudioResponse::Success).await.unwrap();
}
});
let result = handle.flush().await;
assert!(result.is_ok());
}
#[test_log::test(switchy_async::test)]
async fn test_audio_handle_reset() {
let (tx, rx) = flume::bounded(10);
let handle = AudioHandle::new(tx);
switchy_async::task::spawn(async move {
let msg = rx.recv_async().await.unwrap();
assert!(matches!(msg.command, AudioCommand::Reset));
if let Some(resp_tx) = msg.response_sender {
resp_tx.send_async(AudioResponse::Success).await.unwrap();
}
});
let result = handle.reset().await;
assert!(result.is_ok());
}
#[test_log::test(switchy_async::test)]
async fn test_audio_handle_channel_closed() {
let (tx, rx) = flume::bounded(10);
let handle = AudioHandle::new(tx);
drop(rx);
let result = handle.set_volume(0.5).await;
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), AudioError::ChannelSend));
}
#[test_log::test(switchy_async::test)]
async fn test_audio_handle_response_channel_closed() {
let (tx, rx) = flume::bounded(10);
let handle = AudioHandle::new(tx);
switchy_async::task::spawn(async move {
let _msg = rx.recv_async().await.unwrap();
});
let result = handle.set_volume(0.5).await;
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), AudioError::ChannelReceive));
}
}