Skip to content

Commit afd34b7

Browse files
committed
Indicate via feedback if a source is currently not in use anymore
Essentially switches lights off or tears motorized fader down if not in use.
1 parent d53f703 commit afd34b7

File tree

3 files changed

+83
-40
lines changed

3 files changed

+83
-40
lines changed

main/src/domain/main_processor.rs

Lines changed: 77 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,14 @@ use crate::domain::{
33
NormalRealTimeTask, ReaperTarget, WeakSession,
44
};
55
use crossbeam_channel::Sender;
6-
use helgoboss_learn::{ControlValue, MidiSource, MidiSourceValue, Target};
6+
use helgoboss_learn::{ControlValue, MidiSource, MidiSourceValue, Target, UnitValue};
77
use helgoboss_midi::RawShortMessage;
88
use reaper_high::Reaper;
99
use reaper_medium::ControlSurface;
1010
use rxrust::prelude::*;
1111
use slog::{debug, info};
1212
use smallvec::SmallVec;
13-
use std::collections::HashMap;
13+
use std::collections::{HashMap, HashSet};
1414

1515
const NORMAL_TASK_BULK_SIZE: usize = 32;
1616
const FEEDBACK_TASK_BULK_SIZE: usize = 32;
@@ -53,50 +53,79 @@ impl ControlSurface for MainProcessor {
5353
Reaper::get().logger(),
5454
"Main processor: Updating all mappings..."
5555
);
56+
let mut unused_sources = self.currently_feedback_enabled_sources();
5657
// Put into hash map in order to quickly look up mappings by ID
57-
self.mappings = mappings.into_iter().map(|m| (m.id(), m)).collect();
58-
self.handle_feedback_after_batch_mapping_update();
58+
self.mappings = mappings
59+
.into_iter()
60+
.map(|m| {
61+
if m.feedback_is_enabled() {
62+
// Mark source as used
63+
unused_sources.remove(m.source());
64+
}
65+
(m.id(), m)
66+
})
67+
.collect();
68+
self.handle_feedback_after_batch_mapping_update(&unused_sources);
5969
}
6070
UpdateAllTargets(updates) => {
6171
debug!(
6272
Reaper::get().logger(),
6373
"Main processor: Updating all targets..."
6474
);
75+
let mut unused_sources = self.currently_feedback_enabled_sources();
6576
for t in updates.into_iter() {
6677
if let Some(m) = self.mappings.get_mut(&t.id) {
6778
m.update_from_target(t);
79+
if m.feedback_is_enabled() {
80+
// Mark source as used
81+
unused_sources.remove(m.source());
82+
}
6883
} else {
6984
panic!("Couldn't find mapping while updating all targets");
7085
}
7186
}
72-
self.handle_feedback_after_batch_mapping_update();
87+
self.handle_feedback_after_batch_mapping_update(&unused_sources);
7388
}
7489
UpdateSingleMapping(mapping) => {
7590
debug!(
7691
Reaper::get().logger(),
7792
"Main processor: Updating mapping {:?}...",
7893
mapping.id()
7994
);
80-
// TODO-medium We could send a null-feedback to switch off
8195
// (Re)subscribe to or unsubscribe from feedback
8296
match mapping.target() {
83-
// (Re)subscribe
8497
Some(target) if mapping.feedback_is_enabled() => {
98+
// (Re)subscribe
8599
let subscription = send_feedback_when_target_value_changed(
86100
self.self_feedback_sender.clone(),
87101
mapping.id(),
88102
target,
89103
);
90104
self.feedback_subscriptions
91105
.insert(mapping.id(), subscription);
106+
self.send_feedback(mapping.feedback_if_enabled());
92107
}
93-
// Unsubscribe (if the feedback was enabled before)
94108
_ => {
109+
// Unsubscribe (if the feedback was enabled before)
95110
self.feedback_subscriptions.remove(&mapping.id());
111+
// Indicate via feedback that this source is not in use anymore. But
112+
// only if feedback was enabled before (otherwise this could overwrite
113+
// the feedback value of another enabled mapping which has the same
114+
// source).
115+
let was_previously_enabled = self
116+
.mappings
117+
.get(&mapping.id())
118+
.map(|m| m.feedback_is_enabled())
119+
.contains(&true);
120+
if was_previously_enabled {
121+
// We assume that there's no other enabled mapping with the same
122+
// source at this moment. It there is, it would be a weird setup
123+
// with two conflicting feedback value sources - this wouldn't work
124+
// well anyway.
125+
self.send_feedback(mapping.source().feedback(UnitValue::MIN));
126+
}
96127
}
97128
};
98-
// Send feedback if enabled
99-
self.send_feedback(mapping.feedback_if_enabled());
100129
// Update hash map entry
101130
self.mappings.insert(mapping.id(), mapping);
102131
}
@@ -127,19 +156,13 @@ impl ControlSurface for MainProcessor {
127156
Control { mapping_id, value } => {
128157
if let Some(m) = self.mappings.get_mut(&mapping_id) {
129158
// Most of the time, the main processor won't even receive a control
130-
// instruction (from the real-time processor) for a
131-
// mapping for which control is disabled,
132-
// because the real-time processor only ever gets mappings for which control
133-
// is enabled. But if control is (temporarily) disabled because a target
134-
// condition is (temporarily) not met (e.g. "track must be selected"), the
135-
// real-time processor won't know about it (there's no resync to the
136-
// real-time processor in this case in order too not
137-
// reset source state like long/short press just
138-
// because of a selection change). If we want the
139-
// real-time processor to know about it (e.g. in order to reduce
140-
// the amount of sources it has to process), we would need to build a more
141-
// advanced syncing mechanism that uses diffs and retains sources.
142-
// TODO-low Optimize if it causes performance issues, which I don't think.
159+
// instruction (from the real-time processor) for a mapping for which
160+
// control is disabled, because the real-time processor doesn't process
161+
// disabled mappings. But if control is (temporarily) disabled because a
162+
// target condition is (temporarily) not met (e.g. "track must be
163+
// selected") and the real-time processor doesn't yet know about it, there
164+
// might be a short amount of time where we still receive control
165+
// statements. We filter them here.
143166
m.control_if_enabled(value);
144167
};
145168
}
@@ -208,23 +231,39 @@ impl MainProcessor {
208231
.collect()
209232
}
210233

211-
fn handle_feedback_after_batch_mapping_update(&mut self) {
212-
// Resubscribe to target value changes for feedback
234+
fn currently_feedback_enabled_sources(&self) -> HashSet<MidiSource> {
235+
self.mappings
236+
.values()
237+
.filter(|m| m.feedback_is_enabled())
238+
.map(|m| m.source().clone())
239+
.collect()
240+
}
241+
242+
fn handle_feedback_after_batch_mapping_update(
243+
&mut self,
244+
now_unused_sources: &HashSet<MidiSource>,
245+
) {
246+
// Subscribe to target value changes for feedback. Before that, cancel all existing
247+
// subscriptions.
213248
self.feedback_subscriptions.clear();
214-
for m in self.mappings.values() {
215-
match m.target() {
216-
Some(target) if m.feedback_is_enabled() => {
217-
let subscription = send_feedback_when_target_value_changed(
218-
self.self_feedback_sender.clone(),
219-
m.id(),
220-
target,
221-
);
222-
self.feedback_subscriptions.insert(m.id(), subscription);
223-
}
224-
_ => {}
225-
};
249+
for m in self.mappings.values().filter(|m| m.feedback_is_enabled()) {
250+
if let Some(target) = m.target() {
251+
// Subscribe
252+
let subscription = send_feedback_when_target_value_changed(
253+
self.self_feedback_sender.clone(),
254+
m.id(),
255+
target,
256+
);
257+
self.feedback_subscriptions.insert(m.id(), subscription);
258+
}
259+
}
260+
// Send feedback instantly to reflect this change in mappings.
261+
// At first indicate via feedback the sources which are not in use anymore.
262+
for s in now_unused_sources {
263+
self.send_feedback(s.feedback(UnitValue::MIN));
226264
}
227-
// Also send feedback instantly to reflect this change in mappings.
265+
// Then discard the current feedback buffer and send feedback for all new mappings which
266+
// are enabled.
228267
self.feedback_buffer.reset();
229268
self.send_feedback(self.feedback_all());
230269
}

main/src/domain/mapping.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use crate::domain::{MainProcessorTargetUpdate, Mode, ReaperTarget};
2-
use helgoboss_learn::{ControlValue, MidiSource, MidiSourceValue, Target};
2+
use helgoboss_learn::{ControlValue, MidiSource, MidiSourceValue, Target, UnitValue};
33
use helgoboss_midi::RawShortMessage;
44
use rx_util::BoxedUnitEvent;
55
use uuid::Uuid;
@@ -203,6 +203,10 @@ impl MainProcessorMapping {
203203
self.source.feedback(modified_value)
204204
}
205205

206+
pub fn source(&self) -> &MidiSource {
207+
&self.source
208+
}
209+
206210
pub fn target(&self) -> Option<&ReaperTarget> {
207211
self.target.as_ref()
208212
}

0 commit comments

Comments
 (0)