Skip to main content

rill_patchbay/automaton/
random.rs

1//! # Random processes
2//!
3//! Automata for generating random and pseudo-random sequences:
4//! - Random Walk
5//! - Chaos (deterministic chaos)
6//! - Noise (white, pink, brown noise)
7
8use crate::engine::{Automaton, NoAction, Range, Time};
9use rill_core::traits::ParamValue;
10
11/// Type of random process
12#[derive(Debug, Clone, Copy, PartialEq)]
13pub enum RandomType {
14    /// Random walk
15    Walk,
16    /// Logistic map (chaos)
17    Logistic,
18    /// Hénon map
19    Henon,
20    /// Lorenz system
21    Lorenz,
22    /// White noise
23    WhiteNoise,
24    /// Pink noise (1/f)
25    PinkNoise,
26    /// Brown noise (1/f²)
27    BrownNoise,
28}
29
30/// Random process automaton
31#[derive(Debug, Clone)]
32pub struct RandomAutomaton {
33    /// Automaton name
34    name: String,
35    /// Type of random process
36    rng_type: RandomType,
37    /// Output value range
38    range: Range,
39    /// Rate of change (for Walk)
40    rate: f64,
41    /// Chaos parameters
42    chaos_params: (f64, f64, f64), // r, a, b etc.
43    /// Update rate (for noise)
44    update_rate: f64,
45}
46
47impl RandomAutomaton {
48    /// Create a new Random Walk
49    pub fn walk(name: &str, rate: f64) -> Self {
50        Self {
51            name: name.to_string(),
52            rng_type: RandomType::Walk,
53            range: Range::bipolar(),
54            rate: rate.max(0.0),
55            chaos_params: (0.0, 0.0, 0.0),
56            update_rate: 0.0,
57        }
58    }
59
60    /// Create logistic map (chaos)
61    pub fn logistic(name: &str, r: f64) -> Self {
62        Self {
63            name: name.to_string(),
64            rng_type: RandomType::Logistic,
65            range: Range::unipolar(),
66            rate: 0.0,
67            chaos_params: (r.clamp(3.0, 4.0), 0.0, 0.0),
68            update_rate: 0.0,
69        }
70    }
71
72    /// Create Hénon map
73    pub fn henon(name: &str, a: f64, b: f64) -> Self {
74        Self {
75            name: name.to_string(),
76            rng_type: RandomType::Henon,
77            range: Range::bipolar(),
78            rate: 0.0,
79            chaos_params: (a, b, 0.0),
80            update_rate: 0.0,
81        }
82    }
83
84    /// Create white noise generator
85    pub fn white_noise(name: &str, update_rate: f64) -> Self {
86        Self {
87            name: name.to_string(),
88            rng_type: RandomType::WhiteNoise,
89            range: Range::bipolar(),
90            rate: 0.0,
91            chaos_params: (0.0, 0.0, 0.0),
92            update_rate: update_rate.max(1.0),
93        }
94    }
95
96    /// Set the range
97    pub fn with_range(mut self, range: Range) -> Self {
98        self.range = range;
99        self
100    }
101
102    /// Xorshift RNG
103    fn xorshift(&self, state: &mut u64) -> u64 {
104        let mut x = *state;
105        x ^= x << 13;
106        x ^= x >> 7;
107        x ^= x << 17;
108        *state = x;
109        x
110    }
111
112    /// Random number in the range [0, 1)
113    fn random_f64(&self, state: &mut u64) -> f64 {
114        self.xorshift(state) as f64 / u64::MAX as f64
115    }
116}
117
118impl Automaton for RandomAutomaton {
119    type Internal = (u64, f64, f64); // rng_state, last_value, last_update_time
120    type Action = NoAction;
121
122    fn step(
123        &self,
124        internal: &mut Self::Internal,
125        _current: &ParamValue,
126        time: Time,
127        _action: &Self::Action,
128    ) -> ParamValue {
129        let (rng_state, last_value, last_update_time) = internal;
130        let period = if self.update_rate > 0.0 {
131            1.0 / self.update_rate
132        } else {
133            0.0
134        };
135
136        if period > 0.0 && time - *last_update_time < period {
137            return ParamValue::Float(*last_value as f32);
138        }
139        *last_update_time = time;
140
141        let new_value = match self.rng_type {
142            RandomType::Walk => {
143                let step = (self.random_f64(rng_state) - 0.5) * 2.0 * self.rate;
144                (*last_value + step).clamp(-1.0, 1.0)
145            }
146            RandomType::Logistic => {
147                let r = self.chaos_params.0;
148                r * *last_value * (1.0 - *last_value)
149            }
150            RandomType::Henon => {
151                let a = self.chaos_params.0;
152                let x = *last_value;
153                let y = *last_value - x;
154                1.0 - a * x * x + y
155            }
156            RandomType::WhiteNoise => self.random_f64(rng_state) * 2.0 - 1.0,
157            _ => *last_value,
158        };
159
160        *last_value = new_value;
161        let value = self.range.clamp(new_value);
162        ParamValue::Float(value as f32)
163    }
164
165    fn initial_internal(&self) -> Self::Internal {
166        let initial_value = match self.rng_type {
167            RandomType::Logistic => 0.5,
168            RandomType::Henon => 0.0,
169            _ => 0.0,
170        };
171        (123456789, initial_value, 0.0)
172    }
173
174    fn name(&self) -> &str {
175        &self.name
176    }
177}
178
179#[cfg(test)]
180mod tests {
181    use super::*;
182
183    #[test]
184    fn test_random_walk() {
185        let walk = RandomAutomaton::walk("Walk", 0.1);
186        let mut internal = walk.initial_internal();
187        let current = ParamValue::Float(0.0);
188
189        let value = walk.step(&mut internal, &current, 0.01, &NoAction);
190        let val = value.as_f32().unwrap();
191        assert!(val >= -1.0 && val <= 1.0);
192    }
193
194    #[test]
195    fn test_logistic() {
196        let logistic = RandomAutomaton::logistic("Logistic", 3.8);
197        let mut internal = logistic.initial_internal();
198        let current = ParamValue::Float(0.0);
199
200        let value = logistic.step(&mut internal, &current, 0.0, &NoAction);
201        let val = value.as_f32().unwrap();
202        assert!(val >= 0.0 && val <= 1.0);
203    }
204}