summaryrefslogtreecommitdiff
path: root/Source/Core
diff options
context:
space:
mode:
authorJMC47 <JMC4789@gmail.com>2026-05-14 18:36:49 -0400
committerGitHub <noreply@github.com>2026-05-14 18:36:49 -0400
commit604d13e1ccbb7a5dfd9f50d1b3a79ed64b033c59 (patch)
treeb08e40705d53ce535db8fc28a127f37368b0d501 /Source/Core
parentd0d354fbed4999c1c7183d4593bab15601c3d5e9 (diff)
parentadcfbda2a3436151771c80f654c5c490ad9478a3 (diff)
Merge pull request #14448 from TheShrubMaster/main
AudioCommon: Add individual Wiimote audio mixer
Diffstat (limited to 'Source/Core')
-rw-r--r--Source/Core/AudioCommon/CubebStream.cpp82
-rw-r--r--Source/Core/AudioCommon/CubebStream.h13
-rw-r--r--Source/Core/AudioCommon/CubebUtils.cpp70
-rw-r--r--Source/Core/AudioCommon/CubebUtils.h2
-rw-r--r--Source/Core/AudioCommon/Mixer.cpp38
-rw-r--r--Source/Core/AudioCommon/Mixer.h17
-rw-r--r--Source/Core/Core/Config/MainSettings.cpp15
-rw-r--r--Source/Core/Core/Config/MainSettings.h8
-rw-r--r--Source/Core/Core/HW/WiimoteEmu/Speaker.cpp5
-rw-r--r--Source/Core/Core/HW/WiimoteEmu/Speaker.h2
-rw-r--r--Source/Core/Core/HW/WiimoteEmu/WiimoteEmu.cpp2
-rw-r--r--Source/Core/Core/State.cpp2
-rw-r--r--Source/Core/DolphinQt/Settings/AudioPane.cpp125
-rw-r--r--Source/Core/DolphinQt/Settings/AudioPane.h11
14 files changed, 375 insertions, 17 deletions
diff --git a/Source/Core/AudioCommon/CubebStream.cpp b/Source/Core/AudioCommon/CubebStream.cpp
index 2a04985e3b..adc947b426 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 <Objbase.h>
@@ -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<const WiimoteStreamData*>(user_data);
+ data->self->m_mixer->MixWiimoteSpeaker(data->wiimote_index, static_cast<short*>(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,50 @@ bool CubebStream::Init()
cubeb_stream_init(m_ctx.get(), &m_stream, "Dolphin Audio Output", nullptr, nullptr,
nullptr, &params, 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<cubeb_devid>(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 +164,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 +195,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 <array>
#include <memory>
#include <vector>
@@ -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<cubeb> m_ctx;
cubeb_stream* m_stream = nullptr;
+ std::array<WiimoteStreamData, 4> m_wiimote_stream_data{};
+ std::array<cubeb_stream*, 4> m_wiimote_streams{};
std::vector<short> m_short_buffer;
std::vector<float> 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 fda93ac4c6..85fb44767e 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<std::pair<std::string, std::string>> ListOutputDevices()
+{
+ std::vector<std::pair<std::string, std::string>> 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<cubeb> GetContext();
std::vector<std::pair<std::string, std::string>> ListInputDevices();
const void* GetInputDeviceById(std::string_view id);
+std::vector<std::pair<std::string, std::string>> 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 1229d8faa1..a57b6024b7 100644
--- a/Source/Core/AudioCommon/Mixer.cpp
+++ b/Source/Core/AudioCommon/Mixer.cpp
@@ -53,7 +53,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);
@@ -254,22 +259,33 @@ 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;
// WiimoteEmu produces host-endian mono samples.
- m_wiimote_speaker_mixer.SetInputSampleRateDivisor(sample_rate_divisor);
+ m_wiimote_speaker_mixers[wiimote_index].SetInputSampleRateDivisor(sample_rate_divisor);
for (const s16 sample : std::span{samples, num_samples})
{
- m_wiimote_speaker_mixer.PushSample(sample, sample);
+ m_wiimote_speaker_mixers[wiimote_index].PushSample(sample, sample);
}
}
+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())
@@ -322,9 +338,10 @@ void Mixer::SetStreamingVolume(u32 lvolume, u32 rvolume)
std::clamp<u32>(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)
@@ -413,6 +430,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 6e5a52eecd..aca334bcf8 100644
--- a/Source/Core/AudioCommon/Mixer.h
+++ b/Source/Core/AudioCommon/Mixer.h
@@ -30,8 +30,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);
@@ -46,7 +46,8 @@ public:
void SetGBAInputSampleRate(std::size_t device_number, u32 sample_rate);
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);
@@ -166,7 +167,12 @@ private:
MixerFifo m_dma_mixer{this, FIXED_SAMPLE_RATE_DIVIDEND / 32000};
MixerFifo m_streaming_mixer{this, FIXED_SAMPLE_RATE_DIVIDEND / 48000};
- MixerFifo m_wiimote_speaker_mixer{this, FIXED_SAMPLE_RATE_DIVIDEND / 3000};
+ std::array<MixerFifo, 4> m_wiimote_speaker_mixers{
+ MixerFifo{this, FIXED_SAMPLE_RATE_DIVIDEND / 3000},
+ MixerFifo{this, FIXED_SAMPLE_RATE_DIVIDEND / 3000},
+ MixerFifo{this, FIXED_SAMPLE_RATE_DIVIDEND / 3000},
+ MixerFifo{this, FIXED_SAMPLE_RATE_DIVIDEND / 3000},
+ };
MixerFifo m_skylander_portal_mixer{this, FIXED_SAMPLE_RATE_DIVIDEND / 8000};
// GBAs generally use a 65536 sample rate which is not a factor of our FIXED_SAMPLE_RATE_DIVIDEND.
@@ -178,7 +184,6 @@ private:
MixerFifo{this, GBA_SAMPLE_RATE_DIVIDEND / 65536, GBA_SAMPLE_RATE_DIVIDEND},
MixerFifo{this, GBA_SAMPLE_RATE_DIVIDEND / 65536, GBA_SAMPLE_RATE_DIVIDEND},
};
-
u32 m_output_sample_rate;
AudioCommon::SurroundDecoder m_surround_decoder;
@@ -193,6 +198,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<bool, 4> 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 24e2c19571..f0cc8511b7 100644
--- a/Source/Core/Core/Config/MainSettings.cpp
+++ b/Source/Core/Core/Config/MainSettings.cpp
@@ -701,6 +701,21 @@ static std::string GetDefaultTriforceIPRedirections()
const Info<std::string> MAIN_TRIFORCE_IP_REDIRECTIONS{
{System::Main, "Core", "TriforceIPRedirections"}, GetDefaultTriforceIPRedirections()};
+// Main.WiimoteAudioRouting
+
+const Info<bool> MAIN_WIIMOTE_AUDIO_ROUTING_ENABLED{
+ {System::Main, "Core", "WiimoteAudioRoutingEnabled"}, false};
+const std::array<Info<bool>, WIIMOTE_SPEAKER_COUNT> MAIN_WIIMOTE_AUDIO_OUTPUT_ENABLED{
+ Info<bool>{{System::Main, "Core", "Wiimote1AudioOutputEnabled"}, false},
+ Info<bool>{{System::Main, "Core", "Wiimote2AudioOutputEnabled"}, false},
+ Info<bool>{{System::Main, "Core", "Wiimote3AudioOutputEnabled"}, false},
+ Info<bool>{{System::Main, "Core", "Wiimote4AudioOutputEnabled"}, false}};
+const std::array<Info<std::string>, WIIMOTE_SPEAKER_COUNT> MAIN_WIIMOTE_AUDIO_OUTPUT_DEVICE{
+ Info<std::string>{{System::Main, "Core", "Wiimote1AudioOutputDevice"}, ""},
+ Info<std::string>{{System::Main, "Core", "Wiimote2AudioOutputDevice"}, ""},
+ Info<std::string>{{System::Main, "Core", "Wiimote3AudioOutputDevice"}, ""},
+ Info<std::string>{{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 22c87ba110..16cae73e15 100644
--- a/Source/Core/Core/Config/MainSettings.h
+++ b/Source/Core/Core/Config/MainSettings.h
@@ -397,6 +397,14 @@ extern const std::array<Info<s16>, EMULATED_LOGITECH_MIC_COUNT> MAIN_LOGITECH_MI
extern const Info<std::string> MAIN_TRIFORCE_IP_REDIRECTIONS;
+// Main.WiimoteAudioRouting
+
+static constexpr std::size_t WIIMOTE_SPEAKER_COUNT = 4;
+
+extern const Info<bool> MAIN_WIIMOTE_AUDIO_ROUTING_ENABLED;
+extern const std::array<Info<bool>, WIIMOTE_SPEAKER_COUNT> MAIN_WIIMOTE_AUDIO_OUTPUT_ENABLED;
+extern const std::array<Info<std::string>, 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<double> 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 1d7ff9eb6a..5724240ffd 100644
--- a/Source/Core/Core/State.cpp
+++ b/Source/Core/Core/State.cpp
@@ -95,7 +95,7 @@ struct CompressAndDumpStateArgs
static Common::WorkQueueThreadSP<CompressAndDumpStateArgs> s_compress_and_dump_thread;
// Don't forget to increase this after doing changes on the savestate system
-constexpr u32 STATE_VERSION = 189; // Last changed in PR 14560
+constexpr u32 STATE_VERSION = 190; // 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 e48faa7c01..86fd400ebe 100644
--- a/Source/Core/DolphinQt/Settings/AudioPane.cpp
+++ b/Source/Core/DolphinQt/Settings/AudioPane.cpp
@@ -17,12 +17,19 @@
#include <QSpacerItem>
#include <QString>
#include <QVBoxLayout>
+#include <QWidget>
#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"
@@ -201,11 +208,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<std::pair<QString, QString>> 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);
@@ -228,6 +272,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()
@@ -260,6 +318,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)
@@ -284,6 +346,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<int>(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()
@@ -369,4 +466,32 @@ 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. "
+ "This setting cannot be changed while emulation is active."
+ "<br><br>This setting is disabled when Enable Speaker Data is disabled, "
+ "Passthrough a Bluetooth adapter is selected, or an audio backend other than "
+ "Cubeb is selected."
+ "<br><br><dolphin_emphasis>If unsure, leave this unchecked.</dolphin_emphasis>");
+ 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."
+ "<br><br>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 <array>
+
#include <QWidget>
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<ConfigBool*, 4> m_wiimote_output_enable{};
+ std::array<ConfigStringChoice*, 4> m_wiimote_output_device{};
};