Програємо звук на DualShock4 з комп'ютера


Вибираючи геймпад для свого комп'ютера, я зупинився на DualShock4, так як мені сподобалася ідея, що можна буде слухати аудіо через підключаються до нього навушники. Але після покупки я дізнався, що, виявляється, ніхто не знає, як передати звук на геймпад через Bluetooth. Тому я вирішив розібратися з цим питанням. Якщо вам цікаво дізнатися, як DualShock4 спілкується з ігровою консоллю, чекаю під катом.

На жаль, у мене немає PlayStation 4, тому довелося задовольнятися тільки викладеними в Інтернеті дампами, а також уже відомими фрагментами обміну.
У процесі вивчення теми мені дуже допомогла ось ця сторінка. У ній описані основні моменти передачі даних між консоллю і геймпадом, а також викладений дамп цих даних. Нас цікавить файл дампа з ім'ям ds4_uart_hci_cap_playroom_needs_sorting.pcap.gz. Відкриваємо його в Wireshark і починаємо вивчати. Відсортуємо пакети по часу, так як, мабуть, дамп записувався окремо на прийом і передачу. Дамп знімався безпосередньо з UART геймпада, після чого був сконвертирован в pcap.

На початку йде налаштування самого модуля Bluetooth. Далі, з №49 по №163-й пакет, йде встановлення з'єднання і налаштування каналу передачі. Дуже добре цей процес описаний у статті Бездротової звук. Частина 1. Препаруємо Bluetooth.
Але для нашої задачі це неособо важливо.

Після всіх підготовчих робіт» геймпад починає відправляти HID Report. Формат повідомлення описано на сторінці вікі. Перший пакет з даними від консолі — це пакет №70181. Давайте розберемо його, користуючись даними з вікі сторінки.
Нас цікавлять тільки дані, які передаються через HID Profile.
Ось його зміст.


Номер байта bit 7 bit 6 bit 5 bit 4 bit 3 bit 2 bit 1 bit 0
[0] 0x0a – Тип Data 0x00 — Зарезервовано 0x02 — Напрям передачі
[1] 0x11 – Код операції
[2 — 3] Невідомо
[4] 0xf0 Забороняє зміну даних у геймпада, 0xf3 Дозволяє зміна
[5 — 6] Невідомо
[7] Rumble (right / weak)
[8] Rumble (left / strong)
[9] RGB color (Red)
[10] RGB color (Green)
[11] RGB color (Blue)
[12-25] Невідомо
[26] 0x11 – Код операції
[27 — 74] Гучність звучання в %
[75 — 78] CRC-32 від попередніх даних
Хоча 26 байт позначений на вище згаданій сторінці як невідомий, під час моїх експериментів вдалося з'ясувати, що він відповідає за гучність звучання і виставляється у відсотках. Також хоча поле crc присутній, але геймпад його не перевіряє і можна просто відправляти нульове значення.

Так як нам цікаво, які дані передає консоль, давайте відфільтруємо їх з 0-го байту HID Profile, який допоможе нам визначити напрямок пакета. Дані від гемпада мають значення 0xa1, від консолі 0xa2. Фільтр для Wireshark вийде таким: bthid[0] == 0xa2.

Якщо прокрутити пакети, то, починаючи з пакету №98516, сильно збільшився розмір даних. Якщо судити за даними з вікі-сторінки, то початок у пакетів з кодом операції 0x15 і 0x19 таке ж, як і у 0x11, тільки без CRC, яка знаходиться в кінці.

Все є HID
Ось ми і підійшли до найцікавішого — як передати звук на геймпад. Ось як виглядає пакет з аудіоданими.


Якщо уважно подивитися на пакети з кодами операції 0x14, 0x15, 0x17, 0x19, то помітно якесь сталість, а саме йдуть підряд байти 0x9c, 0x75, 0x19. Це дуже схоже на Bluetooth SBC header ( SBC — це один із стандартних кодеків для передачі аудіо через Bluetooth). І хоча для передачі SBC по Bluetooth є стандарт A2DP, творці PS4 вирішили піти своїм шляхом і передавати звук прямо в HID повідомленнях. Також якщо подивитися пакети далі то видно, що також змінюються два байти перед Bluetooth SBC header, це лічильник кадрів. Давайте перевіримо наше припущення, що це стандартний SBC кодек. Для цього скористаємося наступним скриптом на Python.
#!/usr/bin/env python3

from pcapfile import savefile
import collections
import struct

class bluetooth(object):

def __init__(self, packet, number):
self.direction = packet.raw()[3]
self.payload = packet.raw()[4:]
self.time = ((packet.timestamp_ms-444738)/1000000)+(packet.timestamp-3)
self.number = number


pcap = savefile.load_savefile(open('ds4_uart_hci_cap_playroom_needs_sorting.pcap', 'rb'))


bluetooth_packet = []
number=1
for pkt in pcap.packets:
bluetooth_packet.append(bluetooth(pkt, number))
number+=1

sbc = open('test.sbc', 'wb')
bluetooth_packet.sort(key=lambda pkt: pkt.time)
count = 0
for bt in bluetooth_packet:
count+=1
if(bt.payload[0]==2):
l2cap_len = struct.unpack("<H",bt.payload[5:7])[0]
if(l2cap_len>5):
sony_opcode = bt.payload[10]
if(sony_opcode == 0x19):
sbc.write(bt.payload[0x5b:-0x12])

