use oxideav_core::Result;
use super::{Instrument, Voice};
#[derive(Default)]
pub struct ToneInstrument;
impl ToneInstrument {
pub fn new() -> Self {
Self
}
}
impl Instrument for ToneInstrument {
fn name(&self) -> &str {
"pure-tone fallback"
}
fn make_voice(
&self,
program: u8,
key: u8,
velocity: u8,
sample_rate: u32,
) -> Result<Box<dyn Voice>> {
let waveform = waveform_for_program(program);
let frequency = midi_key_to_hz(key);
let v = velocity as f32 / 127.0;
let amplitude = v * v * 0.4; let base_phase_inc = frequency / sample_rate.max(1) as f32;
Ok(Box::new(ToneVoice {
waveform,
phase: 0.0,
base_phase_inc,
phase_inc: base_phase_inc,
amplitude,
pressure_gain: 1.0,
attack_samples: (sample_rate as f32 * 0.005).max(1.0) as u32,
decay_samples: (sample_rate as f32 * 0.05).max(1.0) as u32,
release_samples: (sample_rate as f32 * 0.10).max(1.0) as u32,
sustain_level: 0.7,
elapsed: 0,
release_pos: None,
release_start_level: 1.0,
done: false,
}))
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Waveform {
Sine,
Triangle,
Sawtooth,
Square,
}
pub fn waveform_for_program(program: u8) -> Waveform {
match program {
0..=23 => Waveform::Triangle,
24..=55 => Waveform::Sawtooth,
56..=71 => Waveform::Square,
72..=87 => Waveform::Sine,
88..=119 => Waveform::Triangle,
_ => Waveform::Square,
}
}
pub fn midi_key_to_hz(key: u8) -> f32 {
440.0 * 2f32.powf((key as f32 - 69.0) / 12.0)
}
struct ToneVoice {
waveform: Waveform,
phase: f32,
base_phase_inc: f32,
phase_inc: f32,
amplitude: f32,
pressure_gain: f32,
attack_samples: u32,
decay_samples: u32,
release_samples: u32,
sustain_level: f32,
elapsed: u32,
release_pos: Option<u32>,
release_start_level: f32,
done: bool,
}
impl ToneVoice {
fn envelope_at(&self, t: u32) -> f32 {
if let Some(rel_at) = self.release_pos {
let since = t.saturating_sub(rel_at);
if since >= self.release_samples {
return 0.0;
}
let x = since as f32 / self.release_samples.max(1) as f32;
return self.release_start_level * (1.0 - x) * (1.0 - x);
}
if t < self.attack_samples {
return t as f32 / self.attack_samples.max(1) as f32;
}
let t = t - self.attack_samples;
if t < self.decay_samples {
let x = t as f32 / self.decay_samples.max(1) as f32;
let drop = 1.0 - self.sustain_level;
let curve = 1.0 - (1.0 - x) * (1.0 - x);
return 1.0 - drop * curve;
}
self.sustain_level
}
}
impl Voice for ToneVoice {
fn render(&mut self, out: &mut [f32]) -> usize {
if self.done {
return 0;
}
for (i, slot) in out.iter_mut().enumerate() {
let env = self.envelope_at(self.elapsed);
if self.release_pos.is_some() && env <= 0.0 {
self.done = true;
return i;
}
let osc = match self.waveform {
Waveform::Sine => (self.phase * std::f32::consts::TAU).sin(),
Waveform::Triangle => {
4.0 * (self.phase - 0.5).abs() - 1.0
}
Waveform::Sawtooth => 2.0 * self.phase - 1.0,
Waveform::Square => {
if self.phase < 0.5 {
1.0
} else {
-1.0
}
}
};
*slot = osc * env * self.amplitude * self.pressure_gain;
self.phase += self.phase_inc;
if self.phase >= 1.0 {
self.phase -= 1.0;
}
self.elapsed = self.elapsed.wrapping_add(1);
}
out.len()
}
fn release(&mut self) {
if self.release_pos.is_none() {
self.release_start_level = self.envelope_at(self.elapsed).max(0.0);
self.release_pos = Some(self.elapsed);
}
}
fn done(&self) -> bool {
self.done
}
fn set_pitch_bend_cents(&mut self, cents: i32) {
let bend_ratio = (2.0f32).powf(cents as f32 / 1200.0);
self.phase_inc = self.base_phase_inc * bend_ratio;
}
fn set_pressure(&mut self, pressure: f32) {
let p = pressure.clamp(0.0, 1.0);
self.pressure_gain = 1.0 + 0.5 * p;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn key_60_is_middle_c() {
let f = midi_key_to_hz(60);
assert!((f - 261.6256).abs() < 0.01, "middle C frequency was {f}");
}
#[test]
fn key_69_is_a4() {
assert!((midi_key_to_hz(69) - 440.0).abs() < 1e-3);
}
#[test]
fn waveform_dispatch_covers_full_range() {
for p in 0u8..=127 {
let _ = waveform_for_program(p);
}
}
#[test]
fn voice_produces_nonzero_samples() {
let inst = ToneInstrument::new();
let mut voice = inst.make_voice(0, 69, 100, 48_000).unwrap();
let mut buf = [0.0f32; 1024];
let n = voice.render(&mut buf);
assert_eq!(n, 1024);
let nonzero = buf.iter().filter(|s| s.abs() > 0.001).count();
assert!(nonzero > 100, "expected non-silent output, got {nonzero}");
}
#[test]
fn adsr_envelope_has_distinct_decay_phase() {
let inst = ToneInstrument::new();
let mut voice = inst.make_voice(73, 60, 127, 48_000).unwrap(); let mut buf = vec![0.0f32; 4096];
voice.render(&mut buf);
let attack_peak = buf[230..260].iter().map(|s| s.abs()).fold(0.0f32, f32::max);
let sustain_peak = buf[3400..3500]
.iter()
.map(|s| s.abs())
.fold(0.0f32, f32::max);
assert!(attack_peak > 0.05, "attack peak too quiet: {attack_peak}",);
assert!(
sustain_peak < attack_peak,
"sustain ({sustain_peak}) should be quieter than attack peak ({attack_peak})",
);
let ratio = sustain_peak / attack_peak;
assert!(
(0.5..=0.85).contains(&ratio),
"sustain/attack ratio {ratio} outside expected ADSR shape",
);
}
#[test]
fn pitch_bend_changes_voice_frequency() {
let inst = ToneInstrument::new();
let mut a = inst.make_voice(73, 69, 127, 48_000).unwrap(); let mut b = inst.make_voice(73, 69, 127, 48_000).unwrap();
b.set_pitch_bend_cents(200); let mut buf_a = vec![0.0f32; 1024];
let mut buf_b = vec![0.0f32; 1024];
a.render(&mut buf_a);
b.render(&mut buf_b);
let cross_a = buf_a.windows(2).filter(|w| w[0] * w[1] < 0.0).count();
let cross_b = buf_b.windows(2).filter(|w| w[0] * w[1] < 0.0).count();
assert!(
cross_b > cross_a,
"+2 semitones should give more zero crossings: a={cross_a}, b={cross_b}",
);
}
#[test]
fn pressure_increases_amplitude() {
let inst = ToneInstrument::new();
let mut a = inst.make_voice(73, 69, 100, 48_000).unwrap();
let mut b = inst.make_voice(73, 69, 100, 48_000).unwrap();
b.set_pressure(1.0);
let mut buf_a = vec![0.0f32; 4096];
let mut buf_b = vec![0.0f32; 4096];
a.render(&mut buf_a);
b.render(&mut buf_b);
let peak_a = buf_a[1000..2000]
.iter()
.map(|s| s.abs())
.fold(0.0f32, f32::max);
let peak_b = buf_b[1000..2000]
.iter()
.map(|s| s.abs())
.fold(0.0f32, f32::max);
assert!(
peak_b > peak_a * 1.2,
"pressure should boost amplitude: a={peak_a}, b={peak_b}",
);
}
#[test]
fn voice_eventually_finishes_after_release() {
let inst = ToneInstrument::new();
let mut voice = inst.make_voice(0, 60, 100, 48_000).unwrap();
let mut buf = [0.0f32; 256];
voice.render(&mut buf);
voice.release();
let mut total = 0;
for _ in 0..50 {
let n = voice.render(&mut buf);
total += n;
if voice.done() {
break;
}
}
assert!(voice.done(), "voice should be done after release window");
assert!(total > 0, "voice rendered {total} samples post-release");
}
}