old school music tracker audio backend
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}