summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJosJuice <josjuice@gmail.com>2025-11-25 23:12:39 +0100
committerGitHub <noreply@github.com>2025-11-25 23:12:39 +0100
commit807488caf9cf41599c7c814a8bbcbc2587721ab4 (patch)
treeec0192a662ab7f361f85887005cf1d7fb05747f2
parentfa2a9e72f9fd2ca445f0fdfdebac0a5d2cf97749 (diff)
parent07324ca067078d75517b20d90c353628f7e755b1 (diff)
Merge pull request #14066 from Biendeo/master
IOS: Logitech USB Microphone Basic Support
-rw-r--r--Source/Core/Core/CMakeLists.txt2
-rw-r--r--Source/Core/Core/Config/MainSettings.cpp21
-rw-r--r--Source/Core/Core/Config/MainSettings.h8
-rw-r--r--Source/Core/Core/IOS/Device.h1
-rw-r--r--Source/Core/Core/IOS/USB/Common.h4
-rw-r--r--Source/Core/Core/IOS/USB/Emulated/LogitechMic.cpp739
-rw-r--r--Source/Core/Core/IOS/USB/Emulated/LogitechMic.h66
-rw-r--r--Source/Core/Core/IOS/USB/Emulated/Microphone.cpp62
-rw-r--r--Source/Core/Core/IOS/USB/Emulated/Microphone.h37
-rw-r--r--Source/Core/Core/IOS/USB/Emulated/WiiSpeak.cpp50
-rw-r--r--Source/Core/Core/IOS/USB/Emulated/WiiSpeak.h7
-rw-r--r--Source/Core/Core/IOS/USB/USBScanner.cpp9
-rw-r--r--Source/Core/DolphinLib.props2
-rw-r--r--Source/Core/DolphinQt/CMakeLists.txt2
-rw-r--r--Source/Core/DolphinQt/DolphinQt.vcxproj2
-rw-r--r--Source/Core/DolphinQt/EmulatedUSB/LogitechMicWindow.cpp162
-rw-r--r--Source/Core/DolphinQt/EmulatedUSB/LogitechMicWindow.h31
-rw-r--r--Source/Core/DolphinQt/MainWindow.cpp14
-rw-r--r--Source/Core/DolphinQt/MainWindow.h3
-rw-r--r--Source/Core/DolphinQt/MenuBar.cpp1
-rw-r--r--Source/Core/DolphinQt/MenuBar.h1
21 files changed, 1185 insertions, 39 deletions
diff --git a/Source/Core/Core/CMakeLists.txt b/Source/Core/Core/CMakeLists.txt
index 7fc503e2f9..be75ba89d9 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 b06fb5b496..4e06ec3713 100644
--- a/Source/Core/Core/Config/MainSettings.cpp
+++ b/Source/Core/Core/Config/MainSettings.cpp
@@ -615,6 +615,27 @@ const Info<bool> MAIN_WII_SPEAK_MUTED{{System::Main, "EmulatedUSBDevices", "WiiS
const Info<s16> MAIN_WII_SPEAK_VOLUME_MODIFIER{
{System::Main, "EmulatedUSBDevices", "WiiSpeakVolumeModifier"}, 0};
+const std::array<Info<bool>, EMULATED_LOGITECH_MIC_COUNT> MAIN_EMULATE_LOGITECH_MIC{
+ Info<bool>{{System::Main, "EmulatedUSBDevices", "EmulateLogitechMic1"}, false},
+ Info<bool>{{System::Main, "EmulatedUSBDevices", "EmulateLogitechMic2"}, false},
+ Info<bool>{{System::Main, "EmulatedUSBDevices", "EmulateLogitechMic3"}, false},
+ Info<bool>{{System::Main, "EmulatedUSBDevices", "EmulateLogitechMic4"}, false}};
+const std::array<Info<std::string>, EMULATED_LOGITECH_MIC_COUNT> MAIN_LOGITECH_MIC_MICROPHONE{
+ Info<std::string>{{System::Main, "EmulatedUSBDevices", "LogitechMic1Microphone"}, ""},
+ Info<std::string>{{System::Main, "EmulatedUSBDevices", "LogitechMic2Microphone"}, ""},
+ Info<std::string>{{System::Main, "EmulatedUSBDevices", "LogitechMic3Microphone"}, ""},
+ Info<std::string>{{System::Main, "EmulatedUSBDevices", "LogitechMic4Microphone"}, ""}};
+const std::array<Info<bool>, EMULATED_LOGITECH_MIC_COUNT> MAIN_LOGITECH_MIC_MUTED{
+ Info<bool>{{System::Main, "EmulatedUSBDevices", "LogitechMic1Muted"}, false},
+ Info<bool>{{System::Main, "EmulatedUSBDevices", "LogitechMic2Muted"}, false},
+ Info<bool>{{System::Main, "EmulatedUSBDevices", "LogitechMic3Muted"}, false},
+ Info<bool>{{System::Main, "EmulatedUSBDevices", "LogitechMic4Muted"}, false}};
+const std::array<Info<s16>, EMULATED_LOGITECH_MIC_COUNT> MAIN_LOGITECH_MIC_VOLUME_MODIFIER{
+ Info<s16>{{System::Main, "EmulatedUSBDevices", "LogitechMic1VolumeModifier"}, 0},
+ Info<s16>{{System::Main, "EmulatedUSBDevices", "LogitechMic2VolumeModifier"}, 0},
+ Info<s16>{{System::Main, "EmulatedUSBDevices", "LogitechMic3VolumeModifier"}, 0},
+ Info<s16>{{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 cc64c80d69..86f206d3f4 100644
--- a/Source/Core/Core/Config/MainSettings.h
+++ b/Source/Core/Core/Config/MainSettings.h
@@ -375,6 +375,14 @@ extern const Info<std::string> MAIN_WII_SPEAK_MICROPHONE;
extern const Info<bool> MAIN_WII_SPEAK_MUTED;
extern const Info<s16> MAIN_WII_SPEAK_VOLUME_MODIFIER;
+static constexpr std::size_t EMULATED_LOGITECH_MIC_COUNT = 4;
+
+extern const std::array<Info<bool>, EMULATED_LOGITECH_MIC_COUNT> MAIN_EMULATE_LOGITECH_MIC;
+extern const std::array<Info<std::string>, EMULATED_LOGITECH_MIC_COUNT>
+ MAIN_LOGITECH_MIC_MICROPHONE;
+extern const std::array<Info<bool>, EMULATED_LOGITECH_MIC_COUNT> MAIN_LOGITECH_MIC_MUTED;
+extern const std::array<Info<s16>, 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 d393dcd239..35d2f79e91 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 fbd1de202d..cdc7c39dad 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 0000000000..8380c533dc
--- /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 <algorithm>
+
+#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<ConfigDescriptor> CONFIG_DESCRIPTOR{
+ ConfigDescriptor{0x09, 0x02, 0x0079, 0x02, 0x01, 0x03, 0x80, 0x3c},
+};
+
+std::vector<ConfigDescriptor> LogitechMic::GetConfigurations() const
+{
+ return CONFIG_DESCRIPTOR;
+}
+
+static const std::vector<InterfaceDescriptor> 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<InterfaceDescriptor> LogitechMic::GetInterfaces(u8 /*config*/) const
+{
+ return INTERFACE_DESCRIPTORS;
+}
+
+static constexpr u8 ENDPOINT_AUDIO_IN = 0x84;
+static const std::vector<EndpointDescriptor> ENDPOINT_DESCRIPTORS{
+ EndpointDescriptor{0x09, 0x05, ENDPOINT_AUDIO_IN, 0x0d, 0x0060, 0x01},
+};
+
+std::vector<EndpointDescriptor> LogitechMic::GetEndpoints(u8 /*config*/, u8 interface, u8 alt) const
+{
+ if (interface == 1 && alt == 1)
+ return ENDPOINT_DESCRIPTORS;
+
+ return std::vector<EndpointDescriptor>{};
+}
+
+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<MicrophoneLogitech>(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<u32>((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<CtrlMessage>& cmd)
+{
+ auto& system = cmd->GetEmulationKernel().GetSystem();
+ auto& memory = system.GetMemory();
+ const auto cs = static_cast<FeatureUnitControlSelector>(cmd->value >> 8);
+ const auto request = static_cast<RequestCode>(cmd->request);
+ const u8 cn = static_cast<u8>(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<CtrlMessage>& cmd)
+{
+ auto& system = cmd->GetEmulationKernel().GetSystem();
+ auto& memory = system.GetMemory();
+ const auto cs = static_cast<FeatureUnitControlSelector>(cmd->value >> 8);
+ const auto request = static_cast<RequestCode>(cmd->request);
+ const u8 cn = static_cast<u8>(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<uint16_t>(vol, 255);
+
+ if (m_sampler.volume != vol)
+ m_sampler.volume = static_cast<u8>(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<CtrlMessage>& cmd)
+{
+ auto& system = cmd->GetEmulationKernel().GetSystem();
+ auto& memory = system.GetMemory();
+ const auto cs = static_cast<EndpointControlSelector>(cmd->value >> 8);
+ const auto request = static_cast<RequestCode>(cmd->request);
+ const u8 cn = static_cast<u8>(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<u8, 121> 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<CtrlMessage> 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<size_t>(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<u8>(cmd->index) != m_active_interface)
+ {
+ const int ret = ChangeInterface(static_cast<u8>(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<u8>(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<BulkMessage> cmd)
+{
+ cmd->GetEmulationKernel().EnqueueIPCReply(cmd->ios_request, IPC_SUCCESS);
+ return IPC_SUCCESS;
+}
+
+int LogitechMic::SubmitTransfer(std::unique_ptr<IntrMessage> cmd)
+{
+ cmd->GetEmulationKernel().EnqueueIPCReply(cmd->ios_request, IPC_SUCCESS);
+ return IPC_SUCCESS;
+}
+
+int LogitechMic::SubmitTransfer(std::unique_ptr<IsoMessage> 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 0000000000..3ae5810344
--- /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 <atomic>
+#include <memory>
+#include <vector>
+
+#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<bool> mute;
+ std::atomic<u8> volume;
+ std::atomic<u32> 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<ConfigDescriptor> GetConfigurations() const override;
+ std::vector<InterfaceDescriptor> GetInterfaces(u8 config) const override;
+ std::vector<EndpointDescriptor> 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<CtrlMessage> cmd) override;
+ int SubmitTransfer(std::unique_ptr<BulkMessage> cmd) override;
+ int SubmitTransfer(std::unique_ptr<IntrMessage> cmd) override;
+ int SubmitTransfer(std::unique_ptr<IsoMessage> cmd) override;
+
+private:
+ LogitechMicState m_sampler{};
+
+ int GetAudioControl(std::unique_ptr<CtrlMessage>& cmd);
+ int SetAudioControl(std::unique_ptr<CtrlMessage>& cmd);
+ int EndpointAudioControl(std::unique_ptr<CtrlMessage>& 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<Microphone> 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 be2bc57a42..a80c0e0cfe 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(), &params, &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, &params, nullptr, nullptr,
std::max<u32>(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<Microphone*>(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<Microphone*>(user_data);
return mic->DataCallback(static_cast<const SampleType*>(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<const SampleType> 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<SampleType>(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<u16>(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 a40e6008aa..cf4e2327c1 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 <array>
#include <atomic>
#include <limits>
#include <memory>
#include <mutex>
#include <type_traits>
-#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;
+
+ virtual bool IsSampleOn() const = 0;
+ virtual bool IsMuted() const = 0;
+ virtual u32 GetDefaultSamplingRate() const = 0;
+};
-class Microphone final
+class Microphone
{
public:
using FloatType = float;
using SampleType = s16;
using UnsignedSampleType = std::make_unsigned_t<SampleType>;
- 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<SampleType, STREAM_SIZE> m_stream_buffer{};
+ virtual u32 GetStreamSize() const = 0;
+ std::vector<SampleType> 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<cubeb> 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 686906fd55..eaaadacc94 100644
--- a/Source/Core/Core/IOS/USB/Emulated/WiiSpeak.cpp
+++ b/Source/Core/Core/IOS/USB/Emulated/WiiSpeak.cpp
@@ -5,11 +5,56 @@
#include <algorithm>
+#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<Microphone>(m_sampler);
+ {
+ m_microphone = std::make_unique<MicrophoneWiiSpeak>(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 a58ddb4ffd..9a0ae13674 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<bool> sample_on;
std::atomic<bool> 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 aa213ce412..2893d1016d 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<USB::WiiSpeak>();
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<USB::LogitechMic>(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 f4a9c04a92..5e81cea3a6 100644
--- a/Source/Core/DolphinLib.props
+++ b/Source/Core/DolphinLib.props
@@ -414,6 +414,7 @@
<ClInclude Include="Core\IOS\USB\Bluetooth\WiimoteHIDAttr.h" />
<ClInclude Include="Core\IOS\USB\Common.h" />
<ClInclude Include="Core\IOS\USB\Emulated\Infinity.h" />
+ <ClInclude Include="Core\IOS\USB\Emulated\LogitechMic.h" />
<ClInclude Include="Core\IOS\USB\Emulated\Microphone.h" />
<ClInclude Include="Core\IOS\USB\Emulated\Skylanders\Skylander.h" />
<ClInclude Include="Core\IOS\USB\Emulated\Skylanders\SkylanderCrypto.h" />
@@ -1096,6 +1097,7 @@
<ClCompile Include="Core\IOS\USB\Bluetooth\WiimoteHIDAttr.cpp" />
<ClCompile Include="Core\IOS\USB\Common.cpp" />
<ClCompile Include="Core\IOS\USB\Emulated\Infinity.cpp" />
+ <ClCompile Include="Core\IOS\USB\Emulated\LogitechMic.cpp" />
<ClCompile Include="Core\IOS\USB\Emulated\Microphone.cpp" />
<ClCompile Include="Core\IOS\USB\Emulated\Skylanders\Skylander.cpp" />
<ClCompile Include="Core\IOS\USB\Emulated\Skylanders\SkylanderCrypto.cpp" />
diff --git a/Source/Core/DolphinQt/CMakeLists.txt b/Source/Core/DolphinQt/CMakeLists.txt
index e5e92ecdcf..cd49f7f108 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 fe5a2dda4a..f91dab20f9 100644
--- a/Source/Core/DolphinQt/DolphinQt.vcxproj
+++ b/Source/Core/DolphinQt/DolphinQt.vcxproj
@@ -160,6 +160,7 @@
<ClCompile Include="Debugger\WatchWidget.cpp" />
<ClCompile Include="DiscordHandler.cpp" />
<ClCompile Include="DiscordJoinRequestDialog.cpp" />
+ <ClCompile Include="EmulatedUSB\LogitechMicWindow.cpp" />
<ClCompile Include="EmulatedUSB\WiiSpeakWindow.cpp" />
<ClCompile Include="FIFO\FIFOAnalyzer.cpp" />
<ClCompile Include="FIFO\FIFOPlayerWindow.cpp" />
@@ -387,6 +388,7 @@
<QtMoc Include="Debugger\WatchWidget.h" />
<QtMoc Include="DiscordHandler.h" />
<QtMoc Include="DiscordJoinRequestDialog.h" />
+ <QtMoc Include="EmulatedUSB\LogitechMicWindow.h" />
<QtMoc Include="EmulatedUSB\WiiSpeakWindow.h" />
<QtMoc Include="FIFO\FIFOAnalyzer.h" />
<QtMoc Include="FIFO\FIFOPlayerWindow.h" />
diff --git a/Source/Core/DolphinQt/EmulatedUSB/LogitechMicWindow.cpp b/Source/Core/DolphinQt/EmulatedUSB/LogitechMicWindow.cpp
new file mode 100644
index 0000000000..04b4a8b16e
--- /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 <algorithm>
+
+#include <QCheckBox>
+#include <QComboBox>
+#include <QGridLayout>
+#include <QGroupBox>
+#include <QHBoxLayout>
+#include <QLabel>
+#include <QString>
+
+#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("<center><i>%1</i></center>")
+ .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<int>(
+ 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 0000000000..746aeed3ab
--- /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 <QVBoxLayout>
+#include <QWidget>
+
+#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<ConfigBool*, Config::EMULATED_LOGITECH_MIC_COUNT> m_mic_enabled_checkboxes;
+ std::array<QComboBox*, 4> m_mic_device_comboboxes;
+};
diff --git a/Source/Core/DolphinQt/MainWindow.cpp b/Source/Core/DolphinQt/MainWindow.cpp
index 464c1e20c1..3d2010adee 100644
--- a/Source/Core/DolphinQt/MainWindow.cpp
+++ b/Source/Core/DolphinQt/MainWindow.cpp
@@ -94,6 +94,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"
@@ -572,6 +573,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
@@ -1425,6 +1427,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 c01da7f5d3..a089935f61 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 d23bcd66e7..dfb3eb9ff0 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 5a1a95dc37..1f2abd3a3b 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