InitContext for AuInitContext
{
}
}
-pub(super) struct AuProcessContext {
+pub(super) struct AuProcessContext<'a, P: Plugin> {
pub sink: Arc,
pub transport: Transport,
+ pub input_events: &'a mut VecDeque>,
+ pub output_events: &'a mut VecDeque>,
pub _marker: std::marker::PhantomData,
}
-impl ProcessContext for AuProcessContext
{
+impl ProcessContext for AuProcessContext<'_, P> {
fn plugin_api(&self) -> PluginApi {
PluginApi::Au
}
@@ -72,10 +75,16 @@ impl ProcessContext for AuProcessContext
{
}
fn next_event(&mut self) -> Option> {
- None
+ self.input_events.pop_front()
}
- fn send_event(&mut self, _event: PluginNoteEvent) {}
+ fn send_event(&mut self, event: PluginNoteEvent
) {
+ if self.output_events.len() < self.output_events.capacity() {
+ self.output_events.push_back(event);
+ } else {
+ nih_debug_assert_failure!("The AU MIDI output queue is full, dropping event");
+ }
+ }
fn set_latency_samples(&self, samples: u32) {
self.sink.latency_samples.store(samples, Ordering::Relaxed);
@@ -136,8 +145,11 @@ impl GuiContext for AuGuiContext {
.find(|(p, _)| *p == param)
.map(|(_, id)| id)
{
- let instance = self.inner.instance_bits.load(std::sync::atomic::Ordering::Acquire)
- as usize as au::AudioUnit;
+ let instance = self
+ .inner
+ .instance_bits
+ .load(std::sync::atomic::Ordering::Acquire) as usize
+ as au::AudioUnit;
let au_param = AUParameter {
mAudioUnit: instance,
mParameterID: param_id,
@@ -145,7 +157,9 @@ impl GuiContext for AuGuiContext {
mElement: 0,
};
// SAFETY: AUParameterListenerNotify is safe from any thread.
- unsafe { AUParameterListenerNotify(std::ptr::null_mut(), std::ptr::null_mut(), &au_param) };
+ unsafe {
+ AUParameterListenerNotify(std::ptr::null_mut(), std::ptr::null_mut(), &au_param)
+ };
}
}
@@ -159,9 +173,7 @@ impl GuiContext for AuGuiContext {
unsafe {
crate::wrapper::state::serialize_object::
(
params_arc.clone(),
- param_map
- .iter()
- .map(|(id_str, ptr, _group)| (id_str, *ptr)),
+ param_map.iter().map(|(id_str, ptr, _group)| (id_str, *ptr)),
)
}
}
@@ -176,12 +188,7 @@ impl GuiContext for AuGuiContext {
.map(|(_, ptr, _)| *ptr)
};
unsafe {
- crate::wrapper::state::deserialize_object::
(
- &mut state,
- params_arc,
- getter,
- None,
- );
+ crate::wrapper::state::deserialize_object::
(&mut state, params_arc, getter, None);
}
}
}
diff --git a/src/wrapper/au/midi.rs b/src/wrapper/au/midi.rs
new file mode 100644
index 000000000..eac25b608
--- /dev/null
+++ b/src/wrapper/au/midi.rs
@@ -0,0 +1,1019 @@
+//! AUv2 MIDI input/output plumbing.
+//!
+//! `au-sys` 0.1.1 does not expose the MusicDevice selectors or the legacy
+//! MIDI-output callback structs, so the small C ABI surface needed by the AU
+//! wrapper is mirrored here from the macOS SDK headers.
+
+use std::borrow::Borrow;
+use std::collections::VecDeque;
+use std::ffi::c_void;
+use std::mem;
+use std::sync::atomic::{AtomicPtr, AtomicU32, AtomicU64, AtomicUsize, Ordering};
+use std::sync::Mutex;
+
+use au_sys as au;
+use crossbeam::queue::ArrayQueue;
+
+use crate::midi::{MidiResult, NoteEvent};
+use crate::plugin::Plugin;
+use crate::prelude::{MidiConfig, PluginNoteEvent};
+
+pub(super) const MUSIC_DEVICE_MIDI_EVENT_SELECT: au::SInt16 = 0x0101;
+pub(super) const MUSIC_DEVICE_SYS_EX_SELECT: au::SInt16 = 0x0102;
+pub(super) const MUSIC_DEVICE_START_NOTE_SELECT: au::SInt16 = 0x0105;
+pub(super) const MUSIC_DEVICE_STOP_NOTE_SELECT: au::SInt16 = 0x0106;
+pub(super) const MUSIC_DEVICE_PROPERTY_SUPPORTS_START_STOP_NOTE: au::AudioUnitPropertyID = 1014;
+
+const MUSIC_DEVICE_SAMPLE_FRAME_MASK: u32 = 0x00ff_ffff;
+const MIDI_EVENT_QUEUE_CAPACITY: usize = 1024;
+const MIDI_RENDER_EVENT_CAPACITY: usize = MIDI_EVENT_QUEUE_CAPACITY * 2;
+const ACTIVE_NOTE_CAPACITY: usize = 1024;
+const STOPPING_NOTE_BIT: u64 = 1 << 63;
+const MIDI_PACKET_LIST_BYTES: usize = 65_536;
+const MIDI_PACKET_CHUNK_BYTES: usize = 60_000;
+
+pub(super) type MusicDeviceMidiEventProc = unsafe extern "C" fn(
+ *mut c_void,
+ au::UInt32,
+ au::UInt32,
+ au::UInt32,
+ au::UInt32,
+) -> au::OSStatus;
+pub(super) type MusicDeviceSysExProc =
+ unsafe extern "C" fn(*mut c_void, *const u8, au::UInt32) -> au::OSStatus;
+pub(super) type MusicDeviceStartNoteProc = unsafe extern "C" fn(
+ *mut c_void,
+ au::UInt32,
+ au::UInt32,
+ *mut au::UInt32,
+ au::UInt32,
+ *const MusicDeviceNoteParams,
+) -> au::OSStatus;
+pub(super) type MusicDeviceStopNoteProc =
+ unsafe extern "C" fn(*mut c_void, au::UInt32, au::UInt32, au::UInt32) -> au::OSStatus;
+
+#[repr(C)]
+#[derive(Clone, Copy)]
+pub(super) struct NoteParamsControlValue {
+ pub id: au::AudioUnitParameterID,
+ pub value: au::AudioUnitParameterValue,
+}
+
+/// Variable-length in C. The wrapper only consumes the required pitch and
+/// velocity prefix, so one trailing control is enough to mirror its ABI.
+#[repr(C)]
+pub(super) struct MusicDeviceNoteParams {
+ pub arg_count: au::UInt32,
+ pub pitch: au::Float32,
+ pub velocity: au::Float32,
+ pub controls: [NoteParamsControlValue; 1],
+}
+
+pub(super) type AuMidiOutputCallback = unsafe extern "C" fn(
+ *mut c_void,
+ *const au::AudioTimeStamp,
+ au::UInt32,
+ *const c_void,
+) -> au::OSStatus;
+
+#[repr(C)]
+#[derive(Clone, Copy)]
+pub(super) struct AuMidiOutputCallbackStruct {
+ pub callback: Option,
+ pub user_data: *mut c_void,
+}
+
+// The host owns `user_data`; the wrapper only forwards it to the callback.
+unsafe impl Send for AuMidiOutputCallbackStruct {}
+unsafe impl Sync for AuMidiOutputCallbackStruct {}
+
+/// Lock-free render-side storage for the host's MIDI output callback.
+///
+/// Property writes happen on a control thread and may allocate or wait. The
+/// render thread announces the brief pointer-copy section with a reader count,
+/// and the serialized writer only reclaims the replaced immutable record once
+/// that count reaches zero. The render path therefore never locks, allocates,
+/// waits, or performs reference counting. Hosts retain responsibility for
+/// keeping the opaque `user_data` target alive while callbacks may be in flight,
+/// as required by the AU callback contract.
+struct MidiOutputCallbackRecord {
+ callback: AuMidiOutputCallbackStruct,
+ #[cfg(test)]
+ live_records: std::sync::Arc,
+}
+
+impl Drop for MidiOutputCallbackRecord {
+ fn drop(&mut self) {
+ #[cfg(test)]
+ self.live_records.fetch_sub(1, Ordering::SeqCst);
+ }
+}
+
+pub(super) struct MidiOutputCallbackSlot {
+ current: AtomicPtr,
+ readers: AtomicUsize,
+ writer: Mutex<()>,
+ #[cfg(test)]
+ live_records: std::sync::Arc,
+}
+
+impl MidiOutputCallbackSlot {
+ pub fn new() -> Self {
+ Self {
+ current: AtomicPtr::new(std::ptr::null_mut()),
+ readers: AtomicUsize::new(0),
+ writer: Mutex::new(()),
+ #[cfg(test)]
+ live_records: std::sync::Arc::new(AtomicUsize::new(0)),
+ }
+ }
+
+ pub fn store(&self, callback: Option) -> Result<(), ()> {
+ let _writer = self.writer.lock().map_err(|_| ())?;
+ let replacement = match callback.filter(|callback| callback.callback.is_some()) {
+ Some(callback) => {
+ let record = Box::new(MidiOutputCallbackRecord {
+ callback,
+ #[cfg(test)]
+ live_records: self.live_records.clone(),
+ });
+ #[cfg(test)]
+ self.live_records.fetch_add(1, Ordering::SeqCst);
+ Box::into_raw(record)
+ }
+ None => std::ptr::null_mut(),
+ };
+
+ let retired = self.current.swap(replacement, Ordering::SeqCst);
+ while self.readers.load(Ordering::SeqCst) != 0 {
+ std::thread::yield_now();
+ }
+
+ if !retired.is_null() {
+ // SAFETY: writers are serialized, and the reader count reached
+ // zero after this record stopped being current. A later reader can
+ // therefore only observe `replacement`.
+ unsafe { drop(Box::from_raw(retired)) };
+ }
+
+ Ok(())
+ }
+
+ #[inline]
+ pub fn load(&self) -> Option {
+ self.readers.fetch_add(1, Ordering::SeqCst);
+ let record = self.current.load(Ordering::SeqCst);
+ let callback = if record.is_null() {
+ None
+ } else {
+ // SAFETY: the reader count prevents the writer from reclaiming the
+ // immutable record until after this copy has completed.
+ Some(unsafe { (*record).callback })
+ };
+ self.readers.fetch_sub(1, Ordering::SeqCst);
+ callback
+ }
+
+ #[cfg(test)]
+ fn live_record_count(&self) -> usize {
+ self.live_records.load(Ordering::SeqCst)
+ }
+}
+
+impl Drop for MidiOutputCallbackSlot {
+ fn drop(&mut self) {
+ debug_assert_eq!(*self.readers.get_mut(), 0);
+ let record = *self.current.get_mut();
+ if !record.is_null() {
+ // SAFETY: `&mut self` proves that no safe reader or writer can still
+ // access this slot. AU teardown is also serialized against render.
+ unsafe { drop(Box::from_raw(record)) };
+ }
+ }
+}
+
+pub(super) struct QueuedMidiEvents {
+ sequence: u64,
+ first: PluginNoteEvent,
+ second: Option>,
+}
+
+/// Multi-producer queue used by the MusicDevice selector calls. Hosts may call
+/// these selectors from either their render thread or a control thread, while
+/// `AudioUnitRender` is the single consumer.
+pub(super) struct MidiInputState {
+ queue: ArrayQueue>,
+ sequence: AtomicU64,
+ next_note_id: AtomicU32,
+ active_notes: Box<[AtomicU64]>,
+}
+
+impl MidiInputState {
+ pub fn new() -> Self {
+ Self {
+ queue: ArrayQueue::new(MIDI_EVENT_QUEUE_CAPACITY),
+ sequence: AtomicU64::new(0),
+ next_note_id: AtomicU32::new(1),
+ active_notes: (0..ACTIVE_NOTE_CAPACITY)
+ .map(|_| AtomicU64::new(0))
+ .collect(),
+ }
+ }
+
+ fn push(&self, first: PluginNoteEvent
, second: Option>) -> au::OSStatus {
+ let event = QueuedMidiEvents {
+ sequence: self.sequence.fetch_add(1, Ordering::Relaxed),
+ first,
+ second,
+ };
+
+ if self.queue.push(event).is_err() {
+ au::kAudioUnitErr_TooManyFramesToProcess
+ } else {
+ au::noErr
+ }
+ }
+
+ pub fn push_midi_event(
+ &self,
+ status: au::UInt32,
+ data_1: au::UInt32,
+ data_2: au::UInt32,
+ offset: au::UInt32,
+ ) -> au::OSStatus {
+ if P::MIDI_INPUT < MidiConfig::Basic {
+ return au::noErr;
+ }
+ if status > u8::MAX as u32
+ || !(0x80..0xf0).contains(&(status as u8))
+ || data_1 > 127
+ || data_2 > 127
+ {
+ return au::kAudioUnitErr_InvalidParameter;
+ }
+
+ let timing = offset & MUSIC_DEVICE_SAMPLE_FRAME_MASK;
+ let bytes = [status as u8, data_1 as u8, data_2 as u8];
+ match NoteEvent::from_midi(timing, &bytes) {
+ Ok(event) if input_event_allowed::(&event) => self.push(event, None),
+ // Hosts should not need to special-case a plugin's MIDI dialect.
+ // Unsupported messages are accepted and ignored, matching the
+ // CLAP/VST3 wrappers.
+ _ => au::noErr,
+ }
+ }
+
+ pub unsafe fn push_sysex(&self, data: *const u8, length: au::UInt32) -> au::OSStatus {
+ if P::MIDI_INPUT < MidiConfig::Basic {
+ return au::noErr;
+ }
+ if data.is_null() || length < 2 {
+ return au::kAudioUnitErr_InvalidParameter;
+ }
+
+ let bytes = unsafe { std::slice::from_raw_parts(data, length as usize) };
+ if bytes.first() != Some(&0xf0) || bytes.last() != Some(&0xf7) {
+ return au::kAudioUnitErr_InvalidParameter;
+ }
+
+ match NoteEvent::from_midi(0, bytes) {
+ Ok(event @ NoteEvent::MidiSysEx { .. }) => self.push(event, None),
+ _ => au::noErr,
+ }
+ }
+
+ pub unsafe fn start_note(
+ &self,
+ group: au::UInt32,
+ out_note_id: *mut au::UInt32,
+ offset: au::UInt32,
+ params: *const MusicDeviceNoteParams,
+ ) -> au::OSStatus {
+ if P::MIDI_INPUT < MidiConfig::Basic {
+ return au::kAudioUnitErr_CannotDoInCurrentContext;
+ }
+ if group >= 16 || out_note_id.is_null() || params.is_null() {
+ return au::kAudioUnitErr_InvalidParameter;
+ }
+
+ let params = unsafe { &*params };
+ if params.arg_count < 2
+ || !params.pitch.is_finite()
+ || !(0.0..128.0).contains(¶ms.pitch)
+ || !params.velocity.is_finite()
+ || !(0.0..=127.0).contains(¶ms.velocity)
+ {
+ return au::kAudioUnitErr_InvalidParameter;
+ }
+
+ let rounded_pitch = params.pitch.round().clamp(0.0, 127.0);
+ let note = rounded_pitch as u8;
+ let (note_id, slot, packed) = match self.reserve_active_note(group as u8, note) {
+ Some(active) => active,
+ None => return au::kAudioUnitErr_CannotDoInCurrentContext,
+ };
+ let timing = offset & MUSIC_DEVICE_SAMPLE_FRAME_MASK;
+ let voice_id = Some(note_id as i32);
+ let note_on = NoteEvent::NoteOn {
+ timing,
+ voice_id,
+ channel: group as u8,
+ note,
+ velocity: params.velocity / 127.0,
+ };
+ let tuning = params.pitch - rounded_pitch;
+ let tuning_event = (tuning.abs() > f32::EPSILON).then_some(NoteEvent::PolyTuning {
+ timing,
+ voice_id,
+ channel: group as u8,
+ note,
+ tuning,
+ });
+
+ let status = self.push(note_on, tuning_event);
+ if status != au::noErr {
+ let _ = self.active_notes[slot].compare_exchange(
+ packed,
+ 0,
+ Ordering::AcqRel,
+ Ordering::Acquire,
+ );
+ return status;
+ }
+
+ unsafe { *out_note_id = note_id };
+ au::noErr
+ }
+
+ pub fn stop_note(
+ &self,
+ group: au::UInt32,
+ note_id: au::UInt32,
+ offset: au::UInt32,
+ ) -> au::OSStatus {
+ if P::MIDI_INPUT < MidiConfig::Basic {
+ return au::kAudioUnitErr_CannotDoInCurrentContext;
+ }
+ if group >= 16 || note_id == 0 || note_id > i32::MAX as u32 {
+ return au::kAudioUnitErr_InvalidParameter;
+ }
+
+ for slot in self.active_notes.iter() {
+ let packed = slot.load(Ordering::Acquire);
+ if packed & STOPPING_NOTE_BIT != 0
+ || unpack_note_id(packed) != note_id
+ || unpack_channel(packed) != group as u8
+ {
+ continue;
+ }
+ let claimed = packed | STOPPING_NOTE_BIT;
+ if slot
+ .compare_exchange(packed, claimed, Ordering::AcqRel, Ordering::Acquire)
+ .is_err()
+ {
+ continue;
+ }
+
+ let status = self.push(
+ NoteEvent::NoteOff {
+ timing: offset & MUSIC_DEVICE_SAMPLE_FRAME_MASK,
+ voice_id: Some(note_id as i32),
+ channel: group as u8,
+ note: unpack_note(packed),
+ velocity: 0.0,
+ },
+ None,
+ );
+ if status == au::noErr {
+ slot.store(0, Ordering::Release);
+ } else {
+ let _ = slot.compare_exchange(claimed, packed, Ordering::AcqRel, Ordering::Acquire);
+ }
+ return status;
+ }
+
+ au::kAudioUnitErr_InvalidParameter
+ }
+
+ fn reserve_active_note(&self, channel: u8, note: u8) -> Option<(u32, usize, u64)> {
+ for _ in 0..ACTIVE_NOTE_CAPACITY {
+ let note_id = self
+ .next_note_id
+ .fetch_update(Ordering::AcqRel, Ordering::Acquire, |current| {
+ Some(if current >= i32::MAX as u32 {
+ 1
+ } else {
+ current + 1
+ })
+ })
+ .ok()?;
+ if self
+ .active_notes
+ .iter()
+ .any(|slot| unpack_note_id(slot.load(Ordering::Acquire)) == note_id)
+ {
+ continue;
+ }
+
+ let packed = pack_active_note(note_id, channel, note);
+ for (idx, slot) in self.active_notes.iter().enumerate() {
+ if slot
+ .compare_exchange(0, packed, Ordering::AcqRel, Ordering::Acquire)
+ .is_ok()
+ {
+ return Some((note_id, idx, packed));
+ }
+ }
+ return None;
+ }
+ None
+ }
+
+ pub fn drain_into(&self, state: &mut MidiRenderState
, number_frames: u32) {
+ state.input_batches.clear();
+ state.input_events.clear();
+ state.output_events.clear();
+
+ // Do not consume events that arrive after this render started. They are
+ // intended for the next block and remain queued.
+ let queued_at_start = self.queue.len().min(MIDI_EVENT_QUEUE_CAPACITY);
+ for _ in 0..queued_at_start {
+ if let Some(batch) = self.queue.pop() {
+ state.input_batches.push(batch);
+ }
+ }
+ state
+ .input_batches
+ .sort_unstable_by_key(|batch| (batch.first.timing(), batch.sequence));
+
+ let last_frame = number_frames.saturating_sub(1);
+ for mut batch in state.input_batches.drain(..) {
+ batch.first.clamp_timing(last_frame);
+ state.input_events.push_back(batch.first);
+ if let Some(mut second) = batch.second {
+ second.clamp_timing(last_frame);
+ state.input_events.push_back(second);
+ }
+ }
+ }
+
+ pub fn clear(&self) {
+ while self.queue.pop().is_some() {}
+ for slot in self.active_notes.iter() {
+ slot.store(0, Ordering::Release);
+ }
+ }
+}
+
+fn input_event_allowed(event: &PluginNoteEvent) -> bool {
+ match event {
+ NoteEvent::NoteOn { .. }
+ | NoteEvent::NoteOff { .. }
+ | NoteEvent::PolyPressure { .. }
+ | NoteEvent::MidiSysEx { .. } => P::MIDI_INPUT >= MidiConfig::Basic,
+ NoteEvent::MidiChannelPressure { .. }
+ | NoteEvent::MidiPitchBend { .. }
+ | NoteEvent::MidiCC { .. }
+ | NoteEvent::MidiProgramChange { .. } => P::MIDI_INPUT >= MidiConfig::MidiCCs,
+ _ => false,
+ }
+}
+
+fn output_event_allowed(event: &PluginNoteEvent) -> bool {
+ match event {
+ NoteEvent::NoteOn { .. }
+ | NoteEvent::NoteOff { .. }
+ | NoteEvent::PolyPressure { .. }
+ | NoteEvent::MidiSysEx { .. } => P::MIDI_OUTPUT >= MidiConfig::Basic,
+ NoteEvent::MidiChannelPressure { .. }
+ | NoteEvent::MidiPitchBend { .. }
+ | NoteEvent::MidiCC { .. }
+ | NoteEvent::MidiProgramChange { .. } => P::MIDI_OUTPUT >= MidiConfig::MidiCCs,
+ _ => false,
+ }
+}
+
+fn pack_active_note(note_id: u32, channel: u8, note: u8) -> u64 {
+ ((note_id as u64) << 32) | ((channel as u64) << 8) | note as u64
+}
+
+fn unpack_note_id(packed: u64) -> u32 {
+ ((packed & !STOPPING_NOTE_BIT) >> 32) as u32
+}
+
+fn unpack_channel(packed: u64) -> u8 {
+ ((packed >> 8) & 0xff) as u8
+}
+
+fn unpack_note(packed: u64) -> u8 {
+ (packed & 0xff) as u8
+}
+
+pub(super) struct MidiRenderState {
+ input_batches: Vec>,
+ pub input_events: VecDeque>,
+ pub output_events: VecDeque>,
+ packet_storage: Vec,
+}
+
+impl MidiRenderState {
+ pub fn new() -> Self {
+ Self {
+ input_batches: Vec::with_capacity(MIDI_EVENT_QUEUE_CAPACITY),
+ input_events: VecDeque::with_capacity(MIDI_RENDER_EVENT_CAPACITY),
+ output_events: VecDeque::with_capacity(MIDI_EVENT_QUEUE_CAPACITY),
+ packet_storage: vec![0; MIDI_PACKET_LIST_BYTES / mem::size_of::()],
+ }
+ }
+
+ pub fn clear(&mut self) {
+ self.input_batches.clear();
+ self.input_events.clear();
+ self.output_events.clear();
+ }
+
+ pub unsafe fn flush_output(
+ &mut self,
+ callback: Option,
+ time_stamp: *const au::AudioTimeStamp,
+ number_frames: u32,
+ ) -> au::OSStatus {
+ let callback = match callback.filter(|cb| cb.callback.is_some()) {
+ Some(callback) if P::MIDI_OUTPUT >= MidiConfig::Basic => callback,
+ _ => {
+ self.output_events.clear();
+ return au::noErr;
+ }
+ };
+
+ let mut builder =
+ unsafe { MidiPacketListBuilder::new(callback, time_stamp, &mut self.packet_storage) };
+ while let Some(mut event) = self.output_events.pop_front() {
+ if !output_event_allowed::(&event) {
+ nih_debug_assert_failure!(
+ "Invalid AU output event for the current MIDI_OUTPUT setting"
+ );
+ continue;
+ }
+ event.clamp_timing(number_frames.saturating_sub(1));
+ let timing = event.timing() as u64;
+ match event.as_midi() {
+ Some(MidiResult::Basic(bytes)) => {
+ let length = match bytes[0] & 0xf0 {
+ 0xc0 | 0xd0 => 2,
+ _ => 3,
+ };
+ if let Err(status) = unsafe { builder.push_message(timing, &bytes[..length]) } {
+ return status;
+ }
+ }
+ Some(MidiResult::SysEx(buffer, length)) => {
+ let bytes = buffer.borrow();
+ if let Err(status) = unsafe { builder.push_message(timing, &bytes[..length]) } {
+ return status;
+ }
+ }
+ None => {
+ nih_debug_assert_failure!("AU cannot encode this note expression as MIDI 1.0");
+ }
+ }
+ }
+
+ match unsafe { builder.flush() } {
+ Ok(()) => au::noErr,
+ Err(status) => status,
+ }
+ }
+}
+
+struct MidiPacketListBuilder<'a> {
+ callback: AuMidiOutputCallbackStruct,
+ time_stamp: *const au::AudioTimeStamp,
+ storage: &'a mut [u64],
+ current_packet: *mut c_void,
+ has_packets: bool,
+}
+
+impl<'a> MidiPacketListBuilder<'a> {
+ unsafe fn new(
+ callback: AuMidiOutputCallbackStruct,
+ time_stamp: *const au::AudioTimeStamp,
+ storage: &'a mut [u64],
+ ) -> Self {
+ let current_packet = unsafe { midi_packet_list_init(storage.as_mut_ptr() as *mut c_void) };
+ Self {
+ callback,
+ time_stamp,
+ storage,
+ current_packet,
+ has_packets: false,
+ }
+ }
+
+ unsafe fn reset(&mut self) {
+ self.current_packet =
+ unsafe { midi_packet_list_init(self.storage.as_mut_ptr() as *mut c_void) };
+ self.has_packets = false;
+ }
+
+ unsafe fn push_message(&mut self, timing: u64, data: &[u8]) -> Result<(), au::OSStatus> {
+ let mut offset = 0;
+ while offset < data.len() {
+ let end = (offset + MIDI_PACKET_CHUNK_BYTES).min(data.len());
+ let chunk = &data[offset..end];
+ let mut added = unsafe {
+ midi_packet_list_add(
+ self.storage.as_mut_ptr() as *mut c_void,
+ MIDI_PACKET_LIST_BYTES,
+ self.current_packet,
+ timing,
+ chunk.len(),
+ chunk.as_ptr(),
+ )
+ };
+ if added.is_null() && self.has_packets {
+ unsafe { self.flush()? };
+ added = unsafe {
+ midi_packet_list_add(
+ self.storage.as_mut_ptr() as *mut c_void,
+ MIDI_PACKET_LIST_BYTES,
+ self.current_packet,
+ timing,
+ chunk.len(),
+ chunk.as_ptr(),
+ )
+ };
+ }
+ if added.is_null() {
+ return Err(au::kAudioUnitErr_CannotDoInCurrentContext);
+ }
+ self.current_packet = added;
+ self.has_packets = true;
+ offset = end;
+ if offset < data.len() {
+ unsafe { self.flush()? };
+ }
+ }
+ Ok(())
+ }
+
+ unsafe fn flush(&mut self) -> Result<(), au::OSStatus> {
+ if !self.has_packets {
+ return Ok(());
+ }
+ let callback = self
+ .callback
+ .callback
+ .expect("callback checked by constructor caller");
+ let status = unsafe {
+ callback(
+ self.callback.user_data,
+ self.time_stamp,
+ 0,
+ self.storage.as_ptr() as *const c_void,
+ )
+ };
+ if status != au::noErr {
+ return Err(status);
+ }
+ unsafe { self.reset() };
+ Ok(())
+ }
+}
+
+#[link(name = "CoreMIDI", kind = "framework")]
+extern "C" {
+ #[link_name = "MIDIPacketListInit"]
+ fn midi_packet_list_init(packet_list: *mut c_void) -> *mut c_void;
+ #[link_name = "MIDIPacketListAdd"]
+ fn midi_packet_list_add(
+ packet_list: *mut c_void,
+ list_size: usize,
+ current_packet: *mut c_void,
+ time: u64,
+ data_length: usize,
+ data: *const u8,
+ ) -> *mut c_void;
+}
+
+#[cfg(test)]
+mod tests {
+ use std::sync::Arc;
+
+ use super::*;
+ use crate::prelude::*;
+
+ #[derive(Debug, Clone, Copy, PartialEq)]
+ struct TestSysEx([u8; 4]);
+
+ impl SysExMessage for TestSysEx {
+ type Buffer = [u8; 4];
+
+ fn from_buffer(buffer: &[u8]) -> Option {
+ (buffer.len() == 4).then(|| Self(buffer.try_into().unwrap()))
+ }
+
+ fn to_buffer(self) -> (Self::Buffer, usize) {
+ (self.0, self.0.len())
+ }
+ }
+
+ #[derive(Default)]
+ struct TestParams {}
+
+ unsafe impl Params for TestParams {
+ fn param_map(&self) -> Vec<(String, ParamPtr, String)> {
+ Vec::new()
+ }
+ }
+
+ struct TestPlugin {
+ params: Arc,
+ }
+
+ impl Default for TestPlugin {
+ fn default() -> Self {
+ Self {
+ params: Arc::new(TestParams::default()),
+ }
+ }
+ }
+
+ impl Plugin for TestPlugin {
+ const NAME: &'static str = "AU MIDI Test";
+ const VENDOR: &'static str = "NIH-plug";
+ const URL: &'static str = "https://github.com/robbert-vdh/nih-plug";
+ const EMAIL: &'static str = "test@example.com";
+ const VERSION: &'static str = "0.0.0";
+ const AUDIO_IO_LAYOUTS: &'static [AudioIOLayout] = &[];
+ const MIDI_INPUT: MidiConfig = MidiConfig::MidiCCs;
+ const MIDI_OUTPUT: MidiConfig = MidiConfig::MidiCCs;
+
+ type SysExMessage = TestSysEx;
+ type BackgroundTask = ();
+
+ fn params(&self) -> Arc {
+ self.params.clone()
+ }
+
+ fn process(
+ &mut self,
+ _buffer: &mut Buffer,
+ _aux: &mut AuxiliaryBuffers,
+ _context: &mut impl ProcessContext,
+ ) -> ProcessStatus {
+ ProcessStatus::Normal
+ }
+ }
+
+ #[test]
+ fn music_device_channel_messages_are_sorted_and_clamped() {
+ let input = MidiInputState::::new();
+ let mut render = MidiRenderState::::new();
+
+ assert_eq!(input.push_midi_event(0x91, 60, 100, 20), au::noErr);
+ assert_eq!(input.push_midi_event(0x81, 60, 64, 1), au::noErr);
+ assert_eq!(input.push_midi_event(0xa1, 60, 32, 2), au::noErr);
+ assert_eq!(input.push_midi_event(0xb1, 7, 100, 3), au::noErr);
+ assert_eq!(input.push_midi_event(0xc1, 12, 0, 4), au::noErr);
+ assert_eq!(input.push_midi_event(0xd1, 48, 0, 5), au::noErr);
+ assert_eq!(input.push_midi_event(0xe1, 0, 64, 6), au::noErr);
+
+ input.drain_into(&mut render, 8);
+ assert_eq!(render.input_events.len(), 7);
+ let timings: Vec<_> = render.input_events.iter().map(NoteEvent::timing).collect();
+ assert_eq!(timings, vec![1, 2, 3, 4, 5, 6, 7]);
+ assert!(matches!(render.input_events[0], NoteEvent::NoteOff { .. }));
+ assert!(matches!(
+ render.input_events[1],
+ NoteEvent::PolyPressure { .. }
+ ));
+ assert!(matches!(render.input_events[2], NoteEvent::MidiCC { .. }));
+ assert!(matches!(
+ render.input_events[3],
+ NoteEvent::MidiProgramChange { .. }
+ ));
+ assert!(matches!(
+ render.input_events[4],
+ NoteEvent::MidiChannelPressure { .. }
+ ));
+ assert!(matches!(
+ render.input_events[5],
+ NoteEvent::MidiPitchBend { .. }
+ ));
+ assert!(matches!(render.input_events[6], NoteEvent::NoteOn { .. }));
+ }
+
+ #[test]
+ fn music_device_sysex_uses_the_plugin_parser() {
+ let input = MidiInputState::::new();
+ let mut render = MidiRenderState::::new();
+ let message = [0xf0, 0x01, 0x02, 0xf7];
+
+ assert_eq!(
+ unsafe { input.push_sysex(message.as_ptr(), message.len() as u32) },
+ au::noErr
+ );
+ input.drain_into(&mut render, 32);
+ assert_eq!(
+ render.input_events.pop_front(),
+ Some(NoteEvent::MidiSysEx {
+ timing: 0,
+ message: TestSysEx(message),
+ })
+ );
+ }
+
+ #[test]
+ fn midi_input_queue_fails_closed_when_full() {
+ let input = MidiInputState::::new();
+ for _ in 0..MIDI_EVENT_QUEUE_CAPACITY {
+ assert_eq!(input.push_midi_event(0x90, 60, 100, 0), au::noErr);
+ }
+ assert_eq!(
+ input.push_midi_event(0x90, 61, 100, 0),
+ au::kAudioUnitErr_TooManyFramesToProcess
+ );
+ }
+
+ #[test]
+ fn extended_start_stop_preserves_voice_identity_and_fractional_tuning() {
+ let input = MidiInputState::::new();
+ let mut render = MidiRenderState::::new();
+ let params = MusicDeviceNoteParams {
+ arg_count: 2,
+ pitch: 60.25,
+ velocity: 63.5,
+ controls: [NoteParamsControlValue { id: 0, value: 0.0 }],
+ };
+ let mut note_id = 0;
+
+ assert_eq!(
+ unsafe { input.start_note(3, &mut note_id, 4, ¶ms) },
+ au::noErr
+ );
+ assert_ne!(note_id, 0);
+ assert_eq!(input.stop_note(3, note_id, 9), au::noErr);
+
+ input.drain_into(&mut render, 16);
+ assert_eq!(render.input_events.len(), 3);
+ assert!(matches!(
+ render.input_events[0],
+ NoteEvent::NoteOn {
+ timing: 4,
+ voice_id: Some(id),
+ channel: 3,
+ note: 60,
+ ..
+ } if id == note_id as i32
+ ));
+ assert!(matches!(
+ render.input_events[1],
+ NoteEvent::PolyTuning {
+ timing: 4,
+ voice_id: Some(id),
+ tuning,
+ ..
+ } if id == note_id as i32 && (tuning - 0.25).abs() < f32::EPSILON
+ ));
+ assert!(matches!(
+ render.input_events[2],
+ NoteEvent::NoteOff {
+ timing: 9,
+ voice_id: Some(id),
+ channel: 3,
+ note: 60,
+ ..
+ } if id == note_id as i32
+ ));
+ }
+
+ #[repr(C)]
+ struct PacketCapture {
+ calls: u32,
+ count: u32,
+ timestamps: [u64; 4],
+ lengths: [u16; 4],
+ data: [[u8; 3]; 4],
+ }
+
+ unsafe extern "C" fn capture_packets(
+ user_data: *mut c_void,
+ _time_stamp: *const au::AudioTimeStamp,
+ midi_output_number: au::UInt32,
+ packet_list: *const c_void,
+ ) -> au::OSStatus {
+ assert_eq!(midi_output_number, 0);
+ let capture = unsafe { &mut *(user_data as *mut PacketCapture) };
+ capture.calls += 1;
+ let base = packet_list as *const u8;
+ let count = unsafe { std::ptr::read_unaligned(base as *const u32) };
+ let mut offset = 4usize;
+ for _ in 0..count {
+ let idx = capture.count as usize;
+ capture.timestamps[idx] =
+ unsafe { std::ptr::read_unaligned(base.add(offset) as *const u64) };
+ let length = unsafe { std::ptr::read_unaligned(base.add(offset + 8) as *const u16) };
+ capture.lengths[idx] = length;
+ let bytes =
+ unsafe { std::slice::from_raw_parts(base.add(offset + 10), length as usize) };
+ capture.data[idx][..bytes.len()].copy_from_slice(bytes);
+ capture.count += 1;
+ let next = offset + 10 + length as usize;
+ #[cfg(target_arch = "aarch64")]
+ {
+ offset = (next + 3) & !3;
+ }
+ #[cfg(not(target_arch = "aarch64"))]
+ {
+ offset = next;
+ }
+ }
+ au::noErr
+ }
+
+ #[test]
+ fn midi_output_callback_receives_sample_offset_packets() {
+ let mut render = MidiRenderState::::new();
+ render.output_events.push_back(NoteEvent::NoteOn {
+ timing: 3,
+ voice_id: None,
+ channel: 2,
+ note: 64,
+ velocity: 1.0,
+ });
+ render
+ .output_events
+ .push_back(NoteEvent::MidiProgramChange {
+ timing: 7,
+ channel: 2,
+ program: 12,
+ });
+ let mut capture = PacketCapture {
+ calls: 0,
+ count: 0,
+ timestamps: [0; 4],
+ lengths: [0; 4],
+ data: [[0; 3]; 4],
+ };
+ let callback = AuMidiOutputCallbackStruct {
+ callback: Some(capture_packets),
+ user_data: &mut capture as *mut PacketCapture as *mut c_void,
+ };
+
+ assert_eq!(
+ unsafe { render.flush_output(Some(callback), std::ptr::null(), 16) },
+ au::noErr
+ );
+ assert_eq!(capture.calls, 1);
+ assert_eq!(capture.count, 2);
+ assert_eq!(capture.timestamps[..2], [3, 7]);
+ assert_eq!(capture.lengths[..2], [3, 2]);
+ assert_eq!(capture.data[0], [0x92, 64, 127]);
+ assert_eq!(capture.data[1][..2], [0xc2, 12]);
+ }
+
+ #[test]
+ fn midi_output_callback_replacement_reclaims_records_without_blocking_render_loads() {
+ use std::sync::atomic::{AtomicBool, Ordering as AtomicOrdering};
+ use std::thread;
+
+ const UPDATE_COUNT: usize = 4096;
+ let slot = Arc::new(MidiOutputCallbackSlot::new());
+ slot.store(Some(AuMidiOutputCallbackStruct {
+ callback: Some(capture_packets),
+ user_data: 1usize as *mut c_void,
+ }))
+ .unwrap();
+
+ let writer_slot = slot.clone();
+ let writer_done = Arc::new(AtomicBool::new(false));
+ let writer_done_clone = writer_done.clone();
+ let writer = thread::spawn(move || {
+ for value in 2..=UPDATE_COUNT {
+ writer_slot
+ .store(Some(AuMidiOutputCallbackStruct {
+ callback: Some(capture_packets),
+ user_data: value as *mut c_void,
+ }))
+ .unwrap();
+ }
+ writer_done_clone.store(true, AtomicOrdering::Release);
+ });
+
+ while !writer_done.load(AtomicOrdering::Acquire) {
+ let callback = slot.load().expect("an installed callback disappeared");
+ assert!(callback.callback.is_some());
+ let value = callback.user_data as usize;
+ assert!((1..=UPDATE_COUNT).contains(&value));
+ thread::yield_now();
+ }
+ writer.join().unwrap();
+
+ assert_eq!(slot.load().unwrap().user_data as usize, UPDATE_COUNT);
+ assert_eq!(slot.live_record_count(), 1);
+ slot.store(None).unwrap();
+ assert!(slot.load().is_none());
+ assert_eq!(slot.live_record_count(), 0);
+ }
+}
diff --git a/src/wrapper/au/wrapper.rs b/src/wrapper/au/wrapper.rs
index 6e94a457c..0607d4ef8 100644
--- a/src/wrapper/au/wrapper.rs
+++ b/src/wrapper/au/wrapper.rs
@@ -29,9 +29,10 @@
//! only ever touched from `render()`. AU guarantees render is not
//! re-entered, so a `&mut` borrow inside that scope is sound.
//!
-//! 3. **main↔audio shared state** — `input_callback`. `Mutex>`;
-//! main thread updates rarely, audio thread snapshots into a local `Copy`
-//! at the top of `render()`. The mutex is held only for the snapshot.
+//! 3. **main↔audio shared state** — existing audio-input wiring is protected
+//! by a mutex and snapshotted into a local `Copy`. MIDI output callbacks
+//! use an atomic pointer plus reader-count reclamation so their render path
+//! never blocks while replaced callback records are reclaimed promptly.
use std::any::Any;
use std::cell::UnsafeCell;
@@ -40,10 +41,11 @@ use std::marker::PhantomData;
use std::mem;
use std::num::NonZeroU32;
use std::ptr;
-use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering};
+use std::sync::atomic::{AtomicBool, AtomicI32, AtomicU32, AtomicU64, Ordering};
use std::sync::{Arc, Mutex};
use au_sys as au;
+use core_foundation::array::CFArray;
use core_foundation::base::{CFType, TCFType};
use core_foundation::data::CFData;
use core_foundation::dictionary::CFDictionary;
@@ -56,20 +58,26 @@ use crate::editor::Editor;
use crate::params::internals::ParamPtr;
use crate::params::{ParamFlags, Params};
use crate::plugin::au::AuPlugin;
-use crate::prelude::{AudioIOLayout, AuxiliaryBuffers, BufferConfig, ProcessMode};
+use crate::prelude::{
+ AudioIOLayout, AuxiliaryBuffers, BufferConfig, MidiConfig, ProcessMode, ProcessStatus,
+};
use crate::wrapper::state::{self, PluginState};
-use super::context::{AUParameter, AUParameterListenerNotify, AuGuiContextInner, AuInitContext, AuProcessContext, ContextSink};
+use super::context::{
+ AUParameter, AUParameterListenerNotify, AuGuiContextInner, AuInitContext, AuProcessContext,
+ ContextSink,
+};
use super::factory::fourcc;
+use super::midi;
/// Payload of `kAudioUnitProperty_MakeConnection`.
///
/// AU v2 gives a host two ways to feed an effect's input bus: install a render
/// callback (`kAudioUnitProperty_SetRenderCallback`), or wire a source unit
-/// directly with this property. Logic Pro uses the latter; Ableton Live and
-/// most other hosts use the former. Supporting only callbacks therefore looks
-/// correct everywhere except Logic, where the input bus is never connected and
-/// the plug-in renders silence while reporting no error at all.
+/// directly with this property. These are separate host contracts and both
+/// paths must work. The Logic Pro silence regression originated in the callback
+/// pull path (timestamp forwarding and `mData` pointer replacement), not in
+/// `MakeConnection`; this struct implements the independent connection path.
///
/// `au_sys` 0.1.1 defines the property ID but not this struct, so the
/// AudioToolbox layout is mirrored here.
@@ -161,6 +169,14 @@ pub struct Wrapper {
/// Latency reported via `kAudioUnitProperty_Latency`. f64 bits.
latency_seconds_bits: AtomicU64,
+ /// Tail duration reported by the most recent `Plugin::process()` call.
+ /// Stored as f64 bits so the render thread can update it lock-free.
+ tail_seconds_bits: AtomicU64,
+
+ /// OSStatus returned by the most recent failed render, or `noErr` after a
+ /// successful render.
+ last_render_error: AtomicI32,
+
/// Whether `Plugin::initialize()` has run successfully since the last
/// `Uninitialize` / first construction. Render is a no-op when false.
initialized: AtomicBool,
@@ -231,6 +247,15 @@ pub struct Wrapper {
/// produce valid audio; the next block picks up the new one.
input_connection: Mutex>,
+ /// Bounded queue populated by the MusicDevice selector calls and drained
+ /// once at the start of each render block.
+ midi_input: midi::MidiInputState,
+
+ /// Host callback installed through `kAudioUnitProperty_MIDIOutputCallback`.
+ /// Loads from render are lock-free; the control-thread writer waits for the
+ /// brief pointer-copy section before reclaiming a retired record.
+ midi_output_callback: midi::MidiOutputCallbackSlot,
+
/// Per-aux-input-port render callbacks (elements 1, 2, … of Input scope).
/// Length equals `P::AUDIO_IO_LAYOUTS` max `aux_input_ports.len()`.
/// Each `Mutex` is independent so the audio thread can snapshot without
@@ -243,7 +268,7 @@ pub struct Wrapper {
///
/// `UnsafeCell` because only `render()` touches it after `Initialize`,
/// and AU does not re-enter render.
- render_state: UnsafeCell,
+ render_state: UnsafeCell>,
/// The plugin's `Editor` instance, if the plugin provides one.
/// Created once in `new()` and never replaced.
@@ -263,7 +288,7 @@ pub struct Wrapper {
/// All audio-thread mutable state. Reused across render calls and grown
/// only inside `Initialize` (main thread, before render is allowed). The
/// render hot path only writes existing slots — no allocation.
-struct RenderState {
+struct RenderState {
/// Per-channel scratch for input pulled via the host's render callback.
/// One inner vec per channel, each pre-sized to `max_frames_per_slice`.
input_scratch: Vec>,
@@ -295,9 +320,12 @@ struct RenderState {
/// Per-aux-port `Buffer<'static>` whose slot vectors are pre-grown in
/// `provision`. Slices are cleared after each `render()` call.
aux_buffers: Vec>,
+
+ /// Preallocated MIDI input/output queues and MIDIPacketList storage.
+ midi: midi::MidiRenderState,
}
-impl RenderState {
+impl RenderState {
fn new() -> Self {
Self {
input_scratch: Vec::new(),
@@ -306,6 +334,7 @@ impl RenderState {
aux_input_scratch: Vec::new(),
aux_bl_storages: Vec::new(),
aux_buffers: Vec::new(),
+ midi: midi::MidiRenderState::new(),
}
}
@@ -348,7 +377,8 @@ impl RenderState {
for &port_ch in aux_ports {
let n_ch = port_ch.get() as usize;
// Per-channel scratch frames.
- self.aux_input_scratch.push(vec![vec![0.0_f32; max_frames]; n_ch]);
+ self.aux_input_scratch
+ .push(vec![vec![0.0_f32; max_frames]; n_ch]);
// BufferList backing storage.
let words = bl_byte_size(n_ch).div_ceil(mem::size_of::());
self.aux_bl_storages.push(vec![0u64; words]);
@@ -408,6 +438,8 @@ const NOTIFY_LATENCY: u32 = 1 << 0;
const NOTIFY_STREAM_FORMAT: u32 = 1 << 1;
const NOTIFY_BYPASS_EFFECT: u32 = 1 << 2;
const NOTIFY_MAX_FRAMES_PER_SLICE: u32 = 1 << 3;
+const NOTIFY_TAIL_TIME: u32 = 1 << 4;
+const NOTIFY_LAST_RENDER_ERROR: u32 = 1 << 5;
/// `Send` + `Sync` justification:
///
@@ -463,7 +495,9 @@ impl Wrapper {
execute_background: Arc::new(|_| {}),
execute_gui: Arc::new(|_| {}),
};
- let editor = plugin.editor(async_executor).map(|e| Arc::new(Mutex::new(e)));
+ let editor = plugin
+ .editor(async_executor)
+ .map(|e| Arc::new(Mutex::new(e)));
// `gui_context_inner` is created here with instance = null; the
// real AudioUnit handle is filled in by `open()`. Because
@@ -492,6 +526,8 @@ impl Wrapper {
max_frames_per_slice: AtomicU32::new(1024),
n_channels: AtomicU32::new(2),
latency_seconds_bits: AtomicU64::new(pack_f64(0.0)),
+ tail_seconds_bits: AtomicU64::new(pack_f64(0.0)),
+ last_render_error: AtomicI32::new(au::noErr),
initialized: AtomicBool::new(false),
bypass: AtomicBool::new(false),
bypass_param_idx,
@@ -502,6 +538,8 @@ impl Wrapper {
sink: ContextSink::new(),
input_callback: Mutex::new(None),
input_connection: Mutex::new(None),
+ midi_input: midi::MidiInputState::new(),
+ midi_output_callback: midi::MidiOutputCallbackSlot::new(),
aux_input_callbacks: {
let n_aux = P::AUDIO_IO_LAYOUTS
.iter()
@@ -550,6 +588,26 @@ impl Wrapper {
.store(pack_f64(l), Ordering::Release);
}
#[inline]
+ fn tail_seconds(&self) -> f64 {
+ unpack_f64(self.tail_seconds_bits.load(Ordering::Acquire))
+ }
+ #[inline]
+ fn set_tail_seconds(&self, seconds: f64) {
+ let previous = self
+ .tail_seconds_bits
+ .swap(pack_f64(seconds), Ordering::AcqRel);
+ if previous != pack_f64(seconds) {
+ self.mark_pending(NOTIFY_TAIL_TIME);
+ }
+ }
+ #[inline]
+ fn set_last_render_error(&self, status: au::OSStatus) {
+ let previous = self.last_render_error.swap(status, Ordering::AcqRel);
+ if status != au::noErr && previous != status {
+ self.mark_pending(NOTIFY_LAST_RENDER_ERROR);
+ }
+ }
+ #[inline]
fn n_channels(&self) -> u32 {
self.n_channels.load(Ordering::Acquire)
}
@@ -602,11 +660,20 @@ impl Wrapper {
fire(au::kAudioUnitProperty_Latency, au::kAudioUnitScope_Global);
}
if pending & NOTIFY_STREAM_FORMAT != 0 {
- fire(au::kAudioUnitProperty_StreamFormat, au::kAudioUnitScope_Output);
- fire(au::kAudioUnitProperty_StreamFormat, au::kAudioUnitScope_Input);
+ fire(
+ au::kAudioUnitProperty_StreamFormat,
+ au::kAudioUnitScope_Output,
+ );
+ fire(
+ au::kAudioUnitProperty_StreamFormat,
+ au::kAudioUnitScope_Input,
+ );
}
if pending & NOTIFY_BYPASS_EFFECT != 0 {
- fire(au::kAudioUnitProperty_BypassEffect, au::kAudioUnitScope_Global);
+ fire(
+ au::kAudioUnitProperty_BypassEffect,
+ au::kAudioUnitScope_Global,
+ );
}
if pending & NOTIFY_MAX_FRAMES_PER_SLICE != 0 {
fire(
@@ -614,6 +681,15 @@ impl Wrapper {
au::kAudioUnitScope_Global,
);
}
+ if pending & NOTIFY_TAIL_TIME != 0 {
+ fire(au::kAudioUnitProperty_TailTime, au::kAudioUnitScope_Global);
+ }
+ if pending & NOTIFY_LAST_RENDER_ERROR != 0 {
+ fire(
+ au::kAudioUnitProperty_LastRenderError,
+ au::kAudioUnitScope_Global,
+ );
+ }
}
/// SAFETY: caller must guarantee no other reference (mut or shared) to
@@ -631,7 +707,7 @@ impl Wrapper {
/// `RenderState` for in-place mutation by the audio thread.
#[inline]
#[allow(clippy::mut_from_ref)]
- unsafe fn render_state_mut(&self) -> &mut RenderState {
+ unsafe fn render_state_mut(&self) -> &mut RenderState
{
unsafe { &mut *self.render_state.get() }
}
@@ -653,6 +729,12 @@ impl Wrapper {
unsafe extern "C" fn close(self_ptr: *mut c_void) -> au::OSStatus {
// Drop editor handle before the wrapper is destroyed.
let this = unsafe { Self::from_ptr(self_ptr) };
+ if this.initialized.swap(false, Ordering::AcqRel) {
+ // Hosts are allowed to dispose an initialized AudioUnit without a
+ // preceding Uninitialize call. Keep Plugin's lifecycle balanced.
+ unsafe { this.plugin_mut() }.deactivate();
+ }
+ this.midi_input.clear();
let instance = this.instance.swap(0, Ordering::AcqRel) as usize as *mut c_void;
cocoaui::close_audio_unit_view(instance);
this.editor_handle.clear();
@@ -671,9 +753,7 @@ impl Wrapper {
std::mem::transmute::(Self::uninitialize)
},
au::kAudioUnitGetPropertyInfoSelect => unsafe {
- std::mem::transmute::(
- Self::get_property_info,
- )
+ std::mem::transmute::(Self::get_property_info)
},
au::kAudioUnitGetPropertySelect => unsafe {
std::mem::transmute::(Self::get_property)
@@ -703,6 +783,24 @@ impl Wrapper {
Self::remove_property_listener_with_user_data,
)
},
+ midi::MUSIC_DEVICE_MIDI_EVENT_SELECT if P::MIDI_INPUT >= MidiConfig::Basic => unsafe {
+ std::mem::transmute::(
+ Self::music_device_midi_event,
+ )
+ },
+ midi::MUSIC_DEVICE_SYS_EX_SELECT if P::MIDI_INPUT >= MidiConfig::Basic => unsafe {
+ std::mem::transmute::(Self::music_device_sys_ex)
+ },
+ midi::MUSIC_DEVICE_START_NOTE_SELECT if P::MIDI_INPUT >= MidiConfig::Basic => unsafe {
+ std::mem::transmute::(
+ Self::music_device_start_note,
+ )
+ },
+ midi::MUSIC_DEVICE_STOP_NOTE_SELECT if P::MIDI_INPUT >= MidiConfig::Basic => unsafe {
+ std::mem::transmute::(
+ Self::music_device_stop_note,
+ )
+ },
_ => return None,
};
Some(method)
@@ -716,18 +814,11 @@ impl Wrapper {
let this = unsafe { Self::from_ptr(self_ptr) };
let n_ch = this.n_channels().max(1);
- let chans = NonZeroU32::new(n_ch);
- // Pick the best-matching layout for the current channel count. Prefer a
- // layout whose main_input_channels matches; fall back to const_default.
- let selected_layout = P::AUDIO_IO_LAYOUTS
- .iter()
- .find(|l| l.main_input_channels == chans && l.main_output_channels == chans)
- .copied()
- .unwrap_or(AudioIOLayout {
- main_input_channels: chans,
- main_output_channels: chans,
- ..AudioIOLayout::const_default()
- });
+ let chans = NonZeroU32::new(n_ch).expect("n_ch is clamped to at least one");
+ let selected_layout = match layout_for_output::
(chans) {
+ Some(layout) => *layout,
+ None => return au::kAudioUnitErr_FormatNotSupported,
+ };
let max_frames = this.max_frames_per_slice();
let sr = this.sample_rate();
let buffer_config = BufferConfig {
@@ -758,7 +849,15 @@ impl Wrapper {
// Provision the audio-thread render state so the hot path is
// allocation-free. SAFETY: render is serialised vs. Initialize.
let render_state = unsafe { this.render_state_mut() };
- render_state.provision(n_ch as usize, max_frames as usize, selected_layout.aux_input_ports);
+ render_state.provision(
+ n_ch as usize,
+ max_frames as usize,
+ selected_layout.aux_input_ports,
+ );
+ render_state.midi.clear();
+ this.midi_input.clear();
+ this.set_tail_seconds(0.0);
+ this.set_last_render_error(au::noErr);
let latency = this.sink.latency_samples.load(Ordering::Relaxed);
if latency > 0 && sr > 0.0 {
@@ -785,6 +884,8 @@ impl Wrapper {
// SAFETY: AU forbids Uninitialize concurrent with Render.
unsafe { this.plugin_mut() }.deactivate();
}
+ this.midi_input.clear();
+ unsafe { this.render_state_mut() }.midi.clear();
au::noErr
}
@@ -796,9 +897,61 @@ impl Wrapper {
let this = unsafe { Self::from_ptr(self_ptr) };
// SAFETY: AU calls Reset on the main thread, serialised against render.
unsafe { this.plugin_mut() }.reset();
+ this.midi_input.clear();
+ unsafe { this.render_state_mut() }.midi.clear();
+ this.set_tail_seconds(0.0);
au::noErr
}
+ unsafe extern "C" fn music_device_midi_event(
+ self_ptr: *mut c_void,
+ status: au::UInt32,
+ data_1: au::UInt32,
+ data_2: au::UInt32,
+ offset: au::UInt32,
+ ) -> au::OSStatus {
+ unsafe { Self::from_ptr(self_ptr) }
+ .midi_input
+ .push_midi_event(status, data_1, data_2, offset)
+ }
+
+ unsafe extern "C" fn music_device_sys_ex(
+ self_ptr: *mut c_void,
+ data: *const u8,
+ length: au::UInt32,
+ ) -> au::OSStatus {
+ unsafe { Self::from_ptr(self_ptr) }
+ .midi_input
+ .push_sysex(data, length)
+ }
+
+ unsafe extern "C" fn music_device_start_note(
+ self_ptr: *mut c_void,
+ _instrument: au::UInt32,
+ group: au::UInt32,
+ out_note_id: *mut au::UInt32,
+ offset: au::UInt32,
+ params: *const midi::MusicDeviceNoteParams,
+ ) -> au::OSStatus {
+ unsafe { Self::from_ptr(self_ptr) }.midi_input.start_note(
+ group,
+ out_note_id,
+ offset,
+ params,
+ )
+ }
+
+ unsafe extern "C" fn music_device_stop_note(
+ self_ptr: *mut c_void,
+ group: au::UInt32,
+ note_id: au::UInt32,
+ offset: au::UInt32,
+ ) -> au::OSStatus {
+ unsafe { Self::from_ptr(self_ptr) }
+ .midi_input
+ .stop_note(group, note_id, offset)
+ }
+
fn get_class_info(&self) -> *mut c_void {
let state = unsafe {
state::serialize_object::
(
@@ -898,7 +1051,7 @@ impl Wrapper {
self_ptr: *mut c_void,
id: au::AudioUnitPropertyID,
scope: au::AudioUnitScope,
- _element: au::AudioUnitElement,
+ element: au::AudioUnitElement,
out_data_size: *mut au::UInt32,
out_writable: *mut au::Boolean,
) -> au::OSStatus {
@@ -918,15 +1071,18 @@ impl Wrapper {
match id {
au::kAudioUnitProperty_SampleRate
- if scope == au::kAudioUnitScope_Input
- || scope == au::kAudioUnitScope_Output =>
+ if scope == au::kAudioUnitScope_Input || scope == au::kAudioUnitScope_Output =>
{
respond(std::mem::size_of::() as u32, true)
}
- au::kAudioUnitProperty_StreamFormat => respond(
- std::mem::size_of::() as u32,
- true,
- ),
+ au::kAudioUnitProperty_StreamFormat
+ if bus_channel_count::(this.n_channels(), scope, element).is_some() =>
+ {
+ respond(
+ std::mem::size_of::() as u32,
+ true,
+ )
+ }
au::kAudioUnitProperty_ElementCount => {
respond(std::mem::size_of::() as u32, false)
}
@@ -936,9 +1092,7 @@ impl Wrapper {
au::kAudioUnitProperty_TailTime if scope == au::kAudioUnitScope_Global => {
respond(std::mem::size_of::() as u32, false)
}
- au::kAudioUnitProperty_MaximumFramesPerSlice
- if scope == au::kAudioUnitScope_Global =>
- {
+ au::kAudioUnitProperty_MaximumFramesPerSlice if scope == au::kAudioUnitScope_Global => {
respond(std::mem::size_of::() as u32, true)
}
au::kAudioUnitProperty_ParameterList if scope == au::kAudioUnitScope_Global => {
@@ -948,18 +1102,22 @@ impl Wrapper {
false,
)
}
- au::kAudioUnitProperty_ParameterInfo if scope == au::kAudioUnitScope_Global => {
+ au::kAudioUnitProperty_ParameterInfo if scope == au::kAudioUnitScope_Global => respond(
+ std::mem::size_of::() as u32,
+ false,
+ ),
+ au::kAudioUnitProperty_SupportedNumChannels if scope == au::kAudioUnitScope_Global => {
respond(
- std::mem::size_of::() as u32,
+ (P::AUDIO_IO_LAYOUTS.len() * std::mem::size_of::()) as u32,
false,
)
}
- au::kAudioUnitProperty_SupportedNumChannels
- if scope == au::kAudioUnitScope_Global =>
+ au::kAudioUnitProperty_MakeConnection
+ if scope == au::kAudioUnitScope_Input
+ && element == 0
+ && current_layout::(this.n_channels())
+ .is_some_and(|layout| layout.main_input_channels.is_some()) =>
{
- respond(std::mem::size_of::() as u32, false)
- }
- au::kAudioUnitProperty_MakeConnection if scope == au::kAudioUnitScope_Input => {
respond(std::mem::size_of::() as u32, true)
}
au::kAudioUnitProperty_BypassEffect if scope == au::kAudioUnitScope_Global => {
@@ -968,7 +1126,10 @@ impl Wrapper {
au::kAudioUnitProperty_LastRenderError if scope == au::kAudioUnitScope_Global => {
respond(std::mem::size_of::() as u32, false)
}
- au::kAudioUnitProperty_SetRenderCallback if scope == au::kAudioUnitScope_Input => {
+ au::kAudioUnitProperty_SetRenderCallback
+ if scope == au::kAudioUnitScope_Input
+ && bus_channel_count::(this.n_channels(), scope, element).is_some() =>
+ {
respond(
std::mem::size_of::() as u32,
true,
@@ -983,6 +1144,24 @@ impl Wrapper {
au::kAudioUnitProperty_HostCallbacks if scope == au::kAudioUnitScope_Global => {
respond(std::mem::size_of::() as u32, true)
}
+ au::kAudioUnitProperty_MIDIOutputCallbackInfo
+ if scope == au::kAudioUnitScope_Global && P::MIDI_OUTPUT >= MidiConfig::Basic =>
+ {
+ respond(std::mem::size_of::<*mut c_void>() as u32, false)
+ }
+ au::kAudioUnitProperty_MIDIOutputCallback
+ if scope == au::kAudioUnitScope_Global && P::MIDI_OUTPUT >= MidiConfig::Basic =>
+ {
+ respond(
+ std::mem::size_of::() as u32,
+ true,
+ )
+ }
+ midi::MUSIC_DEVICE_PROPERTY_SUPPORTS_START_STOP_NOTE
+ if scope == au::kAudioUnitScope_Global && P::MIDI_INPUT >= MidiConfig::Basic =>
+ {
+ respond(std::mem::size_of::() as u32, false)
+ }
au::kAudioUnitProperty_CocoaUI if scope == au::kAudioUnitScope_Global => {
if this.editor.is_some() {
respond(std::mem::size_of::() as u32, false)
@@ -1019,11 +1198,17 @@ impl Wrapper {
return au::kAudioUnitErr_InvalidParameter;
}
- match id {
- au::kAudioUnitProperty_SampleRate => {
- if (unsafe { *io_data_size } as usize) < std::mem::size_of::() {
+ macro_rules! require_output {
+ ($ty:ty) => {
+ if (unsafe { *io_data_size } as usize) < std::mem::size_of::<$ty>() {
return au::kAudioUnitErr_InvalidPropertyValue;
}
+ };
+ }
+
+ match id {
+ au::kAudioUnitProperty_SampleRate => {
+ require_output!(au::Float64);
unsafe {
*(out_data as *mut au::Float64) = this.sample_rate();
*io_data_size = std::mem::size_of::() as u32;
@@ -1031,17 +1216,19 @@ impl Wrapper {
au::noErr
}
au::kAudioUnitProperty_ElementCount => {
- // Input scope: element 0 = main, elements 1+ = aux inputs.
+ require_output!(au::UInt32);
+ // Input scope: optional main input followed by aux inputs.
// Output scope: always 1 (aux outputs not yet implemented).
let n_ch = this.n_channels().max(1);
let chans = NonZeroU32::new(n_ch);
- let n_aux_inputs = P::AUDIO_IO_LAYOUTS
- .iter()
- .find(|l| l.main_input_channels == chans && l.main_output_channels == chans)
- .map(|l| l.aux_input_ports.len())
- .unwrap_or(0);
+ let layout = chans.and_then(layout_for_output::
);
let count: au::UInt32 = match scope {
- au::kAudioUnitScope_Input => 1 + n_aux_inputs as u32,
+ au::kAudioUnitScope_Input => layout
+ .map(|layout| {
+ u32::from(layout.main_input_channels.is_some())
+ + layout.aux_input_ports.len() as u32
+ })
+ .unwrap_or(0),
au::kAudioUnitScope_Output => 1,
au::kAudioUnitScope_Global => 1,
_ => 0,
@@ -1053,6 +1240,7 @@ impl Wrapper {
au::noErr
}
au::kAudioUnitProperty_Latency if scope == au::kAudioUnitScope_Global => {
+ require_output!(au::Float64);
unsafe {
*(out_data as *mut au::Float64) = this.latency_seconds();
*io_data_size = std::mem::size_of::() as u32;
@@ -1060,13 +1248,15 @@ impl Wrapper {
au::noErr
}
au::kAudioUnitProperty_TailTime if scope == au::kAudioUnitScope_Global => {
+ require_output!(au::Float64);
unsafe {
- *(out_data as *mut au::Float64) = 0.0;
+ *(out_data as *mut au::Float64) = this.tail_seconds();
*io_data_size = std::mem::size_of::() as u32;
}
au::noErr
}
au::kAudioUnitProperty_MaximumFramesPerSlice => {
+ require_output!(au::UInt32);
unsafe {
*(out_data as *mut au::UInt32) = this.max_frames_per_slice();
*io_data_size = std::mem::size_of::() as u32;
@@ -1074,11 +1264,11 @@ impl Wrapper {
au::noErr
}
au::kAudioUnitProperty_StreamFormat => {
- if (unsafe { *io_data_size } as usize)
- < std::mem::size_of::()
- {
- return au::kAudioUnitErr_InvalidPropertyValue;
- }
+ require_output!(au::AudioStreamBasicDescription);
+ let channels = match bus_channel_count::(this.n_channels(), scope, element) {
+ Some(channels) => channels,
+ None => return au::kAudioUnitErr_InvalidElement,
+ };
let asbd = au::AudioStreamBasicDescription {
mSampleRate: this.sample_rate(),
mFormatID: au::kAudioFormatLinearPCM,
@@ -1088,14 +1278,13 @@ impl Wrapper {
mBytesPerPacket: 4,
mFramesPerPacket: 1,
mBytesPerFrame: 4,
- mChannelsPerFrame: this.n_channels(),
+ mChannelsPerFrame: channels,
mBitsPerChannel: 32,
mReserved: 0,
};
unsafe {
*(out_data as *mut au::AudioStreamBasicDescription) = asbd;
- *io_data_size =
- std::mem::size_of::() as u32;
+ *io_data_size = std::mem::size_of::() as u32;
}
au::noErr
}
@@ -1134,43 +1323,36 @@ impl Wrapper {
}
au::noErr
}
- au::kAudioUnitProperty_SupportedNumChannels
- if scope == au::kAudioUnitScope_Global =>
- {
- if (unsafe { *io_data_size } as usize) < std::mem::size_of::() {
+ au::kAudioUnitProperty_SupportedNumChannels if scope == au::kAudioUnitScope_Global => {
+ let needed = P::AUDIO_IO_LAYOUTS.len() * std::mem::size_of::();
+ if (unsafe { *io_data_size } as usize) < needed {
return au::kAudioUnitErr_InvalidPropertyValue;
}
- // Report the first declared layout's main channel count.
- // If multiple layouts are declared we currently only expose
- // one entry; auval accepts this as a conservative answer.
- let info = match P::AUDIO_IO_LAYOUTS.iter().next() {
- Some(layout) => {
- let in_ch = layout
- .main_input_channels
- .map(|n| n.get() as i16)
- .unwrap_or(0);
- let out_ch = layout
- .main_output_channels
- .map(|n| n.get() as i16)
- .unwrap_or(0);
- au::AUChannelInfo {
- inChannels: in_ch,
- outChannels: out_ch,
- }
+ let dst = out_data as *mut au::AUChannelInfo;
+ for (idx, layout) in P::AUDIO_IO_LAYOUTS.iter().enumerate() {
+ unsafe {
+ *dst.add(idx) = au::AUChannelInfo {
+ inChannels: layout
+ .main_input_channels
+ .map(|n| n.get() as i16)
+ .unwrap_or(0),
+ outChannels: layout
+ .main_output_channels
+ .map(|n| n.get() as i16)
+ .unwrap_or(0),
+ };
}
- None => au::AUChannelInfo {
- inChannels: -1,
- outChannels: -1,
- },
- };
- unsafe {
- *(out_data as *mut au::AUChannelInfo) = info;
- *io_data_size = std::mem::size_of::() as u32;
}
+ unsafe { *io_data_size = needed as u32 };
au::noErr
}
au::kAudioUnitProperty_BypassEffect if scope == au::kAudioUnitScope_Global => {
- let on = if this.bypass.load(Ordering::Acquire) { 1 } else { 0 };
+ require_output!(au::UInt32);
+ let on = if this.bypass.load(Ordering::Acquire) {
+ 1
+ } else {
+ 0
+ };
unsafe {
*(out_data as *mut au::UInt32) = on;
*io_data_size = std::mem::size_of::() as u32;
@@ -1178,13 +1360,16 @@ impl Wrapper {
au::noErr
}
au::kAudioUnitProperty_LastRenderError if scope == au::kAudioUnitScope_Global => {
+ require_output!(au::OSStatus);
unsafe {
- *(out_data as *mut au::OSStatus) = au::noErr;
+ *(out_data as *mut au::OSStatus) =
+ this.last_render_error.load(Ordering::Acquire);
*io_data_size = std::mem::size_of::() as u32;
}
au::noErr
}
au::kAudioUnitProperty_InPlaceProcessing => {
+ require_output!(au::UInt32);
unsafe {
*(out_data as *mut au::UInt32) = 1;
*io_data_size = std::mem::size_of::() as u32;
@@ -1192,9 +1377,7 @@ impl Wrapper {
au::noErr
}
au::kAudioUnitProperty_ClassInfo if scope == au::kAudioUnitScope_Global => {
- if (unsafe { *io_data_size } as usize) < std::mem::size_of::<*mut c_void>() {
- return au::kAudioUnitErr_InvalidPropertyValue;
- }
+ require_output!(*mut c_void);
let dict = this.get_class_info();
unsafe {
*(out_data as *mut *mut c_void) = dict;
@@ -1206,7 +1389,8 @@ impl Wrapper {
if this.editor.is_none() {
return au::kAudioUnitErr_InvalidProperty;
}
- if (unsafe { *io_data_size } as usize) < std::mem::size_of::() {
+ if (unsafe { *io_data_size } as usize) < std::mem::size_of::()
+ {
return au::kAudioUnitErr_InvalidPropertyValue;
}
// Register (or look up) the per-type ObjC view factory class and
@@ -1244,6 +1428,30 @@ impl Wrapper {
}
au::noErr
}
+ au::kAudioUnitProperty_MIDIOutputCallbackInfo
+ if scope == au::kAudioUnitScope_Global && P::MIDI_OUTPUT >= MidiConfig::Basic =>
+ {
+ require_output!(*mut c_void);
+ let names: CFArray =
+ CFArray::from_CFTypes(&[CFString::from_static_string("MIDI Out")]);
+ let names_ref = names.as_concrete_TypeRef();
+ std::mem::forget(names);
+ unsafe {
+ *(out_data as *mut *mut c_void) = names_ref as *mut c_void;
+ *io_data_size = std::mem::size_of::<*mut c_void>() as u32;
+ }
+ au::noErr
+ }
+ midi::MUSIC_DEVICE_PROPERTY_SUPPORTS_START_STOP_NOTE
+ if scope == au::kAudioUnitScope_Global && P::MIDI_INPUT >= MidiConfig::Basic =>
+ {
+ require_output!(au::UInt32);
+ unsafe {
+ *(out_data as *mut au::UInt32) = 1;
+ *io_data_size = std::mem::size_of::() as u32;
+ }
+ au::noErr
+ }
_ => au::kAudioUnitErr_InvalidProperty,
}
}
@@ -1306,6 +1514,9 @@ impl Wrapper {
if asbd.mChannelsPerFrame == 0 {
return au::kAudioUnitErr_InvalidPropertyValue;
}
+ if !(asbd.mSampleRate > 0.0 && asbd.mSampleRate.is_finite()) {
+ return au::kAudioUnitErr_InvalidPropertyValue;
+ }
// Reject non-PCM / non-float / interleaved — we only ever
// advertise non-interleaved 32-bit float in get_property.
if asbd.mFormatID != au::kAudioFormatLinearPCM
@@ -1317,16 +1528,33 @@ impl Wrapper {
if asbd.mBitsPerChannel != 32 {
return au::kAudioUnitErr_FormatNotSupported;
}
- // Match the requested channel count against P::AUDIO_IO_LAYOUTS.
- // For an effect (in_ch == out_ch) we look for a layout where
- // both main_input and main_output match.
let req_ch = asbd.mChannelsPerFrame;
- if !layout_supports::
(req_ch) {
- return au::kAudioUnitErr_FormatNotSupported;
+ match scope {
+ au::kAudioUnitScope_Output if element == 0 => {
+ let req = NonZeroU32::new(req_ch).expect("channel count checked above");
+ if layout_for_output::
(req).is_none() {
+ return au::kAudioUnitErr_FormatNotSupported;
+ }
+ this.n_channels.store(req_ch, Ordering::Release);
+ }
+ au::kAudioUnitScope_Input => {
+ let expected = match bus_channel_count::
(
+ this.n_channels(),
+ au::kAudioUnitScope_Input,
+ element,
+ ) {
+ Some(expected) => expected,
+ None => return au::kAudioUnitErr_InvalidElement,
+ };
+ if expected != req_ch {
+ return au::kAudioUnitErr_FormatNotSupported;
+ }
+ }
+ au::kAudioUnitScope_Output => return au::kAudioUnitErr_InvalidElement,
+ _ => return au::kAudioUnitErr_InvalidScope,
}
this.set_sample_rate(asbd.mSampleRate);
- this.n_channels.store(req_ch, Ordering::Release);
this.mark_pending(NOTIFY_STREAM_FORMAT);
au::noErr
}
@@ -1375,6 +1603,12 @@ impl Wrapper {
if element != 0 {
return au::kAudioUnitErr_InvalidElement;
}
+ let has_main_input = current_layout::
(this.n_channels())
+ .map(|layout| layout.main_input_channels.is_some())
+ .unwrap_or(false);
+ if !has_main_input {
+ return au::kAudioUnitErr_InvalidElement;
+ }
let conn = unsafe { *(in_data as *const AudioUnitConnection) };
// `conn.dest_input_number` is deliberately not consulted: AU's
// convention is that the property's element *is* the
@@ -1400,11 +1634,20 @@ impl Wrapper {
}
au::noErr
}
- au::kAudioUnitProperty_SetRenderCallback => {
+ au::kAudioUnitProperty_SetRenderCallback if scope == au::kAudioUnitScope_Input => {
payload!(au::AURenderCallbackStruct);
let cb = unsafe { *(in_data as *const au::AURenderCallbackStruct) };
- let new_cb = if cb.inputProc.is_some() { Some(cb) } else { None };
- if element == 0 {
+ let new_cb = if cb.inputProc.is_some() {
+ Some(cb)
+ } else {
+ None
+ };
+ let layout = match current_layout::
(this.n_channels()) {
+ Some(layout) => layout,
+ None => return au::kAudioUnitErr_FormatNotSupported,
+ };
+ let aux_base = u32::from(layout.main_input_channels.is_some());
+ if aux_base == 1 && element == 0 {
if let Ok(mut guard) = this.input_callback.lock() {
*guard = new_cb;
}
@@ -1414,12 +1657,19 @@ impl Wrapper {
*guard = None;
}
} else {
- // elements 1+ are aux inputs
- let aux_idx = (element - 1) as usize;
+ if element < aux_base {
+ return au::kAudioUnitErr_InvalidElement;
+ }
+ let aux_idx = (element - aux_base) as usize;
+ if aux_idx >= layout.aux_input_ports.len() {
+ return au::kAudioUnitErr_InvalidElement;
+ }
if let Some(slot) = this.aux_input_callbacks.get(aux_idx) {
if let Ok(mut guard) = slot.lock() {
*guard = new_cb;
}
+ } else {
+ return au::kAudioUnitErr_InvalidElement;
}
}
au::noErr
@@ -1438,6 +1688,22 @@ impl Wrapper {
}
au::noErr
}
+ au::kAudioUnitProperty_MIDIOutputCallback
+ if scope == au::kAudioUnitScope_Global && P::MIDI_OUTPUT >= MidiConfig::Basic =>
+ {
+ payload!(midi::AuMidiOutputCallbackStruct);
+ let callback =
+ unsafe { ptr::read(in_data as *const midi::AuMidiOutputCallbackStruct) };
+ if this
+ .midi_output_callback
+ .store(callback.callback.map(|_| callback))
+ .is_ok()
+ {
+ au::noErr
+ } else {
+ au::kAudioUnitErr_CannotDoInCurrentContext
+ }
+ }
_ => au::kAudioUnitErr_InvalidProperty,
}
}
@@ -1563,6 +1829,7 @@ impl Wrapper {
let this = unsafe { Self::from_ptr(self_ptr) };
if io_data.is_null() {
+ this.set_last_render_error(au::kAudioUnitErr_InvalidParameter);
return au::kAudioUnitErr_InvalidParameter;
}
@@ -1572,6 +1839,22 @@ impl Wrapper {
}
let n_frames = in_number_frames as usize;
+ if in_number_frames > this.max_frames_per_slice() {
+ unsafe { zero_buffer_list(io_data, in_number_frames) };
+ this.set_last_render_error(au::kAudioUnitErr_TooManyFramesToProcess);
+ return au::kAudioUnitErr_TooManyFramesToProcess;
+ }
+
+ let layout = match current_layout::
(this.n_channels()) {
+ Some(layout) => layout,
+ None => {
+ unsafe { zero_buffer_list(io_data, in_number_frames) };
+ this.set_last_render_error(au::kAudioUnitErr_FormatNotSupported);
+ return au::kAudioUnitErr_FormatNotSupported;
+ }
+ };
+ let has_main_input = layout.main_input_channels.is_some();
+ let aux_element_base = u32::from(has_main_input);
// Snapshot the input callback under the mutex (cheap struct copy).
// Released immediately so main-thread updates don't block render
@@ -1579,19 +1862,22 @@ impl Wrapper {
// The two wirings are kept mutually exclusive when the host sets them,
// so at most one of these is ever populated; the callback is preferred
// if both somehow are.
- let input_source = this
- .input_callback
- .lock()
- .ok()
- .and_then(|g| *g)
- .map(InputSource::Callback)
- .or_else(|| {
- this.input_connection
+ let input_source = has_main_input
+ .then(|| {
+ this.input_callback
.lock()
.ok()
.and_then(|g| *g)
- .map(InputSource::Connection)
- });
+ .map(InputSource::Callback)
+ .or_else(|| {
+ this.input_connection
+ .lock()
+ .ok()
+ .and_then(|g| *g)
+ .map(InputSource::Connection)
+ })
+ })
+ .flatten();
// SAFETY: render is not re-entered; we are the sole owner of
// RenderState for the duration of this call.
@@ -1615,8 +1901,7 @@ impl Wrapper {
// The N-tuple of AudioBuffer entries lives at the natural
// C `mBuffers` offset, which the compiler computes
// accounting for any padding after `mNumberBuffers`.
- let header_offset =
- mem::offset_of!(au::AudioBufferList, mBuffers);
+ let header_offset = mem::offset_of!(au::AudioBufferList, mBuffers);
let buffers_ptr = unsafe {
(rs.bl_storage.as_mut_ptr() as *mut u8).add(header_offset)
as *mut au::AudioBuffer
@@ -1626,8 +1911,7 @@ impl Wrapper {
unsafe {
*buffers_ptr.add(ch) = au::AudioBuffer {
mNumberChannels: 1,
- mDataByteSize: (n_frames * mem::size_of::())
- as au::UInt32,
+ mDataByteSize: (n_frames * mem::size_of::()) as au::UInt32,
mData: scratch_ptr as *mut c_void,
};
}
@@ -1671,7 +1955,11 @@ impl Wrapper {
)
},
};
- if status == au::noErr {
+ if status != au::noErr {
+ unsafe { zero_buffer_list(io_data, in_number_frames) };
+ this.set_last_render_error(status);
+ return status;
+ } else {
pulled_input = true;
// The callback is allowed to *replace* the mData pointers
// with its own buffers instead of filling the ones we
@@ -1689,8 +1977,8 @@ impl Wrapper {
let src = b.mData as *const f32;
let dst = rs.input_scratch[ch].as_mut_ptr();
if !src.is_null() && src != dst as *const f32 {
- let frames = n_frames
- .min(b.mDataByteSize as usize / mem::size_of::());
+ let frames =
+ n_frames.min(b.mDataByteSize as usize / mem::size_of::());
unsafe { ptr::copy_nonoverlapping(src, dst, frames) };
}
}
@@ -1711,9 +1999,8 @@ impl Wrapper {
if buf.mData.is_null() || !buffer_fits_frames(buf, n_frames) {
continue;
}
- let dst = unsafe {
- std::slice::from_raw_parts_mut(buf.mData as *mut f32, n_frames)
- };
+ let dst =
+ unsafe { std::slice::from_raw_parts_mut(buf.mData as *mut f32, n_frames) };
let src = &rs.input_scratch[i][..n_frames];
dst.copy_from_slice(src);
}
@@ -1770,10 +2057,7 @@ impl Wrapper {
slots[i] = &mut [];
continue;
}
- let raw = std::slice::from_raw_parts_mut(
- buf.mData as *mut f32,
- n_frames,
- );
+ let raw = std::slice::from_raw_parts_mut(buf.mData as *mut f32, n_frames);
// The slot type carries `'static` because `RenderState`
// is itself field-stored; the slice we put here lives
// only until we clear it below. This mirrors the
@@ -1794,7 +2078,8 @@ impl Wrapper {
if let Some(beat_and_tempo) = cb.beatAndTempoProc {
let mut beat = 0.0;
let mut tempo = 0.0;
- if unsafe { beat_and_tempo(cb.hostUserData, &mut beat, &mut tempo) } == au::noErr
+ if unsafe { beat_and_tempo(cb.hostUserData, &mut beat, &mut tempo) }
+ == au::noErr
{
transport.pos_beats = Some(beat);
transport.tempo = Some(tempo);
@@ -1865,6 +2150,7 @@ impl