use crate::{Error, MidiEvent, MidiEventKind, ParameterInfo, PluginInfo, PluginInstance, PresetInfo, Result};
use smallvec::SmallVec;
use std::ffi::CString;
use std::marker::PhantomData;
use std::ptr::NonNull;
use super::ffi;
use super::util::map_error;
pub struct AudioUnitPlugin {
inner: NonNull<ffi::RackAUPlugin>,
info: PluginInfo,
input_ptrs: Vec<*const f32>,
output_ptrs: Vec<*mut f32>,
input_channels: usize,
output_channels: usize,
_not_sync: PhantomData<*const ()>,
}
unsafe impl Send for AudioUnitPlugin {}
impl AudioUnitPlugin {
pub(crate) fn new(info: &PluginInfo) -> Result<Self> {
unsafe {
let unique_id = CString::new(info.unique_id.as_str())
.map_err(|_| Error::Other("Invalid unique_id (contains null byte)".to_string()))?;
let ptr = ffi::rack_au_plugin_new(unique_id.as_ptr());
if ptr.is_null() {
return Err(Error::PluginNotFound(format!(
"Failed to create AudioUnit instance for {}",
info.name
)));
}
Ok(Self {
inner: NonNull::new_unchecked(ptr),
info: info.clone(),
input_ptrs: Vec::new(),
output_ptrs: Vec::new(),
input_channels: 0,
output_channels: 0,
_not_sync: PhantomData,
})
}
}
}
impl PluginInstance for AudioUnitPlugin {
fn initialize(&mut self, sample_rate: f64, max_block_size: usize) -> Result<()> {
unsafe {
let result = ffi::rack_au_plugin_initialize(
self.inner.as_ptr(),
sample_rate,
max_block_size as u32,
);
if result != ffi::RACK_AU_OK {
return Err(map_error(result));
}
let input_channels = ffi::rack_au_plugin_get_input_channels(self.inner.as_ptr());
let output_channels = ffi::rack_au_plugin_get_output_channels(self.inner.as_ptr());
if input_channels < 0 || output_channels < 0 {
return Err(Error::Other("Failed to query channel configuration".to_string()));
}
self.input_channels = input_channels as usize;
self.output_channels = output_channels as usize;
self.input_ptrs = Vec::with_capacity(self.input_channels.max(8));
self.output_ptrs = Vec::with_capacity(self.output_channels.max(8));
self.input_ptrs.resize(self.input_channels, std::ptr::null());
self.output_ptrs.resize(self.output_channels, std::ptr::null_mut());
Ok(())
}
}
fn reset(&mut self) -> Result<()> {
unsafe {
let result = ffi::rack_au_plugin_reset(self.inner.as_ptr());
if result != ffi::RACK_AU_OK {
return Err(map_error(result));
}
Ok(())
}
}
fn process(
&mut self,
inputs: &[&[f32]],
outputs: &mut [&mut [f32]],
num_frames: usize,
) -> Result<()> {
if !self.is_initialized() {
return Err(Error::NotInitialized);
}
if inputs.len() != self.input_channels {
return Err(Error::Other(format!(
"Input channel count mismatch: plugin expects {}, got {}",
self.input_channels, inputs.len()
)));
}
if outputs.len() != self.output_channels {
return Err(Error::Other(format!(
"Output channel count mismatch: plugin expects {}, got {}",
self.output_channels, outputs.len()
)));
}
if inputs.is_empty() || outputs.is_empty() {
return Err(Error::Other("Empty input or output channels".to_string()));
}
for (i, input) in inputs.iter().enumerate() {
if input.len() < num_frames {
return Err(Error::Other(format!(
"Input channel {} has {} samples, need at least {}",
i,
input.len(),
num_frames
)));
}
}
for (i, output) in outputs.iter().enumerate() {
if output.len() < num_frames {
return Err(Error::Other(format!(
"Output channel {} has {} samples, need at least {}",
i,
output.len(),
num_frames
)));
}
}
for (i, input_ch) in inputs.iter().enumerate() {
self.input_ptrs[i] = input_ch.as_ptr();
}
for (i, output_ch) in outputs.iter_mut().enumerate() {
self.output_ptrs[i] = output_ch.as_mut_ptr();
}
unsafe {
let result = ffi::rack_au_plugin_process(
self.inner.as_ptr(),
self.input_ptrs.as_ptr(),
inputs.len() as u32,
self.output_ptrs.as_ptr(),
outputs.len() as u32,
num_frames as u32,
);
if result != ffi::RACK_AU_OK {
return Err(map_error(result));
}
Ok(())
}
}
fn parameter_count(&self) -> usize {
unsafe {
let count = ffi::rack_au_plugin_parameter_count(self.inner.as_ptr());
if count < 0 {
0
} else {
count as usize
}
}
}
fn parameter_info(&self, index: usize) -> Result<ParameterInfo> {
if !self.is_initialized() {
return Err(Error::NotInitialized);
}
unsafe {
let mut name = vec![0i8; 256];
let mut unit = vec![0i8; 32];
let mut min = 0.0f32;
let mut max = 0.0f32;
let mut default_value = 0.0f32;
let result = ffi::rack_au_plugin_parameter_info(
self.inner.as_ptr(),
index as u32,
name.as_mut_ptr(),
name.len(),
&mut min,
&mut max,
&mut default_value,
unit.as_mut_ptr(),
unit.len(),
);
if result != ffi::RACK_AU_OK {
return Err(map_error(result));
}
let name_cstr = std::ffi::CStr::from_ptr(name.as_ptr());
let name_str = name_cstr
.to_str()
.map_err(|e| Error::Other(format!("Invalid UTF-8 in parameter name: {}", e)))?
.to_string();
let unit_cstr = std::ffi::CStr::from_ptr(unit.as_ptr());
let unit_str = unit_cstr
.to_str()
.map_err(|e| Error::Other(format!("Invalid UTF-8 in parameter unit: {}", e)))?
.to_string();
Ok(ParameterInfo {
index,
name: name_str,
min,
max,
default: default_value,
unit: unit_str,
})
}
}
fn get_parameter(&self, index: usize) -> Result<f32> {
if !self.is_initialized() {
return Err(Error::NotInitialized);
}
unsafe {
let mut value = 0.0f32;
let result =
ffi::rack_au_plugin_get_parameter(self.inner.as_ptr(), index as u32, &mut value);
if result != ffi::RACK_AU_OK {
return Err(map_error(result));
}
Ok(value)
}
}
fn set_parameter(&mut self, index: usize, value: f32) -> Result<()> {
if !self.is_initialized() {
return Err(Error::NotInitialized);
}
unsafe {
let result =
ffi::rack_au_plugin_set_parameter(self.inner.as_ptr(), index as u32, value);
if result != ffi::RACK_AU_OK {
return Err(map_error(result));
}
Ok(())
}
}
fn send_midi(&mut self, events: &[MidiEvent]) -> Result<()> {
if !self.is_initialized() {
return Err(Error::NotInitialized);
}
let ffi_events: SmallVec<[ffi::RackAUMidiEvent; 16]> = events
.iter()
.map(|event| {
let (status, data1, data2, channel) = match event.kind {
MidiEventKind::NoteOn { note, velocity, channel } => {
(ffi::RackAUMidiEventType::NoteOn as u8, note, velocity, channel)
}
MidiEventKind::NoteOff { note, velocity, channel } => {
(ffi::RackAUMidiEventType::NoteOff as u8, note, velocity, channel)
}
MidiEventKind::PolyphonicAftertouch { note, pressure, channel } => {
(ffi::RackAUMidiEventType::PolyphonicAftertouch as u8, note, pressure, channel)
}
MidiEventKind::ControlChange { controller, value, channel } => {
(ffi::RackAUMidiEventType::ControlChange as u8, controller, value, channel)
}
MidiEventKind::ProgramChange { program, channel } => {
(ffi::RackAUMidiEventType::ProgramChange as u8, program, 0, channel)
}
MidiEventKind::ChannelAftertouch { pressure, channel } => {
(ffi::RackAUMidiEventType::ChannelAftertouch as u8, pressure, 0, channel)
}
MidiEventKind::PitchBend { value, channel } => {
let lsb = (value & 0x7F) as u8;
let msb = ((value >> 7) & 0x7F) as u8;
(ffi::RackAUMidiEventType::PitchBend as u8, lsb, msb, channel)
}
MidiEventKind::TimingClock => {
(ffi::RackAUMidiEventType::TimingClock as u8, 0, 0, 0)
}
MidiEventKind::Start => {
(ffi::RackAUMidiEventType::Start as u8, 0, 0, 0)
}
MidiEventKind::Continue => {
(ffi::RackAUMidiEventType::Continue as u8, 0, 0, 0)
}
MidiEventKind::Stop => {
(ffi::RackAUMidiEventType::Stop as u8, 0, 0, 0)
}
MidiEventKind::ActiveSensing => {
(ffi::RackAUMidiEventType::ActiveSensing as u8, 0, 0, 0)
}
MidiEventKind::SystemReset => {
(ffi::RackAUMidiEventType::SystemReset as u8, 0, 0, 0)
}
};
ffi::RackAUMidiEvent {
sample_offset: event.sample_offset,
status,
data1,
data2,
channel,
}
})
.collect();
unsafe {
let result = ffi::rack_au_plugin_send_midi(
self.inner.as_ptr(),
ffi_events.as_ptr(),
ffi_events.len() as u32,
);
if result != ffi::RACK_AU_OK {
let err = map_error(result);
if matches!(self.info.plugin_type, crate::PluginType::Effect) {
return Err(Error::Other(format!(
"Effect plugin '{}' does not support MIDI (only instrument plugins typically respond to MIDI)",
self.info.name
)));
}
return Err(err);
}
Ok(())
}
}
fn preset_count(&self) -> Result<usize> {
if !self.is_initialized() {
return Err(Error::NotInitialized);
}
unsafe {
let count = ffi::rack_au_plugin_get_preset_count(self.inner.as_ptr());
if count < 0 {
Ok(0)
} else {
Ok(count as usize)
}
}
}
fn preset_info(&self, index: usize) -> Result<PresetInfo> {
if !self.is_initialized() {
return Err(Error::NotInitialized);
}
unsafe {
let mut name = vec![0i8; 256];
let mut preset_number: i32 = 0;
let result = ffi::rack_au_plugin_get_preset_info(
self.inner.as_ptr(),
index as u32,
name.as_mut_ptr(),
name.len(),
&mut preset_number,
);
if result != ffi::RACK_AU_OK {
return Err(map_error(result));
}
let name_cstr = std::ffi::CStr::from_ptr(name.as_ptr());
let name_str = name_cstr
.to_str()
.map_err(|e| Error::Other(format!("Invalid UTF-8 in preset name: {}", e)))?
.to_string();
Ok(PresetInfo {
index,
name: name_str,
preset_number,
})
}
}
fn load_preset(&mut self, preset_number: i32) -> Result<()> {
if !self.is_initialized() {
return Err(Error::NotInitialized);
}
unsafe {
let result = ffi::rack_au_plugin_load_preset(self.inner.as_ptr(), preset_number);
if result != ffi::RACK_AU_OK {
return Err(map_error(result));
}
Ok(())
}
}
fn get_state(&self) -> Result<Vec<u8>> {
if !self.is_initialized() {
return Err(Error::NotInitialized);
}
unsafe {
let size = ffi::rack_au_plugin_get_state_size(self.inner.as_ptr());
if size <= 0 {
return Err(Error::Other("Failed to get plugin state size".to_string()));
}
let mut data = vec![0u8; size as usize];
let mut actual_size = data.len();
let result = ffi::rack_au_plugin_get_state(
self.inner.as_ptr(),
data.as_mut_ptr(),
&mut actual_size,
);
if result != ffi::RACK_AU_OK {
return Err(map_error(result));
}
data.resize(actual_size, 0);
Ok(data)
}
}
fn set_state(&mut self, data: &[u8]) -> Result<()> {
if !self.is_initialized() {
return Err(Error::NotInitialized);
}
if data.is_empty() {
return Err(Error::Other("State data is empty".to_string()));
}
unsafe {
let result = ffi::rack_au_plugin_set_state(
self.inner.as_ptr(),
data.as_ptr(),
data.len(),
);
if result != ffi::RACK_AU_OK {
return Err(map_error(result));
}
Ok(())
}
}
fn info(&self) -> &PluginInfo {
&self.info
}
fn is_initialized(&self) -> bool {
unsafe {
let result = ffi::rack_au_plugin_is_initialized(self.inner.as_ptr());
result != 0
}
}
}
impl AudioUnitPlugin {
pub fn input_channels(&self) -> usize {
self.input_channels
}
pub fn output_channels(&self) -> usize {
self.output_channels
}
pub fn create_gui<F>(&mut self, callback: F)
where
F: FnOnce(Result<super::gui::AudioUnitGui>) -> Result<()> + Send + 'static,
{
if !self.is_initialized() {
let _ = callback(Err(Error::NotInitialized));
return;
}
let boxed_callback = Box::new(callback);
let user_data = Box::into_raw(boxed_callback) as *mut std::ffi::c_void;
extern "C" fn trampoline<F>(
user_data: *mut std::ffi::c_void,
gui: *mut ffi::RackAUGui,
error_code: std::os::raw::c_int,
) where
F: FnOnce(Result<super::gui::AudioUnitGui>) -> Result<()> + Send + 'static,
{
let callback = unsafe {
Box::from_raw(user_data as *mut F)
};
let result = if gui.is_null() {
Err(map_error(error_code))
} else {
Ok(unsafe { super::gui::AudioUnitGui::from_raw(gui) })
};
let _ = callback(result);
}
unsafe {
ffi::rack_au_gui_create_async(
self.inner.as_ptr(),
trampoline::<F>,
user_data,
);
}
}
}
impl Drop for AudioUnitPlugin {
fn drop(&mut self) {
unsafe {
ffi::rack_au_plugin_free(self.inner.as_ptr());
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{PluginScanner, PluginType};
fn get_test_plugin() -> Option<PluginInfo> {
use super::super::scanner::AudioUnitScanner;
let scanner = AudioUnitScanner::new().ok()?;
let plugins = scanner.scan().ok()?;
plugins
.into_iter()
.find(|p| p.plugin_type == PluginType::Effect || p.plugin_type == PluginType::Instrument)
}
#[test]
fn test_plugin_creation_with_invalid_id() {
use std::path::PathBuf;
let info = PluginInfo::new(
"Fake Plugin".to_string(),
"Fake Vendor".to_string(),
1,
PluginType::Effect,
PathBuf::from("/fake/path"),
"ffffffff-ffffffff-ffffffff".to_string(),
);
let result = AudioUnitPlugin::new(&info);
assert!(result.is_err());
}
#[test]
fn test_plugin_lifecycle() {
let Some(info) = get_test_plugin() else {
println!("No test plugins available, skipping test");
return;
};
println!("Testing with plugin: {}", info.name);
let mut plugin = AudioUnitPlugin::new(&info).expect("Failed to create plugin");
assert!(!plugin.is_initialized(), "Plugin should not be initialized");
plugin
.initialize(48000.0, 512)
.expect("Failed to initialize plugin");
assert!(plugin.is_initialized(), "Plugin should be initialized");
plugin
.initialize(48000.0, 512)
.expect("Re-initialization should succeed");
assert!(plugin.is_initialized());
}
#[test]
fn test_plugin_info() {
let Some(info) = get_test_plugin() else {
println!("No test plugins available, skipping test");
return;
};
let plugin = AudioUnitPlugin::new(&info).expect("Failed to create plugin");
let retrieved_info = plugin.info();
assert_eq!(retrieved_info.name, info.name);
assert_eq!(retrieved_info.unique_id, info.unique_id);
}
#[test]
fn test_drop_cleanup() {
let Some(info) = get_test_plugin() else {
println!("No test plugins available, skipping test");
return;
};
{
let _plugin = AudioUnitPlugin::new(&info).expect("Failed to create plugin");
}
}
#[test]
fn test_audio_processing() {
use super::super::scanner::AudioUnitScanner;
let scanner = AudioUnitScanner::new().expect("Failed to create scanner");
let plugins = scanner.scan().expect("Failed to scan plugins");
let Some(info) = plugins
.into_iter()
.find(|p| p.plugin_type == PluginType::Effect)
else {
println!("No effect plugins available, skipping test");
return;
};
println!("Testing audio processing with: {}", info.name);
let mut plugin = AudioUnitPlugin::new(&info).expect("Failed to create plugin");
plugin
.initialize(48000.0, 512)
.expect("Failed to initialize plugin");
let frames = 512;
let mut left_in = vec![0.0f32; frames];
let mut right_in = vec![0.0f32; frames];
let mut left_out = vec![0.0f32; frames];
let mut right_out = vec![0.0f32; frames];
let frequency = 440.0f32;
let sample_rate = 48000.0f32;
for i in 0..frames {
let sample = (2.0 * std::f32::consts::PI * frequency * i as f32 / sample_rate).sin() * 0.5;
left_in[i] = sample; right_in[i] = sample; }
plugin
.process(
&[&left_in, &right_in],
&mut [&mut left_out, &mut right_out],
frames
)
.expect("Audio processing failed");
let has_signal = left_out.iter().chain(right_out.iter()).any(|&sample| sample != 0.0);
assert!(
has_signal,
"Output should contain audio signal (not all zeros)"
);
println!("✓ Audio processing succeeded, output contains signal");
}
#[test]
fn test_parameter_count() {
let Some(info) = get_test_plugin() else {
println!("No test plugins available, skipping test");
return;
};
println!("Testing parameter count with: {}", info.name);
let mut plugin = AudioUnitPlugin::new(&info).expect("Failed to create plugin");
plugin
.initialize(48000.0, 512)
.expect("Failed to initialize plugin");
let count = plugin.parameter_count();
println!(" Found {} parameters", count);
}
#[test]
fn test_parameter_info() {
let Some(info) = get_test_plugin() else {
println!("No test plugins available, skipping test");
return;
};
println!("Testing parameter info with: {}", info.name);
let mut plugin = AudioUnitPlugin::new(&info).expect("Failed to create plugin");
plugin
.initialize(48000.0, 512)
.expect("Failed to initialize plugin");
let count = plugin.parameter_count();
if count == 0 {
println!(" Plugin has no parameters, skipping test");
return;
}
let param_info = plugin
.parameter_info(0)
.expect("Failed to get parameter info");
println!(" Parameter 0: {}", param_info.name);
println!(" Range: {} - {}", param_info.min, param_info.max);
println!(" Default: {}", param_info.default);
assert!(!param_info.name.is_empty(), "Parameter name should not be empty");
assert!(param_info.index == 0);
}
#[test]
fn test_get_set_parameter() {
let Some(info) = get_test_plugin() else {
println!("No test plugins available, skipping test");
return;
};
println!("Testing get/set parameter with: {}", info.name);
let mut plugin = AudioUnitPlugin::new(&info).expect("Failed to create plugin");
plugin
.initialize(48000.0, 512)
.expect("Failed to initialize plugin");
let count = plugin.parameter_count();
if count == 0 {
println!(" Plugin has no parameters, skipping test");
return;
}
let original_value = plugin
.get_parameter(0)
.expect("Failed to get parameter");
println!(" Original value: {}", original_value);
plugin
.set_parameter(0, 0.75)
.expect("Failed to set parameter");
let new_value = plugin
.get_parameter(0)
.expect("Failed to get parameter after set");
println!(" New value: {}", new_value);
assert!(
(new_value - 0.75).abs() < 0.01,
"Parameter value should be ~0.75, got {}",
new_value
);
plugin
.set_parameter(0, original_value)
.expect("Failed to restore parameter");
}
#[test]
fn test_parameter_range_clamping() {
let Some(info) = get_test_plugin() else {
println!("No test plugins available, skipping test");
return;
};
let mut plugin = AudioUnitPlugin::new(&info).expect("Failed to create plugin");
plugin
.initialize(48000.0, 512)
.expect("Failed to initialize plugin");
let count = plugin.parameter_count();
if count == 0 {
return;
}
plugin.set_parameter(0, 2.0).expect("Should handle > 1.0");
let value = plugin.get_parameter(0).expect("Failed to get parameter");
assert!(
value <= 1.0,
"Parameter should be clamped to <= 1.0, got {}",
value
);
plugin.set_parameter(0, -1.0).expect("Should handle < 0.0");
let value = plugin.get_parameter(0).expect("Failed to get parameter");
assert!(
value >= 0.0,
"Parameter should be clamped to >= 0.0, got {}",
value
);
}
#[test]
fn test_parameter_out_of_bounds() {
let Some(info) = get_test_plugin() else {
println!("No test plugins available, skipping test");
return;
};
let mut plugin = AudioUnitPlugin::new(&info).expect("Failed to create plugin");
plugin
.initialize(48000.0, 512)
.expect("Failed to initialize plugin");
let count = plugin.parameter_count();
let result = plugin.get_parameter(count + 10);
assert!(result.is_err(), "Should fail for out-of-bounds index");
let result = plugin.set_parameter(count + 10, 0.5);
assert!(result.is_err(), "Should fail for out-of-bounds index");
let result = plugin.parameter_info(count + 10);
assert!(result.is_err(), "Should fail for out-of-bounds index");
}
#[test]
fn test_parameter_unit_strings() {
let Some(info) = get_test_plugin() else {
println!("No test plugins available, skipping test");
return;
};
let mut plugin = AudioUnitPlugin::new(&info).expect("Failed to create plugin");
plugin
.initialize(48000.0, 512)
.expect("Failed to initialize plugin");
let count = plugin.parameter_count();
if count == 0 {
println!("Plugin has no parameters, skipping test");
return;
}
for i in 0..count {
let param = plugin.parameter_info(i).expect("Failed to get parameter info");
println!("Parameter {} unit: '{}'", i, param.unit);
}
}
#[test]
fn test_parameter_operations_before_init() {
let Some(info) = get_test_plugin() else {
println!("No test plugins available, skipping test");
return;
};
let plugin = AudioUnitPlugin::new(&info).expect("Failed to create plugin");
let count = plugin.parameter_count();
assert_eq!(count, 0, "Parameter count should be 0 before initialization");
let result = plugin.parameter_info(0);
assert!(result.is_err(), "parameter_info should fail before init");
let result = plugin.get_parameter(0);
assert!(result.is_err(), "get_parameter should fail before init");
}
#[test]
fn test_parameter_value_round_trip() {
let Some(info) = get_test_plugin() else {
println!("No test plugins available, skipping test");
return;
};
let mut plugin = AudioUnitPlugin::new(&info).expect("Failed to create plugin");
plugin
.initialize(48000.0, 512)
.expect("Failed to initialize plugin");
let count = plugin.parameter_count();
if count == 0 {
return;
}
let test_values = vec![0.0, 0.1, 0.25, 0.5, 0.75, 0.9, 1.0];
for &test_value in &test_values {
plugin.set_parameter(0, test_value).expect("Failed to set parameter");
let read_value = plugin.get_parameter(0).expect("Failed to get parameter");
let diff = (read_value - test_value).abs();
assert!(
diff < 0.01,
"Value round-trip failed: set {}, got {} (diff: {})",
test_value,
read_value,
diff
);
}
}
#[test]
fn test_parameter_extreme_values() {
let Some(info) = get_test_plugin() else {
println!("No test plugins available, skipping test");
return;
};
let mut plugin = AudioUnitPlugin::new(&info).expect("Failed to create plugin");
plugin
.initialize(48000.0, 512)
.expect("Failed to initialize plugin");
let count = plugin.parameter_count();
if count == 0 {
return;
}
plugin.set_parameter(0, -1.0).expect("Should handle negative values");
let value = plugin.get_parameter(0).expect("Failed to get parameter");
assert!(value >= 0.0 && value <= 1.0, "Value should be clamped to 0.0-1.0");
plugin.set_parameter(0, 2.0).expect("Should handle > 1.0 values");
let value = plugin.get_parameter(0).expect("Failed to get parameter");
assert!(value >= 0.0 && value <= 1.0, "Value should be clamped to 0.0-1.0");
}
fn get_instrument_plugin() -> Option<PluginInfo> {
use super::super::scanner::AudioUnitScanner;
let scanner = AudioUnitScanner::new().ok()?;
let plugins = scanner.scan().ok()?;
plugins
.into_iter()
.find(|p| p.plugin_type == PluginType::Instrument)
}
#[test]
fn test_send_midi_note_on_off() {
let Some(info) = get_instrument_plugin() else {
println!("No instrument plugins available, skipping test");
return;
};
println!("Testing MIDI with instrument: {}", info.name);
let mut plugin = AudioUnitPlugin::new(&info).expect("Failed to create plugin");
plugin
.initialize(48000.0, 512)
.expect("Failed to initialize plugin");
let events = vec![
MidiEvent::note_on(60, 100, 0, 0), MidiEvent::note_on(64, 100, 0, 0), MidiEvent::note_on(67, 100, 0, 0), ];
let result = plugin.send_midi(&events);
assert!(result.is_ok(), "Failed to send MIDI events");
println!("✓ MIDI note on events sent successfully");
let left_in = vec![0.0f32; 512];
let right_in = vec![0.0f32; 512];
let mut left_out = vec![0.0f32; 512];
let mut right_out = vec![0.0f32; 512];
let result = plugin.process(
&[&left_in, &right_in],
&mut [&mut left_out, &mut right_out],
512
);
assert!(result.is_ok(), "Failed to process audio after MIDI");
println!("✓ Audio processed after MIDI events");
let events = vec![
MidiEvent::note_off(60, 64, 0, 0),
MidiEvent::note_off(64, 64, 0, 0),
MidiEvent::note_off(67, 64, 0, 0),
];
let result = plugin.send_midi(&events);
assert!(result.is_ok(), "Failed to send MIDI note off events");
println!("✓ MIDI note off events sent successfully");
}
#[test]
fn test_send_midi_control_change() {
let Some(info) = get_instrument_plugin() else {
println!("No instrument plugins available, skipping test");
return;
};
let mut plugin = AudioUnitPlugin::new(&info).expect("Failed to create plugin");
plugin
.initialize(48000.0, 512)
.expect("Failed to initialize plugin");
let events = vec![MidiEvent::control_change(1, 64, 0, 0)];
let result = plugin.send_midi(&events);
assert!(result.is_ok(), "Failed to send MIDI control change");
println!("✓ MIDI control change sent successfully");
}
#[test]
fn test_send_midi_program_change() {
let Some(info) = get_instrument_plugin() else {
println!("No instrument plugins available, skipping test");
return;
};
let mut plugin = AudioUnitPlugin::new(&info).expect("Failed to create plugin");
plugin
.initialize(48000.0, 512)
.expect("Failed to initialize plugin");
let events = vec![MidiEvent::program_change(5, 0, 0)];
let result = plugin.send_midi(&events);
assert!(result.is_ok(), "Failed to send MIDI program change");
println!("✓ MIDI program change sent successfully");
}
#[test]
fn test_send_midi_empty_events() {
let Some(info) = get_instrument_plugin() else {
println!("No instrument plugins available, skipping test");
return;
};
let mut plugin = AudioUnitPlugin::new(&info).expect("Failed to create plugin");
plugin
.initialize(48000.0, 512)
.expect("Failed to initialize plugin");
let events: Vec<MidiEvent> = vec![];
let result = plugin.send_midi(&events);
assert!(result.is_ok(), "Empty MIDI array should succeed");
println!("✓ Empty MIDI event array handled correctly");
}
#[test]
fn test_send_midi_before_init() {
let Some(info) = get_instrument_plugin() else {
println!("No instrument plugins available, skipping test");
return;
};
let mut plugin = AudioUnitPlugin::new(&info).expect("Failed to create plugin");
let events = vec![MidiEvent::note_on(60, 100, 0, 0)];
let result = plugin.send_midi(&events);
assert!(result.is_err(), "send_midi should fail before initialization");
assert!(matches!(result, Err(Error::NotInitialized)));
println!("✓ send_midi correctly fails before initialization");
}
#[test]
fn test_send_midi_multi_channel() {
let Some(info) = get_instrument_plugin() else {
println!("No instrument plugins available, skipping test");
return;
};
let mut plugin = AudioUnitPlugin::new(&info).expect("Failed to create plugin");
plugin
.initialize(48000.0, 512)
.expect("Failed to initialize plugin");
let events = vec![
MidiEvent::note_on(60, 100, 0, 0), MidiEvent::note_on(64, 100, 5, 0), MidiEvent::note_on(67, 100, 10, 0), MidiEvent::note_on(72, 100, 15, 0), ];
let result = plugin.send_midi(&events);
assert!(result.is_ok(), "Failed to send multi-channel MIDI: {:?}", result.err());
let left_in = vec![0.0f32; 512];
let right_in = vec![0.0f32; 512];
let mut left_out = vec![0.0f32; 512];
let mut right_out = vec![0.0f32; 512];
plugin
.process(
&[&left_in, &right_in],
&mut [&mut left_out, &mut right_out],
512
)
.expect("Failed to process audio");
let has_output = left_out.iter().chain(right_out.iter()).any(|&sample| sample.abs() > 1e-6);
assert!(has_output, "Expected audio output from multi-channel MIDI notes");
println!("✓ Multi-channel MIDI events sent successfully");
}
#[test]
fn test_midi_with_effect_plugin() {
use super::super::scanner::AudioUnitScanner;
let scanner = AudioUnitScanner::new().expect("Failed to create scanner");
let plugins = scanner.scan().expect("Failed to scan plugins");
let Some(info) = plugins
.into_iter()
.find(|p| p.plugin_type == PluginType::Effect)
else {
println!("No effect plugins available, skipping test");
return;
};
let mut plugin = AudioUnitPlugin::new(&info).expect("Failed to create plugin");
plugin
.initialize(48000.0, 512)
.expect("Failed to initialize plugin");
let events = vec![MidiEvent::note_on(60, 100, 0, 0)];
let result = plugin.send_midi(&events);
match result {
Ok(_) => println!("Effect plugin accepted MIDI (may be ignored)"),
Err(_) => println!("✓ Effect plugin rejected MIDI as expected"),
}
}
#[test]
fn test_midi_value_clamping() {
let event = MidiEvent::note_on(200, 200, 20, 0);
match event.kind {
MidiEventKind::NoteOn { note, velocity, channel } => {
assert_eq!(note, 127, "Note should be clamped to 127");
assert_eq!(velocity, 127, "Velocity should be clamped to 127");
assert_eq!(channel, 15, "Channel should be clamped to 15");
}
_ => panic!("Expected NoteOn event"),
}
println!("✓ MIDI value clamping works correctly");
}
#[test]
fn test_midi_sample_offset() {
let Some(info) = get_instrument_plugin() else {
println!("No instrument plugins available, skipping test");
return;
};
let mut plugin = AudioUnitPlugin::new(&info).expect("Failed to create plugin");
plugin
.initialize(48000.0, 512)
.expect("Failed to initialize plugin");
let events = vec![
MidiEvent::note_on(60, 100, 0, 0),
MidiEvent::note_on(64, 100, 0, 128),
MidiEvent::note_on(67, 100, 0, 256),
];
let result = plugin.send_midi(&events);
assert!(result.is_ok(), "Failed to send MIDI events with sample offsets");
println!("✓ MIDI events with sample offsets sent successfully");
}
#[test]
fn test_preset_count() {
let Some(info) = get_test_plugin() else {
println!("No test plugins available, skipping test");
return;
};
println!("Testing preset count with: {}", info.name);
let mut plugin = AudioUnitPlugin::new(&info).expect("Failed to create plugin");
plugin
.initialize(48000.0, 512)
.expect("Failed to initialize plugin");
let count = plugin.preset_count().expect("Failed to get preset count");
println!(" Found {} presets", count);
}
#[test]
fn test_preset_info() {
let Some(info) = get_test_plugin() else {
println!("No test plugins available, skipping test");
return;
};
println!("Testing preset info with: {}", info.name);
let mut plugin = AudioUnitPlugin::new(&info).expect("Failed to create plugin");
plugin
.initialize(48000.0, 512)
.expect("Failed to initialize plugin");
let count = plugin.preset_count().expect("Failed to get preset count");
if count == 0 {
println!(" Plugin has no presets, skipping test");
return;
}
let preset_info = plugin
.preset_info(0)
.expect("Failed to get preset info");
println!(" Preset 0: {}", preset_info.name);
println!(" Preset number: {}", preset_info.preset_number);
assert!(!preset_info.name.is_empty(), "Preset name should not be empty");
assert_eq!(preset_info.index, 0);
}
#[test]
fn test_load_preset() {
let Some(info) = get_test_plugin() else {
println!("No test plugins available, skipping test");
return;
};
println!("Testing load preset with: {}", info.name);
let mut plugin = AudioUnitPlugin::new(&info).expect("Failed to create plugin");
plugin
.initialize(48000.0, 512)
.expect("Failed to initialize plugin");
let count = plugin.preset_count().expect("Failed to get preset count");
if count == 0 {
println!(" Plugin has no presets, skipping test");
return;
}
let preset = plugin.preset_info(0).expect("Failed to get preset info");
plugin
.load_preset(preset.preset_number)
.expect("Failed to load preset");
println!(" ✓ Loaded preset: {}", preset.name);
}
#[test]
fn test_state_round_trip() {
let Some(info) = get_test_plugin() else {
println!("No test plugins available, skipping test");
return;
};
println!("Testing state round-trip with: {}", info.name);
let mut plugin = AudioUnitPlugin::new(&info).expect("Failed to create plugin");
plugin
.initialize(48000.0, 512)
.expect("Failed to initialize plugin");
let original_state = plugin.get_state().expect("Failed to get state");
println!(" Original state size: {} bytes", original_state.len());
let param_count = plugin.parameter_count();
let original_param_value = if param_count > 0 {
let value = plugin.get_parameter(0).expect("Failed to get parameter");
plugin
.set_parameter(0, 0.99)
.expect("Failed to set parameter");
let new_value = plugin.get_parameter(0).expect("Failed to get parameter");
println!(" Modified parameter 0: {} -> {}", value, new_value);
Some(value)
} else {
None
};
plugin
.set_state(&original_state)
.expect("Failed to restore state");
println!(" ✓ State restored");
if let Some(expected_value) = original_param_value {
let restored_value = plugin.get_parameter(0).expect("Failed to get parameter");
assert!(
(restored_value - expected_value).abs() < 0.01,
"Parameter not restored correctly (expected {}, got {})",
expected_value,
restored_value
);
println!(" ✓ Parameter restored to: {}", restored_value);
}
}
#[test]
fn test_preset_out_of_bounds() {
let Some(info) = get_test_plugin() else {
println!("No test plugins available, skipping test");
return;
};
let mut plugin = AudioUnitPlugin::new(&info).expect("Failed to create plugin");
plugin
.initialize(48000.0, 512)
.expect("Failed to initialize plugin");
let count = plugin.preset_count().expect("Failed to get preset count");
if count == 0 {
println!("Plugin has no presets, skipping test");
return;
}
let result = plugin.preset_info(count + 10);
assert!(result.is_err(), "Should fail for out-of-bounds index");
println!("✓ Out-of-bounds preset index rejected");
}
#[test]
fn test_preset_before_init() {
let Some(info) = get_test_plugin() else {
println!("No test plugins available, skipping test");
return;
};
let plugin = AudioUnitPlugin::new(&info).expect("Failed to create plugin");
let result = plugin.preset_count();
assert!(result.is_err(), "preset_count should fail before init");
let result = plugin.preset_info(0);
assert!(result.is_err(), "preset_info should fail before init");
println!("✓ Preset operations correctly fail before initialization");
}
#[test]
fn test_state_empty_data() {
let Some(info) = get_test_plugin() else {
println!("No test plugins available, skipping test");
return;
};
let mut plugin = AudioUnitPlugin::new(&info).expect("Failed to create plugin");
plugin
.initialize(48000.0, 512)
.expect("Failed to initialize plugin");
let result = plugin.set_state(&[]);
assert!(result.is_err(), "Should reject empty state data");
println!("✓ Empty state data rejected");
}
#[test]
fn test_state_before_init() {
let Some(info) = get_test_plugin() else {
println!("No test plugins available, skipping test");
return;
};
let plugin = AudioUnitPlugin::new(&info).expect("Failed to create plugin");
let result = plugin.get_state();
assert!(result.is_err(), "get_state should fail before init");
println!("✓ State operations correctly fail before initialization");
}
#[test]
fn test_multiple_presets() {
let Some(info) = get_test_plugin() else {
println!("No test plugins available, skipping test");
return;
};
let mut plugin = AudioUnitPlugin::new(&info).expect("Failed to create plugin");
plugin
.initialize(48000.0, 512)
.expect("Failed to initialize plugin");
let count = plugin.preset_count().expect("Failed to get preset count");
if count < 2 {
println!("Plugin has {} presets, skipping multi-preset test", count);
return;
}
println!("Testing with {} presets", count);
for i in 0..count.min(3) {
let preset = plugin.preset_info(i).expect("Failed to get preset info");
plugin
.load_preset(preset.preset_number)
.expect("Failed to load preset");
println!(" ✓ Loaded preset {}: {}", i, preset.name);
}
}
#[test]
fn test_channel_count_queries() {
let Some(info) = get_test_plugin() else {
println!("No test plugins available, skipping test");
return;
};
let mut plugin = AudioUnitPlugin::new(&info).expect("Failed to create plugin");
assert_eq!(plugin.input_channels(), 0, "Input channels should be 0 before init");
assert_eq!(plugin.output_channels(), 0, "Output channels should be 0 before init");
plugin
.initialize(48000.0, 512)
.expect("Failed to initialize plugin");
let input_ch = plugin.input_channels();
let output_ch = plugin.output_channels();
assert!(input_ch > 0, "Input channels should be > 0 after init");
assert!(output_ch > 0, "Output channels should be > 0 after init");
println!("Plugin configured with {} input, {} output channels", input_ch, output_ch);
println!("✓ Channel count queries work correctly");
}
#[test]
fn test_process_with_wrong_channel_count() {
let Some(info) = get_test_plugin() else {
println!("No test plugins available, skipping test");
return;
};
let mut plugin = AudioUnitPlugin::new(&info).expect("Failed to create plugin");
plugin
.initialize(48000.0, 512)
.expect("Failed to initialize plugin");
let input_ch = plugin.input_channels();
let output_ch = plugin.output_channels();
let left_in = vec![0.0f32; 512];
let mut left_out = vec![0.0f32; 512];
let result = plugin.process(
&[&left_in], &mut [&mut left_out], 512
);
if input_ch != 1 || output_ch != 1 {
assert!(result.is_err(), "process() should fail with wrong channel count");
println!("✓ Correctly rejected wrong channel count (1 vs {}/{})", input_ch, output_ch);
} else {
println!("✓ Plugin is mono, 1 channel succeeded");
}
}
#[test]
fn test_process_with_correct_channel_count() {
let Some(info) = get_test_plugin() else {
println!("No test plugins available, skipping test");
return;
};
let mut plugin = AudioUnitPlugin::new(&info).expect("Failed to create plugin");
plugin
.initialize(48000.0, 512)
.expect("Failed to initialize plugin");
let input_ch = plugin.input_channels();
let output_ch = plugin.output_channels();
let mut inputs: Vec<Vec<f32>> = (0..input_ch).map(|_| vec![0.0f32; 512]).collect();
let mut outputs: Vec<Vec<f32>> = (0..output_ch).map(|_| vec![0.0f32; 512]).collect();
for ch in &mut inputs {
for (i, sample) in ch.iter_mut().enumerate() {
let t = i as f32 / 48000.0;
*sample = (2.0 * std::f32::consts::PI * 440.0 * t).sin() * 0.5;
}
}
let input_refs: Vec<&[f32]> = inputs.iter().map(|v| v.as_slice()).collect();
let mut output_refs: Vec<&mut [f32]> = outputs.iter_mut().map(|v| v.as_mut_slice()).collect();
let result = plugin.process(&input_refs, &mut output_refs, 512);
assert!(result.is_ok(), "process() should succeed with correct channel count");
println!("✓ Successfully processed with {}/{} channels", input_ch, output_ch);
}
#[test]
fn test_process_with_too_many_channels() {
let Some(info) = get_test_plugin() else {
println!("No test plugins available, skipping test");
return;
};
let mut plugin = AudioUnitPlugin::new(&info).expect("Failed to create plugin");
plugin
.initialize(48000.0, 512)
.expect("Failed to initialize plugin");
let input_ch = plugin.input_channels();
let output_ch = plugin.output_channels();
let extra_channels = 2;
let inputs: Vec<Vec<f32>> = (0..(input_ch + extra_channels)).map(|_| vec![0.0f32; 512]).collect();
let mut outputs: Vec<Vec<f32>> = (0..(output_ch + extra_channels)).map(|_| vec![0.0f32; 512]).collect();
let input_refs: Vec<&[f32]> = inputs.iter().map(|v| v.as_slice()).collect();
let mut output_refs: Vec<&mut [f32]> = outputs.iter_mut().map(|v| v.as_mut_slice()).collect();
let result = plugin.process(&input_refs, &mut output_refs, 512);
assert!(result.is_err(), "process() should fail when provided more channels than plugin expects");
if let Err(e) = result {
let err_msg = format!("{:?}", e);
assert!(err_msg.contains("channel count mismatch") || err_msg.contains("mismatch"),
"Error should mention channel mismatch, got: {}", err_msg);
}
println!("✓ Correctly rejected too many channels ({}/{} provided vs {}/{} expected)",
input_ch + extra_channels, output_ch + extra_channels, input_ch, output_ch);
}
#[test]
fn test_process_with_mismatched_buffer_lengths() {
let Some(info) = get_test_plugin() else {
println!("No test plugins available, skipping test");
return;
};
let mut plugin = AudioUnitPlugin::new(&info).expect("Failed to create plugin");
plugin
.initialize(48000.0, 512)
.expect("Failed to initialize plugin");
let input_ch = plugin.input_channels();
let output_ch = plugin.output_channels();
if input_ch < 2 {
println!("✓ Plugin has < 2 input channels, skipping mismatched length test");
return;
}
let mut inputs: Vec<Vec<f32>> = Vec::new();
inputs.push(vec![0.0f32; 512]); inputs.push(vec![0.0f32; 256]); for _ in 2..input_ch {
inputs.push(vec![0.0f32; 512]); }
let mut outputs: Vec<Vec<f32>> = (0..output_ch).map(|_| vec![0.0f32; 512]).collect();
let input_refs: Vec<&[f32]> = inputs.iter().map(|v| v.as_slice()).collect();
let mut output_refs: Vec<&mut [f32]> = outputs.iter_mut().map(|v| v.as_mut_slice()).collect();
let result = plugin.process(&input_refs, &mut output_refs, 512);
assert!(result.is_err(), "process() should fail when channel buffers have insufficient length");
if let Err(e) = result {
let err_msg = format!("{:?}", e);
assert!(err_msg.contains("channel") && err_msg.contains("samples"),
"Error should mention channel and samples, got: {}", err_msg);
}
println!("✓ Correctly rejected mismatched buffer lengths (channel 1: 256 < 512 frames)");
}
#[test]
fn test_reset_before_init() {
let Some(info) = get_test_plugin() else {
println!("No test plugins available, skipping test");
return;
};
let mut plugin = AudioUnitPlugin::new(&info).expect("Failed to create plugin");
let result = plugin.reset();
assert!(result.is_err(), "reset() should fail before initialization");
println!("✓ Reset correctly fails before initialization");
}
#[test]
fn test_reset_after_init() {
let Some(info) = get_test_plugin() else {
println!("No test plugins available, skipping test");
return;
};
let mut plugin = AudioUnitPlugin::new(&info).expect("Failed to create plugin");
plugin
.initialize(48000.0, 512)
.expect("Failed to initialize plugin");
let result = plugin.reset();
assert!(result.is_ok(), "reset() should succeed after initialization");
println!("✓ Reset succeeds after initialization");
}
#[test]
fn test_reset_clears_state() {
let Some(info) = get_test_plugin() else {
println!("No test plugins available, skipping test");
return;
};
let mut plugin = AudioUnitPlugin::new(&info).expect("Failed to create plugin");
plugin
.initialize(48000.0, 512)
.expect("Failed to initialize plugin");
let input_ch = plugin.input_channels();
let output_ch = plugin.output_channels();
let inputs: Vec<Vec<f32>> = (0..input_ch).map(|_| vec![1.0f32; 512]).collect();
let mut outputs: Vec<Vec<f32>> = (0..output_ch).map(|_| vec![0.0f32; 512]).collect();
let input_refs: Vec<&[f32]> = inputs.iter().map(|v| v.as_slice()).collect();
let mut output_refs: Vec<&mut [f32]> = outputs.iter_mut().map(|v| v.as_mut_slice()).collect();
for _ in 0..10 {
plugin.process(&input_refs, &mut output_refs, 512)
.expect("Failed to process audio");
}
plugin.reset().expect("Failed to reset plugin");
let silent_inputs: Vec<Vec<f32>> = (0..input_ch).map(|_| vec![0.0f32; 512]).collect();
let mut silent_outputs: Vec<Vec<f32>> = (0..output_ch).map(|_| vec![0.0f32; 512]).collect();
let silent_input_refs: Vec<&[f32]> = silent_inputs.iter().map(|v| v.as_slice()).collect();
let mut silent_output_refs: Vec<&mut [f32]> = silent_outputs.iter_mut().map(|v| v.as_mut_slice()).collect();
plugin.process(&silent_input_refs, &mut silent_output_refs, 512)
.expect("Failed to process audio after reset");
let max_abs_value = silent_outputs.iter()
.flat_map(|ch| ch.iter())
.map(|&v| v.abs())
.max_by(|a, b| a.partial_cmp(b).unwrap())
.unwrap_or(0.0);
assert!(max_abs_value < 0.1,
"After reset, processing silence should produce minimal output, got max: {}",
max_abs_value);
println!("✓ Reset successfully clears plugin state (verified: max output {:.6} after silence)",
max_abs_value);
}
}