Skip to content

Commit f1b9df9

Browse files
committed
#13 Prevent echo feedback - final implementation
Now works within one mapping only. Much more straightforward, no hidden pitfalls, also easier to implement. In future this could still be expanded to affect multiple mappings, if absolutely necessary.
1 parent 378299a commit f1b9df9

File tree

9 files changed

+67
-64
lines changed

9 files changed

+67
-64
lines changed

main/src/application/mapping_model_data.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ pub struct MappingModelData {
1212
target: TargetModelData,
1313
control_is_enabled: bool,
1414
feedback_is_enabled: bool,
15+
prevent_echo_feedback: bool,
1516
}
1617

1718
impl Default for MappingModelData {
@@ -23,6 +24,7 @@ impl Default for MappingModelData {
2324
target: Default::default(),
2425
control_is_enabled: true,
2526
feedback_is_enabled: true,
27+
prevent_echo_feedback: false,
2628
}
2729
}
2830
}
@@ -36,6 +38,7 @@ impl MappingModelData {
3638
target: TargetModelData::from_model(&model.target_model, context),
3739
control_is_enabled: model.control_is_enabled.get(),
3840
feedback_is_enabled: model.feedback_is_enabled.get(),
41+
prevent_echo_feedback: model.prevent_echo_feedback.get(),
3942
}
4043
}
4144

@@ -57,5 +60,8 @@ impl MappingModelData {
5760
model
5861
.feedback_is_enabled
5962
.set_without_notification(self.feedback_is_enabled);
63+
model
64+
.prevent_echo_feedback
65+
.set_without_notification(self.prevent_echo_feedback);
6066
}
6167
}

main/src/application/target_model_data.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ impl TargetModelData {
139139
"Track not found by GUID {} and name {}, falling back to <This>",
140140
guid.to_string_with_braces(),
141141
name.map(|n| format!("\"{}\"", n))
142-
.unwrap_or("-".to_string())
142+
.unwrap_or_else(|| "-".to_string())
143143
)),
144144
}
145145
VirtualTrack::This

main/src/domain/main_processor.rs

Lines changed: 1 addition & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,7 @@ use reaper_medium::ControlSurface;
1010
use rxrust::prelude::*;
1111
use slog::debug;
1212
use smallvec::SmallVec;
13-
use std::cell::RefCell;
14-
use std::collections::hash_map::Entry;
1513
use std::collections::{HashMap, HashSet};
16-
use std::time::{Duration, Instant};
1714

