diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b0ac3ed --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.aider* diff --git a/Cargo.toml b/Cargo.toml deleted file mode 100644 index 6d51476..0000000 --- a/Cargo.toml +++ /dev/null @@ -1,10 +0,0 @@ -[package] -name = "nih-plug-engine" -version = "0.1.0" -edition = "2021" - -[dependencies] -nih_plug = { git = "https://github.com/robbert-vdh/nih-plug.git" } - -[lib] -crate-type = ["cdylib", "staticlib"] diff --git a/Let's produce the blocks.makefile b/Let's produce the blocks.makefile deleted file mode 100644 index 2fc0513..0000000 --- a/Let's produce the blocks.makefile +++ /dev/null @@ -1,28 +0,0 @@ -CC = gcc -CFLAGS = -Wall -Wextra -O2 -g `pkg-config --cflags jack` -LDFLAGS = `pkg-config --libs jack` -TARGET = jack-looper -SRCS = main.c engine.c -OBJS = $(SRCS:.c=.o) -TEST_SRCS = test_engine.c -TEST_OBJS = $(TEST_SRCS:.c=.o) -TEST_TARGET = test_engine - -.PHONY: all clean test - -all: $(TARGET) - -$(TARGET): $(OBJS) - $(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS) - -%.o: %.c - $(CC) $(CFLAGS) -c -o $@ $< - -test: $(TEST_TARGET) - ./$(TEST_TARGET) - -$(TEST_TARGET): $(TEST_OBJS) engine.o - $(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS) - -clean: - rm -f $(OBJS) $(TEST_OBJS) $(TARGET) $(TEST_TARGET) diff --git a/Makefile b/Makefile deleted file mode 100644 index 7fa2b9b..0000000 --- a/Makefile +++ /dev/null @@ -1,25 +0,0 @@ -CC = gcc -CFLAGS = -Wall -Wextra -g -LDFLAGS = -ljack -lm - -TARGET = jack-looper -TEST_TARGET = test_engine - -SRCS = engine.c main.c -TEST_SRCS = test_engine.c engine.c - -all: $(TARGET) - -$(TARGET): $(SRCS) - $(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS) - -test: $(TEST_TARGET) - ./$(TEST_TARGET) - -$(TEST_TARGET): $(TEST_SRCS) - $(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS) - -clean: - rm -f $(TARGET) $(TEST_TARGET) - -.PHONY: all test clean diff --git a/engine.o b/engine.o new file mode 100644 index 0000000..a35406b Binary files /dev/null and b/engine.o differ diff --git a/jack-looper b/jack-looper new file mode 100755 index 0000000..d2db156 Binary files /dev/null and b/jack-looper differ diff --git a/main.o b/main.o new file mode 100644 index 0000000..744b776 Binary files /dev/null and b/main.o differ diff --git a/src/engine.rs b/src/engine.rs deleted file mode 100644 index 2b67633..0000000 --- a/src/engine.rs +++ /dev/null @@ -1,623 +0,0 @@ -use std::collections::HashMap; - -/// State of a clip -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum ClipState { - /// Empty, waiting for trigger to start recording - Empty, - /// Currently recording audio input - Recording, - /// Looping playback - Looping, - /// Stopped (has recorded content, not playing) - Stopped, -} - -/// A clip that can be recorded or played back -pub struct Clip { - /// Audio samples (interleaved stereo) - pub samples: Vec, - /// Number of channels (1 for mono, 2 for stereo) - pub num_channels: usize, - /// Sample rate - pub sample_rate: f32, - /// Current state - pub state: ClipState, - /// Current write position during recording - pub write_position: usize, - /// Current read position during playback - pub read_position: usize, -} - -impl Clip { - pub fn new(num_channels: usize, sample_rate: f32) -> Self { - Self { - samples: Vec::new(), - num_channels, - sample_rate, - state: ClipState::Empty, - write_position: 0, - read_position: 0, - } - } - - /// Record a block of audio into this clip - pub fn record(&mut self, input: &[f32], num_samples: usize) { - if self.state != ClipState::Recording { - return; - } - // Extend the clip buffer if needed - let needed = self.write_position + num_samples * self.num_channels; - if self.samples.len() < needed { - self.samples.resize(needed, 0.0); - } - // Copy input samples - for (i, sample) in input.iter().enumerate().take(num_samples * self.num_channels) { - if self.write_position + i < self.samples.len() { - self.samples[self.write_position + i] = *sample; - } - } - self.write_position += num_samples * self.num_channels; - } - - /// Play back a block of audio from this clip at the given position - /// Returns the number of samples actually read (may be less than requested at end) - pub fn play(&mut self, output: &mut [f32], num_samples: usize) -> usize { - if self.state != ClipState::Looping || self.samples.is_empty() { - return 0; - } - - let channels = self.num_channels; - let total_frames = self.samples.len() / channels; - let mut samples_read = 0; - - for frame in 0..num_samples { - if self.read_position >= total_frames { - // Loop back to start - self.read_position = 0; - } - - let frame_start = self.read_position * channels; - for ch in 0..channels { - if frame_start + ch < self.samples.len() { - output[frame * channels + ch] = self.samples[frame_start + ch]; - } - } - - self.read_position += 1; - samples_read += 1; - } - - samples_read * channels - } - - /// Handle a trigger event for this clip - /// Returns the new state - pub fn trigger(&mut self) -> ClipState { - match self.state { - ClipState::Empty => { - // Start recording - self.state = ClipState::Recording; - self.samples.clear(); - self.write_position = 0; - self.read_position = 0; - } - ClipState::Recording => { - // Stop recording, start looping - self.state = ClipState::Looping; - self.read_position = 0; - } - ClipState::Looping => { - // Stop - self.state = ClipState::Stopped; - } - ClipState::Stopped => { - // Start looping again - self.state = ClipState::Looping; - self.read_position = 0; - } - } - self.state - } - - /// Get the velocity value representing the current state (0-127) - pub fn state_velocity(&self) -> u8 { - match self.state { - ClipState::Empty => 0, - ClipState::Recording => 64, - ClipState::Looping => 127, - ClipState::Stopped => 32, - } - } -} - -/// The engine that manages clips and audio/MIDI routing -pub struct Engine { - /// Clips indexed by note number (0-127) - pub clips: HashMap, - /// Number of audio channels - pub num_channels: usize, - /// Sample rate - pub sample_rate: f32, -} - -impl Engine { - pub fn new(num_channels: usize, sample_rate: f32) -> Self { - Self { - clips: HashMap::new(), - num_channels, - sample_rate, - } - } - - /// Get or create a clip for the given note - pub fn get_clip(&mut self, note: u8) -> &mut Clip { - self.clips.entry(note).or_insert_with(|| { - Clip::new(self.num_channels, self.sample_rate) - }) - } - - /// Process a MIDI note on/off event - /// Returns the output velocity to send - pub fn process_midi_note(&mut self, note: u8, velocity: u8, is_note_on: bool) -> u8 { - if !is_note_on { - // Note off - pass through with original velocity - return velocity; - } - - // Note on - trigger the clip - let clip = self.get_clip(note); - clip.trigger(); - clip.state_velocity() - } - - /// Process audio: record into recording clips, play back looping clips - pub fn process_audio(&mut self, input: &[f32], output: &mut [f32], num_samples: usize) { - // Clear output first - for sample in output.iter_mut() { - *sample = 0.0; - } - - // Record into any clips that are recording - for clip in self.clips.values_mut() { - if clip.state == ClipState::Recording { - clip.record(input, num_samples); - } - } - - // Play back any clips that are looping - for clip in self.clips.values_mut() { - if clip.state == ClipState::Looping { - clip.play(output, num_samples); - } - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - // Helper to create a test engine - fn test_engine() -> Engine { - Engine::new(2, 44100.0) - } - - // Helper to create a test clip with some samples - fn test_clip_with_samples() -> Clip { - let mut clip = Clip::new(2, 44100.0); - clip.samples = vec![0.1, 0.2, 0.3, 0.4, 0.5, 0.6]; // 3 stereo frames - clip.state = ClipState::Looping; - clip - } - - // ===== ClipState tests ===== - - #[test] - fn test_clip_initial_state_is_empty() { - let clip = Clip::new(2, 44100.0); - assert_eq!(clip.state, ClipState::Empty); - } - - #[test] - fn test_clip_state_velocity_values() { - let mut clip = Clip::new(2, 44100.0); - - clip.state = ClipState::Empty; - assert_eq!(clip.state_velocity(), 0); - - clip.state = ClipState::Recording; - assert_eq!(clip.state_velocity(), 64); - - clip.state = ClipState::Looping; - assert_eq!(clip.state_velocity(), 127); - - clip.state = ClipState::Stopped; - assert_eq!(clip.state_velocity(), 32); - } - - // ===== Trigger state machine tests ===== - - #[test] - fn test_trigger_empty_starts_recording() { - let mut clip = Clip::new(2, 44100.0); - assert_eq!(clip.state, ClipState::Empty); - - let new_state = clip.trigger(); - assert_eq!(new_state, ClipState::Recording); - assert_eq!(clip.state, ClipState::Recording); - assert!(clip.samples.is_empty()); - assert_eq!(clip.write_position, 0); - assert_eq!(clip.read_position, 0); - } - - #[test] - fn test_trigger_recording_starts_looping() { - let mut clip = Clip::new(2, 44100.0); - clip.state = ClipState::Recording; - clip.samples = vec![0.1, 0.2, 0.3, 0.4]; - clip.write_position = 4; - - let new_state = clip.trigger(); - assert_eq!(new_state, ClipState::Looping); - assert_eq!(clip.state, ClipState::Looping); - assert_eq!(clip.read_position, 0); - // Samples should be preserved - assert_eq!(clip.samples.len(), 4); - } - - #[test] - fn test_trigger_looping_stops() { - let mut clip = test_clip_with_samples(); - assert_eq!(clip.state, ClipState::Looping); - - let new_state = clip.trigger(); - assert_eq!(new_state, ClipState::Stopped); - assert_eq!(clip.state, ClipState::Stopped); - // Samples should be preserved - assert!(!clip.samples.is_empty()); - } - - #[test] - fn test_trigger_stopped_starts_looping() { - let mut clip = test_clip_with_samples(); - clip.state = ClipState::Stopped; - - let new_state = clip.trigger(); - assert_eq!(new_state, ClipState::Looping); - assert_eq!(clip.state, ClipState::Looping); - assert_eq!(clip.read_position, 0); - } - - #[test] - fn test_trigger_cycle_empty_recording_looping_stopped_looping() { - let mut clip = Clip::new(2, 44100.0); - - // Empty -> Recording - assert_eq!(clip.trigger(), ClipState::Recording); - - // Recording -> Looping - assert_eq!(clip.trigger(), ClipState::Looping); - - // Looping -> Stopped - assert_eq!(clip.trigger(), ClipState::Stopped); - - // Stopped -> Looping - assert_eq!(clip.trigger(), ClipState::Looping); - - // Looping -> Stopped again - assert_eq!(clip.trigger(), ClipState::Stopped); - } - - // ===== Recording tests ===== - - #[test] - fn test_record_when_not_recording_does_nothing() { - let mut clip = Clip::new(2, 44100.0); - clip.state = ClipState::Empty; - - let input = vec![0.5, 0.6, 0.7, 0.8]; - clip.record(&input, 2); - - assert!(clip.samples.is_empty()); - assert_eq!(clip.write_position, 0); - } - - #[test] - fn test_record_appends_samples() { - let mut clip = Clip::new(2, 44100.0); - clip.state = ClipState::Recording; - - let input = vec![0.1, 0.2, 0.3, 0.4]; - clip.record(&input, 2); - - assert_eq!(clip.samples, vec![0.1, 0.2, 0.3, 0.4]); - assert_eq!(clip.write_position, 4); - } - - #[test] - fn test_record_multiple_blocks() { - let mut clip = Clip::new(2, 44100.0); - clip.state = ClipState::Recording; - - let input1 = vec![0.1, 0.2, 0.3, 0.4]; - clip.record(&input1, 2); - - let input2 = vec![0.5, 0.6, 0.7, 0.8]; - clip.record(&input2, 2); - - assert_eq!(clip.samples, vec![0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8]); - assert_eq!(clip.write_position, 8); - } - - #[test] - fn test_record_clears_on_new_recording() { - let mut clip = Clip::new(2, 44100.0); - clip.state = ClipState::Recording; - - // Record some samples - let input1 = vec![0.1, 0.2, 0.3, 0.4]; - clip.record(&input1, 2); - - // Trigger to stop recording - clip.trigger(); // Now Looping - - // Trigger again to start new recording - clip.trigger(); // Now Stopped - clip.trigger(); // Now Looping - clip.trigger(); // Now Stopped - - // Start new recording - clip.state = ClipState::Recording; - clip.samples.clear(); - clip.write_position = 0; - - let input2 = vec![0.9, 0.8, 0.7, 0.6]; - clip.record(&input2, 2); - - assert_eq!(clip.samples, vec![0.9, 0.8, 0.7, 0.6]); - assert_eq!(clip.write_position, 4); - } - - // ===== Playback tests ===== - - #[test] - fn test_play_when_not_looping_returns_zero() { - let mut clip = Clip::new(2, 44100.0); - clip.state = ClipState::Stopped; - clip.samples = vec![0.1, 0.2, 0.3, 0.4]; - - let mut output = vec![0.0; 4]; - let samples_read = clip.play(&mut output, 2); - - assert_eq!(samples_read, 0); - assert_eq!(output, vec![0.0, 0.0, 0.0, 0.0]); - } - - #[test] - fn test_play_empty_clip_returns_zero() { - let mut clip = Clip::new(2, 44100.0); - clip.state = ClipState::Looping; - - let mut output = vec![0.0; 4]; - let samples_read = clip.play(&mut output, 2); - - assert_eq!(samples_read, 0); - } - - #[test] - fn test_play_outputs_samples() { - let mut clip = test_clip_with_samples(); - - let mut output = vec![0.0; 6]; // 3 stereo frames - let samples_read = clip.play(&mut output, 3); - - assert_eq!(samples_read, 6); - assert_eq!(output, vec![0.1, 0.2, 0.3, 0.4, 0.5, 0.6]); - assert_eq!(clip.read_position, 3); - } - - #[test] - fn test_play_loops_when_reaching_end() { - let mut clip = test_clip_with_samples(); // 3 stereo frames - - // Read all 3 frames - let mut output1 = vec![0.0; 6]; - clip.play(&mut output1, 3); - assert_eq!(clip.read_position, 3); // At end - - // Read more - should loop back - let mut output2 = vec![0.0; 4]; // 2 stereo frames - let samples_read = clip.play(&mut output2, 2); - - assert_eq!(samples_read, 4); - assert_eq!(output2, vec![0.1, 0.2, 0.3, 0.4]); - assert_eq!(clip.read_position, 2); - } - - #[test] - fn test_play_multiple_loops() { - let mut clip = test_clip_with_samples(); // 3 stereo frames - - // Read 6 frames (2 full loops) - let mut output = vec![0.0; 12]; - let samples_read = clip.play(&mut output, 6); - - assert_eq!(samples_read, 12); - // First loop - assert_eq!(output[0..6], vec![0.1, 0.2, 0.3, 0.4, 0.5, 0.6]); - // Second loop - assert_eq!(output[6..12], vec![0.1, 0.2, 0.3, 0.4, 0.5, 0.6]); - assert_eq!(clip.read_position, 0); // Back to start - } - - // ===== Engine tests ===== - - #[test] - fn test_engine_initial_state() { - let engine = test_engine(); - assert!(engine.clips.is_empty()); - assert_eq!(engine.num_channels, 2); - assert_eq!(engine.sample_rate, 44100.0); - } - - #[test] - fn test_engine_get_clip_creates_new_clip() { - let mut engine = test_engine(); - - let clip = engine.get_clip(60); - assert_eq!(clip.state, ClipState::Empty); - assert_eq!(clip.num_channels, 2); - assert_eq!(clip.sample_rate, 44100.0); - assert_eq!(engine.clips.len(), 1); - } - - #[test] - fn test_engine_get_clip_reuses_existing() { - let mut engine = test_engine(); - - // Get the clip once - engine.get_clip(60); - - // Get it again - should reuse the same clip - let clip = engine.get_clip(60); - - assert_eq!(clip.state, ClipState::Empty); - assert_eq!(engine.clips.len(), 1); - } - - #[test] - fn test_engine_get_clip_different_notes() { - let mut engine = test_engine(); - - engine.get_clip(60); - engine.get_clip(61); - engine.get_clip(62); - - assert_eq!(engine.clips.len(), 3); - } - - #[test] - fn test_engine_process_midi_note_on_triggers_clip() { - let mut engine = test_engine(); - - // First note on should start recording - let velocity = engine.process_midi_note(60, 100, true); - assert_eq!(velocity, 0); // Empty state velocity - - let clip = engine.get_clip(60); - assert_eq!(clip.state, ClipState::Recording); - } - - #[test] - fn test_engine_process_midi_note_off_passes_through() { - let mut engine = test_engine(); - - let velocity = engine.process_midi_note(60, 100, false); - assert_eq!(velocity, 100); // Original velocity passed through - } - - #[test] - fn test_engine_process_midi_note_cycle() { - let mut engine = test_engine(); - - // Note on 1: Empty -> Recording (velocity 0) - let v1 = engine.process_midi_note(60, 100, true); - assert_eq!(v1, 0); - assert_eq!(engine.get_clip(60).state, ClipState::Recording); - - // Note on 2: Recording -> Looping (velocity 127) - let v2 = engine.process_midi_note(60, 100, true); - assert_eq!(v2, 127); - assert_eq!(engine.get_clip(60).state, ClipState::Looping); - - // Note on 3: Looping -> Stopped (velocity 32) - let v3 = engine.process_midi_note(60, 100, true); - assert_eq!(v3, 32); - assert_eq!(engine.get_clip(60).state, ClipState::Stopped); - - // Note on 4: Stopped -> Looping (velocity 127) - let v4 = engine.process_midi_note(60, 100, true); - assert_eq!(v4, 127); - assert_eq!(engine.get_clip(60).state, ClipState::Looping); - } - - #[test] - fn test_engine_process_audio_records_and_plays() { - let mut engine = test_engine(); - - // Start recording on note 60 - engine.process_midi_note(60, 100, true); - - // Process audio - should record input - let input = vec![0.1, 0.2, 0.3, 0.4]; - let mut output = vec![0.0; 4]; - engine.process_audio(&input, &mut output, 2); - - // Output should be silence (nothing playing yet) - assert_eq!(output, vec![0.0, 0.0, 0.0, 0.0]); - - // Clip should have recorded the input - let clip = engine.get_clip(60); - assert_eq!(clip.samples, vec![0.1, 0.2, 0.3, 0.4]); - - // Stop recording and start looping - engine.process_midi_note(60, 100, true); - - // Process audio - should play back the recorded clip - let input2 = vec![0.0; 4]; - let mut output2 = vec![0.0; 4]; - engine.process_audio(&input2, &mut output2, 2); - - assert_eq!(output2, vec![0.1, 0.2, 0.3, 0.4]); - } - - #[test] - fn test_engine_multiple_clips_independent() { - let mut engine = test_engine(); - - // Trigger note 60 to record - engine.process_midi_note(60, 100, true); - assert_eq!(engine.get_clip(60).state, ClipState::Recording); - - // Trigger note 61 to record - engine.process_midi_note(61, 100, true); - assert_eq!(engine.get_clip(61).state, ClipState::Recording); - - // Trigger note 60 again to loop - engine.process_midi_note(60, 100, true); - assert_eq!(engine.get_clip(60).state, ClipState::Looping); - - // Note 61 should still be recording - assert_eq!(engine.get_clip(61).state, ClipState::Recording); - } - - #[test] - fn test_engine_process_audio_multiple_looping_clips() { - let mut engine = test_engine(); - - // Create and record clip 60 - engine.process_midi_note(60, 100, true); - let input1 = vec![0.1, 0.2, 0.3, 0.4]; - engine.process_audio(&input1, &mut vec![0.0; 4], 2); - engine.process_midi_note(60, 100, true); // Now looping - - // Create and record clip 61 - engine.process_midi_note(61, 100, true); - let input2 = vec![0.5, 0.6, 0.7, 0.8]; - engine.process_audio(&input2, &mut vec![0.0; 4], 2); - engine.process_midi_note(61, 100, true); // Now looping - - // Both should be looping - output should be sum of both clips - let mut output = vec![0.0; 4]; - engine.process_audio(&vec![0.0; 4], &mut output, 2); - - // Clip 60 plays [0.1, 0.2, 0.3, 0.4] - // Clip 61 plays [0.5, 0.6, 0.7, 0.8] - // Output is sum: [0.6, 0.8, 1.0, 1.2] - assert!((output[0] - 0.6).abs() < 1e-6); - assert!((output[1] - 0.8).abs() < 1e-6); - assert!((output[2] - 1.0).abs() < 1e-6); - assert!((output[3] - 1.2).abs() < 1e-6); - } -} diff --git a/src/lib.rs b/src/lib.rs deleted file mode 100644 index f6c344c..0000000 --- a/src/lib.rs +++ /dev/null @@ -1,168 +0,0 @@ -use std::sync::Arc; -use nih_plug::prelude::*; -use nih_plug::plugin::vst3::Vst3Plugin; -use nih_plug::plugin::clap::ClapPlugin; -use nih_plug::wrapper::clap::features::ClapFeature; -use nih_plug::wrapper::vst3::subcategories::Vst3SubCategory; - -mod engine; -use engine::Engine; - -/// The main plugin struct -pub struct ClipLauncher { - /// The engine managing clips - engine: Engine, - /// Pending MIDI output events - pending_midi: Vec>, -} - -impl Default for ClipLauncher { - fn default() -> Self { - Self { - engine: Engine::new(2, 44100.0), - pending_midi: Vec::new(), - } - } -} - -impl Plugin for ClipLauncher { - const NAME: &'static str = "Clip Launcher"; - const VENDOR: &'static str = "Your Company"; - const URL: &'static str = ""; - const EMAIL: &'static str = ""; - const VERSION: &'static str = "0.1.0"; - - const AUDIO_IO_LAYOUTS: &'static [AudioIOLayout] = &[ - AudioIOLayout { - main_input_channels: NonZeroU32::new(2), - main_output_channels: NonZeroU32::new(2), - ..AudioIOLayout::const_default() - }, - ]; - - const MIDI_INPUT: MidiConfig = MidiConfig::Basic; - const MIDI_OUTPUT: MidiConfig = MidiConfig::Basic; - - const SAMPLE_ACCURATE_AUTOMATION: bool = true; - - type SysExMessage = (); - type BackgroundTask = (); - - fn initialize( - &mut self, - _audio_io_layout: &AudioIOLayout, - buffer_config: &BufferConfig, - _context: &mut impl InitContext, - ) -> bool { - self.engine = Engine::new( - _audio_io_layout.main_input_channels.map(|c| c.get() as usize).unwrap_or(2), - buffer_config.sample_rate, - ); - true - } - - fn reset(&mut self) { - // Clear all clips on reset - self.engine.clips.clear(); - self.pending_midi.clear(); - } - - fn process( - &mut self, - buffer: &mut Buffer, - _aux: &mut AuxiliaryBuffers, - context: &mut impl ProcessContext, - ) -> ProcessStatus { - // Process MIDI input events - while let Some(event) = context.next_event() { - match event { - NoteEvent::NoteOn { timing, note, velocity, channel, voice_id } => { - let output_velocity = self.engine.process_midi_note(note, velocity as u8, true); - // Queue output MIDI event - self.pending_midi.push(NoteEvent::NoteOn { - timing, - note, - velocity: output_velocity as f32, - channel, - voice_id, - }); - } - NoteEvent::NoteOff { timing, note, velocity, channel, voice_id } => { - let output_velocity = self.engine.process_midi_note(note, velocity as u8, false); - self.pending_midi.push(NoteEvent::NoteOff { - timing, - note, - velocity: output_velocity as f32, - channel, - voice_id, - }); - } - _ => {} - } - } - - // Process audio - let num_channels = buffer.channels() as usize; - let num_frames = buffer.samples() as usize; - let input_slices = buffer.as_slice(); // &[&[f32]] - - // Build interleaved input buffer - let mut interleaved_input = vec![0.0; num_frames * num_channels]; - for ch in 0..num_channels { - for frame in 0..num_frames { - interleaved_input[frame * num_channels + ch] = input_slices[ch][frame]; - } - } - - // Build interleaved output buffer (will be filled by engine) - let mut interleaved_output = vec![0.0; num_frames * num_channels]; - - self.engine.process_audio(&interleaved_input, &mut interleaved_output, num_frames); - - // Copy back to buffer's output channels - for (ch, channel_samples) in buffer.iter_samples().enumerate() { - let (_, output) = channel_samples.as_slice(); - for frame in 0..num_frames { - output[frame] = interleaved_output[frame * num_channels + ch]; - } - } - - // Send pending MIDI output events - for event in self.pending_midi.drain(..) { - context.send_event(event); - } - - ProcessStatus::Normal - } - - fn params(&self) -> Arc { - Arc::new(ClipLauncherParams { - dummy: BoolParam::new("Dummy", false), - }) - } -} - -impl Vst3Plugin for ClipLauncher { - const VST3_CLASS_ID: [u8; 16] = *b"ClipLauncher1234"; - const VST3_SUBCATEGORIES: &'static [Vst3SubCategory] = &[Vst3SubCategory::Fx]; -} - -impl ClapPlugin for ClipLauncher { - const CLAP_ID: &'static str = "com.yourcompany.clip-launcher"; - const CLAP_DESCRIPTION: Option<&'static str> = Some("A clip launcher plugin"); - const CLAP_MANUAL_URL: Option<&'static str> = None; - const CLAP_SUPPORT_URL: Option<&'static str> = None; - const CLAP_FEATURES: &'static [ClapFeature] = &[ClapFeature::AudioEffect, ClapFeature::Stereo]; -} - -/// Empty params struct (no parameters needed for this plugin) -#[derive(Params)] -struct ClipLauncherParams { - /// Placeholder parameter to satisfy the derive macro - #[id = "dummy"] - dummy: BoolParam, -} - -// Export the plugin -nih_export_vst3!(ClipLauncher); -nih_export_clap!(ClipLauncher); diff --git a/test_engine b/test_engine new file mode 100755 index 0000000..2879b09 Binary files /dev/null and b/test_engine differ diff --git a/test_engine.o b/test_engine.o new file mode 100644 index 0000000..060dd8a Binary files /dev/null and b/test_engine.o differ