Skip to main content

rill_patchbay/sequencer/
engine.rs

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