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}