Trying to clena the mess opened by plugins
This commit is contained in:
461
plugins/cagire-plugins/src/lib.rs
Normal file
461
plugins/cagire-plugins/src/lib.rs
Normal file
@@ -0,0 +1,461 @@
|
||||
mod editor;
|
||||
mod input_egui;
|
||||
mod params;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use arc_swap::ArcSwap;
|
||||
use crossbeam_channel::{bounded, Receiver, Sender};
|
||||
use nih_plug::prelude::*;
|
||||
use parking_lot::Mutex;
|
||||
use rand::rngs::StdRng;
|
||||
use rand::SeedableRng;
|
||||
use ringbuf::traits::Producer;
|
||||
|
||||
use cagire::engine::{
|
||||
parse_midi_command, spawn_analysis_thread, AnalysisHandle, AudioCommand, MidiCommand,
|
||||
PatternSnapshot, ScopeBuffer, SeqCommand, SequencerState, SharedSequencerState, SpectrumBuffer,
|
||||
StepSnapshot, TickInput,
|
||||
};
|
||||
use cagire::model::{Dictionary, Rng, Variables};
|
||||
use params::CagireParams;
|
||||
|
||||
pub struct PluginBridge {
|
||||
pub cmd_tx: Sender<SeqCommand>,
|
||||
pub cmd_rx: Receiver<SeqCommand>,
|
||||
pub audio_cmd_tx: Sender<AudioCommand>,
|
||||
pub audio_cmd_rx: Receiver<AudioCommand>,
|
||||
pub shared_state: Arc<ArcSwap<SharedSequencerState>>,
|
||||
pub scope_buffer: Arc<ScopeBuffer>,
|
||||
pub spectrum_buffer: Arc<SpectrumBuffer>,
|
||||
pub sample_registry: ArcSwap<Option<Arc<doux::SampleRegistry>>>,
|
||||
}
|
||||
|
||||
struct PendingNoteOff {
|
||||
target_sample: u64,
|
||||
channel: u8,
|
||||
note: u8,
|
||||
}
|
||||
|
||||
pub struct CagirePlugin {
|
||||
params: Arc<CagireParams>,
|
||||
seq_state: Option<SequencerState>,
|
||||
engine: Option<doux::Engine>,
|
||||
sample_rate: f32,
|
||||
prev_beat: f64,
|
||||
sample_pos: u64,
|
||||
bridge: Arc<PluginBridge>,
|
||||
variables: Variables,
|
||||
dict: Dictionary,
|
||||
rng: Rng,
|
||||
cmd_buffer: String,
|
||||
audio_buffer: Vec<f32>,
|
||||
fft_producer: Option<ringbuf::HeapProd<f32>>,
|
||||
_analysis: Option<AnalysisHandle>,
|
||||
pending_note_offs: Vec<PendingNoteOff>,
|
||||
}
|
||||
|
||||
impl Default for CagirePlugin {
|
||||
fn default() -> Self {
|
||||
let variables: Variables = Arc::new(ArcSwap::from_pointee(HashMap::new()));
|
||||
let dict: Dictionary = Arc::new(Mutex::new(HashMap::new()));
|
||||
let rng: Rng = Arc::new(Mutex::new(StdRng::seed_from_u64(0)));
|
||||
|
||||
let (cmd_tx, cmd_rx) = bounded(64);
|
||||
let (audio_cmd_tx, audio_cmd_rx) = bounded(64);
|
||||
let bridge = Arc::new(PluginBridge {
|
||||
cmd_tx,
|
||||
cmd_rx,
|
||||
audio_cmd_tx,
|
||||
audio_cmd_rx,
|
||||
shared_state: Arc::new(ArcSwap::from_pointee(SharedSequencerState::default())),
|
||||
scope_buffer: Arc::new(ScopeBuffer::default()),
|
||||
spectrum_buffer: Arc::new(SpectrumBuffer::default()),
|
||||
sample_registry: ArcSwap::from_pointee(None),
|
||||
});
|
||||
|
||||
Self {
|
||||
params: Arc::new(CagireParams::default()),
|
||||
seq_state: None,
|
||||
engine: None,
|
||||
sample_rate: 44100.0,
|
||||
prev_beat: -1.0,
|
||||
sample_pos: 0,
|
||||
bridge,
|
||||
variables,
|
||||
dict,
|
||||
rng,
|
||||
cmd_buffer: String::with_capacity(256),
|
||||
audio_buffer: Vec::new(),
|
||||
fft_producer: None,
|
||||
_analysis: None,
|
||||
pending_note_offs: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Plugin for CagirePlugin {
|
||||
type SysExMessage = ();
|
||||
type BackgroundTask = ();
|
||||
|
||||
const NAME: &'static str = "Cagire";
|
||||
const VENDOR: &'static str = "Bubobubobubobubo";
|
||||
const URL: &'static str = "https://cagire.raphaelforment.fr";
|
||||
const EMAIL: &'static str = "raphael.forment@gmail.com";
|
||||
const VERSION: &'static str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
const AUDIO_IO_LAYOUTS: &'static [AudioIOLayout] = &[AudioIOLayout {
|
||||
main_input_channels: None,
|
||||
main_output_channels: Some(new_nonzero_u32(2)),
|
||||
aux_input_ports: &[],
|
||||
aux_output_ports: &[],
|
||||
names: PortNames {
|
||||
layout: Some("Stereo"),
|
||||
main_input: None,
|
||||
main_output: Some("Output"),
|
||||
aux_inputs: &[],
|
||||
aux_outputs: &[],
|
||||
},
|
||||
}];
|
||||
|
||||
const MIDI_INPUT: MidiConfig = MidiConfig::MidiCCs;
|
||||
const MIDI_OUTPUT: MidiConfig = MidiConfig::MidiCCs;
|
||||
|
||||
fn params(&self) -> Arc<dyn Params> {
|
||||
self.params.clone()
|
||||
}
|
||||
|
||||
fn editor(&mut self, _async_executor: AsyncExecutor<Self>) -> Option<Box<dyn Editor>> {
|
||||
editor::create_editor(
|
||||
self.params.clone(),
|
||||
self.params.editor_state.clone(),
|
||||
Arc::clone(&self.bridge),
|
||||
Arc::clone(&self.variables),
|
||||
Arc::clone(&self.dict),
|
||||
Arc::clone(&self.rng),
|
||||
)
|
||||
}
|
||||
|
||||
fn initialize(
|
||||
&mut self,
|
||||
_audio_io_layout: &AudioIOLayout,
|
||||
buffer_config: &BufferConfig,
|
||||
_context: &mut impl InitContext<Self>,
|
||||
) -> bool {
|
||||
self.sample_rate = buffer_config.sample_rate;
|
||||
self.sample_pos = 0;
|
||||
self.prev_beat = -1.0;
|
||||
|
||||
self.seq_state = Some(SequencerState::new(
|
||||
Arc::clone(&self.variables),
|
||||
Arc::clone(&self.dict),
|
||||
Arc::clone(&self.rng),
|
||||
None,
|
||||
));
|
||||
|
||||
let engine = doux::Engine::new_with_channels(
|
||||
self.sample_rate,
|
||||
2,
|
||||
64,
|
||||
);
|
||||
self.bridge
|
||||
.sample_registry
|
||||
.store(Arc::new(Some(Arc::clone(&engine.sample_registry))));
|
||||
self.engine = Some(engine);
|
||||
|
||||
let (fft_producer, analysis_handle) = spawn_analysis_thread(
|
||||
self.sample_rate,
|
||||
Arc::clone(&self.bridge.spectrum_buffer),
|
||||
);
|
||||
self.fft_producer = Some(fft_producer);
|
||||
self._analysis = Some(analysis_handle);
|
||||
|
||||
// Seed sequencer with persisted project data
|
||||
let project = self.params.project.lock().clone();
|
||||
for (bank_idx, bank) in project.banks.iter().enumerate() {
|
||||
for (pat_idx, pat) in bank.patterns.iter().enumerate() {
|
||||
let has_content = pat.steps.iter().any(|s| !s.script.is_empty());
|
||||
if !has_content {
|
||||
continue;
|
||||
}
|
||||
let snapshot = PatternSnapshot {
|
||||
speed: pat.speed,
|
||||
length: pat.length,
|
||||
steps: pat
|
||||
.steps
|
||||
.iter()
|
||||
.take(pat.length)
|
||||
.map(|s| StepSnapshot {
|
||||
active: s.active,
|
||||
script: s.script.clone(),
|
||||
source: s.source,
|
||||
})
|
||||
.collect(),
|
||||
quantization: pat.quantization,
|
||||
sync_mode: pat.sync_mode,
|
||||
};
|
||||
let _ = self.bridge.cmd_tx.send(SeqCommand::PatternUpdate {
|
||||
bank: bank_idx,
|
||||
pattern: pat_idx,
|
||||
data: snapshot,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.prev_beat = -1.0;
|
||||
self.sample_pos = 0;
|
||||
if let Some(engine) = &mut self.engine {
|
||||
engine.hush();
|
||||
}
|
||||
}
|
||||
|
||||
fn process(
|
||||
&mut self,
|
||||
buffer: &mut Buffer,
|
||||
_aux: &mut AuxiliaryBuffers,
|
||||
context: &mut impl ProcessContext<Self>,
|
||||
) -> ProcessStatus {
|
||||
let Some(seq_state) = &mut self.seq_state else {
|
||||
return ProcessStatus::Normal;
|
||||
};
|
||||
let Some(engine) = &mut self.engine else {
|
||||
return ProcessStatus::Normal;
|
||||
};
|
||||
|
||||
let transport = context.transport();
|
||||
let buffer_len = buffer.samples();
|
||||
|
||||
let playing = transport.playing;
|
||||
let tempo = transport.tempo.unwrap_or(self.params.tempo.value() as f64);
|
||||
let beat = transport.pos_beats().unwrap_or(0.0);
|
||||
let quantum = transport
|
||||
.time_sig_numerator
|
||||
.map(|n| n as f64)
|
||||
.unwrap_or(4.0);
|
||||
|
||||
let effective_tempo = if self.params.sync_to_host.value() {
|
||||
tempo
|
||||
} else {
|
||||
self.params.tempo.value() as f64
|
||||
};
|
||||
|
||||
let buffer_secs = buffer_len as f64 / self.sample_rate as f64;
|
||||
let lookahead_beats = if effective_tempo > 0.0 {
|
||||
buffer_secs * effective_tempo / 60.0
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
let lookahead_end = beat + lookahead_beats;
|
||||
|
||||
let engine_time = self.sample_pos as f64 / self.sample_rate as f64;
|
||||
|
||||
// Drain commands from the editor
|
||||
let commands: Vec<SeqCommand> = self.bridge.cmd_rx.try_iter().collect();
|
||||
|
||||
let input = TickInput {
|
||||
commands,
|
||||
playing,
|
||||
beat,
|
||||
lookahead_end,
|
||||
tempo: effective_tempo,
|
||||
quantum,
|
||||
fill: false,
|
||||
nudge_secs: 0.0,
|
||||
current_time_us: 0,
|
||||
engine_time,
|
||||
mouse_x: 0.5,
|
||||
mouse_y: 0.5,
|
||||
mouse_down: 0.0,
|
||||
};
|
||||
|
||||
let output = seq_state.tick(input);
|
||||
|
||||
// Publish snapshot for the editor
|
||||
self.bridge
|
||||
.shared_state
|
||||
.store(Arc::new(output.shared_state));
|
||||
|
||||
// Drain audio commands from the editor (preview, hush, load samples, etc.)
|
||||
for audio_cmd in self.bridge.audio_cmd_rx.try_iter() {
|
||||
match audio_cmd {
|
||||
AudioCommand::Evaluate { ref cmd, time } => {
|
||||
let cmd_ref = match time {
|
||||
Some(t) => {
|
||||
self.cmd_buffer.clear();
|
||||
use std::fmt::Write;
|
||||
let _ = write!(&mut self.cmd_buffer, "{cmd}/time/{t:.6}");
|
||||
self.cmd_buffer.as_str()
|
||||
}
|
||||
None => cmd.as_str(),
|
||||
};
|
||||
engine.evaluate(cmd_ref);
|
||||
}
|
||||
AudioCommand::Hush => engine.hush(),
|
||||
AudioCommand::Panic => engine.panic(),
|
||||
AudioCommand::LoadSamples(samples) => {
|
||||
engine.sample_index.extend(samples);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Drain expired pending note-offs
|
||||
self.pending_note_offs.retain(|off| {
|
||||
if off.target_sample <= self.sample_pos {
|
||||
context.send_event(NoteEvent::NoteOff {
|
||||
timing: 0,
|
||||
voice_id: None,
|
||||
channel: off.channel,
|
||||
note: off.note,
|
||||
velocity: 0.0,
|
||||
});
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
});
|
||||
|
||||
// Feed audio + MIDI commands from sequencer
|
||||
for tsc in &output.audio_commands {
|
||||
if tsc.cmd.starts_with("/midi/") {
|
||||
if let Some((midi_cmd, dur, _delta)) = parse_midi_command(&tsc.cmd) {
|
||||
match midi_cmd {
|
||||
MidiCommand::NoteOn { channel, note, velocity, .. } => {
|
||||
context.send_event(NoteEvent::NoteOn {
|
||||
timing: 0,
|
||||
voice_id: None,
|
||||
channel,
|
||||
note,
|
||||
velocity: velocity as f32 / 127.0,
|
||||
});
|
||||
if let Some(dur) = dur {
|
||||
self.pending_note_offs.push(PendingNoteOff {
|
||||
target_sample: self.sample_pos
|
||||
+ (dur * self.sample_rate as f64) as u64,
|
||||
channel,
|
||||
note,
|
||||
});
|
||||
}
|
||||
}
|
||||
MidiCommand::NoteOff { channel, note, .. } => {
|
||||
context.send_event(NoteEvent::NoteOff {
|
||||
timing: 0,
|
||||
voice_id: None,
|
||||
channel,
|
||||
note,
|
||||
velocity: 0.0,
|
||||
});
|
||||
}
|
||||
MidiCommand::CC { channel, cc, value, .. } => {
|
||||
context.send_event(NoteEvent::MidiCC {
|
||||
timing: 0,
|
||||
channel,
|
||||
cc,
|
||||
value: value as f32 / 127.0,
|
||||
});
|
||||
}
|
||||
MidiCommand::PitchBend { channel, value, .. } => {
|
||||
context.send_event(NoteEvent::MidiPitchBend {
|
||||
timing: 0,
|
||||
channel,
|
||||
value: value as f32 / 16383.0,
|
||||
});
|
||||
}
|
||||
MidiCommand::Pressure { channel, value, .. } => {
|
||||
context.send_event(NoteEvent::MidiChannelPressure {
|
||||
timing: 0,
|
||||
channel,
|
||||
pressure: value as f32 / 127.0,
|
||||
});
|
||||
}
|
||||
MidiCommand::ProgramChange { channel, program, .. } => {
|
||||
context.send_event(NoteEvent::MidiProgramChange {
|
||||
timing: 0,
|
||||
channel,
|
||||
program,
|
||||
});
|
||||
}
|
||||
MidiCommand::Clock { .. }
|
||||
| MidiCommand::Start { .. }
|
||||
| MidiCommand::Stop { .. }
|
||||
| MidiCommand::Continue { .. } => {}
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
let cmd_ref = match tsc.time {
|
||||
Some(t) => {
|
||||
self.cmd_buffer.clear();
|
||||
use std::fmt::Write;
|
||||
let _ = write!(&mut self.cmd_buffer, "{}/time/{t:.6}", tsc.cmd);
|
||||
self.cmd_buffer.as_str()
|
||||
}
|
||||
None => &tsc.cmd,
|
||||
};
|
||||
engine.evaluate(cmd_ref);
|
||||
}
|
||||
|
||||
// Process audio block — doux writes interleaved stereo into our buffer
|
||||
let num_samples = buffer_len * 2;
|
||||
self.audio_buffer.resize(num_samples, 0.0);
|
||||
self.audio_buffer.fill(0.0);
|
||||
engine.process_block(&mut self.audio_buffer, &[], &[]);
|
||||
|
||||
// Feed scope and spectrum analysis
|
||||
self.bridge.scope_buffer.write(&self.audio_buffer);
|
||||
if let Some(producer) = &mut self.fft_producer {
|
||||
for chunk in self.audio_buffer.chunks(2) {
|
||||
let mono = (chunk[0] + chunk.get(1).copied().unwrap_or(0.0)) * 0.5;
|
||||
let _ = producer.try_push(mono);
|
||||
}
|
||||
}
|
||||
|
||||
// Copy interleaved doux output → nih-plug channel slices
|
||||
let mut channel_iter = buffer.iter_samples();
|
||||
for frame_idx in 0..buffer_len {
|
||||
if let Some(mut frame) = channel_iter.next() {
|
||||
let left = self.audio_buffer[frame_idx * 2];
|
||||
let right = self.audio_buffer[frame_idx * 2 + 1];
|
||||
if let Some(sample) = frame.get_mut(0) {
|
||||
*sample = left;
|
||||
}
|
||||
if let Some(sample) = frame.get_mut(1) {
|
||||
*sample = right;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.sample_pos += buffer_len as u64;
|
||||
self.prev_beat = lookahead_end;
|
||||
|
||||
ProcessStatus::Normal
|
||||
}
|
||||
}
|
||||
|
||||
impl ClapPlugin for CagirePlugin {
|
||||
const CLAP_ID: &'static str = "com.sova.cagire";
|
||||
const CLAP_DESCRIPTION: Option<&'static str> = Some("Forth-based music sequencer");
|
||||
const CLAP_MANUAL_URL: Option<&'static str> = Some("https://cagire.raphaelforment.fr");
|
||||
const CLAP_SUPPORT_URL: Option<&'static str> = Some("https://cagire.raphaelforment.fr");
|
||||
const CLAP_FEATURES: &'static [ClapFeature] = &[
|
||||
ClapFeature::Instrument,
|
||||
ClapFeature::Synthesizer,
|
||||
ClapFeature::Stereo,
|
||||
];
|
||||
}
|
||||
|
||||
impl Vst3Plugin for CagirePlugin {
|
||||
const VST3_CLASS_ID: [u8; 16] = *b"CagireSovaVST3!!";
|
||||
const VST3_SUBCATEGORIES: &'static [Vst3SubCategory] = &[
|
||||
Vst3SubCategory::Instrument,
|
||||
Vst3SubCategory::Synth,
|
||||
Vst3SubCategory::Stereo,
|
||||
];
|
||||
}
|
||||
|
||||
nih_export_clap!(CagirePlugin);
|
||||
nih_export_vst3!(CagirePlugin);
|
||||
Reference in New Issue
Block a user