1815
const NORMAL_TASK_BULK_SIZE: usize = 32;
1916
const FEEDBACK_TASK_BULK_SIZE: usize = 32;
@@ -28,7 +25,6 @@ pub struct MainProcessor {
2825
mappings: HashMap<MappingId, MainProcessorMapping>,
2926
feedback_buffer: FeedbackBuffer,
3027
feedback_subscriptions: FeedbackSubscriptions,
31-
ephemeral_source_cache: EphemeralSourceCache,
3228
self_feedback_sender: crossbeam_channel::Sender<FeedbackMainTask>,
3329
normal_task_receiver: crossbeam_channel::Receiver<NormalMainTask>,
3430
feedback_task_receiver: crossbeam_channel::Receiver<FeedbackMainTask>,
@@ -57,7 +53,6 @@ impl ControlSurface for MainProcessor {
5753
Reaper::get().logger(),
5854
"Main processor: Updating all mappings..."
5955
);
60-
self.ephemeral_source_cache.clear();
6156
let mut unused_sources = self.currently_feedback_enabled_sources();
6257
// Put into hash map in order to quickly look up mappings by ID
6358
self.mappings = mappings
@@ -97,7 +92,6 @@ impl ControlSurface for MainProcessor {
9792
"Main processor: Updating mapping {:?}...",
9893
mapping.id()
9994
);
100-
self.ephemeral_source_cache.clear();
10195
// (Re)subscribe to or unsubscribe from feedback
10296
match mapping.target() {
10397
Some(target) if mapping.feedback_is_enabled() => {
@@ -169,11 +163,7 @@ impl ControlSurface for MainProcessor {
169163
// selected") and the real-time processor doesn't yet know about it, there
170164
// might be a short amount of time where we still receive control
171165
// statements. We filter them here.
172-
if m.control_is_enabled() {
173-
// TODO-high Only add if prevent immediate feedback enabled
174-
self.ephemeral_source_cache.add(m.source().clone());
175-
m.control(value);
176-
}
166+
m.control_if_enabled(value);
177167
};
178168
}
179169
}
@@ -196,11 +186,6 @@ impl ControlSurface for MainProcessor {
196186
if let Some(mapping_ids) = self.feedback_buffer.poll() {
197187
let source_values = mapping_ids.iter().filter_map(|mapping_id| {
198188
let mapping = self.mappings.get(mapping_id)?;
199-
if mapping.feedback_is_enabled()
200-
&& self.ephemeral_source_cache.contains(mapping.source())
201-
{
202-
return None;
203-
}
204189
mapping.feedback_if_enabled()
205190
});
206191
self.send_feedback(source_values);
@@ -226,7 +211,6 @@ impl MainProcessor {
226211
feedback_buffer: Default::default(),
227212
feedback_subscriptions: Default::default(),
228213
session,
229-
ephemeral_source_cache: Default::default(),
230214
}
231215
}
232216

@@ -379,43 +363,3 @@ impl Drop for MainProcessor {
379363
debug!(Reaper::get().logger(), "Dropping main processor...");
380364
}
381365
}
382-
383-
const EPHEMERAL_SOURCE_CACHE_LIFESPAN: Duration = Duration::from_millis(20);
384-
385-
/// Allows to add sources and keeps them just for a few milliseconds.
386-
#[derive(Debug, Default)]
387-
struct EphemeralSourceCache {
388-
entries: RefCell<HashMap<MidiSource, Instant>>,
389-
}
390-
391-
impl EphemeralSourceCache {
392-
/// Adds the given source
393-
pub fn add(&mut self, source: MidiSource) {
394-
self.entries.borrow_mut().insert(source, Instant::now());
395-
}
396-
397-
/// Checks if the given source is in this cache and not yet expired.
398-
///
399-
/// Also removes the cache entry if expired.
400-
pub fn contains(&self, source: &MidiSource) -> bool {
401-
use Entry::*;
402-
let expired = {
403-
if let Some(source) = self.entries.borrow().get(source) {
404-
source.elapsed() > EPHEMERAL_SOURCE_CACHE_LIFESPAN
405-
} else {
406-
return false;
407-
}
408-
};
409-
if expired {
410-
self.entries.borrow_mut().remove(source);
411-
false
412-
} else {
413-
true
414-
}
415-
}
416-
417-
/// Clears the cache
418-
fn clear(&mut self) {
419-
self.entries.borrow_mut().clear();
420-
}
421-
}

main/src/domain/mapping.rs

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use crate::domain::{MainProcessorTargetUpdate, Mode, ReaperTarget};
22
use helgoboss_learn::{ControlValue, MidiSource, MidiSourceValue, Target};
33
use helgoboss_midi::RawShortMessage;
44

5+
use std::time::{Duration, Instant};
56
use uuid::Uuid;
67

78
#[derive(Debug)]
@@ -12,6 +13,7 @@ pub struct ProcessorMapping {
1213
target: Option<ReaperTarget>,
1314
control_is_enabled: bool,
1415
feedback_is_enabled: bool,
16+
prevent_echo_feedback: bool,
1517
}
1618

1719
impl ProcessorMapping {
@@ -22,6 +24,7 @@ impl ProcessorMapping {
2224
target: Option<ReaperTarget>,
2325
control_is_enabled: bool,
2426
feedback_is_enabled: bool,
27+
prevent_echo_feedback: bool,
2528
) -> ProcessorMapping {
2629
ProcessorMapping {
2730
id,
@@ -30,6 +33,7 @@ impl ProcessorMapping {
3033
target,
3134
control_is_enabled,
3235
feedback_is_enabled,
36+
prevent_echo_feedback,
3337
}
3438
}
3539

@@ -46,6 +50,7 @@ impl ProcessorMapping {
4650
self.target.clone(),
4751
self.control_is_enabled,
4852
self.feedback_is_enabled && feedback_is_globally_enabled,
53+
self.prevent_echo_feedback,
4954
);
5055
(real_time_mapping, main_mapping)
5156
}
@@ -109,6 +114,8 @@ impl RealTimeProcessorMapping {
109114
}
110115
}
111116

117+
const MAX_ECHO_FEEDBACK_DELAY: Duration = Duration::from_millis(20);
118+
112119
#[derive(Debug)]
113120
pub struct MainProcessorMapping {
114121
id: MappingId,
@@ -117,6 +124,8 @@ pub struct MainProcessorMapping {
117124
target: Option<ReaperTarget>,
118125
control_is_enabled: bool,
119126
feedback_is_enabled: bool,
127+
prevent_echo_feedback: bool,
128+
time_of_last_control: Option<Instant>,
120129
}
121130

122131
impl MainProcessorMapping {
@@ -125,16 +134,19 @@ impl MainProcessorMapping {
125134
source: MidiSource,
126135
mode: Mode,
127136
target: Option<ReaperTarget>,
128-
control: bool,
129-
feedback: bool,
137+
control_is_enabled: bool,
138+
feedback_is_enabled: bool,
139+
prevent_echo_feedback: bool,
130140
) -> MainProcessorMapping {
131141
MainProcessorMapping {
132142
id,
133143
source,
134144
mode,
135145
target,
136-
control_is_enabled: control,
137-
feedback_is_enabled: feedback,
146+
control_is_enabled,
147+
feedback_is_enabled,
148+
prevent_echo_feedback,
149+
time_of_last_control: None,
138150
}
139151
}
140152

@@ -165,12 +177,18 @@ impl MainProcessorMapping {
165177
self.feedback_is_enabled
166178
}
167179

168-
pub fn control(&mut self, value: ControlValue) {
180+
pub fn control_if_enabled(&mut self, value: ControlValue) {
181+
if !self.control_is_enabled {
182+
return;
183+
}
169184
let target = match &self.target {
170185
None => return,
171186
Some(t) => t,
172187
};
173188
if let Some(final_value) = self.mode.control(value, target) {
189+
if self.prevent_echo_feedback {
190+
self.time_of_last_control = Some(Instant::now());
191+
}
174192
target.control(final_value).unwrap();
175193
}
176194
}
@@ -179,6 +197,11 @@ impl MainProcessorMapping {
179197
if !self.feedback_is_enabled {
180198
return None;
181199
}
200+
if let Some(t) = self.time_of_last_control {
201+
if t.elapsed() <= MAX_ECHO_FEEDBACK_DELAY {
202+
return None;
203+
}
204+
}
182205
let target = match &self.target {
183206
None => return None,
184207
Some(t) => t,

main/src/domain/mapping_model.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ pub struct MappingModel {
1515
pub name: Prop<String>,
1616
pub control_is_enabled: Prop<bool>,
1717
pub feedback_is_enabled: Prop<bool>,
18+
pub prevent_echo_feedback: Prop<bool>,
1819
pub source_model: MidiSourceModel,
1920
pub mode_model: ModeModel,
2021
pub target_model: TargetModel,
@@ -27,6 +28,7 @@ impl Clone for MappingModel {
2728
name: self.name.clone(),
2829
control_is_enabled: self.control_is_enabled.clone(),
2930
feedback_is_enabled: self.feedback_is_enabled.clone(),
31+
prevent_echo_feedback: self.prevent_echo_feedback.clone(),
3032
source_model: self.source_model.clone(),
3133
mode_model: self.mode_model.clone(),
3234
target_model: self.target_model.clone(),
@@ -41,6 +43,7 @@ impl Default for MappingModel {
4143
name: Default::default(),
4244
control_is_enabled: prop(true),
4345
feedback_is_enabled: prop(true),
46+
prevent_echo_feedback: prop(false),
4447
source_model: Default::default(),
4548
mode_model: Default::default(),
4649
target_model: Default::default(),
@@ -107,6 +110,7 @@ impl MappingModel {
107110
.merge(self.target_model.changed())
108111
.merge(self.control_is_enabled.changed())
109112
.merge(self.feedback_is_enabled.changed())
113+
.merge(self.prevent_echo_feedback.changed())
110114
}
111115
}
112116

@@ -133,6 +137,7 @@ impl<'a> MappingModelWithContext<'a> {
133137
target,
134138
self.mapping.control_is_enabled.get() && target_conditions_are_met,
135139
self.mapping.feedback_is_enabled.get() && target_conditions_are_met,
140+
self.mapping.prevent_echo_feedback.get(),
136141
)
137142
}
138143

main/src/infrastructure/common/bindings.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ pub mod root {
120120
pub const ID_SOURCE_MIDI_MESSAGE_TYPE_LABEL_TEXT: u32 = 40074;
121121
pub const ID_OK: u32 = 40075;
122122
pub const ID_TARGET_ACTION_LABEL_TEXT: u32 = 40076;
123+
pub const ID_MAPPING_PREVENT_ECHO_FEEDBACK_CHECK_BOX: u32 = 40078;
123124
pub const ID_CMD: u32 = 236;
124125
pub const ID_PSRESTARTWINDOWS: u32 = 2;
125126
pub const ID_PSREBOOTSYSTEM: u32 = 3;

main/src/infrastructure/common/realearn.rc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ BEGIN
118118
AUTOCHECKBOX "Rotate", ID_SETTINGS_ROTATE_CHECK_BOX, 124, 317, 37, 8, 0, WS_EX_LEFT
119119
PUSHBUTTON "Pick", ID_TARGET_PICK_ACTION_BUTTON, 410, 95, 26, 14, 0, WS_EX_LEFT
120120
LTEXT "Action name", ID_TARGET_ACTION_LABEL_TEXT, 220, 99, 189, 9, SS_LEFT, WS_EX_LEFT
121-
AUTOCHECKBOX "Prevent feedback echo", ID_MAPPING_CONTROL_ENABLED_CHECK_BOX, 348, 18, 91, 8, 0, WS_EX_LEFT
121+
AUTOCHECKBOX "Prevent echo feedback", ID_MAPPING_PREVENT_ECHO_FEEDBACK_CHECK_BOX, 348, 18, 91, 8, 0, WS_EX_LEFT
122122
END
123123

124124

main/src/infrastructure/common/resource.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,3 +113,4 @@
113113
#define ID_SOURCE_MIDI_MESSAGE_TYPE_LABEL_TEXT 40074
114114
#define ID_OK 40075
115115
#define ID_TARGET_ACTION_LABEL_TEXT 40076
116+
#define ID_MAPPING_PREVENT_ECHO_FEEDBACK_CHECK_BOX 40078

main/src/infrastructure/ui/mapping_panel.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,14 @@ impl<'a> MutableMappingPanel<'a> {
353353
);
354354
}
355355

356+
fn update_mapping_prevent_echo_feedback(&mut self) {
357+
self.mapping.prevent_echo_feedback.set(
358+
self.view
359+
.require_control(root::ID_MAPPING_PREVENT_ECHO_FEEDBACK_CHECK_BOX)
360+
.is_checked(),
361+
);
362+
}
363+
356364
fn update_mapping_name(&mut self) -> Result<(), &'static str> {
357365
let value = self
358366
.view
@@ -866,6 +874,7 @@ impl<'a> ImmutableMappingPanel<'a> {
866874
self.invalidate_mapping_name_edit_control();
867875
self.invalidate_mapping_control_enabled_check_box();
868876
self.invalidate_mapping_feedback_enabled_check_box();
877+
self.invalidate_mapping_prevent_echo_feedback_check_box();
869878
self.invalidate_source_controls();
870879
self.invalidate_target_controls();
871880
self.invalidate_mode_controls();
@@ -907,6 +916,13 @@ impl<'a> ImmutableMappingPanel<'a> {
907916
cb.set_text(format!("{} Feedback enabled", symbols::ARROW_LEFT_SYMBOL));
908917
}
909918

919+
fn invalidate_mapping_prevent_echo_feedback_check_box(&self) {
920+
let cb = self
921+
.view
922+
.require_control(root::ID_MAPPING_PREVENT_ECHO_FEEDBACK_CHECK_BOX);
923+
cb.set_checked(self.mapping.prevent_echo_feedback.get());
924+
}
925+
910926
fn invalidate_source_controls(&self) {
911927
self.invalidate_source_control_appearance();
912928
self.invalidate_source_type_combo_box();
@@ -1423,6 +1439,10 @@ impl<'a> ImmutableMappingPanel<'a> {
14231439
.when_do_sync(self.mapping.feedback_is_enabled.changed(), |view| {
14241440
view.invalidate_mapping_feedback_enabled_check_box();
14251441
});
1442+
self.panel
1443+
.when_do_sync(self.mapping.prevent_echo_feedback.changed(), |view| {
1444+
view.invalidate_mapping_prevent_echo_feedback_check_box();
1445+
});
14261446
}
14271447

14281448
fn register_source_listeners(&self) {
@@ -2072,6 +2092,9 @@ impl View for MappingPanel {
20722092
ID_MAPPING_FEEDBACK_ENABLED_CHECK_BOX => {
20732093
self.write(|p| p.update_mapping_feedback_enabled())
20742094
}
2095+
ID_MAPPING_PREVENT_ECHO_FEEDBACK_CHECK_BOX => {
2096+
self.write(|p| p.update_mapping_prevent_echo_feedback())
2097+
}
20752098
ID_MAPPING_FIND_IN_LIST_BUTTON => {
20762099
self.scroll_to_mapping_in_main_panel();
20772100
}

0 commit comments

Comments
 (0)