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)]
11pub enum Interpolation {
12 Nearest = 0,
13 Linear = 1,
14}
15
16impl From<u8> for Interpolation {
17 fn from(value: u8) -> Self {
18 Self::from_u8(value)
19 }
20}
21
22impl Interpolation {
23 /// Amount of Padding in the SampleData to do each type of Interpolation.
24 /// This much padding is needed at the start and end of the sample.
25 pub const fn pad_needed(&self) -> usize {
26 match self {
27 Interpolation::Nearest => 1,
28 Interpolation::Linear => 1,
29 }
30 }
31
32 pub const fn from_u8(value: u8) -> Self {
33 match value {
34 0 => Self::Nearest,
35 1 => Self::Linear,
36 _ => panic!(),
37 }
38 }
39}
40
41#[derive(Debug)]
42pub struct SamplePlayer {
43 sample: Sample,
44 meta: SampleMetaData,
45
46 note: Note,
47 // position in the sample, the next output frame should be.
48 // Done this way, so 0 is a valid, useful and intuitive value
49 // always a valid position in the sample. checked against sample lenght on each change
50 // stored as fixed point data: usize + f32
51 // f32 ranges 0..1
52 position: (usize, f32),
53 // is_done: bool,
54 out_rate: NonZero<u32>,
55 // how much the position is advanced for each output sample.
56 // computed from in and out rate
57 step_size: f32,
58}
59
60impl SamplePlayer {
61 pub fn new(sample: Sample, meta: SampleMetaData, out_rate: NonZero<u32>, note: Note) -> Self {
62 let step_size = Self::compute_step_size(meta.sample_rate, out_rate, meta.base_note, note);
63 Self {
64 sample,
65 meta,
66 position: (Sample::PAD_SIZE_EACH, 0.),
67 out_rate,
68 step_size,
69 note,
70 }
71 }
72
73 pub fn check_position(&self) -> ControlFlow<()> {
74 if self.position.0 > self.sample.len_with_pad() - Sample::PAD_SIZE_EACH {
75 ControlFlow::Break(())
76 } else {
77 ControlFlow::Continue(())
78 }
79 }
80
81 #[inline]
82 fn compute_step_size(
83 in_rate: NonZero<u32>,
84 out_rate: NonZero<u32>,
85 sample_base_note: Note,
86 playing_note: Note,
87 ) -> f32 {
88 // original formula: (outrate / inrate) * (playing_freq / sample_base_freq).
89 // Where each freq is computed with MIDI tuning standard formula: 440 * 2^((note - 69)/12)
90 // manually reduced formula: 2^((play_note - sample_base_note)/12) * (outrate / inrate)
91 // herbie (https://herbie.uwplse.org/demo/index.html) can't optimize further: https://herbie.uwplse.org/demo/e096ef89ee257ad611dd56378bd139a065a6bea0.02e7ec5a3709ad3e06968daa97db50d636f1e44b/graph.html
92 (f32::from(i16::from(playing_note.get()) - i16::from(sample_base_note.get())) / 12.).exp2()
93 * (out_rate.get() as f32 / in_rate.get() as f32)
94 }
95
96 fn set_step_size(&mut self) {
97 self.step_size = Self::compute_step_size(
98 self.meta.sample_rate,
99 self.out_rate,
100 self.meta.base_note,
101 self.note,
102 );
103 }
104
105 pub fn set_out_samplerate(&mut self, samplerate: NonZero<u32>) {
106 self.out_rate = samplerate;
107 self.set_step_size();
108 }
109
110 /// steps self and sets is_done if needed
111 fn step(&mut self) {
112 self.position.1 += self.step_size;
113 let floor = self.position.1.trunc();
114 self.position.1 -= floor;
115 self.position.0 += floor as usize;
116 }
117
118 pub fn iter<const INTERPOLATION: u8>(&mut self) -> SampleIter<'_, INTERPOLATION> {
119 SampleIter { inner: self }
120 }
121
122 pub fn next<const INTERPOLATION: u8>(&mut self) -> Option<Frame> {
123 // const block allows turning an invalid u8 into compile time error
124 let interpolation = const { Interpolation::from_u8(INTERPOLATION) };
125
126 if self.check_position().is_break() {
127 return None;
128 }
129
130 let out = match interpolation {
131 Interpolation::Nearest => self.compute_nearest(),
132 Interpolation::Linear => self.compute_linear(),
133 };
134
135 self.step();
136 Some(out)
137 }
138
139 fn compute_linear(&mut self) -> Frame {
140 // There are two types that implement ProcessingFrame: f32 and Frame, so stereo and mono audio data.
141 // the compiler will monomorphize this function to both versions and depending on wether that sample is mono
142 // or stereo the correct version will be called.
143 struct Linear(f32);
144 impl<S: ProcessingFrame> ProcessingFunction<2, S> for Linear {
145 fn process(self, data: &[S; 2]) -> S {
146 let diff = data[1] - data[0];
147 (diff * self.0) + data[0]
148 }
149 }
150 self.sample
151 .compute(self.position.0, Linear(self.position.1))
152 }
153
154 fn compute_nearest(&mut self) -> Frame {
155 let load_idx = if self.position.1 < 0.5 {
156 self.position.0
157 } else {
158 self.position.0 + 1
159 };
160
161 self.sample.index(load_idx)
162 }
163}
164
165pub struct SampleIter<'player, const INTERPOLATION: u8> {
166 inner: &'player mut SamplePlayer,
167}
168
169impl<const INTERPOLATION: u8> Iterator for SampleIter<'_, INTERPOLATION> {
170 type Item = Frame;
171
172 fn next(&mut self) -> Option<Self::Item> {
173 self.inner.next::<INTERPOLATION>()
174 }
175}