From 1301cec53886103b7faa4262fb08e0c6d12ecdc7 Mon Sep 17 00:00:00 2001 From: Weston Heard Date: Tue, 10 Mar 2026 14:49:15 -0500 Subject: [PATCH] WiimoteAudio: Add individual Wiimote audio mixer Added Platform checks, Incremented state ID Removed semicolon from PR version comment Riveting change Changed Remote Names and References Updated Wii remote Audio routing to only show enabled when not using passthrough adapter, enabling speaker data, and Cubeb audio backend, and show enable/disable per Remote when supported --- Source/Core/AudioCommon/CubebStream.cpp | 83 ++++++++++++ Source/Core/AudioCommon/CubebStream.h | 13 ++ Source/Core/AudioCommon/CubebUtils.cpp | 70 ++++++++++ Source/Core/AudioCommon/CubebUtils.h | 2 + Source/Core/AudioCommon/Mixer.cpp | 38 ++++-- Source/Core/AudioCommon/Mixer.h | 15 ++- Source/Core/Core/Config/MainSettings.cpp | 15 +++ Source/Core/Core/Config/MainSettings.h | 8 ++ Source/Core/Core/HW/WiimoteEmu/Speaker.cpp | 5 +- Source/Core/Core/HW/WiimoteEmu/Speaker.h | 2 + Source/Core/Core/HW/WiimoteEmu/WiimoteEmu.cpp | 2 + Source/Core/Core/State.cpp | 2 +- Source/Core/DolphinQt/Settings/AudioPane.cpp | 126 ++++++++++++++++++ Source/Core/DolphinQt/Settings/AudioPane.h | 11 ++ 14 files changed, 376 insertions(+), 16 deletions(-) diff --git a/Source/Core/AudioCommon/CubebStream.cpp b/Source/Core/AudioCommon/CubebStream.cpp index dd6a35f13e..8d558c43fc 100644 --- a/Source/Core/AudioCommon/CubebStream.cpp +++ b/Source/Core/AudioCommon/CubebStream.cpp @@ -9,6 +9,7 @@ #include "Common/CommonTypes.h" #include "Common/Logging/Log.h" #include "Core/Config/MainSettings.h" +#include "Core/System.h" #ifdef _WIN32 #include @@ -34,6 +35,20 @@ void CubebStream::StateCallback(cubeb_stream* stream, void* user_data, cubeb_sta { } +long CubebStream::WiimoteDataCallback(cubeb_stream* stream, void* user_data, + const void* /*input_buffer*/, void* output_buffer, + long num_frames) +{ + const auto* data = static_cast(user_data); + data->self->m_mixer->MixWiimoteSpeaker(data->wiimote_index, + static_cast(output_buffer), num_frames); + return num_frames; +} + +void CubebStream::WiimoteStateCallback(cubeb_stream* stream, void* user_data, cubeb_state state) +{ +} + CubebStream::CubebStream() #ifdef _WIN32 : m_work_queue("Cubeb Worker") @@ -86,6 +101,51 @@ bool CubebStream::Init() cubeb_stream_init(m_ctx.get(), &m_stream, "Dolphin Audio Output", nullptr, nullptr, nullptr, ¶ms, std::max(BUFFER_SAMPLES, minimum_latency), DataCallback, StateCallback, this) == CUBEB_OK; + + // Create per-wiimote streams for audio routing (Wii games only, when enabled) + if (return_value && Core::System::GetInstance().IsWii() && + Config::Get(Config::MAIN_WIIMOTE_AUDIO_ROUTING_ENABLED)) + { + static const char* const WIIMOTE_STREAM_NAMES[4] = { + "Dolphin Wiimote 1 Audio", "Dolphin Wiimote 2 Audio", "Dolphin Wiimote 3 Audio", + "Dolphin Wiimote 4 Audio"}; + + cubeb_stream_params wiimote_params{}; + wiimote_params.rate = m_mixer->GetSampleRate(); + wiimote_params.channels = 2; + wiimote_params.format = CUBEB_SAMPLE_S16NE; + wiimote_params.layout = CUBEB_LAYOUT_STEREO; + + for (std::size_t i = 0; i < m_wiimote_streams.size(); ++i) + { + if (!Config::Get(Config::MAIN_WIIMOTE_AUDIO_OUTPUT_ENABLED[i])) + continue; + + const std::string device_id_str = + Config::Get(Config::MAIN_WIIMOTE_AUDIO_OUTPUT_DEVICE[i]); + const cubeb_devid output_devid = + device_id_str.empty() ? nullptr + : static_cast( + CubebUtils::GetOutputDeviceById(device_id_str)); + + u32 wiimote_min_latency = 0; + cubeb_get_min_latency(m_ctx.get(), &wiimote_params, &wiimote_min_latency); + + m_wiimote_stream_data[i] = {this, i}; + const int result = + cubeb_stream_init(m_ctx.get(), &m_wiimote_streams[i], WIIMOTE_STREAM_NAMES[i], + nullptr, nullptr, output_devid, &wiimote_params, + std::max(BUFFER_SAMPLES, wiimote_min_latency), WiimoteDataCallback, + WiimoteStateCallback, &m_wiimote_stream_data[i]); + + if (result != CUBEB_OK) + { + ERROR_LOG_FMT(AUDIO, "Failed to create Cubeb stream for Wiimote {} audio routing", + i + 1); + m_wiimote_streams[i] = nullptr; + } + } + } } #ifdef _WIN32 @@ -105,9 +165,23 @@ bool CubebStream::SetRunning(bool running) m_work_queue.PushBlocking([this, running, &return_value] { #endif if (running) + { return_value = cubeb_stream_start(m_stream) == CUBEB_OK; + for (auto& ws : m_wiimote_streams) + { + if (ws) + cubeb_stream_start(ws); + } + } else + { return_value = cubeb_stream_stop(m_stream) == CUBEB_OK; + for (auto& ws : m_wiimote_streams) + { + if (ws) + cubeb_stream_stop(ws); + } + } #ifdef _WIN32 }); #endif @@ -122,6 +196,15 @@ CubebStream::~CubebStream() #endif cubeb_stream_stop(m_stream); cubeb_stream_destroy(m_stream); + for (auto& ws : m_wiimote_streams) + { + if (ws) + { + cubeb_stream_stop(ws); + cubeb_stream_destroy(ws); + ws = nullptr; + } + } #ifdef _WIN32 if (m_should_couninit) { diff --git a/Source/Core/AudioCommon/CubebStream.h b/Source/Core/AudioCommon/CubebStream.h index db5d14b33e..1c95fd50d8 100644 --- a/Source/Core/AudioCommon/CubebStream.h +++ b/Source/Core/AudioCommon/CubebStream.h @@ -3,6 +3,7 @@ #pragma once +#include #include #include @@ -31,9 +32,17 @@ public: static bool IsValid() { return true; } private: + struct WiimoteStreamData + { + CubebStream* self; + std::size_t wiimote_index; + }; + bool m_stereo = false; std::shared_ptr m_ctx; cubeb_stream* m_stream = nullptr; + std::array m_wiimote_stream_data{}; + std::array m_wiimote_streams{}; std::vector m_short_buffer; std::vector m_floatstereo_buffer; @@ -47,5 +56,9 @@ private: static long DataCallback(cubeb_stream* stream, void* user_data, const void* /*input_buffer*/, void* output_buffer, long num_frames); static void StateCallback(cubeb_stream* stream, void* user_data, cubeb_state state); + static long WiimoteDataCallback(cubeb_stream* stream, void* user_data, + const void* /*input_buffer*/, void* output_buffer, + long num_frames); + static void WiimoteStateCallback(cubeb_stream* stream, void* user_data, cubeb_state state); #endif }; diff --git a/Source/Core/AudioCommon/CubebUtils.cpp b/Source/Core/AudioCommon/CubebUtils.cpp index 73bfafb7fd..ec8d389d6b 100644 --- a/Source/Core/AudioCommon/CubebUtils.cpp +++ b/Source/Core/AudioCommon/CubebUtils.cpp @@ -177,6 +177,76 @@ cubeb_devid GetInputDeviceById(std::string_view id) return device_id; } +std::vector> ListOutputDevices() +{ + std::vector> devices; + + cubeb_device_collection collection; + auto cubeb_ctx = GetContext(); + if (!cubeb_ctx) + return devices; + + const int r = cubeb_enumerate_devices(cubeb_ctx.get(), CUBEB_DEVICE_TYPE_OUTPUT, &collection); + if (r != CUBEB_OK) + { + ERROR_LOG_FMT(AUDIO, "Error listing cubeb output devices"); + return devices; + } + + for (uint32_t i = 0; i < collection.count; i++) + { + const auto& info = collection.device[i]; + if (info.device_id == nullptr) + continue; + + if (info.state == CUBEB_DEVICE_STATE_ENABLED) + { + const char* name = (info.friendly_name != nullptr) ? info.friendly_name : info.device_id; + devices.emplace_back(info.device_id, name); + } + } + + cubeb_device_collection_destroy(cubeb_ctx.get(), &collection); + return devices; +} + +const void* GetOutputDeviceById(std::string_view id) +{ + if (id.empty()) + return nullptr; + + cubeb_device_collection collection; + auto cubeb_ctx = GetContext(); + if (!cubeb_ctx) + return nullptr; + + const int r = cubeb_enumerate_devices(cubeb_ctx.get(), CUBEB_DEVICE_TYPE_OUTPUT, &collection); + if (r != CUBEB_OK) + { + ERROR_LOG_FMT(AUDIO, "Error enumerating cubeb output devices"); + return nullptr; + } + + cubeb_devid device_id = nullptr; + for (uint32_t i = 0; i < collection.count; i++) + { + const auto& info = collection.device[i]; + if (info.device_id && id.compare(info.device_id) == 0) + { + device_id = info.devid; + break; + } + } + + if (device_id == nullptr) + { + WARN_LOG_FMT(AUDIO, "Failed to find selected output device, defaulting to system preferences"); + } + + cubeb_device_collection_destroy(cubeb_ctx.get(), &collection); + return device_id; +} + CoInitSyncWorker::CoInitSyncWorker([[maybe_unused]] std::string worker_name) #ifdef _WIN32 : m_work_queue{std::move(worker_name)} diff --git a/Source/Core/AudioCommon/CubebUtils.h b/Source/Core/AudioCommon/CubebUtils.h index 35e3994e14..1e49b0c8bb 100644 --- a/Source/Core/AudioCommon/CubebUtils.h +++ b/Source/Core/AudioCommon/CubebUtils.h @@ -21,6 +21,8 @@ namespace CubebUtils std::shared_ptr GetContext(); std::vector> ListInputDevices(); const void* GetInputDeviceById(std::string_view id); +std::vector> ListOutputDevices(); +const void* GetOutputDeviceById(std::string_view id); // Helper used to handle Windows COM library for cubeb WASAPI backend class CoInitSyncWorker diff --git a/Source/Core/AudioCommon/Mixer.cpp b/Source/Core/AudioCommon/Mixer.cpp index 0d673a9e0d..7c36e7c2da 100644 --- a/Source/Core/AudioCommon/Mixer.cpp +++ b/Source/Core/AudioCommon/Mixer.cpp @@ -52,7 +52,8 @@ void Mixer::DoState(PointerWrap& p) { m_dma_mixer.DoState(p); m_streaming_mixer.DoState(p); - m_wiimote_speaker_mixer.DoState(p); + for (auto& mixer : m_wiimote_speaker_mixers) + mixer.DoState(p); m_skylander_portal_mixer.DoState(p); for (auto& mixer : m_gba_mixers) mixer.DoState(p); @@ -169,7 +170,11 @@ std::size_t Mixer::Mix(s16* samples, std::size_t num_samples) m_dma_mixer.Mix(samples, num_samples); m_streaming_mixer.Mix(samples, num_samples); - m_wiimote_speaker_mixer.Mix(samples, num_samples); + for (std::size_t i = 0; i < m_wiimote_speaker_mixers.size(); ++i) + { + if (!m_config_wiimote_routing_enabled || !m_config_wiimote_output_enabled[i]) + m_wiimote_speaker_mixers[i].Mix(samples, num_samples); + } m_skylander_portal_mixer.Mix(samples, num_samples); for (auto& mixer : m_gba_mixers) mixer.Mix(samples, num_samples); @@ -258,10 +263,10 @@ void Mixer::PushStreamingSamples(const s16* samples, std::size_t num_samples) } } -void Mixer::PushWiimoteSpeakerSamples(const s16* samples, std::size_t num_samples, - u32 sample_rate_divisor) +void Mixer::PushWiimoteSpeakerSamples(std::size_t wiimote_index, const s16* samples, + std::size_t num_samples, u32 sample_rate_divisor) { - if (!IsOutputSampleRateValid()) + if (!IsOutputSampleRateValid() || wiimote_index >= m_wiimote_speaker_mixers.size()) return; // Max 20 bytes/speaker report, may be 4-bit ADPCM so multiply by 2 @@ -273,7 +278,7 @@ void Mixer::PushWiimoteSpeakerSamples(const s16* samples, std::size_t num_sample MAX_SPEAKER_SAMPLES); if (num_samples <= MAX_SPEAKER_SAMPLES) { - m_wiimote_speaker_mixer.SetInputSampleRateDivisor(sample_rate_divisor); + m_wiimote_speaker_mixers[wiimote_index].SetInputSampleRateDivisor(sample_rate_divisor); for (std::size_t i = 0; i < num_samples; ++i) { @@ -281,10 +286,21 @@ void Mixer::PushWiimoteSpeakerSamples(const s16* samples, std::size_t num_sample samples_stereo[i * 2 + 1] = samples[i]; } - m_wiimote_speaker_mixer.PushSamples(samples_stereo.data(), num_samples); + m_wiimote_speaker_mixers[wiimote_index].PushSamples(samples_stereo.data(), num_samples); } } +std::size_t Mixer::MixWiimoteSpeaker(std::size_t wiimote_index, s16* samples, + std::size_t num_samples) +{ + if (!samples || wiimote_index >= m_wiimote_speaker_mixers.size()) + return 0; + + memset(samples, 0, num_samples * 2 * sizeof(s16)); + m_wiimote_speaker_mixers[wiimote_index].Mix(samples, num_samples); + return num_samples; +} + void Mixer::PushSkylanderPortalSamples(const u8* samples, std::size_t num_samples) { if (!IsOutputSampleRateValid()) @@ -342,9 +358,10 @@ void Mixer::SetStreamingVolume(u32 lvolume, u32 rvolume) std::clamp(rvolume, 0x00, 0xff)); } -void Mixer::SetWiimoteSpeakerVolume(u32 lvolume, u32 rvolume) +void Mixer::SetWiimoteSpeakerVolume(std::size_t wiimote_index, u32 lvolume, u32 rvolume) { - m_wiimote_speaker_mixer.SetVolume(lvolume, rvolume); + if (wiimote_index < m_wiimote_speaker_mixers.size()) + m_wiimote_speaker_mixers[wiimote_index].SetVolume(lvolume, rvolume); } void Mixer::SetGBAVolume(std::size_t device_number, u32 lvolume, u32 rvolume) @@ -433,6 +450,9 @@ void Mixer::RefreshConfig() m_config_audio_preserve_pitch = Config::Get(Config::MAIN_AUDIO_PRESERVE_PITCH); m_config_fill_audio_gaps = Config::Get(Config::MAIN_AUDIO_FILL_GAPS); m_config_audio_buffer_ms = Config::Get(Config::MAIN_AUDIO_BUFFER_SIZE); + m_config_wiimote_routing_enabled = Config::Get(Config::MAIN_WIIMOTE_AUDIO_ROUTING_ENABLED); + for (std::size_t i = 0; i < m_config_wiimote_output_enabled.size(); ++i) + m_config_wiimote_output_enabled[i] = Config::Get(Config::MAIN_WIIMOTE_AUDIO_OUTPUT_ENABLED[i]); } void Mixer::MixerFifo::DoState(PointerWrap& p) diff --git a/Source/Core/AudioCommon/Mixer.h b/Source/Core/AudioCommon/Mixer.h index 87939895e2..217b59b9eb 100644 --- a/Source/Core/AudioCommon/Mixer.h +++ b/Source/Core/AudioCommon/Mixer.h @@ -29,8 +29,8 @@ public: // Called from main thread void PushSamples(const s16* samples, std::size_t num_samples); void PushStreamingSamples(const s16* samples, std::size_t num_samples); - void PushWiimoteSpeakerSamples(const s16* samples, std::size_t num_samples, - u32 sample_rate_divisor); + void PushWiimoteSpeakerSamples(std::size_t wiimote_index, const s16* samples, + std::size_t num_samples, u32 sample_rate_divisor); void PushSkylanderPortalSamples(const u8* samples, std::size_t num_samples); void PushGBASamples(std::size_t device_number, const s16* samples, std::size_t num_samples); @@ -45,7 +45,8 @@ public: void SetGBAInputSampleRateDivisors(std::size_t device_number, u32 rate_divisor); void SetStreamingVolume(u32 lvolume, u32 rvolume); - void SetWiimoteSpeakerVolume(u32 lvolume, u32 rvolume); + void SetWiimoteSpeakerVolume(std::size_t wiimote_index, u32 lvolume, u32 rvolume); + std::size_t MixWiimoteSpeaker(std::size_t wiimote_index, s16* samples, std::size_t num_samples); void SetGBAVolume(std::size_t device_number, u32 lvolume, u32 rvolume); void StartLogDTKAudio(const std::string& filename); @@ -146,7 +147,11 @@ private: MixerFifo m_dma_mixer{this, FIXED_SAMPLE_RATE_DIVIDEND / 32000, false}; MixerFifo m_streaming_mixer{this, FIXED_SAMPLE_RATE_DIVIDEND / 48000, false}; - MixerFifo m_wiimote_speaker_mixer{this, FIXED_SAMPLE_RATE_DIVIDEND / 3000, true}; + std::array m_wiimote_speaker_mixers{ + MixerFifo{this, FIXED_SAMPLE_RATE_DIVIDEND / 3000, true}, + MixerFifo{this, FIXED_SAMPLE_RATE_DIVIDEND / 3000, true}, + MixerFifo{this, FIXED_SAMPLE_RATE_DIVIDEND / 3000, true}, + MixerFifo{this, FIXED_SAMPLE_RATE_DIVIDEND / 3000, true}}; MixerFifo m_skylander_portal_mixer{this, FIXED_SAMPLE_RATE_DIVIDEND / 8000, true}; std::array m_gba_mixers{MixerFifo{this, FIXED_SAMPLE_RATE_DIVIDEND / 48000, true}, MixerFifo{this, FIXED_SAMPLE_RATE_DIVIDEND / 48000, true}, @@ -166,6 +171,8 @@ private: bool m_config_audio_preserve_pitch; bool m_config_fill_audio_gaps; int m_config_audio_buffer_ms; + bool m_config_wiimote_routing_enabled = false; + std::array m_config_wiimote_output_enabled{}; Config::ConfigChangedCallbackID m_config_changed_callback_id; }; diff --git a/Source/Core/Core/Config/MainSettings.cpp b/Source/Core/Core/Config/MainSettings.cpp index 3d46d756bf..3ca5d28df5 100644 --- a/Source/Core/Core/Config/MainSettings.cpp +++ b/Source/Core/Core/Config/MainSettings.cpp @@ -700,6 +700,21 @@ static std::string GetDefaultTriforceIPRedirections() const Info MAIN_TRIFORCE_IP_REDIRECTIONS{ {System::Main, "Core", "TriforceIPRedirections"}, GetDefaultTriforceIPRedirections()}; +// Main.WiimoteAudioRouting + +const Info MAIN_WIIMOTE_AUDIO_ROUTING_ENABLED{ + {System::Main, "Core", "WiimoteAudioRoutingEnabled"}, false}; +const std::array, WIIMOTE_SPEAKER_COUNT> MAIN_WIIMOTE_AUDIO_OUTPUT_ENABLED{ + Info{{System::Main, "Core", "Wiimote1AudioOutputEnabled"}, false}, + Info{{System::Main, "Core", "Wiimote2AudioOutputEnabled"}, false}, + Info{{System::Main, "Core", "Wiimote3AudioOutputEnabled"}, false}, + Info{{System::Main, "Core", "Wiimote4AudioOutputEnabled"}, false}}; +const std::array, WIIMOTE_SPEAKER_COUNT> MAIN_WIIMOTE_AUDIO_OUTPUT_DEVICE{ + Info{{System::Main, "Core", "Wiimote1AudioOutputDevice"}, ""}, + Info{{System::Main, "Core", "Wiimote2AudioOutputDevice"}, ""}, + Info{{System::Main, "Core", "Wiimote3AudioOutputDevice"}, ""}, + Info{{System::Main, "Core", "Wiimote4AudioOutputDevice"}, ""}}; + // 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 a1baf26242..ba99ee754b 100644 --- a/Source/Core/Core/Config/MainSettings.h +++ b/Source/Core/Core/Config/MainSettings.h @@ -396,6 +396,14 @@ extern const std::array, EMULATED_LOGITECH_MIC_COUNT> MAIN_LOGITECH_MI extern const Info MAIN_TRIFORCE_IP_REDIRECTIONS; +// Main.WiimoteAudioRouting + +static constexpr std::size_t WIIMOTE_SPEAKER_COUNT = 4; + +extern const Info MAIN_WIIMOTE_AUDIO_ROUTING_ENABLED; +extern const std::array, WIIMOTE_SPEAKER_COUNT> MAIN_WIIMOTE_AUDIO_OUTPUT_ENABLED; +extern const std::array, WIIMOTE_SPEAKER_COUNT> MAIN_WIIMOTE_AUDIO_OUTPUT_DEVICE; + // GameCube path utility functions // Replaces NTSC-K with some other region, and doesn't replace non-NTSC-K regions diff --git a/Source/Core/Core/HW/WiimoteEmu/Speaker.cpp b/Source/Core/Core/HW/WiimoteEmu/Speaker.cpp index 0394ae9236..7fec3bd651 100644 --- a/Source/Core/Core/HW/WiimoteEmu/Speaker.cpp +++ b/Source/Core/Core/HW/WiimoteEmu/Speaker.cpp @@ -129,12 +129,13 @@ void SpeakerLogic::SpeakerData(const u8* data, int length, float speaker_pan) auto& system = Core::System::GetInstance(); SoundStream* sound_stream = system.GetSoundStream(); - sound_stream->GetMixer()->SetWiimoteSpeakerVolume(l_volume, r_volume); + sound_stream->GetMixer()->SetWiimoteSpeakerVolume(m_wiimote_index, l_volume, r_volume); // ADPCM sample rate is thought to be x2.(3000 x2 = 6000). const unsigned int sample_rate = sample_rate_dividend / reg_data.sample_rate; sound_stream->GetMixer()->PushWiimoteSpeakerSamples( - samples.data(), sample_length, Mixer::FIXED_SAMPLE_RATE_DIVIDEND / (sample_rate * 2)); + m_wiimote_index, samples.data(), sample_length, + Mixer::FIXED_SAMPLE_RATE_DIVIDEND / (sample_rate * 2)); } void SpeakerLogic::Reset() diff --git a/Source/Core/Core/HW/WiimoteEmu/Speaker.h b/Source/Core/Core/HW/WiimoteEmu/Speaker.h index 9a6a652bb1..445ef23f92 100644 --- a/Source/Core/Core/HW/WiimoteEmu/Speaker.h +++ b/Source/Core/Core/HW/WiimoteEmu/Speaker.h @@ -30,6 +30,7 @@ public: void DoState(PointerWrap& p); void SetSpeakerEnabled(bool enabled); + void SetWiimoteIndex(u8 index) { m_wiimote_index = index; } private: // Pan is -1.0 to +1.0 @@ -75,6 +76,7 @@ private: ControllerEmu::SettingValue m_speaker_pan_setting; bool m_speaker_enabled = false; + u8 m_wiimote_index = 0; }; } // namespace WiimoteEmu diff --git a/Source/Core/Core/HW/WiimoteEmu/WiimoteEmu.cpp b/Source/Core/Core/HW/WiimoteEmu/WiimoteEmu.cpp index 7fc81f6321..bbbea66e37 100644 --- a/Source/Core/Core/HW/WiimoteEmu/WiimoteEmu.cpp +++ b/Source/Core/Core/HW/WiimoteEmu/WiimoteEmu.cpp @@ -205,6 +205,8 @@ void Wiimote::Reset() Wiimote::Wiimote(const unsigned int index) : m_index(index), m_bt_device_index(index) { + m_speaker_logic.SetWiimoteIndex(m_index); + using Translatability = ControllerEmu::Translatability; // Buttons diff --git a/Source/Core/Core/State.cpp b/Source/Core/Core/State.cpp index 29aa1f3aeb..4db39e9096 100644 --- a/Source/Core/Core/State.cpp +++ b/Source/Core/Core/State.cpp @@ -95,7 +95,7 @@ struct CompressAndDumpStateArgs static Common::WorkQueueThreadSP s_compress_and_dump_thread; // Don't forget to increase this after doing changes on the savestate system -constexpr u32 STATE_VERSION = 181; // Last changed in PR 14400 +constexpr u32 STATE_VERSION = 182; // Last changed in PR 14448 // Increase this if the StateExtendedHeader definition changes constexpr u32 EXTENDED_HEADER_VERSION = 1; // Last changed in PR 12217 diff --git a/Source/Core/DolphinQt/Settings/AudioPane.cpp b/Source/Core/DolphinQt/Settings/AudioPane.cpp index 435e8f24d5..e4466830f0 100644 --- a/Source/Core/DolphinQt/Settings/AudioPane.cpp +++ b/Source/Core/DolphinQt/Settings/AudioPane.cpp @@ -17,12 +17,19 @@ #include #include #include +#include #include "AudioCommon/AudioCommon.h" #include "AudioCommon/WASAPIStream.h" +#ifdef HAVE_CUBEB +#include "AudioCommon/CubebUtils.h" +#endif + #include "Core/Config/MainSettings.h" +#include "Core/Config/WiimoteSettings.h" #include "Core/Core.h" +#include "Core/HW/Wiimote.h" #include "Core/System.h" #include "DolphinQt/Config/ConfigControls/ConfigBool.h" #include "DolphinQt/Config/ConfigControls/ConfigChoice.h" @@ -200,11 +207,48 @@ void AudioPane::CreateWidgets() playback_layout->setRowStretch(4, 1); playback_box->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); + // Wiimote Audio Routing +#ifdef HAVE_CUBEB + m_wiimote_routing_box = new QGroupBox(tr("Wii Remote Audio Routing")); + auto* wiimote_routing_layout = new QVBoxLayout; + m_wiimote_routing_box->setLayout(wiimote_routing_layout); + + m_wiimote_routing_enable = new ConfigBool(tr("Enable Wii Remote Audio Routing"), + Config::MAIN_WIIMOTE_AUDIO_ROUTING_ENABLED); + wiimote_routing_layout->addWidget(m_wiimote_routing_enable); + + // Build device list once for all wiimote dropdowns + std::vector> output_devices; + output_devices.emplace_back(tr("Default Device"), QStringLiteral("")); + for (const auto& [id, name] : CubebUtils::ListOutputDevices()) + output_devices.emplace_back(QString::fromStdString(name), QString::fromStdString(id)); + + for (std::size_t i = 0; i < 4; ++i) + { + auto* row_widget = new QWidget; + auto* row_layout = new QHBoxLayout(row_widget); + row_layout->setContentsMargins(0, 0, 0, 0); + + m_wiimote_output_enable[i] = new ConfigBool(tr("Wii Remote %1").arg(i + 1), + Config::MAIN_WIIMOTE_AUDIO_OUTPUT_ENABLED[i]); + m_wiimote_output_device[i] = + new ConfigStringChoice(output_devices, Config::MAIN_WIIMOTE_AUDIO_OUTPUT_DEVICE[i]); + + row_layout->addWidget(m_wiimote_output_enable[i]); + row_layout->addWidget(m_wiimote_output_device[i], 1); + + wiimote_routing_layout->addWidget(row_widget); + } +#endif + auto* const main_vbox_layout = new QVBoxLayout; main_vbox_layout->addWidget(dsp_box); main_vbox_layout->addWidget(backend_box); main_vbox_layout->addWidget(playback_box); +#ifdef HAVE_CUBEB + main_vbox_layout->addWidget(m_wiimote_routing_box); +#endif m_main_layout = new QHBoxLayout; m_main_layout->addLayout(main_vbox_layout); @@ -227,6 +271,20 @@ void AudioPane::ConnectWidgets() connect(m_latency_slider, &QSlider::valueChanged, this, [this](int value) { m_latency_label->setText(tr("Latency: %1 ms").arg(value)); }); } + +#ifdef HAVE_CUBEB + connect(m_wiimote_routing_enable, &ConfigBool::toggled, this, + [this](bool) { UpdateWiimoteRoutingEnabled(); }); + for (std::size_t i = 0; i < 4; ++i) + { + connect(m_wiimote_output_enable[i], &ConfigBool::toggled, this, + [this](bool) { UpdateWiimoteRoutingEnabled(); }); + } + // Also react to external config changes: wiimote source type, speaker data, BT passthrough. + connect(&Settings::Instance(), &Settings::ConfigChanged, this, + &AudioPane::UpdateWiimoteRoutingEnabled); + UpdateWiimoteRoutingEnabled(); +#endif } void AudioPane::OnDspChanged() @@ -259,6 +317,10 @@ void AudioPane::OnBackendChanged() m_volume_slider->setEnabled(AudioCommon::SupportsVolumeChanges(backend)); m_volume_indicator->setEnabled(AudioCommon::SupportsVolumeChanges(backend)); + +#ifdef HAVE_CUBEB + UpdateWiimoteRoutingEnabled(); +#endif } void AudioPane::OnEmulationStateChanged(bool running) @@ -283,6 +345,41 @@ void AudioPane::OnEmulationStateChanged(bool running) #ifdef _WIN32 m_wasapi_device_combo->setEnabled(!running); #endif + +#ifdef HAVE_CUBEB + UpdateWiimoteRoutingEnabled(); +#endif +} + +void AudioPane::UpdateWiimoteRoutingEnabled() +{ +#ifdef HAVE_CUBEB + if (!m_wiimote_routing_box) + return; + + const bool running = Core::GetState(Core::System::GetInstance()) != Core::State::Uninitialized; + const bool is_cubeb = Config::Get(Config::MAIN_AUDIO_BACKEND) == BACKEND_CUBEB; + const bool speaker_enabled = Config::Get(Config::MAIN_WIIMOTE_ENABLE_SPEAKER); + const bool bt_passthrough = Config::Get(Config::MAIN_BLUETOOTH_PASSTHROUGH_ENABLED); + + // The entire group requires Cubeb backend, speaker data enabled, no BT passthrough, and + // emulation not running. + const bool group_usable = !running && is_cubeb && speaker_enabled && !bt_passthrough; + + m_wiimote_routing_enable->setEnabled(group_usable); + + const bool routing_on = group_usable && m_wiimote_routing_enable->isChecked(); + + for (std::size_t i = 0; i < 4; ++i) + { + const WiimoteSource source = Config::Get(Config::GetInfoForWiimoteSource(static_cast(i))); + const bool is_emulated = source == WiimoteSource::Emulated; + + m_wiimote_output_enable[i]->setEnabled(routing_on && is_emulated); + m_wiimote_output_device[i]->setEnabled(routing_on && is_emulated && + m_wiimote_output_enable[i]->isChecked()); + } +#endif } void AudioPane::CheckNeedForLatencyControl() @@ -368,4 +465,33 @@ void AudioPane::AddDescriptions() m_audio_preserve_pitch->SetTitle(tr("Preserve Audio Pitch")); m_audio_preserve_pitch->SetDescription(tr(TR_PRESERVE_AUDIO_PITCH_DESCRIPTION)); + +#ifdef HAVE_CUBEB + static const char TR_WIIMOTE_ROUTING_DESCRIPTION[] = + QT_TR_NOOP("Routes each Wii Remote's speaker audio to a separate audio output device. " + "The main audio output continues to work normally; only the Wii Remote speaker " + "audio is redirected. Requires a restart of emulation to take effect." + "

