Skip to main content

unlit3dx_examples/scene/
audio.rs

1//! The scene's music: the synthesizer's voice, and the sequences played into it.
2//!
3//! The audio world is a world of its own, so the scene reaches it through
4//! jobs. Installing the voice is one question: it builds the voice and answers
5//! the entity that carries it, and every note the gameplay world plays is
6//! addressed to that entity afterwards. The sequences themselves live in the
7//! gameplay world, with the clocks that advance them, so a track's play state
8//! is a component field rather than a job.
9
10use std::sync::Arc;
11
12use unlit_ecs::{Bundle, DynamicBundle, Entity, Scope, World, bundle};
13use unlit3dx::prelude::*;
14
15use super::Scene;
16use super::music::{bgm_midi, sequence_midi};
17use super::tick::advancing;
18
19/// How a scene's audio world gets its synthesizer and its output.
20pub enum AudioSetup {
21    /// No synthesizer: the world stays silent, and its control panel
22    /// shows no tracks.
23    Silent,
24    /// Open an output device once the scene asks for it, and build the
25    /// synthesizer from the embedded soundfont. The request comes from the
26    /// control panel's Play button, which every setup — window or browser —
27    /// owes its first click: that click unlocks the output and starts the
28    /// tracks together, so the sequencer's clock and the context's run in
29    /// step from the start.
30    Device,
31    /// Open no device: the host pulls the fills with [`dispatch_audio_fill`],
32    /// so the synthesizer renders at whatever rate those fills name.
33    Pulled {
34        /// The soundfont to build the synthesizer from.
35        soundfont: Arc<[u8]>,
36        /// Start the background track once the synthesizer is ready.
37        autoplay_background: bool,
38    },
39}
40
41/// Install the demo's synthesizer on an audio world, answering the entity
42/// that carries it.
43///
44/// The voice builds its synthesizer on the first fill it renders, at the
45/// rate that fill names, so it can be installed before the world has an
46/// output at all.
47pub fn install_synth(world: &mut World, soundfont: &[u8]) -> Option<Entity> {
48    // Decoding an sf3's vorbis samples is the load's whole cost: a few
49    // seconds in release, tens of them in an unoptimized debug build.
50    log::info!("loading the embedded soundfont");
51    match load_soundfont(soundfont) {
52        Ok(font) => Some(SynthVoice::from_font(world, &font)),
53        Err(error) => {
54            log::error!("the soundfont failed to load ({error})");
55            None
56        }
57    }
58}
59
60/// The tracks the control panel transports.
61#[derive(Clone, Copy, Debug, PartialEq, Eq)]
62pub struct SceneTracks {
63    /// The looping background music.
64    pub background: Entity,
65    /// The one-shot sequence the panel starts.
66    pub sequence: Entity,
67}
68
69/// One sequence playing into the audio world.
70///
71/// The gameplay world owns it and advances it once a frame, which sends the
72/// messages that frame made due to `voice`. The voice is installed once and
73/// addressed ever after, so a track keeps its handle for as long as it lives.
74pub struct Track {
75    /// The sequence, and the clock that advances it.
76    pub midi: MidiSequencer,
77    /// The voice in the audio world the messages go to.
78    pub voice: Entity,
79}
80
81/// One track and the tick that advances it, spawned together.
82///
83/// The tick is the only thing that moves the sequence's clock, so a track
84/// spawned without it would never play, and a tick without its track would
85/// find nothing to advance. It is the one scene object whose tick reads the
86/// frame, whose duration is what paces the sequence.
87pub(super) struct PlayingTrack {
88    /// The sequence the tick advances.
89    track: Track,
90    /// The worlds the track's messages are sent to.
91    scene: Scene,
92}
93
94impl PlayingTrack {
95    /// The sequence `midi` playing into `voice`, ready to spawn.
96    #[must_use]
97    fn new(midi: MidiSequencer, voice: Entity, scene: &Scene) -> Self {
98        Self {
99            track: Track { midi, voice },
100            scene: scene.clone(),
101        }
102    }
103}
104
105impl Bundle for PlayingTrack {
106    fn into_bundle(self) -> DynamicBundle {
107        let Self { track, scene } = self;
108        bundle![
109            track,
110            advancing(move |track: &mut Track, _world, frame| {
111                track
112                    .midi
113                    .advance(f64::from(frame.delta_time), track.voice, &scene.audio);
114            }),
115        ]
116    }
117}
118
119/// The music, living on its own entity in the gameplay world.
120///
121/// It asks the audio world for the synthesizer's voice once, when it is built,
122/// then spends that voice on the two tracks the scene plays: the background
123/// loop, which starts on its own, and the demo sequence, which waits for the
124/// panel.
125pub struct SceneAudio {
126    /// The voice the audio world owes, and the answer once it comes.
127    ///
128    /// The install is one request, asked when the state is built: what it
129    /// names is nothing but itself, so it is asked under no key at all.
130    voice: Pending<(), Option<Entity>>,
131    /// Where the answer lands, reused across ticks.
132    settled: Vec<((), Settled<Option<Entity>>)>,
133    /// Whether the answer has landed, so the scene stops looking.
134    installed: bool,
135}
136
137impl SceneAudio {
138    /// The music state, asking the audio world for a synthesizer built from
139    /// `soundfont`.
140    ///
141    /// The install is asked here rather than on a tick because it is asked
142    /// once: the soundfont goes into the job, and the state keeps nothing of
143    /// it.
144    #[must_use]
145    pub fn new(scene: &Scene, soundfont: Arc<[u8]>) -> Self {
146        let mut voice = Pending::new(scene.audio_state.clone(), None);
147        // The wait has no ticks to run out of: the install is the one thing
148        // the music asks for, so it waits until the world answers or ends.
149        let installed = match voice.request(&scene.audio, (), move |world: &mut World| {
150            install_synth(world, &soundfont)
151        }) {
152            Ok(()) => false,
153            Err(error) => {
154                log::error!("the audio world refused the synthesizer install ({error:?})");
155                true
156            }
157        };
158        Self {
159            voice,
160            settled: Vec::new(),
161            installed,
162        }
163    }
164}
165
166/// The music and the tick that starts it, spawned together.
167///
168/// Starting the tracks is the answer to the audio world's install, so a
169/// music state spawned without its tick would ask for a voice nobody waits
170/// for.
171pub(super) struct Music {
172    /// The state the tick advances.
173    audio: SceneAudio,
174    /// The worlds the synthesizer is asked from and the tracks are put on.
175    scene: Scene,
176    /// Whether the background track starts with the scene.
177    autoplay_background: bool,
178}
179
180impl Music {
181    /// The music for the worlds `scene` reaches, playing `soundfont`.
182    #[must_use]
183    pub(super) fn new(scene: &Scene, soundfont: Arc<[u8]>, autoplay_background: bool) -> Self {
184        Self {
185            audio: SceneAudio::new(scene, soundfont),
186            scene: scene.clone(),
187            autoplay_background,
188        }
189    }
190
191    /// Start the music on the voice the install answered with.
192    fn tick(audio: &mut SceneAudio, world: &World, scene: &Scene, autoplay_background: bool) {
193        if audio.installed {
194            return;
195        }
196        let mut settled = core::mem::take(&mut audio.settled);
197        audio.voice.advance(0.0, &mut settled);
198        for (_, settled) in settled.drain(..) {
199            audio.installed = true;
200            match settled {
201                Settled::Answered(Some(voice)) => {
202                    start_tracks(&world.root_scope(), scene, voice, autoplay_background);
203                    log::info!("the scene tracks are ready");
204                }
205                // No voice: the soundfont failed to load, and the install said
206                // so on its way.
207                Settled::Answered(None) => {}
208                // The install was asked once and never answered, so this is
209                // the audio world ending with the job still in its queue.
210                Settled::Expired => {
211                    log::error!("the audio world ended before it installed the synthesizer");
212                }
213            }
214        }
215        audio.settled = settled;
216    }
217}
218
219impl Bundle for Music {
220    fn into_bundle(self) -> DynamicBundle {
221        let Self {
222            audio,
223            scene,
224            autoplay_background,
225        } = self;
226        bundle![
227            audio,
228            advancing(move |audio, world, _frame| {
229                Self::tick(audio, world, &scene, autoplay_background)
230            }),
231        ]
232    }
233}
234
235/// Put the scene's two sequences on the gameplay world, playing into `voice`.
236///
237/// The background loop runs from the first frame it can; the one-shot
238/// sequence is spawned paused, so the panel's Play is what starts it.
239fn start_tracks(world: &Scope<'_>, scene: &Scene, voice: Entity, autoplay_background: bool) {
240    let commands = world.queue();
241    let mut background = MidiSequencer::new(bgm_midi());
242    background.loop_play = true;
243    background.paused = !autoplay_background;
244    let background = commands.spawn(PlayingTrack::new(background, voice, scene));
245    let mut sequence = MidiSequencer::new(sequence_midi());
246    sequence.paused = true;
247    let sequence = commands.spawn(PlayingTrack::new(sequence, voice, scene));
248    commands.spawn(bundle![SceneTracks {
249        background,
250        sequence,
251    },]);
252}