Skip to main content

rill_patchbay/
utils.rs

1//! Utility functions and helpers for the patchbay.
2//!
3//! Provides value converters, metronome timing, note-type conversion,
4//! test helpers, and test-signal generation.
5
6use crate::automaton::Range;
7use crate::control::Transform;
8
9// =============================================================================
10// Value converters
11// =============================================================================
12
13/// Converts values between different scales using a specified transform.
14#[derive(Debug, Clone)]
15pub struct ValueConverter {
16    input_range: Range,
17    output_range: Range,
18    transform: Transform,
19}
20
21impl ValueConverter {
22    /// Create a new value converter.
23    pub fn new(input_range: Range, output_range: Range, transform: Transform) -> Self {
24        Self {
25            input_range,
26            output_range,
27            transform,
28        }
29    }
30
31    /// Convert a value from the input range to the output range.
32    pub fn convert(&self, value: f64) -> f64 {
33        let norm = self.input_range.normalize(value);
34
35        let transformed = match self.transform {
36            Transform::Linear => norm,
37            Transform::Exponential => norm * norm,
38            Transform::Logarithmic => (1.0 + norm * 9.0).log10(),
39            Transform::Inverted => 1.0 - norm,
40            Transform::Custom(ref f) => f(norm as f32) as f64,
41        };
42
43        self.output_range.denormalize(transformed)
44    }
45
46    /// Convert a value in the reverse direction (approximate).
47    pub fn convert_inverse(&self, value: f64) -> f64 {
48        let norm = self.output_range.normalize(value);
49        self.input_range.denormalize(norm)
50    }
51}
52
53/// Convert a MIDI value (0–127) to a normalised float (0.0–1.0).
54pub fn midi_to_normalized(midi: u8) -> f64 {
55    midi as f64 / 127.0
56}
57
58/// Convert a normalised float (0.0–1.0) to a MIDI value (0–127).
59pub fn normalized_to_midi(norm: f64) -> u8 {
60    (norm.clamp(0.0, 1.0) * 127.0).round() as u8
61}
62
63/// Convert a frequency in Hz to the nearest MIDI note number.
64pub fn freq_to_midi_note(freq: f64) -> f64 {
65    69.0 + 12.0 * (freq / 440.0).log2()
66}
67
68/// Convert a MIDI note number to frequency in Hz.
69pub fn midi_note_to_freq(note: f64) -> f64 {
70    440.0 * 2.0_f64.powf((note - 69.0) / 12.0)
71}
72
73// =============================================================================
74// Timing utilities
75// =============================================================================
76
77/// A metronome for synchronisation with BPM.
78#[derive(Debug, Clone)]
79pub struct Metronome {
80    bpm: f64,
81    last_tick: f64,
82    next_tick: f64,
83    quarter_duration: f64,
84}
85
86impl Metronome {
87    /// Create a new metronome at the given BPM.
88    pub fn new(bpm: f64) -> Self {
89        let quarter_duration = 60.0 / bpm;
90        Self {
91            bpm,
92            last_tick: 0.0,
93            next_tick: quarter_duration,
94            quarter_duration,
95        }
96    }
97
98    /// Advance the metronome and return whether a tick occurred.
99    pub fn update(&mut self, time: f64) -> bool {
100        if time >= self.next_tick {
101            self.last_tick = self.next_tick;
102            self.next_tick += self.quarter_duration;
103            true
104        } else {
105            false
106        }
107    }
108
109    /// Get the current phase (0.0–1.0) within the current quarter note.
110    pub fn phase(&self, time: f64) -> f64 {
111        ((time - self.last_tick) / self.quarter_duration).clamp(0.0, 1.0)
112    }
113
114    /// Set a new BPM value.
115    pub fn set_bpm(&mut self, bpm: f64) {
116        self.bpm = bpm;
117        self.quarter_duration = 60.0 / bpm;
118        self.next_tick = self.last_tick + self.quarter_duration;
119    }
120
121    /// Reset the metronome to the start of a new bar.
122    pub fn reset(&mut self) {
123        self.last_tick = 0.0;
124        self.next_tick = self.quarter_duration;
125    }
126}
127
128/// Convert a note type to duration in seconds at the given BPM.
129pub fn note_duration_to_seconds(note_type: NoteType, bpm: f64) -> f64 {
130    let quarter = 60.0 / bpm;
131    match note_type {
132        NoteType::Whole => quarter * 4.0,
133        NoteType::Half => quarter * 2.0,
134        NoteType::Quarter => quarter,
135        NoteType::Eighth => quarter / 2.0,
136        NoteType::Sixteenth => quarter / 4.0,
137        NoteType::ThirtySecond => quarter / 8.0,
138        NoteType::Dotted(n) => note_duration_to_seconds(*n, bpm) * 1.5,
139        NoteType::Triplet(n) => note_duration_to_seconds(*n, bpm) * 2.0 / 3.0,
140    }
141}
142
143/// A musical note type for duration calculations.
144#[derive(Debug, Clone)]
145pub enum NoteType {
146    /// Whole note (semibreve).
147    Whole,
148    /// Half note (minim).
149    Half,
150    /// Quarter note (crotchet).
151    Quarter,
152    /// Eighth note (quaver).
153    Eighth,
154    /// Sixteenth note (semiquaver).
155    Sixteenth,
156    /// Thirty-second note (demisemiquaver).
157    ThirtySecond,
158    /// Dotted variant of a note type.
159    Dotted(Box<NoteType>),
160    /// Triplet variant of a note type.
161    Triplet(Box<NoteType>),
162}
163
164// =============================================================================
165// Test helpers
166// =============================================================================
167
168/// Records events for testing purposes.
169#[derive(Debug, Default)]
170pub struct EventRecorder {
171    events: Vec<RecordedEvent>,
172}
173
174/// A single recorded event.
175#[derive(Debug, Clone)]
176pub struct RecordedEvent {
177    /// Time of the event.
178    pub time: f64,
179    /// Event type label.
180    pub event_type: String,
181    /// Numeric value.
182    pub value: f64,
183    /// Additional data.
184    pub data: String,
185}
186
187impl EventRecorder {
188    /// Create a new event recorder.
189    pub fn new() -> Self {
190        Self { events: Vec::new() }
191    }
192
193    /// Record an event.
194    pub fn record(&mut self, time: f64, event_type: &str, value: f64, data: &str) {
195        self.events.push(RecordedEvent {
196            time,
197            event_type: event_type.to_string(),
198            value,
199            data: data.to_string(),
200        });
201    }
202
203    /// Return all recorded events.
204    pub fn events(&self) -> &[RecordedEvent] {
205        &self.events
206    }
207
208    /// Clear all recorded events.
209    pub fn clear(&mut self) {
210        self.events.clear();
211    }
212
213    /// Find events by type label.
214    pub fn find_by_type(&self, event_type: &str) -> Vec<&RecordedEvent> {
215        self.events
216            .iter()
217            .filter(|e| e.event_type == event_type)
218            .collect()
219    }
220}
221
222// =============================================================================
223// Test signal generators
224// =============================================================================
225
226/// Generates test signals for verification and debugging.
227pub struct TestSignalGenerator {
228    signal_type: TestSignalType,
229    params: TestSignalParams,
230}
231
232/// Type of test signal.
233#[derive(Debug, Clone)]
234pub enum TestSignalType {
235    /// Sine wave.
236    Sine,
237    /// Square wave.
238    Square,
239    /// Sawtooth wave.
240    Saw,
241    /// White noise.
242    Noise,
243    /// ADSR-like envelope.
244    Envelope,
245}
246
247/// Parameters for a test signal.
248#[derive(Debug, Clone)]
249pub struct TestSignalParams {
250    /// Frequency in Hz.
251    pub frequency: f64,
252    /// Amplitude.
253    pub amplitude: f64,
254    /// DC offset.
255    pub offset: f64,
256    /// Duration in seconds.
257    pub duration: f64,
258}
259
260impl TestSignalGenerator {
261    /// Create a new test signal generator.
262    pub fn new(signal_type: TestSignalType, params: TestSignalParams) -> Self {
263        Self {
264            signal_type,
265            params,
266        }
267    }
268
269    /// Generate the signal value at the given time.
270    pub fn generate(&self, time: f64) -> f64 {
271        if time > self.params.duration {
272            return 0.0;
273        }
274
275        match self.signal_type {
276            TestSignalType::Sine => {
277                let phase = 2.0 * std::f64::consts::PI * self.params.frequency * time;
278                self.params.offset + self.params.amplitude * phase.sin()
279            }
280
281            TestSignalType::Square => {
282                let phase = (self.params.frequency * time) % 1.0;
283                let value = if phase < 0.5 { 1.0 } else { -1.0 };
284                self.params.offset + self.params.amplitude * value
285            }
286
287            TestSignalType::Saw => {
288                let phase = (self.params.frequency * time) % 1.0;
289                let value = 2.0 * phase - 1.0;
290                self.params.offset + self.params.amplitude * value
291            }
292
293            TestSignalType::Noise => {
294                use rand::Rng;
295                let mut rng = rand::thread_rng();
296                self.params.offset + self.params.amplitude * (rng.gen::<f64>() * 2.0 - 1.0)
297            }
298
299            TestSignalType::Envelope => {
300                let attack = 0.1;
301                let decay = 0.2;
302                let sustain = 0.7;
303                let release = 0.3;
304
305                if time < attack {
306                    (time / attack) * self.params.amplitude
307                } else if time < attack + decay {
308                    (1.0 - (1.0 - sustain) * ((time - attack) / decay)) * self.params.amplitude
309                } else if time < self.params.duration - release {
310                    sustain * self.params.amplitude
311                } else {
312                    let rel_time = time - (self.params.duration - release);
313                    (sustain * (1.0 - rel_time / release)) * self.params.amplitude
314                }
315            }
316        }
317    }
318}
319
320// =============================================================================
321// Tests
322// =============================================================================
323
324#[cfg(test)]
325mod tests {
326    use super::*;
327
328    #[test]
329    fn test_value_converter() {
330        let converter = ValueConverter::new(
331            Range::new(0.0, 127.0),
332            Range::new(0.0, 1.0),
333            Transform::Linear,
334        );
335
336        let result = converter.convert(64.0);
337        assert!((result - 0.5).abs() < 0.01);
338    }
339
340    #[test]
341    fn test_metronome() {
342        let mut metro = Metronome::new(120.0);
343
344        assert!(!metro.update(0.2));
345        assert!(metro.update(0.6));
346        assert!((metro.phase(0.6) - 0.2).abs() < 0.01);
347    }
348
349    #[test]
350    fn test_test_signal() {
351        let params = TestSignalParams {
352            frequency: 1.0,
353            amplitude: 1.0,
354            offset: 0.0,
355            duration: 2.0,
356        };
357
358        let gen = TestSignalGenerator::new(TestSignalType::Sine, params);
359        let val = gen.generate(0.25);
360        assert!((val - 1.0).abs() < 0.01);
361    }
362}