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