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}