Update to spice2x-25-04-25 (pre-apply)
> broken commit
This commit is contained in:
+287
-121
@@ -17,6 +17,8 @@ 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,
|
||||
@@ -161,14 +163,17 @@ GameAPI::Buttons::State GameAPI::Buttons::getState(rawinput::RawInputManager *ma
|
||||
break;
|
||||
}
|
||||
case BAT_NEGATIVE:
|
||||
case BAT_POSITIVE: {
|
||||
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) {
|
||||
state = value > 0.6f ? BUTTON_PRESSED : BUTTON_NOT_PRESSED;
|
||||
} else {
|
||||
} else if (current_button->getAnalogType() == BAT_NEGATIVE) {
|
||||
state = value < 0.4f ? BUTTON_PRESSED : BUTTON_NOT_PRESSED;
|
||||
} else {
|
||||
state = value > 0.01f ? BUTTON_PRESSED : BUTTON_NOT_PRESSED;
|
||||
}
|
||||
} else {
|
||||
state = BUTTON_NOT_PRESSED;
|
||||
@@ -216,50 +221,135 @@ GameAPI::Buttons::State GameAPI::Buttons::getState(rawinput::RawInputManager *ma
|
||||
auto midi = device->midiInfo;
|
||||
switch (bat) {
|
||||
case BAT_NONE: {
|
||||
if (vKey < 16 * 128) {
|
||||
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) {
|
||||
|
||||
// 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;
|
||||
|
||||
// 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()) {
|
||||
|
||||
// update event
|
||||
if (!midi->states[vKey] || midi_event > 1)
|
||||
midi->states_events[vKey]--;
|
||||
// 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;
|
||||
}
|
||||
|
||||
} else
|
||||
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 < 16 * 32)
|
||||
state = midi->controls_precision[vKey] > 0 ? BUTTON_PRESSED : BUTTON_NOT_PRESSED;
|
||||
else
|
||||
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 < 16 * 44)
|
||||
state = midi->controls_single[vKey] > 0 ? BUTTON_PRESSED : BUTTON_NOT_PRESSED;
|
||||
else
|
||||
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 < 16 * 6)
|
||||
state = midi->controls_onoff[vKey] ? BUTTON_PRESSED : BUTTON_NOT_PRESSED;
|
||||
else
|
||||
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:
|
||||
state = midi->pitch_bend < 0x2000 ? BUTTON_PRESSED : BUTTON_NOT_PRESSED;
|
||||
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:
|
||||
state = midi->pitch_bend > 0x2000 ? BUTTON_PRESSED : BUTTON_NOT_PRESSED;
|
||||
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;
|
||||
@@ -372,24 +462,64 @@ static float getVelocityHelper(rawinput::RawInputManager *manager, Button &butto
|
||||
device->mutex->lock();
|
||||
|
||||
// determine velocity based on device type
|
||||
switch (device->type) {
|
||||
case rawinput::MIDI: {
|
||||
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;
|
||||
|
||||
// read control
|
||||
if (vKey < 16 * 128) {
|
||||
velocity = (float) device->midiInfo->velocity[vKey] / 127.f;
|
||||
} else {
|
||||
velocity = 0.f;
|
||||
}
|
||||
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;
|
||||
|
||||
// invert
|
||||
if (button.getInvert()) {
|
||||
velocity = 1.f - velocity;
|
||||
}
|
||||
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;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
// unlock device
|
||||
@@ -490,29 +620,82 @@ float GameAPI::Analogs::getState(rawinput::Device *device, Analog &analog) {
|
||||
value = device->hidInfo->value_states[index];
|
||||
}
|
||||
|
||||
// smoothing/sensitivity
|
||||
if (analog.getSmoothing() || analog.isSensitivitySet()) {
|
||||
float rads = value * (float) M_TAU;
|
||||
// deadzone
|
||||
if (analog.isDeadzoneSet()) {
|
||||
value = analog.applyDeadzone(value);
|
||||
}
|
||||
|
||||
// smoothing
|
||||
if (analog.getSmoothing()) {
|
||||
if (analog.isRelativeMode()) {
|
||||
float relative_delta = value - 0.5f;
|
||||
// built-in scaling to make values reasonable
|
||||
relative_delta /= 80.f;
|
||||
|
||||
// preserve direction
|
||||
if (rads >= M_TAU) {
|
||||
rads -= 0.0001f;
|
||||
// integer multiplier/divisor
|
||||
const auto mult = analog.getMultiplier();
|
||||
if (mult < -1) {
|
||||
relative_delta /= -mult;
|
||||
} else if (1 < mult) {
|
||||
relative_delta *= mult;
|
||||
}
|
||||
|
||||
// sensitivity (ranges from 0.0 to 4.0)
|
||||
if (analog.isSensitivitySet()) {
|
||||
relative_delta *= analog.getSensitivity();
|
||||
}
|
||||
|
||||
// translate relative movement to absolute value
|
||||
value = analog.getAbsoluteValue(relative_delta);
|
||||
|
||||
} 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);
|
||||
}
|
||||
|
||||
// calculate angle
|
||||
rads = analog.getSmoothedValue(rads);
|
||||
// sensitivity
|
||||
if (analog.isSensitivitySet()) {
|
||||
rads = analog.applyAngularSensitivity(rads);
|
||||
}
|
||||
|
||||
// apply to value
|
||||
value = rads * (float) M_1_TAU;
|
||||
}
|
||||
}
|
||||
|
||||
// delay
|
||||
if (0 < analog.getDelayBufferDepth()) {
|
||||
auto& queue = analog.getDelayBuffer();
|
||||
|
||||
// ensure the queue isn't too long; drop old values
|
||||
while (analog.getDelayBufferDepth() <= (int)queue.size()) {
|
||||
queue.pop();
|
||||
}
|
||||
|
||||
// sensitivity
|
||||
if (analog.isSensitivitySet()) {
|
||||
rads = analog.applyAngularSensitivity(rads);
|
||||
}
|
||||
// always push new value
|
||||
queue.push(value);
|
||||
|
||||
// apply to value
|
||||
value = rads * (float) M_1_TAU;
|
||||
// get a new value to return
|
||||
if ((int)queue.size() < analog.getDelayBufferDepth()) {
|
||||
// not enough in the queue, stall for now, shouldn't happen often
|
||||
value = analog.getLastState();
|
||||
} else {
|
||||
value = queue.front();
|
||||
queue.pop();
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
@@ -524,6 +707,7 @@ float GameAPI::Analogs::getState(rawinput::Device *device, Analog &analog) {
|
||||
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)
|
||||
@@ -532,81 +716,19 @@ float GameAPI::Analogs::getState(rawinput::Device *device, Analog &analog) {
|
||||
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)
|
||||
value = midi->pitch_bend / 16383.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;
|
||||
}
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
// deadzone logic
|
||||
switch (device->type) {
|
||||
case rawinput::HID:
|
||||
case rawinput::MIDI: {
|
||||
|
||||
// check if set
|
||||
// deadzone
|
||||
if (analog.isDeadzoneSet()) {
|
||||
|
||||
// check sign
|
||||
auto deadzone = analog.getDeadzone();
|
||||
if (deadzone > 0) {
|
||||
|
||||
// calculate values
|
||||
auto delta = value - 0.5f;
|
||||
auto dtlen = 1.f - deadzone;
|
||||
|
||||
// check mirror
|
||||
if (analog.getDeadzoneMirror()) {
|
||||
|
||||
// deadzone on the edges
|
||||
if (dtlen != 0.f) {
|
||||
value = std::max(0.f, std::min(1.f, 0.5f + (delta / dtlen)));
|
||||
} else {
|
||||
value = 0.5f;
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
// deadzone around the middle
|
||||
auto limit = deadzone * 0.5f;
|
||||
if (dtlen != 0.f) {
|
||||
if (delta > limit) {
|
||||
value = std::min(1.f, 0.5f + std::max(0.f, (delta - limit) / dtlen));
|
||||
} else if (delta < -limit) {
|
||||
value = std::max(0.f, 0.5f + std::min(0.f, (delta + limit) / dtlen));
|
||||
} else {
|
||||
value = 0.5f;
|
||||
}
|
||||
} else {
|
||||
value = 0.5f;
|
||||
}
|
||||
}
|
||||
|
||||
} else if (deadzone < 0) {
|
||||
|
||||
// invert for mirror
|
||||
if (analog.getDeadzoneMirror()) {
|
||||
value = 1.f - value;
|
||||
}
|
||||
|
||||
// deadzone from minimum value
|
||||
if (deadzone > -1 && value > -deadzone) {
|
||||
value = std::min(1.f, (value + deadzone) / (1.f + deadzone));
|
||||
} else {
|
||||
value = 0.f;
|
||||
}
|
||||
|
||||
// revert value for mirror
|
||||
if (analog.getDeadzoneMirror()) {
|
||||
value = 1.f - value;
|
||||
}
|
||||
}
|
||||
value = analog.applyDeadzone(value);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
@@ -761,6 +883,7 @@ void GameAPI::Lights::writeLight(rawinput::Device *device, int index, float valu
|
||||
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);
|
||||
}
|
||||
@@ -769,11 +892,30 @@ void GameAPI::Lights::writeLight(rawinput::Device *device, int index, float valu
|
||||
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;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -922,3 +1064,27 @@ void GameAPI::Options::sortOptions(std::vector<Option> &options, const std::vect
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user