iidx, sdvx: SOCD cleaner for knobs and turntables (#497)

## Link to GitHub Issue, if one exists
n/a

## Description of change
Apply SOCD cleaner algorithm to deal with the case where both
counter-clockwise and clockwise buttons are pressed at the same time.

In SDVX, default algorithm is to prefer the most recently pressed
direction. This is better for dealing with slams in rapid succession,
for example, as you don't have to completely let go of one direction to
hit the other now.

In IIDX, default algorithm is neutral (same behavior as before). Using
last or first algorithm results in double scratches which means you'll
likely end up with an excessive poor after hitting a scratch, so neutral
is actually beneficial. Besides, if you are serious about playing on the
keyboard, you should be using `TT +/-` and `TT +/- Alternate`.

## Testing
Tested SDVX 3, EG (old cab), EG valk cab modes, IIDX 24, 27, 33
(tdj/ldj)
This commit is contained in:
bicarus
2026-01-04 19:50:11 -08:00
committed by GitHub
parent 7f281a50c5
commit 7b903d35e2
10 changed files with 262 additions and 24 deletions
+1
View File
@@ -624,6 +624,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
util/execexe.cpp util/execexe.cpp
util/dependencies.cpp util/dependencies.cpp
util/deferlog.cpp util/deferlog.cpp
util/socd_cleaner.cpp
) )
source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} PREFIX "Source Files" FILES ${SOURCE_FILES}) source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} PREFIX "Source Files" FILES ${SOURCE_FILES})
+30 -6
View File
@@ -9,11 +9,22 @@
#include "games/drs/drs.h" #include "games/drs/drs.h"
#include "misc/eamuse.h" #include "misc/eamuse.h"
#include "util/logging.h" #include "util/logging.h"
#include "util/socd_cleaner.h"
#include "util/time.h"
#include "util/utils.h" #include "util/utils.h"
#include "util/tapeled.h" #include "util/tapeled.h"
using namespace GameAPI; using namespace GameAPI;
#define DEBUG_VERBOSE 0
#if DEBUG_VERBOSE
#define log_debug(module, format_str, ...) logger::push( \
LOG_FORMAT("M", module, format_str, ## __VA_ARGS__), logger::Style::GREY)
#else
#define log_debug(module, format_str, ...)
#endif
// state // state
static uint8_t STATUS_BUFFER[272] {}; static uint8_t STATUS_BUFFER[272] {};
static bool STATUS_BUFFER_FREEZE = false; static bool STATUS_BUFFER_FREEZE = false;
@@ -94,18 +105,25 @@ static bool __cdecl ac_io_bi2a_update_control_status_buffer() {
} }
// volume left // volume left
if (Buttons::getState(RI_MGR, buttons.at(games::sdvx::Buttons::VOL_L_Left))) { const auto now = get_performance_milliseconds();
const auto vol_l_state = socd::socd_clean(0,
Buttons::getState(RI_MGR, buttons.at(games::sdvx::Buttons::VOL_L_Left)),
Buttons::getState(RI_MGR, buttons.at(games::sdvx::Buttons::VOL_L_Right)),
now);
if (vol_l_state == socd::SocdCCW) {
BI2A_VOLL = (BI2A_VOLL - games::sdvx::DIGITAL_KNOB_SENS) & 1023; BI2A_VOLL = (BI2A_VOLL - games::sdvx::DIGITAL_KNOB_SENS) & 1023;
} } else if (vol_l_state == socd::SocdCW) {
if (Buttons::getState(RI_MGR, buttons.at(games::sdvx::Buttons::VOL_L_Right))) {
BI2A_VOLL = (BI2A_VOLL + games::sdvx::DIGITAL_KNOB_SENS) & 1023; BI2A_VOLL = (BI2A_VOLL + games::sdvx::DIGITAL_KNOB_SENS) & 1023;
} }
// volume right // volume right
if (Buttons::getState(RI_MGR, buttons.at(games::sdvx::Buttons::VOL_R_Left))) { const auto vol_r_state = socd::socd_clean(1,
Buttons::getState(RI_MGR, buttons.at(games::sdvx::Buttons::VOL_R_Left)),
Buttons::getState(RI_MGR, buttons.at(games::sdvx::Buttons::VOL_R_Right)),
now);
if (vol_r_state == socd::SocdCCW) {
BI2A_VOLR = (BI2A_VOLR - games::sdvx::DIGITAL_KNOB_SENS) & 1023; BI2A_VOLR = (BI2A_VOLR - games::sdvx::DIGITAL_KNOB_SENS) & 1023;
} } else if (vol_r_state == socd::SocdCW) {
if (Buttons::getState(RI_MGR, buttons.at(games::sdvx::Buttons::VOL_R_Right))) {
BI2A_VOLR = (BI2A_VOLR + games::sdvx::DIGITAL_KNOB_SENS) & 1023; BI2A_VOLR = (BI2A_VOLR + games::sdvx::DIGITAL_KNOB_SENS) & 1023;
} }
@@ -127,6 +145,12 @@ static bool __cdecl ac_io_bi2a_update_control_status_buffer() {
// save volumes in buffer // save volumes in buffer
*((uint16_t*) &STATUS_BUFFER[17]) = (uint16_t) ((vol_left) << 2); *((uint16_t*) &STATUS_BUFFER[17]) = (uint16_t) ((vol_left) << 2);
*((uint16_t*) &STATUS_BUFFER[19]) = (uint16_t) ((vol_right) << 2); *((uint16_t*) &STATUS_BUFFER[19]) = (uint16_t) ((vol_right) << 2);
log_debug(
"bi2a",
"knobs = {} {}",
*((uint16_t*) &STATUS_BUFFER[17]),
*((uint16_t*) &STATUS_BUFFER[19]));
} }
// DanceDanceRevolution // DanceDanceRevolution
+26 -6
View File
@@ -8,10 +8,21 @@
#include "games/sdvx/io.h" #include "games/sdvx/io.h"
#include "misc/eamuse.h" #include "misc/eamuse.h"
#include "rawinput/rawinput.h" #include "rawinput/rawinput.h"
#include "util/socd_cleaner.h"
#include "util/time.h"
#include "util/utils.h" #include "util/utils.h"
using namespace GameAPI; using namespace GameAPI;
#define DEBUG_VERBOSE 0
#if DEBUG_VERBOSE
#define log_debug(module, format_str, ...) logger::push( \
LOG_FORMAT("M", module, format_str, ## __VA_ARGS__), logger::Style::GREY)
#else
#define log_debug(module, format_str, ...)
#endif
// globals // globals
uint8_t KFCA_VOL_SOUND = 96; uint8_t KFCA_VOL_SOUND = 96;
uint8_t KFCA_VOL_HEADPHONE = 96; uint8_t KFCA_VOL_HEADPHONE = 96;
@@ -342,18 +353,25 @@ static char __cdecl ac_io_kfca_update_control_status_buffer() {
} }
// volume left // volume left
if (Buttons::getState(RI_MGR, buttons.at(games::sdvx::Buttons::VOL_L_Left))) { const auto now = get_performance_milliseconds();
const auto vol_l_state = socd::socd_clean(0,
Buttons::getState(RI_MGR, buttons.at(games::sdvx::Buttons::VOL_L_Left)),
Buttons::getState(RI_MGR, buttons.at(games::sdvx::Buttons::VOL_L_Right)),
now);
if (vol_l_state == socd::SocdCCW) {
KFCA_VOLL = (KFCA_VOLL - games::sdvx::DIGITAL_KNOB_SENS) & 1023; KFCA_VOLL = (KFCA_VOLL - games::sdvx::DIGITAL_KNOB_SENS) & 1023;
} } else if (vol_l_state == socd::SocdCW) {
if (Buttons::getState(RI_MGR, buttons.at(games::sdvx::Buttons::VOL_L_Right))) {
KFCA_VOLL = (KFCA_VOLL + games::sdvx::DIGITAL_KNOB_SENS) & 1023; KFCA_VOLL = (KFCA_VOLL + games::sdvx::DIGITAL_KNOB_SENS) & 1023;
} }
// volume right // volume right
if (Buttons::getState(RI_MGR, buttons.at(games::sdvx::Buttons::VOL_R_Left))) { const auto vol_r_state = socd::socd_clean(1,
Buttons::getState(RI_MGR, buttons.at(games::sdvx::Buttons::VOL_R_Left)),
Buttons::getState(RI_MGR, buttons.at(games::sdvx::Buttons::VOL_R_Right)),
now);
if (vol_r_state == socd::SocdCCW) {
KFCA_VOLR = (KFCA_VOLR - games::sdvx::DIGITAL_KNOB_SENS) & 1023; KFCA_VOLR = (KFCA_VOLR - games::sdvx::DIGITAL_KNOB_SENS) & 1023;
} } else if (vol_r_state == socd::SocdCW) {
if (Buttons::getState(RI_MGR, buttons.at(games::sdvx::Buttons::VOL_R_Right))) {
KFCA_VOLR = (KFCA_VOLR + games::sdvx::DIGITAL_KNOB_SENS) & 1023; KFCA_VOLR = (KFCA_VOLR + games::sdvx::DIGITAL_KNOB_SENS) & 1023;
} }
@@ -372,6 +390,8 @@ static char __cdecl ac_io_kfca_update_control_status_buffer() {
vol_left %= 1024; vol_left %= 1024;
vol_right %= 1024; vol_right %= 1024;
log_debug("kfca", "knobs = {} {}", vol_left, vol_right);
// save volumes in buffer // save volumes in buffer
STATUS_BUFFER[input_offset + 16 + 0] |= (unsigned char) ((vol_left << 6) & 0xFF); STATUS_BUFFER[input_offset + 16 + 0] |= (unsigned char) ((vol_left << 6) & 0xFF);
STATUS_BUFFER[input_offset + 16 + 1] |= (unsigned char) ((vol_left >> 2) & 0xFF); STATUS_BUFFER[input_offset + 16 + 1] |= (unsigned char) ((vol_left >> 2) & 0xFF);
+16 -5
View File
@@ -29,6 +29,8 @@
#include "util/libutils.h" #include "util/libutils.h"
#include "util/memutils.h" #include "util/memutils.h"
#include "util/sigscan.h" #include "util/sigscan.h"
#include "util/socd_cleaner.h"
#include "util/time.h"
#include "util/utils.h" #include "util/utils.h"
#include "launcher/signal.h" #include "launcher/signal.h"
@@ -507,6 +509,16 @@ namespace games::iidx {
" monitor in Windows settings before launching the game" " monitor in Windows settings before launching the game"
}); });
} }
// socd
socd::ALGORITHM = socd::SocdAlgorithm::Neutral;
if (options->at(launcher::Options::IIDXDigitalTTSocd).is_active()) {
if (options->at(launcher::Options::IIDXDigitalTTSocd).value_text() == "last") {
socd::ALGORITHM = socd::SocdAlgorithm::PreferRecent;
} else if (options->at(launcher::Options::IIDXDigitalTTSocd).value_text() == "first") {
socd::ALGORITHM = socd::SocdAlgorithm::PreferFirst;
}
}
} }
void IIDXGame::detach() { void IIDXGame::detach() {
@@ -730,13 +742,12 @@ namespace games::iidx {
bool ttpm_alt = GameAPI::Buttons::getState(RI_MGR, buttons.at( bool ttpm_alt = GameAPI::Buttons::getState(RI_MGR, buttons.at(
player != 0 ? Buttons::P2_TTPlusMinusAlt : Buttons::P1_TTPlusMinusAlt)); player != 0 ? Buttons::P2_TTPlusMinusAlt : Buttons::P1_TTPlusMinusAlt));
// TT+ const auto tt_socd = socd::socd_clean(player, ttm, ttp, get_performance_milliseconds());
if (ttp) if (tt_socd == socd::SocdCW) {
IIDXIO_TT_STATE[player] += change; IIDXIO_TT_STATE[player] += change;
} else if (tt_socd == socd::SocdCCW) {
// TT-
if (ttm)
IIDXIO_TT_STATE[player] -= change; IIDXIO_TT_STATE[player] -= change;
}
// TT+/- // TT+/-
bool ttpm_rising_edge = !IIDXIO_TT_PRESSED[player] && ttpm; bool ttpm_rising_edge = !IIDXIO_TT_PRESSED[player] && ttpm;
+31 -6
View File
@@ -12,9 +12,20 @@
#include "launcher/options.h" #include "launcher/options.h"
#include "io.h" #include "io.h"
#include "games/sdvx/sdvx.h" #include "games/sdvx/sdvx.h"
#include "util/socd_cleaner.h"
#include "util/tapeled.h" #include "util/tapeled.h"
#include "util/time.h"
#include "acioemu/icca.h" #include "acioemu/icca.h"
#define DEBUG_VERBOSE 0
#if DEBUG_VERBOSE
#define log_debug(module, format_str, ...) logger::push( \
LOG_FORMAT("M", module, format_str, ## __VA_ARGS__), logger::Style::GREY)
#else
#define log_debug(module, format_str, ...)
#endif
namespace games::sdvx { namespace games::sdvx {
constexpr bool BI2X_PASSTHROUGH = false; constexpr bool BI2X_PASSTHROUGH = false;
@@ -176,19 +187,27 @@ namespace games::sdvx {
if (GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::Headphone])) if (GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::Headphone]))
status->buffer[22] = 0x01; status->buffer[22] = 0x01;
const auto now = get_performance_milliseconds();
// volume left // volume left
if (GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::VOL_L_Left])) { const auto vol_l_state = socd::socd_clean(0,
GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::VOL_L_Left]),
GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::VOL_L_Right]),
now);
if (vol_l_state == socd::SocdCCW) {
VOL_L -= ((uint16_t)DIGITAL_KNOB_SENS * 4); VOL_L -= ((uint16_t)DIGITAL_KNOB_SENS * 4);
} } else if (vol_l_state == socd::SocdCW) {
if (GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::VOL_L_Right])) {
VOL_L += ((uint16_t)DIGITAL_KNOB_SENS * 4); VOL_L += ((uint16_t)DIGITAL_KNOB_SENS * 4);
} }
// volume right // volume right
if (GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::VOL_R_Left])) { const auto vol_r_state = socd::socd_clean(1,
GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::VOL_R_Left]),
GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::VOL_R_Right]),
now);
if (vol_r_state == socd::SocdCCW) {
VOL_R -= ((uint16_t)DIGITAL_KNOB_SENS * 4); VOL_R -= ((uint16_t)DIGITAL_KNOB_SENS * 4);
} } else if (vol_r_state == socd::SocdCW) {
if (GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::VOL_R_Right])) {
VOL_R += ((uint16_t)DIGITAL_KNOB_SENS * 4); VOL_R += ((uint16_t)DIGITAL_KNOB_SENS * 4);
} }
@@ -203,6 +222,12 @@ namespace games::sdvx {
*((uint16_t*) &status->buffer[312]) = vol_left; *((uint16_t*) &status->buffer[312]) = vol_left;
*((uint16_t*) &status->buffer[314]) = vol_right; *((uint16_t*) &status->buffer[314]) = vol_right;
log_debug(
"bi2x_hook",
"knobs = {} {}",
*((uint16_t*) &status->buffer[312]),
*((uint16_t*) &status->buffer[314]));
} }
static void __fastcall AIO_IOB2_BI2X_UFC__IoReset(AIO_IOB2_BI2X_UFC *This, static void __fastcall AIO_IOB2_BI2X_UFC__IoReset(AIO_IOB2_BI2X_UFC *This,
+12
View File
@@ -18,6 +18,8 @@
#include "util/detour.h" #include "util/detour.h"
#include "util/logging.h" #include "util/logging.h"
#include "util/sigscan.h" #include "util/sigscan.h"
#include "util/socd_cleaner.h"
#include "util/time.h"
#include "util/libutils.h" #include "util/libutils.h"
#include "misc/wintouchemu.h" #include "misc/wintouchemu.h"
#include "misc/eamuse.h" #include "misc/eamuse.h"
@@ -360,6 +362,16 @@ namespace games::sdvx {
this->detect_sound_output_device(); this->detect_sound_output_device();
#endif #endif
// SOCD
auto options = games::get_options(eamuse_get_game());
socd::ALGORITHM = socd::SocdAlgorithm::PreferRecent;
if (options->at(launcher::Options::SDVXDigitalKnobSocd).is_active()) {
if (options->at(launcher::Options::SDVXDigitalKnobSocd).value_text() == "neutral") {
socd::ALGORITHM = socd::SocdAlgorithm::Neutral;
} else if (options->at(launcher::Options::SDVXDigitalKnobSocd).value_text() == "last") {
socd::ALGORITHM = socd::SocdAlgorithm::PreferRecent;
}
}
} }
void SDVXGame::attach() { void SDVXGame::attach() {
+35 -1
View File
@@ -604,7 +604,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
{ {
.title = "IIDX TDJ Mode (Lightning Model)", .title = "IIDX TDJ Mode (Lightning Model)",
.name = "iidxtdj", .name = "iidxtdj",
.desc = "Enables TDJ cabinet mode. Ensure you also set -iidxsounddevice to desired option", .desc = "Enables TDJ mode (Lightning Model cabinet).",
.type = OptionType::Bool, .type = OptionType::Bool,
.game_name = "Beatmania IIDX", .game_name = "Beatmania IIDX",
.category = "Game Options", .category = "Game Options",
@@ -621,6 +621,23 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
.game_name = "Beatmania IIDX", .game_name = "Beatmania IIDX",
.category = "Game Options (Advanced)", .category = "Game Options (Advanced)",
}, },
{
// IIDXDigitalTTSocd
.title = "IIDX Digital TT SOCD Cleaner",
.name = "iidxsocd",
.desc = "SOCD for turntables when using button input; what happens when both directions are pressed.\n\n"
"last: most recently pressed direction takes priority\n\n"
"first: first pressed direction takes priority\n\n"
"neutral (default): TT does not move when both directions are pressed, recommended to avoid excessive POORs",
.type = OptionType::Enum,
.game_name = "Beatmania IIDX",
.category = "Game Options (Advanced)",
.elements = {
{"last", ""},
{"first", ""},
{"neutral", ""},
},
},
{ {
// spice2x_IIDXLDJForce720p // spice2x_IIDXLDJForce720p
.title = "IIDX LDJ Force 720p (HD)", .title = "IIDX LDJ Force 720p (HD)",
@@ -804,6 +821,23 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
.game_name = "Sound Voltex", .game_name = "Sound Voltex",
.category = "Game Options (Advanced)", .category = "Game Options (Advanced)",
}, },
{
// SDVXDigitalKnobSocd
.title = "SDVX Digital Knob SOCD Cleaner",
.name = "sdvxsocd",
.desc = "SOCD for knobs when using button input; what happens when both directions are pressed.\n\n"
"last (default): most recently pressed direction takes priority, recommended to deal with slams\n\n"
"first: first pressed direction takes priority\n\n"
"neutral: knob does not move when both directions are pressed",
.type = OptionType::Enum,
.game_name = "Sound Voltex",
.category = "Game Options (Advanced)",
.elements = {
{"last", ""},
{"first", ""},
{"neutral", ""},
},
},
{ {
// spice2x_SDVXAsioDriver // spice2x_SDVXAsioDriver
.title = "SDVX ASIO driver", .title = "SDVX ASIO driver",
+2
View File
@@ -67,6 +67,7 @@ namespace launcher {
IIDXBIO2FW, IIDXBIO2FW,
IIDXTDJMode, IIDXTDJMode,
spice2x_IIDXDigitalTTSensitivity, spice2x_IIDXDigitalTTSensitivity,
IIDXDigitalTTSocd,
spice2x_IIDXLDJForce720p, spice2x_IIDXLDJForce720p,
spice2x_IIDXTDJSubSize, spice2x_IIDXTDJSubSize,
spice2x_IIDXLEDFontSize, spice2x_IIDXLEDFontSize,
@@ -84,6 +85,7 @@ namespace launcher {
SDVXDisableCameras, SDVXDisableCameras,
SDVXNativeTouch, SDVXNativeTouch,
spice2x_SDVXDigitalKnobSensitivity, spice2x_SDVXDigitalKnobSensitivity,
SDVXDigitalKnobSocd,
spice2x_SDVXAsioDriver, spice2x_SDVXAsioDriver,
spice2x_SDVXSubPos, spice2x_SDVXSubPos,
spice2x_SDVXSubRedraw, spice2x_SDVXSubRedraw,
+87
View File
@@ -0,0 +1,87 @@
#include "socd_cleaner.h"
#include "util/logging.h"
#define DEBUG_VERBOSE 0
#if DEBUG_VERBOSE
#define log_debug(module, format_str, ...) logger::push( \
LOG_FORMAT("M", module, format_str, ## __VA_ARGS__), logger::Style::GREY)
#else
#define log_debug(module, format_str, ...)
#endif
namespace socd {
SocdAlgorithm ALGORITHM = SocdAlgorithm::Neutral;
static double last_rising_edge[2][2] = {};
static bool last_button_state[2][2] = {};
// this has no locking, use only if you know there is only one i/o thread that will call this
SocdResult socd_clean(uint8_t device, bool ccw, bool cw, double time_now) {
if (device >= 2) {
log_fatal("socd", "invalid device index in socd_clean: {}", device);
}
// SOCD last input algorithm needs to keep track of rising edge times
if (ALGORITHM != SocdAlgorithm::Neutral) {
// detect rising edges
if (!last_button_state[device][SocdCCW] && ccw) {
last_rising_edge[device][SocdCCW] = time_now;
}
if (!last_button_state[device][SocdCW] && cw) {
last_rising_edge[device][SocdCW] = time_now;
}
// update button state for next time
last_button_state[device][SocdCCW] = ccw;
last_button_state[device][SocdCW] = cw;
}
// determine direction: easy cases
if (!ccw && !cw) {
return SocdNone;
}
if (ccw && !cw) {
return SocdCCW;
}
if (!ccw && cw) {
return SocdCW;
}
// SOCD logic; depends on algorithm in use
const auto ccw_time = last_rising_edge[device][SocdCCW];
const auto cw_time = last_rising_edge[device][SocdCW];
log_debug("socd", "ccw={}, cw ={}", ccw_time, cw_time);
if (ALGORITHM == SocdAlgorithm::PreferRecent) {
// SOCD: prefer last input
if (ccw_time < cw_time) {
// while CCW is being held, CW got pressed
return SocdCW;
} else if (ccw_time > cw_time) {
// while CW is being held, CCW got pressed
return SocdCCW;
} else {
// it's a tie; instead of none, we'll pick a direction
return SocdCW;
}
} else if (ALGORITHM == SocdAlgorithm::PreferFirst) {
// SOCD: keep first input
if (ccw_time < cw_time) {
// while CCW is being held, CW got pressed
return SocdCCW;
} else if (ccw_time > cw_time) {
// while CW is being held, CCW got pressed
return SocdCW;
} else {
// it's a tie; instead of none, we'll pick a direction
return SocdCW;
}
} else {
// SOCD: neutral when both are pressed
return SocdNone;
}
}
}
+22
View File
@@ -0,0 +1,22 @@
#pragma once
#include <cstdint>
namespace socd {
enum class SocdAlgorithm {
Neutral,
PreferRecent,
PreferFirst
};
extern SocdAlgorithm ALGORITHM;
typedef enum _SocdResult {
SocdCCW = 0,
SocdCW = 1,
SocdNone = 2
} SocdResult;
SocdResult socd_clean(uint8_t device, bool ccw, bool cw, double time_now);
}