pub mod ffi;
use std::ffi::CString;
use std::os::raw::c_char;
use std::slice;
use std::sync::Arc;
use std::sync::atomic::{AtomicU32, Ordering};
#[cfg(target_os = "macos")]
use truce_core::Float;
use truce_core::SYSEX_POOL_PREALLOC;
use truce_core::cast::{len_u32, sample_pos_i64};
use truce_core::editor::Editor;
#[cfg(any(target_os = "macos", target_os = "ios"))]
use truce_core::editor::{ClosureBridge, PluginContext, RawWindowHandle, SendPtr};
use truce_core::events::{EVENT_LIST_PREALLOC, Event, EventBody, EventList, TransportInfo};
use truce_core::export::PluginExport;
use truce_core::info::PluginCategory;
use truce_core::midi::{decode_short_message, pitch_bend_to_bytes};
use truce_core::process::ProcessContext;
use truce_core::state;
use truce_core::ump::{SysExAssembler, SysExFeed, decode_ump_channel_voice_2};
use truce_core::wrapper::{
default_io_channels, log_missing_bus_layout, run_audio_block, run_extern_callback_with,
run_register,
};
use truce_params::{ParamFlags, Params};
use ffi::{
AuCallbacks, AuMidi2Event, AuMidiEvent, AuParamDescriptor, AuPluginDescriptor,
AuTransportSnapshot,
};
type StateLoadQueue = crossbeam_queue::ArrayQueue<state::DeserializedState>;
struct AuInstance<P: PluginExport> {
plugin: P,
params_arc: Arc<P::Params>,
latency_cache: AtomicU32,
tail_cache: AtomicU32,
event_list: EventList,
output_events: EventList,
sysex_assembler: SysExAssembler,
plugin_id_hash: u64,
sample_rate: f64,
max_block_size: usize,
prepared: bool,
scratch: truce_core::buffer::RawBufferScratch<<P as truce_core::plugin::Plugin>::Sample>,
editor: Option<Box<dyn Editor>>,
transport_slot: Arc<truce_core::TransportSlot>,
pending_state: Arc<StateLoadQueue>,
}
unsafe extern "C" fn cb_create<P: PluginExport>() -> *mut std::ffi::c_void {
let mut plugin = P::create();
plugin.init();
let info = P::info();
let params_arc = plugin.params_arc();
let latency_cache = AtomicU32::new(plugin.latency());
let tail_cache = AtomicU32::new(plugin.tail());
let instance = Box::new(AuInstance::<P> {
plugin,
params_arc,
latency_cache,
tail_cache,
event_list: EventList::with_capacity(EVENT_LIST_PREALLOC),
output_events: EventList::with_capacity(EVENT_LIST_PREALLOC),
sysex_assembler: SysExAssembler::with_capacity(SYSEX_POOL_PREALLOC),
plugin_id_hash: state::shared_plugin_state_hash(&info),
sample_rate: 44100.0,
max_block_size: 8192,
prepared: false,
scratch: truce_core::buffer::RawBufferScratch::default(),
editor: None,
transport_slot: truce_core::TransportSlot::new(),
pending_state: Arc::new(StateLoadQueue::new(1)),
});
Box::into_raw(instance).cast::<std::ffi::c_void>()
}
unsafe extern "C" fn cb_destroy<P: PluginExport>(ctx: *mut std::ffi::c_void) {
unsafe {
if !ctx.is_null() {
drop(Box::from_raw(ctx.cast::<AuInstance<P>>()));
}
}
}
unsafe extern "C" fn cb_reset<P: PluginExport>(
ctx: *mut std::ffi::c_void,
sample_rate: f64,
max_frames: u32,
) {
unsafe {
let inst = &mut *ctx.cast::<AuInstance<P>>();
let max_frames = (max_frames as usize).max(1024);
inst.sample_rate = sample_rate;
inst.max_block_size = max_frames;
let (num_in, num_out) = default_io_channels::<P>().unwrap_or((2, 2));
inst.scratch
.ensure_capacity(num_in as usize, num_out as usize, max_frames);
inst.plugin.reset(sample_rate, max_frames);
inst.plugin.params().set_sample_rate(sample_rate);
inst.plugin.params().snap_smoothers();
inst.latency_cache
.store(inst.plugin.latency(), Ordering::Relaxed);
inst.tail_cache.store(inst.plugin.tail(), Ordering::Relaxed);
inst.prepared = true;
}
}
#[allow(clippy::too_many_lines)] unsafe extern "C" fn cb_process<P: PluginExport>(
ctx: *mut std::ffi::c_void,
inputs: *const *const f32,
outputs: *mut *mut f32,
num_input_channels: u32,
num_output_channels: u32,
num_frames: u32,
events: *const AuMidiEvent,
num_events: u32,
events2: *const AuMidi2Event,
num_events2: u32,
transport_ptr: *const AuTransportSnapshot,
) {
let nf = num_frames as usize;
let ok = run_audio_block::<P>("AU", || unsafe {
let inst = &mut *ctx.cast::<AuInstance<P>>();
let num_frames = nf;
if !inst.prepared {
for ch in 0..num_output_channels as usize {
let ptr = *outputs.add(ch);
if !ptr.is_null() {
std::ptr::write_bytes(ptr, 0, num_frames);
}
}
return;
}
if let Some(state) = inst.pending_state.pop() {
state::apply_state(&mut inst.plugin, &state);
}
inst.event_list.clear();
if !events.is_null() && num_events > 0 {
let event_slice = slice::from_raw_parts(events, num_events as usize);
for ev in event_slice {
if let Some(body) = decode_short_message(ev.status, ev.data1, ev.data2) {
inst.event_list.push(Event {
sample_offset: ev.sample_offset,
body,
});
}
}
}
inst.sysex_assembler.reset();
if !events2.is_null() && num_events2 > 0 {
let slice2 = slice::from_raw_parts(events2, num_events2 as usize);
for ev in slice2 {
let mt = ((ev.words[0] >> 28) & 0xF) as u8;
match mt {
0x4 => {
if let Some(body) = decode_ump_channel_voice_2(ev.words) {
inst.event_list.push(Event {
sample_offset: ev.sample_offset,
body,
});
}
}
0x3 => {
let feed = inst
.sysex_assembler
.push_sysex7_packet([ev.words[0], ev.words[1]]);
if let SysExFeed::Complete(p) = feed {
let _ = inst.event_list.push_sysex(ev.sample_offset, p.bytes);
}
}
0x5 => {
let feed = inst.sysex_assembler.push_sysex8_packet(ev.words);
if let SysExFeed::Complete(p) = feed {
let _ = inst.event_list.push_sysex(ev.sample_offset, p.bytes);
}
}
_ => {
}
}
}
}
inst.event_list.sort();
debug_assert!(
num_frames <= inst.max_block_size,
"host violated AU contract: render() got {num_frames} frames \
but kAudioUnitProperty_MaximumFramesPerSlice declared max {}",
inst.max_block_size
);
let mut audio_buffer = inst.scratch.build(
inputs,
outputs,
num_input_channels,
num_output_channels,
len_u32(num_frames),
P::supports_in_place(),
);
let transport = if !transport_ptr.is_null() && (*transport_ptr).valid != 0 {
let t = &*transport_ptr;
TransportInfo {
playing: t.playing != 0,
recording: t.recording != 0,
tempo: t.tempo,
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
time_sig_num: t.time_sig_num.clamp(0, i32::from(u8::MAX)) as u8,
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
time_sig_den: t.time_sig_den.clamp(0, i32::from(u8::MAX)) as u8,
position_samples: sample_pos_i64(t.position_samples),
position_seconds: 0.0,
position_beats: t.position_beats,
bar_start_beats: t.bar_start_beats,
loop_active: t.loop_active != 0,
loop_start_beats: t.loop_start_beats,
loop_end_beats: t.loop_end_beats,
}
} else {
TransportInfo::default()
};
inst.output_events.clear();
inst.transport_slot.write(&transport);
let mut context = ProcessContext::new(
&transport,
inst.sample_rate,
num_frames,
&mut inst.output_events,
);
inst.plugin
.process(&mut audio_buffer, &inst.event_list, &mut context);
let _ = audio_buffer;
inst.scratch
.finish_widening_f32(outputs, num_output_channels, len_u32(num_frames));
inst.latency_cache
.store(inst.plugin.latency(), Ordering::Relaxed);
inst.tail_cache.store(inst.plugin.tail(), Ordering::Relaxed);
});
if !ok {
unsafe {
for ch in 0..num_output_channels as usize {
let ptr = *outputs.add(ch);
if !ptr.is_null() {
std::ptr::write_bytes(ptr, 0, nf);
}
}
}
}
}
unsafe extern "C" fn cb_param_count<P: PluginExport>(ctx: *mut std::ffi::c_void) -> u32 {
unsafe {
let inst = &*ctx.cast::<AuInstance<P>>();
len_u32(inst.params_arc.count())
}
}
unsafe extern "C" fn cb_param_get_value<P: PluginExport>(
ctx: *mut std::ffi::c_void,
id: u32,
) -> f64 {
unsafe {
let inst = &*ctx.cast::<AuInstance<P>>();
inst.params_arc.get_plain(id).unwrap_or(0.0)
}
}
unsafe extern "C" fn cb_param_set_value<P: PluginExport>(
ctx: *mut std::ffi::c_void,
id: u32,
value: f64,
) {
unsafe {
let inst = &*ctx.cast::<AuInstance<P>>();
inst.params_arc.set_plain(id, value);
}
}
unsafe extern "C" fn cb_param_format_value<P: PluginExport>(
ctx: *mut std::ffi::c_void,
id: u32,
value: f64,
out: *mut c_char,
out_len: u32,
) -> u32 {
unsafe {
if out_len == 0 || out.is_null() {
return 0;
}
let inst = &*ctx.cast::<AuInstance<P>>();
match inst.params_arc.format_value(id, value) {
Some(text) => {
let bytes = text.as_bytes();
let len = bytes.len().min((out_len as usize) - 1);
std::ptr::copy_nonoverlapping(bytes.as_ptr().cast::<c_char>(), out, len);
*out.add(len) = 0;
len_u32(len)
}
None => 0,
}
}
}
unsafe extern "C" fn cb_state_save<P: PluginExport>(
ctx: *mut std::ffi::c_void,
out_data: *mut *mut u8,
out_len: *mut u32,
) {
unsafe {
*out_data = std::ptr::null_mut();
*out_len = 0;
}
run_extern_callback_with::<P, ()>("au", "save_state", (), || unsafe {
let inst = &*ctx.cast::<AuInstance<P>>();
let (ids, values) = inst.params_arc.collect_values();
let extra = inst.plugin.save_state();
let blob = state::serialize_state(inst.plugin_id_hash, &ids, &values, &extra);
let len = blob.len();
let ptr = malloc(len).cast::<u8>();
if ptr.is_null() {
return;
}
std::ptr::copy_nonoverlapping(blob.as_ptr(), ptr, len);
*out_data = ptr;
*out_len = len_u32(len);
});
}
unsafe extern "C" fn cb_state_load<P: PluginExport>(
ctx: *mut std::ffi::c_void,
data: *const u8,
len: u32,
) {
run_extern_callback_with::<P, ()>("au", "load_state", (), || unsafe {
let inst = &mut *ctx.cast::<AuInstance<P>>();
if data.is_null() || len == 0 {
return;
}
let blob = slice::from_raw_parts(data, len as usize);
if let Some(deserialized) = state::deserialize_state(blob, inst.plugin_id_hash) {
state::apply_params(&*inst.params_arc, &deserialized);
let _ = inst.pending_state.force_push(deserialized);
if let Some(ref mut editor) = inst.editor {
editor.state_changed();
}
}
});
}
unsafe extern "C" fn cb_state_free(data: *mut u8, _len: u32) {
unsafe {
if !data.is_null() {
free(data.cast::<std::ffi::c_void>());
}
}
}
fn try_encode_au_midi(event: &Event) -> Option<AuMidiEvent> {
let (status, data1, data2) = match &event.body {
EventBody::NoteOn {
channel,
note,
velocity,
..
} => (0x90 | (channel & 0x0F), *note, *velocity),
EventBody::NoteOff {
channel,
note,
velocity,
..
} => (0x80 | (channel & 0x0F), *note, *velocity),
EventBody::ControlChange {
channel, cc, value, ..
} => (0xB0 | (channel & 0x0F), *cc, *value),
EventBody::Aftertouch {
channel,
note,
pressure,
..
} => (0xA0 | (channel & 0x0F), *note, *pressure),
EventBody::ChannelPressure {
channel, pressure, ..
} => (0xD0 | (channel & 0x0F), *pressure, 0),
EventBody::PitchBend { channel, value, .. } => {
let (lsb, msb) = pitch_bend_to_bytes(*value);
(0xE0 | (channel & 0x0F), lsb, msb)
}
EventBody::ProgramChange {
channel, program, ..
} => (0xC0 | (channel & 0x0F), *program, 0),
_ => return None,
};
Some(AuMidiEvent {
sample_offset: event.sample_offset,
status,
data1,
data2,
_pad: 0,
})
}
unsafe extern "C" fn cb_output_event_count<P: PluginExport>(ctx: *mut std::ffi::c_void) -> u32 {
unsafe {
let inst = &*ctx.cast::<AuInstance<P>>();
let n = inst
.output_events
.iter()
.filter(|e| try_encode_au_midi(e).is_some())
.count();
len_u32(n)
}
}
unsafe extern "C" fn cb_output_event_at<P: PluginExport>(
ctx: *mut std::ffi::c_void,
index: u32,
out: *mut AuMidiEvent,
) {
unsafe {
let inst = &*ctx.cast::<AuInstance<P>>();
if let Some(packet) = inst
.output_events
.iter()
.filter_map(try_encode_au_midi)
.nth(index as usize)
{
*out = packet;
}
}
}
unsafe extern "C" fn cb_output_sysex_count<P: PluginExport>(ctx: *mut std::ffi::c_void) -> u32 {
unsafe {
let inst = &*ctx.cast::<AuInstance<P>>();
len_u32(
inst.output_events
.iter()
.filter(|e| matches!(e.body, EventBody::SysEx { .. }))
.count(),
)
}
}
unsafe extern "C" fn cb_output_sysex_at<P: PluginExport>(
ctx: *mut std::ffi::c_void,
index: u32,
out_delta_frames: *mut u32,
out_bytes: *mut *const u8,
out_len: *mut u32,
) {
unsafe {
let inst = &*ctx.cast::<AuInstance<P>>();
if let Some(event) = inst
.output_events
.iter()
.filter(|e| matches!(e.body, EventBody::SysEx { .. }))
.nth(index as usize)
{
let bytes = inst.output_events.sysex_bytes(&event.body);
*out_delta_frames = event.sample_offset;
*out_bytes = bytes.as_ptr();
*out_len = len_u32(bytes.len());
}
}
}
unsafe extern "C" fn cb_gui_has_editor<P: PluginExport>(ctx: *mut std::ffi::c_void) -> i32 {
unsafe {
if ctx.is_null() {
return 0;
}
let inst = &mut *ctx.cast::<AuInstance<P>>();
if inst.editor.is_none() {
inst.editor = inst.plugin.editor();
}
i32::from(inst.editor.is_some())
}
}
unsafe extern "C" fn cb_gui_get_size<P: PluginExport>(
ctx: *mut std::ffi::c_void,
w: *mut u32,
h: *mut u32,
) {
unsafe {
if ctx.is_null() {
return;
}
let inst = &mut *ctx.cast::<AuInstance<P>>();
if inst.editor.is_none() {
inst.editor = inst.plugin.editor();
}
if let Some(ref editor) = inst.editor {
let (ew, eh) = editor.size();
*w = ew;
*h = eh;
}
}
}
unsafe extern "C" fn cb_gui_open<P: PluginExport>(
ctx: *mut std::ffi::c_void,
parent: *mut std::ffi::c_void,
) {
#[cfg(not(any(target_os = "macos", target_os = "ios")))]
{
let _ = ctx;
let _ = parent;
let _ = std::marker::PhantomData::<P>;
}
#[cfg(any(target_os = "macos", target_os = "ios"))]
unsafe {
let inst = &mut *ctx.cast::<AuInstance<P>>();
if let Some(ref mut editor) = inst.editor {
let params = inst.plugin.params_arc();
let plugin_ptr = SendPtr::new(&raw const inst.plugin);
let ctx_raw = SendPtr::new(ctx);
let params_for_set = params.clone();
let params_for_get = params.clone();
let params_for_plain = params.clone();
let params_for_fmt = params.clone();
let params_for_ctx = params.clone();
let params_for_state = params.clone();
let pending_state_for_set = inst.pending_state.clone();
let plugin_id_hash_for_set = inst.plugin_id_hash;
let transport_slot = inst.transport_slot.clone();
let ctx_for_begin = ctx_raw;
let ctx_for_end = ctx_raw;
let context = PluginContext::from_closures(
ClosureBridge {
#[cfg(target_os = "macos")]
begin_edit: Box::new(move |id| {
truce_au_v2_host_begin_param_gesture(ctx_for_begin.as_ptr().cast_mut(), id);
}),
#[cfg(target_os = "ios")]
begin_edit: Box::new(move |_id| {
let _ = ctx_for_begin;
}),
#[cfg(target_os = "macos")]
set_param: Box::new(move |id, value| {
let plain =
f32::from_f64(params_for_set.set_normalized_returning_plain(id, value));
truce_au_v2_host_set_param(ctx_raw.as_ptr().cast_mut(), id, plain);
}),
#[cfg(target_os = "ios")]
set_param: Box::new(move |id, value| {
let _ = ctx_raw;
let _ = params_for_set.set_normalized_returning_plain(id, value);
}),
#[cfg(target_os = "macos")]
end_edit: Box::new(move |id| {
truce_au_v2_host_end_param_gesture(ctx_for_end.as_ptr().cast_mut(), id);
}),
#[cfg(target_os = "ios")]
end_edit: Box::new(move |_id| {
let _ = ctx_for_end;
}),
request_resize: Box::new(|_w, _h| false),
get_param: Box::new(move |id| params_for_get.get_normalized(id).unwrap_or(0.0)),
get_param_plain: Box::new(move |id| {
params_for_plain.get_plain(id).unwrap_or(0.0)
}),
format_param: Box::new(move |id| {
let plain = params_for_fmt.get_plain(id).unwrap_or(0.0);
params_for_fmt
.format_value(id, plain)
.unwrap_or_else(|| format!("{plain:.1}"))
}),
get_meter: Box::new(move |id| {
let plugin = plugin_ptr.get();
plugin.get_meter(id)
}),
get_state: Box::new(move || {
let plugin = plugin_ptr.get();
plugin.save_state()
}),
set_state: Box::new(move |bytes| {
if let Some(deserialized) =
state::deserialize_state(&bytes, plugin_id_hash_for_set)
{
state::apply_params(&*params_for_state, &deserialized);
let _ = pending_state_for_set.force_push(deserialized);
}
}),
transport: Box::new(move || transport_slot.read()),
},
params_for_ctx,
);
#[cfg(target_os = "macos")]
let handle = RawWindowHandle::AppKit(parent);
#[cfg(target_os = "ios")]
let handle = RawWindowHandle::UiKit(parent);
editor.open(handle, context);
}
}
}
unsafe extern "C" fn cb_gui_close<P: PluginExport>(ctx: *mut std::ffi::c_void) {
unsafe {
let inst = &mut *ctx.cast::<AuInstance<P>>();
if let Some(ref mut editor) = inst.editor {
editor.close();
}
}
}
unsafe extern "C" {
fn malloc(size: usize) -> *mut std::ffi::c_void;
fn free(ptr: *mut std::ffi::c_void);
}
#[cfg(target_os = "macos")]
unsafe extern "C" {
fn truce_au_v2_host_set_param(ctx: *mut std::ffi::c_void, param_id: u32, value: f32);
fn truce_au_v2_host_begin_param_gesture(ctx: *mut std::ffi::c_void, param_id: u32);
fn truce_au_v2_host_end_param_gesture(ctx: *mut std::ffi::c_void, param_id: u32);
}
fn resolved_plugin_name(info: &truce_core::info::PluginInfo) -> &'static str {
truce_core::info::resolve_name_override(info.au_name, info.name)
}
pub fn register_au<P: PluginExport>() {
run_register::<P>("AU", || {
let Some((num_inputs, num_outputs)) = default_io_channels::<P>() else {
log_missing_bus_layout::<P>("AU");
return;
};
register_au_inner::<P>(num_inputs, num_outputs);
});
}
fn register_au_inner<P: PluginExport>(num_inputs: u32, num_outputs: u32) {
let info = P::info();
let param_infos = P::param_infos_static();
let mut param_descs: Vec<AuParamDescriptor> = Vec::with_capacity(param_infos.len());
for pi in ¶m_infos {
let cs = truce_core::wrapper::ParamCStrings::from_info(pi);
param_descs.push(AuParamDescriptor {
id: pi.id,
name: cs.name.into_raw(),
min: pi.range.min(),
max: pi.range.max(),
default_value: pi.default_plain,
step_count: pi.range.step_count().map_or(0, std::num::NonZero::get),
unit: cs.unit.into_raw(),
group: cs.group.into_raw(),
});
}
let name = CString::new(resolved_plugin_name(&info)).unwrap_or_default();
let vendor = CString::new(info.vendor).unwrap_or_default();
let bypass_param_id = param_infos
.iter()
.find(|pi| pi.flags.contains(ParamFlags::IS_BYPASS))
.map_or(u32::MAX, |pi| pi.id);
let has_midi_output = i32::from(matches!(info.category, PluginCategory::NoteEffect));
let descriptor = Box::leak(Box::new(AuPluginDescriptor {
component_type: info.au_type,
component_subtype: info.fourcc,
component_manufacturer: info.au_manufacturer,
name: name.into_raw(),
vendor: vendor.into_raw(),
version: 0x0001_0000, num_inputs,
num_outputs,
bypass_param_id,
has_midi_output,
}));
let callbacks = Box::leak(Box::new(AuCallbacks {
create: cb_create::<P>,
destroy: cb_destroy::<P>,
reset: cb_reset::<P>,
process: cb_process::<P>,
param_count: cb_param_count::<P>,
param_get_value: cb_param_get_value::<P>,
param_set_value: cb_param_set_value::<P>,
param_format_value: cb_param_format_value::<P>,
state_save: cb_state_save::<P>,
state_load: cb_state_load::<P>,
state_free: cb_state_free,
output_event_count: cb_output_event_count::<P>,
output_event_at: cb_output_event_at::<P>,
output_sysex_count: cb_output_sysex_count::<P>,
output_sysex_at: cb_output_sysex_at::<P>,
gui_has_editor: cb_gui_has_editor::<P>,
gui_get_size: cb_gui_get_size::<P>,
gui_open: cb_gui_open::<P>,
gui_close: cb_gui_close::<P>,
}));
let param_descs = param_descs.leak();
unsafe {
ffi::truce_au_register(
std::ptr::from_ref::<AuPluginDescriptor>(descriptor),
std::ptr::from_ref::<AuCallbacks>(callbacks),
param_descs.as_ptr(),
len_u32(param_descs.len()),
);
}
}
#[macro_export]
macro_rules! export_au {
($plugin_type:ty) => {
#[cfg(target_os = "macos")]
mod _au_entry {
use super::*;
#[unsafe(no_mangle)]
pub extern "C" fn truce_au_init() {
::truce_au::register_au::<$plugin_type>();
}
unsafe extern "C" {
fn truce_au_v2_factory_bridge(
desc: *const ::std::ffi::c_void,
) -> *mut ::std::ffi::c_void;
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn TruceAUFactory(
desc: *const ::std::ffi::c_void,
) -> *mut ::std::ffi::c_void {
truce_au_v2_factory_bridge(desc)
}
}
#[cfg(target_os = "ios")]
mod _au_entry {
use super::*;
#[unsafe(no_mangle)]
pub extern "C" fn truce_au_init() {
::truce_au::register_au::<$plugin_type>();
}
}
};
}
#[cfg(test)]
mod tests {
use truce_core::SYSEX_POOL_PREALLOC;
use truce_shim_types::AU_SHIM_TYPES_H;
#[test]
fn sysex_pool_prealloc_matches_header() {
let needle = format!("#define TRUCE_SYSEX_POOL_PREALLOC ({SYSEX_POOL_PREALLOC})");
let needle_paren = format!(
"#define TRUCE_SYSEX_POOL_PREALLOC ({} * 1024)",
SYSEX_POOL_PREALLOC / 1024,
);
assert!(
AU_SHIM_TYPES_H.contains(&needle) || AU_SHIM_TYPES_H.contains(&needle_paren),
"au_shim_types.h::TRUCE_SYSEX_POOL_PREALLOC must equal \
truce_core::SYSEX_POOL_PREALLOC ({} bytes / {} KiB). \
Looked for `{}` or `{}` in the header.",
SYSEX_POOL_PREALLOC,
SYSEX_POOL_PREALLOC / 1024,
needle,
needle_paren,
);
}
}