Skip to main content

pfx_editor/
audio.rs

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}