Выбирая геймпад для своего компьютера, я остановился на 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

И у нас получилось (почти). Звук идет, но периодически слышны небольшие заикания. С чем они связаны, я понять так и не смог. Возможно, я не совсем правильно работаю с hid устройством в linux — если кто-нибудь сможет подсказать, как сделать правильнее, я буду благодарен. Попытка испопользования Bluetooth сокета успехом также не увенчалась — через полсекунды проигрывания звука всё заканчивалось(Смотри UPD).

Заключение


Хотелось бы выразить благодарность таким проектам, как 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)


  1. amdf
    01.06.2016 11:29
    +1

    Давно этого ждал. Надеюсь, скоро данная функция появится в DS4Windows.


  1. f0rmat1k
    01.06.2016 12:41
    +1

    Привет. А вы донесли результаты своих исследований до авторов проектов? Может имеет смысл статью перевести на англ. и им создать ишью?


    1. a1ien_n3t
      01.06.2016 12:44
      +1

      Да я и планировал в ближайшее время так поступить.


  1. syrompe
    01.06.2016 13:24

    Этот геймпад еще и отправлять звук умеет, если к ему гарнитуру с микрофоном подключить.
    Также в самом геймпаде есть небольшой динамик чтоб без отдельных наушников работал (так себе динамик конечно)

    Кстати, идейка, на ебее продаются геймпады с отломанными кнопками задешего. Выдрать из них платку и понаделать себе «безпроводных» гарнитур…


    1. jonic
      01.06.2016 13:51

      смотря на сколько дешего, не дешевле ли с али готовое?


      1. syrompe
        01.06.2016 14:57

        ну наберите в поиске ебея «dualshock 4 broken».
        4 штуки за тысячу рублей выдает сходу…

        Еще учтите что помимо самой электроники там еще будет неплохая литиевая батарейка с зарядкой от USB.


    1. ViceCily
      02.06.2016 05:56

      А для чего используется встроенный в геймпад динамик и как его задейстовать?


      1. a1ien_n3t
        02.06.2016 10:40

        Ну на него можно воспроизводить любой звук. А где вы хотите его задействовать, с PS4 или PC?


        1. ViceCily
          02.06.2016 12:10

          С PS4. Консоль купил, но я не игроман. Запускал пару раз Assassin's Creed и целенаправленно прошел Uncharted 4. За все это время даже не догадывался, что в геймпаде есть динамик, он себя никак не проявлял. Кажется уже нашел мануал официальный, как его проверить. Спасибо!


          1. Oslegg
            02.06.2016 22:34

            Для PS4 есть игры которые используют этот динамик. Например игра Zombies использует динамик контроллера как персональную рацию. Тоесть все звуки идут через телевизор (или звук. систему если есть) а вот рация только с контроллера.

            Так-же в мультиплеере большинства игр VoIP идёт через контроллер.


  1. foxyrus
    01.06.2016 14:14

    Эх вот бы прикрутить к PS4 обычные bluetooth гарнитуры для передачи звука с приставки. Обычные гарнитуры видны в ps4, но не подключаются :(


    1. a1ien_n3t
      01.06.2016 14:37

      Продается же usb донгл который вставляется в PS4 и подключаем к нему стандартные BT гарнитуры.
      Вот напримр первое нагугленное видео


      1. Maxtws
        02.06.2016 10:34

        Да, есть такая штука. Только частота дискретизации на выходе чуть ли не 8kHz. Пользоваться таким просто невозможно.
        Я уже полгода ищу возможность подключить стороннюю Bluetooth гарнитуру. Но пока все сводится к опическому выходу с консоли (либо ТВ) на топовые беспроводные наушники с оптическим входом. Sony в этом плане крайне пропроетарны. Их родные беспроводные гарнитуры я не беру в расчет.


  1. Sixshaman
    01.06.2016 16:09
    +2

    Эх, а я уже думал, что автор создал хитрую систему, которая контролирует частоту вибрации геймпада, благодаря чему он издаёт разборчивый звук.


  1. FirExpl
    01.06.2016 19:02

    По поводу заикания звука:
    Лично у меня это обычное явление для Dualshock 4. Особенно сильно проявляется если рядом активно работает WiFi устройство.
    Ну, это как один из возможных вариантов :)


    1. ViceCily
      02.06.2016 05:54

      Если я правильно помню, то переключение канала Wi-Fi с «auto» на «13» решит проблему пересечения частот с Bluetooth-устройствами.
      Автору спасибо, только после этого поста узнал, что в Dualshock 4 можно подключать обычные стерео наушники и выводить все звуки PS4 на них, а не только чат!


  1. a1ien_n3t
    05.06.2016 16:20

    Обновил немного статью. Разобрался с заиканием и добавил строчку для захватывать всего что идет на аудио выход.