if(sony_opcode == 0x17):
sbc.write(bt.payload[0x10:-0x8])

if(sony_opcode == 0x15):
sbc.write(bt.payload[0x5b:-0x1D])

if(sony_opcode == 0x14):
sbc.write(bt.payload[0x10:-0x28])

Скрипт працює наступним чином: відкриваємо дамп, кладемо всі пакети в список, після чого сортуємо по часу. Потім проходимо по порядку всі пакети, дістаючи аудіодані повідомлення з кодом операції 0x19,0x17,0x15 і 0x14 і записуючи їх у файл.

Тепер спробуємо відтворити отриманий файл, для чого скористаємося gstreamer'ом:

gst-launch-1.0 filesrc location=test.sbc ! sbcparse ! sbcdec ! autoaudiosink
 

На початку файлу буде тиша (це видно і по збережених даних). Для зручності перетворимо дані в wav:

gst-launch-1.0 filesrc location=test.sbc ! sbcparse ! sbcdec ! audioconvert ! wavenc ! filesink location=output.wav
 

Якщо перемотати на 41 секунду одержаний wav, ми почуємо звук.
Таким чином, ми впевнилися, що DualShock4 використовує звичайне SBC кодування для передачі звуку.

Тепер цікаво спробувати самим згенерувати дані для відтворення на геймпаді.
Скористаємося для цього все тими ж інструментами. Gstreamer буде кодувати, а Python буде передавати дані на DualShock4.
В Linux можна дуже просто працювати з геймпадом завдяки тому, що в ньому все (включаючи пристрої) є файлами.
Дізнатися, який файл відповідає геймпаду, можна після сполучення DualShock4 з комп'ютером. В результаті вдалого сполучення у висновку dmesg з'явиться рядок
sony 0005:054C:05C4.0007: input,hidraw5: BLUETOOTH HID v1.00 Gamepad [Wireless Controller]
Значить, наш контролер є в системі у вигляді файлу з ім'ям /dev/hidraw5, і ми можемо передавати дані на геймпад, просто записуючи необхідні дані у файл.
Ось скрипт, за допомогою якого це можна робити:
#!/usr/bin/env python3
import struct
from import sys stdin
import os
from io import FileIO

hiddev = os.open("/dev/hidraw5", os.O_RDWR | os.O_NONBLOCK)
pf = FileIO(hiddev, "wb+", closefd=False)
#pf=open("ds_my.bin", "wb+")

rumble_l = 0
rumble_r = 0
r = 0
g = 0
b = 50
crc = 0
volume = 50
flash_bright = 150
flash_dark = 150


def frame_number(inc):
res = struct.pack("<H", frame_number.n)
frame_number.n += inc
if frame_number.n > 0xffff:
frame_number.n = 0
return res
frame_number.n = 0

def joy_data():
data = [0xf3,0x4,0x00]
data.extend([rumble_l,rumble_r,r,g,b,flash_bright,flash_dark])
data.extend([0]*8)
data.extend([0x43,0x43,0x00,volume,0x85])
return data

def _11_report():
data = joy_data()
data.extend([0]*(48))
data.append(crc)
return bytearray(data)

def _14_report(audo_data):
return b'\x14\x40\xA0'+ frame_number(2) + b'\x02'+ audo_data + bytearray(40)

def _15_report(audo_data):
data = joy_data();
data.extend([0]*(52))
return b'\x15\xC0\xA0' + bytearray(data)+ frame_number(2) + b'\x02' + audo_data + bytearray(29)

def _17_report(audo_data):
return b'\x17\x40\xA0' + frame_number(4) + b'\x02' + audo_data + bytearray(8)

stdin = stdin.detach()
data = bytearray()
count = 1
while True:
# if count % 200:
if True:
data = _14_report(stdin.read(224)) if count % 3 else _15_report(stdin.read(224))
else:
data = _17_report(stdin.read(448))
print('big')
count+=1

pf.write(data)


Скрипт читає зі стандартного потоку закодовані в SBC аудіодані і формує два типи пакетів 0x14 і 0x15 (також коментуванням/раскомментированием рядків можна включити формування збільшеного в два рази пакета з опкодом 0x17) і відправляє їх на геймпад шляхом запису в hidraw девайс.
Спробуємо використовувати цей скрипт, щоб програти тестовий сигнал.
Даний сигнал буде генеруватися за допомогою gstreamer і вирушати на стандартний потік виводу, звідки його буде забирати скрипт.

gst-launch-1.0 -q audiotestsrc ! sbcenc ! 'audio/x, sbc,channels=2,rate=32000,channel mode=dual,blocks=16,subbands=8,bitpool=25' ! queue ! fdsink | ./play.py
 

І у нас вийшло (майже). Звук іде, але періодично чутні невеликі заїкання. З чим вони пов'язані, я зрозуміти так і не зміг. Можливо, я не зовсім правильно працюю з hid пристроєм в linux — якщо хто-небудь зможе підказати, як зробити правильніше, я буду вдячний. Спроба испопользования Bluetooth сокета також не увінчалася успіхом — через півсекунди програвання звуку все закінчувалося.

Висновок
Хотілося б висловити подяку таким проектам, як DS4Windows і ds4drv.
Дані проекти дозволяють використовувати геймпад на комп'ютері. Сподіваюся, ця стаття допоможе додати також і підтримку передачі звуку в ці проекти.

Дякую за увагу.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.