Skip to content

Commit a4135d5

Browse files
committed
Add new standalone plugin format
- optional feature, disabled by default - CPAL for audio IO, opening the default device with default settings - midir for note input, opening the first available MIDI input device - winit to create a slint host window
1 parent 2002514 commit a4135d5

13 files changed

Lines changed: 564 additions & 0 deletions

File tree

examples/gain-plugin/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ name = "gain-plugin"
33
version = "0.1.0"
44
edition = "2024"
55

6+
[features]
7+
standalone = ["plinth-plugin/standalone"]
8+
69
[lib]
710
crate-type = ["cdylib", "lib", "staticlib"]
811

examples/gain-plugin/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,5 @@ mod parameters;
33
mod plugin;
44
mod processor;
55
mod view;
6+
7+
pub use plugin::GainPlugin;

plinth-plugin/Cargo.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ readme = "README.md"
88
repository = "https://github.com/ilmai/plugin-things"
99
license = "MIT"
1010

11+
[features]
12+
standalone = ["dep:cpal", "dep:midir", "dep:winit"]
13+
1114
[dependencies]
1215
atomic_refcell = "0.1"
1316
clap-sys = "0.5"
@@ -22,6 +25,10 @@ thiserror = "2"
2225
vst3 = "0.3"
2326
widestring = "1"
2427
xxhash-rust = { version = "0.8", features = ["xxh32"] }
28+
# standalone features
29+
winit = { version = "0.30", optional = true }
30+
cpal = { version = "0.17", optional = true }
31+
midir = { version = "0.10", optional = true }
2532

2633
[build-dependencies]
2734
bindgen = "0.72"

plinth-plugin/src/formats.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,16 @@ use std::fmt::Display;
33
#[cfg(target_os="macos")]
44
pub mod auv3;
55
pub mod clap;
6+
#[cfg(feature = "standalone")]
7+
pub mod standalone;
68
pub mod vst3;
79

810
#[derive(Clone, Copy, Debug)]
911
pub enum PluginFormat {
1012
Auv3,
1113
Clap,
14+
#[cfg(feature = "standalone")]
15+
Standalone,
1216
Vst3,
1317
}
1418

