#![cfg(target_vendor = "apple")]
use truce_rack_core::buffer::AudioBuffer;
use truce_rack_core::bus::BusLayout;
use truce_rack_core::error::{Error, Result};
use truce_rack_core::events::EventList;
use truce_rack_core::info::{ParameterInfo, PluginCategory, PluginInfo, PresetInfo};
use truce_rack_core::plugin::{Plugin, PluginCore, ProcessContext, ProcessStatus};
use truce_rack_core::scanner::PluginScanner;
use truce_rack_core::transport::TransportInfo;
use objc2_audio_toolbox::{
AUPreset, AURenderCallbackStruct, AudioComponent, AudioComponentCopyName,
AudioComponentDescription, AudioComponentFindNext, AudioComponentFlags, AudioComponentInstance,
AudioComponentInstanceDispose, AudioComponentInstanceNew, AudioComponentInstantiate,
AudioComponentInstantiationOptions, AudioUnitGetParameter, AudioUnitGetProperty,
AudioUnitGetPropertyInfo, AudioUnitInitialize, AudioUnitParameterID, AudioUnitParameterInfo,
AudioUnitRender, AudioUnitRenderActionFlags, AudioUnitSetParameter, AudioUnitSetProperty,
AudioUnitUninitialize, HostCallbackInfo, MusicDeviceMIDIEvent,
kAudioUnitErr_CannotDoInCurrentContext, kAudioUnitProperty_ClassInfo,
kAudioUnitProperty_FactoryPresets, kAudioUnitProperty_HostCallbacks,
kAudioUnitProperty_MaximumFramesPerSlice, kAudioUnitProperty_ParameterInfo,
kAudioUnitProperty_ParameterList, kAudioUnitProperty_PresentPreset,
kAudioUnitProperty_SetRenderCallback, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Global,
kAudioUnitScope_Input, kAudioUnitScope_Output, kAudioUnitType_Effect, kAudioUnitType_Generator,
kAudioUnitType_MIDIProcessor, kAudioUnitType_Mixer, kAudioUnitType_MusicDevice,
kAudioUnitType_MusicEffect,
};
use objc2_core_audio_types::{
AudioBuffer as CAAudioBuffer, AudioBufferList, AudioStreamBasicDescription, AudioTimeStamp,
AudioTimeStampFlags, kAudioFormatFlagIsFloat, kAudioFormatFlagIsNonInterleaved,
kAudioFormatFlagIsPacked, kAudioFormatLinearPCM,
};
use objc2_core_foundation::CFString;
use std::path::Path;
use std::ptr;
pub const FORMAT: &str = "au";
const SCAN_TYPES: &[u32] = &[
kAudioUnitType_Effect,
kAudioUnitType_MusicDevice,
kAudioUnitType_Generator,
kAudioUnitType_MusicEffect,
kAudioUnitType_MIDIProcessor,
kAudioUnitType_Mixer,
];
#[derive(Debug, Default)]
pub struct AuScanner;
impl AuScanner {
#[must_use]
pub fn new() -> Self {
Self
}
}
impl PluginScanner for AuScanner {
type Plugin = AuPlugin;
fn scan(&self) -> Result<Vec<PluginInfo>> {
let mut out = Vec::new();
for &type_code in SCAN_TYPES {
unsafe { scan_family(type_code, &mut out) };
}
Ok(out)
}
fn scan_path(&self, _path: &Path) -> Result<Vec<PluginInfo>> {
Err(Error::Other(
"truce-rack-au path-bounded scan is not meaningful (AU uses a registry)".into(),
))
}
fn load(&self, info: &PluginInfo) -> Result<Self::Plugin> {
AuPlugin::load_from(info)
}
}
unsafe fn scan_family(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();
let category = type_category(component_type);
let accepts_midi =
component_type != kAudioUnitType_Effect && component_type != kAudioUnitType_Generator;
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 _ = unsafe {
objc2_audio_toolbox::AudioComponentGetDescription(
component,
ptr::NonNull::new_unchecked(&raw mut comp_desc),
)
};
if AudioComponentFlags::from_bits_retain(comp_desc.componentFlags)
.contains(AudioComponentFlags::IsV3AudioUnit)
{
continue;
}
let name = unsafe { component_name(component) };
let (vendor, display) = split_name(&name);
out.push(PluginInfo {
name: display,
vendor,
version: unsafe { component_version(component) },
category,
path: std::path::PathBuf::new(),
unique_id: format_unique_id(&comp_desc),
format: FORMAT,
has_editor: false,
accepts_midi,
});
}
}
unsafe fn fetch_parameter_info(
unit: AudioComponentInstance,
id: AudioUnitParameterID,
) -> Result<AudioUnitParameterInfo> {
let mut info = AudioUnitParameterInfo {
name: [0; 52],
unitName: ptr::null(),
clumpID: 0,
cfNameString: ptr::null(),
unit: objc2_audio_toolbox::AudioUnitParameterUnit::Generic,
minValue: 0.0,
maxValue: 0.0,
defaultValue: 0.0,
flags: objc2_audio_toolbox::AudioUnitParameterOptions::empty(),
};
#[allow(clippy::cast_possible_truncation)]
let mut size = std::mem::size_of::<AudioUnitParameterInfo>() as u32;
let status = unsafe {
AudioUnitGetProperty(
unit,
kAudioUnitProperty_ParameterInfo,
kAudioUnitScope_Global,
id,
ptr::NonNull::new_unchecked((&raw mut info).cast()),
ptr::NonNull::new_unchecked(&raw mut size),
)
};
if status != 0 {
return Err(Error::Other(format!(
"kAudioUnitProperty_ParameterInfo failed: OSStatus {status}"
)));
}
Ok(info)
}
#[allow(clippy::cast_sign_loss)]
fn au_parameter_to_rack(id: AudioUnitParameterID, info: &AudioUnitParameterInfo) -> ParameterInfo {
let name = if info.cfNameString.is_null() {
let bytes: Vec<u8> = info
.name
.iter()
.take_while(|&&c| c != 0)
.map(|&c| c as u8)
.collect();
String::from_utf8_lossy(&bytes).into_owned()
} else {
let s = unsafe { &*info.cfNameString };
s.to_string()
};
let unit_name = if info.unitName.is_null() {
String::new()
} else {
let s = unsafe { &*info.unitName };
s.to_string()
};
let flags = au_param_flags_to_rack(info);
ParameterInfo {
id,
name: name.clone(),
short_name: name,
unit: unit_name,
min: f64::from(info.minValue),
max: f64::from(info.maxValue),
default: f64::from(info.defaultValue),
step_count: 0,
flags,
}
}
fn au_param_flags_to_rack(info: &AudioUnitParameterInfo) -> truce_rack_core::info::ParameterFlags {
use objc2_audio_toolbox::AudioUnitParameterOptions as Opt;
let mut flags = truce_rack_core::info::ParameterFlags::empty();
if info.flags.contains(Opt::Flag_MeterReadOnly) {
flags |= truce_rack_core::info::ParameterFlags::READ_ONLY;
} else {
flags |= truce_rack_core::info::ParameterFlags::AUTOMATABLE;
}
if info.flags.contains(Opt::Flag_OmitFromPresets) {
flags |= truce_rack_core::info::ParameterFlags::HIDDEN;
}
flags
}
unsafe fn fetch_factory_presets(unit: AudioComponentInstance) -> Option<Vec<PresetInfo>> {
use objc2_core_foundation::{CFArray, CFRetained};
let mut presets: *const CFArray = ptr::null();
let mut size = u32::try_from(std::mem::size_of::<*const CFArray>()).unwrap_or(0);
let status = unsafe {
AudioUnitGetProperty(
unit,
kAudioUnitProperty_FactoryPresets,
kAudioUnitScope_Global,
0,
ptr::NonNull::new_unchecked((&raw mut presets).cast()),
ptr::NonNull::new_unchecked(&raw mut size),
)
};
if status != 0 || presets.is_null() {
return None;
}
let array = unsafe { CFRetained::from_raw(ptr::NonNull::new_unchecked(presets.cast_mut())) };
let count = array.count();
if count <= 0 {
return Some(Vec::new());
}
#[allow(clippy::cast_sign_loss)]
let count_usize = count as usize;
let mut out = Vec::with_capacity(count_usize);
for i in 0..count {
let value = unsafe { array.value_at_index(i) };
if value.is_null() {
continue;
}
let preset = unsafe { &*value.cast::<AUPreset>() };
let name = if preset.presetName.is_null() {
format!("Preset {}", preset.presetNumber)
} else {
unsafe { &*preset.presetName }.to_string()
};
#[allow(clippy::cast_sign_loss)]
out.push(PresetInfo {
index: i as usize,
name,
preset_number: preset.presetNumber,
});
}
Some(out)
}
fn send_midi(unit: AudioComponentInstance, event: &truce_rack_core::events::Event) {
use truce_rack_core::events::{EventBody, MidiData};
let offset = event.sample_offset;
let (status, d1, d2) = match event.body {
EventBody::Midi(MidiData::NoteOn {
channel,
note,
velocity,
}) => (
0x90 | u32::from(channel & 0x0F),
u32::from(note & 0x7F),
u32::from(velocity & 0x7F),
),
EventBody::Midi(MidiData::NoteOff {
channel,
note,
velocity,
}) => (
0x80 | u32::from(channel & 0x0F),
u32::from(note & 0x7F),
u32::from(velocity & 0x7F),
),
EventBody::Midi(MidiData::ControlChange {
channel,
controller,
value,
}) => (
0xB0 | u32::from(channel & 0x0F),
u32::from(controller & 0x7F),
u32::from(value & 0x7F),
),
EventBody::Midi(MidiData::ProgramChange { channel, program }) => (
0xC0 | u32::from(channel & 0x0F),
u32::from(program & 0x7F),
0,
),
EventBody::Midi(MidiData::ChannelAftertouch { channel, pressure }) => (
0xD0 | u32::from(channel & 0x0F),
u32::from(pressure & 0x7F),
0,
),
EventBody::Midi(MidiData::PolyAftertouch {
channel,
note,
pressure,
}) => (
0xA0 | u32::from(channel & 0x0F),
u32::from(note & 0x7F),
u32::from(pressure & 0x7F),
),
EventBody::Midi(MidiData::PitchBend { channel, value }) => (
0xE0 | u32::from(channel & 0x0F),
u32::from(value & 0x7F),
u32::from((value >> 7) & 0x7F),
),
EventBody::Midi(MidiData::Raw { len, data }) if len >= 1 => {
let s = u32::from(data[0]);
let d1 = if len >= 2 { u32::from(data[1]) } else { 0 };
let d2 = if len >= 3 { u32::from(data[2]) } else { 0 };
(s, d1, d2)
}
_ => return,
};
let _ = unsafe { MusicDeviceMIDIEvent(unit, status, d1, d2, offset) };
}
fn instantiate_sync(
component: AudioComponent,
info_path: &std::path::Path,
) -> Result<AudioComponentInstance> {
let mut instance: AudioComponentInstance = ptr::null_mut();
let status = unsafe {
AudioComponentInstanceNew(component, ptr::NonNull::new_unchecked(&raw mut instance))
};
if status != 0 || instance.is_null() {
return Err(Error::LoadFailed {
path: info_path.to_path_buf(),
reason: format!("AudioComponentInstanceNew failed with OSStatus {status}"),
});
}
Ok(instance)
}
fn instantiate_async(
component: AudioComponent,
info_path: &std::path::Path,
) -> Result<AudioComponentInstance> {
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicI32, AtomicPtr, Ordering};
use std::time::{Duration, Instant};
let done = Arc::new(AtomicBool::new(false));
let inst_ptr: Arc<AtomicPtr<std::ffi::c_void>> = Arc::new(AtomicPtr::new(ptr::null_mut()));
let status_atom = Arc::new(AtomicI32::new(0));
let done_clone = Arc::clone(&done);
let inst_clone = Arc::clone(&inst_ptr);
let status_clone = Arc::clone(&status_atom);
let block = block2::RcBlock::new(move |inst: AudioComponentInstance, status: i32| {
inst_clone.store(inst.cast(), Ordering::Release);
status_clone.store(status, Ordering::Release);
done_clone.store(true, Ordering::Release);
});
unsafe {
AudioComponentInstantiate(
component,
AudioComponentInstantiationOptions::empty(),
&block,
);
}
let deadline = Instant::now() + Duration::from_secs(10);
while !done.load(Ordering::Acquire) {
if Instant::now() > deadline {
return Err(Error::LoadFailed {
path: info_path.to_path_buf(),
reason: "AudioComponentInstantiate timed out after 10s".into(),
});
}
let mode = unsafe { objc2_core_foundation::kCFRunLoopDefaultMode };
objc2_core_foundation::CFRunLoop::run_in_mode(mode, 0.05, true);
}
let status = status_atom.load(Ordering::Acquire);
let raw = inst_ptr.load(Ordering::Acquire);
if status != 0 || raw.is_null() {
return Err(Error::LoadFailed {
path: info_path.to_path_buf(),
reason: format!("AudioComponentInstantiate failed with OSStatus {status}"),
});
}
Ok(raw.cast::<objc2_audio_toolbox::OpaqueAudioComponentInstance>())
}
fn type_category(type_code: u32) -> PluginCategory {
match type_code {
t if t == kAudioUnitType_MusicDevice => PluginCategory::Instrument,
t if t == kAudioUnitType_MIDIProcessor => PluginCategory::NoteEffect,
t if t == kAudioUnitType_Mixer => PluginCategory::Tool,
_ => PluginCategory::Effect,
}
}
fn split_name(full: &str) -> (String, String) {
full.split_once(": ").map_or_else(
|| (String::new(), full.to_string()),
|(v, n)| (v.to_string(), n.to_string()),
)
}
unsafe fn component_name(component: AudioComponent) -> String {
let mut cf_str: *const CFString = ptr::null();
let status =
unsafe { 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 format_unique_id(desc: &AudioComponentDescription) -> String {
format!(
"{}:{}:{}",
four_cc(desc.componentType),
four_cc(desc.componentSubType),
four_cc(desc.componentManufacturer),
)
}
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}")
}
}
fn parse_unique_id(id: &str) -> Option<AudioComponentDescription> {
let mut parts = id.split(':');
let t = parse_four_cc(parts.next()?)?;
let s = parse_four_cc(parts.next()?)?;
let m = parse_four_cc(parts.next()?)?;
Some(AudioComponentDescription {
componentType: t,
componentSubType: s,
componentManufacturer: m,
componentFlags: 0,
componentFlagsMask: 0,
})
}
fn parse_four_cc(s: &str) -> Option<u32> {
if s.len() == 4 && s.bytes().all(|b| b.is_ascii_graphic()) {
let b = s.as_bytes();
Some(u32::from_be_bytes([b[0], b[1], b[2], b[3]]))
} else if s.len() == 8 {
u32::from_str_radix(s, 16).ok()
} else {
None
}
}
pub struct AuPlugin {
info: PluginInfo,
layouts: Vec<BusLayout>,
active_layout: Option<BusLayout>,
instance: AudioComponentInstance,
param_ids: Vec<AudioUnitParameterID>,
render_ctx: Option<Box<RenderContext>>,
output_buffer_storage: Vec<u8>,
sample_time: f64,
host_transport: Option<Box<HostTransport>>,
input_channels: u32,
output_channels: u32,
editor: AuEditorState,
}
#[derive(Default)]
struct AuEditorState {
factory: Option<objc2::rc::Retained<objc2::runtime::AnyObject>>,
view: Option<objc2::rc::Retained<objc2_app_kit::NSView>>,
}
struct RenderContext {
input_planes: Vec<*const f32>,
frames: usize,
}
unsafe extern "C-unwind" fn render_input_callback(
in_ref_con: std::ptr::NonNull<std::ffi::c_void>,
_io_action_flags: std::ptr::NonNull<AudioUnitRenderActionFlags>,
_in_time_stamp: std::ptr::NonNull<AudioTimeStamp>,
_in_bus_number: u32,
in_number_frames: u32,
io_data: *mut AudioBufferList,
) -> i32 {
if io_data.is_null() {
return -1;
}
let ctx_ptr = in_ref_con.as_ptr().cast::<RenderContext>();
let ctx = unsafe { &*ctx_ptr };
let list = unsafe { &mut *io_data };
let frames = in_number_frames as usize;
let buffer_count = list.mNumberBuffers as usize;
let buffers_ptr: *mut CAAudioBuffer = list.mBuffers.as_mut_ptr();
let bytes_u32 = u32::try_from(frames * std::mem::size_of::<f32>()).unwrap_or(0);
let bytes_usize = bytes_u32 as usize;
for i in 0..buffer_count {
let buffer = unsafe { &mut *buffers_ptr.add(i) };
buffer.mDataByteSize = bytes_u32;
if let Some(&plane) = ctx.input_planes.get(i)
&& !plane.is_null()
&& frames <= ctx.frames
{
buffer.mData = plane.cast::<std::ffi::c_void>().cast_mut();
continue;
}
if !buffer.mData.is_null() {
unsafe {
std::ptr::write_bytes(buffer.mData.cast::<u8>(), 0, bytes_usize);
}
}
}
0
}
#[derive(Default)]
#[allow(clippy::struct_excessive_bools)]
struct HostTransport {
valid: bool,
tempo: f64,
beat: f64,
bar_start_beat: f64,
time_sig_num: f32,
time_sig_den: u32,
sample_in_timeline: f64,
sample_rate: f64,
playing: bool,
recording: bool,
cycling: bool,
}
#[allow(clippy::cast_precision_loss)]
fn update_host_transport(
ht: &mut HostTransport,
transport: Option<TransportInfo>,
sample_rate: f64,
sample_time: f64,
) {
let Some(t) = transport else {
ht.valid = false;
return;
};
let (num, den) = t.time_signature.unwrap_or((4, 4));
ht.valid = true;
ht.tempo = t.tempo_bpm.unwrap_or(120.0);
ht.beat = t.song_position_beats.unwrap_or(0.0);
ht.bar_start_beat = t.bar_start_beats.unwrap_or(0.0);
ht.time_sig_num = num as f32;
ht.time_sig_den = den;
ht.sample_in_timeline = t.song_position_samples.map_or(sample_time, |s| s as f64);
ht.sample_rate = sample_rate;
ht.playing = t.playing;
ht.recording = t.recording;
ht.cycling = t.loop_active;
}
unsafe extern "C-unwind" fn host_beat_and_tempo(
in_host_user_data: *mut std::ffi::c_void,
out_beat: *mut f64,
out_tempo: *mut f64,
) -> i32 {
let t = unsafe { &*in_host_user_data.cast::<HostTransport>() };
if !t.valid {
return kAudioUnitErr_CannotDoInCurrentContext;
}
if !out_beat.is_null() {
unsafe { *out_beat = t.beat };
}
if !out_tempo.is_null() {
unsafe { *out_tempo = t.tempo };
}
0
}
#[allow(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::cast_precision_loss
)]
unsafe extern "C-unwind" fn host_musical_time(
in_host_user_data: *mut std::ffi::c_void,
out_delta_to_next_beat: *mut u32,
out_time_sig_num: *mut f32,
out_time_sig_den: *mut u32,
out_measure_downbeat: *mut f64,
) -> i32 {
let t = unsafe { &*in_host_user_data.cast::<HostTransport>() };
if !t.valid {
return kAudioUnitErr_CannotDoInCurrentContext;
}
if !out_delta_to_next_beat.is_null() {
let frac = t.beat - t.beat.floor();
let samples_per_beat = if t.tempo > 0.0 {
t.sample_rate * 60.0 / t.tempo
} else {
0.0
};
let delta = ((1.0 - frac) * samples_per_beat).round();
let delta = if delta.is_finite() && delta >= 0.0 {
delta as u32
} else {
0
};
unsafe { *out_delta_to_next_beat = delta };
}
if !out_time_sig_num.is_null() {
unsafe { *out_time_sig_num = t.time_sig_num };
}
if !out_time_sig_den.is_null() {
unsafe { *out_time_sig_den = t.time_sig_den };
}
if !out_measure_downbeat.is_null() {
unsafe { *out_measure_downbeat = t.bar_start_beat };
}
0
}
unsafe extern "C-unwind" fn host_transport_state(
in_host_user_data: *mut std::ffi::c_void,
out_is_playing: *mut u8,
out_transport_changed: *mut u8,
out_sample_in_timeline: *mut f64,
out_is_cycling: *mut u8,
out_cycle_start: *mut f64,
out_cycle_end: *mut f64,
) -> i32 {
let t = unsafe { &*in_host_user_data.cast::<HostTransport>() };
if !t.valid {
return kAudioUnitErr_CannotDoInCurrentContext;
}
if !out_is_playing.is_null() {
unsafe { *out_is_playing = u8::from(t.playing) };
}
if !out_transport_changed.is_null() {
unsafe { *out_transport_changed = 0 };
}
if !out_sample_in_timeline.is_null() {
unsafe { *out_sample_in_timeline = t.sample_in_timeline };
}
if !out_is_cycling.is_null() {
unsafe { *out_is_cycling = u8::from(t.cycling) };
}
if !out_cycle_start.is_null() {
unsafe { *out_cycle_start = 0.0 };
}
if !out_cycle_end.is_null() {
unsafe { *out_cycle_end = 0.0 };
}
0
}
unsafe extern "C-unwind" fn host_transport_state2(
in_host_user_data: *mut std::ffi::c_void,
out_is_playing: *mut u8,
out_is_recording: *mut u8,
out_transport_changed: *mut u8,
out_sample_in_timeline: *mut f64,
out_is_cycling: *mut u8,
out_cycle_start: *mut f64,
out_cycle_end: *mut f64,
) -> i32 {
let t = unsafe { &*in_host_user_data.cast::<HostTransport>() };
if !t.valid {
return kAudioUnitErr_CannotDoInCurrentContext;
}
if !out_is_playing.is_null() {
unsafe { *out_is_playing = u8::from(t.playing) };
}
if !out_is_recording.is_null() {
unsafe { *out_is_recording = u8::from(t.recording) };
}
if !out_transport_changed.is_null() {
unsafe { *out_transport_changed = 0 };
}
if !out_sample_in_timeline.is_null() {
unsafe { *out_sample_in_timeline = t.sample_in_timeline };
}
if !out_is_cycling.is_null() {
unsafe { *out_is_cycling = u8::from(t.cycling) };
}
if !out_cycle_start.is_null() {
unsafe { *out_cycle_start = 0.0 };
}
if !out_cycle_end.is_null() {
unsafe { *out_cycle_end = 0.0 };
}
0
}
unsafe impl Send for AuPlugin {}
impl AuPlugin {
fn load_from(info: &PluginInfo) -> Result<Self> {
let mut desc = parse_unique_id(&info.unique_id).ok_or_else(|| Error::LoadFailed {
path: info.path.clone(),
reason: format!("could not parse AU unique_id {:?}", info.unique_id),
})?;
let component = unsafe {
AudioComponentFindNext(ptr::null_mut(), ptr::NonNull::new_unchecked(&raw mut desc))
};
if component.is_null() {
return Err(Error::LoadFailed {
path: info.path.clone(),
reason: format!("no AU matching {}", info.unique_id),
});
}
let mut comp_desc = AudioComponentDescription {
componentType: 0,
componentSubType: 0,
componentManufacturer: 0,
componentFlags: 0,
componentFlagsMask: 0,
};
let _ = unsafe {
objc2_audio_toolbox::AudioComponentGetDescription(
component,
ptr::NonNull::new_unchecked(&raw mut comp_desc),
)
};
let flags = AudioComponentFlags::from_bits_retain(comp_desc.componentFlags);
let needs_async = flags.contains(AudioComponentFlags::RequiresAsyncInstantiation);
let instance = if needs_async {
instantiate_async(component, &info.path)?
} else {
instantiate_sync(component, &info.path)?
};
let param_ids = unsafe { fetch_parameter_ids(instance) };
let has_editor = unsafe { has_cocoa_ui(instance) };
let mut updated = info.clone();
updated.has_editor = has_editor;
Ok(Self {
info: updated,
layouts: vec![BusLayout::stereo()],
active_layout: None,
instance,
param_ids,
render_ctx: None,
output_buffer_storage: Vec::new(),
sample_time: 0.0,
host_transport: None,
input_channels: 0,
output_channels: 0,
editor: AuEditorState::default(),
})
}
}
unsafe fn has_cocoa_ui(unit: AudioComponentInstance) -> bool {
use objc2_audio_toolbox::kAudioUnitProperty_CocoaUI;
let mut size: u32 = 0;
let info_status = unsafe {
AudioUnitGetPropertyInfo(
unit,
kAudioUnitProperty_CocoaUI,
kAudioUnitScope_Global,
0,
&raw mut size,
ptr::null_mut(),
)
};
info_status == 0 && size as usize >= std::mem::size_of::<usize>() * 2
}
fn alloc_audio_buffer_list(n_buffers: usize) -> Vec<u8> {
let n = n_buffers.max(1);
let size =
std::mem::size_of::<AudioBufferList>() + (n - 1) * std::mem::size_of::<CAAudioBuffer>();
vec![0u8; size]
}
unsafe fn fetch_parameter_ids(unit: AudioComponentInstance) -> Vec<AudioUnitParameterID> {
let mut size: u32 = 0;
let info_status = unsafe {
AudioUnitGetPropertyInfo(
unit,
kAudioUnitProperty_ParameterList,
kAudioUnitScope_Global,
0,
&raw mut size,
ptr::null_mut(),
)
};
if info_status != 0 || size == 0 {
return Vec::new();
}
let count = (size as usize) / std::mem::size_of::<AudioUnitParameterID>();
let mut ids: Vec<AudioUnitParameterID> = vec![0; count];
let mut io_size = size;
let get_status = unsafe {
AudioUnitGetProperty(
unit,
kAudioUnitProperty_ParameterList,
kAudioUnitScope_Global,
0,
ptr::NonNull::new_unchecked(ids.as_mut_ptr().cast()),
ptr::NonNull::new_unchecked(&raw mut io_size),
)
};
if get_status != 0 {
return Vec::new();
}
ids.truncate(io_size as usize / std::mem::size_of::<AudioUnitParameterID>());
ids
}
impl Drop for AuPlugin {
fn drop(&mut self) {
if !self.instance.is_null() {
unsafe { AudioComponentInstanceDispose(self.instance) };
}
}
}
impl PluginCore for AuPlugin {
fn info(&self) -> &PluginInfo {
&self.info
}
fn active_layout(&self) -> Option<&BusLayout> {
self.active_layout.as_ref()
}
fn supported_layouts(&self) -> &[BusLayout] {
&self.layouts
}
fn parameter_count(&self) -> usize {
self.param_ids.len()
}
fn parameter_info(&self, index: usize) -> Result<ParameterInfo> {
let id = *self
.param_ids
.get(index)
.ok_or(Error::InvalidParameter(index))?;
let info = unsafe { fetch_parameter_info(self.instance, id)? };
Ok(au_parameter_to_rack(id, &info))
}
fn parameter_value(&self, index: usize) -> Result<f64> {
let id = *self
.param_ids
.get(index)
.ok_or(Error::InvalidParameter(index))?;
let mut value: f32 = 0.0;
let status = unsafe {
AudioUnitGetParameter(
self.instance,
id,
kAudioUnitScope_Global,
0,
ptr::NonNull::new_unchecked(&raw mut value),
)
};
if status != 0 {
return Err(Error::Other(format!(
"AudioUnitGetParameter failed: OSStatus {status}"
)));
}
Ok(f64::from(value))
}
fn parameter_value_string(&self, _index: usize, _value: f64) -> Result<String> {
Err(Error::Other(
"au parameter_value_string not yet wired".into(),
))
}
fn set_parameter(&mut self, index: usize, value: f64) -> Result<()> {
let id = *self
.param_ids
.get(index)
.ok_or(Error::InvalidParameter(index))?;
#[allow(clippy::cast_possible_truncation)]
let v = value as f32;
let status =
unsafe { AudioUnitSetParameter(self.instance, id, kAudioUnitScope_Global, 0, v, 0) };
if status != 0 {
return Err(Error::Other(format!(
"AudioUnitSetParameter failed: OSStatus {status}"
)));
}
Ok(())
}
fn preset_count(&self) -> usize {
unsafe { fetch_factory_presets(self.instance) }.map_or(0, |v| v.len())
}
fn preset_info(&self, index: usize) -> Result<PresetInfo> {
let presets = unsafe { fetch_factory_presets(self.instance) }
.ok_or_else(|| Error::Other("kAudioUnitProperty_FactoryPresets failed".into()))?;
let preset = presets.get(index).ok_or(Error::InvalidParameter(index))?;
Ok(preset.clone())
}
fn load_preset(&mut self, preset_number: i32) -> Result<()> {
let preset = AUPreset {
presetNumber: preset_number,
presetName: ptr::null(),
};
let status = unsafe {
AudioUnitSetProperty(
self.instance,
kAudioUnitProperty_PresentPreset,
kAudioUnitScope_Global,
0,
(&raw const preset).cast(),
u32::try_from(std::mem::size_of::<AUPreset>()).unwrap_or(0),
)
};
if status != 0 {
return Err(Error::Other(format!(
"AudioUnitSetProperty(PresentPreset) failed: OSStatus {status}"
)));
}
Ok(())
}
fn save_state(&self) -> Result<Vec<u8>> {
use objc2_core_foundation::{
CFData, CFPropertyList, CFPropertyListCreateData, CFPropertyListFormat, CFRetained,
};
let mut class_info: *const CFPropertyList = ptr::null();
let mut size = u32::try_from(std::mem::size_of::<*const CFPropertyList>()).unwrap_or(0);
let status = unsafe {
AudioUnitGetProperty(
self.instance,
kAudioUnitProperty_ClassInfo,
kAudioUnitScope_Global,
0,
ptr::NonNull::new_unchecked((&raw mut class_info).cast()),
ptr::NonNull::new_unchecked(&raw mut size),
)
};
if status != 0 || class_info.is_null() {
return Err(Error::Other(format!(
"kAudioUnitProperty_ClassInfo failed: OSStatus {status}"
)));
}
let plist = unsafe {
CFRetained::<CFPropertyList>::from_raw(ptr::NonNull::new_unchecked(
class_info.cast_mut(),
))
};
let mut error: *mut objc2_core_foundation::CFError = ptr::null_mut();
let data: Option<CFRetained<CFData>> = unsafe {
CFPropertyListCreateData(
None,
Some(&plist),
CFPropertyListFormat::BinaryFormat_v1_0,
0,
&raw mut error,
)
};
let data =
data.ok_or_else(|| Error::Other("CFPropertyListCreateData returned null".into()))?;
let len = data.length();
if len < 0 {
return Err(Error::Other("CFData length negative".into()));
}
#[allow(clippy::cast_sign_loss)]
let len_usize = len as usize;
let mut out = vec![0u8; len_usize];
unsafe {
data.bytes(
objc2_core_foundation::CFRange {
location: 0,
length: len,
},
out.as_mut_ptr(),
);
}
Ok(out)
}
fn load_state(&mut self, bytes: &[u8]) -> Result<()> {
use objc2_core_foundation::{
CFData, CFPropertyList, CFPropertyListCreateWithData, CFPropertyListFormat, CFRetained,
};
if bytes.is_empty() {
return Err(Error::Other("empty AU state".into()));
}
#[allow(clippy::cast_possible_wrap)]
let cf_data: CFRetained<CFData> =
unsafe { CFData::new(None, bytes.as_ptr(), bytes.len() as isize) }
.ok_or_else(|| Error::Other("CFData::new returned null".into()))?;
let mut error: *mut objc2_core_foundation::CFError = ptr::null_mut();
let mut format = CFPropertyListFormat::BinaryFormat_v1_0;
let plist: Option<CFRetained<CFPropertyList>> = unsafe {
CFPropertyListCreateWithData(None, Some(&cf_data), 0, &raw mut format, &raw mut error)
};
let plist = plist
.ok_or_else(|| Error::Other("CFPropertyListCreateWithData returned null".into()))?;
let plist_ptr: *const CFPropertyList = CFRetained::as_ptr(&plist).as_ptr().cast_const();
let status = unsafe {
AudioUnitSetProperty(
self.instance,
kAudioUnitProperty_ClassInfo,
kAudioUnitScope_Global,
0,
(&raw const plist_ptr).cast(),
u32::try_from(std::mem::size_of::<*const CFPropertyList>()).unwrap_or(0),
)
};
if status != 0 {
return Err(Error::Other(format!(
"AudioUnitSetProperty(ClassInfo) failed: OSStatus {status}"
)));
}
Ok(())
}
fn activate(
&mut self,
layout: BusLayout,
sample_rate: f64,
max_block_size: usize,
) -> Result<()> {
let channels: u32 = 2;
#[allow(clippy::cast_possible_truncation)]
let format = AudioStreamBasicDescription {
mSampleRate: sample_rate,
mFormatID: kAudioFormatLinearPCM,
mFormatFlags: kAudioFormatFlagIsFloat
| kAudioFormatFlagIsPacked
| kAudioFormatFlagIsNonInterleaved,
mBytesPerPacket: 4,
mFramesPerPacket: 1,
mBytesPerFrame: 4,
mChannelsPerFrame: channels,
mBitsPerChannel: 32,
mReserved: 0,
};
let _ = unsafe {
AudioUnitSetProperty(
self.instance,
kAudioUnitProperty_StreamFormat,
kAudioUnitScope_Input,
0,
(&raw const format).cast(),
u32::try_from(std::mem::size_of::<AudioStreamBasicDescription>()).unwrap_or(0),
)
};
let _ = unsafe {
AudioUnitSetProperty(
self.instance,
kAudioUnitProperty_StreamFormat,
kAudioUnitScope_Output,
0,
(&raw const format).cast(),
u32::try_from(std::mem::size_of::<AudioStreamBasicDescription>()).unwrap_or(0),
)
};
let max_frames = u32::try_from(max_block_size).unwrap_or(u32::MAX);
let _ = unsafe {
AudioUnitSetProperty(
self.instance,
kAudioUnitProperty_MaximumFramesPerSlice,
kAudioUnitScope_Global,
0,
(&raw const max_frames).cast(),
u32::try_from(std::mem::size_of::<u32>()).unwrap_or(0),
)
};
let mut render_ctx = Box::new(RenderContext {
input_planes: vec![ptr::null(); channels as usize],
frames: max_block_size,
});
let render_ctx_ptr: *mut RenderContext = render_ctx.as_mut();
let callback_struct = AURenderCallbackStruct {
inputProc: Some(render_input_callback),
inputProcRefCon: render_ctx_ptr.cast(),
};
let _ = unsafe {
AudioUnitSetProperty(
self.instance,
kAudioUnitProperty_SetRenderCallback,
kAudioUnitScope_Input,
0,
(&raw const callback_struct).cast(),
u32::try_from(std::mem::size_of::<AURenderCallbackStruct>()).unwrap_or(0),
)
};
let mut host_transport = Box::new(HostTransport::default());
let host_transport_ptr: *mut HostTransport = host_transport.as_mut();
let host_callbacks = HostCallbackInfo {
hostUserData: host_transport_ptr.cast(),
beatAndTempoProc: Some(host_beat_and_tempo),
musicalTimeLocationProc: Some(host_musical_time),
transportStateProc: Some(host_transport_state),
transportStateProc2: Some(host_transport_state2),
};
let _ = unsafe {
AudioUnitSetProperty(
self.instance,
kAudioUnitProperty_HostCallbacks,
kAudioUnitScope_Global,
0,
(&raw const host_callbacks).cast(),
u32::try_from(std::mem::size_of::<HostCallbackInfo>()).unwrap_or(0),
)
};
let status = unsafe { AudioUnitInitialize(self.instance) };
if status != 0 {
return Err(Error::Other(format!(
"AudioUnitInitialize failed: OSStatus {status}"
)));
}
self.param_ids = unsafe { fetch_parameter_ids(self.instance) };
self.render_ctx = Some(render_ctx);
self.host_transport = Some(host_transport);
self.output_buffer_storage = alloc_audio_buffer_list(channels as usize);
self.input_channels = channels;
self.output_channels = channels;
self.sample_time = 0.0;
self.active_layout = Some(layout);
Ok(())
}
fn deactivate(&mut self) {
if self.is_active() {
let _ = unsafe { AudioUnitUninitialize(self.instance) };
}
self.render_ctx = None;
self.active_layout = None;
}
fn is_active(&self) -> bool {
self.active_layout.is_some()
}
fn editor(&mut self) -> Option<&mut dyn truce_rack_core::editor::PluginEditor> {
if self.info.has_editor {
Some(self)
} else {
None
}
}
}
impl truce_rack_core::editor::PluginEditor for AuPlugin {
fn open(&mut self, parent: truce_rack_core::editor::WindowHandle, _scale: f64) -> Result<()> {
use truce_rack_core::editor::WindowHandle;
let WindowHandle::NSView(parent_ptr) = parent else {
return Err(Error::Other("AU editor requires an NSView parent".into()));
};
if parent_ptr.is_null() {
return Err(Error::Other("AU editor: parent NSView is null".into()));
}
if self.editor.view.is_some() {
return Ok(());
}
unsafe { open_cocoa_editor(self, parent_ptr) }
}
fn close(&mut self) {
if let Some(view) = self.editor.view.take() {
unsafe { remove_from_superview(&view) };
}
self.editor.factory = None;
}
fn is_open(&self) -> bool {
self.editor.view.is_some()
}
fn size(&self) -> Option<(u32, u32)> {
let view = self.editor.view.as_ref()?;
let frame = unsafe { view_frame(view) };
#[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
Some((frame.2.max(0.0) as u32, frame.3.max(0.0) as u32))
}
fn is_resizable(&self) -> bool {
false
}
fn set_size(&mut self, width: u32, height: u32) -> Option<(u32, u32)> {
let view = self.editor.view.as_ref()?;
unsafe { set_view_frame(view, f64::from(width), f64::from(height)) };
Some((width, height))
}
fn show(&mut self) {
if let Some(view) = self.editor.view.as_ref() {
unsafe { set_hidden(view, false) };
}
}
fn hide(&mut self) {
if let Some(view) = self.editor.view.as_ref() {
unsafe { set_hidden(view, true) };
}
}
}
type UiViewFn = unsafe extern "C-unwind" fn(
*mut objc2::runtime::AnyObject,
objc2::runtime::Sel,
*mut std::ffi::c_void,
objc2_foundation::NSSize,
) -> *mut objc2_app_kit::NSView;
unsafe fn open_cocoa_editor(
plugin: &mut AuPlugin,
parent_ptr: *mut std::ffi::c_void,
) -> Result<()> {
use objc2::msg_send;
use objc2::runtime::AnyObject;
use objc2_audio_toolbox::{AudioUnitCocoaViewInfo, kAudioUnitProperty_CocoaUI};
use objc2_core_foundation::{CFRetained, CFString, CFURL};
let mut size: u32 = 0;
let info_status = unsafe {
AudioUnitGetPropertyInfo(
plugin.instance,
kAudioUnitProperty_CocoaUI,
kAudioUnitScope_Global,
0,
&raw mut size,
ptr::null_mut(),
)
};
if info_status != 0 || (size as usize) < std::mem::size_of::<AudioUnitCocoaViewInfo>() {
return Err(Error::Other(format!(
"kAudioUnitProperty_CocoaUI not advertised (status {info_status}, size {size})"
)));
}
let mut storage = vec![0u8; size as usize];
let mut io_size = size;
let get_status = unsafe {
AudioUnitGetProperty(
plugin.instance,
kAudioUnitProperty_CocoaUI,
kAudioUnitScope_Global,
0,
ptr::NonNull::new_unchecked(storage.as_mut_ptr().cast()),
ptr::NonNull::new_unchecked(&raw mut io_size),
)
};
if get_status != 0 {
return Err(Error::Other(format!(
"kAudioUnitProperty_CocoaUI get failed: OSStatus {get_status}"
)));
}
#[allow(clippy::cast_ptr_alignment)]
let view_info = unsafe { &*storage.as_ptr().cast::<AudioUnitCocoaViewInfo>() };
let bundle_url = unsafe { CFRetained::<CFURL>::from_raw(view_info.mCocoaAUViewBundleLocation) };
let class_name = unsafe { CFRetained::<CFString>::from_raw(view_info.mCocoaAUViewClass[0]) };
let bundle_url_ns: &objc2_foundation::NSURL = unsafe {
&*CFRetained::as_ptr(&bundle_url)
.as_ptr()
.cast::<objc2_foundation::NSURL>()
};
let class_name_ns: &objc2_foundation::NSString = unsafe {
&*CFRetained::as_ptr(&class_name)
.as_ptr()
.cast::<objc2_foundation::NSString>()
};
let ns_bundle_class = objc2::class!(NSBundle);
let bundle: *mut AnyObject =
unsafe { msg_send![ns_bundle_class, bundleWithURL: bundle_url_ns] };
if bundle.is_null() {
return Err(Error::Other("NSBundle bundleWithURL: returned nil".into()));
}
let view_factory_class: *mut objc2::runtime::AnyClass =
unsafe { msg_send![bundle, classNamed: class_name_ns] };
if view_factory_class.is_null() {
return Err(Error::Other(format!(
"view factory class not found in bundle: {}",
unsafe { (*CFRetained::as_ptr(&class_name).as_ptr()).to_string() }
)));
}
let factory_alloc: *mut AnyObject = unsafe { msg_send![view_factory_class, alloc] };
let factory: *mut AnyObject = unsafe { msg_send![factory_alloc, init] };
if factory.is_null() {
return Err(Error::Other("AU view factory init returned nil".into()));
}
let factory_retained = unsafe { objc2::rc::Retained::from_raw(factory) }
.ok_or_else(|| Error::Other("could not retain AU view factory".into()))?;
let pref = objc2_foundation::NSSize::new(0.0, 0.0);
let sel = objc2::sel!(uiViewForAudioUnit:withSize:);
let factory_class = (*factory_retained).class();
let Some(method) = factory_class.instance_method(sel) else {
return Err(Error::Other(
"AU view factory has no uiViewForAudioUnit:withSize: method".into(),
));
};
let imp = method.implementation();
let typed: UiViewFn = unsafe { std::mem::transmute(imp) };
let factory_obj: *mut objc2::runtime::AnyObject =
objc2::rc::Retained::as_ptr(&factory_retained).cast_mut();
let view_raw: *mut objc2_app_kit::NSView =
unsafe { typed(factory_obj, sel, plugin.instance.cast(), pref) };
if view_raw.is_null() {
return Err(Error::Other(
"uiViewForAudioUnit:withSize: returned nil".into(),
));
}
let view = unsafe { objc2::rc::Retained::retain(view_raw) }
.ok_or_else(|| Error::Other("could not retain AU NSView".into()))?;
let parent_view: *mut objc2_app_kit::NSView = parent_ptr.cast();
let _: () = unsafe { msg_send![parent_view, addSubview: &*view] };
plugin.editor.factory = Some(factory_retained);
plugin.editor.view = Some(view);
Ok(())
}
unsafe fn remove_from_superview(view: &objc2_app_kit::NSView) {
use objc2::msg_send;
let _: () = unsafe { msg_send![view, removeFromSuperview] };
}
unsafe fn set_hidden(view: &objc2_app_kit::NSView, hidden: bool) {
use objc2::msg_send;
let flag: objc2::runtime::Bool = objc2::runtime::Bool::new(hidden);
let _: () = unsafe { msg_send![view, setHidden: flag] };
}
unsafe fn view_frame(view: &objc2_app_kit::NSView) -> (f64, f64, f64, f64) {
use objc2::msg_send;
use objc2_foundation::NSRect;
let _: () = unsafe { msg_send![view, layoutSubtreeIfNeeded] };
let r: NSRect = unsafe { msg_send![view, frame] };
let (extent_w, extent_h) = unsafe { subtree_extent(view) };
let w = extent_w.max(r.size.width);
let h = extent_h.max(r.size.height);
(r.origin.x, r.origin.y, w, h)
}
unsafe fn subtree_extent(view: &objc2_app_kit::NSView) -> (f64, f64) {
use objc2::msg_send;
use objc2_foundation::{NSArray, NSRect};
let subviews: *mut NSArray<objc2_app_kit::NSView> = unsafe { msg_send![view, subviews] };
if subviews.is_null() {
return (0.0, 0.0);
}
let count: usize = unsafe { msg_send![subviews, count] };
let mut max_x = 0.0_f64;
let mut max_y = 0.0_f64;
for i in 0..count {
let sub: *mut objc2_app_kit::NSView = unsafe { msg_send![subviews, objectAtIndex: i] };
if sub.is_null() {
continue;
}
let frame: NSRect = unsafe { msg_send![sub, frame] };
max_x = max_x.max(frame.origin.x + frame.size.width);
max_y = max_y.max(frame.origin.y + frame.size.height);
let (sub_w, sub_h) = unsafe { subtree_extent(&*sub) };
max_x = max_x.max(frame.origin.x + sub_w);
max_y = max_y.max(frame.origin.y + sub_h);
}
(max_x, max_y)
}
unsafe fn set_view_frame(view: &objc2_app_kit::NSView, w: f64, h: f64) {
use objc2::msg_send;
use objc2_foundation::{NSPoint, NSRect, NSSize};
let r = NSRect::new(NSPoint::new(0.0, 0.0), NSSize::new(w, h));
let _: () = unsafe { msg_send![view, setFrame: r] };
}
impl Plugin<f32> for AuPlugin {
fn process(
&mut self,
buffer: &mut AudioBuffer<'_, f32>,
events: &EventList,
context: &mut ProcessContext<'_>,
) -> Result<ProcessStatus> {
if !self.is_active() {
return Err(Error::NotActivated);
}
let frames = buffer.num_frames();
let frames_u32 = u32::try_from(frames).unwrap_or(u32::MAX);
let sample_time = self.sample_time;
if let Some(ht) = self.host_transport.as_deref_mut() {
update_host_transport(ht, context.transport, context.sample_rate, sample_time);
}
for event in events {
send_midi(self.instance, event);
}
let main_inputs = buffer.main_inputs();
if let Some(ctx) = self.render_ctx.as_deref_mut() {
ctx.frames = frames;
ctx.input_planes.clear();
for chan in main_inputs.iter().take(self.input_channels as usize) {
ctx.input_planes.push(chan.as_ptr());
}
while ctx.input_planes.len() < self.input_channels as usize {
ctx.input_planes.push(ptr::null());
}
}
let main_outputs = buffer.main_outputs();
#[allow(clippy::cast_ptr_alignment)]
let list_ptr = self
.output_buffer_storage
.as_mut_ptr()
.cast::<AudioBufferList>();
unsafe {
(*list_ptr).mNumberBuffers = self.output_channels;
let buffers_start: *mut CAAudioBuffer = (*list_ptr).mBuffers.as_mut_ptr();
for ch in 0..self.output_channels as usize {
let buf = &mut *buffers_start.add(ch);
buf.mNumberChannels = 1;
buf.mDataByteSize = u32::try_from(frames * std::mem::size_of::<f32>()).unwrap_or(0);
buf.mData = main_outputs
.get_mut(ch)
.map_or(ptr::null_mut(), |c| c.as_mut_ptr().cast());
}
}
let mut flags = AudioUnitRenderActionFlags::empty();
let timestamp = AudioTimeStamp {
mSampleTime: self.sample_time,
mHostTime: 0,
mRateScalar: 1.0,
mWordClockTime: 0,
mSMPTETime: objc2_core_audio_types::SMPTETime {
mSubframes: 0,
mSubframeDivisor: 0,
mCounter: 0,
mType: objc2_core_audio_types::SMPTETimeType(0),
mFlags: objc2_core_audio_types::SMPTETimeFlags(0),
mHours: 0,
mMinutes: 0,
mSeconds: 0,
mFrames: 0,
},
mFlags: AudioTimeStampFlags::SampleTimeValid,
mReserved: 0,
};
let status = unsafe {
AudioUnitRender(
self.instance,
&raw mut flags,
ptr::NonNull::new_unchecked((&raw const timestamp).cast_mut()),
0, frames_u32,
ptr::NonNull::new_unchecked(list_ptr),
)
};
#[allow(clippy::cast_precision_loss)]
let frames_f = frames as f64;
self.sample_time += frames_f;
if status != 0 {
return Ok(ProcessStatus::Error);
}
Ok(ProcessStatus::Continue)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn four_cc_roundtrip() {
let aufx = u32::from_be_bytes(*b"aufx");
assert_eq!(four_cc(aufx), "aufx");
assert_eq!(parse_four_cc("aufx"), Some(aufx));
}
#[test]
fn unique_id_roundtrip() {
let desc = AudioComponentDescription {
componentType: u32::from_be_bytes(*b"aufx"),
componentSubType: u32::from_be_bytes(*b"dely"),
componentManufacturer: u32::from_be_bytes(*b"appl"),
componentFlags: 0,
componentFlagsMask: 0,
};
let id = format_unique_id(&desc);
assert_eq!(id, "aufx:dely:appl");
let parsed = parse_unique_id(&id).unwrap();
assert_eq!(parsed.componentType, desc.componentType);
assert_eq!(parsed.componentSubType, desc.componentSubType);
assert_eq!(parsed.componentManufacturer, desc.componentManufacturer);
}
}