Skip to content

Commit

Permalink
Add a volume parameter to the polysynth example
Browse files Browse the repository at this point in the history
  • Loading branch information
prokopyl committed Nov 20, 2023
1 parent fdaf263 commit 00b4f21
Show file tree
Hide file tree
Showing 5 changed files with 211 additions and 27 deletions.
2 changes: 1 addition & 1 deletion plugin/examples/polysynth/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
50 changes: 33 additions & 17 deletions plugin/examples/polysynth/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<dyn PluginDescriptor> {
use clack_plugin::plugin::descriptor::features::*;
Expand All @@ -31,25 +34,29 @@ impl Plugin for PolySynthPlugin {
fn declare_extensions(builder: &mut PluginExtensions<Self>, _shared: &PolySynthPluginShared) {
builder
.register::<PluginAudioPorts>()
.register::<PluginNotePorts>();
.register::<PluginNotePorts>()
.register::<PluginParams>()
.register::<PluginState>();
}
}

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<Self, PluginError> {
Ok(Self {
poly_osc: PolyOscillator::new(16, audio_config.sample_rate as f32),
shared,
})
}

Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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<Self, PluginError> {
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<Self, PluginError> {
Ok(Self)
Ok(Self { shared })
}
}

Expand Down
9 changes: 3 additions & 6 deletions plugin/examples/polysynth/src/oscillator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
169 changes: 169 additions & 0 deletions plugin/examples/polysynth/src/params.rs
Original file line number Diff line number Diff line change
@@ -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<f64> {
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<f64> {
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())
}
8 changes: 5 additions & 3 deletions plugin/examples/polysynth/src/poly_oscillator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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);
}
}

Expand Down

0 comments on commit 00b4f21

Please sign in to comment.