1use pfx_play::PlaySession;
2use pfx_sound::{ChannelId, Level, Output, PcmSender};
3
4use crate::log::Log;
5
6pub const AHEAD_SECONDS: f32 = 0.1;
7pub const PRIME_SECONDS: f32 = 0.05;
8pub const DEFAULT_RATE: u32 = 48_000;
9pub const DEFAULT_CHANNELS: u16 = 2;
10
11pub trait Stream {
12 fn push(&mut self, samples: &[f32]) -> usize;
13 fn queued_frames(&self) -> usize;
14 fn finish(&mut self);
15}
16
17pub trait Sink {
18 fn rate(&self) -> u32;
19 fn channels(&self) -> u16;
20 fn stream(&mut self, seconds: f32) -> Box<dyn Stream>;
21 fn set_muted(&mut self, muted: bool);
22}
23
24pub type Opener = Box<dyn FnMut() -> Result<Box<dyn Sink>, String>>;
25
26struct Device {
27 output: Output,
28}
29
30struct Pcm {
31 sender: PcmSender,
32}
33
34impl Stream for Pcm {
35 fn push(&mut self, samples: &[f32]) -> usize {
36 self.sender.push(samples)
37 }
38
39 fn queued_frames(&self) -> usize {
40 self.sender.queued_frames()
41 }
42
43 fn finish(&mut self) {
44 self.sender.finish();
45 }
46}
47
48impl Sink for Device {
49 fn rate(&self) -> u32 {
50 self.output.rate()
51 }
52
53 fn channels(&self) -> u16 {
54 self.output.channels()
55 }
56
57 fn stream(&mut self, seconds: f32) -> Box<dyn Stream> {
58 let (_, sender) = self.output.pcm_stream(
59 ChannelId::EFFECTS,
60 self.output.channels(),
61 seconds,
62 Level::default(),
63 );
64 Box::new(Pcm { sender })
65 }
66
67 fn set_muted(&mut self, muted: bool) {
68 self.output.set_master_muted(muted);
69 }
70}
71
72pub fn open_device() -> Result<Box<dyn Sink>, String> {
73 Ok(Box::new(Device {
74 output: Output::open()?,
75 }))
76}
77
78enum Source {
79 Off,
80 Unopened(Opener),
81 Ready(Box<dyn Sink>),
82 Missing,
83}
84
85#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
86pub struct Stats {
87 pub dropped_frames: u64,
88 pub underruns: u32,
89}
90
91pub struct Audio {
92 source: Source,
93 stream: Option<Box<dyn Stream>>,
94 muted: bool,
95 pending: Vec<f32>,
96 primed: bool,
97 pub stats: Stats,
98}
99
100impl Audio {
101 pub fn off() -> Audio {
102 Audio::from(Source::Off)
103 }
104
105 pub fn device() -> Audio {
106 Audio::with_opener(Box::new(open_device))
107 }
108
109 pub fn with_opener(opener: Opener) -> Audio {
110 Audio::from(Source::Unopened(opener))
111 }
112
113 fn from(source: Source) -> Audio {
114 Audio {
115 source,
116 stream: None,
117 muted: false,
118 pending: Vec::new(),
119 primed: false,
120 stats: Stats::default(),
121 }
122 }
123
124 pub fn muted(&self) -> bool {
125 self.muted
126 }
127
128 pub fn set_muted(&mut self, muted: bool) {
129 self.muted = muted;
130 if let Source::Ready(sink) = &mut self.source {
131 sink.set_muted(muted);
132 }
133 }
134
135 pub fn available(&self) -> bool {
136 matches!(self.source, Source::Unopened(_) | Source::Ready(_))
137 }
138
139 pub fn has_device(&self) -> bool {
140 matches!(self.source, Source::Ready(_))
141 }
142
143 pub fn streaming(&self) -> bool {
144 self.stream.is_some()
145 }
146
147 fn format(&self) -> Option<(u32, u16)> {
148 match &self.source {
149 Source::Ready(sink) => Some((sink.rate().max(1), sink.channels().max(1))),
150 _ => None,
151 }
152 }
153
154 pub fn begin(&mut self, log: &mut Log) -> (u32, u16) {
155 if let Source::Unopened(opener) = &mut self.source {
156 self.source = match opener() {
157 Ok(mut sink) => {
158 sink.set_muted(self.muted);
159 Source::Ready(sink)
160 }
161 Err(error) => {
162 log.warn(format!("sound: {error}; play runs silent"));
163 Source::Missing
164 }
165 };
166 }
167 self.stream = None;
168 self.pending.clear();
169 self.primed = false;
170 self.stats = Stats::default();
171 let Some(format) = self.format() else {
172 return (DEFAULT_RATE, DEFAULT_CHANNELS);
173 };
174 if let Source::Ready(sink) = &mut self.source {
175 self.stream = Some(sink.stream(AHEAD_SECONDS));
176 }
177 format
178 }
179
180 fn frames(&self, seconds: f32) -> usize {
181 let rate = self.format().map_or(DEFAULT_RATE, |format| format.0);
182 ((rate as f32 * seconds).round() as usize).max(1)
183 }
184
185 fn channels(&self) -> usize {
186 usize::from(self.format().map_or(DEFAULT_CHANNELS, |format| format.1))
187 }
188
189 pub fn push(&mut self, samples: &[f32]) {
190 self.feed(samples, false);
191 }
192
193 fn feed(&mut self, samples: &[f32], flush: bool) {
194 let limit = self.frames(AHEAD_SECONDS);
195 let prime = self.frames(PRIME_SECONDS);
196 let channels = self.channels();
197 let Some(stream) = self.stream.as_mut() else {
198 return;
199 };
200 self.pending.extend_from_slice(samples);
201 let whole = self.pending.len() / channels * channels;
202 self.pending.truncate(whole);
203 if self.primed && stream.queued_frames() == 0 && !self.pending.is_empty() {
204 self.stats.underruns += 1;
205 self.primed = false;
206 }
207 let waiting = self.pending.len() / channels;
208 if !flush && !self.primed {
209 if waiting < prime {
210 self.trim(limit, channels);
211 return;
212 }
213 self.primed = true;
214 }
215 let room = if flush {
216 usize::MAX
217 } else {
218 limit.saturating_sub(stream.queued_frames())
219 };
220 let offer = waiting.min(room);
221 let taken = stream.push(&self.pending[..offer * channels]);
222 self.pending.drain(..taken * channels);
223 self.trim(limit, channels);
224 }
225
226 fn trim(&mut self, limit: usize, channels: usize) {
227 let frames = self.pending.len() / channels;
228 if frames > limit {
229 let excess = frames - limit;
230 self.pending.drain(..excess * channels);
231 self.stats.dropped_frames += excess as u64;
232 }
233 }
234
235 pub fn advance(&mut self, session: &mut PlaySession) {
236 let samples = session.take_audio();
237 self.push(&samples);
238 }
239
240 pub fn pause(&mut self, session: &mut PlaySession) {
241 let samples = session.take_audio();
242 self.feed(&samples, true);
243 self.primed = false;
244 }
245
246 pub fn abandon(&mut self) {
247 if let Some(mut stream) = self.stream.take() {
248 stream.finish();
249 }
250 self.pending.clear();
251 self.primed = false;
252 }
253
254 pub fn resume(&mut self) {
255 self.primed = false;
256 }
257
258 pub fn step(&mut self, session: &mut PlaySession) {
259 let samples = session.take_audio();
260 self.feed(&samples, true);
261 self.primed = false;
262 }
263
264 pub fn stop(&mut self, session: &mut PlaySession) {
265 let samples = session.take_audio();
266 self.feed(&samples, true);
267 if let Some(mut stream) = self.stream.take() {
268 stream.finish();
269 }
270 self.stats.dropped_frames += (self.pending.len() / self.channels()) as u64;
271 self.pending.clear();
272 self.primed = false;
273 }
274
275 pub fn line(&self) -> Option<String> {
276 let mut parts = Vec::new();
277 if self.muted {
278 parts.push("muted".to_string());
279 }
280 if self.stats.underruns > 0 {
281 parts.push(format!("{} underruns", self.stats.underruns));
282 }
283 if self.stats.dropped_frames > 0 {
284 parts.push(format!("{} frames dropped", self.stats.dropped_frames));
285 }
286 (!parts.is_empty()).then(|| parts.join(" ยท "))
287 }
288}
289
290#[cfg(test)]
291mod tests {
292 use std::cell::RefCell;
293 use std::fs;
294 use std::rc::Rc;
295
296 use pfx_play::{Options, PlaySession, SceneGame};
297 use pfx_sound::Clip;
298
299 use super::*;
300 use crate::editor::Editor;
301 use crate::tests::scene_copy;
302
303 const RATE: u32 = 48_000;
304 const CHANNELS: u16 = 2;
305 const TICK_FRAMES: usize = 800;
306 const FRAME: f32 = 1.0 / 60.0;
307
308 #[derive(Default)]
309 struct Shared {
310 opened: usize,
311 streams: usize,
312 pushed: Vec<f32>,
313 finished: usize,
314 queued: usize,
315 capacity: usize,
316 muted: Vec<bool>,
317 }
318
319 type Handle = Rc<RefCell<Shared>>;
320
321 struct Stand(Handle);
322
323 struct StandStream(Handle);
324
325 impl Sink for Stand {
326 fn rate(&self) -> u32 {
327 RATE
328 }
329
330 fn channels(&self) -> u16 {
331 CHANNELS
332 }
333
334 fn stream(&mut self, seconds: f32) -> Box<dyn Stream> {
335 let mut shared = self.0.borrow_mut();
336 shared.streams += 1;
337 shared.queued = 0;
338 shared.capacity = (RATE as f32 * seconds) as usize;
339 Box::new(StandStream(Rc::clone(&self.0)))
340 }
341
342 fn set_muted(&mut self, muted: bool) {
343 self.0.borrow_mut().muted.push(muted);
344 }
345 }
346
347 impl Stream for StandStream {
348 fn push(&mut self, samples: &[f32]) -> usize {
349 let mut shared = self.0.borrow_mut();
350 let room = shared.capacity.saturating_sub(shared.queued);
351 let frames = (samples.len() / usize::from(CHANNELS)).min(room);
352 shared
353 .pushed
354 .extend_from_slice(&samples[..frames * usize::from(CHANNELS)]);
355 shared.queued += frames;
356 frames
357 }
358
359 fn queued_frames(&self) -> usize {
360 self.0.borrow().queued
361 }
362
363 fn finish(&mut self) {
364 self.0.borrow_mut().finished += 1;
365 }
366 }
367
368 fn stand() -> (Audio, Handle) {
369 let handle = Handle::default();
370 let opener = Rc::clone(&handle);
371 let audio = Audio::with_opener(Box::new(move || {
372 opener.borrow_mut().opened += 1;
373 Ok(Box::new(Stand(Rc::clone(&opener))) as Box<dyn Sink>)
374 }));
375 (audio, handle)
376 }
377
378 fn drain(handle: &Handle, frames: usize) {
379 let mut shared = handle.borrow_mut();
380 shared.queued = shared.queued.saturating_sub(frames);
381 }
382
383 fn scene(name: &str) -> pfx_load::scene::Scene {
384 let path = scene_copy(name);
385 let tone = Clip {
386 rate: RATE,
387 channels: 1,
388 samples: (0..4_800)
389 .map(|i| ((i as f32) * 0.07).sin() * 0.5)
390 .collect(),
391 };
392 fs::write(path.with_file_name("tone.wav"), tone.to_wav()).unwrap();
393 let mut text = fs::read_to_string(&path).unwrap();
394 text.push_str("\n[sound.hum]\nfile = \"tone.wav\"\nloop = true\nvolume = 0.5\n");
395 fs::write(&path, text).unwrap();
396 Editor::open(&path).unwrap().scene().clone()
397 }
398
399 fn play(scene: &pfx_load::scene::Scene) -> PlaySession {
400 PlaySession::play_with(
401 scene,
402 None,
403 Box::new(SceneGame),
404 Options {
405 audio_rate: RATE,
406 audio_channels: CHANNELS,
407 ..Options::default()
408 },
409 )
410 .unwrap()
411 }
412
413 #[test]
414 fn the_frames_pushed_match_the_sessions_audio_in_order_across_play_pause_step_resume_and_stop()
415 {
416 let scene = scene("audio order");
417 let (mut audio, handle) = stand();
418 let mut log = Log::default();
419 assert_eq!(audio.begin(&mut log), (RATE, CHANNELS));
420 assert!(log.entries.is_empty());
421 let mut session = play(&scene);
422 let mut twin = play(&scene);
423 let mut heard = Vec::new();
424
425 let run = |session: &mut PlaySession,
426 twin: &mut PlaySession,
427 audio: &mut Audio,
428 heard: &mut Vec<f32>,
429 frames: usize| {
430 for _ in 0..frames {
431 session.advance(FRAME);
432 twin.advance(FRAME);
433 audio.advance(session);
434 heard.extend(twin.take_audio());
435 drain(&handle, TICK_FRAMES);
436 }
437 };
438
439 run(&mut session, &mut twin, &mut audio, &mut heard, 12);
440 session.pause();
441 twin.pause();
442 audio.pause(&mut session);
443 heard.extend(twin.take_audio());
444 let paused_at = handle.borrow().pushed.len();
445 assert_eq!(paused_at, heard.len());
446 assert!(paused_at > 0);
447
448 run(&mut session, &mut twin, &mut audio, &mut heard, 5);
449 assert_eq!(handle.borrow().pushed.len(), paused_at, "pause is silent");
450
451 for step in 1..=2 {
452 assert!(session.step() && twin.step());
453 audio.step(&mut session);
454 heard.extend(twin.take_audio());
455 assert_eq!(
456 handle.borrow().pushed.len(),
457 paused_at + step * TICK_FRAMES * usize::from(CHANNELS),
458 "a step pushes its tick's audio"
459 );
460 }
461
462 session.resume();
463 twin.resume();
464 audio.resume();
465 run(&mut session, &mut twin, &mut audio, &mut heard, 9);
466
467 session.advance(FRAME);
468 twin.advance(FRAME);
469 audio.stop(&mut session);
470 heard.extend(twin.take_audio());
471
472 let shared = handle.borrow();
473 assert_eq!(shared.pushed, heard);
474 assert!(heard.iter().any(|sample| sample.abs() > 0.01));
475 assert_eq!(shared.finished, 1);
476 assert_eq!(audio.stats, Stats::default());
477 assert!(!audio.streaming());
478 }
479
480 #[test]
481 fn play_again_opens_a_fresh_stream_on_the_same_device() {
482 let (mut audio, handle) = stand();
483 let mut log = Log::default();
484 audio.begin(&mut log);
485 audio.abandon();
486 audio.begin(&mut log);
487 let shared = handle.borrow();
488 assert_eq!(shared.opened, 1);
489 assert_eq!(shared.streams, 2);
490 assert_eq!(shared.finished, 1);
491 assert!(audio.has_device() && audio.streaming());
492 }
493
494 #[test]
495 fn no_device_logs_once_and_plays_silent() {
496 let scene = scene("audio silent");
497 let mut audio = Audio::with_opener(Box::new(|| Err("no output device".to_string())));
498 let mut log = Log::default();
499 for _ in 0..2 {
500 let (rate, channels) = audio.begin(&mut log);
501 assert_eq!((rate, channels), (DEFAULT_RATE, DEFAULT_CHANNELS));
502 let mut session = PlaySession::play_with(
503 &scene,
504 None,
505 Box::new(SceneGame),
506 Options {
507 audio_rate: rate,
508 audio_channels: channels,
509 ..Options::default()
510 },
511 )
512 .unwrap();
513 for _ in 0..5 {
514 session.advance(FRAME);
515 audio.advance(&mut session);
516 assert!(session.take_audio().is_empty());
517 }
518 audio.stop(&mut session);
519 }
520 assert_eq!(log.entries.len(), 1);
521 assert!(log.last().unwrap().text.contains("no output device"));
522 assert!(!audio.has_device() && !audio.streaming());
523 assert_eq!(audio.stats, Stats::default());
524 }
525
526 #[test]
527 fn an_off_audio_logs_nothing() {
528 let mut audio = Audio::off();
529 let mut log = Log::default();
530 assert_eq!(audio.begin(&mut log), (DEFAULT_RATE, DEFAULT_CHANNELS));
531 audio.push(&[0.5; 64]);
532 assert!(log.entries.is_empty());
533 assert_eq!(audio.line(), None);
534 }
535
536 #[test]
537 fn a_full_stream_drops_the_oldest_frames_and_counts_them() {
538 let (mut audio, handle) = stand();
539 audio.begin(&mut Log::default());
540 let chunk: Vec<f32> = (0..TICK_FRAMES * 2).map(|i| i as f32).collect();
541 for _ in 0..60 {
542 audio.push(&chunk);
543 }
544 let ahead = (RATE as f32 * AHEAD_SECONDS) as usize;
545 assert_eq!(handle.borrow().queued, ahead);
546 assert_eq!(
547 audio.stats.dropped_frames as usize,
548 60 * TICK_FRAMES - 2 * ahead
549 );
550 assert_eq!(audio.stats.underruns, 0);
551 assert_eq!(
552 audio.line().unwrap(),
553 format!("{} frames dropped", audio.stats.dropped_frames)
554 );
555 }
556
557 #[test]
558 fn a_dry_stream_counts_an_underrun_and_primes_again() {
559 let (mut audio, handle) = stand();
560 audio.begin(&mut Log::default());
561 let chunk = vec![0.25; TICK_FRAMES * 2 * usize::from(CHANNELS)];
562 for _ in 0..2 {
563 audio.push(&chunk);
564 }
565 assert_eq!(handle.borrow().queued, TICK_FRAMES * 4);
566 drain(&handle, usize::MAX);
567 audio.push(&chunk);
568 assert_eq!(audio.stats.underruns, 1);
569 assert_eq!(handle.borrow().queued, 0);
570 audio.push(&chunk);
571 assert!(handle.borrow().queued > 0);
572 assert_eq!(audio.line().unwrap(), "1 underruns");
573 }
574
575 #[test]
576 fn mute_reaches_the_device_before_and_after_it_opens() {
577 let (mut audio, handle) = stand();
578 audio.set_muted(true);
579 assert!(handle.borrow().muted.is_empty());
580 audio.begin(&mut Log::default());
581 audio.set_muted(false);
582 assert_eq!(handle.borrow().muted, [true, false]);
583 assert!(!audio.muted());
584 audio.set_muted(true);
585 assert_eq!(audio.line().unwrap(), "muted");
586 }
587
588 struct Singer {
589 stream: Option<pfx_play::PcmStream>,
590 phase: u64,
591 }
592
593 impl pfx_play::Game for Singer {
594 fn start(&mut self, world: &mut pfx_play::World) {
595 self.stream = Some(world.sounds.stream(
596 pfx_sound::ChannelId::MUSIC,
597 1,
598 0.1,
599 pfx_sound::Level::default(),
600 ));
601 }
602
603 fn tick(
604 &mut self,
605 world: &mut pfx_play::World,
606 _tick: &pfx_core::clock::Tick,
607 _input: &pfx_input::Input,
608 ) {
609 let due = world.audio_due();
610 let rate = world.sounds.rate() as f32;
611 let samples: Vec<f32> = (0..due)
612 .map(|i| {
613 let t = (self.phase + i as u64) as f32 / rate;
614 (t * std::f32::consts::TAU * 330.0).sin() * 0.5
615 })
616 .collect();
617 self.phase += due as u64;
618 if let Some(stream) = &self.stream {
619 stream.push(&samples);
620 }
621 }
622 }
623
624 fn singing(audio_rate: u32, audio_channels: u16) -> pfx_game::Driver {
625 let session = PlaySession::play_with(
626 &pfx_load::scene::Scene::empty(),
627 None,
628 Box::new(Singer {
629 stream: None,
630 phase: 0,
631 }),
632 Options {
633 audio_rate,
634 audio_channels,
635 tunables: Some(pfx_play::Tunables::default()),
636 rigs: Some(pfx_play::Rigs::default()),
637 ..Options::default()
638 },
639 )
640 .unwrap();
641 pfx_game::Driver::new(
642 session,
643 pfx_play::Settings::default(),
644 crate::session::free_policy(),
645 pfx_gpu::screens::Device::Desktop,
646 pfx_gpu::window::Size {
647 width: 640,
648 height: 360,
649 },
650 None,
651 )
652 }
653
654 #[test]
655 fn a_games_own_pcm_stream_plays_through_the_world_mixer_in_the_editors_play() {
656 let (mut audio, handle) = stand();
657 let (rate, channels) = audio.begin(&mut Log::default());
658 let mut played = singing(rate, channels);
659 let mut twin = singing(rate, channels);
660 let mut heard = Vec::new();
661 let frame_ns = 16_666_667;
662 for frame in 0..30u64 {
663 played.update(frame * frame_ns);
664 twin.update(frame * frame_ns);
665 audio.advance(played.session_mut());
666 heard.extend(twin.take_audio());
667 drain(&handle, TICK_FRAMES);
668 }
669 audio.stop(played.session_mut());
670 heard.extend(twin.take_audio());
671 let shared = handle.borrow();
672 assert_eq!(shared.pushed, heard, "the same mix as a run of its own");
673 assert!(played.ticks() >= 28, "{} ticks", played.ticks());
674 let loud = shared
675 .pushed
676 .iter()
677 .fold(0.0f32, |peak, sample| peak.max(sample.abs()));
678 assert!(loud > 0.3, "the game's sine is heard: peak {loud}");
679 assert_eq!(audio.stats, Stats::default());
680 }
681}