1 #include <QScopedPointer>
2 
3 #include <gmock/gmock.h>
4 
5 #include "test/mixxxtest.h"
6 #include "controllers/midi/midicontroller.h"
7 #include "controllers/midi/midicontrollerpreset.h"
8 #include "controllers/midi/midimessage.h"
9 #include "control/controlpushbutton.h"
10 #include "control/controlpotmeter.h"
11 #include "util/time.h"
12 
13 class MockMidiController : public MidiController {
14   public:
MockMidiController(UserSettingsPointer pConfig)15     explicit MockMidiController(UserSettingsPointer pConfig)
16             : MidiController(pConfig) {
17     }
~MockMidiController()18     ~MockMidiController() override { }
19 
20     MOCK_METHOD0(open, int());
21     MOCK_METHOD0(close, int());
22     MOCK_METHOD3(sendShortMsg, void(unsigned char status,
23                                     unsigned char byte1,
24                                     unsigned char byte2));
25     MOCK_METHOD1(send, void(const QByteArray& data));
26     MOCK_CONST_METHOD0(isPolling, bool());
27 };
28 
29 class MidiControllerTest : public MixxxTest {
30   protected:
SetUp()31     void SetUp() override {
32         m_pController.reset(new MockMidiController(config()));
33     }
34 
addMapping(const MidiInputMapping & mapping)35     void addMapping(const MidiInputMapping& mapping) {
36         m_preset.addInputMapping(mapping.key.key, mapping);
37     }
38 
loadPreset(const MidiControllerPreset & preset)39     void loadPreset(const MidiControllerPreset& preset) {
40         m_pController->visit(&preset);
41     }
42 
receive(unsigned char status,unsigned char control,unsigned char value)43     void receive(unsigned char status, unsigned char control,
44                  unsigned char value) {
45         // TODO(rryan): This test doesn't care about timestamps.
46         m_pController->receive(status, control, value, mixxx::Time::elapsed());
47     }
48 
49     MidiControllerPreset m_preset;
50     QScopedPointer<MockMidiController> m_pController;
51 };
52 
TEST_F(MidiControllerTest,ReceiveMessage_PushButtonCO_PushOnOff)53 TEST_F(MidiControllerTest, ReceiveMessage_PushButtonCO_PushOnOff) {
54     // Most MIDI controller send push-buttons as (NOTE_ON, 0x7F) for press and
55     // (NOTE_OFF, 0x00) for release.
56     ConfigKey key("[Channel1]", "hotcue_1_activate");
57     ControlPushButton cpb(key);
58 
59     unsigned char channel = 0x01;
60     unsigned char control = 0x10;
61 
62     addMapping(MidiInputMapping(MidiKey(MIDI_NOTE_ON | channel, control),
63                                 MidiOptions(), key));
64     addMapping(MidiInputMapping(MidiKey(MIDI_NOTE_OFF | channel, control),
65                                 MidiOptions(), key));
66     loadPreset(m_preset);
67 
68     // Receive an on/off, sets the control on/off with each press.
69     receive(MIDI_NOTE_ON | channel, control, 0x7F);
70     EXPECT_LT(0.0, cpb.get());
71     receive(MIDI_NOTE_OFF | channel, control, 0x00);
72     EXPECT_DOUBLE_EQ(0.0, cpb.get());
73 
74     // Receive an on/off, sets the control on/off with each press.
75     receive(MIDI_NOTE_ON | channel, control, 0x7F);
76     EXPECT_LT(0.0, cpb.get());
77     receive(MIDI_NOTE_OFF | channel, control, 0x00);
78     EXPECT_DOUBLE_EQ(0.0, cpb.get());
79 }
80 
TEST_F(MidiControllerTest,ReceiveMessage_PushButtonCO_PushOnOn)81 TEST_F(MidiControllerTest, ReceiveMessage_PushButtonCO_PushOnOn) {
82     // Some MIDI controllers send push-buttons as (NOTE_ON, 0x7f) for press and
83     // (NOTE_ON, 0x00) for release.
84     ConfigKey key("[Channel1]", "hotcue_1_activate");
85     ControlPushButton cpb(key);
86 
87     unsigned char channel = 0x01;
88     unsigned char control = 0x10;
89 
90     addMapping(MidiInputMapping(MidiKey(MIDI_NOTE_ON | channel, control),
91                                 MidiOptions(), key));
92     loadPreset(m_preset);
93 
94     // Receive an on/off, sets the control on/off with each press.
95     receive(MIDI_NOTE_ON | channel, control, 0x7F);
96     EXPECT_LT(0.0, cpb.get());
97     receive(MIDI_NOTE_ON | channel, control, 0x00);
98     EXPECT_DOUBLE_EQ(0.0, cpb.get());
99 
100     // Receive an on/off, sets the control on/off with each press.
101     receive(MIDI_NOTE_ON | channel, control, 0x7F);
102     EXPECT_LT(0.0, cpb.get());
103     receive(MIDI_NOTE_ON | channel, control, 0x00);
104     EXPECT_DOUBLE_EQ(0.0, cpb.get());
105 }
106 
TEST_F(MidiControllerTest,ReceiveMessage_PushButtonCO_ToggleOnOff_ButtonMidiOption)107 TEST_F(MidiControllerTest, ReceiveMessage_PushButtonCO_ToggleOnOff_ButtonMidiOption) {
108     // Using the button MIDI option allows you to use a MIDI toggle button as a
109     // push button.
110     ConfigKey key("[Channel1]", "hotcue_1_activate");
111     ControlPushButton cpb(key);
112 
113     unsigned char channel = 0x01;
114     unsigned char control = 0x10;
115 
116     MidiOptions options;
117     options.button = true;
118 
119     addMapping(MidiInputMapping(MidiKey(MIDI_NOTE_ON | channel, control),
120                                 options, key));
121     addMapping(MidiInputMapping(MidiKey(MIDI_NOTE_OFF | channel, control),
122                                 options, key));
123     loadPreset(m_preset);
124 
125     // NOTE(rryan): This behavior is broken!
126 
127     // Toggle the switch on, sets the push button on.
128     receive(MIDI_NOTE_ON | channel, control, 0x7F);
129     EXPECT_LT(0.0, cpb.get());
130 
131     // The push button is stuck down here!
132 
133     // Toggle the switch off, sets the push button off.
134     receive(MIDI_NOTE_OFF | channel, control, 0x00);
135     EXPECT_DOUBLE_EQ(0.0, cpb.get());
136 }
137 
TEST_F(MidiControllerTest,ReceiveMessage_PushButtonCO_ToggleOnOff_SwitchMidiOption)138 TEST_F(MidiControllerTest, ReceiveMessage_PushButtonCO_ToggleOnOff_SwitchMidiOption) {
139     // Using the switch MIDI option interprets a MIDI toggle button as a toggle
140     // button rather than a momentary push button.
141     ConfigKey key("[Channel1]", "hotcue_1_activate");
142     ControlPushButton cpb(key);
143 
144     unsigned char channel = 0x01;
145     unsigned char control = 0x10;
146 
147     MidiOptions options;
148     options.sw = true;
149 
150     addMapping(MidiInputMapping(MidiKey(MIDI_NOTE_ON | channel, control),
151                                 options, key));
152     addMapping(MidiInputMapping(MidiKey(MIDI_NOTE_OFF | channel, control),
153                                 options, key));
154     loadPreset(m_preset);
155 
156     // NOTE(rryan): This behavior is broken!
157 
158     // Toggle the switch on, sets the push button on.
159     receive(MIDI_NOTE_ON | channel, control, 0x7F);
160     EXPECT_LT(0.0, cpb.get());
161 
162     // The push button is stuck down here!
163 
164     // Toggle the switch off, sets the push button on again.
165     receive(MIDI_NOTE_OFF | channel, control, 0x00);
166     EXPECT_LT(0.0, cpb.get());
167 
168     // NOTE(rryan): What is supposed to happen in this case? It's an open
169     // question I think. I think if you want to connect a switch MIDI control to
170     // a push button CO then the switch should directly set the CO. After all,
171     // the preset author asked for the switch to be interpreted as a switch. If
172     // they want the switch to act like a push button, they should use the
173     // button MIDI option.
174     //
175     // Most of our push buttons trigger behavior on press and do nothing on
176     // release, and most don't care about being "stuck down" except for hotcue
177     // and cue controls that have preview behavior.
178 
179     // "reverse" is an example of a push button that is a push button because we
180     // want the default behavior to be momentary press and not toggle. If I
181     // mapped a switch to it, I would expect the switch to enable it (set it to
182     // 1) when the switch was enabled and set it to 0 when the switch was
183     // disabled. So I think we should change the switch option to behave like
184     // this.
185 }
186 
TEST_F(MidiControllerTest,ReceiveMessage_PushButtonCO_PushCC)187 TEST_F(MidiControllerTest, ReceiveMessage_PushButtonCO_PushCC) {
188     // Some MIDI controllers (e.g. Korg nanoKONTROL) send momentary push-buttons
189     // as (CC, 0x7f) for press and (CC, 0x00) for release.
190     ConfigKey key("[Channel1]", "hotcue_1_activate");
191     ControlPushButton cpb(key);
192 
193     unsigned char channel = 0x01;
194     unsigned char control = 0x10;
195 
196     addMapping(MidiInputMapping(MidiKey(MIDI_CC | channel, control),
197                                 MidiOptions(), key));
198     loadPreset(m_preset);
199 
200     // Receive an on/off, sets the control on/off with each press.
201     receive(MIDI_CC | channel, control, 0x7F);
202     EXPECT_LT(0.0, cpb.get());
203     receive(MIDI_CC | channel, control, 0x00);
204     EXPECT_DOUBLE_EQ(0.0, cpb.get());
205 
206     // Receive an on/off, sets the control on/off with each press.
207     receive(MIDI_CC | channel, control, 0x7F);
208     EXPECT_LT(0.0, cpb.get());
209     receive(MIDI_CC | channel, control, 0x00);
210     EXPECT_DOUBLE_EQ(0.0, cpb.get());
211 }
212 
TEST_F(MidiControllerTest,ReceiveMessage_ToggleCO_PushOnOff)213 TEST_F(MidiControllerTest, ReceiveMessage_ToggleCO_PushOnOff) {
214     // Most MIDI controller send push-buttons as (NOTE_ON, 0x7F) for press and
215     // (NOTE_OFF, 0x00) for release.
216     ConfigKey key("[Channel1]", "keylock");
217     ControlPushButton cpb(key);
218     cpb.setButtonMode(ControlPushButton::TOGGLE);
219 
220     unsigned char channel = 0x01;
221     unsigned char control = 0x10;
222 
223     addMapping(MidiInputMapping(MidiKey(MIDI_NOTE_ON | channel, control),
224                                 MidiOptions(), key));
225     addMapping(MidiInputMapping(MidiKey(MIDI_NOTE_OFF | channel, control),
226                                 MidiOptions(), key));
227     loadPreset(m_preset);
228 
229     // Receive an on/off, toggles the control.
230     receive(MIDI_NOTE_ON | channel, control, 0x7F);
231     receive(MIDI_NOTE_OFF | channel, control, 0x00);
232 
233     EXPECT_LT(0.0, cpb.get());
234 
235     // Receive an on/off, toggles the control.
236     receive(MIDI_NOTE_ON | channel, control, 0x7F);
237     receive(MIDI_NOTE_OFF | channel, control, 0x00);
238 
239     EXPECT_DOUBLE_EQ(0.0, cpb.get());
240 }
241 
TEST_F(MidiControllerTest,ReceiveMessage_ToggleCO_PushOnOn)242 TEST_F(MidiControllerTest, ReceiveMessage_ToggleCO_PushOnOn) {
243     // Some MIDI controllers send push-buttons as (NOTE_ON, 0x7f) for press and
244     // (NOTE_ON, 0x00) for release.
245     ConfigKey key("[Channel1]", "keylock");
246     ControlPushButton cpb(key);
247     cpb.setButtonMode(ControlPushButton::TOGGLE);
248 
249     unsigned char channel = 0x01;
250     unsigned char control = 0x10;
251 
252     addMapping(MidiInputMapping(MidiKey(MIDI_NOTE_ON | channel, control),
253                                 MidiOptions(), key));
254     loadPreset(m_preset);
255 
256     // Receive an on/off, toggles the control.
257     receive(MIDI_NOTE_ON | channel, control, 0x7F);
258     receive(MIDI_NOTE_ON | channel, control, 0x00);
259 
260     EXPECT_LT(0.0, cpb.get());
261 
262     // Receive an on/off, toggles the control.
263     receive(MIDI_NOTE_ON | channel, control, 0x7F);
264     receive(MIDI_NOTE_ON | channel, control, 0x00);
265 
266     EXPECT_DOUBLE_EQ(0.0, cpb.get());
267 }
268 
TEST_F(MidiControllerTest,ReceiveMessage_ToggleCO_ToggleOnOff_ButtonMidiOption)269 TEST_F(MidiControllerTest, ReceiveMessage_ToggleCO_ToggleOnOff_ButtonMidiOption) {
270     // Using the button MIDI option allows you to use a MIDI toggle button as a
271     // push button.
272     ConfigKey key("[Channel1]", "keylock");
273     ControlPushButton cpb(key);
274     cpb.setButtonMode(ControlPushButton::TOGGLE);
275 
276     unsigned char channel = 0x01;
277     unsigned char control = 0x10;
278 
279     MidiOptions options;
280     options.button = true;
281 
282     addMapping(MidiInputMapping(MidiKey(MIDI_NOTE_ON | channel, control),
283                                 options, key));
284     addMapping(MidiInputMapping(MidiKey(MIDI_NOTE_OFF | channel, control),
285                                 options, key));
286     loadPreset(m_preset);
287 
288     // NOTE(rryan): If the intended behavior of the button MIDI option is to
289     // make a toggle MIDI button act like a push button then this isn't
290     // working. The toggle on toggles the CO but the toggle off does nothing.
291 
292     // Toggle the switch on, since it is interpreted as a button press it
293     // toggles the button on.
294     receive(MIDI_NOTE_ON | channel, control, 0x7F);
295     EXPECT_LT(0.0, cpb.get());
296 
297     // Toggle the switch off, since it is interpreted as a button release it
298     // does nothing to the toggle button.
299     receive(MIDI_NOTE_OFF | channel, control, 0x00);
300     EXPECT_LT(0.0, cpb.get());
301 }
302 
TEST_F(MidiControllerTest,ReceiveMessage_ToggleCO_ToggleOnOff_SwitchMidiOption)303 TEST_F(MidiControllerTest, ReceiveMessage_ToggleCO_ToggleOnOff_SwitchMidiOption) {
304     // Using the switch MIDI option interprets a MIDI toggle button as a toggle
305     // button rather than a momentary push button.
306     ConfigKey key("[Channel1]", "keylock");
307     ControlPushButton cpb(key);
308     cpb.setButtonMode(ControlPushButton::TOGGLE);
309 
310     unsigned char channel = 0x01;
311     unsigned char control = 0x10;
312 
313     MidiOptions options;
314     options.sw = true;
315 
316     addMapping(MidiInputMapping(MidiKey(MIDI_NOTE_ON | channel, control),
317                                 options, key));
318     addMapping(MidiInputMapping(MidiKey(MIDI_NOTE_OFF | channel, control),
319                                 options, key));
320     loadPreset(m_preset);
321 
322     // NOTE(rryan): If the intended behavior of switch MIDI option is to make a
323     // toggle MIDI button act like a toggle button then this isn't working. The
324     // toggle on presses the CO and the toggle off presses the CO. This toggles
325     // the control but allows it to get out of sync.
326 
327     // Toggle the switch on, since it is interpreted as a button press it
328     // toggles the control on.
329     receive(MIDI_NOTE_ON | channel, control, 0x7F);
330     EXPECT_LT(0.0, cpb.get());
331 
332     // Toggle the switch off, since it is interpreted as a button press it
333     // toggles the control off.
334     receive(MIDI_NOTE_OFF | channel, control, 0x00);
335     EXPECT_DOUBLE_EQ(0.0, cpb.get());
336 
337     // Meanwhile, the GUI toggles the control on again.
338     // NOTE(rryan): Now the MIDI toggle button is out of sync with the toggle
339     // CO.
340     cpb.set(1.0);
341 
342     // Toggle the switch on, since it is interpreted as a button press it
343     // toggles the control off (since it was on).
344     receive(MIDI_NOTE_ON | channel, control, 0x7F);
345     EXPECT_DOUBLE_EQ(0.0, cpb.get());
346 
347     // Toggle the switch off, since it is interpreted as a button press it
348     // toggles the control on (since it was off).
349     receive(MIDI_NOTE_OFF | channel, control, 0x00);
350     EXPECT_LT(0.0, cpb.get());
351 }
352 
TEST_F(MidiControllerTest,ReceiveMessage_ToggleCO_PushCC)353 TEST_F(MidiControllerTest, ReceiveMessage_ToggleCO_PushCC) {
354     // Some MIDI controllers (e.g. Korg nanoKONTROL) send momentary push-buttons
355     // as (CC, 0x7f) for press and (CC, 0x00) for release.
356     ConfigKey key("[Channel1]", "keylock");
357     ControlPushButton cpb(key);
358     cpb.setButtonMode(ControlPushButton::TOGGLE);
359 
360     unsigned char channel = 0x01;
361     unsigned char control = 0x10;
362 
363     addMapping(MidiInputMapping(MidiKey(MIDI_CC | channel, control),
364                                 MidiOptions(), key));
365     loadPreset(m_preset);
366 
367     // Receive an on/off, toggles the control.
368     receive(MIDI_CC | channel, control, 0x7F);
369     receive(MIDI_CC | channel, control, 0x00);
370 
371     EXPECT_LT(0.0, cpb.get());
372 
373     // Receive an on/off, toggles the control.
374     receive(MIDI_CC | channel, control, 0x7F);
375     receive(MIDI_CC | channel, control, 0x00);
376 
377     EXPECT_DOUBLE_EQ(0.0, cpb.get());
378 }
379 
TEST_F(MidiControllerTest,ReceiveMessage_PotMeterCO_7BitCC)380 TEST_F(MidiControllerTest, ReceiveMessage_PotMeterCO_7BitCC) {
381     ConfigKey key("[Channel1]", "playposition");
382 
383     const double kMinValue = -1234.5;
384     const double kMaxValue = 678.9;
385     const double kMiddleValue = (kMinValue + kMaxValue) * 0.5;
386     ControlPotmeter potmeter(key, kMinValue, kMaxValue);
387 
388     unsigned char channel = 0x01;
389     unsigned char control = 0x10;
390 
391     addMapping(MidiInputMapping(MidiKey(MIDI_CC | channel, control),
392                                 MidiOptions(), key));
393     loadPreset(m_preset);
394 
395     // Receive a 0, MIDI parameter should map to the min value.
396     receive(MIDI_CC | channel, control, 0x00);
397     EXPECT_DOUBLE_EQ(kMinValue, potmeter.get());
398 
399     // Receive a 0x7F, MIDI parameter should map to the potmeter max value.
400     receive(MIDI_CC | channel, control, 0x7F);
401     EXPECT_DOUBLE_EQ(kMaxValue, potmeter.get());
402 
403     // Receive a 0x40, MIDI parameter should map to the potmeter middle value.
404     receive(MIDI_CC | channel, control, 0x40);
405     EXPECT_DOUBLE_EQ(kMiddleValue, potmeter.get());
406 }
407 
TEST_F(MidiControllerTest,ReceiveMessage_PotMeterCO_14BitCC)408 TEST_F(MidiControllerTest, ReceiveMessage_PotMeterCO_14BitCC) {
409     ConfigKey key("[Channel1]", "playposition");
410 
411     const double kMinValue = -1234.5;
412     const double kMaxValue = 678.9;
413     const double kMiddleValue = (kMinValue + kMaxValue) * 0.5;
414     ControlPotmeter potmeter(key, kMinValue, kMaxValue);
415     potmeter.set(0);
416 
417     unsigned char channel = 0x01;
418     unsigned char lsb_control = 0x10;
419     unsigned char msb_control = 0x11;
420 
421     MidiOptions lsb;
422     lsb.fourteen_bit_lsb = true;
423 
424     MidiOptions msb;
425     msb.fourteen_bit_msb = true;
426 
427     addMapping(MidiInputMapping(MidiKey(MIDI_CC | channel, lsb_control),
428                                 lsb, key));
429     addMapping(MidiInputMapping(MidiKey(MIDI_CC | channel, msb_control),
430                                 msb, key));
431     loadPreset(m_preset);
432 
433     // If kMinValue or kMaxValue are such that the middle value is 0 then the
434     // set(0) commands below allow us to hide failures.
435     ASSERT_NE(0.0, kMiddleValue);
436 
437     // Receive a 0x0000 (lsb-first), MIDI parameter should map to the min value.
438     potmeter.set(0);
439     receive(MIDI_CC | channel, lsb_control, 0x00);
440     receive(MIDI_CC | channel, msb_control, 0x00);
441     EXPECT_DOUBLE_EQ(kMinValue, potmeter.get());
442 
443     // Receive a 0x0000 (msb-first), MIDI parameter should map to the min value.
444     potmeter.set(0);
445     receive(MIDI_CC | channel, msb_control, 0x00);
446     receive(MIDI_CC | channel, lsb_control, 0x00);
447     EXPECT_DOUBLE_EQ(kMinValue, potmeter.get());
448 
449     // Receive a 0x3FFF (lsb-first), MIDI parameter should map to the max value.
450     potmeter.set(0);
451     receive(MIDI_CC | channel, lsb_control, 0x7F);
452     receive(MIDI_CC | channel, msb_control, 0x7F);
453     EXPECT_DOUBLE_EQ(kMaxValue, potmeter.get());
454 
455     // Receive a 0x3FFF (msb-first), MIDI parameter should map to the max value.
456     potmeter.set(0);
457     receive(MIDI_CC | channel, msb_control, 0x7F);
458     receive(MIDI_CC | channel, lsb_control, 0x7F);
459     EXPECT_DOUBLE_EQ(kMaxValue, potmeter.get());
460 
461     // Receive a 0x2000 (lsb-first), MIDI parameter should map to the middle
462     // value.
463     potmeter.set(0);
464     receive(MIDI_CC | channel, lsb_control, 0x00);
465     receive(MIDI_CC | channel, msb_control, 0x40);
466     EXPECT_DOUBLE_EQ(kMiddleValue, potmeter.get());
467 
468     // Receive a 0x2000 (msb-first), MIDI parameter should map to the middle
469     // value.
470     potmeter.set(0);
471     receive(MIDI_CC | channel, msb_control, 0x40);
472     receive(MIDI_CC | channel, lsb_control, 0x00);
473     EXPECT_DOUBLE_EQ(kMiddleValue, potmeter.get());
474 
475     // Check the 14-bit resolution is actually present. Receive a 0x2001
476     // (msb-first), MIDI parameter should map to the middle value plus a tiny
477     // amount. Scaling is not quite linear for MIDI parameters so just check
478     // that incrementing the LSB by 1 is greater than the middle value.
479     potmeter.set(0);
480     receive(MIDI_CC | channel, msb_control, 0x40);
481     receive(MIDI_CC | channel, lsb_control, 0x01);
482     EXPECT_LT(kMiddleValue, potmeter.get());
483 
484     // Check the 14-bit resolution is actually present. Receive a 0x2001
485     // (lsb-first), MIDI parameter should map to the middle value plus a tiny
486     // amount. Scaling is not quite linear for MIDI parameters so just check
487     // that incrementing the LSB by 1 is greater than the middle value.
488     potmeter.set(0);
489     receive(MIDI_CC | channel, lsb_control, 0x01);
490     receive(MIDI_CC | channel, msb_control, 0x40);
491     EXPECT_LT(kMiddleValue, potmeter.get());
492 }
493 
TEST_F(MidiControllerTest,ReceiveMessage_PotMeterCO_14BitPitchBend)494 TEST_F(MidiControllerTest, ReceiveMessage_PotMeterCO_14BitPitchBend) {
495     ConfigKey key("[Channel1]", "rate");
496 
497     const double kMinValue = -1234.5;
498     const double kMaxValue = 678.9;
499     const double kMiddleValue = (kMinValue + kMaxValue) * 0.5;
500     ControlPotmeter potmeter(key, kMinValue, kMaxValue);
501     unsigned char channel = 0x01;
502 
503     // The control is ignored in mappings for messages where the control is part
504     // of the payload.
505     addMapping(MidiInputMapping(MidiKey(MIDI_PITCH_BEND | channel, 0xFF),
506                                 MidiOptions(), key));
507     loadPreset(m_preset);
508 
509     // Receive a 0x0000, MIDI parameter should map to the min value.
510     receive(MIDI_PITCH_BEND | channel, 0x00, 0x00);
511     EXPECT_DOUBLE_EQ(kMinValue, potmeter.get());
512 
513     // Receive a 0x3FFF, MIDI parameter should map to the potmeter max value.
514     receive(MIDI_PITCH_BEND | channel, 0x7F, 0x7F);
515     EXPECT_DOUBLE_EQ(kMaxValue, potmeter.get());
516 
517     // Receive a 0x2000, MIDI parameter should map to the potmeter middle value.
518     receive(MIDI_PITCH_BEND | channel, 0x00, 0x40);
519     EXPECT_DOUBLE_EQ(kMiddleValue, potmeter.get());
520 
521     // Check the 14-bit resolution is actually present. Receive a 0x2001, MIDI
522     // parameter should map to the middle value plus a tiny amount. Scaling is
523     // not quite linear for MIDI parameters so just check that incrementing the
524     // LSB by 1 is greater than the middle value.
525     receive(MIDI_PITCH_BEND | channel, 0x01, 0x40);
526     EXPECT_LT(kMiddleValue, potmeter.get());
527 }
528