Skip to main content

truce_rack_au3/
lib.rs

1//! AU v3 (Audio Unit App Extension) host for the truce-rack framework.
2//! macOS / iOS only.
3//!
4//! # How v3 differs from v2
5//!
6//! AU v3 plugins ship as sandboxed App Extensions discovered via
7//! `NSExtension` rather than as dylibs in `/Library/Audio/Plug-Ins/Components`.
8//! The host communicates with the extension over XPC inside the
9//! per-plugin sandbox. From the audio-rendering perspective, once
10//! an `AudioComponentInstance` is in hand the interface is
11//! identical to AU v2 — so truce-rack-au3's scanner filters the same
12//! `AudioComponentFindNext` walk by the
13//! `kAudioComponentFlag_IsV3AudioUnit` flag, and `AuPlugin` from
14//! `truce-rack-au` is re-used to hold the resulting handle.
15//!
16//! # Status
17//!
18//! Scanning is implemented. Loading is forwarded to `truce-rack-au`'s
19//! [`truce_rack_au::AuScanner::load`], which currently scaffolds the
20//! instance but leaves the AU v3 specific async instantiation
21//! (`AudioComponentInstantiate` with a completion block) as a
22//! TODO. v3 plugins flagged `kAudioComponentFlag_RequiresAsyncInstantiation`
23//! will fail synchronous load until that lands.
24
25#![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
40/// Format identifier used on returned [`PluginInfo`].
41pub 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/// AU v3 scanner.
53#[derive(Debug, Default)]
54pub struct Au3Scanner;
55
56impl Au3Scanner {
57    /// Construct a default scanner.
58    #[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        // Re-stamp the format so consumers can tell v2 vs v3 in
73        // their browser even though both paths route through the
74        // same `AuPlugin` type.
75        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        // Once the truce-rack-au loader switches to the async
89        // instantiation path for `RequiresAsyncInstantiation`
90        // components, this re-dispatches without further change.
91        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        // Filter to AU v3 only — v2 components are surfaced by
131        // truce-rack-au's scanner instead.
132        let flags = AudioComponentFlags::from_bits_truncate(comp_desc.componentFlags);
133        if !flags.contains(AudioComponentFlags::IsV3AudioUnit) {
134            continue;
135        }
136
137        // Funnel into truce-rack-au's PluginInfo builder by faking the
138        // walk it does — the only difference is the v3 flag check
139        // above. We rebuild here rather than calling into truce-rack-au's
140        // internals to keep the v3 / v2 paths independent.
141        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}