truce_rack_core/events.rs
1//! Sample-accurate event list shared between MIDI / parameter
2//! automation / transport flags.
3//!
4//! An [`EventList`] arrives sorted by `sample_offset` and gets
5//! consumed by the plugin during one block of `process`. The
6//! output `EventList` on [`crate::ProcessContext`] is the
7//! plugin's path back to the host for outbound MIDI and
8//! parameter touches.
9
10use smallvec::SmallVec;
11
12/// One event with sample-accurate timing.
13#[derive(Debug, Clone, Copy)]
14pub struct Event {
15 /// Sample offset within the current `process` block.
16 pub sample_offset: u32,
17 /// Event payload.
18 pub body: EventBody,
19}
20
21/// What this event carries.
22#[derive(Debug, Clone, Copy)]
23pub enum EventBody {
24 /// MIDI 1.0 / 2.0 message.
25 Midi(MidiData),
26 /// Host-driven parameter automation point.
27 ParamValue {
28 /// Parameter id from [`crate::ParameterInfo::id`].
29 param_id: u32,
30 /// New value in the parameter's native range.
31 value: f64,
32 },
33 /// Plugin-emitted "user touched this parameter" notification.
34 /// Hosts use the touch / release pair to delimit a gesture
35 /// for undo grouping and automation.
36 ParamGesture {
37 /// Parameter id.
38 param_id: u32,
39 /// `true` = begin gesture, `false` = end.
40 active: bool,
41 },
42 /// Host transport state changed mid-block (e.g. user hit
43 /// play between samples 256 and 257). Plugins that care
44 /// about exact transport flip points read these out of the
45 /// input event list.
46 TransportFlag(TransportFlag),
47}
48
49/// Sub-flags describing transport state transitions.
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub enum TransportFlag {
52 /// Playback started.
53 PlayStart,
54 /// Playback stopped.
55 PlayStop,
56 /// Recording armed → engaged.
57 RecordStart,
58 /// Recording stopped.
59 RecordStop,
60 /// Loop boundary crossed (host jumped from end to start).
61 Looped,
62}
63
64/// MIDI message body.
65///
66/// MIDI 1.0 channel-voice messages are first-class; system
67/// real-time and `SysEx` ride in [`MidiData::Raw`] as raw bytes
68/// for the rare hosts that care.
69#[derive(Debug, Clone, Copy)]
70pub enum MidiData {
71 /// Note On — velocity 0 is treated as Note Off per the
72 /// MIDI 1.0 spec, but we represent that explicitly via
73 /// [`MidiData::NoteOff`] when known.
74 NoteOn {
75 /// MIDI channel, 0-15.
76 channel: u8,
77 /// Note number, 0-127.
78 note: u8,
79 /// Velocity, 0-127.
80 velocity: u8,
81 },
82 /// Note Off.
83 NoteOff {
84 /// MIDI channel, 0-15.
85 channel: u8,
86 /// Note number, 0-127.
87 note: u8,
88 /// Release velocity, 0-127.
89 velocity: u8,
90 },
91 /// Polyphonic key pressure.
92 PolyAftertouch {
93 /// MIDI channel, 0-15.
94 channel: u8,
95 /// Note number, 0-127.
96 note: u8,
97 /// Pressure, 0-127.
98 pressure: u8,
99 },
100 /// Control change.
101 ControlChange {
102 /// MIDI channel, 0-15.
103 channel: u8,
104 /// Controller number, 0-127.
105 controller: u8,
106 /// Value, 0-127.
107 value: u8,
108 },
109 /// Program change.
110 ProgramChange {
111 /// MIDI channel, 0-15.
112 channel: u8,
113 /// Program number, 0-127.
114 program: u8,
115 },
116 /// Channel pressure.
117 ChannelAftertouch {
118 /// MIDI channel, 0-15.
119 channel: u8,
120 /// Pressure, 0-127.
121 pressure: u8,
122 },
123 /// Pitch bend, 14-bit (0-16383, 8192 = center).
124 PitchBend {
125 /// MIDI channel, 0-15.
126 channel: u8,
127 /// Bend value, 0-16383.
128 value: u16,
129 },
130 /// Raw MIDI bytes — system real-time, `SysEx` fragments,
131 /// anything the channel-voice variants don't cover.
132 /// `len` bytes of `data` are meaningful; trailing bytes
133 /// are undefined. Cap of 8 covers MIDI 2.0 UMP 64-bit and
134 /// most system messages without spilling to the heap.
135 Raw {
136 /// Number of meaningful bytes in `data`.
137 len: u8,
138 /// Message bytes, big-endian.
139 data: [u8; 8],
140 },
141}
142
143/// Reasonable inline capacity for the per-block event list.
144/// Few hosts produce more than ~16 events per audio block; sizing
145/// the inline buffer this way keeps the audio thread out of the
146/// allocator for the vast majority of blocks.
147const EVENT_LIST_INLINE: usize = 32;
148
149/// Sample-ordered event buffer used for one `process` block.
150///
151/// Backed by `SmallVec<[Event; 32]>`: 32 inline entries cover
152/// almost every block without heap allocation; bursts spill to
153/// the heap rather than getting dropped. Cleared between blocks
154/// by [`EventList::clear`] (keeps the heap allocation when one
155/// was forced).
156#[derive(Debug, Default, Clone)]
157pub struct EventList {
158 events: SmallVec<[Event; EVENT_LIST_INLINE]>,
159}
160
161impl EventList {
162 /// An empty list with inline capacity for 32 events before
163 /// spilling to the heap.
164 #[must_use]
165 pub fn new() -> Self {
166 Self::default()
167 }
168
169 /// Build from an existing slice.
170 #[must_use]
171 pub fn from_slice(events: &[Event]) -> Self {
172 Self {
173 events: SmallVec::from_slice(events),
174 }
175 }
176
177 /// Append an event. Caller is responsible for keeping the
178 /// list sample-offset-sorted.
179 pub fn push(&mut self, event: Event) {
180 self.events.push(event);
181 }
182
183 /// Reset to empty without dropping any heap allocation.
184 pub fn clear(&mut self) {
185 self.events.clear();
186 }
187
188 /// Number of events.
189 #[must_use]
190 pub fn len(&self) -> usize {
191 self.events.len()
192 }
193
194 /// `true` when the list contains no events.
195 #[must_use]
196 pub fn is_empty(&self) -> bool {
197 self.events.is_empty()
198 }
199
200 /// Borrow the events as a slice.
201 #[must_use]
202 pub fn as_slice(&self) -> &[Event] {
203 &self.events
204 }
205
206 /// Iterate over the events.
207 pub fn iter(&self) -> std::slice::Iter<'_, Event> {
208 self.events.iter()
209 }
210}
211
212impl<'a> IntoIterator for &'a EventList {
213 type Item = &'a Event;
214 type IntoIter = std::slice::Iter<'a, Event>;
215 fn into_iter(self) -> Self::IntoIter {
216 self.events.iter()
217 }
218}