Skip to content
Open
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
4,739 changes: 4,177 additions & 562 deletions Cargo.lock

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ zstd = "0.13.3"
[patch."https://github.com/robbert-vdh/vst3-sys.git"]
vst3-sys = { git = "https://github.com/RustAudio/vst3-sys", optional = true }

# [patch."https://github.com/RustAudio/baseview.git"]
# baseview = { git = "https://github.com/blepfx/baseview.git", branch = "x11-fix-mods" }

# [patch."https://github.com/vizia/baseview.git"]
# baseview = { git = "https://github.com/blepfx/baseview.git", branch = "x11-fix-mods" }

[profile.release]
lto = "thin"
strip = "symbols"
Expand Down
3 changes: 3 additions & 0 deletions crates/bells/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,13 @@ crate-type = ["cdylib"]
[dependencies]
common = { workspace = true }
config = { workspace = true, features = ["macro"] }
crossbeam = "0.8"
cyma = { git = "https://github.com/exa04/cyma.git", branch = "vizia_plug" }
engine = { workspace = true }
fx = { workspace = true }
nih_plug = { workspace = true }
rkyv = { workspace = true }
rustc-hash = "2.1.1"
strum = { workspace = true }
vizia_plug = { git = "https://github.com/vizia/vizia-plug.git" }
zstd = { workspace = true }
71 changes: 71 additions & 0 deletions crates/bells/src/editor.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
use std::sync::Arc;

use cyma::bus::{Bus, MonoBus};
use cyma::prelude::Oscilloscope;
use cyma::prelude::ValueScaling;
use nih_plug::prelude::{Editor, Enum};
use strum::VariantNames;
use vizia_plug::vizia::prelude::*;
use vizia_plug::widgets::*;
use vizia_plug::{create_vizia_editor, ViziaState, ViziaTheming};

use crate::{BellsParams, Presets};

#[derive(Lens)]
struct Data {
params: Arc<BellsParams>,
presets: Vec<&'static str>,
selected_option: usize,
}

enum EditorEvent {
SetPreset(usize),
}

impl Model for Data {
fn event(&mut self, cx: &mut EventContext, event: &mut Event) {
event.map(|editor_event, _| match editor_event {
EditorEvent::SetPreset(index) => {
self.selected_option = *index;

cx.emit(
ParamEvent::SetParameter(&self.params.preset, Presets::from_index(*index))
.upcast(),
);
}
});
}
}

pub(crate) fn default_state() -> Arc<ViziaState> {
ViziaState::new(|| (1000, 800))
}

pub(crate) fn create(
params: Arc<BellsParams>,
editor_state: Arc<ViziaState>,
bus: Arc<MonoBus>,
) -> Option<Box<dyn Editor>> {
create_vizia_editor(editor_state, ViziaTheming::Custom, move |cx, _| {
Data {
params: params.clone(),
presets: Presets::VARIANTS.to_vec(),
selected_option: params.preset.value().to_index(),
}
.build(cx);

bus.subscribe(cx);

VStack::new(cx, |cx| {
ComboBox::new(cx, Data::presets, Data::selected_option)
.on_select(|cx, index| {
cx.emit(EditorEvent::SetPreset(index));
})
.width(Pixels(100.0));

Oscilloscope::new(cx, bus.clone(), 4.0, (-1.0, 1.0), ValueScaling::Linear)
.color(Color::rgb(120, 120, 120));
})
.alignment(Alignment::TopCenter);
})
}
6 changes: 6 additions & 0 deletions crates/bells/src/instrument.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,10 @@ impl Instrument {
.collect(),
}
}

/// Clears the instrument's name and samples.
pub fn clear(&mut self) {
self.name.clear();
self.samples.clear();
}
}
162 changes: 119 additions & 43 deletions crates/bells/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,29 +1,43 @@
#![allow(non_snake_case, non_upper_case_globals)]
use std::sync::{
atomic::{AtomicBool, Ordering},
Arc,
};
use std::sync::Arc;

use crossbeam::channel::{unbounded, Receiver, Sender};
use cyma::bus::{Bus, MonoBus};
use engine::{Adsr, Voice};
use instrument::Instrument;
use nih_plug::prelude::*;
use presets::Presets;
use vizia_plug::ViziaState;

mod editor;
pub mod instrument;
mod presets;

