old school music tracker audio backend
at dev 206 lines 7.1 kB view raw
1use std::{num::NonZero, ops::ControlFlow}; 2 3use crate::{ 4 project::note_event::Note, 5 sample::{ProcessingFrame, ProcessingFunction, Sample, SampleMetaData}, 6}; 7 8use super::Frame; 9 10#[repr(u8)] 11// quadratic is probably enough i can't hear it anymore 12#[derive(Copy, Clone, Debug)] 13pub enum Interpolation { 14 Nearest = 0, 15 Linear = 1, 16 Quadratic = 2, 17} 18 19impl From<u8> for Interpolation { 20 fn from(value: u8) -> Self { 21 Self::from_u8(value) 22 } 23} 24 25impl Interpolation { 26 /// Amount of Padding in the SampleData to do each type of Interpolation. 27 /// This much padding is needed at the start and end of the sample. 28 pub const fn pad_needed(&self) -> usize { 29 match self { 30 Interpolation::Nearest => 1, 31 Interpolation::Linear => 1, 32 Interpolation::Quadratic => 2, 33 } 34 } 35 36 pub const fn from_u8(value: u8) -> Self { 37 match value { 38 0 => Self::Nearest, 39 1 => Self::Linear, 40 2 => Self::Quadratic, 41 _ => panic!(), 42 } 43 } 44} 45 46#[derive(Debug)] 47pub struct SamplePlayer { 48 sample: Sample, 49 meta: SampleMetaData, 50 51 note: Note, 52 // position in the sample, the next output frame should be. 53 // Done this way, so 0 is a valid, useful and intuitive value 54 // always a valid position in the sample. checked against sample lenght on each change 55 // stored as fixed point data: usize + f32 56 // f32 ranges 0..1 57 position: (usize, f32), 58 // is_done: bool, 59 out_rate: NonZero<u32>, 60 // how much the position is advanced for each output sample. 61 // computed from in and out rate 62 step_size: f32, 63} 64 65impl SamplePlayer { 66 pub fn new(sample: Sample, meta: SampleMetaData, out_rate: NonZero<u32>, note: Note) -> Self { 67 let step_size = Self::compute_step_size(meta.sample_rate, out_rate, meta.base_note, note); 68 Self { 69 sample, 70 meta, 71 position: (Sample::PAD_SIZE_EACH, 0.), 72 out_rate, 73 step_size, 74 note, 75 } 76 } 77 78 pub fn check_position(&self) -> ControlFlow<()> { 79 if self.position.0 > self.sample.len_with_pad() - Sample::PAD_SIZE_EACH { 80 ControlFlow::Break(()) 81 } else { 82 ControlFlow::Continue(()) 83 } 84 } 85 86 #[inline] 87 fn compute_step_size( 88 in_rate: NonZero<u32>, 89 out_rate: NonZero<u32>, 90 sample_base_note: Note, 91 playing_note: Note, 92 ) -> f32 { 93 // original formula: (outrate / inrate) * (playing_freq / sample_base_freq). 94 // Where each freq is computed with MIDI tuning standard formula: 440 * 2^((note - 69)/12) 95 // manually reduced formula: 2^((play_note - sample_base_note)/12) * (outrate / inrate) 96 // herbie (https://herbie.uwplse.org/demo/index.html) can't optimize further: https://herbie.uwplse.org/demo/e096ef89ee257ad611dd56378bd139a065a6bea0.02e7ec5a3709ad3e06968daa97db50d636f1e44b/graph.html 97 (f32::from(i16::from(playing_note.get()) - i16::from(sample_base_note.get())) / 12.).exp2() 98 * (out_rate.get() as f32 / in_rate.get() as f32) 99 } 100 101 fn set_step_size(&mut self) { 102 self.step_size = Self::compute_step_size( 103 self.meta.sample_rate, 104 self.out_rate, 105 self.meta.base_note, 106 self.note, 107 ); 108 } 109 110 pub fn set_out_samplerate(&mut self, samplerate: NonZero<u32>) { 111 self.out_rate = samplerate; 112 self.set_step_size(); 113 } 114 115 /// steps self and sets is_done if needed 116 fn step(&mut self) { 117 self.position.1 += self.step_size; 118 let floor = self.position.1.trunc(); 119 self.position.1 -= floor; 120 self.position.0 += floor as usize; 121 } 122 123 pub fn iter<const INTERPOLATION: u8>(&mut self) -> impl Iterator<Item = Frame> { 124 std::iter::from_fn(|| self.next::<INTERPOLATION>()) 125 } 126 127 pub fn next<const INTERPOLATION: u8>(&mut self) -> Option<Frame> { 128 // const block allows turning an invalid u8 into compile time error 129 let interpolation = const { Interpolation::from_u8(INTERPOLATION) }; 130 131 if self.check_position().is_break() { 132 return None; 133 } 134 135 let out = match interpolation { 136 Interpolation::Nearest => self.compute_nearest(), 137 Interpolation::Linear => self.compute_linear(), 138 Interpolation::Quadratic => self.compute_quadratic(), 139 }; 140 141 self.step(); 142 Some(out) 143 } 144 145 fn compute_linear(&mut self) -> Frame { 146 // There are two types that implement ProcessingFrame: f32 and Frame, so stereo and mono audio data. 147 // the compiler will monomorphize this function to both versions and depending on wether that sample is mono 148 // or stereo the correct version will be called. 149 struct Linear; 150 impl<S: ProcessingFrame> ProcessingFunction<2, S> for Linear { 151 fn process(pos: f32, data: &[S; 2]) -> S { 152 let diff = data[1] - data[0]; 153 (diff * pos) + data[0] 154 } 155 } 156 self.sample.compute::<2, Linear>(self.position) 157 } 158 159 fn compute_nearest(&mut self) -> Frame { 160 let load_idx = if self.position.1 < 0.5 { 161 self.position.0 162 } else { 163 self.position.0 + 1 164 }; 165 166 self.sample.index(load_idx) 167 } 168 169 // need to hear it on a better system. With standard output i can't hear a difference. 170 // maybe also look at the waveforms 171 fn compute_quadratic(&mut self) -> Frame { 172 struct Quadratic; 173 impl<S: ProcessingFrame> ProcessingFunction<3, S> for Quadratic { 174 fn process(pos: f32, data: &[S; 3]) -> S { 175 // let y0_half = data[0] / 2.; 176 // let y2_half = data[2] / 2.; 177 // let y1 = data[1]; 178 179 // (y0_half - y1 + y2_half) * pos * pos + (y2_half - y0_half) * pos + y1 180 // (data[0] / 2 - data[1] + data[2] / 2) * pos * pos + (data[2] / 2- data[0] / 2) * pos + y1 181 // https://herbie.uwplse.org/demo/e824f96dd380ac5390d6cb0362398b0e9defed73.0cc3b6492c83efca5bd11399e0830e7873c749d9/graph.html 182 // alternative 1, accuracy 100%, 1.3x speed 183 // using fused multiply add can be faster and more accurate 184 // S::mul_add( 185 // (data[2] - data[0]) / 2. - data[1], 186 // pos * pos, 187 // S::mul_add((data[2] - data[0]) * pos, 0.5, data[1]), 188 // ) 189 // 190 // alternative 2, accuracy 100%, 1.5x speed 191 // even alternative 3 with 98.5% accuracy sounds really bad 192 S::mul_add( 193 S::mul_add( 194 (data[2] + data[0]) * 0.5 - data[1], 195 pos, 196 (data[0] - data[2]) * -0.5, 197 ), 198 pos, 199 data[1], 200 ) 201 } 202 } 203 204 self.sample.compute::<3, Quadratic>(self.position) 205 } 206}