This setting is disabled when Enable Speaker Data is disabled, a " + "Passthrough a Bluetooth adapter is selected, or an audio backend other than " + "Cubeb is selected." + "

If unsure, leave this unchecked."); + static const char TR_WIIMOTE_OUTPUT_ENABLE_DESCRIPTION[] = + QT_TR_NOOP("Enables routing this Wii Remote's speaker audio to a separate output device."); + static const char TR_WIIMOTE_OUTPUT_DEVICE_DESCRIPTION[] = + QT_TR_NOOP("Selects the audio output device for this Wii Remote's speaker audio." + "

This setting is disabled when this Wii Remote is not set to Emulated " + "Wii Remote."); + + if (m_wiimote_routing_box) + { + m_wiimote_routing_enable->SetTitle(tr("Enable Wii Remote Audio Routing")); + m_wiimote_routing_enable->SetDescription(tr(TR_WIIMOTE_ROUTING_DESCRIPTION)); + for (std::size_t i = 0; i < 4; ++i) + { + m_wiimote_output_enable[i]->SetDescription(tr(TR_WIIMOTE_OUTPUT_ENABLE_DESCRIPTION)); + m_wiimote_output_device[i]->SetTitle(tr("Wii Remote %1").arg(i + 1)); + m_wiimote_output_device[i]->SetDescription(tr(TR_WIIMOTE_OUTPUT_DEVICE_DESCRIPTION)); + } + } +#endif } diff --git a/Source/Core/DolphinQt/Settings/AudioPane.h b/Source/Core/DolphinQt/Settings/AudioPane.h index 4896f0e446..5938e4caa7 100644 --- a/Source/Core/DolphinQt/Settings/AudioPane.h +++ b/Source/Core/DolphinQt/Settings/AudioPane.h @@ -3,6 +3,8 @@ #pragma once +#include + #include namespace AudioCommon @@ -16,6 +18,7 @@ class ConfigComplexChoice; class ConfigRadioBool; class ConfigSlider; class ConfigStringChoice; +class QGroupBox; class QHBoxLayout; class QLabel; class QRadioButton; @@ -37,6 +40,8 @@ private: void OnBackendChanged(); void OnDspChanged(); + void UpdateWiimoteRoutingEnabled(); + void CheckNeedForLatencyControl(); bool m_latency_control_supported; @@ -68,4 +73,10 @@ private: ConfigBool* m_audio_fill_gaps; ConfigBool* m_audio_preserve_pitch; ConfigBool* m_speed_up_mute_enable; + + // Wiimote Audio Routing + QGroupBox* m_wiimote_routing_box = nullptr; + ConfigBool* m_wiimote_routing_enable = nullptr; + std::array m_wiimote_output_enable{}; + std::array m_wiimote_output_device{}; };