Skip to content

Commit

Permalink
#1365 Support MIDI real-time FX parameter adjustments under certain c…
Browse files Browse the repository at this point in the history
…onditions
  • Loading branch information
helgoboss committed Dec 18, 2024
1 parent 49320af commit dad850b
Show file tree
Hide file tree
Showing 11 changed files with 68 additions and 9 deletions.
2 changes: 2 additions & 0 deletions api/src/persistence/target.rs
Original file line number Diff line number Diff line change
Expand Up @@ -816,6 +816,8 @@ pub struct FxParameterValueTarget {
pub poll_for_feedback: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub retrigger: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub real_time: Option<bool>,
}

#[derive(Eq, PartialEq, Serialize, Deserialize)]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
13 changes: 13 additions & 0 deletions main/src/application/target_model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ pub enum TargetCommand {
SetParamName(String),
SetParamExpression(String),
SetRetrigger(bool),
SetRealTime(bool),
SetRouteSelectorType(TrackRouteSelectorType),
SetRouteType(TrackRouteType),
SetRouteId(Option<Guid>),
Expand Down Expand Up @@ -209,6 +210,7 @@ pub enum TargetProp {
ParamName,
ParamExpression,
Retrigger,
RealTime,
RouteSelectorType,
RouteType,
RouteId,
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -1272,6 +1280,10 @@ impl TargetModel {
self.retrigger
}

pub fn real_time(&self) -> bool {
self.real_time
}

pub fn tags(&self) -> &[Tag] {
&self.tags
}
Expand Down Expand Up @@ -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(
Expand Down
2 changes: 1 addition & 1 deletion main/src/domain/realearn_target_context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions main/src/domain/reaper_target.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 }),
Expand Down Expand Up @@ -484,6 +485,7 @@ impl ReaperTarget {
param,
poll_for_feedback: false,
retrigger: false,
real_time_even_if_not_rendering: false,
};
Some(FxParameter(t).into())
}))
Expand Down
12 changes: 7 additions & 5 deletions main/src/domain/targets/fx_parameter_target.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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)
})
Expand All @@ -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 {
Expand Down Expand Up @@ -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))
}
Expand All @@ -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 {}
Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions main/src/infrastructure/api/convert/defaults.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 2 additions & 0 deletions main/src/infrastructure/api/convert/from_data/target.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 => {
Expand Down
1 change: 1 addition & 0 deletions main/src/infrastructure/api/convert/to_data/target.rs
Original file line number Diff line number Diff line change
Expand Up @@ -605,6 +605,7 @@ pub fn convert_target(t: Target) -> ConversionResult<TargetModelData> {
.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)
}
}
Expand Down
4 changes: 4 additions & 0 deletions main/src/infrastructure/data/target_model_data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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()),
Expand Down Expand Up @@ -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
Expand Down
6 changes: 5 additions & 1 deletion main/src/infrastructure/ui/mapping_panel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 => {
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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()))
}
Expand Down

0 comments on commit dad850b

Please sign in to comment.