lib: Add support for trigger effects and controller haptics

By default, no trigger effects and haptics are requested from the
console, lib users have to explicitly enable them for a session by
setting the new `enable_dualsense` flag on the session's
`ChiakiConnectInfo` struct.

Trigger Effects are simply a new Takion message type `11`  and
include the type of each effect and the effect data (10 bytes) for
each of the triggers. They are exposed as a new Chiaki event type
`CHIAKI_EVENT_TRIGGER_EFFECTS`.

Haptic effects are implemented in the protocol as a separate audio
stream, for which packets are only sent when there are actually
effects being played, i.e. silence is not explicitly encoded.
Audio data is 3kHz little endian 16 bit stereo sent in frames of
10 samples every 100ms. Note that the Takion AV header has the
codec field set to Opus, however this is not true.
Users can provide a new `ChiakiAudioSink` dedicated to haptics
via the new `chiaki_session_set_haptics_sink` API, which behaves
identical to the regular audio sink, except that it has a lower
frequency.
This commit is contained in:
Johannes Baiter 2022-11-01 10:01:21 +01:00 committed by Florian Märkl
parent 40a9dee4ed
commit 74d39e6314
11 changed files with 145 additions and 28 deletions

View file

@ -38,7 +38,7 @@ CHIAKI_EXPORT void chiaki_feedback_state_format_v9(uint8_t *buf, ChiakiFeedbackS
/**
* @param buf buffer of at least CHIAKI_FEEDBACK_STATE_BUF_SIZE_V12
*/
CHIAKI_EXPORT void chiaki_feedback_state_format_v12(uint8_t *buf, ChiakiFeedbackState *state);
CHIAKI_EXPORT void chiaki_feedback_state_format_v12(uint8_t *buf, ChiakiFeedbackState *state, bool enable_dualsense);
#define CHIAKI_HISTORY_EVENT_SIZE_MAX 0x5

View file

@ -78,6 +78,7 @@ typedef struct chiaki_connect_info_t
ChiakiConnectVideoProfile video_profile;
bool video_profile_auto_downgrade; // Downgrade video_profile if server does not seem to support it.
bool enable_keyboard;
bool enable_dualsense;
} ChiakiConnectInfo;
@ -121,6 +122,14 @@ typedef struct chiaki_rumble_event_t
uint8_t right; // high-frequency
} ChiakiRumbleEvent;
typedef struct chiaki_trigger_effects_event_t
{
uint8_t type_left;
uint8_t type_right;
uint8_t left[10];
uint8_t right[10];
} ChiakiTriggerEffectsEvent;
typedef enum {
CHIAKI_EVENT_CONNECTED,
CHIAKI_EVENT_LOGIN_PIN_REQUEST,
@ -129,6 +138,7 @@ typedef enum {
CHIAKI_EVENT_KEYBOARD_REMOTE_CLOSE,
CHIAKI_EVENT_RUMBLE,
CHIAKI_EVENT_QUIT,
CHIAKI_EVENT_TRIGGER_EFFECTS,
} ChiakiEventType;
typedef struct chiaki_event_t
@ -139,6 +149,7 @@ typedef struct chiaki_event_t
ChiakiQuitEvent quit;
ChiakiKeyboardEvent keyboard;
ChiakiRumbleEvent rumble;
ChiakiTriggerEffectsEvent trigger_effects;
struct
{
bool pin_incorrect; // false on first request, true if the pin entered before was incorrect
@ -170,6 +181,7 @@ typedef struct chiaki_session_t
ChiakiConnectVideoProfile video_profile;
bool video_profile_auto_downgrade;
bool enable_keyboard;
bool enable_dualsense;
} connect_info;
ChiakiTarget target;
@ -191,6 +203,7 @@ typedef struct chiaki_session_t
ChiakiVideoSampleCallback video_sample_cb;
void *video_sample_cb_user;
ChiakiAudioSink audio_sink;
ChiakiAudioSink haptics_sink;
ChiakiThread session_thread;
@ -246,6 +259,14 @@ static inline void chiaki_session_set_audio_sink(ChiakiSession *session, ChiakiA
session->audio_sink = *sink;
}
/**
* @param sink contents are copied
*/
static inline void chiaki_session_set_haptics_sink(ChiakiSession *session, ChiakiAudioSink *sink)
{
session->haptics_sink = *sink;
}
#ifdef __cplusplus
}
#endif

