unlit3dx/synth.rs
1//! Soundfont-backed fill sources: one [`SynthVoice`] per soundfont, mixed.
2//!
3//! A voice lives in the audio world with the [`OnAudioFill`] that renders it,
4//! so it sounds by existing. Its synthesizer waits for the first fill, which
5//! reports the rate the stream really runs at: nothing has to know that rate
6//! before the device opens.
7//!
8//! Gameplay never touches the synthesizer. A live note is a [`midi_job`]; a
9//! sequence is a [`MidiSequencer`] in the gameplay world, advanced once a
10//! frame, which sends the same jobs at the moments its events come due. The
11//! audio thread only turns messages into samples and sums every source into
12//! the stream's interleaved frames.
13
14use std::sync::Arc;
15
16pub use rustysynth_ext::{MidiFile, MidiFileLoopType, MidiMessage, SoundFont, Synthesizer};
17use rustysynth_ext::{MidiFileError, SoundFontError, SynthesizerError, SynthesizerSettings};
18use unlit_ecs::{Bundle, DynamicBundle, Entity, World, bundle};
19
20use crate::audio::{AudioChunk, AudioFillOrder, OnAudioFill};
21use crate::job::{Job, JobSender};
22
23/// Parses the soundfont in `bytes`.
24///
25/// # Errors
26///
27/// Returns the soundfont error when the bytes are malformed.
28pub fn load_soundfont(bytes: &[u8]) -> Result<Arc<SoundFont>, VoiceError> {
29 let mut reader = bytes;
30 Ok(Arc::new(SoundFont::new(&mut reader)?))
31}
32
33/// Parses a standard MIDI file from `bytes`, reading its loop markers the way
34/// `loop_type` describes.
35///
36/// A file's loop markers are controller messages that only mean "loop here"
37/// to the tool that wrote them, so the reader has to be told which convention
38/// to look for. [`MidiFileLoopType::LoopPoint`] takes the byte position of
39/// the loop instead, and `LoopPoint(0)` marks nothing.
40///
41/// # Errors
42///
43/// Returns the MIDI error when the bytes are not a valid SMF.
44pub fn load_midi_file_with_loop_type(
45 bytes: &[u8],
46 loop_type: MidiFileLoopType,
47) -> Result<Arc<MidiFile>, VoiceError> {
48 let mut reader = bytes;
49 Ok(Arc::new(MidiFile::new_with_loop_type(
50 &mut reader,
51 loop_type,
52 )?))
53}
54
55/// One MIDI message and when to send it: seconds since the sequence starts,
56/// then the message.
57pub type MidiEvent = (f64, MidiEventKind);
58
59/// A MIDI message the synthesizer acts on.
60///
61/// These are the five commands it implements; anything else a file may carry
62/// (aftertouch, channel pressure, system messages) is not part of making a
63/// sound and has no variant here.
64///
65/// The channel is always the low nibble of the status byte, so `0` is the
66/// first channel.
67#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
68#[non_exhaustive]
69pub enum MidiEventKind {
70 /// Press a key. `velocity` is 1 to 127; zero releases the key instead.
71 NoteOn {
72 /// The channel the note plays on.
73 channel: u8,
74 /// The key, 0 to 127, where 60 is middle C.
75 key: u8,
76 /// How hard the key was struck, 0 to 127.
77 velocity: u8,
78 },
79 /// Release a key.
80 NoteOff {
81 /// The channel the note plays on.
82 channel: u8,
83 /// The key, 0 to 127.
84 key: u8,
85 },
86 /// Set a controller, such as the volume (7), the expression (11) or the
87 /// sustain pedal (64).
88 ControlChange {
89 /// The channel the controller belongs to.
90 channel: u8,
91 /// The controller number, 0 to 127.
92 controller: u8,
93 /// The controller value, 0 to 127.
94 value: u8,
95 },
96 /// Switch the channel to another instrument of the soundfont.
97 ProgramChange {
98 /// The channel that changes instrument.
99 channel: u8,
100 /// The General MIDI program number, zero-based.
101 program: u8,
102 },
103 /// Bend the channel's pitch, from `-8192` (fully down) through `0` (no
104 /// bend) to `8191` (fully up).
105 PitchBend {
106 /// The channel that bends.
107 channel: u8,
108 /// The bend, clamped to `-8192..=8191`.
109 value: i16,
110 },
111}
112
113impl MidiEventKind {
114 /// The message this kind stands for.
115 #[must_use]
116 pub fn into_message(self) -> MidiMessage {
117 match self {
118 Self::NoteOn {
119 channel,
120 key,
121 velocity,
122 } => MidiMessage::Normal {
123 status: 0x90 | (channel & 0x0F),
124 data1: key,
125 data2: velocity,
126 },
127 Self::NoteOff { channel, key } => MidiMessage::Normal {
128 status: 0x80 | (channel & 0x0F),
129 data1: key,
130 data2: 0,
131 },
132 Self::ControlChange {
133 channel,
134 controller,
135 value,
136 } => MidiMessage::Normal {
137 status: 0xB0 | (channel & 0x0F),
138 data1: controller,
139 data2: value,
140 },
141 Self::ProgramChange { channel, program } => MidiMessage::Normal {
142 status: 0xC0 | (channel & 0x0F),
143 data1: program,
144 data2: 0,
145 },
146 Self::PitchBend { channel, value } => {
147 let raw = (i32::from(value) + 8192).clamp(0, 16_383);
148 MidiMessage::Normal {
149 status: 0xE0 | (channel & 0x0F),
150 data1: (raw & 0x7F) as u8,
151 data2: ((raw >> 7) & 0x7F) as u8,
152 }
153 }
154 }
155 }
156
157 /// The kind a message stands for, or `None` for a message the synthesizer
158 /// does not act on.
159 ///
160 /// A note-on with velocity zero stays a note-on: the synthesizer releases
161 /// the key itself, and rewriting the message would make the two spellings
162 /// of one release indistinguishable.
163 #[must_use]
164 pub fn from_message(message: MidiMessage) -> Option<Self> {
165 let MidiMessage::Normal {
166 status,
167 data1,
168 data2,
169 } = message
170 else {
171 return None;
172 };
173 let channel = status & 0x0F;
174 Some(match status & 0xF0 {
175 0x90 => Self::NoteOn {
176 channel,
177 key: data1,
178 velocity: data2,
179 },
180 0x80 => Self::NoteOff {
181 channel,
182 key: data1,
183 },
184 0xB0 => Self::ControlChange {
185 channel,
186 controller: data1,
187 value: data2,
188 },
189 0xC0 => Self::ProgramChange {
190 channel,
191 program: data1,
192 },
193 0xE0 => Self::PitchBend {
194 channel,
195 value: ((i16::from(data2) << 7) | i16::from(data1)) - 8192,
196 },
197 _ => return None,
198 })
199 }
200}
201
202/// Builds a MIDI file from timed messages, ready for [`MidiSequencer::new`].
203///
204/// The events may come in any order: they are sorted by time, and events at
205/// the same time keep the order they were given in.
206///
207/// # Errors
208///
209/// Returns the MIDI error when a time is not a number.
210pub fn midi_file(events: impl IntoIterator<Item = MidiEvent>) -> Result<Arc<MidiFile>, VoiceError> {
211 let mut events: Vec<MidiEvent> = events.into_iter().collect();
212 events.sort_by(|one, other| one.0.total_cmp(&other.0));
213 Ok(Arc::new(MidiFile::new_with_events(
214 events
215 .into_iter()
216 .map(|(time, kind)| (time, kind.into_message())),
217 )?))
218}
219
220/// The synthesizer's block size, and how finely a fill places a message inside
221/// itself: a note lands on a block boundary, so this is the timing resolution
222/// below one fill.
223const BLOCK_FRAMES: usize = 64;
224
225/// The synthesizer `font` plays through at `rate`, set up the way a voice
226/// wants it: full scale out of the box, because the library's default of 0.5
227/// is a -6 dB cut that every caller would otherwise have to undo.
228fn synthesizer(font: &Arc<SoundFont>, rate: i32) -> Result<Synthesizer, SynthesizerError> {
229 let mut settings = SynthesizerSettings::new(rate);
230 settings.block_size = BLOCK_FRAMES;
231 let mut synthesizer = Synthesizer::new(font, &settings)?;
232 synthesizer.set_master_volume(1.0);
233 Ok(synthesizer)
234}
235
236/// A loaded soundfont and the synthesizer playing it, as one object.
237///
238/// Spawning it also spawns the [`OnAudioFill`] that renders it, so the voice is
239/// a source of the world's mix: [`midi_job`] presses its keys and the audio
240/// thread turns them into samples. A voice on its own would never be asked for
241/// samples, and a fill source without one would have nothing to render, so the
242/// two are spawned together.
243pub struct SynthVoice {
244 /// The soundfont to build the synthesizer from, until it is built. Taken
245 /// away when building fails, which is what makes a voice that cannot sound
246 /// fail once instead of on every fill.
247 font: Option<Arc<SoundFont>>,
248 /// The synthesizer that turns messages into samples, built on the first
249 /// fill from the rate that fill reports.
250 pub synthesizer: Option<Synthesizer>,
251 /// Messages waiting for their moment, in seconds from the next fill's
252 /// start.
253 queued: Vec<(f64, MidiMessage)>,
254 /// The left channel's block, reused across fills.
255 left: Vec<f32>,
256 /// The right channel's block, reused across fills.
257 right: Vec<f32>,
258 /// Whether building gave up, so no queue is kept for it any more.
259 gave_up: bool,
260}
261
262impl Bundle for SynthVoice {
263 fn into_bundle(self) -> DynamicBundle {
264 bundle![
265 self,
266 OnAudioFill::new(AudioFillOrder::default(), fill_voice)
267 ]
268 }
269}
270
271impl SynthVoice {
272 /// Spawns a voice playing `font`, and its fill source.
273 pub fn from_font(world: &mut World, font: &Arc<SoundFont>) -> Entity {
274 let voice = Self {
275 font: Some(font.clone()),
276 synthesizer: None,
277 queued: Vec::new(),
278 left: Vec::new(),
279 right: Vec::new(),
280 gave_up: false,
281 };
282 world.root_scope_mut().spawn(voice)
283 }
284
285 /// Parses `bytes` as a soundfont and spawns a voice playing it.
286 ///
287 /// # Errors
288 ///
289 /// Returns the soundfont error when the bytes are malformed.
290 pub fn from_bytes(world: &mut World, bytes: &[u8]) -> Result<Entity, VoiceError> {
291 Ok(Self::from_font(world, &load_soundfont(bytes)?))
292 }
293
294 /// Builds the synthesizer at `rate`, or gives the voice up for good.
295 ///
296 /// Returns whether the voice can render now.
297 fn build(&mut self, rate: i32) -> bool {
298 let Some(font) = self.font.clone() else {
299 return false;
300 };
301 match synthesizer(&font, rate) {
302 Ok(synthesizer) => {
303 self.synthesizer = Some(synthesizer);
304 true
305 }
306 Err(error) => {
307 self.give_up(error);
308 false
309 }
310 }
311 }
312
313 /// Stops trying to sound, naming `reason` once: a failure that would
314 /// repeat on every fill belongs in the log once, not sixty times a second.
315 fn give_up(&mut self, reason: impl std::fmt::Display) {
316 if self.font.take().is_some() {
317 log::error!("a synth voice cannot be built ({reason}); it stays silent");
318 }
319 self.synthesizer = None;
320 self.queued.clear();
321 self.gave_up = true;
322 }
323}
324
325/// One sequence, its clock and where it loops.
326///
327/// The gameplay world owns it as a component and advances it once a frame, so
328/// playback state is ordinary data in the world that asks for it: pausing is a
329/// field. Nothing here touches the audio thread; [`MidiSequencer::advance`]
330/// sends it [`midi_job`]s.
331///
332/// The voice a sequence plays into is shared with every other sequence in the
333/// scene, so anything that cuts a pass short — a rewind, a pause, a loop back
334/// to the marker — releases the notes this sequence pressed and leaves the
335/// rest alone, instead of letting them ring into the next pass.
336pub struct MidiSequencer {
337 /// The sequence being played.
338 pub file: Arc<MidiFile>,
339 /// Seconds since the sequence started.
340 pub time: f64,
341 /// The index of the next event to send.
342 pub index: usize,
343 /// Where the sequence returns to once it reaches its end: seconds and event
344 /// index, from the file's loop marker. `None` until a marker is reached,
345 /// in which case the sequence repeats from its start.
346 ///
347 /// Written by hand, it loops a passage the file never marked; see
348 /// [`MidiSequencer::advance`].
349 pub loop_point: Option<(f64, usize)>,
350 /// Whether reaching the end returns to the loop point instead of stopping.
351 pub loop_play: bool,
352 /// Whether the clock stands still.
353 pub paused: bool,
354 /// Whether the sequence reached its end and stayed there.
355 pub finished: bool,
356 /// The notes this sequence pressed and has not released, by channel and
357 /// key. A shared voice is why the exact notes are remembered rather than
358 /// silenced wholesale: an all-notes-off would cut another sequence's
359 /// music too.
360 held: Vec<(u8, u8)>,
361 /// Whether the next advance has to release those notes before it plays
362 /// anything, because a rewind cut a pass short since the last one.
363 silent: bool,
364}
365
366impl MidiSequencer {
367 /// Starts `file` from its beginning, playing once.
368 pub fn new(file: Arc<MidiFile>) -> Self {
369 Self {
370 file,
371 time: 0.0,
372 index: 0,
373 loop_point: None,
374 loop_play: false,
375 paused: false,
376 finished: false,
377 held: Vec::new(),
378 silent: false,
379 }
380 }
381
382 /// Puts the sequence back at its start, playing.
383 ///
384 /// `loop_point` is left alone, because a hand-written one has no marker to
385 /// be restored from. The notes the pass left sounding are released on the
386 /// next [`MidiSequencer::advance`]: a rewind has no sender to reach the
387 /// voice with, and they must not ring into the new pass.
388 pub fn rewind(&mut self) {
389 self.time = 0.0;
390 self.index = 0;
391 self.finished = false;
392 self.silent |= !self.held.is_empty();
393 }
394
395 /// Sends every event that came due in the last `delta` seconds to `voice`,
396 /// and loops or stops when the sequence runs out.
397 ///
398 /// A message's `delay` keeps its distance from the start of the frame, so
399 /// events due within one frame still reach the synthesizer in order and at
400 /// their own offsets. The audio thread can only place them within the fill
401 /// it renders, so an event lands closer to its moment than one frame but
402 /// not exactly on it.
403 ///
404 /// The voice is shared with every other sequence in the scene, so a note
405 /// held across the end of a pass would ring into the next one, and a note
406 /// held when the clock stops would ring through the pause. Reaching the
407 /// end, looping, pausing and rewinding all release the notes this sequence
408 /// pressed, and leave the other sequences' notes sounding.
409 pub fn advance(&mut self, delta: f64, voice: Entity, audio: &JobSender) {
410 if self.paused || self.finished {
411 // A pause leaves the notes of the pass sounding on the shared
412 // voice, so the first frame that finds the clock stopped releases
413 // them; a finished sequence has already done so.
414 self.release(voice, audio);
415 return;
416 }
417 if self.silent {
418 self.release(voice, audio);
419 }
420 self.time += delta;
421 let mut wraps = 0;
422 while let Some((time, message)) = self.due() {
423 match message {
424 MidiMessage::Normal { .. } => {
425 if let Some(kind) = MidiEventKind::from_message(message) {
426 self.pressed(kind);
427 let delay = (time - (self.time - delta)).max(0.0);
428 let _ = audio.send(midi_job(voice, kind, delay));
429 }
430 }
431 MidiMessage::LoopStart => {
432 self.loop_point = Some((time, self.index));
433 }
434 MidiMessage::LoopEnd if self.loop_play => {
435 self.release(voice, audio);
436 let (time, index) = self.loop_point.unwrap_or((0.0, 0));
437 self.time = time;
438 self.index = index;
439 // A loop body of no length would keep coming due at the
440 // same instant; one wrap per frame is all a real file
441 // asks for.
442 wraps += 1;
443 if wraps > 1 {
444 break;
445 }
446 }
447 _ => {}
448 }
449 self.index += 1;
450 }
451 if self.index == self.file.get_messages().len() {
452 self.release(voice, audio);
453 if !self.loop_play {
454 self.finished = true;
455 return;
456 }
457 let (time, index) = self.loop_point.unwrap_or((0.0, 0));
458 self.time = time;
459 self.index = index;
460 }
461 }
462
463 /// The event at the cursor, once the clock has reached it.
464 ///
465 /// Read through a borrow that ends with the call, so the advance is free
466 /// to change the sequence between two events.
467 fn due(&self) -> Option<(f64, MidiMessage)> {
468 let times = self.file.get_times();
469 let messages = self.file.get_messages();
470 if self.index >= messages.len() || times[self.index] > self.time {
471 return None;
472 }
473 Some((times[self.index], messages[self.index]))
474 }
475
476 /// Records what `kind` does to the notes this sequence holds sounding.
477 fn pressed(&mut self, kind: MidiEventKind) {
478 match kind {
479 MidiEventKind::NoteOn {
480 channel,
481 key,
482 velocity,
483 } if velocity > 0 => {
484 if !self.held.contains(&(channel, key)) {
485 self.held.push((channel, key));
486 }
487 }
488 MidiEventKind::NoteOn { channel, key, .. }
489 | MidiEventKind::NoteOff { channel, key } => {
490 self.held.retain(|held| *held != (channel, key));
491 }
492 MidiEventKind::ControlChange {
493 channel,
494 controller: 120 | 123,
495 ..
496 } => self.held.retain(|held| held.0 != channel),
497 _ => {}
498 }
499 }
500
501 /// Releases the notes this sequence pressed, and only those: the voice is
502 /// shared, and an all-notes-off would cut another sequence's music.
503 fn release(&mut self, voice: Entity, audio: &JobSender) {
504 self.silent = false;
505 for (channel, key) in std::mem::take(&mut self.held) {
506 let kind = MidiEventKind::NoteOff { channel, key };
507 let _ = audio.send(midi_job(voice, kind, 0.0));
508 }
509 }
510}
511
512/// Why a soundfont or a MIDI file could not be built.
513#[derive(Debug, thiserror::Error)]
514pub enum VoiceError {
515 /// The bytes are not a soundfont.
516 #[error("the soundfont failed ({0})")]
517 SoundFont(#[from] SoundFontError),
518 /// The bytes are not a MIDI file.
519 #[error("the midi file failed ({0})")]
520 MidiFile(#[from] MidiFileError),
521}
522
523/// A job that sends `kind` to `voice`, `delay` seconds from the next fill's
524/// start. A negative delay is treated as zero.
525///
526/// A voice that is gone ignores the job, and so does one that gave up on
527/// sounding: nothing would ever drain its queue.
528pub fn midi_job(voice: Entity, kind: MidiEventKind, delay: f64) -> Job {
529 Box::new(move |world| {
530 let Ok(mut guard) = world.root_scope().query_one::<&mut SynthVoice>(voice) else {
531 return;
532 };
533 let voice = &mut *guard.get();
534 // A queue a voice that gave up can never drain only grows.
535 if voice.gave_up {
536 return;
537 }
538 voice.queued.push((delay.max(0.0), kind.into_message()));
539 })
540}
541
542/// Builds the voice's synthesizer if it has none, adds the queue's messages at
543/// their offsets, and renders the whole fill into the chunk.
544fn fill_voice(world: &World, entity: Entity, chunk: &mut AudioChunk) {
545 let Ok(mut guard) = world.root_scope().query_one::<&mut SynthVoice>(entity) else {
546 return;
547 };
548 // Borrowed through the guard the fields are not disjoint; reborrowed as a
549 // reference they are, so the render can fill the voice's own block.
550 let voice = &mut *guard.get();
551 let Ok(rate) = i32::try_from(chunk.sample_rate) else {
552 voice.give_up(format!("the stream's {} Hz", chunk.sample_rate));
553 return;
554 };
555 let stale = voice
556 .synthesizer
557 .as_ref()
558 .is_some_and(|synthesizer| synthesizer.get_sample_rate() != rate);
559 if stale {
560 // A synthesizer renders at one rate; a stream that changed rate needs
561 // a new one.
562 voice.synthesizer = None;
563 }
564 if voice.synthesizer.is_none() && !voice.build(rate) {
565 return;
566 }
567 let frames = chunk.frames();
568 let seconds_per_frame = 1.0 / f64::from(chunk.sample_rate);
569 let SynthVoice {
570 synthesizer,
571 queued,
572 left,
573 right,
574 ..
575 } = voice;
576 let Some(synthesizer) = synthesizer.as_mut() else {
577 return;
578 };
579 left.resize(frames, 0.0);
580 right.resize(frames, 0.0);
581 // Taken out so the messages can be walked while the fields are borrowed to
582 // render; put back with its allocation, whatever is left over.
583 let mut pending = std::mem::take(queued);
584 pending.sort_by(|one, other| one.0.total_cmp(&other.0));
585 let mut wrote = 0;
586 let mut applied = 0;
587 for (delay, message) in &pending {
588 let offset = (*delay / seconds_per_frame).round();
589 if offset >= frames as f64 {
590 break;
591 }
592 let offset = offset as usize;
593 if offset > wrote {
594 synthesizer.render(&mut left[wrote..offset], &mut right[wrote..offset]);
595 wrote = offset;
596 }
597 if let MidiMessage::Normal {
598 status,
599 data1,
600 data2,
601 } = *message
602 {
603 synthesizer.process_midi_message(
604 i32::from(status & 0x0F),
605 i32::from(status & 0xF0),
606 i32::from(data1),
607 i32::from(data2),
608 );
609 }
610 applied += 1;
611 }
612 synthesizer.render(&mut left[wrote..frames], &mut right[wrote..frames]);
613 pending.drain(..applied);
614 let elapsed = frames as f64 * seconds_per_frame;
615 for (delay, _) in &mut pending {
616 *delay = (*delay - elapsed).max(0.0);
617 }
618 *queued = pending;
619 add_stereo(left, right, chunk);
620}
621
622/// Adds a stereo block onto the chunk's interleaved frames.
623///
624/// A mono stream gets the two channels averaged, so neither is lost; a stream
625/// wider than stereo gets them in its first two channels and nothing in the
626/// rest, which keeps whatever other sources added.
627fn add_stereo(left: &[f32], right: &[f32], chunk: &mut AudioChunk) {
628 let channels = usize::from(chunk.channels.max(1));
629 for (frame, samples) in chunk.out.chunks_mut(channels).enumerate() {
630 if channels == 1 {
631 if let Some(sample) = samples.first_mut() {
632 *sample += (left[frame] + right[frame]) * 0.5;
633 }
634 } else {
635 if let Some(sample) = samples.first_mut() {
636 *sample += left[frame];
637 }
638 if let Some(sample) = samples.get_mut(1) {
639 *sample += right[frame];
640 }
641 }
642 }
643}
644#[cfg(test)]
645mod tests;