Skip to content
This repository was archived by the owner on Apr 17, 2026. It is now read-only.

Commit 8484d30

Browse files
committed
remove sync requirement for nih_plug gui state, update nih_plug example
1 parent 68796c1 commit 8484d30

4 files changed

Lines changed: 197 additions & 16 deletions

File tree

examples/nih_plug_gain_egui/Cargo.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,9 @@ nih_plug = { workspace = true, default-features = true, features = [
2222
] }
2323
nih_plug_egui = { path = "../../nih_plug_egui" }
2424
atomic_float = "1.1"
25+
# Optional, only include if you want to pass messages between the
26+
# GUI and the audio thread.
27+
rtrb = "0.3"
28+
# Optional, only include if you want to sync complex state between
29+
# the GUI and the audio thread.
30+
triple_buffer = "8.1"

examples/nih_plug_gain_egui/src/lib.rs

Lines changed: 183 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,17 @@ use nih_plug_egui::{
77
};
88
use std::sync::Arc;
99

10+
const MIN_WINDOW_WIDTH: u32 = 300;
11+
const MIN_WINDOW_HEIGHT: u32 = 220;
12+
1013
/// The time it takes for the peak meter to decay by 12 dB after switching to complete silence.
1114
const PEAK_METER_DECAY_MS: f64 = 150.0;
1215

