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:
parent
9520234b57
commit
1424b98e38
|
@ -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
|
||||
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]
|
||||
|
||||
- The `ClapPlugin::CLAP_HARD_REALTIME` constant was moved to the general
|
||||
|
|
64
src/midi.rs
64
src/midi.rs
|
@ -71,6 +71,7 @@ pub enum NoteEvent {
|
|||
/// The note's MIDI key number, from 0 to 127.
|
||||
note: u8,
|
||||
},
|
||||
|
||||
/// 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
|
||||
/// event.
|
||||
|
@ -86,7 +87,42 @@ pub enum NoteEvent {
|
|||
},
|
||||
/// 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
|
||||
/// 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 {
|
||||
timing: u32,
|
||||
/// 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()`
|
||||
/// method.
|
||||
poly_modulation_id: u32,
|
||||
/// The parameter's new normalized value. This value needs to be converted to the plain
|
||||
/// value using `param.preview_plain(event.normalized_value)`, and it should be used **in
|
||||
/// 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
|
||||
/// [`Smoother::set_target()`][crate::prelude::Smoother::set_target()], and then use that
|
||||
/// smoother to produce values for the voice.
|
||||
/// The normalized offset value. See the event's docstring for more information.
|
||||
normalized_offset: f32,
|
||||
},
|
||||
/// A notification to inform the plugin that a polyphonically modulated parameter has received a
|
||||
/// new automation value. This is used in conjuction with the `PolyModulation` event. See that
|
||||
/// 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,
|
||||
},
|
||||
|
||||
/// A polyphonic note pressure/aftertouch event, available on [`MidiConfig::Basic`] and up. Not
|
||||
/// all hosts may support polyphonic aftertouch.
|
||||
///
|
||||
|
@ -252,6 +298,7 @@ impl NoteEvent {
|
|||
NoteEvent::Choke { timing, .. } => *timing,
|
||||
NoteEvent::VoiceTerminated { timing, .. } => *timing,
|
||||
NoteEvent::PolyModulation { timing, .. } => *timing,
|
||||
NoteEvent::MonoAutomation { timing, .. } => *timing,
|
||||
NoteEvent::PolyPressure { timing, .. } => *timing,
|
||||
NoteEvent::PolyVolume { timing, .. } => *timing,
|
||||
NoteEvent::PolyPan { timing, .. } => *timing,
|
||||
|
@ -273,6 +320,7 @@ impl NoteEvent {
|
|||
NoteEvent::Choke { voice_id, .. } => *voice_id,
|
||||
NoteEvent::VoiceTerminated { voice_id, .. } => *voice_id,
|
||||
NoteEvent::PolyModulation { voice_id, .. } => Some(*voice_id),
|
||||
NoteEvent::MonoAutomation { .. } => None,
|
||||
NoteEvent::PolyPressure { voice_id, .. } => *voice_id,
|
||||
NoteEvent::PolyVolume { voice_id, .. } => *voice_id,
|
||||
NoteEvent::PolyPan { voice_id, .. } => *voice_id,
|
||||
|
@ -411,6 +459,7 @@ impl NoteEvent {
|
|||
NoteEvent::Choke { .. }
|
||||
| NoteEvent::VoiceTerminated { .. }
|
||||
| NoteEvent::PolyModulation { .. }
|
||||
| NoteEvent::MonoAutomation { .. }
|
||||
| NoteEvent::PolyVolume { .. }
|
||||
| NoteEvent::PolyPan { .. }
|
||||
| NoteEvent::PolyTuning { .. }
|
||||
|
@ -429,6 +478,7 @@ impl NoteEvent {
|
|||
NoteEvent::Choke { timing, .. } => *timing -= samples,
|
||||
NoteEvent::VoiceTerminated { timing, .. } => *timing -= samples,
|
||||
NoteEvent::PolyModulation { timing, .. } => *timing -= samples,
|
||||
NoteEvent::MonoAutomation { timing, .. } => *timing -= samples,
|
||||
NoteEvent::PolyPressure { timing, .. } => *timing -= samples,
|
||||
NoteEvent::PolyVolume { timing, .. } => *timing -= samples,
|
||||
NoteEvent::PolyPan { timing, .. } => *timing -= samples,
|
||||
|
|
|
@ -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
|
||||
/// plugin, then polyphonic modulation will be enabled for that parameter. Polyphonic modulation
|
||||
/// is sent through [`NoteEvent::PolyModulation][crate::prelude::NoteEvent::PolyModulation`]
|
||||
/// events containing a **normalized** value for this parameter. This value must be converted to
|
||||
/// a plain value using [`preview_plain()`][Self::preview_plain()] before it can be used. The
|
||||
/// plugin should use this value in place of the parameter's normal (smoothed) value for the
|
||||
/// affected note, and it should apply smooth to these values as necessary.
|
||||
/// is communicated to the plugin through
|
||||
/// [`NoteEvent::PolyModulation][crate::prelude::NoteEvent::PolyModulation`] and
|
||||
/// [`NoteEvent::MonoAutomation][crate::prelude::NoteEvent::MonoAutomation`] events. See the
|
||||
/// documentation on those events for more information.
|
||||
///
|
||||
/// # Important
|
||||
///
|
||||
|
|
|
@ -226,7 +226,7 @@ impl BoolParam {
|
|||
/// Enable polyphonic modulation for this parameter. The ID is used to uniquely identify this
|
||||
/// parameter in [`NoteEvent::PolyModulation][crate::prelude::NoteEvent::PolyModulation`]
|
||||
/// 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]
|
||||
/// constant when enabling this.
|
||||
///
|
||||
|
|
|
@ -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
|
||||
/// parameter in [`NoteEvent::PolyModulation][crate::prelude::NoteEvent::PolyModulation`]
|
||||
/// 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]
|
||||
/// constant when enabling this.
|
||||
///
|
||||
|
|
|
@ -294,7 +294,7 @@ impl FloatParam {
|
|||
/// Enable polyphonic modulation for this parameter. The ID is used to uniquely identify this
|
||||
/// parameter in [`NoteEvent::PolyModulation][crate::prelude::NoteEvent::PolyModulation`]
|
||||
/// 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]
|
||||
/// constant when enabling this.
|
||||
///
|
||||
|
|
|
@ -258,7 +258,7 @@ impl IntParam {
|
|||
/// Enable polyphonic modulation for this parameter. The ID is used to uniquely identify this
|
||||
/// parameter in [`NoteEvent::PolyModulation][crate::prelude::NoteEvent::PolyModulation`]
|
||||
/// 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]
|
||||
/// constant when enabling this.
|
||||
///
|
||||
|
|
|
@ -114,6 +114,13 @@ impl<T: Smoothable> Smoother<T> {
|
|||
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
|
||||
/// values.
|
||||
#[inline]
|
||||
|
|
|
@ -1326,6 +1326,24 @@ impl<P: ClapPlugin> Wrapper<P> {
|
|||
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
|
||||
}
|
||||
(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 {
|
||||
match self.poly_mod_ids_by_hash.get(&event.param_id) {
|
||||
Some(poly_modulation_id) => {
|
||||
// To make things simpler (because they already are kind of
|
||||
// complicated), we'll send the _normalized parameter value_ to the
|
||||
// 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.
|
||||
// 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_modulated_value = param_ptr.normalized_value()
|
||||
+ (event.amount as f32
|
||||
/ param_ptr.step_count().unwrap_or(1) as f32);
|
||||
let normalized_offset =
|
||||
event.amount as f32 / param_ptr.step_count().unwrap_or(1) as f32;
|
||||
|
||||
// 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
|
||||
|
@ -1351,7 +1365,7 @@ impl<P: ClapPlugin> Wrapper<P> {
|
|||
timing,
|
||||
voice_id: event.note_id,
|
||||
poly_modulation_id: *poly_modulation_id,
|
||||
normalized_value: normalized_modulated_value,
|
||||
normalized_offset,
|
||||
});
|
||||
|
||||
return true;
|
||||
|
|
Loading…
Reference in a new issue