Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
.DS_Store
.vscode
.zed
justfile
Cargo.lock
target
8 changes: 8 additions & 0 deletions examples/gain-plugin/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,17 @@ name = "gain-plugin"
version = "0.1.0"
edition = "2024"

[features]
standalone = ["plinth-plugin/standalone"]

[lib]
crate-type = ["cdylib", "lib", "staticlib"]

[[bin]]
name = "gain-standalone"
path = "src/main.rs"
required-features = ["standalone"]

[dependencies]
plinth-derive.workspace = true
plinth-plugin.workspace = true
Expand Down
27 changes: 27 additions & 0 deletions examples/gain-plugin/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Gain Plugin Example

A minimal example audio effect plugin demonstrating the `plugin-things` framework.

## Building

> Append `--release` to any command below for a release build.

### Plugin Bundles (CLAP & VST3)

```sh
cargo xtask bundle gain-plugin
```

### Standalone App

```sh
cargo run -p gain-plugin --features standalone
```

### Standalone App with Live Preview

Hot-reload the Slint UI when modifying .slint UI files without recompiling the app.

```sh
SLINT_LIVE_PREVIEW=1 cargo run -p gain-plugin --features=standalone,slint/live-preview
```
2 changes: 2 additions & 0 deletions examples/gain-plugin/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@ mod parameters;
mod plugin;
mod processor;
mod view;

pub use plugin::GainPlugin;
6 changes: 6 additions & 0 deletions examples/gain-plugin/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
use plinth_plugin::standalone::run_standalone;
use gain_plugin::GainPlugin;

fn main() {
run_standalone::<GainPlugin>();
}
5 changes: 4 additions & 1 deletion examples/gain-plugin/src/plugin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use crate::editor::{EditorSettings, GainPluginEditor};
use crate::{parameters::GainParameters, processor::GainPluginProcessor};

