diff --git a/Source/Core/Core/CMakeLists.txt b/Source/Core/Core/CMakeLists.txt index cb2a3ac58ab..76fe62bc68d 100644 --- a/Source/Core/Core/CMakeLists.txt +++ b/Source/Core/Core/CMakeLists.txt @@ -439,6 +439,8 @@ add_library(core IOS/USB/Emulated/Skylanders/SkylanderFigure.h IOS/USB/Emulated/WiiSpeak.cpp IOS/USB/Emulated/WiiSpeak.h + IOS/USB/Emulated/LogitechMic.cpp + IOS/USB/Emulated/LogitechMic.h IOS/USB/Host.cpp IOS/USB/Host.h IOS/USB/OH0/OH0.cpp diff --git a/Source/Core/Core/Config/MainSettings.cpp b/Source/Core/Core/Config/MainSettings.cpp index 2a9bad56e03..42442d4753d 100644 --- a/Source/Core/Core/Config/MainSettings.cpp +++ b/Source/Core/Core/Config/MainSettings.cpp @@ -611,6 +611,27 @@ const Info MAIN_WII_SPEAK_MUTED{{System::Main, "EmulatedUSBDevices", "WiiS const Info MAIN_WII_SPEAK_VOLUME_MODIFIER{ {System::Main, "EmulatedUSBDevices", "WiiSpeakVolumeModifier"}, 0}; +const std::array, EMULATED_LOGITECH_MIC_COUNT> MAIN_EMULATE_LOGITECH_MIC{ + Info{{System::Main, "EmulatedUSBDevices", "EmulateLogitechMic1"}, false}, + Info{{System::Main, "EmulatedUSBDevices", "EmulateLogitechMic2"}, false}, + Info{{System::Main, "EmulatedUSBDevices", "EmulateLogitechMic3"}, false}, + Info{{System::Main, "EmulatedUSBDevices", "EmulateLogitechMic4"}, false}}; +const std::array, EMULATED_LOGITECH_MIC_COUNT> MAIN_LOGITECH_MIC_MICROPHONE{ + Info{{System::Main, "EmulatedUSBDevices", "LogitechMic1Microphone"}, ""}, + Info{{System::Main, "EmulatedUSBDevices", "LogitechMic2Microphone"}, ""}, + Info{{System::Main, "EmulatedUSBDevices", "LogitechMic3Microphone"}, ""}, + Info{{System::Main, "EmulatedUSBDevices", "LogitechMic4Microphone"}, ""}}; +const std::array, EMULATED_LOGITECH_MIC_COUNT> MAIN_LOGITECH_MIC_MUTED{ + Info{{System::Main, "EmulatedUSBDevices", "LogitechMic1Muted"}, false}, + Info{{System::Main, "EmulatedUSBDevices", "LogitechMic2Muted"}, false}, + Info{{System::Main, "EmulatedUSBDevices", "LogitechMic3Muted"}, false}, + Info{{System::Main, "EmulatedUSBDevices", "LogitechMic4Muted"}, false}}; +const std::array, EMULATED_LOGITECH_MIC_COUNT> MAIN_LOGITECH_MIC_VOLUME_MODIFIER{ + Info{{System::Main, "EmulatedUSBDevices", "LogitechMic1VolumeModifier"}, 0}, + Info{{System::Main, "EmulatedUSBDevices", "LogitechMic2VolumeModifier"}, 0}, + Info{{System::Main, "EmulatedUSBDevices", "LogitechMic3VolumeModifier"}, 0}, + Info{{System::Main, "EmulatedUSBDevices", "LogitechMic4VolumeModifier"}, 0}}; + // The reason we need this function is because some memory card code // expects to get a non-NTSC-K region even if we're emulating an NTSC-K Wii. DiscIO::Region ToGameCubeRegion(DiscIO::Region region) diff --git a/Source/Core/Core/Config/MainSettings.h b/Source/Core/Core/Config/MainSettings.h index 2355508f9f4..9f110f478b4 100644 --- a/Source/Core/Core/Config/MainSettings.h +++ b/Source/Core/Core/Config/MainSettings.h @@ -373,6 +373,14 @@ extern const Info MAIN_WII_SPEAK_MICROPHONE; extern const Info MAIN_WII_SPEAK_MUTED; extern const Info MAIN_WII_SPEAK_VOLUME_MODIFIER; +static constexpr std::size_t EMULATED_LOGITECH_MIC_COUNT = 4; + +extern const std::array, EMULATED_LOGITECH_MIC_COUNT> MAIN_EMULATE_LOGITECH_MIC; +extern const std::array, EMULATED_LOGITECH_MIC_COUNT> + MAIN_LOGITECH_MIC_MICROPHONE; +extern const std::array, EMULATED_LOGITECH_MIC_COUNT> MAIN_LOGITECH_MIC_MUTED; +extern const std::array, EMULATED_LOGITECH_MIC_COUNT> MAIN_LOGITECH_MIC_VOLUME_MODIFIER; + // GameCube path utility functions // Replaces NTSC-K with some other region, and doesn't replace non-NTSC-K regions diff --git a/Source/Core/Core/IOS/Device.h b/Source/Core/Core/IOS/Device.h index d393dcd239a..35d2f79e91e 100644 --- a/Source/Core/Core/IOS/Device.h +++ b/Source/Core/Core/IOS/Device.h @@ -25,6 +25,7 @@ enum ReturnCode : s32 IPC_SUCCESS = 0, // Success IPC_EACCES = -1, // Permission denied IPC_EEXIST = -2, // File exists + IPC_STALL = -3, // Fail stall IPC_EINVAL = -4, // Invalid argument or fd IPC_EMAX = -5, // Too many file descriptors open IPC_ENOENT = -6, // File not found diff --git a/Source/Core/Core/IOS/USB/Common.h b/Source/Core/Core/IOS/USB/Common.h index fbd1de202d2..cdc7c39dadf 100644 --- a/Source/Core/Core/IOS/USB/Common.h +++ b/Source/Core/Core/IOS/USB/Common.h @@ -29,9 +29,13 @@ enum ControlRequestTypes DIR_HOST2DEVICE = 0, DIR_DEVICE2HOST = 1, TYPE_STANDARD = 0, + TYPE_CLASS = 1, TYPE_VENDOR = 2, + TYPE_RESERVED = 3, REC_DEVICE = 0, REC_INTERFACE = 1, + REC_ENDPOINT = 2, + REC_OTHER = 3, }; constexpr u16 USBHDR(u8 dir, u8 type, u8 recipient, u8 request) diff --git a/Source/Core/Core/IOS/USB/Emulated/LogitechMic.cpp b/Source/Core/Core/IOS/USB/Emulated/LogitechMic.cpp new file mode 100644 index 00000000000..8380c533dcc --- /dev/null +++ b/Source/Core/Core/IOS/USB/Emulated/LogitechMic.cpp @@ -0,0 +1,739 @@ +// Copyright 2025 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "Core/IOS/USB/Emulated/LogitechMic.h" + +#include + +#include "Core/Config/MainSettings.h" +#include "Core/HW/Memmap.h" +#include "Core/System.h" + +namespace IOS::HLE::USB +{ +enum class RequestCode : u8 +{ + SetCur = 0x01, + GetCur = 0x81, + SetMin = 0x02, + GetMin = 0x82, + SetMax = 0x03, + GetMax = 0x83, + SetRes = 0x04, + GetRes = 0x84, + SetMem = 0x05, + GetMem = 0x85, + GetStat = 0xff +}; + +enum class FeatureUnitControlSelector : u8 +{ + Mute = 0x01, + Volume = 0x02, + Bass = 0x03, + Midi = 0x04, + Treble = 0x05, + GraphicEqualizer = 0x06, + AutomaticGain = 0x07, + Delay = 0x08, + BassBoost = 0x09, + Loudness = 0x0a +}; + +enum class EndpointControlSelector : u8 +{ + SamplingFreq = 0x01, + Pitch = 0x02 +}; + +bool LogitechMicState::IsSampleOn() const +{ + return true; +} + +bool LogitechMicState::IsMuted() const +{ + return mute; +} + +u32 LogitechMicState::GetDefaultSamplingRate() const +{ + return DEFAULT_SAMPLING_RATE; +} + +namespace +{ +class MicrophoneLogitech final : public Microphone +{ +public: + explicit MicrophoneLogitech(const LogitechMicState& sampler, u8 index) + : Microphone(sampler, fmt::format("Logitech Mic {}", index)), m_sampler(sampler), + m_index(index) + { + } + +private: +#ifdef HAVE_CUBEB + std::string GetInputDeviceId() const override + { + return Config::Get(Config::MAIN_LOGITECH_MIC_MICROPHONE[m_index]); + } + std::string GetCubebStreamName() const override + { + return "Dolphin Emulated Logitech USB Microphone " + std::to_string(m_index); + } + s16 GetVolumeModifier() const override + { + return Config::Get(Config::MAIN_LOGITECH_MIC_VOLUME_MODIFIER[m_index]); + } + bool AreSamplesByteSwapped() const override { return false; } +#endif + + bool IsMicrophoneMuted() const override + { + return Config::Get(Config::MAIN_LOGITECH_MIC_MUTED[m_index]); + } + u32 GetStreamSize() const override { return BUFF_SIZE_SAMPLES * m_sampler.sample_rate / 250; } + + const LogitechMicState& m_sampler; + const u8 m_index; +}; +} // namespace + +LogitechMic::LogitechMic(u8 index) : m_index(index) +{ + assert(index >= 0 && index <= 3); + m_id = u64(m_vid) << 32 | u64(m_pid) << 16 | u64(9) << 8 | u64(index + 1); +} + +static const DeviceDescriptor DEVICE_DESCRIPTOR{0x12, 0x01, 0x0200, 0x00, 0x00, 0x00, 0x08, + 0x046d, 0x0a03, 0x0001, 0x01, 0x02, 0x00, 0x01}; + +DeviceDescriptor LogitechMic::GetDeviceDescriptor() const +{ + return DEVICE_DESCRIPTOR; +} + +static const std::vector CONFIG_DESCRIPTOR{ + ConfigDescriptor{0x09, 0x02, 0x0079, 0x02, 0x01, 0x03, 0x80, 0x3c}, +}; + +std::vector LogitechMic::GetConfigurations() const +{ + return CONFIG_DESCRIPTOR; +} + +static const std::vector INTERFACE_DESCRIPTORS{ + InterfaceDescriptor{0x09, 0x04, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00}, + InterfaceDescriptor{0x09, 0x04, 0x01, 0x00, 0x00, 0x01, 0x02, 0x00, 0x00}, + InterfaceDescriptor{0x09, 0x04, 0x01, 0x01, 0x01, 0x01, 0x02, 0x00, 0x00}}; + +std::vector LogitechMic::GetInterfaces(u8 /*config*/) const +{ + return INTERFACE_DESCRIPTORS; +} + +static constexpr u8 ENDPOINT_AUDIO_IN = 0x84; +static const std::vector ENDPOINT_DESCRIPTORS{ + EndpointDescriptor{0x09, 0x05, ENDPOINT_AUDIO_IN, 0x0d, 0x0060, 0x01}, +}; + +std::vector LogitechMic::GetEndpoints(u8 /*config*/, u8 interface, u8 alt) const +{ + if (interface == 1 && alt == 1) + return ENDPOINT_DESCRIPTORS; + + return std::vector{}; +} + +bool LogitechMic::Attach() +{ + if (m_device_attached) + return true; + + DEBUG_LOG_FMT(IOS_USB, "[{:04x}:{:04x} {}] Opening device", m_vid, m_pid, m_index); + if (!m_microphone) + { + m_microphone = std::make_unique(m_sampler, m_index); + m_microphone->Initialize(); + } + m_device_attached = true; + return true; +} + +bool LogitechMic::AttachAndChangeInterface(const u8 interface) +{ + if (!Attach()) + return false; + + if (interface != m_active_interface) + return ChangeInterface(interface) == 0; + + return true; +} + +int LogitechMic::CancelTransfer(const u8 endpoint) +{ + DEBUG_LOG_FMT(IOS_USB, "[{:04x}:{:04x} {}:{}] Cancelling transfers (endpoint {:#x})", m_vid, + m_pid, m_index, m_active_interface, endpoint); + + return IPC_SUCCESS; +} + +int LogitechMic::ChangeInterface(const u8 interface) +{ + DEBUG_LOG_FMT(IOS_USB, "[{:04x}:{:04x} {}:{}] Changing interface to {}", m_vid, m_pid, m_index, + m_active_interface, interface); + m_active_interface = interface; + return 0; +} + +int LogitechMic::GetNumberOfAltSettings(u8 interface) +{ + if (interface == 1) + return 2; + + return 0; +} + +int LogitechMic::SetAltSetting(u8 /*alt_setting*/) +{ + return 0; +} + +static constexpr u32 USBGETAID(u8 cs, u8 request, u16 index) +{ + return static_cast((cs << 24) | (request << 16) | index); +} + +static constexpr u32 USBGETAID(FeatureUnitControlSelector cs, RequestCode request, u16 index) +{ + return USBGETAID(Common::ToUnderlying(cs), Common::ToUnderlying(request), index); +} + +static constexpr u32 USBGETAID(EndpointControlSelector cs, RequestCode request, u16 index) +{ + return USBGETAID(Common::ToUnderlying(cs), Common::ToUnderlying(request), index); +} + +int LogitechMic::GetAudioControl(std::unique_ptr& cmd) +{ + auto& system = cmd->GetEmulationKernel().GetSystem(); + auto& memory = system.GetMemory(); + const auto cs = static_cast(cmd->value >> 8); + const auto request = static_cast(cmd->request); + const u8 cn = static_cast(cmd->value - 1); + const u32 aid = USBGETAID(cs, request, cmd->index); + int ret = IPC_STALL; + DEBUG_LOG_FMT(IOS_USB, + "GetAudioControl: bCs={:02x} bCn={:02x} bRequestType={:02x} bRequest={:02x} " + "bIndex={:02x} aid={:08x}", + Common::ToUnderlying(cs), cn, cmd->request_type, Common::ToUnderlying(request), + cmd->index, aid); + switch (aid) + { + case USBGETAID(FeatureUnitControlSelector::Mute, RequestCode::GetCur, 0x0200): + { + memory.Write_U8(m_sampler.mute ? 1 : 0, cmd->data_address); + ret = 1; + break; + } + case USBGETAID(FeatureUnitControlSelector::Volume, RequestCode::GetCur, 0x0200): + { + if (cn < 1 || cn == 0xff) + { + const uint16_t vol = (m_sampler.volume * 0x8800 + 127) / 255 + 0x8000; + DEBUG_LOG_FMT(IOS_USB, "GetAudioControl: Get volume {:04x}", vol); + memory.Write_U16(vol, cmd->data_address); + ret = 2; + } + break; + } + case USBGETAID(FeatureUnitControlSelector::Volume, RequestCode::GetMin, 0x0200): + { + if (cn < 1 || cn == 0xff) + { + memory.Write_U16(0x8001, cmd->data_address); + ret = 2; + } + break; + } + case USBGETAID(FeatureUnitControlSelector::Volume, RequestCode::GetMax, 0x0200): + { + if (cn < 1 || cn == 0xff) + { + memory.Write_U16(0x0800, cmd->data_address); + ret = 2; + } + break; + } + case USBGETAID(FeatureUnitControlSelector::Volume, RequestCode::GetRes, 0x0200): + { + if (cn < 1 || cn == 0xff) + { + memory.Write_U16(0x0088, cmd->data_address); + ret = 2; + } + break; + } + default: + { + WARN_LOG_FMT(IOS_USB, + "GetAudioControl: Unknown request: bCs={:02x} bCn={:02x} bRequestType={:02x} " + "bRequest={:02x} bIndex={:02x} aid={:08x}", + Common::ToUnderlying(cs), cn, cmd->request_type, Common::ToUnderlying(request), + cmd->index, aid); + break; + } + } + return ret; +} + +int LogitechMic::SetAudioControl(std::unique_ptr& cmd) +{ + auto& system = cmd->GetEmulationKernel().GetSystem(); + auto& memory = system.GetMemory(); + const auto cs = static_cast(cmd->value >> 8); + const auto request = static_cast(cmd->request); + const u8 cn = static_cast(cmd->value - 1); + const u32 aid = USBGETAID(cs, request, cmd->index); + int ret = IPC_STALL; + DEBUG_LOG_FMT(IOS_USB, + "SetAudioControl: bCs={:02x} bCn={:02x} bRequestType={:02x} bRequest={:02x} " + "bIndex={:02x} aid={:08x}", + Common::ToUnderlying(cs), cn, cmd->request_type, Common::ToUnderlying(request), + cmd->index, aid); + switch (aid) + { + case USBGETAID(FeatureUnitControlSelector::Mute, RequestCode::SetCur, 0x0200): + { + m_sampler.mute = memory.Read_U8(cmd->data_address) & 0x01; + DEBUG_LOG_FMT(IOS_USB, "SetAudioControl: Setting mute to {}", m_sampler.mute.load()); + ret = 0; + break; + } + case USBGETAID(FeatureUnitControlSelector::Volume, RequestCode::SetCur, 0x0200): + { + if (cn < 1 || cn == 0xff) + { + // TODO: Lego Rock Band's mic sensitivity setting provides unknown values to this. + uint16_t vol = memory.Read_U16(cmd->data_address); + const uint16_t original_vol = vol; + + vol -= 0x8000; + vol = (vol * 255 + 0x4400) / 0x8800; + vol = std::min(vol, 255); + + if (m_sampler.volume != vol) + m_sampler.volume = static_cast(vol); + + DEBUG_LOG_FMT(IOS_USB, "SetAudioControl: Setting volume to [{:02x}] [original={:04x}]", + m_sampler.volume.load(), original_vol); + + ret = 0; + } + break; + } + case USBGETAID(FeatureUnitControlSelector::AutomaticGain, RequestCode::SetCur, 0x0200): + { + ret = 0; + break; + } + default: + { + WARN_LOG_FMT(IOS_USB, + "SetAudioControl: Unknown request: bCs={:02x} bCn={:02x} bRequestType={:02x} " + "bRequest={:02x} bIndex={:02x} aid={:08x}", + Common::ToUnderlying(cs), cn, cmd->request_type, Common::ToUnderlying(request), + cmd->index, aid); + break; + } + } + return ret; +} + +int LogitechMic::EndpointAudioControl(std::unique_ptr& cmd) +{ + auto& system = cmd->GetEmulationKernel().GetSystem(); + auto& memory = system.GetMemory(); + const auto cs = static_cast(cmd->value >> 8); + const auto request = static_cast(cmd->request); + const u8 cn = static_cast(cmd->value - 1); + const u32 aid = USBGETAID(cs, request, cmd->index); + int ret = IPC_STALL; + DEBUG_LOG_FMT(IOS_USB, + "EndpointAudioControl: bCs={:02x} bCn={:02x} bRequestType={:02x} bRequest={:02x} " + "bIndex={:02x} aid:{:08x}", + Common::ToUnderlying(cs), cn, cmd->request_type, Common::ToUnderlying(request), + cmd->index, aid); + switch (aid) + { + case USBGETAID(EndpointControlSelector::SamplingFreq, RequestCode::SetCur, ENDPOINT_AUDIO_IN): + { + if (cn == 0xff) + { + const uint32_t sr = memory.Read_U8(cmd->data_address) | + (memory.Read_U8(cmd->data_address + 1) << 8) | + (memory.Read_U8(cmd->data_address + 2) << 16); + if (m_sampler.sample_rate != sr) + { + m_sampler.sample_rate = sr; + if (m_microphone != nullptr) + { + DEBUG_LOG_FMT(IOS_USB, "EndpointAudioControl: Setting sampling rate to {:d}", sr); + m_microphone->SetSamplingRate(sr); + } + } + } + else if (cn < 1) + { + WARN_LOG_FMT(IOS_USB, "EndpointAudioControl: Unsupported SAMPLER_FREQ_CONTROL set [cn={:d}]", + cn); + } + ret = 0; + break; + } + case USBGETAID(EndpointControlSelector::SamplingFreq, RequestCode::GetCur, ENDPOINT_AUDIO_IN): + { + memory.Write_U8(m_sampler.sample_rate & 0xff, cmd->data_address + 2); + memory.Write_U8((m_sampler.sample_rate >> 8) & 0xff, cmd->data_address + 1); + memory.Write_U8((m_sampler.sample_rate >> 16) & 0xff, cmd->data_address); + ret = 3; + break; + } + default: + { + WARN_LOG_FMT(IOS_USB, + "SetAudioControl: Unknown request: bCs={:02x} bCn={:02x} bRequestType={:02x} " + "bRequest={:02x} bIndex={:02x} aid={:08x}", + Common::ToUnderlying(cs), cn, cmd->request_type, Common::ToUnderlying(request), + cmd->index, aid); + break; + } + } + return ret; +} + +static constexpr std::array FULL_DESCRIPTOR = { + /* Configuration 1 */ + 0x09, /* bLength */ + 0x02, /* bDescriptorType */ + 0x79, 0x00, /* wTotalLength */ + 0x02, /* bNumInterfaces */ + 0x01, /* bConfigurationValue */ + 0x03, /* iConfiguration */ + 0x80, /* bmAttributes */ + 0x3c, /* bMaxPower */ + + /* Interface 0, Alternate Setting 0, Audio Control */ + 0x09, /* bLength */ + 0x04, /* bDescriptorType */ + 0x00, /* bInterfaceNumber */ + 0x00, /* bAlternateSetting */ + 0x00, /* bNumEndpoints */ + 0x01, /* bInterfaceClass */ + 0x01, /* bInterfaceSubClass */ + 0x00, /* bInterfaceProtocol */ + 0x00, /* iInterface */ + + /* Audio Control Interface */ + 0x09, /* bLength */ + 0x24, /* bDescriptorType */ + 0x01, /* bDescriptorSubtype */ + 0x00, 0x01, /* bcdADC */ + 0x27, 0x00, /* wTotalLength */ + 0x01, /* bInCollection */ + 0x01, /* baInterfaceNr */ + + /* Audio Input Terminal */ + 0x0c, /* bLength */ + 0x24, /* bDescriptorType */ + 0x02, /* bDescriptorSubtype */ + 0x0d, /* bTerminalID */ + 0x01, 0x02, /* wTerminalType */ + 0x00, /* bAssocTerminal */ + 0x01, /* bNrChannels */ + 0x00, 0x00, /* wChannelConfig */ + 0x00, /* iChannelNames */ + 0x00, /* iTerminal */ + + /* Audio Feature Unit */ + 0x09, /* bLength */ + 0x24, /* bDescriptorType */ + 0x06, /* bDescriptorSubtype */ + 0x02, /* bUnitID */ + 0x0d, /* bSourceID */ + 0x01, /* bControlSize */ + 0x03, /* bmaControls(0) */ + 0x00, /* bmaControls(1) */ + 0x00, /* iFeature */ + + /* Audio Output Terminal */ + 0x09, /* bLength */ + 0x24, /* bDescriptorType */ + 0x03, /* bDescriptorSubtype */ + 0x0a, /* bTerminalID */ + 0x01, 0x01, /* wTerminalType */ + 0x00, /* bAssocTerminal */ + 0x02, /* bSourceID */ + 0x00, /* iTerminal */ + + /* Interface 1, Alternate Setting 0, Audio Streaming - Zero Bandwith */ + 0x09, /* bLength */ + 0x04, /* bDescriptorType */ + 0x01, /* bInterfaceNumber */ + 0x00, /* bAlternateSetting */ + 0x00, /* bNumEndpoints */ + 0x01, /* bInterfaceClass */ + 0x02, /* bInterfaceSubClass */ + 0x00, /* bInterfaceProtocol */ + 0x00, /* iInterface */ + + /* Interface 1, Alternate Setting 1, Audio Streaming - 1 channel */ + 0x09, /* bLength */ + 0x04, /* bDescriptorType */ + 0x01, /* bInterfaceNumber */ + 0x01, /* bAlternateSetting */ + 0x01, /* bNumEndpoints */ + 0x01, /* bInterfaceClass */ + 0x02, /* bInterfaceSubClass */ + 0x00, /* bInterfaceProtocol */ + 0x00, /* iInterface */ + + /* Audio Streaming Interface */ + 0x07, /* bLength */ + 0x24, /* bDescriptorType */ + 0x01, /* bDescriptorSubtype */ + 0x0a, /* bTerminalLink */ + 0x00, /* bDelay */ + 0x01, 0x00, /* wFormatTag */ + + /* Audio Type I Format */ + 0x17, /* bLength */ + 0x24, /* bDescriptorType */ + 0x02, /* bDescriptorSubtype */ + 0x01, /* bFormatType */ + 0x01, /* bNrChannels */ + 0x02, /* bSubFrameSize */ + 0x10, /* bBitResolution */ + 0x05, /* bSamFreqType */ + 0x40, 0x1f, 0x00, /* tSamFreq 1 */ + 0x11, 0x2b, 0x00, /* tSamFreq 2 */ + 0x22, 0x56, 0x00, /* tSamFreq 3 */ + 0x44, 0xac, 0x00, /* tSamFreq 4 */ + 0x80, 0xbb, 0x00, /* tSamFreq 5 */ + + /* Endpoint - Standard Descriptor */ + 0x09, /* bLength */ + 0x05, /* bDescriptorType */ + 0x84, /* bEndpointAddress */ + 0x0d, /* bmAttributes */ + 0x60, 0x00, /* wMaxPacketSize */ + 0x01, /* bInterval */ + 0x00, /* bRefresh */ + 0x00, /* bSynchAddress */ + + /* Endpoint - Audio Streaming */ + 0x07, /* bLength */ + 0x25, /* bDescriptorType */ + 0x01, /* bDescriptor */ + 0x01, /* bmAttributes */ + 0x02, /* bLockDelayUnits */ + 0x01, 0x00 /* wLockDelay */ +}; + +static constexpr u16 LogitechUSBHDR(u8 dir, u8 type, u8 recipient, RequestCode request) +{ + return USBHDR(dir, type, recipient, Common::ToUnderlying(request)); +} + +int LogitechMic::SubmitTransfer(std::unique_ptr cmd) +{ + // Reference: https://www.usb.org/sites/default/files/audio10.pdf + DEBUG_LOG_FMT(IOS_USB, + "[{:04x}:{:04x} {}:{}] Control: bRequestType={:02x} bRequest={:02x} wValue={:04x}" + " wIndex={:04x} wLength={:04x}", + m_vid, m_pid, m_index, m_active_interface, cmd->request_type, cmd->request, + cmd->value, cmd->index, cmd->length); + switch (cmd->request_type << 8 | cmd->request) + { + case USBHDR(DIR_DEVICE2HOST, TYPE_STANDARD, REC_DEVICE, REQUEST_GET_DESCRIPTOR): + { + // It seems every game always pokes this at first twice; one with a length of 9 which gets the + // config, and then another with the length provided by the config. + DEBUG_LOG_FMT(IOS_USB, "[{:04x}:{:04x} {}:{}] REQUEST_GET_DESCRIPTOR index={:04x} value={:04x}", + m_vid, m_pid, m_index, m_active_interface, cmd->index, cmd->value); + cmd->FillBuffer(FULL_DESCRIPTOR.data(), std::min(cmd->length, FULL_DESCRIPTOR.size())); + cmd->GetEmulationKernel().EnqueueIPCReply(cmd->ios_request, IPC_SUCCESS); + break; + } + case USBHDR(DIR_HOST2DEVICE, TYPE_STANDARD, REC_INTERFACE, REQUEST_SET_INTERFACE): + { + DEBUG_LOG_FMT(IOS_USB, "[{:04x}:{:04x} {}:{}] REQUEST_SET_INTERFACE index={:04x} value={:04x}", + m_vid, m_pid, m_index, m_active_interface, cmd->index, cmd->value); + if (static_cast(cmd->index) != m_active_interface) + { + const int ret = ChangeInterface(static_cast(cmd->index)); + if (ret < 0) + { + ERROR_LOG_FMT(IOS_USB, "[{:04x}:{:04x} {}:{}] Failed to change interface to {}", m_vid, + m_pid, m_index, m_active_interface, cmd->index); + return ret; + } + } + const int ret = SetAltSetting(static_cast(cmd->value)); + if (ret == 0) + cmd->GetEmulationKernel().EnqueueIPCReply(cmd->ios_request, cmd->length); + return ret; + } + case LogitechUSBHDR(DIR_DEVICE2HOST, TYPE_CLASS, REC_INTERFACE, RequestCode::GetCur): + case LogitechUSBHDR(DIR_DEVICE2HOST, TYPE_CLASS, REC_INTERFACE, RequestCode::GetMin): + case LogitechUSBHDR(DIR_DEVICE2HOST, TYPE_CLASS, REC_INTERFACE, RequestCode::GetMax): + case LogitechUSBHDR(DIR_DEVICE2HOST, TYPE_CLASS, REC_INTERFACE, RequestCode::GetRes): + { + DEBUG_LOG_FMT(IOS_USB, "[{:04x}:{:04x} {}:{}] Get Control index={:04x} value={:04x}", m_vid, + m_pid, m_index, m_active_interface, cmd->index, cmd->value); + const int ret = GetAudioControl(cmd); + if (ret < 0) + { + ERROR_LOG_FMT(IOS_USB, + "[{:04x}:{:04x} {}:{}] Get Control Failed index={:04x} value={:04x} ret={}", + m_vid, m_pid, m_index, m_active_interface, cmd->index, cmd->value, ret); + cmd->GetEmulationKernel().EnqueueIPCReply(cmd->ios_request, IPC_STALL); + } + else + { + cmd->GetEmulationKernel().EnqueueIPCReply(cmd->ios_request, ret); + } + break; + } + case LogitechUSBHDR(DIR_DEVICE2HOST, TYPE_CLASS, REC_INTERFACE, RequestCode::SetCur): + case LogitechUSBHDR(DIR_DEVICE2HOST, TYPE_CLASS, REC_INTERFACE, RequestCode::SetMin): + case LogitechUSBHDR(DIR_DEVICE2HOST, TYPE_CLASS, REC_INTERFACE, RequestCode::SetMax): + case LogitechUSBHDR(DIR_DEVICE2HOST, TYPE_CLASS, REC_INTERFACE, RequestCode::SetRes): + { + DEBUG_LOG_FMT(IOS_USB, "[{:04x}:{:04x} {}:{}] Set Control index={:04x} value={:04x}", m_vid, + m_pid, m_index, m_active_interface, cmd->index, cmd->value); + const int ret = SetAudioControl(cmd); + if (ret < 0) + { + ERROR_LOG_FMT(IOS_USB, + "[{:04x}:{:04x} {}:{}] Set Control Failed index={:04x} value={:04x} ret={}", + m_vid, m_pid, m_index, m_active_interface, cmd->index, cmd->value, ret); + cmd->GetEmulationKernel().EnqueueIPCReply(cmd->ios_request, IPC_STALL); + } + else + { + cmd->GetEmulationKernel().EnqueueIPCReply(cmd->ios_request, ret); + } + break; + } + case LogitechUSBHDR(DIR_HOST2DEVICE, TYPE_CLASS, REC_ENDPOINT, RequestCode::GetCur): + case LogitechUSBHDR(DIR_HOST2DEVICE, TYPE_CLASS, REC_ENDPOINT, RequestCode::GetMin): + case LogitechUSBHDR(DIR_HOST2DEVICE, TYPE_CLASS, REC_ENDPOINT, RequestCode::GetMax): + case LogitechUSBHDR(DIR_HOST2DEVICE, TYPE_CLASS, REC_ENDPOINT, RequestCode::GetRes): + case LogitechUSBHDR(DIR_HOST2DEVICE, TYPE_CLASS, REC_ENDPOINT, RequestCode::SetCur): + case LogitechUSBHDR(DIR_HOST2DEVICE, TYPE_CLASS, REC_ENDPOINT, RequestCode::SetMin): + case LogitechUSBHDR(DIR_HOST2DEVICE, TYPE_CLASS, REC_ENDPOINT, RequestCode::SetMax): + case LogitechUSBHDR(DIR_HOST2DEVICE, TYPE_CLASS, REC_ENDPOINT, RequestCode::SetRes): + { + DEBUG_LOG_FMT(IOS_USB, "[{:04x}:{:04x} {}:{}] REC_ENDPOINT index={:04x} value={:04x}", m_vid, + m_pid, m_index, m_active_interface, cmd->index, cmd->value); + const int ret = EndpointAudioControl(cmd); + if (ret < 0) + { + ERROR_LOG_FMT( + IOS_USB, + "[{:04x}:{:04x} {}:{}] Enndpoint Control Failed index={:04x} value={:04x} ret={}", m_vid, + m_pid, m_index, m_active_interface, cmd->index, cmd->value, ret); + cmd->GetEmulationKernel().EnqueueIPCReply(cmd->ios_request, IPC_STALL); + } + else + { + cmd->GetEmulationKernel().EnqueueIPCReply(cmd->ios_request, ret); + } + break; + } + case LogitechUSBHDR(DIR_HOST2DEVICE, TYPE_CLASS, REC_INTERFACE, RequestCode::SetCur): + case LogitechUSBHDR(DIR_HOST2DEVICE, TYPE_CLASS, REC_INTERFACE, RequestCode::SetMin): + case LogitechUSBHDR(DIR_HOST2DEVICE, TYPE_CLASS, REC_INTERFACE, RequestCode::SetMax): + case LogitechUSBHDR(DIR_HOST2DEVICE, TYPE_CLASS, REC_INTERFACE, RequestCode::SetRes): + { + DEBUG_LOG_FMT(IOS_USB, + "[{:04x}:{:04x} {}:{}] Set Control HOST2DEVICE index={:04x} value={:04x}", m_vid, + m_pid, m_index, m_active_interface, cmd->index, cmd->value); + const int ret = SetAudioControl(cmd); + if (ret < 0) + { + ERROR_LOG_FMT( + IOS_USB, + "[{:04x}:{:04x} {}:{}] Set Control HOST2DEVICE Failed index={:04x} value={:04x} ret={}", + m_vid, m_pid, m_index, m_active_interface, cmd->index, cmd->value, ret); + cmd->GetEmulationKernel().EnqueueIPCReply(cmd->ios_request, IPC_STALL); + } + else + { + cmd->GetEmulationKernel().EnqueueIPCReply(cmd->ios_request, ret); + } + break; + } + default: + NOTICE_LOG_FMT(IOS_USB, "Unknown command"); + cmd->GetEmulationKernel().EnqueueIPCReply(cmd->ios_request, IPC_STALL); + } + + return IPC_SUCCESS; +} + +int LogitechMic::SubmitTransfer(std::unique_ptr cmd) +{ + cmd->GetEmulationKernel().EnqueueIPCReply(cmd->ios_request, IPC_SUCCESS); + return IPC_SUCCESS; +} + +int LogitechMic::SubmitTransfer(std::unique_ptr cmd) +{ + cmd->GetEmulationKernel().EnqueueIPCReply(cmd->ios_request, IPC_SUCCESS); + return IPC_SUCCESS; +} + +int LogitechMic::SubmitTransfer(std::unique_ptr cmd) +{ + auto& system = cmd->GetEmulationKernel().GetSystem(); + auto& memory = system.GetMemory(); + + u8* packets = memory.GetPointerForRange(cmd->data_address, cmd->length); + if (packets == nullptr) + { + ERROR_LOG_FMT(IOS_USB, "Logitech USB Microphone command invalid"); + return IPC_EINVAL; + } + + switch (cmd->endpoint) + { + case ENDPOINT_AUDIO_IN: + { + u16 size = 0; + if (m_microphone && m_microphone->HasData(cmd->length / sizeof(s16))) + size = m_microphone->ReadIntoBuffer(packets, cmd->length); + for (std::size_t i = 0; i < cmd->num_packets; i++) + { + cmd->SetPacketReturnValue(i, std::min(size, cmd->packet_sizes[i])); + size = (size > cmd->packet_sizes[i]) ? (size - cmd->packet_sizes[i]) : 0; + } + break; + } + default: + { + WARN_LOG_FMT(IOS_USB, + "Logitech Mic isochronous transfer, unsupported endpoint: length={:04x} " + "endpoint={:02x} num_packets={:02x}", + cmd->length, cmd->endpoint, cmd->num_packets); + } + } + + cmd->FillBuffer(packets, cmd->length); + cmd->ScheduleTransferCompletion(cmd->length, 1000); + return IPC_SUCCESS; +} +} // namespace IOS::HLE::USB diff --git a/Source/Core/Core/IOS/USB/Emulated/LogitechMic.h b/Source/Core/Core/IOS/USB/Emulated/LogitechMic.h new file mode 100644 index 00000000000..3ae5810344c --- /dev/null +++ b/Source/Core/Core/IOS/USB/Emulated/LogitechMic.h @@ -0,0 +1,66 @@ +// Copyright 2025 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include + +#include "Common/CommonTypes.h" +#include "Core/IOS/USB/Common.h" +#include "Core/IOS/USB/Emulated/Microphone.h" + +namespace IOS::HLE::USB +{ + +class LogitechMicState final : public MicrophoneState +{ +public: + // Use atomic for members concurrently used by the data callback + std::atomic mute; + std::atomic volume; + std::atomic sample_rate = DEFAULT_SAMPLING_RATE; + + static constexpr u32 DEFAULT_SAMPLING_RATE = 48000; + + bool IsSampleOn() const override; + bool IsMuted() const override; + u32 GetDefaultSamplingRate() const override; +}; + +class LogitechMic final : public Device +{ +public: + explicit LogitechMic(u8 index); + + DeviceDescriptor GetDeviceDescriptor() const override; + std::vector GetConfigurations() const override; + std::vector GetInterfaces(u8 config) const override; + std::vector GetEndpoints(u8 config, u8 interface, u8 alt) const override; + bool Attach() override; + bool AttachAndChangeInterface(u8 interface) override; + int CancelTransfer(u8 endpoint) override; + int ChangeInterface(u8 interface) override; + int GetNumberOfAltSettings(u8 interface) override; + int SetAltSetting(u8 alt_setting) override; + int SubmitTransfer(std::unique_ptr cmd) override; + int SubmitTransfer(std::unique_ptr cmd) override; + int SubmitTransfer(std::unique_ptr cmd) override; + int SubmitTransfer(std::unique_ptr cmd) override; + +private: + LogitechMicState m_sampler{}; + + int GetAudioControl(std::unique_ptr& cmd); + int SetAudioControl(std::unique_ptr& cmd); + int EndpointAudioControl(std::unique_ptr& cmd); + + const u16 m_vid = 0x046d; + const u16 m_pid = 0x0a03; + u8 m_index = 0; + u8 m_active_interface = 0; + bool m_device_attached = false; + std::unique_ptr m_microphone{}; +}; +} // namespace IOS::HLE::USB diff --git a/Source/Core/Core/IOS/USB/Emulated/Microphone.cpp b/Source/Core/Core/IOS/USB/Emulated/Microphone.cpp index be2bc57a426..a80c0e0cfe7 100644 --- a/Source/Core/Core/IOS/USB/Emulated/Microphone.cpp +++ b/Source/Core/Core/IOS/USB/Emulated/Microphone.cpp @@ -17,9 +17,7 @@ #include "Common/Logging/Log.h" #include "Common/MathUtil.h" #include "Common/Swap.h" -#include "Core/Config/MainSettings.h" #include "Core/Core.h" -#include "Core/IOS/USB/Emulated/WiiSpeak.h" #include "Core/System.h" #ifdef _WIN32 @@ -32,16 +30,28 @@ namespace IOS::HLE::USB { -Microphone::Microphone(const WiiSpeakState& sampler) : m_sampler(sampler) +#ifdef HAVE_CUBEB +Microphone::Microphone(const MicrophoneState& sampler, const std::string& worker_name) + : m_sampler(sampler), m_worker(worker_name) { - StreamInit(); } +#else +Microphone::Microphone(const MicrophoneState& sampler, const std::string& worker_name) + : m_sampler(sampler) +{ +} +#endif Microphone::~Microphone() { StreamTerminate(); } +void Microphone::Initialize() +{ + StreamInit(); +} + #ifndef HAVE_CUBEB void Microphone::StreamInit() { @@ -63,12 +73,12 @@ void Microphone::StreamInit() { if (!m_worker.Execute([this] { m_cubeb_ctx = CubebUtils::GetContext(); })) { - ERROR_LOG_FMT(IOS_USB, "Failed to init Wii Speak stream"); + ERROR_LOG_FMT(IOS_USB, "Failed to init microphone stream"); return; } // TODO: Not here but rather inside the WiiSpeak device if possible? - StreamStart(m_sampler.DEFAULT_SAMPLING_RATE); + StreamStart(m_sampler.GetDefaultSamplingRate()); } void Microphone::StreamTerminate() @@ -108,6 +118,7 @@ void Microphone::StreamStart(u32 sampling_rate) params.channels = 1; params.layout = CUBEB_LAYOUT_MONO; + std::lock_guard lock(m_ring_lock); u32 minimum_latency; if (cubeb_get_min_latency(m_cubeb_ctx.get(), ¶ms, &minimum_latency) != CUBEB_OK) { @@ -115,9 +126,8 @@ void Microphone::StreamStart(u32 sampling_rate) minimum_latency = 16; } - cubeb_devid input_device = - CubebUtils::GetInputDeviceById(Config::Get(Config::MAIN_WII_SPEAK_MICROPHONE)); - if (cubeb_stream_init(m_cubeb_ctx.get(), &m_cubeb_stream, "Dolphin Emulated Wii Speak", + cubeb_devid input_device = CubebUtils::GetInputDeviceById(GetInputDeviceId()); + if (cubeb_stream_init(m_cubeb_ctx.get(), &m_cubeb_stream, GetCubebStreamName().c_str(), input_device, ¶ms, nullptr, nullptr, std::max(16, minimum_latency), CubebDataCallback, StateCallback, this) != CUBEB_OK) @@ -132,6 +142,8 @@ void Microphone::StreamStart(u32 sampling_rate) return; } + m_stream_buffer.resize(GetStreamSize()); + m_stream_wpos = 0; INFO_LOG_FMT(IOS_USB, "started cubeb stream"); }); } @@ -156,12 +168,13 @@ long Microphone::CubebDataCallback(cubeb_stream* stream, void* user_data, const if (Core::GetState(Core::System::GetInstance()) != Core::State::Running) return nframes; + auto* mic = static_cast(user_data); + // Skip data when HLE Wii Speak is muted // TODO: Update cubeb and use cubeb_stream_set_input_mute - if (Config::Get(Config::MAIN_WII_SPEAK_MUTED)) + if (mic->IsMicrophoneMuted()) return nframes; - auto* mic = static_cast(user_data); return mic->DataCallback(static_cast(input_buffer), nframes); } @@ -170,27 +183,29 @@ long Microphone::DataCallback(const SampleType* input_buffer, long nframes) std::lock_guard lock(m_ring_lock); // Skip data if sampling is off or mute is on - if (!m_sampler.sample_on || m_sampler.mute) + if (!m_sampler.IsSampleOn() || m_sampler.IsMuted()) return nframes; std::span buffer(input_buffer, nframes); - const auto gain = ComputeGain(Config::Get(Config::MAIN_WII_SPEAK_VOLUME_MODIFIER)); + const auto gain = ComputeGain(GetVolumeModifier()); const auto apply_gain = [gain](SampleType sample) { return MathUtil::SaturatingCast(sample * gain); }; + const u32 stream_size = GetStreamSize(); + const bool are_samples_byte_swapped = AreSamplesByteSwapped(); for (const SampleType le_sample : std::ranges::transform_view(buffer, apply_gain)) { UpdateLoudness(le_sample); - m_stream_buffer[m_stream_wpos] = Common::swap16(le_sample); - m_stream_wpos = (m_stream_wpos + 1) % STREAM_SIZE; + m_stream_buffer[m_stream_wpos] = + are_samples_byte_swapped ? Common::swap16(le_sample) : le_sample; + m_stream_wpos = (m_stream_wpos + 1) % stream_size; } m_samples_avail += nframes; - if (m_samples_avail > STREAM_SIZE) + if (m_samples_avail > stream_size) { - WARN_LOG_FMT(IOS_USB, "Wii Speak ring buffer is full, data will be lost!"); - m_samples_avail = STREAM_SIZE; + m_samples_avail = stream_size; } return nframes; @@ -201,12 +216,9 @@ u16 Microphone::ReadIntoBuffer(u8* ptr, u32 size) { static constexpr u32 SINGLE_READ_SIZE = BUFF_SIZE_SAMPLES * sizeof(SampleType); - // Avoid buffer overflow during memcpy - static_assert((STREAM_SIZE % BUFF_SIZE_SAMPLES) == 0, - "The STREAM_SIZE isn't a multiple of BUFF_SIZE_SAMPLES"); - std::lock_guard lock(m_ring_lock); + const u32 stream_size = GetStreamSize(); u8* begin = ptr; for (u8* end = begin + size; ptr < end; ptr += SINGLE_READ_SIZE, size -= SINGLE_READ_SIZE) { @@ -218,14 +230,14 @@ u16 Microphone::ReadIntoBuffer(u8* ptr, u32 size) m_samples_avail -= BUFF_SIZE_SAMPLES; m_stream_rpos += BUFF_SIZE_SAMPLES; - m_stream_rpos %= STREAM_SIZE; + m_stream_rpos %= stream_size; } return static_cast(ptr - begin); } u16 Microphone::GetLoudnessLevel() const { - if (m_sampler.mute || Config::Get(Config::MAIN_WII_SPEAK_MUTED)) + if (m_sampler.IsMuted() || IsMicrophoneMuted()) return 0; return m_loudness_level; } @@ -365,7 +377,7 @@ void Microphone::Loudness::LogStats() const auto crest_factor_db = GetDecibel(crest_factor); INFO_LOG_FMT(IOS_USB, - "Wii Speak loudness stats (sample count: {}/{}):\n" + "Microphone loudness stats (sample count: {}/{}):\n" " - min={} max={} amplitude={} ({} dB)\n" " - rms={} ({} dB) \n" " - abs_mean={} ({} dB)\n" diff --git a/Source/Core/Core/IOS/USB/Emulated/Microphone.h b/Source/Core/Core/IOS/USB/Emulated/Microphone.h index a40e6008aa8..cf4e2327c10 100644 --- a/Source/Core/Core/IOS/USB/Emulated/Microphone.h +++ b/Source/Core/Core/IOS/USB/Emulated/Microphone.h @@ -3,14 +3,12 @@ #pragma once -#include #include #include #include #include #include -#include "AudioCommon/CubebUtils.h" #include "Common/CommonTypes.h" #ifdef HAVE_CUBEB @@ -22,31 +20,48 @@ struct cubeb_stream; namespace IOS::HLE::USB { -struct WiiSpeakState; +class MicrophoneState +{ +public: + virtual ~MicrophoneState() = default; -class Microphone final + virtual bool IsSampleOn() const = 0; + virtual bool IsMuted() const = 0; + virtual u32 GetDefaultSamplingRate() const = 0; +}; + +class Microphone { public: using FloatType = float; using SampleType = s16; using UnsignedSampleType = std::make_unsigned_t; - Microphone(const WiiSpeakState& sampler); - ~Microphone(); + Microphone(const MicrophoneState& sampler, const std::string& worker_name); + virtual ~Microphone(); + void Initialize(); bool HasData(u32 sample_count) const; u16 ReadIntoBuffer(u8* ptr, u32 size); u16 GetLoudnessLevel() const; FloatType ComputeGain(FloatType relative_db) const; void SetSamplingRate(u32 sampling_rate); +protected: + static constexpr u32 BUFF_SIZE_SAMPLES = 32; + private: #ifdef HAVE_CUBEB static long CubebDataCallback(cubeb_stream* stream, void* user_data, const void* input_buffer, void* output_buffer, long nframes); + virtual std::string GetInputDeviceId() const = 0; + virtual std::string GetCubebStreamName() const = 0; + virtual s16 GetVolumeModifier() const = 0; + virtual bool AreSamplesByteSwapped() const = 0; #endif long DataCallback(const SampleType* input_buffer, long nframes); + virtual bool IsMicrophoneMuted() const = 0; void UpdateLoudness(SampleType sample); void StreamInit(); @@ -54,10 +69,8 @@ private: void StreamStart(u32 sampling_rate); void StreamStop(); - static constexpr u32 BUFF_SIZE_SAMPLES = 32; - static constexpr u32 STREAM_SIZE = BUFF_SIZE_SAMPLES * 500; - - std::array m_stream_buffer{}; + virtual u32 GetStreamSize() const = 0; + std::vector m_stream_buffer{}; u32 m_stream_wpos = 0; u32 m_stream_rpos = 0; u32 m_samples_avail = 0; @@ -102,12 +115,12 @@ private: mutable std::mutex m_ring_lock; - const WiiSpeakState& m_sampler; + const MicrophoneState& m_sampler; #ifdef HAVE_CUBEB std::shared_ptr m_cubeb_ctx = nullptr; cubeb_stream* m_cubeb_stream = nullptr; - CubebUtils::CoInitSyncWorker m_worker{"Wii Speak Worker"}; + CubebUtils::CoInitSyncWorker m_worker; #endif }; } // namespace IOS::HLE::USB diff --git a/Source/Core/Core/IOS/USB/Emulated/WiiSpeak.cpp b/Source/Core/Core/IOS/USB/Emulated/WiiSpeak.cpp index 686906fd555..eaaadacc945 100644 --- a/Source/Core/Core/IOS/USB/Emulated/WiiSpeak.cpp +++ b/Source/Core/Core/IOS/USB/Emulated/WiiSpeak.cpp @@ -5,11 +5,56 @@ #include +#include "Core/Config/MainSettings.h" #include "Core/HW/Memmap.h" #include "Core/System.h" namespace IOS::HLE::USB { +bool WiiSpeakState::IsSampleOn() const +{ + return sample_on; +} + +bool WiiSpeakState::IsMuted() const +{ + return mute; +} + +u32 WiiSpeakState::GetDefaultSamplingRate() const +{ + return DEFAULT_SAMPLING_RATE; +} + +namespace +{ +class MicrophoneWiiSpeak final : public Microphone +{ +public: + explicit MicrophoneWiiSpeak(const WiiSpeakState& sampler) + : Microphone(sampler, "Wii Speak Worker") + { + } + +private: +#ifdef HAVE_CUBEB + std::string GetInputDeviceId() const override + { + return Config::Get(Config::MAIN_WII_SPEAK_MICROPHONE); + } + std::string GetCubebStreamName() const override { return "Dolphin Emulated Wii Speak"; } + s16 GetVolumeModifier() const override + { + return Config::Get(Config::MAIN_WII_SPEAK_VOLUME_MODIFIER); + } + bool AreSamplesByteSwapped() const override { return true; } +#endif + + bool IsMicrophoneMuted() const override { return Config::Get(Config::MAIN_WII_SPEAK_MUTED); } + u32 GetStreamSize() const override { return BUFF_SIZE_SAMPLES * 500; } +}; +} // namespace + WiiSpeak::WiiSpeak() { m_id = u64(m_vid) << 32 | u64(m_pid) << 16 | u64(9) << 8 | u64(1); @@ -44,7 +89,10 @@ bool WiiSpeak::Attach() DEBUG_LOG_FMT(IOS_USB, "[{:04x}:{:04x}] Opening device", m_vid, m_pid); if (!m_microphone) - m_microphone = std::make_unique(m_sampler); + { + m_microphone = std::make_unique(m_sampler); + m_microphone->Initialize(); + } m_device_attached = true; return true; } diff --git a/Source/Core/Core/IOS/USB/Emulated/WiiSpeak.h b/Source/Core/Core/IOS/USB/Emulated/WiiSpeak.h index a58ddb4ffd5..9a0ae136746 100644 --- a/Source/Core/Core/IOS/USB/Emulated/WiiSpeak.h +++ b/Source/Core/Core/IOS/USB/Emulated/WiiSpeak.h @@ -13,8 +13,9 @@ namespace IOS::HLE::USB { -struct WiiSpeakState +class WiiSpeakState final : public MicrophoneState { +public: // Use atomic for members concurrently used by the data callback std::atomic sample_on; std::atomic mute; @@ -24,6 +25,10 @@ struct WiiSpeakState bool sp_on; static constexpr u32 DEFAULT_SAMPLING_RATE = 16000; + + bool IsSampleOn() const override; + bool IsMuted() const override; + u32 GetDefaultSamplingRate() const override; }; class WiiSpeak final : public Device diff --git a/Source/Core/Core/IOS/USB/USBScanner.cpp b/Source/Core/Core/IOS/USB/USBScanner.cpp index aa213ce412d..2893d1016d8 100644 --- a/Source/Core/Core/IOS/USB/USBScanner.cpp +++ b/Source/Core/Core/IOS/USB/USBScanner.cpp @@ -22,6 +22,7 @@ #include "Core/Core.h" #include "Core/IOS/USB/Common.h" #include "Core/IOS/USB/Emulated/Infinity.h" +#include "Core/IOS/USB/Emulated/LogitechMic.h" #include "Core/IOS/USB/Emulated/Skylanders/Skylander.h" #include "Core/IOS/USB/Emulated/WiiSpeak.h" #include "Core/IOS/USB/Host.h" @@ -189,6 +190,14 @@ void USBScanner::AddEmulatedDevices(DeviceMap* new_devices) auto wii_speak = std::make_unique(); AddDevice(std::move(wii_speak), new_devices); } + for (u8 index = 0; index != Config::EMULATED_LOGITECH_MIC_COUNT; ++index) + { + if (Config::Get(Config::MAIN_EMULATE_LOGITECH_MIC[index]) && !NetPlay::IsNetPlayRunning()) + { + auto logitech_mic = std::make_unique(index); + AddDevice(std::move(logitech_mic), new_devices); + } + } } void USBScanner::WakeupSantrollerDevice(libusb_device* device) diff --git a/Source/Core/DolphinLib.props b/Source/Core/DolphinLib.props index 5049ac785f6..ecf1f6a1841 100644 --- a/Source/Core/DolphinLib.props +++ b/Source/Core/DolphinLib.props @@ -411,6 +411,7 @@ + @@ -1091,6 +1092,7 @@ + diff --git a/Source/Core/DolphinQt/CMakeLists.txt b/Source/Core/DolphinQt/CMakeLists.txt index ba2e5522a42..23b34161ebd 100644 --- a/Source/Core/DolphinQt/CMakeLists.txt +++ b/Source/Core/DolphinQt/CMakeLists.txt @@ -262,6 +262,8 @@ add_executable(dolphin-emu DiscordJoinRequestDialog.h EmulatedUSB/WiiSpeakWindow.cpp EmulatedUSB/WiiSpeakWindow.h + EmulatedUSB/LogitechMicWindow.cpp + EmulatedUSB/LogitechMicWindow.h FIFO/FIFOAnalyzer.cpp FIFO/FIFOAnalyzer.h FIFO/FIFOPlayerWindow.cpp diff --git a/Source/Core/DolphinQt/DolphinQt.vcxproj b/Source/Core/DolphinQt/DolphinQt.vcxproj index fe5a2dda4a7..f91dab20f93 100644 --- a/Source/Core/DolphinQt/DolphinQt.vcxproj +++ b/Source/Core/DolphinQt/DolphinQt.vcxproj @@ -160,6 +160,7 @@ + @@ -387,6 +388,7 @@ + diff --git a/Source/Core/DolphinQt/EmulatedUSB/LogitechMicWindow.cpp b/Source/Core/DolphinQt/EmulatedUSB/LogitechMicWindow.cpp new file mode 100644 index 00000000000..04b4a8b16e0 --- /dev/null +++ b/Source/Core/DolphinQt/EmulatedUSB/LogitechMicWindow.cpp @@ -0,0 +1,162 @@ +// Copyright 2025 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "DolphinQt/EmulatedUSB/LogitechMicWindow.h" + +#include + +#include +#include +#include +#include +#include +#include +#include + +#ifdef HAVE_CUBEB +#include "AudioCommon/CubebUtils.h" +#endif +#include "Core/Config/MainSettings.h" +#include "Core/Core.h" +#include "Core/System.h" +#include "DolphinQt/Config/ConfigControls/ConfigBool.h" +#include "DolphinQt/Resources.h" +#include "DolphinQt/Settings.h" + +LogitechMicWindow::LogitechMicWindow(QWidget* parent) : QWidget(parent) +{ + setWindowTitle(tr("Logitech USB Microphone Manager")); + setWindowIcon(Resources::GetAppIcon()); + setObjectName(QStringLiteral("logitech_mic_manager")); + setMinimumSize(QSize(700, 200)); + + CreateMainWindow(); + + connect(&Settings::Instance(), &Settings::EmulationStateChanged, this, + &LogitechMicWindow::OnEmulationStateChanged); + + OnEmulationStateChanged(Core::GetState(Core::System::GetInstance())); +} + +void LogitechMicWindow::CreateMainWindow() +{ + auto* const main_layout = new QVBoxLayout; + auto* const label = new QLabel; + label->setText(QStringLiteral("
%1
") + .arg(tr("Some settings cannot be changed when emulation is running."))); + main_layout->addWidget(label); + + CreateCheckboxGroup(main_layout); + + CreateMicrophoneConfigurationGroup(main_layout); + + setLayout(main_layout); +} + +void LogitechMicWindow::CreateCheckboxGroup(QVBoxLayout* main_layout) +{ + auto* checkbox_group = new QGroupBox(); + auto* checkbox_layout = new QHBoxLayout(); + checkbox_layout->setAlignment(Qt::AlignHCenter); + + for (std::size_t index = 0; index != Config::EMULATED_LOGITECH_MIC_COUNT; ++index) + { + m_mic_enabled_checkboxes[index] = new ConfigBool( + tr("Emulate Logitech USB Mic %1").arg(index + 1), Config::MAIN_EMULATE_LOGITECH_MIC[index]); + checkbox_layout->addWidget(m_mic_enabled_checkboxes[index]); + } + + checkbox_group->setLayout(checkbox_layout); + main_layout->addWidget(checkbox_group); +} + +void LogitechMicWindow::CreateMicrophoneConfigurationGroup(QVBoxLayout* main_layout) +{ + auto* main_config_group = new QGroupBox(tr("Microphone Configuration")); + auto* main_config_layout = new QVBoxLayout(); + + for (std::size_t index = 0; index != Config::EMULATED_LOGITECH_MIC_COUNT; ++index) + { + // i18n: %1 is a number from 1 to 4. + auto* config_group = new QGroupBox(tr("Microphone %1 Configuration").arg(index + 1)); + auto* const config_layout = new QHBoxLayout(); + + auto* const mic_muted = new ConfigBool(tr("Mute"), Config::MAIN_LOGITECH_MIC_MUTED[index]); + mic_muted->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); + config_layout->addWidget(mic_muted); + + static constexpr int FILTER_MIN = -50; + static constexpr int FILTER_MAX = 50; + + auto* volume_layout = new QGridLayout(); + const int volume_modifier = std::clamp( + Config::Get(Config::MAIN_LOGITECH_MIC_VOLUME_MODIFIER[index]), FILTER_MIN, FILTER_MAX); + auto* const filter_slider = new QSlider(Qt::Horizontal, this); + auto* const slider_label = new QLabel(tr("Volume modifier (value: %1dB)").arg(volume_modifier)); + connect(filter_slider, &QSlider::valueChanged, this, [slider_label, index](int value) { + Config::SetBaseOrCurrent(Config::MAIN_LOGITECH_MIC_VOLUME_MODIFIER[index], value); + slider_label->setText(tr("Volume modifier (value: %1dB)").arg(value)); + }); + filter_slider->setMinimum(FILTER_MIN); + filter_slider->setMaximum(FILTER_MAX); + filter_slider->setValue(volume_modifier); + filter_slider->setTickPosition(QSlider::TicksBothSides); + filter_slider->setTickInterval(10); + filter_slider->setSingleStep(1); + volume_layout->addWidget(new QLabel(QStringLiteral("%1dB").arg(FILTER_MIN)), 0, 0, + Qt::AlignLeft); + volume_layout->addWidget(slider_label, 0, 1, Qt::AlignCenter); + volume_layout->addWidget(new QLabel(QStringLiteral("%1dB").arg(FILTER_MAX)), 0, 2, + Qt::AlignRight); + volume_layout->addWidget(filter_slider, 1, 0, 1, 3); + config_layout->addLayout(volume_layout); + config_layout->setStretch(1, 3); + + m_mic_device_comboboxes[index] = new QComboBox(); +#ifndef HAVE_CUBEB + m_combobox_microphone[index]->addItem( + QLatin1String("(%1)").arg(tr("Audio backend unsupported")), QString{}); +#else + m_mic_device_comboboxes[index]->addItem( + QLatin1String("(%1)").arg(tr("Autodetect preferred microphone")), QString{}); + for (auto& [device_id, device_name] : CubebUtils::ListInputDevices()) + { + const auto user_data = QString::fromStdString(device_id); + m_mic_device_comboboxes[index]->addItem(QString::fromStdString(device_name), user_data); + } +#endif + auto current_device_id = + QString::fromStdString(Config::Get(Config::MAIN_LOGITECH_MIC_MICROPHONE[index])); + m_mic_device_comboboxes[index]->setCurrentIndex( + m_mic_device_comboboxes[index]->findData(current_device_id)); + connect(m_mic_device_comboboxes[index], &QComboBox::currentIndexChanged, this, + [index, this]() { OnInputDeviceChange(index); }); + config_layout->addWidget(m_mic_device_comboboxes[index]); + + config_group->setLayout(config_layout); + main_config_layout->addWidget(config_group); + } + + main_config_group->setLayout(main_config_layout); + main_layout->addWidget(main_config_group); +} + +void LogitechMicWindow::OnEmulationStateChanged(Core::State state) +{ + const bool running = state != Core::State::Uninitialized; + + for (std::size_t index = 0; index != Config::EMULATED_LOGITECH_MIC_COUNT; ++index) + { + m_mic_enabled_checkboxes[index]->setEnabled(!running); + } +} + +void LogitechMicWindow::OnInputDeviceChange(std::size_t index) +{ + auto user_data = m_mic_device_comboboxes[index]->currentData(); + if (user_data.isValid()) + { + const std::string device_id = user_data.toString().toStdString(); + Config::SetBaseOrCurrent(Config::MAIN_LOGITECH_MIC_MICROPHONE[index], device_id); + } +} diff --git a/Source/Core/DolphinQt/EmulatedUSB/LogitechMicWindow.h b/Source/Core/DolphinQt/EmulatedUSB/LogitechMicWindow.h new file mode 100644 index 00000000000..746aeed3aba --- /dev/null +++ b/Source/Core/DolphinQt/EmulatedUSB/LogitechMicWindow.h @@ -0,0 +1,31 @@ +// Copyright 2025 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include + +#include "Core/Config/MainSettings.h" +#include "Core/Core.h" + +class ConfigBool; +class QComboBox; + +class LogitechMicWindow final : public QWidget +{ + Q_OBJECT +public: + explicit LogitechMicWindow(QWidget* parent = nullptr); + +private: + void CreateMainWindow(); + void CreateCheckboxGroup(QVBoxLayout* main_layout); + void CreateMicrophoneConfigurationGroup(QVBoxLayout* main_layout); + + void OnEmulationStateChanged(Core::State state); + void OnInputDeviceChange(std::size_t index); + + std::array m_mic_enabled_checkboxes; + std::array m_mic_device_comboboxes; +}; diff --git a/Source/Core/DolphinQt/MainWindow.cpp b/Source/Core/DolphinQt/MainWindow.cpp index 55dcb321b87..99aaaffae63 100644 --- a/Source/Core/DolphinQt/MainWindow.cpp +++ b/Source/Core/DolphinQt/MainWindow.cpp @@ -93,6 +93,7 @@ #include "DolphinQt/Debugger/ThreadWidget.h" #include "DolphinQt/Debugger/WatchWidget.h" #include "DolphinQt/DiscordHandler.h" +#include "DolphinQt/EmulatedUSB/LogitechMicWindow.h" #include "DolphinQt/EmulatedUSB/WiiSpeakWindow.h" #include "DolphinQt/FIFO/FIFOPlayerWindow.h" #include "DolphinQt/GCMemcardManager.h" @@ -571,6 +572,7 @@ void MainWindow::ConnectMenuBar() connect(m_menu_bar, &MenuBar::ShowSkylanderPortal, this, &MainWindow::ShowSkylanderPortal); connect(m_menu_bar, &MenuBar::ShowInfinityBase, this, &MainWindow::ShowInfinityBase); connect(m_menu_bar, &MenuBar::ShowWiiSpeakWindow, this, &MainWindow::ShowWiiSpeakWindow); + connect(m_menu_bar, &MenuBar::ShowLogitechMicWindow, this, &MainWindow::ShowLogitechMicWindow); connect(m_menu_bar, &MenuBar::ConnectWiiRemote, this, &MainWindow::OnConnectWiiRemote); #ifdef USE_RETRO_ACHIEVEMENTS @@ -1424,6 +1426,18 @@ void MainWindow::ShowWiiSpeakWindow() m_wii_speak_window->activateWindow(); } +void MainWindow::ShowLogitechMicWindow() +{ + if (!m_logitech_mic_window) + { + m_logitech_mic_window = new LogitechMicWindow(); + } + + m_logitech_mic_window->show(); + m_logitech_mic_window->raise(); + m_logitech_mic_window->activateWindow(); +} + void MainWindow::StateLoad() { QString dialog_path = (Config::Get(Config::MAIN_CURRENT_STATE_PATH).empty()) ? diff --git a/Source/Core/DolphinQt/MainWindow.h b/Source/Core/DolphinQt/MainWindow.h index c01da7f5d37..a089935f612 100644 --- a/Source/Core/DolphinQt/MainWindow.h +++ b/Source/Core/DolphinQt/MainWindow.h @@ -55,6 +55,7 @@ class ToolBar; class WatchWidget; class WiiTASInputWindow; class WiiSpeakWindow; +class LogitechMicWindow; struct WindowSystemInfo; namespace Core @@ -177,6 +178,7 @@ private: void ShowSkylanderPortal(); void ShowInfinityBase(); void ShowWiiSpeakWindow(); + void ShowLogitechMicWindow(); void ShowMemcardManager(); void ShowResourcePackManager(); void ShowCheatsManager(); @@ -252,6 +254,7 @@ private: SkylanderPortalWindow* m_skylander_window = nullptr; InfinityBaseWindow* m_infinity_window = nullptr; WiiSpeakWindow* m_wii_speak_window = nullptr; + LogitechMicWindow* m_logitech_mic_window = nullptr; MappingWindow* m_hotkey_window = nullptr; FreeLookWindow* m_freelook_window = nullptr; diff --git a/Source/Core/DolphinQt/MenuBar.cpp b/Source/Core/DolphinQt/MenuBar.cpp index 152bfa648c9..f98db4320ec 100644 --- a/Source/Core/DolphinQt/MenuBar.cpp +++ b/Source/Core/DolphinQt/MenuBar.cpp @@ -281,6 +281,7 @@ void MenuBar::AddToolsMenu() usb_device_menu->addAction(tr("&Skylanders Portal"), this, &MenuBar::ShowSkylanderPortal); usb_device_menu->addAction(tr("&Infinity Base"), this, &MenuBar::ShowInfinityBase); usb_device_menu->addAction(tr("&Wii Speak"), this, &MenuBar::ShowWiiSpeakWindow); + usb_device_menu->addAction(tr("&Logitech USB Microphone"), this, &MenuBar::ShowLogitechMicWindow); tools_menu->addMenu(usb_device_menu); tools_menu->addSeparator(); diff --git a/Source/Core/DolphinQt/MenuBar.h b/Source/Core/DolphinQt/MenuBar.h index 5a1a95dc37e..1f2abd3a3b8 100644 --- a/Source/Core/DolphinQt/MenuBar.h +++ b/Source/Core/DolphinQt/MenuBar.h @@ -98,6 +98,7 @@ signals: void ShowSkylanderPortal(); void ShowInfinityBase(); void ShowWiiSpeakWindow(); + void ShowLogitechMicWindow(); void ConnectWiiRemote(int id); #ifdef USE_RETRO_ACHIEVEMENTS