13-
/// This is mostly identical to the gain example, minus some fluff, and with a GUI.
16+
/// If you are using message channels, then allocate enough capacity for the expected worse case
17+
/// scenario for your plugin.
18+
const GUI_TO_AUDIO_MSG_CHANNEL_CAPACITY: usize = 512;
19+
const AUDIO_TO_GUI_MSG_CHANNEL_CAPACITY: usize = 128;
20+
1421
pub struct Gain {
1522
params: Arc<GainParams>,
1623

@@ -22,6 +29,25 @@ pub struct Gain {
2229
///
2330
/// This is stored as voltage gain.
2431
peak_meter: Arc<AtomicF32>,
32+
33+
/// A message channel to send events between the GUI and the audio thread.
34+
///
35+
/// This is optional. If you don't need to pass events, you can omit this field.
36+
msg_channel: AudioMsgChannel,
37+
/// Used to demonstrate how to pass heap-allocated data from the GUI to the audio thread.
38+
heap_data_example: Vec<f32>,
39+
40+
/// State that is synced between the GUI and the audio thread using a triple buffer.
41+
/// This can be used as an alternative to the message channel approach. Note, the roles of which
42+
/// thread has the input and which has the output can be reversed.
43+
///
44+
/// The downside to this approach is that it takes 3x the memory.
45+
///
46+
/// This is optional. If you don't need this, you can omit it.
47+
triple_buffer_state: triple_buffer::Output<TripleBufferState>,
48+
49+
/// Temporarily hold on to the initial GUI state until the editor is first opened.
50+
initial_gui_state: Option<GuiState>,
2551
}
2652

2753
#[derive(Params)]
@@ -41,19 +67,43 @@ pub struct GainParams {
4167

4268
impl Default for Gain {
4369
fn default() -> Self {
70+
let (to_audio_tx, from_gui_rx) = rtrb::RingBuffer::new(GUI_TO_AUDIO_MSG_CHANNEL_CAPACITY);
71+
let (to_gui_tx, from_audio_rx) = rtrb::RingBuffer::new(AUDIO_TO_GUI_MSG_CHANNEL_CAPACITY);
72+
73+
let (triple_buffer_input, triple_buffer_output) =
74+
triple_buffer::triple_buffer(&TripleBufferState::default());
75+
4476
Self {
4577
params: Arc::new(GainParams::default()),
4678

4779
peak_meter_decay_weight: 1.0,
4880
peak_meter: Arc::new(AtomicF32::new(util::MINUS_INFINITY_DB)),
81+
82+
msg_channel: AudioMsgChannel {
83+
to_gui_tx,
84+
from_gui_rx,
85+
msg_sent: false,
86+
},
87+
heap_data_example: Vec::new(),
88+
89+
triple_buffer_state: triple_buffer_output,
90+
91+
initial_gui_state: Some(GuiState {
92+
msg_channel: GuiMsgChannel {
93+
to_audio_tx,
94+
from_audio_rx,
95+
},
96+
triple_buffer_state: triple_buffer_input,
97+
next_value: 0,
98+
}),
4999
}
50100
}
51101
}
52102

53103
impl Default for GainParams {
54104
fn default() -> Self {
55105
Self {
56-
editor_state: EguiState::from_size(300, 180),
106+
editor_state: EguiState::from_size(MIN_WINDOW_WIDTH, MIN_WINDOW_HEIGHT),
57107

58108
// See the main gain example for more details
59109
gain: FloatParam::new(
@@ -74,6 +124,71 @@ impl Default for GainParams {
74124
}
75125
}
76126

127+
/// Here you can store any state you need for your GUI.
128+
///
129+
/// This state persists across editor openings.
130+
pub struct GuiState {
131+
/// A message channel to send events between the GUI and the audio thread.
132+
///
133+
/// This is optional. If you don't need to pass events, you can omit this field.
134+
msg_channel: GuiMsgChannel,
135+
136+
/// State that is synced between the GUI and the audio thread using a triple buffer.
137+
/// This can be used as an alternative the message channel approach. Note, the roles of which
138+
/// thread has the input and which has the output can be reversed.
139+
///
140+
/// The downside to this approach is that it takes 3x the memory.
141+
///
142+
/// This is optional. If you don't need this, you can omit it.
143+
triple_buffer_state: triple_buffer::Input<TripleBufferState>,
144+
next_value: u64,
145+
}
146+
147+
/// A message channel to send events between the GUI and the audio thread.
148+
///
149+
/// This is optional. If you don't need to pass events, you can omit this.
150+
pub struct GuiMsgChannel {
151+
/// A message channel to send events from the GUI to the audio thread.
152+
to_audio_tx: rtrb::Producer<GuiToAudioMsg>,
153+
/// A message channel to receive events from the audio thread.
154+
from_audio_rx: rtrb::Consumer<AudioToGuiMsg>,
155+
}
156+
/// A message channel to send events between the GUI and the audio thread.
157+
///
158+
/// This is optional. If you don't need to pass events, you can omit this.
159+
pub struct AudioMsgChannel {
160+
/// A message channel to send events from the audio thread to the GUI thread.
161+
to_gui_tx: rtrb::Producer<AudioToGuiMsg>,
162+
/// A message channel to receive events from the GUI thread.
163+
from_gui_rx: rtrb::Consumer<GuiToAudioMsg>,
164+
msg_sent: bool,
165+
}
166+
167+
#[derive(Debug)]
168+
pub enum GuiToAudioMsg {
169+
MessageA,
170+
MessageWithHeapData(Vec<f32>),
171+
}
172+
#[derive(Debug)]
173+
pub enum AudioToGuiMsg {
174+
MessageA,
175+
DropOldHeapData(Vec<f32>),
176+
}
177+
178+
/// State that is synced between the GUI and the audio thread using a triple buffer.
179+
/// This can be used as an alternative the message channel approach. Note, the roles
180+
/// of which thread has the input and which has the output can be reversed.
181+
///
182+
/// The downside to this approach is that it takes 3x the memory.
183+
///
184+
/// This is optional. If you don't need this, you can omit it.
185+
#[derive(Debug, Default, Clone)]
186+
pub struct TripleBufferState {
187+
value_a: bool,
188+
value_b: u64,
189+
some_data: Vec<u32>,
190+
}
191+
77192
impl Plugin for Gain {
78193
const NAME: &'static str = "Gain (nih_plug_egui)";
79194
const VENDOR: &'static str = "Moist Plugins GmbH";
@@ -108,17 +223,16 @@ impl Plugin for Gain {
108223
let params = self.params.clone();
109224
let peak_meter = self.peak_meter.clone();
110225
let egui_state = params.editor_state.clone();
226+
111227
create_egui_editor(
112228
self.params.editor_state.clone(),
113-
(),
229+
self.initial_gui_state.take().unwrap(),
114230
Default::default(),
115-
|_, _, _| {},
116-
move |egui_ctx, setter, _queue, _state| {
231+
|_egui_ctx, _queue, _gui_state| {},
232+
move |egui_ctx, setter, _queue, gui_state| {
117233
ResizableWindow::new("res-wind")
118-
.min_size(Vec2::new(128.0, 128.0))
234+
.min_size(Vec2::new(MIN_WINDOW_WIDTH as f32, MIN_WINDOW_HEIGHT as f32))
119235
.show(egui_ctx, egui_state.as_ref(), |ui| {
120-
// NOTE: See `plugins/diopser/src/editor.rs` for an example using the generic UI widget
121-
122236
// This is a fancy widget that can get all the information it needs to properly
123237
// display and modify the parameter from the parametr itself
124238
// It's not yet fully implemented, as the text is missing.
@@ -161,6 +275,25 @@ impl Plugin for Gain {
161275
egui::widgets::ProgressBar::new(peak_meter_normalized)
162276
.text(peak_meter_text),
163277
);
278+
279+
// Demonstrate sending a message to the audio thread.
280+
if ui.button("send message").clicked() {
281+
if let Err(e) = gui_state.msg_channel.to_audio_tx.push(GuiToAudioMsg::MessageA) {
282+
nih_error!("Failed to send message to audio thread: {}", e);
283+
}
284+
}
285+
// Demonstrate receiving messages from the audio thread.
286+
while let Ok(msg) = gui_state.msg_channel.from_audio_rx.pop() {
287+
nih_log!("Got message from audio thread: {:?}", &msg);
288+
}
289+
290+
// Demonstrate mutating synced triple buffer state.
291+
if ui.button("mutate synced state").clicked() {
292+
gui_state.next_value += 1;
293+
// Note, `triple_buffer_state.input_buffer_mut()` will not work for syncing state
294+
// this way. You must always completely overwrite the state with new data.
295+
gui_state.triple_buffer_state.write(TripleBufferState { value_a: false, value_b: gui_state.next_value, some_data: Vec::new() });
296+
}
164297
});
165298
},
166299
)
@@ -187,6 +320,48 @@ impl Plugin for Gain {
187320
_aux: &mut AuxiliaryBuffers,
188321
_context: &mut impl ProcessContext<Self>,
189322
) -> ProcessStatus {
323+
// Demonstrate receiving messages from the GUI thread.
324+
while let Ok(msg) = self.msg_channel.from_gui_rx.pop() {
325+
match msg {
326+
GuiToAudioMsg::MessageA => {
327+
nih_dbg!("Got MessageA from GUI");
328+
}
329+
GuiToAudioMsg::MessageWithHeapData(mut heap_data) => {
330+
nih_dbg!("Got MessageWithHeapData from GUI");
331+
332+
// Replace the old heap data with the new data.
333+
std::mem::swap(&mut self.heap_data_example, &mut heap_data);
334+
335+
// Note, you must be careful not to drop heap-allocated data on the audio
336+
// thread. Send the old data back to the GUI thread to be deallocated there.
337+
if let Err(e) = self
338+
.msg_channel
339+
.to_gui_tx
340+
.push(AudioToGuiMsg::DropOldHeapData(heap_data))
341+
{
342+
nih_error!("Failed to send message to GUI thread: {}", e);
343+
}
344+
}
345+
}
346+
}
347+
348+
// Demonstrate sending messages to the GUI thread.
349+
if self.params.editor_state.is_open() && !self.msg_channel.msg_sent {
350+
if let Err(e) = self.msg_channel.to_gui_tx.push(AudioToGuiMsg::MessageA) {
351+
nih_error!("Failed to send message to GUI thread: {}", e);
352+
}
353+
354+
// Only send the example message once to avoid spamming the GUI.
355+
self.msg_channel.msg_sent = true;
356+
}
357+
358+
// Demonstrate triple buffer usage.
359+
let state = self.triple_buffer_state.read();
360+
// Use the state somehow...
361+
let _ = &state.value_a;
362+
let _ = &state.value_b;
363+
let _ = &state.some_data;
364+
190365
for channel_samples in buffer.iter_samples() {
191366
let mut amplitude = 0.0;
192367
let num_samples = channel_samples.len();

nih_plug_egui/src/editor.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use egui_baseview::egui::Context;
1111
use egui_baseview::EguiWindow;
1212
use egui_baseview::Queue;
1313
use nih_plug::prelude::{Editor, GuiContext, ParamSetter, ParentWindowHandle};
14-
use parking_lot::RwLock;
14+
use parking_lot::Mutex;
1515
use raw_window_handle::{HasRawWindowHandle, RawWindowHandle};
1616
use std::sync::atomic::Ordering;
1717
use std::sync::Arc;
@@ -20,7 +20,7 @@ use std::sync::Arc;
2020
pub(crate) struct EguiEditor<T> {
2121
pub(crate) egui_state: Arc<EguiState>,
2222
/// The plugin's state. This is kept in between editor openenings.
23-
pub(crate) user_state: Arc<RwLock<T>>,
23+
pub(crate) user_state: Arc<Mutex<T>>,
2424

2525
pub(crate) settings: Arc<EguiSettings>,
2626

@@ -63,7 +63,7 @@ unsafe impl HasRawWindowHandle for ParentWindowHandleAdapter {
6363

6464
impl<T> Editor for EguiEditor<T>
6565
where
66-
T: 'static + Send + Sync,
66+
T: 'static + Send,
6767
{
6868
fn spawn(
6969
&self,
@@ -111,7 +111,7 @@ where
111111
},
112112
self.settings.graphics_config.clone(),
113113
state,
114-
move |egui_ctx, queue, state| build(egui_ctx, queue, &mut state.write()),
114+
move |egui_ctx, queue, state| build(egui_ctx, queue, &mut state.lock()),
115115
move |egui_ctx, queue, state| {
116116
let setter = ParamSetter::new(context.as_ref());
117117

@@ -141,7 +141,7 @@ where
141141
// this we would also have a blank GUI when it gets first opened because most DAWs open
142142
// their GUI while the window is still unmapped.
143143
egui_ctx.request_repaint();
144-
(update)(egui_ctx, &setter, queue, &mut state.write());
144+
(update)(egui_ctx, &setter, queue, &mut state.lock());
145145
},
146146
);
147147

nih_plug_egui/src/lib.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use crossbeam::atomic::AtomicCell;
99
use egui::Context;
1010
use nih_plug::params::persist::PersistentField;
1111
use nih_plug::prelude::{Editor, ParamSetter};
12-
use parking_lot::RwLock;
12+
use parking_lot::Mutex;
1313
use serde::{Deserialize, Serialize};
1414
use std::sync::atomic::{AtomicBool, Ordering};
1515
use std::sync::Arc;
@@ -88,13 +88,13 @@ pub fn create_egui_editor<T, B, U>(
8888
update: U,
8989
) -> Option<Box<dyn Editor>>
9090
where
91-
T: 'static + Send + Sync,
91+
T: 'static + Send,
9292
B: Fn(&Context, &mut Queue, &mut T) + 'static + Send + Sync,
9393
U: Fn(&Context, &ParamSetter, &mut Queue, &mut T) + 'static + Send + Sync,
9494
{
9595
Some(Box::new(editor::EguiEditor {
9696
egui_state,
97-
user_state: Arc::new(RwLock::new(user_state)),
97+
user_state: Arc::new(Mutex::new(user_state)),
9898
settings: Arc::new(settings),
9999
build: Arc::new(build),
100100
update: Arc::new(update),

0 commit comments

Comments
 (0)