diff --git a/ds4drv/__main__.py b/ds4drv/__main__.py index fa915a2..f3601fb 100644 --- a/ds4drv/__main__.py +++ b/ds4drv/__main__.py @@ -9,10 +9,11 @@ from .daemon import Daemon from .eventloop import EventLoop from .exceptions import BackendError +from .audio import PulseaudioSBCStream class DS4Controller(object): - def __init__(self, index, options, dynamic=False): + def __init__(self, index, options, sbc_stream, dynamic=False): self.index = index self.dynamic = dynamic self.logger = Daemon.logger.new_module("controller {0}".format(index)) @@ -33,6 +34,8 @@ def __init__(self, index, options, dynamic=False): if self.profiles: self.profiles.append("default") + self.sbc_stream = sbc_stream + self.load_options(self.options) def fire_event(self, event, *args): @@ -120,8 +123,10 @@ def exit(self, *args, error = True): self.logger.info(*args) -def create_controller_thread(index, controller_options, dynamic=False): - controller = DS4Controller(index, controller_options, dynamic=dynamic) +def create_controller_thread(index, controller_options, sbc_stream, + dynamic=False): + controller = DS4Controller(index, controller_options, sbc_stream, + dynamic=dynamic) thread = Thread(target=controller.run) thread.controller = controller @@ -131,26 +136,37 @@ def create_controller_thread(index, controller_options, dynamic=False): class SigintHandler(object): - def __init__(self, threads): - self.threads = threads + def __init__(self, controller_threads, sbc_stream): + self.controller_threads = controller_threads + self.sbc_stream = sbc_stream - def cleanup_controller_threads(self): - for thread in self.threads: + def cleanup_controller_controller_threads(self): + for thread in self.controller_threads: thread.controller.exit("Cleaning up...", error=False) thread.controller.loop.stop() thread.join() + def cleanup_sbc_stream(self): + self.sbc_stream.stop() + def __call__(self, signum, frame): signal.signal(signum, signal.SIG_DFL) - self.cleanup_controller_threads() + self.cleanup_sbc_stream() + self.cleanup_controller_controller_threads() + sys.exit(0) def main(): - threads = [] + sbc_stream = PulseaudioSBCStream( + "ds4drv", "Test ds4drv sink" + ) + sbc_stream.run() + + controller_threads = [] - sigint_handler = SigintHandler(threads) + sigint_handler = SigintHandler(controller_threads, sbc_stream) signal.signal(signal.SIGINT, sigint_handler) try: @@ -172,12 +188,14 @@ def main(): Daemon.fork(options.daemon_log, options.daemon_pid) for index, controller_options in enumerate(options.controllers): - thread = create_controller_thread(index + 1, controller_options) - threads.append(thread) + thread = create_controller_thread( + index + 1, controller_options, sbc_stream + ) + controller_threads.append(thread) for device in backend.devices: connected_devices = [] - for thread in threads: + for thread in controller_threads: # Controller has received a fatal error, exit if thread.controller.error: sys.exit(1) @@ -185,22 +203,23 @@ def main(): if thread.controller.device: connected_devices.append(thread.controller.device.device_addr) - # Clean up dynamic threads + # Clean up dynamic controller_threads if not thread.is_alive(): - threads.remove(thread) + controller_threads.remove(thread) if device.device_addr in connected_devices: backend.logger.warning("Ignoring already connected device: {0}", device.device_addr) continue - for thread in filter(lambda t: not t.controller.device, threads): + for thread in filter(lambda t: not t.controller.device, controller_threads): break else: - thread = create_controller_thread(len(threads) + 1, + thread = create_controller_thread(len(controller_threads) + 1, options.default_controller, + sbc_stream, dynamic=True) - threads.append(thread) + controller_threads.append(thread) thread.controller.setup_device(device) diff --git a/ds4drv/actions/__init__.py b/ds4drv/actions/__init__.py index 2d37a0c..65ccab9 100644 --- a/ds4drv/actions/__init__.py +++ b/ds4drv/actions/__init__.py @@ -7,3 +7,4 @@ from . import input from . import led from . import status +from . import audio diff --git a/ds4drv/actions/audio.py b/ds4drv/actions/audio.py new file mode 100644 index 0000000..260e9da --- /dev/null +++ b/ds4drv/actions/audio.py @@ -0,0 +1,43 @@ +from ..action import Action +from ..audio import SBCHeaders +from ..audio import pulseaudio_sbc_stream + +import os + +class AudioAction(Action): + """Plays audio through the device""" + + def setup(self, device): + self.sbc_stream = self.controller.sbc_stream + self.loop = self.controller.loop + + self.read_pipe, self.write_pipe = os.pipe() + + self.sbc_stream.add_fd(self.write_pipe) + + self.loop.add_watcher(self.read_pipe, self.play_audio) + + + def disable(self): + self.sbc_stream.remove_fd(self.write_pipe) + + + def play_audio(self): + if not self.controller.device: + return + + # Read the SBC header + frame_header = os.read(self.read_pipe, 10) + + # Parse and find the length of the frame + sbc_header = SBCHeaders() + sbc_header.parse_header(frame_header) + sbc_len = sbc_header.calculate_frame_length() + + # Read the rese of the frame + rest_of_frame = os.read(self.read_pipe, sbc_len - 10) + + sbc_frame = frame_header + rest_of_frame + + # Call play_audio + self.controller.device.play_audio(sbc_header, sbc_frame) diff --git a/ds4drv/audio/__init__.py b/ds4drv/audio/__init__.py new file mode 100644 index 0000000..1db883c --- /dev/null +++ b/ds4drv/audio/__init__.py @@ -0,0 +1,2 @@ +from .sbc_headers import SBCHeaders +from .pulseaudio_sbc_stream import PulseaudioSBCStream diff --git a/ds4drv/audio/pulseaudio_sbc_stream.cc b/ds4drv/audio/pulseaudio_sbc_stream.cc new file mode 100644 index 0000000..10f50cd --- /dev/null +++ b/ds4drv/audio/pulseaudio_sbc_stream.cc @@ -0,0 +1,318 @@ +#include "pulseaudio_sbc_stream.hh" + +#include +#include +#include +#include + +#include + + +#include + + +void PulseaudioSBCStream::add_fd(int fd) { + this->fds.insert(fd); +} + +void PulseaudioSBCStream::remove_fd(int fd) { + this->fds.erase(fd); +} + +void PulseaudioSBCStream::read_pulse_stream( + pa_stream *s, std::size_t length, void *self_v +) { + Self* self = static_cast(self_v); + + sbc_t* sbc = &(self->audio_loop_sbc); + + std::size_t sbc_frame_length = sbc_get_frame_length(sbc); + std::size_t sbc_frame_buflen = 10*sbc_frame_length+10; + uint8_t sbc_frame_buf[sbc_frame_buflen]; + + std::size_t sbc_audio_length = sbc_get_codesize(sbc); + + while(pa_stream_readable_size(s) > 0) { + const char* data8 = NULL; + std::size_t length = 0; + + pa_stream_peek(s, reinterpret_cast(&data8), &length); + + self->audio_buffer.insert( + self->audio_buffer.end(), data8, data8+length + ); + + if(self->audio_buffer.size() >= sbc_audio_length) { + ssize_t written = 0; + + std::size_t read = sbc_encode( + sbc, + self->audio_buffer.linearize(), self->audio_buffer.size(), + sbc_frame_buf, sbc_frame_buflen, + &written + ); + + if(written > 0) { + + // Write frames to supplied file descriptors + for( + FDList::iterator fd_it = self->fds.begin(); + fd_it != self->fds.end(); + fd_it++ + ) { + write(*fd_it, sbc_frame_buf, written); + } + + self->audio_buffer.erase_begin(read); + } + } + + pa_stream_drop(s); + } +} + +void PulseaudioSBCStream::setup_pulse_stream( + pa_context* c, const pa_sink_info* i, int eol, void* self_v +) { + Self* self = static_cast(self_v); + + if(i && eol == 0 && i->owner_module == self->sink_module_id) { + + // Fix sbc encoder format + + // Endianness + if(i->sample_spec.format == PA_SAMPLE_S16BE) { + printf( + "[info][PulseaudioSBCStream] " + "Stream format s16be\n" + ); + self->audio_loop_sbc.endian = SBC_BE; + } else if(i->sample_spec.format == PA_SAMPLE_S16LE) { + printf( + "[info][PulseaudioSBCStream] " + "Stream format s16le\n" + ); + self->audio_loop_sbc.endian = SBC_LE; + } else { + printf( + "[error][PulseaudioSBCStream] " + "Unable to determine stream format\n" + ); + } + + // Sample rate + if(i->sample_spec.rate == 16000) { + printf( + "[info][PulseaudioSBCStream] " + "Stream sample rate 16000\n" + ); + self->audio_loop_sbc.frequency = SBC_FREQ_16000; + } else if(i->sample_spec.rate == 32000) { + printf( + "[info][PulseaudioSBCStream] " + "Stream sample rate 32000\n" + ); + self->audio_loop_sbc.frequency = SBC_FREQ_32000; + } + + // Some info + std::size_t sbc_codesize = sbc_get_codesize( + &(self->audio_loop_sbc) + ); + std::size_t sbc_frame_length = sbc_get_frame_length( + &(self->audio_loop_sbc) + ); + printf( + "[info][PulseaudioSBCStream] " + "Stream codesize: %zu\n", sbc_codesize + ); + printf( + "[info][PulseaudioSBCStream] " + "Stream frame_length: %zu\n", sbc_frame_length + ); + + + // Set up stream + pa_stream* stream = pa_stream_new( + c, self->sink_description.c_str(), &(i->sample_spec), NULL + ); + + pa_stream_set_read_callback(stream, read_pulse_stream, self_v); + + + pa_buffer_attr buffer_attr; + buffer_attr.maxlength = (uint32_t) -1; + buffer_attr.prebuf = (uint32_t) -1; + buffer_attr.fragsize = (uint32_t) -1; + buffer_attr.tlength = (uint32_t) -1; + buffer_attr.minreq = (uint32_t) -1; + buffer_attr.fragsize = pa_usec_to_bytes(4000, &(i->sample_spec)); + + pa_stream_flags_t flags = static_cast( + PA_STREAM_ADJUST_LATENCY + ); + + char device_strbuf[1024]; + snprintf(device_strbuf, 1024, "%s.monitor", i->name); + pa_stream_connect_record( + stream, device_strbuf, &buffer_attr, flags + ); + } +} + +void PulseaudioSBCStream::module_setup_cb( + pa_context* c, uint32_t idx, void* self_v +) { + Self* self = static_cast(self_v); + + self->sink_module_id = idx; + + pa_operation* op = pa_context_get_sink_info_list( + c, setup_pulse_stream, self_v + ); + if(op != NULL) pa_operation_unref(op); +} + +void PulseaudioSBCStream::context_state_cb(pa_context* c, void* self_v) { + Self* self = static_cast(self_v); + + // Context connected. Setup stream. + if(pa_context_get_state(c) == PA_CONTEXT_READY) { + printf("[info][PulseaudioSBCStream] Connecting to Pulseaudio\n"); + + Self* self = static_cast(self_v); + + // Build new sink_description string with spaces escaped. + std::string sanitized_description = self->sink_description; + std::size_t last_pos = 0; + std::size_t found = 0; + while( + (found = sanitized_description.find(' ', last_pos)) + != sanitized_description.npos + ) { + sanitized_description.replace(found, 1, "\\ "); + last_pos = found + 2; + } + char options_buf[1024]; + snprintf( + options_buf, 1024, + "rate=\"%d\" format=\"%s\" channels=\"%d\"" + "sink_name=\"%s\" sink_properties=device.description=\"%s\"", + self->sample_rate, pa_sample_format_to_string(PA_SAMPLE_S16NE), 2, + self->sink_name.c_str(), sanitized_description.c_str() + ); + pa_operation* op = pa_context_load_module( + c, "module-null-sink", options_buf, module_setup_cb, self_v + ); + if(op != NULL) pa_operation_unref(op); + } + + // Context failed. Assume pulse will restart and try reconnecting. + if(pa_context_get_state(c) == PA_CONTEXT_FAILED) { + printf( + "[info][PulseaudioSBCStream] Context failed. Reconnecting...\n" + ); + + unsigned int sleep_time = 1; + + // Try reconnecting every sleep_time seconds. + while(self->setup_context() < 0) { + sleep(sleep_time); + } + } +} + +void PulseaudioSBCStream::unload_module_success( + pa_context* c, int success, void* self_v +) { + Self* self = static_cast(self_v); + + pa_mainloop_api* api = pa_threaded_mainloop_get_api(self->mainloop); + api->quit(api, 20); + + printf("[info][PulseaudioSBCStream] Disconnect successful\n"); +} + +PulseaudioSBCStream::PulseaudioSBCStream( + std::string sink_name, + std::string sink_description +): + context(NULL), + sink_name(sink_name), + sink_description(sink_description), + sink_module_id(-1), + + sample_rate(32000), + + audio_buffer(512*100) +{ + sbc_init(&(this->audio_loop_sbc), 0); + + this->audio_loop_sbc.frequency = SBC_FREQ_32000; // Possibly reset later. + this->audio_loop_sbc.blocks = SBC_BLK_16; + this->audio_loop_sbc.subbands = SBC_SB_8; + this->audio_loop_sbc.mode = SBC_MODE_DUAL_CHANNEL; + this->audio_loop_sbc.allocation = SBC_AM_LOUDNESS; + this->audio_loop_sbc.bitpool = 25; + this->audio_loop_sbc.endian = SBC_BE; // Possibly reset later. + + pa_threaded_mainloop* mainloop = pa_threaded_mainloop_new(); + this->mainloop = mainloop; + + this->setup_context(); +} + +PulseaudioSBCStream::~PulseaudioSBCStream() { + this->stop(); + + pa_context_disconnect(this->context); + + sbc_finish(&(this->audio_loop_sbc)); +} + +int PulseaudioSBCStream::setup_context() { + pa_proplist* proplist = pa_proplist_new(); + pa_proplist_sets( + proplist, PA_PROP_DEVICE_STRING, this->sink_name.c_str() + ); + pa_proplist_sets( + proplist, PA_PROP_DEVICE_DESCRIPTION, this->sink_description.c_str() + ); + + pa_context* c = pa_context_new_with_proplist( + pa_threaded_mainloop_get_api(mainloop), + this->sink_name.c_str(), proplist + ); + + this->context = c; + + int err = pa_context_connect(c, NULL, PA_CONTEXT_NOFLAGS, NULL); + if(err >= 0) { + pa_context_set_state_callback(c, context_state_cb, this); + } + else { + printf("[error][PulseaudioSBCStream] Error connecting context\n"); + } + + return err; +} + +void PulseaudioSBCStream::run() { + int err = pa_threaded_mainloop_start(this->mainloop); + if(err < 0) { + printf("[error][PulseaudioSBCStream] Error starting mainloop\n"); + } +} + +void PulseaudioSBCStream::stop() { + printf("[info][PulseaudioSBCStream] Disconnecting from Pulseaudio\n"); + if(this->sink_module_id > 0) { + pa_operation* op = pa_context_unload_module( + this->context, this->sink_module_id, + unload_module_success, this + ); + if(op != NULL) pa_operation_unref(op); + } else { + unload_module_success(this->context, 0, this); + } +} diff --git a/ds4drv/audio/pulseaudio_sbc_stream.hh b/ds4drv/audio/pulseaudio_sbc_stream.hh new file mode 100644 index 0000000..05b2ecb --- /dev/null +++ b/ds4drv/audio/pulseaudio_sbc_stream.hh @@ -0,0 +1,65 @@ +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include + +#include + +class PulseaudioSBCStream { +public: + typedef PulseaudioSBCStream Self; + + pa_context* context; + pa_threaded_mainloop* mainloop; + + std::string sink_name; + std::string sink_description; + uint32_t sink_module_id; + + uint32_t sample_rate; + + sbc_t audio_loop_sbc; + boost::circular_buffer audio_buffer; + + typedef std::set FDList; + FDList fds; + + void add_fd(int fd); + void remove_fd(int fd); + + + static void read_pulse_stream( + pa_stream *s, std::size_t length, void *self_v + ); + + static void setup_pulse_stream( + pa_context* c, const pa_sink_info* i, int eol, void* self_v + ); + + static void module_setup_cb(pa_context* c, uint32_t idx, void* self_v); + + static void context_state_cb(pa_context* c, void* self_v); + + static void unload_module_success(pa_context* c, int success, void* self_v); + + PulseaudioSBCStream( + std::string sink_name, + std::string sink_description + ); + + ~PulseaudioSBCStream(); + + int setup_context(); + + void run(); + + void stop(); +}; diff --git a/ds4drv/audio/pulseaudio_sbc_stream.i b/ds4drv/audio/pulseaudio_sbc_stream.i new file mode 100644 index 0000000..3743bbd --- /dev/null +++ b/ds4drv/audio/pulseaudio_sbc_stream.i @@ -0,0 +1,26 @@ +%module pulseaudio_sbc_stream + +/* + * Include SWIG typemaps for std::string +*/ +%include +%include +%include +%include + +/* + * Include files and definitions to be written verbatim to _wrap.cpp + */ +%{ +#include "audio/pulseaudio_sbc_stream.hh" +%} + +%include "audio/pulseaudio_sbc_stream.hh" + + +/* + * Make a CharArray and size_t_p type for getting SBC frame data from + * PulseaudioSBCStream::read_sbc_frame. + */ +%pointer_class(std::size_t, size_t_p); +%array_class(unsigned char, CharArray); diff --git a/ds4drv/audio/sbc_headers.py b/ds4drv/audio/sbc_headers.py new file mode 100644 index 0000000..28d77d2 --- /dev/null +++ b/ds4drv/audio/sbc_headers.py @@ -0,0 +1,244 @@ +class SBCHeaders(object): + + MONO = 0 + DUAL_CHANNEL = 1 + STEREO = 2 + JOINT_STEREO = 3 + + + def __init__( + self, + sampling_frequency = 32000, + bitpool = 50, + channel_mode = None, + nrof_blocks = 16, + nrof_subbands = 8 + ): + if channel_mode == None: + channel_mode = SBCHeaders.STEREO + + self.syncword = 156 + + self.nrof_subbands = nrof_subbands + self.channel_mode = channel_mode + self.nrof_channels = 2 + if self.channel_mode == SBCHeaders.MONO: + self.nrof_channels = 1 + self.nrof_blocks = nrof_blocks + self.join = 0 + if self.channel_mode == SBCHeaders.JOINT_STEREO: + self.join = 1 + self.bitpool = bitpool + self.sampling_frequency = sampling_frequency + + + self.frame_length = None + self.bitrate = None + + + def calculate_frame_length(self): + # Calculate frame length + def ceildiv(a, b): + return -(-a // b) + + if ( + (self.channel_mode == SBCHeaders.MONO) + or (self.channel_mode == + SBCHeaders.DUAL_CHANNEL) + ): + + self.frame_length = ( + 4 + ( + 4 + * self.nrof_subbands + * self.nrof_channels + )//8 + + ceildiv( + self.nrof_blocks + * self.nrof_channels + * self.bitpool, + 8 + ) + ) + else: + self.frame_length = ( + 4 + ( + 4 + * self.nrof_subbands + * self.nrof_channels + )//8 + + ceildiv( + self.join + * self.nrof_subbands + + self.nrof_blocks + * self.bitpool, + 8 + ) + ) + + return self.frame_length + + + def calculate_bit_rate(self): + if self.frame_length == None: + self.calculate_frame_length() + + # Calculate bit rate + self.bit_rate = ( + 8 * self.frame_length * self.sampling_frequency + // self.nrof_subbands // self.nrof_blocks + ) + + return self.bit_rate + + + def parse_header(self, raw_header): + # Info in SBC headers from + # https://tools.ietf.org/html/draft-ietf-avt-rtp-sbc-01#section-6.3 + + # Syncword should be 0x9C + self.syncword = raw_header[0] + + self.nrof_subbands = \ + SBCHeaders.parse_number_of_subbands( + raw_header + ) + self.channel_mode = SBCHeaders.parse_channel_mode( + raw_header + ) + self.nrof_channels = 2 + if self.channel_mode == SBCHeaders.MONO: + self.nrof_channels = 1 + self.nrof_blocks = SBCHeaders.parse_number_of_blocks( + raw_header + ) + self.join = 0 + if self.channel_mode == SBCHeaders.JOINT_STEREO: + self.join = 1 + self.nrof_subbands = \ + SBCHeaders.parse_number_of_subbands( + raw_header + ) + self.bitpool = SBCHeaders.parse_bitpool(raw_header) + self.sampling_frequency = SBCHeaders.parse_sampling( + raw_header + ) + + + def print_values(self): + # Info in SBC headers from + # https://tools.ietf.org/html/draft-ietf-avt-rtp-sbc-01#section-6.3 + + print("syncword: ", self.syncword) + + print("nrof_subbands", self.nrof_subbands) + print("channel_mode", [ + "MONO", "DUAL_CHANNEL", "STEREO", "JOINT_STEREO" + ][self.channel_mode] + ) + print("nrof_channels", self.nrof_channels) + print("nrof_blocks", self.nrof_blocks) + print("join: ", self.join) + print("nrof_subbands", self.nrof_subbands) + print("bitpool", self.bitpool) + print("sampling_frequency", self.sampling_frequency) + print("frame_length", self.frame_length) + print("bit_rate", self.bit_rate) + + + @staticmethod + def parse_sampling(raw_header): + + sf_word = raw_header[1] + + # Find sampling frequency from rightmost 2 bits + if sf_word & 0x80 == 0x80: + bit_0 = 1 + else: + bit_0 = 0 + + if sf_word & 0x40 == 0x40: + bit_1 = 1 + else: + bit_1 = 0 + + if (bit_0 == 0) and (bit_1 == 0): + sampling_frequency = 16000 + elif (bit_0 == 0) and (bit_1 == 1): + sampling_frequency = 32000 + elif (bit_0 == 1) and (bit_1 == 0): + sampling_frequency = 44100 + elif (bit_0 == 1) and (bit_1 == 1): + sampling_frequency = 48000 + + return sampling_frequency + + + @staticmethod + def parse_number_of_blocks(raw_header): + + nb_word = raw_header[1] + + if nb_word & 0x20 == 0x20: + bit_0 = 1 + else: + bit_0 = 0 + + if nb_word & 0x10 == 0x10: + bit_1 = 1 + else: + bit_1 = 0 + + + if (bit_0 == 0) and (bit_1 == 0): + number_of_blocks = 4 + elif (bit_0 == 0) and (bit_1 == 1): + number_of_blocks = 8 + elif (bit_0 == 1) and (bit_1 == 0): + number_of_blocks = 12 + elif (bit_0 == 1) and (bit_1 == 1): + number_of_blocks = 16 + + return number_of_blocks + + + @staticmethod + def parse_channel_mode(raw_header): + + ch_word = raw_header[1] + + if ch_word & 0x08 == 0x08: + bit_0 = 1 + else: + bit_0 = 0 + + if ch_word & 0x04 == 0x04: + bit_1 = 1 + else: + bit_1 = 0 + + if (bit_0 == 0) and (bit_1 == 0): + channel_mode = SBCHeaders.MONO + elif (bit_0 == 0) and (bit_1 == 1): + channel_mode = SBCHeaders.DUAL_CHANNEL + elif (bit_0 == 1) and (bit_1 == 0): + channel_mode = SBCHeaders.STEREO + elif (bit_0 == 1) and (bit_1 == 1): + channel_mode = SBCHeaders.JOINT_STEREO + + return channel_mode + + + @staticmethod + def parse_number_of_subbands(raw_header): + if raw_header[1] & 0x01 == 0x01: + number_of_subbands = 8 + else: + number_of_subbands = 4 + + return number_of_subbands + + + @staticmethod + def parse_bitpool(raw_header): + return int(raw_header[2]) diff --git a/ds4drv/backends/hidraw.py b/ds4drv/backends/hidraw.py index ae1cd04..56c00ce 100644 --- a/ds4drv/backends/hidraw.py +++ b/ds4drv/backends/hidraw.py @@ -1,6 +1,11 @@ import fcntl import itertools import os +import struct +import signal + +import signal +from multiprocessing import Pool from io import FileIO from time import sleep @@ -19,11 +24,74 @@ HIDIOCGFEATURE = lambda size: IOC_RW | (0x07 << 0) | (size << 16) +class HidrawWriter: + def __init__(self, hidraw_device, *args, **kwargs): + super(HidrawWriter, self).__init__(*args, **kwargs) + + self.hidraw_device = hidraw_device + + self.write_pool = Pool( + processes = 1, + initializer = HidrawWriter.pool_init, initargs = (hidraw_device,) + ) + + @staticmethod + def pool_init(hidraw_device): + # Signals have been inherited from the parent. In particular, + # the cleanup signals from __main__. Signals are completely ignored + # here since Pool hangs otherwise. + signal.signal(signal.SIGINT, signal.SIG_IGN) + + HidrawWriter.report_fd = os.open( + hidraw_device, + os.O_WRONLY | os.O_SYNC + ) + + @staticmethod + def pool_close_fds(): + pass + + @staticmethod + def sigalrm_handler(signum, frame): + raise TimeoutError + + @staticmethod + def pool_write(data, timeout): + try: + if timeout != None: + old_sigalrm_handler = signal.getsignal(signal.SIGALRM) + signal.signal(signal.SIGALRM, HidrawWriter.sigalrm_handler) + signal.setitimer(signal.ITIMER_REAL, timeout) + + os.write(HidrawWriter.report_fd, data) + + except TimeoutError: + pass + + except OSError: + pass + + finally: + if timeout != None: + signal.setitimer(signal.ITIMER_REAL, 0) + signal.signal(signal.SIGALRM, old_sigalrm_handler) + + def write(self, data, timeout = None): + return self.write_pool.apply(HidrawWriter.pool_write, (data, timeout)) + + def close(self): + self.write_pool.apply(HidrawWriter.pool_close_fds) + self.write_pool.close() + self.write_pool.join() + + class HidrawDS4Device(DS4Device): def __init__(self, name, addr, type, hidraw_device, event_device): try: - self.report_fd = os.open(hidraw_device, os.O_RDWR | os.O_NONBLOCK) + self.hidraw_writer = HidrawWriter(hidraw_device) + self.report_fd = os.open(hidraw_device, os.O_RDONLY | os.O_NONBLOCK) self.fd = FileIO(self.report_fd, "rb+", closefd=False) + self.input_device = InputDevice(event_device) self.input_device.grab() except (OSError, IOError) as err: @@ -62,17 +130,18 @@ def read_feature_report(self, report_id, size): return fcntl.ioctl(self.fd, op, bytes(buf)) - def write_report(self, report_id, data): - if self.type == "bluetooth": - # TODO: Add a check for a kernel that supports writing - # output reports when such a kernel has been released. - return - hid = bytearray((report_id,)) - self.fd.write(hid + data) + def write_report(self, report_id, data, timeout = None): + try: + hid = bytearray((report_id,)) + self.hidraw_writer.write(hid + data, timeout = timeout) + except TimeoutError: + pass + def close(self): try: + self.hidraw_writer.close() self.fd.close() self.input_device.ungrab() except IOError: @@ -85,9 +154,46 @@ class HidrawBluetoothDS4Device(HidrawDS4Device): report_size = 78 valid_report_id = 0x11 + audio_buffer_size = 448 + audio_buffer = b'' + frame_number = 0 + def set_operational(self): self.read_feature_report(0x02, 37) + def increment_frame_number(self, inc): + self.frame_number += inc + if self.frame_number > 0xffff: + self.frame_number = 0 + + def play_audio(self, headers, data): + if len(self.audio_buffer) + len(data) <= self.audio_buffer_size: + self.audio_buffer += data + return + + crc = b'\x00\x00\x00\x00' + audio_header = b'\x24' + + self.increment_frame_number(4) + + report_id = 0x17 + report = ( + b'\x40\xA0' + + struct.pack("=0.3.0", "pyudev>=0.16"], classifiers=[ "Development Status :: 4 - Beta",