mirror of
https://git.sr.ht/~thestr4ng3r/chiaki
synced 2025-07-05 20:42:08 -07:00
gui: Support for DualSense haptics and trigger effects
Haptics with PulseAudio does not seem to be working properly, so using Pipewire as a backend is recommended (and picked by default, if available via an SDL hint).
This commit is contained in:
parent
7a490b5aae
commit
c2f0932670
12 changed files with 343 additions and 71 deletions
|
@ -37,7 +37,7 @@ for:
|
||||||
install:
|
install:
|
||||||
- git submodule update --init --recursive
|
- git submodule update --init --recursive
|
||||||
- sudo pip3 install protobuf
|
- sudo pip3 install protobuf
|
||||||
- brew install qt@5 opus openssl@1.1 nasm sdl2 protobuf
|
- HOMEBREW_NO_AUTO_UPDATE=1 brew install qt@5 opus openssl@1.1 nasm sdl2 protobuf
|
||||||
- scripts/build-ffmpeg.sh
|
- scripts/build-ffmpeg.sh
|
||||||
|
|
||||||
build_script:
|
build_script:
|
||||||
|
|
|
@ -67,12 +67,12 @@ if(CHIAKI_ENABLE_CLI)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
target_link_libraries(chiaki Qt5::Core Qt5::Widgets Qt5::Gui Qt5::Concurrent Qt5::Multimedia Qt5::OpenGL Qt5::Svg)
|
target_link_libraries(chiaki Qt5::Core Qt5::Widgets Qt5::Gui Qt5::Concurrent Qt5::Multimedia Qt5::OpenGL Qt5::Svg)
|
||||||
|
target_link_libraries(chiaki SDL2::SDL2)
|
||||||
if(APPLE)
|
if(APPLE)
|
||||||
target_link_libraries(chiaki Qt5::MacExtras)
|
target_link_libraries(chiaki Qt5::MacExtras)
|
||||||
target_compile_definitions(chiaki PRIVATE CHIAKI_GUI_ENABLE_QT_MACEXTRAS)
|
target_compile_definitions(chiaki PRIVATE CHIAKI_GUI_ENABLE_QT_MACEXTRAS)
|
||||||
endif()
|
endif()
|
||||||
if(CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER)
|
if(CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER)
|
||||||
target_link_libraries(chiaki SDL2::SDL2)
|
|
||||||
target_compile_definitions(chiaki PRIVATE CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER)
|
target_compile_definitions(chiaki PRIVATE CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER)
|
||||||
endif()
|
endif()
|
||||||
if(CHIAKI_ENABLE_SETSU)
|
if(CHIAKI_ENABLE_SETSU)
|
||||||
|
|
|
@ -77,6 +77,7 @@ class Controller : public QObject
|
||||||
int id;
|
int id;
|
||||||
ChiakiOrientationTracker orientation_tracker;
|
ChiakiOrientationTracker orientation_tracker;
|
||||||
ChiakiControllerState state;
|
ChiakiControllerState state;
|
||||||
|
bool is_dualsense;
|
||||||
|
|
||||||
#ifdef CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER
|
#ifdef CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER
|
||||||
QMap<QPair<Sint64, Sint64>, uint8_t> touch_ids;
|
QMap<QPair<Sint64, Sint64>, uint8_t> touch_ids;
|
||||||
|
@ -91,9 +92,43 @@ class Controller : public QObject
|
||||||
QString GetName();
|
QString GetName();
|
||||||
ChiakiControllerState GetState();
|
ChiakiControllerState GetState();
|
||||||
void SetRumble(uint8_t left, uint8_t right);
|
void SetRumble(uint8_t left, uint8_t right);
|
||||||
|
void SetTriggerEffects(uint8_t type_left, const uint8_t *data_left, uint8_t type_right, const uint8_t *data_right);
|
||||||
|
bool IsDualSense();
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void StateChanged();
|
void StateChanged();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/* PS5 trigger effect documentation:
|
||||||
|
https://controllers.fandom.com/wiki/Sony_DualSense#FFB_Trigger_Modes
|
||||||
|
|
||||||
|
Taken from SDL2, licensed under the zlib license,
|
||||||
|
Copyright (C) 1997-2022 Sam Lantinga <slouken@libsdl.org>
|
||||||
|
https://github.com/libsdl-org/SDL/blob/release-2.24.1/test/testgamecontroller.c#L263-L289
|
||||||
|
*/
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
Uint8 ucEnableBits1; /* 0 */
|
||||||
|
Uint8 ucEnableBits2; /* 1 */
|
||||||
|
Uint8 ucRumbleRight; /* 2 */
|
||||||
|
Uint8 ucRumbleLeft; /* 3 */
|
||||||
|
Uint8 ucHeadphoneVolume; /* 4 */
|
||||||
|
Uint8 ucSpeakerVolume; /* 5 */
|
||||||
|
Uint8 ucMicrophoneVolume; /* 6 */
|
||||||
|
Uint8 ucAudioEnableBits; /* 7 */
|
||||||
|
Uint8 ucMicLightMode; /* 8 */
|
||||||
|
Uint8 ucAudioMuteBits; /* 9 */
|
||||||
|
Uint8 rgucRightTriggerEffect[11]; /* 10 */
|
||||||
|
Uint8 rgucLeftTriggerEffect[11]; /* 21 */
|
||||||
|
Uint8 rgucUnknown1[6]; /* 32 */
|
||||||
|
Uint8 ucLedFlags; /* 38 */
|
||||||
|
Uint8 rgucUnknown2[2]; /* 39 */
|
||||||
|
Uint8 ucLedAnim; /* 41 */
|
||||||
|
Uint8 ucLedBrightness; /* 42 */
|
||||||
|
Uint8 ucPadLights; /* 43 */
|
||||||
|
Uint8 ucLedRed; /* 44 */
|
||||||
|
Uint8 ucLedGreen; /* 45 */
|
||||||
|
Uint8 ucLedBlue; /* 46 */
|
||||||
|
} DS5EffectsState_t;
|
||||||
|
|
||||||
#endif // CHIAKI_CONTROLLERMANAGER_H
|
#endif // CHIAKI_CONTROLLERMANAGER_H
|
||||||
|
|
|
@ -63,6 +63,9 @@ class Settings : public QObject
|
||||||
void SetLogVerbose(bool enabled) { settings.setValue("settings/log_verbose", enabled); }
|
void SetLogVerbose(bool enabled) { settings.setValue("settings/log_verbose", enabled); }
|
||||||
uint32_t GetLogLevelMask();
|
uint32_t GetLogLevelMask();
|
||||||
|
|
||||||
|
bool GetDualSenseEnabled() const { return settings.value("settings/dualsense_enabled", false).toBool(); }
|
||||||
|
void SetDualSenseEnabled(bool enabled) { settings.setValue("settings/dualsense_enabled", enabled); }
|
||||||
|
|
||||||
ChiakiVideoResolutionPreset GetResolution() const;
|
ChiakiVideoResolutionPreset GetResolution() const;
|
||||||
void SetResolution(ChiakiVideoResolutionPreset resolution);
|
void SetResolution(ChiakiVideoResolutionPreset resolution);
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,7 @@ class SettingsDialog : public QDialog
|
||||||
|
|
||||||
QCheckBox *log_verbose_check_box;
|
QCheckBox *log_verbose_check_box;
|
||||||
QComboBox *disconnect_action_combo_box;
|
QComboBox *disconnect_action_combo_box;
|
||||||
|
QCheckBox *dualsense_check_box;
|
||||||
|
|
||||||
QComboBox *resolution_combo_box;
|
QComboBox *resolution_combo_box;
|
||||||
QComboBox *fps_combo_box;
|
QComboBox *fps_combo_box;
|
||||||
|
@ -37,6 +38,7 @@ class SettingsDialog : public QDialog
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void LogVerboseChanged();
|
void LogVerboseChanged();
|
||||||
|
void DualSenseChanged();
|
||||||
void DisconnectActionSelected();
|
void DisconnectActionSelected();
|
||||||
|
|
||||||
void ResolutionSelected();
|
void ResolutionSelected();
|
||||||
|
|
|
@ -56,6 +56,7 @@ struct StreamSessionConnectInfo
|
||||||
bool fullscreen;
|
bool fullscreen;
|
||||||
TransformMode transform_mode;
|
TransformMode transform_mode;
|
||||||
bool enable_keyboard;
|
bool enable_keyboard;
|
||||||
|
bool enable_dualsense;
|
||||||
|
|
||||||
StreamSessionConnectInfo(
|
StreamSessionConnectInfo(
|
||||||
Settings *settings,
|
Settings *settings,
|
||||||
|
@ -101,17 +102,23 @@ class StreamSession : public QObject
|
||||||
unsigned int audio_buffer_size;
|
unsigned int audio_buffer_size;
|
||||||
QAudioOutput *audio_output;
|
QAudioOutput *audio_output;
|
||||||
QIODevice *audio_io;
|
QIODevice *audio_io;
|
||||||
|
SDL_AudioDeviceID haptics_output;
|
||||||
|
uint8_t *haptics_resampler_buf;
|
||||||
|
|
||||||
QMap<Qt::Key, int> key_map;
|
QMap<Qt::Key, int> key_map;
|
||||||
|
|
||||||
void PushAudioFrame(int16_t *buf, size_t samples_count);
|
void PushAudioFrame(int16_t *buf, size_t samples_count);
|
||||||
|
void PushHapticsFrame(uint8_t *buf, size_t buf_size);
|
||||||
#if CHIAKI_GUI_ENABLE_SETSU
|
#if CHIAKI_GUI_ENABLE_SETSU
|
||||||
void HandleSetsuEvent(SetsuEvent *event);
|
void HandleSetsuEvent(SetsuEvent *event);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void InitAudio(unsigned int channels, unsigned int rate);
|
void InitAudio(unsigned int channels, unsigned int rate);
|
||||||
|
void InitHaptics();
|
||||||
void Event(ChiakiEvent *event);
|
void Event(ChiakiEvent *event);
|
||||||
|
void DisconnectHaptics();
|
||||||
|
void ConnectHaptics();
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit StreamSession(const StreamSessionConnectInfo &connect_info, QObject *parent = nullptr);
|
explicit StreamSession(const StreamSessionConnectInfo &connect_info, QObject *parent = nullptr);
|
||||||
|
|
|
@ -68,6 +68,12 @@ static QSet<QString> chiaki_motion_controller_guids({
|
||||||
"030000008f0e00001431000000000000",
|
"030000008f0e00001431000000000000",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
static QSet<QPair<int16_t, int16_t>> chiaki_dualsense_controller_ids({
|
||||||
|
// in format (vendor id, product id)
|
||||||
|
QPair<int16_t, int16_t>(0x054c, 0x0ce6), // DualSense controller
|
||||||
|
QPair<int16_t, int16_t>(0x054c, 0x0df2), // DualSense Edge controller
|
||||||
|
});
|
||||||
|
|
||||||
static ControllerManager *instance = nullptr;
|
static ControllerManager *instance = nullptr;
|
||||||
|
|
||||||
#define UPDATE_INTERVAL_MS 4
|
#define UPDATE_INTERVAL_MS 4
|
||||||
|
@ -84,6 +90,15 @@ ControllerManager::ControllerManager(QObject *parent)
|
||||||
{
|
{
|
||||||
#ifdef CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER
|
#ifdef CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER
|
||||||
SDL_SetMainReady();
|
SDL_SetMainReady();
|
||||||
|
#ifdef SDL_HINT_JOYSTICK_HIDAPI_PS4_RUMBLE
|
||||||
|
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS4_RUMBLE, "1");
|
||||||
|
#endif
|
||||||
|
#ifdef SDL_HINT_JOYSTICK_HIDAPI_PS5_RUMBLE
|
||||||
|
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS5_RUMBLE, "1");
|
||||||
|
#endif
|
||||||
|
#ifdef SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS
|
||||||
|
SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1");
|
||||||
|
#endif
|
||||||
if(SDL_Init(SDL_INIT_GAMECONTROLLER) < 0)
|
if(SDL_Init(SDL_INIT_GAMECONTROLLER) < 0)
|
||||||
{
|
{
|
||||||
const char *err = SDL_GetError();
|
const char *err = SDL_GetError();
|
||||||
|
@ -225,7 +240,8 @@ void ControllerManager::ControllerClosed(Controller *controller)
|
||||||
open_controllers.remove(controller->GetDeviceID());
|
open_controllers.remove(controller->GetDeviceID());
|
||||||
}
|
}
|
||||||
|
|
||||||
Controller::Controller(int device_id, ControllerManager *manager) : QObject(manager)
|
Controller::Controller(int device_id, ControllerManager *manager)
|
||||||
|
: QObject(manager), is_dualsense(false)
|
||||||
{
|
{
|
||||||
this->id = device_id;
|
this->id = device_id;
|
||||||
this->manager = manager;
|
this->manager = manager;
|
||||||
|
@ -244,8 +260,10 @@ Controller::Controller(int device_id, ControllerManager *manager) : QObject(mana
|
||||||
SDL_GameControllerSetSensorEnabled(controller, SDL_SENSOR_ACCEL, SDL_TRUE);
|
SDL_GameControllerSetSensorEnabled(controller, SDL_SENSOR_ACCEL, SDL_TRUE);
|
||||||
if(SDL_GameControllerHasSensor(controller, SDL_SENSOR_GYRO))
|
if(SDL_GameControllerHasSensor(controller, SDL_SENSOR_GYRO))
|
||||||
SDL_GameControllerSetSensorEnabled(controller, SDL_SENSOR_GYRO, SDL_TRUE);
|
SDL_GameControllerSetSensorEnabled(controller, SDL_SENSOR_GYRO, SDL_TRUE);
|
||||||
break;
|
|
||||||
#endif
|
#endif
|
||||||
|
auto controller_id = QPair<int16_t, int16_t>(SDL_GameControllerGetVendor(controller), SDL_GameControllerGetProduct(controller));
|
||||||
|
is_dualsense = chiaki_dualsense_controller_ids.contains(controller_id);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
@ -255,7 +273,12 @@ Controller::~Controller()
|
||||||
{
|
{
|
||||||
#ifdef CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER
|
#ifdef CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER
|
||||||
if(controller)
|
if(controller)
|
||||||
|
{
|
||||||
|
// Clear trigger effects, SDL doesn't do it automatically
|
||||||
|
const uint8_t clear_effect[10] = { 0 };
|
||||||
|
this->SetTriggerEffects(0x05, clear_effect, 0x05, clear_effect);
|
||||||
SDL_GameControllerClose(controller);
|
SDL_GameControllerClose(controller);
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
manager->ControllerClosed(this);
|
manager->ControllerClosed(this);
|
||||||
}
|
}
|
||||||
|
@ -487,3 +510,28 @@ void Controller::SetRumble(uint8_t left, uint8_t right)
|
||||||
SDL_GameControllerRumble(controller, (uint16_t)left << 8, (uint16_t)right << 8, 5000);
|
SDL_GameControllerRumble(controller, (uint16_t)left << 8, (uint16_t)right << 8, 5000);
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Controller::SetTriggerEffects(uint8_t type_left, const uint8_t *data_left, uint8_t type_right, const uint8_t *data_right)
|
||||||
|
{
|
||||||
|
#if defined(CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER) && SDL_VERSION_ATLEAST(2, 0, 16)
|
||||||
|
if(!is_dualsense || !controller)
|
||||||
|
return;
|
||||||
|
DS5EffectsState_t state;
|
||||||
|
SDL_zero(state);
|
||||||
|
state.ucEnableBits1 |= (0x04 /* left trigger */ | 0x08 /* right trigger */);
|
||||||
|
state.rgucLeftTriggerEffect[0] = type_left;
|
||||||
|
SDL_memcpy(state.rgucLeftTriggerEffect + 1, data_left, 10);
|
||||||
|
state.rgucRightTriggerEffect[0] = type_right;
|
||||||
|
SDL_memcpy(state.rgucRightTriggerEffect + 1, data_right, 10);
|
||||||
|
SDL_GameControllerSendEffect(controller, &state, sizeof(state));
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Controller::IsDualSense()
|
||||||
|
{
|
||||||
|
#ifdef CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER
|
||||||
|
if(!controller)
|
||||||
|
return false;
|
||||||
|
return is_dualsense;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
|
@ -69,6 +69,11 @@ SettingsDialog::SettingsDialog(Settings *settings, QWidget *parent) : QDialog(pa
|
||||||
log_verbose_check_box->setChecked(settings->GetLogVerbose());
|
log_verbose_check_box->setChecked(settings->GetLogVerbose());
|
||||||
connect(log_verbose_check_box, &QCheckBox::stateChanged, this, &SettingsDialog::LogVerboseChanged);
|
connect(log_verbose_check_box, &QCheckBox::stateChanged, this, &SettingsDialog::LogVerboseChanged);
|
||||||
|
|
||||||
|
dualsense_check_box = new QCheckBox(this);
|
||||||
|
general_layout->addRow(tr("Extended DualSense Support:\nEnable haptics and adaptive triggers\nfor attached DualSense controllers.\nThis is currently experimental."), dualsense_check_box);
|
||||||
|
dualsense_check_box->setChecked(settings->GetDualSenseEnabled());
|
||||||
|
connect(dualsense_check_box, &QCheckBox::stateChanged, this, &SettingsDialog::DualSenseChanged);
|
||||||
|
|
||||||
auto log_directory_label = new QLineEdit(GetLogBaseDir(), this);
|
auto log_directory_label = new QLineEdit(GetLogBaseDir(), this);
|
||||||
log_directory_label->setReadOnly(true);
|
log_directory_label->setReadOnly(true);
|
||||||
general_layout->addRow(tr("Log Directory:"), log_directory_label);
|
general_layout->addRow(tr("Log Directory:"), log_directory_label);
|
||||||
|
@ -322,6 +327,11 @@ void SettingsDialog::LogVerboseChanged()
|
||||||
settings->SetLogVerbose(log_verbose_check_box->isChecked());
|
settings->SetLogVerbose(log_verbose_check_box->isChecked());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void SettingsDialog::DualSenseChanged()
|
||||||
|
{
|
||||||
|
settings->SetDualSenseEnabled(dualsense_check_box->isChecked());
|
||||||
|
}
|
||||||
|
|
||||||
void SettingsDialog::FPSSelected()
|
void SettingsDialog::FPSSelected()
|
||||||
{
|
{
|
||||||
settings->SetFPS((ChiakiVideoFPSPreset)fps_combo_box->currentData().toInt());
|
settings->SetFPS((ChiakiVideoFPSPreset)fps_combo_box->currentData().toInt());
|
||||||
|
|
|
@ -14,6 +14,12 @@
|
||||||
|
|
||||||
#define SETSU_UPDATE_INTERVAL_MS 4
|
#define SETSU_UPDATE_INTERVAL_MS 4
|
||||||
|
|
||||||
|
#ifdef Q_OS_LINUX
|
||||||
|
#define DUALSENSE_AUDIO_DEVICE_NEEDLE "DualSense"
|
||||||
|
#else
|
||||||
|
#define DUALSENSE_AUDIO_DEVICE_NEEDLE "Wireless Controller"
|
||||||
|
#endif
|
||||||
|
|
||||||
StreamSessionConnectInfo::StreamSessionConnectInfo(
|
StreamSessionConnectInfo::StreamSessionConnectInfo(
|
||||||
Settings *settings,
|
Settings *settings,
|
||||||
ChiakiTarget target,
|
ChiakiTarget target,
|
||||||
|
@ -39,10 +45,12 @@ StreamSessionConnectInfo::StreamSessionConnectInfo(
|
||||||
this->fullscreen = fullscreen;
|
this->fullscreen = fullscreen;
|
||||||
this->transform_mode = transform_mode;
|
this->transform_mode = transform_mode;
|
||||||
this->enable_keyboard = false; // TODO: from settings
|
this->enable_keyboard = false; // TODO: from settings
|
||||||
|
this->enable_dualsense = settings->GetDualSenseEnabled();
|
||||||
}
|
}
|
||||||
|
|
||||||
static void AudioSettingsCb(uint32_t channels, uint32_t rate, void *user);
|
static void AudioSettingsCb(uint32_t channels, uint32_t rate, void *user);
|
||||||
static void AudioFrameCb(int16_t *buf, size_t samples_count, void *user);
|
static void AudioFrameCb(int16_t *buf, size_t samples_count, void *user);
|
||||||
|
static void HapticsFrameCb(uint8_t *buf, size_t buf_size, void *user);
|
||||||
static void EventCb(ChiakiEvent *event, void *user);
|
static void EventCb(ChiakiEvent *event, void *user);
|
||||||
#if CHIAKI_GUI_ENABLE_SETSU
|
#if CHIAKI_GUI_ENABLE_SETSU
|
||||||
static void SessionSetsuCb(SetsuEvent *event, void *user);
|
static void SessionSetsuCb(SetsuEvent *event, void *user);
|
||||||
|
@ -57,7 +65,9 @@ StreamSession::StreamSession(const StreamSessionConnectInfo &connect_info, QObje
|
||||||
pi_decoder(nullptr),
|
pi_decoder(nullptr),
|
||||||
#endif
|
#endif
|
||||||
audio_output(nullptr),
|
audio_output(nullptr),
|
||||||
audio_io(nullptr)
|
audio_io(nullptr),
|
||||||
|
haptics_output(0),
|
||||||
|
haptics_resampler_buf(nullptr)
|
||||||
{
|
{
|
||||||
connected = false;
|
connected = false;
|
||||||
ChiakiErrorCode err;
|
ChiakiErrorCode err;
|
||||||
|
@ -116,6 +126,7 @@ StreamSession::StreamSession(const StreamSessionConnectInfo &connect_info, QObje
|
||||||
chiaki_connect_info.video_profile = connect_info.video_profile;
|
chiaki_connect_info.video_profile = connect_info.video_profile;
|
||||||
chiaki_connect_info.video_profile_auto_downgrade = true;
|
chiaki_connect_info.video_profile_auto_downgrade = true;
|
||||||
chiaki_connect_info.enable_keyboard = false;
|
chiaki_connect_info.enable_keyboard = false;
|
||||||
|
chiaki_connect_info.enable_dualsense = connect_info.enable_dualsense;
|
||||||
|
|
||||||
#if CHIAKI_LIB_ENABLE_PI_DECODER
|
#if CHIAKI_LIB_ENABLE_PI_DECODER
|
||||||
if(connect_info.decoder == Decoder::Pi && chiaki_connect_info.video_profile.codec != CHIAKI_CODEC_H264)
|
if(connect_info.decoder == Decoder::Pi && chiaki_connect_info.video_profile.codec != CHIAKI_CODEC_H264)
|
||||||
|
@ -144,6 +155,14 @@ StreamSession::StreamSession(const StreamSessionConnectInfo &connect_info, QObje
|
||||||
chiaki_opus_decoder_get_sink(&opus_decoder, &audio_sink);
|
chiaki_opus_decoder_get_sink(&opus_decoder, &audio_sink);
|
||||||
chiaki_session_set_audio_sink(&session, &audio_sink);
|
chiaki_session_set_audio_sink(&session, &audio_sink);
|
||||||
|
|
||||||
|
if(connect_info.enable_dualsense)
|
||||||
|
{
|
||||||
|
ChiakiAudioSink haptics_sink;
|
||||||
|
haptics_sink.user = this;
|
||||||
|
haptics_sink.frame_cb = HapticsFrameCb;
|
||||||
|
chiaki_session_set_haptics_sink(&session, &haptics_sink);
|
||||||
|
}
|
||||||
|
|
||||||
#if CHIAKI_LIB_ENABLE_PI_DECODER
|
#if CHIAKI_LIB_ENABLE_PI_DECODER
|
||||||
if(pi_decoder)
|
if(pi_decoder)
|
||||||
chiaki_session_set_video_sample_cb(&session, chiaki_pi_decoder_video_sample_cb, pi_decoder);
|
chiaki_session_set_video_sample_cb(&session, chiaki_pi_decoder_video_sample_cb, pi_decoder);
|
||||||
|
@ -181,6 +200,10 @@ StreamSession::StreamSession(const StreamSessionConnectInfo &connect_info, QObje
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
key_map = connect_info.key_map;
|
key_map = connect_info.key_map;
|
||||||
|
if(connect_info.enable_dualsense)
|
||||||
|
{
|
||||||
|
InitHaptics();
|
||||||
|
}
|
||||||
UpdateGamepads();
|
UpdateGamepads();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -208,6 +231,16 @@ StreamSession::~StreamSession()
|
||||||
chiaki_ffmpeg_decoder_fini(ffmpeg_decoder);
|
chiaki_ffmpeg_decoder_fini(ffmpeg_decoder);
|
||||||
delete ffmpeg_decoder;
|
delete ffmpeg_decoder;
|
||||||
}
|
}
|
||||||
|
if(haptics_output > 0)
|
||||||
|
{
|
||||||
|
SDL_CloseAudioDevice(haptics_output);
|
||||||
|
haptics_output = 0;
|
||||||
|
}
|
||||||
|
if(haptics_resampler_buf)
|
||||||
|
{
|
||||||
|
free(haptics_resampler_buf);
|
||||||
|
haptics_resampler_buf = nullptr;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void StreamSession::Start()
|
void StreamSession::Start()
|
||||||
|
@ -312,6 +345,8 @@ void StreamSession::UpdateGamepads()
|
||||||
{
|
{
|
||||||
CHIAKI_LOGI(log.GetChiakiLog(), "Controller %d disconnected", controller->GetDeviceID());
|
CHIAKI_LOGI(log.GetChiakiLog(), "Controller %d disconnected", controller->GetDeviceID());
|
||||||
controllers.remove(controller_id);
|
controllers.remove(controller_id);
|
||||||
|
if(controller->IsDualSense())
|
||||||
|
DisconnectHaptics();
|
||||||
delete controller;
|
delete controller;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -330,6 +365,11 @@ void StreamSession::UpdateGamepads()
|
||||||
CHIAKI_LOGI(log.GetChiakiLog(), "Controller %d opened: \"%s\"", controller_id, controller->GetName().toLocal8Bit().constData());
|
CHIAKI_LOGI(log.GetChiakiLog(), "Controller %d opened: \"%s\"", controller_id, controller->GetName().toLocal8Bit().constData());
|
||||||
connect(controller, &Controller::StateChanged, this, &StreamSession::SendFeedbackState);
|
connect(controller, &Controller::StateChanged, this, &StreamSession::SendFeedbackState);
|
||||||
controllers[controller_id] = controller;
|
controllers[controller_id] = controller;
|
||||||
|
if(controller->IsDualSense())
|
||||||
|
{
|
||||||
|
// Connect haptics audio device with a delay to give the sound system time to set up
|
||||||
|
QTimer::singleShot(1000, this, &StreamSession::ConnectHaptics);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -388,6 +428,82 @@ void StreamSession::InitAudio(unsigned int channels, unsigned int rate)
|
||||||
channels, rate, audio_output->bufferSize());
|
channels, rate, audio_output->bufferSize());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void StreamSession::InitHaptics()
|
||||||
|
{
|
||||||
|
haptics_output = 0;
|
||||||
|
haptics_resampler_buf = nullptr;
|
||||||
|
#ifdef Q_OS_LINUX
|
||||||
|
// Haptics work most reliably with Pipewire, so try to use that if available
|
||||||
|
SDL_SetHint("SDL_AUDIODRIVER", "pipewire");
|
||||||
|
#endif
|
||||||
|
|
||||||
|
if(SDL_Init(SDL_INIT_AUDIO) < 0)
|
||||||
|
{
|
||||||
|
CHIAKI_LOGE(log.GetChiakiLog(), "Could not initialize SDL Audio for haptics output: %s", SDL_GetError());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef Q_OS_LINUX
|
||||||
|
if(!strstr(SDL_GetCurrentAudioDriver(), "pipewire"))
|
||||||
|
{
|
||||||
|
CHIAKI_LOGW(
|
||||||
|
log.GetChiakiLog(),
|
||||||
|
"Haptics output is not using Pipewire, this may not work reliably. (was: '%s')",
|
||||||
|
SDL_GetCurrentAudioDriver());
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
SDL_AudioCVT cvt;
|
||||||
|
SDL_BuildAudioCVT(&cvt, AUDIO_S16LSB, 4, 3000, AUDIO_S16LSB, 4, 48000);
|
||||||
|
cvt.len = 240; // 10 16bit stereo samples
|
||||||
|
haptics_resampler_buf = (uint8_t*) calloc(cvt.len * cvt.len_mult, sizeof(uint8_t));
|
||||||
|
}
|
||||||
|
|
||||||
|
void StreamSession::DisconnectHaptics()
|
||||||
|
{
|
||||||
|
if(this->haptics_output > 0)
|
||||||
|
{
|
||||||
|
SDL_CloseAudioDevice(haptics_output);
|
||||||
|
this->haptics_output = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void StreamSession::ConnectHaptics()
|
||||||
|
{
|
||||||
|
if(this->haptics_output > 0)
|
||||||
|
{
|
||||||
|
CHIAKI_LOGW(this->log.GetChiakiLog(), "Haptics already connected to an attached DualSense controller, ignoring additional controllers.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_AudioSpec want, have;
|
||||||
|
SDL_zero(want);
|
||||||
|
want.freq = 48000;
|
||||||
|
want.format = AUDIO_S16LSB;
|
||||||
|
want.channels = 4;
|
||||||
|
want.samples = 480; // 10ms buffer
|
||||||
|
want.callback = NULL;
|
||||||
|
|
||||||
|
const char *device_name = nullptr;
|
||||||
|
for(int i=0; i < SDL_GetNumAudioDevices(0); i++)
|
||||||
|
{
|
||||||
|
device_name = SDL_GetAudioDeviceName(i, 0);
|
||||||
|
if(!device_name || !strstr(device_name, DUALSENSE_AUDIO_DEVICE_NEEDLE))
|
||||||
|
continue;
|
||||||
|
haptics_output = SDL_OpenAudioDevice(device_name, 0, &want, &have, 0);
|
||||||
|
if(haptics_output == 0)
|
||||||
|
{
|
||||||
|
CHIAKI_LOGE(log.GetChiakiLog(), "Could not open SDL Audio Device %s for haptics output: %s", device_name, SDL_GetError());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
SDL_PauseAudioDevice(haptics_output, 0);
|
||||||
|
CHIAKI_LOGI(log.GetChiakiLog(), "Haptics Audio Device '%s' opened with %d channels @ %d Hz, buffer size %u (driver=%s)", device_name, have.channels, have.freq, have.size, SDL_GetCurrentAudioDriver());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
CHIAKI_LOGW(log.GetChiakiLog(), "DualSense features were enabled and a DualSense is connected, but could not find the DualSense audio device!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
void StreamSession::PushAudioFrame(int16_t *buf, size_t samples_count)
|
void StreamSession::PushAudioFrame(int16_t *buf, size_t samples_count)
|
||||||
{
|
{
|
||||||
if(!audio_io)
|
if(!audio_io)
|
||||||
|
@ -395,6 +511,35 @@ void StreamSession::PushAudioFrame(int16_t *buf, size_t samples_count)
|
||||||
audio_io->write((const char *)buf, static_cast<qint64>(samples_count * 2 * 2));
|
audio_io->write((const char *)buf, static_cast<qint64>(samples_count * 2 * 2));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void StreamSession::PushHapticsFrame(uint8_t *buf, size_t buf_size)
|
||||||
|
{
|
||||||
|
if(haptics_output == 0)
|
||||||
|
return;
|
||||||
|
SDL_AudioCVT cvt;
|
||||||
|
// Haptics samples are coming in at 3KHZ, but the DualSense expects 48KHZ
|
||||||
|
SDL_BuildAudioCVT(&cvt, AUDIO_S16LSB, 4, 3000, AUDIO_S16LSB, 4, 48000);
|
||||||
|
cvt.len = buf_size * 2;
|
||||||
|
cvt.buf = haptics_resampler_buf;
|
||||||
|
// Remix to 4 channels
|
||||||
|
for (int i=0; i < buf_size; i+=4)
|
||||||
|
{
|
||||||
|
SDL_memset(haptics_resampler_buf + i * 2, 0, 4);
|
||||||
|
SDL_memcpy(haptics_resampler_buf + (i * 2) + 4, buf + i, 4);
|
||||||
|
}
|
||||||
|
// Resample to 48kHZ
|
||||||
|
if(SDL_ConvertAudio(&cvt) != 0)
|
||||||
|
{
|
||||||
|
CHIAKI_LOGE(log.GetChiakiLog(), "Failed to resample haptics audio: %s", SDL_GetError());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(SDL_QueueAudio(haptics_output, cvt.buf, cvt.len_cvt) < 0)
|
||||||
|
{
|
||||||
|
CHIAKI_LOGE(log.GetChiakiLog(), "Failed to submit haptics audio to device: %s", SDL_GetError());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void StreamSession::Event(ChiakiEvent *event)
|
void StreamSession::Event(ChiakiEvent *event)
|
||||||
{
|
{
|
||||||
switch(event->type)
|
switch(event->type)
|
||||||
|
@ -418,6 +563,19 @@ void StreamSession::Event(ChiakiEvent *event)
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case CHIAKI_EVENT_TRIGGER_EFFECTS: {
|
||||||
|
uint8_t type_left = event->trigger_effects.type_left;
|
||||||
|
uint8_t data_left[10];
|
||||||
|
memcpy(data_left, event->trigger_effects.left, 10);
|
||||||
|
uint8_t data_right[10];
|
||||||
|
memcpy(data_right, event->trigger_effects.right, 10);
|
||||||
|
uint8_t type_right = event->trigger_effects.type_right;
|
||||||
|
QMetaObject::invokeMethod(this, [this, type_left, data_left, type_right, data_right]() {
|
||||||
|
for(auto controller : controllers)
|
||||||
|
controller->SetTriggerEffects(type_left, data_left, type_right, data_right);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -544,6 +702,7 @@ class StreamSessionPrivate
|
||||||
}
|
}
|
||||||
|
|
||||||
static void PushAudioFrame(StreamSession *session, int16_t *buf, size_t samples_count) { session->PushAudioFrame(buf, samples_count); }
|
static void PushAudioFrame(StreamSession *session, int16_t *buf, size_t samples_count) { session->PushAudioFrame(buf, samples_count); }
|
||||||
|
static void PushHapticsFrame(StreamSession *session, uint8_t *buf, size_t buf_size) { session->PushHapticsFrame(buf, buf_size); }
|
||||||
static void Event(StreamSession *session, ChiakiEvent *event) { session->Event(event); }
|
static void Event(StreamSession *session, ChiakiEvent *event) { session->Event(event); }
|
||||||
#if CHIAKI_GUI_ENABLE_SETSU
|
#if CHIAKI_GUI_ENABLE_SETSU
|
||||||
static void HandleSetsuEvent(StreamSession *session, SetsuEvent *event) { session->HandleSetsuEvent(event); }
|
static void HandleSetsuEvent(StreamSession *session, SetsuEvent *event) { session->HandleSetsuEvent(event); }
|
||||||
|
@ -563,6 +722,12 @@ static void AudioFrameCb(int16_t *buf, size_t samples_count, void *user)
|
||||||
StreamSessionPrivate::PushAudioFrame(session, buf, samples_count);
|
StreamSessionPrivate::PushAudioFrame(session, buf, samples_count);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void HapticsFrameCb(uint8_t *buf, size_t buf_size, void *user)
|
||||||
|
{
|
||||||
|
auto session = reinterpret_cast<StreamSession *>(user);
|
||||||
|
StreamSessionPrivate::PushHapticsFrame(session, buf, buf_size);
|
||||||
|
}
|
||||||
|
|
||||||
static void EventCb(ChiakiEvent *event, void *user)
|
static void EventCb(ChiakiEvent *event, void *user)
|
||||||
{
|
{
|
||||||
auto session = reinterpret_cast<StreamSession *>(user);
|
auto session = reinterpret_cast<StreamSession *>(user);
|
||||||
|
|
|
@ -30,14 +30,15 @@ ninja
|
||||||
ninja install
|
ninja install
|
||||||
cd ../..
|
cd ../..
|
||||||
|
|
||||||
wget https://mirror.firedaemon.com/OpenSSL/openssl-1.1.1q.zip && 7z x openssl-1.1.1q.zip
|
wget https://download.firedaemon.com/FireDaemon-OpenSSL/openssl-1.1.1s.zip && 7z x openssl-1.1.*.zip
|
||||||
|
|
||||||
wget https://www.libsdl.org/release/SDL2-devel-2.0.10-VC.zip && 7z x SDL2-devel-2.0.10-VC.zip
|
wget https://www.libsdl.org/release/SDL2-devel-2.26.2-VC.zip && 7z x SDL2-devel-2.26.2-VC.zip
|
||||||
export SDL_ROOT="$BUILD_ROOT/SDL2-2.0.10"
|
export SDL_ROOT="$BUILD_ROOT/SDL2-2.26.2"
|
||||||
export SDL_ROOT=${SDL_ROOT//[\\]//}
|
export SDL_ROOT=${SDL_ROOT//[\\]//}
|
||||||
echo "set(SDL2_INCLUDE_DIRS \"$SDL_ROOT/include\")
|
echo "set(SDL2_INCLUDE_DIRS \"$SDL_ROOT/include\")
|
||||||
set(SDL2_LIBRARIES \"$SDL_ROOT/lib/x64/SDL2.lib\")
|
set(SDL2_LIBRARIES \"$SDL_ROOT/lib/x64/SDL2.lib\")
|
||||||
set(SDL2_LIBDIR \"$SDL_ROOT/lib/x64\")" > "$SDL_ROOT/SDL2Config.cmake"
|
set(SDL2_LIBDIR \"$SDL_ROOT/lib/x64\")
|
||||||
|
include($SDL_ROOT/cmake/sdl2-config-version.cmake)" > "$SDL_ROOT/SDL2Config.cmake"
|
||||||
|
|
||||||
mkdir protoc && cd protoc
|
mkdir protoc && cd protoc
|
||||||
wget https://github.com/protocolbuffers/protobuf/releases/download/v3.9.1/protoc-3.9.1-win64.zip && 7z x protoc-3.9.1-win64.zip
|
wget https://github.com/protocolbuffers/protobuf/releases/download/v3.9.1/protoc-3.9.1-win64.zip && 7z x protoc-3.9.1-win64.zip
|
||||||
|
@ -55,6 +56,7 @@ echo "-- Configure"
|
||||||
|
|
||||||
mkdir build && cd build
|
mkdir build && cd build
|
||||||
|
|
||||||
|
|
||||||
cmake \
|
cmake \
|
||||||
-G Ninja \
|
-G Ninja \
|
||||||
-DCMAKE_C_COMPILER=cl \
|
-DCMAKE_C_COMPILER=cl \
|
||||||
|
|
|
@ -22,14 +22,14 @@ mkdir -p build && cd build || exit 1
|
||||||
cmake \
|
cmake \
|
||||||
-DCMAKE_INSTALL_PREFIX="$ROOT/sdl2-prefix" \
|
-DCMAKE_INSTALL_PREFIX="$ROOT/sdl2-prefix" \
|
||||||
-DSDL_ATOMIC=OFF \
|
-DSDL_ATOMIC=OFF \
|
||||||
-DSDL_AUDIO=OFF \
|
-DSDL_AUDIO=ON \
|
||||||
-DSDL_CPUINFO=OFF \
|
-DSDL_CPUINFO=OFF \
|
||||||
-DSDL_EVENTS=ON \
|
-DSDL_EVENTS=ON \
|
||||||
-DSDL_FILE=OFF \
|
-DSDL_FILE=OFF \
|
||||||
-DSDL_FILESYSTEM=OFF \
|
-DSDL_FILESYSTEM=OFF \
|
||||||
-DSDL_HAPTIC=ON \
|
-DSDL_HAPTIC=ON \
|
||||||
-DSDL_JOYSTICK=ON \
|
-DSDL_JOYSTICK=ON \
|
||||||
-DSDL_LOADSO=OFF \
|
-DSDL_LOADSO=ON \
|
||||||
-DSDL_RENDER=OFF \
|
-DSDL_RENDER=OFF \
|
||||||
-DSDL_SHARED=ON \
|
-DSDL_SHARED=ON \
|
||||||
-DSDL_STATIC=OFF \
|
-DSDL_STATIC=OFF \
|
||||||
|
|
|
@ -1,64 +1,64 @@
|
||||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||||
|
|
||||||
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
|
||||||
=cUKW
|
=8BHh
|
||||||
-----END PGP PUBLIC KEY BLOCK-----
|
-----END PGP PUBLIC KEY BLOCK-----
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue