From d1b885e7d870f3c8420badce1241bcfbbaf1f9d1 Mon Sep 17 00:00:00 2001 From: Sketch <75850871+SketchMaster2001@users.noreply.github.com> Date: Tue, 6 Feb 2024 22:06:40 -0500 Subject: [PATCH 01/22] IOS/USB: Emulate Wii Speak (OpenAL) Credits to @degasus and @shuffle2 (godisgovernment): https://github.com/degasus/dolphin/tree/wiispeak --- Source/Core/Core/CMakeLists.txt | 4 + Source/Core/Core/Config/MainSettings.cpp | 6 + Source/Core/Core/Config/MainSettings.h | 2 + .../Core/Core/IOS/USB/Emulated/Microphone.cpp | 91 +++++ .../Core/Core/IOS/USB/Emulated/Microphone.h | 113 ++++++ .../Core/Core/IOS/USB/Emulated/WiiSpeak.cpp | 374 ++++++++++++++++++ Source/Core/Core/IOS/USB/Emulated/WiiSpeak.h | 95 +++++ Source/Core/Core/IOS/USB/USBScanner.cpp | 4 + Source/Core/DolphinLib.props | 4 + Source/Core/DolphinQt/CMakeLists.txt | 2 + Source/Core/DolphinQt/DolphinQt.vcxproj | 2 + .../DolphinQt/EmulatedUSB/WiiSpeakWindow.cpp | 84 ++++ .../DolphinQt/EmulatedUSB/WiiSpeakWindow.h | 27 ++ Source/Core/DolphinQt/MainWindow.cpp | 14 + Source/Core/DolphinQt/MainWindow.h | 3 + Source/Core/DolphinQt/MenuBar.cpp | 1 + Source/Core/DolphinQt/MenuBar.h | 1 + 17 files changed, 827 insertions(+) create mode 100644 Source/Core/Core/IOS/USB/Emulated/Microphone.cpp create mode 100644 Source/Core/Core/IOS/USB/Emulated/Microphone.h create mode 100644 Source/Core/Core/IOS/USB/Emulated/WiiSpeak.cpp create mode 100644 Source/Core/Core/IOS/USB/Emulated/WiiSpeak.h create mode 100644 Source/Core/DolphinQt/EmulatedUSB/WiiSpeakWindow.cpp create mode 100644 Source/Core/DolphinQt/EmulatedUSB/WiiSpeakWindow.h diff --git a/Source/Core/Core/CMakeLists.txt b/Source/Core/Core/CMakeLists.txt index 6036777bbb..6c1e39bac3 100644 --- a/Source/Core/Core/CMakeLists.txt +++ b/Source/Core/Core/CMakeLists.txt @@ -429,12 +429,16 @@ add_library(core IOS/USB/Common.h IOS/USB/Emulated/Infinity.cpp IOS/USB/Emulated/Infinity.h + IOS/USB/Emulated/Microphone.cpp + IOS/USB/Emulated/Microphone.h IOS/USB/Emulated/Skylanders/Skylander.cpp IOS/USB/Emulated/Skylanders/Skylander.h IOS/USB/Emulated/Skylanders/SkylanderCrypto.cpp IOS/USB/Emulated/Skylanders/SkylanderCrypto.h IOS/USB/Emulated/Skylanders/SkylanderFigure.cpp IOS/USB/Emulated/Skylanders/SkylanderFigure.h + IOS/USB/Emulated/WiiSpeak.cpp + IOS/USB/Emulated/WiiSpeak.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 9f4d1cce17..fd6fecd5d5 100644 --- a/Source/Core/Core/Config/MainSettings.cpp +++ b/Source/Core/Core/Config/MainSettings.cpp @@ -594,6 +594,12 @@ const Info MAIN_EMULATE_SKYLANDER_PORTAL{ const Info MAIN_EMULATE_INFINITY_BASE{ {System::Main, "EmulatedUSBDevices", "EmulateInfinityBase"}, false}; +const Info MAIN_EMULATE_WII_SPEAK{{System::Main, "EmulatedUSBDevices", "EmulateWiiSpeak"}, + false}; + +const Info MAIN_WII_SPEAK_MICROPHONE{{System::Main, "General", "WiiSpeakMicrophone"}, + ""}; + // 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 28a044d253..7a0085e8e4 100644 --- a/Source/Core/Core/Config/MainSettings.h +++ b/Source/Core/Core/Config/MainSettings.h @@ -362,6 +362,8 @@ void SetUSBDeviceWhitelist(const std::set>& devices); extern const Info MAIN_EMULATE_SKYLANDER_PORTAL; extern const Info MAIN_EMULATE_INFINITY_BASE; +extern const Info MAIN_EMULATE_WII_SPEAK; +extern const Info MAIN_WII_SPEAK_MICROPHONE; // GameCube path utility functions diff --git a/Source/Core/Core/IOS/USB/Emulated/Microphone.cpp b/Source/Core/Core/IOS/USB/Emulated/Microphone.cpp new file mode 100644 index 0000000000..d5bd6f89cd --- /dev/null +++ b/Source/Core/Core/IOS/USB/Emulated/Microphone.cpp @@ -0,0 +1,91 @@ +// Copyright 2025 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "Core/IOS/USB/Emulated/Microphone.h" + +#include "Common/Swap.h" + +#include + +namespace IOS::HLE::USB +{ +std::vector Microphone::ListDevices() +{ + std::vector devices{}; + const ALchar* pDeviceList = alcGetString(nullptr, ALC_CAPTURE_DEVICE_SPECIFIER); + while (*pDeviceList) + { + devices.emplace_back(pDeviceList); + pDeviceList += strlen(pDeviceList) + 1; + } + + return devices; +} + +int Microphone::OpenMicrophone() +{ + m_device = alcCaptureOpenDevice(nullptr, SAMPLING_RATE, AL_FORMAT_MONO16, BUFFER_SIZE); + m_dsp_data.resize(BUFFER_SIZE, 0); + m_temp_buffer.resize(BUFFER_SIZE, 0); + return static_cast(alcGetError(m_device)); +} + +int Microphone::StartCapture() +{ + alcCaptureStart(m_device); + return static_cast(alcGetError(m_device)); +} + +void Microphone::StopCapture() +{ + alcCaptureStop(m_device); +} + +void Microphone::PerformAudioCapture() +{ + m_num_of_samples = BUFFER_SIZE / 2; + + ALCint samples_in{}; + alcGetIntegerv(m_device, ALC_CAPTURE_SAMPLES, 1, &samples_in); + m_num_of_samples = std::min(m_num_of_samples, static_cast(samples_in)); + + if (m_num_of_samples == 0) + return; + + alcCaptureSamples(m_device, m_dsp_data.data(), m_num_of_samples); +} + +void Microphone::ByteSwap(const void* src, void* dst) const +{ + *static_cast(dst) = Common::swap16(*static_cast(src)); +} + +void Microphone::GetSoundData() +{ + if (m_num_of_samples == 0) + return; + + u8* ptr = const_cast(m_temp_buffer.data()); + // Convert LE to BE + for (u32 i = 0; i < m_num_of_samples; i++) + { + for (u32 indchan = 0; indchan < 1; indchan++) + { + const u32 curindex = (i * 2) + indchan * (16 / 8); + ByteSwap(m_dsp_data.data() + curindex, ptr + curindex); + } + } + + m_rbuf_dsp.write_bytes(ptr, m_num_of_samples * 2); +} + +void Microphone::ReadIntoBuffer(u8* dst, u32 size) +{ + m_rbuf_dsp.read_bytes(dst, size); +} + +bool Microphone::HasData() const +{ + return m_num_of_samples != 0; +} +} // namespace IOS::HLE::USB diff --git a/Source/Core/Core/IOS/USB/Emulated/Microphone.h b/Source/Core/Core/IOS/USB/Emulated/Microphone.h new file mode 100644 index 0000000000..41d21452f0 --- /dev/null +++ b/Source/Core/Core/IOS/USB/Emulated/Microphone.h @@ -0,0 +1,113 @@ +// Copyright 2025 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include + +#include +#include + +#include "Common/CommonTypes.h" + +namespace IOS::HLE::USB +{ +template +class simple_ringbuf +{ +public: + simple_ringbuf() { m_container.resize(S); } + + bool has_data() const { return m_used != 0; } + + u32 read_bytes(u8* buf, const u32 size) + { + u32 to_read = size > m_used ? m_used : size; + if (!to_read) + return 0; + + u8* data = m_container.data(); + u32 new_tail = m_tail + to_read; + + if (new_tail >= S) + { + u32 first_chunk_size = S - m_tail; + std::memcpy(buf, data + m_tail, first_chunk_size); + std::memcpy(buf + first_chunk_size, data, to_read - first_chunk_size); + m_tail = (new_tail - S); + } + else + { + std::memcpy(buf, data + m_tail, to_read); + m_tail = new_tail; + } + + m_used -= to_read; + + return to_read; + } + + void write_bytes(const u8* buf, const u32 size) + { + if (u32 over_size = m_used + size; over_size > S) + { + m_tail += (over_size - S); + if (m_tail > S) + m_tail -= S; + + m_used = S; + } + else + { + m_used = over_size; + } + + u8* data = m_container.data(); + u32 new_head = m_head + size; + + if (new_head >= S) + { + u32 first_chunk_size = S - m_head; + std::memcpy(data + m_head, buf, first_chunk_size); + std::memcpy(data, buf + first_chunk_size, size - first_chunk_size); + m_head = (new_head - S); + } + else + { + std::memcpy(data + m_head, buf, size); + m_head = new_head; + } + } + +protected: + std::vector m_container; + u32 m_head = 0, m_tail = 0, m_used = 0; +}; + +class Microphone final +{ +public: + static std::vector ListDevices(); + + int OpenMicrophone(); + int StartCapture(); + void StopCapture(); + void PerformAudioCapture(); + void GetSoundData(); + void ReadIntoBuffer(u8* dst, u32 size); + bool HasData() const; + +private: + void ByteSwap(const void* src, void* dst) const; + + static constexpr u32 SAMPLING_RATE = 8000; + static constexpr u32 BUFFER_SIZE = SAMPLING_RATE / 2; + + ALCdevice* m_device; + u32 m_num_of_samples{}; + std::vector m_dsp_data{}; + std::vector m_temp_buffer{}; + simple_ringbuf m_rbuf_dsp; +}; +} // namespace IOS::HLE::USB diff --git a/Source/Core/Core/IOS/USB/Emulated/WiiSpeak.cpp b/Source/Core/Core/IOS/USB/Emulated/WiiSpeak.cpp new file mode 100644 index 0000000000..6dc47019bf --- /dev/null +++ b/Source/Core/Core/IOS/USB/Emulated/WiiSpeak.cpp @@ -0,0 +1,374 @@ +// Copyright 2025 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "Core/IOS/USB/Emulated/WiiSpeak.h" + +#include "Core/HW/Memmap.h" +#include "Core/System.h" + +namespace IOS::HLE::USB +{ +WiiSpeak::WiiSpeak() +{ + m_vid = 0x57E; + m_pid = 0x0308; + m_id = (u64(m_vid) << 32 | u64(m_pid) << 16 | u64(9) << 8 | u64(1)); + m_device_descriptor = + DeviceDescriptor{0x12, 0x1, 0x200, 0, 0, 0, 0x10, 0x57E, 0x0308, 0x0214, 0x1, 0x2, 0x0, 0x1}; + m_config_descriptor.emplace_back(ConfigDescriptor{0x9, 0x2, 0x0030, 0x1, 0x1, 0x0, 0x80, 0x32}); + m_interface_descriptor.emplace_back( + InterfaceDescriptor{0x9, 0x4, 0x0, 0x0, 0x0, 0xFF, 0xFF, 0xFF, 0x0}); + m_interface_descriptor.emplace_back( + InterfaceDescriptor{0x9, 0x4, 0x0, 0x01, 0x03, 0xFF, 0xFF, 0xFF, 0x0}); + m_endpoint_descriptor.emplace_back(EndpointDescriptor{0x7, 0x5, 0x81, 0x1, 0x0020, 0x1}); + m_endpoint_descriptor.emplace_back(EndpointDescriptor{0x7, 0x5, 0x2, 0x2, 0x0020, 0}); + m_endpoint_descriptor.emplace_back(EndpointDescriptor{0x7, 0x5, 0x3, 0x1, 0x0040, 1}); + + m_microphone = Microphone(); + if (m_microphone.OpenMicrophone() != 0) + { + ERROR_LOG_FMT(IOS_USB, "Error opening the microphone."); + b_is_mic_connected = false; + return; + } + + if (m_microphone.StartCapture() != 0) + { + ERROR_LOG_FMT(IOS_USB, "Error starting captures."); + b_is_mic_connected = false; + return; + } + + m_microphone_thread = std::thread([this] { + u64 timeout{}; + constexpr u64 TIMESTEP = 256ull * 1'000'000ull / 48000ull; + while (true) + { + if (m_shutdown_event.WaitFor(std::chrono::microseconds{timeout})) + return; + + std::lock_guard lg(m_mutex); + timeout = TIMESTEP - (std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()) + .count() % + TIMESTEP); + m_microphone.PerformAudioCapture(); + m_microphone.GetSoundData(); + } + }); +} + +WiiSpeak::~WiiSpeak() +{ + { + std::lock_guard lg(m_mutex); + if (!m_microphone_thread.joinable()) + return; + + m_shutdown_event.Set(); + } + + m_microphone_thread.join(); + m_microphone.StopCapture(); +} + +DeviceDescriptor WiiSpeak::GetDeviceDescriptor() const +{ + return m_device_descriptor; +} + +std::vector WiiSpeak::GetConfigurations() const +{ + return m_config_descriptor; +} + +std::vector WiiSpeak::GetInterfaces(u8 config) const +{ + return m_interface_descriptor; +} + +std::vector WiiSpeak::GetEndpoints(u8 config, u8 interface, u8 alt) const +{ + return m_endpoint_descriptor; +} + +bool WiiSpeak::Attach() +{ + if (m_device_attached) + return true; + + DEBUG_LOG_FMT(IOS_USB, "[{:04x}:{:04x}] Opening device", m_vid, m_pid); + m_device_attached = true; + return true; +} + +bool WiiSpeak::AttachAndChangeInterface(const u8 interface) +{ + if (!Attach()) + return false; + + if (interface != m_active_interface) + return ChangeInterface(interface) == 0; + + return true; +} + +int WiiSpeak::CancelTransfer(const u8 endpoint) +{ + INFO_LOG_FMT(IOS_USB, "[{:04x}:{:04x} {}] Cancelling transfers (endpoint {:#x})", m_vid, m_pid, + m_active_interface, endpoint); + + return IPC_SUCCESS; +} + +int WiiSpeak::ChangeInterface(const u8 interface) +{ + DEBUG_LOG_FMT(IOS_USB, "[{:04x}:{:04x} {}] Changing interface to {}", m_vid, m_pid, + m_active_interface, interface); + m_active_interface = interface; + return 0; +} + +int WiiSpeak::GetNumberOfAltSettings(u8 interface) +{ + return 0; +} + +int WiiSpeak::SetAltSetting(u8 alt_setting) +{ + return 0; +} + +int WiiSpeak::SubmitTransfer(std::unique_ptr cmd) +{ + DEBUG_LOG_FMT(IOS_USB, + "[{:04x}:{:04x} {}] Control: bRequestType={:02x} bRequest={:02x} wValue={:04x}" + " wIndex={:04x} wLength={:04x}", + m_vid, m_pid, m_active_interface, cmd->request_type, cmd->request, cmd->value, + cmd->index, cmd->length); + + if (!b_is_mic_connected) + return IPC_ENOENT; + + switch (cmd->request_type << 8 | cmd->request) + { + case USBHDR(DIR_DEVICE2HOST, TYPE_STANDARD, REC_INTERFACE, REQUEST_GET_INTERFACE): + { + constexpr std::array data{1}; + cmd->FillBuffer(data.data(), 1); + cmd->ScheduleTransferCompletion(1, 100); + break; + } + case USBHDR(DIR_HOST2DEVICE, TYPE_VENDOR, REC_INTERFACE, 0): + { + init = false; + cmd->GetEmulationKernel().EnqueueIPCReply(cmd->ios_request, IPC_SUCCESS); + break; + } + case USBHDR(DIR_DEVICE2HOST, TYPE_VENDOR, REC_INTERFACE, REQUEST_GET_DESCRIPTOR): + { + if (!init) + { + constexpr std::array data{0}; + cmd->FillBuffer(data.data(), 1); + cmd->GetEmulationKernel().EnqueueIPCReply(cmd->ios_request, IPC_SUCCESS); + init = true; + } + else + { + constexpr std::array data{1}; + cmd->FillBuffer(data.data(), 1); + cmd->GetEmulationKernel().EnqueueIPCReply(cmd->ios_request, IPC_SUCCESS); + } + break; + } + case USBHDR(DIR_HOST2DEVICE, TYPE_VENDOR, REC_INTERFACE, 1): + SetRegister(cmd); + cmd->GetEmulationKernel().EnqueueIPCReply(cmd->ios_request, IPC_SUCCESS); + break; + case USBHDR(DIR_DEVICE2HOST, TYPE_VENDOR, REC_INTERFACE, 2): + GetRegister(cmd); + cmd->GetEmulationKernel().EnqueueIPCReply(cmd->ios_request, IPC_SUCCESS); + break; + default: + NOTICE_LOG_FMT(IOS_USB, "Unknown command"); + cmd->GetEmulationKernel().EnqueueIPCReply(cmd->ios_request, IPC_SUCCESS); + } + + return IPC_SUCCESS; +} + +int WiiSpeak::SubmitTransfer(std::unique_ptr cmd) +{ + cmd->GetEmulationKernel().EnqueueIPCReply(cmd->ios_request, IPC_SUCCESS); + return IPC_SUCCESS; +} + +int WiiSpeak::SubmitTransfer(std::unique_ptr cmd) +{ + cmd->GetEmulationKernel().EnqueueIPCReply(cmd->ios_request, IPC_SUCCESS); + return IPC_SUCCESS; +} + +int WiiSpeak::SubmitTransfer(std::unique_ptr cmd) +{ + if (!b_is_mic_connected) + return IPC_ENOENT; + + 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, "Wii Speak command invalid"); + return IPC_EINVAL; + } + + if (cmd->endpoint == 0x81 && m_microphone.HasData()) + m_microphone.ReadIntoBuffer(packets, cmd->length); + + // Anything more causes the visual cue to not appear. + // Anything less is more choppy audio. + cmd->ScheduleTransferCompletion(IPC_SUCCESS, 20000); + return IPC_SUCCESS; +}; + +void WiiSpeak::SetRegister(std::unique_ptr& cmd) +{ + auto& system = cmd->GetEmulationKernel().GetSystem(); + auto& memory = system.GetMemory(); + const u8 reg = memory.Read_U8(cmd->data_address + 1) & ~1; + const u16 arg1 = memory.Read_U16(cmd->data_address + 2); + const u16 arg2 = memory.Read_U16(cmd->data_address + 4); + + switch (reg) + { + case SAMPLER_STATE: + sampler.sample_on = !!arg1; + break; + case SAMPLER_FREQ: + switch (arg1) + { + case FREQ_8KHZ: + sampler.freq = 8000; + break; + case FREQ_11KHZ: + sampler.freq = 11025; + break; + case FREQ_RESERVED: + case FREQ_16KHZ: + default: + sampler.freq = 16000; + break; + } + break; + case SAMPLER_GAIN: + switch (arg1 & ~0x300) + { + case GAIN_00dB: + sampler.gain = 0; + break; + case GAIN_15dB: + sampler.gain = 15; + break; + case GAIN_30dB: + sampler.gain = 30; + break; + case GAIN_36dB: + default: + sampler.gain = 36; + break; + } + break; + case EC_STATE: + sampler.ec_reset = !!arg1; + break; + case SP_STATE: + switch (arg1) + { + case SP_ENABLE: + sampler.sp_on = arg2 == 0; + break; + case SP_SIN: + case SP_SOUT: + case SP_RIN: + break; + } + break; + case SAMPLER_MUTE: + sampler.mute = !!arg1; + break; + } +} + +void WiiSpeak::GetRegister(std::unique_ptr& cmd) +{ + auto& system = cmd->GetEmulationKernel().GetSystem(); + auto& memory = system.GetMemory(); + const u8 reg = memory.Read_U8(cmd->data_address + 1) & ~1; + const u32 arg1 = cmd->data_address + 2; + const u32 arg2 = cmd->data_address + 4; + + switch (reg) + { + case SAMPLER_STATE: + memory.Write_U16(sampler.sample_on ? 1 : 0, arg1); + break; + case SAMPLER_FREQ: + switch (sampler.freq) + { + case 8000: + memory.Write_U16(FREQ_8KHZ, arg1); + break; + case 11025: + memory.Write_U16(FREQ_11KHZ, arg1); + break; + case 16000: + default: + memory.Write_U16(FREQ_16KHZ, arg1); + break; + } + break; + case SAMPLER_GAIN: + switch (sampler.gain) + { + case 0: + memory.Write_U16(0x300 | GAIN_00dB, arg1); + break; + case 15: + memory.Write_U16(0x300 | GAIN_15dB, arg1); + break; + case 30: + memory.Write_U16(0x300 | GAIN_30dB, arg1); + break; + case 36: + default: + memory.Write_U16(0x300 | GAIN_36dB, arg1); + break; + } + break; + case EC_STATE: + memory.Write_U16(sampler.ec_reset ? 1 : 0, arg1); + break; + case SP_STATE: + switch (memory.Read_U16(arg1)) + { + case SP_ENABLE: + memory.Write_U16(1, arg2); + break; + case SP_SIN: + break; + case SP_SOUT: + memory.Write_U16(0x39B0, arg2); // 6dB + break; + case SP_RIN: + break; + } + break; + case SAMPLER_MUTE: + memory.Write_U16(sampler.mute ? 1 : 0, arg1); + break; + } +} +} // namespace IOS::HLE::USB diff --git a/Source/Core/Core/IOS/USB/Emulated/WiiSpeak.h b/Source/Core/Core/IOS/USB/Emulated/WiiSpeak.h new file mode 100644 index 0000000000..f430cc44e1 --- /dev/null +++ b/Source/Core/Core/IOS/USB/Emulated/WiiSpeak.h @@ -0,0 +1,95 @@ +// Copyright 2025 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include +#include + +#include "Common/CommonTypes.h" +#include "Common/Event.h" +#include "Core/IOS/USB/Common.h" +#include "Core/IOS/USB/Emulated/Microphone.h" + +namespace IOS::HLE::USB +{ +class WiiSpeak final : public Device +{ +public: + WiiSpeak(); + ~WiiSpeak(); + 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 message) override; + int SubmitTransfer(std::unique_ptr message) override; + int SubmitTransfer(std::unique_ptr message) override; + int SubmitTransfer(std::unique_ptr message) override; + +private: + struct WSState + { + bool sample_on; + bool mute; + int freq; + int gain; + bool ec_reset; + bool sp_on; + }; + + WSState sampler{}; + + enum Registers + { + SAMPLER_STATE = 0, + SAMPLER_MUTE = 0xc0, + + SAMPLER_FREQ = 2, + FREQ_8KHZ = 0, + FREQ_11KHZ = 1, + FREQ_RESERVED = 2, + FREQ_16KHZ = 3, // default + + SAMPLER_GAIN = 4, + GAIN_00dB = 0, + GAIN_15dB = 1, + GAIN_30dB = 2, + GAIN_36dB = 3, // default + + EC_STATE = 0x14, + + SP_STATE = 0x38, + SP_ENABLE = 0x1010, + SP_SIN = 0x2001, + SP_SOUT = 0x2004, + SP_RIN = 0x200d + }; + + void GetRegister(std::unique_ptr& cmd); + void SetRegister(std::unique_ptr& cmd); + + u16 m_vid = 0; + u16 m_pid = 0; + u8 m_active_interface = 0; + bool m_device_attached = false; + bool init = false; + bool b_is_mic_connected = true; + Microphone m_microphone; + DeviceDescriptor m_device_descriptor{}; + std::vector m_config_descriptor; + std::vector m_interface_descriptor; + std::vector m_endpoint_descriptor; + std::thread m_microphone_thread; + std::mutex m_mutex; + Common::Event m_shutdown_event; +}; +} // namespace IOS::HLE::USB diff --git a/Source/Core/Core/IOS/USB/USBScanner.cpp b/Source/Core/Core/IOS/USB/USBScanner.cpp index 8a8ef2da0b..783cf9749c 100644 --- a/Source/Core/Core/IOS/USB/USBScanner.cpp +++ b/Source/Core/Core/IOS/USB/USBScanner.cpp @@ -23,6 +23,7 @@ #include "Core/IOS/USB/Common.h" #include "Core/IOS/USB/Emulated/Infinity.h" #include "Core/IOS/USB/Emulated/Skylanders/Skylander.h" +#include "Core/IOS/USB/Emulated/WiiSpeak.h" #include "Core/IOS/USB/Host.h" #include "Core/IOS/USB/LibusbDevice.h" #include "Core/NetPlayProto.h" @@ -177,6 +178,9 @@ void USBScanner::AddEmulatedDevices(DeviceMap* new_devices) auto infinity_base = std::make_unique(); AddDevice(std::move(infinity_base), new_devices); } + + auto wii_speak = std::make_unique(); + AddDevice(std::move(wii_speak), new_devices); } void USBScanner::AddDevice(std::unique_ptr device, DeviceMap* new_devices) diff --git a/Source/Core/DolphinLib.props b/Source/Core/DolphinLib.props index 67e3a15f4c..e20ceb0fc9 100644 --- a/Source/Core/DolphinLib.props +++ b/Source/Core/DolphinLib.props @@ -404,9 +404,11 @@ + + @@ -1069,9 +1071,11 @@ + + diff --git a/Source/Core/DolphinQt/CMakeLists.txt b/Source/Core/DolphinQt/CMakeLists.txt index b6e1659d4f..f903773797 100644 --- a/Source/Core/DolphinQt/CMakeLists.txt +++ b/Source/Core/DolphinQt/CMakeLists.txt @@ -247,6 +247,8 @@ add_executable(dolphin-emu DiscordHandler.h DiscordJoinRequestDialog.cpp DiscordJoinRequestDialog.h + EmulatedUSB/WiiSpeakWindow.cpp + EmulatedUSB/WiiSpeakWindow.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 41349a4cee..454208f066 100644 --- a/Source/Core/DolphinQt/DolphinQt.vcxproj +++ b/Source/Core/DolphinQt/DolphinQt.vcxproj @@ -157,6 +157,7 @@ + @@ -374,6 +375,7 @@ + diff --git a/Source/Core/DolphinQt/EmulatedUSB/WiiSpeakWindow.cpp b/Source/Core/DolphinQt/EmulatedUSB/WiiSpeakWindow.cpp new file mode 100644 index 0000000000..5a457d8d1e --- /dev/null +++ b/Source/Core/DolphinQt/EmulatedUSB/WiiSpeakWindow.cpp @@ -0,0 +1,84 @@ +// Copyright 2025 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "DolphinQt/EmulatedUSB/WiiSpeakWindow.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "Common/IOFile.h" + +#include "Core/Config/MainSettings.h" +#include "Core/Core.h" +#include "Core/IOS/USB/Emulated/Microphone.h" +#include "Core/System.h" + +#include "DolphinQt/QtUtils/DolphinFileDialog.h" +#include "DolphinQt/Settings.h" + +WiiSpeakWindow::WiiSpeakWindow(QWidget* parent) : QWidget(parent) +{ + setWindowTitle(tr("Wii Speak Manager")); + setObjectName(QStringLiteral("wii_speak_manager")); + setMinimumSize(QSize(700, 200)); + + CreateMainWindow(); + + connect(&Settings::Instance(), &Settings::EmulationStateChanged, this, + &WiiSpeakWindow::OnEmulationStateChanged); + + installEventFilter(this); + + OnEmulationStateChanged(Core::GetState(Core::System::GetInstance())); +}; + +WiiSpeakWindow::~WiiSpeakWindow() = default; + +void WiiSpeakWindow::CreateMainWindow() +{ + auto* main_layout = new QVBoxLayout(); + + auto* checkbox_group = new QGroupBox(); + auto* checkbox_layout = new QHBoxLayout(); + checkbox_layout->setAlignment(Qt::AlignLeft); + m_checkbox = new QCheckBox(tr("Emulate Wii Speak"), this); + m_checkbox->setChecked(Config::Get(Config::MAIN_EMULATE_WII_SPEAK)); + connect(m_checkbox, &QCheckBox::toggled, this, &WiiSpeakWindow::EmulateWiiSpeak); + checkbox_layout->addWidget(m_checkbox); + checkbox_group->setLayout(checkbox_layout); + + m_combobox_microphones = new QComboBox(); + for (const std::string& device : IOS::HLE::USB::Microphone::ListDevices()) + { + m_combobox_microphones->addItem(QString::fromStdString(device)); + } + + checkbox_layout->addWidget(m_combobox_microphones); + + main_layout->addWidget(checkbox_group); + setLayout(main_layout); +} + +void WiiSpeakWindow::EmulateWiiSpeak(bool emulate) +{ + Config::SetBaseOrCurrent(Config::MAIN_EMULATE_WII_SPEAK, emulate); +} + +void WiiSpeakWindow::OnEmulationStateChanged(Core::State state) +{ + const bool running = state != Core::State::Uninitialized; + + m_checkbox->setEnabled(!running); +} diff --git a/Source/Core/DolphinQt/EmulatedUSB/WiiSpeakWindow.h b/Source/Core/DolphinQt/EmulatedUSB/WiiSpeakWindow.h new file mode 100644 index 0000000000..dd7fa758a3 --- /dev/null +++ b/Source/Core/DolphinQt/EmulatedUSB/WiiSpeakWindow.h @@ -0,0 +1,27 @@ +// Copyright 2025 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "Core/Core.h" + +class QCheckBox; +class QComboBox; + +class WiiSpeakWindow : public QWidget +{ + Q_OBJECT +public: + explicit WiiSpeakWindow(QWidget* parent = nullptr); + ~WiiSpeakWindow() override; + +private: + void CreateMainWindow(); + void OnEmulationStateChanged(Core::State state); + void EmulateWiiSpeak(bool emulate); + + QCheckBox* m_checkbox; + QComboBox* m_combobox_microphones; +}; diff --git a/Source/Core/DolphinQt/MainWindow.cpp b/Source/Core/DolphinQt/MainWindow.cpp index 09f2681f94..f84de11019 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/WiiSpeakWindow.h" #include "DolphinQt/FIFO/FIFOPlayerWindow.h" #include "DolphinQt/GCMemcardManager.h" #include "DolphinQt/GameList/GameList.h" @@ -579,6 +580,7 @@ void MainWindow::ConnectMenuBar() connect(m_menu_bar, &MenuBar::ShowFIFOPlayer, this, &MainWindow::ShowFIFOPlayer); 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::ConnectWiiRemote, this, &MainWindow::OnConnectWiiRemote); #ifdef USE_RETRO_ACHIEVEMENTS @@ -1438,6 +1440,18 @@ void MainWindow::ShowInfinityBase() m_infinity_window->activateWindow(); } +void MainWindow::ShowWiiSpeakWindow() +{ + if (!m_wii_speak_window) + { + m_wii_speak_window = new WiiSpeakWindow(); + } + + m_wii_speak_window->show(); + m_wii_speak_window->raise(); + m_wii_speak_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 a43f98ead0..536b30133f 100644 --- a/Source/Core/DolphinQt/MainWindow.h +++ b/Source/Core/DolphinQt/MainWindow.h @@ -52,6 +52,7 @@ class ThreadWidget; class ToolBar; class WatchWidget; class WiiTASInputWindow; +class WiiSpeakWindow; struct WindowSystemInfo; namespace Core @@ -173,6 +174,7 @@ private: void ShowFIFOPlayer(); void ShowSkylanderPortal(); void ShowInfinityBase(); + void ShowWiiSpeakWindow(); void ShowMemcardManager(); void ShowResourcePackManager(); void ShowCheatsManager(); @@ -246,6 +248,7 @@ private: FIFOPlayerWindow* m_fifo_window = nullptr; SkylanderPortalWindow* m_skylander_window = nullptr; InfinityBaseWindow* m_infinity_window = nullptr; + WiiSpeakWindow* m_wii_speak_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 232ea1e337..6f6919a142 100644 --- a/Source/Core/DolphinQt/MenuBar.cpp +++ b/Source/Core/DolphinQt/MenuBar.cpp @@ -279,6 +279,7 @@ void MenuBar::AddToolsMenu() auto* usb_device_menu = new QMenu(tr("Emulated USB Devices"), tools_menu); 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); tools_menu->addMenu(usb_device_menu); tools_menu->addSeparator(); diff --git a/Source/Core/DolphinQt/MenuBar.h b/Source/Core/DolphinQt/MenuBar.h index c3309a2f54..8a73caf97c 100644 --- a/Source/Core/DolphinQt/MenuBar.h +++ b/Source/Core/DolphinQt/MenuBar.h @@ -94,6 +94,7 @@ signals: void ShowResourcePackManager(); void ShowSkylanderPortal(); void ShowInfinityBase(); + void ShowWiiSpeakWindow(); void ConnectWiiRemote(int id); #ifdef USE_RETRO_ACHIEVEMENTS From c6a99f7be520d6cf547c81740880c4a261e5e709 Mon Sep 17 00:00:00 2001 From: Sepalani Date: Thu, 9 May 2024 14:51:30 +0400 Subject: [PATCH 02/22] IOS/USB: Emulate Wii Speak using cubeb Based on @noahpistilli (Sketch) PR: https://github.com/dolphin-emu/dolphin/pull/12567 Fixed the Windows support and the heisenbug caused by uninitialized members. Config system integration finalized. --- Source/Core/AudioCommon/CubebUtils.cpp | 87 ++++++ Source/Core/AudioCommon/CubebUtils.h | 6 + Source/Core/Core/Config/MainSettings.cpp | 7 +- Source/Core/Core/Config/MainSettings.h | 1 + .../Core/Core/IOS/USB/Emulated/Microphone.cpp | 248 +++++++++++++----- .../Core/Core/IOS/USB/Emulated/Microphone.h | 124 +++------ .../Core/Core/IOS/USB/Emulated/WiiSpeak.cpp | 137 ++++------ Source/Core/Core/IOS/USB/Emulated/WiiSpeak.h | 35 +-- Source/Core/Core/IOS/USB/USBScanner.cpp | 8 +- .../DolphinQt/EmulatedUSB/WiiSpeakWindow.cpp | 74 ++++-- .../DolphinQt/EmulatedUSB/WiiSpeakWindow.h | 6 +- 11 files changed, 443 insertions(+), 290 deletions(-) diff --git a/Source/Core/AudioCommon/CubebUtils.cpp b/Source/Core/AudioCommon/CubebUtils.cpp index 75b78f687f..cd873d7f3e 100644 --- a/Source/Core/AudioCommon/CubebUtils.cpp +++ b/Source/Core/AudioCommon/CubebUtils.cpp @@ -82,3 +82,90 @@ std::shared_ptr CubebUtils::GetContext() weak = shared = {ctx, DestroyContext}; return shared; } + +std::vector> CubebUtils::ListInputDevices() +{ + std::vector> devices; + + cubeb_device_collection collection; + auto cubeb_ctx = CubebUtils::GetContext(); + int r = cubeb_enumerate_devices(cubeb_ctx.get(), CUBEB_DEVICE_TYPE_INPUT, &collection); + + if (r != CUBEB_OK) + { + ERROR_LOG_FMT(AUDIO, "Error listing cubeb input devices"); + return devices; + } + + INFO_LOG_FMT(AUDIO, "Listing cubeb input devices:"); + for (uint32_t i = 0; i < collection.count; i++) + { + auto& info = collection.device[i]; + auto& device_state = info.state; + const char* state_name = [device_state] { + switch (device_state) + { + case CUBEB_DEVICE_STATE_DISABLED: + return "disabled"; + case CUBEB_DEVICE_STATE_UNPLUGGED: + return "unplugged"; + case CUBEB_DEVICE_STATE_ENABLED: + return "enabled"; + default: + return "unknown?"; + } + }(); + + INFO_LOG_FMT(AUDIO, + "[{}] Device ID: {}\n" + "\tName: {}\n" + "\tGroup ID: {}\n" + "\tVendor: {}\n" + "\tState: {}", + i, info.device_id, info.friendly_name, info.group_id, + (info.vendor_name == nullptr) ? "(null)" : info.vendor_name, state_name); + if (info.state == CUBEB_DEVICE_STATE_ENABLED) + { + devices.emplace_back(info.device_id, info.friendly_name); + } + } + + cubeb_device_collection_destroy(cubeb_ctx.get(), &collection); + + return devices; +} + +cubeb_devid CubebUtils::GetInputDeviceById(std::string_view id) +{ + if (id.empty()) + return nullptr; + + cubeb_device_collection collection; + auto cubeb_ctx = CubebUtils::GetContext(); + int r = cubeb_enumerate_devices(cubeb_ctx.get(), CUBEB_DEVICE_TYPE_INPUT, &collection); + + if (r != CUBEB_OK) + { + ERROR_LOG_FMT(AUDIO, "Error enumerating cubeb input devices"); + return nullptr; + } + + cubeb_devid device_id = nullptr; + for (uint32_t i = 0; i < collection.count; i++) + { + auto& info = collection.device[i]; + if (id.compare(info.device_id) == 0) + { + device_id = info.devid; + break; + } + } + if (device_id == nullptr) + { + WARN_LOG_FMT(AUDIO, "Failed to find selected input device, defaulting to system preferences"); + } + + cubeb_device_collection_destroy(cubeb_ctx.get(), &collection); + + return device_id; +} diff --git a/Source/Core/AudioCommon/CubebUtils.h b/Source/Core/AudioCommon/CubebUtils.h index f0effc2e8f..943ea83359 100644 --- a/Source/Core/AudioCommon/CubebUtils.h +++ b/Source/Core/AudioCommon/CubebUtils.h @@ -5,10 +5,16 @@ #include #include +#include +#include +#include +#include struct cubeb; namespace CubebUtils { std::shared_ptr GetContext(); +std::vector> ListInputDevices(); +const void* GetInputDeviceById(std::string_view id); } // namespace CubebUtils diff --git a/Source/Core/Core/Config/MainSettings.cpp b/Source/Core/Core/Config/MainSettings.cpp index fd6fecd5d5..6555f20f17 100644 --- a/Source/Core/Core/Config/MainSettings.cpp +++ b/Source/Core/Core/Config/MainSettings.cpp @@ -597,8 +597,11 @@ const Info MAIN_EMULATE_INFINITY_BASE{ const Info MAIN_EMULATE_WII_SPEAK{{System::Main, "EmulatedUSBDevices", "EmulateWiiSpeak"}, false}; -const Info MAIN_WII_SPEAK_MICROPHONE{{System::Main, "General", "WiiSpeakMicrophone"}, - ""}; +const Info MAIN_WII_SPEAK_MICROPHONE{ + {System::Main, "EmulatedUSBDevices", "WiiSpeakMicrophone"}, ""}; + +const Info MAIN_WII_SPEAK_CONNECTED{{System::Main, "EmulatedUSBDevices", "WiiSpeakConnected"}, + false}; // 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. diff --git a/Source/Core/Core/Config/MainSettings.h b/Source/Core/Core/Config/MainSettings.h index 7a0085e8e4..057de249b1 100644 --- a/Source/Core/Core/Config/MainSettings.h +++ b/Source/Core/Core/Config/MainSettings.h @@ -364,6 +364,7 @@ extern const Info MAIN_EMULATE_SKYLANDER_PORTAL; extern const Info MAIN_EMULATE_INFINITY_BASE; extern const Info MAIN_EMULATE_WII_SPEAK; extern const Info MAIN_WII_SPEAK_MICROPHONE; +extern const Info MAIN_WII_SPEAK_CONNECTED; // GameCube path utility functions diff --git a/Source/Core/Core/IOS/USB/Emulated/Microphone.cpp b/Source/Core/Core/IOS/USB/Emulated/Microphone.cpp index d5bd6f89cd..c264eb768f 100644 --- a/Source/Core/Core/IOS/USB/Emulated/Microphone.cpp +++ b/Source/Core/Core/IOS/USB/Emulated/Microphone.cpp @@ -3,89 +3,215 @@ #include "Core/IOS/USB/Emulated/Microphone.h" -#include "Common/Swap.h" - #include +#include + +#include "AudioCommon/CubebUtils.h" +#include "Common/Event.h" +#include "Common/Logging/Log.h" +#include "Common/ScopeGuard.h" +#include "Common/Swap.h" +#include "Core/Config/MainSettings.h" + +#ifdef _WIN32 +#include +#endif + namespace IOS::HLE::USB { -std::vector Microphone::ListDevices() +Microphone::Microphone() { - std::vector devices{}; - const ALchar* pDeviceList = alcGetString(nullptr, ALC_CAPTURE_DEVICE_SPECIFIER); - while (*pDeviceList) +#ifdef _WIN32 + Common::Event sync_event; + m_work_queue.EmplaceItem([this, &sync_event] { + Common::ScopeGuard sync_event_guard([&sync_event] { sync_event.Set(); }); + auto result = ::CoInitializeEx(nullptr, COINIT_MULTITHREADED | COINIT_DISABLE_OLE1DDE); + m_coinit_success = result == S_OK; + m_should_couninit = result == S_OK || result == S_FALSE; + }); + sync_event.Wait(); +#endif + + StreamInit(); +} + +Microphone::~Microphone() +{ + StreamTerminate(); + +#ifdef _WIN32 + if (m_should_couninit) { - devices.emplace_back(pDeviceList); - pDeviceList += strlen(pDeviceList) + 1; + Common::Event sync_event; + m_work_queue.EmplaceItem([this, &sync_event] { + Common::ScopeGuard sync_event_guard([&sync_event] { sync_event.Set(); }); + m_should_couninit = false; + CoUninitialize(); + }); + sync_event.Wait(); } - - return devices; + m_coinit_success = false; +#endif } -int Microphone::OpenMicrophone() +void Microphone::StreamInit() { - m_device = alcCaptureOpenDevice(nullptr, SAMPLING_RATE, AL_FORMAT_MONO16, BUFFER_SIZE); - m_dsp_data.resize(BUFFER_SIZE, 0); - m_temp_buffer.resize(BUFFER_SIZE, 0); - return static_cast(alcGetError(m_device)); -} - -int Microphone::StartCapture() -{ - alcCaptureStart(m_device); - return static_cast(alcGetError(m_device)); -} - -void Microphone::StopCapture() -{ - alcCaptureStop(m_device); -} - -void Microphone::PerformAudioCapture() -{ - m_num_of_samples = BUFFER_SIZE / 2; - - ALCint samples_in{}; - alcGetIntegerv(m_device, ALC_CAPTURE_SAMPLES, 1, &samples_in); - m_num_of_samples = std::min(m_num_of_samples, static_cast(samples_in)); - - if (m_num_of_samples == 0) - return; - - alcCaptureSamples(m_device, m_dsp_data.data(), m_num_of_samples); -} - -void Microphone::ByteSwap(const void* src, void* dst) const -{ - *static_cast(dst) = Common::swap16(*static_cast(src)); -} - -void Microphone::GetSoundData() -{ - if (m_num_of_samples == 0) - return; - - u8* ptr = const_cast(m_temp_buffer.data()); - // Convert LE to BE - for (u32 i = 0; i < m_num_of_samples; i++) +#ifdef _WIN32 + if (!m_coinit_success) { - for (u32 indchan = 0; indchan < 1; indchan++) + ERROR_LOG_FMT(IOS_USB, "Failed to init Wii Speak stream"); + return; + } + Common::Event sync_event; + m_work_queue.EmplaceItem([this, &sync_event] { + Common::ScopeGuard sync_event_guard([&sync_event] { sync_event.Set(); }); +#endif + m_cubeb_ctx = CubebUtils::GetContext(); +#ifdef _WIN32 + }); + sync_event.Wait(); +#endif + + // TODO: Not here but rather inside the WiiSpeak device if possible? + StreamStart(); +} + +void Microphone::StreamTerminate() +{ + StreamStop(); + + if (m_cubeb_ctx) + { +#ifdef _WIN32 + if (!m_coinit_success) + return; + Common::Event sync_event; + m_work_queue.EmplaceItem([this, &sync_event] { + Common::ScopeGuard sync_event_guard([&sync_event] { sync_event.Set(); }); +#endif + m_cubeb_ctx.reset(); +#ifdef _WIN32 + }); + sync_event.Wait(); +#endif + } +} + +static void state_callback(cubeb_stream* stream, void* user_data, cubeb_state state) +{ +} + +void Microphone::StreamStart() +{ + if (!m_cubeb_ctx) + return; + +#ifdef _WIN32 + if (!m_coinit_success) + return; + Common::Event sync_event; + m_work_queue.EmplaceItem([this, &sync_event] { + Common::ScopeGuard sync_event_guard([&sync_event] { sync_event.Set(); }); +#endif + + cubeb_stream_params params{}; + params.format = CUBEB_SAMPLE_S16LE; + params.rate = SAMPLING_RATE; + params.channels = 1; + params.layout = CUBEB_LAYOUT_MONO; + + u32 minimum_latency; + if (cubeb_get_min_latency(m_cubeb_ctx.get(), ¶ms, &minimum_latency) != CUBEB_OK) { - const u32 curindex = (i * 2) + indchan * (16 / 8); - ByteSwap(m_dsp_data.data() + curindex, ptr + curindex); + WARN_LOG_FMT(IOS_USB, "Error getting minimum latency"); } + + 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", + input_device, ¶ms, nullptr, nullptr, + std::max(16, minimum_latency), DataCallback, state_callback, + this) != CUBEB_OK) + { + ERROR_LOG_FMT(IOS_USB, "Error initializing cubeb stream"); + return; + } + + if (cubeb_stream_start(m_cubeb_stream) != CUBEB_OK) + { + ERROR_LOG_FMT(IOS_USB, "Error starting cubeb stream"); + return; + } + + INFO_LOG_FMT(IOS_USB, "started cubeb stream"); +#ifdef _WIN32 + }); + sync_event.Wait(); +#endif +} + +void Microphone::StreamStop() +{ + if (m_cubeb_stream) + { +#ifdef _WIN32 + Common::Event sync_event; + m_work_queue.EmplaceItem([this, &sync_event] { + Common::ScopeGuard sync_event_guard([&sync_event] { sync_event.Set(); }); +#endif + if (cubeb_stream_stop(m_cubeb_stream) != CUBEB_OK) + ERROR_LOG_FMT(IOS_USB, "Error stopping cubeb stream"); + cubeb_stream_destroy(m_cubeb_stream); + m_cubeb_stream = nullptr; +#ifdef _WIN32 + }); + sync_event.Wait(); +#endif + } +} + +long Microphone::DataCallback(cubeb_stream* stream, void* user_data, const void* input_buffer, + void* /*output_buffer*/, long nframes) +{ + auto* mic = static_cast(user_data); + + std::lock_guard lk(mic->m_ring_lock); + + const s16* buff_in = static_cast(input_buffer); + for (long i = 0; i < nframes; i++) + { + mic->m_stream_buffer[mic->m_stream_wpos] = Common::swap16(buff_in[i]); + mic->m_stream_wpos = (mic->m_stream_wpos + 1) % mic->STREAM_SIZE; } - m_rbuf_dsp.write_bytes(ptr, m_num_of_samples * 2); + mic->m_samples_avail += nframes; + if (mic->m_samples_avail > mic->STREAM_SIZE) + { + mic->m_samples_avail = 0; + } + + return nframes; } void Microphone::ReadIntoBuffer(u8* dst, u32 size) { - m_rbuf_dsp.read_bytes(dst, size); + std::lock_guard lk(m_ring_lock); + + if (m_samples_avail >= BUFF_SIZE_SAMPLES) + { + u8* last_buffer = reinterpret_cast(&m_stream_buffer[m_stream_rpos]); + std::memcpy(dst, static_cast(last_buffer), size); + + m_samples_avail -= BUFF_SIZE_SAMPLES; + + m_stream_rpos += BUFF_SIZE_SAMPLES; + m_stream_rpos %= STREAM_SIZE; + } } bool Microphone::HasData() const { - return m_num_of_samples != 0; + return m_samples_avail > 0 && Config::Get(Config::MAIN_WII_SPEAK_CONNECTED); } } // namespace IOS::HLE::USB diff --git a/Source/Core/Core/IOS/USB/Emulated/Microphone.h b/Source/Core/Core/IOS/USB/Emulated/Microphone.h index 41d21452f0..a938430dae 100644 --- a/Source/Core/Core/IOS/USB/Emulated/Microphone.h +++ b/Source/Core/Core/IOS/USB/Emulated/Microphone.h @@ -3,111 +3,55 @@ #pragma once -#include -#include - -#include -#include +#include +#include +#include #include "Common/CommonTypes.h" +#include "Common/WorkQueueThread.h" + +struct cubeb; +struct cubeb_stream; namespace IOS::HLE::USB { -template -class simple_ringbuf -{ -public: - simple_ringbuf() { m_container.resize(S); } - - bool has_data() const { return m_used != 0; } - - u32 read_bytes(u8* buf, const u32 size) - { - u32 to_read = size > m_used ? m_used : size; - if (!to_read) - return 0; - - u8* data = m_container.data(); - u32 new_tail = m_tail + to_read; - - if (new_tail >= S) - { - u32 first_chunk_size = S - m_tail; - std::memcpy(buf, data + m_tail, first_chunk_size); - std::memcpy(buf + first_chunk_size, data, to_read - first_chunk_size); - m_tail = (new_tail - S); - } - else - { - std::memcpy(buf, data + m_tail, to_read); - m_tail = new_tail; - } - - m_used -= to_read; - - return to_read; - } - - void write_bytes(const u8* buf, const u32 size) - { - if (u32 over_size = m_used + size; over_size > S) - { - m_tail += (over_size - S); - if (m_tail > S) - m_tail -= S; - - m_used = S; - } - else - { - m_used = over_size; - } - - u8* data = m_container.data(); - u32 new_head = m_head + size; - - if (new_head >= S) - { - u32 first_chunk_size = S - m_head; - std::memcpy(data + m_head, buf, first_chunk_size); - std::memcpy(data, buf + first_chunk_size, size - first_chunk_size); - m_head = (new_head - S); - } - else - { - std::memcpy(data + m_head, buf, size); - m_head = new_head; - } - } - -protected: - std::vector m_container; - u32 m_head = 0, m_tail = 0, m_used = 0; -}; - class Microphone final { public: - static std::vector ListDevices(); + Microphone(); + ~Microphone(); - int OpenMicrophone(); - int StartCapture(); - void StopCapture(); - void PerformAudioCapture(); - void GetSoundData(); - void ReadIntoBuffer(u8* dst, u32 size); bool HasData() const; + void ReadIntoBuffer(u8* dst, u32 size); private: - void ByteSwap(const void* src, void* dst) const; + static long DataCallback(cubeb_stream* stream, void* user_data, const void* input_buffer, + void* output_buffer, long nframes); + + void StreamInit(); + void StreamTerminate(); + void StreamStart(); + void StreamStop(); static constexpr u32 SAMPLING_RATE = 8000; static constexpr u32 BUFFER_SIZE = SAMPLING_RATE / 2; + static constexpr u32 BUFF_SIZE_SAMPLES = 16; + static constexpr u32 STREAM_SIZE = BUFF_SIZE_SAMPLES * 500; - ALCdevice* m_device; - u32 m_num_of_samples{}; - std::vector m_dsp_data{}; - std::vector m_temp_buffer{}; - simple_ringbuf m_rbuf_dsp; + std::array m_stream_buffer{}; + u32 m_stream_wpos = 0; + u32 m_stream_rpos = 0; + u32 m_samples_avail = 0; + + std::mutex m_ring_lock; + std::shared_ptr m_cubeb_ctx = nullptr; + cubeb_stream* m_cubeb_stream = nullptr; + +#ifdef _WIN32 + Common::WorkQueueThread> m_work_queue{ + "Wii Speak Worker", [](const std::function& func) { func(); }}; + bool m_coinit_success = false; + bool m_should_couninit = false; +#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 6dc47019bf..55896e1c38 100644 --- a/Source/Core/Core/IOS/USB/Emulated/WiiSpeak.cpp +++ b/Source/Core/Core/IOS/USB/Emulated/WiiSpeak.cpp @@ -3,6 +3,7 @@ #include "Core/IOS/USB/Emulated/WiiSpeak.h" +#include "Core/Config/MainSettings.h" #include "Core/HW/Memmap.h" #include "Core/System.h" @@ -10,67 +11,10 @@ namespace IOS::HLE::USB { WiiSpeak::WiiSpeak() { - m_vid = 0x57E; - m_pid = 0x0308; - m_id = (u64(m_vid) << 32 | u64(m_pid) << 16 | u64(9) << 8 | u64(1)); - m_device_descriptor = - DeviceDescriptor{0x12, 0x1, 0x200, 0, 0, 0, 0x10, 0x57E, 0x0308, 0x0214, 0x1, 0x2, 0x0, 0x1}; - m_config_descriptor.emplace_back(ConfigDescriptor{0x9, 0x2, 0x0030, 0x1, 0x1, 0x0, 0x80, 0x32}); - m_interface_descriptor.emplace_back( - InterfaceDescriptor{0x9, 0x4, 0x0, 0x0, 0x0, 0xFF, 0xFF, 0xFF, 0x0}); - m_interface_descriptor.emplace_back( - InterfaceDescriptor{0x9, 0x4, 0x0, 0x01, 0x03, 0xFF, 0xFF, 0xFF, 0x0}); - m_endpoint_descriptor.emplace_back(EndpointDescriptor{0x7, 0x5, 0x81, 0x1, 0x0020, 0x1}); - m_endpoint_descriptor.emplace_back(EndpointDescriptor{0x7, 0x5, 0x2, 0x2, 0x0020, 0}); - m_endpoint_descriptor.emplace_back(EndpointDescriptor{0x7, 0x5, 0x3, 0x1, 0x0040, 1}); - - m_microphone = Microphone(); - if (m_microphone.OpenMicrophone() != 0) - { - ERROR_LOG_FMT(IOS_USB, "Error opening the microphone."); - b_is_mic_connected = false; - return; - } - - if (m_microphone.StartCapture() != 0) - { - ERROR_LOG_FMT(IOS_USB, "Error starting captures."); - b_is_mic_connected = false; - return; - } - - m_microphone_thread = std::thread([this] { - u64 timeout{}; - constexpr u64 TIMESTEP = 256ull * 1'000'000ull / 48000ull; - while (true) - { - if (m_shutdown_event.WaitFor(std::chrono::microseconds{timeout})) - return; - - std::lock_guard lg(m_mutex); - timeout = TIMESTEP - (std::chrono::duration_cast( - std::chrono::steady_clock::now().time_since_epoch()) - .count() % - TIMESTEP); - m_microphone.PerformAudioCapture(); - m_microphone.GetSoundData(); - } - }); + m_id = u64(m_vid) << 32 | u64(m_pid) << 16 | u64(9) << 8 | u64(1); } -WiiSpeak::~WiiSpeak() -{ - { - std::lock_guard lg(m_mutex); - if (!m_microphone_thread.joinable()) - return; - - m_shutdown_event.Set(); - } - - m_microphone_thread.join(); - m_microphone.StopCapture(); -} +WiiSpeak::~WiiSpeak() = default; DeviceDescriptor WiiSpeak::GetDeviceDescriptor() const { @@ -98,6 +42,8 @@ bool WiiSpeak::Attach() return true; DEBUG_LOG_FMT(IOS_USB, "[{:04x}:{:04x}] Opening device", m_vid, m_pid); + if (!m_microphone) + m_microphone = std::make_unique(); m_device_attached = true; return true; } @@ -147,15 +93,18 @@ int WiiSpeak::SubmitTransfer(std::unique_ptr cmd) m_vid, m_pid, m_active_interface, cmd->request_type, cmd->request, cmd->value, cmd->index, cmd->length); - if (!b_is_mic_connected) - return IPC_ENOENT; + // Without a proper way to reconnect the emulated Wii Speak, + // this error after being raised prevents some games to use the microphone later. + // + // if (!IsMicrophoneConnected()) + // return IPC_ENOENT; switch (cmd->request_type << 8 | cmd->request) { case USBHDR(DIR_DEVICE2HOST, TYPE_STANDARD, REC_INTERFACE, REQUEST_GET_INTERFACE): { - constexpr std::array data{1}; - cmd->FillBuffer(data.data(), 1); + constexpr u8 data{1}; + cmd->FillBuffer(&data, sizeof(data)); cmd->ScheduleTransferCompletion(1, 100); break; } @@ -169,15 +118,15 @@ int WiiSpeak::SubmitTransfer(std::unique_ptr cmd) { if (!init) { - constexpr std::array data{0}; - cmd->FillBuffer(data.data(), 1); + constexpr u8 data{0}; + cmd->FillBuffer(&data, sizeof(data)); cmd->GetEmulationKernel().EnqueueIPCReply(cmd->ios_request, IPC_SUCCESS); init = true; } else { - constexpr std::array data{1}; - cmd->FillBuffer(data.data(), 1); + constexpr u8 data{1}; + cmd->FillBuffer(&data, sizeof(data)); cmd->GetEmulationKernel().EnqueueIPCReply(cmd->ios_request, IPC_SUCCESS); } break; @@ -212,8 +161,8 @@ int WiiSpeak::SubmitTransfer(std::unique_ptr cmd) int WiiSpeak::SubmitTransfer(std::unique_ptr cmd) { - if (!b_is_mic_connected) - return IPC_ENOENT; + // if (!IsMicrophoneConnected()) + // return IPC_ENOENT; auto& system = cmd->GetEmulationKernel().GetSystem(); auto& memory = system.GetMemory(); @@ -224,17 +173,16 @@ int WiiSpeak::SubmitTransfer(std::unique_ptr cmd) ERROR_LOG_FMT(IOS_USB, "Wii Speak command invalid"); return IPC_EINVAL; } - - if (cmd->endpoint == 0x81 && m_microphone.HasData()) - m_microphone.ReadIntoBuffer(packets, cmd->length); + if (cmd->endpoint == 0x81 && m_microphone && m_microphone->HasData()) + m_microphone->ReadIntoBuffer(packets, cmd->length); // Anything more causes the visual cue to not appear. // Anything less is more choppy audio. - cmd->ScheduleTransferCompletion(IPC_SUCCESS, 20000); + cmd->ScheduleTransferCompletion(IPC_SUCCESS, 2500); return IPC_SUCCESS; -}; +} -void WiiSpeak::SetRegister(std::unique_ptr& cmd) +void WiiSpeak::SetRegister(const std::unique_ptr& cmd) { auto& system = cmd->GetEmulationKernel().GetSystem(); auto& memory = system.GetMemory(); @@ -245,21 +193,21 @@ void WiiSpeak::SetRegister(std::unique_ptr& cmd) switch (reg) { case SAMPLER_STATE: - sampler.sample_on = !!arg1; + m_sampler.sample_on = !!arg1; break; case SAMPLER_FREQ: switch (arg1) { case FREQ_8KHZ: - sampler.freq = 8000; + m_sampler.freq = 8000; break; case FREQ_11KHZ: - sampler.freq = 11025; + m_sampler.freq = 11025; break; case FREQ_RESERVED: case FREQ_16KHZ: default: - sampler.freq = 16000; + m_sampler.freq = 16000; break; } break; @@ -267,28 +215,28 @@ void WiiSpeak::SetRegister(std::unique_ptr& cmd) switch (arg1 & ~0x300) { case GAIN_00dB: - sampler.gain = 0; + m_sampler.gain = 0; break; case GAIN_15dB: - sampler.gain = 15; + m_sampler.gain = 15; break; case GAIN_30dB: - sampler.gain = 30; + m_sampler.gain = 30; break; case GAIN_36dB: default: - sampler.gain = 36; + m_sampler.gain = 36; break; } break; case EC_STATE: - sampler.ec_reset = !!arg1; + m_sampler.ec_reset = !!arg1; break; case SP_STATE: switch (arg1) { case SP_ENABLE: - sampler.sp_on = arg2 == 0; + m_sampler.sp_on = arg2 == 0; break; case SP_SIN: case SP_SOUT: @@ -297,12 +245,12 @@ void WiiSpeak::SetRegister(std::unique_ptr& cmd) } break; case SAMPLER_MUTE: - sampler.mute = !!arg1; + m_sampler.mute = !!arg1; break; } } -void WiiSpeak::GetRegister(std::unique_ptr& cmd) +void WiiSpeak::GetRegister(const std::unique_ptr& cmd) const { auto& system = cmd->GetEmulationKernel().GetSystem(); auto& memory = system.GetMemory(); @@ -313,10 +261,10 @@ void WiiSpeak::GetRegister(std::unique_ptr& cmd) switch (reg) { case SAMPLER_STATE: - memory.Write_U16(sampler.sample_on ? 1 : 0, arg1); + memory.Write_U16(m_sampler.sample_on ? 1 : 0, arg1); break; case SAMPLER_FREQ: - switch (sampler.freq) + switch (m_sampler.freq) { case 8000: memory.Write_U16(FREQ_8KHZ, arg1); @@ -331,7 +279,7 @@ void WiiSpeak::GetRegister(std::unique_ptr& cmd) } break; case SAMPLER_GAIN: - switch (sampler.gain) + switch (m_sampler.gain) { case 0: memory.Write_U16(0x300 | GAIN_00dB, arg1); @@ -349,7 +297,7 @@ void WiiSpeak::GetRegister(std::unique_ptr& cmd) } break; case EC_STATE: - memory.Write_U16(sampler.ec_reset ? 1 : 0, arg1); + memory.Write_U16(m_sampler.ec_reset ? 1 : 0, arg1); break; case SP_STATE: switch (memory.Read_U16(arg1)) @@ -367,8 +315,13 @@ void WiiSpeak::GetRegister(std::unique_ptr& cmd) } break; case SAMPLER_MUTE: - memory.Write_U16(sampler.mute ? 1 : 0, arg1); + memory.Write_U16(m_sampler.mute ? 1 : 0, arg1); break; } } + +bool WiiSpeak::IsMicrophoneConnected() const +{ + return Config::Get(Config::MAIN_WII_SPEAK_CONNECTED); +} } // namespace IOS::HLE::USB diff --git a/Source/Core/Core/IOS/USB/Emulated/WiiSpeak.h b/Source/Core/Core/IOS/USB/Emulated/WiiSpeak.h index f430cc44e1..760fb874bf 100644 --- a/Source/Core/Core/IOS/USB/Emulated/WiiSpeak.h +++ b/Source/Core/Core/IOS/USB/Emulated/WiiSpeak.h @@ -4,12 +4,9 @@ #pragma once #include -#include -#include #include #include "Common/CommonTypes.h" -#include "Common/Event.h" #include "Core/IOS/USB/Common.h" #include "Core/IOS/USB/Emulated/Microphone.h" @@ -20,6 +17,7 @@ class WiiSpeak final : public Device public: WiiSpeak(); ~WiiSpeak(); + DeviceDescriptor GetDeviceDescriptor() const override; std::vector GetConfigurations() const override; std::vector GetInterfaces(u8 config) const override; @@ -46,7 +44,7 @@ private: bool sp_on; }; - WSState sampler{}; + WSState m_sampler{}; enum Registers { @@ -74,22 +72,25 @@ private: SP_RIN = 0x200d }; - void GetRegister(std::unique_ptr& cmd); - void SetRegister(std::unique_ptr& cmd); + void GetRegister(const std::unique_ptr& cmd) const; + void SetRegister(const std::unique_ptr& cmd); + bool IsMicrophoneConnected() const; - u16 m_vid = 0; - u16 m_pid = 0; + const u16 m_vid = 0x057E; + const u16 m_pid = 0x0308; u8 m_active_interface = 0; bool m_device_attached = false; bool init = false; - bool b_is_mic_connected = true; - Microphone m_microphone; - DeviceDescriptor m_device_descriptor{}; - std::vector m_config_descriptor; - std::vector m_interface_descriptor; - std::vector m_endpoint_descriptor; - std::thread m_microphone_thread; - std::mutex m_mutex; - Common::Event m_shutdown_event; + std::unique_ptr m_microphone{}; + const DeviceDescriptor m_device_descriptor{0x12, 0x1, 0x200, 0, 0, 0, 0x10, + 0x57E, 0x0308, 0x0214, 0x1, 0x2, 0x0, 0x1}; + const std::vector m_config_descriptor{ + {0x9, 0x2, 0x0030, 0x1, 0x1, 0x0, 0x80, 0x32}}; + const std::vector m_interface_descriptor{ + {0x9, 0x4, 0x0, 0x0, 0x0, 0xFF, 0xFF, 0xFF, 0x0}, + {0x9, 0x4, 0x0, 0x01, 0x03, 0xFF, 0xFF, 0xFF, 0x0}}; + const std::vector m_endpoint_descriptor{{0x7, 0x5, 0x81, 0x1, 0x0020, 0x1}, + {0x7, 0x5, 0x2, 0x2, 0x0020, 0}, + {0x7, 0x5, 0x3, 0x1, 0x0040, 1}}; }; } // namespace IOS::HLE::USB diff --git a/Source/Core/Core/IOS/USB/USBScanner.cpp b/Source/Core/Core/IOS/USB/USBScanner.cpp index 783cf9749c..128e59f13c 100644 --- a/Source/Core/Core/IOS/USB/USBScanner.cpp +++ b/Source/Core/Core/IOS/USB/USBScanner.cpp @@ -178,9 +178,11 @@ void USBScanner::AddEmulatedDevices(DeviceMap* new_devices) auto infinity_base = std::make_unique(); AddDevice(std::move(infinity_base), new_devices); } - - auto wii_speak = std::make_unique(); - AddDevice(std::move(wii_speak), new_devices); + if (Config::Get(Config::MAIN_EMULATE_WII_SPEAK) && !NetPlay::IsNetPlayRunning()) + { + auto wii_speak = std::make_unique(); + AddDevice(std::move(wii_speak), new_devices); + } } void USBScanner::AddDevice(std::unique_ptr device, DeviceMap* new_devices) diff --git a/Source/Core/DolphinQt/EmulatedUSB/WiiSpeakWindow.cpp b/Source/Core/DolphinQt/EmulatedUSB/WiiSpeakWindow.cpp index 5a457d8d1e..3d644827b1 100644 --- a/Source/Core/DolphinQt/EmulatedUSB/WiiSpeakWindow.cpp +++ b/Source/Core/DolphinQt/EmulatedUSB/WiiSpeakWindow.cpp @@ -3,33 +3,22 @@ #include "DolphinQt/EmulatedUSB/WiiSpeakWindow.h" -#include - #include #include -#include -#include #include -#include -#include -#include -#include -#include +#include #include #include -#include "Common/IOFile.h" - +#include "AudioCommon/CubebUtils.h" #include "Core/Config/MainSettings.h" #include "Core/Core.h" -#include "Core/IOS/USB/Emulated/Microphone.h" #include "Core/System.h" - -#include "DolphinQt/QtUtils/DolphinFileDialog.h" #include "DolphinQt/Settings.h" WiiSpeakWindow::WiiSpeakWindow(QWidget* parent) : QWidget(parent) { + // i18n: Window for managing the Wii Speak microphone setWindowTitle(tr("Wii Speak Manager")); setObjectName(QStringLiteral("wii_speak_manager")); setMinimumSize(QSize(700, 200)); @@ -52,33 +41,70 @@ void WiiSpeakWindow::CreateMainWindow() auto* checkbox_group = new QGroupBox(); auto* checkbox_layout = new QHBoxLayout(); - checkbox_layout->setAlignment(Qt::AlignLeft); - m_checkbox = new QCheckBox(tr("Emulate Wii Speak"), this); - m_checkbox->setChecked(Config::Get(Config::MAIN_EMULATE_WII_SPEAK)); - connect(m_checkbox, &QCheckBox::toggled, this, &WiiSpeakWindow::EmulateWiiSpeak); - checkbox_layout->addWidget(m_checkbox); + checkbox_layout->setAlignment(Qt::AlignHCenter); + m_checkbox_enabled = new QCheckBox(tr("Emulate Wii Speak"), this); + m_checkbox_enabled->setChecked(Config::Get(Config::MAIN_EMULATE_WII_SPEAK)); + connect(m_checkbox_enabled, &QCheckBox::toggled, this, &WiiSpeakWindow::EmulateWiiSpeak); + checkbox_layout->addWidget(m_checkbox_enabled); checkbox_group->setLayout(checkbox_layout); + main_layout->addWidget(checkbox_group); + + m_config_group = new QGroupBox(tr("Microphone Configuration")); + auto* config_layout = new QHBoxLayout(); + + auto checkbox_mic_connected = new QCheckBox(tr("Connect"), this); + checkbox_mic_connected->setChecked(Config::Get(Config::MAIN_WII_SPEAK_CONNECTED)); + connect(checkbox_mic_connected, &QCheckBox::toggled, this, + &WiiSpeakWindow::SetWiiSpeakConnectionState); + config_layout->addWidget(checkbox_mic_connected); m_combobox_microphones = new QComboBox(); - for (const std::string& device : IOS::HLE::USB::Microphone::ListDevices()) + m_combobox_microphones->addItem(QLatin1String("(%1)").arg(tr("Autodetect preferred microphone")), + QString{}); + for (auto& [device_id, device_name] : CubebUtils::ListInputDevices()) { - m_combobox_microphones->addItem(QString::fromStdString(device)); + const auto user_data = QString::fromStdString(device_id); + m_combobox_microphones->addItem(QString::fromStdString(device_name), user_data); } + connect(m_combobox_microphones, &QComboBox::currentIndexChanged, this, + &WiiSpeakWindow::OnInputDeviceChange); - checkbox_layout->addWidget(m_combobox_microphones); + auto current_device_id = QString::fromStdString(Config::Get(Config::MAIN_WII_SPEAK_MICROPHONE)); + m_combobox_microphones->setCurrentIndex(m_combobox_microphones->findData(current_device_id)); + config_layout->addWidget(m_combobox_microphones); + + m_config_group->setLayout(config_layout); + m_config_group->setVisible(Config::Get(Config::MAIN_EMULATE_WII_SPEAK)); + main_layout->addWidget(m_config_group); - main_layout->addWidget(checkbox_group); setLayout(main_layout); } void WiiSpeakWindow::EmulateWiiSpeak(bool emulate) { Config::SetBaseOrCurrent(Config::MAIN_EMULATE_WII_SPEAK, emulate); + m_config_group->setVisible(emulate); +} + +void WiiSpeakWindow::SetWiiSpeakConnectionState(bool connected) +{ + Config::SetBaseOrCurrent(Config::MAIN_WII_SPEAK_CONNECTED, connected); } void WiiSpeakWindow::OnEmulationStateChanged(Core::State state) { const bool running = state != Core::State::Uninitialized; - m_checkbox->setEnabled(!running); + m_checkbox_enabled->setEnabled(!running); + m_combobox_microphones->setEnabled(!running); +} + +void WiiSpeakWindow::OnInputDeviceChange() +{ + auto user_data = m_combobox_microphones->currentData(); + if (!user_data.isValid()) + return; + + const std::string device_id = user_data.toString().toStdString(); + Config::SetBaseOrCurrent(Config::MAIN_WII_SPEAK_MICROPHONE, device_id); } diff --git a/Source/Core/DolphinQt/EmulatedUSB/WiiSpeakWindow.h b/Source/Core/DolphinQt/EmulatedUSB/WiiSpeakWindow.h index dd7fa758a3..1f49796573 100644 --- a/Source/Core/DolphinQt/EmulatedUSB/WiiSpeakWindow.h +++ b/Source/Core/DolphinQt/EmulatedUSB/WiiSpeakWindow.h @@ -9,6 +9,7 @@ class QCheckBox; class QComboBox; +class QGroupBox; class WiiSpeakWindow : public QWidget { @@ -21,7 +22,10 @@ private: void CreateMainWindow(); void OnEmulationStateChanged(Core::State state); void EmulateWiiSpeak(bool emulate); + void SetWiiSpeakConnectionState(bool connected); + void OnInputDeviceChange(); - QCheckBox* m_checkbox; + QCheckBox* m_checkbox_enabled; QComboBox* m_combobox_microphones; + QGroupBox* m_config_group; }; From 28277d690dc112119b3d291c720fd5413a065b7c Mon Sep 17 00:00:00 2001 From: Sepalani Date: Sat, 11 May 2024 01:34:57 +0400 Subject: [PATCH 03/22] IOS/USB: Add more logs to the Wii Speak code --- .../Core/Core/IOS/USB/Emulated/WiiSpeak.cpp | 96 +++++++++++++++++-- Source/Core/Core/IOS/USB/Emulated/WiiSpeak.h | 10 +- 2 files changed, 96 insertions(+), 10 deletions(-) diff --git a/Source/Core/Core/IOS/USB/Emulated/WiiSpeak.cpp b/Source/Core/Core/IOS/USB/Emulated/WiiSpeak.cpp index 55896e1c38..fc3971ffe1 100644 --- a/Source/Core/Core/IOS/USB/Emulated/WiiSpeak.cpp +++ b/Source/Core/Core/IOS/USB/Emulated/WiiSpeak.cpp @@ -173,12 +173,34 @@ int WiiSpeak::SubmitTransfer(std::unique_ptr cmd) ERROR_LOG_FMT(IOS_USB, "Wii Speak command invalid"); return IPC_EINVAL; } - if (cmd->endpoint == 0x81 && m_microphone && m_microphone->HasData()) - m_microphone->ReadIntoBuffer(packets, cmd->length); + + switch (cmd->endpoint) + { + case ENDPOINT_AUDIO_IN: + // Transfer: Wii Speak -> Wii + if (m_microphone && m_microphone->HasData()) + m_microphone->ReadIntoBuffer(packets, cmd->length); + break; + case ENDPOINT_AUDIO_OUT: + // Transfer: Wii -> Wii Speak + break; + default: + WARN_LOG_FMT(IOS_USB, "Wii Speak unsupported isochronous transfer (endpoint={:02x})", + cmd->endpoint); + break; + } // Anything more causes the visual cue to not appear. // Anything less is more choppy audio. - cmd->ScheduleTransferCompletion(IPC_SUCCESS, 2500); + DEBUG_LOG_FMT(IOS_USB, + "Wii Speak isochronous transfer: length={:04x} endpoint={:02x} num_packets={:02x}", + cmd->length, cmd->endpoint, cmd->num_packets); + + // According to the Wii Speak specs on wiibrew, it's "USB 2.0 Full-speed Device Module", + // so the length of a single frame should be 1 ms. + // TODO: Find a proper way to compute the transfer timing. + const u32 transfer_timing = 2500; // 2.5 ms + cmd->ScheduleTransferCompletion(IPC_SUCCESS, transfer_timing); return IPC_SUCCESS; } @@ -190,6 +212,25 @@ void WiiSpeak::SetRegister(const std::unique_ptr& cmd) const u16 arg1 = memory.Read_U16(cmd->data_address + 2); const u16 arg2 = memory.Read_U16(cmd->data_address + 4); + DEBUG_LOG_FMT(IOS_USB, "Wii Speak register set (reg={:02x}, arg1={:04x}, arg2={:04x})", reg, arg1, + arg2); + + // TODO + // + // - On Wii Speak Channel start + // W[IOS_USB]: Wii Speak unsupported register set (reg=0c, arg1=0000, arg2=0000) + // W[IOS_USB]: Wii Speak unsupported register get (reg=0c, arg1=109091e2, arg2=109091e4) + // + // - On Wii Speak Channel close + // W[IOS_USB]: Wii Speak unsupported register set (reg=0c, arg1=0001, arg2=0000) + // W[IOS_USB]: Wii Speak unsupported register get (reg=0c, arg1=109091e2, arg2=109091e4) + // + // - On Monster Hunter 3 (RMHE08) online start + // N[OSREPORT_HLE]: 80450a20->80418adc| ok to call PMICStartAsync() -> 0 + // W[IOS_USB]: Wii Speak unsupported register set (reg=0c, arg1=0000, arg2=0000) + // W[IOS_USB]: Wii Speak unsupported register get (reg=0c, arg1=10037f62, arg2=10037f64) + // N[OSREPORT_HLE]: 80450a94->80418adc| ok to start P-Mic -> 0. + switch (reg) { case SAMPLER_STATE: @@ -205,8 +246,13 @@ void WiiSpeak::SetRegister(const std::unique_ptr& cmd) m_sampler.freq = 11025; break; case FREQ_RESERVED: - case FREQ_16KHZ: default: + WARN_LOG_FMT(IOS_USB, + "Wii Speak unsupported SAMPLER_FREQ set (arg1={:04x}, arg2={:04x}) defaulting " + "to FREQ_16KHZ", + arg1, arg2); + [[fallthrough]]; + case FREQ_16KHZ: m_sampler.freq = 16000; break; } @@ -223,8 +269,13 @@ void WiiSpeak::SetRegister(const std::unique_ptr& cmd) case GAIN_30dB: m_sampler.gain = 30; break; - case GAIN_36dB: default: + WARN_LOG_FMT(IOS_USB, + "Wii Speak unsupported SAMPLER_GAIN set (arg1={:04x}, arg2={:04x}) defaulting " + "to GAIN_36dB", + arg1, arg2); + [[fallthrough]]; + case GAIN_36dB: m_sampler.gain = 36; break; } @@ -242,11 +293,20 @@ void WiiSpeak::SetRegister(const std::unique_ptr& cmd) case SP_SOUT: case SP_RIN: break; + default: + WARN_LOG_FMT(IOS_USB, "Wii Speak unsupported SP_STATE set (arg1={:04x}, arg2={:04x})", arg1, + arg2); + break; } break; case SAMPLER_MUTE: m_sampler.mute = !!arg1; break; + default: + WARN_LOG_FMT(IOS_USB, + "Wii Speak unsupported register set (reg={:02x}, arg1={:04x}, arg2={:04x})", reg, + arg1, arg2); + break; } } @@ -258,6 +318,9 @@ void WiiSpeak::GetRegister(const std::unique_ptr& cmd) const const u32 arg1 = cmd->data_address + 2; const u32 arg2 = cmd->data_address + 4; + DEBUG_LOG_FMT(IOS_USB, "Wii Speak register get (reg={:02x}, arg1={:08x}, arg2={:08x})", reg, arg1, + arg2); + switch (reg) { case SAMPLER_STATE: @@ -272,8 +335,13 @@ void WiiSpeak::GetRegister(const std::unique_ptr& cmd) const case 11025: memory.Write_U16(FREQ_11KHZ, arg1); break; - case 16000: default: + WARN_LOG_FMT(IOS_USB, + "Wii Speak unsupported SAMPLER_FREQ get (arg1={:04x}, arg2={:04x}) defaulting " + "to FREQ_16KHZ", + arg1, arg2); + [[fallthrough]]; + case 16000: memory.Write_U16(FREQ_16KHZ, arg1); break; } @@ -290,8 +358,13 @@ void WiiSpeak::GetRegister(const std::unique_ptr& cmd) const case 30: memory.Write_U16(0x300 | GAIN_30dB, arg1); break; - case 36: default: + WARN_LOG_FMT(IOS_USB, + "Wii Speak unsupported SAMPLER_GAIN get (arg1={:04x}, arg2={:04x}) defaulting " + "to GAIN_36dB", + arg1, arg2); + [[fallthrough]]; + case 36: memory.Write_U16(0x300 | GAIN_36dB, arg1); break; } @@ -312,11 +385,20 @@ void WiiSpeak::GetRegister(const std::unique_ptr& cmd) const break; case SP_RIN: break; + default: + WARN_LOG_FMT(IOS_USB, "Wii Speak unsupported SP_STATE get (arg1={:04x}, arg2={:04x})", arg1, + arg2); + break; } break; case SAMPLER_MUTE: memory.Write_U16(m_sampler.mute ? 1 : 0, arg1); break; + default: + WARN_LOG_FMT(IOS_USB, + "Wii Speak unsupported register get (reg={:02x}, arg1={:08x}, arg2={:08x})", reg, + arg1, arg2); + break; } } diff --git a/Source/Core/Core/IOS/USB/Emulated/WiiSpeak.h b/Source/Core/Core/IOS/USB/Emulated/WiiSpeak.h index 760fb874bf..306b6f048f 100644 --- a/Source/Core/Core/IOS/USB/Emulated/WiiSpeak.h +++ b/Source/Core/Core/IOS/USB/Emulated/WiiSpeak.h @@ -89,8 +89,12 @@ private: const std::vector m_interface_descriptor{ {0x9, 0x4, 0x0, 0x0, 0x0, 0xFF, 0xFF, 0xFF, 0x0}, {0x9, 0x4, 0x0, 0x01, 0x03, 0xFF, 0xFF, 0xFF, 0x0}}; - const std::vector m_endpoint_descriptor{{0x7, 0x5, 0x81, 0x1, 0x0020, 0x1}, - {0x7, 0x5, 0x2, 0x2, 0x0020, 0}, - {0x7, 0x5, 0x3, 0x1, 0x0040, 1}}; + static constexpr u8 ENDPOINT_AUDIO_IN = 0x81; + static constexpr u8 ENDPOINT_AUDIO_OUT = 0x3; + static constexpr u8 ENDPOINT_DATA_OUT = 0x2; + const std::vector m_endpoint_descriptor{ + {0x7, 0x5, ENDPOINT_AUDIO_IN, 0x1, 0x0020, 0x1}, + {0x7, 0x5, ENDPOINT_DATA_OUT, 0x2, 0x0020, 0}, + {0x7, 0x5, ENDPOINT_AUDIO_OUT, 0x1, 0x0040, 0x1}}; }; } // namespace IOS::HLE::USB From 9e7b6c40e0a45168f0b5f490f241f55bf2526a0f Mon Sep 17 00:00:00 2001 From: Sepalani Date: Sat, 11 May 2024 02:11:45 +0400 Subject: [PATCH 04/22] IOS/USB: Fix Wii Speak SAMPLER_MUTE register The register should be 12 (i.e. 0x0c) instead of 0xc0 --- Source/Core/Core/IOS/USB/Emulated/WiiSpeak.cpp | 16 ---------------- Source/Core/Core/IOS/USB/Emulated/WiiSpeak.h | 2 +- 2 files changed, 1 insertion(+), 17 deletions(-) diff --git a/Source/Core/Core/IOS/USB/Emulated/WiiSpeak.cpp b/Source/Core/Core/IOS/USB/Emulated/WiiSpeak.cpp index fc3971ffe1..bacfbcdae7 100644 --- a/Source/Core/Core/IOS/USB/Emulated/WiiSpeak.cpp +++ b/Source/Core/Core/IOS/USB/Emulated/WiiSpeak.cpp @@ -215,22 +215,6 @@ void WiiSpeak::SetRegister(const std::unique_ptr& cmd) DEBUG_LOG_FMT(IOS_USB, "Wii Speak register set (reg={:02x}, arg1={:04x}, arg2={:04x})", reg, arg1, arg2); - // TODO - // - // - On Wii Speak Channel start - // W[IOS_USB]: Wii Speak unsupported register set (reg=0c, arg1=0000, arg2=0000) - // W[IOS_USB]: Wii Speak unsupported register get (reg=0c, arg1=109091e2, arg2=109091e4) - // - // - On Wii Speak Channel close - // W[IOS_USB]: Wii Speak unsupported register set (reg=0c, arg1=0001, arg2=0000) - // W[IOS_USB]: Wii Speak unsupported register get (reg=0c, arg1=109091e2, arg2=109091e4) - // - // - On Monster Hunter 3 (RMHE08) online start - // N[OSREPORT_HLE]: 80450a20->80418adc| ok to call PMICStartAsync() -> 0 - // W[IOS_USB]: Wii Speak unsupported register set (reg=0c, arg1=0000, arg2=0000) - // W[IOS_USB]: Wii Speak unsupported register get (reg=0c, arg1=10037f62, arg2=10037f64) - // N[OSREPORT_HLE]: 80450a94->80418adc| ok to start P-Mic -> 0. - switch (reg) { case SAMPLER_STATE: diff --git a/Source/Core/Core/IOS/USB/Emulated/WiiSpeak.h b/Source/Core/Core/IOS/USB/Emulated/WiiSpeak.h index 306b6f048f..e827e4e3d9 100644 --- a/Source/Core/Core/IOS/USB/Emulated/WiiSpeak.h +++ b/Source/Core/Core/IOS/USB/Emulated/WiiSpeak.h @@ -49,7 +49,7 @@ private: enum Registers { SAMPLER_STATE = 0, - SAMPLER_MUTE = 0xc0, + SAMPLER_MUTE = 0x0c, SAMPLER_FREQ = 2, FREQ_8KHZ = 0, From 045545a9b929b3318d4e9917825868c011cd30c8 Mon Sep 17 00:00:00 2001 From: Sepalani Date: Sat, 11 May 2024 14:18:02 +0400 Subject: [PATCH 05/22] IOS/USB: Fix Wii Speak buffer memcpy size parameter It seems to fix random echoes and reduce noises when nobody is speaking --- .../Core/Core/IOS/USB/Emulated/Microphone.cpp | 21 +++++++++++++++---- .../Core/Core/IOS/USB/Emulated/Microphone.h | 4 ++-- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/Source/Core/Core/IOS/USB/Emulated/Microphone.cpp b/Source/Core/Core/IOS/USB/Emulated/Microphone.cpp index c264eb768f..93e4bcfbc6 100644 --- a/Source/Core/Core/IOS/USB/Emulated/Microphone.cpp +++ b/Source/Core/Core/IOS/USB/Emulated/Microphone.cpp @@ -188,6 +188,7 @@ long Microphone::DataCallback(cubeb_stream* stream, void* user_data, const void* mic->m_samples_avail += nframes; if (mic->m_samples_avail > mic->STREAM_SIZE) { + WARN_LOG_FMT(IOS_USB, "Wii Speak ring buffer is full, data will be lost!"); mic->m_samples_avail = 0; } @@ -196,18 +197,30 @@ long Microphone::DataCallback(cubeb_stream* stream, void* user_data, const void* void Microphone::ReadIntoBuffer(u8* dst, 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 lk(m_ring_lock); - if (m_samples_avail >= BUFF_SIZE_SAMPLES) + for (u8* end = dst + size; dst < end; dst += SINGLE_READ_SIZE, size -= SINGLE_READ_SIZE) { - u8* last_buffer = reinterpret_cast(&m_stream_buffer[m_stream_rpos]); - std::memcpy(dst, static_cast(last_buffer), size); + if (size < SINGLE_READ_SIZE || m_samples_avail < BUFF_SIZE_SAMPLES) + break; + + SampleType* last_buffer = &m_stream_buffer[m_stream_rpos]; + std::memcpy(dst, last_buffer, SINGLE_READ_SIZE); m_samples_avail -= BUFF_SIZE_SAMPLES; - m_stream_rpos += BUFF_SIZE_SAMPLES; m_stream_rpos %= STREAM_SIZE; } + if (size != 0) + { + std::memset(dst, 0, size); + } } bool Microphone::HasData() const diff --git a/Source/Core/Core/IOS/USB/Emulated/Microphone.h b/Source/Core/Core/IOS/USB/Emulated/Microphone.h index a938430dae..1b02e81d42 100644 --- a/Source/Core/Core/IOS/USB/Emulated/Microphone.h +++ b/Source/Core/Core/IOS/USB/Emulated/Microphone.h @@ -34,11 +34,11 @@ private: void StreamStop(); static constexpr u32 SAMPLING_RATE = 8000; - static constexpr u32 BUFFER_SIZE = SAMPLING_RATE / 2; + using SampleType = s16; static constexpr u32 BUFF_SIZE_SAMPLES = 16; static constexpr u32 STREAM_SIZE = BUFF_SIZE_SAMPLES * 500; - std::array m_stream_buffer{}; + std::array m_stream_buffer{}; u32 m_stream_wpos = 0; u32 m_stream_rpos = 0; u32 m_samples_avail = 0; From e4fd94b24b42bb8ac74298ea9256eecedbb27098 Mon Sep 17 00:00:00 2001 From: Sepalani Date: Sun, 12 May 2024 12:58:53 +0400 Subject: [PATCH 06/22] IOS/USB: Try to fix the Wii Speak Channel record/playback feature --- .../Core/Core/IOS/USB/Emulated/WiiSpeak.cpp | 27 ++++++++++++++++--- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/Source/Core/Core/IOS/USB/Emulated/WiiSpeak.cpp b/Source/Core/Core/IOS/USB/Emulated/WiiSpeak.cpp index bacfbcdae7..0bd30a4109 100644 --- a/Source/Core/Core/IOS/USB/Emulated/WiiSpeak.cpp +++ b/Source/Core/Core/IOS/USB/Emulated/WiiSpeak.cpp @@ -190,16 +190,35 @@ int WiiSpeak::SubmitTransfer(std::unique_ptr cmd) break; } - // Anything more causes the visual cue to not appear. - // Anything less is more choppy audio. + // Transferring too slow causes the visual cue to not appear, + // while transferring too fast results in more choppy audio. DEBUG_LOG_FMT(IOS_USB, "Wii Speak isochronous transfer: length={:04x} endpoint={:02x} num_packets={:02x}", cmd->length, cmd->endpoint, cmd->num_packets); // According to the Wii Speak specs on wiibrew, it's "USB 2.0 Full-speed Device Module", // so the length of a single frame should be 1 ms. - // TODO: Find a proper way to compute the transfer timing. - const u32 transfer_timing = 2500; // 2.5 ms + // + // Monster Hunter 3 and the Wii Speak Channel use cmd->length=0x100, allowing 256/2 samples + // (i.e. 128 samples in 16-bit mono) per frame transfer. The Microphone class is using cubeb + // configured with a sample rate of 8000. + // + // Based on these numbers, here are some theoretical speeds: + // - fastest transfer speed would be 8000 samples in 63 ms (i.e. 8000 * 1/128 = 62.5) + // * using a timing of 1 ms per frame of 128 samples + // * here cubeb sample rate is the bottleneck + // - slowest transfer speed would be 8000 samples in 1000 ms (i.e. 128 * 1000/8000 = 16) + // * using a timing of 16 ms per frame of 128 samples + // * matching cubeb sampling rate + // + // A decent timing would be 16ms. However, it seems that the Wii Speak Channel record feature is + // broken if the timing is less than 32ms and that the record playback is broken when around 34ms + // and above. Monster Hunter 3 doesn't seem affected by this timing issue. + // + // TODO: Investigate and ensure that's the proper way to fix it. + // Maybe it's related to the Wii DSP and its stereo/mono mode? + // If so, what would be the impact of using DSP LLE/HLE in mono/stereo? + const u32 transfer_timing = 32000; cmd->ScheduleTransferCompletion(IPC_SUCCESS, transfer_timing); return IPC_SUCCESS; } From 15b614ceef3739ffc7317ba0b4847c3236947b81 Mon Sep 17 00:00:00 2001 From: Sepalani Date: Sun, 12 May 2024 15:48:11 +0400 Subject: [PATCH 07/22] IOS/USB: Only sample Wii Speak data when necessary --- .../Core/Core/IOS/USB/Emulated/Microphone.cpp | 19 +++++++++++++++- .../Core/Core/IOS/USB/Emulated/Microphone.h | 7 +++++- .../Core/Core/IOS/USB/Emulated/WiiSpeak.cpp | 2 +- Source/Core/Core/IOS/USB/Emulated/WiiSpeak.h | 22 +++++++++---------- 4 files changed, 36 insertions(+), 14 deletions(-) diff --git a/Source/Core/Core/IOS/USB/Emulated/Microphone.cpp b/Source/Core/Core/IOS/USB/Emulated/Microphone.cpp index 93e4bcfbc6..7490749557 100644 --- a/Source/Core/Core/IOS/USB/Emulated/Microphone.cpp +++ b/Source/Core/Core/IOS/USB/Emulated/Microphone.cpp @@ -13,6 +13,9 @@ #include "Common/ScopeGuard.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 #include @@ -20,7 +23,7 @@ namespace IOS::HLE::USB { -Microphone::Microphone() +Microphone::Microphone(const WiiSpeakState& sampler) : m_sampler(sampler) { #ifdef _WIN32 Common::Event sync_event; @@ -174,7 +177,16 @@ void Microphone::StreamStop() long Microphone::DataCallback(cubeb_stream* stream, void* user_data, const void* input_buffer, void* /*output_buffer*/, long nframes) { + // Skip data when core isn't running + if (Core::GetState(Core::System::GetInstance()) != Core::State::Running) + return nframes; + auto* mic = static_cast(user_data); + const auto& sampler = mic->GetSampler(); + + // Skip data if sampling is off or mute is on + if (!sampler.sample_on || sampler.mute) + return nframes; std::lock_guard lk(mic->m_ring_lock); @@ -227,4 +239,9 @@ bool Microphone::HasData() const { return m_samples_avail > 0 && Config::Get(Config::MAIN_WII_SPEAK_CONNECTED); } + +const WiiSpeakState& Microphone::GetSampler() const +{ + return m_sampler; +} } // namespace IOS::HLE::USB diff --git a/Source/Core/Core/IOS/USB/Emulated/Microphone.h b/Source/Core/Core/IOS/USB/Emulated/Microphone.h index 1b02e81d42..1919abfa21 100644 --- a/Source/Core/Core/IOS/USB/Emulated/Microphone.h +++ b/Source/Core/Core/IOS/USB/Emulated/Microphone.h @@ -15,14 +15,17 @@ struct cubeb_stream; namespace IOS::HLE::USB { +struct WiiSpeakState; + class Microphone final { public: - Microphone(); + Microphone(const WiiSpeakState& sampler); ~Microphone(); bool HasData() const; void ReadIntoBuffer(u8* dst, u32 size); + const WiiSpeakState& GetSampler() const; private: static long DataCallback(cubeb_stream* stream, void* user_data, const void* input_buffer, @@ -47,6 +50,8 @@ private: std::shared_ptr m_cubeb_ctx = nullptr; cubeb_stream* m_cubeb_stream = nullptr; + const WiiSpeakState& m_sampler; + #ifdef _WIN32 Common::WorkQueueThread> m_work_queue{ "Wii Speak Worker", [](const std::function& func) { func(); }}; diff --git a/Source/Core/Core/IOS/USB/Emulated/WiiSpeak.cpp b/Source/Core/Core/IOS/USB/Emulated/WiiSpeak.cpp index 0bd30a4109..aa13b66b89 100644 --- a/Source/Core/Core/IOS/USB/Emulated/WiiSpeak.cpp +++ b/Source/Core/Core/IOS/USB/Emulated/WiiSpeak.cpp @@ -43,7 +43,7 @@ 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_microphone = std::make_unique(m_sampler); 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 e827e4e3d9..7eafee155a 100644 --- a/Source/Core/Core/IOS/USB/Emulated/WiiSpeak.h +++ b/Source/Core/Core/IOS/USB/Emulated/WiiSpeak.h @@ -12,6 +12,16 @@ namespace IOS::HLE::USB { +struct WiiSpeakState +{ + bool sample_on; + bool mute; + int freq; + int gain; + bool ec_reset; + bool sp_on; +}; + class WiiSpeak final : public Device { public: @@ -34,17 +44,7 @@ public: int SubmitTransfer(std::unique_ptr message) override; private: - struct WSState - { - bool sample_on; - bool mute; - int freq; - int gain; - bool ec_reset; - bool sp_on; - }; - - WSState m_sampler{}; + WiiSpeakState m_sampler{}; enum Registers { From 69a315ecb5428071a868f22b166ec87f3d3c9ee0 Mon Sep 17 00:00:00 2001 From: Sepalani Date: Sun, 12 May 2024 23:29:00 +0400 Subject: [PATCH 08/22] Android: Add emulated Wii Speak --- Source/Android/app/src/main/AndroidManifest.xml | 3 +++ .../features/settings/model/BooleanSetting.kt | 15 ++++++++++++++- .../settings/ui/SettingsFragmentPresenter.kt | 16 ++++++++++++++++ .../Android/app/src/main/res/values/strings.xml | 2 ++ 4 files changed, 35 insertions(+), 1 deletion(-) diff --git a/Source/Android/app/src/main/AndroidManifest.xml b/Source/Android/app/src/main/AndroidManifest.xml index 6d33d6158a..a14e13b135 100644 --- a/Source/Android/app/src/main/AndroidManifest.xml +++ b/Source/Android/app/src/main/AndroidManifest.xml @@ -28,6 +28,9 @@ + = HashSet(listOf(*NOT_RUNTIME_EDITABLE_ARRAY)) diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentPresenter.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentPresenter.kt index ff82f4c63c..b4427cf99c 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentPresenter.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentPresenter.kt @@ -892,6 +892,22 @@ class SettingsFragmentPresenter( 0 ) ) + sl.add( + SwitchSetting( + context, + BooleanSetting.MAIN_EMULATE_WII_SPEAK, + R.string.emulate_wii_speak, + 0 + ) + ) + sl.add( + InvertedSwitchSetting( + context, + BooleanSetting.MAIN_WII_SPEAK_CONNECTED, + R.string.disconnect_wii_speak, + 0 + ) + ) } private fun addAdvancedSettings(sl: ArrayList) { diff --git a/Source/Android/app/src/main/res/values/strings.xml b/Source/Android/app/src/main/res/values/strings.xml index 1b08fc6475..ac51331caa 100644 --- a/Source/Android/app/src/main/res/values/strings.xml +++ b/Source/Android/app/src/main/res/values/strings.xml @@ -941,4 +941,6 @@ It can efficiently compress both junk data and encrypted Wii data. Incompatible Figure Selected Please select a compatible figure file + Wii Speak + Mute Wii Speak From 78f37429acdb1e516b45d07417c412b2046b805d Mon Sep 17 00:00:00 2001 From: Sepalani Date: Tue, 14 May 2024 01:52:52 +0400 Subject: [PATCH 09/22] IOS/USB: Report Wii Speak packet size properly --- .../Core/Core/IOS/USB/Emulated/Microphone.cpp | 12 +++---- .../Core/Core/IOS/USB/Emulated/Microphone.h | 2 +- .../Core/Core/IOS/USB/Emulated/WiiSpeak.cpp | 33 ++++++++++--------- 3 files changed, 23 insertions(+), 24 deletions(-) diff --git a/Source/Core/Core/IOS/USB/Emulated/Microphone.cpp b/Source/Core/Core/IOS/USB/Emulated/Microphone.cpp index 7490749557..67f8e4ecf8 100644 --- a/Source/Core/Core/IOS/USB/Emulated/Microphone.cpp +++ b/Source/Core/Core/IOS/USB/Emulated/Microphone.cpp @@ -207,7 +207,7 @@ long Microphone::DataCallback(cubeb_stream* stream, void* user_data, const void* return nframes; } -void Microphone::ReadIntoBuffer(u8* dst, u32 size) +u16 Microphone::ReadIntoBuffer(u8* ptr, u32 size) { static constexpr u32 SINGLE_READ_SIZE = BUFF_SIZE_SAMPLES * sizeof(SampleType); @@ -217,22 +217,20 @@ void Microphone::ReadIntoBuffer(u8* dst, u32 size) std::lock_guard lk(m_ring_lock); - for (u8* end = dst + size; dst < end; dst += SINGLE_READ_SIZE, size -= SINGLE_READ_SIZE) + u8* begin = ptr; + for (u8* end = begin + size; ptr < end; ptr += SINGLE_READ_SIZE, size -= SINGLE_READ_SIZE) { if (size < SINGLE_READ_SIZE || m_samples_avail < BUFF_SIZE_SAMPLES) break; SampleType* last_buffer = &m_stream_buffer[m_stream_rpos]; - std::memcpy(dst, last_buffer, SINGLE_READ_SIZE); + std::memcpy(ptr, last_buffer, SINGLE_READ_SIZE); m_samples_avail -= BUFF_SIZE_SAMPLES; m_stream_rpos += BUFF_SIZE_SAMPLES; m_stream_rpos %= STREAM_SIZE; } - if (size != 0) - { - std::memset(dst, 0, size); - } + return static_cast(ptr - begin); } bool Microphone::HasData() const diff --git a/Source/Core/Core/IOS/USB/Emulated/Microphone.h b/Source/Core/Core/IOS/USB/Emulated/Microphone.h index 1919abfa21..af7d7a8362 100644 --- a/Source/Core/Core/IOS/USB/Emulated/Microphone.h +++ b/Source/Core/Core/IOS/USB/Emulated/Microphone.h @@ -24,7 +24,7 @@ public: ~Microphone(); bool HasData() const; - void ReadIntoBuffer(u8* dst, u32 size); + u16 ReadIntoBuffer(u8* ptr, u32 size); const WiiSpeakState& GetSampler() const; private: diff --git a/Source/Core/Core/IOS/USB/Emulated/WiiSpeak.cpp b/Source/Core/Core/IOS/USB/Emulated/WiiSpeak.cpp index aa13b66b89..15c145a80f 100644 --- a/Source/Core/Core/IOS/USB/Emulated/WiiSpeak.cpp +++ b/Source/Core/Core/IOS/USB/Emulated/WiiSpeak.cpp @@ -3,6 +3,8 @@ #include "Core/IOS/USB/Emulated/WiiSpeak.h" +#include + #include "Core/Config/MainSettings.h" #include "Core/HW/Memmap.h" #include "Core/System.h" @@ -177,10 +179,18 @@ int WiiSpeak::SubmitTransfer(std::unique_ptr cmd) switch (cmd->endpoint) { case ENDPOINT_AUDIO_IN: + { // Transfer: Wii Speak -> Wii + u16 size = 0; if (m_microphone && m_microphone->HasData()) - m_microphone->ReadIntoBuffer(packets, cmd->length); + 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; + } case ENDPOINT_AUDIO_OUT: // Transfer: Wii -> Wii Speak break; @@ -203,22 +213,13 @@ int WiiSpeak::SubmitTransfer(std::unique_ptr cmd) // (i.e. 128 samples in 16-bit mono) per frame transfer. The Microphone class is using cubeb // configured with a sample rate of 8000. // - // Based on these numbers, here are some theoretical speeds: - // - fastest transfer speed would be 8000 samples in 63 ms (i.e. 8000 * 1/128 = 62.5) - // * using a timing of 1 ms per frame of 128 samples - // * here cubeb sample rate is the bottleneck - // - slowest transfer speed would be 8000 samples in 1000 ms (i.e. 128 * 1000/8000 = 16) - // * using a timing of 16 ms per frame of 128 samples - // * matching cubeb sampling rate + // Based on USB sniffing using Wireshark + Dolphin USB passthrough: + // - 125 frames are received per second (i.e. timing 8 ms per frame) + // - however, the cmd->length=0x80 which doesn't match the HLE emulation + // - each frame having 8 packets of 0x10 bytes // - // A decent timing would be 16ms. However, it seems that the Wii Speak Channel record feature is - // broken if the timing is less than 32ms and that the record playback is broken when around 34ms - // and above. Monster Hunter 3 doesn't seem affected by this timing issue. - // - // TODO: Investigate and ensure that's the proper way to fix it. - // Maybe it's related to the Wii DSP and its stereo/mono mode? - // If so, what would be the impact of using DSP LLE/HLE in mono/stereo? - const u32 transfer_timing = 32000; + // Let's sample at a reasonable speed. + const u32 transfer_timing = 2000; cmd->ScheduleTransferCompletion(IPC_SUCCESS, transfer_timing); return IPC_SUCCESS; } From 2dd01bae6eaff352b11835d9c4efe681d4c54822 Mon Sep 17 00:00:00 2001 From: Sepalani Date: Mon, 27 May 2024 02:25:04 +0400 Subject: [PATCH 10/22] Android: Ask for RECORD_AUDIO permission --- .../ui/viewholder/SwitchSettingViewHolder.kt | 16 ++++++++++++++++ .../dolphinemu/utils/PermissionsHandler.java | 1 + 2 files changed, 17 insertions(+) diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/viewholder/SwitchSettingViewHolder.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/viewholder/SwitchSettingViewHolder.kt index fc575ead48..482be3dc16 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/viewholder/SwitchSettingViewHolder.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/viewholder/SwitchSettingViewHolder.kt @@ -2,6 +2,10 @@ package org.dolphinemu.dolphinemu.features.settings.ui.viewholder +import android.Manifest +import android.app.Activity +import android.content.pm.PackageManager +import android.os.Build import android.view.View import android.widget.CompoundButton import org.dolphinemu.dolphinemu.databinding.ListItemSettingSwitchBinding @@ -10,6 +14,7 @@ import org.dolphinemu.dolphinemu.features.settings.model.view.SettingsItem import org.dolphinemu.dolphinemu.features.settings.model.view.SwitchSetting import org.dolphinemu.dolphinemu.features.settings.ui.SettingsAdapter import org.dolphinemu.dolphinemu.utils.DirectoryInitialization +import org.dolphinemu.dolphinemu.utils.PermissionsHandler import java.io.File import java.util.* @@ -57,6 +62,17 @@ class SwitchSettingViewHolder( binding.settingSwitch.isEnabled = false } + if (setting.setting === BooleanSetting.MAIN_EMULATE_WII_SPEAK && isChecked) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && + itemView.context.checkSelfPermission(Manifest.permission.RECORD_AUDIO) + != PackageManager.PERMISSION_GRANTED) { + val settingsActivity = itemView.context as Activity + settingsActivity.requestPermissions( + arrayOf(Manifest.permission.RECORD_AUDIO), + PermissionsHandler.REQUEST_CODE_RECORD_AUDIO) + } + } + adapter.onBooleanClick(setting, binding.settingSwitch.isChecked) setStyle(binding.textSettingName, setting) diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/PermissionsHandler.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/PermissionsHandler.java index a4c69281f2..ab68470e28 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/PermissionsHandler.java +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/PermissionsHandler.java @@ -15,6 +15,7 @@ import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE; public class PermissionsHandler { public static final int REQUEST_CODE_WRITE_PERMISSION = 500; + public static final int REQUEST_CODE_RECORD_AUDIO = 501; private static boolean sWritePermissionDenied = false; public static void requestWritePermission(final FragmentActivity activity) From c7c96ff1f220e051703520617240d8928f8307c6 Mon Sep 17 00:00:00 2001 From: Sepalani Date: Tue, 4 Jun 2024 20:15:00 +0400 Subject: [PATCH 11/22] IOS/USB: Add a warning about RECORD_AUDIO permission --- .../dolphinemu/DolphinApplication.java | 10 +++++- .../ui/viewholder/SwitchSettingViewHolder.kt | 13 ++----- .../dolphinemu/utils/ActivityTracker.kt | 9 ++++- .../dolphinemu/utils/PermissionsHandler.java | 34 +++++++++++++++++++ .../app/src/main/res/values/strings.xml | 2 ++ Source/Android/jni/AndroidCommon/IDCache.cpp | 30 ++++++++++++++++ Source/Android/jni/AndroidCommon/IDCache.h | 4 +++ .../Core/Core/IOS/USB/Emulated/Microphone.cpp | 17 ++++++++++ 8 files changed, 107 insertions(+), 12 deletions(-) diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/DolphinApplication.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/DolphinApplication.java index e0816e408b..8ff78ebb0e 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/DolphinApplication.java +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/DolphinApplication.java @@ -2,6 +2,7 @@ package org.dolphinemu.dolphinemu; +import android.app.Activity; import android.app.Application; import android.content.Context; import android.hardware.usb.UsbManager; @@ -15,13 +16,15 @@ import org.dolphinemu.dolphinemu.utils.VolleyUtil; public class DolphinApplication extends Application { private static DolphinApplication application; + private static ActivityTracker sActivityTracker; @Override public void onCreate() { super.onCreate(); application = this; - registerActivityLifecycleCallbacks(new ActivityTracker()); + sActivityTracker = new ActivityTracker(); + registerActivityLifecycleCallbacks(sActivityTracker); VolleyUtil.init(getApplicationContext()); System.loadLibrary("main"); @@ -36,4 +39,9 @@ public class DolphinApplication extends Application { return application.getApplicationContext(); } + + public static Activity getAppActivity() + { + return sActivityTracker.getCurrentActivity(); + } } diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/viewholder/SwitchSettingViewHolder.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/viewholder/SwitchSettingViewHolder.kt index 482be3dc16..48034dae41 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/viewholder/SwitchSettingViewHolder.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/viewholder/SwitchSettingViewHolder.kt @@ -2,10 +2,7 @@ package org.dolphinemu.dolphinemu.features.settings.ui.viewholder -import android.Manifest import android.app.Activity -import android.content.pm.PackageManager -import android.os.Build import android.view.View import android.widget.CompoundButton import org.dolphinemu.dolphinemu.databinding.ListItemSettingSwitchBinding @@ -63,13 +60,9 @@ class SwitchSettingViewHolder( } if (setting.setting === BooleanSetting.MAIN_EMULATE_WII_SPEAK && isChecked) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && - itemView.context.checkSelfPermission(Manifest.permission.RECORD_AUDIO) - != PackageManager.PERMISSION_GRANTED) { - val settingsActivity = itemView.context as Activity - settingsActivity.requestPermissions( - arrayOf(Manifest.permission.RECORD_AUDIO), - PermissionsHandler.REQUEST_CODE_RECORD_AUDIO) + if (!PermissionsHandler.hasRecordAudioPermission(itemView.context)) { + val settingsActivity = itemView.context as Activity + PermissionsHandler.requestRecordAudioPermission(settingsActivity) } } diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/ActivityTracker.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/ActivityTracker.kt index 83a34e3e8c..0b4079bebf 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/ActivityTracker.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/ActivityTracker.kt @@ -9,12 +9,15 @@ class ActivityTracker : ActivityLifecycleCallbacks { private val resumedActivities = HashSet() private var backgroundExecutionAllowed = false private var firstStart = true + var currentActivity : Activity? = null + private set private fun isMainActivity(activity: Activity): Boolean { return activity is MainView } override fun onActivityCreated(activity: Activity, bundle: Bundle?) { + currentActivity = activity if (isMainActivity(activity)) { firstStart = bundle == null } @@ -51,7 +54,11 @@ class ActivityTracker : ActivityLifecycleCallbacks { override fun onActivitySaveInstanceState(activity: Activity, bundle: Bundle) {} - override fun onActivityDestroyed(activity: Activity) {} + override fun onActivityDestroyed(activity: Activity) { + if (currentActivity === activity) { + currentActivity = null + } + } companion object { @JvmStatic diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/PermissionsHandler.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/PermissionsHandler.java index ab68470e28..527139ada6 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/PermissionsHandler.java +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/PermissionsHandler.java @@ -2,6 +2,7 @@ package org.dolphinemu.dolphinemu.utils; +import android.app.Activity; import android.content.Context; import android.content.pm.PackageManager; import android.os.Build; @@ -11,6 +12,11 @@ import androidx.core.content.ContextCompat; import androidx.fragment.app.FragmentActivity; import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE; +import static android.Manifest.permission.RECORD_AUDIO; + +import org.dolphinemu.dolphinemu.R; +import org.dolphinemu.dolphinemu.DolphinApplication; +import org.dolphinemu.dolphinemu.NativeLibrary; public class PermissionsHandler { @@ -53,4 +59,32 @@ public class PermissionsHandler { return sWritePermissionDenied; } + + public static boolean hasRecordAudioPermission(Context context) + { + if (context == null) + context = DolphinApplication.getAppContext(); + int hasRecordPermission = ContextCompat.checkSelfPermission(context, RECORD_AUDIO); + return hasRecordPermission == PackageManager.PERMISSION_GRANTED; + } + + public static void requestRecordAudioPermission(Activity activity) + { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) + return; + + if (activity == null) + { + // Calling from C++ code + activity = DolphinApplication.getAppActivity(); + // Since the emulation (and cubeb) has already started, enabling the microphone permission + // now might require restarting the game to be effective. Warn the user about it. + NativeLibrary.displayAlertMsg( + activity.getString(R.string.wii_speak_permission_warning), + activity.getString(R.string.wii_speak_permission_warning_description), + false, true, false); + } + + activity.requestPermissions(new String[]{RECORD_AUDIO}, REQUEST_CODE_RECORD_AUDIO); + } } diff --git a/Source/Android/app/src/main/res/values/strings.xml b/Source/Android/app/src/main/res/values/strings.xml index ac51331caa..f063c937c7 100644 --- a/Source/Android/app/src/main/res/values/strings.xml +++ b/Source/Android/app/src/main/res/values/strings.xml @@ -943,4 +943,6 @@ It can efficiently compress both junk data and encrypted Wii data. Wii Speak Mute Wii Speak + Missing Microphone Permission + Wii Speak emulation requires microphone permission. You might need to restart the game for the permission to be effective. diff --git a/Source/Android/jni/AndroidCommon/IDCache.cpp b/Source/Android/jni/AndroidCommon/IDCache.cpp index ed382745c0..08ec2c9804 100644 --- a/Source/Android/jni/AndroidCommon/IDCache.cpp +++ b/Source/Android/jni/AndroidCommon/IDCache.cpp @@ -116,6 +116,10 @@ static jmethodID s_core_device_control_constructor; static jclass s_input_detector_class; static jfieldID s_input_detector_pointer; +static jclass s_permission_handler_class; +static jmethodID s_permission_handler_has_record_audio_permission; +static jmethodID s_permission_handler_request_record_audio_permission; + static jmethodID s_runnable_run; namespace IDCache @@ -538,6 +542,21 @@ jfieldID GetInputDetectorPointer() return s_input_detector_pointer; } +jclass GetPermissionHandlerClass() +{ + return s_permission_handler_class; +} + +jmethodID GetPermissionHandlerHasRecordAudioPermission() +{ + return s_permission_handler_has_record_audio_permission; +} + +jmethodID GetPermissionHandlerRequestRecordAudioPermission() +{ + return s_permission_handler_request_record_audio_permission; +} + jmethodID GetRunnableRun() { return s_runnable_run; @@ -765,6 +784,16 @@ JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) s_input_detector_pointer = env->GetFieldID(input_detector_class, "pointer", "J"); env->DeleteLocalRef(input_detector_class); + const jclass permission_handler_class = + env->FindClass("org/dolphinemu/dolphinemu/utils/PermissionsHandler"); + s_permission_handler_class = + reinterpret_cast(env->NewGlobalRef(permission_handler_class)); + s_permission_handler_has_record_audio_permission = env->GetStaticMethodID( + permission_handler_class, "hasRecordAudioPermission", "(Landroid/content/Context;)Z"); + s_permission_handler_request_record_audio_permission = env->GetStaticMethodID( + permission_handler_class, "requestRecordAudioPermission", "(Landroid/app/Activity;)V"); + env->DeleteLocalRef(permission_handler_class); + const jclass runnable_class = env->FindClass("java/lang/Runnable"); s_runnable_run = env->GetMethodID(runnable_class, "run", "()V"); env->DeleteLocalRef(runnable_class); @@ -804,5 +833,6 @@ JNIEXPORT void JNI_OnUnload(JavaVM* vm, void* reserved) env->DeleteGlobalRef(s_core_device_class); env->DeleteGlobalRef(s_core_device_control_class); env->DeleteGlobalRef(s_input_detector_class); + env->DeleteGlobalRef(s_permission_handler_class); } } diff --git a/Source/Android/jni/AndroidCommon/IDCache.h b/Source/Android/jni/AndroidCommon/IDCache.h index 0b01d14b42..3cc8c11b5e 100644 --- a/Source/Android/jni/AndroidCommon/IDCache.h +++ b/Source/Android/jni/AndroidCommon/IDCache.h @@ -115,6 +115,10 @@ jmethodID GetCoreDeviceControlConstructor(); jclass GetInputDetectorClass(); jfieldID GetInputDetectorPointer(); +jclass GetPermissionHandlerClass(); +jmethodID GetPermissionHandlerHasRecordAudioPermission(); +jmethodID GetPermissionHandlerRequestRecordAudioPermission(); + jmethodID GetRunnableRun(); } // namespace IDCache diff --git a/Source/Core/Core/IOS/USB/Emulated/Microphone.cpp b/Source/Core/Core/IOS/USB/Emulated/Microphone.cpp index 67f8e4ecf8..2e10be897d 100644 --- a/Source/Core/Core/IOS/USB/Emulated/Microphone.cpp +++ b/Source/Core/Core/IOS/USB/Emulated/Microphone.cpp @@ -21,6 +21,10 @@ #include #endif +#ifdef ANDROID +#include "jni/AndroidCommon/IDCache.h" +#endif + namespace IOS::HLE::USB { Microphone::Microphone(const WiiSpeakState& sampler) : m_sampler(sampler) @@ -118,6 +122,19 @@ void Microphone::StreamStart() Common::ScopeGuard sync_event_guard([&sync_event] { sync_event.Set(); }); #endif +#ifdef ANDROID + JNIEnv* env = IDCache::GetEnvForThread(); + if (jboolean result = env->CallStaticBooleanMethod( + IDCache::GetPermissionHandlerClass(), + IDCache::GetPermissionHandlerHasRecordAudioPermission(), nullptr); + result == JNI_FALSE) + { + env->CallStaticVoidMethod(IDCache::GetPermissionHandlerClass(), + IDCache::GetPermissionHandlerRequestRecordAudioPermission(), + nullptr); + } +#endif + cubeb_stream_params params{}; params.format = CUBEB_SAMPLE_S16LE; params.rate = SAMPLING_RATE; From 7520f427a9217fb3ce8afbc4ee7bdaaf2c92946e Mon Sep 17 00:00:00 2001 From: Sepalani Date: Sun, 25 Aug 2024 00:06:50 +0400 Subject: [PATCH 12/22] CubebUtils: Add COM helper class --- Source/Core/AudioCommon/CubebUtils.cpp | 75 ++++++++++++++- Source/Core/AudioCommon/CubebUtils.h | 23 +++++ .../Core/Core/IOS/USB/Emulated/Microphone.cpp | 91 +++---------------- .../Core/Core/IOS/USB/Emulated/Microphone.h | 9 +- 4 files changed, 107 insertions(+), 91 deletions(-) diff --git a/Source/Core/AudioCommon/CubebUtils.cpp b/Source/Core/AudioCommon/CubebUtils.cpp index cd873d7f3e..9910234a80 100644 --- a/Source/Core/AudioCommon/CubebUtils.cpp +++ b/Source/Core/AudioCommon/CubebUtils.cpp @@ -16,6 +16,13 @@ #include +#ifdef _WIN32 +#include + +#include "Common/Event.h" +#include "Common/ScopeGuard.h" +#endif + static ptrdiff_t s_path_cutoff_point = 0; static void LogCallback(const char* format, ...) @@ -49,7 +56,9 @@ static void DestroyContext(cubeb* ctx) } } -std::shared_ptr CubebUtils::GetContext() +namespace CubebUtils +{ +std::shared_ptr GetContext() { static std::weak_ptr weak; @@ -83,12 +92,12 @@ std::shared_ptr CubebUtils::GetContext() return shared; } -std::vector> CubebUtils::ListInputDevices() +std::vector> ListInputDevices() { std::vector> devices; cubeb_device_collection collection; - auto cubeb_ctx = CubebUtils::GetContext(); + auto cubeb_ctx = GetContext(); int r = cubeb_enumerate_devices(cubeb_ctx.get(), CUBEB_DEVICE_TYPE_INPUT, &collection); if (r != CUBEB_OK) @@ -135,13 +144,13 @@ std::vector> CubebUtils::ListInputDevices() return devices; } -cubeb_devid CubebUtils::GetInputDeviceById(std::string_view id) +cubeb_devid GetInputDeviceById(std::string_view id) { if (id.empty()) return nullptr; cubeb_device_collection collection; - auto cubeb_ctx = CubebUtils::GetContext(); + auto cubeb_ctx = GetContext(); int r = cubeb_enumerate_devices(cubeb_ctx.get(), CUBEB_DEVICE_TYPE_INPUT, &collection); if (r != CUBEB_OK) @@ -169,3 +178,59 @@ cubeb_devid CubebUtils::GetInputDeviceById(std::string_view id) return device_id; } + +CoInitSyncWorker::CoInitSyncWorker([[maybe_unused]] std::string_view worker_name) +#ifdef _WIN32 + : m_work_queue +{ + worker_name, [](const CoInitSyncWorker::FunctionType& f) { f(); } +} +#endif +{ +#ifdef _WIN32 + Common::Event sync_event; + m_work_queue.EmplaceItem([this, &sync_event] { + Common::ScopeGuard sync_event_guard([&sync_event] { sync_event.Set(); }); + auto result = ::CoInitializeEx(nullptr, COINIT_MULTITHREADED | COINIT_DISABLE_OLE1DDE); + m_coinit_success = result == S_OK; + m_should_couninit = result == S_OK || result == S_FALSE; + }); + sync_event.Wait(); +#endif +} + +CoInitSyncWorker::~CoInitSyncWorker() +{ +#ifdef _WIN32 + if (m_should_couninit) + { + Common::Event sync_event; + m_work_queue.EmplaceItem([this, &sync_event] { + Common::ScopeGuard sync_event_guard([&sync_event] { sync_event.Set(); }); + m_should_couninit = false; + CoUninitialize(); + }); + sync_event.Wait(); + } + m_coinit_success = false; +#endif +} + +bool CoInitSyncWorker::Execute(FunctionType f) +{ +#ifdef _WIN32 + if (!m_coinit_success) + return false; + + Common::Event sync_event; + m_work_queue.EmplaceItem([&sync_event, f] { + Common::ScopeGuard sync_event_guard([&sync_event] { sync_event.Set(); }); +#endif + f(); +#ifdef _WIN32 + }); + sync_event.Wait(); +#endif + return true; +} +} // namespace CubebUtils diff --git a/Source/Core/AudioCommon/CubebUtils.h b/Source/Core/AudioCommon/CubebUtils.h index 943ea83359..ac60fd434f 100644 --- a/Source/Core/AudioCommon/CubebUtils.h +++ b/Source/Core/AudioCommon/CubebUtils.h @@ -10,6 +10,10 @@ #include #include +#ifdef _WIN32 +#include "Common/WorkQueueThread.h" +#endif + struct cubeb; namespace CubebUtils @@ -17,4 +21,23 @@ namespace CubebUtils std::shared_ptr GetContext(); std::vector> ListInputDevices(); const void* GetInputDeviceById(std::string_view id); + +// Helper used to handle Windows COM library for cubeb WASAPI backend +class CoInitSyncWorker +{ +public: + using FunctionType = std::function; + + CoInitSyncWorker(std::string_view worker_name); + ~CoInitSyncWorker(); + + bool Execute(FunctionType f); + +#ifdef _WIN32 +private: + Common::WorkQueueThread m_work_queue; + bool m_coinit_success = false; + bool m_should_couninit = false; +#endif +}; } // namespace CubebUtils diff --git a/Source/Core/Core/IOS/USB/Emulated/Microphone.cpp b/Source/Core/Core/IOS/USB/Emulated/Microphone.cpp index 2e10be897d..5a0cf12587 100644 --- a/Source/Core/Core/IOS/USB/Emulated/Microphone.cpp +++ b/Source/Core/Core/IOS/USB/Emulated/Microphone.cpp @@ -8,9 +8,7 @@ #include #include "AudioCommon/CubebUtils.h" -#include "Common/Event.h" #include "Common/Logging/Log.h" -#include "Common/ScopeGuard.h" #include "Common/Swap.h" #include "Core/Config/MainSettings.h" #include "Core/Core.h" @@ -29,56 +27,21 @@ namespace IOS::HLE::USB { Microphone::Microphone(const WiiSpeakState& sampler) : m_sampler(sampler) { -#ifdef _WIN32 - Common::Event sync_event; - m_work_queue.EmplaceItem([this, &sync_event] { - Common::ScopeGuard sync_event_guard([&sync_event] { sync_event.Set(); }); - auto result = ::CoInitializeEx(nullptr, COINIT_MULTITHREADED | COINIT_DISABLE_OLE1DDE); - m_coinit_success = result == S_OK; - m_should_couninit = result == S_OK || result == S_FALSE; - }); - sync_event.Wait(); -#endif - StreamInit(); } Microphone::~Microphone() { StreamTerminate(); - -#ifdef _WIN32 - if (m_should_couninit) - { - Common::Event sync_event; - m_work_queue.EmplaceItem([this, &sync_event] { - Common::ScopeGuard sync_event_guard([&sync_event] { sync_event.Set(); }); - m_should_couninit = false; - CoUninitialize(); - }); - sync_event.Wait(); - } - m_coinit_success = false; -#endif } void Microphone::StreamInit() { -#ifdef _WIN32 - if (!m_coinit_success) + if (!m_worker.Execute([this] { m_cubeb_ctx = CubebUtils::GetContext(); })) { ERROR_LOG_FMT(IOS_USB, "Failed to init Wii Speak stream"); return; } - Common::Event sync_event; - m_work_queue.EmplaceItem([this, &sync_event] { - Common::ScopeGuard sync_event_guard([&sync_event] { sync_event.Set(); }); -#endif - m_cubeb_ctx = CubebUtils::GetContext(); -#ifdef _WIN32 - }); - sync_event.Wait(); -#endif // TODO: Not here but rather inside the WiiSpeak device if possible? StreamStart(); @@ -89,20 +52,7 @@ void Microphone::StreamTerminate() StreamStop(); if (m_cubeb_ctx) - { -#ifdef _WIN32 - if (!m_coinit_success) - return; - Common::Event sync_event; - m_work_queue.EmplaceItem([this, &sync_event] { - Common::ScopeGuard sync_event_guard([&sync_event] { sync_event.Set(); }); -#endif - m_cubeb_ctx.reset(); -#ifdef _WIN32 - }); - sync_event.Wait(); -#endif - } + m_worker.Execute([this] { m_cubeb_ctx.reset(); }); } static void state_callback(cubeb_stream* stream, void* user_data, cubeb_state state) @@ -114,14 +64,7 @@ void Microphone::StreamStart() if (!m_cubeb_ctx) return; -#ifdef _WIN32 - if (!m_coinit_success) - return; - Common::Event sync_event; - m_work_queue.EmplaceItem([this, &sync_event] { - Common::ScopeGuard sync_event_guard([&sync_event] { sync_event.Set(); }); -#endif - + m_worker.Execute([this] { #ifdef ANDROID JNIEnv* env = IDCache::GetEnvForThread(); if (jboolean result = env->CallStaticBooleanMethod( @@ -165,30 +108,20 @@ void Microphone::StreamStart() } INFO_LOG_FMT(IOS_USB, "started cubeb stream"); -#ifdef _WIN32 }); - sync_event.Wait(); -#endif } void Microphone::StreamStop() { - if (m_cubeb_stream) - { -#ifdef _WIN32 - Common::Event sync_event; - m_work_queue.EmplaceItem([this, &sync_event] { - Common::ScopeGuard sync_event_guard([&sync_event] { sync_event.Set(); }); -#endif - if (cubeb_stream_stop(m_cubeb_stream) != CUBEB_OK) - ERROR_LOG_FMT(IOS_USB, "Error stopping cubeb stream"); - cubeb_stream_destroy(m_cubeb_stream); - m_cubeb_stream = nullptr; -#ifdef _WIN32 - }); - sync_event.Wait(); -#endif - } + if (!m_cubeb_stream) + return; + + m_worker.Execute([this] { + if (cubeb_stream_stop(m_cubeb_stream) != CUBEB_OK) + ERROR_LOG_FMT(IOS_USB, "Error stopping cubeb stream"); + cubeb_stream_destroy(m_cubeb_stream); + m_cubeb_stream = nullptr; + }); } long Microphone::DataCallback(cubeb_stream* stream, void* user_data, const void* input_buffer, diff --git a/Source/Core/Core/IOS/USB/Emulated/Microphone.h b/Source/Core/Core/IOS/USB/Emulated/Microphone.h index af7d7a8362..f3b476bc1f 100644 --- a/Source/Core/Core/IOS/USB/Emulated/Microphone.h +++ b/Source/Core/Core/IOS/USB/Emulated/Microphone.h @@ -7,8 +7,8 @@ #include #include +#include "AudioCommon/CubebUtils.h" #include "Common/CommonTypes.h" -#include "Common/WorkQueueThread.h" struct cubeb; struct cubeb_stream; @@ -52,11 +52,6 @@ private: const WiiSpeakState& m_sampler; -#ifdef _WIN32 - Common::WorkQueueThread> m_work_queue{ - "Wii Speak Worker", [](const std::function& func) { func(); }}; - bool m_coinit_success = false; - bool m_should_couninit = false; -#endif + CubebUtils::CoInitSyncWorker m_worker{"Wii Speak Worker"}; }; } // namespace IOS::HLE::USB From 820cf5bcd856c4dccd3a96770bba56ed5fe97d1a Mon Sep 17 00:00:00 2001 From: Sepalani Date: Sun, 1 Sep 2024 12:31:26 +0400 Subject: [PATCH 13/22] Android: Fix PermissionsHandler activity detection --- .../dolphinemu/features/settings/ui/SettingsAdapter.kt | 4 ++++ .../settings/ui/viewholder/SwitchSettingViewHolder.kt | 4 ++-- .../org/dolphinemu/dolphinemu/utils/ActivityTracker.kt | 10 +++++----- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsAdapter.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsAdapter.kt index bcf68e67ec..6a8132d32d 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsAdapter.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsAdapter.kt @@ -15,6 +15,7 @@ import android.widget.TextView import androidx.annotation.ColorInt import androidx.appcompat.app.AlertDialog import androidx.core.content.ContextCompat +import androidx.fragment.app.FragmentActivity import androidx.recyclerview.widget.RecyclerView import com.google.android.material.color.MaterialColors import com.google.android.material.datepicker.CalendarConstraints @@ -59,6 +60,9 @@ class SettingsAdapter( val settings: Settings? get() = fragmentView.settings + val fragmentActivity: FragmentActivity + get() = fragmentView.fragmentActivity + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SettingViewHolder { val inflater = LayoutInflater.from(parent.context) return when (viewType) { diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/viewholder/SwitchSettingViewHolder.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/viewholder/SwitchSettingViewHolder.kt index 48034dae41..ddcf1d39b5 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/viewholder/SwitchSettingViewHolder.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/viewholder/SwitchSettingViewHolder.kt @@ -61,8 +61,8 @@ class SwitchSettingViewHolder( if (setting.setting === BooleanSetting.MAIN_EMULATE_WII_SPEAK && isChecked) { if (!PermissionsHandler.hasRecordAudioPermission(itemView.context)) { - val settingsActivity = itemView.context as Activity - PermissionsHandler.requestRecordAudioPermission(settingsActivity) + val currentActivity = adapter.fragmentActivity as Activity + PermissionsHandler.requestRecordAudioPermission(currentActivity) } } diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/ActivityTracker.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/ActivityTracker.kt index 0b4079bebf..3d56dcd1ba 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/ActivityTracker.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/ActivityTracker.kt @@ -31,6 +31,7 @@ class ActivityTracker : ActivityLifecycleCallbacks { } override fun onActivityResumed(activity: Activity) { + currentActivity = activity resumedActivities.add(activity) if (!backgroundExecutionAllowed && !resumedActivities.isEmpty()) { backgroundExecutionAllowed = true @@ -39,6 +40,9 @@ class ActivityTracker : ActivityLifecycleCallbacks { } override fun onActivityPaused(activity: Activity) { + if (currentActivity === activity) { + currentActivity = null + } resumedActivities.remove(activity) if (backgroundExecutionAllowed && resumedActivities.isEmpty()) { backgroundExecutionAllowed = false @@ -54,11 +58,7 @@ class ActivityTracker : ActivityLifecycleCallbacks { override fun onActivitySaveInstanceState(activity: Activity, bundle: Bundle) {} - override fun onActivityDestroyed(activity: Activity) { - if (currentActivity === activity) { - currentActivity = null - } - } + override fun onActivityDestroyed(activity: Activity) {} companion object { @JvmStatic From f95a357cb53c0316006c1beb416402e6201d0dc0 Mon Sep 17 00:00:00 2001 From: Sepalani Date: Sun, 1 Sep 2024 14:00:56 +0400 Subject: [PATCH 14/22] Config: Rename MAIN_WII_SPEAK_CONNECTED to MAIN_WII_SPEAK_MUTED --- .../features/settings/model/BooleanSetting.kt | 6 +++--- .../settings/ui/SettingsFragmentPresenter.kt | 6 +++--- .../Android/app/src/main/res/values/strings.xml | 2 +- Source/Core/Core/Config/MainSettings.cpp | 3 +-- Source/Core/Core/Config/MainSettings.h | 2 +- Source/Core/Core/IOS/USB/Emulated/Microphone.cpp | 2 +- Source/Core/Core/IOS/USB/Emulated/WiiSpeak.cpp | 15 --------------- Source/Core/Core/IOS/USB/Emulated/WiiSpeak.h | 1 - .../Core/DolphinQt/EmulatedUSB/WiiSpeakWindow.cpp | 12 ++++++------ 9 files changed, 16 insertions(+), 33 deletions(-) diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/BooleanSetting.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/BooleanSetting.kt index b2d9ab1b8b..77c5792b27 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/BooleanSetting.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/BooleanSetting.kt @@ -238,11 +238,11 @@ enum class BooleanSetting( "EmulateWiiSpeak", false ), - MAIN_WII_SPEAK_CONNECTED( + MAIN_WII_SPEAK_MUTED( Settings.FILE_DOLPHIN, Settings.SECTION_EMULATED_USB_DEVICES, - "WiiSpeakConnected", - false + "WiiSpeakMuted", + true ), MAIN_SHOW_GAME_TITLES( Settings.FILE_DOLPHIN, diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentPresenter.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentPresenter.kt index b4427cf99c..b961fed382 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentPresenter.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentPresenter.kt @@ -901,10 +901,10 @@ class SettingsFragmentPresenter( ) ) sl.add( - InvertedSwitchSetting( + SwitchSetting( context, - BooleanSetting.MAIN_WII_SPEAK_CONNECTED, - R.string.disconnect_wii_speak, + BooleanSetting.MAIN_WII_SPEAK_MUTED, + R.string.mute_wii_speak, 0 ) ) diff --git a/Source/Android/app/src/main/res/values/strings.xml b/Source/Android/app/src/main/res/values/strings.xml index f063c937c7..7676a4ccb5 100644 --- a/Source/Android/app/src/main/res/values/strings.xml +++ b/Source/Android/app/src/main/res/values/strings.xml @@ -942,7 +942,7 @@ It can efficiently compress both junk data and encrypted Wii data. Please select a compatible figure file Wii Speak - Mute Wii Speak + Mute Wii Speak Missing Microphone Permission Wii Speak emulation requires microphone permission. You might need to restart the game for the permission to be effective. diff --git a/Source/Core/Core/Config/MainSettings.cpp b/Source/Core/Core/Config/MainSettings.cpp index 6555f20f17..6e782fdb4e 100644 --- a/Source/Core/Core/Config/MainSettings.cpp +++ b/Source/Core/Core/Config/MainSettings.cpp @@ -600,8 +600,7 @@ const Info MAIN_EMULATE_WII_SPEAK{{System::Main, "EmulatedUSBDevices", "Em const Info MAIN_WII_SPEAK_MICROPHONE{ {System::Main, "EmulatedUSBDevices", "WiiSpeakMicrophone"}, ""}; -const Info MAIN_WII_SPEAK_CONNECTED{{System::Main, "EmulatedUSBDevices", "WiiSpeakConnected"}, - false}; +const Info MAIN_WII_SPEAK_MUTED{{System::Main, "EmulatedUSBDevices", "WiiSpeakMuted"}, true}; // 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. diff --git a/Source/Core/Core/Config/MainSettings.h b/Source/Core/Core/Config/MainSettings.h index 057de249b1..add214340b 100644 --- a/Source/Core/Core/Config/MainSettings.h +++ b/Source/Core/Core/Config/MainSettings.h @@ -364,7 +364,7 @@ extern const Info MAIN_EMULATE_SKYLANDER_PORTAL; extern const Info MAIN_EMULATE_INFINITY_BASE; extern const Info MAIN_EMULATE_WII_SPEAK; extern const Info MAIN_WII_SPEAK_MICROPHONE; -extern const Info MAIN_WII_SPEAK_CONNECTED; +extern const Info MAIN_WII_SPEAK_MUTED; // GameCube path utility functions diff --git a/Source/Core/Core/IOS/USB/Emulated/Microphone.cpp b/Source/Core/Core/IOS/USB/Emulated/Microphone.cpp index 5a0cf12587..e98af9206b 100644 --- a/Source/Core/Core/IOS/USB/Emulated/Microphone.cpp +++ b/Source/Core/Core/IOS/USB/Emulated/Microphone.cpp @@ -185,7 +185,7 @@ u16 Microphone::ReadIntoBuffer(u8* ptr, u32 size) bool Microphone::HasData() const { - return m_samples_avail > 0 && Config::Get(Config::MAIN_WII_SPEAK_CONNECTED); + return m_samples_avail > 0 && !Config::Get(Config::MAIN_WII_SPEAK_MUTED); } const WiiSpeakState& Microphone::GetSampler() const diff --git a/Source/Core/Core/IOS/USB/Emulated/WiiSpeak.cpp b/Source/Core/Core/IOS/USB/Emulated/WiiSpeak.cpp index 15c145a80f..3252c47061 100644 --- a/Source/Core/Core/IOS/USB/Emulated/WiiSpeak.cpp +++ b/Source/Core/Core/IOS/USB/Emulated/WiiSpeak.cpp @@ -5,7 +5,6 @@ #include -#include "Core/Config/MainSettings.h" #include "Core/HW/Memmap.h" #include "Core/System.h" @@ -95,12 +94,6 @@ int WiiSpeak::SubmitTransfer(std::unique_ptr cmd) m_vid, m_pid, m_active_interface, cmd->request_type, cmd->request, cmd->value, cmd->index, cmd->length); - // Without a proper way to reconnect the emulated Wii Speak, - // this error after being raised prevents some games to use the microphone later. - // - // if (!IsMicrophoneConnected()) - // return IPC_ENOENT; - switch (cmd->request_type << 8 | cmd->request) { case USBHDR(DIR_DEVICE2HOST, TYPE_STANDARD, REC_INTERFACE, REQUEST_GET_INTERFACE): @@ -163,9 +156,6 @@ int WiiSpeak::SubmitTransfer(std::unique_ptr cmd) int WiiSpeak::SubmitTransfer(std::unique_ptr cmd) { - // if (!IsMicrophoneConnected()) - // return IPC_ENOENT; - auto& system = cmd->GetEmulationKernel().GetSystem(); auto& memory = system.GetMemory(); @@ -405,9 +395,4 @@ void WiiSpeak::GetRegister(const std::unique_ptr& cmd) const break; } } - -bool WiiSpeak::IsMicrophoneConnected() const -{ - return Config::Get(Config::MAIN_WII_SPEAK_CONNECTED); -} } // namespace IOS::HLE::USB diff --git a/Source/Core/Core/IOS/USB/Emulated/WiiSpeak.h b/Source/Core/Core/IOS/USB/Emulated/WiiSpeak.h index 7eafee155a..57e2959196 100644 --- a/Source/Core/Core/IOS/USB/Emulated/WiiSpeak.h +++ b/Source/Core/Core/IOS/USB/Emulated/WiiSpeak.h @@ -74,7 +74,6 @@ private: void GetRegister(const std::unique_ptr& cmd) const; void SetRegister(const std::unique_ptr& cmd); - bool IsMicrophoneConnected() const; const u16 m_vid = 0x057E; const u16 m_pid = 0x0308; diff --git a/Source/Core/DolphinQt/EmulatedUSB/WiiSpeakWindow.cpp b/Source/Core/DolphinQt/EmulatedUSB/WiiSpeakWindow.cpp index 3d644827b1..b5b66a100e 100644 --- a/Source/Core/DolphinQt/EmulatedUSB/WiiSpeakWindow.cpp +++ b/Source/Core/DolphinQt/EmulatedUSB/WiiSpeakWindow.cpp @@ -52,11 +52,11 @@ void WiiSpeakWindow::CreateMainWindow() m_config_group = new QGroupBox(tr("Microphone Configuration")); auto* config_layout = new QHBoxLayout(); - auto checkbox_mic_connected = new QCheckBox(tr("Connect"), this); - checkbox_mic_connected->setChecked(Config::Get(Config::MAIN_WII_SPEAK_CONNECTED)); - connect(checkbox_mic_connected, &QCheckBox::toggled, this, + auto checkbox_mic_muted = new QCheckBox(tr("Mute"), this); + checkbox_mic_muted->setChecked(Config::Get(Config::MAIN_WII_SPEAK_MUTED)); + connect(checkbox_mic_muted, &QCheckBox::toggled, this, &WiiSpeakWindow::SetWiiSpeakConnectionState); - config_layout->addWidget(checkbox_mic_connected); + config_layout->addWidget(checkbox_mic_muted); m_combobox_microphones = new QComboBox(); m_combobox_microphones->addItem(QLatin1String("(%1)").arg(tr("Autodetect preferred microphone")), @@ -86,9 +86,9 @@ void WiiSpeakWindow::EmulateWiiSpeak(bool emulate) m_config_group->setVisible(emulate); } -void WiiSpeakWindow::SetWiiSpeakConnectionState(bool connected) +void WiiSpeakWindow::SetWiiSpeakConnectionState(bool muted) { - Config::SetBaseOrCurrent(Config::MAIN_WII_SPEAK_CONNECTED, connected); + Config::SetBaseOrCurrent(Config::MAIN_WII_SPEAK_MUTED, muted); } void WiiSpeakWindow::OnEmulationStateChanged(Core::State state) From a9f712c80b102dde287ad0d113c73bdee9d107fd Mon Sep 17 00:00:00 2001 From: Sepalani Date: Sun, 1 Sep 2024 14:14:18 +0400 Subject: [PATCH 15/22] WiiSpeakWindow: Don't hide QGroupBox --- .../Core/DolphinQt/EmulatedUSB/WiiSpeakWindow.cpp | 13 ++++++++----- Source/Core/DolphinQt/EmulatedUSB/WiiSpeakWindow.h | 2 -- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/Source/Core/DolphinQt/EmulatedUSB/WiiSpeakWindow.cpp b/Source/Core/DolphinQt/EmulatedUSB/WiiSpeakWindow.cpp index b5b66a100e..31c482537d 100644 --- a/Source/Core/DolphinQt/EmulatedUSB/WiiSpeakWindow.cpp +++ b/Source/Core/DolphinQt/EmulatedUSB/WiiSpeakWindow.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include @@ -38,6 +39,10 @@ WiiSpeakWindow::~WiiSpeakWindow() = default; void WiiSpeakWindow::CreateMainWindow() { auto* main_layout = new QVBoxLayout(); + auto* label = new QLabel(); + label->setText(QStringLiteral("
%1
") + .arg(tr("Some settings cannot be changed when emulation is running."))); + main_layout->addWidget(label); auto* checkbox_group = new QGroupBox(); auto* checkbox_layout = new QHBoxLayout(); @@ -49,7 +54,7 @@ void WiiSpeakWindow::CreateMainWindow() checkbox_group->setLayout(checkbox_layout); main_layout->addWidget(checkbox_group); - m_config_group = new QGroupBox(tr("Microphone Configuration")); + auto* config_group = new QGroupBox(tr("Microphone Configuration")); auto* config_layout = new QHBoxLayout(); auto checkbox_mic_muted = new QCheckBox(tr("Mute"), this); @@ -73,9 +78,8 @@ void WiiSpeakWindow::CreateMainWindow() m_combobox_microphones->setCurrentIndex(m_combobox_microphones->findData(current_device_id)); config_layout->addWidget(m_combobox_microphones); - m_config_group->setLayout(config_layout); - m_config_group->setVisible(Config::Get(Config::MAIN_EMULATE_WII_SPEAK)); - main_layout->addWidget(m_config_group); + config_group->setLayout(config_layout); + main_layout->addWidget(config_group); setLayout(main_layout); } @@ -83,7 +87,6 @@ void WiiSpeakWindow::CreateMainWindow() void WiiSpeakWindow::EmulateWiiSpeak(bool emulate) { Config::SetBaseOrCurrent(Config::MAIN_EMULATE_WII_SPEAK, emulate); - m_config_group->setVisible(emulate); } void WiiSpeakWindow::SetWiiSpeakConnectionState(bool muted) diff --git a/Source/Core/DolphinQt/EmulatedUSB/WiiSpeakWindow.h b/Source/Core/DolphinQt/EmulatedUSB/WiiSpeakWindow.h index 1f49796573..2da4887cc9 100644 --- a/Source/Core/DolphinQt/EmulatedUSB/WiiSpeakWindow.h +++ b/Source/Core/DolphinQt/EmulatedUSB/WiiSpeakWindow.h @@ -9,7 +9,6 @@ class QCheckBox; class QComboBox; -class QGroupBox; class WiiSpeakWindow : public QWidget { @@ -27,5 +26,4 @@ private: QCheckBox* m_checkbox_enabled; QComboBox* m_combobox_microphones; - QGroupBox* m_config_group; }; From 65a09924d9e6f47cfc31de9d5c85cf095f76db92 Mon Sep 17 00:00:00 2001 From: Sepalani Date: Sun, 29 Sep 2024 00:16:49 +0400 Subject: [PATCH 16/22] IOS/USB: Implement Wii Speak REQUEST_SET_INTERFACE command Based on LibusbDevice::SubmitTransfer code --- .../Core/Core/IOS/USB/Emulated/WiiSpeak.cpp | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/Source/Core/Core/IOS/USB/Emulated/WiiSpeak.cpp b/Source/Core/Core/IOS/USB/Emulated/WiiSpeak.cpp index 3252c47061..58eb297f3e 100644 --- a/Source/Core/Core/IOS/USB/Emulated/WiiSpeak.cpp +++ b/Source/Core/Core/IOS/USB/Emulated/WiiSpeak.cpp @@ -103,6 +103,25 @@ int WiiSpeak::SubmitTransfer(std::unique_ptr cmd) cmd->ScheduleTransferCompletion(1, 100); break; } + case USBHDR(DIR_HOST2DEVICE, TYPE_STANDARD, REC_INTERFACE, REQUEST_SET_INTERFACE): + { + INFO_LOG_FMT(IOS_USB, "[{:04x}:{:04x} {}] REQUEST_SET_INTERFACE index={:04x} value={:04x}", + m_vid, m_pid, 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_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 USBHDR(DIR_HOST2DEVICE, TYPE_VENDOR, REC_INTERFACE, 0): { init = false; From 82a03ca73bca537379821156aadf775ca9309275 Mon Sep 17 00:00:00 2001 From: Sepalani Date: Tue, 1 Oct 2024 14:52:55 +0400 Subject: [PATCH 17/22] IOS/USB: Skip data when HLE Wii Speak is muted --- Source/Core/Core/IOS/USB/Emulated/Microphone.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Source/Core/Core/IOS/USB/Emulated/Microphone.cpp b/Source/Core/Core/IOS/USB/Emulated/Microphone.cpp index e98af9206b..dc128d60db 100644 --- a/Source/Core/Core/IOS/USB/Emulated/Microphone.cpp +++ b/Source/Core/Core/IOS/USB/Emulated/Microphone.cpp @@ -131,6 +131,11 @@ long Microphone::DataCallback(cubeb_stream* stream, void* user_data, const void* if (Core::GetState(Core::System::GetInstance()) != Core::State::Running) return nframes; + // 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)) + return nframes; + auto* mic = static_cast(user_data); const auto& sampler = mic->GetSampler(); @@ -185,7 +190,7 @@ u16 Microphone::ReadIntoBuffer(u8* ptr, u32 size) bool Microphone::HasData() const { - return m_samples_avail > 0 && !Config::Get(Config::MAIN_WII_SPEAK_MUTED); + return m_samples_avail > 0; } const WiiSpeakState& Microphone::GetSampler() const From f7eadc1f11f990bd9b7815b0e4544118be716f9a Mon Sep 17 00:00:00 2001 From: Sepalani Date: Wed, 2 Oct 2024 14:58:04 +0400 Subject: [PATCH 18/22] IOS/USB: Lock microphone buffer less frequently --- Source/Core/Core/IOS/USB/Emulated/Microphone.cpp | 4 ++-- Source/Core/Core/IOS/USB/Emulated/Microphone.h | 2 +- Source/Core/Core/IOS/USB/Emulated/WiiSpeak.cpp | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Source/Core/Core/IOS/USB/Emulated/Microphone.cpp b/Source/Core/Core/IOS/USB/Emulated/Microphone.cpp index dc128d60db..400029f695 100644 --- a/Source/Core/Core/IOS/USB/Emulated/Microphone.cpp +++ b/Source/Core/Core/IOS/USB/Emulated/Microphone.cpp @@ -188,9 +188,9 @@ u16 Microphone::ReadIntoBuffer(u8* ptr, u32 size) return static_cast(ptr - begin); } -bool Microphone::HasData() const +bool Microphone::HasData(u32 sample_count = BUFF_SIZE_SAMPLES) const { - return m_samples_avail > 0; + return m_samples_avail >= sample_count; } const WiiSpeakState& Microphone::GetSampler() const diff --git a/Source/Core/Core/IOS/USB/Emulated/Microphone.h b/Source/Core/Core/IOS/USB/Emulated/Microphone.h index f3b476bc1f..30af6b09c5 100644 --- a/Source/Core/Core/IOS/USB/Emulated/Microphone.h +++ b/Source/Core/Core/IOS/USB/Emulated/Microphone.h @@ -23,7 +23,7 @@ public: Microphone(const WiiSpeakState& sampler); ~Microphone(); - bool HasData() const; + bool HasData(u32 sample_count) const; u16 ReadIntoBuffer(u8* ptr, u32 size); const WiiSpeakState& GetSampler() const; diff --git a/Source/Core/Core/IOS/USB/Emulated/WiiSpeak.cpp b/Source/Core/Core/IOS/USB/Emulated/WiiSpeak.cpp index 58eb297f3e..0456a328eb 100644 --- a/Source/Core/Core/IOS/USB/Emulated/WiiSpeak.cpp +++ b/Source/Core/Core/IOS/USB/Emulated/WiiSpeak.cpp @@ -191,7 +191,7 @@ int WiiSpeak::SubmitTransfer(std::unique_ptr cmd) { // Transfer: Wii Speak -> Wii u16 size = 0; - if (m_microphone && m_microphone->HasData()) + 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++) { From 5021cc0c1e7a8fe007962a46341e10beb587b6b4 Mon Sep 17 00:00:00 2001 From: Sepalani Date: Sat, 16 Nov 2024 15:50:06 +0400 Subject: [PATCH 19/22] IOS/USB: Add more Wii Speak log messages --- Source/Core/Core/IOS/USB/Emulated/WiiSpeak.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Source/Core/Core/IOS/USB/Emulated/WiiSpeak.cpp b/Source/Core/Core/IOS/USB/Emulated/WiiSpeak.cpp index 0456a328eb..720640e0e6 100644 --- a/Source/Core/Core/IOS/USB/Emulated/WiiSpeak.cpp +++ b/Source/Core/Core/IOS/USB/Emulated/WiiSpeak.cpp @@ -250,6 +250,8 @@ void WiiSpeak::SetRegister(const std::unique_ptr& cmd) m_sampler.sample_on = !!arg1; break; case SAMPLER_FREQ: + WARN_LOG_FMT(IOS_USB, "Wii Speak SAMPLER_FREQ set (arg1={:04x}, arg2={:04x}) not implemented", + arg1, arg2); switch (arg1) { case FREQ_8KHZ: @@ -271,6 +273,8 @@ void WiiSpeak::SetRegister(const std::unique_ptr& cmd) } break; case SAMPLER_GAIN: + WARN_LOG_FMT(IOS_USB, "Wii Speak SAMPLER_GAIN set (arg1={:04x}, arg2={:04x}) not implemented", + arg1, arg2); switch (arg1 & ~0x300) { case GAIN_00dB: From 61e539ca339c910c89def3ef5b3dc58a70a30e8c Mon Sep 17 00:00:00 2001 From: Sepalani Date: Wed, 2 Oct 2024 13:53:02 +0400 Subject: [PATCH 20/22] IOS/USB: Implement a bare-bones Wii Speak loudness level Add a volume modifier to the UI which relies on gain. --- Source/Core/Core/Config/MainSettings.cpp | 2 + Source/Core/Core/Config/MainSettings.h | 1 + .../Core/Core/IOS/USB/Emulated/Microphone.cpp | 151 +++++++++++++++++- .../Core/Core/IOS/USB/Emulated/Microphone.h | 67 ++++++++ .../Core/Core/IOS/USB/Emulated/WiiSpeak.cpp | 4 +- .../DolphinQt/EmulatedUSB/WiiSpeakWindow.cpp | 29 ++++ 6 files changed, 249 insertions(+), 5 deletions(-) diff --git a/Source/Core/Core/Config/MainSettings.cpp b/Source/Core/Core/Config/MainSettings.cpp index 6e782fdb4e..f1b14be930 100644 --- a/Source/Core/Core/Config/MainSettings.cpp +++ b/Source/Core/Core/Config/MainSettings.cpp @@ -601,6 +601,8 @@ const Info MAIN_WII_SPEAK_MICROPHONE{ {System::Main, "EmulatedUSBDevices", "WiiSpeakMicrophone"}, ""}; const Info MAIN_WII_SPEAK_MUTED{{System::Main, "EmulatedUSBDevices", "WiiSpeakMuted"}, true}; +const Info MAIN_WII_SPEAK_VOLUME_MODIFIER{ + {System::Main, "EmulatedUSBDevices", "WiiSpeakVolumeModifier"}, 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. diff --git a/Source/Core/Core/Config/MainSettings.h b/Source/Core/Core/Config/MainSettings.h index add214340b..1944b90f87 100644 --- a/Source/Core/Core/Config/MainSettings.h +++ b/Source/Core/Core/Config/MainSettings.h @@ -365,6 +365,7 @@ extern const Info MAIN_EMULATE_INFINITY_BASE; extern const Info MAIN_EMULATE_WII_SPEAK; extern const Info MAIN_WII_SPEAK_MICROPHONE; extern const Info MAIN_WII_SPEAK_MUTED; +extern const Info MAIN_WII_SPEAK_VOLUME_MODIFIER; // GameCube path utility functions diff --git a/Source/Core/Core/IOS/USB/Emulated/Microphone.cpp b/Source/Core/Core/IOS/USB/Emulated/Microphone.cpp index 400029f695..eaa9d4f5b1 100644 --- a/Source/Core/Core/IOS/USB/Emulated/Microphone.cpp +++ b/Source/Core/Core/IOS/USB/Emulated/Microphone.cpp @@ -3,12 +3,13 @@ #include "Core/IOS/USB/Emulated/Microphone.h" -#include +#include #include #include "AudioCommon/CubebUtils.h" #include "Common/Logging/Log.h" +#include "Common/MathUtil.h" #include "Common/Swap.h" #include "Core/Config/MainSettings.h" #include "Core/Core.h" @@ -144,11 +145,17 @@ long Microphone::DataCallback(cubeb_stream* stream, void* user_data, const void* return nframes; std::lock_guard lk(mic->m_ring_lock); + std::span buff_in(static_cast(input_buffer), nframes); + const auto gain = mic->ComputeGain(Config::Get(Config::MAIN_WII_SPEAK_VOLUME_MODIFIER)); + auto processed_buff_in = buff_in | std::views::transform([gain](s16 sample) { + return MathUtil::SaturatingCast(sample * gain); + }); - const s16* buff_in = static_cast(input_buffer); - for (long i = 0; i < nframes; i++) + mic->UpdateLoudness(processed_buff_in); + + for (s16 le_sample : processed_buff_in) { - mic->m_stream_buffer[mic->m_stream_wpos] = Common::swap16(buff_in[i]); + mic->m_stream_buffer[mic->m_stream_wpos] = Common::swap16(le_sample); mic->m_stream_wpos = (mic->m_stream_wpos + 1) % mic->STREAM_SIZE; } @@ -188,6 +195,45 @@ u16 Microphone::ReadIntoBuffer(u8* ptr, u32 size) return static_cast(ptr - begin); } +u16 Microphone::GetLoudnessLevel() const +{ + if (m_sampler.mute || Config::Get(Config::MAIN_WII_SPEAK_MUTED)) + return 0; + return m_loudness_level; +} + +// Based on graphical cues on Monster Hunter 3, the level seems properly displayed with values +// between 0 and 0x3a00. +// +// TODO: Proper hardware testing, documentation, formulas... +void Microphone::UpdateLoudness(std::ranges::input_range auto&& samples) +{ + // Based on MH3 graphical cues, let's use a 0x4000 window + static const u32 WINDOW = 0x4000; + static const float UNIT = (m_loudness.DB_MAX - m_loudness.DB_MIN) / WINDOW; + + m_loudness.Update(samples); + + if (m_loudness.samples_count >= m_loudness.SAMPLES_NEEDED) + { + const float amp_db = m_loudness.GetAmplitudeDb(); + m_loudness_level = static_cast((amp_db - m_loudness.DB_MIN) / UNIT); + +#ifdef WII_SPEAK_LOG_STATS + m_loudness.LogStats(); +#else + DEBUG_LOG_FMT(IOS_USB, + "Wii Speak loudness stats (sample count: {}/{}):\n" + " - min={} max={} amplitude={} dB\n" + " - level={:04x}", + m_loudness.samples_count, m_loudness.SAMPLES_NEEDED, m_loudness.peak_min, + m_loudness.peak_max, amp_db, m_loudness_level); +#endif + + m_loudness.Reset(); + } +} + bool Microphone::HasData(u32 sample_count = BUFF_SIZE_SAMPLES) const { return m_samples_avail >= sample_count; @@ -197,4 +243,101 @@ const WiiSpeakState& Microphone::GetSampler() const { return m_sampler; } + +Microphone::FloatType Microphone::ComputeGain(FloatType relative_db) const +{ + return m_loudness.ComputeGain(relative_db); +} + +const Microphone::FloatType Microphone::Loudness::DB_MIN = + 20 * std::log10(FloatType(1) / MAX_AMPLITUDE); +const Microphone::FloatType Microphone::Loudness::DB_MAX = 20 * std::log10(FloatType(1)); + +Microphone::Loudness::SampleType Microphone::Loudness::GetPeak() const +{ + return std::max(std::abs(peak_min), std::abs(peak_max)); +} + +Microphone::FloatType Microphone::Loudness::GetDecibel(FloatType value) const +{ + return 20 * std::log10(value); +} + +Microphone::FloatType Microphone::Loudness::GetAmplitude() const +{ + return GetPeak() / MAX_AMPLITUDE; +} + +Microphone::FloatType Microphone::Loudness::GetAmplitudeDb() const +{ + return GetDecibel(GetAmplitude()); +} + +Microphone::FloatType Microphone::Loudness::GetAbsoluteMean() const +{ + return FloatType(absolute_sum) / samples_count; +} + +Microphone::FloatType Microphone::Loudness::GetAbsoluteMeanDb() const +{ + return GetDecibel(GetAbsoluteMean()); +} + +Microphone::FloatType Microphone::Loudness::GetRootMeanSquare() const +{ + return std::sqrt(square_sum / samples_count); +} + +Microphone::FloatType Microphone::Loudness::GetRootMeanSquareDb() const +{ + return GetDecibel(GetRootMeanSquare()); +} + +Microphone::FloatType Microphone::Loudness::GetCrestFactor() const +{ + const auto rms = GetRootMeanSquare(); + if (rms == 0) + return FloatType(0); + return GetPeak() / rms; +} + +Microphone::FloatType Microphone::Loudness::GetCrestFactorDb() const +{ + return GetDecibel(GetCrestFactor()); +} + +Microphone::FloatType Microphone::Loudness::ComputeGain(FloatType db) const +{ + return std::pow(FloatType(10), db / 20); +} + +void Microphone::Loudness::Reset() +{ + samples_count = 0; + absolute_sum = 0; + square_sum = FloatType(0); + peak_min = 0; + peak_max = 0; +} + +void Microphone::Loudness::LogStats() +{ + const auto amplitude = GetAmplitude(); + const auto amplitude_db = GetDecibel(amplitude); + const auto rms = GetRootMeanSquare(); + const auto rms_db = GetDecibel(rms); + const auto abs_mean = GetAbsoluteMean(); + const auto abs_mean_db = GetDecibel(abs_mean); + const auto crest_factor = GetCrestFactor(); + const auto crest_factor_db = GetDecibel(crest_factor); + + INFO_LOG_FMT(IOS_USB, + "Wii Speak loudness stats (sample count: {}/{}):\n" + " - min={} max={} amplitude={} ({} dB)\n" + " - rms={} ({} dB) \n" + " - abs_mean={} ({} dB)\n" + " - crest_factor={} ({} dB)", + samples_count, SAMPLES_NEEDED, peak_min, peak_max, amplitude, amplitude_db, rms, + rms_db, abs_mean, abs_mean_db, crest_factor, crest_factor_db); +} } // namespace IOS::HLE::USB diff --git a/Source/Core/Core/IOS/USB/Emulated/Microphone.h b/Source/Core/Core/IOS/USB/Emulated/Microphone.h index 30af6b09c5..27d773008e 100644 --- a/Source/Core/Core/IOS/USB/Emulated/Microphone.h +++ b/Source/Core/Core/IOS/USB/Emulated/Microphone.h @@ -3,9 +3,15 @@ #pragma once +#include #include +#include +#include #include #include +#include +#include +#include #include "AudioCommon/CubebUtils.h" #include "Common/CommonTypes.h" @@ -20,12 +26,17 @@ struct WiiSpeakState; class Microphone final { public: + using FloatType = float; + Microphone(const WiiSpeakState& sampler); ~Microphone(); bool HasData(u32 sample_count) const; u16 ReadIntoBuffer(u8* ptr, u32 size); + u16 GetLoudnessLevel() const; + void UpdateLoudness(std::ranges::input_range auto&& samples); const WiiSpeakState& GetSampler() const; + FloatType ComputeGain(FloatType relative_db) const; private: static long DataCallback(cubeb_stream* stream, void* user_data, const void* input_buffer, @@ -46,6 +57,62 @@ private: u32 m_stream_rpos = 0; u32 m_samples_avail = 0; + // TODO: Find how this level is calculated on real hardware + u16 m_loudness_level = 0; + struct Loudness + { + using SampleType = s16; + using UnsignedSampleType = std::make_unsigned_t; + + void Update(const auto& samples) + { + samples_count += static_cast(samples.size()); + + const auto [min_element, max_element] = std::ranges::minmax_element(samples); + peak_min = std::min(*min_element, peak_min); + peak_max = std::max(*max_element, peak_max); + + const auto begin = samples.begin(); + const auto end = samples.end(); + absolute_sum = std::reduce(begin, end, absolute_sum, + [](u32 a, SampleType b) { return a + std::abs(b); }); + square_sum = std::reduce(begin, end, square_sum, [](FloatType a, s16 b) { + return a + std::pow(FloatType(b), FloatType(2)); + }); + } + + SampleType GetPeak() const; + FloatType GetDecibel(FloatType value) const; + FloatType GetAmplitude() const; + FloatType GetAmplitudeDb() const; + FloatType GetAbsoluteMean() const; + FloatType GetAbsoluteMeanDb() const; + FloatType GetRootMeanSquare() const; + FloatType GetRootMeanSquareDb() const; + FloatType GetCrestFactor() const; + FloatType GetCrestFactorDb() const; + FloatType ComputeGain(FloatType db) const; + + void Reset(); + void LogStats(); + + // Samples used to compute the loudness level + static constexpr u16 SAMPLES_NEEDED = SAMPLING_RATE / 125; + static_assert((SAMPLES_NEEDED % BUFF_SIZE_SAMPLES) == 0); + + static constexpr FloatType MAX_AMPLITUDE = + FloatType{std::numeric_limits::max() / 2}; + static const FloatType DB_MIN; + static const FloatType DB_MAX; + + u16 samples_count = 0; + u32 absolute_sum = 0; + FloatType square_sum = FloatType(0); + SampleType peak_min = 0; + SampleType peak_max = 0; + }; + Loudness m_loudness; + std::mutex m_ring_lock; std::shared_ptr m_cubeb_ctx = nullptr; cubeb_stream* m_cubeb_stream = nullptr; diff --git a/Source/Core/Core/IOS/USB/Emulated/WiiSpeak.cpp b/Source/Core/Core/IOS/USB/Emulated/WiiSpeak.cpp index 720640e0e6..1cea22c89b 100644 --- a/Source/Core/Core/IOS/USB/Emulated/WiiSpeak.cpp +++ b/Source/Core/Core/IOS/USB/Emulated/WiiSpeak.cpp @@ -398,7 +398,9 @@ void WiiSpeak::GetRegister(const std::unique_ptr& cmd) const case SP_SIN: break; case SP_SOUT: - memory.Write_U16(0x39B0, arg2); // 6dB + // TODO: Find how it was measured and how accurate it was + // memory.Write_U16(0x39B0, arg2); // 6dB + memory.Write_U16(m_microphone->GetLoudnessLevel(), arg2); break; case SP_RIN: break; diff --git a/Source/Core/DolphinQt/EmulatedUSB/WiiSpeakWindow.cpp b/Source/Core/DolphinQt/EmulatedUSB/WiiSpeakWindow.cpp index 31c482537d..67d8433677 100644 --- a/Source/Core/DolphinQt/EmulatedUSB/WiiSpeakWindow.cpp +++ b/Source/Core/DolphinQt/EmulatedUSB/WiiSpeakWindow.cpp @@ -3,8 +3,11 @@ #include "DolphinQt/EmulatedUSB/WiiSpeakWindow.h" +#include + #include #include +#include #include #include #include @@ -61,8 +64,34 @@ void WiiSpeakWindow::CreateMainWindow() checkbox_mic_muted->setChecked(Config::Get(Config::MAIN_WII_SPEAK_MUTED)); connect(checkbox_mic_muted, &QCheckBox::toggled, this, &WiiSpeakWindow::SetWiiSpeakConnectionState); + checkbox_mic_muted->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); config_layout->addWidget(checkbox_mic_muted); + auto* volume_layout = new QGridLayout(); + static constexpr int FILTER_MIN = -50; + static constexpr int FILTER_MAX = 50; + const int volume_modifier = + std::clamp(Config::Get(Config::MAIN_WII_SPEAK_VOLUME_MODIFIER), FILTER_MIN, FILTER_MAX); + auto filter_slider = new QSlider(Qt::Horizontal, this); + auto slider_label = new QLabel(tr("Volume modifier (value: %1dB)").arg(volume_modifier)); + connect(filter_slider, &QSlider::valueChanged, this, [slider_label](int value) { + Config::SetBaseOrCurrent(Config::MAIN_WII_SPEAK_VOLUME_MODIFIER, 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_combobox_microphones = new QComboBox(); m_combobox_microphones->addItem(QLatin1String("(%1)").arg(tr("Autodetect preferred microphone")), QString{}); From 76387cbd6c9a881deb8950ee9d0f47bdde6af964 Mon Sep 17 00:00:00 2001 From: Sepalani Date: Fri, 4 Apr 2025 23:55:08 +0400 Subject: [PATCH 21/22] IOS/USB: Implement Wii Speak SAMPLER_FREQ register properly Fix the default sampling rate which should be 16KHz --- Source/Core/Core/IOS/USB/Emulated/Microphone.cpp | 14 ++++++++++---- Source/Core/Core/IOS/USB/Emulated/Microphone.h | 10 +++++----- Source/Core/Core/IOS/USB/Emulated/WiiSpeak.cpp | 4 ++-- Source/Core/Core/IOS/USB/Emulated/WiiSpeak.h | 2 ++ 4 files changed, 19 insertions(+), 11 deletions(-) diff --git a/Source/Core/Core/IOS/USB/Emulated/Microphone.cpp b/Source/Core/Core/IOS/USB/Emulated/Microphone.cpp index eaa9d4f5b1..6d8b4740e8 100644 --- a/Source/Core/Core/IOS/USB/Emulated/Microphone.cpp +++ b/Source/Core/Core/IOS/USB/Emulated/Microphone.cpp @@ -45,7 +45,7 @@ void Microphone::StreamInit() } // TODO: Not here but rather inside the WiiSpeak device if possible? - StreamStart(); + StreamStart(m_sampler.DEFAULT_SAMPLING_RATE); } void Microphone::StreamTerminate() @@ -60,12 +60,12 @@ static void state_callback(cubeb_stream* stream, void* user_data, cubeb_state st { } -void Microphone::StreamStart() +void Microphone::StreamStart(u32 sampling_rate) { if (!m_cubeb_ctx) return; - m_worker.Execute([this] { + m_worker.Execute([this, sampling_rate] { #ifdef ANDROID JNIEnv* env = IDCache::GetEnvForThread(); if (jboolean result = env->CallStaticBooleanMethod( @@ -81,7 +81,7 @@ void Microphone::StreamStart() cubeb_stream_params params{}; params.format = CUBEB_SAMPLE_S16LE; - params.rate = SAMPLING_RATE; + params.rate = sampling_rate; params.channels = 1; params.layout = CUBEB_LAYOUT_MONO; @@ -249,6 +249,12 @@ Microphone::FloatType Microphone::ComputeGain(FloatType relative_db) const return m_loudness.ComputeGain(relative_db); } +void Microphone::SetSamplingRate(u32 sampling_rate) +{ + StreamStop(); + StreamStart(sampling_rate); +} + const Microphone::FloatType Microphone::Loudness::DB_MIN = 20 * std::log10(FloatType(1) / MAX_AMPLITUDE); const Microphone::FloatType Microphone::Loudness::DB_MAX = 20 * std::log10(FloatType(1)); diff --git a/Source/Core/Core/IOS/USB/Emulated/Microphone.h b/Source/Core/Core/IOS/USB/Emulated/Microphone.h index 27d773008e..638448dc11 100644 --- a/Source/Core/Core/IOS/USB/Emulated/Microphone.h +++ b/Source/Core/Core/IOS/USB/Emulated/Microphone.h @@ -37,6 +37,7 @@ public: void UpdateLoudness(std::ranges::input_range auto&& samples); const WiiSpeakState& GetSampler() const; FloatType ComputeGain(FloatType relative_db) const; + void SetSamplingRate(u32 sampling_rate); private: static long DataCallback(cubeb_stream* stream, void* user_data, const void* input_buffer, @@ -44,12 +45,11 @@ private: void StreamInit(); void StreamTerminate(); - void StreamStart(); + void StreamStart(u32 sampling_rate); void StreamStop(); - static constexpr u32 SAMPLING_RATE = 8000; using SampleType = s16; - static constexpr u32 BUFF_SIZE_SAMPLES = 16; + static constexpr u32 BUFF_SIZE_SAMPLES = 32; static constexpr u32 STREAM_SIZE = BUFF_SIZE_SAMPLES * 500; std::array m_stream_buffer{}; @@ -96,8 +96,8 @@ private: void Reset(); void LogStats(); - // Samples used to compute the loudness level - static constexpr u16 SAMPLES_NEEDED = SAMPLING_RATE / 125; + // Samples used to compute the loudness level (arbitrarily chosen) + static constexpr u16 SAMPLES_NEEDED = 128; static_assert((SAMPLES_NEEDED % BUFF_SIZE_SAMPLES) == 0); static constexpr FloatType MAX_AMPLITUDE = diff --git a/Source/Core/Core/IOS/USB/Emulated/WiiSpeak.cpp b/Source/Core/Core/IOS/USB/Emulated/WiiSpeak.cpp index 1cea22c89b..686906fd55 100644 --- a/Source/Core/Core/IOS/USB/Emulated/WiiSpeak.cpp +++ b/Source/Core/Core/IOS/USB/Emulated/WiiSpeak.cpp @@ -250,8 +250,6 @@ void WiiSpeak::SetRegister(const std::unique_ptr& cmd) m_sampler.sample_on = !!arg1; break; case SAMPLER_FREQ: - WARN_LOG_FMT(IOS_USB, "Wii Speak SAMPLER_FREQ set (arg1={:04x}, arg2={:04x}) not implemented", - arg1, arg2); switch (arg1) { case FREQ_8KHZ: @@ -271,6 +269,8 @@ void WiiSpeak::SetRegister(const std::unique_ptr& cmd) m_sampler.freq = 16000; break; } + if (m_microphone) + m_microphone->SetSamplingRate(m_sampler.freq); break; case SAMPLER_GAIN: WARN_LOG_FMT(IOS_USB, "Wii Speak SAMPLER_GAIN set (arg1={:04x}, arg2={:04x}) not implemented", diff --git a/Source/Core/Core/IOS/USB/Emulated/WiiSpeak.h b/Source/Core/Core/IOS/USB/Emulated/WiiSpeak.h index 57e2959196..b4d298f778 100644 --- a/Source/Core/Core/IOS/USB/Emulated/WiiSpeak.h +++ b/Source/Core/Core/IOS/USB/Emulated/WiiSpeak.h @@ -20,6 +20,8 @@ struct WiiSpeakState int gain; bool ec_reset; bool sp_on; + + static constexpr u32 DEFAULT_SAMPLING_RATE = 16000; }; class WiiSpeak final : public Device From 85b29e4f96c9c7f8c4a9b45e38f3932f5da0e96c Mon Sep 17 00:00:00 2001 From: Sepalani Date: Thu, 24 Apr 2025 03:35:16 +0400 Subject: [PATCH 22/22] DolphinLib: Fix compilation issues when HAVE_CUBEB is not defined --- .../Core/Core/IOS/USB/Emulated/Microphone.cpp | 21 +++++++++++++++++++ .../Core/Core/IOS/USB/Emulated/Microphone.h | 13 +++++++++--- Source/Core/DolphinLib.props | 16 ++++++++------ .../DolphinQt/EmulatedUSB/WiiSpeakWindow.cpp | 7 +++++++ Source/VSProps/Base.Dolphin.props | 3 ++- 5 files changed, 50 insertions(+), 10 deletions(-) diff --git a/Source/Core/Core/IOS/USB/Emulated/Microphone.cpp b/Source/Core/Core/IOS/USB/Emulated/Microphone.cpp index 6d8b4740e8..e58812488d 100644 --- a/Source/Core/Core/IOS/USB/Emulated/Microphone.cpp +++ b/Source/Core/Core/IOS/USB/Emulated/Microphone.cpp @@ -7,7 +7,10 @@ #include +#ifdef HAVE_CUBEB #include "AudioCommon/CubebUtils.h" +#endif + #include "Common/Logging/Log.h" #include "Common/MathUtil.h" #include "Common/Swap.h" @@ -36,6 +39,23 @@ Microphone::~Microphone() StreamTerminate(); } +#ifndef HAVE_CUBEB +void Microphone::StreamInit() +{ +} + +void Microphone::StreamStart([[maybe_unused]] u32 sampling_rate) +{ +} + +void Microphone::StreamStop() +{ +} + +void Microphone::StreamTerminate() +{ +} +#else void Microphone::StreamInit() { if (!m_worker.Execute([this] { m_cubeb_ctx = CubebUtils::GetContext(); })) @@ -168,6 +188,7 @@ long Microphone::DataCallback(cubeb_stream* stream, void* user_data, const void* return nframes; } +#endif u16 Microphone::ReadIntoBuffer(u8* ptr, u32 size) { diff --git a/Source/Core/Core/IOS/USB/Emulated/Microphone.h b/Source/Core/Core/IOS/USB/Emulated/Microphone.h index 638448dc11..4bb8229db4 100644 --- a/Source/Core/Core/IOS/USB/Emulated/Microphone.h +++ b/Source/Core/Core/IOS/USB/Emulated/Microphone.h @@ -13,11 +13,14 @@ #include #include -#include "AudioCommon/CubebUtils.h" #include "Common/CommonTypes.h" +#ifdef HAVE_CUBEB +#include "AudioCommon/CubebUtils.h" + struct cubeb; struct cubeb_stream; +#endif namespace IOS::HLE::USB { @@ -40,8 +43,10 @@ public: void SetSamplingRate(u32 sampling_rate); private: +#ifdef HAVE_CUBEB static long DataCallback(cubeb_stream* stream, void* user_data, const void* input_buffer, void* output_buffer, long nframes); +#endif void StreamInit(); void StreamTerminate(); @@ -114,11 +119,13 @@ private: Loudness m_loudness; std::mutex m_ring_lock; - std::shared_ptr m_cubeb_ctx = nullptr; - cubeb_stream* m_cubeb_stream = nullptr; const WiiSpeakState& 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"}; +#endif }; } // namespace IOS::HLE::USB diff --git a/Source/Core/DolphinLib.props b/Source/Core/DolphinLib.props index e20ceb0fc9..bd68021aaf 100644 --- a/Source/Core/DolphinLib.props +++ b/Source/Core/DolphinLib.props @@ -1,9 +1,12 @@ - - + + + + + @@ -289,7 +292,6 @@ - @@ -773,10 +775,13 @@ - - + + + + + @@ -960,7 +965,6 @@ - diff --git a/Source/Core/DolphinQt/EmulatedUSB/WiiSpeakWindow.cpp b/Source/Core/DolphinQt/EmulatedUSB/WiiSpeakWindow.cpp index 67d8433677..f22fe93117 100644 --- a/Source/Core/DolphinQt/EmulatedUSB/WiiSpeakWindow.cpp +++ b/Source/Core/DolphinQt/EmulatedUSB/WiiSpeakWindow.cpp @@ -14,7 +14,9 @@ #include #include +#ifdef HAVE_CUBEB #include "AudioCommon/CubebUtils.h" +#endif #include "Core/Config/MainSettings.h" #include "Core/Core.h" #include "Core/System.h" @@ -93,6 +95,10 @@ void WiiSpeakWindow::CreateMainWindow() config_layout->setStretch(1, 3); m_combobox_microphones = new QComboBox(); +#ifndef HAVE_CUBEB + m_combobox_microphones->addItem(QLatin1String("(%1)").arg(tr("Audio backend unsupported")), + QString{}); +#else m_combobox_microphones->addItem(QLatin1String("(%1)").arg(tr("Autodetect preferred microphone")), QString{}); for (auto& [device_id, device_name] : CubebUtils::ListInputDevices()) @@ -100,6 +106,7 @@ void WiiSpeakWindow::CreateMainWindow() const auto user_data = QString::fromStdString(device_id); m_combobox_microphones->addItem(QString::fromStdString(device_name), user_data); } +#endif connect(m_combobox_microphones, &QComboBox::currentIndexChanged, this, &WiiSpeakWindow::OnInputDeviceChange); diff --git a/Source/VSProps/Base.Dolphin.props b/Source/VSProps/Base.Dolphin.props index 3410b9fef6..f2f213b009 100644 --- a/Source/VSProps/Base.Dolphin.props +++ b/Source/VSProps/Base.Dolphin.props @@ -3,6 +3,7 @@ $(ProjectName)$(TargetSuffix) + true @@ -47,7 +48,7 @@ USE_RETRO_ACHIEVEMENTS;%(PreprocessorDefinitions) RC_CLIENT_SUPPORTS_HASH;%(PreprocessorDefinitions) RC_CLIENT_SUPPORTS_RAINTEGRATION;%(PreprocessorDefinitions) - HAVE_CUBEB;%(PreprocessorDefinitions) + HAVE_CUBEB;%(PreprocessorDefinitions)