From 0e6e00b1ef4fcc17025736e34389ac681dce193f Mon Sep 17 00:00:00 2001 From: William Sullivan Date: Sun, 20 Sep 2026 15:10:16 -0500 Subject: [PATCH] Integrated GBA Audio Routing --- Source/Core/AudioCommon/CubebStream.cpp | 74 +++++++++++++++ Source/Core/AudioCommon/CubebStream.h | 13 +++ Source/Core/AudioCommon/Mixer.cpp | 19 +++- Source/Core/AudioCommon/Mixer.h | 6 +- Source/Core/Core/Config/MainSettings.cpp | 17 ++++ Source/Core/Core/Config/MainSettings.h | 8 ++ Source/Core/DolphinQt/Settings/AudioPane.cpp | 94 ++++++++++++++++++++ Source/Core/DolphinQt/Settings/AudioPane.h | 9 ++ 8 files changed, 237 insertions(+), 3 deletions(-) diff --git a/Source/Core/AudioCommon/CubebStream.cpp b/Source/Core/AudioCommon/CubebStream.cpp index 0cdf7f3b99..47f334d537 100644 --- a/Source/Core/AudioCommon/CubebStream.cpp +++ b/Source/Core/AudioCommon/CubebStream.cpp @@ -49,6 +49,19 @@ void CubebStream::WiimoteStateCallback(cubeb_stream* stream, void* user_data, cu { } +long CubebStream::GBADataCallback(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->MixGBA(data->gba_index, static_cast(output_buffer), num_frames); + return num_frames; +} + +void CubebStream::GBAStateCallback(cubeb_stream* stream, void* user_data, cubeb_state state) +{ +} + CubebStream::CubebStream() #ifdef _WIN32 : m_work_queue("Cubeb Worker") @@ -145,6 +158,48 @@ bool CubebStream::Init() } } } + + // Create per-GBA streams for audio routing when enabled + if (return_value && Config::Get(Config::MAIN_GBA_AUDIO_ROUTING_ENABLED)) + { + static const char* const GBA_STREAM_NAMES[4] = { + "Dolphin GBA 1 Audio", "Dolphin GBA 2 Audio", "Dolphin GBA 3 Audio", + "Dolphin GBA 4 Audio"}; + + cubeb_stream_params gba_params{}; + gba_params.rate = m_mixer->GetSampleRate(); + gba_params.channels = 2; + gba_params.format = CUBEB_SAMPLE_S16NE; + gba_params.layout = CUBEB_LAYOUT_STEREO; + + for (std::size_t i = 0; i < m_gba_streams.size(); ++i) + { + if (!Config::Get(Config::MAIN_GBA_AUDIO_OUTPUT_ENABLED[i])) + continue; + + const std::string device_id_str = Config::Get(Config::MAIN_GBA_AUDIO_OUTPUT_DEVICE[i]); + const cubeb_devid output_devid = + device_id_str.empty() ? + nullptr : + static_cast(CubebUtils::GetOutputDeviceById(device_id_str)); + + u32 gba_min_latency = 0; + cubeb_get_min_latency(m_ctx.get(), &gba_params, &gba_min_latency); + + m_gba_stream_data[i] = {this, i}; + + const int result = cubeb_stream_init( + m_ctx.get(), &m_gba_streams[i], GBA_STREAM_NAMES[i], nullptr, nullptr, output_devid, + &gba_params, std::max(BUFFER_SAMPLES, gba_min_latency), GBADataCallback, + GBAStateCallback, &m_gba_stream_data[i]); + + if (result != CUBEB_OK) + { + ERROR_LOG_FMT(AUDIO, "Failed to create Cubeb stream for GBA {} audio split", i + 1); + m_gba_streams[i] = nullptr; + } + } + } } #ifdef _WIN32 @@ -171,6 +226,11 @@ bool CubebStream::SetRunning(bool running) if (ws) cubeb_stream_start(ws); } + for (auto& gs : m_gba_streams) + { + if (gs) + cubeb_stream_start(gs); + } } else { @@ -180,6 +240,11 @@ bool CubebStream::SetRunning(bool running) if (ws) cubeb_stream_stop(ws); } + for (auto& gs : m_gba_streams) + { + if (gs) + cubeb_stream_stop(gs); + } } #ifdef _WIN32 }); @@ -204,6 +269,15 @@ CubebStream::~CubebStream() ws = nullptr; } } + for (auto& gs : m_gba_streams) + { + if (gs) + { + cubeb_stream_stop(gs); + cubeb_stream_destroy(gs); + gs = nullptr; + } + } #ifdef _WIN32 if (m_should_couninit) { diff --git a/Source/Core/AudioCommon/CubebStream.h b/Source/Core/AudioCommon/CubebStream.h index 1c95fd50d8..4bc0070e88 100644 --- a/Source/Core/AudioCommon/CubebStream.h +++ b/Source/Core/AudioCommon/CubebStream.h @@ -16,6 +16,8 @@ #include #endif +#include "Core/Config/MainSettings.h" + class CubebStream final : public SoundStream { #ifdef HAVE_CUBEB @@ -38,11 +40,19 @@ private: std::size_t wiimote_index; }; + struct GBAStreamData + { + CubebStream* self; + std::size_t gba_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::array m_gba_stream_data{}; + std::array m_gba_streams{}; std::vector m_short_buffer; std::vector m_floatstereo_buffer; @@ -60,5 +70,8 @@ private: const void* /*input_buffer*/, void* output_buffer, long num_frames); static void WiimoteStateCallback(cubeb_stream* stream, void* user_data, cubeb_state state); + static long GBADataCallback(cubeb_stream* stream, void* user_data, const void* /*input_buffer*/, + void* output_buffer, long num_frames); + static void GBAStateCallback(cubeb_stream* stream, void* user_data, cubeb_state state); #endif }; diff --git a/Source/Core/AudioCommon/Mixer.cpp b/Source/Core/AudioCommon/Mixer.cpp index a57b6024b7..fe7f0e7349 100644 --- a/Source/Core/AudioCommon/Mixer.cpp +++ b/Source/Core/AudioCommon/Mixer.cpp @@ -176,8 +176,11 @@ std::size_t Mixer::Mix(s16* samples, std::size_t num_samples) 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); + for (std::size_t i = 0; i < m_gba_mixers.size(); ++i) + { + if (!m_config_gba_routing_enabled || !m_config_gba_output_enabled[i]) + m_gba_mixers[i].Mix(samples, num_samples); + } return num_samples; } @@ -317,6 +320,15 @@ void Mixer::PushGBASamples(std::size_t device_number, const s16* samples, std::s } } +std::size_t Mixer::MixGBA(std::size_t device_number, s16* samples, std::size_t num_samples) +{ + if (!samples || device_number >= m_gba_mixers.size()) + return 0; + memset(samples, 0, num_samples * 2 * sizeof(s16)); + m_gba_mixers[device_number].Mix(samples, num_samples); + return num_samples; +} + void Mixer::SetDMAInputSampleRateDivisor(u32 rate_divisor) { m_dma_mixer.SetInputSampleRateDivisor(rate_divisor); @@ -433,6 +445,9 @@ void Mixer::RefreshConfig() 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]); + m_config_gba_routing_enabled = Config::Get(Config::MAIN_GBA_AUDIO_ROUTING_ENABLED); + for (std::size_t i = 0; i < m_config_gba_output_enabled.size(); ++i) + m_config_gba_output_enabled[i] = Config::Get(Config::MAIN_GBA_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 aca334bcf8..26e179f03b 100644 --- a/Source/Core/AudioCommon/Mixer.h +++ b/Source/Core/AudioCommon/Mixer.h @@ -12,6 +12,7 @@ #include "Common/CommonTypes.h" #include "Common/Config/Config.h" #include "Common/Inline.h" +#include "Core/Config/MainSettings.h" class PointerWrap; @@ -49,6 +50,7 @@ public: 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); + std::size_t MixGBA(std::size_t device_number, s16* samples, std::size_t num_samples); void StartLogDTKAudio(const std::string& filename); void StopLogDTKAudio(); @@ -178,7 +180,7 @@ private: // GBAs generally use a 65536 sample rate which is not a factor of our FIXED_SAMPLE_RATE_DIVIDEND. static constexpr u32 GBA_SAMPLE_RATE_DIVIDEND = 0x1000000; - std::array m_gba_mixers{ + std::array m_gba_mixers{ MixerFifo{this, GBA_SAMPLE_RATE_DIVIDEND / 65536, GBA_SAMPLE_RATE_DIVIDEND}, MixerFifo{this, GBA_SAMPLE_RATE_DIVIDEND / 65536, GBA_SAMPLE_RATE_DIVIDEND}, MixerFifo{this, GBA_SAMPLE_RATE_DIVIDEND / 65536, GBA_SAMPLE_RATE_DIVIDEND}, @@ -200,6 +202,8 @@ private: int m_config_audio_buffer_ms; bool m_config_wiimote_routing_enabled = false; std::array m_config_wiimote_output_enabled{}; + bool m_config_gba_routing_enabled = false; + std::array m_config_gba_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 f0cc8511b7..06bb536ffc 100644 --- a/Source/Core/Core/Config/MainSettings.cpp +++ b/Source/Core/Core/Config/MainSettings.cpp @@ -716,6 +716,23 @@ const std::array, WIIMOTE_SPEAKER_COUNT> MAIN_WIIMOTE_AUDIO_OU Info{{System::Main, "Core", "Wiimote3AudioOutputDevice"}, ""}, Info{{System::Main, "Core", "Wiimote4AudioOutputDevice"}, ""}}; +// Main.GBAAudioRouting + +const Info MAIN_GBA_AUDIO_ROUTING_ENABLED{{System::Main, "Core", "GBAAudioRoutingEnabled"}, + false}; +const std::array, GBA_SPEAKER_COUNT> MAIN_GBA_AUDIO_OUTPUT_ENABLED = { + Info{{System::Main, "Core", "GBA1AudioOutputEnabled"}, false}, + Info{{System::Main, "Core", "GBA2AudioOutputEnabled"}, false}, + Info{{System::Main, "Core", "GBA3AudioOutputEnabled"}, false}, + Info{{System::Main, "Core", "GBA4AudioOutputEnabled"}, false}, +}; +const std::array, GBA_SPEAKER_COUNT> MAIN_GBA_AUDIO_OUTPUT_DEVICE = { + Info{{System::Main, "Core", "GBA1AudioOutputDevice"}, ""}, + Info{{System::Main, "Core", "GBA2AudioOutputDevice"}, ""}, + Info{{System::Main, "Core", "GBA3AudioOutputDevice"}, ""}, + Info{{System::Main, "Core", "GBA4AudioOutputDevice"}, ""}, +}; + // 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 16cae73e15..ee6143b5b2 100644 --- a/Source/Core/Core/Config/MainSettings.h +++ b/Source/Core/Core/Config/MainSettings.h @@ -405,6 +405,14 @@ 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; +// Main.GBAAudioRouting + +static constexpr std::size_t GBA_SPEAKER_COUNT = 4; + +extern const Info MAIN_GBA_AUDIO_ROUTING_ENABLED; +extern const std::array, GBA_SPEAKER_COUNT> MAIN_GBA_AUDIO_OUTPUT_ENABLED; +extern const std::array, GBA_SPEAKER_COUNT> MAIN_GBA_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/DolphinQt/Settings/AudioPane.cpp b/Source/Core/DolphinQt/Settings/AudioPane.cpp index 86fd400ebe..f204fb7f03 100644 --- a/Source/Core/DolphinQt/Settings/AudioPane.cpp +++ b/Source/Core/DolphinQt/Settings/AudioPane.cpp @@ -29,6 +29,7 @@ #include "Core/Config/MainSettings.h" #include "Core/Config/WiimoteSettings.h" #include "Core/Core.h" +#include "Core/HW/SI/SI_Device.h" #include "Core/HW/Wiimote.h" #include "Core/System.h" #include "DolphinQt/Config/ConfigControls/ConfigBool.h" @@ -242,6 +243,34 @@ void AudioPane::CreateWidgets() } #endif +// GBA Audio Routing +#ifdef HAVE_CUBEB + m_gba_routing_box = new QGroupBox(tr("GBA Audio Routing")); + auto* gba_routing_layout = new QVBoxLayout; + m_gba_routing_box->setLayout(gba_routing_layout); + + m_gba_routing_enable = + new ConfigBool(tr("Enable GBA Audio Routing"), Config::MAIN_GBA_AUDIO_ROUTING_ENABLED); + gba_routing_layout->addWidget(m_gba_routing_enable); + + for (std::size_t i = 0; i < Config::GBA_SPEAKER_COUNT; ++i) + { + auto* row_widget = new QWidget; + auto* row_layout = new QHBoxLayout(row_widget); + row_layout->setContentsMargins(0, 0, 0, 0); + + m_gba_output_enable[i] = + new ConfigBool(tr("GBA %1").arg(i + 1), Config::MAIN_GBA_AUDIO_OUTPUT_ENABLED[i]); + m_gba_output_device[i] = + new ConfigStringChoice(output_devices, Config::MAIN_GBA_AUDIO_OUTPUT_DEVICE[i]); + + row_layout->addWidget(m_gba_output_enable[i]); + row_layout->addWidget(m_gba_output_device[i], 1); + + gba_routing_layout->addWidget(row_widget); + } +#endif + auto* const main_vbox_layout = new QVBoxLayout; main_vbox_layout->addWidget(dsp_box); @@ -249,6 +278,7 @@ void AudioPane::CreateWidgets() main_vbox_layout->addWidget(playback_box); #ifdef HAVE_CUBEB main_vbox_layout->addWidget(m_wiimote_routing_box); + main_vbox_layout->addWidget(m_gba_routing_box); #endif m_main_layout = new QHBoxLayout; @@ -276,15 +306,25 @@ void AudioPane::ConnectWidgets() #ifdef HAVE_CUBEB connect(m_wiimote_routing_enable, &ConfigBool::toggled, this, [this](bool) { UpdateWiimoteRoutingEnabled(); }); + connect(m_gba_routing_enable, &ConfigBool::toggled, this, + [this](bool) { UpdateGBARoutingEnabled(); }); for (std::size_t i = 0; i < 4; ++i) { connect(m_wiimote_output_enable[i], &ConfigBool::toggled, this, [this](bool) { UpdateWiimoteRoutingEnabled(); }); } + for (std::size_t i = 0; i < Config::GBA_SPEAKER_COUNT; ++i) + { + connect(m_gba_output_enable[i], &ConfigBool::toggled, this, + [this](bool) { UpdateGBARoutingEnabled(); }); + } // Also react to external config changes: wiimote source type, speaker data, BT passthrough. connect(&Settings::Instance(), &Settings::ConfigChanged, this, &AudioPane::UpdateWiimoteRoutingEnabled); + connect(&Settings::Instance(), &Settings::ConfigChanged, this, + &AudioPane::UpdateGBARoutingEnabled); UpdateWiimoteRoutingEnabled(); + UpdateGBARoutingEnabled(); #endif } @@ -321,6 +361,7 @@ void AudioPane::OnBackendChanged() #ifdef HAVE_CUBEB UpdateWiimoteRoutingEnabled(); + UpdateGBARoutingEnabled(); #endif } @@ -349,6 +390,7 @@ void AudioPane::OnEmulationStateChanged(bool running) #ifdef HAVE_CUBEB UpdateWiimoteRoutingEnabled(); + UpdateGBARoutingEnabled(); #endif } @@ -383,6 +425,33 @@ void AudioPane::UpdateWiimoteRoutingEnabled() #endif } +void AudioPane::UpdateGBARoutingEnabled() +{ +#ifdef HAVE_CUBEB + if (!m_gba_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 group_usable = !running && is_cubeb; + + m_gba_routing_enable->setEnabled(group_usable); + + const bool routing_on = group_usable && m_gba_routing_enable->isChecked(); + + for (std::size_t i = 0; i < Config::GBA_SPEAKER_COUNT; ++i) + { + const u32 device_type = Config::Get(Config::GetInfoForSIDevice(static_cast(i))); + const bool is_gba_connected = static_cast(device_type) == + SerialInterface::SIDEVICE_GC_GBA_EMULATED; + + m_gba_output_enable[i]->setEnabled(routing_on && is_gba_connected); + m_gba_output_device[i]->setEnabled(routing_on && is_gba_connected && + m_gba_output_enable[i]->isChecked()); + } +#endif +} + void AudioPane::CheckNeedForLatencyControl() { std::vector backends = AudioCommon::GetSoundBackends(); @@ -493,5 +562,30 @@ void AudioPane::AddDescriptions() m_wiimote_output_device[i]->SetDescription(tr(TR_WIIMOTE_OUTPUT_DEVICE_DESCRIPTION)); } } + + static const char TR_GBA_ROUTING_DESCRIPTION[] = + QT_TR_NOOP("Routes each GBA's speaker audio to a separate audio output device. " + "This setting cannot be changed while emulation is active." + "

This setting is disabled when an audio backend other than " + "Cubeb is selected." + "

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

This setting is disabled when this port is not set to GBA " + "(Integrated)."); + + if (m_gba_routing_box) + { + m_gba_routing_enable->SetTitle(tr("Enable GBA Audio Routing")); + m_gba_routing_enable->SetDescription(tr(TR_GBA_ROUTING_DESCRIPTION)); + for (std::size_t i = 0; i < Config::GBA_SPEAKER_COUNT; ++i) + { + m_gba_output_enable[i]->SetDescription(tr(TR_GBA_OUTPUT_ENABLE_DESCRIPTION)); + m_gba_output_device[i]->SetTitle(tr("GBA %1").arg(i + 1)); + m_gba_output_device[i]->SetDescription(tr(TR_GBA_OUTPUT_DEVICE_DESCRIPTION)); + } + } #endif } diff --git a/Source/Core/DolphinQt/Settings/AudioPane.h b/Source/Core/DolphinQt/Settings/AudioPane.h index 5938e4caa7..7da5a0a257 100644 --- a/Source/Core/DolphinQt/Settings/AudioPane.h +++ b/Source/Core/DolphinQt/Settings/AudioPane.h @@ -7,6 +7,8 @@ #include +#include "Core/Config/MainSettings.h" + namespace AudioCommon { enum class DPL2Quality; @@ -41,6 +43,7 @@ private: void OnDspChanged(); void UpdateWiimoteRoutingEnabled(); + void UpdateGBARoutingEnabled(); void CheckNeedForLatencyControl(); bool m_latency_control_supported; @@ -79,4 +82,10 @@ private: ConfigBool* m_wiimote_routing_enable = nullptr; std::array m_wiimote_output_enable{}; std::array m_wiimote_output_device{}; + + // GBA Audio Routing + QGroupBox* m_gba_routing_box = nullptr; + ConfigBool* m_gba_routing_enable = nullptr; + std::array m_gba_output_enable{}; + std::array m_gba_output_device{}; };