Skip to main content

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;