mirror of
https://git.sr.ht/~thestr4ng3r/chiaki
synced 2025-07-05 12:36:38 -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:
|
||||
- git submodule update --init --recursive
|
||||
- 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
|
||||
|
||||
build_script:
|
||||
|
|
|
@ -67,12 +67,12 @@ if(CHIAKI_ENABLE_CLI)
|
|||
endif()
|
||||
|
||||
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)
|
||||
target_link_libraries(chiaki Qt5::MacExtras)
|
||||
target_compile_definitions(chiaki PRIVATE CHIAKI_GUI_ENABLE_QT_MACEXTRAS)
|
||||
endif()
|
||||
if(CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER)
|
||||
target_link_libraries(chiaki SDL2::SDL2)
|
||||
target_compile_definitions(chiaki PRIVATE CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER)
|
||||
endif()
|
||||
if(CHIAKI_ENABLE_SETSU)
|
||||
|
|
|
@ -77,6 +77,7 @@ class Controller : public QObject
|
|||
int id;
|
||||
ChiakiOrientationTracker orientation_tracker;
|
||||
ChiakiControllerState state;
|
||||
bool is_dualsense;
|
||||
|
||||
#ifdef CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER
|
||||
QMap<QPair<Sint64, Sint64>, uint8_t> touch_ids;
|
||||
|
@ -91,9 +92,43 @@ class Controller : public QObject
|
|||
QString GetName();
|
||||
ChiakiControllerState GetState();
|
||||
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:
|
||||
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
|
||||
|
|
|
@ -63,6 +63,9 @@ class Settings : public QObject
|
|||
void SetLogVerbose(bool enabled) { settings.setValue("settings/log_verbose", enabled); }
|
||||
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;
|
||||
void SetResolution(ChiakiVideoResolutionPreset resolution);
|
||||
|
||||
|
|
|
@ -20,6 +20,7 @@ class SettingsDialog : public QDialog
|
|||
|
||||
QCheckBox *log_verbose_check_box;
|
||||
QComboBox *disconnect_action_combo_box;
|
||||
QCheckBox *dualsense_check_box;
|
||||
|
||||
QComboBox *resolution_combo_box;
|
||||
QComboBox *fps_combo_box;
|
||||
|
@ -37,6 +38,7 @@ class SettingsDialog : public QDialog
|
|||
|
||||
private slots:
|
||||
void LogVerboseChanged();
|
||||
void DualSenseChanged();
|
||||
void DisconnectActionSelected();
|
||||
|
||||
void ResolutionSelected();
|
||||
|
|
|
@ -56,6 +56,7 @@ struct StreamSessionConnectInfo
|
|||
bool fullscreen;
|
||||
TransformMode transform_mode;
|
||||
bool enable_keyboard;
|
||||
bool enable_dualsense;
|
||||
|
||||
StreamSessionConnectInfo(
|
||||
Settings *settings,
|
||||
|
@ -101,17 +102,23 @@ class StreamSession : public QObject
|
|||
unsigned int audio_buffer_size;
|
||||
QAudioOutput *audio_output;
|
||||
QIODevice *audio_io;
|
||||
SDL_AudioDeviceID haptics_output;
|
||||
uint8_t *haptics_resampler_buf;
|
||||
|
||||
QMap<Qt::Key, int> key_map;
|
||||
|
||||
void PushAudioFrame(int16_t *buf, size_t samples_count);
|
||||
void PushHapticsFrame(uint8_t *buf, size_t buf_size);
|
||||
#if CHIAKI_GUI_ENABLE_SETSU
|
||||
void HandleSetsuEvent(SetsuEvent *event);
|
||||
#endif
|
||||
|
||||
private slots:
|
||||
void InitAudio(unsigned int channels, unsigned int rate);
|
||||
void InitHaptics();
|
||||
void Event(ChiakiEvent *event);
|
||||
void DisconnectHaptics();
|
||||
void ConnectHaptics();
|
||||
|
||||
public:
|
||||
explicit StreamSession(const StreamSessionConnectInfo &connect_info, QObject *parent = nullptr);
|
||||
|
|
|
@ -68,6 +68,12 @@ static QSet<QString> chiaki_motion_controller_guids({
|
|||
"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;
|
||||
|
||||
#define UPDATE_INTERVAL_MS 4
|
||||
|
@ -84,6 +90,15 @@ ControllerManager::ControllerManager(QObject *parent)
|
|||
{
|
||||
#ifdef CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER
|
||||
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)
|
||||
{
|
||||
const char *err = SDL_GetError();
|
||||
|
@ -225,7 +240,8 @@ void ControllerManager::ControllerClosed(Controller *controller)
|
|||
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->manager = manager;
|
||||
|
@ -244,8 +260,10 @@ Controller::Controller(int device_id, ControllerManager *manager) : QObject(mana
|
|||
SDL_GameControllerSetSensorEnabled(controller, SDL_SENSOR_ACCEL, SDL_TRUE);
|
||||
if(SDL_GameControllerHasSensor(controller, SDL_SENSOR_GYRO))
|
||||
SDL_GameControllerSetSensorEnabled(controller, SDL_SENSOR_GYRO, SDL_TRUE);
|
||||
break;
|
||||
#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
|
||||
|
@ -255,7 +273,12 @@ Controller::~Controller()
|
|||
{
|
||||
#ifdef CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER
|
||||
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);
|
||||
}
|
||||
#endif
|
||||
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);
|
||||
#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());
|
||||
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);
|
||||
log_directory_label->setReadOnly(true);
|
||||
general_layout->addRow(tr("Log Directory:"), log_directory_label);
|
||||
|
@ -322,6 +327,11 @@ void SettingsDialog::LogVerboseChanged()
|
|||
settings->SetLogVerbose(log_verbose_check_box->isChecked());
|
||||
}
|
||||
|
||||
void SettingsDialog::DualSenseChanged()
|
||||
{
|
||||
settings->SetDualSenseEnabled(dualsense_check_box->isChecked());
|
||||
}
|
||||
|
||||
void SettingsDialog::FPSSelected()
|
||||
{
|
||||
settings->SetFPS((ChiakiVideoFPSPreset)fps_combo_box->currentData().toInt());
|
||||
|
|
|
@ -14,6 +14,12 @@
|
|||
|
||||
#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(
|
||||
Settings *settings,
|
||||
ChiakiTarget target,
|
||||
|
@ -39,10 +45,12 @@ StreamSessionConnectInfo::StreamSessionConnectInfo(
|
|||
this->fullscreen = fullscreen;
|
||||
this->transform_mode = transform_mode;
|
||||
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 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);
|
||||
#if CHIAKI_GUI_ENABLE_SETSU
|
||||
static void SessionSetsuCb(SetsuEvent *event, void *user);
|
||||
|
@ -57,7 +65,9 @@ StreamSession::StreamSession(const StreamSessionConnectInfo &connect_info, QObje
|
|||
pi_decoder(nullptr),
|
||||
#endif
|
||||
audio_output(nullptr),
|
||||
audio_io(nullptr)
|
||||
audio_io(nullptr),
|
||||
haptics_output(0),
|
||||
haptics_resampler_buf(nullptr)
|
||||
{
|
||||
connected = false;
|
||||
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_auto_downgrade = true;
|
||||
chiaki_connect_info.enable_keyboard = false;
|
||||
chiaki_connect_info.enable_dualsense = connect_info.enable_dualsense;
|
||||
|
||||
#if CHIAKI_LIB_ENABLE_PI_DECODER
|
||||
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_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(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
|
||||
|
||||
key_map = connect_info.key_map;
|
||||
if(connect_info.enable_dualsense)
|
||||
{
|
||||
InitHaptics();
|
||||
}
|
||||
UpdateGamepads();
|
||||
}
|
||||
|
||||
|
@ -208,6 +231,16 @@ StreamSession::~StreamSession()
|
|||
chiaki_ffmpeg_decoder_fini(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()
|
||||
|
@ -312,6 +345,8 @@ void StreamSession::UpdateGamepads()
|
|||
{
|
||||
CHIAKI_LOGI(log.GetChiakiLog(), "Controller %d disconnected", controller->GetDeviceID());
|
||||
controllers.remove(controller_id);
|
||||
if(controller->IsDualSense())
|
||||
DisconnectHaptics();
|
||||
delete controller;
|
||||
}
|
||||
}
|
||||
|
@ -330,6 +365,11 @@ void StreamSession::UpdateGamepads()
|
|||
CHIAKI_LOGI(log.GetChiakiLog(), "Controller %d opened: \"%s\"", controller_id, controller->GetName().toLocal8Bit().constData());
|
||||
connect(controller, &Controller::StateChanged, this, &StreamSession::SendFeedbackState);
|
||||
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());
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
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));
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
switch(event->type)
|
||||
|
@ -418,6 +563,19 @@ void StreamSession::Event(ChiakiEvent *event)
|
|||
});
|
||||
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:
|
||||
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 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); }
|
||||
#if CHIAKI_GUI_ENABLE_SETSU
|
||||
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);
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
auto session = reinterpret_cast<StreamSession *>(user);
|
||||
|
|
|
@ -30,14 +30,15 @@ ninja
|
|||
ninja install
|
||||
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
|
||||
export SDL_ROOT="$BUILD_ROOT/SDL2-2.0.10"
|
||||
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.26.2"
|
||||
export SDL_ROOT=${SDL_ROOT//[\\]//}
|
||||
echo "set(SDL2_INCLUDE_DIRS \"$SDL_ROOT/include\")
|
||||
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
|
||||
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
|
||||
|
||||
|
||||
cmake \
|
||||
-G Ninja \
|
||||
-DCMAKE_C_COMPILER=cl \
|
||||
|
|
|
@ -22,14 +22,14 @@ mkdir -p build && cd build || exit 1
|
|||
cmake \
|
||||
-DCMAKE_INSTALL_PREFIX="$ROOT/sdl2-prefix" \
|
||||
-DSDL_ATOMIC=OFF \
|
||||
-DSDL_AUDIO=OFF \
|
||||
-DSDL_AUDIO=ON \
|
||||
-DSDL_CPUINFO=OFF \
|
||||
-DSDL_EVENTS=ON \
|
||||
-DSDL_FILE=OFF \
|
||||
-DSDL_FILESYSTEM=OFF \
|
||||
-DSDL_HAPTIC=ON \
|
||||
-DSDL_JOYSTICK=ON \
|
||||
-DSDL_LOADSO=OFF \
|
||||
-DSDL_LOADSO=ON \
|
||||
-DSDL_RENDER=OFF \
|
||||
-DSDL_SHARED=ON \
|
||||
-DSDL_STATIC=OFF \
|
||||
|
|
|
@ -1,64 +1,64 @@
|
|||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
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
|
||||
=cUKW
|
||||
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
|
||||
=8BHh
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue