@@ -3,14 +3,14 @@ use crate::domain::{
3
3
NormalRealTimeTask , ReaperTarget , WeakSession ,
4
4
} ;
5
5
use crossbeam_channel:: Sender ;
6
- use helgoboss_learn:: { ControlValue , MidiSource , MidiSourceValue , Target } ;
6
+ use helgoboss_learn:: { ControlValue , MidiSource , MidiSourceValue , Target , UnitValue } ;
7
7
use helgoboss_midi:: RawShortMessage ;
8
8
use reaper_high:: Reaper ;
9
9
use reaper_medium:: ControlSurface ;
10
10
use rxrust:: prelude:: * ;
11
11
use slog:: { debug, info} ;
12
12
use smallvec:: SmallVec ;
13
- use std:: collections:: HashMap ;
13
+ use std:: collections:: { HashMap , HashSet } ;
14
14
15
15
const NORMAL_TASK_BULK_SIZE : usize = 32 ;
16
16
const FEEDBACK_TASK_BULK_SIZE : usize = 32 ;
@@ -53,50 +53,79 @@ impl ControlSurface for MainProcessor {
53
53
Reaper :: get( ) . logger( ) ,
54
54
"Main processor: Updating all mappings..."
55
55
) ;
56
+ let mut unused_sources = self . currently_feedback_enabled_sources ( ) ;
56
57
// 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) ;
59
69
}
60
70
UpdateAllTargets ( updates) => {
61
71
debug ! (
62
72
Reaper :: get( ) . logger( ) ,
63
73
"Main processor: Updating all targets..."
64
74
) ;
75
+ let mut unused_sources = self . currently_feedback_enabled_sources ( ) ;
65
76
for t in updates. into_iter ( ) {
66
77
if let Some ( m) = self . mappings . get_mut ( & t. id ) {
67
78
m. update_from_target ( t) ;
79
+ if m. feedback_is_enabled ( ) {
80
+ // Mark source as used
81
+ unused_sources. remove ( m. source ( ) ) ;
82
+ }
68
83
} else {
69
84
panic ! ( "Couldn't find mapping while updating all targets" ) ;
70
85
}
71
86
}
72
- self . handle_feedback_after_batch_mapping_update ( ) ;
87
+ self . handle_feedback_after_batch_mapping_update ( & unused_sources ) ;
73
88
}
74
89
UpdateSingleMapping ( mapping) => {
75
90
debug ! (
76
91
Reaper :: get( ) . logger( ) ,
77
92
"Main processor: Updating mapping {:?}..." ,
78
93
mapping. id( )
79
94
) ;
80
- // TODO-medium We could send a null-feedback to switch off
81
95
// (Re)subscribe to or unsubscribe from feedback
82
96
match mapping. target ( ) {
83
- // (Re)subscribe
84
97
Some ( target) if mapping. feedback_is_enabled ( ) => {
98
+ // (Re)subscribe
85
99
let subscription = send_feedback_when_target_value_changed (
86
100
self . self_feedback_sender . clone ( ) ,
87
101
mapping. id ( ) ,
88
102
target,
89
103
) ;
90
104
self . feedback_subscriptions
91
105
. insert ( mapping. id ( ) , subscription) ;
106
+ self . send_feedback ( mapping. feedback_if_enabled ( ) ) ;
92
107
}
93
- // Unsubscribe (if the feedback was enabled before)
94
108
_ => {
109
+ // Unsubscribe (if the feedback was enabled before)
95
110
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
+ }
96
127
}
97
128
} ;
98
- // Send feedback if enabled
99
- self . send_feedback ( mapping. feedback_if_enabled ( ) ) ;
100
129
// Update hash map entry
101
130
self . mappings . insert ( mapping. id ( ) , mapping) ;
102
131
}
@@ -127,19 +156,13 @@ impl ControlSurface for MainProcessor {
127
156
Control { mapping_id, value } => {
128
157
if let Some ( m) = self . mappings . get_mut ( & mapping_id) {
129
158
// 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.
143
166
m. control_if_enabled ( value) ;
144
167
} ;
145
168
}
@@ -208,23 +231,39 @@ impl MainProcessor {
208
231
. collect ( )
209
232
}
210
233
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.
213
248
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 ) ) ;
226
264
}
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.
228
267
self . feedback_buffer . reset ( ) ;
229
268
self . send_feedback ( self . feedback_all ( ) ) ;
230
269
}
0 commit comments