diff --git a/BREAKING_CHANGES.md b/BREAKING_CHANGES.md index d25200c7..ebbd994d 100644 --- a/BREAKING_CHANGES.md +++ b/BREAKING_CHANGES.md @@ -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 diff --git a/src/midi.rs b/src/midi.rs index 65050b93..5b8945a9 100644 --- a/src/midi.rs +++ b/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, diff --git a/src/param.rs b/src/param.rs index b1e989f1..1214b929 100644 --- a/src/param.rs +++ b/src/param.rs @@ -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 /// diff --git a/src/param/boolean.rs b/src/param/boolean.rs index 1b5d1ded..95d7cc87 100644 --- a/src/param/boolean.rs +++ b/src/param/boolean.rs @@ -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. /// diff --git a/src/param/enums.rs b/src/param/enums.rs index 4e796da6..884e6477 100644 --- a/src/param/enums.rs +++ b/src/param/enums.rs @@ -337,7 +337,7 @@ impl EnumParam { /// 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. /// diff --git a/src/param/float.rs b/src/param/float.rs index b3f258d0..07863c28 100644 --- a/src/param/float.rs +++ b/src/param/float.rs @@ -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. /// diff --git a/src/param/integer.rs b/src/param/integer.rs index b05ea4c7..bb0d12b3 100644 --- a/src/param/integer.rs +++ b/src/param/integer.rs @@ -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. /// diff --git a/src/param/smoothing.rs b/src/param/smoothing.rs index 5224055b..512de8c2 100644 --- a/src/param/smoothing.rs +++ b/src/param/smoothing.rs @@ -114,6 +114,13 @@ impl Smoother { 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] diff --git a/src/wrapper/clap/wrapper.rs b/src/wrapper/clap/wrapper.rs index cfa72ea3..10b5c86c 100644 --- a/src/wrapper/clap/wrapper.rs +++ b/src/wrapper/clap/wrapper.rs @@ -1326,6 +1326,24 @@ impl Wrapper

{ 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 Wrapper

{ 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 Wrapper

{ timing, voice_id: event.note_id, poly_modulation_id: *poly_modulation_id, - normalized_value: normalized_modulated_value, + normalized_offset, }); return true;