Выбирая геймпад для своего компьютера, я остановился на 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-24] | Неизвестно | |||||||
[25] | Громкость звучания в % | |||||||
[26 — 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 sys import 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 is-live=true ! sbcenc ! 'audio/x-sbc,channels=2,rate=32000,channel-mode=dual,blocks=16,subbands=8,bitpool=25' ! queue ! fdsink | ./play.py
И у нас получилось
Заключение
Хотелось бы выразить благодарность таким проектам, как DS4Windows и ds4drv.
Данные проекты позволяют использовать геймпад на компьютере. Надеюсь, эта статья поможет добавить также и поддержку передачи звука в эти проекты.
Спасибо за внимание.
UPD:
Небольшие дополнение.
Если добавить is-live=true к audiotestsrc то звук идет почти без заиканий.
Вот полезный pipeline для gstreamer который позволяет захватывать все, что идет на аудио выход и отправлять на DualShock4.
gst-launch-1.0 -q pulsesrc device="alsa_output.pci-0000_00_1b.0.analog-stereo.monitor" ! queue ! audioresample ! 'audio/x-raw,rate=32000' ! audioconvert ! sbcenc ! 'audio/x-sbc,channels=2,rate=32000,channel-mode=dual,blocks=16,subbands=8,bitpool=25' ! queue ! fdsink | ./play.py
Получить имя девайса можно следующей командой.
pacmd list-sources | grep -e device.string -e 'name:'
Комментарии (17)
syrompe
01.06.2016 13:24Этот геймпад еще и отправлять звук умеет, если к ему гарнитуру с микрофоном подключить.
Также в самом геймпаде есть небольшой динамик чтоб без отдельных наушников работал (так себе динамик конечно)
Кстати, идейка, на ебее продаются геймпады с отломанными кнопками задешего. Выдрать из них платку и понаделать себе «безпроводных» гарнитур…ViceCily
02.06.2016 05:56А для чего используется встроенный в геймпад динамик и как его задейстовать?
a1ien_n3t
02.06.2016 10:40Ну на него можно воспроизводить любой звук. А где вы хотите его задействовать, с PS4 или PC?
ViceCily
02.06.2016 12:10С PS4. Консоль купил, но я не игроман. Запускал пару раз Assassin's Creed и целенаправленно прошел Uncharted 4. За все это время даже не догадывался, что в геймпаде есть динамик, он себя никак не проявлял. Кажется уже нашел мануал официальный, как его проверить. Спасибо!
Oslegg
02.06.2016 22:34Для PS4 есть игры которые используют этот динамик. Например игра Zombies использует динамик контроллера как персональную рацию. Тоесть все звуки идут через телевизор (или звук. систему если есть) а вот рация только с контроллера.
Так-же в мультиплеере большинства игр VoIP идёт через контроллер.
foxyrus
01.06.2016 14:14Эх вот бы прикрутить к PS4 обычные bluetooth гарнитуры для передачи звука с приставки. Обычные гарнитуры видны в ps4, но не подключаются :(
a1ien_n3t
01.06.2016 14:37Продается же usb донгл который вставляется в PS4 и подключаем к нему стандартные BT гарнитуры.
Вот напримр первое нагугленное видеоMaxtws
02.06.2016 10:34Да, есть такая штука. Только частота дискретизации на выходе чуть ли не 8kHz. Пользоваться таким просто невозможно.
Я уже полгода ищу возможность подключить стороннюю Bluetooth гарнитуру. Но пока все сводится к опическому выходу с консоли (либо ТВ) на топовые беспроводные наушники с оптическим входом. Sony в этом плане крайне пропроетарны. Их родные беспроводные гарнитуры я не беру в расчет.
Sixshaman
01.06.2016 16:09+2Эх, а я уже думал, что автор создал хитрую систему, которая контролирует частоту вибрации геймпада, благодаря чему он издаёт разборчивый звук.
FirExpl
01.06.2016 19:02По поводу заикания звука:
Лично у меня это обычное явление для Dualshock 4. Особенно сильно проявляется если рядом активно работает WiFi устройство.
Ну, это как один из возможных вариантов :)ViceCily
02.06.2016 05:54Если я правильно помню, то переключение канала Wi-Fi с «auto» на «13» решит проблему пересечения частот с Bluetooth-устройствами.
Автору спасибо, только после этого поста узнал, что в Dualshock 4 можно подключать обычные стерео наушники и выводить все звуки PS4 на них, а не только чат!
a1ien_n3t
05.06.2016 16:20Обновил немного статью. Разобрался с заиканием и добавил строчку для захватывать всего что идет на аудио выход.
amdf
Давно этого ждал. Надеюсь, скоро данная функция появится в DS4Windows.