Building and running

This commit is contained in:
Loic Coenen
2026-05-01 08:36:16 +00:00
parent d63a3f5ab2
commit 47dbd1148f
11 changed files with 1 additions and 854 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
.aider*

View File

@@ -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"]

View File

@@ -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)

View File

@@ -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

BIN
engine.o Normal file
View File

Binary file not shown.

BIN
jack-looper Executable file
View File

Binary file not shown.

BIN
main.o Normal file
View File

Binary file not shown.

View File

@@ -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<f32>,
/// 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<u8, Clip>,
/// 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);
}
}

View File

@@ -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<NoteEvent<()>>,
}
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<Self>,
) -> 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<Self>,
) -> 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<dyn Params> {
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);

BIN
test_engine Executable file
View File

Binary file not shown.

BIN
test_engine.o Normal file
View File

Binary file not shown.