Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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;
3 changes: 3 additions & 0 deletions examples/gain-plugin/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
use gain_plugin::GainPlugin;

plinth_plugin::export_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
12 changes: 12 additions & 0 deletions plinth-plugin/src/formats/standalone.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
mod audio;
mod config;
mod host;
mod macros;
mod midi;
mod parameters;
mod plugin;
mod runner;

pub use config::{AudioDeviceDriver, AudioOutputConfig, MidiInputConfig};
pub use plugin::StandalonePlugin;
pub use runner::run_standalone;
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 saudio thread"
Comment thread
ilmai marked this conversation as resolved.
Outdated
);

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