mirror of
https://github.com/spice2x/spice2x.github.io.git
synced 2026-10-02 11:48:00 +03:00
## Link to GitHub Issue or related Pull Request, if one exists Fixes #603 ## Description of change Scanning for midi devices can take a while on Windows 11 with MIDI 2.0 service. Sometimes it'll take a couple seconds. Sometimes, on a PC with zero MIDI devices, it takes 10-11 seconds. This was causing two issues: 1. slow startup time 2. crash on invalid memory access due to a race condition Address both. Also fix a bug: when a device change event fires, we do a MIDI scan, and invalidated all existing MIDI devices in favor of creating new handles. Stop doing this, and instead check for duplicates by matching the ID and keep existing device handles alone. Properly clean up devices on unplug. Also: fix MIDI buttons being stuck on when unplugged while holding a key. ## Testing Tested with Nostroller in MIDI mode. rtpMIDI works too.
1269 lines
46 KiB
C++
1269 lines
46 KiB
C++
#include "api.h"
|
|
|
|
#include <cassert>
|
|
#include <optional>
|
|
|
|
#include "launcher/superexit.h"
|
|
#include "rawinput/rawinput.h"
|
|
#include "rawinput/piuio.h"
|
|
#include "util/time.h"
|
|
#include "util/utils.h"
|
|
|
|
#include "config.h"
|
|
|
|
using namespace GameAPI;
|
|
|
|
std::vector<Button> GameAPI::Buttons::getButtons(const std::string &game_name) {
|
|
return Config::getInstance().getButtons(game_name);
|
|
}
|
|
|
|
std::vector<Button> GameAPI::Buttons::getButtons(Game *game) {
|
|
return Config::getInstance().getButtons(game);
|
|
}
|
|
|
|
static Buttons::State getMidiV2ButtonState(float last_on_time, float last_off_time);
|
|
|
|
std::vector<Button> GameAPI::Buttons::sortButtons(
|
|
const std::vector<Button> &buttons,
|
|
const std::vector<std::string> &button_names,
|
|
const std::vector<unsigned short> *vkey_defaults)
|
|
{
|
|
std::vector<Button> sorted;
|
|
|
|
bool button_found;
|
|
int index = 0;
|
|
for (auto &name : button_names) {
|
|
button_found = false;
|
|
|
|
for (auto &bt : buttons) {
|
|
if (name == bt.getName()) {
|
|
button_found = true;
|
|
Button button_new = bt;
|
|
if (vkey_defaults) {
|
|
button_new.setVKeyDefault(vkey_defaults->at(index));
|
|
}
|
|
sorted.push_back(button_new);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!button_found) {
|
|
auto &button = sorted.emplace_back(name);
|
|
|
|
if (vkey_defaults) {
|
|
button.setVKey(vkey_defaults->at(index));
|
|
button.setVKeyDefault(vkey_defaults->at(index));
|
|
}
|
|
}
|
|
|
|
++index;
|
|
}
|
|
|
|
return sorted;
|
|
}
|
|
|
|
GameAPI::Buttons::State GameAPI::Buttons::getState(rawinput::RawInputManager *manager, Button &_button, bool check_alts) {
|
|
|
|
// check override
|
|
if (_button.override_enabled) {
|
|
return _button.override_state;
|
|
}
|
|
|
|
// for iterating button alternatives
|
|
auto current_button = &_button;
|
|
auto alternatives = check_alts ? ¤t_button->getAlternatives() : nullptr;
|
|
unsigned int button_count = 0;
|
|
std::optional<bool> window_has_focus;
|
|
while (true) {
|
|
|
|
// naive behavior
|
|
if (current_button->isNaive()) {
|
|
GameAPI::Buttons::State state;
|
|
auto vkey = current_button->getVKey();
|
|
|
|
// check for focus
|
|
if (vkey != INVALID_VKEY && rawinput::NAIVE_REQUIRE_FOCUS) {
|
|
if (!window_has_focus.has_value()) {
|
|
window_has_focus = superexit::has_focus();
|
|
}
|
|
if (!window_has_focus.value()) {
|
|
vkey = INVALID_VKEY;
|
|
}
|
|
}
|
|
|
|
// read
|
|
if (vkey == INVALID_VKEY) {
|
|
state = BUTTON_NOT_PRESSED;
|
|
} else {
|
|
state = (GetAsyncKeyState(vkey) & 0x8000) ? BUTTON_PRESSED : BUTTON_NOT_PRESSED;
|
|
}
|
|
|
|
// invert
|
|
if (current_button->getInvert()) {
|
|
if (state == BUTTON_PRESSED) {
|
|
state = BUTTON_NOT_PRESSED;
|
|
} else {
|
|
state = BUTTON_PRESSED;
|
|
}
|
|
}
|
|
|
|
// return state
|
|
if (state != BUTTON_NOT_PRESSED) {
|
|
return state;
|
|
}
|
|
|
|
// get next button
|
|
button_count++;
|
|
if (!alternatives || alternatives->empty() || button_count - 1 >= alternatives->size()) {
|
|
return BUTTON_NOT_PRESSED;
|
|
} else {
|
|
current_button = &alternatives->at(button_count - 1);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// get device
|
|
auto &devid = current_button->getDeviceIdentifier();
|
|
auto device = manager->devices_get(devid, false); // TODO: fix to update only
|
|
|
|
// check for focus
|
|
if (device && rawinput::RAWINPUT_REQUIRE_FOCUS) {
|
|
if (!window_has_focus.has_value()) {
|
|
window_has_focus = superexit::has_focus();
|
|
}
|
|
if (!window_has_focus.value()) {
|
|
device = nullptr;
|
|
}
|
|
}
|
|
|
|
// get state if device was marked as updated
|
|
GameAPI::Buttons::State state = current_button->getLastState();
|
|
double *last_up = nullptr;
|
|
double *last_down = nullptr;
|
|
if (device) {
|
|
|
|
// lock device
|
|
device->mutex->lock();
|
|
|
|
// get vkey
|
|
auto vKey = current_button->getVKey();
|
|
|
|
// update state based on device type
|
|
switch (device->type) {
|
|
case rawinput::MOUSE: {
|
|
if (vKey < sizeof(device->mouseInfo->key_states)) {
|
|
auto mouse = device->mouseInfo;
|
|
state = mouse->key_states[vKey] ? BUTTON_PRESSED : BUTTON_NOT_PRESSED;
|
|
last_up = &mouse->key_up[vKey];
|
|
last_down = &mouse->key_down[vKey];
|
|
}
|
|
break;
|
|
}
|
|
case rawinput::KEYBOARD: {
|
|
if (vKey < sizeof(device->keyboardInfo->key_states)) {
|
|
auto kb = device->keyboardInfo;
|
|
state = kb->key_states[vKey] ? BUTTON_PRESSED : BUTTON_NOT_PRESSED;
|
|
last_up = &kb->key_up[vKey];
|
|
last_down = &kb->key_down[vKey];
|
|
}
|
|
break;
|
|
}
|
|
case rawinput::HID: {
|
|
auto hid = device->hidInfo;
|
|
auto bat = current_button->getAnalogType();
|
|
switch (bat) {
|
|
case BAT_NONE: {
|
|
auto button_states_it = hid->button_states.begin();
|
|
auto button_up_it = hid->button_up.begin();
|
|
auto button_down_it = hid->button_down.begin();
|
|
while (button_states_it != hid->button_states.end()) {
|
|
auto size = button_states_it->size();
|
|
if (vKey < size) {
|
|
state = (*button_states_it)[vKey] ? BUTTON_PRESSED : BUTTON_NOT_PRESSED;
|
|
last_up = &(*button_up_it)[vKey];
|
|
last_down = &(*button_down_it)[vKey];
|
|
break;
|
|
} else {
|
|
vKey -= size;
|
|
++button_states_it;
|
|
++button_up_it;
|
|
++button_down_it;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case BAT_NEGATIVE:
|
|
case BAT_POSITIVE:
|
|
case BAT_ANY: {
|
|
auto value_states = &hid->value_states;
|
|
if (vKey < value_states->size()) {
|
|
auto value = value_states->at(vKey);
|
|
if (current_button->getAnalogType() == BAT_POSITIVE) {
|
|
float threshold = 0.6f;
|
|
if (current_button->getBatThreshold() > 0) {
|
|
threshold = std::clamp(current_button->getBatThreshold() / 100.f, 0.01f, 0.99f);
|
|
}
|
|
state = value > threshold ? BUTTON_PRESSED : BUTTON_NOT_PRESSED;
|
|
|
|
} else if (current_button->getAnalogType() == BAT_NEGATIVE) {
|
|
float threshold = 0.4f;
|
|
if (current_button->getBatThreshold() > 0) {
|
|
threshold = std::clamp((100 - current_button->getBatThreshold()) / 100.f, 0.01f, 0.99f);
|
|
}
|
|
state = value < threshold ? BUTTON_PRESSED : BUTTON_NOT_PRESSED;
|
|
|
|
} else {
|
|
state = value > 0.01f ? BUTTON_PRESSED : BUTTON_NOT_PRESSED;
|
|
}
|
|
} else {
|
|
state = BUTTON_NOT_PRESSED;
|
|
}
|
|
break;
|
|
}
|
|
case BAT_HS_UP:
|
|
case BAT_HS_UPRIGHT:
|
|
case BAT_HS_RIGHT:
|
|
case BAT_HS_DOWNRIGHT:
|
|
case BAT_HS_DOWN:
|
|
case BAT_HS_DOWNLEFT:
|
|
case BAT_HS_LEFT:
|
|
case BAT_HS_UPLEFT:
|
|
case BAT_HS_NEUTRAL: {
|
|
auto &value_states = hid->value_states;
|
|
if (vKey < value_states.size()) {
|
|
auto value = value_states.at(vKey);
|
|
|
|
// get hat switch values
|
|
ButtonAnalogType buffer[3];
|
|
Button::getHatSwitchValues(value, buffer);
|
|
|
|
// check if one of the values match our analog type
|
|
state = BUTTON_NOT_PRESSED;
|
|
for (ButtonAnalogType &buffer_bat : buffer) {
|
|
if (buffer_bat == bat) {
|
|
state = BUTTON_PRESSED;
|
|
break;
|
|
}
|
|
}
|
|
|
|
} else
|
|
state = BUTTON_NOT_PRESSED;
|
|
break;
|
|
}
|
|
default:
|
|
state = BUTTON_NOT_PRESSED;
|
|
break;
|
|
}
|
|
break;
|
|
}
|
|
case rawinput::MIDI: {
|
|
auto bat = current_button->getAnalogType();
|
|
auto midi = device->midiInfo;
|
|
switch (bat) {
|
|
case BAT_NONE: {
|
|
if (rawinput::get_midi_algorithm() == rawinput::MidiNoteAlgorithm::LEGACY) {
|
|
// spicetools legacy midi logic: use event log
|
|
//
|
|
// drums send NOTE_ON and NOTE_OFF in rapid succession, before game engine has a chance
|
|
// to poll for it - to address this, keep a counter (states_events array) and the last
|
|
// state (states array), incrementing the states_events on rising edges (NOTE_ON)
|
|
// and popping events off the queue every time it's checked.
|
|
//
|
|
// if the same drum pad is mapped to multiple buttons, multiple issues arise:
|
|
// 1. we run through this logic for each button, which consumes an event every time;
|
|
// therefore, the first button may see the ON event, but subsequent mappings may
|
|
// completely miss it as it already has been drained
|
|
// 2. it is impossible to implement velocity threshold with this logic since the
|
|
// velocity is a per-note value that goes away as soon as NOTE_OFF is detected
|
|
if (vKey < midi->states_events.size()) {
|
|
// check for event
|
|
auto midi_event = midi->states_events[vKey];
|
|
if (midi_event) {
|
|
|
|
// choose state based on event
|
|
state = (midi_event % 2) ? BUTTON_PRESSED : BUTTON_NOT_PRESSED;
|
|
|
|
// update event
|
|
if (!midi->states[vKey] || midi_event > 1) {
|
|
midi->states_events[vKey]--;
|
|
}
|
|
} else {
|
|
state = BUTTON_NOT_PRESSED;
|
|
}
|
|
}
|
|
} else {
|
|
// spice2x midi logic (new!)
|
|
//
|
|
// for every MIDI NOTE ON message, latch the "on" for a certain time, even if NOTE
|
|
// OFF message is seen immediately afterwards.
|
|
//
|
|
// each ON event is held long enough for the game's input poll to see it (e.g., gitadora
|
|
// polls every 16ms or so, rawinput holds it for 20ms by default)
|
|
//
|
|
// this is much simpler and does not have the issues mentioned above for the legacy
|
|
// logic, however the downside is that there is a risk of coalescing rapid inputs into
|
|
// one.
|
|
//
|
|
// that being said:
|
|
// * default value of 20ms should be reasonable; humans can't realistically hit the
|
|
// same note faster than this; in fact it's likely to be a misfire
|
|
// * we can tweak it per-game if needed to suit the game's polling period (in the
|
|
// future)
|
|
// * as a last resort the user can always override it via the option (MidiNoteSustain)
|
|
if (vKey < midi->v2_last_on_time.size()) {
|
|
|
|
// take the velocity threshold from first button binding we encounter here
|
|
// this hardware key may be mapped to multiple bindings, but the UI should keep them
|
|
// the same value, as only one threshold value can be set per MIDI key
|
|
// (otherwise it makes the sustain logic too complicated)
|
|
const auto sw_threshold = current_button->getVelocityThreshold();
|
|
if (0 < sw_threshold && !midi->v2_velocity_threshold_set_on_device[vKey]) {
|
|
midi->v2_velocity_threshold_set_on_device[vKey] = true;
|
|
midi->v2_velocity_threshold[vKey] = sw_threshold;
|
|
}
|
|
|
|
state = getMidiV2ButtonState(
|
|
midi->v2_last_on_time[vKey],
|
|
midi->v2_last_off_time[vKey]);
|
|
|
|
} else {
|
|
state = BUTTON_NOT_PRESSED;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case BAT_MIDI_CTRL_PRECISION: {
|
|
if (vKey < midi->controls_precision.size()) {
|
|
if (rawinput::get_midi_algorithm() == rawinput::MidiNoteAlgorithm::LEGACY) {
|
|
state = midi->controls_precision[vKey] > 0 ? BUTTON_PRESSED : BUTTON_NOT_PRESSED;
|
|
} else {
|
|
// not using getVelocityHelper here to avoid locking and other checks
|
|
const auto v = device->midiInfo->controls_precision[vKey];
|
|
// velocity threshold ranges from [0, 127], so do some math for double precision
|
|
const auto threshold = (current_button->getVelocityThreshold() << 7u) | 0x7f;
|
|
state = (threshold < v) ? BUTTON_PRESSED : BUTTON_NOT_PRESSED;
|
|
}
|
|
} else {
|
|
state = BUTTON_NOT_PRESSED;
|
|
}
|
|
break;
|
|
}
|
|
case BAT_MIDI_CTRL_SINGLE: {
|
|
if (vKey < midi->controls_single.size()) {
|
|
if (rawinput::get_midi_algorithm() == rawinput::MidiNoteAlgorithm::LEGACY) {
|
|
state = midi->controls_single[vKey] > 0 ? BUTTON_PRESSED : BUTTON_NOT_PRESSED;
|
|
} else {
|
|
// not using getVelocityHelper here to avoid locking and other checks
|
|
const auto v = device->midiInfo->controls_single[vKey];
|
|
state = (current_button->getVelocityThreshold() < v) ?
|
|
BUTTON_PRESSED : BUTTON_NOT_PRESSED;
|
|
}
|
|
} else {
|
|
state = BUTTON_NOT_PRESSED;
|
|
}
|
|
break;
|
|
}
|
|
case BAT_MIDI_CTRL_ONOFF: {
|
|
if (vKey < midi->controls_onoff.size()) {
|
|
if (rawinput::get_midi_algorithm() == rawinput::MidiNoteAlgorithm::LEGACY) {
|
|
state = midi->controls_onoff[vKey] ? BUTTON_PRESSED : BUTTON_NOT_PRESSED;
|
|
} else {
|
|
state = getMidiV2ButtonState(
|
|
midi->v2_controls_onoff_last_on_time[vKey],
|
|
midi->v2_controls_onoff_last_off_time[vKey]);
|
|
}
|
|
} else {
|
|
state = BUTTON_NOT_PRESSED;
|
|
}
|
|
break;
|
|
}
|
|
case BAT_MIDI_PITCH_DOWN:
|
|
if (vKey < midi->pitch_bend.size()) {
|
|
state = midi->pitch_bend[vKey] < 0 ? BUTTON_PRESSED : BUTTON_NOT_PRESSED;
|
|
} else {
|
|
state = BUTTON_NOT_PRESSED;
|
|
}
|
|
break;
|
|
case BAT_MIDI_PITCH_UP:
|
|
if (vKey < midi->pitch_bend.size()) {
|
|
state = midi->pitch_bend[vKey] > 0 ? BUTTON_PRESSED : BUTTON_NOT_PRESSED;
|
|
} else {
|
|
state = BUTTON_NOT_PRESSED;
|
|
}
|
|
break;
|
|
default: {
|
|
state = BUTTON_NOT_PRESSED;
|
|
break;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case rawinput::PIUIO_DEVICE: {
|
|
state = device->piuioDev->IsPressed(vKey) ? BUTTON_PRESSED : BUTTON_NOT_PRESSED;
|
|
break;
|
|
}
|
|
case rawinput::XINPUT_GAMEPAD: {
|
|
assert(reinterpret_cast<uintptr_t>(device->handle) < XUSER_MAX_COUNT);
|
|
const auto player = static_cast<uint8_t>(reinterpret_cast<uintptr_t>(device->handle));
|
|
state = manager->XINPUT_MGR->is_button_pressed(
|
|
player,
|
|
static_cast<xinput::XInputButtonEnum>(vKey)) ?
|
|
BUTTON_PRESSED : BUTTON_NOT_PRESSED;
|
|
break;
|
|
}
|
|
case rawinput::DESTROYED:
|
|
// device was unplugged. release the button instead of leaving
|
|
// state at getLastState(), which would latch a held button on
|
|
// forever (e.g. a MIDI note held while the device is removed)
|
|
state = BUTTON_NOT_PRESSED;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
// unlock device
|
|
device->mutex->unlock();
|
|
}
|
|
|
|
// debounce
|
|
if (state == BUTTON_NOT_PRESSED) {
|
|
if (last_up) {
|
|
auto debounce_up = current_button->getDebounceUp();
|
|
if (debounce_up > 0.0 && get_performance_seconds() - *last_up < debounce_up) {
|
|
state = BUTTON_PRESSED;
|
|
}
|
|
}
|
|
} else {
|
|
if (last_down) {
|
|
auto debounce_down = current_button->getDebounceDown();
|
|
if (debounce_down > 0.0 && get_performance_seconds() - *last_down < debounce_down) {
|
|
state = BUTTON_NOT_PRESSED;
|
|
}
|
|
}
|
|
}
|
|
|
|
// set last state
|
|
current_button->setLastState(state);
|
|
|
|
// invert
|
|
if (current_button->getInvert()) {
|
|
if (state == BUTTON_PRESSED) {
|
|
state = BUTTON_NOT_PRESSED;
|
|
} else {
|
|
state = BUTTON_PRESSED;
|
|
}
|
|
}
|
|
|
|
// early quit
|
|
if (state == BUTTON_PRESSED) {
|
|
return state;
|
|
}
|
|
|
|
// get next button
|
|
button_count++;
|
|
if (!alternatives || alternatives->empty() || button_count - 1 >= alternatives->size()) {
|
|
return BUTTON_NOT_PRESSED;
|
|
} else {
|
|
current_button = &alternatives->at(button_count - 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
Buttons::State Buttons::getState(std::unique_ptr<rawinput::RawInputManager> &manager, Button &button, bool check_alts) {
|
|
if (manager) {
|
|
return getState(manager.get(), button, check_alts);
|
|
} else {
|
|
return button.getLastState();
|
|
}
|
|
}
|
|
|
|
static float getVelocityHelper(rawinput::RawInputManager *manager, Button &button) {
|
|
|
|
// check override
|
|
if (button.override_enabled) {
|
|
return button.override_velocity;
|
|
}
|
|
|
|
// naive behavior
|
|
if (button.isNaive()) {
|
|
if (button.getInvert()) {
|
|
return (GetAsyncKeyState(button.getVKey()) & 0x8000) ? 0.f : 1.f;
|
|
} else {
|
|
return (GetAsyncKeyState(button.getVKey()) & 0x8000) ? 1.f : 0.f;
|
|
}
|
|
}
|
|
|
|
// get button state
|
|
Buttons::State button_state = Buttons::getState(manager, button, false);
|
|
|
|
// check if button isn't being pressed
|
|
if (button_state != Buttons::BUTTON_PRESSED) {
|
|
return 0.f;
|
|
}
|
|
|
|
// get device
|
|
auto &devid = button.getDeviceIdentifier();
|
|
auto device = manager->devices_get(devid, false);
|
|
|
|
// return last velocity if device wasn't found
|
|
if (!device) {
|
|
return button.getLastVelocity();
|
|
}
|
|
|
|
// prepare
|
|
float velocity = 1.f;
|
|
auto vKey = button.getVKey();
|
|
|
|
// lock device
|
|
device->mutex->lock();
|
|
|
|
// determine velocity based on device type
|
|
if (device->type == rawinput::MIDI) {
|
|
switch (button.getAnalogType()) {
|
|
case ButtonAnalogType::BAT_MIDI_CTRL_PRECISION:
|
|
if (vKey < device->midiInfo->controls_precision.size()) {
|
|
velocity = device->midiInfo->controls_precision[vKey] / 16383.f;
|
|
} else {
|
|
velocity = 0.f;
|
|
}
|
|
break;
|
|
|
|
case ButtonAnalogType::BAT_MIDI_CTRL_SINGLE:
|
|
if (vKey < device->midiInfo->controls_single.size()) {
|
|
velocity = device->midiInfo->controls_single[vKey] / 127.f;
|
|
} else {
|
|
velocity = 0.f;
|
|
}
|
|
break;
|
|
|
|
case ButtonAnalogType::BAT_MIDI_CTRL_ONOFF:
|
|
if (vKey < device->midiInfo->controls_onoff.size()) {
|
|
velocity = device->midiInfo->controls_onoff[vKey] ? 1.f : 0.f;
|
|
} else {
|
|
velocity = 0.f;
|
|
}
|
|
break;
|
|
|
|
case ButtonAnalogType::BAT_MIDI_PITCH_DOWN:
|
|
if (vKey < device->midiInfo->pitch_bend.size()) {
|
|
velocity = device->midiInfo->pitch_bend[vKey] < 0 ? 1.f : 0.f;
|
|
} else {
|
|
velocity = 0.f;
|
|
}
|
|
break;
|
|
|
|
case ButtonAnalogType::BAT_MIDI_PITCH_UP:
|
|
if (vKey < device->midiInfo->pitch_bend.size()) {
|
|
// pitch range is [-8192, 8191]
|
|
velocity = (device->midiInfo->pitch_bend[vKey]) > 0 ? 1.f : 0.f;
|
|
} else {
|
|
velocity = 0.f;
|
|
}
|
|
break;
|
|
|
|
case ButtonAnalogType::BAT_NONE:
|
|
default:
|
|
// velocity sensitive
|
|
if (vKey < device->midiInfo->velocity.size()) {
|
|
velocity = (float) device->midiInfo->velocity[vKey] / 127.f;
|
|
} else {
|
|
velocity = 0.f;
|
|
}
|
|
break;
|
|
}
|
|
|
|
// invert
|
|
if (button.getInvert()) {
|
|
velocity = 1.f - velocity;
|
|
}
|
|
}
|
|
|
|
// unlock device
|
|
device->mutex->unlock();
|
|
|
|
// set last velocity
|
|
button.setLastVelocity(velocity);
|
|
|
|
// return determined velocity
|
|
return velocity;
|
|
}
|
|
|
|
float GameAPI::Buttons::getVelocity(rawinput::RawInputManager *manager, Button &button) {
|
|
|
|
// get button velocity
|
|
auto velocity = getVelocityHelper(manager, button);
|
|
|
|
// check alternatives
|
|
for (auto &alternative : button.getAlternatives()) {
|
|
auto alt_velocity = getVelocityHelper(manager, alternative);
|
|
if (alt_velocity > velocity) {
|
|
velocity = alt_velocity;
|
|
}
|
|
}
|
|
|
|
// return highest velocity detected
|
|
return velocity;
|
|
}
|
|
|
|
float Buttons::getVelocity(std::unique_ptr<rawinput::RawInputManager> &manager, Button &button) {
|
|
if (manager) {
|
|
return getVelocity(manager.get(), button);
|
|
} else {
|
|
return button.getLastVelocity();
|
|
}
|
|
}
|
|
|
|
float GameAPI::Analogs::getState(rawinput::RawInputManager *manager, rawinput::Device *device, Analog &analog) {
|
|
float value = 0.5f;
|
|
if (!device) {
|
|
return value;
|
|
}
|
|
|
|
auto index = analog.getIndex();
|
|
auto inverted = analog.getInvert();
|
|
device->mutex->lock();
|
|
|
|
// get value from device
|
|
switch (device->type) {
|
|
case rawinput::MOUSE: {
|
|
|
|
// check for focus
|
|
if (rawinput::NAIVE_REQUIRE_FOCUS && !superexit::has_focus()) {
|
|
value = analog.getLastState();
|
|
break;
|
|
}
|
|
|
|
// get mouse position
|
|
auto mouse = device->mouseInfo;
|
|
long pos;
|
|
switch (index) {
|
|
case rawinput::MOUSEPOS_X:
|
|
pos = mouse->pos_x;
|
|
break;
|
|
case rawinput::MOUSEPOS_Y:
|
|
pos = mouse->pos_y;
|
|
break;
|
|
case rawinput::MOUSEPOS_WHEEL:
|
|
pos = mouse->pos_wheel;
|
|
break;
|
|
default:
|
|
pos = 0;
|
|
break;
|
|
}
|
|
|
|
// apply sensitivity
|
|
auto val = (int) roundf(pos * analog.getSensitivity());
|
|
if (val < 0) {
|
|
inverted = !inverted;
|
|
}
|
|
|
|
// modulo & normalize to [0.0, 1.0]
|
|
if (index != rawinput::MOUSEPOS_WHEEL) {
|
|
val = std::abs(val) % 257;
|
|
value = val / 256.f;
|
|
} else {
|
|
val = std::abs(val) % 65;
|
|
value = val / 64.f;
|
|
}
|
|
|
|
// invert
|
|
if (inverted) {
|
|
value = 1.f - value;
|
|
}
|
|
|
|
break;
|
|
}
|
|
case rawinput::HID: {
|
|
|
|
// get value
|
|
if (index < device->hidInfo->value_states.size()) {
|
|
if (inverted) {
|
|
value = 1.f - device->hidInfo->value_states[index];
|
|
} else {
|
|
value = device->hidInfo->value_states[index];
|
|
}
|
|
}
|
|
|
|
// deadzone
|
|
// do not apply deadzone to circular analogs since it doesn't make sense (except in relative mode)
|
|
if (analog.isDeadzoneSet() &&
|
|
(analog.getType() != AnalogType::Circular || analog.isRelativeMode())) {
|
|
value = analog.applyDeadzone(value);
|
|
}
|
|
|
|
if (analog.getType() == AnalogType::Circular) {
|
|
|
|
if (analog.isRelativeMode()) {
|
|
value = analog.getRelativeModeValue(value);
|
|
|
|
} else {
|
|
// integer multiplier
|
|
value = analog.applyMultiplier(value);
|
|
|
|
// smoothing/sensitivity
|
|
if (analog.getSmoothing() || analog.isSensitivitySet()) {
|
|
float rads = value * (float) M_TAU;
|
|
|
|
// smoothing
|
|
if (analog.getSmoothing()) {
|
|
|
|
// preserve direction
|
|
if (rads >= M_TAU) {
|
|
rads -= 0.0001f;
|
|
}
|
|
|
|
// calculate angle
|
|
rads = analog.getSmoothedValue(rads);
|
|
}
|
|
|
|
// sensitivity
|
|
if (analog.isSensitivitySet()) {
|
|
rads = analog.applyAngularSensitivity(rads);
|
|
}
|
|
|
|
// apply to value
|
|
value = rads * (float) M_1_TAU;
|
|
}
|
|
}
|
|
} else {
|
|
// sensitivity
|
|
if (analog.isSensitivitySet()) {
|
|
// adjust curve
|
|
// values < 1.f : less sensitive around neutral
|
|
// values > 1.f : more sensitive around neutral
|
|
float curve = analog.getSensitivity();
|
|
if (curve <= 0.f) {
|
|
curve = 0.01f;
|
|
}
|
|
curve = 1.f / curve;
|
|
|
|
if (analog.getType() == AnalogType::LinearCentered) {
|
|
// convert 0.0..1.0 to -1.0..+1.0
|
|
float signed_raw = (value - 0.5f) * 2.0f;
|
|
// apply curve
|
|
float sign = signed_raw < 0.0f ? -1.0f : 1.0f;
|
|
float magnitude = fabsf(signed_raw);
|
|
float curved = sign * powf(magnitude, curve);
|
|
// convert back to 0.0..1.0
|
|
value = curved * 0.5f + 0.5f;
|
|
} else {
|
|
value = powf(value, curve);
|
|
}
|
|
|
|
value = std::clamp(value, 0.f, 1.f);
|
|
}
|
|
|
|
// multiplier / divisor
|
|
if (analog.getMultiplier() < -1) {
|
|
if (analog.getType() == AnalogType::LinearCentered) {
|
|
value = (value - 0.5f) / (-analog.getMultiplier()) + 0.5f;
|
|
} else {
|
|
value /= -analog.getMultiplier();
|
|
}
|
|
|
|
value = std::clamp(value, 0.f, 1.f);
|
|
|
|
} else if (analog.getMultiplier() > 1) {
|
|
if (analog.getType() == AnalogType::LinearCentered) {
|
|
value = (value - 0.5f) * analog.getMultiplier() + 0.5f;
|
|
} else {
|
|
value *= analog.getMultiplier();
|
|
}
|
|
|
|
value = std::clamp(value, 0.f, 1.f);
|
|
}
|
|
}
|
|
|
|
// delay
|
|
if (analog.getDelayMs() > 0) {
|
|
value = analog.getDelayedValue(value);
|
|
}
|
|
|
|
break;
|
|
}
|
|
case rawinput::MIDI: {
|
|
|
|
// get sizes
|
|
auto midi = device->midiInfo;
|
|
auto prec_count = (int) midi->controls_precision.size();
|
|
auto single_count = (int) midi->controls_single.size();
|
|
auto onoff_count = (int) midi->controls_onoff.size();
|
|
auto pitch_count = (int) midi->pitch_bend.size();
|
|
|
|
// decide on value
|
|
if (index < prec_count)
|
|
value = midi->controls_precision[index] / 16383.f;
|
|
else if (index < prec_count + single_count)
|
|
value = midi->controls_single[index - prec_count] / 127.f;
|
|
else if (index < prec_count + single_count + onoff_count)
|
|
value = midi->controls_onoff[index - prec_count - single_count] ? 1.f : 0.f;
|
|
else if (index < prec_count + single_count + onoff_count + pitch_count)
|
|
value = (midi->pitch_bend[index - prec_count - single_count - onoff_count] + 0x2000) / 16383.f;
|
|
|
|
// invert value
|
|
if (inverted) {
|
|
value = 1.f - value;
|
|
}
|
|
|
|
// deadzone
|
|
if (analog.isDeadzoneSet()) {
|
|
value = analog.applyDeadzone(value);
|
|
}
|
|
break;
|
|
}
|
|
case rawinput::XINPUT_GAMEPAD: {
|
|
assert(reinterpret_cast<uintptr_t>(device->handle) < XUSER_MAX_COUNT);
|
|
const auto player = static_cast<uint8_t>(reinterpret_cast<uintptr_t>(device->handle));
|
|
value = manager->XINPUT_MGR->get_analog_state(
|
|
player,
|
|
static_cast<xinput::XInputAnalogEnum>(index));
|
|
// invert value
|
|
if (inverted) {
|
|
value = 1.f - value;
|
|
}
|
|
break;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
|
|
device->mutex->unlock();
|
|
|
|
return value;
|
|
}
|
|
|
|
std::vector<Analog> GameAPI::Analogs::getAnalogs(const std::string &game_name) {
|
|
return Config::getInstance().getAnalogs(game_name);
|
|
}
|
|
|
|
std::vector<Analog> GameAPI::Analogs::sortAnalogs(
|
|
const std::vector<Analog> &analogs,
|
|
const std::vector<std::string> &analog_names)
|
|
{
|
|
std::vector<Analog> sorted;
|
|
|
|
bool analog_found;
|
|
for (auto &name : analog_names) {
|
|
analog_found = false;
|
|
|
|
for (auto &analog : analogs) {
|
|
if (name == analog.getName()) {
|
|
analog_found = true;
|
|
sorted.push_back(analog);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!analog_found) {
|
|
sorted.emplace_back(name);
|
|
}
|
|
}
|
|
|
|
return sorted;
|
|
}
|
|
|
|
static std::vector<Analog> sortAnalogsWithTypeInternal(
|
|
std::vector<Analog> &analogs,
|
|
const std::initializer_list<GameAPI::Analogs::AnalogWithType> list) {
|
|
|
|
std::vector<Analog> sorted;
|
|
|
|
bool analog_found;
|
|
for (auto &a : list) {
|
|
analog_found = false;
|
|
|
|
for (auto &analog : analogs) {
|
|
if (a.name == analog.getName()) {
|
|
analog_found = true;
|
|
analog.setType(a.type);
|
|
sorted.push_back(analog);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!analog_found) {
|
|
sorted.emplace_back(a.name, a.type);
|
|
}
|
|
}
|
|
|
|
return sorted;
|
|
}
|
|
|
|
void GameAPI::Analogs::sortAnalogsWithType(
|
|
std::vector<Analog> *analogs,
|
|
const std::initializer_list<AnalogWithType> list) {
|
|
|
|
if (analogs) {
|
|
*analogs = sortAnalogsWithTypeInternal(*analogs, list);
|
|
}
|
|
}
|
|
|
|
float GameAPI::Analogs::getState(rawinput::RawInputManager *manager, Analog &analog) {
|
|
|
|
// check override
|
|
if (analog.override_enabled) {
|
|
return analog.override_state;
|
|
}
|
|
|
|
// get device
|
|
auto &devid = analog.getDeviceIdentifier();
|
|
auto device = manager->devices_get(devid, false); // TODO: fix to update only
|
|
|
|
// return last state if device wasn't updated
|
|
if (!device) {
|
|
return analog.getLastState();
|
|
}
|
|
|
|
float state = getState(manager, device, analog);
|
|
analog.setLastState(state);
|
|
|
|
return state;
|
|
}
|
|
|
|
float Analogs::getState(std::unique_ptr<rawinput::RawInputManager> &manager, Analog &analog) {
|
|
if (manager) {
|
|
return getState(manager.get(), analog);
|
|
} else {
|
|
return analog.getLastState();
|
|
}
|
|
}
|
|
|
|
std::vector<Light> GameAPI::Lights::getLights(const std::string &game_name) {
|
|
return Config::getInstance().getLights(game_name);
|
|
}
|
|
|
|
std::vector<Light> GameAPI::Lights::sortLights(
|
|
const std::vector<Light> &lights,
|
|
const std::vector<std::string> &light_names)
|
|
{
|
|
std::vector<Light> sorted;
|
|
|
|
bool light_found;
|
|
for (auto &name : light_names) {
|
|
light_found = false;
|
|
|
|
for (auto &light : lights) {
|
|
if (name == light.getName()) {
|
|
light_found = true;
|
|
sorted.push_back(light);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!light_found) {
|
|
sorted.emplace_back(name);
|
|
}
|
|
}
|
|
|
|
return sorted;
|
|
}
|
|
|
|
static std::vector<Light> sortLightsWithCategoryInternal(
|
|
const std::vector<Light> &lights,
|
|
const std::initializer_list<GameAPI::Lights::LightAndCategory> list)
|
|
{
|
|
std::vector<Light> sorted;
|
|
|
|
bool light_found;
|
|
for (auto &name : list) {
|
|
light_found = false;
|
|
|
|
for (auto &light : lights) {
|
|
if (name.light_name == light.getName()) {
|
|
light_found = true;
|
|
Light light_new = light;
|
|
light_new.setCategory(name.category_name);
|
|
sorted.push_back(light_new);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!light_found) {
|
|
sorted.emplace_back(name.light_name, name.category_name);
|
|
}
|
|
}
|
|
|
|
return sorted;
|
|
}
|
|
|
|
void GameAPI::Lights::sortLightsWithCategory(
|
|
std::vector<Light> *lights,
|
|
const std::initializer_list<LightAndCategory> list) {
|
|
|
|
if (lights) {
|
|
*lights = sortLightsWithCategoryInternal(*lights, list);
|
|
}
|
|
}
|
|
|
|
|
|
void GameAPI::Lights::writeLight(rawinput::RawInputManager *manager, rawinput::Device *device, int index, float value) {
|
|
|
|
// check device
|
|
if (!device) {
|
|
return;
|
|
}
|
|
|
|
// clamp to range [0,1]
|
|
value = CLAMP(value, 0.f, 1.f);
|
|
|
|
// lock device
|
|
device->mutex->lock();
|
|
|
|
// enable output
|
|
device->output_enabled = true;
|
|
|
|
// check type
|
|
switch (device->type) {
|
|
case rawinput::HID: {
|
|
auto hid = device->hidInfo;
|
|
|
|
// find in buttons
|
|
bool button_found = false;
|
|
for (auto &button_states : hid->button_output_states) {
|
|
if ((size_t) index < button_states.size()) {
|
|
auto new_state = value > 0.5f;
|
|
if (button_states[index] != new_state) {
|
|
button_states[index] = new_state;
|
|
device->output_pending = true;
|
|
}
|
|
button_found = true;
|
|
break;
|
|
} else
|
|
index -= button_states.size();
|
|
}
|
|
|
|
// find in values
|
|
if (!button_found) {
|
|
auto &value_states = hid->value_output_states;
|
|
if ((size_t) index < value_states.size()) {
|
|
auto cur_state = &value_states[index];
|
|
if (*cur_state != value) {
|
|
*cur_state = value;
|
|
device->output_pending = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
break;
|
|
}
|
|
case rawinput::SEXTET_OUTPUT: {
|
|
if (index < rawinput::SextetDevice::LIGHT_COUNT) {
|
|
device->sextetInfo->light_state[index] = value > 0;
|
|
device->sextetInfo->push_light_state();
|
|
device->output_pending = true;
|
|
} else {
|
|
log_warning("api", "invalid sextet light index: {}", index);
|
|
}
|
|
break;
|
|
}
|
|
case rawinput::PIUIO_DEVICE: {
|
|
if (index < rawinput::PIUIO::PIUIO_MAX_NUM_OF_LIGHTS) {
|
|
device->piuioDev->SetLight(index, value > 0);
|
|
device->output_pending = true;
|
|
} else {
|
|
log_warning("api", "invalid piuio light index: {}", index);
|
|
}
|
|
break;
|
|
}
|
|
case rawinput::SMX_STAGE: {
|
|
if (index < rawinput::SmxStageDevice::TOTAL_LIGHT_COUNT) {
|
|
device->smxstageInfo->SetLightByIndex(index, static_cast<uint8_t>(value*255.f));
|
|
device->output_pending = true;
|
|
} else {
|
|
log_warning("api", "invalid smx stage light index: {}", index);
|
|
}
|
|
break;
|
|
}
|
|
case rawinput::SMX_DEDICAB: {
|
|
if (index < rawinput::SmxDedicabDevice::LIGHTS_COUNT) {
|
|
device->smxdedicabInfo->SetLightByIndex(index, static_cast<uint8_t>(value * 255.f));
|
|
device->output_pending = true;
|
|
} else {
|
|
log_warning("api", "invalid SMX dedicab light index: {}", index);
|
|
}
|
|
break;
|
|
}
|
|
case rawinput::XINPUT_GAMEPAD: {
|
|
assert(reinterpret_cast<uintptr_t>(device->handle) < XUSER_MAX_COUNT);
|
|
const auto player = static_cast<uint8_t>(reinterpret_cast<uintptr_t>(device->handle));
|
|
if (index < static_cast<int>(xinput::XInputOutputEnum::COUNT)) {
|
|
manager->XINPUT_MGR->set_output_state(
|
|
player, static_cast<xinput::XInputOutputEnum>(index), value);
|
|
// no need to set output_pending; xinput output handled immediately
|
|
} else {
|
|
log_warning("api", "invalid xinput light index: {}", index);
|
|
}
|
|
break;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
|
|
// unlock device
|
|
device->mutex->unlock();
|
|
}
|
|
|
|
void GameAPI::Lights::writeLight(rawinput::RawInputManager *manager, Light &light, float value) {
|
|
|
|
// clamp to range [0,1]
|
|
value = CLAMP(value, 0.f, 1.f);
|
|
|
|
// write to last state
|
|
light.last_state = value;
|
|
|
|
// get device
|
|
auto &devid = light.getDeviceIdentifier();
|
|
auto device = manager->devices_get(devid, false);
|
|
|
|
// check device
|
|
if (device) {
|
|
|
|
// write state
|
|
if (light.override_enabled) {
|
|
writeLight(manager, device, light.getIndex(), light.override_state);
|
|
} else {
|
|
writeLight(manager, device, light.getIndex(), value);
|
|
}
|
|
}
|
|
|
|
// alternatives
|
|
for (auto &alternative : light.getAlternatives()) {
|
|
if (light.override_enabled) {
|
|
alternative.override_enabled = true;
|
|
alternative.override_state = light.override_state;
|
|
writeLight(manager, alternative, light.override_state);
|
|
} else {
|
|
alternative.override_enabled = false;
|
|
writeLight(manager, alternative, value);
|
|
}
|
|
}
|
|
}
|
|
|
|
void Lights::writeLight(std::unique_ptr<rawinput::RawInputManager> &manager, Light &light, float value) {
|
|
if (manager) {
|
|
writeLight(manager.get(), light, value);
|
|
}
|
|
}
|
|
|
|
float GameAPI::Lights::readLight(rawinput::Device *device, int index) {
|
|
float ret = 0.f;
|
|
|
|
// lock device
|
|
device->mutex->lock();
|
|
|
|
// check type
|
|
switch (device->type) {
|
|
case rawinput::HID: {
|
|
auto hid = device->hidInfo;
|
|
|
|
// find in buttons
|
|
bool button_found = false;
|
|
for (auto &button_states : hid->button_output_states) {
|
|
if ((size_t) index < button_states.size()) {
|
|
ret = button_states[index] ? 1.f : 0.f;
|
|
button_found = true;
|
|
break;
|
|
} else
|
|
index -= button_states.size();
|
|
}
|
|
|
|
// find in values
|
|
if (!button_found) {
|
|
auto value_states = &hid->value_output_states;
|
|
if ((size_t) index < value_states->size()) {
|
|
ret = (*value_states)[index];
|
|
}
|
|
}
|
|
|
|
break;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
|
|
// unlock device
|
|
device->mutex->unlock();
|
|
|
|
// return result
|
|
return ret;
|
|
}
|
|
|
|
float GameAPI::Lights::readLight(rawinput::RawInputManager *manager, Light &light) {
|
|
|
|
// check override
|
|
if (light.override_enabled) {
|
|
return light.override_state;
|
|
}
|
|
|
|
// just return last state since that reflects the last value being written
|
|
return light.last_state;
|
|
}
|
|
|
|
float Lights::readLight(std::unique_ptr<rawinput::RawInputManager> &manager, Light &light) {
|
|
if (manager) {
|
|
return readLight(manager.get(), light);
|
|
} else {
|
|
|
|
// check override
|
|
if (light.override_enabled) {
|
|
return light.override_state;
|
|
}
|
|
|
|
// just return last state since that reflects the last value being written
|
|
return light.last_state;
|
|
}
|
|
}
|
|
|
|
std::vector<Option> GameAPI::Options::getOptions(const std::string &gameName) {
|
|
return Config::getInstance().getOptions(gameName);
|
|
}
|
|
|
|
void GameAPI::Options::sortOptions(std::vector<Option> &options, const std::vector<OptionDefinition> &definitions) {
|
|
std::vector<Option> sorted;
|
|
bool option_found;
|
|
|
|
for (const auto &definition : definitions) {
|
|
option_found = false;
|
|
|
|
for (auto &option : options) {
|
|
if (definition.name == option.get_definition().name) {
|
|
option_found = true;
|
|
|
|
auto &new_option = sorted.emplace_back(option);
|
|
new_option.set_definition(definition);
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!option_found) {
|
|
sorted.emplace_back(definition);
|
|
}
|
|
}
|
|
|
|
options = std::move(sorted);
|
|
}
|
|
|
|
static Buttons::State getMidiV2ButtonState(float on, float off) {
|
|
if (on == 0.0) {
|
|
return Buttons::State::BUTTON_NOT_PRESSED;
|
|
} else if (off < on) {
|
|
// if OFF was not observed strictly after ON, we can confidently say that the note
|
|
// remains ON; in case of a tie (rarely in v2, all the time in v2_drum), prefer to keep note
|
|
// off since that's better than a note stuck on
|
|
return Buttons::State::BUTTON_PRESSED;
|
|
} else {
|
|
// otherwise, this is an ON-OFF sequence
|
|
// check for time the most recent ON message
|
|
//
|
|
// if recent, consider the button to be on - even if there were OFF messages following it
|
|
// this is needed to detect things like MIDI drums which send a quick ON-OFF sequence
|
|
// between the game's polling period
|
|
const auto now = get_performance_milliseconds();
|
|
if ((now - on) < (double)rawinput::MIDI_NOTE_SUSTAIN) {
|
|
return Buttons::State::BUTTON_PRESSED;
|
|
} else {
|
|
return Buttons::State::BUTTON_NOT_PRESSED;
|
|
}
|
|
}
|
|
} |