Skip to content

Commit

Permalink
#660 Almost finish implementing X-Touch Mackie LCD color feedback
Browse files Browse the repository at this point in the history
  • Loading branch information
helgoboss committed Sep 13, 2022
1 parent 609d549 commit c6e8c67
Show file tree
Hide file tree
Showing 10 changed files with 226 additions and 93 deletions.
14 changes: 7 additions & 7 deletions main/src/application/session.rs
Expand Up @@ -13,12 +13,12 @@ use crate::domain::{
convert_plugin_param_index_range_to_iter, BackboneState, BasicSettings, Compartment,
CompartmentParamIndex, CompartmentParams, CompoundMappingSource, ControlContext, ControlInput,
DomainEvent, DomainEventHandler, ExtendedProcessorContext, FeedbackAudioHookTask,
FeedbackOutput, FeedbackRealTimeTask, GroupId, GroupKey, IncomingCompoundSourceValue,
InputDescriptor, InstanceContainer, InstanceId, InstanceState, MainMapping, MappingId,
MappingKey, MappingMatchedEvent, MessageCaptureEvent, MidiControlInput, NormalMainTask,
NormalRealTimeTask, OscFeedbackTask, ParamSetting, PluginParams, ProcessorContext,
ProjectionFeedbackValue, QualifiedMappingId, RealearnClipMatrix, RealearnTarget, ReaperTarget,
SharedInstanceState, SourceFeedbackValue, StayActiveWhenProjectInBackground, Tag,
FeedbackOutput, FeedbackRealTimeTask, FinalSourceFeedbackValue, GroupId, GroupKey,
IncomingCompoundSourceValue, InputDescriptor, InstanceContainer, InstanceId, InstanceState,
MainMapping, MappingId, MappingKey, MappingMatchedEvent, MessageCaptureEvent, MidiControlInput,
NormalMainTask, NormalRealTimeTask, OscFeedbackTask, ParamSetting, PluginParams,
ProcessorContext, ProjectionFeedbackValue, QualifiedMappingId, RealearnClipMatrix,
RealearnTarget, ReaperTarget, SharedInstanceState, StayActiveWhenProjectInBackground, Tag,
TargetValueChangedEvent, VirtualControlElementId, VirtualFx, VirtualSource, VirtualSourceValue,
};
use derivative::Derivative;
Expand Down Expand Up @@ -2196,7 +2196,7 @@ impl Session {
/// Good for checking produced feedback when doing integration testing.
pub fn use_integration_test_feedback_sender(
&self,
sender: SenderToNormalThread<SourceFeedbackValue>,
sender: SenderToNormalThread<FinalSourceFeedbackValue>,
) {
self.normal_main_task_sender
.send_complaining(NormalMainTask::UseIntegrationTestFeedbackSender(sender));
Expand Down
14 changes: 7 additions & 7 deletions main/src/domain/control_surface.rs
Expand Up @@ -2,12 +2,12 @@ use crate::base::{Global, NamedChannelSender, SenderToNormalThread};
use crate::domain::{
BackboneState, CompoundMappingSource, ControlEvent, ControlEventTimestamp,
DeviceChangeDetector, DeviceControlInput, DeviceFeedbackOutput, DomainEventHandler,
EelTransformation, FeedbackOutput, FeedbackRealTimeTask, InstanceId, LifecycleMidiData,
MainProcessor, MidiCaptureSender, MidiDeviceChangePayload, NormalRealTimeTask, OscDeviceId,
OscInputDevice, OscScanResult, QualifiedClipMatrixEvent, RealTimeCompoundMappingTarget,
RealTimeMapping, RealTimeMappingUpdate, RealTimeTargetUpdate, ReaperConfigChangeDetector,
ReaperMessage, ReaperTarget, SharedMainProcessors, SharedRealTimeProcessor,
SourceFeedbackValue, TouchedTrackParameterType,
EelTransformation, FeedbackOutput, FeedbackRealTimeTask, FinalSourceFeedbackValue, InstanceId,
LifecycleMidiData, MainProcessor, MidiCaptureSender, MidiDeviceChangePayload,
NormalRealTimeTask, OscDeviceId, OscInputDevice, OscScanResult, QualifiedClipMatrixEvent,
RealTimeCompoundMappingTarget, RealTimeMapping, RealTimeMappingUpdate, RealTimeTargetUpdate,
ReaperConfigChangeDetector, ReaperMessage, ReaperTarget, SharedMainProcessors,
SharedRealTimeProcessor, TouchedTrackParameterType,
};
use crossbeam_channel::Receiver;
use helgoboss_learn::{AbstractTimestamp, ModeGarbage, RawMidiEvents};
Expand Down Expand Up @@ -149,7 +149,7 @@ pub struct IoUpdatedEvent {
pub struct SourceReleasedEvent {
pub instance_id: InstanceId,
pub feedback_output: FeedbackOutput,
pub feedback_value: SourceFeedbackValue,
pub feedback_value: FinalSourceFeedbackValue,
}

#[derive(Debug)]
Expand Down
118 changes: 118 additions & 0 deletions main/src/domain/feedback_collector.rs
@@ -0,0 +1,118 @@
use crate::domain::{
FeedbackOutput, FinalRealFeedbackValue, FinalSourceFeedbackValue, MidiDestination,
PreliminaryRealFeedbackValue, PreliminarySourceFeedbackValue, RealearnSourceState,
};
use helgoboss_learn::devices::x_touch::XTouchMackieLcdState;
use helgoboss_learn::{
MackieLcdScope, MidiSourceValue, PreliminaryMidiSourceFeedbackValue, RawMidiEvent,
XTouchMackieLcdColorRequest,
};
use std::collections::HashSet;

/// Responsible for collecting non-final feedback values and aggregating them into final ones.
pub struct FeedbackCollector<'a> {
x_touch_mackie_lcd_feedback_collector: Option<XTouchMackieLcdFeedbackCollector<'a>>,
}

struct XTouchMackieLcdFeedbackCollector<'a> {
state: &'a mut XTouchMackieLcdState,
changed_x_touch_mackie_lcd_extenders: HashSet<u8>,
}

impl<'a> FeedbackCollector<'a> {
pub fn new(
global_source_state: &'a mut RealearnSourceState,
feedback_output: Option<FeedbackOutput>,
) -> Self {
let x_touch_mackie_lcd_state = match feedback_output {
Some(FeedbackOutput::Midi(MidiDestination::Device(dev_id))) => {
Some(global_source_state.get_x_touch_mackie_lcd_state_mut(dev_id))
}
// No or no direct MIDI device output. Then we can ignore this because
// the X-Touch!
_ => None,
};
Self {
x_touch_mackie_lcd_feedback_collector: x_touch_mackie_lcd_state.map(|state| {
XTouchMackieLcdFeedbackCollector {
state,
changed_x_touch_mackie_lcd_extenders: Default::default(),
}
}),
}
}

pub fn process(
&mut self,
preliminary_feedback_value: PreliminaryRealFeedbackValue,
) -> Option<FinalRealFeedbackValue> {
match preliminary_feedback_value.source {
// Has projection part only.
None => FinalRealFeedbackValue::new(preliminary_feedback_value.projection, None),
Some(preliminary_source_feedback_value) => match preliminary_source_feedback_value {
PreliminarySourceFeedbackValue::Midi(v) => match v {
// Is final MIDI value already.
PreliminaryMidiSourceFeedbackValue::Final(v) => FinalRealFeedbackValue::new(
preliminary_feedback_value.projection,
Some(FinalSourceFeedbackValue::Midi(v)),
),
// Is non-final.
PreliminaryMidiSourceFeedbackValue::XTouchMackieLcdColor(req) => {
self.process_x_touch_mackie_lcd_color_request(req);
None
}
},
// Is final OSC value already.
PreliminarySourceFeedbackValue::Osc(v) => FinalRealFeedbackValue::new(
preliminary_feedback_value.projection,
Some(FinalSourceFeedbackValue::Osc(v)),
),
},
}
}

/// Takes the collected and aggregated material and produces the final feedback values.
pub fn generate_final_feedback_values(self) -> impl Iterator<Item = FinalRealFeedbackValue> {
self.x_touch_mackie_lcd_feedback_collector
.into_iter()
.flat_map(|x_touch_collector| {
x_touch_collector
.changed_x_touch_mackie_lcd_extenders
.into_iter()
.filter_map(|extender_index| {
let sysex = x_touch_collector.state.sysex(extender_index);
let midi_event = RawMidiEvent::try_from_iter(0, sysex).ok()?;
let source_feedback_value = FinalSourceFeedbackValue::Midi(
MidiSourceValue::single_raw(None, midi_event),
);
FinalRealFeedbackValue::new(None, Some(source_feedback_value))
})
})
}

fn process_x_touch_mackie_lcd_color_request(&mut self, req: XTouchMackieLcdColorRequest) {
let collector = match &mut self.x_touch_mackie_lcd_feedback_collector {
None => return,
Some(c) => c,
};
let channels = match req.channel {
None => (0..MackieLcdScope::CHANNEL_COUNT),
Some(ch) => (ch..ch + 1),
};
let mut at_least_one_color_change = false;
for ch in channels {
let changed =
collector
.state
.notify_color_requested(req.extender_index, ch, req.color_index);
if changed {
at_least_one_color_change = true;
}
}
if at_least_one_color_change {
collector
.changed_x_touch_mackie_lcd_extenders
.insert(req.extender_index);
}
}
}
78 changes: 48 additions & 30 deletions main/src/domain/main_processor.rs
Expand Up @@ -4,18 +4,19 @@ use crate::domain::{
CompoundMappingSourceAddress, CompoundMappingTarget, ControlContext, ControlEvent,
ControlEventTimestamp, ControlInput, ControlMode, ControlOutcome, DeviceFeedbackOutput,
DomainEvent, DomainEventHandler, ExtendedProcessorContext, FeedbackAudioHookTask,
FeedbackDestinations, FeedbackOutput, FeedbackRealTimeTask, FeedbackResolution,
FeedbackSendBehavior, GroupId, HitInstructionContext, HitInstructionResponse,
InstanceContainer, InstanceOrchestrationEvent, InstanceStateChanged, IoUpdatedEvent,
KeyMessage, LimitedAsciiString, MainMapping, MainSourceMessage, MappingActivationEffect,
FeedbackCollector, FeedbackDestinations, FeedbackOutput, FeedbackRealTimeTask,
FeedbackResolution, FeedbackSendBehavior, FinalRealFeedbackValue, FinalSourceFeedbackValue,
GroupId, HitInstructionContext, HitInstructionResponse, InstanceContainer,
InstanceOrchestrationEvent, InstanceStateChanged, IoUpdatedEvent, KeyMessage,
LimitedAsciiString, MainMapping, MainSourceMessage, MappingActivationEffect,
MappingControlResult, MappingId, MappingInfo, MessageCaptureEvent, MessageCaptureResult,
MidiControlInput, MidiDestination, MidiScanResult, NormalRealTimeTask, OrderedMappingIdSet,
OrderedMappingMap, OscDeviceId, OscFeedbackTask, PluginParamIndex, PluginParams,
ProcessorContext, ProjectOptions, ProjectionFeedbackValue, QualifiedClipMatrixEvent,
QualifiedMappingId, QualifiedSource, RawParamValue, RealFeedbackValue, RealTimeMappingUpdate,
QualifiedMappingId, QualifiedSource, RawParamValue, RealTimeMappingUpdate,
RealTimeTargetUpdate, RealearnMonitoringFxParameterValueChangedEvent,
RealearnParameterChangePayload, ReaperConfigChange, ReaperMessage, ReaperTarget,
SharedInstanceState, SourceFeedbackValue, SourceReleasedEvent, SpecificCompoundFeedbackValue,
SharedInstanceState, SourceReleasedEvent, SpecificCompoundFeedbackValue,
TargetValueChangedEvent, UpdatedSingleMappingOnStateEvent, VirtualControlElement,
VirtualSourceValue,
};
Expand Down Expand Up @@ -90,12 +91,15 @@ struct Basics<EH: DomainEventHandler> {
// "experiment/feedback-change-detection-mutable") but it the end it turned out to be impossible
// because the reaper-rs control surface doesn't emit feedback-triggering events in a mutable
// context. Rightfully so, because it's potentially reentrant!
// TODO-low This reason is now outdated. We detected a general issue with reentrancy.
// https://github.com/helgoboss/reaper-rs/issues/54
last_feedback_checksum_by_address:
RefCell<HashMap<CompoundMappingSourceAddress, FeedbackChecksum>>,
target_based_conditional_activation_processors:
EnumMap<Compartment, TargetBasedConditionalActivationProcessor>,
}

/// Used for detecting and preventing subsequent duplicate feedback.
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
enum FeedbackChecksum {
MidiPlain(RawShortMessage),
Expand All @@ -106,11 +110,10 @@ enum FeedbackChecksum {
}

impl FeedbackChecksum {
fn from_value(v: &SourceFeedbackValue) -> Self {
use SourceFeedbackValue::*;
fn from_value(v: &FinalSourceFeedbackValue) -> Self {
match v {
Midi(v) => Self::from_midi(v),
Osc(v) => Self::from_osc(v),
FinalSourceFeedbackValue::Midi(v) => Self::from_midi(v),
FinalSourceFeedbackValue::Osc(v) => Self::from_osc(v),
}
}

Expand Down Expand Up @@ -230,7 +233,7 @@ struct Channels {
osc_feedback_task_sender: SenderToNormalThread<OscFeedbackTask>,
additional_feedback_event_sender: SenderToNormalThread<AdditionalFeedbackEvent>,
instance_orchestration_event_sender: SenderToNormalThread<InstanceOrchestrationEvent>,
integration_test_feedback_sender: Option<SenderToNormalThread<SourceFeedbackValue>>,
integration_test_feedback_sender: Option<SenderToNormalThread<FinalSourceFeedbackValue>>,
}

impl<EH: DomainEventHandler> MainProcessor<EH> {
Expand Down Expand Up @@ -361,7 +364,7 @@ impl<EH: DomainEventHandler> MainProcessor<EH> {
pub fn finally_switch_off_source(
&self,
feedback_output: FeedbackOutput,
feedback_value: SourceFeedbackValue,
feedback_value: FinalSourceFeedbackValue,
) {
debug!(
self.basics.logger,
Expand Down Expand Up @@ -2643,7 +2646,7 @@ pub enum NormalMainTask {
},
DisableControl,
ReturnToControlMode,
UseIntegrationTestFeedbackSender(SenderToNormalThread<SourceFeedbackValue>),
UseIntegrationTestFeedbackSender(SenderToNormalThread<FinalSourceFeedbackValue>),
}

#[derive(Copy, Clone, Debug, Default)]
Expand Down Expand Up @@ -3402,6 +3405,9 @@ impl<EH: DomainEventHandler> Basics<EH> {
feedback_reason: FeedbackReason,
feedback_values: impl IntoIterator<Item = CompoundFeedbackValue>,
) {
let mut global_source_state = BackboneState::source_state().borrow_mut();
let mut feedback_collector =
FeedbackCollector::new(&mut global_source_state, self.settings.feedback_output);
for feedback_value in feedback_values.into_iter() {
match feedback_value.value {
SpecificCompoundFeedbackValue::Virtual {
Expand Down Expand Up @@ -3438,36 +3444,48 @@ impl<EH: DomainEventHandler> Basics<EH> {
&self.source_context,
);
if let Some(SpecificCompoundFeedbackValue::Real(
final_feedback_value,
preliminary_feedback_value,
)) = compound_feedback_value
{
// Successful virtual-to-real feedback
self.send_direct_feedback(
feedback_reason,
final_feedback_value,
feedback_value.is_feedback_after_control,
);
if let Some(final_feedback_value) =
feedback_collector.process(preliminary_feedback_value)
{
self.send_direct_feedback(
feedback_reason,
final_feedback_value,
feedback_value.is_feedback_after_control,
);
}
}
}
}
}
}
SpecificCompoundFeedbackValue::Real(final_feedback_value) => {
self.send_direct_feedback(
feedback_reason,
final_feedback_value,
feedback_value.is_feedback_after_control,
);
SpecificCompoundFeedbackValue::Real(preliminary_feedback_value) => {
if let Some(final_feedback_value) =
feedback_collector.process(preliminary_feedback_value)
{
self.send_direct_feedback(
feedback_reason,
final_feedback_value,
feedback_value.is_feedback_after_control,
);
}
}
}
}
// 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);
}
}

pub fn send_direct_source_feedback(
&self,
feedback_output: FeedbackOutput,
feedback_reason: FeedbackReason,
source_feedback_value: SourceFeedbackValue,
source_feedback_value: FinalSourceFeedbackValue,
is_feedback_after_control: bool,
) {
if feedback_reason.is_reset_because_of_source_release()
Expand Down Expand Up @@ -3507,7 +3525,7 @@ impl<EH: DomainEventHandler> Basics<EH> {
} else {
// Production
match (source_feedback_value, feedback_output) {
(SourceFeedbackValue::Midi(v), FeedbackOutput::Midi(midi_output)) => {
(FinalSourceFeedbackValue::Midi(v), FeedbackOutput::Midi(midi_output)) => {
match midi_output {
MidiDestination::FxOutput => {
if self.settings.real_output_logging_enabled {
Expand Down Expand Up @@ -3545,7 +3563,7 @@ impl<EH: DomainEventHandler> Basics<EH> {
}
}
}
(SourceFeedbackValue::Osc(msg), FeedbackOutput::Osc(dev_id)) => {
(FinalSourceFeedbackValue::Osc(msg), FeedbackOutput::Osc(dev_id)) => {
if self.settings.real_output_logging_enabled {
log_real_feedback_output(&self.instance_id, format_osc_message(&msg));
}
Expand All @@ -3561,7 +3579,7 @@ impl<EH: DomainEventHandler> Basics<EH> {
fn send_direct_feedback(
&self,
feedback_reason: FeedbackReason,
feedback_value: RealFeedbackValue,
feedback_value: FinalRealFeedbackValue,
is_feedback_after_control: bool,
) {
self.send_direct_device_feedback(
Expand All @@ -3584,7 +3602,7 @@ impl<EH: DomainEventHandler> Basics<EH> {
fn send_direct_device_feedback(
&self,
feedback_reason: FeedbackReason,
feedback_value: Option<SourceFeedbackValue>,
feedback_value: Option<FinalSourceFeedbackValue>,
is_feedback_after_control: bool,
) {
if !feedback_reason.is_always_allowed() && !self.instance_feedback_is_effectively_enabled()
Expand Down

0 comments on commit c6e8c67

Please sign in to comment.