#[derive(Default)]
struct GainPlugin {
pub struct GainPlugin {
parameters: Rc<GainParameters>,
editor_settings: Rc<RefCell<EditorSettings>>,
}
Expand Down Expand Up @@ -88,3 +88,6 @@ impl Vst3Plugin for GainPlugin {

export_clap!(GainPlugin);
export_vst3!(GainPlugin);

#[cfg(feature = "standalone")]
impl plinth_plugin::standalone::StandalonePlugin for GainPlugin {}
7 changes: 7 additions & 0 deletions plinth-plugin/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ readme = "README.md"
repository = "https://github.com/ilmai/plugin-things"
license = "MIT"

[features]
standalone = ["dep:cpal", "dep:midir", "dep:winit"]

[dependencies]
atomic_refcell = "0.1"
clap-sys = "0.5"
Expand All @@ -22,6 +25,10 @@ thiserror = "2"
vst3 = "0.3"
widestring = "1"
xxhash-rust = { version = "0.8", features = ["xxh32"] }
# standalone features
winit = { version = "0.30", optional = true }
cpal = { version = "0.17", optional = true, features = ["asio", "jack"] }
midir = { version = "0.11", optional = true }

[build-dependencies]
bindgen = "0.72"
Expand Down
6 changes: 6 additions & 0 deletions plinth-plugin/src/formats.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,16 @@ use std::fmt::Display;
#[cfg(target_os="macos")]
pub mod auv3;
pub mod clap;
#[cfg(feature = "standalone")]
pub mod standalone;
pub mod vst3;

#[derive(Clone, Copy, Debug)]
pub enum PluginFormat {
Auv3,
Clap,
#[cfg(feature = "standalone")]
Standalone,
Vst3,
}

Expand All @@ -17,6 +21,8 @@ impl Display for PluginFormat {
match self {
PluginFormat::Auv3 => f.write_str("AUv3"),
PluginFormat::Clap => f.write_str("CLAP"),
#[cfg(feature = "standalone")]
PluginFormat::Standalone => f.write_str("Standalone"),
PluginFormat::Vst3 => f.write_str("VST3"),
}
}
Expand Down
11 changes: 11 additions & 0 deletions plinth-plugin/src/formats/standalone.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
mod audio;
mod config;
mod host;
mod midi;
mod parameters;
mod plugin;
mod runner;

pub use config::{AudioDeviceDriver, AudioOutputConfig, MidiInputConfig};
pub use plugin::StandalonePlugin;
pub use runner::{run_standalone, run_standalone_with_config};
115 changes: 115 additions & 0 deletions plinth-plugin/src/formats/standalone/audio.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
use std::sync::{Arc, mpsc::Receiver};

use cpal::{FromSample, Sample};
use plinth_core::{ buffers::buffer::Buffer, signals::{ signal::{Signal, SignalMut}, signal_base::SignalBase } };

use super::parameters::StandaloneParameterEventMap;
use super::plugin::StandalonePlugin;
use crate::{Event, Processor};

/// Push events to a event list vec, printing a warning when preallocated memory exceeded.
trait EventListPush {
type EventType;
fn push_event(&mut self, event: Self::EventType);
}

impl EventListPush for Vec<Event> {
type EventType = Event;
fn push_event(&mut self, event: Event) {
if self.len() == self.capacity() {
log::warn!(
"Event queue exceeded preallocated capacity of {} - allocating more. \
Increase EVENT_QUEUE_LEN to avoid allocation on the audio thread.",
self.capacity()
);
self.reserve(128);
}
self.push(event);
}
}

/// Runs a plinth processor on a CPAL audio stream
pub struct AudioState<P: StandalonePlugin> {
pub processor: P::Processor,
pub buffer: Buffer,
pub channels: usize,
pub midi_receiver: Receiver<Event>,
pub parameter_event_map: Arc<StandaloneParameterEventMap>,
pending_events: Vec<Event>,
}

impl<P: StandalonePlugin> AudioState<P> {
pub fn new(
processor: P::Processor,
channels: usize,
midi_receiver: Receiver<Event>,
parameter_event_map: Arc<StandaloneParameterEventMap>,
) -> Self {
Self {
processor,
buffer: Buffer::new(channels, P::MAX_BLOCK_SIZE),
channels,
midi_receiver,
parameter_event_map,
pending_events: Vec::with_capacity(P::EVENT_QUEUE_LEN),
}
}

pub fn process<T>(&mut self, data: &mut [T], channels: usize)
where
T: Sample + FromSample<f32>,
f32: FromSample<T>,
{
let frame_count = data.len() / channels;

// Drain MIDI events
self.pending_events.clear();
while let Ok(event) = self.midi_receiver.try_recv() {
self.pending_events.push_event(event);
}

// Collect pending parameter change events
for event in self.parameter_event_map.iter_events() {
self.pending_events.push_event(event);
}

// Process audio, ensuring we don't call process with more than P::MAX_BLOCK_SIZE frames
debug_assert!(
self.buffer.capacity() == P::MAX_BLOCK_SIZE,
"Buffer must be preallocated to avoid allocation on the audio thread"
);

let mut frame_offset = 0;
while frame_offset < frame_count {
let chunk_size = (frame_count - frame_offset).min(P::MAX_BLOCK_SIZE);

// Truncate or extend buffer to fit the chunk
if self.buffer.len() != chunk_size {
self.buffer.resize(chunk_size);
Comment thread
ilmai marked this conversation as resolved.
}

// Deinterleave chunk from CPAL buffer
for frame in 0..chunk_size {
for ch in 0..self.channels {
self.buffer.channel_mut(ch)[frame] =
f32::from_sample(data[(frame_offset + frame) * self.channels + ch]);
}
}

// Process and drain all events on first run, assuming they have no time tags
let aux: Option<&Buffer> = None;
self.processor
.process(&mut self.buffer, aux, None, self.pending_events.drain(..));

// Reinterleave chunk back into CPAL buffer
for frame in 0..chunk_size {
for ch in 0..self.channels {
data[(frame_offset + frame) * self.channels + ch] =
T::from_sample(self.buffer.channel(ch)[frame]);
}
}

frame_offset += chunk_size;
}
}
}
Loading