const DEFAULT_ATTACK_S: f32 = 0.01;
const DEFAULT_ATTACK_S: f32 = 0.0;
const DEFAULT_DECAY_S: f32 = 0.1;
const DEFAULT_SUSTAIN_LEVEL: f32 = 1.0;
const DEFAULT_RELEASE_S: f32 = 0.2;
const ORIGINAL_SAMPLE_RATE: f32 = 44100.0;

enum Task {
LoadPreset { preset: Presets, sample_rate: f32 },
}

enum Command {
LoadPreset(Presets),
}

struct Bells {
params: Arc<BellsParams>,
voices: Vec<Voice>,
instrument: Instrument,
sample_rate: f32,
adsr: Adsr,
Copy link

Copilot AI Oct 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The bus field lacks documentation. Add a doc comment explaining its purpose, such as '/// Audio bus for sending samples to the editor's oscilloscope visualization.'

Suggested change
adsr: Adsr,
adsr: Adsr,
/// Audio bus for sending samples to the editor's oscilloscope visualization.

Copilot uses AI. Check for mistakes.
bus: Arc<MonoBus>,
/// Receives the loaded `Instrument` from the background task.
task_channel: (Sender<Instrument>, Receiver<Instrument>),
/// Sends/Receives commands to/on the audio thread.
command_channel: (Sender<Command>, Receiver<Command>),
}

#[derive(Params)]
Expand All @@ -40,28 +54,29 @@ struct BellsParams {
pub release: FloatParam,
#[id = "preset"]
pub preset: EnumParam<Presets>,
// This flag is used to signal the audio thread that the preset has changed.
pub preset_change: Arc<AtomicBool>,
#[persist = "editor-state"]
editor_state: Arc<ViziaState>,
}

impl Default for Bells {
fn default() -> Self {
let sample_rate = 44100.0;
let command_channel = unbounded();

Self {
params: Arc::new(BellsParams::default()),
params: Arc::new(BellsParams::new(command_channel.0.clone())),
voices: Vec::new(),
instrument: Instrument::default(),
sample_rate,
adsr: Adsr::new(sample_rate),
sample_rate: ORIGINAL_SAMPLE_RATE,
adsr: Adsr::new(ORIGINAL_SAMPLE_RATE),
bus: Default::default(),
task_channel: unbounded(),
command_channel,
}
}
}

