1 #include <QSet>
2 #include <QMap>
3 #include <QtDebug>
4 
5 #include "controllers/learningutils.h"
6 #include "controllers/midi/midiutils.h"
7 #include "control/controlobject.h"
8 
9 typedef QPair<MidiKey, unsigned char> MidiKeyAndValue;
10 
11 struct MessageStats {
MessageStatsMessageStats12     MessageStats()
13             : message_count(0),
14               last_value(0) {
15     }
16 
addMessageMessageStats17     void addMessage(const MidiKeyAndValue& message) {
18         // If we've never seen a message before, set last_value to this value.
19         if (message_count == 0) {
20             last_value = message.second;
21         }
22 
23         MidiOpCode opcode = MidiUtils::opCodeFromStatus(message.first.status);
24         unsigned char channel = MidiUtils::channelFromStatus(message.first.status);
25         opcodes.insert(opcode);
26         channels.insert(channel);
27         controls.insert(message.first.control);
28 
29         message_count++;
30         value_histogram[message.second]++;
31 
32         // Convert to integers first to get negative values.
33         int absolute_difference = abs(static_cast<int>(message.second) -
34                                       static_cast<int>(last_value));
35         abs_diff_histogram[absolute_difference]++;
36         last_value = message.second;
37     }
38 
39     int message_count;
40     QSet<MidiOpCode> opcodes;
41     QSet<unsigned char> channels;
42     QSet<unsigned char> controls;
43     QMap<unsigned char, int> value_histogram;
44     // The range of differences for unsigned char is -255 through 255.
45     QMap<int, int> abs_diff_histogram;
46     unsigned char last_value;
47 };
48 
49 // static
guessMidiInputMappings(const ConfigKey & control,const QList<QPair<MidiKey,unsigned char>> & messages)50 MidiInputMappings LearningUtils::guessMidiInputMappings(
51         const ConfigKey& control,
52         const QList<QPair<MidiKey, unsigned char> >& messages) {
53     QMap<unsigned char, MessageStats> stats_by_control;
54     MessageStats stats;
55 
56     // Analyze the message
57     foreach (const MidiKeyAndValue& message, messages) {
58         stats.addMessage(message);
59         stats_by_control[message.first.control].addMessage(message);
60     }
61 
62     qDebug() << "LearningUtils guessing MIDI mapping from" << messages.size() << "messages.";
63 
64     foreach (MidiOpCode opcode, stats.opcodes) {
65         qDebug() << "Opcode:" << opcode;
66     }
67 
68     foreach (unsigned char channel, stats.channels) {
69         qDebug() << "Channel:" << channel;
70     }
71 
72     foreach (unsigned char control, stats.controls) {
73         qDebug() << "Control:" << control;
74     }
75 
76     for (auto it = stats.value_histogram.constBegin();
77          it != stats.value_histogram.constEnd(); ++it) {
78         qDebug() << "Overall Value:" << it.key()
79                  << "count" << it.value();
80     }
81 
82     for (auto control_it = stats_by_control.constBegin();
83          control_it != stats_by_control.constEnd(); ++control_it) {
84         QString controlName = QString("Control %1").arg(control_it.key());
85         for (auto it = control_it->value_histogram.constBegin();
86              it != control_it->value_histogram.constEnd(); ++it) {
87             qDebug() << controlName << "Value:" << it.key()
88                      << "count" << it.value();
89         }
90     }
91 
92     MidiInputMappings mappings;
93 
94     bool one_control = stats.controls.size() == 1;
95     bool one_channel = stats.channels.size() == 1;
96     bool only_note_on = stats.opcodes.size() == 1 && stats.opcodes.contains(MIDI_NOTE_ON);
97     bool only_note_on_and_note_off = stats.opcodes.size() == 2 &&
98             stats.opcodes.contains(MIDI_NOTE_ON) &&
99             stats.opcodes.contains(MIDI_NOTE_OFF);
100 
101     bool has_cc = stats.opcodes.contains(MIDI_CC);
102     bool only_cc = stats.opcodes.size() == 1 && has_cc;
103     int num_cc_controls = 0;
104     for (auto it = stats_by_control.constBegin();
105          it != stats_by_control.constEnd(); ++it) {
106         if (it->opcodes.contains(MIDI_CC)) {
107             num_cc_controls++;
108         }
109     }
110 
111     bool two_values_7bit_max_and_min = stats.value_histogram.size() == 2 &&
112             stats.value_histogram.contains(0x00) && stats.value_histogram.contains(0x7F);
113     bool one_value_7bit_max_or_min = stats.value_histogram.size() == 1 &&
114             (stats.value_histogram.contains(0x00) || stats.value_histogram.contains(0x7F));
115     bool multiple_one_or_7f_values = stats.value_histogram.value(0x01, 0) > 1 ||
116             stats.value_histogram.value(0x7F, 0) > 1;
117     bool under_8_distinct_values = stats.value_histogram.size() < 8;
118     bool no_0x00_value = !stats.value_histogram.contains(0x00);
119     bool no_0x40_value = !stats.value_histogram.contains(0x40);
120     bool multiple_values_around_0x40 = stats.value_histogram.value(0x41, 0) > 1 &&
121             stats.value_histogram.value(0x3F, 0) > 1 && no_0x40_value &&
122             under_8_distinct_values;
123 
124     // QMap keys are sorted so we can check this easily by checking the last key
125     // is <= 0x7F.
126     bool only_7bit_values = !stats.value_histogram.isEmpty() &&
127             (stats.value_histogram.end() - 1).key() <= 0x7F;
128 
129     // A 7-bit two's complement ticker swinging from +1 to -1 can generate
130     // unsigned differences of up to 126 (0x7E). If we see differences in
131     // individual messages above 96 (0x60) that's a good hint that we're looking
132     // at a two's complement ticker.
133     bool abs_differences_above_60 = !stats.abs_diff_histogram.isEmpty() &&
134             (stats.abs_diff_histogram.end() - 1).key() >= 0x60;
135 
136     if (one_control && one_channel &&
137         two_values_7bit_max_and_min &&
138         only_note_on_and_note_off) {
139         // A standard button that sends NOTE_ON commands with 0x7F for
140         // down-press and NOTE_OFF commands with 0x00 for release.
141         MidiOptions options;
142 
143         MidiKey note_on;
144         note_on.status = MIDI_NOTE_ON | *stats.channels.begin();
145         note_on.control = *stats.controls.begin();
146         mappings.append(MidiInputMapping(note_on, options, control));
147 
148         MidiKey note_off;
149         note_off.status = MIDI_NOTE_OFF | *stats.channels.begin();
150         note_off.control = note_on.control;
151         mappings.append(MidiInputMapping(note_off, options, control));
152     } else if (one_control && one_channel &&
153                two_values_7bit_max_and_min &&
154                only_note_on) {
155         // A standard button that only sends NOTE_ON commands with 0x7F for
156         // down-press and 0x00 for release.
157         MidiOptions options;
158 
159         MidiKey note_on;
160         note_on.status = MIDI_NOTE_ON | *stats.channels.begin();
161         note_on.control = *stats.controls.begin();
162         mappings.append(MidiInputMapping(note_on, options, control));
163     } else if (one_control && one_channel &&
164                one_value_7bit_max_or_min &&
165                (only_note_on || only_cc)) {
166         // This looks like a toggle switch. If we only got one value and it's
167         // either min or max then this behaves like hard-coded toggle buttons on
168         // the VCI-400. The opcode can be MIDI_NOTE_ON or MIDI_CC.
169         // Examples:
170         // - VCI-400 vinyl toggle button (NOTE_ON)
171         // - Korg nanoKontrol switches (CC)
172         MidiOptions options;
173         options.sw = true;
174 
175         MidiKey note_on;
176         // The predicate ensures only NOTE_ON or CC messages can trigger this
177         // logic.
178         MidiOpCode code = only_note_on ? MIDI_NOTE_ON : MIDI_CC;
179         note_on.status = code | *stats.channels.begin();
180         note_on.control = *stats.controls.begin();
181         mappings.append(MidiInputMapping(note_on, options, control));
182     } else if (one_control && one_channel &&
183                only_cc && only_7bit_values &&
184                no_0x00_value && (abs_differences_above_60 ||
185                                  (under_8_distinct_values &&
186                                   multiple_one_or_7f_values))) {
187         // A two's complement +/- ticker (e.g. selector knobs and some jog
188         // wheels). Values are typically +1 (0x01) and -1 (0x7F) but rapid
189         // changes on some controllers can produce multiple ticks per
190         // message. This must come before the standard knob CC block because it
191         // looks like a standard CC knob other than the large swings in value
192         // and repeats of 0x01 and 0x7F values.
193 
194         // We have a dedicated MidiOption for processing two's complement (even
195         // though it is called 'selectknob' it is actually only two's complement
196         // processing).
197         MidiOptions options;
198         options.selectknob = true;
199         MidiKey knob;
200         knob.status = MIDI_CC | *stats.channels.begin();
201         knob.control = *stats.controls.begin();
202         mappings.append(MidiInputMapping(knob, options, control));
203     } else if (one_control && one_channel && multiple_values_around_0x40) {
204         // A "spread 64" ticker, where 0x40 is zero, positive jog values are
205         // 0x41 and above, and negative jog values are 0x3F and below.
206         MidiOptions options;
207         options.spread64 = true;
208 
209         MidiKey knob;
210         knob.status = MIDI_CC | *stats.channels.begin();
211         knob.control = *stats.controls.begin();
212         mappings.append(MidiInputMapping(knob, options, control));
213     } else if (one_channel && has_cc && num_cc_controls == 1 && only_7bit_values) {
214         // A simple 7-bit knob that may have other messages mixed in. Some
215         // controllers (e.g. the VCI-100) emit a center-point NOTE_ON (with
216         // value 0x7F or 0x00 depending on if you are arriving at or leaving the
217         // center point) instead of a CC value of 0x40. If the control we are
218         // mapping has a reset control then we map the NOTE_ON messages to the
219         // reset control.
220         ConfigKey resetControl = control;
221         resetControl.item.append("_set_default");
222         bool hasResetControl = ControlObject::getControl(resetControl,
223                                        ControlFlag::NoWarnIfMissing) != nullptr;
224 
225         // Find the CC control (based on the predicate one must exist) and add a
226         // binding for it.
227         for (auto it = stats_by_control.constBegin();
228              it != stats_by_control.constEnd(); ++it) {
229             if (it->opcodes.contains(MIDI_CC)) {
230                 MidiKey knob;
231                 knob.status = MIDI_CC | *stats.channels.begin();
232                 knob.control = it.key();
233                 mappings.append(MidiInputMapping(knob, MidiOptions(), control));
234             }
235 
236             // If we found a NOTE_ON, map it to reset if the control exists.
237             // TODO(rryan): We need to modularize each recognizer here so we can
238             // run the button recognizer on these messages minus the CC
239             // messages.
240             if (hasResetControl && it->opcodes.contains(MIDI_NOTE_ON)) {
241                 MidiKey note_on;
242                 note_on.status = MIDI_NOTE_ON | *stats.channels.begin();
243                 note_on.control = it.key();
244                 mappings.append(MidiInputMapping(note_on, MidiOptions(), resetControl));
245             }
246         }
247     } else if (one_channel && only_cc && stats.controls.size() == 2 &&
248             stats_by_control.begin()->message_count > 10 &&
249             stats_by_control.begin()->message_count ==
250                     (++stats_by_control.begin())->message_count) {
251         // If there are two CC controls with the same number of messages then we
252         // assume this is a 14-bit CC knob. Now we need to determine which
253         // control is the LSB and which is the MSB.
254 
255         // When observing an MSB/LSB sweep, the LSB will be very high frequency
256         // compared to the MSB. Instead of doing an actual FFT on the two
257         // signals, we can hack this by looking at the absolute differences
258         // between messages. We expect to see many high/low wrap-arounds for the
259         // LSB.
260         int control1 = *stats.controls.begin();
261         int control2 = *(++stats.controls.begin());
262 
263         int control1_max_abs_diff =
264                 (stats_by_control[control1].abs_diff_histogram.end() - 1).key();
265         int control2_max_abs_diff =
266                 (stats_by_control[control2].abs_diff_histogram.end() - 1).key();
267 
268         // The control with the larger abs difference in messages is the LSB. If
269         // they are equal we choose one arbitrarily (depends on QSet iteration
270         // order which is undefined).
271         int lsb_control = control1_max_abs_diff > control2_max_abs_diff ? control1 : control2;
272         int msb_control = control1_max_abs_diff > control2_max_abs_diff ? control2 : control1;
273 
274         // NOTE(rryan): There is an industry convention that a 14-bit CC control
275         // is a pair of controls offset by 32 (the lower is the MSB, the higher
276         // is the LSB). My VCI-400 follows this convention, for example. I don't
277         // use that convention here because it's not universal and we should be
278         // able to come up with reasonable heuristics to identify an LSB and an
279         // MSB.
280 
281         MidiKey msb;
282         msb.status = MIDI_CC | *stats.channels.begin();
283         msb.control = msb_control;
284         MidiOptions msb_option;
285         msb_option.fourteen_bit_msb = true;
286         mappings.append(MidiInputMapping(msb, msb_option, control));
287 
288         MidiKey lsb;
289         lsb.status = MIDI_CC | *stats.channels.begin();
290         lsb.control = lsb_control;
291         MidiOptions lsb_option;
292         lsb_option.fourteen_bit_lsb = true;
293         mappings.append(MidiInputMapping(lsb, lsb_option, control));
294     }
295 
296     if (mappings.isEmpty() && !messages.isEmpty()) {
297         // Fall back. Map the first message we got. By dumb luck this might work
298         // for 14-bit faders, for example if the high-order byte is first (it
299         // will just be a 7-bit fader).
300         MidiOptions options;
301 
302         // TODO(rryan): Feedback to the user that we didn't do anything
303         // intelligent here.
304         mappings.append(MidiInputMapping(messages.first().first, options, control));
305     }
306 
307     // Add control and description info to each learned input mapping.
308     for (MidiInputMappings::iterator it = mappings.begin();
309          it != mappings.end(); ++it) {
310         MidiInputMapping& mapping = *it;
311         mapping.description = QString("MIDI Learned from %1 messages.")
312                 .arg(messages.size());
313     }
314 
315     return mappings;
316 }
317