diff --git a/config_spec.yml b/config_spec.yml index d013c806ac..bc7655a5c9 100644 --- a/config_spec.yml +++ b/config_spec.yml @@ -33,21 +33,73 @@ input: port1: peripheral_type_0: integer peripheral_param_0: string + xblc1_settings: + output_device_name: + type: string + default: "Default" + output_device_volume: + type: number + default: 100.0 + input_device_name: + type: string + default: "Default" + input_device_volume: + type: number + default: 100.0 peripheral_type_1: integer peripheral_param_1: string port2: peripheral_type_0: integer peripheral_param_0: string + xblc2_settings: + output_device_name: + type: string + default: "Default" + output_device_volume: + type: number + default: 100.0 + input_device_name: + type: string + default: "Default" + input_device_volume: + type: number + default: 100.0 peripheral_type_1: integer peripheral_param_1: string port3: peripheral_type_0: integer peripheral_param_0: string + xblc3_settings: + output_device_name: + type: string + default: "Default" + output_device_volume: + type: number + default: 100.0 + input_device_name: + type: string + default: "Default" + input_device_volume: + type: number + default: 100.0 peripheral_type_1: integer peripheral_param_1: string port4: peripheral_type_0: integer peripheral_param_0: string + xblc4_settings: + output_device_name: + type: string + default: "Default" + output_device_volume: + type: number + default: 100.0 + input_device_name: + type: string + default: "Default" + input_device_volume: + type: number + default: 100.0 peripheral_type_1: integer peripheral_param_1: string gamecontrollerdb_path: string diff --git a/data/meson.build b/data/meson.build index 4a82350d44..ec28581d61 100644 --- a/data/meson.build +++ b/data/meson.build @@ -2,6 +2,7 @@ pfiles = [ 'controller_mask.png', 'controller_mask_s.png', 'xmu_mask.png', + 'xblc_mask.png', 'logo_sdf.png', 'xemu_64x64.png', 'abxy.ttf', diff --git a/data/xblc_mask.png b/data/xblc_mask.png new file mode 100644 index 0000000000..0896d9f0b2 Binary files /dev/null and b/data/xblc_mask.png differ diff --git a/data/xblc_mask.svg b/data/xblc_mask.svg new file mode 100644 index 0000000000..988e0698aa --- /dev/null +++ b/data/xblc_mask.svg @@ -0,0 +1,66 @@ + + + Layer 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/hw/xbox/xblc.c b/hw/xbox/xblc.c index 60badb10a7..eb7dc51d25 100644 --- a/hw/xbox/xblc.c +++ b/hw/xbox/xblc.c @@ -2,6 +2,7 @@ * QEMU USB Xbox Live Communicator (XBLC) Device * * Copyright (c) 2022 Ryan Wendland + * Copyright (c) 2025 faha223 * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -26,8 +27,8 @@ #include "hw/usb.h" #include "hw/usb/desc.h" #include "ui/xemu-input.h" -#include "audio/audio.h" #include "qemu/fifo8.h" +#include "xblc.h" //#define DEBUG_XBLC #ifdef DEBUG_XBLC @@ -51,32 +52,32 @@ #define XBLC_MAX_PACKET 48 #define XBLC_FIFO_SIZE (XBLC_MAX_PACKET * 100) //~100 ms worth of audio at 16bit 24kHz +#define NULL_DEFAULT(a, b) (a == NULL ? b : a) + static const uint8_t silence[256] = {0}; static const uint16_t xblc_sample_rates[5] = { 8000, 11025, 16000, 22050, 24000 }; +typedef struct XBLCStream { + char *device_name; + SDL_AudioDeviceID voice; + SDL_AudioSpec spec; + uint8_t packet[XBLC_MAX_PACKET]; + Fifo8 fifo; + int volume; + int peak_volume; +} XBLCStream; + 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; + XBLCStream out; + XBLCStream in; } USBXBLCState; enum { @@ -164,85 +165,228 @@ static void usb_xblc_handle_reset(USBDevice *dev) USBXBLCState *s = (USBXBLCState *)dev; DPRINTF("[XBLC] Reset\n"); + + if(s->in.voice != 0) + SDL_LockAudioDevice(s->in.voice); + if(s->out.voice != 0) + SDL_LockAudioDevice(s->out.voice); + fifo8_reset(&s->in.fifo); fifo8_reset(&s->out.fifo); + + if(s->in.voice != 0) + SDL_UnlockAudioDevice(s->in.voice); + if(s->out.voice != 0) + SDL_UnlockAudioDevice(s->out.voice); } -static void output_callback(void *opaque, int avail) +float xblc_audio_stream_get_current_input_volume(void *dev) { - USBXBLCState *s = (USBXBLCState *)opaque; + USBXBLCState *s = (USBXBLCState*)dev; + return s->in.peak_volume / 32768.0f; +} + +float xblc_audio_stream_get_output_volume(void *dev) +{ + USBXBLCState *s = (USBXBLCState*)dev; + return (float)s->out.volume / SDL_MIX_MAXVOLUME; +} + +float xblc_audio_stream_get_input_volume(void *dev) +{ + USBXBLCState *s = (USBXBLCState*)dev; + return (float)s->in.volume / SDL_MIX_MAXVOLUME; +} + +void xblc_audio_stream_set_output_volume(void *dev, float volume) +{ + USBXBLCState *s = (USBXBLCState*)dev; + s->out.volume = MIN(SDL_MIX_MAXVOLUME, MAX(0, (int)(volume * SDL_MIX_MAXVOLUME))); +} +void xblc_audio_stream_set_input_volume(void *dev, float volume) +{ + USBXBLCState *s = (USBXBLCState*)dev; + s->in.volume = MIN(SDL_MIX_MAXVOLUME, MAX(0, (int)(volume * SDL_MIX_MAXVOLUME))); +} + +static void output_callback(void *userdata, uint8_t *stream, int len) +{ + USBXBLCState *s = (USBXBLCState *)userdata; const uint8_t *data; - uint32_t processed, max_len; + uint32_t 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; - } + memcpy(stream, (void*)silence, MIN(len, ARRAY_SIZE(silence))); + } else { + // Write speaker data into audio backend + while (len > 0 && !fifo8_is_empty(&s->out.fifo)) { + max_len = MIN(fifo8_num_used(&s->out.fifo), (uint32_t)len); + data = fifo8_pop_bufptr(&s->out.fifo, max_len, &max_len); - // 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; + if(s->out.volume < SDL_MIX_MAXVOLUME) { + memset(stream, 0, len); + SDL_MixAudioFormat(stream, data, AUDIO_S16LSB, max_len, MAX(0, s->out.volume)); + } else { + memcpy(stream, data, max_len); + } + stream += max_len; + len -= max_len; + } } } -static void input_callback(void *opaque, int avail) +static int calc_peak_amplitude(const int16_t *samples, int len) { + int max = 0; + for(int i = 0; i < len; i++) { + max = MAX(max, abs(samples[i])); + } + return max; +} + +static void input_callback(void *userdata, uint8_t *stream, int len) { - USBXBLCState *s = (USBXBLCState *)opaque; - uint32_t processed, max_len; + USBXBLCState *s = (USBXBLCState *)userdata; + uint8_t buffer[XBLC_FIFO_SIZE]; + + s->in.peak_volume = s->in.volume * + calc_peak_amplitude((int16_t*)stream, len / sizeof(int16_t)) / SDL_MIX_MAXVOLUME; - // 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; + // Don't try to put more into the queue than will fit + uint32_t max_len = MIN(len, fifo8_num_free(&s->in.fifo)); + if (max_len > 0) { + if(s->in.volume < SDL_MIX_MAXVOLUME) { + memset(buffer, 0, max_len); + SDL_MixAudioFormat(buffer, stream, AUDIO_S16LSB, max_len, MAX(s->in.volume, 0)); + fifo8_push_all(&s->in.fifo, buffer, max_len); + } else { + fifo8_push_all(&s->in.fifo, stream, max_len); + } } +} - // Flush excess/old data - this can happen if the user program stops the iso transfers after it - // has setup the xblc. - while (avail > 0) +#ifdef DEBUG_XBLC +static const char *GetFormatString(SDL_AudioFormat format) +{ + switch(format) { - processed = AUD_read(s->in.voice, s->in.packet, XBLC_MAX_PACKET); - avail -= processed; - if (processed == 0) break; + case AUDIO_S16LSB: + return "AUDIO_S16LSB"; + case AUDIO_S16MSB: + return "AUDIO_S16MSB"; + case AUDIO_S32LSB: + return "AUDIO_S32LSB"; + case AUDIO_S32MSB: + return "AUDIO_S32MSB"; + case AUDIO_F32LSB: + return "AUDIO_F32LSB"; + case AUDIO_F32MSB: + return "AUDIO_F32MSB"; + default: + return "Unknown"; } } +#endif + +static void xblc_audio_channel_init(USBXBLCState *s, bool capture, const char *device_name) +{ + XBLCStream *channel = capture ? &s->in : &s->out; + + if(channel->voice != 0) { + SDL_PauseAudioDevice(channel->voice, 1); + SDL_CloseAudioDevice(channel->voice); + channel->voice = 0; + } + + if(channel->device_name != NULL) + g_free(channel->device_name); + if(device_name == NULL) + channel->device_name = NULL; + else + channel->device_name = g_strdup(device_name); + + fifo8_reset(&channel->fifo); + if(capture) + s->in.peak_volume = 0; + + SDL_AudioSpec desired_spec; + desired_spec.channels = 1; + desired_spec.freq = s->sample_rate; + desired_spec.format = AUDIO_S16LSB; + desired_spec.samples = 100; + desired_spec.userdata = (void*)s; + desired_spec.callback = capture ? input_callback : output_callback; + + channel->voice = SDL_OpenAudioDevice(device_name, + (int)capture, + &desired_spec, + &channel->spec, + 0); + + DPRINTF("%sputDevice: %s\n", capture ? "In" : "Out", NULL_DEFAULT(device_name, "Default")); + DPRINTF("%sputDevice: Wanted %d Channels, Obtained %d Channels\n", capture ? "In" : "Out", desired_spec.channels, channel->spec.channels); + DPRINTF("%sputDevice: Wanted %d hz, Obtained %d hz\n", capture ? "In" : "Out", desired_spec.freq, channel->spec.freq); + DPRINTF("%sputDevice: Wanted %s, Obtained %s\n", capture ? "In" : "Out", GetFormatString(desired_spec.format), GetFormatString(channel->spec.format)); + DPRINTF("%sputDevice: Wanted samples %d, Obtained samples %d\n", capture ? "In" : "Out", desired_spec.samples, channel->spec.samples); + + SDL_PauseAudioDevice(channel->voice, 0); +} + +static bool should_init_stream(const XBLCStream *stream, const char *requested_device_name) +{ + // If the voice has not been initialized, initialize it + if (stream->voice == 0) + return true; + + // If one of the names is null and the other is not, initialize it + if ((stream->device_name == NULL) ^ (requested_device_name == NULL)) + return true; + + // If neither name is null, but they don't match, initialize it + if (stream->device_name != NULL && + requested_device_name != NULL && + strcmp(stream->device_name, requested_device_name) != 0) + return true; + + // We don't need to initialize it + return false; +} static void xblc_audio_stream_init(USBDevice *dev, uint16_t sample_rate) { USBXBLCState *s = (USBXBLCState *)dev; + bool init_input_stream = false, init_output_stream = false; - AUD_set_active_out(s->out.voice, FALSE); - AUD_set_active_in(s->in.voice, FALSE); + ControllerState *controller = xemu_input_get_bound(s->device_index); + assert(controller->peripheral_types[0] == PERIPHERAL_XBLC); + assert(controller->peripherals[0] != NULL); - fifo8_reset(&s->in.fifo); - fifo8_reset(&s->out.fifo); + XblcState *xblc = (XblcState*)controller->peripherals[0]; - s->as.freq = sample_rate; - s->as.nchannels = 1; - s->as.fmt = AUDIO_FORMAT_S16; - s->as.endianness = 0; + if(s->sample_rate != sample_rate) { + init_input_stream = true; + init_output_stream = true; + s->sample_rate = sample_rate; + } - s->out.voice = AUD_open_out(&s->card, s->out.voice, TYPE_USB_XBLC "-speaker", - s, output_callback, &s->as); + init_input_stream |= should_init_stream(&s->in, xblc->input_device_name); + init_output_stream |= should_init_stream(&s->out, xblc->output_device_name); - s->in.voice = AUD_open_in(&s->card, s->in.voice, TYPE_USB_XBLC "-microphone", - s, input_callback, &s->as); + // If either channel needs to be initialized, initialize both channels + if (init_input_stream || init_output_stream) { + xblc_audio_channel_init(s, true, xblc->input_device_name); + xblc_audio_channel_init(s, false, xblc->output_device_name); + } - 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); } +void xblc_audio_stream_reinit(void *dev) +{ + USBXBLCState *s = (USBXBLCState *)dev; + xblc_audio_stream_init(dev, s->sample_rate); +} + static void usb_xblc_handle_control(USBDevice *dev, USBPacket *p, int request, int value, int index, int length, uint8_t *data) { @@ -298,6 +442,11 @@ static void usb_xblc_handle_data(USBDevice *dev, USBPacket *p) to_process -= chunk_len; } + // 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); + break; case USB_TOKEN_OUT: // Speaker data - get data from usb packet then push to fifo. @@ -312,27 +461,20 @@ static void usb_xblc_handle_data(USBDevice *dev, USBPacket *p) 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); - + SDL_PauseAudioDevice(s->in.voice, 1); + SDL_PauseAudioDevice(s->out.voice, 1); + 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); + SDL_CloseAudioDevice(s->in.voice); + SDL_CloseAudioDevice(s->out.voice); } static void usb_xblc_class_initfn(ObjectClass *klass, void *data) @@ -349,10 +491,12 @@ 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); + + s->in.volume = SDL_MIX_MAXVOLUME; + s->out.volume = SDL_MIX_MAXVOLUME; } static Property xblc_properties[] = { diff --git a/hw/xbox/xblc.h b/hw/xbox/xblc.h new file mode 100644 index 0000000000..c66a6b15dd --- /dev/null +++ b/hw/xbox/xblc.h @@ -0,0 +1,42 @@ +/* + * QEMU USB Xbox Live Communicator (XBLC) Device + * + * Copyright (c) 2025 faha223 + * + * 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 . + */ + +#ifndef HW_XBOX_XBLC_H +#define HW_XBOX_XBLC_H + +#ifdef __cplusplus +extern "C" { +#endif + +void xblc_audio_stream_reinit(void *dev); + +/* Outputs a value on the interval [0, 1] where 0 is muted and 1 is full volume */ +float xblc_audio_stream_get_output_volume(void *dev); +float xblc_audio_stream_get_input_volume(void *dev); + +/* Accepts a value on the interval [0, 1] where 0 is muted and 1 is full volume */ +void xblc_audio_stream_set_output_volume(void *dev, float volume); +void xblc_audio_stream_set_input_volume(void *dev, float volume); +float xblc_audio_stream_get_current_input_volume(void *dev); + +#ifdef __cplusplus +} +#endif + +#endif \ No newline at end of file diff --git a/subprojects/cpp-httplib b/subprojects/cpp-httplib new file mode 160000 index 0000000000..0f1b62c2b3 --- /dev/null +++ b/subprojects/cpp-httplib @@ -0,0 +1 @@ +Subproject commit 0f1b62c2b3d0898cbab7aa685c2593303ffdc1a2 diff --git a/ui/xemu-input.c b/ui/xemu-input.c index 31a51eda9d..c808b9c938 100644 --- a/ui/xemu-input.c +++ b/ui/xemu-input.c @@ -33,6 +33,7 @@ #include "xemu-settings.h" #include "sysemu/blockdev.h" +#include "../hw/xbox/xblc.h" // #define DEBUG_INPUT @@ -126,6 +127,34 @@ static const char **peripheral_params_settings_map[4][2] = { &g_config.input.peripherals.port4.peripheral_param_1 } }; +static const char **xblc_input_device_map[4] = { + &g_config.input.peripherals.port1.xblc1_settings.input_device_name, + &g_config.input.peripherals.port2.xblc2_settings.input_device_name, + &g_config.input.peripherals.port3.xblc3_settings.input_device_name, + &g_config.input.peripherals.port4.xblc4_settings.input_device_name, +}; + +static float *xblc_input_volume_map[4] = { + &g_config.input.peripherals.port1.xblc1_settings.input_device_volume, + &g_config.input.peripherals.port2.xblc2_settings.input_device_volume, + &g_config.input.peripherals.port3.xblc3_settings.input_device_volume, + &g_config.input.peripherals.port4.xblc4_settings.input_device_volume, +}; + +static const char **xblc_output_device_map[4] = { + &g_config.input.peripherals.port1.xblc1_settings.output_device_name, + &g_config.input.peripherals.port2.xblc2_settings.output_device_name, + &g_config.input.peripherals.port3.xblc3_settings.output_device_name, + &g_config.input.peripherals.port4.xblc4_settings.output_device_name, +}; + +static float *xblc_output_volume_map[4] = { + &g_config.input.peripherals.port1.xblc1_settings.output_device_volume, + &g_config.input.peripherals.port2.xblc2_settings.output_device_volume, + &g_config.input.peripherals.port3.xblc3_settings.output_device_volume, + &g_config.input.peripherals.port4.xblc4_settings.output_device_volume, +}; + static int sdl_kbd_scancode_map[25]; static const char *get_bound_driver(int port) @@ -149,6 +178,12 @@ static const char *get_bound_driver(int port) static const int port_map[4] = { 3, 4, 1, 2 }; +const char *peripheral_type_names[3] = { + "None", + "Memory Unit", + "Xbox Live Communicator" +}; + void xemu_input_init(void) { if (g_config.input.background_input_capture) { @@ -217,7 +252,7 @@ void xemu_input_init(void) char buf[128]; snprintf(buf, sizeof(buf), "Connected '%s' to port %d", new_con->name, port+1); xemu_queue_notification(buf); - xemu_input_rebind_xmu(port); + xemu_input_rebind_peripherals(port); } QTAILQ_INSERT_TAIL(&available_controllers, new_con, entry); @@ -340,7 +375,7 @@ void xemu_input_process_sdl_events(const SDL_Event *event) char buf[128]; snprintf(buf, sizeof(buf), "Connected '%s' to port %d", new_con->name, port+1); xemu_queue_notification(buf); - xemu_input_rebind_xmu(port); + xemu_input_rebind_peripherals(port); } } else if (event->type == SDL_CONTROLLERDEVICEREMOVED) { DPRINTF("Controller Removed: %d\n", event->cdevice.which); @@ -392,6 +427,33 @@ void xemu_input_process_sdl_events(const SDL_Event *event) } } +void xemu_input_save_xblc_settings(int port, XblcState *xblc) +{ + const char *default_device_name = "Default"; + *peripheral_types_settings_map[port][0] = PERIPHERAL_XBLC; + *xblc_output_device_map[port] = xblc->output_device_name == NULL ? + default_device_name : xblc->output_device_name; + *xblc_output_volume_map[port] = xblc->output_device_volume * 100; + *xblc_input_device_map[port] = xblc->input_device_name == NULL ? + default_device_name : xblc->input_device_name; + *xblc_input_volume_map[port] = xblc->input_device_volume * 100; +} + +XblcState *xemu_input_load_xblc_settings(int port) +{ + const char *default_device_name = "Default"; + XblcState *xblc = (XblcState*)g_malloc(sizeof(XblcState)); + memset(xblc, 0, sizeof(XblcState)); + xblc->output_device_name = *xblc_output_device_map[port] == NULL ? + default_device_name : *xblc_output_device_map[port]; + xblc->output_device_volume = *xblc_output_volume_map[port] / 100; + xblc->input_device_name = *xblc_input_device_map[port] == NULL ? + default_device_name : *xblc_input_device_map[port]; + xblc->input_device_volume = *xblc_input_volume_map[port] / 100; + + return xblc; +} + void xemu_input_update_controller(ControllerState *state) { int64_t now = qemu_clock_get_us(QEMU_CLOCK_REALTIME); @@ -519,13 +581,12 @@ void xemu_input_bind(int index, ControllerState *state, int save) assert(bound_controllers[index]->device != NULL); Error *err = NULL; - // Unbind any XMUs + // Unbind any peripherals for (int i = 0; i < 2; i++) { if (bound_controllers[index]->peripherals[i]) { - // If this was an XMU, unbind the XMU - if (bound_controllers[index]->peripheral_types[i] == - PERIPHERAL_XMU) - xemu_input_unbind_xmu(index, i); + // If a peripheral was bound, unbind the peripheral + if (bound_controllers[index]->peripheral_types[i] != PERIPHERAL_NONE) + xemu_input_unbind_peripheral(index, i); // Free up the XmuState and set the peripheral type to none g_free(bound_controllers[index]->peripherals[i]); @@ -612,6 +673,28 @@ void xemu_input_bind(int index, ControllerState *state, int save) } } +static void xemu_input_unbind_xmu(int player_index, int expansion_slot_index) +{ + assert(player_index >= 0 && player_index < 4); + assert(expansion_slot_index >= 0 && expansion_slot_index < 2); + + ControllerState *state = bound_controllers[player_index]; + if (state->peripheral_types[expansion_slot_index] != PERIPHERAL_XMU) + return; + + XmuState *xmu = (XmuState *)state->peripherals[expansion_slot_index]; + if (xmu != NULL) { + if (xmu->dev != NULL) { + qdev_unplug((DeviceState *)xmu->dev, &error_abort); + object_unref(OBJECT(xmu->dev)); + xmu->dev = NULL; + } + + g_free((void *)xmu->filename); + xmu->filename = NULL; + } +} + bool xemu_input_bind_xmu(int player_index, int expansion_slot_index, const char *filename, bool is_rebind) { @@ -716,71 +799,288 @@ bool xemu_input_bind_xmu(int player_index, int expansion_slot_index, return true; } -void xemu_input_unbind_xmu(int player_index, int expansion_slot_index) +static void xemu_input_rebind_xmu(int port, int expansion_slot_index) { - assert(player_index >= 0 && player_index < 4); - assert(expansion_slot_index >= 0 && expansion_slot_index < 2); + enum peripheral_type peripheral_type = + (enum peripheral_type)(*peripheral_types_settings_map[port][expansion_slot_index]); - ControllerState *state = bound_controllers[player_index]; - if (state->peripheral_types[expansion_slot_index] != PERIPHERAL_XMU) + // If peripheralType is out of range, change the settings for this + // controller and peripheral port to default + if (peripheral_type < PERIPHERAL_NONE || + peripheral_type >= PERIPHERAL_TYPE_COUNT) { + xemu_save_peripheral_settings(port, expansion_slot_index, PERIPHERAL_NONE, NULL); + peripheral_type = PERIPHERAL_NONE; return; + } - XmuState *xmu = (XmuState *)state->peripherals[expansion_slot_index]; - if (xmu != NULL) { - if (xmu->dev != NULL) { - qdev_unplug((DeviceState *)xmu->dev, &error_abort); - object_unref(OBJECT(xmu->dev)); - xmu->dev = NULL; + const char *param = *peripheral_params_settings_map[port][expansion_slot_index]; + + if (peripheral_type == PERIPHERAL_XMU) { + if (param != NULL && strlen(param) > 0) { + // This is an XMU and needs to be bound to this controller + if (qemu_access(param, R_OK | W_OK) == 0) { + bound_controllers[port]->peripheral_types[expansion_slot_index] = + peripheral_type; + bound_controllers[port]->peripherals[expansion_slot_index] = + g_malloc(sizeof(XmuState)); + memset(bound_controllers[port]->peripherals[expansion_slot_index], 0, + sizeof(XmuState)); + bool did_bind = xemu_input_bind_xmu(port, expansion_slot_index, param, true); + if (did_bind) { + char *buf = + g_strdup_printf("Connected Memory Unit %s to Player %d Expansion Slot %c", + param, port + 1, 'A' + expansion_slot_index); + xemu_queue_notification(buf); + g_free(buf); + } + } else { + char *buf = + g_strdup_printf("Unable to bind Memory Unit at %s to Player %d Expansion Slot %c", + param, port + 1, 'A' + expansion_slot_index); + xemu_queue_error_message(buf); + g_free(buf); + } } - - g_free((void *)xmu->filename); - xmu->filename = NULL; } } -void xemu_input_rebind_xmu(int port) +static void xemu_input_unbind_xblc(int player_index) +{ + const int expansion_slot_index = 0; + assert(player_index >= 0 && player_index < 4); + + ControllerState *state = bound_controllers[player_index]; + if(state == NULL) + return; + if (state->peripheral_types[expansion_slot_index] != PERIPHERAL_XBLC) + return; + + XblcState *xblc = (XblcState *)state->peripherals[expansion_slot_index]; + if (xblc != NULL) { + if (xblc->dev != NULL) { + qdev_unplug((DeviceState *)xblc->dev, &error_abort); + object_unref(OBJECT(xblc->dev)); + xblc->dev = NULL; + } + + g_free((void *)xblc->output_device_name); + g_free((void *)xblc->input_device_name); + xblc->output_device_name = NULL; + xblc->input_device_name = NULL; + } +} + +bool xemu_input_bind_xblc(int player_index, const char *output_device, + const char *input_device, bool is_rebind) +{ + // Xbox Live Communicator is keyed such that it can only go into expansion slot 0 + DPRINTF("Connecting Xbox Live Communicator Headset\n"); + DPRINTF("XBLC Output Device: %s\n", output_device); + DPRINTF("XBLC Input Device: %s\n", input_device); + + assert(player_index >= 0 && player_index < 4); + + ControllerState *player = bound_controllers[player_index]; + if(player == NULL) + return false; + enum peripheral_type peripheral_type = + player->peripheral_types[0]; + if (peripheral_type != PERIPHERAL_XBLC) + return false; + + XblcState *xblc = (XblcState *)player->peripherals[0]; + + // Unbind existing XBLC + if (xblc->dev != NULL) { + xemu_input_unbind_xblc(player_index); + } + + // Look for any other XBLCs that are using these devices + for (int player_i = 0; player_i < 4; player_i++) { + ControllerState *state = bound_controllers[player_i]; + if (state != NULL) { + if (state->peripheral_types[0] == PERIPHERAL_XBLC) { + XblcState *xblc_i = + (XblcState *)state->peripherals[0]; + assert(xblc_i); + + if(xblc_i->dev != NULL) { + bool already_bound = false; + + if (!((xblc_i->output_device_name == NULL) ^ (output_device == NULL))) { + if(output_device ==NULL) + already_bound = true; + else + already_bound = strcmp(xblc_i->output_device_name, output_device) == 0; + } + + if (already_bound) { + if(output_device == NULL) + output_device = "Default"; + char *buf = + g_strdup_printf("Output Device %s is already mounted on " + "player %d slot %c\r\n", output_device, + player_i + 1, 'A'); + xemu_queue_notification(buf); + g_free(buf); + return false; + } + + already_bound = false; + + if (!((xblc_i->input_device_name == NULL) ^ (input_device == NULL))) { + if(input_device == NULL) + already_bound = true; + else + already_bound = strcmp(xblc_i->input_device_name, input_device) == 0; + } + + if (already_bound) { + char *buf = + g_strdup_printf("Input Device %s is already mounted on " + "player %d slot %c\r\n", input_device, + player_i + 1, 'A'); + xemu_queue_notification(buf); + g_free(buf); + return false; + } + } + } + } + } + + if(xblc->input_device_name != NULL) + g_free((void*)xblc->input_device_name); + if(input_device == NULL) + xblc->input_device_name = NULL; + else + xblc->input_device_name = g_strdup(input_device); + + if(xblc->output_device_name != NULL) + g_free((void*)xblc->output_device_name); + if(output_device == NULL) + xblc->output_device_name = NULL; + else + xblc->output_device_name = g_strdup(output_device); + + char *tmp; + + static int id_counter = 0; + tmp = g_strdup_printf("xblc_%d", id_counter++); + + // Create the usb-storage device + QDict *qdict = qdict_new(); + + // Specify device driver + qdict_put_str(qdict, "driver", "usb-xblc"); + + // Specify index/port + tmp = g_strdup_printf("1.%d.2", port_map[player_index]); + qdict_put_str(qdict, "port", tmp); + g_free(tmp); + + // Create the device + QemuOpts *opts = + qemu_opts_from_qdict(qemu_find_opts("device"), qdict, &error_abort); + + DeviceState *dev = qdev_device_add(opts, &error_abort); + assert(dev); + + xblc->dev = (void *)dev; + xblc_audio_stream_set_output_volume(xblc->dev, xblc->output_device_volume); + xblc_audio_stream_set_input_volume(xblc->dev, xblc->input_device_volume); + + // Unref for eventual cleanup + qobject_unref(qdict); + + if (!is_rebind) { + xemu_input_save_xblc_settings(player_index, xblc); + } + + return true; +} + +static void xemu_input_rebind_xblc(int port) +{ + enum peripheral_type peripheral_type = + (enum peripheral_type)(*peripheral_types_settings_map[port][0]); + + // If peripheralType is out of range, change the settings for this + // controller and peripheral port to default + if (peripheral_type < PERIPHERAL_NONE || + peripheral_type >= PERIPHERAL_TYPE_COUNT) { + xemu_save_peripheral_settings(port, 0, PERIPHERAL_NONE, NULL); + peripheral_type = PERIPHERAL_NONE; + return; + } + + if (peripheral_type == PERIPHERAL_XBLC) { + bound_controllers[port]->peripheral_types[0] = peripheral_type; + bound_controllers[port]->peripherals[0] = xemu_input_load_xblc_settings(port); + XblcState *xblc = (XblcState*)bound_controllers[port]->peripherals[0]; + + char *output_temp = xblc->output_device_name == NULL ? NULL : g_strdup(xblc->output_device_name); + char *input_temp = xblc->input_device_name == NULL ? NULL : g_strdup(xblc->input_device_name); + + bool did_bind = xemu_input_bind_xblc(port, output_temp, input_temp, true); + if (did_bind) { + char *buf = + g_strdup_printf("Connected Xbox Live Communicator Headset to Player %d Expansion Slot A", + port + 1); + xemu_queue_notification(buf); + g_free(buf); + } + + if(output_temp != NULL) + g_free(output_temp); + if(input_temp != NULL) + g_free(input_temp); + } +} + +void xemu_input_unbind_peripheral(int player_index, int expansion_slot_index) +{ + ControllerState *state = bound_controllers[player_index]; + if(state != NULL) + { + switch(state->peripheral_types[expansion_slot_index]) + { + case PERIPHERAL_XMU: + xemu_input_unbind_xmu(player_index, expansion_slot_index); + break; + case PERIPHERAL_XBLC: + assert(player_index == 0); + xemu_input_unbind_xblc(player_index); + break; + case PERIPHERAL_NONE: + break; + default: + assert(false); + } + } +} + +void xemu_input_rebind_peripherals(int port) { - // Try to bind peripherals back to controller for (int i = 0; i < 2; i++) { enum peripheral_type peripheral_type = (enum peripheral_type)(*peripheral_types_settings_map[port][i]); - // If peripheralType is out of range, change the settings for this - // controller and peripheral port to default - if (peripheral_type < PERIPHERAL_NONE || - peripheral_type >= PERIPHERAL_TYPE_COUNT) { - xemu_save_peripheral_settings(port, i, PERIPHERAL_NONE, NULL); - peripheral_type = PERIPHERAL_NONE; - } - - const char *param = *peripheral_params_settings_map[port][i]; - - if (peripheral_type == PERIPHERAL_XMU) { - if (param != NULL && strlen(param) > 0) { - // This is an XMU and needs to be bound to this controller - if (qemu_access(param, R_OK | W_OK) == 0) { - bound_controllers[port]->peripheral_types[i] = - peripheral_type; - bound_controllers[port]->peripherals[i] = - g_malloc(sizeof(XmuState)); - memset(bound_controllers[port]->peripherals[i], 0, - sizeof(XmuState)); - bool did_bind = xemu_input_bind_xmu(port, i, param, true); - if (did_bind) { - char *buf = - g_strdup_printf("Connected XMU %s to port %d%c", - param, port + 1, 'A' + i); - xemu_queue_notification(buf); - g_free(buf); - } - } else { - char *buf = - g_strdup_printf("Unable to bind XMU at %s to port %d%c", - param, port + 1, 'A' + i); - xemu_queue_error_message(buf); - g_free(buf); + switch(peripheral_type) + { + case PERIPHERAL_XMU: + xemu_input_rebind_xmu(port, i); + break; + case PERIPHERAL_XBLC: + if(i != 0) { + // An Xbox Live Communicator Headset cannot be plugged into Expansion Slot B + xemu_save_peripheral_settings(port, i, PERIPHERAL_NONE, NULL); + continue; } - } + xemu_input_rebind_xblc(port); + break; + default: + continue; } } } diff --git a/ui/xemu-input.h b/ui/xemu-input.h index 23c1a9f91b..7c6bbac514 100644 --- a/ui/xemu-input.h +++ b/ui/xemu-input.h @@ -71,13 +71,22 @@ enum controller_input_device_type { INPUT_DEVICE_SDL_GAMECONTROLLER, }; -enum peripheral_type { PERIPHERAL_NONE, PERIPHERAL_XMU, PERIPHERAL_TYPE_COUNT }; +enum peripheral_type { PERIPHERAL_NONE, PERIPHERAL_XMU, PERIPHERAL_XBLC, PERIPHERAL_TYPE_COUNT }; +extern const char *peripheral_type_names[3]; typedef struct XmuState { const char *filename; void *dev; } XmuState; +typedef struct XblcState { + const char *output_device_name; + const char *input_device_name; + float input_device_volume; + float output_device_volume; + void *dev; +} XblcState; + typedef struct ControllerState { QTAILQ_ENTRY(ControllerState) entry; @@ -130,9 +139,13 @@ ControllerState *xemu_input_get_bound(int index); void xemu_input_bind(int index, ControllerState *state, int save); bool xemu_input_bind_xmu(int player_index, int peripheral_port_index, const char *filename, bool is_rebind); -void xemu_input_rebind_xmu(int port); -void xemu_input_unbind_xmu(int player_index, int peripheral_port_index); +bool xemu_input_bind_xblc(int player_index, const char *output_device, + const char *input_device, bool is_rebind); +void xemu_input_unbind_peripheral(int player_index, int expansion_slot_index); +void xemu_input_rebind_peripherals(int port); int xemu_input_get_controller_default_bind_port(ControllerState *state, int start); +void xemu_input_save_xblc_settings(int port, XblcState *xblc); +XblcState *xemu_input_load_xblc_settings(int port); void xemu_save_peripheral_settings(int player_index, int peripheral_index, int peripheral_type, const char *peripheral_parameter); diff --git a/ui/xui/gl-helpers.cc b/ui/xui/gl-helpers.cc index 0a06b7f16a..6d5c21adb2 100644 --- a/ui/xui/gl-helpers.cc +++ b/ui/xui/gl-helpers.cc @@ -24,6 +24,7 @@ #include "data/logo_sdf.png.h" #include "data/xemu_64x64.png.h" #include "data/xmu_mask.png.h" +#include "data/xblc_mask.png.h" #include "notifications.hh" #include "stb_image.h" #include @@ -32,9 +33,11 @@ #include #include "ui/shader/xemu-logo-frag.h" +#include "../../hw/xbox/xblc.h" -Fbo *controller_fbo, *xmu_fbo, *logo_fbo; -GLuint g_controller_duke_tex, g_controller_s_tex, g_logo_tex, g_icon_tex, g_xmu_tex; +Fbo *controller_fbo, *xmu_fbo, *xblc_fbo, *logo_fbo; +GLuint g_controller_duke_tex, g_controller_s_tex, + g_logo_tex, g_icon_tex, g_xmu_tex, g_xblc_tex; enum class ShaderType { Blit, @@ -422,7 +425,8 @@ static const struct rect tex_items[] = { { 67, 48, 28, 28 }, // obj_port_lbl_2 { 67, 20, 28, 28 }, // obj_port_lbl_3 { 95, 76, 28, 28 }, // obj_port_lbl_4 - { 0, 0, 512, 512 } // obj_xmu + { 0, 0, 512, 512 }, // obj_xmu + { 0, 0, 512, 512 }, // obj_xblc }; enum tex_item_names { @@ -434,7 +438,8 @@ enum tex_item_names { obj_port_lbl_2, obj_port_lbl_3, obj_port_lbl_4, - obj_xmu + obj_xmu, + obj_xblc }; void InitCustomRendering(void) @@ -450,6 +455,9 @@ void InitCustomRendering(void) g_xmu_tex = LoadTextureFromMemory(xmu_mask_data, xmu_mask_size); xmu_fbo = new Fbo(512, 256); + g_xblc_tex = LoadTextureFromMemory(xblc_mask_data, xblc_mask_size); + xblc_fbo = new Fbo(256, 256); + g_logo_tex = LoadTextureFromMemory(logo_sdf_data, logo_sdf_size); g_logo_shader = NewDecalShader(ShaderType::Logo); logo_fbo = new Fbo(512, 512); @@ -853,6 +861,31 @@ void RenderXmu(float frame_x, float frame_y, uint32_t primary_color, glUseProgram(0); } +void RenderXblc(XblcState *xblc, float frame_x, float frame_y, + uint32_t primary_color, uint32_t secondary_color) +{ + glUseProgram(g_decal_shader->prog); + glBindVertexArray(g_decal_shader->vao); + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, g_xblc_tex); + glBlendEquation(GL_FUNC_ADD); + glBlendFunc(GL_ONE, GL_ZERO); + + // Render xblc + RenderDecal(g_decal_shader, frame_x, frame_y, 256, 256, + tex_items[obj_xblc].x, tex_items[obj_xblc].y, + tex_items[obj_xblc].w, tex_items[obj_xblc].h, primary_color, + secondary_color, 0); + + if(xblc->dev != NULL) { + float volume = xblc_audio_stream_get_current_input_volume(xblc->dev); + RenderMeter(g_decal_shader, 4, 4, 252, 5, volume, primary_color + 0x40, primary_color + 0xFF); + } + + glBindVertexArray(0); + glUseProgram(0); +} + void RenderLogo(uint32_t time) { uint32_t color = 0x62ca13ff; diff --git a/ui/xui/gl-helpers.hh b/ui/xui/gl-helpers.hh index 82da963e6b..bd8f5bb21d 100644 --- a/ui/xui/gl-helpers.hh +++ b/ui/xui/gl-helpers.hh @@ -38,7 +38,7 @@ public: void Restore(); }; -extern Fbo *controller_fbo, *xmu_fbo, *logo_fbo; +extern Fbo *controller_fbo, *xmu_fbo, *xblc_fbo, *logo_fbo; extern GLuint g_icon_tex; void InitCustomRendering(void); @@ -49,6 +49,8 @@ void RenderControllerPort(float frame_x, float frame_y, int i, uint32_t port_color); void RenderXmu(float frame_x, float frame_y, uint32_t primary_color, uint32_t secondary_color); +void RenderXblc(XblcState *state, float frame_x, float frame_y, + uint32_t primary_color, uint32_t secondary_color); void RenderFramebuffer(GLint tex, int width, int height, bool flip); void RenderFramebuffer(GLint tex, int width, int height, bool flip, float scale[2]); bool RenderFramebufferToPng(GLuint tex, bool flip, std::vector &png, int max_width = 0, int max_height = 0); diff --git a/ui/xui/main-menu.cc b/ui/xui/main-menu.cc index 9bb5dcf33f..a4a38263e9 100644 --- a/ui/xui/main-menu.cc +++ b/ui/xui/main-menu.cc @@ -41,6 +41,7 @@ #include "../xemu-xbe.h" #include "../thirdparty/fatx/fatx.h" +#include "../../hw/xbox/xblc.h" #define DEFAULT_XMU_SIZE 8388608 @@ -90,9 +91,6 @@ void MainMenuInputView::Draw() // Dimensions of controller (rendered at origin) float controller_width = 477.0f; float controller_height = 395.0f; - // Dimensions of XMU - float xmu_x = 0, xmu_x_stride = 256, xmu_y = 0; - float xmu_w = 256, xmu_h = 256; // Setup rendering to fbo for controller and port images controller_fbo->Target(); @@ -254,7 +252,7 @@ void MainMenuInputView::Draw() // If we previously had no controller connected, we can rebind // the XMU if (bound_state == NULL) - xemu_input_rebind_xmu(active); + xemu_input_rebind_peripherals(active); bound_state = iter; } @@ -323,164 +321,8 @@ void MainMenuInputView::Draw() ImGui::PopFont(); ImGui::SetCursorPos(pos); - if (bound_state) { - SectionTitle("Expansion Slots"); - // Begin a 2-column layout to render the expansion slots - ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, - g_viewport_mgr.Scale(ImVec2(0, 12))); - ImGui::Columns(2, "mixed", false); - - xmu_fbo->Target(); - id = (ImTextureID)(intptr_t)xmu_fbo->Texture(); - - const char *img_file_filters = ".img Files\0*.img\0All Files\0*.*\0"; - const char *comboLabels[2] = { "###ExpansionSlotA", - "###ExpansionSlotB" }; - for (int i = 0; i < 2; i++) { - // Display a combo box to allow the user to choose the type of - // peripheral they want to use - enum peripheral_type selected_type = - bound_state->peripheral_types[i]; - const char *peripheral_type_names[2] = { "None", "Memory Unit" }; - const char *selected_peripheral_type = - peripheral_type_names[selected_type]; - ImGui::SetNextItemWidth(-FLT_MIN); - if (ImGui::BeginCombo(comboLabels[i], selected_peripheral_type, - ImGuiComboFlags_NoArrowButton)) { - // Handle all available peripheral types - for (int j = 0; j < 2; j++) { - bool is_selected = selected_type == j; - ImGui::PushID(j); - const char *selectable_label = peripheral_type_names[j]; - - if (ImGui::Selectable(selectable_label, is_selected)) { - // Free any existing peripheral - if (bound_state->peripherals[i] != NULL) { - if (bound_state->peripheral_types[i] == - PERIPHERAL_XMU) { - // Another peripheral was already bound. - // Unplugging - xemu_input_unbind_xmu(active, i); - } - - // Free the existing state - g_free((void *)bound_state->peripherals[i]); - bound_state->peripherals[i] = NULL; - } - - // Change the peripheral type to the newly selected type - bound_state->peripheral_types[i] = - (enum peripheral_type)j; - - // Allocate state for the new peripheral - if (j == PERIPHERAL_XMU) { - bound_state->peripherals[i] = - g_malloc(sizeof(XmuState)); - memset(bound_state->peripherals[i], 0, - sizeof(XmuState)); - } - - xemu_save_peripheral_settings( - active, i, bound_state->peripheral_types[i], NULL); - } - - if (is_selected) { - ImGui::SetItemDefaultFocus(); - } - - ImGui::PopID(); - } - - ImGui::EndCombo(); - } - DrawComboChevron(); - - // Set an X offset to center the image button within the column - ImGui::SetCursorPosX( - ImGui::GetCursorPosX() + - (int)((ImGui::GetColumnWidth() - - xmu_w * g_viewport_mgr.m_scale - - 2 * port_padding * g_viewport_mgr.m_scale) / - 2)); - - selected_type = bound_state->peripheral_types[i]; - if (selected_type == PERIPHERAL_XMU) { - float x = xmu_x + i * xmu_x_stride; - float y = xmu_y; - - XmuState *xmu = (XmuState *)bound_state->peripherals[i]; - if (xmu->filename != NULL && strlen(xmu->filename) > 0) { - RenderXmu(x, y, 0x81dc8a00, 0x0f0f0f00); - - } else { - RenderXmu(x, y, 0x1f1f1f00, 0x0f0f0f00); - } - - ImVec2 xmu_display_size; - if (ImGui::GetContentRegionMax().x < - xmu_h * g_viewport_mgr.m_scale) { - xmu_display_size.x = ImGui::GetContentRegionMax().x / 2; - xmu_display_size.y = xmu_display_size.x * xmu_h / xmu_w; - } else { - xmu_display_size = ImVec2(xmu_w * g_viewport_mgr.m_scale, - xmu_h * g_viewport_mgr.m_scale); - } - - ImGui::SetCursorPosX( - ImGui::GetCursorPosX() + - (int)((ImGui::GetColumnWidth() - xmu_display_size.x) / - 2.0)); - - ImGui::Image(id, xmu_display_size, ImVec2(0.5f * i, 1), - ImVec2(0.5f * (i + 1), 0)); - - // Button to generate a new XMU - ImGui::PushID(i); - if (ImGui::Button("New Image", ImVec2(250, 0))) { - int flags = NOC_FILE_DIALOG_SAVE | - NOC_FILE_DIALOG_OVERWRITE_CONFIRMATION; - const char *new_path = PausedFileOpen( - flags, img_file_filters, NULL, "xmu.img"); - - if (new_path) { - if (create_fatx_image(new_path, DEFAULT_XMU_SIZE)) { - // XMU was created successfully. Bind it - xemu_input_bind_xmu(active, i, new_path, false); - } else { - // Show alert message - char *msg = g_strdup_printf( - "Unable to create XMU image at %s", new_path); - xemu_queue_error_message(msg); - g_free(msg); - } - } - } - - const char *xmu_port_path = NULL; - if (xmu->filename == NULL) - xmu_port_path = g_strdup(""); - else - xmu_port_path = g_strdup(xmu->filename); - if (FilePicker("Image", &xmu_port_path, img_file_filters)) { - if (strlen(xmu_port_path) == 0) { - xemu_input_unbind_xmu(active, i); - } else { - xemu_input_bind_xmu(active, i, xmu_port_path, false); - } - } - g_free((void *)xmu_port_path); - - ImGui::PopID(); - } - - ImGui::NextColumn(); - } - - xmu_fbo->Restore(); - - ImGui::PopStyleVar(); // ItemSpacing - ImGui::Columns(1); - } + if (bound_state) + DrawExpansionSlotOptions(active); SectionTitle("Options"); Toggle("Auto-bind controllers", &g_config.input.auto_bind, @@ -490,6 +332,369 @@ void MainMenuInputView::Draw() "Capture even if window is unfocused (requires restart)"); } +void MainMenuInputView::DrawExpansionSlotOptions(int active) +{ + SectionTitle("Expansion Slots"); + + // Begin a 2-column layout to render the expansion slots + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, + g_viewport_mgr.Scale(ImVec2(8, 12))); + ImGui::Columns(2, nullptr, false); + + for (int i = 0; i < 2; i++) { + DrawExpansionSlotOptions(active, i); + ImGui::NextColumn(); + } + + ImGui::Columns(1); + ImGui::PopStyleVar(); // ItemSpacing +} + +void MainMenuInputView::DrawExpansionSlotOptions(int active, int expansion_slot_index) +{ + ControllerState *bound_state = xemu_input_get_bound(active); + const char *comboLabels[2] = { "###ExpansionSlotA", + "###ExpansionSlotB" }; + // Display a combo box to allow the user to choose the type of + // peripheral they want to use + enum peripheral_type selected_type = + bound_state->peripheral_types[expansion_slot_index]; + enum peripheral_type peripheral_types[2][3] = { + { PERIPHERAL_NONE, PERIPHERAL_XMU, PERIPHERAL_XBLC }, // Slot A + { PERIPHERAL_NONE, PERIPHERAL_XMU } // Slot B + }; + const char *selected_peripheral_type = peripheral_type_names[selected_type]; + ImGui::SetNextItemWidth(ImGui::GetColumnWidth() - (g_viewport_mgr.m_scale * 10)); + if (ImGui::BeginCombo(comboLabels[expansion_slot_index], selected_peripheral_type, ImGuiComboFlags_NoArrowButton)) { + // Handle all available peripheral types + for (int j = 0; j < 3 - expansion_slot_index; j++) { + bool is_selected = selected_type == j; + ImGui::PushID(j); + const char *selectable_label = peripheral_type_names[peripheral_types[expansion_slot_index][j]]; + + if (ImGui::Selectable(selectable_label, is_selected)) { + // Free any existing peripheral + if (bound_state->peripherals[expansion_slot_index] != NULL) { + xemu_input_unbind_peripheral(active, expansion_slot_index); + + // Free the existing state + g_free((void *)bound_state->peripherals[expansion_slot_index]); + bound_state->peripherals[expansion_slot_index] = NULL; + } + + // Change the peripheral type to the newly selected type + bound_state->peripheral_types[expansion_slot_index] = + (enum peripheral_type)j; + + // Allocate state for the new peripheral + switch(j) + { + case PERIPHERAL_XMU: + bound_state->peripherals[expansion_slot_index] = g_malloc(sizeof(XmuState)); + memset(bound_state->peripherals[expansion_slot_index], 0, sizeof(XmuState)); + xemu_save_peripheral_settings(active, expansion_slot_index, bound_state->peripheral_types[expansion_slot_index], NULL); + break; + case PERIPHERAL_XBLC: + bound_state->peripherals[expansion_slot_index] = g_malloc(sizeof(XblcState)); + memset(bound_state->peripherals[expansion_slot_index], 0, sizeof(XblcState)); + XblcState *xblc = (XblcState*)bound_state->peripherals[expansion_slot_index]; + xblc->output_device_volume = 0.5; + xblc->input_device_volume = 0.5; + if(xemu_input_bind_xblc(active, NULL, NULL, false)) { + char *buf = g_strdup_printf( + "Connected Xbox Live Communicator Headset to Player %d Expansion Slot %c.", + active + 1, 'A' + expansion_slot_index); + xemu_queue_notification(buf); + g_free(buf); + } + break; + } + } + + if (is_selected) { + ImGui::SetItemDefaultFocus(); + } + + ImGui::PopID(); + } + + ImGui::EndCombo(); + } + DrawComboChevron(); + + // Set an X offset to center the image button within the column + selected_type = bound_state->peripheral_types[expansion_slot_index]; + if (selected_type == PERIPHERAL_XMU) { + DrawXmuSettings(active, expansion_slot_index); + } else if(selected_type == PERIPHERAL_XBLC) { + DrawXblcSettings(active, expansion_slot_index); + } +} + +void MainMenuInputView::DrawXmuSettings(int active, int expansion_slot_index) +{ + // Dimensions of XMU + const float xmu_x = 0, xmu_x_stride = 256, xmu_y = 0; + const float xmu_w = 256, xmu_h = 256; + const int port_padding = 8; + float max_width = ImGui::GetColumnWidth() - (10 * g_viewport_mgr.m_scale); + + const char *img_file_filters = ".img Files\0*.img\0All Files\0*.*\0"; + + ControllerState *bound_state = xemu_input_get_bound(active); + assert(bound_state); + + float x = xmu_x + expansion_slot_index * xmu_x_stride; + float y = xmu_y; + + ImGui::SetCursorPosX( + ImGui::GetCursorPosX() + + (int)((ImGui::GetColumnWidth() - + xmu_w * g_viewport_mgr.m_scale - + 2 * port_padding * g_viewport_mgr.m_scale) / + 2)); + + xmu_fbo->Target(); + ImTextureID id = (ImTextureID)(intptr_t)xmu_fbo->Texture(); + + XmuState *xmu = (XmuState *)bound_state->peripherals[expansion_slot_index]; + uint32_t fg_color = (xmu->filename != NULL && strlen(xmu->filename) > 0) ? 0x81dc8a00 : 0x1f1f1f00; + RenderXmu(x, y, fg_color, 0x0f0f0f00); + + ImVec2 xmu_display_size; + if (max_width < xmu_w * g_viewport_mgr.m_scale) { + xmu_display_size.x = max_width; + xmu_display_size.y = xmu_display_size.x * xmu_h / xmu_w; + } else { + xmu_display_size = ImVec2(xmu_w * g_viewport_mgr.m_scale, + xmu_h * g_viewport_mgr.m_scale); + } + + ImGui::SetCursorPosX( + ImGui::GetCursorPosX() + + (int)((ImGui::GetColumnWidth() - xmu_display_size.x) / + 2.0)); + + ImGui::Image(id, xmu_display_size, ImVec2(0.5f * expansion_slot_index, 1), + ImVec2(0.5f * (expansion_slot_index + 1), 0)); + ImVec2 pos = ImGui::GetCursorPos(); + + xmu_fbo->Restore(); + + ImGui::SetCursorPos(pos); + + // Button to generate a new XMU + ImGui::PushID(expansion_slot_index); + ImGui::SetNextItemWidth(max_width); + if (ImGui::Button("New Image", ImVec2(250, 0))) { + int flags = NOC_FILE_DIALOG_SAVE | + NOC_FILE_DIALOG_OVERWRITE_CONFIRMATION; + const char *new_path = PausedFileOpen( + flags, img_file_filters, NULL, "xmu.img"); + + if (new_path) { + if (create_fatx_image(new_path, DEFAULT_XMU_SIZE)) { + // XMU was created successfully. Bind it + xemu_input_bind_xmu(active, expansion_slot_index, new_path, false); + } else { + // Show alert message + char *msg = g_strdup_printf( + "Unable to create XMU image at %s", new_path); + xemu_queue_error_message(msg); + g_free(msg); + } + } + } + + const char *xmu_port_path = NULL; + if (xmu->filename == NULL) + xmu_port_path = g_strdup(""); + else + xmu_port_path = g_strdup(xmu->filename); + if (FilePicker("Image", &xmu_port_path, img_file_filters)) { + if (strlen(xmu_port_path) == 0) { + xemu_input_unbind_peripheral(active, expansion_slot_index); + } else { + xemu_input_bind_xmu(active, expansion_slot_index, xmu_port_path, false); + } + } + g_free((void*)xmu_port_path); + + ImGui::PopID(); +} + +static int num_input_devices = 0; +static int num_output_devices = 0; +static const char **input_device_names = nullptr; +static const char **output_device_names = nullptr; + +static void DrawAudioDeviceSelectComboBox(int active, XblcState *xblc, int is_capture) +{ + ControllerState *bound_state = xemu_input_get_bound(active); + assert(bound_state); + + float max_width = ImGui::GetColumnWidth() - (10 * g_viewport_mgr.m_scale); + + const char *default_device_name = "Default"; + const char *selected_device = (is_capture == 0) ? xblc->output_device_name : xblc->input_device_name; + if(selected_device == NULL) + selected_device = default_device_name; + + int num_devices = SDL_GetNumAudioDevices(is_capture); + // Get pointers to the correct device name cache + const char ***device_names = (is_capture ? &input_device_names : &output_device_names); + int *num_device_names = (is_capture ? &num_input_devices : &num_output_devices); + + // If the number of devices is incorrect, update the cache + if (num_devices != *num_device_names) { + *num_device_names = num_devices; + // Update the device name cache + if(*device_names == nullptr) + g_free(*device_names); + if(num_devices == 0) + *device_names = nullptr; + else { + *device_names = (const char**)g_malloc(num_devices * sizeof(const char*)); + for(int i = 0; i < num_devices; i++) + { + (*device_names)[i] = SDL_GetAudioDeviceName(i, is_capture); + } + } + } + + const char *combo_label = (is_capture == 0) ? "###Speaker" : "###Microphone"; + // ImGui::Text("%s", label_text); + ImGui::SetNextItemWidth(max_width); + if(ImGui::BeginCombo(combo_label, selected_device, ImGuiComboFlags_NoArrowButton)) { + for(int device_index = -1; device_index < num_devices; device_index++) { + const char *device_name = default_device_name; + if(device_index >= 0) + device_name = (*device_names)[device_index]; + + // Default: device_index is -1, label is "Default", value is NULL + bool is_selected = (device_index == -1) && (selected_device == default_device_name); + + // If not default, strings are safe to compare + if(!is_selected && selected_device != default_device_name) + is_selected = strcmp(device_name, selected_device) == 0; + + if(ImGui::Selectable(device_name, is_selected)) { + if(is_capture == 0) { + // Free existing output_device_name, if it's not NULL + if(xblc->output_device_name != NULL) + g_free((void*)xblc->output_device_name); + + // If device_index is -1, set it to NULL + xblc->output_device_name = (device_index == -1) ? NULL : g_strdup(device_name); + } else { + // Free existing input_device_name, if it's not NULL + if(xblc->input_device_name != NULL) + g_free((void*)xblc->input_device_name); + + // If device_index is -1, set it to NULL + xblc->input_device_name = (device_index == -1) ? NULL : g_strdup(device_name); + } + + // If the usb-xblc device is already bound, reinitialize it + if(xblc->dev != NULL) + xblc_audio_stream_reinit(xblc->dev); + + // Save the changes + xemu_input_save_xblc_settings(active, xblc); + } + + if (is_selected) { + ImGui::SetItemDefaultFocus(); + } + } + + ImGui::EndCombo(); + } + DrawComboChevron(); +} + +static void DrawVolumeControlSlider(int active, XblcState *xblc, int is_capture) +{ + float max_width = ImGui::GetColumnWidth() - (10 * g_viewport_mgr.m_scale); + xblc->input_device_volume = xblc_audio_stream_get_input_volume(xblc->dev); + xblc->output_device_volume = xblc_audio_stream_get_output_volume(xblc->dev); + float *ui_volume = (is_capture == 0) ? &xblc->output_device_volume : &xblc->input_device_volume; + float original_volume = *ui_volume; + const char *label = (is_capture == 0) ? "Speaker" : "Microphone"; + char description[32]; + sprintf(description, "Volume: %.0f%%", 100 * original_volume); + + // This is not respected. Not sure why + ImGui::SetNextItemWidth(max_width); + + // Render the slider + ImGui::PushID(label); + Slider(label, ui_volume, description); + ImGui::PopID(); + + // If the slider value has changed, update the backend value + if(*ui_volume != original_volume) { + if(is_capture) + xblc_audio_stream_set_input_volume(xblc->dev, *ui_volume); + else + xblc_audio_stream_set_output_volume(xblc->dev, *ui_volume); + + // Save the changes + xemu_input_save_xblc_settings(active, xblc); + } +} + +void MainMenuInputView::DrawXblcSettings(int active, int expansion_slot_index) +{ + // Dimensions of XBLC + const float xblc_x = 0, xblc_y = 0; + const float xblc_w = 256, xblc_h = 256; + const int port_padding = 8; + float max_width = ImGui::GetColumnWidth() - (10 * g_viewport_mgr.m_scale); + + ControllerState *bound_state = xemu_input_get_bound(active); + assert(bound_state); + + float x = xblc_x; + float y = xblc_y; + + ImGui::SetCursorPosX( + ImGui::GetCursorPosX() + + (int)((max_width - + xblc_w * g_viewport_mgr.m_scale - + 2 * port_padding * g_viewport_mgr.m_scale) / + 2)); + + xblc_fbo->Target(); + ImTextureID id = (ImTextureID)(intptr_t)xblc_fbo->Texture(); + + XblcState *xblc = (XblcState *)bound_state->peripherals[expansion_slot_index]; + uint32_t fg_color = (xblc->dev == NULL) ? 0x1f1f1f00 : 0x81dc8a00; + RenderXblc(xblc, x, y, fg_color, 0x0f0f0f00); + + ImVec2 xblc_display_size; + if (max_width < xblc_w * g_viewport_mgr.m_scale) { + xblc_display_size.x = max_width; + xblc_display_size.y = xblc_display_size.x * xblc_h / xblc_w; + } else { + xblc_display_size = ImVec2(xblc_w * g_viewport_mgr.m_scale, + xblc_h * g_viewport_mgr.m_scale); + } + + ImGui::SetCursorPosX( + ImGui::GetCursorPosX() + + (int)((ImGui::GetColumnWidth() - xblc_display_size.x) / 2.0)); + + ImGui::Image(id, xblc_display_size, ImVec2(0, 1), ImVec2(1, 0)); + + xblc_fbo->Restore(); + + DrawVolumeControlSlider(active, xblc, 0); + DrawAudioDeviceSelectComboBox(active, xblc, 0); + DrawVolumeControlSlider(active, xblc, 1); + DrawAudioDeviceSelectComboBox(active, xblc, 1); +} + void MainMenuDisplayView::Draw() { SectionTitle("Renderer"); diff --git a/ui/xui/main-menu.hh b/ui/xui/main-menu.hh index bce3927333..8b794f652b 100644 --- a/ui/xui/main-menu.hh +++ b/ui/xui/main-menu.hh @@ -48,6 +48,10 @@ class MainMenuInputView : public virtual MainMenuTabView { public: void Draw() override; + void DrawExpansionSlotOptions(int active); + void DrawExpansionSlotOptions(int active, int expansion_slot_index); + void DrawXmuSettings(int active, int expansion_slot_index); + void DrawXblcSettings(int active, int expansion_slot_index); }; class MainMenuDisplayView : public virtual MainMenuTabView