flycast/core/hw/naomi/midiffb.cpp

329 lines
8.5 KiB
C++

/*
Copyright 2025 flyinghead
This file is part of Flycast.
Flycast is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
Flycast is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Flycast. If not, see <https://www.gnu.org/licenses/>.
*/
#include "midiffb.h"
#include "naomi_cart.h"
#include "hw/aica/aica_if.h"
#include "hw/maple/maple_cfg.h"
#include "serialize.h"
#include "input/haptic.h"
#include "oslib/oslib.h"
#include "network/output.h"
namespace midiffb {
static bool initialized;
static u8 midiTxBuf[4];
static u32 midiTxBufIndex;
static bool calibrating;
static float damperParam;
static float damperSpeed;
static float power = 0.8f;
static bool active;
static float position = 8192.f;
static float torque;
static float springForce;
static u8 maxSpring = 0x7f;
static void midiSend(u8 b1, u8 b2, u8 b3)
{
aica::midiSend(b1);
aica::midiSend(b2);
aica::midiSend(b3);
aica::midiSend((b1 ^ b2 ^ b3) & 0x7f);
}
// Thanks to njz3 and boomslangnz for their help
// https://www.arcade-projects.com/threads/force-feedback-translator-sega-midi-sega-rs422-and-namco-rs232.924/
static void midiReceiver(u8 data)
{
// fake position used during calibration
position = std::min(16383.f, std::max(0.f, position + torque));
if (data & 0x80)
midiTxBufIndex = 0;
midiTxBuf[midiTxBufIndex] = data;
if (midiTxBufIndex == 3 && ((midiTxBuf[0] ^ midiTxBuf[1] ^ midiTxBuf[2]) & 0x7f) == midiTxBuf[3])
{
const u8 cmd = midiTxBuf[0] & 0x7f;
switch (cmd)
{
case 0:
// FFB on/off
// b1 & 1 => Temporary (a few 10th of a second) else permanently
// b2 & 1 => FFB enabled
if (midiTxBuf[2] == 0)
{
active = false;
haptic::stopAll(0);
if (calibrating) {
calibrating = false;
os_notify("Calibration done", 2000);
}
}
else if (midiTxBuf[2] == 1)
{
active = true;
haptic::setDamper(0, damperSpeed * power, damperParam);
haptic::setSpring(0, springForce * power, 1.f);
}
break;
case 1:
// Set force
// b1: max torque forward (centering force)
// b2: max torque backward (anti-centering force or friction)
// Ex: 30 40 then 7f 40 (sgdrvsim search base pos)
// 30 40 (*2 initdv3e init, clubk after init)
// 7f 7f (kingrt init)
// 1f 1f kingrt test menu
maxSpring = midiTxBuf[1];
break;
case 2:
// Unknown, seems to be used to enable something, usually follows 1 or 6
// b1: 00 or 04 or 1F or 7F
// b2: 54
// Ex: 00 54 (sgdrvsim search base pos, kingrt)
// 7f 54 (*2 initdv3e init)
// 04 54 (clubk after init)
break;
case 3:
// Drive power
// b1: power level [0-7f]
// b2: ? initdv3, clubk2k3: 4, kingrt: 0, sgdrvsim: 28
power = (midiTxBuf[1] >> 3) / 15.f;
break;
case 4:
// Torque
// from 0 (max torque to the right) to 17f (max torque to the left)
torque = ((midiTxBuf[1] << 7) | midiTxBuf[2]) - 0x80;
if (active && !calibrating)
haptic::setTorque(0, torque / 128.f * power);
break;
case 5:
// Rumble
// b1: frequency in half Hz
// b2: amplitude
// Examples:
// * 02 40: large sine of 1Hz
// * 0A 20: smaller sine of 5 Hz
// decoding from FFB Arcade Plugin (by Boomslangnz)
// https://github.com/Boomslangnz/FFBArcadePlugin/blob/master/Game%20Files/Demul.cpp
if (active)
{
const float intensity = std::clamp((float)(midiTxBuf[2] - 1) / 80.f * power, 0.f, 1.f);
const float freq = midiTxBuf[1] / 2.f;
haptic::setSine(0, intensity, freq, 1000);
}
break;
case 6:
// Damper effect expressed as a ratio param1/param2, or a pole of
// a transfer function.
// Examples:
// * 10 7F: strong damper
// * 40 40: light damper
// * XX 00: disabled
damperSpeed = midiTxBuf[1] / 127.f;
damperParam = midiTxBuf[2] / 127.f;
// clubkart sets it to 28 40 in menus, and 04 12 when in game (not changing in between)
// initdv3 starts with 02 2c // FIXME no effect with sat=2
// changes it in-game: 02 11-2c
// finish line(?): 02 5a
// ends with 00 00
// kingrt66: 60 0a (start), 40 0a (in game)
// sgdrvsim: 08 20 when calibrating center
// 18 40 init
// 28 nn in menus (nn is viscoSpeed<<6 >>8 from table: 20,28,30,38,...,98)
// 1e+n 0+m in game (n and m are set in test menu, default 1e, 0))
// also: 8+n 0a+m and 0+n 3c+m
if (active && !calibrating)
haptic::setDamper(0, damperSpeed * power, damperParam);
break;
// 07 nn nn: Spring angle offset from center.
// b1: 00 right, 01 left
// b2: offset [0-7f] max 90 deg
// 08: Spring effect inverted gain or limit?
// 09 00 00: kingrt init
// 03 40: end init
// 0A 10 58: kingrt end init
case 0xB:
// Spring force
// b1: force
// b2: 00
// sgdrvsim: 20 10 in menus, else 00 00. Also [0-7f] 7f
// kingrt: 1b 00 (boot)
// clubk2k: 00 00 (boot) 20 10 (menus) 00 00 (drive)
springForce = std::min(midiTxBuf[1], maxSpring) / 127.f;
if (active && !calibrating)
haptic::setSpring(0, springForce * power, 1.f);
break;
// 70 00 00: Set wheel center
// kingrt init (before 7f & 7a), kingrt test menu (after 7f)
// 7A 00 nn: Set reply mode
// 10: long status
// 14: long status + encoder feedback (club2k, drvsimm)
// 1f: short status
// 00 10,14: clubk,initv3e,kingrt init/test menu
// 7C 00 nn Start phase alignment. From 0 (disabled) to 7f (very strong)
// 00 3f: initdv3e init
// 20: clubk init
// 30: kingrt init
// 7D 00 00: Query status
case 0x7f:
// Reset board
// FIXME also set when entering the service menu (kingrt66)
os_notify("Calibrating the wheel. Keep it centered.", 10000);
calibrating = true;
haptic::setSpring(0, 0.8f, 1.f);
position = 8192.f;
break;
}
if (!calibrating)
{
int direction = -1;
if (NaomiGameInputs != nullptr)
direction = NaomiGameInputs->axes[0].inverted ? 1 : -1;
position = std::clamp(mapleInputState[0].fullAxes[0] * direction / 4.f + 8192.f, 0.f, 16383.f);
}
// required: b1 & 0x1f == 0x10 && b1 & 0x40 == 0
midiSend(0x90, ((int)position >> 7) & 0x7f, (int)position & 0x7f);
if (cmd != 0x7d) {
networkOutput.output("midiffb", (midiTxBuf[0] << 16) | (midiTxBuf[1]) << 8 | midiTxBuf[2]);
DEBUG_LOG(NAOMI, "midiFFB: %02x %02x %02x", cmd, midiTxBuf[1], midiTxBuf[2]);
}
}
midiTxBufIndex = (midiTxBufIndex + 1) % std::size(midiTxBuf);
}
void init()
{
aica::setMidiReceiver(midiReceiver);
initialized = true;
reset();
}
void reset()
{
active = false;
calibrating = false;
midiTxBufIndex = 0;
power = 0.8f;
damperParam = 0.f;
damperSpeed = 0.f;
torque = 0.f;
}
void term()
{
aica::setMidiReceiver(nullptr);
initialized = false;
}
void serialize(Serializer& ser)
{
if (initialized)
{
ser << midiTxBuf;
ser << midiTxBufIndex;
ser << calibrating;
ser << active;
ser << power;
ser << damperParam;
ser << damperSpeed;
ser << position;
ser << torque;
ser << maxSpring;
ser << springForce;
}
}
void deserialize(Deserializer& deser)
{
if (deser.version() >= Deserializer::V27)
{
if (initialized) {
deser >> midiTxBuf;
deser >> midiTxBufIndex;
}
else if (deser.version() < Deserializer::V51) {
deser.skip(4); // midiTxBuf
deser.skip<u32>(); // midiTxBufIndex
}
}
else {
midiTxBufIndex = 0;
}
if (deser.version() >= Deserializer::V34)
{
if (initialized)
deser >> calibrating;
else if (deser.version() < Deserializer::V51)
deser.skip<bool>(); // calibrating
}
else {
calibrating = false;
}
if (initialized)
{
maxSpring = 0x7f;
springForce = 0;
if (deser.version() >= Deserializer::V51)
{
deser >> active;
deser >> power;
deser >> damperParam;
deser >> damperSpeed;
deser >> position;
deser >> torque;
if (deser.version() >= Deserializer::V55) {
deser >> maxSpring;
deser >> springForce;
}
if (active && !calibrating) {
haptic::setDamper(0, damperSpeed * power, damperParam);
haptic::setSpring(0, springForce * power, 1.f);
}
}
else
{
active = false;
power = 0.8f;
damperParam = 0.f;
damperSpeed = 0.f;
position = 8192.f;
torque = 0.f;
}
}
}
} // namespace midiffb