From dad850b068001b585b2c4c1661d615584a80d097 Mon Sep 17 00:00:00 2001 From: Benjamin Klum Date: Wed, 18 Dec 2024 16:41:53 +0100 Subject: [PATCH] #1365 Support MIDI real-time FX parameter adjustments under certain conditions --- api/src/persistence/target.rs | 2 ++ .../pages/targets/fx-parameter/set-value.adoc | 32 +++++++++++++++++-- main/src/application/target_model.rs | 13 ++++++++ main/src/domain/realearn_target_context.rs | 2 +- main/src/domain/reaper_target.rs | 2 ++ .../src/domain/targets/fx_parameter_target.rs | 12 ++++--- .../infrastructure/api/convert/defaults.rs | 1 + .../api/convert/from_data/target.rs | 2 ++ .../api/convert/to_data/target.rs | 1 + .../infrastructure/data/target_model_data.rs | 4 +++ main/src/infrastructure/ui/mapping_panel.rs | 6 +++- 11 files changed, 68 insertions(+), 9 deletions(-) diff --git a/api/src/persistence/target.rs b/api/src/persistence/target.rs index 096bdedf1..347bd486e 100644 --- a/api/src/persistence/target.rs +++ b/api/src/persistence/target.rs @@ -816,6 +816,8 @@ pub struct FxParameterValueTarget { pub poll_for_feedback: Option, #[serde(skip_serializing_if = "Option::is_none")] pub retrigger: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub real_time: Option, } #[derive(Eq, PartialEq, Serialize, Deserialize)] diff --git a/doc/realearn/modules/ROOT/pages/targets/fx-parameter/set-value.adoc b/doc/realearn/modules/ROOT/pages/targets/fx-parameter/set-value.adoc index 37e7baf12..7094ab313 100644 --- a/doc/realearn/modules/ROOT/pages/targets/fx-parameter/set-value.adoc +++ b/doc/realearn/modules/ROOT/pages/targets/fx-parameter/set-value.adoc @@ -4,11 +4,39 @@ Sets the value of a particular track FX parameter. == Parameter controls -The parameter to be controlled. +Use them to set the parameter to be controlled. Please note that both xref:further-concepts/target.adoc#particular-fx-selector[] and xref:further-concepts/target.adoc#at-position-selector[] address the FX by its position in the FX chain. The difference between the two is that xref:further-concepts/target.adoc#particular-selector[] shows a dropdown containing the available parameters and xref:further-concepts/target.adoc#at-position-selector[] lets you enter the position as a number in a text field. -Latter is useful if at the time of choosing the position, the FX is not available. +The latter is useful if at the time of choosing the position, the FX is not available. + +== Retrigger checkbox + +By default, ReaLearn doesn't set the parameter if it already has the desired value. That prevents unnecessary invocations. + +However, some FX parameters are more like triggers. They don't actually have a value and are just used to trigger some action within that FX. In this case, it's important to enable _Retrigger_, which sets the parameter no matter what. + +== Real-time checkbox + +=== Main thread vs. real-time thread + +By default, ReaLearn does FX parameter value adjustments from the so-called _main_ thread instead of the _real-time_ thread. That means, in the worst case, we get latency as long as one main loop cycle. One main loop cycle is usually around 30 ms. + +In many control scenarios, this is completely acceptable. Basically, all control surface solutions including REAPER's built-in control surfaces, CSI and DrivenByMoss adjust FX parameters in the _main_ thread. Imagine you adjust a volume with a knob for mixing purposes. Such adjustments are usually rather slow and gradual, so it won't matter if the effect comes in 5 ms or 30 ms later. + +However, ReaLearn is a tool not just for mixing, also for performing. And in performing, there's sometimes demand for low latencies and fast responses, even when controlling FX parameters. + +=== Enabling real-time + +If you enable this checkbox, ReaLearn will **under certain conditions** control the FX parameter from a _real-time_ thread, enabling much lower latencies. In particular, the latency will correspond to the configured audio device block size -- the same thing that influences how fast virtual instruments respond when you press a note. + +The conditions are as follows: + +Condition 1: Same track:: The controlled FX must be on the **same track** as the ReaLearn instance. + +Condition 2: FX input:: The xref:key-concepts.adoc#input-port[] must be set to xref:user-interface/main-panel/input-output-section.adoc#fx-input[], **not** to a particular device. + +In all other circumstances, ReaLearn will fall back to adjusting the FX parameter from the _main_ thread. == Target-specific properties diff --git a/main/src/application/target_model.rs b/main/src/application/target_model.rs index 69ff3055e..d9e7f2061 100644 --- a/main/src/application/target_model.rs +++ b/main/src/application/target_model.rs @@ -108,6 +108,7 @@ pub enum TargetCommand { SetParamName(String), SetParamExpression(String), SetRetrigger(bool), + SetRealTime(bool), SetRouteSelectorType(TrackRouteSelectorType), SetRouteType(TrackRouteType), SetRouteId(Option), @@ -209,6 +210,7 @@ pub enum TargetProp { ParamName, ParamExpression, Retrigger, + RealTime, RouteSelectorType, RouteType, RouteId, @@ -392,6 +394,10 @@ impl<'a> Change<'a> for TargetModel { self.retrigger = v; One(P::Retrigger) } + C::SetRealTime(v) => { + self.real_time = v; + One(P::RealTime) + } C::SetRouteSelectorType(v) => { self.route_selector_type = v; One(P::RouteSelectorType) @@ -718,6 +724,7 @@ pub struct TargetModel { param_name: String, param_expression: String, retrigger: bool, + real_time: bool, // # For track route targets route_selector_type: TrackRouteSelectorType, route_type: TrackRouteType, @@ -885,6 +892,7 @@ impl Default for TargetModel { param_name: "".to_owned(), param_expression: "".to_owned(), retrigger: false, + real_time: false, route_selector_type: Default::default(), route_type: Default::default(), route_id: None, @@ -1272,6 +1280,10 @@ impl TargetModel { self.retrigger } + pub fn real_time(&self) -> bool { + self.real_time + } + pub fn tags(&self) -> &[Tag] { &self.tags } @@ -2285,6 +2297,7 @@ impl TargetModel { fx_parameter_descriptor: self.fx_parameter_descriptor()?, poll_for_feedback: self.poll_for_feedback, retrigger: self.retrigger, + real_time_even_if_not_rendering: self.real_time, }) } FxParameterTouchState => UnresolvedReaperTarget::FxParameterTouchState( diff --git a/main/src/domain/realearn_target_context.rs b/main/src/domain/realearn_target_context.rs index 543b29ec7..54a4018ca 100644 --- a/main/src/domain/realearn_target_context.rs +++ b/main/src/domain/realearn_target_context.rs @@ -4,7 +4,7 @@ use crate::domain::{ }; use base::hash_util::{NonCryptoHashMap, NonCryptoHashSet}; use base::{NamedChannelSender, SenderToNormalThread}; -use reaper_high::{Fx, GroupingBehavior, Reaper, Track}; +use reaper_high::{Fx, Track}; use reaper_medium::{GangBehavior, MediaTrack, NotificationBehavior, ValueChange}; /// Feedback for most targets comes from REAPER itself but there are some targets for which ReaLearn diff --git a/main/src/domain/reaper_target.rs b/main/src/domain/reaper_target.rs index f85dac199..b1633c366 100644 --- a/main/src/domain/reaper_target.rs +++ b/main/src/domain/reaper_target.rs @@ -428,6 +428,7 @@ impl ReaperTarget { // touched" anyway! poll_for_feedback: false, retrigger: false, + real_time_even_if_not_rendering: false, }) } FxPresetChanged(e) => FxPreset(FxPresetTarget { fx: e.fx }), @@ -484,6 +485,7 @@ impl ReaperTarget { param, poll_for_feedback: false, retrigger: false, + real_time_even_if_not_rendering: false, }; Some(FxParameter(t).into()) })) diff --git a/main/src/domain/targets/fx_parameter_target.rs b/main/src/domain/targets/fx_parameter_target.rs index 64404f20c..bee6cf81b 100644 --- a/main/src/domain/targets/fx_parameter_target.rs +++ b/main/src/domain/targets/fx_parameter_target.rs @@ -22,6 +22,7 @@ pub struct UnresolvedFxParameterTarget { pub fx_parameter_descriptor: FxParameterDescriptor, pub poll_for_feedback: bool, pub retrigger: bool, + pub real_time_even_if_not_rendering: bool, } impl UnresolvedReaperTargetDef for UnresolvedFxParameterTarget { @@ -41,6 +42,7 @@ impl UnresolvedReaperTargetDef for UnresolvedFxParameterTarget { param, poll_for_feedback: self.poll_for_feedback, retrigger: self.retrigger, + real_time_even_if_not_rendering: self.real_time_even_if_not_rendering, }; ReaperTarget::FxParameter(target) }) @@ -67,6 +69,7 @@ pub struct FxParameterTarget { pub param: FxParameter, pub poll_for_feedback: bool, pub retrigger: bool, + pub real_time_even_if_not_rendering: bool, } impl FxParameterTarget { @@ -262,6 +265,7 @@ impl RealearnTarget for FxParameterTarget { fx_location: self.param.fx().query_index(), param_index: self.param.index(), retrigger: self.retrigger, + real_time_even_if_not_rendering: self.real_time_even_if_not_rendering, }; Some(RealTimeReaperTarget::FxParameter(target)) } @@ -288,6 +292,7 @@ pub struct RealTimeFxParameterTarget { fx_location: TrackFxLocation, param_index: u32, retrigger: bool, + real_time_even_if_not_rendering: bool, } unsafe impl Send for RealTimeFxParameterTarget {} @@ -304,11 +309,8 @@ impl RealTimeFxParameterTarget { // from the audio hook (control input = MIDI hardware device). return false; } - if !is_rendering { - // We want real-time control only during rendering. Because REAPER won't invoke the - // change notifications when called in real-time (ReaLearn and maybe also other - // control surface implementations relies on those during normal playing to make - // feedback work). + if !self.real_time_even_if_not_rendering && !is_rendering { + // By default, we want real-time control only during rendering. return false; } true diff --git a/main/src/infrastructure/api/convert/defaults.rs b/main/src/infrastructure/api/convert/defaults.rs index c83483125..26fa60a46 100644 --- a/main/src/infrastructure/api/convert/defaults.rs +++ b/main/src/infrastructure/api/convert/defaults.rs @@ -35,6 +35,7 @@ pub const TARGET_BOOKMARK_SET_TIME_SELECTION: bool = false; pub const TARGET_BOOKMARK_SET_LOOP_POINTS: bool = false; pub const TARGET_POLL_FOR_FEEDBACK: bool = true; pub const TARGET_RETRIGGER: bool = false; +pub const TARGET_REAL_TIME: bool = false; pub const TARGET_TRACK_SELECTION_SCROLL_ARRANGE_VIEW: bool = false; pub const TARGET_TRACK_SELECTION_SCROLL_MIXER: bool = false; pub const TARGET_SEEK_USE_TIME_SELECTION: bool = false; diff --git a/main/src/infrastructure/api/convert/from_data/target.rs b/main/src/infrastructure/api/convert/from_data/target.rs index 84b35dfef..532cd2b02 100644 --- a/main/src/infrastructure/api/convert/from_data/target.rs +++ b/main/src/infrastructure/api/convert/from_data/target.rs @@ -224,6 +224,8 @@ fn convert_real_target( ), retrigger: style .required_value_with_default(data.retrigger, defaults::TARGET_RETRIGGER), + real_time: style + .required_value_with_default(data.real_time, defaults::TARGET_REAL_TIME), parameter: convert_fx_parameter_descriptor(data, style), }), CompartmentParameterValue => { diff --git a/main/src/infrastructure/api/convert/to_data/target.rs b/main/src/infrastructure/api/convert/to_data/target.rs index a1613ab81..a3708628c 100644 --- a/main/src/infrastructure/api/convert/to_data/target.rs +++ b/main/src/infrastructure/api/convert/to_data/target.rs @@ -605,6 +605,7 @@ pub fn convert_target(t: Target) -> ConversionResult { .poll_for_feedback .unwrap_or(defaults::TARGET_POLL_FOR_FEEDBACK), retrigger: d.retrigger.unwrap_or(defaults::TARGET_RETRIGGER), + real_time: d.real_time.unwrap_or(defaults::TARGET_REAL_TIME), ..init(d.commons) } } diff --git a/main/src/infrastructure/data/target_model_data.rs b/main/src/infrastructure/data/target_model_data.rs index 9ff88a8d7..e0b86ebf0 100644 --- a/main/src/infrastructure/data/target_model_data.rs +++ b/main/src/infrastructure/data/target_model_data.rs @@ -338,6 +338,8 @@ pub struct TargetModelData { pub poll_for_feedback: bool, #[serde(default, skip_serializing_if = "is_default")] pub retrigger: bool, + #[serde(default, skip_serializing_if = "is_default")] + pub real_time: bool, #[serde( default, deserialize_with = "deserialize_null_default", @@ -606,6 +608,7 @@ impl TargetModelData { buffered: false, poll_for_feedback: model.poll_for_feedback(), retrigger: model.retrigger(), + real_time: model.real_time(), tags: model.tags().to_vec(), mapping_snapshot: model.mapping_snapshot_desc_for_load(), take_mapping_snapshot: Some(model.mapping_snapshot_desc_for_take()), @@ -856,6 +859,7 @@ impl TargetModelData { model.change(C::SetOscDevId(self.osc_dev_id)); model.change(C::SetPollForFeedback(self.poll_for_feedback)); model.change(C::SetRetrigger(self.retrigger)); + model.change(C::SetRealTime(self.real_time)); model.change(C::SetTags(self.tags.clone())); model.change(C::SetExclusivity(self.exclusivity)); let group_id = conversion_context diff --git a/main/src/infrastructure/ui/mapping_panel.rs b/main/src/infrastructure/ui/mapping_panel.rs index 4aeac2836..6ed60733a 100644 --- a/main/src/infrastructure/ui/mapping_panel.rs +++ b/main/src/infrastructure/ui/mapping_panel.rs @@ -585,7 +585,7 @@ impl MappingPanel { P::UseRegions => { view.invalidate_target_check_boxes(); } - P::UseLoopPoints | P::PollForFeedback | P::Retrigger => { + P::UseLoopPoints | P::PollForFeedback | P::Retrigger | P::RealTime => { view.invalidate_target_check_boxes(); } P::UseTimeSelection => { @@ -3037,6 +3037,9 @@ impl<'a> MutableMappingPanel<'a> { .is_checked(); match self.target_category() { TargetCategory::Reaper => match self.reaper_target_type() { + ReaperTargetType::FxParameterValue => self.change_mapping( + MappingCommand::ChangeTarget(TargetCommand::SetRealTime(is_checked)), + ), ReaperTargetType::Seek | ReaperTargetType::GoToBookmark => { self.change_mapping(MappingCommand::ChangeTarget( TargetCommand::SetUseTimeSelection(is_checked), @@ -6204,6 +6207,7 @@ impl<'a> ImmutableMappingPanel<'a> { fn invalidate_target_check_box_6(&self) { let state = match self.target.category() { TargetCategory::Reaper => match self.target.target_type() { + ReaperTargetType::FxParameterValue => Some(("Real-time", self.target.real_time())), ReaperTargetType::Seek => { Some(("Use time selection", self.target.use_time_selection())) }