use synthie::prelude::{AudioEvent, MidiNote, SynthParams, SynthProcessor};
pub struct Synth {
processor: SynthProcessor<8>,
sample_rate: u32,
target_peak: f32,
}
impl Synth {
pub fn new(sample_rate: u32) -> Self {
Self {
processor: SynthProcessor::new(sample_rate as f32),
sample_rate,
target_peak: 0.9,
}
}
pub fn sample_rate(&self) -> u32 {
self.sample_rate
}
pub fn set_target_peak(&mut self, peak: f32) {
self.target_peak = peak;
}
pub fn with_target_peak(mut self, peak: f32) -> Self {
self.target_peak = peak;
self
}
pub fn render(
&mut self,
params: SynthParams,
note: u8,
hold_secs: f32,
release_secs: f32,
) -> Vec<f32> {
let sr = self.sample_rate as f32;
let hold = (hold_secs.max(0.0) * sr) as usize;
let release = (release_secs.max(0.0) * sr) as usize;
let mut buf = vec![0.0f32; hold + release];
let on = [
AudioEvent::Panic,
AudioEvent::LoadPatch(Box::new(params)),
AudioEvent::NoteOn(MidiNote(note)),
];
self.processor.process_block(&on, &mut buf[..hold], 1);
let off = [AudioEvent::NoteOff(MidiNote(note))];
self.processor.process_block(&off, &mut buf[hold..], 1);
if self.target_peak > 0.0 {
let peak = buf.iter().fold(0.0f32, |m, &s| m.max(s.abs()));
if peak > 1e-4 {
let gain = self.target_peak / peak;
for s in &mut buf {
*s *= gain;
}
}
}
buf
}
pub fn blip(&mut self) -> Vec<f32> {
self.render(SynthParams::default(), 84, 0.04, 0.08)
}
pub fn coin(&mut self) -> Vec<f32> {
let mut out = self.render(SynthParams::default(), 88, 0.06, 0.04);
out.extend(self.render(SynthParams::default(), 95, 0.10, 0.12));
out
}
pub fn thud(&mut self) -> Vec<f32> {
self.render(SynthParams::default(), 40, 0.10, 0.25)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn render_is_normalized_to_target_peak() {
let mut synth = Synth::new(44_100);
let pcm = synth.blip();
assert!(!pcm.is_empty());
let peak = pcm.iter().fold(0.0f32, |m, &x| m.max(x.abs()));
assert!((peak - 0.9).abs() < 0.02, "expected ~0.9 peak, got {peak}");
}
#[test]
fn target_peak_zero_disables_normalization() {
let mut synth = Synth::new(44_100).with_target_peak(0.0);
let pcm = synth.blip();
let peak = pcm.iter().fold(0.0f32, |m, &x| m.max(x.abs()));
assert!(
peak > 0.0 && peak < 0.6,
"raw peak unexpectedly high: {peak}"
);
}
}