impl Default for BellsParams {
fn default() -> Self {
let preset_change = Arc::new(AtomicBool::new(false));

impl BellsParams {
fn new(command_sender: Sender<Command>) -> Self {
Self {
gain: FloatParam::new(
"Gain",
Expand Down Expand Up @@ -113,13 +128,12 @@ impl Default for BellsParams {
},
)
.with_unit(" s"),
preset: EnumParam::new("Preset", Presets::default()).with_callback({
let preset_change = preset_change.clone();
Arc::new(move |_| {
preset_change.store(true, Ordering::Relaxed);
})
}),
preset_change,
preset: EnumParam::new("Preset", Presets::default()).with_callback(Arc::new(
move |preset_value| {
command_sender.send(Command::LoadPreset(preset_value)).ok();
},
)),
editor_state: editor::default_state(),
}
}
}
Expand Down Expand Up @@ -149,25 +163,63 @@ impl Plugin for Bells {

type SysExMessage = ();

type BackgroundTask = ();
type BackgroundTask = Task;

fn task_executor(&mut self) -> TaskExecutor<Self> {
let sender = self.task_channel.0.clone();
Box::new(move |task| match task {
Task::LoadPreset {
preset,
sample_rate,
} => {
nih_log!("Starting background task: LoadPreset for {:?}", preset);
let instrument = Bells::load_instrument_data(preset, sample_rate);
nih_log!("Finished loading instrument in background task.");
if let Err(err) = sender.send(instrument) {
nih_log!(
"Failed to send loaded instrument from background task: {}",
err
);
} else {
nih_log!("Successfully sent instrument to audio thread.");
}
}
})
}

fn params(&self) -> Arc<dyn Params> {
self.params.clone()
}

fn editor(&mut self, _async_executor: AsyncExecutor<Self>) -> Option<Box<dyn Editor>> {
editor::create(
self.params.clone(),
self.params.editor_state.clone(),
self.bus.clone(),
)
}

fn initialize(
&mut self,
_audio_io_layout: &AudioIOLayout,
buffer_config: &BufferConfig,
_context: &mut impl InitContext<Self>,
) -> bool {
if self.instrument.samples.is_empty() || self.sample_rate != buffer_config.sample_rate {
self.sample_rate = buffer_config.sample_rate;
self.load_preset(self.params.preset.value());
}

nih_log!("Initializing Bells plugin.");
self.sample_rate = buffer_config.sample_rate;
self.bus.set_sample_rate(self.sample_rate);
self.adsr = Adsr::new(self.sample_rate);

self.voices.clear();

// Signal the `process` function to load the initial preset.
// This is non-blocking. Use the sender (index 0).
self.command_channel
.0
.send(Command::LoadPreset(self.params.preset.value()))
.ok();
nih_log!("Initialization complete. Preset load task signaled for process loop.");

true
}

Expand All @@ -181,6 +233,30 @@ impl Plugin for Bells {
_aux: &mut AuxiliaryBuffers,
context: &mut impl ProcessContext<Self>,
) -> ProcessStatus {
// Check for commands from the UI/param callback.
while let Ok(command) = self.command_channel.1.try_recv() {
match command {
Command::LoadPreset(preset) => {
nih_log!(
"Preset change command received. Clearing voices and starting background load."
);
self.instrument.clear();
self.voices.clear();
context.execute_background(Task::LoadPreset {
preset,
sample_rate: self.sample_rate,
});
}
}
}

// Check if a new instrument has been loaded by a background task.
if let Ok(new_instrument) = self.task_channel.1.try_recv() {
nih_log!("New instrument received. Swapping and clearing voices.");
self.instrument = new_instrument;
self.voices.clear();
}

let mut next_event = context.next_event();

// Update ADSR parameters from the plugin's state.
Expand All @@ -192,7 +268,6 @@ impl Plugin for Bells {
);

for (sample_id, channel_samples) in buffer.iter_samples().enumerate() {
// Process MIDI events for this sample.
while let Some(event) = next_event {
if event.timing() > sample_id as u32 {
break;
Expand Down Expand Up @@ -240,27 +315,28 @@ impl Plugin for Bells {
// Remove voices that are no longer active.
self.voices.retain(|v| v.is_active());

// Check if the preset has been changed on the GUI thread.
if self.params.preset_change.swap(false, Ordering::Relaxed) {
self.load_preset(self.params.preset.value());
if self.params.editor_state.is_open() {
self.bus.send_buffer_summing(buffer);
}

ProcessStatus::Normal
}
}

impl Bells {
pub fn load_preset(&mut self, preset: Presets) {
self.voices.clear();

fn load_instrument_data(preset: Presets, sample_rate: f32) -> Instrument {
let instrument_data = preset.content().to_vec();

// Step 1: Decode the instrument data.
let mut instrument = std::thread::spawn(move || Instrument::decode(instrument_data))
.join()
.expect("Failed to load preset on a different thread");
nih_log!("Decoding instrument data for {:?}.", preset);
let mut instrument = Instrument::decode(instrument_data);
nih_log!("Finished decoding.");

if self.sample_rate != ORIGINAL_SAMPLE_RATE {
if sample_rate != ORIGINAL_SAMPLE_RATE {
nih_log!(
"Resampling instrument from {} Hz to {} Hz.",
ORIGINAL_SAMPLE_RATE,
sample_rate
);
instrument.samples = instrument
.samples
.into_iter()
Expand All @@ -270,14 +346,15 @@ impl Bells {
Arc::new(common::resampler::resample(
&v,
ORIGINAL_SAMPLE_RATE,
self.sample_rate,
sample_rate,
)),
)
})
.collect();
nih_log!("Finished resampling.");
}

self.instrument = instrument;
instrument
}
}

Expand All @@ -287,7 +364,6 @@ impl ClapPlugin for Bells {
const CLAP_MANUAL_URL: Option<&'static str> = Some(Self::URL);
const CLAP_SUPPORT_URL: Option<&'static str> = Some(Self::URL);

// Don't forget to change these features
const CLAP_FEATURES: &'static [ClapFeature] = &[ClapFeature::Sampler, ClapFeature::Instrument];
}

Expand Down
Loading