use std::{io::Read, sync::Arc};
use rustysynth::{SoundFont, Synthesizer, SynthesizerSettings};
use crate::{Error, Frame, MidiSynth, Result};
const BLOCK_SIZE: usize = 8;
pub struct GmSynth {
synth: Synthesizer,
rate: u32,
left: [f32; BLOCK_SIZE],
right: [f32; BLOCK_SIZE],
}
impl GmSynth {
pub fn load_soundfont(reader: &mut impl Read) -> Result<Arc<SoundFont>> {
SoundFont::new(reader)
.map(Arc::new)
.map_err(|_| Error::Format("invalid or unsupported SoundFont"))
}
pub fn with_soundfont(font: Arc<SoundFont>, sample_rate: u32) -> Result<Self> {
if sample_rate == 0 {
return Err(Error::Config("GM sample rate must not be zero"));
}
let rate = sample_rate.clamp(16_000, 192_000);
let mut settings = SynthesizerSettings::new(rate as i32);
settings.block_size = BLOCK_SIZE;
let synth = Synthesizer::new(&font, &settings)
.map_err(|_| Error::Config("invalid GM synthesizer settings"))?;
Ok(Self {
synth,
rate,
left: [0.0; BLOCK_SIZE],
right: [0.0; BLOCK_SIZE],
})
}
fn is_gm_reset(bytes: &[u8]) -> bool {
matches!(bytes, [0xf0, 0x7e, device, 0x09, 0x01 | 0x03, 0xf7] if *device < 0x80)
}
}
impl MidiSynth for GmSynth {
fn sample_rate(&self) -> u32 {
self.rate
}
fn message(&mut self, bytes: &[u8]) {
if bytes == [0xff] || Self::is_gm_reset(bytes) {
self.synth.reset();
return;
}
let Some(&status @ 0x80..=0xef) = bytes.first() else {
return;
};
let length = if matches!(status & 0xf0, 0xc0 | 0xd0) {
2
} else {
3
};
if bytes.len() != length || bytes[1..].iter().any(|&value| value >= 0x80) {
return;
}
self.synth.process_midi_message(
i32::from(status & 15),
i32::from(status & 0xf0),
i32::from(bytes[1]),
i32::from(*bytes.get(2).unwrap_or(&0)),
);
}
fn render(&mut self, out: &mut [Frame]) {
for chunk in out.chunks_mut(BLOCK_SIZE) {
let count = chunk.len();
self.synth
.render(&mut self.left[..count], &mut self.right[..count]);
for (index, frame) in chunk.iter_mut().enumerate() {
*frame = [self.left[index], self.right[index]];
}
}
}
}