#![cfg(target_vendor = "apple")]
use truce_rack_core::error::{Error, Result};
use truce_rack_core::info::PluginInfo;
use truce_rack_core::scanner::PluginScanner;
use objc2_audio_toolbox::{
AudioComponent, AudioComponentDescription, AudioComponentFindNext, AudioComponentFlags,
kAudioUnitType_Effect, kAudioUnitType_Generator, kAudioUnitType_MIDIProcessor,
kAudioUnitType_Mixer, kAudioUnitType_MusicDevice, kAudioUnitType_MusicEffect,
};
use std::path::Path;
use std::ptr;
pub const FORMAT: &str = "au3";
const SCAN_TYPES: &[u32] = &[
kAudioUnitType_Effect,
kAudioUnitType_MusicDevice,
kAudioUnitType_Generator,
kAudioUnitType_MusicEffect,
kAudioUnitType_MIDIProcessor,
kAudioUnitType_Mixer,
];
#[derive(Debug, Default)]
pub struct Au3Scanner;
impl Au3Scanner {
#[must_use]
pub fn new() -> Self {
Self
}
}
impl PluginScanner for Au3Scanner {
type Plugin = truce_rack_au::AuPlugin;
fn scan(&self) -> Result<Vec<PluginInfo>> {
let mut out = Vec::new();
for &type_code in SCAN_TYPES {
unsafe { scan_family_v3(type_code, &mut out) };
}
for info in &mut out {
info.format = FORMAT;
}
Ok(out)
}
fn scan_path(&self, _path: &Path) -> Result<Vec<PluginInfo>> {
Err(Error::Other(
"truce-rack-au3 path-bounded scan is not meaningful (AU uses a registry)".into(),
))
}
fn load(&self, info: &PluginInfo) -> Result<Self::Plugin> {
truce_rack_au::AuScanner::new().load(info)
}
}
unsafe fn scan_family_v3(component_type: u32, out: &mut Vec<PluginInfo>) {
let mut desc = AudioComponentDescription {
componentType: component_type,
componentSubType: 0,
componentManufacturer: 0,
componentFlags: 0,
componentFlagsMask: 0,
};
let mut component: AudioComponent = ptr::null_mut();
loop {
let next = unsafe {
AudioComponentFindNext(component, ptr::NonNull::new_unchecked(&raw mut desc))
};
if next.is_null() {
break;
}
component = next;
let mut comp_desc = AudioComponentDescription {
componentType: 0,
componentSubType: 0,
componentManufacturer: 0,
componentFlags: 0,
componentFlagsMask: 0,
};
let status = unsafe {
objc2_audio_toolbox::AudioComponentGetDescription(
component,
ptr::NonNull::new_unchecked(&raw mut comp_desc),
)
};
if status != 0 {
continue;
}
let flags = AudioComponentFlags::from_bits_truncate(comp_desc.componentFlags);
if !flags.contains(AudioComponentFlags::IsV3AudioUnit) {
continue;
}
out.push(unsafe { component_to_info(component, &comp_desc) });
}
}
unsafe fn component_to_info(
component: AudioComponent,
comp_desc: &AudioComponentDescription,
) -> PluginInfo {
let name = unsafe { component_name(component) };
let (vendor, display) = name.split_once(": ").map_or_else(
|| (String::new(), name.clone()),
|(v, n)| (v.to_string(), n.to_string()),
);
let category = match comp_desc.componentType {
t if t == kAudioUnitType_MusicDevice => truce_rack_core::info::PluginCategory::Instrument,
t if t == kAudioUnitType_MIDIProcessor => truce_rack_core::info::PluginCategory::NoteEffect,
t if t == kAudioUnitType_Mixer => truce_rack_core::info::PluginCategory::Tool,
_ => truce_rack_core::info::PluginCategory::Effect,
};
let accepts_midi = comp_desc.componentType != kAudioUnitType_Effect
&& comp_desc.componentType != kAudioUnitType_Generator;
PluginInfo {
name: display,
vendor,
version: unsafe { component_version(component) },
category,
path: std::path::PathBuf::new(),
unique_id: format!(
"{}:{}:{}",
four_cc(comp_desc.componentType),
four_cc(comp_desc.componentSubType),
four_cc(comp_desc.componentManufacturer),
),
format: FORMAT,
has_editor: false,
accepts_midi,
}
}
unsafe fn component_name(component: AudioComponent) -> String {
use objc2_core_foundation::CFString;
let mut cf_str: *const CFString = ptr::null();
let status = unsafe {
objc2_audio_toolbox::AudioComponentCopyName(
component,
ptr::NonNull::new_unchecked(&raw mut cf_str),
)
};
if status != 0 || cf_str.is_null() {
return String::new();
}
let retained = unsafe {
objc2_core_foundation::CFRetained::from_raw(ptr::NonNull::new_unchecked(cf_str.cast_mut()))
};
retained.to_string()
}
unsafe fn component_version(component: AudioComponent) -> u32 {
let mut version: u32 = 0;
let _ = unsafe {
objc2_audio_toolbox::AudioComponentGetVersion(
component,
ptr::NonNull::new_unchecked(&raw mut version),
)
};
version
}
fn four_cc(code: u32) -> String {
let bytes = code.to_be_bytes();
if bytes.iter().all(|b| b.is_ascii_graphic() && *b != b':') {
String::from_utf8_lossy(&bytes).into_owned()
} else {
format!("{code:08x}")
}
}