diff --git a/plugin/examples/polysynth/Cargo.toml b/plugin/examples/polysynth/Cargo.toml index d47c5fe5..ee2d7b29 100644 --- a/plugin/examples/polysynth/Cargo.toml +++ b/plugin/examples/polysynth/Cargo.toml @@ -8,4 +8,4 @@ crate-type = ["rlib", "cdylib"] [dependencies] clack-plugin = { workspace = true } -clack-extensions = { workspace = true, features = ["audio-ports", "clack-plugin", "note-ports", "params"] } +clack-extensions = { workspace = true, features = ["audio-ports", "clack-plugin", "note-ports", "params", "state"] } diff --git a/plugin/examples/polysynth/src/lib.rs b/plugin/examples/polysynth/src/lib.rs index 340e0b8d..f093e943 100644 --- a/plugin/examples/polysynth/src/lib.rs +++ b/plugin/examples/polysynth/src/lib.rs @@ -2,19 +2,22 @@ #![doc = include_str!("../README.md")] // #![deny(missing_docs, clippy::missing_docs_in_private_items, unsafe_code)] +use crate::params::PolySynthParams; use crate::poly_oscillator::PolyOscillator; +use clack_extensions::state::PluginState; use clack_extensions::{audio_ports::*, note_ports::*, params::*}; use clack_plugin::prelude::*; mod oscillator; +mod params; mod poly_oscillator; pub struct PolySynthPlugin; impl Plugin for PolySynthPlugin { - type AudioProcessor<'a> = PolySynthAudioProcessor; + type AudioProcessor<'a> = PolySynthAudioProcessor<'a>; type Shared<'a> = PolySynthPluginShared; - type MainThread<'a> = PolySynthPluginMainThread; + type MainThread<'a> = PolySynthPluginMainThread<'a>; fn get_descriptor() -> Box { use clack_plugin::plugin::descriptor::features::*; @@ -31,25 +34,29 @@ impl Plugin for PolySynthPlugin { fn declare_extensions(builder: &mut PluginExtensions, _shared: &PolySynthPluginShared) { builder .register::() - .register::(); + .register::() + .register::() + .register::(); } } -pub struct PolySynthAudioProcessor { +pub struct PolySynthAudioProcessor<'a> { poly_osc: PolyOscillator, + shared: &'a PolySynthPluginShared, } -impl<'a> PluginAudioProcessor<'a, PolySynthPluginShared, PolySynthPluginMainThread> - for PolySynthAudioProcessor +impl<'a> PluginAudioProcessor<'a, PolySynthPluginShared, PolySynthPluginMainThread<'a>> + for PolySynthAudioProcessor<'a> { fn activate( _host: HostAudioThreadHandle<'a>, _main_thread: &mut PolySynthPluginMainThread, - _shared: &'a PolySynthPluginShared, + shared: &'a PolySynthPluginShared, audio_config: AudioConfiguration, ) -> Result { Ok(Self { poly_osc: PolyOscillator::new(16, audio_config.sample_rate as f32), + shared, }) } @@ -76,11 +83,14 @@ impl<'a> PluginAudioProcessor<'a, PolySynthPluginShared, PolySynthPluginMainThre for event_batch in events.input.batch() { for event in event_batch.events() { - self.poly_osc.process_event(event) + self.poly_osc.handle_event(event); + self.shared.params.handle_event(event); } + let volume = self.shared.params.get_volume(); + let output_buffer = &mut output_buffer[event_batch.sample_bounds()]; - self.poly_osc.generate_next_samples(output_buffer) + self.poly_osc.generate_next_samples(output_buffer, volume); } // If somehow the host didn't give us a mono output, we copy the output to all channels @@ -105,7 +115,7 @@ impl<'a> PluginAudioProcessor<'a, PolySynthPluginShared, PolySynthPluginMainThre } } -impl PluginAudioPortsImpl for PolySynthPluginMainThread { +impl<'a> PluginAudioPortsImpl for PolySynthPluginMainThread<'a> { fn count(&self, is_input: bool) -> u32 { if is_input { 0 @@ -128,7 +138,7 @@ impl PluginAudioPortsImpl for PolySynthPluginMainThread { } } -impl PluginNotePortsImpl for PolySynthPluginMainThread { +impl<'a> PluginNotePortsImpl for PolySynthPluginMainThread<'a> { fn count(&self, is_input: bool) -> u32 { if is_input { 1 @@ -149,22 +159,28 @@ impl PluginNotePortsImpl for PolySynthPluginMainThread { } } -pub struct PolySynthPluginShared; +pub struct PolySynthPluginShared { + params: PolySynthParams, +} impl<'a> PluginShared<'a> for PolySynthPluginShared { fn new(_host: HostHandle<'a>) -> Result { - Ok(Self) + Ok(Self { + params: PolySynthParams::new(), + }) } } -pub struct PolySynthPluginMainThread; +pub struct PolySynthPluginMainThread<'a> { + shared: &'a PolySynthPluginShared, +} -impl<'a> PluginMainThread<'a, PolySynthPluginShared> for PolySynthPluginMainThread { +impl<'a> PluginMainThread<'a, PolySynthPluginShared> for PolySynthPluginMainThread<'a> { fn new( _host: HostMainThreadHandle<'a>, - _shared: &'a PolySynthPluginShared, + shared: &'a PolySynthPluginShared, ) -> Result { - Ok(Self) + Ok(Self { shared }) } } diff --git a/plugin/examples/polysynth/src/oscillator.rs b/plugin/examples/polysynth/src/oscillator.rs index 53ac92d2..f96d19c0 100644 --- a/plugin/examples/polysynth/src/oscillator.rs +++ b/plugin/examples/polysynth/src/oscillator.rs @@ -36,15 +36,12 @@ impl SquareOscillator { } #[inline] - pub fn add_next_samples_to_buffer(&mut self, buf: &mut [f32]) { - // Keep enough headroom to play a few notes at once without "clipping". - const VOLUME: f32 = 0.2; - + pub fn add_next_samples_to_buffer(&mut self, buf: &mut [f32], volume: f32) { for value in buf { if self.current_phase <= PI { - *value += VOLUME; + *value += volume; } else { - *value -= VOLUME; + *value -= volume; } self.current_phase += self.phase_increment; diff --git a/plugin/examples/polysynth/src/params.rs b/plugin/examples/polysynth/src/params.rs new file mode 100644 index 00000000..32a55385 --- /dev/null +++ b/plugin/examples/polysynth/src/params.rs @@ -0,0 +1,169 @@ +use crate::{PolySynthAudioProcessor, PolySynthPluginMainThread}; +use clack_extensions::params::implementation::{ + ParamDisplayWriter, ParamInfoWriter, PluginAudioProcessorParams, PluginMainThreadParams, +}; +use clack_extensions::params::info::{ParamInfoData, ParamInfoFlags}; +use clack_extensions::state::PluginStateImpl; +use clack_plugin::events::spaces::CoreEventSpace; +use clack_plugin::events::UnknownEvent; +use clack_plugin::plugin::PluginError; +use clack_plugin::prelude::{InputEvents, OutputEvents}; +use clack_plugin::stream::{InputStream, OutputStream}; +use std::fmt::Write as _; +use std::io::{Read, Write as _}; +use std::sync::atomic::{AtomicU32, Ordering}; + +const DEFAULT_VOLUME: f32 = 0.2; + +pub struct PolySynthParams { + volume: AtomicF32, +} + +impl PolySynthParams { + pub fn new() -> Self { + Self { + volume: AtomicF32::new(DEFAULT_VOLUME), + } + } + + #[inline] + pub fn get_volume(&self) -> f32 { + self.volume.load(Ordering::SeqCst) + } + + #[inline] + pub fn set_volume(&self, new_volume: f32) { + let new_volume = new_volume.clamp(0., 1.); + self.volume.store(new_volume, Ordering::SeqCst) + } + + pub fn handle_event(&self, event: &UnknownEvent) { + if let Some(CoreEventSpace::ParamValue(event)) = event.as_core_event() { + if event.param_id() == 1 { + self.set_volume(event.value() as f32) + } + } + } +} + +impl<'a> PluginStateImpl for PolySynthPluginMainThread<'a> { + fn save(&mut self, output: &mut OutputStream) -> Result<(), PluginError> { + let volume_param = self.shared.params.get_volume(); + + output.write_all(&volume_param.to_le_bytes())?; + Ok(()) + } + + fn load(&mut self, input: &mut InputStream) -> Result<(), PluginError> { + let mut buf = [0; 4]; + input.read_exact(&mut buf)?; + let volume_value = f32::from_le_bytes(buf); + self.shared.params.set_volume(volume_value); + Ok(()) + } +} + +impl<'a> PluginMainThreadParams for PolySynthPluginMainThread<'a> { + fn count(&self) -> u32 { + 1 + } + + fn get_info(&self, param_index: u32, info: &mut ParamInfoWriter) { + if param_index != 0 { + return; + } + info.set(&ParamInfoData { + id: 1, + flags: ParamInfoFlags::IS_AUTOMATABLE, + cookie: Default::default(), + name: "Volume", + module: "", + min_value: 0.0, + max_value: 1.0, + default_value: DEFAULT_VOLUME as f64, + }) + } + + fn get_value(&self, param_id: u32) -> Option { + if param_id == 1 { + Some(self.shared.params.get_volume() as f64) + } else { + None + } + } + + fn value_to_text( + &self, + param_id: u32, + value: f64, + writer: &mut ParamDisplayWriter, + ) -> std::fmt::Result { + if param_id == 1 { + write!(writer, "{0:.2} %", value * 100.0) + } else { + Err(std::fmt::Error) + } + } + + fn text_to_value(&self, param_id: u32, text: &str) -> Option { + if param_id == 1 { + let text = text.strip_suffix(" %")?; + let value = text.parse().ok()?; + + Some(value) + } else { + None + } + } + + fn flush( + &mut self, + input_parameter_changes: &InputEvents, + _output_parameter_changes: &mut OutputEvents, + ) { + for event in input_parameter_changes { + self.shared.params.handle_event(event) + } + } +} + +impl<'a> PluginAudioProcessorParams for PolySynthAudioProcessor<'a> { + fn flush( + &mut self, + input_parameter_changes: &InputEvents, + _output_parameter_changes: &mut OutputEvents, + ) { + for event in input_parameter_changes { + self.shared.params.handle_event(event) + } + } +} + +struct AtomicF32(AtomicU32); + +impl AtomicF32 { + #[inline] + fn new(value: f32) -> Self { + Self(AtomicU32::new(f32_to_u32_bytes(value))) + } + + #[inline] + fn store(&self, new_value: f32, order: Ordering) { + self.0.store(f32_to_u32_bytes(new_value), order) + } + + #[inline] + fn load(&self, order: Ordering) -> f32 { + f32_from_u32_bytes(self.0.load(order)) + } +} + +#[inline] +fn f32_to_u32_bytes(value: f32) -> u32 { + u32::from_ne_bytes(value.to_ne_bytes()) +} + +#[inline] +fn f32_from_u32_bytes(bytes: u32) -> f32 { + f32::from_ne_bytes(bytes.to_ne_bytes()) +} diff --git a/plugin/examples/polysynth/src/poly_oscillator.rs b/plugin/examples/polysynth/src/poly_oscillator.rs index 28126155..131c6ab2 100644 --- a/plugin/examples/polysynth/src/poly_oscillator.rs +++ b/plugin/examples/polysynth/src/poly_oscillator.rs @@ -63,7 +63,7 @@ impl PolyOscillator { self.active_voice_count = 0; } - pub fn process_event(&mut self, event: &UnknownEvent) { + pub fn handle_event(&mut self, event: &UnknownEvent) { match event.as_core_event() { Some(CoreEventSpace::NoteOn(NoteOnEvent(note_event))) => { // Ignore invalid or negative note keys. @@ -91,9 +91,11 @@ impl PolyOscillator { } } - pub fn generate_next_samples(&mut self, output_buffer: &mut [f32]) { + pub fn generate_next_samples(&mut self, output_buffer: &mut [f32], volume: f32) { for voice in &mut self.voice_buffer[..self.active_voice_count] { - voice.oscillator.add_next_samples_to_buffer(output_buffer); + voice + .oscillator + .add_next_samples_to_buffer(output_buffer, volume); } }