Skip to main content

rill_patchbay/sequencer/
engine.rs

1use std::collections::HashMap;
2
3use super::pattern::{Pattern, StepPlayMode};
4use super::snapshot::Snapshot;
5use rill_core::queues::{SetParameter, SignalOrigin};
6use rill_core::traits::ParamValue;
7use rill_core::traits::{ParameterId, PortId};
8
9/// The core sequencer state machine.
10///
11/// Driven by incoming CLOCK_TICK telemetry from the audio thread.  Call
12/// [`tick`](Self::tick) or [`tick_ext`](Self::tick_ext) every time a clock
13/// sample-position arrives; the sequencer checks whether the current step's
14/// duration has elapsed and advances if so, returning the p-lock parameter
15/// commands for the new step.
16///
17/// # Thread safety
18///
19/// `SnapshotSequencer` is `Send` but **not** `Sync`.  It should live inside
20/// a single task (typically the tokio task that drains the telemetry
21/// receiver).  External control (start/stop/pattern change) uses a command
22/// channel (see [`SequencerHandle`]).
23#[derive(Debug, Clone)]
24pub struct SnapshotSequencer {
25    /// Named snapshots for quick recall.
26    snapshots: HashMap<String, Snapshot>,
27    /// Registered patterns.
28    patterns: HashMap<String, Pattern>,
29    /// Currently active pattern ID.
30    active_pattern: String,
31    /// Index of the current step within the active pattern.
32    current_step: usize,
33    /// Absolute sample position when the current step started.
34    step_start_sample: u64,
35    /// Play direction for PingPong mode (1 = forward, -1 = backward).
36    direction: i8,
37    /// Whether the sequencer is running.
38    running: bool,
39    /// Latest beat position received from telemetry.
40    latest_beat_position: f32,
41    /// Whether the latest tick was a new beat boundary.
42    latest_new_beat: bool,
43    /// Whether the latest tick was a new bar boundary.
44    latest_new_bar: bool,
45}
46
47impl SnapshotSequencer {
48    /// Create a new, empty sequencer (no patterns, not running).
49    pub fn new() -> Self {
50        Self {
51            snapshots: HashMap::new(),
52            patterns: HashMap::new(),
53            active_pattern: String::new(),
54            current_step: 0,
55            step_start_sample: 0,
56            direction: 1,
57            running: false,
58            latest_beat_position: 0.0,
59            latest_new_beat: false,
60            latest_new_bar: false,
61        }
62    }
63
64    /// Create a sequencer pre-loaded with snapshots and patterns.
65    ///
66    /// The first pattern in the vec becomes the active pattern.
67    pub fn with_lib(snapshots: Vec<Snapshot>, patterns: Vec<Pattern>) -> Self {
68        let mut s = Self::new();
69        for snap in snapshots {
70            s.add_snapshot(snap);
71        }
72        for pat in patterns {
73            s.add_pattern(pat);
74        }
75        if let Some(first) = s.patterns.keys().next() {
76            s.active_pattern = first.clone();
77        }
78        s
79    }
80
81    // ── Snapshots ────────────────────────────────────────────────────
82
83    /// Register or replace a named snapshot.
84    pub fn add_snapshot(&mut self, snapshot: Snapshot) {
85        self.snapshots.insert(snapshot.id.clone(), snapshot);
86    }
87
88    /// Get a snapshot by ID, if present.
89    pub fn get_snapshot(&self, id: &str) -> Option<&Snapshot> {
90        self.snapshots.get(id)
91    }
92
93    /// Remove a snapshot by ID.  Does *not* affect existing patterns.
94    pub fn remove_snapshot(&mut self, id: &str) -> bool {
95        self.snapshots.remove(id).is_some()
96    }
97
98    // ── Patterns ─────────────────────────────────────────────────────
99
100    /// Register or replace a named pattern.
101    pub fn add_pattern(&mut self, pattern: Pattern) {
102        if self.active_pattern.is_empty() {
103            self.active_pattern = pattern.id.clone();
104        }
105        self.patterns.insert(pattern.id.clone(), pattern);
106    }
107
108    /// Get a pattern by ID, if present.
109    pub fn get_pattern(&self, id: &str) -> Option<&Pattern> {
110        self.patterns.get(id)
111    }
112
113    /// Remove a pattern by ID.  If it is the active pattern the sequencer
114    /// stops.
115    pub fn remove_pattern(&mut self, id: &str) -> bool {
116        if self.patterns.remove(id).is_some() {
117            if self.active_pattern == id {
118                self.active_pattern.clear();
119                self.running = false;
120            }
121            true
122        } else {
123            false
124        }
125    }
126
127    /// Switch to a different pattern (may be empty).
128    ///
129    /// Resets the step counter to 0.  If the pattern does not exist the
130    /// call is ignored.
131    pub fn set_active_pattern(&mut self, id: &str) {
132        if self.patterns.contains_key(id) || id.is_empty() {
133            self.active_pattern = id.to_string();
134            self.current_step = 0;
135            self.step_start_sample = 0;
136            self.direction = 1;
137        }
138    }
139
140    /// Active pattern ID.
141    pub fn active_pattern(&self) -> &str {
142        &self.active_pattern
143    }
144
145    // ── Transport ────────────────────────────────────────────────────
146
147    /// Start or resume the sequencer from the current step.
148    pub fn start(&mut self) {
149        self.running = true;
150    }
151
152    /// Pause the sequencer (keeps current step position).
153    pub fn stop(&mut self) {
154        self.running = false;
155    }
156
157    /// Reset the sequencer to step 0 at the given sample position.
158    pub fn reset(&mut self, sample_pos: u64) {
159        self.current_step = 0;
160        self.step_start_sample = sample_pos;
161        self.direction = 1;
162    }
163
164    /// Whether the sequencer is running.
165    pub fn is_running(&self) -> bool {
166        self.running
167    }
168
169    /// Index of the current step within the active pattern.
170    pub fn current_step(&self) -> usize {
171        self.current_step
172    }
173
174    /// Latest beat position received via [`tick_ext`](Self::tick_ext).
175    ///
176    /// Updated on every clock tick; `0.0` if no tempo data or
177    /// [`tick`](Self::tick) is used.
178    pub fn latest_beat_position(&self) -> f32 {
179        self.latest_beat_position
180    }
181
182    /// Whether the latest clock tick was at a beat boundary.
183    pub fn is_new_beat(&self) -> bool {
184        self.latest_new_beat
185    }
186
187    /// Whether the latest clock tick was at a bar boundary.
188    pub fn is_new_bar(&self) -> bool {
189        self.latest_new_bar
190    }
191
192    // ── Tick (the main state-machine entry point) ────────────────────
193
194    /// Advance the sequencer by one clock tick (basic version).
195    ///
196    /// Convenience wrapper around [`tick_ext`](Self::tick_ext) that passes
197    /// default beat info (beat_position=0, no beat/bar boundaries).
198    /// Prefer `tick_ext` when beat-aware CLOCK_TICK telemetry is available.
199    pub fn tick(&mut self, sample_pos: u64, sample_rate: f32, tempo: f32) -> Vec<SetParameter> {
200        self.tick_ext(sample_pos, sample_rate, tempo, 0.0, false, false)
201    }
202
203    /// Advance the sequencer by one clock tick with beat-aware telemetry.
204    ///
205    /// Call this from the telemetry listener task every time a `CLOCK_TICK`
206    /// event arrives from the audio thread.  The extended parameters
207    /// (`beat_position`, `is_new_beat`, `is_new_bar`) are stored in the
208    /// sequencer state and can be queried by algorithmic sequencer logic
209    /// (see [`latest_beat_position`](Self::latest_beat_position),
210    /// [`is_new_beat`](Self::is_new_beat),
211    /// [`is_new_bar`](Self::is_new_bar)).
212    ///
213    /// Returns a batch of [`SetParameter`] values to push when a step
214    /// boundary is crossed, or an empty `Vec` if no step change occurred.
215    pub fn tick_ext(
216        &mut self,
217        sample_pos: u64,
218        sample_rate: f32,
219        tempo: f32,
220        beat_position: f32,
221        is_new_beat: bool,
222        is_new_bar: bool,
223    ) -> Vec<SetParameter> {
224        self.latest_beat_position = beat_position;
225        self.latest_new_beat = is_new_beat;
226        self.latest_new_bar = is_new_bar;
227        if !self.running {
228            return Vec::new();
229        }
230
231        let (len, play_mode, step_dur) = {
232            let pat = match self.patterns.get(&self.active_pattern) {
233                Some(p) if !p.steps.is_empty() => p,
234                _ => return Vec::new(),
235            };
236            let step = &pat.steps[self.current_step];
237            (
238                pat.steps.len(),
239                pat.play_mode,
240                step.duration_samples(tempo, sample_rate),
241            )
242        };
243
244        let elapsed = sample_pos.saturating_sub(self.step_start_sample);
245
246        if elapsed >= step_dur {
247            self.current_step = self.advance_step(len, play_mode);
248            self.step_start_sample = sample_pos;
249
250            if let Some(pat) = self.patterns.get(&self.active_pattern) {
251                if self.current_step < pat.steps.len() {
252                    let new_step = &pat.steps[self.current_step];
253                    return new_step
254                        .parameters
255                        .iter()
256                        .map(|p| {
257                            SetParameter::new(
258                                PortId::param(p.node_id, 0),
259                                ParameterId::new(&p.param_name).unwrap(),
260                                ParamValue::Float(p.value),
261                                SignalOrigin::Manual,
262                            )
263                        })
264                        .collect();
265                }
266            }
267        }
268
269        Vec::new()
270    }
271
272    /// Pick the next step index and update direction for PingPong mode.
273    fn advance_step(&mut self, len: usize, play_mode: StepPlayMode) -> usize {
274        if len == 0 {
275            return 0;
276        }
277        match play_mode {
278            StepPlayMode::OneShot => (self.current_step + 1).min(len.saturating_sub(1)),
279            StepPlayMode::Loop => (self.current_step + 1) % len,
280            StepPlayMode::PingPong => {
281                let next = self.current_step as isize + self.direction as isize;
282                if next < 0 {
283                    self.direction = 1;
284                    1
285                } else if next >= len as isize {
286                    self.direction = -1;
287                    len.saturating_sub(2)
288                } else {
289                    next as usize
290                }
291            }
292            StepPlayMode::Random => {
293                use rand::Rng;
294                let mut rng = rand::thread_rng();
295                rng.gen_range(0..len)
296            }
297            StepPlayMode::Brownian => {
298                use rand::Rng;
299                let mut rng = rand::thread_rng();
300                let offset: isize = rng.gen_range(-1..=1);
301                (self.current_step as isize + offset).clamp(0, len.saturating_sub(1) as isize)
302                    as usize
303            }
304        }
305    }
306}
307
308impl Default for SnapshotSequencer {
309    fn default() -> Self {
310        Self::new()
311    }
312}
313
314// =============================================================================
315// Sequencer command channel & handle
316// =============================================================================
317
318/// Commands sent to a running sequencer from another thread.
319#[derive(Debug, Clone, PartialEq)]
320pub enum SequencerCommand {
321    /// Start playback.
322    Start,
323    /// Stop playback.
324    Stop,
325    /// Reset to the given sample position.
326    Reset {
327        /// Target sample position for the reset.
328        sample_pos: u64,
329    },
330    /// Switch to a named pattern.
331    SetPattern(String),
332}
333
334/// Handle for controlling a [`SnapshotSequencer`] that lives inside a
335/// tokio task.
336///
337/// Clone the handle to share control of the sequencer across multiple
338/// threads (e.g. multiple OSC handlers).
339#[derive(Debug, Clone)]
340pub struct SequencerHandle {
341    cmd_tx: std::sync::Arc<crossbeam_channel::Sender<SequencerCommand>>,
342}
343
344impl SequencerHandle {
345    pub(crate) fn new(cmd_tx: crossbeam_channel::Sender<SequencerCommand>) -> Self {
346        Self {
347            cmd_tx: std::sync::Arc::new(cmd_tx),
348        }
349    }
350
351    /// Start the sequencer.
352    pub fn start(&self) {
353        let _ = self.cmd_tx.try_send(SequencerCommand::Start);
354    }
355
356    /// Stop the sequencer.
357    pub fn stop(&self) {
358        let _ = self.cmd_tx.try_send(SequencerCommand::Stop);
359    }
360
361    /// Reset the sequencer to the given sample position.
362    pub fn reset(&self, sample_pos: u64) {
363        let _ = self.cmd_tx.try_send(SequencerCommand::Reset { sample_pos });
364    }
365
366    /// Switch to a different pattern by ID.
367    pub fn set_pattern(&self, id: &str) {
368        let _ = self
369            .cmd_tx
370            .try_send(SequencerCommand::SetPattern(id.to_string()));
371    }
372}
373
374// =============================================================================
375// Tests
376// =============================================================================
377
378#[cfg(test)]
379mod tests {
380    use super::*;
381    use crate::sequencer::{ParameterTarget, SequenceStep};
382    use rill_core::NodeId;
383
384    fn make_step(value: f32, dur: f64) -> SequenceStep {
385        SequenceStep::single(NodeId(1), "param", value, dur)
386    }
387
388    fn simple_pattern() -> Pattern {
389        Pattern::new(
390            "p1",
391            vec![
392                make_step(0.0, 1.0),
393                make_step(0.5, 1.0),
394                make_step(1.0, 1.0),
395                make_step(0.5, 1.0),
396            ],
397        )
398    }
399
400    #[test]
401    fn test_sequencer_loop() {
402        let mut seq = SnapshotSequencer::with_lib(vec![], vec![simple_pattern()]);
403        seq.start();
404
405        let sr = 48000.0;
406        let tempo = 120.0;
407
408        let cmds = seq.tick(24000, sr, tempo);
409        assert!(!cmds.is_empty(), "should advance to step 1");
410        assert_eq!(seq.current_step, 1);
411
412        let cmds = seq.tick(48000, sr, tempo);
413        assert!(!cmds.is_empty());
414        assert_eq!(seq.current_step, 2);
415
416        let cmds = seq.tick(72000, sr, tempo);
417        assert!(!cmds.is_empty());
418        assert_eq!(seq.current_step, 3);
419
420        let cmds = seq.tick(96000, sr, tempo);
421        assert!(!cmds.is_empty());
422        assert_eq!(seq.current_step, 0);
423    }
424
425    #[test]
426    fn test_sequencer_not_running() {
427        let mut seq = SnapshotSequencer::with_lib(vec![], vec![simple_pattern()]);
428        let cmds = seq.tick(24000, 48000.0, 120.0);
429        assert!(cmds.is_empty());
430        assert_eq!(seq.current_step, 0);
431    }
432
433    #[test]
434    fn test_sequencer_stop() {
435        let mut seq = SnapshotSequencer::with_lib(vec![], vec![simple_pattern()]);
436        seq.start();
437        seq.tick(24000, 48000.0, 120.0);
438        assert_eq!(seq.current_step, 1);
439
440        seq.stop();
441        seq.tick(48000, 48000.0, 120.0);
442        assert_eq!(seq.current_step, 1, "should not advance after stop");
443    }
444
445    #[test]
446    fn test_sequencer_pingpong() {
447        let mut seq = SnapshotSequencer::with_lib(
448            vec![],
449            vec![Pattern::new(
450                "p1",
451                vec![
452                    make_step(0.0, 1.0),
453                    make_step(0.5, 1.0),
454                    make_step(1.0, 1.0),
455                ],
456            )
457            .with_mode(StepPlayMode::PingPong)],
458        );
459        seq.start();
460
461        seq.tick(24000, 48000.0, 120.0);
462        assert_eq!(seq.current_step, 1);
463        seq.tick(48000, 48000.0, 120.0);
464        assert_eq!(seq.current_step, 2);
465        seq.tick(72000, 48000.0, 120.0);
466        assert_eq!(seq.current_step, 1);
467        seq.tick(96000, 48000.0, 120.0);
468        assert_eq!(seq.current_step, 0);
469    }
470
471    #[test]
472    fn test_sequencer_oneshot() {
473        let mut seq = SnapshotSequencer::with_lib(
474            vec![],
475            vec![
476                Pattern::new("p1", vec![make_step(0.0, 1.0), make_step(0.5, 1.0)])
477                    .with_mode(StepPlayMode::OneShot),
478            ],
479        );
480        seq.start();
481
482        seq.tick(24000, 48000.0, 120.0);
483        assert_eq!(seq.current_step, 1);
484        seq.tick(48000, 48000.0, 120.0);
485        assert_eq!(seq.current_step, 1);
486    }
487
488    #[test]
489    fn test_sequencer_set_pattern() {
490        let mut seq = SnapshotSequencer::with_lib(
491            vec![],
492            vec![
493                Pattern::new("a", vec![make_step(1.0, 1.0)]),
494                Pattern::new("b", vec![make_step(0.0, 1.0), make_step(0.5, 1.0)]),
495            ],
496        );
497        seq.set_active_pattern("a");
498        seq.start();
499
500        assert_eq!(seq.active_pattern(), "a");
501
502        seq.set_active_pattern("b");
503        assert_eq!(seq.active_pattern(), "b");
504        assert_eq!(seq.current_step, 0);
505
506        let cmds = seq.tick(24000, 48000.0, 120.0);
507        assert!(!cmds.is_empty());
508        assert_eq!(seq.current_step, 1);
509
510        let cmds = seq.tick(48000, 48000.0, 120.0);
511        assert!(!cmds.is_empty());
512        assert_eq!(seq.current_step, 0);
513    }
514
515    #[test]
516    fn test_step_duration_samples() {
517        let step = make_step(0.5, 1.0);
518        assert_eq!(step.duration_samples(120.0, 48000.0), 24000);
519        assert_eq!(step.duration_samples(120.0, 44100.0), 22050);
520        assert_eq!(step.duration_samples(60.0, 48000.0), 48000);
521
522        let eighth = make_step(0.5, 0.5);
523        assert_eq!(eighth.duration_samples(120.0, 48000.0), 12000);
524
525        let sixteenth = make_step(0.5, 0.25);
526        assert_eq!(sixteenth.duration_samples(120.0, 48000.0), 6000);
527    }
528
529    #[test]
530    fn test_parameter_target_creation() {
531        let pt = ParameterTarget::new(NodeId(1), "gain", 0.5);
532        assert_eq!(pt.node_id, NodeId(1));
533        assert_eq!(pt.param_name, "gain");
534        assert_eq!(pt.value, 0.5);
535    }
536
537    #[test]
538    fn test_sequencer_handle_send() {
539        let (tx, rx) = crossbeam_channel::unbounded::<SequencerCommand>();
540        let handle = SequencerHandle::new(tx);
541
542        handle.start();
543        assert_eq!(rx.try_recv(), Ok(SequencerCommand::Start));
544
545        handle.stop();
546        assert_eq!(rx.try_recv(), Ok(SequencerCommand::Stop));
547
548        handle.set_pattern("foo");
549        assert_eq!(
550            rx.try_recv(),
551            Ok(SequencerCommand::SetPattern("foo".into()))
552        );
553
554        handle.reset(12345);
555        assert_eq!(
556            rx.try_recv(),
557            Ok(SequencerCommand::Reset { sample_pos: 12345 })
558        );
559    }
560
561    #[test]
562    fn test_tick_ext_stores_beat_info() {
563        let mut seq = SnapshotSequencer::new();
564        let pat = Pattern::new("p1", vec![SequenceStep::single(NodeId(1), "p", 0.5, 1.0)]);
565        seq.add_pattern(pat);
566        seq.set_active_pattern("p1");
567        seq.start();
568
569        let _ = seq.tick_ext(0, 48000.0, 120.0, 0.0, true, true);
570        assert!((seq.latest_beat_position() - 0.0).abs() < 1e-6);
571        assert!(seq.is_new_beat());
572        assert!(seq.is_new_bar());
573
574        let _ = seq.tick(24000, 48000.0, 120.0);
575        assert!((seq.latest_beat_position() - 0.0).abs() < 1e-6);
576        assert!(!seq.is_new_beat());
577        assert!(!seq.is_new_bar());
578
579        let _ = seq.tick_ext(48000, 48000.0, 120.0, 2.5, false, false);
580        assert!((seq.latest_beat_position() - 2.5).abs() < 1e-6);
581        assert!(!seq.is_new_beat());
582        assert!(!seq.is_new_bar());
583    }
584}