#include #include #include #include "controllers/learningutils.h" #include "controllers/midi/midiutils.h" #include "control/controlobject.h" typedef QPair MidiKeyAndValue; struct MessageStats { MessageStats() : message_count(0), last_value(0) { } void addMessage(const MidiKeyAndValue& message) { // If we've never seen a message before, set last_value to this value. if (message_count == 0) { last_value = message.second; } MidiOpCode opcode = MidiUtils::opCodeFromStatus(message.first.status); unsigned char channel = MidiUtils::channelFromStatus(message.first.status); opcodes.insert(opcode); channels.insert(channel); controls.insert(message.first.control); message_count++; value_histogram[message.second]++; // Convert to integers first to get negative values. int absolute_difference = abs(static_cast(message.second) - static_cast(last_value)); abs_diff_histogram[absolute_difference]++; last_value = message.second; } int message_count; QSet opcodes; QSet channels; QSet controls; QMap value_histogram; // The range of differences for unsigned char is -255 through 255. QMap abs_diff_histogram; unsigned char last_value; }; // static MidiInputMappings LearningUtils::guessMidiInputMappings( const ConfigKey& control, const QList>& messages) { QMap stats_by_control; MessageStats stats; // Analyze the message foreach (const MidiKeyAndValue& message, messages) { stats.addMessage(message); stats_by_control[message.first.control].addMessage(message); } qDebug() << "LearningUtils guessing MIDI mapping from" << messages.size() << "messages."; foreach (MidiOpCode opcode, stats.opcodes) { qDebug() << "Opcode:" << opcode; } foreach (unsigned char channel, stats.channels) { qDebug() << "Channel:" << channel; } foreach (unsigned char control, stats.controls) { qDebug() << "Control:" << control; } for (auto it = stats.value_histogram.constBegin(); it != stats.value_histogram.constEnd(); ++it) { qDebug() << "Overall Value:" << it.key() << "count" << it.value(); } for (auto control_it = stats_by_control.constBegin(); control_it != stats_by_control.constEnd(); ++control_it) { QString controlName = QString("Control %1").arg(control_it.key()); for (auto it = control_it->value_histogram.constBegin(); it != control_it->value_histogram.constEnd(); ++it) { qDebug() << controlName << "Value:" << it.key() << "count" << it.value(); } } MidiInputMappings mappings; bool one_control = stats.controls.size() == 1; bool one_channel = stats.channels.size() == 1; bool only_note_on = stats.opcodes.size() == 1 && stats.opcodes.contains(MidiOpCode::NoteOn); bool only_note_on_and_note_off = stats.opcodes.size() == 2 && stats.opcodes.contains(MidiOpCode::NoteOn) && stats.opcodes.contains(MidiOpCode::NoteOff); bool has_cc = stats.opcodes.contains(MidiOpCode::ControlChange); bool only_cc = stats.opcodes.size() == 1 && has_cc; int num_cc_controls = 0; for (auto it = stats_by_control.constBegin(); it != stats_by_control.constEnd(); ++it) { if (it->opcodes.contains(MidiOpCode::ControlChange)) { num_cc_controls++; } } bool two_values_7bit_max_and_min = stats.value_histogram.size() == 2 && stats.value_histogram.contains(0x00) && stats.value_histogram.contains(0x7F); bool one_value_7bit_max_or_min = stats.value_histogram.size() == 1 && (stats.value_histogram.contains(0x00) || stats.value_histogram.contains(0x7F)); bool multiple_one_or_7f_values = stats.value_histogram.value(0x01, 0) > 1 || stats.value_histogram.value(0x7F, 0) > 1; bool under_8_distinct_values = stats.value_histogram.size() < 8; bool no_0x00_value = !stats.value_histogram.contains(0x00); bool no_0x40_value = !stats.value_histogram.contains(0x40); bool multiple_values_around_0x40 = stats.value_histogram.value(0x41, 0) > 1 && stats.value_histogram.value(0x3F, 0) > 1 && no_0x40_value && under_8_distinct_values; // QMap keys are sorted so we can check this easily by checking the last key // is <= 0x7F. bool only_7bit_values = !stats.value_histogram.isEmpty() && stats.value_histogram.lastKey() <= 0x7F; // A 7-bit two's complement ticker swinging from +1 to -1 can generate // unsigned differences of up to 126 (0x7E). If we see differences in // individual messages above 96 (0x60) that's a good hint that we're looking // at a two's complement ticker. bool abs_differences_above_60 = !stats.abs_diff_histogram.isEmpty() && stats.abs_diff_histogram.lastKey() >= 0x60; if (one_control && one_channel && two_values_7bit_max_and_min && only_note_on_and_note_off) { // A standard button that sends NOTE_ON commands with 0x7F for // down-press and NOTE_OFF commands with 0x00 for release. MidiOptions options; MidiKey note_on; note_on.status = MidiUtils::statusFromOpCodeAndChannel( MidiOpCode::NoteOn, *stats.channels.begin()); note_on.control = *stats.controls.begin(); mappings.append(MidiInputMapping(note_on, options, control)); MidiKey note_off; note_off.status = MidiUtils::statusFromOpCodeAndChannel( MidiOpCode::NoteOff, *stats.channels.begin()); note_off.control = note_on.control; mappings.append(MidiInputMapping(note_off, options, control)); } else if (one_control && one_channel && two_values_7bit_max_and_min && only_note_on) { // A standard button that only sends NOTE_ON commands with 0x7F for // down-press and 0x00 for release. MidiOptions options; MidiKey note_on; note_on.status = MidiUtils::statusFromOpCodeAndChannel( MidiOpCode::NoteOn, *stats.channels.begin()); note_on.control = *stats.controls.begin(); mappings.append(MidiInputMapping(note_on, options, control)); } else if (one_control && one_channel && one_value_7bit_max_or_min && (only_note_on || only_cc)) { // This looks like a toggle switch. If we only got one value and it's // either min or max then this behaves like hard-coded toggle buttons on // the VCI-400. The opcode can be MidiOpCode::NoteOn or MidiOpCode::ControlChange. // Examples: // - VCI-400 vinyl toggle button (NOTE_ON) // - Korg nanoKontrol switches (CC) MidiOptions options; options.setFlag(MidiOption::Switch); MidiKey note_on; // The predicate ensures only NOTE_ON or CC messages can trigger this // logic. MidiOpCode code = only_note_on ? MidiOpCode::NoteOn : MidiOpCode::ControlChange; note_on.status = MidiUtils::statusFromOpCodeAndChannel(code, *stats.channels.begin()); note_on.control = *stats.controls.begin(); mappings.append(MidiInputMapping(note_on, options, control)); } else if (one_control && one_channel && only_cc && only_7bit_values && no_0x00_value && (abs_differences_above_60 || (under_8_distinct_values && multiple_one_or_7f_values))) { // A two's complement +/- ticker (e.g. selector knobs and some jog // wheels). Values are typically +1 (0x01) and -1 (0x7F) but rapid // changes on some controllers can produce multiple ticks per // message. This must come before the standard knob CC block because it // looks like a standard CC knob other than the large swings in value // and repeats of 0x01 and 0x7F values. // We have a dedicated MidiOption for processing two's complement (even // though it is called 'selectknob' it is actually only two's complement // processing). MidiOptions options; options.setFlag(MidiOption::SelectKnob); MidiKey knob; knob.status = MidiUtils::statusFromOpCodeAndChannel( MidiOpCode::ControlChange, *stats.channels.begin()); knob.control = *stats.controls.begin(); mappings.append(MidiInputMapping(knob, options, control)); } else if (one_control && one_channel && multiple_values_around_0x40) { // A "spread 64" ticker, where 0x40 is zero, positive jog values are // 0x41 and above, and negative jog values are 0x3F and below. MidiOptions options; options.setFlag(MidiOption::Spread64); MidiKey knob; knob.status = MidiUtils::statusFromOpCodeAndChannel( MidiOpCode::ControlChange, *stats.channels.begin()); knob.control = *stats.controls.begin(); mappings.append(MidiInputMapping(knob, options, control)); } else if (one_channel && has_cc && num_cc_controls == 1 && only_7bit_values) { // A simple 7-bit knob that may have other messages mixed in. Some // controllers (e.g. the VCI-100) emit a center-point NOTE_ON (with // value 0x7F or 0x00 depending on if you are arriving at or leaving the // center point) instead of a CC value of 0x40. If the control we are // mapping has a reset control then we map the NOTE_ON messages to the // reset control. ConfigKey resetControl = control; resetControl.item.append("_set_default"); bool hasResetControl = ControlObject::getControl(resetControl, ControlFlag::NoWarnIfMissing) != nullptr; // Find the CC control (based on the predicate one must exist) and add a // binding for it. for (auto it = stats_by_control.constBegin(); it != stats_by_control.constEnd(); ++it) { if (it->opcodes.contains(MidiOpCode::ControlChange)) { MidiKey knob; knob.status = MidiUtils::statusFromOpCodeAndChannel( MidiOpCode::ControlChange, *stats.channels.begin()); knob.control = it.key(); mappings.append(MidiInputMapping(knob, MidiOptions(), control)); } // If we found a NOTE_ON, map it to reset if the control exists. // TODO(rryan): We need to modularize each recognizer here so we can // run the button recognizer on these messages minus the CC // messages. if (hasResetControl && it->opcodes.contains(MidiOpCode::NoteOn)) { MidiKey note_on; note_on.status = MidiUtils::statusFromOpCodeAndChannel( MidiOpCode::NoteOn, *stats.channels.begin()); note_on.control = it.key(); mappings.append(MidiInputMapping(note_on, MidiOptions(), resetControl)); } } } else if (one_channel && only_cc && stats.controls.size() == 2 && stats_by_control.begin()->message_count > 10 && stats_by_control.begin()->message_count == (++stats_by_control.begin())->message_count) { // If there are two CC controls with the same number of messages then we // assume this is a 14-bit CC knob. Now we need to determine which // control is the LSB and which is the MSB. // When observing an MSB/LSB sweep, the LSB will be very high frequency // compared to the MSB. Instead of doing an actual FFT on the two // signals, we can hack this by looking at the absolute differences // between messages. We expect to see many high/low wrap-arounds for the // LSB. int control1 = *stats.controls.begin(); int control2 = *(++stats.controls.begin()); int control1_max_abs_diff = stats_by_control[control1].abs_diff_histogram.lastKey(); int control2_max_abs_diff = stats_by_control[control2].abs_diff_histogram.lastKey(); // The control with the larger abs difference in messages is the LSB. If // they are equal we choose one arbitrarily (depends on QSet iteration // order which is undefined). int lsb_control = control1_max_abs_diff > control2_max_abs_diff ? control1 : control2; int msb_control = control1_max_abs_diff > control2_max_abs_diff ? control2 : control1; // NOTE(rryan): There is an industry convention that a 14-bit CC control // is a pair of controls offset by 32 (the lower is the MSB, the higher // is the LSB). My VCI-400 follows this convention, for example. I don't // use that convention here because it's not universal and we should be // able to come up with reasonable heuristics to identify an LSB and an // MSB. MidiKey msb; msb.status = MidiUtils::statusFromOpCodeAndChannel( MidiOpCode::ControlChange, *stats.channels.begin()); msb.control = msb_control; MidiOptions msb_option; msb_option.setFlag(MidiOption::FourteenBitMSB); mappings.append(MidiInputMapping(msb, msb_option, control)); MidiKey lsb; lsb.status = MidiUtils::statusFromOpCodeAndChannel( MidiOpCode::ControlChange, *stats.channels.begin()); lsb.control = lsb_control; MidiOptions lsb_option; lsb_option.setFlag(MidiOption::FourteenBitLSB); mappings.append(MidiInputMapping(lsb, lsb_option, control)); } if (mappings.isEmpty() && !messages.isEmpty()) { // Fall back. Map the first message we got. By dumb luck this might work // for 14-bit faders, for example if the high-order byte is first (it // will just be a 7-bit fader). MidiOptions options; // TODO(rryan): Feedback to the user that we didn't do anything // intelligent here. mappings.append(MidiInputMapping(messages.first().first, options, control)); } // Add control and description info to each learned input mapping. for (MidiInputMappings::iterator it = mappings.begin(); it != mappings.end(); ++it) { MidiInputMapping& mapping = *it; mapping.description = QString("MIDI Learned from %1 messages.") .arg(messages.size()); } return mappings; }