View file

@ -32,6 +32,7 @@ typedef struct chiaki_stream_connection_t
ChiakiPacketStats packet_stats;
ChiakiAudioReceiver *audio_receiver;
ChiakiVideoReceiver *video_receiver;
ChiakiAudioReceiver *haptics_receiver;
ChiakiFeedbackSender feedback_sender;
/**

View file

@ -27,7 +27,8 @@ extern "C" {
typedef enum chiaki_takion_message_data_type_t {
CHIAKI_TAKION_MESSAGE_DATA_TYPE_PROTOBUF = 0,
CHIAKI_TAKION_MESSAGE_DATA_TYPE_RUMBLE = 7,
CHIAKI_TAKION_MESSAGE_DATA_TYPE_9 = 9
CHIAKI_TAKION_MESSAGE_DATA_TYPE_9 = 9,
CHIAKI_TAKION_MESSAGE_DATA_TYPE_TRIGGER_EFFECTS = 11,
} ChiakiTakionMessageDataType;
typedef struct chiaki_takion_av_packet_t
@ -36,6 +37,7 @@ typedef struct chiaki_takion_av_packet_t
ChiakiSeqNum16 frame_index;
bool uses_nalu_info_structs;
bool is_video;
bool is_haptics;
ChiakiSeqNum16 unit_index;
uint16_t units_in_frame_total; // source + units_in_frame_fec
uint16_t units_in_frame_fec;
@ -46,8 +48,6 @@ typedef struct chiaki_takion_av_packet_t
uint64_t key_pos;
uint8_t byte_before_audio_data;
uint8_t *data; // not owned
size_t data_size;
} ChiakiTakionAVPacket;
@ -106,6 +106,7 @@ typedef struct chiaki_takion_connect_info_t
ChiakiTakionCallback cb;
void *cb_user;
bool enable_crypt;
bool enable_dualsense;
uint8_t protocol_version;
} ChiakiTakionConnectInfo;
@ -162,6 +163,8 @@ typedef struct chiaki_takion_t
ChiakiTakionAVPacketParse av_packet_parse;
ChiakiKeyState key_state;
bool enable_dualsense;
} ChiakiTakion;

View file

@ -312,7 +312,8 @@ message ControllerConnectionPayload {
VITA = 3;
XINPUT = 4;
MOBILE = 5;
BOND = 6;
DUALSENSE = 6;
VR2SENSE = 7;
}
}

View file

@ -5,7 +5,7 @@
#include <string.h>
static void chiaki_audio_receiver_frame(ChiakiAudioReceiver *audio_receiver, ChiakiSeqNum16 frame_index, uint8_t *buf, size_t buf_size);
static void chiaki_audio_receiver_frame(ChiakiAudioReceiver *audio_receiver, ChiakiSeqNum16 frame_index, bool is_haptics, uint8_t *buf, size_t buf_size);
CHIAKI_EXPORT ChiakiErrorCode chiaki_audio_receiver_init(ChiakiAudioReceiver *audio_receiver, ChiakiSession *session, ChiakiPacketStats *packet_stats)
{
@ -102,14 +102,14 @@ CHIAKI_EXPORT void chiaki_audio_receiver_av_packet(ChiakiAudioReceiver *audio_re
frame_index = packet->frame_index - fec_units_count + fec_index;
}
chiaki_audio_receiver_frame(audio_receiver, frame_index, packet->data + unit_size * i, unit_size);
chiaki_audio_receiver_frame(audio_receiver, frame_index, packet->is_haptics, packet->data + unit_size * i, unit_size);
}
if(audio_receiver->packet_stats)
chiaki_packet_stats_push_seq(audio_receiver->packet_stats, packet->frame_index);
}
static void chiaki_audio_receiver_frame(ChiakiAudioReceiver *audio_receiver, ChiakiSeqNum16 frame_index, uint8_t *buf, size_t buf_size)
static void chiaki_audio_receiver_frame(ChiakiAudioReceiver *audio_receiver, ChiakiSeqNum16 frame_index, bool is_haptics, uint8_t *buf, size_t buf_size)
{
chiaki_mutex_lock(&audio_receiver->mutex);
@ -117,7 +117,9 @@ static void chiaki_audio_receiver_frame(ChiakiAudioReceiver *audio_receiver, Chi
goto beach;
audio_receiver->frame_index_prev = frame_index;
if(audio_receiver->session->audio_sink.frame_cb)
if(is_haptics && audio_receiver->session->haptics_sink.frame_cb)
audio_receiver->session->haptics_sink.frame_cb(buf, buf_size, audio_receiver->session->haptics_sink.user);
else if(!is_haptics && audio_receiver->session->audio_sink.frame_cb)
audio_receiver->session->audio_sink.frame_cb(buf, buf_size, audio_receiver->session->audio_sink.user);
beach:

View file

@ -488,8 +488,15 @@ static void ctrl_message_received(ChiakiCtrl *ctrl, uint16_t msg_type, uint8_t *
static void ctrl_enable_optional_features(ChiakiCtrl *ctrl)
{
if(!ctrl->session->connect_info.enable_keyboard)
return;
if(ctrl->session->connect_info.enable_dualsense)
{
CHIAKI_LOGI(ctrl->session->log, "Enabling DualSense features");
const uint8_t enable[3] = { 0x00, 0x40, 0x00 };
ctrl_message_send(ctrl, 0x13, enable, 3);
}
if(ctrl->session->connect_info.enable_keyboard)
{
CHIAKI_LOGI(ctrl->session->log, "Enabling Keyboard");
// TODO: Last byte of pre_enable request is random (?)
// TODO: Signature ?!
uint8_t enable = 1;
@ -500,6 +507,7 @@ static void ctrl_enable_optional_features(ChiakiCtrl *ctrl)
ctrl_message_send(ctrl, CTRL_MESSAGE_TYPE_KEYBOARD_ENABLE_TOGGLE, &enable, 1);
ctrl_message_send(ctrl, 0x36, pre_enable, 4);
}
}
static void ctrl_message_received_session_id(ChiakiCtrl *ctrl, uint8_t *payload, size_t payload_size)
{

View file

@ -76,12 +76,12 @@ CHIAKI_EXPORT void chiaki_feedback_state_format_v9(uint8_t *buf, ChiakiFeedbackS
*((chiaki_unaligned_uint16_t *)(buf + 0x17)) = htons((uint16_t)state->right_y);
}
CHIAKI_EXPORT void chiaki_feedback_state_format_v12(uint8_t *buf, ChiakiFeedbackState *state)
CHIAKI_EXPORT void chiaki_feedback_state_format_v12(uint8_t *buf, ChiakiFeedbackState *state, bool enable_dualsense)
{
chiaki_feedback_state_format_v9(buf, state);
buf[0x19] = 0x0;
buf[0x1a] = 0x0;
buf[0x1b] = 0x1; // 1 for Shock, 0 for Sense
buf[0x1b] = enable_dualsense ? 0x0 : 0x1;
}
CHIAKI_EXPORT ChiakiErrorCode chiaki_feedback_history_event_set_button(ChiakiFeedbackHistoryEvent *event, uint64_t button, uint8_t state)

View file

@ -227,6 +227,7 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_session_init(ChiakiSession *session, Chiaki
session->connect_info.video_profile = connect_info->video_profile;
session->connect_info.video_profile_auto_downgrade = connect_info->video_profile_auto_downgrade;
session->connect_info.enable_keyboard = connect_info->enable_keyboard;
session->connect_info.enable_dualsense = connect_info->enable_dualsense;
return CHIAKI_ERR_SUCCESS;
error_stop_pipe:

View file

@ -47,7 +47,9 @@ static void stream_connection_takion_cb(ChiakiTakionEvent *event, void *user);
static void stream_connection_takion_data(ChiakiStreamConnection *stream_connection, ChiakiTakionMessageDataType data_type, uint8_t *buf, size_t buf_size);
static void stream_connection_takion_data_protobuf(ChiakiStreamConnection *stream_connection, uint8_t *buf, size_t buf_size);
static void stream_connection_takion_data_rumble(ChiakiStreamConnection *stream_connection, uint8_t *buf, size_t buf_size);
static void stream_connection_takion_data_trigger_effects(ChiakiStreamConnection *stream_connection, uint8_t *buf, size_t buf_size);
static ChiakiErrorCode stream_connection_send_big(ChiakiStreamConnection *stream_connection);
static ChiakiErrorCode stream_connection_send_controller_connection(ChiakiStreamConnection *stream_connection);
static ChiakiErrorCode stream_connection_send_disconnect(ChiakiStreamConnection *stream_connection);
static void stream_connection_takion_data_idle(ChiakiStreamConnection *stream_connection, uint8_t *buf, size_t buf_size);
static void stream_connection_takion_data_expect_bang(ChiakiStreamConnection *stream_connection, uint8_t *buf, size_t buf_size);
@ -79,6 +81,7 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_stream_connection_init(ChiakiStreamConnecti
stream_connection->video_receiver = NULL;
stream_connection->audio_receiver = NULL;
stream_connection->haptics_receiver = NULL;
err = chiaki_mutex_init(&stream_connection->feedback_sender_mutex, false);
if(err != CHIAKI_ERR_SUCCESS)
@ -143,6 +146,7 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_stream_connection_run(ChiakiStreamConnectio
takion_info.ip_dontfrag = false;
takion_info.enable_crypt = true;
takion_info.enable_dualsense = session->connect_info.enable_dualsense;
takion_info.protocol_version = chiaki_target_is_ps5(session->target) ? 12 : 9;
takion_info.cb = stream_connection_takion_cb;
@ -164,12 +168,20 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_stream_connection_run(ChiakiStreamConnectio
return CHIAKI_ERR_UNKNOWN;
}
stream_connection->haptics_receiver = chiaki_audio_receiver_new(session, NULL);
if(!stream_connection->haptics_receiver)
{
CHIAKI_LOGE(session->log, "StreamConnection failed to initialize Haptics Receiver");
err = CHIAKI_ERR_UNKNOWN;
goto err_audio_receiver;
}
stream_connection->video_receiver = chiaki_video_receiver_new(session, &stream_connection->packet_stats);
if(!stream_connection->video_receiver)
{
CHIAKI_LOGE(session->log, "StreamConnection failed to initialize Video Receiver");
err = CHIAKI_ERR_UNKNOWN;
goto err_audio_receiver;
goto err_haptics_receiver;
}
stream_connection->state = STATE_TAKION_CONNECT;
@ -321,6 +333,10 @@ err_video_receiver:
chiaki_video_receiver_free(stream_connection->video_receiver);
stream_connection->video_receiver = NULL;
err_haptics_receiver:
chiaki_audio_receiver_free(stream_connection->haptics_receiver);
stream_connection->haptics_receiver = NULL;
err_audio_receiver:
chiaki_audio_receiver_free(stream_connection->audio_receiver);
stream_connection->audio_receiver = NULL;
@ -376,6 +392,9 @@ static void stream_connection_takion_data(ChiakiStreamConnection *stream_connect
case CHIAKI_TAKION_MESSAGE_DATA_TYPE_RUMBLE:
stream_connection_takion_data_rumble(stream_connection, buf, buf_size);
break;
case CHIAKI_TAKION_MESSAGE_DATA_TYPE_TRIGGER_EFFECTS:
stream_connection_takion_data_trigger_effects(stream_connection, buf, buf_size);
break;
default:
break;
}
@ -415,6 +434,24 @@ static void stream_connection_takion_data_rumble(ChiakiStreamConnection *stream_
chiaki_session_send_event(stream_connection->session, &event);
}
static void stream_connection_takion_data_trigger_effects(ChiakiStreamConnection *stream_connection, uint8_t *buf, size_t buf_size)
{
if(buf_size < 25)
{
CHIAKI_LOGE(stream_connection->log, "StreamConnection got trigger effects packet with size %#llx < 25",
(unsigned long long)buf_size);
return;
}
ChiakiEvent event = { 0 };
event.type = CHIAKI_EVENT_TRIGGER_EFFECTS;
event.trigger_effects.type_left = buf[1];
event.trigger_effects.type_right = buf[2];
memcpy(&event.trigger_effects.left, buf + 5, 10);
memcpy(&event.trigger_effects.right, buf + 15, 10);
chiaki_session_send_event(stream_connection->session, &event);
}
static void stream_connection_takion_data_handle_disconnect(ChiakiStreamConnection *stream_connection, uint8_t *buf, size_t buf_size)
{
tkproto_TakionMessage msg;
@ -460,7 +497,7 @@ static void stream_connection_takion_data_idle(ChiakiStreamConnection *stream_co
return;
}
CHIAKI_LOGV(stream_connection->log, "StreamConnection received data");
CHIAKI_LOGV(stream_connection->log, "StreamConnection received data with msg.type == %d", msg.type);
chiaki_log_hexdump(stream_connection->log, CHIAKI_LOG_VERBOSE, buf, buf_size);
if(msg.type == tkproto_TakionMessage_PayloadType_DISCONNECT)
@ -522,7 +559,8 @@ static void stream_connection_takion_data_expect_bang(ChiakiStreamConnection *st
return;
}
CHIAKI_LOGE(stream_connection->log, "StreamConnection expected bang payload but received something else");
CHIAKI_LOGE(stream_connection->log, "StreamConnection expected bang payload but received something else: %d", msg.type);
chiaki_log_hexdump(stream_connection->log, CHIAKI_LOG_VERBOSE, buf, buf_size);
return;
}
@ -584,6 +622,12 @@ static void stream_connection_takion_data_expect_bang(ChiakiStreamConnection *st
// stream_connection->state_mutex is expected to be locked by the caller of this function
stream_connection->state_finished = true;
chiaki_cond_signal(&stream_connection->state_cond);
err = stream_connection_send_controller_connection(stream_connection);
if(err != CHIAKI_ERR_SUCCESS)
{
CHIAKI_LOGE(stream_connection->log, "StreamConnection failed to send controller connection");
goto error;
}
return;
error:
stream_connection->state_failed = true;
@ -811,6 +855,37 @@ static ChiakiErrorCode stream_connection_send_big(ChiakiStreamConnection *stream
return err;
}
static ChiakiErrorCode stream_connection_send_controller_connection(ChiakiStreamConnection *stream_connection)
{
ChiakiSession *session = stream_connection->session;
tkproto_TakionMessage msg;
memset(&msg, 0, sizeof(msg));
msg.type = tkproto_TakionMessage_PayloadType_CONTROLLERCONNECTION;
msg.has_controller_connection_payload = true;
msg.controller_connection_payload.has_connected = true;
msg.controller_connection_payload.connected = true;
msg.controller_connection_payload.has_controller_id = false;
msg.controller_connection_payload.has_controller_type = true;
msg.controller_connection_payload.controller_type = session->connect_info.enable_dualsense
? tkproto_ControllerConnectionPayload_ControllerType_DUALSENSE
: tkproto_ControllerConnectionPayload_ControllerType_DUALSHOCK4;
uint8_t buf[2048];
size_t buf_size;
pb_ostream_t stream = pb_ostream_from_buffer(buf, sizeof(buf));
bool pbr = pb_encode(&stream, tkproto_TakionMessage_fields, &msg);
if(!pbr)
{
CHIAKI_LOGE(stream_connection->log, "StreamConnection controller connection protobuf encoding failed");
return CHIAKI_ERR_UNKNOWN;
}
buf_size = stream.bytes_written;
return chiaki_takion_send_message_data(&stream_connection->takion, 1, 1, buf, buf_size, NULL);
}
static ChiakiErrorCode stream_connection_send_streaminfo_ack(ChiakiStreamConnection *stream_connection)
{
tkproto_TakionMessage msg;
@ -867,6 +942,8 @@ static void stream_connection_takion_av(ChiakiStreamConnection *stream_connectio
if(packet->is_video)
chiaki_video_receiver_av_packet(stream_connection->video_receiver, packet);
else if(packet->is_haptics)
chiaki_audio_receiver_av_packet(stream_connection->haptics_receiver, packet);
else
chiaki_audio_receiver_av_packet(stream_connection->audio_receiver, packet);
}

View file

@ -57,7 +57,8 @@ typedef enum takion_packet_type_t {
TAKION_PACKET_TYPE_CONGESTION = 5,
TAKION_PACKET_TYPE_FEEDBACK_STATE = 6,
TAKION_PACKET_TYPE_CLIENT_INFO = 8,
TAKION_PACKET_TYPE_PAD_INFO_EVENT = 9
TAKION_PACKET_TYPE_PAD_INFO_EVENT = 9,
TAKION_PACKET_TYPE_PAD_ADAPTIVE_TRIGGERS = 11,
} TakionPacketType;
/**
@ -215,6 +216,7 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_takion_connect(ChiakiTakion *takion, Chiaki
takion->postponed_packets = NULL;
takion->postponed_packets_size = 0;
takion->postponed_packets_count = 0;
takion->enable_dualsense = info->enable_dualsense;
CHIAKI_LOGI(takion->log, "Takion connecting (version %u)", (unsigned int)info->protocol_version);
@ -556,7 +558,7 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_takion_send_feedback_state(ChiakiTakion *ta
else
{
buf_sz = 0xc + CHIAKI_FEEDBACK_STATE_BUF_SIZE_V12;
chiaki_feedback_state_format_v12(buf + 0xc, feedback_state);
chiaki_feedback_state_format_v12(buf + 0xc, feedback_state, takion->enable_dualsense);
}
return takion_send_feedback_packet(takion, buf, buf_sz);
}
@ -950,6 +952,7 @@ static void takion_flush_data_queue(ChiakiTakion *takion)
if(data_type != CHIAKI_TAKION_MESSAGE_DATA_TYPE_PROTOBUF
&& data_type != CHIAKI_TAKION_MESSAGE_DATA_TYPE_RUMBLE
&& data_type != CHIAKI_TAKION_MESSAGE_DATA_TYPE_TRIGGER_EFFECTS
&& data_type != CHIAKI_TAKION_MESSAGE_DATA_TYPE_9)
{
CHIAKI_LOGW(takion->log, "Takion received data with unexpected data type %#x", data_type);
@ -1308,7 +1311,7 @@ static ChiakiErrorCode av_packet_parse(bool v12, ChiakiTakionAVPacket *packet, C
if(v12 && !packet->is_video)
{
packet->byte_before_audio_data = *av;
packet->is_haptics = *av == 0x02;
av += 1;
av_size -= 1;
}