1#![cfg(target_vendor = "apple")]
26
27use truce_rack_core::error::{Error, Result};
28use truce_rack_core::info::PluginInfo;
29use truce_rack_core::scanner::PluginScanner;
30
31use objc2_audio_toolbox::{
32 AudioComponent, AudioComponentDescription, AudioComponentFindNext, AudioComponentFlags,
33 kAudioUnitType_Effect, kAudioUnitType_Generator, kAudioUnitType_MIDIProcessor,
34 kAudioUnitType_Mixer, kAudioUnitType_MusicDevice, kAudioUnitType_MusicEffect,
35};
36
37use std::path::Path;
38use std::ptr;
39
40pub const FORMAT: &str = "au3";
42
43const SCAN_TYPES: &[u32] = &[
44 kAudioUnitType_Effect,
45 kAudioUnitType_MusicDevice,
46 kAudioUnitType_Generator,
47 kAudioUnitType_MusicEffect,
48 kAudioUnitType_MIDIProcessor,
49 kAudioUnitType_Mixer,
50];
51
52#[derive(Debug, Default)]
54pub struct Au3Scanner;
55
56impl Au3Scanner {
57 #[must_use]
59 pub fn new() -> Self {
60 Self
61 }
62}
63
64impl PluginScanner for Au3Scanner {
65 type Plugin = truce_rack_au::AuPlugin;
66
67 fn scan(&self) -> Result<Vec<PluginInfo>> {
68 let mut out = Vec::new();
69 for &type_code in SCAN_TYPES {
70 unsafe { scan_family_v3(type_code, &mut out) };
71 }
72 for info in &mut out {
76 info.format = FORMAT;
77 }
78 Ok(out)
79 }
80
81 fn scan_path(&self, _path: &Path) -> Result<Vec<PluginInfo>> {
82 Err(Error::Other(
83 "truce-rack-au3 path-bounded scan is not meaningful (AU uses a registry)".into(),
84 ))
85 }
86
87 fn load(&self, info: &PluginInfo) -> Result<Self::Plugin> {
88 truce_rack_au::AuScanner::new().load(info)
92 }
93}
94
95unsafe fn scan_family_v3(component_type: u32, out: &mut Vec<PluginInfo>) {
96 let mut desc = AudioComponentDescription {
97 componentType: component_type,
98 componentSubType: 0,
99 componentManufacturer: 0,
100 componentFlags: 0,
101 componentFlagsMask: 0,
102 };
103 let mut component: AudioComponent = ptr::null_mut();
104 loop {
105 let next = unsafe {
106 AudioComponentFindNext(component, ptr::NonNull::new_unchecked(&raw mut desc))
107 };
108 if next.is_null() {
109 break;
110 }
111 component = next;
112
113 let mut comp_desc = AudioComponentDescription {
114 componentType: 0,
115 componentSubType: 0,
116 componentManufacturer: 0,
117 componentFlags: 0,
118 componentFlagsMask: 0,
119 };
120 let status = unsafe {
121 objc2_audio_toolbox::AudioComponentGetDescription(
122 component,
123 ptr::NonNull::new_unchecked(&raw mut comp_desc),
124 )
125 };
126 if status != 0 {
127 continue;
128 }
129
130 let flags = AudioComponentFlags::from_bits_truncate(comp_desc.componentFlags);
133 if !flags.contains(AudioComponentFlags::IsV3AudioUnit) {
134 continue;
135 }
136
137 out.push(unsafe { component_to_info(component, &comp_desc) });
142 }
143}
144
145unsafe fn component_to_info(
146 component: AudioComponent,
147 comp_desc: &AudioComponentDescription,
148) -> PluginInfo {
149 let name = unsafe { component_name(component) };
150 let (vendor, display) = name.split_once(": ").map_or_else(
151 || (String::new(), name.clone()),
152 |(v, n)| (v.to_string(), n.to_string()),
153 );
154 let category = match comp_desc.componentType {
155 t if t == kAudioUnitType_MusicDevice => truce_rack_core::info::PluginCategory::Instrument,
156 t if t == kAudioUnitType_MIDIProcessor => truce_rack_core::info::PluginCategory::NoteEffect,
157 t if t == kAudioUnitType_Mixer => truce_rack_core::info::PluginCategory::Tool,
158 _ => truce_rack_core::info::PluginCategory::Effect,
159 };
160 let accepts_midi = comp_desc.componentType != kAudioUnitType_Effect
161 && comp_desc.componentType != kAudioUnitType_Generator;
162 PluginInfo {
163 name: display,
164 vendor,
165 version: unsafe { component_version(component) },
166 category,
167 path: std::path::PathBuf::new(),
168 unique_id: format!(
169 "{}:{}:{}",
170 four_cc(comp_desc.componentType),
171 four_cc(comp_desc.componentSubType),
172 four_cc(comp_desc.componentManufacturer),
173 ),
174 format: FORMAT,
175 has_editor: false,
176 accepts_midi,
177 }
178}
179
180unsafe fn component_name(component: AudioComponent) -> String {
181 use objc2_core_foundation::CFString;
182 let mut cf_str: *const CFString = ptr::null();
183 let status = unsafe {
184 objc2_audio_toolbox::AudioComponentCopyName(
185 component,
186 ptr::NonNull::new_unchecked(&raw mut cf_str),
187 )
188 };
189 if status != 0 || cf_str.is_null() {
190 return String::new();
191 }
192 let retained = unsafe {
193 objc2_core_foundation::CFRetained::from_raw(ptr::NonNull::new_unchecked(cf_str.cast_mut()))
194 };
195 retained.to_string()
196}
197
198unsafe fn component_version(component: AudioComponent) -> u32 {
199 let mut version: u32 = 0;
200 let _ = unsafe {
201 objc2_audio_toolbox::AudioComponentGetVersion(
202 component,
203 ptr::NonNull::new_unchecked(&raw mut version),
204 )
205 };
206 version
207}
208
209fn four_cc(code: u32) -> String {
210 let bytes = code.to_be_bytes();
211 if bytes.iter().all(|b| b.is_ascii_graphic() && *b != b':') {
212 String::from_utf8_lossy(&bytes).into_owned()
213 } else {
214 format!("{code:08x}")
215 }
216}