1#![cfg(target_vendor = "apple")]
27
28use truce_rack_core::buffer::AudioBuffer;
29use truce_rack_core::bus::BusLayout;
30use truce_rack_core::error::{Error, Result};
31use truce_rack_core::events::EventList;
32use truce_rack_core::info::{ParameterInfo, PluginCategory, PluginInfo, PresetInfo};
33use truce_rack_core::plugin::{Plugin, PluginCore, ProcessContext, ProcessStatus};
34use truce_rack_core::scanner::PluginScanner;
35use truce_rack_core::transport::TransportInfo;
36
37use objc2_audio_toolbox::{
38 AUPreset, AURenderCallbackStruct, AudioComponent, AudioComponentCopyName,
39 AudioComponentDescription, AudioComponentFindNext, AudioComponentFlags, AudioComponentInstance,
40 AudioComponentInstanceDispose, AudioComponentInstanceNew, AudioComponentInstantiate,
41 AudioComponentInstantiationOptions, AudioUnitGetParameter, AudioUnitGetProperty,
42 AudioUnitGetPropertyInfo, AudioUnitInitialize, AudioUnitParameterID, AudioUnitParameterInfo,
43 AudioUnitRender, AudioUnitRenderActionFlags, AudioUnitSetParameter, AudioUnitSetProperty,
44 AudioUnitUninitialize, HostCallbackInfo, MusicDeviceMIDIEvent,
45 kAudioUnitErr_CannotDoInCurrentContext, kAudioUnitProperty_ClassInfo,
46 kAudioUnitProperty_FactoryPresets, kAudioUnitProperty_HostCallbacks,
47 kAudioUnitProperty_MaximumFramesPerSlice, kAudioUnitProperty_ParameterInfo,
48 kAudioUnitProperty_ParameterList, kAudioUnitProperty_PresentPreset,
49 kAudioUnitProperty_SetRenderCallback, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Global,
50 kAudioUnitScope_Input, kAudioUnitScope_Output, kAudioUnitType_Effect, kAudioUnitType_Generator,
51 kAudioUnitType_MIDIProcessor, kAudioUnitType_Mixer, kAudioUnitType_MusicDevice,
52 kAudioUnitType_MusicEffect,
53};
54use objc2_core_audio_types::{
55 AudioBuffer as CAAudioBuffer, AudioBufferList, AudioStreamBasicDescription, AudioTimeStamp,
56 AudioTimeStampFlags, kAudioFormatFlagIsFloat, kAudioFormatFlagIsNonInterleaved,
57 kAudioFormatFlagIsPacked, kAudioFormatLinearPCM,
58};
59use objc2_core_foundation::CFString;
60
61use std::path::Path;
62use std::ptr;
63
64pub const FORMAT: &str = "au";
66
67const SCAN_TYPES: &[u32] = &[
71 kAudioUnitType_Effect,
72 kAudioUnitType_MusicDevice,
73 kAudioUnitType_Generator,
74 kAudioUnitType_MusicEffect,
75 kAudioUnitType_MIDIProcessor,
76 kAudioUnitType_Mixer,
77];
78
79#[derive(Debug, Default)]
81pub struct AuScanner;
82
83impl AuScanner {
84 #[must_use]
86 pub fn new() -> Self {
87 Self
88 }
89}
90
91impl PluginScanner for AuScanner {
92 type Plugin = AuPlugin;
93
94 fn scan(&self) -> Result<Vec<PluginInfo>> {
95 let mut out = Vec::new();
96 for &type_code in SCAN_TYPES {
97 unsafe { scan_family(type_code, &mut out) };
98 }
99 Ok(out)
100 }
101
102 fn scan_path(&self, _path: &Path) -> Result<Vec<PluginInfo>> {
103 Err(Error::Other(
105 "truce-rack-au path-bounded scan is not meaningful (AU uses a registry)".into(),
106 ))
107 }
108
109 fn load(&self, info: &PluginInfo) -> Result<Self::Plugin> {
110 AuPlugin::load_from(info)
111 }
112}
113
114unsafe fn scan_family(component_type: u32, out: &mut Vec<PluginInfo>) {
115 let mut desc = AudioComponentDescription {
116 componentType: component_type,
117 componentSubType: 0,
118 componentManufacturer: 0,
119 componentFlags: 0,
120 componentFlagsMask: 0,
121 };
122 let mut component: AudioComponent = ptr::null_mut();
123 let category = type_category(component_type);
124 let accepts_midi =
125 component_type != kAudioUnitType_Effect && component_type != kAudioUnitType_Generator;
126 loop {
127 let next = unsafe {
128 AudioComponentFindNext(component, ptr::NonNull::new_unchecked(&raw mut desc))
129 };
130 if next.is_null() {
131 break;
132 }
133 component = next;
134 let mut comp_desc = AudioComponentDescription {
135 componentType: 0,
136 componentSubType: 0,
137 componentManufacturer: 0,
138 componentFlags: 0,
139 componentFlagsMask: 0,
140 };
141 let _ = unsafe {
142 objc2_audio_toolbox::AudioComponentGetDescription(
143 component,
144 ptr::NonNull::new_unchecked(&raw mut comp_desc),
145 )
146 };
147 if AudioComponentFlags::from_bits_retain(comp_desc.componentFlags)
152 .contains(AudioComponentFlags::IsV3AudioUnit)
153 {
154 continue;
155 }
156 let name = unsafe { component_name(component) };
157 let (vendor, display) = split_name(&name);
158 out.push(PluginInfo {
159 name: display,
160 vendor,
161 version: unsafe { component_version(component) },
162 category,
163 path: std::path::PathBuf::new(),
164 unique_id: format_unique_id(&comp_desc),
165 format: FORMAT,
166 has_editor: false,
167 accepts_midi,
168 });
169 }
170}
171
172unsafe fn fetch_parameter_info(
174 unit: AudioComponentInstance,
175 id: AudioUnitParameterID,
176) -> Result<AudioUnitParameterInfo> {
177 let mut info = AudioUnitParameterInfo {
178 name: [0; 52],
179 unitName: ptr::null(),
180 clumpID: 0,
181 cfNameString: ptr::null(),
182 unit: objc2_audio_toolbox::AudioUnitParameterUnit::Generic,
183 minValue: 0.0,
184 maxValue: 0.0,
185 defaultValue: 0.0,
186 flags: objc2_audio_toolbox::AudioUnitParameterOptions::empty(),
187 };
188 #[allow(clippy::cast_possible_truncation)]
189 let mut size = std::mem::size_of::<AudioUnitParameterInfo>() as u32;
190 let status = unsafe {
191 AudioUnitGetProperty(
192 unit,
193 kAudioUnitProperty_ParameterInfo,
194 kAudioUnitScope_Global,
195 id,
196 ptr::NonNull::new_unchecked((&raw mut info).cast()),
197 ptr::NonNull::new_unchecked(&raw mut size),
198 )
199 };
200 if status != 0 {
201 return Err(Error::Other(format!(
202 "kAudioUnitProperty_ParameterInfo failed: OSStatus {status}"
203 )));
204 }
205 Ok(info)
206}
207
208#[allow(clippy::cast_sign_loss)]
209fn au_parameter_to_rack(id: AudioUnitParameterID, info: &AudioUnitParameterInfo) -> ParameterInfo {
210 let name = if info.cfNameString.is_null() {
214 let bytes: Vec<u8> = info
217 .name
218 .iter()
219 .take_while(|&&c| c != 0)
220 .map(|&c| c as u8)
221 .collect();
222 String::from_utf8_lossy(&bytes).into_owned()
223 } else {
224 let s = unsafe { &*info.cfNameString };
227 s.to_string()
228 };
229 let unit_name = if info.unitName.is_null() {
230 String::new()
231 } else {
232 let s = unsafe { &*info.unitName };
233 s.to_string()
234 };
235 let flags = au_param_flags_to_rack(info);
236 ParameterInfo {
237 id,
238 name: name.clone(),
239 short_name: name,
240 unit: unit_name,
241 min: f64::from(info.minValue),
242 max: f64::from(info.maxValue),
243 default: f64::from(info.defaultValue),
244 step_count: 0,
245 flags,
246 }
247}
248
249fn au_param_flags_to_rack(info: &AudioUnitParameterInfo) -> truce_rack_core::info::ParameterFlags {
250 use objc2_audio_toolbox::AudioUnitParameterOptions as Opt;
251 let mut flags = truce_rack_core::info::ParameterFlags::empty();
252 if info.flags.contains(Opt::Flag_MeterReadOnly) {
255 flags |= truce_rack_core::info::ParameterFlags::READ_ONLY;
256 } else {
257 flags |= truce_rack_core::info::ParameterFlags::AUTOMATABLE;
258 }
259 if info.flags.contains(Opt::Flag_OmitFromPresets) {
260 flags |= truce_rack_core::info::ParameterFlags::HIDDEN;
261 }
262 flags
263}
264
265unsafe fn fetch_factory_presets(unit: AudioComponentInstance) -> Option<Vec<PresetInfo>> {
270 use objc2_core_foundation::{CFArray, CFRetained};
271 let mut presets: *const CFArray = ptr::null();
272 let mut size = u32::try_from(std::mem::size_of::<*const CFArray>()).unwrap_or(0);
273 let status = unsafe {
274 AudioUnitGetProperty(
275 unit,
276 kAudioUnitProperty_FactoryPresets,
277 kAudioUnitScope_Global,
278 0,
279 ptr::NonNull::new_unchecked((&raw mut presets).cast()),
280 ptr::NonNull::new_unchecked(&raw mut size),
281 )
282 };
283 if status != 0 || presets.is_null() {
284 return None;
285 }
286 let array = unsafe { CFRetained::from_raw(ptr::NonNull::new_unchecked(presets.cast_mut())) };
289 let count = array.count();
290 if count <= 0 {
291 return Some(Vec::new());
292 }
293 #[allow(clippy::cast_sign_loss)]
294 let count_usize = count as usize;
295 let mut out = Vec::with_capacity(count_usize);
296 for i in 0..count {
297 let value = unsafe { array.value_at_index(i) };
300 if value.is_null() {
301 continue;
302 }
303 let preset = unsafe { &*value.cast::<AUPreset>() };
304 let name = if preset.presetName.is_null() {
305 format!("Preset {}", preset.presetNumber)
306 } else {
307 unsafe { &*preset.presetName }.to_string()
308 };
309 #[allow(clippy::cast_sign_loss)]
310 out.push(PresetInfo {
311 index: i as usize,
312 name,
313 preset_number: preset.presetNumber,
314 });
315 }
316 Some(out)
317}
318
319fn send_midi(unit: AudioComponentInstance, event: &truce_rack_core::events::Event) {
325 use truce_rack_core::events::{EventBody, MidiData};
326 let offset = event.sample_offset;
327 let (status, d1, d2) = match event.body {
328 EventBody::Midi(MidiData::NoteOn {
329 channel,
330 note,
331 velocity,
332 }) => (
333 0x90 | u32::from(channel & 0x0F),
334 u32::from(note & 0x7F),
335 u32::from(velocity & 0x7F),
336 ),
337 EventBody::Midi(MidiData::NoteOff {
338 channel,
339 note,
340 velocity,
341 }) => (
342 0x80 | u32::from(channel & 0x0F),
343 u32::from(note & 0x7F),
344 u32::from(velocity & 0x7F),
345 ),
346 EventBody::Midi(MidiData::ControlChange {
347 channel,
348 controller,
349 value,
350 }) => (
351 0xB0 | u32::from(channel & 0x0F),
352 u32::from(controller & 0x7F),
353 u32::from(value & 0x7F),
354 ),
355 EventBody::Midi(MidiData::ProgramChange { channel, program }) => (
356 0xC0 | u32::from(channel & 0x0F),
357 u32::from(program & 0x7F),
358 0,
359 ),
360 EventBody::Midi(MidiData::ChannelAftertouch { channel, pressure }) => (
361 0xD0 | u32::from(channel & 0x0F),
362 u32::from(pressure & 0x7F),
363 0,
364 ),
365 EventBody::Midi(MidiData::PolyAftertouch {
366 channel,
367 note,
368 pressure,
369 }) => (
370 0xA0 | u32::from(channel & 0x0F),
371 u32::from(note & 0x7F),
372 u32::from(pressure & 0x7F),
373 ),
374 EventBody::Midi(MidiData::PitchBend { channel, value }) => (
375 0xE0 | u32::from(channel & 0x0F),
376 u32::from(value & 0x7F),
377 u32::from((value >> 7) & 0x7F),
378 ),
379 EventBody::Midi(MidiData::Raw { len, data }) if len >= 1 => {
380 let s = u32::from(data[0]);
381 let d1 = if len >= 2 { u32::from(data[1]) } else { 0 };
382 let d2 = if len >= 3 { u32::from(data[2]) } else { 0 };
383 (s, d1, d2)
384 }
385 _ => return,
386 };
387 let _ = unsafe { MusicDeviceMIDIEvent(unit, status, d1, d2, offset) };
390}
391
392fn instantiate_sync(
395 component: AudioComponent,
396 info_path: &std::path::Path,
397) -> Result<AudioComponentInstance> {
398 let mut instance: AudioComponentInstance = ptr::null_mut();
399 let status = unsafe {
400 AudioComponentInstanceNew(component, ptr::NonNull::new_unchecked(&raw mut instance))
401 };
402 if status != 0 || instance.is_null() {
403 return Err(Error::LoadFailed {
404 path: info_path.to_path_buf(),
405 reason: format!("AudioComponentInstanceNew failed with OSStatus {status}"),
406 });
407 }
408 Ok(instance)
409}
410
411fn instantiate_async(
418 component: AudioComponent,
419 info_path: &std::path::Path,
420) -> Result<AudioComponentInstance> {
421 use std::sync::Arc;
422 use std::sync::atomic::{AtomicBool, AtomicI32, AtomicPtr, Ordering};
423 use std::time::{Duration, Instant};
424
425 let done = Arc::new(AtomicBool::new(false));
426 let inst_ptr: Arc<AtomicPtr<std::ffi::c_void>> = Arc::new(AtomicPtr::new(ptr::null_mut()));
427 let status_atom = Arc::new(AtomicI32::new(0));
428
429 let done_clone = Arc::clone(&done);
430 let inst_clone = Arc::clone(&inst_ptr);
431 let status_clone = Arc::clone(&status_atom);
432
433 let block = block2::RcBlock::new(move |inst: AudioComponentInstance, status: i32| {
439 inst_clone.store(inst.cast(), Ordering::Release);
440 status_clone.store(status, Ordering::Release);
441 done_clone.store(true, Ordering::Release);
442 });
443 unsafe {
444 AudioComponentInstantiate(
445 component,
446 AudioComponentInstantiationOptions::empty(),
447 &block,
448 );
449 }
450
451 let deadline = Instant::now() + Duration::from_secs(10);
455 while !done.load(Ordering::Acquire) {
456 if Instant::now() > deadline {
457 return Err(Error::LoadFailed {
458 path: info_path.to_path_buf(),
459 reason: "AudioComponentInstantiate timed out after 10s".into(),
460 });
461 }
462 let mode = unsafe { objc2_core_foundation::kCFRunLoopDefaultMode };
463 objc2_core_foundation::CFRunLoop::run_in_mode(mode, 0.05, true);
464 }
465
466 let status = status_atom.load(Ordering::Acquire);
467 let raw = inst_ptr.load(Ordering::Acquire);
468 if status != 0 || raw.is_null() {
469 return Err(Error::LoadFailed {
470 path: info_path.to_path_buf(),
471 reason: format!("AudioComponentInstantiate failed with OSStatus {status}"),
472 });
473 }
474 Ok(raw.cast::<objc2_audio_toolbox::OpaqueAudioComponentInstance>())
475}
476
477fn type_category(type_code: u32) -> PluginCategory {
478 match type_code {
479 t if t == kAudioUnitType_MusicDevice => PluginCategory::Instrument,
480 t if t == kAudioUnitType_MIDIProcessor => PluginCategory::NoteEffect,
481 t if t == kAudioUnitType_Mixer => PluginCategory::Tool,
482 _ => PluginCategory::Effect,
483 }
484}
485
486fn split_name(full: &str) -> (String, String) {
489 full.split_once(": ").map_or_else(
490 || (String::new(), full.to_string()),
491 |(v, n)| (v.to_string(), n.to_string()),
492 )
493}
494
495unsafe fn component_name(component: AudioComponent) -> String {
496 let mut cf_str: *const CFString = ptr::null();
497 let status =
498 unsafe { AudioComponentCopyName(component, ptr::NonNull::new_unchecked(&raw mut cf_str)) };
499 if status != 0 || cf_str.is_null() {
500 return String::new();
501 }
502 let retained = unsafe {
506 objc2_core_foundation::CFRetained::from_raw(ptr::NonNull::new_unchecked(cf_str.cast_mut()))
507 };
508 retained.to_string()
509}
510
511unsafe fn component_version(component: AudioComponent) -> u32 {
512 let mut version: u32 = 0;
513 let _ = unsafe {
514 objc2_audio_toolbox::AudioComponentGetVersion(
515 component,
516 ptr::NonNull::new_unchecked(&raw mut version),
517 )
518 };
519 version
520}
521
522fn format_unique_id(desc: &AudioComponentDescription) -> String {
523 format!(
524 "{}:{}:{}",
525 four_cc(desc.componentType),
526 four_cc(desc.componentSubType),
527 four_cc(desc.componentManufacturer),
528 )
529}
530
531fn four_cc(code: u32) -> String {
535 let bytes = code.to_be_bytes();
536 if bytes.iter().all(|b| b.is_ascii_graphic() && *b != b':') {
537 String::from_utf8_lossy(&bytes).into_owned()
538 } else {
539 format!("{code:08x}")
540 }
541}
542
543fn parse_unique_id(id: &str) -> Option<AudioComponentDescription> {
546 let mut parts = id.split(':');
547 let t = parse_four_cc(parts.next()?)?;
548 let s = parse_four_cc(parts.next()?)?;
549 let m = parse_four_cc(parts.next()?)?;
550 Some(AudioComponentDescription {
551 componentType: t,
552 componentSubType: s,
553 componentManufacturer: m,
554 componentFlags: 0,
555 componentFlagsMask: 0,
556 })
557}
558
559fn parse_four_cc(s: &str) -> Option<u32> {
560 if s.len() == 4 && s.bytes().all(|b| b.is_ascii_graphic()) {
561 let b = s.as_bytes();
562 Some(u32::from_be_bytes([b[0], b[1], b[2], b[3]]))
563 } else if s.len() == 8 {
564 u32::from_str_radix(s, 16).ok()
565 } else {
566 None
567 }
568}
569
570pub struct AuPlugin {
573 info: PluginInfo,
574 layouts: Vec<BusLayout>,
575 active_layout: Option<BusLayout>,
576 instance: AudioComponentInstance,
577 param_ids: Vec<AudioUnitParameterID>,
581 render_ctx: Option<Box<RenderContext>>,
586 output_buffer_storage: Vec<u8>,
590 sample_time: f64,
592 host_transport: Option<Box<HostTransport>>,
597 input_channels: u32,
600 output_channels: u32,
601 editor: AuEditorState,
606}
607
608#[derive(Default)]
611struct AuEditorState {
612 factory: Option<objc2::rc::Retained<objc2::runtime::AnyObject>>,
613 view: Option<objc2::rc::Retained<objc2_app_kit::NSView>>,
614}
615
616struct RenderContext {
620 input_planes: Vec<*const f32>,
624 frames: usize,
627}
628
629unsafe extern "C-unwind" fn render_input_callback(
630 in_ref_con: std::ptr::NonNull<std::ffi::c_void>,
631 _io_action_flags: std::ptr::NonNull<AudioUnitRenderActionFlags>,
632 _in_time_stamp: std::ptr::NonNull<AudioTimeStamp>,
633 _in_bus_number: u32,
634 in_number_frames: u32,
635 io_data: *mut AudioBufferList,
636) -> i32 {
637 if io_data.is_null() {
638 return -1;
639 }
640 let ctx_ptr = in_ref_con.as_ptr().cast::<RenderContext>();
641 let ctx = unsafe { &*ctx_ptr };
642 let list = unsafe { &mut *io_data };
643 let frames = in_number_frames as usize;
644 let buffer_count = list.mNumberBuffers as usize;
645 let buffers_ptr: *mut CAAudioBuffer = list.mBuffers.as_mut_ptr();
648 let bytes_u32 = u32::try_from(frames * std::mem::size_of::<f32>()).unwrap_or(0);
649 let bytes_usize = bytes_u32 as usize;
650 for i in 0..buffer_count {
651 let buffer = unsafe { &mut *buffers_ptr.add(i) };
652 buffer.mDataByteSize = bytes_u32;
653 if let Some(&plane) = ctx.input_planes.get(i)
654 && !plane.is_null()
655 && frames <= ctx.frames
656 {
657 buffer.mData = plane.cast::<std::ffi::c_void>().cast_mut();
658 continue;
659 }
660 if !buffer.mData.is_null() {
663 unsafe {
664 std::ptr::write_bytes(buffer.mData.cast::<u8>(), 0, bytes_usize);
665 }
666 }
667 }
668 0
669}
670
671#[derive(Default)]
677#[allow(clippy::struct_excessive_bools)]
678struct HostTransport {
679 valid: bool,
682 tempo: f64,
683 beat: f64,
685 bar_start_beat: f64,
687 time_sig_num: f32,
688 time_sig_den: u32,
689 sample_in_timeline: f64,
690 sample_rate: f64,
691 playing: bool,
692 recording: bool,
693 cycling: bool,
694}
695
696#[allow(clippy::cast_precision_loss)]
701fn update_host_transport(
702 ht: &mut HostTransport,
703 transport: Option<TransportInfo>,
704 sample_rate: f64,
705 sample_time: f64,
706) {
707 let Some(t) = transport else {
708 ht.valid = false;
709 return;
710 };
711 let (num, den) = t.time_signature.unwrap_or((4, 4));
712 ht.valid = true;
713 ht.tempo = t.tempo_bpm.unwrap_or(120.0);
714 ht.beat = t.song_position_beats.unwrap_or(0.0);
715 ht.bar_start_beat = t.bar_start_beats.unwrap_or(0.0);
716 ht.time_sig_num = num as f32;
717 ht.time_sig_den = den;
718 ht.sample_in_timeline = t.song_position_samples.map_or(sample_time, |s| s as f64);
719 ht.sample_rate = sample_rate;
720 ht.playing = t.playing;
721 ht.recording = t.recording;
722 ht.cycling = t.loop_active;
723}
724
725unsafe extern "C-unwind" fn host_beat_and_tempo(
727 in_host_user_data: *mut std::ffi::c_void,
728 out_beat: *mut f64,
729 out_tempo: *mut f64,
730) -> i32 {
731 let t = unsafe { &*in_host_user_data.cast::<HostTransport>() };
732 if !t.valid {
733 return kAudioUnitErr_CannotDoInCurrentContext;
734 }
735 if !out_beat.is_null() {
736 unsafe { *out_beat = t.beat };
737 }
738 if !out_tempo.is_null() {
739 unsafe { *out_tempo = t.tempo };
740 }
741 0
742}
743
744#[allow(
747 clippy::cast_possible_truncation,
748 clippy::cast_sign_loss,
749 clippy::cast_precision_loss
750)]
751unsafe extern "C-unwind" fn host_musical_time(
752 in_host_user_data: *mut std::ffi::c_void,
753 out_delta_to_next_beat: *mut u32,
754 out_time_sig_num: *mut f32,
755 out_time_sig_den: *mut u32,
756 out_measure_downbeat: *mut f64,
757) -> i32 {
758 let t = unsafe { &*in_host_user_data.cast::<HostTransport>() };
759 if !t.valid {
760 return kAudioUnitErr_CannotDoInCurrentContext;
761 }
762 if !out_delta_to_next_beat.is_null() {
763 let frac = t.beat - t.beat.floor();
764 let samples_per_beat = if t.tempo > 0.0 {
765 t.sample_rate * 60.0 / t.tempo
766 } else {
767 0.0
768 };
769 let delta = ((1.0 - frac) * samples_per_beat).round();
770 let delta = if delta.is_finite() && delta >= 0.0 {
771 delta as u32
772 } else {
773 0
774 };
775 unsafe { *out_delta_to_next_beat = delta };
776 }
777 if !out_time_sig_num.is_null() {
778 unsafe { *out_time_sig_num = t.time_sig_num };
779 }
780 if !out_time_sig_den.is_null() {
781 unsafe { *out_time_sig_den = t.time_sig_den };
782 }
783 if !out_measure_downbeat.is_null() {
784 unsafe { *out_measure_downbeat = t.bar_start_beat };
785 }
786 0
787}
788
789unsafe extern "C-unwind" fn host_transport_state(
792 in_host_user_data: *mut std::ffi::c_void,
793 out_is_playing: *mut u8,
794 out_transport_changed: *mut u8,
795 out_sample_in_timeline: *mut f64,
796 out_is_cycling: *mut u8,
797 out_cycle_start: *mut f64,
798 out_cycle_end: *mut f64,
799) -> i32 {
800 let t = unsafe { &*in_host_user_data.cast::<HostTransport>() };
801 if !t.valid {
802 return kAudioUnitErr_CannotDoInCurrentContext;
803 }
804 if !out_is_playing.is_null() {
805 unsafe { *out_is_playing = u8::from(t.playing) };
806 }
807 if !out_transport_changed.is_null() {
808 unsafe { *out_transport_changed = 0 };
809 }
810 if !out_sample_in_timeline.is_null() {
811 unsafe { *out_sample_in_timeline = t.sample_in_timeline };
812 }
813 if !out_is_cycling.is_null() {
814 unsafe { *out_is_cycling = u8::from(t.cycling) };
815 }
816 if !out_cycle_start.is_null() {
817 unsafe { *out_cycle_start = 0.0 };
818 }
819 if !out_cycle_end.is_null() {
820 unsafe { *out_cycle_end = 0.0 };
821 }
822 0
823}
824
825unsafe extern "C-unwind" fn host_transport_state2(
828 in_host_user_data: *mut std::ffi::c_void,
829 out_is_playing: *mut u8,
830 out_is_recording: *mut u8,
831 out_transport_changed: *mut u8,
832 out_sample_in_timeline: *mut f64,
833 out_is_cycling: *mut u8,
834 out_cycle_start: *mut f64,
835 out_cycle_end: *mut f64,
836) -> i32 {
837 let t = unsafe { &*in_host_user_data.cast::<HostTransport>() };
838 if !t.valid {
839 return kAudioUnitErr_CannotDoInCurrentContext;
840 }
841 if !out_is_playing.is_null() {
842 unsafe { *out_is_playing = u8::from(t.playing) };
843 }
844 if !out_is_recording.is_null() {
845 unsafe { *out_is_recording = u8::from(t.recording) };
846 }
847 if !out_transport_changed.is_null() {
848 unsafe { *out_transport_changed = 0 };
849 }
850 if !out_sample_in_timeline.is_null() {
851 unsafe { *out_sample_in_timeline = t.sample_in_timeline };
852 }
853 if !out_is_cycling.is_null() {
854 unsafe { *out_is_cycling = u8::from(t.cycling) };
855 }
856 if !out_cycle_start.is_null() {
857 unsafe { *out_cycle_start = 0.0 };
858 }
859 if !out_cycle_end.is_null() {
860 unsafe { *out_cycle_end = 0.0 };
861 }
862 0
863}
864
865unsafe impl Send for AuPlugin {}
869
870impl AuPlugin {
871 fn load_from(info: &PluginInfo) -> Result<Self> {
872 let mut desc = parse_unique_id(&info.unique_id).ok_or_else(|| Error::LoadFailed {
873 path: info.path.clone(),
874 reason: format!("could not parse AU unique_id {:?}", info.unique_id),
875 })?;
876 let component = unsafe {
877 AudioComponentFindNext(ptr::null_mut(), ptr::NonNull::new_unchecked(&raw mut desc))
878 };
879 if component.is_null() {
880 return Err(Error::LoadFailed {
881 path: info.path.clone(),
882 reason: format!("no AU matching {}", info.unique_id),
883 });
884 }
885
886 let mut comp_desc = AudioComponentDescription {
890 componentType: 0,
891 componentSubType: 0,
892 componentManufacturer: 0,
893 componentFlags: 0,
894 componentFlagsMask: 0,
895 };
896 let _ = unsafe {
897 objc2_audio_toolbox::AudioComponentGetDescription(
898 component,
899 ptr::NonNull::new_unchecked(&raw mut comp_desc),
900 )
901 };
902 let flags = AudioComponentFlags::from_bits_retain(comp_desc.componentFlags);
903 let needs_async = flags.contains(AudioComponentFlags::RequiresAsyncInstantiation);
904
905 let instance = if needs_async {
906 instantiate_async(component, &info.path)?
907 } else {
908 instantiate_sync(component, &info.path)?
909 };
910 let param_ids = unsafe { fetch_parameter_ids(instance) };
911 let has_editor = unsafe { has_cocoa_ui(instance) };
912 let mut updated = info.clone();
913 updated.has_editor = has_editor;
914 Ok(Self {
915 info: updated,
916 layouts: vec![BusLayout::stereo()],
917 active_layout: None,
918 instance,
919 param_ids,
920 render_ctx: None,
921 output_buffer_storage: Vec::new(),
922 sample_time: 0.0,
923 host_transport: None,
924 input_channels: 0,
925 output_channels: 0,
926 editor: AuEditorState::default(),
927 })
928 }
929}
930
931unsafe fn has_cocoa_ui(unit: AudioComponentInstance) -> bool {
934 use objc2_audio_toolbox::kAudioUnitProperty_CocoaUI;
935 let mut size: u32 = 0;
936 let info_status = unsafe {
937 AudioUnitGetPropertyInfo(
938 unit,
939 kAudioUnitProperty_CocoaUI,
940 kAudioUnitScope_Global,
941 0,
942 &raw mut size,
943 ptr::null_mut(),
944 )
945 };
946 info_status == 0 && size as usize >= std::mem::size_of::<usize>() * 2
947}
948
949fn alloc_audio_buffer_list(n_buffers: usize) -> Vec<u8> {
953 let n = n_buffers.max(1);
954 let size =
955 std::mem::size_of::<AudioBufferList>() + (n - 1) * std::mem::size_of::<CAAudioBuffer>();
956 vec![0u8; size]
957}
958
959unsafe fn fetch_parameter_ids(unit: AudioComponentInstance) -> Vec<AudioUnitParameterID> {
963 let mut size: u32 = 0;
964 let info_status = unsafe {
965 AudioUnitGetPropertyInfo(
966 unit,
967 kAudioUnitProperty_ParameterList,
968 kAudioUnitScope_Global,
969 0,
970 &raw mut size,
971 ptr::null_mut(),
972 )
973 };
974 if info_status != 0 || size == 0 {
975 return Vec::new();
976 }
977 let count = (size as usize) / std::mem::size_of::<AudioUnitParameterID>();
978 let mut ids: Vec<AudioUnitParameterID> = vec![0; count];
979 let mut io_size = size;
980 let get_status = unsafe {
981 AudioUnitGetProperty(
982 unit,
983 kAudioUnitProperty_ParameterList,
984 kAudioUnitScope_Global,
985 0,
986 ptr::NonNull::new_unchecked(ids.as_mut_ptr().cast()),
987 ptr::NonNull::new_unchecked(&raw mut io_size),
988 )
989 };
990 if get_status != 0 {
991 return Vec::new();
992 }
993 ids.truncate(io_size as usize / std::mem::size_of::<AudioUnitParameterID>());
994 ids
995}
996
997impl Drop for AuPlugin {
998 fn drop(&mut self) {
999 if !self.instance.is_null() {
1000 unsafe { AudioComponentInstanceDispose(self.instance) };
1001 }
1002 }
1003}
1004
1005impl PluginCore for AuPlugin {
1006 fn info(&self) -> &PluginInfo {
1007 &self.info
1008 }
1009 fn active_layout(&self) -> Option<&BusLayout> {
1010 self.active_layout.as_ref()
1011 }
1012 fn supported_layouts(&self) -> &[BusLayout] {
1013 &self.layouts
1014 }
1015 fn parameter_count(&self) -> usize {
1016 self.param_ids.len()
1017 }
1018
1019 fn parameter_info(&self, index: usize) -> Result<ParameterInfo> {
1020 let id = *self
1021 .param_ids
1022 .get(index)
1023 .ok_or(Error::InvalidParameter(index))?;
1024 let info = unsafe { fetch_parameter_info(self.instance, id)? };
1025 Ok(au_parameter_to_rack(id, &info))
1026 }
1027
1028 fn parameter_value(&self, index: usize) -> Result<f64> {
1029 let id = *self
1030 .param_ids
1031 .get(index)
1032 .ok_or(Error::InvalidParameter(index))?;
1033 let mut value: f32 = 0.0;
1034 let status = unsafe {
1035 AudioUnitGetParameter(
1036 self.instance,
1037 id,
1038 kAudioUnitScope_Global,
1039 0,
1040 ptr::NonNull::new_unchecked(&raw mut value),
1041 )
1042 };
1043 if status != 0 {
1044 return Err(Error::Other(format!(
1045 "AudioUnitGetParameter failed: OSStatus {status}"
1046 )));
1047 }
1048 Ok(f64::from(value))
1049 }
1050
1051 fn parameter_value_string(&self, _index: usize, _value: f64) -> Result<String> {
1052 Err(Error::Other(
1060 "au parameter_value_string not yet wired".into(),
1061 ))
1062 }
1063
1064 fn set_parameter(&mut self, index: usize, value: f64) -> Result<()> {
1065 let id = *self
1066 .param_ids
1067 .get(index)
1068 .ok_or(Error::InvalidParameter(index))?;
1069 #[allow(clippy::cast_possible_truncation)]
1070 let v = value as f32;
1071 let status =
1072 unsafe { AudioUnitSetParameter(self.instance, id, kAudioUnitScope_Global, 0, v, 0) };
1073 if status != 0 {
1074 return Err(Error::Other(format!(
1075 "AudioUnitSetParameter failed: OSStatus {status}"
1076 )));
1077 }
1078 Ok(())
1079 }
1080 fn preset_count(&self) -> usize {
1081 unsafe { fetch_factory_presets(self.instance) }.map_or(0, |v| v.len())
1082 }
1083
1084 fn preset_info(&self, index: usize) -> Result<PresetInfo> {
1085 let presets = unsafe { fetch_factory_presets(self.instance) }
1086 .ok_or_else(|| Error::Other("kAudioUnitProperty_FactoryPresets failed".into()))?;
1087 let preset = presets.get(index).ok_or(Error::InvalidParameter(index))?;
1088 Ok(preset.clone())
1089 }
1090
1091 fn load_preset(&mut self, preset_number: i32) -> Result<()> {
1092 let preset = AUPreset {
1093 presetNumber: preset_number,
1094 presetName: ptr::null(),
1095 };
1096 let status = unsafe {
1097 AudioUnitSetProperty(
1098 self.instance,
1099 kAudioUnitProperty_PresentPreset,
1100 kAudioUnitScope_Global,
1101 0,
1102 (&raw const preset).cast(),
1103 u32::try_from(std::mem::size_of::<AUPreset>()).unwrap_or(0),
1104 )
1105 };
1106 if status != 0 {
1107 return Err(Error::Other(format!(
1108 "AudioUnitSetProperty(PresentPreset) failed: OSStatus {status}"
1109 )));
1110 }
1111 Ok(())
1112 }
1113 fn save_state(&self) -> Result<Vec<u8>> {
1114 use objc2_core_foundation::{
1115 CFData, CFPropertyList, CFPropertyListCreateData, CFPropertyListFormat, CFRetained,
1116 };
1117 let mut class_info: *const CFPropertyList = ptr::null();
1118 let mut size = u32::try_from(std::mem::size_of::<*const CFPropertyList>()).unwrap_or(0);
1119 let status = unsafe {
1120 AudioUnitGetProperty(
1121 self.instance,
1122 kAudioUnitProperty_ClassInfo,
1123 kAudioUnitScope_Global,
1124 0,
1125 ptr::NonNull::new_unchecked((&raw mut class_info).cast()),
1126 ptr::NonNull::new_unchecked(&raw mut size),
1127 )
1128 };
1129 if status != 0 || class_info.is_null() {
1130 return Err(Error::Other(format!(
1131 "kAudioUnitProperty_ClassInfo failed: OSStatus {status}"
1132 )));
1133 }
1134 let plist = unsafe {
1135 CFRetained::<CFPropertyList>::from_raw(ptr::NonNull::new_unchecked(
1136 class_info.cast_mut(),
1137 ))
1138 };
1139 let mut error: *mut objc2_core_foundation::CFError = ptr::null_mut();
1140 let data: Option<CFRetained<CFData>> = unsafe {
1141 CFPropertyListCreateData(
1142 None,
1143 Some(&plist),
1144 CFPropertyListFormat::BinaryFormat_v1_0,
1145 0,
1146 &raw mut error,
1147 )
1148 };
1149 let data =
1150 data.ok_or_else(|| Error::Other("CFPropertyListCreateData returned null".into()))?;
1151 let len = data.length();
1152 if len < 0 {
1153 return Err(Error::Other("CFData length negative".into()));
1154 }
1155 #[allow(clippy::cast_sign_loss)]
1156 let len_usize = len as usize;
1157 let mut out = vec![0u8; len_usize];
1158 unsafe {
1159 data.bytes(
1160 objc2_core_foundation::CFRange {
1161 location: 0,
1162 length: len,
1163 },
1164 out.as_mut_ptr(),
1165 );
1166 }
1167 Ok(out)
1168 }
1169
1170 fn load_state(&mut self, bytes: &[u8]) -> Result<()> {
1171 use objc2_core_foundation::{
1172 CFData, CFPropertyList, CFPropertyListCreateWithData, CFPropertyListFormat, CFRetained,
1173 };
1174 if bytes.is_empty() {
1175 return Err(Error::Other("empty AU state".into()));
1176 }
1177 #[allow(clippy::cast_possible_wrap)]
1178 let cf_data: CFRetained<CFData> =
1179 unsafe { CFData::new(None, bytes.as_ptr(), bytes.len() as isize) }
1180 .ok_or_else(|| Error::Other("CFData::new returned null".into()))?;
1181 let mut error: *mut objc2_core_foundation::CFError = ptr::null_mut();
1182 let mut format = CFPropertyListFormat::BinaryFormat_v1_0;
1183 let plist: Option<CFRetained<CFPropertyList>> = unsafe {
1184 CFPropertyListCreateWithData(None, Some(&cf_data), 0, &raw mut format, &raw mut error)
1185 };
1186 let plist = plist
1187 .ok_or_else(|| Error::Other("CFPropertyListCreateWithData returned null".into()))?;
1188 let plist_ptr: *const CFPropertyList = CFRetained::as_ptr(&plist).as_ptr().cast_const();
1189 let status = unsafe {
1190 AudioUnitSetProperty(
1191 self.instance,
1192 kAudioUnitProperty_ClassInfo,
1193 kAudioUnitScope_Global,
1194 0,
1195 (&raw const plist_ptr).cast(),
1196 u32::try_from(std::mem::size_of::<*const CFPropertyList>()).unwrap_or(0),
1197 )
1198 };
1199 if status != 0 {
1200 return Err(Error::Other(format!(
1201 "AudioUnitSetProperty(ClassInfo) failed: OSStatus {status}"
1202 )));
1203 }
1204 Ok(())
1205 }
1206 fn activate(
1207 &mut self,
1208 layout: BusLayout,
1209 sample_rate: f64,
1210 max_block_size: usize,
1211 ) -> Result<()> {
1212 let channels: u32 = 2;
1220 #[allow(clippy::cast_possible_truncation)]
1221 let format = AudioStreamBasicDescription {
1222 mSampleRate: sample_rate,
1223 mFormatID: kAudioFormatLinearPCM,
1224 mFormatFlags: kAudioFormatFlagIsFloat
1225 | kAudioFormatFlagIsPacked
1226 | kAudioFormatFlagIsNonInterleaved,
1227 mBytesPerPacket: 4,
1228 mFramesPerPacket: 1,
1229 mBytesPerFrame: 4,
1230 mChannelsPerFrame: channels,
1231 mBitsPerChannel: 32,
1232 mReserved: 0,
1233 };
1234 let _ = unsafe {
1235 AudioUnitSetProperty(
1236 self.instance,
1237 kAudioUnitProperty_StreamFormat,
1238 kAudioUnitScope_Input,
1239 0,
1240 (&raw const format).cast(),
1241 u32::try_from(std::mem::size_of::<AudioStreamBasicDescription>()).unwrap_or(0),
1242 )
1243 };
1244 let _ = unsafe {
1245 AudioUnitSetProperty(
1246 self.instance,
1247 kAudioUnitProperty_StreamFormat,
1248 kAudioUnitScope_Output,
1249 0,
1250 (&raw const format).cast(),
1251 u32::try_from(std::mem::size_of::<AudioStreamBasicDescription>()).unwrap_or(0),
1252 )
1253 };
1254 let max_frames = u32::try_from(max_block_size).unwrap_or(u32::MAX);
1255 let _ = unsafe {
1256 AudioUnitSetProperty(
1257 self.instance,
1258 kAudioUnitProperty_MaximumFramesPerSlice,
1259 kAudioUnitScope_Global,
1260 0,
1261 (&raw const max_frames).cast(),
1262 u32::try_from(std::mem::size_of::<u32>()).unwrap_or(0),
1263 )
1264 };
1265 let mut render_ctx = Box::new(RenderContext {
1268 input_planes: vec![ptr::null(); channels as usize],
1269 frames: max_block_size,
1270 });
1271 let render_ctx_ptr: *mut RenderContext = render_ctx.as_mut();
1272 let callback_struct = AURenderCallbackStruct {
1273 inputProc: Some(render_input_callback),
1274 inputProcRefCon: render_ctx_ptr.cast(),
1275 };
1276 let _ = unsafe {
1280 AudioUnitSetProperty(
1281 self.instance,
1282 kAudioUnitProperty_SetRenderCallback,
1283 kAudioUnitScope_Input,
1284 0,
1285 (&raw const callback_struct).cast(),
1286 u32::try_from(std::mem::size_of::<AURenderCallbackStruct>()).unwrap_or(0),
1287 )
1288 };
1289
1290 let mut host_transport = Box::new(HostTransport::default());
1296 let host_transport_ptr: *mut HostTransport = host_transport.as_mut();
1297 let host_callbacks = HostCallbackInfo {
1298 hostUserData: host_transport_ptr.cast(),
1299 beatAndTempoProc: Some(host_beat_and_tempo),
1300 musicalTimeLocationProc: Some(host_musical_time),
1301 transportStateProc: Some(host_transport_state),
1302 transportStateProc2: Some(host_transport_state2),
1303 };
1304 let _ = unsafe {
1305 AudioUnitSetProperty(
1306 self.instance,
1307 kAudioUnitProperty_HostCallbacks,
1308 kAudioUnitScope_Global,
1309 0,
1310 (&raw const host_callbacks).cast(),
1311 u32::try_from(std::mem::size_of::<HostCallbackInfo>()).unwrap_or(0),
1312 )
1313 };
1314
1315 let status = unsafe { AudioUnitInitialize(self.instance) };
1316 if status != 0 {
1317 return Err(Error::Other(format!(
1318 "AudioUnitInitialize failed: OSStatus {status}"
1319 )));
1320 }
1321
1322 self.param_ids = unsafe { fetch_parameter_ids(self.instance) };
1325 self.render_ctx = Some(render_ctx);
1326 self.host_transport = Some(host_transport);
1327 self.output_buffer_storage = alloc_audio_buffer_list(channels as usize);
1328 self.input_channels = channels;
1329 self.output_channels = channels;
1330 self.sample_time = 0.0;
1331 self.active_layout = Some(layout);
1332 Ok(())
1333 }
1334 fn deactivate(&mut self) {
1335 if self.is_active() {
1336 let _ = unsafe { AudioUnitUninitialize(self.instance) };
1337 }
1338 self.render_ctx = None;
1339 self.active_layout = None;
1340 }
1341 fn is_active(&self) -> bool {
1342 self.active_layout.is_some()
1343 }
1344
1345 fn editor(&mut self) -> Option<&mut dyn truce_rack_core::editor::PluginEditor> {
1346 if self.info.has_editor {
1347 Some(self)
1348 } else {
1349 None
1350 }
1351 }
1352}
1353
1354impl truce_rack_core::editor::PluginEditor for AuPlugin {
1355 fn open(&mut self, parent: truce_rack_core::editor::WindowHandle, _scale: f64) -> Result<()> {
1356 use truce_rack_core::editor::WindowHandle;
1357 let WindowHandle::NSView(parent_ptr) = parent else {
1358 return Err(Error::Other("AU editor requires an NSView parent".into()));
1359 };
1360 if parent_ptr.is_null() {
1361 return Err(Error::Other("AU editor: parent NSView is null".into()));
1362 }
1363 if self.editor.view.is_some() {
1364 return Ok(());
1365 }
1366 unsafe { open_cocoa_editor(self, parent_ptr) }
1367 }
1368
1369 fn close(&mut self) {
1370 if let Some(view) = self.editor.view.take() {
1371 unsafe { remove_from_superview(&view) };
1372 }
1373 self.editor.factory = None;
1374 }
1375
1376 fn is_open(&self) -> bool {
1377 self.editor.view.is_some()
1378 }
1379
1380 fn size(&self) -> Option<(u32, u32)> {
1381 let view = self.editor.view.as_ref()?;
1382 let frame = unsafe { view_frame(view) };
1383 #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
1384 Some((frame.2.max(0.0) as u32, frame.3.max(0.0) as u32))
1385 }
1386
1387 fn is_resizable(&self) -> bool {
1388 false
1393 }
1394
1395 fn set_size(&mut self, width: u32, height: u32) -> Option<(u32, u32)> {
1396 let view = self.editor.view.as_ref()?;
1397 unsafe { set_view_frame(view, f64::from(width), f64::from(height)) };
1398 Some((width, height))
1399 }
1400
1401 fn show(&mut self) {
1402 if let Some(view) = self.editor.view.as_ref() {
1403 unsafe { set_hidden(view, false) };
1404 }
1405 }
1406
1407 fn hide(&mut self) {
1408 if let Some(view) = self.editor.view.as_ref() {
1409 unsafe { set_hidden(view, true) };
1410 }
1411 }
1412}
1413
1414type UiViewFn = unsafe extern "C-unwind" fn(
1420 *mut objc2::runtime::AnyObject,
1421 objc2::runtime::Sel,
1422 *mut std::ffi::c_void,
1423 objc2_foundation::NSSize,
1424) -> *mut objc2_app_kit::NSView;
1425
1426unsafe fn open_cocoa_editor(
1431 plugin: &mut AuPlugin,
1432 parent_ptr: *mut std::ffi::c_void,
1433) -> Result<()> {
1434 use objc2::msg_send;
1435 use objc2::runtime::AnyObject;
1436 use objc2_audio_toolbox::{AudioUnitCocoaViewInfo, kAudioUnitProperty_CocoaUI};
1437 use objc2_core_foundation::{CFRetained, CFString, CFURL};
1438
1439 let mut size: u32 = 0;
1440 let info_status = unsafe {
1441 AudioUnitGetPropertyInfo(
1442 plugin.instance,
1443 kAudioUnitProperty_CocoaUI,
1444 kAudioUnitScope_Global,
1445 0,
1446 &raw mut size,
1447 ptr::null_mut(),
1448 )
1449 };
1450 if info_status != 0 || (size as usize) < std::mem::size_of::<AudioUnitCocoaViewInfo>() {
1451 return Err(Error::Other(format!(
1452 "kAudioUnitProperty_CocoaUI not advertised (status {info_status}, size {size})"
1453 )));
1454 }
1455 let mut storage = vec![0u8; size as usize];
1460 let mut io_size = size;
1461 let get_status = unsafe {
1462 AudioUnitGetProperty(
1463 plugin.instance,
1464 kAudioUnitProperty_CocoaUI,
1465 kAudioUnitScope_Global,
1466 0,
1467 ptr::NonNull::new_unchecked(storage.as_mut_ptr().cast()),
1468 ptr::NonNull::new_unchecked(&raw mut io_size),
1469 )
1470 };
1471 if get_status != 0 {
1472 return Err(Error::Other(format!(
1473 "kAudioUnitProperty_CocoaUI get failed: OSStatus {get_status}"
1474 )));
1475 }
1476 #[allow(clippy::cast_ptr_alignment)]
1481 let view_info = unsafe { &*storage.as_ptr().cast::<AudioUnitCocoaViewInfo>() };
1482 let bundle_url = unsafe { CFRetained::<CFURL>::from_raw(view_info.mCocoaAUViewBundleLocation) };
1483 let class_name = unsafe { CFRetained::<CFString>::from_raw(view_info.mCocoaAUViewClass[0]) };
1484
1485 let bundle_url_ns: &objc2_foundation::NSURL = unsafe {
1492 &*CFRetained::as_ptr(&bundle_url)
1493 .as_ptr()
1494 .cast::<objc2_foundation::NSURL>()
1495 };
1496 let class_name_ns: &objc2_foundation::NSString = unsafe {
1497 &*CFRetained::as_ptr(&class_name)
1498 .as_ptr()
1499 .cast::<objc2_foundation::NSString>()
1500 };
1501
1502 let ns_bundle_class = objc2::class!(NSBundle);
1504 let bundle: *mut AnyObject =
1505 unsafe { msg_send![ns_bundle_class, bundleWithURL: bundle_url_ns] };
1506 if bundle.is_null() {
1507 return Err(Error::Other("NSBundle bundleWithURL: returned nil".into()));
1508 }
1509 let view_factory_class: *mut objc2::runtime::AnyClass =
1511 unsafe { msg_send![bundle, classNamed: class_name_ns] };
1512 if view_factory_class.is_null() {
1513 return Err(Error::Other(format!(
1514 "view factory class not found in bundle: {}",
1515 unsafe { (*CFRetained::as_ptr(&class_name).as_ptr()).to_string() }
1516 )));
1517 }
1518 let factory_alloc: *mut AnyObject = unsafe { msg_send![view_factory_class, alloc] };
1520 let factory: *mut AnyObject = unsafe { msg_send![factory_alloc, init] };
1521 if factory.is_null() {
1522 return Err(Error::Other("AU view factory init returned nil".into()));
1523 }
1524 let factory_retained = unsafe { objc2::rc::Retained::from_raw(factory) }
1525 .ok_or_else(|| Error::Other("could not retain AU view factory".into()))?;
1526
1527 let pref = objc2_foundation::NSSize::new(0.0, 0.0);
1530 let sel = objc2::sel!(uiViewForAudioUnit:withSize:);
1538 let factory_class = (*factory_retained).class();
1539 let Some(method) = factory_class.instance_method(sel) else {
1540 return Err(Error::Other(
1541 "AU view factory has no uiViewForAudioUnit:withSize: method".into(),
1542 ));
1543 };
1544 let imp = method.implementation();
1545 let typed: UiViewFn = unsafe { std::mem::transmute(imp) };
1548 let factory_obj: *mut objc2::runtime::AnyObject =
1549 objc2::rc::Retained::as_ptr(&factory_retained).cast_mut();
1550 let view_raw: *mut objc2_app_kit::NSView =
1551 unsafe { typed(factory_obj, sel, plugin.instance.cast(), pref) };
1552 if view_raw.is_null() {
1553 return Err(Error::Other(
1554 "uiViewForAudioUnit:withSize: returned nil".into(),
1555 ));
1556 }
1557 let view = unsafe { objc2::rc::Retained::retain(view_raw) }
1559 .ok_or_else(|| Error::Other("could not retain AU NSView".into()))?;
1560
1561 let parent_view: *mut objc2_app_kit::NSView = parent_ptr.cast();
1563 let _: () = unsafe { msg_send![parent_view, addSubview: &*view] };
1564
1565 plugin.editor.factory = Some(factory_retained);
1566 plugin.editor.view = Some(view);
1567 Ok(())
1568}
1569
1570unsafe fn remove_from_superview(view: &objc2_app_kit::NSView) {
1571 use objc2::msg_send;
1572 let _: () = unsafe { msg_send![view, removeFromSuperview] };
1573}
1574
1575unsafe fn set_hidden(view: &objc2_app_kit::NSView, hidden: bool) {
1576 use objc2::msg_send;
1577 let flag: objc2::runtime::Bool = objc2::runtime::Bool::new(hidden);
1578 let _: () = unsafe { msg_send![view, setHidden: flag] };
1579}
1580
1581unsafe fn view_frame(view: &objc2_app_kit::NSView) -> (f64, f64, f64, f64) {
1582 use objc2::msg_send;
1583 use objc2_foundation::NSRect;
1584
1585 let _: () = unsafe { msg_send![view, layoutSubtreeIfNeeded] };
1590
1591 let r: NSRect = unsafe { msg_send![view, frame] };
1592
1593 let (extent_w, extent_h) = unsafe { subtree_extent(view) };
1600 let w = extent_w.max(r.size.width);
1601 let h = extent_h.max(r.size.height);
1602 (r.origin.x, r.origin.y, w, h)
1603}
1604
1605unsafe fn subtree_extent(view: &objc2_app_kit::NSView) -> (f64, f64) {
1610 use objc2::msg_send;
1611 use objc2_foundation::{NSArray, NSRect};
1612
1613 let subviews: *mut NSArray<objc2_app_kit::NSView> = unsafe { msg_send![view, subviews] };
1614 if subviews.is_null() {
1615 return (0.0, 0.0);
1616 }
1617 let count: usize = unsafe { msg_send![subviews, count] };
1618 let mut max_x = 0.0_f64;
1619 let mut max_y = 0.0_f64;
1620 for i in 0..count {
1621 let sub: *mut objc2_app_kit::NSView = unsafe { msg_send![subviews, objectAtIndex: i] };
1622 if sub.is_null() {
1623 continue;
1624 }
1625 let frame: NSRect = unsafe { msg_send![sub, frame] };
1626 max_x = max_x.max(frame.origin.x + frame.size.width);
1627 max_y = max_y.max(frame.origin.y + frame.size.height);
1628 let (sub_w, sub_h) = unsafe { subtree_extent(&*sub) };
1631 max_x = max_x.max(frame.origin.x + sub_w);
1632 max_y = max_y.max(frame.origin.y + sub_h);
1633 }
1634 (max_x, max_y)
1635}
1636
1637unsafe fn set_view_frame(view: &objc2_app_kit::NSView, w: f64, h: f64) {
1638 use objc2::msg_send;
1639 use objc2_foundation::{NSPoint, NSRect, NSSize};
1640 let r = NSRect::new(NSPoint::new(0.0, 0.0), NSSize::new(w, h));
1641 let _: () = unsafe { msg_send![view, setFrame: r] };
1642}
1643
1644impl Plugin<f32> for AuPlugin {
1645 fn process(
1646 &mut self,
1647 buffer: &mut AudioBuffer<'_, f32>,
1648 events: &EventList,
1649 context: &mut ProcessContext<'_>,
1650 ) -> Result<ProcessStatus> {
1651 if !self.is_active() {
1652 return Err(Error::NotActivated);
1653 }
1654 let frames = buffer.num_frames();
1655 let frames_u32 = u32::try_from(frames).unwrap_or(u32::MAX);
1656
1657 let sample_time = self.sample_time;
1660 if let Some(ht) = self.host_transport.as_deref_mut() {
1661 update_host_transport(ht, context.transport, context.sample_rate, sample_time);
1662 }
1663
1664 for event in events {
1671 send_midi(self.instance, event);
1672 }
1673
1674 let main_inputs = buffer.main_inputs();
1678 if let Some(ctx) = self.render_ctx.as_deref_mut() {
1679 ctx.frames = frames;
1680 ctx.input_planes.clear();
1681 for chan in main_inputs.iter().take(self.input_channels as usize) {
1682 ctx.input_planes.push(chan.as_ptr());
1683 }
1684 while ctx.input_planes.len() < self.input_channels as usize {
1685 ctx.input_planes.push(ptr::null());
1686 }
1687 }
1688
1689 let main_outputs = buffer.main_outputs();
1693 #[allow(clippy::cast_ptr_alignment)]
1698 let list_ptr = self
1699 .output_buffer_storage
1700 .as_mut_ptr()
1701 .cast::<AudioBufferList>();
1702 unsafe {
1703 (*list_ptr).mNumberBuffers = self.output_channels;
1704 let buffers_start: *mut CAAudioBuffer = (*list_ptr).mBuffers.as_mut_ptr();
1705 for ch in 0..self.output_channels as usize {
1706 let buf = &mut *buffers_start.add(ch);
1707 buf.mNumberChannels = 1;
1708 buf.mDataByteSize = u32::try_from(frames * std::mem::size_of::<f32>()).unwrap_or(0);
1709 buf.mData = main_outputs
1710 .get_mut(ch)
1711 .map_or(ptr::null_mut(), |c| c.as_mut_ptr().cast());
1712 }
1713 }
1714
1715 let mut flags = AudioUnitRenderActionFlags::empty();
1716 let timestamp = AudioTimeStamp {
1717 mSampleTime: self.sample_time,
1718 mHostTime: 0,
1719 mRateScalar: 1.0,
1720 mWordClockTime: 0,
1721 mSMPTETime: objc2_core_audio_types::SMPTETime {
1722 mSubframes: 0,
1723 mSubframeDivisor: 0,
1724 mCounter: 0,
1725 mType: objc2_core_audio_types::SMPTETimeType(0),
1726 mFlags: objc2_core_audio_types::SMPTETimeFlags(0),
1727 mHours: 0,
1728 mMinutes: 0,
1729 mSeconds: 0,
1730 mFrames: 0,
1731 },
1732 mFlags: AudioTimeStampFlags::SampleTimeValid,
1733 mReserved: 0,
1734 };
1735 let status = unsafe {
1736 AudioUnitRender(
1737 self.instance,
1738 &raw mut flags,
1739 ptr::NonNull::new_unchecked((&raw const timestamp).cast_mut()),
1740 0, frames_u32,
1742 ptr::NonNull::new_unchecked(list_ptr),
1743 )
1744 };
1745 #[allow(clippy::cast_precision_loss)]
1746 let frames_f = frames as f64;
1747 self.sample_time += frames_f;
1748 if status != 0 {
1749 return Ok(ProcessStatus::Error);
1750 }
1751 Ok(ProcessStatus::Continue)
1752 }
1753}
1754
1755#[cfg(test)]
1756mod tests {
1757 use super::*;
1758
1759 #[test]
1760 fn four_cc_roundtrip() {
1761 let aufx = u32::from_be_bytes(*b"aufx");
1762 assert_eq!(four_cc(aufx), "aufx");
1763 assert_eq!(parse_four_cc("aufx"), Some(aufx));
1764 }
1765
1766 #[test]
1767 fn unique_id_roundtrip() {
1768 let desc = AudioComponentDescription {
1769 componentType: u32::from_be_bytes(*b"aufx"),
1770 componentSubType: u32::from_be_bytes(*b"dely"),
1771 componentManufacturer: u32::from_be_bytes(*b"appl"),
1772 componentFlags: 0,
1773 componentFlagsMask: 0,
1774 };
1775 let id = format_unique_id(&desc);
1776 assert_eq!(id, "aufx:dely:appl");
1777 let parsed = parse_unique_id(&id).unwrap();
1778 assert_eq!(parsed.componentType, desc.componentType);
1779 assert_eq!(parsed.componentSubType, desc.componentSubType);
1780 assert_eq!(parsed.componentManufacturer, desc.componentManufacturer);
1781 }
1782}