1
0
Fork 0

Break PolyModulation into that and MonoAutomation

This is needed to properly support polyphonic modulation, since the
modulated value may still be automated in the meantime and the
polyphonic modulation must act as an offset for that value. This does
mean that the plugin must add the normalized value and normaliezd offset
by itself. The `PolyModulation` event now also contains a description of
how this can be used. It would have been nicer to be able to send
polyphonic automation-style events instead (like the PolyModulation was
doing before), but that's sadly not feasible without NIH-plug being
involved in the voice management.
This commit is contained in:
Robbert van der Helm 2022-07-06 13:37:47 +02:00
parent 9520234b57
commit 1424b98e38
9 changed files with 100 additions and 25 deletions

View file

@ -6,6 +6,11 @@ new and what's changed, this document lists all breaking changes in reverse
chronological order. If a new feature did not require any changes to existing chronological order. If a new feature did not require any changes to existing
code then it will not be listed here. code then it will not be listed here.
## [2022-07-06]
- There are new `NoteEvent::PolyModulation` and `NoteEvent::MonoAutomation` as
part of polyphonic modulation support for CLAP plugins.
## [2022-07-05] ## [2022-07-05]
- The `ClapPlugin::CLAP_HARD_REALTIME` constant was moved to the general - The `ClapPlugin::CLAP_HARD_REALTIME` constant was moved to the general

View file

