conformal_poly/
lib.rs

1#![doc = include_str!("../docs_boilerplate.md")]
2#![doc = include_str!("../README.md")]
3
4use crate::splice::{TimedStateChange, splice_numeric_buffer_states};
5
6use self::state::{State, UpdateScratch};
7use conformal_component::{
8    ProcessingEnvironment,
9    audio::{BufferMut, channels_mut},
10    events::{self as component_events, NoteID},
11    parameters::{
12        NumericBufferState, PiecewiseLinearCurvePoint, left_numeric_buffer, right_numeric_buffer,
13    },
14    synth::{self, valid_range_for_per_note_expression},
15};
16
17pub use conformal_component::events::NoteData;
18
19mod splice;
20mod state;
21
22/// The data associated with an event, independent of the time it occurred.
23#[derive(Clone, Debug, PartialEq)]
24pub enum EventData {
25    /// A note began.
26    NoteOn {
27        /// Data associated with the note.
28        data: NoteData,
29    },
30    /// A note ended.
31    NoteOff {
32        /// Data associated with the note.
33        data: NoteData,
34    },
35}
36
37/// An event that occurred at a specific time within a buffer.
38#[derive(Clone, Debug, PartialEq)]
39pub struct Event {
40    /// Number of sample frames after the beginning of the buffer that this event occurred.
41    pub sample_offset: usize,
42    /// Data about the event.
43    pub data: EventData,
44}
45
46impl TryFrom<component_events::Data> for EventData {
47    type Error = ();
48    fn try_from(value: component_events::Data) -> Result<Self, Self::Error> {
49        #[allow(unreachable_patterns)]
50        match value {
51            component_events::Data::NoteOn { data } => Ok(EventData::NoteOn { data }),
52            component_events::Data::NoteOff { data } => Ok(EventData::NoteOff { data }),
53            _ => Err(()),
54        }
55    }
56}
57
58impl TryFrom<component_events::Event> for Event {
59    type Error = ();
60    fn try_from(value: component_events::Event) -> Result<Self, Self::Error> {
61        Ok(Event {
62            sample_offset: value.sample_offset,
63            data: value.data.try_into()?,
64        })
65    }
66}
67
68fn add_in_place(x: &[f32], y: &mut [f32]) {
69    for (x, y) in x.iter().zip(y.iter_mut()) {
70        *y += *x;
71    }
72}
73
74fn mul_constant_in_place(x: f32, y: &mut [f32]) {
75    for y in y.iter_mut() {
76        *y *= x;
77    }
78}
79
80// Optimization opportunity - allow `Voice` to indicate that not all output
81// was filled. This will let us skip rendering until a voice is playing
82// and also skip mixing silence.
83
84/// Non-audio data availble to voices during the processing call.
85///
86/// This includes events that occur during the buffer, as well as relevant parameter values.
87pub trait VoiceProcessContext {
88    /// Returns an iterator of events that occurred for this voice during the processing call.
89    fn events(&self) -> impl Iterator<Item = Event> + Clone;
90
91    /// Returns the parameter states for this processing call.
92    fn parameters(&self) -> &impl synth::SynthParamBufferStates;
93
94    /// Returns the state of per-note expression routed to this voice.
95    ///
96    /// Note that most of the time, this will include data for just one note.
97    /// However in some cases, a voice will have to play multiple notes within one buffer,
98    /// which is handled by this call.
99    fn per_note_expression(
100        &self,
101        expression: synth::NumericPerNoteExpression,
102    ) -> NumericBufferState<impl Iterator<Item = PiecewiseLinearCurvePoint> + Clone>;
103}
104
105/// A single voice in a polyphonic synth.
106pub trait Voice {
107    /// Data that is shared across all voices. This could include things like
108    /// low frequency oscillators that are used by multiple voices.
109    type SharedData<'a>: Clone;
110
111    /// Creates a new voice.
112    fn new(voice_index: usize, max_samples_per_process_call: usize, sampling_rate: f32) -> Self;
113
114    /// Handles a single event outside of audio processing.
115    ///
116    /// Note that events sent during a [`process`](`Voice::process`) call must be handled there.
117    fn handle_event(&mut self, event: &EventData);
118
119    /// Renders audio for this voice.
120    ///
121    /// Audio for the voice will be written into the `output` buffer, which will
122    /// start out filled with silence.
123    fn process(
124        &mut self,
125        context: &impl VoiceProcessContext,
126        shared_data: &Self::SharedData<'_>,
127        output: &mut [f32],
128    );
129
130    /// Returns whether this voice is currently outputng audio.
131    ///
132    /// When this returns `true`, [`process`](`Voice::process`) will not be called for this
133    /// voice again until a new note is started. This can improve performance by
134    /// allowing voices to skip processing.
135    #[must_use]
136    fn quiescent(&self) -> bool;
137
138    /// Called in lieu of [`process`](`Voice::process`) when the voice is quiescent.
139    ///
140    /// Voices can use this call to update internal state such as oscillator
141    /// phase, to simulate the effect we'd get if we had processed `num_samples`
142    /// of audio.
143    fn skip_samples(&mut self, _num_samples: usize) {}
144
145    /// Resets the voice to its initial state.
146    fn reset(&mut self);
147}
148
149/// This stores some expensive-to-compute info about when
150/// notes change on a voice, needed to implement
151/// [`VoiceProcessContext::per_note_expression`].
152#[derive(Clone)]
153struct NoteChangesInfo {
154    /// The effective initial note id. Note that if the buffer started without this voice
155    /// playing a note, this may still have a value
156    effective_initial_note_id: Option<NoteID>,
157
158    /// True if we have any note changes inside the buffer, indicating we need to splice
159    /// expression data between two sources.
160    has_any_change: bool,
161
162    /// True if the initial state of the voice was "none" - in this case,
163    /// we start splicing from the _second_ note change.
164    initial_state_was_off: bool,
165}
166
167struct ProcessContextImpl<'a, E, P> {
168    initial_note_id: Option<NoteID>,
169    events_fn: E,
170    parameters: &'a P,
171    buffer_size: usize,
172    note_changes_info: &'a NoteChangesInfo,
173}
174
175#[derive(Clone, Debug, PartialEq)]
176struct TimedNoteChange {
177    note_id: NoteID,
178    sample_offset: usize,
179}
180
181// Returns an iterator of _changes_ in note id from the given initial note id in an event stream.
182//
183// Note offs do not change the effective note id, so they are ignored.
184//
185// Note ons only represent note _changes_ if they represent a change from the previous note id.
186fn note_changes_iter(
187    initial_note_id: Option<NoteID>,
188    events: impl Iterator<Item = Event> + Clone,
189) -> impl Iterator<Item = TimedNoteChange> + Clone {
190    let mut last_note_id = initial_note_id;
191    events.filter_map(move |e| match e.data {
192        EventData::NoteOn { data } => {
193            if Some(data.id) == last_note_id {
194                None
195            } else {
196                last_note_id = Some(data.id);
197                Some(TimedNoteChange {
198                    note_id: data.id,
199                    sample_offset: e.sample_offset,
200                })
201            }
202        }
203        EventData::NoteOff { .. } => None,
204    })
205}
206
207fn keep_last_per_sample(
208    iter: impl Iterator<Item = Event> + Clone,
209) -> impl Iterator<Item = Event> + Clone {
210    let mut iter = iter.peekable();
211    std::iter::from_fn(move || {
212        loop {
213            let current = iter.next()?;
214            if iter
215                .peek()
216                .is_some_and(|next| next.sample_offset == current.sample_offset)
217            {
218                continue;
219            }
220            return Some(current);
221        }
222    })
223}
224
225impl<I: Iterator<Item = Event> + Clone, E: Fn() -> I, P: synth::SynthParamBufferStates>
226    ProcessContextImpl<'_, E, P>
227{
228    fn get_note_changes(&self) -> impl Iterator<Item = TimedNoteChange> + Clone {
229        // Note that we keep only the last note change per sample. This is because the splice
230        // implementation requires no more than one change per sample, and there's no such
231        // rule for our voice event stream - we could have multiple note ons per sample in
232        note_changes_iter(
233            self.initial_note_id,
234            keep_last_per_sample(
235                self.events()
236                    .filter(|e| matches!(e.data, EventData::NoteOn { .. })),
237            ),
238        )
239    }
240}
241
242impl<I: Iterator<Item = Event> + Clone, E: Fn() -> I, P: synth::SynthParamBufferStates>
243    VoiceProcessContext for ProcessContextImpl<'_, E, P>
244{
245    fn events(&self) -> impl Iterator<Item = Event> + Clone {
246        (self.events_fn)()
247    }
248
249    fn parameters(&self) -> &impl synth::SynthParamBufferStates {
250        self.parameters
251    }
252
253    fn per_note_expression(
254        &self,
255        expression: synth::NumericPerNoteExpression,
256    ) -> NumericBufferState<impl Iterator<Item = PiecewiseLinearCurvePoint> + Clone> {
257        // There are two cases to consider:
258        //  1) The note does not change during the buffer (common case). We can make one
259        //     query to the parameter state to get the buffer state for this expression.
260        //     in this case we use the "left" branch.
261        //  2) We have a note change during the buffer and we have to splice buffer states.
262        //     this is a much more awkward case, and we use the "splice" helper and the "right"
263        //     branch.
264
265        let note_change_to_state_change =
266            move |TimedNoteChange {
267                      note_id,
268                      sample_offset,
269                  }| TimedStateChange {
270                sample_offset,
271                state: self
272                    .parameters
273                    .get_numeric_expression_for_note(expression, note_id),
274            };
275        match (
276            self.note_changes_info.effective_initial_note_id,
277            self.note_changes_info.has_any_change,
278        ) {
279            // Easy case - we have a note playing, and we received no events. In this case,
280            // we just grab the state of the note we started with.
281            (Some(initial_note_id), false) => left_numeric_buffer(
282                self.parameters
283                    .get_numeric_expression_for_note(expression, initial_note_id),
284            ),
285            // Easy case - we have no note playing, and we received no events. In this case,
286            // we just return a constant zero. Note that this is in range for all expression types.
287            (None, false) => NumericBufferState::Constant(Default::default()),
288            // In this case, we definitely have to splice
289            (Some(initial_note_id), true) => {
290                // If the initial state was off, we got the effective initial note from the first note change,
291                // so we need to skip it when sending to the splice helper.
292                let prefix_len = usize::from(self.note_changes_info.initial_state_was_off);
293                let note_changes = self.get_note_changes().skip(prefix_len);
294                right_numeric_buffer(splice_numeric_buffer_states(
295                    self.parameters
296                        .get_numeric_expression_for_note(expression, initial_note_id),
297                    note_changes.map(note_change_to_state_change),
298                    self.buffer_size,
299                    valid_range_for_per_note_expression(expression),
300                ))
301            }
302            // This case is structurally impossible, since we consider the first change to be the "effective initial note"
303            (None, true) => unreachable!(),
304        }
305    }
306}
307
308/// A helper struct for implementing polyphonic synths.
309///
310/// This struct handles common tasks such as routing events to voices, updating note expression curves,
311/// and mixing the output of voices.
312///
313/// To use it, you must implement the [`Voice`] trait for your synth. Then, use the methods
314/// on this struct to implement the required [`conformal_component::synth::Synth`] trait methods.
315pub struct Poly<V, const MAX_VOICES: usize = 32> {
316    voices: Vec<V>,
317    state: State<MAX_VOICES>,
318    update_scratch: UpdateScratch<MAX_VOICES>,
319    voice_scratch_buffer: Vec<f32>,
320}
321
322impl<V: std::fmt::Debug, const MAX_VOICES: usize> std::fmt::Debug for Poly<V, MAX_VOICES> {
323    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
324        f.debug_struct("Poly")
325            .field("voices", &self.voices)
326            .field("state", &self.state)
327            .finish_non_exhaustive()
328    }
329}
330
331impl<V: Voice, const MAX_VOICES: usize> Poly<V, MAX_VOICES> {
332    /// Creates a new [`Poly`] struct.
333    #[must_use]
334    pub fn new(environment: &ProcessingEnvironment) -> Self {
335        let voices = (0..MAX_VOICES)
336            .map(|voice_index| {
337                V::new(
338                    voice_index,
339                    environment.max_samples_per_process_call,
340                    environment.sampling_rate,
341                )
342            })
343            .collect();
344        let state = State::new();
345
346        Self {
347            voices,
348            state,
349            update_scratch: Default::default(),
350            voice_scratch_buffer: vec![0f32; environment.max_samples_per_process_call],
351        }
352    }
353
354    /// Handles a set of events without rendering audio.
355    ///
356    /// This can be used to implement [`conformal_component::synth::Synth::handle_events`].
357    pub fn handle_events(&mut self, context: &impl synth::HandleEventsContext) {
358        let poly_events = context.events().filter_map(|data| {
359            EventData::try_from(data).ok().map(|data| Event {
360                sample_offset: 0,
361                data,
362            })
363        });
364
365        for (v, ev) in self.state.clone().dispatch_events(poly_events.clone()) {
366            self.voices[v].handle_event(&ev.data);
367        }
368
369        self.state.update(poly_events, &mut self.update_scratch);
370    }
371
372    /// Renders the audio for the synth.
373    ///
374    /// This can be used to implement [`conformal_component::synth::Synth::process`].
375    /// For any voices with active notes, [`Voice::process`] will be called.
376    pub fn process(
377        &mut self,
378        context: &impl synth::ProcessContext,
379        shared_data: &V::SharedData<'_>,
380        output: &mut impl BufferMut,
381    ) {
382        let params = context.parameters();
383        let poly_events = context
384            .events()
385            .into_iter()
386            .filter_map(|e| Event::try_from(e).ok());
387        self.process_inner(poly_events, params, shared_data, output);
388    }
389
390    fn process_inner(
391        &mut self,
392        events: impl Iterator<Item = Event> + Clone,
393        params: &impl synth::SynthParamBufferStates,
394        shared_data: &V::SharedData<'_>,
395        output: &mut impl BufferMut,
396    ) {
397        let buffer_size: usize = output.num_frames();
398        #[allow(clippy::cast_precision_loss)]
399        let voice_scale = 1f32 / self.voices.len() as f32;
400        let mut voices_with_events = [false; MAX_VOICES];
401        let mut note_changes_infos: [NoteChangesInfo; MAX_VOICES] = std::array::from_fn(|i| {
402            let initial_note_id = self.state.note_id_for_voice(i);
403            NoteChangesInfo {
404                effective_initial_note_id: initial_note_id,
405                has_any_change: false,
406                initial_state_was_off: initial_note_id.is_none(),
407            }
408        });
409        let mut note_changes_infos_updated_sample_offset = [0; MAX_VOICES];
410
411        // This is a bit subtle, basically this calculates the data needed to implement [`VoiceProcessContext::per_note_expression`].
412        // We calculate it here to avoid having to clone state there to re-run the dispatch.
413        for (voice_index, event) in self.state.clone().dispatch_events(events.clone()) {
414            voices_with_events[voice_index] = true;
415            match event.data {
416                EventData::NoteOn { data } => {
417                    if note_changes_infos[voice_index]
418                        .effective_initial_note_id
419                        .is_none()
420                        || note_changes_infos_updated_sample_offset[voice_index]
421                            == event.sample_offset
422                    {
423                        note_changes_infos[voice_index].effective_initial_note_id = Some(data.id);
424                        note_changes_infos_updated_sample_offset[voice_index] = event.sample_offset;
425                    } else {
426                        note_changes_infos[voice_index].has_any_change = true;
427                    }
428                }
429                EventData::NoteOff { .. } => {}
430            }
431        }
432        let mut cleared = false;
433        for (index, voice) in self.voices.iter_mut().enumerate() {
434            if !voices_with_events[index] && voice.quiescent() {
435                voice.skip_samples(buffer_size);
436                // Clear the "prev note" id for this voice since it's no longer active.
437                self.state.clear_prev_note_id_for_voice(index);
438                continue;
439            }
440            let events_fn = || {
441                self.state
442                    .clone()
443                    .dispatch_events(events.clone())
444                    .filter_map(|(i, event)| if i == index { Some(event) } else { None })
445            };
446            voice.process(
447                &ProcessContextImpl {
448                    initial_note_id: self.state.note_id_for_voice(index),
449                    events_fn,
450                    parameters: params,
451                    buffer_size: output.num_frames(),
452                    note_changes_info: &note_changes_infos[index],
453                },
454                shared_data,
455                &mut self.voice_scratch_buffer[0..output.num_frames()],
456            );
457            mul_constant_in_place(voice_scale, &mut self.voice_scratch_buffer);
458            if cleared {
459                for channel_mut in channels_mut(output) {
460                    add_in_place(&self.voice_scratch_buffer[0..buffer_size], channel_mut);
461                }
462            } else {
463                for channel_mut in channels_mut(output) {
464                    channel_mut.copy_from_slice(&self.voice_scratch_buffer[0..buffer_size]);
465                }
466                cleared = true;
467            }
468        }
469        if !cleared {
470            for channel_mut in channels_mut(output) {
471                channel_mut.fill(0f32);
472            }
473        }
474        self.state.update(events, &mut self.update_scratch);
475    }
476
477    /// Resets the state of the polyphonic synth.
478    ///
479    /// This can be used to implement [`conformal_component::Processor::set_processing`].
480    pub fn reset(&mut self) {
481        for voice in &mut self.voices {
482            voice.reset();
483        }
484        self.state.reset();
485    }
486}