1 #include <stdint.h>
2 
3 #include "control/controlobject.h"
4 #include "controllers/learningutils.h"
5 #include "test/mixxxtest.h"
6 #include "util/memory.h"
7 
operator <<(std::ostream & stream,const MidiInputMapping & mapping)8 std::ostream& operator<<(std::ostream& stream, const MidiInputMapping& mapping) {
9     stream << mapping.key.key << mapping.options.all;
10     return stream;
11 }
12 
13 class LearningUtilsTest : public MixxxTest {
14   protected:
addMessage(unsigned char status,unsigned char control,unsigned char value)15     void addMessage(unsigned char status, unsigned char control, unsigned char value) {
16         m_messages.append(qMakePair(MidiKey(status, control), value));
17     }
18 
19     /// Check if mapping in present in mapping list.
20     /// Similar to MidiInputMappings::contains(const MidiInputMapping&), but
21     /// does not compare the description.
containsMapping(const MidiInputMappings & haystack,const MidiInputMapping & needle)22     bool containsMapping(const MidiInputMappings& haystack, const MidiInputMapping& needle) {
23         for (const MidiInputMapping& mapping : haystack) {
24             if (mapping.key == needle.key &&
25                     mapping.options == needle.options &&
26                     mapping.control == needle.control) {
27                 return true;
28             }
29         }
30         return false;
31     }
32 
33     QList<QPair<MidiKey, unsigned char> > m_messages;
34 };
35 
TEST_F(LearningUtilsTest,NoteOnButton)36 TEST_F(LearningUtilsTest, NoteOnButton) {
37     // Status: 0x91, Control: 0x10
38     addMessage(MIDI_NOTE_ON | 0x01, 0x10, 0x7F);
39     addMessage(MIDI_NOTE_ON | 0x01, 0x10, 0x00);
40 
41     ConfigKey control("[Test]", "SomeControl");
42     MidiInputMappings mappings =
43             LearningUtils::guessMidiInputMappings(control, m_messages);
44 
45     ASSERT_EQ(1, mappings.size());
46     EXPECT_EQ(MidiInputMapping(MidiKey(MIDI_NOTE_ON | 0x01, 0x10),
47                       MidiOptions(),
48                       control,
49                       mappings.first().description),
50             mappings.first());
51 }
52 
TEST_F(LearningUtilsTest,NoteOnNoteOffButton)53 TEST_F(LearningUtilsTest, NoteOnNoteOffButton) {
54     // Status: 0x91, Control: 0x10
55     addMessage(MIDI_NOTE_ON | 0x01, 0x10, 0x7F);
56     addMessage(MIDI_NOTE_OFF | 0x01, 0x10, 0x00);
57 
58     ConfigKey control("[Test]", "SomeControl");
59     MidiInputMappings mappings =
60             LearningUtils::guessMidiInputMappings(control, m_messages);
61 
62     ASSERT_EQ(2, mappings.size());
63     EXPECT_EQ(MidiInputMapping(MidiKey(MIDI_NOTE_ON | 0x01, 0x10),
64                       MidiOptions(),
65                       control,
66                       mappings.at(0).description),
67             mappings.at(0));
68     EXPECT_EQ(MidiInputMapping(MidiKey(MIDI_NOTE_OFF | 0x01, 0x10),
69                       MidiOptions(),
70                       control,
71                       mappings.at(1).description),
72             mappings.at(1));
73 }
74 
TEST_F(LearningUtilsTest,CC7BitKnob)75 TEST_F(LearningUtilsTest, CC7BitKnob) {
76     // Standard CC 7-bit knobs show up as a MIDI_CC message, single channel,
77     // single control and a variety of values in the range of 0x00 to 0x7F.
78     // Status: 0x81 Control: 0x01
79     addMessage(MIDI_CC | 0x01, 0x10, 0x7F);
80     addMessage(MIDI_CC | 0x01, 0x10, 0x70);
81     addMessage(MIDI_CC | 0x01, 0x10, 0x60);
82     addMessage(MIDI_CC | 0x01, 0x10, 0x50);
83     addMessage(MIDI_CC | 0x01, 0x10, 0x60);
84     addMessage(MIDI_CC | 0x01, 0x10, 0x50);
85     addMessage(MIDI_CC | 0x01, 0x10, 0x40);
86 
87     ConfigKey control("[Test]", "SomeControl");
88     MidiInputMappings mappings =
89             LearningUtils::guessMidiInputMappings(control, m_messages);
90 
91     ASSERT_EQ(1, mappings.size());
92     EXPECT_EQ(MidiInputMapping(MidiKey(MIDI_CC | 0x01, 0x10),
93                       MidiOptions(),
94                       control,
95                       mappings.first().description),
96             mappings.first());
97 }
98 
TEST_F(LearningUtilsTest,CC7BitKnob_CenterPointButton_NoteOn)99 TEST_F(LearningUtilsTest, CC7BitKnob_CenterPointButton_NoteOn) {
100     // This style of 7-bit CC knob is found on controllers like the
101     // VCI-100. They do not emit a center-point CC message with value 0x40 and
102     // instead emit a NOTE_ON (0x7F, 0x00) pair for the center point. This
103     // causes Mixxx to confuse these knobs for buttons and also cause them to
104     // never truly reach their center point. We map the button to the
105     // _set_default control if it exists.
106 
107     // Starting outside the center point.
108     addMessage(MIDI_CC | 0x01, 0x10, 0x7F);
109     addMessage(MIDI_CC | 0x01, 0x10, 0x70);
110     addMessage(MIDI_CC | 0x01, 0x10, 0x60);
111     addMessage(MIDI_CC | 0x01, 0x10, 0x50);
112     addMessage(MIDI_CC | 0x01, 0x10, 0x41);
113     addMessage(MIDI_NOTE_ON | 0x01, 0xE0, 0x7F);
114     addMessage(MIDI_NOTE_ON | 0x01, 0xE0, 0x00);
115     addMessage(MIDI_CC | 0x01, 0x10, 0x3F);
116     addMessage(MIDI_CC | 0x01, 0x10, 0x30);
117     addMessage(MIDI_CC | 0x01, 0x10, 0x20);
118     addMessage(MIDI_CC | 0x01, 0x10, 0x10);
119     addMessage(MIDI_CC | 0x01, 0x10, 0x00);
120 
121     ConfigKey control("[Test]", "SomeControl");
122     // This message recognizer checks for the existence of a _reset control.
123     ConfigKey resetControl("[Test]", "SomeControl_set_default");
124     auto pResetControl = std::make_unique<ControlObject>(resetControl);
125     MidiInputMappings mappings =
126             LearningUtils::guessMidiInputMappings(control, m_messages);
127 
128     ASSERT_EQ(2, mappings.size());
129     EXPECT_TRUE(containsMapping(mappings,
130             MidiInputMapping(
131                     MidiKey(MIDI_CC | 0x01, 0x10), MidiOptions(), control)));
132     EXPECT_TRUE(containsMapping(mappings,
133             MidiInputMapping(MidiKey(MIDI_NOTE_ON | 0x01, 0xE0),
134                     MidiOptions(),
135                     resetControl)));
136 
137     m_messages.clear();
138 
139     // Starting on the center point the VCI-100 only sends a NOTE_ON with value
140     // 0x00 to indicate we started moving but are leaving the center point.
141     addMessage(MIDI_NOTE_ON | 0x01, 0xE0, 0x00);
142     addMessage(MIDI_CC | 0x01, 0x10, 0x3F);
143     addMessage(MIDI_CC | 0x01, 0x10, 0x35);
144     addMessage(MIDI_CC | 0x01, 0x10, 0x30);
145     addMessage(MIDI_CC | 0x01, 0x10, 0x25);
146     addMessage(MIDI_CC | 0x01, 0x10, 0x20);
147     addMessage(MIDI_CC | 0x01, 0x10, 0x15);
148     addMessage(MIDI_CC | 0x01, 0x10, 0x10);
149     addMessage(MIDI_CC | 0x01, 0x10, 0x05);
150     addMessage(MIDI_CC | 0x01, 0x10, 0x00);
151 
152     ASSERT_EQ(2, mappings.size());
153     EXPECT_TRUE(containsMapping(mappings,
154             MidiInputMapping(
155                     MidiKey(MIDI_CC | 0x01, 0x10), MidiOptions(), control)));
156     EXPECT_TRUE(containsMapping(mappings,
157             MidiInputMapping(MidiKey(MIDI_NOTE_ON | 0x01, 0xE0),
158                     MidiOptions(),
159                     resetControl)));
160 }
161 
TEST_F(LearningUtilsTest,CC14BitKnob_MSBFirst)162 TEST_F(LearningUtilsTest, CC14BitKnob_MSBFirst) {
163     // A CC 14-bit knob with the MSB as the first message. We treat streams of
164     // single-channel, CC opcode, 2 control, equal size message streams as
165     // 14-bit CC knobs. The order of the LSB and MSB does not matter to us.
166 
167     addMessage(MIDI_CC | 0x01, 0x11, 0x00);
168     addMessage(MIDI_CC | 0x01, 0x10, 0x00);
169     addMessage(MIDI_CC | 0x01, 0x11, 0x00);
170     addMessage(MIDI_CC | 0x01, 0x10, 0x40);
171     addMessage(MIDI_CC | 0x01, 0x11, 0x00);
172     addMessage(MIDI_CC | 0x01, 0x10, 0x7F);
173     addMessage(MIDI_CC | 0x01, 0x11, 0x01);
174     addMessage(MIDI_CC | 0x01, 0x10, 0x00);
175     addMessage(MIDI_CC | 0x01, 0x11, 0x01);
176     addMessage(MIDI_CC | 0x01, 0x10, 0x40);
177     addMessage(MIDI_CC | 0x01, 0x11, 0x01);
178     addMessage(MIDI_CC | 0x01, 0x10, 0x7F);
179     addMessage(MIDI_CC | 0x01, 0x11, 0x02);
180     addMessage(MIDI_CC | 0x01, 0x10, 0x00);
181     addMessage(MIDI_CC | 0x01, 0x11, 0x02);
182     addMessage(MIDI_CC | 0x01, 0x10, 0x40);
183     addMessage(MIDI_CC | 0x01, 0x11, 0x02);
184     addMessage(MIDI_CC | 0x01, 0x10, 0x7F);
185     addMessage(MIDI_CC | 0x01, 0x11, 0x03);
186     addMessage(MIDI_CC | 0x01, 0x10, 0x00);
187     addMessage(MIDI_CC | 0x01, 0x11, 0x03);
188     addMessage(MIDI_CC | 0x01, 0x10, 0x40);
189     addMessage(MIDI_CC | 0x01, 0x11, 0x03);
190     addMessage(MIDI_CC | 0x01, 0x10, 0x7F);
191 
192     ConfigKey control("[Test]", "SomeControl");
193     MidiInputMappings mappings =
194             LearningUtils::guessMidiInputMappings(control, m_messages);
195 
196     ASSERT_EQ(2, mappings.size());
197     MidiOptions lsb_option;
198     lsb_option.fourteen_bit_lsb = true;
199     EXPECT_TRUE(containsMapping(mappings,
200             MidiInputMapping(
201                     MidiKey(MIDI_CC | 0x01, 0x10), lsb_option, control)));
202     MidiOptions msb_option;
203     msb_option.fourteen_bit_msb = true;
204     EXPECT_TRUE(containsMapping(mappings,
205             MidiInputMapping(
206                     MidiKey(MIDI_CC | 0x01, 0x11), msb_option, control)));
207 }
208 
TEST_F(LearningUtilsTest,CC14BitKnob_LSBFirst)209 TEST_F(LearningUtilsTest, CC14BitKnob_LSBFirst) {
210     // A CC 14-bit knob with the LSB as the first message. We treat streams of
211     // single-channel, CC opcode, 2 control, equal size message streams as
212     // 14-bit CC knobs. The order of the LSB and MSB does not matter to us.
213 
214     addMessage(MIDI_CC | 0x01, 0x10, 0x00);
215     addMessage(MIDI_CC | 0x01, 0x11, 0x00);
216     addMessage(MIDI_CC | 0x01, 0x10, 0x40);
217     addMessage(MIDI_CC | 0x01, 0x11, 0x00);
218     addMessage(MIDI_CC | 0x01, 0x10, 0x7F);
219     addMessage(MIDI_CC | 0x01, 0x11, 0x00);
220     addMessage(MIDI_CC | 0x01, 0x10, 0x00);
221     addMessage(MIDI_CC | 0x01, 0x11, 0x01);
222     addMessage(MIDI_CC | 0x01, 0x10, 0x40);
223     addMessage(MIDI_CC | 0x01, 0x11, 0x01);
224     addMessage(MIDI_CC | 0x01, 0x10, 0x7F);
225     addMessage(MIDI_CC | 0x01, 0x11, 0x01);
226     addMessage(MIDI_CC | 0x01, 0x10, 0x00);
227     addMessage(MIDI_CC | 0x01, 0x11, 0x02);
228     addMessage(MIDI_CC | 0x01, 0x10, 0x40);
229     addMessage(MIDI_CC | 0x01, 0x11, 0x02);
230     addMessage(MIDI_CC | 0x01, 0x10, 0x7F);
231     addMessage(MIDI_CC | 0x01, 0x11, 0x02);
232     addMessage(MIDI_CC | 0x01, 0x10, 0x00);
233     addMessage(MIDI_CC | 0x01, 0x11, 0x03);
234     addMessage(MIDI_CC | 0x01, 0x10, 0x40);
235     addMessage(MIDI_CC | 0x01, 0x11, 0x03);
236     addMessage(MIDI_CC | 0x01, 0x10, 0x7F);
237     addMessage(MIDI_CC | 0x01, 0x11, 0x03);
238 
239     ConfigKey control("[Test]", "SomeControl");
240     MidiInputMappings mappings =
241             LearningUtils::guessMidiInputMappings(control, m_messages);
242 
243     ASSERT_EQ(2, mappings.size());
244     MidiOptions lsb_option;
245     lsb_option.fourteen_bit_lsb = true;
246     EXPECT_TRUE(containsMapping(mappings,
247             MidiInputMapping(
248                     MidiKey(MIDI_CC | 0x01, 0x10), lsb_option, control)));
249     MidiOptions msb_option;
250     msb_option.fourteen_bit_msb = true;
251     EXPECT_TRUE(containsMapping(mappings,
252             MidiInputMapping(
253                     MidiKey(MIDI_CC | 0x01, 0x11), msb_option, control)));
254 }
255 
TEST_F(LearningUtilsTest,CC7BitKnob_ConfusableForCC7BitTicker_Zeroes)256 TEST_F(LearningUtilsTest, CC7BitKnob_ConfusableForCC7BitTicker_Zeroes) {
257     // Make sure that 0x00 is a tell that we are not a 2's complement select
258     // knob.
259     addMessage(MIDI_CC | 0x01, 0x10, 0x01);
260     addMessage(MIDI_CC | 0x01, 0x10, 0x02);
261     addMessage(MIDI_CC | 0x01, 0x10, 0x03);
262     addMessage(MIDI_CC | 0x01, 0x10, 0x02);
263     addMessage(MIDI_CC | 0x01, 0x10, 0x01);
264 
265     ConfigKey control("[Test]", "SomeControl");
266     MidiInputMappings mappings =
267             LearningUtils::guessMidiInputMappings(control, m_messages);
268 
269     MidiOptions options;
270     options.selectknob = true;
271     ASSERT_EQ(1, mappings.size());
272     EXPECT_EQ(MidiInputMapping(MidiKey(MIDI_CC | 0x01, 0x10),
273                       options,
274                       control,
275                       mappings.first().description),
276             mappings.first());
277 
278     m_messages.clear();
279 
280     addMessage(MIDI_CC | 0x01, 0x10, 0x00);
281     addMessage(MIDI_CC | 0x01, 0x10, 0x01);
282     addMessage(MIDI_CC | 0x01, 0x10, 0x02);
283     addMessage(MIDI_CC | 0x01, 0x10, 0x03);
284     addMessage(MIDI_CC | 0x01, 0x10, 0x02);
285     addMessage(MIDI_CC | 0x01, 0x10, 0x01);
286 
287     mappings = LearningUtils::guessMidiInputMappings(control, m_messages);
288 
289     ASSERT_EQ(1, mappings.size());
290     EXPECT_EQ(MidiInputMapping(MidiKey(MIDI_CC | 0x01, 0x10),
291                       MidiOptions(),
292                       control,
293                       mappings.first().description),
294             mappings.first());
295 }
296 
TEST_F(LearningUtilsTest,CC7BitKnob_ConfusableForCC7BitTicker_ZeroIncluded)297 TEST_F(LearningUtilsTest, CC7BitKnob_ConfusableForCC7BitTicker_ZeroIncluded) {
298     // Moving a CC knob through its range multiple times is confusable for
299     // select knobs in some cases. More than 8 distinct values tell us this is
300     // not a select knob.
301     addMessage(MIDI_CC | 0x01, 0x10, 0x7F);
302     addMessage(MIDI_CC | 0x01, 0x10, 0x60);
303     addMessage(MIDI_CC | 0x01, 0x10, 0x50);
304     addMessage(MIDI_CC | 0x01, 0x10, 0x40);
305     addMessage(MIDI_CC | 0x01, 0x10, 0x30);
306     addMessage(MIDI_CC | 0x01, 0x10, 0x20);
307     addMessage(MIDI_CC | 0x01, 0x10, 0x10);
308     addMessage(MIDI_CC | 0x01, 0x10, 0x01);
309     // Zero tells us this is not a select knob.
310     addMessage(MIDI_CC | 0x01, 0x10, 0x00);
311     addMessage(MIDI_CC | 0x01, 0x10, 0x01);
312     addMessage(MIDI_CC | 0x01, 0x10, 0x10);
313     addMessage(MIDI_CC | 0x01, 0x10, 0x20);
314     addMessage(MIDI_CC | 0x01, 0x10, 0x30);
315     addMessage(MIDI_CC | 0x01, 0x10, 0x40);
316     addMessage(MIDI_CC | 0x01, 0x10, 0x50);
317     addMessage(MIDI_CC | 0x01, 0x10, 0x60);
318     addMessage(MIDI_CC | 0x01, 0x10, 0x7F);
319 
320     ConfigKey control("[Test]", "SomeControl");
321     MidiInputMappings mappings =
322             LearningUtils::guessMidiInputMappings(control, m_messages);
323 
324     ASSERT_EQ(1, mappings.size());
325     EXPECT_EQ(MidiInputMapping(MidiKey(MIDI_CC | 0x01, 0x10),
326                       MidiOptions(),
327                       control,
328                       mappings.first().description),
329             mappings.first());
330 }
331 
TEST_F(LearningUtilsTest,CC7BitKnob_ConfusableForCC7BitTicker)332 TEST_F(LearningUtilsTest, CC7BitKnob_ConfusableForCC7BitTicker) {
333     // Moving a CC knob through its range multiple times is confusable for
334     // select knobs when a 0x7F is repeated. More than 8 distinct values tells
335     // us this is not a select knob.
336     addMessage(MIDI_CC | 0x01, 0x10, 0x7F);
337     addMessage(MIDI_CC | 0x01, 0x10, 0x75);
338     addMessage(MIDI_CC | 0x01, 0x10, 0x70);
339     addMessage(MIDI_CC | 0x01, 0x10, 0x65);
340     addMessage(MIDI_CC | 0x01, 0x10, 0x60);
341     addMessage(MIDI_CC | 0x01, 0x10, 0x55);
342     addMessage(MIDI_CC | 0x01, 0x10, 0x50);
343     addMessage(MIDI_CC | 0x01, 0x10, 0x45);
344     addMessage(MIDI_CC | 0x01, 0x10, 0x50);
345     addMessage(MIDI_CC | 0x01, 0x10, 0x60);
346     addMessage(MIDI_CC | 0x01, 0x10, 0x75);
347     addMessage(MIDI_CC | 0x01, 0x10, 0x7F);
348 
349     ConfigKey control("[Test]", "SomeControl");
350     MidiInputMappings mappings =
351             LearningUtils::guessMidiInputMappings(control, m_messages);
352 
353     ASSERT_EQ(1, mappings.size());
354     EXPECT_EQ(MidiInputMapping(MidiKey(MIDI_CC | 0x01, 0x10),
355                       MidiOptions(),
356                       control,
357                       mappings.first().description),
358             mappings.first());
359 }
360 
TEST_F(LearningUtilsTest,CC7BitKnob_ConfusableForSpread64Ticker_StartAndStopOn41)361 TEST_F(LearningUtilsTest,
362         CC7BitKnob_ConfusableForSpread64Ticker_StartAndStopOn41) {
363     // Moving a CC knob through its range multiple times is confusable for
364     // Spread64 select knobs when a 0x41 or 0x3F is repeated. If we start and
365     // stop on 0x41 (and don't pass through 0x40) then this can set off Spread64
366     // detection. We require <8 distinct values for Spread64 detection to
367     // prevent this.
368     addMessage(MIDI_CC | 0x01, 0x10, 0x41);
369     addMessage(MIDI_CC | 0x01, 0x10, 0x45);
370     addMessage(MIDI_CC | 0x01, 0x10, 0x50);
371     addMessage(MIDI_CC | 0x01, 0x10, 0x55);
372     addMessage(MIDI_CC | 0x01, 0x10, 0x60);
373     addMessage(MIDI_CC | 0x01, 0x10, 0x65);
374     addMessage(MIDI_CC | 0x01, 0x10, 0x70);
375     addMessage(MIDI_CC | 0x01, 0x10, 0x75);
376     addMessage(MIDI_CC | 0x01, 0x10, 0x62);
377     addMessage(MIDI_CC | 0x01, 0x10, 0x52);
378     addMessage(MIDI_CC | 0x01, 0x10, 0x48);
379     addMessage(MIDI_CC | 0x01, 0x10, 0x46);
380     addMessage(MIDI_CC | 0x01, 0x10, 0x41);
381 
382     ConfigKey control("[Test]", "SomeControl");
383     MidiInputMappings mappings =
384             LearningUtils::guessMidiInputMappings(control, m_messages);
385 
386     ASSERT_EQ(1, mappings.size());
387     EXPECT_EQ(MidiInputMapping(MidiKey(MIDI_CC | 0x01, 0x10),
388                       MidiOptions(),
389                       control,
390                       mappings.first().description),
391             mappings.first());
392 }
393 
TEST_F(LearningUtilsTest,CC7BitKnob_ConfusableForSpread64Ticker_0x40Included)394 TEST_F(LearningUtilsTest, CC7BitKnob_ConfusableForSpread64Ticker_0x40Included) {
395     // Moving a CC knob through its range multiple times is confusable for
396     // Spread64 select knobs when a 0x41 or 0x3F is repeated.
397     addMessage(MIDI_CC | 0x01, 0x10, 0x70);
398     addMessage(MIDI_CC | 0x01, 0x10, 0x60);
399     addMessage(MIDI_CC | 0x01, 0x10, 0x50);
400     addMessage(MIDI_CC | 0x01, 0x10, 0x41);
401     addMessage(MIDI_CC | 0x01, 0x10, 0x3F);
402     addMessage(MIDI_CC | 0x01, 0x10, 0x30);
403     addMessage(MIDI_CC | 0x01, 0x10, 0x20);
404     addMessage(MIDI_CC | 0x01, 0x10, 0x10);
405     addMessage(MIDI_CC | 0x01, 0x10, 0x00);
406     addMessage(MIDI_CC | 0x01, 0x10, 0x10);
407     addMessage(MIDI_CC | 0x01, 0x10, 0x20);
408     addMessage(MIDI_CC | 0x01, 0x10, 0x30);
409     addMessage(MIDI_CC | 0x01, 0x10, 0x3F);
410     addMessage(MIDI_CC | 0x01, 0x10, 0x41);
411     // 0x40 tells us this is not a Spread64 select knob.
412     addMessage(MIDI_CC | 0x01, 0x10, 0x40);
413     addMessage(MIDI_CC | 0x01, 0x10, 0x50);
414     addMessage(MIDI_CC | 0x01, 0x10, 0x60);
415     addMessage(MIDI_CC | 0x01, 0x10, 0x70);
416 
417     ConfigKey control("[Test]", "SomeControl");
418     MidiInputMappings mappings =
419             LearningUtils::guessMidiInputMappings(control, m_messages);
420 
421     ASSERT_EQ(1, mappings.size());
422     EXPECT_EQ(MidiInputMapping(MidiKey(MIDI_CC | 0x01, 0x10),
423                       MidiOptions(),
424                       control,
425                       mappings.first().description),
426             mappings.first());
427 }
428 
TEST_F(LearningUtilsTest,CC7BitTicker)429 TEST_F(LearningUtilsTest, CC7BitTicker) {
430     // A CC 7-bit ticker (select knob, jog wheel, etc.) shows up as a MIDI_CC
431     // message, single channel, single control and a variety of values in two's
432     // complement. We detect this by looking at jumps across the boundary of
433     // 0x7F to 0x00 so the user has to go forward and backward.
434     // Status: 0x81 Control: 0x10
435     addMessage(MIDI_CC | 0x01, 0x10, 0x7F);
436     addMessage(MIDI_CC | 0x01, 0x10, 0x01);
437     addMessage(MIDI_CC | 0x01, 0x10, 0x02);
438     addMessage(MIDI_CC | 0x01, 0x10, 0x05);
439     addMessage(MIDI_CC | 0x01, 0x10, 0x7E);
440     addMessage(MIDI_CC | 0x01, 0x10, 0x7C);
441     addMessage(MIDI_CC | 0x01, 0x10, 0x70);
442 
443     ConfigKey control("[Test]", "SomeControl");
444     MidiInputMappings mappings =
445             LearningUtils::guessMidiInputMappings(control, m_messages);
446 
447     ASSERT_EQ(1, mappings.size());
448     MidiOptions options;
449     options.selectknob = true;
450     EXPECT_EQ(MidiInputMapping(MidiKey(MIDI_CC | 0x01, 0x10),
451                       options,
452                       control,
453                       mappings.first().description),
454             mappings.first());
455 }
456 
TEST_F(LearningUtilsTest,Spread64Ticker)457 TEST_F(LearningUtilsTest, Spread64Ticker) {
458     // A Spread64 ticker (select knob, jog wheel, etc.) shows up as a MIDI_CC
459     // message, single channel, single control and a variety of values centered
460     // around 0x40 but never including that value (since 0x40 means "not moving")
461     addMessage(MIDI_CC | 0x01, 0x10, 0x41);
462     addMessage(MIDI_CC | 0x01, 0x10, 0x41);
463     addMessage(MIDI_CC | 0x01, 0x10, 0x42);
464     addMessage(MIDI_CC | 0x01, 0x10, 0x41);
465     addMessage(MIDI_CC | 0x01, 0x10, 0x3F);
466     addMessage(MIDI_CC | 0x01, 0x10, 0x3F);
467     addMessage(MIDI_CC | 0x01, 0x10, 0x3F);
468 
469     ConfigKey control("[Test]", "SomeControl");
470     MidiInputMappings mappings =
471             LearningUtils::guessMidiInputMappings(control, m_messages);
472 
473     ASSERT_EQ(1, mappings.size());
474     MidiOptions options;
475     options.spread64 = true;
476     EXPECT_EQ(MidiInputMapping(MidiKey(MIDI_CC | 0x01, 0x10),
477                       options,
478                       control,
479                       mappings.first().description),
480             mappings.first());
481 }
482 
TEST_F(LearningUtilsTest,CC7BitTicker_SingleDirection)483 TEST_F(LearningUtilsTest, CC7BitTicker_SingleDirection) {
484     // A CC 7-bit ticker (select knob, jog wheel, etc.) shows up as a MIDI_CC
485     // message, single channel, single control and a variety of values in two's
486     // complement. It's harder to detect when the user does not change direction
487     // because we do not see 0x00 boundary crossings that are tell-tale signs of
488     // two's complement. If we see repeat 0x01 or 0x7F values then we interpret
489     // those as being a ticker as well.
490 
491     // Status: 0x81 Control: 0x10
492     addMessage(MIDI_CC | 0x01, 0x10, 0x01);
493     addMessage(MIDI_CC | 0x01, 0x10, 0x02);
494     addMessage(MIDI_CC | 0x01, 0x10, 0x01);
495     addMessage(MIDI_CC | 0x01, 0x10, 0x03);
496     addMessage(MIDI_CC | 0x01, 0x10, 0x01);
497 
498     MidiOptions options;
499     options.selectknob = true;
500 
501     ConfigKey control("[Test]", "SomeControl");
502     MidiInputMappings mappings =
503             LearningUtils::guessMidiInputMappings(control, m_messages);
504     ASSERT_EQ(1, mappings.size());
505     EXPECT_EQ(MidiInputMapping(MidiKey(MIDI_CC | 0x01, 0x10),
506                       options,
507                       control,
508                       mappings.first().description),
509             mappings.first());
510 
511     m_messages.clear();
512 
513     // Status: 0x81 Control: 0x10
514     addMessage(MIDI_CC | 0x01, 0x10, 0x7F);
515     addMessage(MIDI_CC | 0x01, 0x10, 0x7E);
516     addMessage(MIDI_CC | 0x01, 0x10, 0x7F);
517     addMessage(MIDI_CC | 0x01, 0x10, 0x7C);
518 
519     mappings = LearningUtils::guessMidiInputMappings(control, m_messages);
520     ASSERT_EQ(1, mappings.size());
521     EXPECT_EQ(MidiInputMapping(MidiKey(MIDI_CC | 0x01, 0x10),
522                       options,
523                       control,
524                       mappings.first().description),
525             mappings.first());
526 }
527 
TEST_F(LearningUtilsTest,SingleMessageSwitchMode_NoteOn)528 TEST_F(LearningUtilsTest, SingleMessageSwitchMode_NoteOn) {
529     // If we only get one NOTE_ON message that is either 0x7F or 0x00 then we
530     // assume this is a binary toggle switch.
531 
532     // Status: 0x91 Control: 0x10
533     addMessage(MIDI_NOTE_ON | 0x01, 0x10, 0x7F);
534 
535     ConfigKey control("[Test]", "SomeControl");
536     MidiInputMappings mappings =
537             LearningUtils::guessMidiInputMappings(control, m_messages);
538 
539     ASSERT_EQ(1, mappings.size());
540     MidiOptions options;
541     options.sw = true;
542     EXPECT_EQ(MidiInputMapping(MidiKey(MIDI_NOTE_ON | 0x01, 0x10),
543                       options,
544                       control,
545                       mappings.first().description),
546             mappings.first());
547 
548     m_messages.clear();
549 
550     // Status: 0x91 Control: 0x10
551     addMessage(MIDI_NOTE_ON | 0x01, 0x10, 0x00);
552 
553     mappings = LearningUtils::guessMidiInputMappings(control, m_messages);
554 
555     ASSERT_EQ(1, mappings.size());
556     EXPECT_EQ(MidiInputMapping(MidiKey(MIDI_NOTE_ON | 0x01, 0x10),
557                       options,
558                       control,
559                       mappings.first().description),
560             mappings.first());
561 }
562 
TEST_F(LearningUtilsTest,SingleMessageSwitchMode_CC)563 TEST_F(LearningUtilsTest, SingleMessageSwitchMode_CC) {
564     // If we only get one CC message that is either 0x7F or 0x00 then we
565     // assume this is a binary toggle switch.
566 
567     // Status: 0xB1 Control: 0x10
568     addMessage(MIDI_CC | 0x01, 0x10, 0x7F);
569 
570     ConfigKey control("[Test]", "SomeControl");
571     MidiInputMappings mappings =
572             LearningUtils::guessMidiInputMappings(control, m_messages);
573 
574     ASSERT_EQ(1, mappings.size());
575     MidiOptions options;
576     options.sw = true;
577     EXPECT_EQ(MidiInputMapping(MidiKey(MIDI_CC | 0x01, 0x10),
578                       options,
579                       control,
580                       mappings.first().description),
581             mappings.first());
582 
583     m_messages.clear();
584 
585     // Status: 0x91 Control: 0x10
586     addMessage(MIDI_CC | 0x01, 0x10, 0x00);
587 
588     mappings = LearningUtils::guessMidiInputMappings(control, m_messages);
589 
590     ASSERT_EQ(1, mappings.size());
591     EXPECT_EQ(MidiInputMapping(MidiKey(MIDI_CC | 0x01, 0x10),
592                       options,
593                       control,
594                       mappings.first().description),
595             mappings.first());
596 }
597 
TEST_F(LearningUtilsTest,MultipleControlsUnrecognized_BindsFirst)598 TEST_F(LearningUtilsTest, MultipleControlsUnrecognized_BindsFirst) {
599     // Status 0x91, Control 0x10
600     addMessage(MIDI_NOTE_ON | 0x01, 0x10, 0x7F);
601     addMessage(MIDI_NOTE_ON | 0x01, 0x10, 0x00);
602 
603     // Status 0x91, Control 0x11
604     addMessage(MIDI_NOTE_ON | 0x01, 0x11, 0x7F);
605     addMessage(MIDI_NOTE_ON | 0x01, 0x11, 0x00);
606 
607     ConfigKey control("[Test]", "SomeControl");
608     MidiInputMappings mappings =
609             LearningUtils::guessMidiInputMappings(control, m_messages);
610     ASSERT_EQ(1, mappings.size());
611     EXPECT_EQ(MidiInputMapping(MidiKey(MIDI_NOTE_ON | 0x01, 0x10),
612                       MidiOptions(),
613                       control,
614                       mappings.first().description),
615             mappings.first());
616 }
617 
TEST_F(LearningUtilsTest,MultipleChannelsUnrecognized_BindsFirst)618 TEST_F(LearningUtilsTest, MultipleChannelsUnrecognized_BindsFirst) {
619     // Status 0x91, Control 0x10
620     addMessage(MIDI_NOTE_ON | 0x01, 0x10, 0x7F);
621     addMessage(MIDI_NOTE_ON | 0x01, 0x10, 0x00);
622 
623     // Status 0x91, Control 0x11
624     addMessage(MIDI_NOTE_ON | 0x02, 0x10, 0x7F);
625     addMessage(MIDI_NOTE_ON | 0x02, 0x10, 0x00);
626 
627     ConfigKey control("[Test]", "SomeControl");
628     MidiInputMappings mappings =
629             LearningUtils::guessMidiInputMappings(control, m_messages);
630     ASSERT_EQ(1, mappings.size());
631     EXPECT_EQ(MidiInputMapping(MidiKey(MIDI_NOTE_ON | 0x01, 0x10),
632                       MidiOptions(),
633                       control,
634                       mappings.first().description),
635             mappings.first());
636 }
637