Skip to main content

sim_lib_sound_bridge/
pool.rs

1use std::time::Duration;
2
3use sim_lib_sound_core::{Frequency, Tone};
4use sim_lib_sound_timbre::Timbre;
5
6use crate::{SoundBridgeError, TimbreBank};
7
8/// A tone scheduled to start at a given time, with stereo placement and its
9/// originating MIDI channel and key.
10#[derive(Clone, Debug, PartialEq)]
11pub struct ScheduledTone {
12    /// Start time of the tone relative to the bridge clock.
13    pub start: Duration,
14    /// The rendered tone.
15    pub tone: Tone,
16    /// Stereo pan in `-1.0..=1.0` (left to right).
17    pub pan: f32,
18    /// Originating MIDI channel.
19    pub channel: u8,
20    /// Originating MIDI note key.
21    pub key: u8,
22}
23
24/// The lifecycle phase of a voice in the pool.
25#[derive(Copy, Clone, Debug, PartialEq, Eq)]
26pub enum VoicePhase {
27    /// Key is held down.
28    Held,
29    /// Key released but held by the sustain pedal.
30    SustainHeld,
31    /// Voice is in its release stage.
32    Released,
33}
34
35/// An allocated voice sounding a single note.
36#[derive(Clone, Debug, PartialEq)]
37pub struct Voice {
38    /// MIDI channel that triggered the voice.
39    pub channel: u8,
40    /// MIDI note key.
41    pub key: u8,
42    /// Time the voice started.
43    pub started_at: Duration,
44    /// Time the voice entered its release stage, if any.
45    pub release_at: Option<Duration>,
46    /// Time the release stage completes, if known.
47    pub release_end: Option<Duration>,
48    /// Sounding frequency, including any pitch bend or tuning offset.
49    pub frequency: Frequency,
50    /// Amplitude gain applied to the voice.
51    pub amplitude_gain: f64,
52    /// Stereo pan in `-1.0..=1.0`.
53    pub pan: f32,
54    /// Current lifecycle phase.
55    pub phase: VoicePhase,
56    /// Timbre used to render the voice.
57    pub timbre: Timbre,
58}
59
60impl Voice {
61    fn to_scheduled_tone(&self) -> ScheduledTone {
62        let released_at = self.release_at.unwrap_or(self.started_at);
63        let total_duration =
64            released_at.saturating_sub(self.started_at) + self.timbre.default_envelope.release;
65        let tone = self
66            .timbre
67            .render(self.frequency, total_duration)
68            .amplify(self.amplitude_gain);
69        ScheduledTone {
70            start: self.started_at,
71            tone,
72            pan: self.pan,
73            channel: self.channel,
74            key: self.key,
75        }
76    }
77}
78
79/// A polyphony-limited pool of voices that emits scheduled tones as voices
80/// finish or are stolen.
81#[derive(Clone, Debug, PartialEq)]
82pub struct VoicePool {
83    polyphony_limit: usize,
84    voices: Vec<Voice>,
85    emitted: Vec<ScheduledTone>,
86    stolen_voice_count: usize,
87}
88
89impl VoicePool {
90    /// Builds a pool with the given polyphony limit, rejecting a limit of zero.
91    pub fn new(polyphony_limit: usize) -> Result<Self, SoundBridgeError> {
92        if polyphony_limit == 0 {
93            return Err(SoundBridgeError::ZeroPolyphony);
94        }
95        Ok(Self {
96            polyphony_limit,
97            voices: Vec::new(),
98            emitted: Vec::new(),
99            stolen_voice_count: 0,
100        })
101    }
102
103    /// Returns the maximum number of simultaneous voices.
104    pub fn polyphony_limit(&self) -> usize {
105        self.polyphony_limit
106    }
107
108    /// Returns the number of currently active voices.
109    pub fn active_voice_count(&self) -> usize {
110        self.voices.len()
111    }
112
113    /// Returns the currently active voices.
114    pub fn voices(&self) -> &[Voice] {
115        &self.voices
116    }
117
118    /// Returns the scheduled tones emitted but not yet drained.
119    pub fn emitted(&self) -> &[ScheduledTone] {
120        &self.emitted
121    }
122
123    /// Returns the running count of stolen voices.
124    pub fn stolen_voice_count(&self) -> usize {
125        self.stolen_voice_count
126    }
127
128    /// Drains and returns the emitted scheduled tones, clearing the buffer.
129    pub fn drain_emitted(&mut self) -> Vec<ScheduledTone> {
130        std::mem::take(&mut self.emitted)
131    }
132
133    /// Starts a new voice at `now`, retiring any same-key voice and stealing a
134    /// voice if the polyphony limit is reached.
135    pub fn note_on(&mut self, mut voice: Voice, now: Duration) {
136        self.reap_finished(now);
137        self.note_off(voice.channel, voice.key, now, false);
138        if self.voices.len() >= self.polyphony_limit {
139            self.steal_voice(now);
140        }
141        voice.phase = VoicePhase::Held;
142        voice.release_at = None;
143        voice.release_end = None;
144        self.voices.push(voice);
145    }
146
147    /// Releases the most recent voice matching `channel`/`key`, holding it
148    /// under the sustain pedal when `sustain` is set.
149    pub fn note_off(&mut self, channel: u8, key: u8, now: Duration, sustain: bool) {
150        self.reap_finished(now);
151        if let Some(voice) = self
152            .voices
153            .iter_mut()
154            .rev()
155            .find(|voice| voice.channel == channel && voice.key == key)
156        {
157            if sustain {
158                voice.phase = VoicePhase::SustainHeld;
159            } else {
160                voice.phase = VoicePhase::Released;
161                voice.release_at = Some(now);
162                voice.release_end = Some(now + voice.timbre.default_envelope.release);
163            }
164        }
165    }
166
167    /// Releases all sustain-held voices on `channel` at `now`.
168    pub fn release_channel_sustain(&mut self, channel: u8, now: Duration) {
169        self.reap_finished(now);
170        for voice in &mut self.voices {
171            if voice.channel == channel && voice.phase == VoicePhase::SustainHeld {
172                voice.phase = VoicePhase::Released;
173                voice.release_at = Some(now);
174                voice.release_end = Some(now + voice.timbre.default_envelope.release);
175            }
176        }
177    }
178
179    /// Shifts the frequency of all non-released voices on `channel` by `cents`.
180    pub fn transpose_channel_cents(&mut self, channel: u8, cents: f64) {
181        for voice in &mut self.voices {
182            if voice.channel == channel && voice.phase != VoicePhase::Released {
183                voice.frequency = voice.frequency.shift_cents(cents);
184            }
185        }
186    }
187
188    /// Emits and removes voices whose release stage has completed by `now`.
189    pub fn reap_finished(&mut self, now: Duration) {
190        let mut keep = Vec::with_capacity(self.voices.len());
191        for voice in self.voices.drain(..) {
192            if voice.release_end.is_some_and(|release_end| {
193                release_end <= now && voice.phase == VoicePhase::Released
194            }) {
195                self.emitted.push(voice.to_scheduled_tone());
196            } else {
197                keep.push(voice);
198            }
199        }
200        self.voices = keep;
201    }
202
203    /// Releases every remaining voice and emits all of them.
204    pub fn flush_all(&mut self, now: Duration) {
205        for voice in &mut self.voices {
206            if voice.phase != VoicePhase::Released {
207                voice.phase = VoicePhase::Released;
208                voice.release_at = Some(now);
209                voice.release_end = Some(now + voice.timbre.default_envelope.release);
210            }
211        }
212        let max_end = self
213            .voices
214            .iter()
215            .filter_map(|voice| voice.release_end)
216            .max()
217            .unwrap_or(now);
218        self.reap_finished(max_end);
219    }
220
221    fn steal_voice(&mut self, now: Duration) {
222        let release_candidate = self
223            .voices
224            .iter()
225            .enumerate()
226            .filter(|(_, voice)| voice.phase == VoicePhase::Released)
227            .min_by_key(|(_, voice)| voice.release_at.unwrap_or(voice.started_at))
228            .map(|(index, _)| index);
229        let active_candidate = self
230            .voices
231            .iter()
232            .enumerate()
233            .min_by_key(|(_, voice)| voice.started_at)
234            .map(|(index, _)| index);
235        let index = release_candidate.or(active_candidate).unwrap_or(0);
236        let mut stolen = self.voices.swap_remove(index);
237        if stolen.release_at.is_none() {
238            stolen.release_at = Some(now);
239            stolen.release_end = Some(now);
240        }
241        self.stolen_voice_count += 1;
242        self.emitted.push(stolen.to_scheduled_tone());
243    }
244}
245
246/// Per-channel controller state tracked by the bridge.
247#[derive(Clone, Debug, PartialEq)]
248pub struct BridgeChannelState {
249    /// Bank-select MSB.
250    pub bank_msb: u8,
251    /// Bank-select LSB.
252    pub bank_lsb: u8,
253    /// Selected program number.
254    pub program: u8,
255    /// Channel volume in `0.0..=1.0`.
256    pub volume: f64,
257    /// Expression level in `0.0..=1.0`.
258    pub expression: f64,
259    /// Stereo pan in `-1.0..=1.0`.
260    pub pan: f32,
261    /// Whether the sustain pedal is held.
262    pub sustain: bool,
263    /// Current pitch-bend offset, in cents.
264    pub pitch_bend_cents: f64,
265}
266
267impl Default for BridgeChannelState {
268    fn default() -> Self {
269        Self {
270            bank_msb: 0,
271            bank_lsb: 0,
272            program: 0,
273            volume: 1.0,
274            expression: 1.0,
275            pan: 0.0,
276            sustain: false,
277            pitch_bend_cents: 0.0,
278        }
279    }
280}
281
282/// Configuration for a [`MidiToSoundBridge`](crate::MidiToSoundBridge).
283#[derive(Clone, Debug, PartialEq)]
284pub struct BridgeOptions {
285    /// Maximum number of simultaneous voices.
286    pub polyphony_limit: usize,
287    /// Full-scale pitch-bend range, in cents.
288    pub bend_range_cents: f64,
289}
290
291impl Default for BridgeOptions {
292    fn default() -> Self {
293        Self {
294            polyphony_limit: 32,
295            bend_range_cents: 200.0,
296        }
297    }
298}
299
300impl BridgeOptions {
301    /// Builds bridge options, rejecting a zero polyphony limit.
302    pub fn new(polyphony_limit: usize, bend_range_cents: f64) -> Result<Self, SoundBridgeError> {
303        if polyphony_limit == 0 {
304            return Err(SoundBridgeError::ZeroPolyphony);
305        }
306        Ok(Self {
307            polyphony_limit,
308            bend_range_cents,
309        })
310    }
311}
312
313pub(crate) fn tone_gain(channel: &BridgeChannelState, velocity: u8) -> f64 {
314    let velocity_gain = f64::from(velocity) / 127.0;
315    velocity_gain * channel.volume * channel.expression
316}
317
318pub(crate) fn current_timbre<'a>(bank: &'a TimbreBank, channel: &BridgeChannelState) -> &'a Timbre {
319    bank.get(channel.bank_msb, channel.bank_lsb, channel.program)
320}