@@ -17,6 +21,8 @@ impl Display for PluginFormat {
1721
match self {
1822
PluginFormat::Auv3 => f.write_str("AUv3"),
1923
PluginFormat::Clap => f.write_str("CLAP"),
24+
#[cfg(feature = "standalone")]
25+
PluginFormat::Standalone => f.write_str("Standalone"),
2026
PluginFormat::Vst3 => f.write_str("VST3"),
2127
}
2228
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
mod audio;
2+
mod host;
3+
mod macros;
4+
mod midi;
5+
mod parameters;
6+
mod plugin;
7+
mod runner;
8+
9+
pub use plugin::StandalonePlugin;
10+
pub use runner::run_standalone;
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
use std::sync::{Arc, mpsc::Receiver};
2+
3+
use cpal::{FromSample, Sample};
4+
use plinth_core::{buffers::buffer::Buffer, signals::{signal::{Signal, SignalMut}, signal_base::SignalBase}};
5+
6+
use super::parameters::StandaloneParameterEventMap;
7+
use super::plugin::StandalonePlugin;
8+
use crate::{Event, Processor};
9+
10+
pub struct AudioState<P: StandalonePlugin> {
11+
pub processor: P::Processor,
12+
pub buffer: Buffer,
13+
pub channels: usize,
14+
pub midi_receiver: Receiver<Event>,
15+
pub parameter_event_map: Arc<StandaloneParameterEventMap>,
16+
pending_events: Vec<Event>,
17+
}
18+
19+
impl<P: StandalonePlugin> AudioState<P> {
20+
pub fn new(
21+
processor: P::Processor,
22+
channels: usize,
23+
midi_receiver: Receiver<Event>,
24+
parameter_event_map: Arc<StandaloneParameterEventMap>,
25+
) -> Self {
26+
Self {
27+
processor,
28+
buffer: Buffer::new(channels, P::MAX_BLOCK_SIZE),
29+
channels,
30+
midi_receiver,
31+
parameter_event_map,
32+
pending_events: Vec::with_capacity(P::EVENT_QUEUE_LEN),
33+
}
34+
}
35+
36+
pub fn process<T>(&mut self, data: &mut [T], channels: usize)
37+
where
38+
T: Sample + FromSample<f32>,
39+
f32: FromSample<T>,
40+
{
41+
let frame_count = data.len() / channels;
42+
43+
// Drain MIDI events
44+
self.pending_events.clear();
45+
while let Ok(event) = self.midi_receiver.try_recv() {
46+
self.pending_events.push(event);
47+
}
48+
for event in self.parameter_event_map.iter_events() {
49+
self.pending_events.push(event);
50+
}
51+
52+
// Process audio, ensuring we don't call process with more than P::MAX_BLOCK_SIZE frames
53+
let mut frame_offset = 0;
54+
while frame_offset < frame_count {
55+
let chunk_size = (frame_count - frame_offset).min(P::MAX_BLOCK_SIZE);
56+
57+
if self.buffer.len() != chunk_size {
58+
self.buffer.resize(chunk_size);
59+
}
60+
61+
// Deinterleave chunk from CPAL buffer
62+
for frame in 0..chunk_size {
63+
for ch in 0..self.channels {
64+
self.buffer.channel_mut(ch)[frame] =
65+
f32::from_sample(data[(frame_offset + frame) * self.channels + ch]);
66+
}
67+
}
68+
69+
// Process and drain all events on first run, assuming they have no time tags
70+
let aux: Option<&Buffer> = None;
71+
self.processor
72+
.process(&mut self.buffer, aux, None, self.pending_events.drain(..));
73+
74+
// Reinterleave chunk back into CPAL buffer
75+
for frame in 0..chunk_size {
76+
for ch in 0..self.channels {
77+
data[(frame_offset + frame) * self.channels + ch] =
78+
T::from_sample(self.buffer.channel(ch)[frame]);
79+
}
80+
}
81+
82+
frame_offset += chunk_size;
83+
}
84+
}
85+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
use std::sync::{Arc, mpsc::Sender};
2+
3+
use crate::{Event, Host, ParameterId, ParameterValue};
4+
5+
use super::parameters::StandaloneParameterEventMap;
6+
7+
pub struct StandaloneHost {
8+
parameter_event_map: Arc<StandaloneParameterEventMap>,
9+
to_plugin_sender: Sender<Event>,
10+
}
11+
12+
impl StandaloneHost {
13+
pub fn new(
14+
parameter_event_map: Arc<StandaloneParameterEventMap>,
15+
to_plugin_sender: Sender<Event>,
16+
) -> Self {
17+
Self {
18+
parameter_event_map,
19+
to_plugin_sender,
20+
}
21+
}
22+
}
23+
24+
impl Host for StandaloneHost {
25+
fn can_resize(&self) -> bool {
26+
false
27+
}
28+
29+
fn resize_view(&self, _width: f64, _height: f64) -> bool {
30+
false
31+
}
32+
33+
fn start_parameter_change(&self, id: ParameterId) {
34+
let _ = self.to_plugin_sender.send(Event::StartParameterChange { id });
35+
}
36+
37+
fn change_parameter_value(&self, id: ParameterId, normalized: ParameterValue) {
38+
self.parameter_event_map.change_parameter_value(id, normalized);
39+
40+
let _ = self.to_plugin_sender.send(Event::ParameterValue {
41+
sample_offset: 0,
42+
id,
43+
value: normalized,
44+
});
45+
}
46+
47+
fn end_parameter_change(&self, id: ParameterId) {
48+
let _ = self.to_plugin_sender.send(Event::EndParameterChange { id });
49+
}
50+
51+
fn reload_parameters(&self) {}
52+
53+
fn mark_state_dirty(&self) {}
54+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
#[macro_export]
2+
macro_rules! export_standalone {
3+
($plugin:ty) => {
4+
fn main() {
5+
::plinth_plugin::standalone::run_standalone::<$plugin>();
6+
}
7+
};
8+
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
use std::sync::mpsc::Sender;
2+
3+
use midir::{MidiInput, MidiInputConnection};
4+
5+
use crate::Event;
6+
7+
pub fn connect_midi_inputs(sender: Sender<Event>) -> Vec<MidiInputConnection<()>> {
8+
let midi_in = match MidiInput::new("plinth-standalone") {
9+
Ok(midi_in) => midi_in,
10+
Err(err) => {
11+
log::warn!("Failed to create MIDI input: {err}");
12+
return vec![];
13+
}
14+
};
15+
16+
let ports = midi_in.ports();
17+
let mut connections = Vec::with_capacity(ports.len());
18+
19+
for port in &ports {
20+
let port_name = midi_in.port_name(port).unwrap_or_else(|_| port.id());
21+
let midi_in = match MidiInput::new("plinth-standalone") {
22+
Ok(m) => m,
23+
Err(e) => {
24+
log::warn!("Failed to create MIDI input for port '{port_name}': {e}");
25+
continue;
26+
}
27+
};
28+
29+
let sender = sender.clone();
30+
match midi_in.connect(
31+
port,
32+
"plinth-midi-input",
33+
move |_timestamp, data, _| {
34+
if let Some(event) = parse_midi(data) {
35+
let _ = sender.send(event);
36+
}
37+
},
38+
(),
39+
) {
40+
Ok(conn) => connections.push(conn),
41+
Err(e) => log::warn!("Failed to connect to MIDI input port '{port_name}': {e}"),
42+
}
43+
}
44+
45+
connections
46+
}
47+
48+
fn parse_midi(data: &[u8]) -> Option<Event> {
49+
if data.len() < 2 {
50+
return None;
51+
}
52+
53+
let status = data[0] & 0xF0;
54+
let channel = (data[0] & 0x0F) as i16;
55+
let key = data[1] as i16;
56+
let velocity = if data.len() >= 3 {
57+
data[2] as f64 / 127.0
58+
} else {
59+
0.0
60+
};
61+
62+
match status {
63+
0x90 if data.len() >= 3 && data[2] > 0 => Some(Event::NoteOn {
64+
sample_offset: 0,
65+
channel,
66+
key,
67+
note: -1,
68+
velocity,
69+
}),
70+
0x80 | 0x90 => Some(Event::NoteOff {
71+
sample_offset: 0,
72+
channel,
73+
key,
74+
note: -1,
75+
velocity,
76+
}),
77+
0xE0 if data.len() >= 3 => {
78+
let lsb = data[1] as i16;
79+
let msb = data[2] as i16;
80+
let bend = (msb << 7 | lsb) - 8192;
81+
let semitones = bend as f64 / 8192.0 * 2.0;
82+
Some(Event::PitchBend {
83+
sample_offset: 0,
84+
channel,
85+
key: -1,
86+
note: -1,
87+
semitones,
88+
})
89+
}
90+
_ => None,
91+
}
92+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
use std::collections::{btree_map, BTreeMap};
2+
use std::sync::atomic::{AtomicBool, Ordering};
3+
4+
use portable_atomic::AtomicF64;
5+
6+
use crate::{Event, ParameterId, ParameterValue, Parameters};
7+
8+
#[derive(Default)]
9+
struct ParameterEventInfo {
10+
value: AtomicF64,
11+
changed: AtomicBool,
12+
}
13+
14+
pub(crate) struct StandaloneParameterEventMap {
15+
parameter_event_info: BTreeMap<ParameterId, ParameterEventInfo>,
16+
}
17+
18+
impl StandaloneParameterEventMap {
19+
pub(crate) fn new(parameters: &impl Parameters) -> Self {
20+
let mut parameter_event_info = BTreeMap::new();
21+
22+
for &id in parameters.ids() {
23+
parameter_event_info.insert(id, Default::default());
24+
}
25+
26+
Self { parameter_event_info }
27+
}
28+
29+
pub(crate) fn change_parameter_value(&self, id: ParameterId, value: ParameterValue) {
30+
let info = self.parameter_event_info.get(&id).unwrap();
31+
info.value.store(value, Ordering::Release);
32+
info.changed.store(true, Ordering::Release);
33+
}
34+
35+
pub(crate) fn iter_events(&self) -> StandaloneParameterEventIterator<'_> {
36+
StandaloneParameterEventIterator {
37+
event_info_iterator: self.parameter_event_info.iter(),
38+
}
39+
}
40+
}
41+
42+
pub(crate) struct StandaloneParameterEventIterator<'a> {
43+
event_info_iterator: btree_map::Iter<'a, ParameterId, ParameterEventInfo>,
44+
}
45+
46+
impl Iterator for StandaloneParameterEventIterator<'_> {
47+
type Item = Event;
48+
49+
fn next(&mut self) -> Option<Self::Item> {
50+
loop {
51+
let (&id, info) = self.event_info_iterator.next()?;
52+
53+
if info.changed.swap(false, Ordering::AcqRel) {
54+
return Some(Event::ParameterValue {
55+
sample_offset: 0,
56+
id,
57+
value: info.value.load(Ordering::Acquire),
58+
});
59+
}
60+
}
61+
}
62+
}

0 commit comments

Comments
 (0)