conformal_component/
events.rs

1//! Contains data structures representing _events_ sent to [`crate::synth::Synth`]s
2
3#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
4enum NoteIDInternals {
5    NoteIDWithID(i32),
6    NoteIDFromPitch(u8),
7    NoteIDFromChannelID(i16),
8}
9
10/// Represents an identifier for a note
11///
12/// This is an opaque identifier that can be used to refer to a specific note
13/// that is playing.
14#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
15pub struct NoteID {
16    internals: NoteIDInternals,
17}
18
19impl NoteID {
20    /// Create a new `NoteID` from a numeric ID.
21    ///
22    /// Note that the `NoteID`s will be considered equal if they come from
23    /// the same numeric ID, and different if they come from different numeric IDs.
24    ///
25    /// # Examples
26    ///
27    /// ```
28    /// use conformal_component::events::NoteID;
29    /// assert_eq!(NoteID::from_id(42), NoteID::from_id(42));
30    /// assert_ne!(NoteID::from_id(42), NoteID::from_id(43));
31    /// ```
32    #[must_use]
33    pub const fn from_id(id: i32) -> Self {
34        Self {
35            internals: NoteIDInternals::NoteIDWithID(id),
36        }
37    }
38
39    #[doc(hidden)]
40    #[must_use]
41    pub const fn from_pitch(pitch: u8) -> Self {
42        Self {
43            internals: NoteIDInternals::NoteIDFromPitch(pitch),
44        }
45    }
46
47    #[doc(hidden)]
48    #[must_use]
49    pub const fn from_channel_for_mpe_quirks(id: i16) -> Self {
50        Self {
51            internals: NoteIDInternals::NoteIDFromChannelID(id),
52        }
53    }
54}
55
56#[doc(hidden)]
57#[must_use]
58pub fn to_vst_note_id(note_id: NoteID) -> i32 {
59    match note_id.internals {
60        NoteIDInternals::NoteIDWithID(id) => id,
61        NoteIDInternals::NoteIDFromPitch(_) | NoteIDInternals::NoteIDFromChannelID(_) => -1,
62    }
63}
64
65#[doc(hidden)]
66#[must_use]
67pub fn to_vst_note_channel_for_mpe_quirks(note_id: NoteID) -> i16 {
68    match note_id.internals {
69        NoteIDInternals::NoteIDFromChannelID(id) => id,
70        NoteIDInternals::NoteIDFromPitch(_) | NoteIDInternals::NoteIDWithID(_) => 0,
71    }
72}
73
74/// Contains data common to both `NoteOn` and `NoteOff` events.
75#[derive(Copy, Clone, Debug, PartialEq)]
76pub struct NoteData {
77    /// Opaque ID of the note.
78    pub id: NoteID,
79
80    /// Pitch of the note in terms of semitones higher than C-2
81    pub pitch: u8,
82
83    /// 0->1 velocity of the note on or off
84    pub velocity: f32,
85
86    /// Microtuning of the note in cents.
87    pub tuning: f32,
88}
89
90/// A specific type of note expression.
91#[derive(Copy, Clone, Debug, PartialEq)]
92pub enum NoteExpression {
93    /// Pitch bend note expression.
94    ///
95    /// This corresponds to the [`crate::synth::PITCH_BEND_PARAMETER`] controller and should
96    /// change the tuning of the note.
97    ///
98    /// This is expressed in semitones away from the root note of the note (which may itself
99    /// be affected by the global [`crate::synth::PITCH_BEND_PARAMETER`] controller).
100    PitchBend(f32),
101
102    /// Vertical movement note expression, meant to control some sort of timbre of the synth.
103    ///
104    /// This is called "slide" in some DAW UIs.
105    ///
106    /// This corresponds to the "timbre" controller ([`crate::synth::TIMBRE_PARAMETER`]), and
107    /// its effects must be combined with the global controller.
108    ///
109    /// This value varies from 0->1, 0 being the bottommost position,
110    /// and 1 being the topmost position.
111    Timbre(f32),
112
113    /// Depthwise note expression.
114    ///
115    /// This is called "Pressure" in some DAW UIs.
116    ///
117    /// This value varies from 0->1, 0 being neutral, and 1 being the maximum depth.
118    ///
119    /// This corresponds to the [`crate::synth::AFTERTOUCH_PARAMETER`] controller which
120    /// affects all notes. The total effect must be a combination of this per-note note
121    /// expression and the global controller.
122    Aftertouch(f32),
123}
124
125/// Contains data about note expression.
126#[derive(Copy, Clone, Debug, PartialEq)]
127pub struct NoteExpressionData {
128    /// Opaque ID of the note. This will always refer to a note that is
129    /// currently "on".
130    pub id: NoteID,
131
132    /// The expression that is being sent.
133    pub expression: NoteExpression,
134}
135
136/// The data associated with an event, independent of the time it occurred.
137#[derive(Clone, Debug, PartialEq)]
138pub enum Data {
139    /// A note began.
140    ///
141    /// This will never be sent while a note with the same ID is still playing.
142    NoteOn {
143        /// Data associated with the note.
144        data: NoteData,
145    },
146
147    /// A note ended.
148    ///
149    /// This will never be sent while a note with the same ID is not playing.
150    NoteOff {
151        /// Data associated with the note.
152        data: NoteData,
153    },
154
155    /// A note expression was sent.
156    ///
157    /// This will never be sent while a note with the same ID is not playing.
158    NoteExpression {
159        /// Data associated with the note expression.
160        data: NoteExpressionData,
161    },
162}
163
164/// An event that occurred at a specific time within a buffer.
165#[derive(Clone, Debug, PartialEq)]
166pub struct Event {
167    /// Number of sample frames after the beginning of the buffer that this event occurred
168    pub sample_offset: usize,
169
170    /// Data about the event!
171    pub data: Data,
172}
173
174/// Contains an iterator that yields events in order of increasing sample offset.
175///
176/// Invariants:
177///  - All events will have a sample offset in the range of the buffer
178///  - Events are sorted by `sample_offset`
179#[derive(Clone, Debug)]
180pub struct Events<I> {
181    events: I,
182}
183
184fn check_events_invariants<I: Iterator<Item = Event>>(iter: I, buffer_size: usize) -> bool {
185    let mut last = None;
186    for event in iter {
187        if event.sample_offset >= buffer_size {
188            return false;
189        }
190        if let Some(last) = last
191            && event.sample_offset < last
192        {
193            return false;
194        }
195        last = Some(event.sample_offset);
196    }
197    true
198}
199
200impl<I: Iterator<Item = Event> + Clone> Events<I> {
201    /// Create an `Events` object from the given iterator of events.
202    ///
203    /// Note that if any of the invariants are missed, this will return `None`.
204    pub fn new(events: I, buffer_size: usize) -> Option<Self> {
205        if check_events_invariants(events.clone(), buffer_size) {
206            Some(Self { events })
207        } else {
208            None
209        }
210    }
211}
212
213impl<I: Iterator<Item = Event>> IntoIterator for Events<I> {
214    type Item = Event;
215    type IntoIter = I;
216
217    fn into_iter(self) -> Self::IntoIter {
218        self.events
219    }
220}
221
222#[cfg(test)]
223mod tests {
224    use super::{Data, Event, Events, NoteData, NoteID};
225
226    static EXAMPLE_NOTE: NoteData = NoteData {
227        id: NoteID::from_pitch(60),
228        pitch: 60,
229        velocity: 1.0,
230        tuning: 0.0,
231    };
232
233    #[test]
234    fn out_of_order_events_rejected() {
235        assert!(
236            Events::new(
237                (&[
238                    Event {
239                        sample_offset: 5,
240                        data: Data::NoteOn {
241                            data: EXAMPLE_NOTE.clone()
242                        }
243                    },
244                    Event {
245                        sample_offset: 4,
246                        data: Data::NoteOff {
247                            data: EXAMPLE_NOTE.clone()
248                        }
249                    }
250                ])
251                    .iter()
252                    .cloned(),
253                10
254            )
255            .is_none()
256        )
257    }
258
259    #[test]
260    fn out_of_bounds_events_rejected() {
261        assert!(
262            Events::new(
263                (&[Event {
264                    sample_offset: 50,
265                    data: Data::NoteOn {
266                        data: EXAMPLE_NOTE.clone()
267                    }
268                },])
269                    .iter()
270                    .cloned(),
271                10
272            )
273            .is_none()
274        )
275    }
276
277    #[test]
278    fn empty_events_accepted() {
279        assert!(Events::new((&[]).iter().cloned(), 10).is_some())
280    }
281}