#include "rawinput.h" #include #include "util/logging.h" #include "util/time.h" #include "util/utils.h" namespace rawinput { static MidiNoteAlgorithm MIDI_NOTE_ALGORITHM = MidiNoteAlgorithm::V2; } rawinput::MidiNoteAlgorithm rawinput::get_midi_algorithm() { return rawinput::MIDI_NOTE_ALGORITHM; } void rawinput::set_midi_algorithm(rawinput::MidiNoteAlgorithm new_algo) { rawinput::MIDI_NOTE_ALGORITHM = new_algo; std::string s = "Unknown"; switch (new_algo) { case rawinput::MidiNoteAlgorithm::LEGACY: s = "legacy"; break; case rawinput::MidiNoteAlgorithm::V2: s = "v2"; break; case rawinput::MidiNoteAlgorithm::V2_DRUM: s = "v2_drum"; break; default: log_info("rawinput", "assert failed: invalid midi algorithm"); break; } log_info("rawinput", "using MIDI algorithm: {}", s); } void rawinput::RawInputManager::midi_scan_start() { // single-flight: only one scan runs at a time. if one is already running, set // the pending flag so it rescans once more when it finishes - MIDI hotplug // events fire while the slow enumeration is still going and must not be lost. // the scheduler mutex makes this check-and-set atomic with the worker's // exit-or-rescan decision below, so a request set while a scan is running is // never dropped { std::lock_guard lock(this->midi_scan_m); if (this->midi_scan_active) { this->midi_scan_pending = true; log_misc("rawinput", "MIDI scan already running, queued rescan"); return; } this->midi_scan_active = true; this->midi_scan_pending = false; } // clean up the previous (already finished) scan thread handle this->midi_scan_join(); // run the (potentially slow) MIDI enumeration on its own thread so callers are // not blocked while the Windows MIDI subsystem starts up. rescan if a request // arrived while we were scanning log_misc("rawinput", "starting async MIDI scan thread"); this->midi_thread = new std::thread([this]() { for (;;) { this->devices_scan_midi(); // decide whether to exit under the scheduler lock, atomically with any // concurrent midi_scan_start(): if a rescan was requested, consume it // and loop; otherwise clear active and exit. because both sides take // the same lock, a request set while active is true is never lost, so // we never strand a hotplug event waiting for a future one std::lock_guard lock(this->midi_scan_m); if (!this->midi_scan_pending) { this->midi_scan_active = false; log_misc("rawinput", "async MIDI scan thread finished"); return; } this->midi_scan_pending = false; log_misc("rawinput", "async MIDI scan rescanning (event arrived during scan)"); } }); } void rawinput::RawInputManager::midi_scan_join() { if (this->midi_thread) { if (this->midi_thread->joinable()) { // this blocks until the scan worker returns. if it ever hangs here the // worker is stuck - most likely in midi_close_deferred_flush() waiting // on a WinMM close. a missing "joined" line pinpoints the hang log_misc("rawinput", "joining MIDI scan thread..."); this->midi_thread->join(); log_misc("rawinput", "MIDI scan thread joined"); } delete this->midi_thread; this->midi_thread = nullptr; } } void rawinput::RawInputManager::midi_close_deferred_flush() { // take the queued handles under the lock, then close them without it. WinMM // midiInReset/midiInClose block until in-flight input_midi_proc callbacks // return, and those callbacks take devices_mutex, so closing under the lock // would deadlock std::vector handles; { std::lock_guard lock(this->devices_mutex); handles.swap(this->midi_close_deferred); } if (handles.empty()) { return; } // if a hang is ever reported here it is the classic WinMM deadlock: an // in-flight input_midi_proc callback is blocked on devices_mutex while // midiInReset/midiInClose waits for that callback to return. the per-handle // log below pinpoints exactly which close did not come back log_misc("rawinput", "closing {} deferred MIDI handle(s)", handles.size()); for (size_t i = 0; i < handles.size(); i++) { log_misc("rawinput", "closing deferred MIDI handle {}/{}", i + 1, handles.size()); midiInReset(handles[i]); midiInClose(handles[i]); } log_misc("rawinput", "deferred MIDI handles closed"); } void rawinput::RawInputManager::devices_scan_midi() { log_misc("rawinput", "scan MIDI devices..."); // note: the WinMM MIDI calls below (midiInGetNumDevs / midiInGetDevCaps / // midiInOpen / midiInStart) can block for seconds while the Windows MIDI // subsystem starts up, so they must NOT run under devices_mutex. only the // list mutation at the end of each iteration is guarded. // identifiers of every MIDI device seen in this scan; used below to // tombstone devices that have since been unplugged std::vector present_identifiers; // add midi devices auto midi_device_count = midiInGetNumDevs(); for (size_t midi_device_id = 0; midi_device_id < midi_device_count; midi_device_id++) { // get dev caps MIDIINCAPS midi_device_caps{}; if (midiInGetDevCaps(midi_device_id, &midi_device_caps, sizeof(MIDIINCAPS)) != MMSYSERR_NOERROR) { continue; } log_misc("rawinput", "found MIDI device: id {}, name {}, mid {}, pid {}", midi_device_id, midi_device_caps.szPname, midi_device_caps.wMid, midi_device_caps.wPid); // build identifier for MIDI // ;MIDI; format is now set in stone (in other parts of the code base and in the config xml file) // so it should never be changed std::ostringstream midi_identifier_stream; midi_identifier_stream << ";" << "MIDI"; midi_identifier_stream << ";" << midi_device_id; midi_identifier_stream << ";" << midi_device_caps.szPname; midi_identifier_stream << ";" << midi_device_caps.wMid; midi_identifier_stream << ";" << midi_device_caps.wPid; const auto midi_identifier = midi_identifier_stream.str(); // record that this device is currently present present_identifiers.push_back(midi_identifier); // if already open, leave it alone: hotplug fires many change events, and // reopening on every rescan would drop the WinMM handle (and its input). // only (re)open when the device is missing or a destroyed tombstone { std::lock_guard lock(this->devices_mutex); bool already_open = false; for (auto &device : this->devices) { if (device.type == MIDI && device.name == midi_identifier) { already_open = true; break; } } if (already_open) { continue; } } // open device HMIDIIN midi_device_handle; if (midiInOpen(&midi_device_handle, (UINT) midi_device_id, (DWORD_PTR) &input_midi_proc, (DWORD_PTR) this, CALLBACK_FUNCTION) != MMSYSERR_NOERROR) { continue; } // start input if (midiInStart(midi_device_handle) != MMSYSERR_NOERROR) { // close the handle we just opened so it does not leak on repeated rescans midiInClose(midi_device_handle); continue; } // device info DeviceInfo midi_device_info {}; // device midi info auto midi_device_midi_info = new DeviceMIDIInfo(); midi_device_midi_info->states = std::vector(16 * 128); midi_device_midi_info->states_events = std::vector(16 * 128); midi_device_midi_info->bind_states = std::vector(16 * 128); midi_device_midi_info->v2_last_on_time = std::vector(16 * 128); midi_device_midi_info->v2_last_off_time = std::vector(16 * 128); midi_device_midi_info->v2_velocity_threshold = std::vector(16 * 128); midi_device_midi_info->v2_velocity_threshold_set_on_device = std::vector(16 * 128); midi_device_midi_info->velocity = std::vector(16 * 128); midi_device_midi_info->freeze = false; midi_device_midi_info->controls_precision = std::vector(16 * 32); midi_device_midi_info->controls_precision_bind = std::vector(16 * 32); midi_device_midi_info->controls_precision_msb = std::vector(16 * 32); midi_device_midi_info->controls_precision_lsb = std::vector(16 * 32); midi_device_midi_info->controls_precision_set = std::vector(16 * 32); midi_device_midi_info->controls_single = std::vector(16 * 44); midi_device_midi_info->controls_single_bind = std::vector(16 * 44); midi_device_midi_info->controls_single_set = std::vector(16 * 44); midi_device_midi_info->controls_onoff = std::vector(16 * 6); midi_device_midi_info->controls_onoff_bind = std::vector(16 * 6); midi_device_midi_info->controls_onoff_set = std::vector(16 * 6); midi_device_midi_info->v2_controls_onoff_last_on_time = std::vector(16 * 6); midi_device_midi_info->v2_controls_onoff_last_off_time = std::vector(16 * 6); midi_device_midi_info->pitch_bend = std::vector(16 * 6); midi_device_midi_info->pitch_bend_set = std::vector(16 * 6); // build device Device midi_device {}; midi_device.type = MIDI; midi_device.handle = midi_device_handle; midi_device.name = midi_identifier; midi_device.desc = to_string(midi_device_caps.szPname); midi_device.info = midi_device_info; midi_device.midiInfo = midi_device_midi_info; // mutate the shared device list under lock (the slow WinMM calls above // ran without it so other threads were not blocked) std::lock_guard lock(this->devices_mutex); midi_device.id = devices.size() + 1; // reuse a previously destroyed tombstone with the same identifier, if any. // (a live device with this identifier was already skipped above) bool replaced = false; for (auto &device : this->devices) { if (device.name == midi_identifier) { // carry over ID midi_device.id = device.id; // destruct and replace the slot in place under its locks this->devices_destruct(&device); replace_device_slot(device, midi_device); // notify change for (auto &cb : this->callback_change) { cb.f(cb.data, &device); } replaced = true; break; } } if (replaced) { continue; } // add device to list midi_device.mutex = new std::mutex(); midi_device.mutex_out = new std::mutex(); auto &device = this->devices.emplace_back(midi_device); // notify add for (auto &cb : this->callback_add) { cb.f(cb.data, &device); } } // tombstone MIDI devices that were open but are no longer present (unplugged). // otherwise a replugged device matches the stale live entry in the skip check // above and never gets reopened, silently losing its input { std::lock_guard lock(this->devices_mutex); for (auto &device : this->devices) { if (device.type != MIDI) { continue; } bool present = false; for (const auto &identifier : present_identifiers) { if (identifier == device.name) { present = true; break; } } if (!present) { log_info("rawinput", "MIDI device unplugged, releasing: {}", device.desc); this->devices_destruct(&device); } } } // close the MIDI handles detached above, now that devices_mutex is released this->midi_close_deferred_flush(); log_misc("rawinput", "scan MIDI devices done ({} enumerated)", (unsigned) midi_device_count); } void CALLBACK rawinput::RawInputManager::input_midi_proc(HMIDIIN hMidiIn, UINT wMsg, DWORD_PTR dwInstance, DWORD_PTR dwParam1, DWORD_PTR dwParam2) { // get instance auto ri_mgr = reinterpret_cast(dwInstance); // handle message switch (wMsg) { case MIM_OPEN: case MIM_CLOSE: break; case MIM_MOREDATA: case MIM_DATA: { // lock the device list so a concurrent scan can't mutate it while we iterate std::lock_guard devices_lock(ri_mgr->devices_mutex); // param mapping auto dwMidiMessage = dwParam1; //auto dwTimestamp = dwParam2; // message unpacking auto midi_status = LOBYTE(LOWORD(dwMidiMessage)); auto midi_status_command = (midi_status & 0xF0u) >> 4u; auto midi_status_channel = (midi_status & 0x0Fu); auto midi_byte1 = HIBYTE(LOWORD(dwMidiMessage)); auto midi_byte2 = LOBYTE(HIWORD(dwMidiMessage)); // callbacks for (auto &callback : ri_mgr->callback_midi) { // find device for (auto &device : ri_mgr->devices_get()) { if (device.type == MIDI && device.handle == hMidiIn) { // call function callback.f(callback.data, &device, midi_status_command, midi_status_channel, midi_byte1, midi_byte2); } } } // skip unused messages types early for performance bool skip = false; switch (midi_status_command) { case 0xA: // POLYPHONIC PRESSURE case 0xC: // PROGRAM CHANGE case 0xD: // CHANNEL PRESSURE case 0xF: // SYSTEM EXCLUSIVE skip = true; break; default: break; } if (skip) { break; } // find device for (auto &device : ri_mgr->devices_get()) { // filter non MIDI devices if (device.type != MIDI) { continue; } // filter wrong handles if (device.handle != hMidiIn) { continue; } // get input time const auto input_time = get_performance_seconds(); // lock device std::lock_guard lock(*device.mutex); // update hz auto diff_time = input_time - device.input_time; if (diff_time > 0.0001) { device.input_hz = 1.f / diff_time; device.input_hz_max = MAX(device.input_hz_max, device.input_hz); device.input_time = input_time; } // command logic switch (midi_status_command) { case 0x8: { // NOTE OFF // param mapping const auto midi_note = midi_byte1 & 127u; // log_misc("midi", "[{}] OFF", midi_note); // get index const auto midi_index = midi_status_channel * 128 + midi_note; if (midi_index < 16 * 128) { if (MIDI_NOTE_ALGORITHM == MidiNoteAlgorithm::LEGACY) { // update velocity device.midiInfo->velocity[midi_index] = 0; // disable note if (device.midiInfo->states_events[midi_index]) { device.midiInfo->states[midi_index] = false; } device.updated = true; } else { // v2 logic // exactly the same as NOTE ON with 0 velocity // velocity is kept; api will ignore it if button is not pressed if (MIDI_NOTE_ALGORITHM == MidiNoteAlgorithm::V2) { device.midiInfo->v2_last_off_time[midi_index] = get_performance_milliseconds(); device.updated = true; } // for v2_drum, NOTE OFF is ignored } } break; } case 0x9: { // NOTE ON // param mapping const auto midi_note = midi_byte1 & 127u; // per MIDI spec, if NOTE ON is sent with 0 velocity, it's the same thing as NOTE OFF. const auto midi_velocity = midi_byte2 & 127u; // log_misc("midi", "[{}] ON v={}", midi_note, midi_velocity); // get index const auto midi_index = midi_status_channel * 128 + midi_note; if (midi_index < 16 * 128) { if (MIDI_NOTE_ALGORITHM == MidiNoteAlgorithm::LEGACY) { // update velocity device.midiInfo->velocity[midi_index] = (uint8_t) midi_velocity; if (midi_velocity) { // update events (for legacy logic) // how does this work? see the comment in api.cpp around the check for // get_midi_algorithm() for an explanation // so currently it's meant to be turned on device.midiInfo->states[midi_index] = true; // if its already on just increase it by one to turn it off if (device.midiInfo->states_events[midi_index] % 2) device.midiInfo->states_events[midi_index]++; else device.midiInfo->states_events[midi_index] += 2; } else if (!device.midiInfo->freeze) { // velocity 0 means turn it off device.midiInfo->states[midi_index] = false; } device.updated = true; } else { // v2 logic const auto now = get_performance_milliseconds(); auto threshold = device.midiInfo->v2_velocity_threshold[midi_index]; // when device is frozen (binding is happening) ignore the velocity threshold // this allows users to bind keys even if the midi note is set to high threshold at // rawinput layer, either from a previous binding that was cleared, or existing binding // for another button if (device.midiInfo->freeze) { threshold = 0; } if (threshold < midi_velocity) { device.midiInfo->velocity[midi_index] = (uint8_t)midi_velocity; device.midiInfo->v2_last_on_time[midi_index] = now; // disable holds and release all notes immediately if (MIDI_NOTE_ALGORITHM == MidiNoteAlgorithm::V2_DRUM) { device.midiInfo->v2_last_off_time[midi_index] = now; } device.updated = true; } else { if (MIDI_NOTE_ALGORITHM == MidiNoteAlgorithm::V2) { // insufficient velocity ON == exactly the same as NOTE OFF device.midiInfo->v2_last_off_time[midi_index] = now; device.updated = true; } // for v2_drum, NOTE ON with insufficient velocity is ignored } } } break; } case 0xA: // POLYPHONIC PRESSURE break; // skipped above (!) case 0xB: { // CONTROL CHANGE // param mapping auto midi_control = midi_byte1 & 127; auto midi_value = midi_byte2 & 127u; // get index auto channel_offset = midi_status_channel * 128; auto midi_index = channel_offset + midi_control; if (midi_index < 16 * 128) { // continuous controller MSB if (midi_control >= 0x00 && midi_control <= 0x1F) { // update index midi_index = midi_status_channel * 32 + midi_control; device.midiInfo->controls_precision_set[midi_index] = true; // check if MSB wasn't sent yet if (!device.midiInfo->controls_precision_msb[midi_index]) { device.midiInfo->controls_precision_msb[midi_index] = true; // move LSB value to actual position device.midiInfo->controls_precision[midi_index] >>= 7u; } // update MSB auto tmp = device.midiInfo->controls_precision[midi_index]; tmp = (tmp & 127u) | midi_value << 7u; if (!device.midiInfo->controls_precision_lsb[midi_index]) tmp = (tmp & (127u << 7u)) | midi_value; if (device.midiInfo->controls_precision[midi_index] != tmp) { device.midiInfo->controls_precision[midi_index] = tmp; device.updated = true; } } // continuous controller LSB else if (midi_control >= 0x20 && midi_control <= 0x3F) { // update index midi_index = midi_status_channel * 32 + midi_control - 0x20; device.midiInfo->controls_precision_set[midi_index] = true; device.midiInfo->controls_precision_lsb[midi_index] = true; // check for MSB flag if (device.midiInfo->controls_precision_msb[midi_index]) { // update LSB only auto tmp = device.midiInfo->controls_precision[midi_index]; tmp &= 127u << 7u; tmp |= midi_value; if (device.midiInfo->controls_precision[midi_index] != tmp) { device.midiInfo->controls_precision[midi_index] = tmp; device.updated = true; } } else { // cast to MSB if (device.midiInfo->controls_precision[midi_index] != midi_value << 7u) { device.midiInfo->controls_precision[midi_index] = midi_value << 7u | midi_value; device.updated = true; } } } // on/off controls else if (midi_control >= 0x40 && midi_control <= 0x45) { // update index midi_index = midi_status_channel * 6 + midi_control - 0x40; device.midiInfo->controls_onoff_set[midi_index] = true; // get on/off state const auto onoff_state = midi_value >= 64; // update device if (MIDI_NOTE_ALGORITHM == MidiNoteAlgorithm::LEGACY) { if (device.midiInfo->controls_onoff[midi_index] != onoff_state) { device.midiInfo->controls_onoff[midi_index] = onoff_state; device.updated = true; } } else { // v2 and v2_drum: // unlike notes (drum pads), controls can send continuous ON signal // therefore, check for rising and falling edges const auto now = get_performance_milliseconds(); const auto previous_value = device.midiInfo->controls_onoff[midi_index]; if (!previous_value && onoff_state) { device.midiInfo->v2_controls_onoff_last_on_time[midi_index] = now; device.updated = true; } else if (previous_value && !onoff_state) { device.midiInfo->v2_controls_onoff_last_off_time[midi_index] = now; device.updated = true; } device.midiInfo->controls_onoff[midi_index] = onoff_state; } } // single byte controllers else if (midi_control >= 0x46 && midi_control <= 0x5F) { // update index midi_index = midi_status_channel * 44 + midi_control - 0x46; device.midiInfo->controls_single_set[midi_index] = true; // update device if (device.midiInfo->controls_single[midi_index] != midi_value) { device.midiInfo->controls_single[midi_index] = midi_value; device.updated = true; } } // increment/decrement and parameter numbers else if (midi_control >= 0x60 && midi_control <= 0x65) { // skip } // undefined single-byte controllers else if (midi_control >= 0x66 && midi_control <= 0x77) { // update index auto sbc_count = 0x5F - 0x46 + 1; midi_index = midi_status_channel * 44 + midi_control - 0x66 + sbc_count; device.midiInfo->controls_single_set[midi_index] = true; // update device if (device.midiInfo->controls_single[midi_index] != midi_value) { device.midiInfo->controls_single[midi_index] = midi_value; device.updated = true; } } // channel mode messages else if (midi_control >= 0x78 && midi_control <= 0x7F) { switch (midi_control) { case 0x78: // all sound off break; case 0x79: { // reset all controllers for (int i = 0; i < 32; i++) device.midiInfo->controls_precision[midi_status_channel * 32 + i] = 0; for (int i = 0; i < 44; i++) device.midiInfo->controls_single[midi_status_channel * 44 + i] = 0; for (int i = 0; i < 6; i++) { const auto index = midi_status_channel * 6 + i; device.midiInfo->controls_onoff[index] = false; device.midiInfo->v2_controls_onoff_last_on_time[index] = 0; device.midiInfo->v2_controls_onoff_last_off_time[index] = 0; } device.updated = true; break; } case 0x7A: // local control on/off break; case 0x7B: // all notes off case 0x7C: // omni mode off + all notes off case 0x7D: // omni mode on + all notes off case 0x7E: // mono mode on + poly off + all notes off case 0x7F: // poly mode on + mono off + all notes off for (int i = 0; i < 128; i++) { // common device.midiInfo->velocity[channel_offset + i] = 0; device.midiInfo->bind_states[channel_offset + i] = false; // legacy device.midiInfo->states[channel_offset + i] = false; device.midiInfo->states_events[channel_offset + i] = 0; // v2 device.midiInfo->v2_last_off_time[channel_offset + i] = 0.0; device.midiInfo->v2_last_on_time[channel_offset + i] = 0.0; } device.updated = true; break; default: break; } break; } } break; } case 0xC: // PROGRAM CHANGE break; // skipped above (!) case 0xD: // CHANNEL PRESSURE break; // skipped above (!) case 0xE: { // PITCH BENDING // raw values range from [0, 0x3FFF] (16383) // build value, centered around zero [-8192, 8191] int16_t value = ((midi_byte1) | (midi_byte2 << 7u)) - 0x2000; // update device if (device.midiInfo->pitch_bend[midi_status_channel] != value) { device.midiInfo->pitch_bend[midi_status_channel] = value; device.midiInfo->pitch_bend_set[midi_status_channel] = true; device.updated = true; } break; } case 0xF: // SYSTEM EXCLUSIVE break; // skipped above (!) default: break; } // don't iterate through the other devices break; } break; } case MIM_LONGDATA: case MIM_ERROR: case MIM_LONGERROR: break; default: break; } }