IOS/USB: Emulate Wii Speak (OpenAL)

Credits to @degasus and @shuffle2 (godisgovernment):
https://github.com/degasus/dolphin/tree/wiispeak
This commit is contained in:
Sketch 2024-02-06 22:06:40 -05:00 committed by Sepalani
parent 8f3483fdd4
commit d1b885e7d8
17 changed files with 827 additions and 0 deletions

View file

@ -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

View file

@ -594,6 +594,12 @@ const Info<bool> MAIN_EMULATE_SKYLANDER_PORTAL{
const Info<bool> MAIN_EMULATE_INFINITY_BASE{
{System::Main, "EmulatedUSBDevices", "EmulateInfinityBase"}, false};
const Info<bool> MAIN_EMULATE_WII_SPEAK{{System::Main, "EmulatedUSBDevices", "EmulateWiiSpeak"},
false};
const Info<std::string> 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)

View file

@ -362,6 +362,8 @@ void SetUSBDeviceWhitelist(const std::set<std::pair<u16, u16>>& devices);
extern const Info<bool> MAIN_EMULATE_SKYLANDER_PORTAL;
extern const Info<bool> MAIN_EMULATE_INFINITY_BASE;
extern const Info<bool> MAIN_EMULATE_WII_SPEAK;
extern const Info<std::string> MAIN_WII_SPEAK_MICROPHONE;
// GameCube path utility functions

View file

