diff --git a/main/src/domain/main_processor.rs b/main/src/domain/main_processor.rs index aec7cd383..d29e73d60 100644 --- a/main/src/domain/main_processor.rs +++ b/main/src/domain/main_processor.rs @@ -5,9 +5,9 @@ use crate::domain::{ CompoundMappingSourceAddress, CompoundMappingTarget, ControlContext, ControlEvent, ControlEventTimestamp, ControlInput, ControlLogContext, ControlLogEntry, ControlLogEntryKind, ControlMode, ControlOutcome, DeviceFeedbackOutput, DomainEvent, DomainEventHandler, - ExtendedProcessorContext, FeedbackAudioHookTask, FeedbackCollector, FeedbackDestinations, - FeedbackLogEntry, FeedbackOutput, FeedbackRealTimeTask, FeedbackResolution, - FeedbackSendBehavior, FinalRealFeedbackValue, FinalSourceFeedbackValue, + ExtendedProcessorContext, FeedbackAudioHookTask, FeedbackCause, FeedbackCollector, + FeedbackDestinations, FeedbackLogEntry, FeedbackOutput, FeedbackRealTimeTask, + FeedbackResolution, FeedbackSendBehavior, FinalRealFeedbackValue, FinalSourceFeedbackValue, GlobalControlAndFeedbackState, GroupId, HitInstructionContext, HitInstructionResponse, InstanceId, InternalInfoEvent, IoUpdatedEvent, KeyMessage, MainMapping, MainSourceMessage, MappingActivationEffect, MappingControlResult, MappingId, MappingInfo, MessageCaptureEvent, @@ -394,8 +394,8 @@ impl MainProcessor { self.basics.send_direct_source_feedback( feedback_output, FeedbackReason::FinallySwitchOffSource, + FeedbackCause::Normal, feedback_value, - false, ); } @@ -3563,15 +3563,15 @@ impl Basics { let with_source_feedback = self.instance_feedback_is_effectively_enabled() && mapping_feedback_is_effectively_on; let logger = self.source_feedback_logger(m.qualified_id()); - let feedback_value = m - .feedback_entry_point( + let feedback_value = m.feedback_entry_point( + FeedbackDestinations { with_projection_feedback, with_source_feedback, - new_target_value, - self.control_context(m.compartment()), - logger, - ) - .map(CompoundFeedbackValue::normal); + }, + new_target_value, + self.control_context(m.compartment()), + logger, + ); self.send_feedback( mappings_with_virtual_targets, FeedbackReason::Normal, @@ -3741,6 +3741,7 @@ impl Basics { }, self.source_context(m.compartment()), self.source_feedback_logger(m.qualified_id()), + feedback_value.cause, ); if let Some(SpecificCompoundFeedbackValue::Real( preliminary_feedback_value, @@ -3752,8 +3753,8 @@ impl Basics { { self.send_direct_feedback( feedback_reason, + feedback_value.cause, final_feedback_value, - feedback_value.is_feedback_after_control, ); } } @@ -3767,8 +3768,8 @@ impl Basics { { self.send_direct_feedback( feedback_reason, + feedback_value.cause, final_feedback_value, - feedback_value.is_feedback_after_control, ); } } @@ -3776,18 +3777,18 @@ impl Basics { } // Send special collected feedback for final_feedback_value in feedback_collector.generate_final_feedback_values() { - self.send_direct_feedback(feedback_reason, final_feedback_value, false); + self.send_direct_feedback(feedback_reason, FeedbackCause::Normal, final_feedback_value); } } pub fn send_direct_source_feedback( &self, - feedback_output: FeedbackOutput, - feedback_reason: FeedbackReason, + output: FeedbackOutput, + reason: FeedbackReason, + cause: FeedbackCause, source_feedback_value: FinalSourceFeedbackValue, - is_feedback_after_control: bool, ) { - if feedback_reason.is_reset_because_of_source_release() + if reason.is_reset_because_of_source_release() && !self.settings.reset_feedback_when_releasing_source { return; @@ -3805,13 +3806,13 @@ impl Basics { // send because that's sort of the point of this feature. If it's a source-takeover, we // also need to send because we don't know what the other instance sent before that // (https://github.com/helgoboss/helgobox/issues/727). - if !is_feedback_after_control - && feedback_reason != FeedbackReason::TakeOverSource + if cause != FeedbackCause::FeedbackAfterControl + && reason != FeedbackReason::TakeOverSource && Some(checksum) == previous_checksum { trace!( "Block feedback because duplicate (reason: {:?}): {:?}", - feedback_reason, + reason, source_feedback_value ); return; @@ -3819,7 +3820,7 @@ impl Basics { } trace!( "Schedule sending feedback because {:?}: {:?}", - feedback_reason, + reason, source_feedback_value ); if let Some(test_sender) = self.channels.integration_test_feedback_sender.as_ref() { @@ -3828,14 +3829,14 @@ impl Basics { test_sender.send_if_space(source_feedback_value); } else { // Production - match (source_feedback_value, feedback_output) { + match (source_feedback_value, output) { (FinalSourceFeedbackValue::Midi(v), FeedbackOutput::Midi(midi_output)) => { match midi_output { MidiDestination::FxOutput => { if self.settings.real_output_logging_enabled { log_real_feedback_output( self.unit_id, - feedback_reason, + reason, format_midi_source_value(&v), ); } @@ -3857,7 +3858,7 @@ impl Basics { if self.settings.real_output_logging_enabled { log_real_feedback_output( self.unit_id, - feedback_reason, + reason, format_midi_source_value(&v), ); } @@ -3871,11 +3872,7 @@ impl Basics { } (FinalSourceFeedbackValue::Osc(msg), FeedbackOutput::Osc(dev_id)) => { if self.settings.real_output_logging_enabled { - log_real_feedback_output( - self.unit_id, - feedback_reason, - format_osc_message(&msg), - ); + log_real_feedback_output(self.unit_id, reason, format_osc_message(&msg)); } self.channels .osc_feedback_task_sender @@ -3891,16 +3888,12 @@ impl Basics { fn send_direct_feedback( &self, - feedback_reason: FeedbackReason, - feedback_value: FinalRealFeedbackValue, - is_feedback_after_control: bool, + reason: FeedbackReason, + cause: FeedbackCause, + value: FinalRealFeedbackValue, ) { - self.send_direct_device_feedback( - feedback_reason, - feedback_value.source, - is_feedback_after_control, - ); - self.send_direct_projection_feedback(feedback_value.projection); + self.send_direct_device_feedback(reason, cause, value.source); + self.send_direct_projection_feedback(value.projection); } fn send_direct_projection_feedback(&self, feedback_value: Option) { @@ -3914,19 +3907,18 @@ impl Basics { fn send_direct_device_feedback( &self, - feedback_reason: FeedbackReason, - feedback_value: Option, - is_feedback_after_control: bool, + reason: FeedbackReason, + cause: FeedbackCause, + value: Option, ) { - if !feedback_reason.is_always_allowed() && !self.instance_feedback_is_effectively_enabled() - { + if !reason.is_always_allowed() && !self.instance_feedback_is_effectively_enabled() { return; } if let Some(feedback_output) = self.settings.feedback_output { - if let Some(source_feedback_value) = feedback_value { + if let Some(source_feedback_value) = value { // At this point we can be sure that this mapping can't have a // virtual source. - if feedback_reason.is_source_release() { + if reason.is_source_release() { // Possible interference with other instances. Don't switch off yet! // Give other instances the chance to take over. let event = UnitOrchestrationEvent::SourceReleased(SourceReleasedEvent { @@ -3941,9 +3933,9 @@ impl Basics { // Send feedback right now. self.send_direct_source_feedback( feedback_output, - feedback_reason, + reason, + cause, source_feedback_value, - is_feedback_after_control, ); } } diff --git a/main/src/domain/mapping.rs b/main/src/domain/mapping.rs index 2d1a312cd..c8905929e 100644 --- a/main/src/domain/mapping.rs +++ b/main/src/domain/mapping.rs @@ -28,6 +28,7 @@ use std::cell::Cell; use crate::domain::unresolved_reaper_target::UnresolvedReaperTargetDef; use base::hash_util::{NonCryptoHashSet, NonCryptoIndexMap, NonCryptoIndexSet}; +use helgobox_api::persistence::FeedbackBehavior; use playtime_api::persistence::{ColumnAddress, RowAddress, SlotAddress}; use reaper_high::{Fx, Project, Track, TrackRoute}; use reaper_medium::MidiInputDeviceId; @@ -1078,8 +1079,20 @@ impl MainMapping { new_target_value: Option, control_context: ControlContext, ) -> Option { - self.feedback_entry_point(true, true, new_target_value, control_context, NoopLogger) - .map(CompoundFeedbackValue::normal) + let value = self.feedback_entry_point( + FeedbackDestinations { + with_projection_feedback: true, + with_source_feedback: true, + }, + new_target_value, + control_context, + NoopLogger, + )?; + let compound_value = CompoundFeedbackValue { + value: value.value, + cause: FeedbackCause::ManualFeedbackBecauseOfTarget, + }; + Some(compound_value) } /// Returns `None` when used on mappings with virtual targets. @@ -1089,13 +1102,14 @@ impl MainMapping { context: ControlContext, ) -> Option { self.feedback_entry_point( - with_projection_feedback, - true, + FeedbackDestinations { + with_projection_feedback, + with_source_feedback: true, + }, self.current_aggregated_target_value(context), context, NoopLogger, ) - .map(CompoundFeedbackValue::normal) } /// This is the primary entry point to feedback! @@ -1103,12 +1117,11 @@ impl MainMapping { /// Returns `None` when used on mappings with virtual targets. pub fn feedback_entry_point( &self, - with_projection_feedback: bool, - with_source_feedback: bool, + destinations: FeedbackDestinations, combined_target_value: Option, control_context: ControlContext, logger: impl SourceFeedbackLogger, - ) -> Option { + ) -> Option { // - We shouldn't ask the source if it wants the given numerical feedback value or a textual // value because a virtual source wouldn't know! Even asking a real source wouldn't make // much sense because real sources could be capable of processing both numerical and @@ -1126,22 +1139,23 @@ impl MainMapping { let style = self.core.mode.feedback_style(&prop_provider); FeedbackValue::Numeric(NumericFeedbackValue::new(style, combined_target_value?)) }; - let source_feedback_is_okay = if self.core.options.feedback_send_behavior - == FeedbackSendBehavior::PreventEchoFeedback - { - !self.core.is_echo() + let cause = if self.core.is_echo() { + FeedbackCause::Echo } else { - true + FeedbackCause::Normal }; - self.feedback_given_target_value( + let specific_compound_value = self.feedback_given_target_value( Cow::Owned(feedback_value), - FeedbackDestinations { - with_projection_feedback, - with_source_feedback: with_source_feedback && source_feedback_is_okay, - }, + destinations, control_context.source_context, logger, - ) + cause, + )?; + let compound_value = CompoundFeedbackValue { + value: specific_compound_value, + cause, + }; + Some(compound_value) } pub fn current_aggregated_target_value( @@ -1171,7 +1185,9 @@ impl MainMapping { destinations: FeedbackDestinations, source_context: RealearnSourceContext, logger: impl SourceFeedbackLogger, + cause: FeedbackCause, ) -> Option { + let destinations = destinations.resolve(cause, self.core.options.feedback_send_behavior); let options = ModeFeedbackOptions { source_is_virtual: self.core.source.is_virtual(), max_discrete_source_value: self.core.source.max_discrete_value(), @@ -1217,15 +1233,19 @@ impl MainMapping { logger.log(FeedbackLogEntry { feedback_value: &feedback_value, }); - self.feedback_given_mode_value( + let value = self.feedback_given_mode_value( Cow::Owned(feedback_value), FeedbackDestinations { with_projection_feedback: true, with_source_feedback: true, }, source_context, - ) - .map(CompoundFeedbackValue::normal) + )?; + let compound_value = CompoundFeedbackValue { + value, + cause: FeedbackCause::Normal, + }; + Some(compound_value) } fn manual_feedback_after_control_if_enabled( @@ -1239,14 +1259,20 @@ impl MainMapping { { if self.feedback_is_effectively_on() { // No projection feedback in this case! Just the source controller needs this hack. - self.feedback_entry_point( - false, - true, + let value = self.feedback_entry_point( + FeedbackDestinations { + with_projection_feedback: false, + with_source_feedback: true, + }, self.current_aggregated_target_value(context), context, NoopLogger, - ) - .map(CompoundFeedbackValue::feedback_after_control) + )?; + let compound_value = CompoundFeedbackValue { + value: value.value, + cause: FeedbackCause::FeedbackAfterControl, + }; + Some(compound_value) } else { None } @@ -1814,22 +1840,24 @@ impl CompoundMappingSource { #[derive(Clone, PartialEq, Debug)] pub struct CompoundFeedbackValue { pub value: SpecificCompoundFeedbackValue, - pub is_feedback_after_control: bool, + pub cause: FeedbackCause, } -impl CompoundFeedbackValue { - pub fn normal(value: SpecificCompoundFeedbackValue) -> Self { - Self { - value, - is_feedback_after_control: false, - } - } +#[derive(Copy, Clone, Eq, PartialEq, Debug)] +pub enum FeedbackCause { + /// Target value change was caused e.g. by a mouse move in REAPER and was observed by ReaLearn. + Normal, + /// Target value change was caused by the ReaLearn mapping itself and was observed by ReaLearn. + Echo, + /// Target value itself can't be observed. Sending manually after control. + ManualFeedbackBecauseOfTarget, + /// Feedback sent because "Send feedback after control" enabled. + FeedbackAfterControl, +} - pub fn feedback_after_control(value: SpecificCompoundFeedbackValue) -> Self { - Self { - value, - is_feedback_after_control: true, - } +impl CompoundFeedbackValue { + pub fn new(value: SpecificCompoundFeedbackValue, cause: FeedbackCause) -> Self { + Self { value, cause } } } @@ -1854,6 +1882,20 @@ impl FeedbackDestinations { pub fn is_all_off(&self) -> bool { !self.with_source_feedback && !self.with_projection_feedback } + + pub fn resolve( + &self, + cause: FeedbackCause, + feedback_send_behavior: FeedbackSendBehavior, + ) -> Self { + let source_feedback_is_okay = !(feedback_send_behavior + == FeedbackSendBehavior::PreventEchoFeedback + && cause == FeedbackCause::Echo); + Self { + with_projection_feedback: self.with_projection_feedback, + with_source_feedback: self.with_source_feedback && source_feedback_is_okay, + } + } } impl SpecificCompoundFeedbackValue {