pub mod ffi;
use std::ffi::CString;
use std::os::raw::c_char;
use std::slice;
use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::{Arc, OnceLock};
#[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;
use truce_core::chunked_process::{ChunkedProcess, process_chunked};
#[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::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, ParamInfo, Params};
use ffi::{
AuCallbacks, AuMidi2Event, AuMidiEvent, AuParamDescriptor, AuParamEvent, 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,
sub_event_scratch: EventList,
param_infos: Vec<ParamInfo>,
min_subblock_samples: u32,
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::PluginRuntime>::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 param_infos = plugin.params().param_infos();
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),
sub_event_scratch: EventList::with_capacity(EVENT_LIST_PREALLOC),
param_infos,
min_subblock_samples: info.automation.min_subblock_samples,
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() {
let raw = ctx.cast::<AuInstance<P>>();
let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
drop(Box::from_raw(raw));
}));
}
}
}
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,
param_events: *const AuParamEvent,
num_param_events: 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);
}
}
_ => {
}
}
}
}
if !param_events.is_null() && num_param_events > 0 {
let pe_slice = slice::from_raw_parts(param_events, num_param_events as usize);
for pe in pe_slice {
inst.event_list.push(Event {
sample_offset: pe.sample_offset,
body: EventBody::ParamChange {
id: pe.param_id,
value: f64::from(pe.value),
},
});
}
}
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 transport_snap = transport;
let chunk_args = ChunkedProcess {
events: &inst.event_list,
sub_event_scratch: &mut inst.sub_event_scratch,
transport: &mut transport_snap,
sample_rate: inst.sample_rate,
output_events: &mut inst.output_events,
params_fn: None,
meters_fn: None,
param_infos: &inst.param_infos,
min_subblock_samples: inst.min_subblock_samples,
};
process_chunked(
&mut inst.plugin,
inst.params_arc.as_ref() as &dyn Params,
&mut audio_buffer,
chunk_args,
);
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>());
}
}
}
struct FactoryPresetEntry {
name: CString,
path: std::path::PathBuf,
}
static FACTORY_PRESETS: OnceLock<Vec<FactoryPresetEntry>> = OnceLock::new();
fn factory_presets<P: PluginExport>() -> &'static [FactoryPresetEntry] {
FACTORY_PRESETS.get_or_init(|| {
let Some(root) = component_presets_root::<P>() else {
return Vec::new();
};
let info = P::info();
let mut refs = truce_core::presets::enumerate_scope(
&root,
truce_core::presets::PresetScope::Factory,
info.vendor,
info.name,
);
refs.sort_by_key(|preset| !preset.default);
refs.into_iter()
.filter_map(|preset| {
let display = match &preset.category {
Some(category) => format!("{category}/{}", preset.name),
None => preset.name.clone(),
};
Some(FactoryPresetEntry {
name: CString::new(display).ok()?,
path: preset.path,
})
})
.collect()
})
}
#[repr(C)]
#[allow(clippy::struct_field_names)]
struct DlInfo {
dli_fname: *const c_char,
dli_fbase: *mut std::ffi::c_void,
dli_sname: *const c_char,
dli_saddr: *mut std::ffi::c_void,
}
unsafe extern "C" {
fn dladdr(addr: *const std::ffi::c_void, info: *mut DlInfo) -> i32;
}
fn component_presets_root<P: PluginExport>() -> Option<std::path::PathBuf> {
let mut info = DlInfo {
dli_fname: std::ptr::null(),
dli_fbase: std::ptr::null_mut(),
dli_sname: std::ptr::null(),
dli_saddr: std::ptr::null_mut(),
};
let probe = cb_factory_preset_count::<P> as *const std::ffi::c_void;
if unsafe { dladdr(probe, &raw mut info) } == 0 || info.dli_fname.is_null() {
return None;
}
let exe = unsafe { std::ffi::CStr::from_ptr(info.dli_fname) };
let exe = std::path::Path::new(exe.to_str().ok()?);
let parent = exe.parent()?;
[parent.parent()?, parent]
.into_iter()
.map(|dir| dir.join("Resources/Presets"))
.find(|root| root.is_dir())
}
unsafe extern "C" fn cb_factory_preset_count<P: PluginExport>(_ctx: *mut std::ffi::c_void) -> u32 {
run_extern_callback_with::<P, u32>("au", "factory_preset_count", 0, || {
len_u32(factory_presets::<P>().len())
})
}
unsafe extern "C" fn cb_factory_preset_name<P: PluginExport>(
_ctx: *mut std::ffi::c_void,
index: u32,
) -> *const c_char {
run_extern_callback_with::<P, *const c_char>(
"au",
"factory_preset_name",
std::ptr::null(),
|| {
factory_presets::<P>()
.get(index as usize)
.map_or(std::ptr::null(), |entry| entry.name.as_ptr())
},
)
}
unsafe extern "C" fn cb_factory_preset_load<P: PluginExport>(
ctx: *mut std::ffi::c_void,
index: u32,
) -> i32 {
run_extern_callback_with::<P, i32>("au", "factory_preset_load", 0, || unsafe {
let inst = &mut *ctx.cast::<AuInstance<P>>();
let Some(entry) = factory_presets::<P>().get(index as usize) else {
return 0;
};
let Some(deserialized) =
truce_core::presets::load_preset_file(&entry.path, inst.plugin_id_hash)
else {
return 0;
};
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();
}
1
})
}
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_can_resize<P: PluginExport>(ctx: *mut std::ffi::c_void) -> i32 {
unsafe {
if ctx.is_null() {
return 0;
}
let inst = &*ctx.cast::<AuInstance<P>>();
i32::from(inst.editor.as_ref().is_some_and(|e| e.can_resize()))
}
}
unsafe extern "C" fn cb_gui_set_size<P: PluginExport>(ctx: *mut std::ffi::c_void, w: u32, h: u32) {
unsafe {
if ctx.is_null() || w == 0 || h == 0 {
return;
}
let inst = &mut *ctx.cast::<AuInstance<P>>();
if let Some(ref mut editor) = inst.editor
&& editor.can_resize()
{
let (cw, ch) = clamp_logical_to_editor(w, h, editor.as_ref());
editor.set_size(cw, ch);
}
}
}
fn clamp_logical_to_editor(w: u32, h: u32, editor: &dyn truce_core::editor::Editor) -> (u32, u32) {
let (min_w, min_h) = editor.min_size();
let (max_w, max_h) = editor.max_size();
let mut w = w.clamp(min_w.max(1), max_w);
let mut h = h.clamp(min_h.max(1), max_h);
if let Some((num, denom)) = editor.aspect_ratio()
&& num > 0
&& denom > 0
{
let num64 = u64::from(num);
let denom64 = u64::from(denom);
let h_implied = (u64::from(w) * denom64 / num64).clamp(1, u64::from(u32::MAX));
#[allow(clippy::cast_possible_truncation)]
let h_implied_u32 = h_implied as u32;
if h_implied_u32 >= min_h.max(1) && h_implied_u32 <= max_h {
h = h_implied_u32;
} else {
let w_implied = (u64::from(h) * num64 / denom64).clamp(1, u64::from(u32::MAX));
#[allow(clippy::cast_possible_truncation)]
let w_implied_u32 = w_implied as u32;
w = w_implied_u32.clamp(min_w.max(1), max_w);
let h_final = (u64::from(w) * denom64 / num64).clamp(1, u64::from(u32::MAX));
#[allow(clippy::cast_possible_truncation)]
{
h = (h_final as u32).clamp(min_h.max(1), max_h);
}
}
}
(w, h)
}
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 pending_state_for_set = inst.pending_state.clone();
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(move |w, h| {
if w == 0 || h == 0 {
return false;
}
let inst = &mut *ctx_raw.as_ptr().cast_mut().cast::<AuInstance<P>>();
inst.editor.as_mut().is_some_and(|e| e.set_size(w, h))
}),
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| {
let _ = pending_state_for_set.force_push(state::DeserializedState {
params: Vec::new(),
extra: Some(bytes),
});
}),
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(editor) = inst.editor.as_mut() {
let editor_ptr: *mut dyn truce_core::editor::Editor = editor.as_mut();
let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
(*editor_ptr).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>,
gui_can_resize: cb_gui_can_resize::<P>,
gui_set_size: cb_gui_set_size::<P>,
factory_preset_count: cb_factory_preset_count::<P>,
factory_preset_name: cb_factory_preset_name::<P>,
factory_preset_load: cb_factory_preset_load::<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,
);
}
}