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.
|
||||
const NUM_VOICES: u32 = 16;
|
||||
|
||||
/// 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.
|
||||
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
|
||||
/// `NoteEvent::PolyModulation` for another source of information on how to use this.
|
||||
struct PolyModSynth {
|
||||
|
@ -25,8 +29,12 @@ struct PolyModSynth {
|
|||
next_internal_voice_id: u64,
|
||||
}
|
||||
|
||||
#[derive(Default, Params)]
|
||||
struct PolyModSynthParams {}
|
||||
#[derive(Params)]
|
||||
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
|
||||
/// struct of arrays instead of having a struct for each voice.
|
||||
|
@ -53,6 +61,10 @@ struct Voice {
|
|||
phase_delta: f32,
|
||||
/// The square root of the note's velocity. This is used as a gain multiplier.
|
||||
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 {
|
||||
|
@ -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 {
|
||||
const NAME: &'static str = "Poly Mod Synth";
|
||||
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
|
||||
// hand.
|
||||
let num_samples = buffer.len();
|
||||
let sample_rate = context.transport().sample_rate;
|
||||
let output = buffer.as_slice();
|
||||
|
||||
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);
|
||||
while block_start < num_samples {
|
||||
// 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 {
|
||||
match next_event {
|
||||
// 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
|
||||
voice.phase = initial_phase;
|
||||
voice.phase_delta =
|
||||
util::midi_note_to_freq(note) / context.transport().sample_rate;
|
||||
voice.phase_delta = util::midi_note_to_freq(note) / sample_rate;
|
||||
voice.velocity_sqrt = velocity.sqrt();
|
||||
}
|
||||
NoteEvent::NoteOff {
|
||||
|
@ -161,18 +202,104 @@ impl Plugin for PolyModSynth {
|
|||
} => {
|
||||
self.terminate_voice(context, timing, voice_id, channel, note);
|
||||
}
|
||||
// TODO: Handle poly modulation
|
||||
NoteEvent::PolyModulation {
|
||||
timing,
|
||||
timing: _,
|
||||
voice_id,
|
||||
poly_modulation_id,
|
||||
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 {
|
||||
timing,
|
||||
timing: _,
|
||||
poly_modulation_id,
|
||||
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[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: Some form of band limiting
|
||||
// TODO: Filter
|
||||
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: And as mentioned above, basic PolyBLEP or something
|
||||
let gain = voice.velocity_sqrt;
|
||||
let sample = (voice.phase * 2.0 - 1.0) * gain;
|
||||
let amp = voice.velocity_sqrt * gain[value_idx];
|
||||
let sample = (voice.phase * 2.0 - 1.0) * amp;
|
||||
|
||||
voice.phase += voice.phase_delta;
|
||||
if voice.phase >= 1.0 {
|
||||
|
@ -223,12 +370,12 @@ impl Plugin for PolyModSynth {
|
|||
}
|
||||
|
||||
impl PolyModSynth {
|
||||
/// Get an active voice by its voice ID, if the voice exists
|
||||
fn get_voice_mut(&mut self, voice_id: i32) -> Option<&mut Voice> {
|
||||
self.voices.iter_mut().find_map(|voice| match voice {
|
||||
Some(voice) if voice.voice_id == voice_id => Some(voice),
|
||||
_ => None,
|
||||
})
|
||||
/// Get the index of a voice by its voice ID, if the voice exists. This does not immediately
|
||||
/// reutnr a reference to the voice to avoid lifetime issues.
|
||||
fn get_voice_idx(&mut self, voice_id: i32) -> Option<usize> {
|
||||
self.voices
|
||||
.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
|
||||
|
@ -250,6 +397,8 @@ impl PolyModSynth {
|
|||
velocity_sqrt: 1.0,
|
||||
phase: 0.0,
|
||||
phase_delta: 0.0,
|
||||
|
||||
voice_gain: None,
|
||||
};
|
||||
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`]).
|
||||
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`].
|
||||
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
|
||||
// Send+Sync interior mutability
|
||||
Self {
|
||||
style: self.style.clone(),
|
||||
style: self.style,
|
||||
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)),
|
||||
target: self.target.clone(),
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue