use crate::composition::PulseSong;
use crate::error::{PulseError, PulseResult};
use crate::files::PulseImportedMidi;
use std::rc::Rc;
use tunes::engine::{AudioEngine, SoundId};
use tunes::track::Mixer;
#[derive(Clone)]
pub struct PulsePlaybackEngine {
engine: Rc<AudioEngine>,
}
#[derive(Clone)]
pub struct PulsePlaybackHandle {
engine: Rc<AudioEngine>,
sound_id: SoundId,
}
impl std::fmt::Debug for PulsePlaybackHandle {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("PulsePlaybackHandle")
.field("sound_id", &self.sound_id)
.finish_non_exhaustive()
}
}
pub enum PulsePlayable<'a> {
Song(&'a PulseSong),
ImportedMidi(&'a PulseImportedMidi),
}
fn playback_error(error: impl std::fmt::Display) -> PulseError {
PulseError::PlaybackFailed {
message: error.to_string(),
}
}
pub fn mixer_from_playable(playable: PulsePlayable<'_>) -> PulseResult<Mixer> {
match playable {
PulsePlayable::Song(song) => song.to_mixer(),
PulsePlayable::ImportedMidi(imported) => Ok(imported.cloned_mixer()),
}
}
pub fn validate_playback_rate(rate: f32) -> PulseResult<f32> {
if !rate.is_finite() || rate <= 0.0 {
return Err(PulseError::InvalidPlaybackRate { rate });
}
Ok(rate)
}
pub fn validate_playback_volume(volume: f32) -> PulseResult<f32> {
if !volume.is_finite() || !(0.0..=1.0).contains(&volume) {
return Err(PulseError::InvalidPlaybackVolume { volume });
}
Ok(volume)
}
pub fn validate_playback_pan(pan: f32) -> PulseResult<f32> {
if !pan.is_finite() || !(-1.0..=1.0).contains(&pan) {
return Err(PulseError::InvalidPlaybackPan { pan });
}
Ok(pan)
}
impl PulsePlaybackEngine {
pub fn new() -> PulseResult<Self> {
let engine = AudioEngine::new().map_err(playback_error)?;
Ok(Self {
engine: Rc::new(engine),
})
}
pub fn play(&self, playable: PulsePlayable<'_>) -> PulseResult<()> {
let mixer = mixer_from_playable(playable)?;
self.engine.play_mixer(&mixer).map_err(playback_error)
}
pub fn play_realtime(&self, playable: PulsePlayable<'_>) -> PulseResult<PulsePlaybackHandle> {
let mixer = mixer_from_playable(playable)?;
let sound_id = self
.engine
.play_mixer_realtime(&mixer)
.map_err(playback_error)?;
Ok(PulsePlaybackHandle::from_parts(
Rc::clone(&self.engine),
sound_id,
))
}
pub fn play_looping(&self, playable: PulsePlayable<'_>) -> PulseResult<PulsePlaybackHandle> {
let mixer = mixer_from_playable(playable)?;
let sound_id = self.engine.play_looping(&mixer).map_err(playback_error)?;
Ok(PulsePlaybackHandle::from_parts(
Rc::clone(&self.engine),
sound_id,
))
}
pub fn play_at_rate(&self, playable: PulsePlayable<'_>, rate: f32) -> PulseResult<()> {
let rate = validate_playback_rate(rate)?;
let mixer = mixer_from_playable(playable)?;
self.engine
.play_mixer_at_rate(&mixer, rate)
.map_err(playback_error)
}
pub fn play_realtime_at_rate(
&self,
playable: PulsePlayable<'_>,
rate: f32,
) -> PulseResult<PulsePlaybackHandle> {
let rate = validate_playback_rate(rate)?;
let mixer = mixer_from_playable(playable)?;
let sound_id = self
.engine
.play_mixer_realtime_at_rate(&mixer, rate)
.map_err(playback_error)?;
Ok(PulsePlaybackHandle::from_parts(
Rc::clone(&self.engine),
sound_id,
))
}
}
impl PulsePlaybackHandle {
#[must_use]
pub fn from_parts(engine: Rc<AudioEngine>, sound_id: SoundId) -> Self {
Self { engine, sound_id }
}
#[must_use]
pub fn sound_id(&self) -> SoundId {
self.sound_id
}
pub fn stop(&self) -> PulseResult<Self> {
self.engine.stop(self.sound_id).map_err(playback_error)?;
Ok(self.clone())
}
pub fn volume(&self, volume: f32) -> PulseResult<Self> {
let volume = validate_playback_volume(volume)?;
self.engine
.set_volume(self.sound_id, volume)
.map_err(playback_error)?;
Ok(self.clone())
}
pub fn pan(&self, pan: f32) -> PulseResult<Self> {
let pan = validate_playback_pan(pan)?;
self.engine
.set_pan(self.sound_id, pan)
.map_err(playback_error)?;
Ok(self.clone())
}
pub fn rate(&self, rate: f32) -> PulseResult<Self> {
let rate = validate_playback_rate(rate)?;
self.engine
.set_playback_rate(self.sound_id, rate)
.map_err(playback_error)?;
Ok(self.clone())
}
pub fn pause(&self) -> PulseResult<Self> {
self.engine.pause(self.sound_id).map_err(playback_error)?;
Ok(self.clone())
}
pub fn resume(&self) -> PulseResult<Self> {
self.engine.resume(self.sound_id).map_err(playback_error)?;
Ok(self.clone())
}
}
#[cfg(test)]
mod tests {
use super::*;
use tunes::composition::Tempo;
#[test]
fn playback_rate_rejects_zero_negative_and_non_finite_values() {
assert_eq!(
validate_playback_rate(0.0)
.expect_err("zero rate should fail")
.to_string(),
"invalid playback rate: 0"
);
assert_eq!(
validate_playback_rate(-1.0)
.expect_err("negative rate should fail")
.to_string(),
"invalid playback rate: -1"
);
assert_eq!(
validate_playback_rate(f32::NAN)
.expect_err("NaN rate should fail")
.to_string(),
"invalid playback rate: NaN"
);
assert_eq!(validate_playback_rate(1.25).unwrap(), 1.25);
}
#[test]
fn playback_volume_accepts_unit_range_only() {
assert_eq!(validate_playback_volume(0.0).unwrap(), 0.0);
assert_eq!(validate_playback_volume(1.0).unwrap(), 1.0);
assert_eq!(
validate_playback_volume(1.5)
.expect_err("too-loud volume should fail")
.to_string(),
"invalid playback volume: 1.5"
);
assert_eq!(
validate_playback_volume(f32::INFINITY)
.expect_err("infinite volume should fail")
.to_string(),
"invalid playback volume: inf"
);
}
#[test]
fn playback_pan_accepts_stereo_range_only() {
assert_eq!(validate_playback_pan(-1.0).unwrap(), -1.0);
assert_eq!(validate_playback_pan(1.0).unwrap(), 1.0);
assert_eq!(
validate_playback_pan(2.0)
.expect_err("pan outside stereo range should fail")
.to_string(),
"invalid playback pan: 2"
);
assert_eq!(
validate_playback_pan(f32::NEG_INFINITY)
.expect_err("non-finite pan should fail")
.to_string(),
"invalid playback pan: -inf"
);
}
#[test]
fn playback_handle_exposes_sound_id() {
let engine = Rc::new(AudioEngine::new().expect("audio engine should initialize"));
let handle = PulsePlaybackHandle::from_parts(engine, 42);
assert_eq!(handle.sound_id(), 42);
}
#[test]
fn playable_song_builds_a_mixer_without_playing_audio() {
let song = PulseSong::new();
let mixer = mixer_from_playable(PulsePlayable::Song(&song));
assert!(mixer.is_ok());
}
#[test]
fn playable_imported_midi_clones_its_mixer_without_playing_audio() {
let imported = PulseImportedMidi::new(Mixer::new(Tempo::new(120.0)));
let mixer = mixer_from_playable(PulsePlayable::ImportedMidi(&imported));
assert!(mixer.is_ok());
}
#[test]
fn playable_song_preserves_master_effects_in_mixer() {
let effect =
crate::effects::effect_from_options("eq", crate::effects::EffectOptions::Default)
.expect("default eq should parse");
let song = PulseSong::new().with_master_effect(effect);
let mixer = mixer_from_playable(PulsePlayable::Song(&song))
.expect("song with master effect should build mixer");
assert!(mixer.master.eq.is_some());
}
#[test]
fn playback_engine_can_be_constructed_without_playing_audio() {
let engine = PulsePlaybackEngine::new();
assert!(engine.is_ok());
}
#[test]
fn handle_control_methods_validate_arguments_before_forwarding() {
let engine = Rc::new(AudioEngine::new().expect("audio engine should initialize"));
let handle = PulsePlaybackHandle::from_parts(engine, 99);
assert_eq!(
handle
.volume(1.5)
.expect_err("invalid volume should fail before engine call")
.to_string(),
"invalid playback volume: 1.5"
);
assert_eq!(
handle
.pan(2.0)
.expect_err("invalid pan should fail before engine call")
.to_string(),
"invalid playback pan: 2"
);
assert_eq!(
handle
.rate(0.0)
.expect_err("invalid rate should fail before engine call")
.to_string(),
"invalid playback rate: 0"
);
}
#[test]
fn engine_rate_methods_validate_arguments_before_playing() {
let engine = PulsePlaybackEngine::new().expect("playback engine should initialize");
let song = PulseSong::new();
assert_eq!(
engine
.play_at_rate(PulsePlayable::Song(&song), 0.0)
.expect_err("invalid rate should fail before playback")
.to_string(),
"invalid playback rate: 0"
);
assert_eq!(
engine
.play_realtime_at_rate(PulsePlayable::Song(&song), f32::INFINITY)
.expect_err("invalid rate should fail before playback")
.to_string(),
"invalid playback rate: inf"
);
}
#[test]
fn playback_handle_invalid_controls_are_chainable_lua_methods() {
let lua = mlua::Lua::new();
let engine = Rc::new(AudioEngine::new().expect("audio engine should initialize"));
let handle = PulsePlaybackHandle::from_parts(engine, 7);
lua.globals().set("handle", handle).unwrap();
lua.load(
r#"
assert(type(handle.stop) == "function")
assert(type(handle.volume) == "function")
assert(type(handle.pan) == "function")
assert(type(handle.rate) == "function")
assert(type(handle.pause) == "function")
assert(type(handle.resume) == "function")
local ok_volume = pcall(function()
handle:volume(1.5)
end)
assert(ok_volume == false)
local ok_pan = pcall(function()
handle:pan(2)
end)
assert(ok_pan == false)
local ok_rate = pcall(function()
handle:rate(0)
end)
assert(ok_rate == false)
"#,
)
.exec()
.expect("handle methods should be visible and validate before engine calls");
}
}