bbx_dsp/
voice.rs

1//! Voice state management for MIDI-controlled synthesis.
2//!
3//! This module provides monophonic voice state tracking with support
4//! for legato playing (last-note priority).
5
6use bbx_core::StackVec;
7
8/// Converts a MIDI note number to frequency in Hz.
9///
10/// Uses A4 = 440 Hz as the reference.
11#[inline]
12pub fn midi_note_to_frequency(note: u8) -> f32 {
13    440.0 * 2.0f32.powf((note as f32 - 69.0) / 12.0)
14}
15
16/// Monophonic voice state for MIDI-controlled synthesis.
17///
18/// Tracks the currently active note, velocity, gate state, and frequency.
19/// Supports legato playing with last-note priority: when multiple notes
20/// are held, releasing a note will return to the previous held note
21/// without retriggering the envelope.
22#[derive(Debug, Clone)]
23pub struct VoiceState {
24    /// The currently active MIDI note number, if any.
25    pub active_note: Option<u8>,
26    /// Velocity of the active note (0.0 to 1.0).
27    pub velocity: f32,
28    /// Gate state: true while a note is held.
29    pub gate: bool,
30    /// Frequency in Hz for the current note.
31    pub frequency: f32,
32    /// Stack of held notes for legato playing: (note, velocity).
33    note_stack: StackVec<(u8, u8), 16>,
34}
35
36impl VoiceState {
37    /// Create a new voice state.
38    pub fn new() -> Self {
39        Self {
40            active_note: None,
41            velocity: 0.0,
42            gate: false,
43            frequency: 440.0,
44            note_stack: StackVec::new(),
45        }
46    }
47
48    /// Process a note-on event.
49    ///
50    /// Updates the active note, velocity, gate state, and frequency.
51    /// The note is also pushed onto the note stack for legato handling.
52    /// If the note stack is full (16 notes), the note is silently dropped
53    /// from the stack but still becomes the active note.
54    pub fn note_on(&mut self, note: u8, velocity: u8) {
55        let vel_normalized = velocity as f32 / 127.0;
56
57        let _ = self.note_stack.push((note, velocity));
58
59        self.active_note = Some(note);
60        self.velocity = vel_normalized;
61        self.gate = true;
62        self.frequency = midi_note_to_frequency(note);
63    }
64
65    /// Process a note-off event.
66    ///
67    /// Returns `true` if the voice should enter release stage (no more notes held),
68    /// or `false` if switching to a previous legato note.
69    pub fn note_off(&mut self, note: u8) -> bool {
70        let mut i = 0;
71        while i < self.note_stack.len() {
72            if self.note_stack[i].0 == note {
73                for j in i..self.note_stack.len() - 1 {
74                    self.note_stack[j] = self.note_stack[j + 1];
75                }
76                self.note_stack.pop();
77            } else {
78                i += 1;
79            }
80        }
81
82        if self.active_note == Some(note) {
83            if let Some(&(prev_note, prev_vel)) = self.note_stack.as_slice().last() {
84                self.active_note = Some(prev_note);
85                self.velocity = prev_vel as f32 / 127.0;
86                self.frequency = midi_note_to_frequency(prev_note);
87                false
88            } else {
89                self.active_note = None;
90                self.gate = false;
91                true
92            }
93        } else {
94            false
95        }
96    }
97
98    /// Reset the voice state.
99    ///
100    /// Clears all state and the note stack.
101    pub fn reset(&mut self) {
102        self.active_note = None;
103        self.velocity = 0.0;
104        self.gate = false;
105        self.frequency = 440.0;
106        self.note_stack.clear();
107    }
108
109    /// Returns true if a note is currently active.
110    #[inline]
111    pub fn is_active(&self) -> bool {
112        self.active_note.is_some()
113    }
114
115    /// Returns the number of notes currently held.
116    #[inline]
117    pub fn held_note_count(&self) -> usize {
118        self.note_stack.len()
119    }
120}
121
122impl Default for VoiceState {
123    fn default() -> Self {
124        Self::new()
125    }
126}