Skip to main content

shuyi_playdate/
audio.rs

1//! [`Speaker`]: the shared player's [`Backend`] on the SDK's sample players
2//! (sounds in memory, each playback a player of its own) and file players
3//! (music and voice clips streamed from the `.pdx`).
4//!
5//! `shuyi export playdate` bakes each sound under
6//! [`BAKED_SOUND_DIR`] and each track under [`BAKED_MUSIC_DIR`], and the SDK
7//! opens a file by its path without the extension. A track the game doesn't
8//! ship there is silence, with the player's warning.
9//!
10//! The SDK can pause both kinds of player and set their rates, so this keeps
11//! no default of [`Backend`]'s. A sound the game makes at run time is a
12//! sample the SDK makes from a copy of its samples, freed when it is
13//! removed.
14
15use alloc::borrow::ToOwned;
16use alloc::collections::BTreeMap;
17use alloc::format;
18use alloc::string::String;
19use alloc::vec::Vec;
20use core::cell::Cell;
21use core::future::{Future, ready};
22use core::pin::pin;
23use core::task::{Context, Poll, Waker};
24
25use shuyi_assets::audio::{
26    AudioManifest, BAKED_MUSIC_DIR, BAKED_SOUND_DIR, Files, without_extension,
27};
28use shuyi_platform::audio::player::{Backend, Player, RATE_ONE, Unsupported};
29use shuyi_ui::audio::SharedSamples;
30
31use crate::sdk::Sdk;
32
33/// How many sounds play at once: one sample player each, made at the start.
34/// A sound asked for while all play stops one of them, each in turn.
35pub const SAMPLE_PLAYERS: usize = 8;
36
37/// Where the voice clips would be in the `.pdx`: the export bakes none yet.
38const VOICE_DIR: &str = "voice";
39
40/// A rate of [`Backend::set_rate`]'s as the SDK takes it.
41fn rate_factor(rate: u32) -> f32 {
42    // A rate is small: the precision lost is far below hearing.
43    #[allow(clippy::cast_precision_loss)]
44    let rate = rate as f32;
45    #[allow(clippy::cast_precision_loss)]
46    let one = RATE_ONE as f32;
47    rate / one
48}
49
50/// One sample player.
51#[derive(Debug)]
52struct Slot<S: Sdk> {
53    player: S::SamplePlayer,
54    /// What it last played.
55    sample: Option<S::Sample>,
56    /// Which playback it is on: an old playback's handle does nothing.
57    serial: u32,
58    /// Whether it is paused: it can't be taken for another sound.
59    paused: bool,
60}
61
62/// The SDK's speaker.
63#[derive(Debug)]
64pub struct Speaker<S: Sdk> {
65    sdk: S,
66    slots: Vec<Slot<S>>,
67    /// The samples loaded, by path: cues that share a file share its memory.
68    loaded: BTreeMap<String, S::Sample>,
69    /// The samples made from a game's own samples, freed when removed.
70    made: Vec<S::Sample>,
71    /// The serial of the last playback.
72    serial: u32,
73    /// The slot a sound takes when all play: they are taken in turn.
74    oldest: usize,
75}
76
77/// A sound the SDK holds, or a file player open on a track or a clip.
78#[derive(Debug)]
79pub enum Clip<S: Sdk> {
80    /// A sound in memory, for the whole game.
81    Sample {
82        /// The SDK's sample.
83        sample: S::Sample,
84        /// Its rate from its next play, 16.16.
85        rate: Cell<u32>,
86    },
87    /// A file player, closed when this goes.
88    File(FileClip<S>),
89}
90
91/// A file player open on its file: dropping it stops and frees it, which is
92/// how a track the player lets go of leaves the memory.
93#[derive(Debug)]
94pub struct FileClip<S: Sdk> {
95    sdk: S,
96    player: S::FilePlayer,
97    /// Whether it has played: playing again starts it from the top.
98    played: Cell<bool>,
99    /// Its rate from its next play, 16.16.
100    rate: Cell<u32>,
101}
102
103impl<S: Sdk> Drop for FileClip<S> {
104    fn drop(&mut self) {
105        self.sdk.file_close(self.player);
106    }
107}
108
109/// One playback.
110#[derive(Debug)]
111pub enum Playing<S: Sdk> {
112    /// Nothing plays: the SDK has no sample player to give.
113    Nothing,
114    /// A sample player, on playback `serial`.
115    Sample {
116        /// Which of the speaker's sample players.
117        slot: usize,
118        /// Which playback.
119        serial: u32,
120    },
121    /// A file player.
122    File {
123        /// The file player.
124        player: S::FilePlayer,
125        /// Whether it loops, which its resume says again.
126        looped: bool,
127        /// Whether it is paused: a track that ended has nothing to resume.
128        paused: Cell<bool>,
129    },
130}
131
132impl<S: Sdk> Speaker<S> {
133    /// The speaker on `sdk`, with its sample players made.
134    pub fn new(sdk: S) -> Self {
135        let slots = (0..SAMPLE_PLAYERS)
136            .filter_map(|_| sdk.sample_player_new())
137            .map(|player| Slot {
138                player,
139                sample: None,
140                serial: 0,
141                paused: false,
142            })
143            .collect();
144        Self {
145            sdk,
146            slots,
147            loaded: BTreeMap::new(),
148            made: Vec::new(),
149            serial: 0,
150            oldest: 0,
151        }
152    }
153
154    /// The slot for a new sound: one that is free, else the next in turn that
155    /// isn't paused. `None` if all are paused.
156    fn free_slot(&mut self) -> Option<usize> {
157        let free = self
158            .slots
159            .iter()
160            .position(|slot| !slot.paused && !self.sdk.sample_player_is_playing(slot.player));
161        if free.is_some() {
162            return free;
163        }
164        let count = self.slots.len();
165        let taken = (0..count)
166            .map(|turn| (self.oldest + turn) % count)
167            .find(|&at| !self.slots[at].paused)?;
168        self.oldest = taken + 1;
169        Some(taken)
170    }
171
172    /// The slot of `playing`, if it is still on that playback.
173    fn slot_of(&mut self, playing: &Playing<S>) -> Option<&mut Slot<S>> {
174        match playing {
175            Playing::Sample { slot, serial } => {
176                self.slots.get_mut(*slot).filter(|at| at.serial == *serial)
177            }
178            _ => None,
179        }
180    }
181
182    /// The sample players on `sample`.
183    fn slots_on(&self, sample: S::Sample) -> impl Iterator<Item = &Slot<S>> {
184        self.slots
185            .iter()
186            .filter(move |slot| slot.sample == Some(sample))
187    }
188}
189
190impl<S: Sdk> Backend for Speaker<S> {
191    type Sound = Clip<S>;
192    type Loading = Option<Result<Clip<S>, String>>;
193    type Playing = Playing<S>;
194
195    /// The SDK loads a sound by its path ([`load_sound_file`](Self::load_sound_file)):
196    /// it is baked to a format only `pdc` writes, and no bytes come here.
197    fn load_sound(&mut self, _bytes: &[u8]) -> impl Future<Output = Result<Clip<S>, String>> {
198        // check-text: not player text (for the log)
199        ready(Err("the Playdate loads a sound from its path".to_owned()))
200    }
201
202    fn load_sound_file(
203        &mut self,
204        file: &str,
205        _files: &Files<'_>,
206    ) -> impl Future<Output = Result<Clip<S>, String>> {
207        let path = format!("{BAKED_SOUND_DIR}/{}", without_extension(file));
208        let sample = match self.loaded.get(&path) {
209            Some(sample) => Some(*sample),
210            None => self.sdk.sample_load(&path),
211        };
212        ready(match sample {
213            Some(sample) => {
214                self.loaded.insert(path, sample);
215                Ok(Clip::Sample {
216                    sample,
217                    rate: Cell::new(RATE_ONE),
218                })
219            }
220            // check-text: not player text (for the log)
221            None => Err("not in the .pdx".to_owned()),
222        })
223    }
224
225    fn load_music(&mut self, path: &str) -> Self::Loading {
226        let opened = self.sdk.file_open(without_extension(path));
227        Some(match opened {
228            Some(player) => Ok(Clip::File(FileClip {
229                sdk: self.sdk.clone(),
230                player,
231                played: Cell::new(false),
232                rate: Cell::new(RATE_ONE),
233            })),
234            // check-text: not player text (for the log)
235            None => Err("not in the .pdx".to_owned()),
236        })
237    }
238
239    fn poll_music(&mut self, loading: &mut Self::Loading) -> Option<Result<Clip<S>, String>> {
240        loading.take()
241    }
242
243    fn length(&self, sound: &Clip<S>) -> Option<f64> {
244        let seconds = match sound {
245            Clip::Sample { sample, .. } => self.sdk.sample_length(*sample),
246            Clip::File(file) => self.sdk.file_length(file.player),
247        };
248        (seconds > 0.0).then(|| f64::from(seconds))
249    }
250
251    fn play(&mut self, sound: &Clip<S>, volume: f32, looped: bool) -> Playing<S> {
252        match sound {
253            Clip::Sample { sample, rate } => {
254                let Some(slot) = self.free_slot() else {
255                    return Playing::Nothing;
256                };
257                self.serial = self.serial.wrapping_add(1);
258                let at = &mut self.slots[slot];
259                at.sample = Some(*sample);
260                at.serial = self.serial;
261                let factor = rate_factor(rate.get());
262                self.sdk
263                    .sample_player_play(at.player, *sample, volume, looped, factor);
264                Playing::Sample {
265                    slot,
266                    serial: self.serial,
267                }
268            }
269            Clip::File(file) => {
270                if file.played.replace(true) {
271                    self.sdk.file_stop(file.player);
272                    self.sdk.file_rewind(file.player);
273                }
274                self.sdk
275                    .file_set_rate(file.player, rate_factor(file.rate.get()));
276                self.sdk.file_set_volume(file.player, volume);
277                if !self.sdk.file_play(file.player, looped) {
278                    // check-text: not player text (for the log)
279                    self.sdk.log("audio: the SDK could not start a track");
280                }
281                Playing::File {
282                    player: file.player,
283                    looped,
284                    paused: Cell::new(false),
285                }
286            }
287        }
288    }
289
290    fn set_volume(&mut self, sound: &Clip<S>, volume: f32) {
291        match sound {
292            Clip::Sample { sample, .. } => {
293                for slot in self.slots_on(*sample) {
294                    self.sdk.sample_player_set_volume(slot.player, volume);
295                }
296            }
297            Clip::File(file) => self.sdk.file_set_volume(file.player, volume),
298        }
299    }
300
301    fn stop(&mut self, sound: &Clip<S>) {
302        match sound {
303            Clip::Sample { sample, .. } => {
304                for slot in self.slots_on(*sample) {
305                    self.sdk.sample_player_stop(slot.player);
306                }
307            }
308            Clip::File(file) => self.sdk.file_stop(file.player),
309        }
310    }
311
312    fn set_playing_volume(&mut self, playing: &Playing<S>, volume: f32) {
313        if let Playing::File { player, .. } = playing {
314            self.sdk.file_set_volume(*player, volume);
315        } else if let Some(player) = self.slot_of(playing).map(|slot| slot.player) {
316            self.sdk.sample_player_set_volume(player, volume);
317        }
318    }
319
320    fn stop_playing(&mut self, playing: Playing<S>) {
321        if let Playing::File { player, .. } = playing {
322            self.sdk.file_stop(player);
323        } else if let Some(slot) = self.slot_of(&playing) {
324            slot.paused = false;
325            let player = slot.player;
326            self.sdk.sample_player_stop(player);
327        }
328    }
329
330    fn pause(&mut self, playing: &Playing<S>) -> Result<(), Unsupported> {
331        if let Playing::File { player, paused, .. } = playing {
332            if self.sdk.file_is_playing(*player) {
333                self.sdk.file_pause(*player);
334                paused.set(true);
335            }
336        } else if let Some(player) = self.slot_of(playing).map(|slot| slot.player)
337            && self.sdk.sample_player_is_playing(player)
338        {
339            self.sdk.sample_player_set_paused(player, true);
340            if let Some(slot) = self.slot_of(playing) {
341                slot.paused = true;
342            }
343        }
344        Ok(())
345    }
346
347    fn resume(&mut self, playing: &Playing<S>) -> Result<(), Unsupported> {
348        if let Playing::File {
349            player,
350            looped,
351            paused,
352        } = playing
353        {
354            // The SDK goes on from where its pause left the file.
355            if paused.replace(false) && !self.sdk.file_play(*player, *looped) {
356                // check-text: not player text (for the log)
357                self.sdk.log("audio: the SDK could not resume a track");
358            }
359        } else if let Some(slot) = self.slot_of(playing).filter(|slot| slot.paused) {
360            slot.paused = false;
361            let player = slot.player;
362            self.sdk.sample_player_set_paused(player, false);
363        }
364        Ok(())
365    }
366
367    fn set_rate(&mut self, sound: &Clip<S>, rate: u32) -> Result<(), Unsupported> {
368        match sound {
369            Clip::Sample { rate: at, .. } => at.set(rate),
370            Clip::File(file) => file.rate.set(rate),
371        }
372        Ok(())
373    }
374
375    /// A sample the SDK makes from a copy of the samples, at their own rate.
376    fn add_sound(&mut self, samples: &SharedSamples) -> Result<Clip<S>, String> {
377        let stereo = samples.channels() == 2;
378        let Some(sample) = self
379            .sdk
380            .sample_from_data(samples.samples(), samples.rate(), stereo)
381        else {
382            // check-text: not player text (for the log)
383            return Err("the SDK made no sample of it: no memory, or no samples".to_owned());
384        };
385        self.made.push(sample);
386        Ok(Clip::Sample {
387            sample,
388            rate: Cell::new(RATE_ONE),
389        })
390    }
391
392    /// Stops the sample players on it, lets go of it, and frees it.
393    fn remove_sound(&mut self, sound: Clip<S>) {
394        self.stop(&sound);
395        let Clip::Sample { sample, .. } = sound else {
396            return;
397        };
398        let Some(at) = self.made.iter().position(|made| *made == sample) else {
399            return;
400        };
401        self.made.swap_remove(at);
402        for slot in &mut self.slots {
403            if slot.sample == Some(sample) {
404                slot.sample = None;
405                slot.paused = false;
406            }
407        }
408        self.sdk.sample_free(sample);
409    }
410
411    fn warn(&mut self, line: &str) {
412        // check-text: not player text (for the log)
413        self.sdk.log(&format!("audio: {line}"));
414    }
415}
416
417/// How the player's warnings name a sound of the manifest: by its path in
418/// the `.pdx`.
419fn shown(file: &str) -> String {
420    format!("{BAKED_SOUND_DIR}/{}", without_extension(file))
421}
422
423/// The player of `manifest` on `speaker`: its sounds loaded, from the `.pdx`.
424pub fn player<S: Sdk>(
425    speaker: &mut Speaker<S>,
426    manifest: AudioManifest,
427    seed: u64,
428) -> Player<Speaker<S>> {
429    // No bytes come from here, as the Playdate reads a file for itself, and
430    // what is left out was settled by the export.
431    let files = Files::new(&|_| None, &shown, &|_| false, BAKED_MUSIC_DIR);
432    let music = BAKED_MUSIC_DIR.to_owned();
433    let voice = VOICE_DIR.to_owned();
434    ready_now(Player::load(speaker, manifest, &files, music, voice, seed))
435}
436
437/// What `future` gives, when nothing it waits on is pending: on the SDK every
438/// load is done when it returns.
439fn ready_now<F: Future>(future: F) -> F::Output {
440    let mut future = pin!(future);
441    let mut cx = Context::from_waker(Waker::noop());
442    loop {
443        if let Poll::Ready(value) = future.as_mut().poll(&mut cx) {
444            return value;
445        }
446    }
447}
448
449#[cfg(test)]
450mod tests;