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{}; };