@ -71,6 +71,7 @@ pub enum NoteEvent {
/// The note's MIDI key number, from 0 to 127. /// The note's MIDI key number, from 0 to 127.
note: u8, note: u8,
}, },
/// Sent by the plugin to the host to indicate that a voice has ended. This **needs** to be sent /// Sent by the plugin to the host to indicate that a voice has ended. This **needs** to be sent
/// when a voice terminates when using polyphonic modulation. Otherwise you can ignore this /// when a voice terminates when using polyphonic modulation. Otherwise you can ignore this
/// event. /// event.
@ -86,7 +87,42 @@ pub enum NoteEvent {
}, },
/// A polyphonic modulation event, available on [`MidiConfig::Basic`] and up. This will only be /// A polyphonic modulation event, available on [`MidiConfig::Basic`] and up. This will only be
/// sent for parameters that were decorated with the `.with_poly_modulation_id()` modifier, and /// sent for parameters that were decorated with the `.with_poly_modulation_id()` modifier, and
/// only by supported hosts. The /// only by supported hosts. This event contains a _normalized offset value_ for the parameter's
/// current, **unmodulated** value. That is, an offset for the current value before monophonic
/// modulation is applied, as polyphonic modulation overrides monophonic modulation. There are
/// multiple ways to incorporate this polyphonic modulation into a synthesizer, but a simple way
/// to incorporate this would work as follows:
///
/// - By default, a voice uses the parameter's global value, which may or may not include
/// monophonic modulation. This is `parameter.value` for unsmoothed parameters, and smoothed
/// parameters should use block smoothing so the smoothed values can be reused by multiple
/// voices.
/// - If a `PolyModulation` event is emited for the voice, that voice should use the the
/// _normalized offset_ contained within the event to compute the voice's modulated value and
/// use that in place of the global value.
/// - This value can be obtained by calling `param.preview_plain(param.normalized_value() +
/// event.normalized_offset)`. These functions automatically clamp the values as necessary.
/// - If the parameter uses smoothing, then the parameter's smoother can be copied to the
/// voice. [`Smoother::set_target()`][crate::prelude::Smoother::set_target()] can then be
/// used to have the smoother use the modulated value.
/// - One caveat with smoothing is that copying the smoother like this only works correctly if it last
/// produced a value during the sample before the `PolyModulation` event. Otherwise there
/// may still be an audible jump in parameter values. A solution for this would be to first
/// call the [`Smoother::reset()`][crate::prelude::Smoother::reset()] with the current
/// sample's global value before calling `set_target()`.
/// - Finally, if the polyphonic modulation happens on the same sample as the `NoteOn` event,
/// then the smoothing should not start at the current global value. In this case, `reset()`
/// should be called with the voice's modulated value.
/// - If a `MonoAutomation` event is emitted for a parameter, then the values or target values
/// (if the parameter uses smoothing) for all voices must be updated. The normalized value
/// from the `MonoAutomation` and the voice's normalized modulation offset must be added and
/// converted back to a plain value. This value can be used directly for unsmoothed
/// parameters, or passed to `set_target()` for smoothed parameters. The global value will
/// have already been updated, so this event only serves as a notification to update
/// polyphonic modulation.
/// - When a voice ends, either because the amplitude envelope has hit zero or because the voice
/// was stolen, the plugin must send a `VoiceTerminated` to the host to let it know that it
/// can reuse the resources it used to modulate the value.
PolyModulation { PolyModulation {
timing: u32, timing: u32,
/// The identifier of the voice this polyphonic modulation event should affect. This voice /// The identifier of the voice this polyphonic modulation event should affect. This voice
@ -96,14 +132,24 @@ pub enum NoteEvent {
/// The ID that was set for the modulated parameter using the `.with_poly_modulation_id()` /// The ID that was set for the modulated parameter using the `.with_poly_modulation_id()`
/// method. /// method.
poly_modulation_id: u32, poly_modulation_id: u32,
/// The parameter's new normalized value. This value needs to be converted to the plain /// The normalized offset value. See the event's docstring for more information.
/// value using `param.preview_plain(event.normalized_value)`, and it should be used **in normalized_offset: f32,
/// place of** the global value. If the modulated parameter is smoothed, you may want to },
/// copy the smoother to the affected voice, set this value using /// A notification to inform the plugin that a polyphonically modulated parameter has received a
/// [`Smoother::set_target()`][crate::prelude::Smoother::set_target()], and then use that /// new automation value. This is used in conjuction with the `PolyModulation` event. See that
/// smoother to produce values for the voice. /// event's documentation for more details. The parameter's global value has already been
/// updated when this event is emited.
MonoAutomation {
timing: u32,
/// The ID that was set for the modulated parameter using the `.with_poly_modulation_id()`
/// method.
poly_modulation_id: u32,
/// The parameter's new normalized value. This needs to be added to a voice's normalized
/// offset to get that voice's modulated normalized value. See the `PolyModulation` event's
/// docstring for more information.
normalized_value: f32, normalized_value: f32,
}, },
/// A polyphonic note pressure/aftertouch event, available on [`MidiConfig::Basic`] and up. Not /// A polyphonic note pressure/aftertouch event, available on [`MidiConfig::Basic`] and up. Not
/// all hosts may support polyphonic aftertouch. /// all hosts may support polyphonic aftertouch.
/// ///
@ -252,6 +298,7 @@ impl NoteEvent {
NoteEvent::Choke { timing, .. } => *timing, NoteEvent::Choke { timing, .. } => *timing,
NoteEvent::VoiceTerminated { timing, .. } => *timing, NoteEvent::VoiceTerminated { timing, .. } => *timing,
NoteEvent::PolyModulation { timing, .. } => *timing, NoteEvent::PolyModulation { timing, .. } => *timing,
NoteEvent::MonoAutomation { timing, .. } => *timing,
NoteEvent::PolyPressure { timing, .. } => *timing, NoteEvent::PolyPressure { timing, .. } => *timing,
NoteEvent::PolyVolume { timing, .. } => *timing, NoteEvent::PolyVolume { timing, .. } => *timing,
NoteEvent::PolyPan { timing, .. } => *timing, NoteEvent::PolyPan { timing, .. } => *timing,
@ -273,6 +320,7 @@ impl NoteEvent {
NoteEvent::Choke { voice_id, .. } => *voice_id, NoteEvent::Choke { voice_id, .. } => *voice_id,
NoteEvent::VoiceTerminated { voice_id, .. } => *voice_id, NoteEvent::VoiceTerminated { voice_id, .. } => *voice_id,
NoteEvent::PolyModulation { voice_id, .. } => Some(*voice_id), NoteEvent::PolyModulation { voice_id, .. } => Some(*voice_id),
NoteEvent::MonoAutomation { .. } => None,
NoteEvent::PolyPressure { voice_id, .. } => *voice_id, NoteEvent::PolyPressure { voice_id, .. } => *voice_id,
NoteEvent::PolyVolume { voice_id, .. } => *voice_id, NoteEvent::PolyVolume { voice_id, .. } => *voice_id,
NoteEvent::PolyPan { voice_id, .. } => *voice_id, NoteEvent::PolyPan { voice_id, .. } => *voice_id,
@ -411,6 +459,7 @@ impl NoteEvent {
NoteEvent::Choke { .. } NoteEvent::Choke { .. }
| NoteEvent::VoiceTerminated { .. } | NoteEvent::VoiceTerminated { .. }
| NoteEvent::PolyModulation { .. } | NoteEvent::PolyModulation { .. }
| NoteEvent::MonoAutomation { .. }
| NoteEvent::PolyVolume { .. } | NoteEvent::PolyVolume { .. }
| NoteEvent::PolyPan { .. } | NoteEvent::PolyPan { .. }
| NoteEvent::PolyTuning { .. } | NoteEvent::PolyTuning { .. }
@ -429,6 +478,7 @@ impl NoteEvent {
NoteEvent::Choke { timing, .. } => *timing -= samples, NoteEvent::Choke { timing, .. } => *timing -= samples,
NoteEvent::VoiceTerminated { timing, .. } => *timing -= samples, NoteEvent::VoiceTerminated { timing, .. } => *timing -= samples,
NoteEvent::PolyModulation { timing, .. } => *timing -= samples, NoteEvent::PolyModulation { timing, .. } => *timing -= samples,
NoteEvent::MonoAutomation { timing, .. } => *timing -= samples,
NoteEvent::PolyPressure { timing, .. } => *timing -= samples, NoteEvent::PolyPressure { timing, .. } => *timing -= samples,
NoteEvent::PolyVolume { timing, .. } => *timing -= samples, NoteEvent::PolyVolume { timing, .. } => *timing -= samples,
NoteEvent::PolyPan { timing, .. } => *timing -= samples, NoteEvent::PolyPan { timing, .. } => *timing -= samples,

View file

@ -62,11 +62,10 @@ pub trait Param: Display {
/// Get this parameter's polyphonic modulation ID. If this is set for a parameter in a CLAP /// Get this parameter's polyphonic modulation ID. If this is set for a parameter in a CLAP
/// plugin, then polyphonic modulation will be enabled for that parameter. Polyphonic modulation /// plugin, then polyphonic modulation will be enabled for that parameter. Polyphonic modulation
/// is sent through [`NoteEvent::PolyModulation][crate::prelude::NoteEvent::PolyModulation`] /// is communicated to the plugin through
/// events containing a **normalized** value for this parameter. This value must be converted to /// [`NoteEvent::PolyModulation][crate::prelude::NoteEvent::PolyModulation`] and
/// a plain value using [`preview_plain()`][Self::preview_plain()] before it can be used. The /// [`NoteEvent::MonoAutomation][crate::prelude::NoteEvent::MonoAutomation`] events. See the
/// plugin should use this value in place of the parameter's normal (smoothed) value for the /// documentation on those events for more information.
/// affected note, and it should apply smooth to these values as necessary.
/// ///
/// # Important /// # Important
/// ///

View file

@ -226,7 +226,7 @@ impl BoolParam {
/// Enable polyphonic modulation for this parameter. The ID is used to uniquely identify this /// Enable polyphonic modulation for this parameter. The ID is used to uniquely identify this
/// parameter in [`NoteEvent::PolyModulation][crate::prelude::NoteEvent::PolyModulation`] /// parameter in [`NoteEvent::PolyModulation][crate::prelude::NoteEvent::PolyModulation`]
/// events, and must thus be unique between _all_ polyphonically modulatable parameters. See the /// events, and must thus be unique between _all_ polyphonically modulatable parameters. See the
/// event's documentation on how to do this. Consider configuring the /// event's documentation on how to use polyphonic modulation. Also consider configuring the
/// [`ClapPlugin::CLAP_POLY_MODULATION_CONFIG`][crate::prelude::ClapPlugin::CLAP_POLY_MODULATION_CONFIG] /// [`ClapPlugin::CLAP_POLY_MODULATION_CONFIG`][crate::prelude::ClapPlugin::CLAP_POLY_MODULATION_CONFIG]
/// constant when enabling this. /// constant when enabling this.
/// ///

View file

@ -337,7 +337,7 @@ impl<T: Enum + PartialEq + 'static> EnumParam<T> {
/// Enable polyphonic modulation for this parameter. The ID is used to uniquely identify this /// Enable polyphonic modulation for this parameter. The ID is used to uniquely identify this
/// parameter in [`NoteEvent::PolyModulation][crate::prelude::NoteEvent::PolyModulation`] /// parameter in [`NoteEvent::PolyModulation][crate::prelude::NoteEvent::PolyModulation`]
/// events, and must thus be unique between _all_ polyphonically modulatable parameters. See the /// events, and must thus be unique between _all_ polyphonically modulatable parameters. See the
/// event's documentation on how to do this. Consider configuring the /// event's documentation on how to use polyphonic modulation. Also consider configuring the
/// [`ClapPlugin::CLAP_POLY_MODULATION_CONFIG`][crate::prelude::ClapPlugin::CLAP_POLY_MODULATION_CONFIG] /// [`ClapPlugin::CLAP_POLY_MODULATION_CONFIG`][crate::prelude::ClapPlugin::CLAP_POLY_MODULATION_CONFIG]
/// constant when enabling this. /// constant when enabling this.
/// ///

View file

@ -294,7 +294,7 @@ impl FloatParam {
/// Enable polyphonic modulation for this parameter. The ID is used to uniquely identify this /// Enable polyphonic modulation for this parameter. The ID is used to uniquely identify this
/// parameter in [`NoteEvent::PolyModulation][crate::prelude::NoteEvent::PolyModulation`] /// parameter in [`NoteEvent::PolyModulation][crate::prelude::NoteEvent::PolyModulation`]
/// events, and must thus be unique between _all_ polyphonically modulatable parameters. See the /// events, and must thus be unique between _all_ polyphonically modulatable parameters. See the
/// event's documentation on how to do this. Consider configuring the /// event's documentation on how to use polyphonic modulation. Also consider configuring the
/// [`ClapPlugin::CLAP_POLY_MODULATION_CONFIG`][crate::prelude::ClapPlugin::CLAP_POLY_MODULATION_CONFIG] /// [`ClapPlugin::CLAP_POLY_MODULATION_CONFIG`][crate::prelude::ClapPlugin::CLAP_POLY_MODULATION_CONFIG]
/// constant when enabling this. /// constant when enabling this.
/// ///

View file

@ -258,7 +258,7 @@ impl IntParam {
/// Enable polyphonic modulation for this parameter. The ID is used to uniquely identify this /// Enable polyphonic modulation for this parameter. The ID is used to uniquely identify this
/// parameter in [`NoteEvent::PolyModulation][crate::prelude::NoteEvent::PolyModulation`] /// parameter in [`NoteEvent::PolyModulation][crate::prelude::NoteEvent::PolyModulation`]
/// events, and must thus be unique between _all_ polyphonically modulatable parameters. See the /// events, and must thus be unique between _all_ polyphonically modulatable parameters. See the
/// event's documentation on how to do this. Consider configuring the /// event's documentation on how to use polyphonic modulation. Also consider configuring the
/// [`ClapPlugin::CLAP_POLY_MODULATION_CONFIG`][crate::prelude::ClapPlugin::CLAP_POLY_MODULATION_CONFIG] /// [`ClapPlugin::CLAP_POLY_MODULATION_CONFIG`][crate::prelude::ClapPlugin::CLAP_POLY_MODULATION_CONFIG]
/// constant when enabling this. /// constant when enabling this.
/// ///

View file

@ -114,6 +114,13 @@ impl<T: Smoothable> Smoother<T> {
Default::default() Default::default()
} }
/// Get the smoother's style. This is useful when a per-voice smoother wants to use the same
/// style as a parameter's global smoother.
#[inline]
pub fn style(&self) -> SmoothingStyle {
self.style
}
/// The number of steps left until calling [`next()`][Self::next()] will stop yielding new /// The number of steps left until calling [`next()`][Self::next()] will stop yielding new
/// values. /// values.
#[inline] #[inline]

View file

@ -1326,6 +1326,24 @@ impl<P: ClapPlugin> Wrapper<P> {
self.current_buffer_config.load().map(|c| c.sample_rate), self.current_buffer_config.load().map(|c| c.sample_rate),
); );
// If the parameter supports polyphonic modulation, then the plugin needs to be
// informed that the parmaeter has been monophonicall automated. This allows the
// plugin to update all of its polyphonic modulation values, since polyphonic
// modulation acts as an offset to the monophonic value.
if let Some(poly_modulation_id) = self.poly_mod_ids_by_hash.get(&event.param_id) {
// The modulation offset needs to be normalized to account for modulated
// integer or enum parmaeters
let param_ptr = self.param_by_hash[&event.param_id];
let normalized_value =
event.value as f32 / param_ptr.step_count().unwrap_or(1) as f32;
input_events.push_back(NoteEvent::MonoAutomation {
timing,
poly_modulation_id: *poly_modulation_id,
normalized_value,
});
}
true true
} }
(CLAP_CORE_EVENT_SPACE_ID, CLAP_EVENT_PARAM_MOD) => { (CLAP_CORE_EVENT_SPACE_ID, CLAP_EVENT_PARAM_MOD) => {
@ -1334,15 +1352,11 @@ impl<P: ClapPlugin> Wrapper<P> {
if event.note_id != -1 && P::MIDI_INPUT >= MidiConfig::Basic { if event.note_id != -1 && P::MIDI_INPUT >= MidiConfig::Basic {
match self.poly_mod_ids_by_hash.get(&event.param_id) { match self.poly_mod_ids_by_hash.get(&event.param_id) {
Some(poly_modulation_id) => { Some(poly_modulation_id) => {
// To make things simpler (because they already are kind of // The modulation offset needs to be normalized to account for modulated
// complicated), we'll send the _normalized parameter value_ to the // integer or enum parmaeters
// plugin. So the plugin doesn't need to add modulation offsets here, as
// that might result in confusing situations when there's also
// monophonic modulation going on.
let param_ptr = self.param_by_hash[&event.param_id]; let param_ptr = self.param_by_hash[&event.param_id];
let normalized_modulated_value = param_ptr.normalized_value() let normalized_offset =
+ (event.amount as f32 event.amount as f32 / param_ptr.step_count().unwrap_or(1) as f32;
/ param_ptr.step_count().unwrap_or(1) as f32);
// The host may also add key and channel information here, but it may // The host may also add key and channel information here, but it may
// also pass -1. So not having that information here at all seems like // also pass -1. So not having that information here at all seems like
@ -1351,7 +1365,7 @@ impl<P: ClapPlugin> Wrapper<P> {
timing, timing,
voice_id: event.note_id, voice_id: event.note_id,
poly_modulation_id: *poly_modulation_id, poly_modulation_id: *poly_modulation_id,
normalized_value: normalized_modulated_value, normalized_offset,
}); });
return true; return true;