Skip to main content

rill_core_dsp/generators/
sample_player.rs

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