1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
//! Pre-allocated SysEx output buffer pool for real-time safety.
//!
//! This module provides `SysExOutputPool`, which pre-allocates buffer slots
//! to avoid heap allocation during audio processing.
/// Pre-allocated pool for SysEx output messages.
///
/// Avoids heap allocation during audio processing by pre-allocating
/// a fixed number of buffer slots at initialization time.
pub struct SysExOutputPool {
/// Pre-allocated buffer slots for SysEx data
buffers: Vec<Vec<u8>>,
/// Length of valid data in each slot
lengths: Vec<usize>,
/// Maximum number of slots
max_slots: usize,
/// Maximum buffer size per slot
max_buffer_size: usize,
/// Next available slot index
next_slot: usize,
/// Set to true when an allocation fails due to pool exhaustion
overflowed: bool,
/// Heap-backed fallback buffer for overflow (only when feature enabled).
#[cfg(feature = "sysex-heap-fallback")]
fallback: Vec<Vec<u8>>,
}
impl SysExOutputPool {
/// Default number of SysEx slots per process block.
pub const DEFAULT_SLOTS: usize = 16;
/// Default maximum size per SysEx message.
pub const DEFAULT_BUFFER_SIZE: usize = 512;
/// Create a new pool with default capacity.
pub fn new() -> Self {
Self::with_capacity(Self::DEFAULT_SLOTS, Self::DEFAULT_BUFFER_SIZE)
}
/// Create a new pool with the specified capacity.
///
/// Pre-allocates all buffers to avoid heap allocation during process().
pub fn with_capacity(slots: usize, buffer_size: usize) -> Self {
let mut buffers = Vec::with_capacity(slots);
for _ in 0..slots {
buffers.push(vec![0u8; buffer_size]);
}
let lengths = vec![0usize; slots];
Self {
buffers,
lengths,
max_slots: slots,
max_buffer_size: buffer_size,
next_slot: 0,
overflowed: false,
#[cfg(feature = "sysex-heap-fallback")]
fallback: Vec::new(),
}
}
/// Clear the pool for reuse. O(1) operation.
///
/// Note: This does NOT clear the fallback buffer, which is drained separately
/// at the start of the next process block.
#[inline]
pub fn clear(&mut self) {
self.next_slot = 0;
self.overflowed = false;
}
/// Allocate a slot and copy SysEx data into it.
///
/// Returns `Some((pointer, length))` on success, `None` if pool exhausted.
/// The pointer is stable until `clear()` is called.
///
/// Sets the overflow flag when the pool is exhausted.
/// With `sysex-heap-fallback` feature: overflow messages are stored in a
/// heap-backed fallback buffer instead of being dropped.
pub fn allocate(&mut self, data: &[u8]) -> Option<(*const u8, usize)> {
if self.next_slot >= self.max_slots {
self.overflowed = true;
#[cfg(feature = "sysex-heap-fallback")]
{
let copy_len = data.len().min(self.max_buffer_size);
self.fallback.push(data[..copy_len].to_vec());
}
return None;
}
let slot = self.next_slot;
self.next_slot += 1;
let copy_len = data.len().min(self.max_buffer_size);
self.buffers[slot][..copy_len].copy_from_slice(&data[..copy_len]);
self.lengths[slot] = copy_len;
Some((self.buffers[slot].as_ptr(), copy_len))
}
/// Allocate and return a slice reference instead of raw pointer.
///
/// Safer API for contexts that don't need raw pointers.
pub fn allocate_slice(&mut self, data: &[u8]) -> Option<&[u8]> {
if self.next_slot >= self.max_slots {
self.overflowed = true;
#[cfg(feature = "sysex-heap-fallback")]
{
let copy_len = data.len().min(self.max_buffer_size);
self.fallback.push(data[..copy_len].to_vec());
}
return None;
}
let slot = self.next_slot;
self.next_slot += 1;
let copy_len = data.len().min(self.max_buffer_size);
self.buffers[slot][..copy_len].copy_from_slice(&data[..copy_len]);
self.lengths[slot] = copy_len;
Some(&self.buffers[slot][..copy_len])
}
/// Check if the pool overflowed during this block.
#[inline]
pub fn has_overflowed(&self) -> bool {
self.overflowed
}
/// Get the pool's slot capacity.
#[inline]
pub fn capacity(&self) -> usize {
self.max_slots
}
/// Get number of slots currently used.
#[inline]
pub fn used(&self) -> usize {
self.next_slot
}
/// Check if fallback buffer has pending messages (feature-gated).
#[cfg(feature = "sysex-heap-fallback")]
#[inline]
pub fn has_fallback(&self) -> bool {
!self.fallback.is_empty()
}
/// Take ownership of fallback messages (feature-gated).
///
/// These messages should be emitted at the start of the current process block.
#[cfg(feature = "sysex-heap-fallback")]
#[inline]
pub fn take_fallback(&mut self) -> Vec<Vec<u8>> {
std::mem::take(&mut self.fallback)
}
}
impl Default for SysExOutputPool {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
#[allow(clippy::undocumented_unsafe_blocks)]
mod tests {
use super::*;
#[test]
fn test_new_pool() {
let pool = SysExOutputPool::new();
assert_eq!(pool.capacity(), SysExOutputPool::DEFAULT_SLOTS);
assert_eq!(pool.used(), 0);
assert!(!pool.has_overflowed());
}
#[test]
fn test_allocate() {
let mut pool = SysExOutputPool::with_capacity(2, 64);
let data = [0xF0, 0x41, 0x10, 0xF7];
let result = pool.allocate(&data);
assert!(result.is_some());
assert_eq!(pool.used(), 1);
let (ptr, len) = result.unwrap();
assert_eq!(len, 4);
let slice = unsafe { std::slice::from_raw_parts(ptr, len) };
assert_eq!(slice, &data);
}
#[test]
fn test_allocate_slice() {
let mut pool = SysExOutputPool::with_capacity(2, 64);
let data = [0xF0, 0x41, 0x10, 0xF7];
let result = pool.allocate_slice(&data);
assert!(result.is_some());
assert_eq!(result.unwrap(), &data);
assert_eq!(pool.used(), 1);
}
#[test]
fn test_overflow() {
let mut pool = SysExOutputPool::with_capacity(1, 64);
let data = [0xF0, 0xF7];
assert!(pool.allocate(&data).is_some());
assert!(!pool.has_overflowed());
assert!(pool.allocate(&data).is_none());
assert!(pool.has_overflowed());
}
#[test]
fn test_clear() {
let mut pool = SysExOutputPool::with_capacity(1, 64);
let data = [0xF0, 0xF7];
pool.allocate(&data);
pool.allocate(&data); // Overflow
assert!(pool.has_overflowed());
assert_eq!(pool.used(), 1);
pool.clear();
assert!(!pool.has_overflowed());
assert_eq!(pool.used(), 0);
}
#[test]
fn test_truncation() {
let mut pool = SysExOutputPool::with_capacity(1, 4);
let data = [0xF0, 0x41, 0x10, 0x42, 0x00, 0xF7]; // 6 bytes
let result = pool.allocate_slice(&data);
assert!(result.is_some());
assert_eq!(result.unwrap().len(), 4); // Truncated to buffer size
}
}