diff --git a/hw/xbox/meson.build b/hw/xbox/meson.build index 907ebdc4e8..b8199e55a8 100644 --- a/hw/xbox/meson.build +++ b/hw/xbox/meson.build @@ -15,6 +15,7 @@ specific_ss.add(files( 'xbox.c', 'xbox_pci.c', 'xid.c', + 'xblc.c', )) subdir('nv2a') subdir('mcpx') diff --git a/hw/xbox/xblc.c b/hw/xbox/xblc.c new file mode 100644 index 0000000000..60badb10a7 --- /dev/null +++ b/hw/xbox/xblc.c @@ -0,0 +1,402 @@ +/* + * QEMU USB Xbox Live Communicator (XBLC) Device + * + * Copyright (c) 2022 Ryan Wendland + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, see . + */ + +#include "qemu/osdep.h" +#include "hw/qdev-properties.h" +#include "migration/vmstate.h" +#include "sysemu/sysemu.h" +#include "hw/hw.h" +#include "ui/console.h" +#include "hw/usb.h" +#include "hw/usb/desc.h" +#include "ui/xemu-input.h" +#include "audio/audio.h" +#include "qemu/fifo8.h" + +//#define DEBUG_XBLC +#ifdef DEBUG_XBLC +#define DPRINTF printf +#else +#define DPRINTF(...) +#endif + +#define TYPE_USB_XBLC "usb-xblc" +#define USB_XBLC(obj) OBJECT_CHECK(USBXBLCState, (obj), TYPE_USB_XBLC) + +#define XBLC_STR "Microsoft Xbox Live Communicator" +#define XBLC_INTERFACE_CLASS 0x78 +#define XBLC_INTERFACE_SUBCLASS 0x00 +#define XBLC_EP_OUT 0x04 +#define XBLC_EP_IN 0x05 + +#define XBLC_SET_SAMPLE_RATE 0x00 +#define XBLC_SET_AGC 0x01 + +#define XBLC_MAX_PACKET 48 +#define XBLC_FIFO_SIZE (XBLC_MAX_PACKET * 100) //~100 ms worth of audio at 16bit 24kHz + +static const uint8_t silence[256] = {0}; + +static const uint16_t xblc_sample_rates[5] = { + 8000, 11025, 16000, 22050, 24000 +}; + +typedef struct USBXBLCState { + USBDevice dev; + uint8_t device_index; + uint8_t auto_gain_control; + uint16_t sample_rate; + + QEMUSoundCard card; + struct audsettings as; + + struct { + SWVoiceOut* voice; + uint8_t packet[XBLC_MAX_PACKET]; + Fifo8 fifo; + } out; + + struct { + SWVoiceIn *voice; + uint8_t packet[XBLC_MAX_PACKET]; + Fifo8 fifo; + } in; +} USBXBLCState; + +enum { + STR_MANUFACTURER = 1, + STR_PRODUCT, + STR_SERIALNUMBER, +}; + +static const USBDescStrings desc_strings = { + [STR_MANUFACTURER] = "xemu", + [STR_PRODUCT] = XBLC_STR, + [STR_SERIALNUMBER] = "1", +}; + +static const USBDescIface desc_iface[]= { + { + .bInterfaceNumber = 0, + .bNumEndpoints = 1, + .bInterfaceClass = XBLC_INTERFACE_CLASS, + .bInterfaceSubClass = XBLC_INTERFACE_SUBCLASS, + .bInterfaceProtocol = 0x00, + .eps = (USBDescEndpoint[]) { + { + .bEndpointAddress = USB_DIR_OUT | XBLC_EP_OUT, + .bmAttributes = USB_ENDPOINT_XFER_ISOC, + .wMaxPacketSize = XBLC_MAX_PACKET, + .is_audio = 1, + .bInterval = 1, + .bRefresh = 0, + .bSynchAddress = 0, + } + }, + }, + { + .bInterfaceNumber = 1, + .bNumEndpoints = 1, + .bInterfaceClass = XBLC_INTERFACE_CLASS, + .bInterfaceSubClass = XBLC_INTERFACE_SUBCLASS, + .bInterfaceProtocol = 0x00, + .eps = (USBDescEndpoint[]) { + { + .bEndpointAddress = USB_DIR_IN | XBLC_EP_IN, + .bmAttributes = USB_ENDPOINT_XFER_ISOC, + .wMaxPacketSize = XBLC_MAX_PACKET, + .is_audio = 1, + .bInterval = 1, + .bRefresh = 0, + .bSynchAddress = 0, + } + }, + } +}; + +static const USBDescDevice desc_device = { + .bcdUSB = 0x0110, + .bMaxPacketSize0 = 8, + .bNumConfigurations = 1, + .confs = (USBDescConfig[]) { + { + .bNumInterfaces = 2, + .bConfigurationValue = 1, + .bmAttributes = USB_CFG_ATT_ONE, + .bMaxPower = 100, + .nif = ARRAY_SIZE(desc_iface), + .ifs = desc_iface, + }, + }, +}; + +static const USBDesc desc_xblc = { + .id = { + .idVendor = 0x045e, + .idProduct = 0x0283, + .bcdDevice = 0x0110, + .iManufacturer = STR_MANUFACTURER, + .iProduct = STR_PRODUCT, + .iSerialNumber = STR_SERIALNUMBER, + }, + .full = &desc_device, + .str = desc_strings, +}; + +static void usb_xblc_handle_reset(USBDevice *dev) +{ + USBXBLCState *s = (USBXBLCState *)dev; + + DPRINTF("[XBLC] Reset\n"); + fifo8_reset(&s->in.fifo); + fifo8_reset(&s->out.fifo); +} + +static void output_callback(void *opaque, int avail) +{ + USBXBLCState *s = (USBXBLCState *)opaque; + const uint8_t *data; + uint32_t processed, max_len; + + // Not enough data to send, wait a bit longer, fill with silence for now + if (fifo8_num_used(&s->out.fifo) < XBLC_MAX_PACKET) { + do { + processed = AUD_write(s->out.voice, (void *)silence, ARRAY_SIZE(silence)); + avail -= processed; + } while (avail > 0 && processed >= XBLC_MAX_PACKET); + return; + } + + // Write speaker data into audio backend + while (avail > 0 && !fifo8_is_empty(&s->out.fifo)) { + max_len = MIN(fifo8_num_used(&s->out.fifo), avail); + data = fifo8_pop_bufptr(&s->out.fifo, max_len, &max_len); + processed = AUD_write(s->out.voice, (void *)data, max_len); + avail -= processed; + if (processed < max_len) return; + } +} + +static void input_callback(void *opaque, int avail) +{ + USBXBLCState *s = (USBXBLCState *)opaque; + uint32_t processed, max_len; + + // Get microphone data from audio backend + while (avail > 0 && !fifo8_is_full(&s->in.fifo)) { + max_len = MIN(sizeof(s->in.packet), fifo8_num_free(&s->in.fifo)); + processed = AUD_read(s->in.voice, s->in.packet, max_len); + avail -= processed; + fifo8_push_all(&s->in.fifo, s->in.packet, processed); + if (processed < max_len) return; + } + + // Flush excess/old data - this can happen if the user program stops the iso transfers after it + // has setup the xblc. + while (avail > 0) + { + processed = AUD_read(s->in.voice, s->in.packet, XBLC_MAX_PACKET); + avail -= processed; + if (processed == 0) break; + } +} + +static void xblc_audio_stream_init(USBDevice *dev, uint16_t sample_rate) +{ + USBXBLCState *s = (USBXBLCState *)dev; + + AUD_set_active_out(s->out.voice, FALSE); + AUD_set_active_in(s->in.voice, FALSE); + + fifo8_reset(&s->in.fifo); + fifo8_reset(&s->out.fifo); + + s->as.freq = sample_rate; + s->as.nchannels = 1; + s->as.fmt = AUDIO_FORMAT_S16; + s->as.endianness = 0; + + s->out.voice = AUD_open_out(&s->card, s->out.voice, TYPE_USB_XBLC "-speaker", + s, output_callback, &s->as); + + s->in.voice = AUD_open_in(&s->card, s->in.voice, TYPE_USB_XBLC "-microphone", + s, input_callback, &s->as); + + AUD_set_active_out(s->out.voice, TRUE); + AUD_set_active_in(s->in.voice, TRUE); + DPRINTF("[XBLC] Init audio streams at %d Hz\n", sample_rate); +} + +static void usb_xblc_handle_control(USBDevice *dev, USBPacket *p, + int request, int value, int index, int length, uint8_t *data) +{ + USBXBLCState *s = (USBXBLCState *)dev; + + if (usb_desc_handle_control(dev, p, request, value, index, length, data) >= 0) { + DPRINTF("[XBLC] USB Control request handled by usb_desc_handle_control\n"); + return; + } + + switch (request) { + case VendorInterfaceOutRequest | USB_REQ_SET_FEATURE: + if (index == XBLC_SET_SAMPLE_RATE) + { + uint8_t rate = value & 0xFF; + assert(rate < ARRAY_SIZE(xblc_sample_rates)); + DPRINTF("[XBLC] Set Sample Rate to %04x\n", rate); + s->sample_rate = xblc_sample_rates[rate]; + xblc_audio_stream_init(dev, s->sample_rate); + break; + } + else if (index == XBLC_SET_AGC) + { + DPRINTF("[XBLC] Set Auto Gain Control to %d\n", value); + s->auto_gain_control = (value) ? 1 : 0; + break; + } + // Fallthrough + default: + DPRINTF("[XBLC] USB stalled on request 0x%x value 0x%x\n", request, value); + p->status = USB_RET_STALL; + assert(false); + return; + } +} + +static void usb_xblc_handle_data(USBDevice *dev, USBPacket *p) +{ + USBXBLCState *s = (USBXBLCState *)dev; + uint32_t to_process, chunk_len; + + switch (p->pid) { + case USB_TOKEN_IN: + // Microphone Data - Get data from fifo and copy into usb packet + assert(p->ep->nr == XBLC_EP_IN); + to_process = MIN(fifo8_num_used(&s->in.fifo), p->iov.size); + chunk_len = 0; + + // fifo may not give us a contiguous packet, so may need multiple calls + while (to_process) { + const uint8_t *packet = fifo8_pop_bufptr(&s->in.fifo, to_process, &chunk_len); + usb_packet_copy(p, (void *)packet, chunk_len); + to_process -= chunk_len; + } + + break; + case USB_TOKEN_OUT: + // Speaker data - get data from usb packet then push to fifo. + assert(p->ep->nr == XBLC_EP_OUT); + to_process = MIN(fifo8_num_free(&s->out.fifo), p->iov.size); + usb_packet_copy(p, s->out.packet, to_process); + fifo8_push_all(&s->out.fifo, s->out.packet, to_process); + + break; + default: + //Iso cannot report STALL/HALT, but we shouldn't be here anyway. + assert(false); + break; + } + + // Ensure we fill the entire packet regardless of if we have audio data so we don't + // cause an underrun error. + if (p->actual_length < p->iov.size) + usb_packet_copy(p, (void *)silence, p->iov.size - p->actual_length); + +} + +static void usb_xbox_communicator_unrealize(USBDevice *dev) +{ + USBXBLCState *s = USB_XBLC(dev); + + AUD_set_active_out(s->out.voice, false); + AUD_set_active_in(s->in.voice, false); + + fifo8_destroy(&s->out.fifo); + fifo8_destroy(&s->in.fifo); + + AUD_close_out(&s->card, s->out.voice); + AUD_close_in(&s->card, s->in.voice); + AUD_remove_card(&s->card); +} + +static void usb_xblc_class_initfn(ObjectClass *klass, void *data) +{ + USBDeviceClass *uc = USB_DEVICE_CLASS(klass); + uc->handle_reset = usb_xblc_handle_reset; + uc->handle_control = usb_xblc_handle_control; + uc->handle_data = usb_xblc_handle_data; + uc->handle_attach = usb_desc_attach; +} + +static void usb_xbox_communicator_realize(USBDevice *dev, Error **errp) +{ + USBXBLCState *s = USB_XBLC(dev); + usb_desc_create_serial(dev); + usb_desc_init(dev); + AUD_register_card(TYPE_USB_XBLC, &s->card, errp); + + fifo8_create(&s->in.fifo, XBLC_FIFO_SIZE); + fifo8_create(&s->out.fifo, XBLC_FIFO_SIZE); +} + +static Property xblc_properties[] = { + DEFINE_PROP_UINT8("index", USBXBLCState, device_index, 0), + DEFINE_PROP_END_OF_LIST(), +}; + +static const VMStateDescription usb_xblc_vmstate = { + .name = TYPE_USB_XBLC, + .version_id = 1, + .minimum_version_id = 1, + .fields = (VMStateField[]) { + VMSTATE_USB_DEVICE(dev, USBXBLCState), + // FIXME + VMSTATE_END_OF_LIST() + }, +}; + +static void usb_xbox_communicator_class_initfn(ObjectClass *klass, void *data) +{ + DeviceClass *dc = DEVICE_CLASS(klass); + USBDeviceClass *uc = USB_DEVICE_CLASS(klass); + + uc->product_desc = XBLC_STR; + uc->usb_desc = &desc_xblc; + uc->realize = usb_xbox_communicator_realize; + uc->unrealize = usb_xbox_communicator_unrealize; + usb_xblc_class_initfn(klass, data); + set_bit(DEVICE_CATEGORY_INPUT, dc->categories); + dc->vmsd = &usb_xblc_vmstate; + device_class_set_props(dc, xblc_properties); + dc->desc = XBLC_STR; +} + +static const TypeInfo info_xblc = { + .name = TYPE_USB_XBLC, + .parent = TYPE_USB_DEVICE, + .instance_size = sizeof(USBXBLCState), + .class_init = usb_xbox_communicator_class_initfn, +}; + +static void usb_xblc_register_types(void) +{ + type_register_static(&info_xblc); +} + +type_init(usb_xblc_register_types)