feat: implement clip launcher plugin with MIDI-triggered recording and looping

Co-authored-by: aider (deepseek/deepseek-coder) <aider@aider.chat>
This commit is contained in:
Loic Coenen
2026-04-30 22:29:27 +00:00
parent f48b053c7a
commit 5ccc29ff82
3 changed files with 336 additions and 0 deletions

View File

@@ -0,0 +1,131 @@
use nih_plug::prelude::*;
mod engine;
use engine::{Engine, ClipState};
/// 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()).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 } => {
let output_velocity = self.engine.process_midi_note(note, velocity, true);
// Queue output MIDI event
self.pending_midi.push(NoteEvent::NoteOn {
timing,
note,
velocity: output_velocity,
channel,
});
}
NoteEvent::NoteOff { timing, note, velocity, channel } => {
let output_velocity = self.engine.process_midi_note(note, velocity, false);
self.pending_midi.push(NoteEvent::NoteOff {
timing,
note,
velocity: output_velocity,
channel,
});
}
_ => {}
}
}
// Process audio
for channel_samples in buffer.iter_samples() {
let (input, mut output) = channel_samples;
let num_samples = input.len();
// Process this block
self.engine.process_audio(input, &mut output, num_samples);
}
// 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)
}
}
/// Empty params struct (no parameters needed for this plugin)
#[derive(Params)]
struct ClipLauncherParams;
impl Default for ClipLauncherParams {
fn default() -> Self {
Self
}
}
// Export the plugin
nih_export_vst3!(ClipLauncher);
nih_export_clap!(ClipLauncher);