Add a poly mod gain parameter to PolyModSynth
This commit is contained in:
parent
097d6c9fc4
commit
35e584b3c8
|
@ -5,11 +5,15 @@ use std::sync::Arc;
|
||||||
|
|
||||||
/// The number of simultaneous voices for this synth.
|
/// The number of simultaneous voices for this synth.
|
||||||
const NUM_VOICES: u32 = 16;
|
const NUM_VOICES: u32 = 16;
|
||||||
|
|
||||||
/// The maximum size of an audio block. We'll split up the audio in blocks and render smoothed
|
/// The maximum size of an audio block. We'll split up the audio in blocks and render smoothed
|
||||||
/// values to buffers since these values may need to be reused for multiple voices.
|
/// values to buffers since these values may need to be reused for multiple voices.
|
||||||
const MAX_BLOCK_SIZE: usize = 64;
|
const MAX_BLOCK_SIZE: usize = 64;
|
||||||
|
|
||||||
|
// Polyphonic modulation works by assigning integer IDs to parameters. Pattern matching on these in
|
||||||
|
// `PolyModulation` and `MonoAutomation` events makes it possible to easily link these events to the
|
||||||
|
// correct parameter.
|
||||||
|
const GAIN_POLY_MOD_ID: u32 = 0;
|
||||||
|
|
||||||
/// A simple polyphonic synthesizer with support for CLAP's polyphonic modulation. See
|
/// A simple polyphonic synthesizer with support for CLAP's polyphonic modulation. See
|
||||||
/// `NoteEvent::PolyModulation` for another source of information on how to use this.
|
/// `NoteEvent::PolyModulation` for another source of information on how to use this.
|
||||||
struct PolyModSynth {
|
struct PolyModSynth {
|
||||||
|
@ -25,8 +29,12 @@ struct PolyModSynth {
|
||||||
next_internal_voice_id: u64,
|
next_internal_voice_id: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Params)]
|
#[derive(Params)]
|
||||||
struct PolyModSynthParams {}
|
struct PolyModSynthParams {
|
||||||
|
/// A voice's gain. This can be polyphonically modulated.
|
||||||
|
#[id = "gain"]
|
||||||
|
gain: FloatParam,
|
||||||
|
}
|
||||||
|
|
||||||
/// Data for a single synth voice. In a real synth where performance matter, you may want to use a
|
/// Data for a single synth voice. In a real synth where performance matter, you may want to use a
|
||||||
/// struct of arrays instead of having a struct for each voice.
|
/// struct of arrays instead of having a struct for each voice.
|
||||||
|
@ -53,6 +61,10 @@ struct Voice {
|
||||||
phase_delta: f32,
|
phase_delta: f32,
|
||||||
/// The square root of the note's velocity. This is used as a gain multiplier.
|
/// The square root of the note's velocity. This is used as a gain multiplier.
|
||||||
velocity_sqrt: f32,
|
velocity_sqrt: f32,
|
||||||
|
|
||||||
|
/// If this voice has polyphonic gain modulation applied, then this contains the normalized
|
||||||
|
/// offset and a smoother.
|
||||||
|
voice_gain: Option<(f32, Smoother<f32>)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for PolyModSynth {
|
impl Default for PolyModSynth {
|
||||||
|
@ -68,6 +80,30 @@ impl Default for PolyModSynth {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for PolyModSynthParams {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
gain: FloatParam::new(
|
||||||
|
"Gain",
|
||||||
|
util::db_to_gain(-12.0),
|
||||||
|
// Because we're representing gain as decibels the range is already logarithmic
|
||||||
|
FloatRange::Linear {
|
||||||
|
min: util::db_to_gain(-36.0),
|
||||||
|
max: util::db_to_gain(0.0),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
// This enables polyphonic mdoulation for this parameter by representing all related
|
||||||
|
// events with this ID. After enabling this, the plugin **must** start sending
|
||||||
|
// `VoiceTerminated` events to the host whenever a voice has ended.
|
||||||
|
.with_poly_modulation_id(GAIN_POLY_MOD_ID)
|
||||||
|
.with_smoother(SmoothingStyle::Logarithmic(5.0))
|
||||||
|
.with_unit(" dB")
|
||||||
|
.with_value_to_string(formatters::v2s_f32_gain_to_db(2))
|
||||||
|
.with_string_to_value(formatters::s2v_f32_gain_to_db()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Plugin for PolyModSynth {
|
impl Plugin for PolyModSynth {
|
||||||
const NAME: &'static str = "Poly Mod Synth";
|
const NAME: &'static str = "Poly Mod Synth";
|
||||||
const VENDOR: &'static str = "Moist Plugins GmbH";
|
const VENDOR: &'static str = "Moist Plugins GmbH";
|
||||||
|
@ -110,6 +146,7 @@ impl Plugin for PolyModSynth {
|
||||||
// split on note events, it's easier to work with raw audio here and to do the splitting by
|
// split on note events, it's easier to work with raw audio here and to do the splitting by
|
||||||
// hand.
|
// hand.
|
||||||
let num_samples = buffer.len();
|
let num_samples = buffer.len();
|
||||||
|
let sample_rate = context.transport().sample_rate;
|
||||||
let output = buffer.as_slice();
|
let output = buffer.as_slice();
|
||||||
|
|
||||||
let mut next_event = context.next_event();
|
let mut next_event = context.next_event();
|
||||||
|
@ -117,7 +154,12 @@ impl Plugin for PolyModSynth {
|
||||||
let mut block_end: usize = MAX_BLOCK_SIZE.min(num_samples);
|
let mut block_end: usize = MAX_BLOCK_SIZE.min(num_samples);
|
||||||
while block_start < num_samples {
|
while block_start < num_samples {
|
||||||
// First of all, handle all note events that happen at the start of the block, and cut
|
// First of all, handle all note events that happen at the start of the block, and cut
|
||||||
// the block short if another event happens before the end of it
|
// the block short if another event happens before the end of it. To handle polyphonic
|
||||||
|
// modulation for new notes properly, we'll keep track of the next internal note index
|
||||||
|
// at the block's start. If we receive polyphonic modulation that matches a voice that
|
||||||
|
// has an internal note ID that's great than or equal to this one, then we should start
|
||||||
|
// the note's smoother at the new value instead of fading in from the global value.
|
||||||
|
let this_sample_internal_voice_id_start = self.next_internal_voice_id;
|
||||||
'events: loop {
|
'events: loop {
|
||||||
match next_event {
|
match next_event {
|
||||||
// If the event happens now, then we'll keep processing events
|
// If the event happens now, then we'll keep processing events
|
||||||
|
@ -138,8 +180,7 @@ impl Plugin for PolyModSynth {
|
||||||
|
|
||||||
// TODO: Add and set the other fields
|
// TODO: Add and set the other fields
|
||||||
voice.phase = initial_phase;
|
voice.phase = initial_phase;
|
||||||
voice.phase_delta =
|
voice.phase_delta = util::midi_note_to_freq(note) / sample_rate;
|
||||||
util::midi_note_to_freq(note) / context.transport().sample_rate;
|
|
||||||
voice.velocity_sqrt = velocity.sqrt();
|
voice.velocity_sqrt = velocity.sqrt();
|
||||||
}
|
}
|
||||||
NoteEvent::NoteOff {
|
NoteEvent::NoteOff {
|
||||||
|
@ -161,18 +202,104 @@ impl Plugin for PolyModSynth {
|
||||||
} => {
|
} => {
|
||||||
self.terminate_voice(context, timing, voice_id, channel, note);
|
self.terminate_voice(context, timing, voice_id, channel, note);
|
||||||
}
|
}
|
||||||
// TODO: Handle poly modulation
|
|
||||||
NoteEvent::PolyModulation {
|
NoteEvent::PolyModulation {
|
||||||
timing,
|
timing: _,
|
||||||
voice_id,
|
voice_id,
|
||||||
poly_modulation_id,
|
poly_modulation_id,
|
||||||
normalized_offset,
|
normalized_offset,
|
||||||
} => todo!(),
|
} => {
|
||||||
|
// Polyphonic modulation events are matched to voices using the
|
||||||
|
// voice ID, and to parameters using the poly modulation ID
|
||||||
|
match self.get_voice_idx(voice_id) {
|
||||||
|
Some(voice_idx) => {
|
||||||
|
let voice = self.voices[voice_idx].as_mut().unwrap();
|
||||||
|
|
||||||
|
match poly_modulation_id {
|
||||||
|
GAIN_POLY_MOD_ID => {
|
||||||
|
// This should either create a smoother for this
|
||||||
|
// modulated parameter or update the existing one.
|
||||||
|
// Notice how this uses the parameter's unmodulated
|
||||||
|
// normalized value in combination with the normalized
|
||||||
|
// offset to create the target plain value
|
||||||
|
let target_plain_value = self
|
||||||
|
.params
|
||||||
|
.gain
|
||||||
|
.preview_modulated(normalized_offset);
|
||||||
|
let (_, smoother) =
|
||||||
|
voice.voice_gain.get_or_insert_with(|| {
|
||||||
|
(
|
||||||
|
normalized_offset,
|
||||||
|
self.params.gain.smoothed.clone(),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
// If this `PolyModulation` events happens on the
|
||||||
|
// same sample as a voice's `NoteOn` event, then it
|
||||||
|
// should immediately use the modulated value
|
||||||
|
// instead of slowly fading in
|
||||||
|
if voice.internal_voice_id
|
||||||
|
>= this_sample_internal_voice_id_start
|
||||||
|
{
|
||||||
|
smoother.reset(target_plain_value);
|
||||||
|
} else {
|
||||||
|
smoother.set_target(
|
||||||
|
sample_rate,
|
||||||
|
target_plain_value,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
n => nih_debug_assert_failure!(
|
||||||
|
"Polyphonic modulation sent for unknown poly \
|
||||||
|
modulation ID {}",
|
||||||
|
n
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// TODO: Bitwig sends the polyphonic modulation event before the
|
||||||
|
// NoteOn, and there will also be some more events after
|
||||||
|
// the voice has been terminated
|
||||||
|
None => nih_debug_assert_failure!(
|
||||||
|
"Polyphonic modulation sent for unknown voice {}",
|
||||||
|
voice_id
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
NoteEvent::MonoAutomation {
|
NoteEvent::MonoAutomation {
|
||||||
timing,
|
timing: _,
|
||||||
poly_modulation_id,
|
poly_modulation_id,
|
||||||
normalized_value,
|
normalized_value,
|
||||||
} => todo!(),
|
} => {
|
||||||
|
// Modulation always acts as an offset to the parameter's current
|
||||||
|
// automated value. So if the host sends a new automation value for
|
||||||
|
// a modulated parameter, the modulated values/smoothing targets
|
||||||
|
// need to be updated for all polyphonically modulated voices.
|
||||||
|
for voice in self.voices.iter_mut().filter_map(|v| v.as_mut()) {
|
||||||
|
match poly_modulation_id {
|
||||||
|
GAIN_POLY_MOD_ID => {
|
||||||
|
let (normalized_offset, smoother) =
|
||||||
|
match voice.voice_gain.as_mut() {
|
||||||
|
Some((o, s)) => (o, s),
|
||||||
|
// If the voice does not have existing
|
||||||
|
// polyphonic modulation, then there's nothing
|
||||||
|
// to do here. The global automation/monophonic
|
||||||
|
// modulation has already been taken care of by
|
||||||
|
// the framework.
|
||||||
|
None => continue,
|
||||||
|
};
|
||||||
|
let target_plain_value =
|
||||||
|
self.params.gain.preview_plain(
|
||||||
|
normalized_value + *normalized_offset,
|
||||||
|
);
|
||||||
|
smoother.set_target(sample_rate, target_plain_value);
|
||||||
|
}
|
||||||
|
n => nih_debug_assert_failure!(
|
||||||
|
"Automation event sent for unknown poly modulation ID \
|
||||||
|
{}",
|
||||||
|
n
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
_ => (),
|
_ => (),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -192,16 +319,36 @@ impl Plugin for PolyModSynth {
|
||||||
output[0][block_start..block_end].fill(0.0);
|
output[0][block_start..block_end].fill(0.0);
|
||||||
output[1][block_start..block_end].fill(0.0);
|
output[1][block_start..block_end].fill(0.0);
|
||||||
|
|
||||||
// TODO: Poly modulation
|
// These are the smoothed global parameter values. These are used for voices that do not
|
||||||
|
// have polyphonic modulation applied to them. With a plugin as simple as this it would
|
||||||
|
// be possible to avoid this completely by simply always copying the smoother into the
|
||||||
|
// voice's struct, but that may not be realistic when the plugin has hundreds of
|
||||||
|
// parameters. The `voice_*` arrays are scratch arrays that an individual voice can use.
|
||||||
|
let block_len = block_end - block_start;
|
||||||
|
let mut gain = [0.0; MAX_BLOCK_SIZE];
|
||||||
|
let mut voice_gain = [0.0; MAX_BLOCK_SIZE];
|
||||||
|
self.params.gain.smoothed.next_block(&mut gain, block_len);
|
||||||
|
|
||||||
// TODO: Amp envelope
|
// TODO: Amp envelope
|
||||||
// TODO: Some form of band limiting
|
// TODO: Some form of band limiting
|
||||||
// TODO: Filter
|
// TODO: Filter
|
||||||
for voice in self.voices.iter_mut().filter_map(|v| v.as_mut()) {
|
for voice in self.voices.iter_mut().filter_map(|v| v.as_mut()) {
|
||||||
for sample_idx in block_start..block_end {
|
for (value_idx, sample_idx) in (block_start..block_end).enumerate() {
|
||||||
|
// Depending on whether the voice has polyphonic modulation applied to it,
|
||||||
|
// either the global parameter values are used, or the voice's smoother is used
|
||||||
|
// to generate unique modulated values for that voice
|
||||||
|
let gain = match &voice.voice_gain {
|
||||||
|
Some((_, smoother)) => {
|
||||||
|
smoother.next_block(&mut voice_gain, block_len);
|
||||||
|
&voice_gain
|
||||||
|
}
|
||||||
|
None => &gain,
|
||||||
|
};
|
||||||
|
|
||||||
// TODO: This should of course take the envelope and probably a poly mod param into account
|
// TODO: This should of course take the envelope and probably a poly mod param into account
|
||||||
// TODO: And as mentioned above, basic PolyBLEP or something
|
// TODO: And as mentioned above, basic PolyBLEP or something
|
||||||
let gain = voice.velocity_sqrt;
|
let amp = voice.velocity_sqrt * gain[value_idx];
|
||||||
let sample = (voice.phase * 2.0 - 1.0) * gain;
|
let sample = (voice.phase * 2.0 - 1.0) * amp;
|
||||||
|
|
||||||
voice.phase += voice.phase_delta;
|
voice.phase += voice.phase_delta;
|
||||||
if voice.phase >= 1.0 {
|
if voice.phase >= 1.0 {
|
||||||
|
@ -223,12 +370,12 @@ impl Plugin for PolyModSynth {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PolyModSynth {
|
impl PolyModSynth {
|
||||||
/// Get an active voice by its voice ID, if the voice exists
|
/// Get the index of a voice by its voice ID, if the voice exists. This does not immediately
|
||||||
fn get_voice_mut(&mut self, voice_id: i32) -> Option<&mut Voice> {
|
/// reutnr a reference to the voice to avoid lifetime issues.
|
||||||
self.voices.iter_mut().find_map(|voice| match voice {
|
fn get_voice_idx(&mut self, voice_id: i32) -> Option<usize> {
|
||||||
Some(voice) if voice.voice_id == voice_id => Some(voice),
|
self.voices
|
||||||
_ => None,
|
.iter_mut()
|
||||||
})
|
.position(|voice| matches!(voice, Some(voice) if voice.voice_id == voice_id))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Start a new voice with the given voice ID. If all voices are currently in use, the oldest
|
/// Start a new voice with the given voice ID. If all voices are currently in use, the oldest
|
||||||
|
@ -250,6 +397,8 @@ impl PolyModSynth {
|
||||||
velocity_sqrt: 1.0,
|
velocity_sqrt: 1.0,
|
||||||
phase: 0.0,
|
phase: 0.0,
|
||||||
phase_delta: 0.0,
|
phase_delta: 0.0,
|
||||||
|
|
||||||
|
voice_gain: None,
|
||||||
};
|
};
|
||||||
self.next_internal_voice_id = self.next_internal_voice_id.wrapping_add(1);
|
self.next_internal_voice_id = self.next_internal_voice_id.wrapping_add(1);
|
||||||
|
|
||||||
|
|
|
@ -147,6 +147,14 @@ pub trait Param: Display {
|
||||||
/// wrappers. This **does** snap to step sizes for continuous parameters (i.e. [`FloatParam`]).
|
/// wrappers. This **does** snap to step sizes for continuous parameters (i.e. [`FloatParam`]).
|
||||||
fn preview_plain(&self, normalized: f32) -> Self::Plain;
|
fn preview_plain(&self, normalized: f32) -> Self::Plain;
|
||||||
|
|
||||||
|
/// Get the plain, unnormalized value for this parameter after polyphonic modulation has been
|
||||||
|
/// applied. This is a convenience method for calling [`preview_plain()`][Self::preview_plain()]
|
||||||
|
/// with `unmodulated_normalized_value() + normalized_offset`.`
|
||||||
|
#[inline]
|
||||||
|
fn preview_modulated(&self, normalized_offset: f32) -> Self::Plain {
|
||||||
|
self.preview_plain(self.unmodulated_normalized_value() + normalized_offset)
|
||||||
|
}
|
||||||
|
|
||||||
/// Flags to control the parameter's behavior. See [`ParamFlags`].
|
/// Flags to control the parameter's behavior. See [`ParamFlags`].
|
||||||
fn flags(&self) -> ParamFlags;
|
fn flags(&self) -> ParamFlags;
|
||||||
|
|
||||||
|
|
|
@ -96,9 +96,9 @@ impl<T: Clone> Clone for Smoother<T> {
|
||||||
// We can't derive clone because of the atomics, but these atomics are only here to allow
|
// We can't derive clone because of the atomics, but these atomics are only here to allow
|
||||||
// Send+Sync interior mutability
|
// Send+Sync interior mutability
|
||||||
Self {
|
Self {
|
||||||
style: self.style.clone(),
|
style: self.style,
|
||||||
steps_left: AtomicI32::new(self.steps_left.load(Ordering::Relaxed)),
|
steps_left: AtomicI32::new(self.steps_left.load(Ordering::Relaxed)),
|
||||||
step_size: self.step_size.clone(),
|
step_size: self.step_size,
|
||||||
current: AtomicF32::new(self.current.load(Ordering::Relaxed)),
|
current: AtomicF32::new(self.current.load(Ordering::Relaxed)),
|
||||||
target: self.target.clone(),
|
target: self.target.clone(),
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue