use std::{num::NonZero, ops::ControlFlow}; use crate::{ project::note_event::Note, sample::{ProcessingFrame, ProcessingFunction, Sample, SampleMetaData}, }; use super::Frame; #[repr(u8)] // quadratic is probably enough i can't hear it anymore #[derive(Copy, Clone, Debug)] pub enum Interpolation { Nearest = 0, Linear = 1, Quadratic = 2, } impl From for Interpolation { fn from(value: u8) -> Self { Self::from_u8(value) } } impl Interpolation { /// Amount of Padding in the SampleData to do each type of Interpolation. /// This much padding is needed at the start and end of the sample. pub const fn pad_needed(&self) -> usize { match self { Interpolation::Nearest => 1, Interpolation::Linear => 1, Interpolation::Quadratic => 2, } } pub const fn from_u8(value: u8) -> Self { match value { 0 => Self::Nearest, 1 => Self::Linear, 2 => Self::Quadratic, _ => panic!(), } } } #[derive(Debug)] pub struct SamplePlayer { sample: Sample, meta: SampleMetaData, note: Note, // position in the sample, the next output frame should be. // Done this way, so 0 is a valid, useful and intuitive value // always a valid position in the sample. checked against sample lenght on each change // stored as fixed point data: usize + f32 // f32 ranges 0..1 position: (usize, f32), // is_done: bool, out_rate: NonZero, // how much the position is advanced for each output sample. // computed from in and out rate step_size: f32, } impl SamplePlayer { pub fn new(sample: Sample, meta: SampleMetaData, out_rate: NonZero, note: Note) -> Self { let step_size = Self::compute_step_size(meta.sample_rate, out_rate, meta.base_note, note); Self { sample, meta, position: (Sample::PAD_SIZE_EACH, 0.), out_rate, step_size, note, } } pub fn check_position(&self) -> ControlFlow<()> { if self.position.0 > self.sample.len_with_pad() - Sample::PAD_SIZE_EACH { ControlFlow::Break(()) } else { ControlFlow::Continue(()) } } #[inline] fn compute_step_size( in_rate: NonZero, out_rate: NonZero, sample_base_note: Note, playing_note: Note, ) -> f32 { // original formula: (outrate / inrate) * (playing_freq / sample_base_freq). // Where each freq is computed with MIDI tuning standard formula: 440 * 2^((note - 69)/12) // manually reduced formula: 2^((play_note - sample_base_note)/12) * (outrate / inrate) // herbie (https://herbie.uwplse.org/demo/index.html) can't optimize further: https://herbie.uwplse.org/demo/e096ef89ee257ad611dd56378bd139a065a6bea0.02e7ec5a3709ad3e06968daa97db50d636f1e44b/graph.html (f32::from(i16::from(playing_note.get()) - i16::from(sample_base_note.get())) / 12.).exp2() * (out_rate.get() as f32 / in_rate.get() as f32) } fn set_step_size(&mut self) { self.step_size = Self::compute_step_size( self.meta.sample_rate, self.out_rate, self.meta.base_note, self.note, ); } pub fn set_out_samplerate(&mut self, samplerate: NonZero) { self.out_rate = samplerate; self.set_step_size(); } /// steps self and sets is_done if needed fn step(&mut self) { self.position.1 += self.step_size; let floor = self.position.1.trunc(); self.position.1 -= floor; self.position.0 += floor as usize; } pub fn iter(&mut self) -> impl Iterator { std::iter::from_fn(|| self.next::()) } pub fn next(&mut self) -> Option { // const block allows turning an invalid u8 into compile time error let interpolation = const { Interpolation::from_u8(INTERPOLATION) }; if self.check_position().is_break() { return None; } let out = match interpolation { Interpolation::Nearest => self.compute_nearest(), Interpolation::Linear => self.compute_linear(), Interpolation::Quadratic => self.compute_quadratic(), }; self.step(); Some(out) } fn compute_linear(&mut self) -> Frame { // There are two types that implement ProcessingFrame: f32 and Frame, so stereo and mono audio data. // the compiler will monomorphize this function to both versions and depending on wether that sample is mono // or stereo the correct version will be called. struct Linear; impl ProcessingFunction<2, S> for Linear { fn process(pos: f32, data: &[S; 2]) -> S { let diff = data[1] - data[0]; (diff * pos) + data[0] } } self.sample.compute::<2, Linear>(self.position) } fn compute_nearest(&mut self) -> Frame { let load_idx = if self.position.1 < 0.5 { self.position.0 } else { self.position.0 + 1 }; self.sample.index(load_idx) } // need to hear it on a better system. With standard output i can't hear a difference. // maybe also look at the waveforms fn compute_quadratic(&mut self) -> Frame { struct Quadratic; impl ProcessingFunction<3, S> for Quadratic { fn process(pos: f32, data: &[S; 3]) -> S { // let y0_half = data[0] / 2.; // let y2_half = data[2] / 2.; // let y1 = data[1]; // (y0_half - y1 + y2_half) * pos * pos + (y2_half - y0_half) * pos + y1 // (data[0] / 2 - data[1] + data[2] / 2) * pos * pos + (data[2] / 2- data[0] / 2) * pos + y1 // https://herbie.uwplse.org/demo/e824f96dd380ac5390d6cb0362398b0e9defed73.0cc3b6492c83efca5bd11399e0830e7873c749d9/graph.html // alternative 1, accuracy 100%, 1.3x speed // using fused multiply add can be faster and more accurate // S::mul_add( // (data[2] - data[0]) / 2. - data[1], // pos * pos, // S::mul_add((data[2] - data[0]) * pos, 0.5, data[1]), // ) // // alternative 2, accuracy 100%, 1.5x speed // even alternative 3 with 98.5% accuracy sounds really bad S::mul_add( S::mul_add( (data[2] + data[0]) * 0.5 - data[1], pos, (data[0] - data[2]) * -0.5, ), pos, data[1], ) } } self.sample.compute::<3, Quadratic>(self.position) } }