Skip to main content

rill_core_dsp/generators/
sample_player.rs

1use crate::algorithm::{Algorithm, AlgorithmCategory, AlgorithmMetadata};
2use crate::generators::{Generator, InterpolatedReader};
3use rill_core::traits::{ActionContext, ProcessResult};
4use rill_core::Transcendental;
5
6/// Loop behaviour for [`SamplePlayer`].
7#[derive(Debug, Clone, Copy, PartialEq)]
8pub enum LoopMode {
9    /// Play once from start to end, then output silence.
10    OneShot,
11    /// Loop forward when reaching the end position.
12    Forward,
13    /// Reverse direction at loop boundaries (bouncing loop).
14    PingPong,
15}
16
17/// Playback state.
18#[derive(Debug, Clone, Copy, PartialEq)]
19pub enum PlayState {
20    Stopped,
21    Playing,
22}
23
24/// Sample-playback algorithm built on [`InterpolatedReader`].
25///
26/// Plays back a fixed buffer with variable rate, interpolation,
27/// and configurable looping. Implements [`Algorithm`] for use in
28/// signal graphs, and [`Generator`] for compatibility with the
29/// oscillator trait hierarchy.
30///
31/// # Generator parameter mapping
32///
33/// | `Generator` | Meaning for `SamplePlayer` |
34/// |---|---|
35/// | `frequency` | Pitch: `rate = freq * len / sample_rate` |
36/// | `phase` | Normalised position (0 = start, 1 = end of buffer) |
37/// | `amplitude` | Output gain |
38pub struct SamplePlayer<T: Transcendental> {
39    reader: InterpolatedReader<T>,
40    loop_mode: LoopMode,
41    loop_start: f64,
42    loop_end: f64,
43    state: PlayState,
44    gate: bool,
45    amplitude: T,
46    sample_rate: f32,
47}
48
49impl<T: Transcendental> SamplePlayer<T> {
50    pub fn new(buffer: Vec<T>) -> Self {
51        let len = buffer.len() as f64;
52        Self {
53            reader: InterpolatedReader::new(buffer),
54            loop_mode: LoopMode::OneShot,
55            loop_start: 0.0,
56            loop_end: len,
57            state: PlayState::Stopped,
58            gate: false,
59            amplitude: T::from_f32(1.0),
60            sample_rate: 44100.0,
61        }
62    }
63
64    pub fn from_boxed(buffer: Box<[T]>) -> Self {
65        let len = buffer.len() as f64;
66        Self {
67            reader: InterpolatedReader::from_boxed(buffer),
68            loop_mode: LoopMode::OneShot,
69            loop_start: 0.0,
70            loop_end: len,
71            state: PlayState::Stopped,
72            gate: false,
73            amplitude: T::from_f32(1.0),
74            sample_rate: 44100.0,
75        }
76    }
77
78    pub fn len(&self) -> usize {
79        self.reader.len()
80    }
81
82    pub fn is_empty(&self) -> bool {
83        self.reader.is_empty()
84    }
85
86    pub fn loop_mode(&self) -> LoopMode {
87        self.loop_mode
88    }
89
90    pub fn set_loop_mode(&mut self, mode: LoopMode) {
91        self.loop_mode = mode;
92    }
93
94    pub fn loop_start(&self) -> f64 {
95        self.loop_start
96    }
97
98    pub fn set_loop_start(&mut self, start: f64) {
99        self.loop_start = start.clamp(0.0, self.reader.len() as f64);
100    }
101
102    pub fn loop_end(&self) -> f64 {
103        self.loop_end
104    }
105
106    pub fn set_loop_end(&mut self, end: f64) {
107        let max = self.reader.len() as f64;
108        self.loop_end = end.clamp(0.0, max);
109    }
110
111    pub fn gate(&self) -> bool {
112        self.gate
113    }
114
115    /// Start / stop playback.
116    ///
117    /// Setting `gate = true` restarts from the beginning.
118    pub fn set_gate(&mut self, gate: bool) {
119        if gate && !self.gate {
120            self.reader.set_position(self.loop_start);
121            self.state = PlayState::Playing;
122        } else if !gate {
123            self.state = PlayState::Stopped;
124        }
125        self.gate = gate;
126    }
127
128    pub fn play_state(&self) -> PlayState {
129        self.state
130    }
131
132    /// Replace the sample buffer and reset to loop-start.
133    pub fn set_buffer(&mut self, buffer: Vec<T>) {
134        self.reader.set_buffer(buffer);
135        self.loop_end = self.reader.len() as f64;
136        self.loop_start = 0.0;
137    }
138
139    pub fn set_cubic(&mut self, cubic: bool) {
140        self.reader.set_cubic(cubic);
141    }
142
143    pub fn is_cubic(&self) -> bool {
144        self.reader.is_cubic()
145    }
146
147    pub fn set_playback_rate(&mut self, rate: f64) {
148        self.reader.set_rate(rate);
149    }
150
151    pub fn playback_rate(&self) -> f64 {
152        self.reader.rate()
153    }
154
155    fn playable_len(&self) -> f64 {
156        (self.loop_end - self.loop_start).max(1.0)
157    }
158}
159
160impl<T: Transcendental> Algorithm<T> for SamplePlayer<T> {
161    fn init(&mut self, sample_rate: f32) {
162        self.sample_rate = sample_rate;
163        self.reader.set_position(self.loop_start);
164        self.state = PlayState::Stopped;
165    }
166
167    fn reset(&mut self) {
168        self.gate = false;
169        self.state = PlayState::Stopped;
170        self.reader.set_position(self.loop_start);
171    }
172
173    fn process(
174        &mut self,
175        _input: Option<&[T]>,
176        output: &mut [T],
177        _ctx: &ActionContext,
178    ) -> ProcessResult<()> {
179        if !self.gate || self.state == PlayState::Stopped || self.is_empty() {
180            for s in output.iter_mut() {
181                *s = T::ZERO;
182            }
183            return Ok(());
184        }
185
186        let amp = self.amplitude;
187        let start = self.loop_start;
188        let end = self.loop_end;
189
190        for s in output.iter_mut() {
191            if !self.gate || self.state == PlayState::Stopped {
192                *s = T::ZERO;
193                continue;
194            }
195
196            *s = self.reader.read_one() * amp;
197            self.reader.advance();
198
199            let pos = self.reader.position();
200            let going_forward = self.reader.rate() >= 0.0;
201
202            if self.loop_mode == LoopMode::OneShot {
203                if pos >= end || pos < 0.0 {
204                    self.state = PlayState::Stopped;
205                    self.gate = false;
206                }
207            } else if self.loop_mode == LoopMode::Forward {
208                if going_forward && pos >= end {
209                    self.reader.set_position(start + (pos - end));
210                } else if !going_forward && pos < start {
211                    self.reader.set_position(end - (start - pos));
212                }
213            } else if going_forward && pos >= end {
214                // PingPong: hit forward boundary → reverse
215                self.reader.set_rate(-self.reader.rate());
216                self.reader.set_position(end - 1.0);
217            } else if !going_forward && pos <= start {
218                // PingPong: hit backward boundary → forward
219                self.reader.set_rate(-self.reader.rate());
220                self.reader.set_position(start);
221            }
222        }
223
224        Ok(())
225    }
226
227    fn metadata(&self) -> AlgorithmMetadata {
228        AlgorithmMetadata {
229            name: "SamplePlayer",
230            category: AlgorithmCategory::Generator,
231            description: "Sample playback with loop modes".into(),
232            author: "Rill",
233            version: env!("CARGO_PKG_VERSION"),
234        }
235    }
236}
237
238impl<T: Transcendental + Copy> Generator<T> for SamplePlayer<T> {
239    fn phase(&self) -> T {
240        let len = self.reader.len() as f64;
241        if len == 0.0 {
242            return T::ZERO;
243        }
244        T::from_f64((self.reader.position() / len).clamp(0.0, 1.0))
245    }
246
247    fn set_phase(&mut self, phase: T) {
248        let p = phase.to_f64().clamp(0.0, 1.0);
249        let len = self.reader.len() as f64;
250        self.reader.set_position(p * len);
251    }
252
253    fn reset_phase(&mut self) {
254        self.reader.set_position(0.0);
255    }
256
257    fn frequency(&self) -> f32 {
258        if self.is_empty() {
259            return 0.0;
260        }
261        let rate = self.reader.rate();
262        let len = self.reader.len() as f64;
263        (rate * self.sample_rate as f64 / len) as f32
264    }
265
266    fn set_frequency(&mut self, freq: f32) {
267        if self.is_empty() {
268            return;
269        }
270        let len = self.reader.len() as f64;
271        let rate = freq as f64 * len / self.sample_rate as f64;
272        self.reader.set_rate(rate);
273    }
274
275    fn amplitude(&self) -> T {
276        self.amplitude
277    }
278
279    fn set_amplitude(&mut self, amp: T) {
280        self.amplitude = amp.clamp(T::ZERO, T::from_f32(1.0));
281    }
282}
283
284#[cfg(test)]
285mod tests {
286    use super::*;
287    use rill_core::time::ClockTick;
288    use rill_core::traits::ActionContext;
289
290    fn process(player: &mut SamplePlayer<f64>, out: &mut [f64]) {
291        let tick = ClockTick::new(0, 0, 44100.0);
292        player.process(None, out, &ActionContext::new(&tick)).unwrap();
293    }
294
295    #[test]
296    fn test_one_shot() {
297        let buf = vec![1.0, 2.0, 3.0, 4.0];
298        let mut player = SamplePlayer::new(buf);
299        player.set_gate(true);
300        let mut out = [0.0f64; 6];
301        process(&mut player, &mut out[..3]);
302        assert!(player.play_state() == PlayState::Playing, "still playing after 3/4");
303        process(&mut player, &mut out[3..]);
304        assert_eq!(out[0..4], [1.0, 2.0, 3.0, 4.0], "all samples read");
305        assert_eq!(out[4..6], [0.0, 0.0], "silence after end");
306        assert_eq!(player.play_state(), PlayState::Stopped, "stopped after end");
307    }
308
309    #[test]
310    fn test_loop_forward() {
311        let buf = vec![1.0, 2.0, 3.0];
312        let mut player = SamplePlayer::new(buf);
313        player.set_loop_mode(LoopMode::Forward);
314        player.set_gate(true);
315        let mut out = [0.0f64; 6];
316        process(&mut player, &mut out);
317        assert_eq!(out, [1.0, 2.0, 3.0, 1.0, 2.0, 3.0]);
318    }
319
320    #[test]
321    fn test_ping_pong() {
322        let buf = vec![1.0, 2.0, 3.0, 4.0];
323        let mut player = SamplePlayer::new(buf);
324        player.set_loop_mode(LoopMode::PingPong);
325        player.set_gate(true);
326        let mut out = [0.0f64; 12];
327        process(&mut player, &mut out);
328        // Forward [1,2,3,4], reverse [4,3,2,1], forward...
329        assert_eq!(out[0..4], [1.0, 2.0, 3.0, 4.0], "forward pass");
330        assert_eq!(out[5..8], [3.0, 2.0, 1.0], "reverse pass (minus endpoint)");
331        assert_eq!(out[8..11], [2.0, 3.0, 4.0], "second forward pass");
332    }
333
334    #[test]
335    fn test_gate_restart() {
336        let buf = vec![10.0, 20.0, 30.0];
337        let mut player = SamplePlayer::new(buf);
338        player.set_gate(true);
339        let mut out = [0.0f64; 2];
340        process(&mut player, &mut out);
341        assert_eq!(out, [10.0, 20.0]);
342        player.set_gate(false);
343        process(&mut player, &mut out);
344        assert_eq!(out, [0.0, 0.0]);
345        player.set_gate(true);
346        process(&mut player, &mut out);
347        assert_eq!(out, [10.0, 20.0]);
348    }
349
350    #[test]
351    fn test_frequency_mapping() {
352        let buf = vec![1.0, 2.0, 3.0, 4.0];
353        let mut player = SamplePlayer::new(buf);
354        player.init(44100.0);
355        let freq_at_unit_rate = player.frequency();
356        assert!((freq_at_unit_rate - 44100.0 / 4.0).abs() < 1.0,
357            "expected ~11025 Hz at rate=1, got {}", freq_at_unit_rate);
358        player.set_frequency(freq_at_unit_rate * 2.0);
359        assert!((player.playback_rate() - 2.0).abs() < 1e-6,
360            "expected rate=2.0, got {}", player.playback_rate());
361    }
362
363    #[test]
364    fn test_empty_buffer() {
365        let buf: Vec<f64> = vec![];
366        let mut player = SamplePlayer::new(buf);
367        player.set_gate(true);
368        let mut out = [1.0f64; 4];
369        process(&mut player, &mut out);
370        assert_eq!(out, [0.0; 4]);
371    }
372}