@ -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 <algorithm>
namespace IOS::HLE::USB
{
std::vector<std::string> Microphone::ListDevices()
{
std::vector<std::string> 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<int>(alcGetError(m_device));
}
int Microphone::StartCapture()
{
alcCaptureStart(m_device);
return static_cast<int>(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<u32>(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<u16*>(dst) = Common::swap16(*static_cast<const u16*>(src));
}
void Microphone::GetSoundData()
{
if (m_num_of_samples == 0)
return;
u8* ptr = const_cast<u8*>(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

View file

@ -0,0 +1,113 @@
// Copyright 2025 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <OpenAL/al.h>
#include <OpenAL/alc.h>
#include <string>
#include <vector>
#include "Common/CommonTypes.h"
namespace IOS::HLE::USB
{
template <size_t S>
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<u8> m_container;
u32 m_head = 0, m_tail = 0, m_used = 0;
};
class Microphone final
{
public:
static std::vector<std::string> 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<u8> m_dsp_data{};
std::vector<u8> m_temp_buffer{};
simple_ringbuf<BUFFER_SIZE> m_rbuf_dsp;
};
} // namespace IOS::HLE::USB

View file

@ -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::microseconds>(
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<ConfigDescriptor> WiiSpeak::GetConfigurations() const
{
return m_config_descriptor;
}
std::vector<InterfaceDescriptor> WiiSpeak::GetInterfaces(u8 config) const
{
return m_interface_descriptor;
}
std::vector<EndpointDescriptor> 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<CtrlMessage> 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<u8, 1> 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<u8, 1> data{0};
cmd->FillBuffer(data.data(), 1);
cmd->GetEmulationKernel().EnqueueIPCReply(cmd->ios_request, IPC_SUCCESS);
init = true;
}
else
{
constexpr std::array<u8, 1> 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<BulkMessage> cmd)
{
cmd->GetEmulationKernel().EnqueueIPCReply(cmd->ios_request, IPC_SUCCESS);
return IPC_SUCCESS;
}
int WiiSpeak::SubmitTransfer(std::unique_ptr<IntrMessage> cmd)
{
cmd->GetEmulationKernel().EnqueueIPCReply(cmd->ios_request, IPC_SUCCESS);
return IPC_SUCCESS;
}
int WiiSpeak::SubmitTransfer(std::unique_ptr<IsoMessage> 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<CtrlMessage>& 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<CtrlMessage>& 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

View file

@ -0,0 +1,95 @@
// Copyright 2025 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <memory>
#include <mutex>
#include <thread>
#include <vector>
#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<ConfigDescriptor> GetConfigurations() const override;
std::vector<InterfaceDescriptor> GetInterfaces(u8 config) const override;
std::vector<EndpointDescriptor> GetEndpoints(u8 config, u8 interface, u8 alt) const override;
bool Attach() override;
bool AttachAndChangeInterface(u8 interface) override;
int CancelTransfer(u8 endpoint) override;
int ChangeInterface(u8 interface) override;
int GetNumberOfAltSettings(u8 interface) override;
int SetAltSetting(u8 alt_setting) override;
int SubmitTransfer(std::unique_ptr<CtrlMessage> message) override;
int SubmitTransfer(std::unique_ptr<BulkMessage> message) override;
int SubmitTransfer(std::unique_ptr<IntrMessage> message) override;
int SubmitTransfer(std::unique_ptr<IsoMessage> 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<CtrlMessage>& cmd);
void SetRegister(std::unique_ptr<CtrlMessage>& 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<ConfigDescriptor> m_config_descriptor;
std::vector<InterfaceDescriptor> m_interface_descriptor;
std::vector<EndpointDescriptor> m_endpoint_descriptor;
std::thread m_microphone_thread;
std::mutex m_mutex;
Common::Event m_shutdown_event;
};
} // namespace IOS::HLE::USB

View file

@ -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<USB::InfinityUSB>();
AddDevice(std::move(infinity_base), new_devices);
}
auto wii_speak = std::make_unique<USB::WiiSpeak>();
AddDevice(std::move(wii_speak), new_devices);
}
void USBScanner::AddDevice(std::unique_ptr<USB::Device> device, DeviceMap* new_devices)

View file

@ -404,9 +404,11 @@
<ClInclude Include="Core\IOS\USB\Bluetooth\WiimoteHIDAttr.h" />
<ClInclude Include="Core\IOS\USB\Common.h" />
<ClInclude Include="Core\IOS\USB\Emulated\Infinity.h" />
<ClInclude Include="Core\IOS\USB\Emulated\Microphone.h" />
<ClInclude Include="Core\IOS\USB\Emulated\Skylanders\Skylander.h" />
<ClInclude Include="Core\IOS\USB\Emulated\Skylanders\SkylanderCrypto.h" />
<ClInclude Include="Core\IOS\USB\Emulated\Skylanders\SkylanderFigure.h" />
<ClInclude Include="Core\IOS\USB\Emulated\WiiSpeak.h" />
<ClInclude Include="Core\IOS\USB\Host.h" />
<ClInclude Include="Core\IOS\USB\LibusbDevice.h" />
<ClInclude Include="Core\IOS\USB\OH0\OH0.h" />
@ -1069,9 +1071,11 @@
<ClCompile Include="Core\IOS\USB\Bluetooth\WiimoteHIDAttr.cpp" />
<ClCompile Include="Core\IOS\USB\Common.cpp" />
<ClCompile Include="Core\IOS\USB\Emulated\Infinity.cpp" />
<ClCompile Include="Core\IOS\USB\Emulated\Microphone.cpp" />
<ClCompile Include="Core\IOS\USB\Emulated\Skylanders\Skylander.cpp" />
<ClCompile Include="Core\IOS\USB\Emulated\Skylanders\SkylanderCrypto.cpp" />
<ClCompile Include="Core\IOS\USB\Emulated\Skylanders\SkylanderFigure.cpp" />
<ClCompile Include="Core\IOS\USB\Emulated\WiiSpeak.cpp" />
<ClCompile Include="Core\IOS\USB\Host.cpp" />
<ClCompile Include="Core\IOS\USB\LibusbDevice.cpp" />
<ClCompile Include="Core\IOS\USB\OH0\OH0.cpp" />

View file

@ -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

View file

@ -157,6 +157,7 @@
<ClCompile Include="Debugger\WatchWidget.cpp" />
<ClCompile Include="DiscordHandler.cpp" />
<ClCompile Include="DiscordJoinRequestDialog.cpp" />
<ClCompile Include="EmulatedUSB\WiiSpeakWindow.cpp" />
<ClCompile Include="FIFO\FIFOAnalyzer.cpp" />
<ClCompile Include="FIFO\FIFOPlayerWindow.cpp" />
<ClCompile Include="GameList\GameList.cpp" />
@ -374,6 +375,7 @@
<QtMoc Include="Debugger\WatchWidget.h" />
<QtMoc Include="DiscordHandler.h" />
<QtMoc Include="DiscordJoinRequestDialog.h" />
<QtMoc Include="EmulatedUSB\WiiSpeakWindow.h" />
<QtMoc Include="FIFO\FIFOAnalyzer.h" />
<QtMoc Include="FIFO\FIFOPlayerWindow.h" />
<QtMoc Include="GameList\GameList.h" />

View file

@ -0,0 +1,84 @@
// Copyright 2025 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "DolphinQt/EmulatedUSB/WiiSpeakWindow.h"
#include <string>
#include <QCheckBox>
#include <QComboBox>
#include <QCompleter>
#include <QDialogButtonBox>
#include <QGroupBox>
#include <QLabel>
#include <QLineEdit>
#include <QMessageBox>
#include <QPushButton>
#include <QScrollArea>
#include <QString>
#include <QVBoxLayout>
#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);
}

View file

@ -0,0 +1,27 @@
// Copyright 2025 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <QWidget>
#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;
};

View file

@ -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()) ?

View file

@ -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;

View file

@ -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();

View file

@ -94,6 +94,7 @@ signals:
void ShowResourcePackManager();
void ShowSkylanderPortal();
void ShowInfinityBase();
void ShowWiiSpeakWindow();
void ConnectWiiRemote(int id);
#ifdef USE_RETRO_ACHIEVEMENTS