1use crate::composition::PulseSong;
4use crate::error::{PulseError, PulseResult};
5use crate::files::PulseImportedMidi;
6use std::rc::Rc;
7use tunes::engine::{AudioEngine, SoundId};
8use tunes::track::Mixer;
9
10#[derive(Clone)]
11pub struct PulsePlaybackEngine {
13 engine: Rc<AudioEngine>,
14}
15
16#[derive(Clone)]
17pub struct PulsePlaybackHandle {
19 engine: Rc<AudioEngine>,
20 sound_id: SoundId,
21}
22
23impl std::fmt::Debug for PulsePlaybackHandle {
24 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
25 formatter
26 .debug_struct("PulsePlaybackHandle")
27 .field("sound_id", &self.sound_id)
28 .finish_non_exhaustive()
29 }
30}
31
32pub enum PulsePlayable<'a> {
34 Song(&'a PulseSong),
36 ImportedMidi(&'a PulseImportedMidi),
38}
39
40fn playback_error(error: impl std::fmt::Display) -> PulseError {
41 PulseError::PlaybackFailed {
42 message: error.to_string(),
43 }
44}
45
46pub fn mixer_from_playable(playable: PulsePlayable<'_>) -> PulseResult<Mixer> {
52 match playable {
53 PulsePlayable::Song(song) => song.to_mixer(),
54 PulsePlayable::ImportedMidi(imported) => Ok(imported.cloned_mixer()),
55 }
56}
57
58pub fn validate_playback_rate(rate: f32) -> PulseResult<f32> {
64 if !rate.is_finite() || rate <= 0.0 {
65 return Err(PulseError::InvalidPlaybackRate { rate });
66 }
67 Ok(rate)
68}
69
70pub fn validate_playback_volume(volume: f32) -> PulseResult<f32> {
76 if !volume.is_finite() || !(0.0..=1.0).contains(&volume) {
77 return Err(PulseError::InvalidPlaybackVolume { volume });
78 }
79 Ok(volume)
80}
81
82pub fn validate_playback_pan(pan: f32) -> PulseResult<f32> {
88 if !pan.is_finite() || !(-1.0..=1.0).contains(&pan) {
89 return Err(PulseError::InvalidPlaybackPan { pan });
90 }
91 Ok(pan)
92}
93
94impl PulsePlaybackEngine {
95 pub fn new() -> PulseResult<Self> {
101 let engine = AudioEngine::new().map_err(playback_error)?;
102 Ok(Self {
103 engine: Rc::new(engine),
104 })
105 }
106
107 pub fn play(&self, playable: PulsePlayable<'_>) -> PulseResult<()> {
113 let mixer = mixer_from_playable(playable)?;
114 self.engine.play_mixer(&mixer).map_err(playback_error)
115 }
116
117 pub fn play_realtime(&self, playable: PulsePlayable<'_>) -> PulseResult<PulsePlaybackHandle> {
123 let mixer = mixer_from_playable(playable)?;
124 let sound_id = self
125 .engine
126 .play_mixer_realtime(&mixer)
127 .map_err(playback_error)?;
128 Ok(PulsePlaybackHandle::from_parts(
129 Rc::clone(&self.engine),
130 sound_id,
131 ))
132 }
133
134 pub fn play_looping(&self, playable: PulsePlayable<'_>) -> PulseResult<PulsePlaybackHandle> {
140 let mixer = mixer_from_playable(playable)?;
141 let sound_id = self.engine.play_looping(&mixer).map_err(playback_error)?;
142 Ok(PulsePlaybackHandle::from_parts(
143 Rc::clone(&self.engine),
144 sound_id,
145 ))
146 }
147
148 pub fn play_at_rate(&self, playable: PulsePlayable<'_>, rate: f32) -> PulseResult<()> {
154 let rate = validate_playback_rate(rate)?;
155 let mixer = mixer_from_playable(playable)?;
156 self.engine
157 .play_mixer_at_rate(&mixer, rate)
158 .map_err(playback_error)
159 }
160
161 pub fn play_realtime_at_rate(
167 &self,
168 playable: PulsePlayable<'_>,
169 rate: f32,
170 ) -> PulseResult<PulsePlaybackHandle> {
171 let rate = validate_playback_rate(rate)?;
172 let mixer = mixer_from_playable(playable)?;
173 let sound_id = self
174 .engine
175 .play_mixer_realtime_at_rate(&mixer, rate)
176 .map_err(playback_error)?;
177 Ok(PulsePlaybackHandle::from_parts(
178 Rc::clone(&self.engine),
179 sound_id,
180 ))
181 }
182}
183
184impl PulsePlaybackHandle {
185 #[must_use]
187 pub fn from_parts(engine: Rc<AudioEngine>, sound_id: SoundId) -> Self {
188 Self { engine, sound_id }
189 }
190
191 #[must_use]
193 pub fn sound_id(&self) -> SoundId {
194 self.sound_id
195 }
196
197 pub fn stop(&self) -> PulseResult<Self> {
203 self.engine.stop(self.sound_id).map_err(playback_error)?;
204 Ok(self.clone())
205 }
206
207 pub fn volume(&self, volume: f32) -> PulseResult<Self> {
213 let volume = validate_playback_volume(volume)?;
214 self.engine
215 .set_volume(self.sound_id, volume)
216 .map_err(playback_error)?;
217 Ok(self.clone())
218 }
219
220 pub fn pan(&self, pan: f32) -> PulseResult<Self> {
226 let pan = validate_playback_pan(pan)?;
227 self.engine
228 .set_pan(self.sound_id, pan)
229 .map_err(playback_error)?;
230 Ok(self.clone())
231 }
232
233 pub fn rate(&self, rate: f32) -> PulseResult<Self> {
239 let rate = validate_playback_rate(rate)?;
240 self.engine
241 .set_playback_rate(self.sound_id, rate)
242 .map_err(playback_error)?;
243 Ok(self.clone())
244 }
245
246 pub fn pause(&self) -> PulseResult<Self> {
252 self.engine.pause(self.sound_id).map_err(playback_error)?;
253 Ok(self.clone())
254 }
255
256 pub fn resume(&self) -> PulseResult<Self> {
262 self.engine.resume(self.sound_id).map_err(playback_error)?;
263 Ok(self.clone())
264 }
265}
266
267#[cfg(test)]
268mod tests {
269 use super::*;
270 use tunes::composition::Tempo;
271
272 #[test]
273 fn playback_rate_rejects_zero_negative_and_non_finite_values() {
274 assert_eq!(
275 validate_playback_rate(0.0)
276 .expect_err("zero rate should fail")
277 .to_string(),
278 "invalid playback rate: 0"
279 );
280 assert_eq!(
281 validate_playback_rate(-1.0)
282 .expect_err("negative rate should fail")
283 .to_string(),
284 "invalid playback rate: -1"
285 );
286 assert_eq!(
287 validate_playback_rate(f32::NAN)
288 .expect_err("NaN rate should fail")
289 .to_string(),
290 "invalid playback rate: NaN"
291 );
292 assert_eq!(validate_playback_rate(1.25).unwrap(), 1.25);
293 }
294
295 #[test]
296 fn playback_volume_accepts_unit_range_only() {
297 assert_eq!(validate_playback_volume(0.0).unwrap(), 0.0);
298 assert_eq!(validate_playback_volume(1.0).unwrap(), 1.0);
299 assert_eq!(
300 validate_playback_volume(1.5)
301 .expect_err("too-loud volume should fail")
302 .to_string(),
303 "invalid playback volume: 1.5"
304 );
305 assert_eq!(
306 validate_playback_volume(f32::INFINITY)
307 .expect_err("infinite volume should fail")
308 .to_string(),
309 "invalid playback volume: inf"
310 );
311 }
312
313 #[test]
314 fn playback_pan_accepts_stereo_range_only() {
315 assert_eq!(validate_playback_pan(-1.0).unwrap(), -1.0);
316 assert_eq!(validate_playback_pan(1.0).unwrap(), 1.0);
317 assert_eq!(
318 validate_playback_pan(2.0)
319 .expect_err("pan outside stereo range should fail")
320 .to_string(),
321 "invalid playback pan: 2"
322 );
323 assert_eq!(
324 validate_playback_pan(f32::NEG_INFINITY)
325 .expect_err("non-finite pan should fail")
326 .to_string(),
327 "invalid playback pan: -inf"
328 );
329 }
330
331 #[test]
332 fn playback_handle_exposes_sound_id() {
333 let engine = Rc::new(AudioEngine::new().expect("audio engine should initialize"));
334 let handle = PulsePlaybackHandle::from_parts(engine, 42);
335
336 assert_eq!(handle.sound_id(), 42);
337 }
338
339 #[test]
340 fn playable_song_builds_a_mixer_without_playing_audio() {
341 let song = PulseSong::new();
342
343 let mixer = mixer_from_playable(PulsePlayable::Song(&song));
344
345 assert!(mixer.is_ok());
346 }
347
348 #[test]
349 fn playable_imported_midi_clones_its_mixer_without_playing_audio() {
350 let imported = PulseImportedMidi::new(Mixer::new(Tempo::new(120.0)));
351
352 let mixer = mixer_from_playable(PulsePlayable::ImportedMidi(&imported));
353
354 assert!(mixer.is_ok());
355 }
356
357 #[test]
358 fn playable_song_preserves_master_effects_in_mixer() {
359 let effect =
360 crate::effects::effect_from_options("eq", crate::effects::EffectOptions::Default)
361 .expect("default eq should parse");
362 let song = PulseSong::new().with_master_effect(effect);
363
364 let mixer = mixer_from_playable(PulsePlayable::Song(&song))
365 .expect("song with master effect should build mixer");
366
367 assert!(mixer.master.eq.is_some());
368 }
369
370 #[test]
371 fn playback_engine_can_be_constructed_without_playing_audio() {
372 let engine = PulsePlaybackEngine::new();
373
374 assert!(engine.is_ok());
375 }
376
377 #[test]
378 fn handle_control_methods_validate_arguments_before_forwarding() {
379 let engine = Rc::new(AudioEngine::new().expect("audio engine should initialize"));
380 let handle = PulsePlaybackHandle::from_parts(engine, 99);
381
382 assert_eq!(
383 handle
384 .volume(1.5)
385 .expect_err("invalid volume should fail before engine call")
386 .to_string(),
387 "invalid playback volume: 1.5"
388 );
389 assert_eq!(
390 handle
391 .pan(2.0)
392 .expect_err("invalid pan should fail before engine call")
393 .to_string(),
394 "invalid playback pan: 2"
395 );
396 assert_eq!(
397 handle
398 .rate(0.0)
399 .expect_err("invalid rate should fail before engine call")
400 .to_string(),
401 "invalid playback rate: 0"
402 );
403 }
404
405 #[test]
406 fn engine_rate_methods_validate_arguments_before_playing() {
407 let engine = PulsePlaybackEngine::new().expect("playback engine should initialize");
408 let song = PulseSong::new();
409
410 assert_eq!(
411 engine
412 .play_at_rate(PulsePlayable::Song(&song), 0.0)
413 .expect_err("invalid rate should fail before playback")
414 .to_string(),
415 "invalid playback rate: 0"
416 );
417 assert_eq!(
418 engine
419 .play_realtime_at_rate(PulsePlayable::Song(&song), f32::INFINITY)
420 .expect_err("invalid rate should fail before playback")
421 .to_string(),
422 "invalid playback rate: inf"
423 );
424 }
425
426 #[test]
427 fn playback_handle_invalid_controls_are_chainable_lua_methods() {
428 let lua = mlua::Lua::new();
429 let engine = Rc::new(AudioEngine::new().expect("audio engine should initialize"));
430 let handle = PulsePlaybackHandle::from_parts(engine, 7);
431
432 lua.globals().set("handle", handle).unwrap();
433 lua.load(
434 r#"
435 assert(type(handle.stop) == "function")
436 assert(type(handle.volume) == "function")
437 assert(type(handle.pan) == "function")
438 assert(type(handle.rate) == "function")
439 assert(type(handle.pause) == "function")
440 assert(type(handle.resume) == "function")
441
442 local ok_volume = pcall(function()
443 handle:volume(1.5)
444 end)
445 assert(ok_volume == false)
446
447 local ok_pan = pcall(function()
448 handle:pan(2)
449 end)
450 assert(ok_pan == false)
451
452 local ok_rate = pcall(function()
453 handle:rate(0)
454 end)
455 assert(ok_rate == false)
456 "#,
457 )
458 .exec()
459 .expect("handle methods should be visible and validate before engine calls");
460 }
461}