From 8f2c87eee50ef8a991a935a5da4823d72ff6c707 Mon Sep 17 00:00:00 2001 From: dendi2008 Date: Sun, 25 Oct 2026 19:55:44 +0300 Subject: [PATCH] Add dualshock speaker feature. --- CMakeLists.txt | 2 + src/audio/audiobackend.h | 15 ++ src/audio/audiochannel.h | 6 + src/audio/audiocommon.h | 2 + src/audio/audiosystem.cpp | 110 +++++++++++++-- src/audio/controlleraudio.cpp | 180 ++++++++++++++++++++++++ src/audio/controlleraudio.h | 17 +++ src/audio/ffmpegaudioplayer.h | 4 + src/audio/openal/audiobackend.cpp | 149 +++++++++++++++++++- src/audio/openal/audiobackend.h | 34 ++++- src/audio/openal/ffmpegaudioplayer.cpp | 46 ++++-- src/audio/openal/ffmpegaudioplayer.h | 6 + src/audio/openal/openalaudiochannel.cpp | 70 +++++++-- src/audio/openal/openalaudiochannel.h | 4 + src/overlay.cpp | 18 +++ src/userconfig.cpp | 11 +- src/userconfig.h | 9 ++ src/video/ffmpegplayer.cpp | 4 + src/video/ffmpegplayer.h | 1 + src/video/videoplayer.h | 2 + src/video/videosystem.cpp | 6 + src/video/videosystem.h | 1 + 22 files changed, 657 insertions(+), 40 deletions(-) create mode 100644 src/audio/controlleraudio.cpp create mode 100644 src/audio/controlleraudio.h diff --git a/CMakeLists.txt b/CMakeLists.txt index c6a81dca5..8b672a634 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -390,6 +390,7 @@ set(Impacto_Src src/audio/audiosystem.cpp src/audio/audiochannel.cpp src/audio/audiostream.cpp + src/audio/controlleraudio.cpp src/audio/vorbisaudiostream.cpp src/audio/atrac9audiostream.cpp src/audio/adxaudiostream.cpp @@ -770,6 +771,7 @@ set(Impacto_Header src/audio/audiosystem.h src/audio/audiocommon.h src/audio/audiochannel.h + src/audio/controlleraudio.h src/audio/audiostream.h src/audio/buffering.h src/audio/ffmpegaudioplayer.h diff --git a/src/audio/audiobackend.h b/src/audio/audiobackend.h index a1248c0c6..e58094827 100644 --- a/src/audio/audiobackend.h +++ b/src/audio/audiobackend.h @@ -2,6 +2,9 @@ #include "audiocommon.h" +#include +#include + namespace Impacto { namespace Audio { @@ -16,6 +19,18 @@ class AudioBackend { virtual bool DidDeviceChanged() { return false; }; virtual bool ReopenSupported() { return false; } + + virtual std::vector GetPlaybackDevices() { return {}; } + + virtual bool DidDeviceListChanged() { return false; } + + virtual bool OpenOutput(AudioOutputId output, std::string const& deviceName) { + return false; + } + + virtual void CloseOutput(AudioOutputId output) {} + + virtual bool IsOutputOpen(AudioOutputId output) { return output == AO_Main; } }; } // namespace Audio diff --git a/src/audio/audiochannel.h b/src/audio/audiochannel.h index 6f8eb9daa..8a949dbe5 100644 --- a/src/audio/audiochannel.h +++ b/src/audio/audiochannel.h @@ -40,6 +40,9 @@ class AudioChannel { float GetVolume() const { return Volume; } void SetVolume(float volume) { Volume = volume; } + AudioOutputId GetOutput() const { return Output; } + virtual void SetOutput(AudioOutputId output) = 0; + const AudioStream* GetStream() const { return Stream.get(); } // may be negative for no fixed duration, 0 for no audio @@ -52,6 +55,7 @@ class AudioChannel { AudioChannelId Id; AudioChannelGroup Group; AudioChannelState State = ACS_Stopped; + AudioOutputId Output = AO_Main; float Volume = 1.0f; bool Looping = false; @@ -85,6 +89,8 @@ class EmptyAudioChannel : public AudioChannel { void Reinit() override {} + void SetOutput(AudioOutputId output) override { Output = output; } + void Update(float dt) override {} float PositionInSeconds() const override { return 0; } diff --git a/src/audio/audiocommon.h b/src/audio/audiocommon.h index 5a405d31c..7885d6eeb 100644 --- a/src/audio/audiocommon.h +++ b/src/audio/audiocommon.h @@ -23,6 +23,8 @@ enum AudioChannelId { AC_Count }; +enum AudioOutputId { AO_Main, AO_Controller, AO_Count }; + enum AudioChannelState { ACS_Stopped, ACS_Playing, diff --git a/src/audio/audiosystem.cpp b/src/audio/audiosystem.cpp index 876912a80..d656bcdb8 100644 --- a/src/audio/audiosystem.cpp +++ b/src/audio/audiosystem.cpp @@ -1,10 +1,12 @@ #include "audiosystem.h" +#include "controlleraudio.h" #include "../userconfig.h" #include "../log.h" #include "../profile/game.h" #include "../profile/scriptvars.h" #include "../profile/configsystem.h" #include "../text/dialoguepage.h" +#include "../video/videosystem.h" #ifndef IMPACTO_DISABLE_OPENAL #include "openal/audiobackend.h" @@ -148,11 +150,101 @@ void ResumeChannelGroup(AudioChannelGroup group) { } } +static std::string ControllerOutputDevice; + +static bool IsPlayedOnController(AudioChannelId id) { + auto const& config = UserConfig::CommonSettings.ControllerAudio; + switch (id) { + case AC_SE0: + case AC_SE1: + case AC_SE2: + return config.SE; + case AC_VOICE0: + case AC_VOICE1: + case AC_VOICE2: + case AC_REV: + return config.Voice; + case AC_BGM0: + case AC_BGM1: + case AC_BGM2: + return config.BGM; + case AC_SSE0: + case AC_SSE1: + case AC_SSE2: + case AC_SSE3: + return config.SSE; + case AC_Count: + break; + } + return false; +} + +static bool IsControllerAudioUsed() { + bool used = UserConfig::CommonSettings.ControllerAudio.Movie; + for (int i = 0; i < AC_Count; i++) { + used |= IsPlayedOnController((AudioChannelId)i); + } + return used; +} + +static void SetMovieOutput(AudioOutputId output) { + if (+Profile::Game::GameFeatures & +GameFeature::Video) { + Video::SetAudioOutput(output); + } +} + +static void CloseControllerOutput() { + for (int i = 0; i < AC_Count; i++) { + if (Channels[i]->GetOutput() == AO_Controller) { + Channels[i]->SetOutput(AO_Main); + } + } + SetMovieOutput(AO_Main); + Backend->CloseOutput(AO_Controller); + + ImpLog(LogLevel::Info, LogChannel::Audio, + "Stopped playing controller audio on {:s}\n", ControllerOutputDevice); + ControllerOutputDevice.clear(); +} + +static void UpdateControllerOutput(bool deviceListChanged) { + std::string const& device = ControllerAudio::GetDevice(); + + if (device == ControllerOutputDevice) { + const bool reopen = deviceListChanged && !device.empty() && + !Backend->IsOutputOpen(AO_Controller); + if (!reopen) return; + } + + if (!ControllerOutputDevice.empty()) CloseControllerOutput(); + ControllerOutputDevice = device; + if (!ControllerOutputDevice.empty() && + Backend->OpenOutput(AO_Controller, ControllerOutputDevice)) { + ImpLog(LogLevel::Info, LogChannel::Audio, + "Playing controller audio on {:s}\n", ControllerOutputDevice); + } +} + +static void UpdateChannelOutputs() { + const AudioOutputId controllerOutput = + Backend->IsOutputOpen(AO_Controller) ? AO_Controller : AO_Main; + for (int i = 0; i < AC_Count; i++) { + const AudioOutputId output = + IsPlayedOnController((AudioChannelId)i) ? controllerOutput : AO_Main; + if (Channels[i]->GetOutput() != output) Channels[i]->SetOutput(output); + } + SetMovieOutput(UserConfig::CommonSettings.ControllerAudio.Movie + ? controllerOutput + : AO_Main); +} + void AudioShutdown() { for (int i = 0; i < AC_Count; i++) { Channels[i] = nullptr; } IsInit = false; + SetMovieOutput(AO_Main); + ControllerAudio::Shutdown(); Backend->Shutdown(); } @@ -176,6 +268,8 @@ void AudioInit() { } if (!Backend->Init()) return; + ControllerOutputDevice.clear(); + ControllerAudio::Init(); for (int i = AC_SE0; i <= AC_SE2; i++) Channels[i] = AudioChannel::Create((AudioChannelId)i, ACG_SE); for (int i = AC_VOICE0; i <= AC_REV; i++) @@ -197,14 +291,9 @@ void AudioReinit() { if (Backend->ReopenSupported()) return; } - for (int i = AC_SE0; i <= AC_SE2; i++) Channels[i]->Reinit(); - for (int i = AC_VOICE0; i <= AC_REV; i++) Channels[i]->Reinit(); - for (int i = AC_BGM0; i <= AC_BGM2; i++) Channels[i]->Reinit(); - - Channels[AC_SSE0]->Reinit(); - Channels[AC_SSE1]->Reinit(); - Channels[AC_SSE2]->Reinit(); - Channels[AC_SSE3]->Reinit(); + for (int i = 0; i < AC_Count; i++) { + if (Channels[i]->GetOutput() == AO_Main) Channels[i]->Reinit(); + } } void AudioSubtitlesStart(AudioChannel* channel) { @@ -251,6 +340,11 @@ void PlayInGroup(AudioChannelGroup group, std::string const& mountpoint, } void AudioUpdate(float dt) { + const bool deviceListChanged = Backend->DidDeviceListChanged(); + ControllerAudio::Update(dt, IsControllerAudioUsed(), deviceListChanged); + UpdateControllerOutput(deviceListChanged); + UpdateChannelOutputs(); + // Set voice modifier for each voice channel for (int i = AC_VOICE0; i <= AC_VOICE2; i++) { const int charId = DialoguePages[i - AC_VOICE0].CurrentVoiceCharacterId; diff --git a/src/audio/controlleraudio.cpp b/src/audio/controlleraudio.cpp new file mode 100644 index 000000000..abddafdf6 --- /dev/null +++ b/src/audio/controlleraudio.cpp @@ -0,0 +1,180 @@ +#include "controlleraudio.h" +#include "audiosystem.h" +#include "../log.h" + +#include +#include +#include +#include + +namespace Impacto { +namespace Audio { +namespace ControllerAudio { + +static constexpr uint8_t DualShock4InputReportId = 0x01; +static constexpr int DualShock4StatusOffset = 30; +static constexpr uint8_t DualShock4HeadphonesBit = 0x20; + +static constexpr float JackDebounceDuration = 0.25f; + +static std::string ControllerDevice; +static bool ControllerEnabled = false; +static bool ControllerUsable = false; +static std::atomic GamepadListChanged = false; + +static SDL_hid_device* DualShock4Device = nullptr; +static bool HeadphonesReported = false; +static bool HeadphonesPlugged = false; +static float JackStableTime = 0.0f; + +static bool GamepadEventCallback(void* userdata, SDL_Event* event) { + if (event->type == SDL_EVENT_GAMEPAD_ADDED || + event->type == SDL_EVENT_GAMEPAD_REMOVED) { + GamepadListChanged = true; + } + return true; +} + +static void CloseDualShock4() { + if (!DualShock4Device) return; + + SDL_hid_close(DualShock4Device); + SDL_hid_exit(); + DualShock4Device = nullptr; + HeadphonesReported = false; + HeadphonesPlugged = false; + JackStableTime = 0.0f; +} + +static std::string GetProductName(SDL_hid_device_info const* info) { + char* productName = SDL_iconv_wchar_utf8(info->product_string); + if (!productName) return {}; + + std::string result = productName; + SDL_free(productName); + return result; +} + +static std::string FindAudioDevice(SDL_hid_device* device) { + SDL_hid_device_info* info = SDL_hid_get_device_info(device); + if (!info || !info->product_string || + info->bus_type == SDL_HID_API_BUS_BLUETOOTH) { + return {}; + } + + const std::string productName = GetProductName(info); + if (productName.empty()) return {}; + + const std::string bracketedName = fmt::format("({})", productName); + for (std::string const& audioDevice : Backend->GetPlaybackDevices()) { + if (audioDevice.starts_with(productName) || + audioDevice.find(bracketedName) != std::string::npos) { + return audioDevice; + } + } + return {}; +} + +static void FindControllerDevice() { + if (ControllerEnabled && DualShock4Device) { + ControllerDevice = FindAudioDevice(DualShock4Device); + if (!ControllerDevice.empty()) return; + } + + ControllerDevice.clear(); + CloseDualShock4(); + if (!ControllerEnabled || SDL_hid_init() != 0) return; + + int count = 0; + if (SDL_JoystickID* ids = SDL_GetGamepads(&count)) { + for (int i = 0; i < count && !DualShock4Device; i++) { + if (SDL_GetRealGamepadTypeForID(ids[i]) != SDL_GAMEPAD_TYPE_PS4) continue; + + const char* path = SDL_GetGamepadPathForID(ids[i]); + SDL_hid_device* device = path ? SDL_hid_open_path(path) : nullptr; + if (!device) { + ImpLog(LogLevel::Warning, LogChannel::Audio, + "Could not open DualShock 4 to check its headphone jack\n"); + continue; + } + + ControllerDevice = FindAudioDevice(device); + if (ControllerDevice.empty()) { + SDL_hid_close(device); + } else { + DualShock4Device = device; + } + } + SDL_free(ids); + } + + if (!DualShock4Device) { + SDL_hid_exit(); + return; + } + SDL_hid_set_nonblocking(DualShock4Device, 1); +} + +static bool UpdateHeadphones(float dt) { + if (!DualShock4Device) return false; + + bool reported = HeadphonesReported; + uint8_t report[64]; + while (true) { + const int bytesRead = + SDL_hid_read(DualShock4Device, report, sizeof(report)); + if (bytesRead == 0) break; + if (bytesRead < 0) { + CloseDualShock4(); + return false; + } + + if (report[0] == DualShock4InputReportId && + bytesRead > DualShock4StatusOffset) { + reported = + (report[DualShock4StatusOffset] & DualShock4HeadphonesBit) != 0; + } + } + + if (reported != HeadphonesReported) { + HeadphonesReported = reported; + JackStableTime = 0.0f; + } + JackStableTime += dt; + if (JackStableTime >= JackDebounceDuration) { + HeadphonesPlugged = HeadphonesReported; + } + return HeadphonesPlugged; +} + +void Init() { + ControllerDevice.clear(); + ControllerEnabled = false; + ControllerUsable = false; + GamepadListChanged = false; + SDL_AddEventWatch(GamepadEventCallback, nullptr); +} + +void Update(float dt, bool enabled, bool deviceListChanged) { + const bool gamepadListChanged = GamepadListChanged.exchange(false); + if (enabled != ControllerEnabled || deviceListChanged || gamepadListChanged) { + ControllerEnabled = enabled; + FindControllerDevice(); + } + + ControllerUsable = !ControllerDevice.empty() && UpdateHeadphones(dt); +} + +void Shutdown() { + SDL_RemoveEventWatch(GamepadEventCallback, nullptr); + CloseDualShock4(); +} + +std::string const& GetDevice() { + static const std::string noDevice; + return ControllerUsable ? ControllerDevice : noDevice; +} + +} // namespace ControllerAudio +} // namespace Audio +} // namespace Impacto diff --git a/src/audio/controlleraudio.h b/src/audio/controlleraudio.h new file mode 100644 index 000000000..3b28a646d --- /dev/null +++ b/src/audio/controlleraudio.h @@ -0,0 +1,17 @@ +#pragma once + +#include + +namespace Impacto { +namespace Audio { +namespace ControllerAudio { + +void Init(); +void Update(float dt, bool enabled, bool deviceListChanged); +void Shutdown(); + +std::string const& GetDevice(); + +} // namespace ControllerAudio +} // namespace Audio +} // namespace Impacto diff --git a/src/audio/ffmpegaudioplayer.h b/src/audio/ffmpegaudioplayer.h index 87eb78eb0..1f3680b7d 100644 --- a/src/audio/ffmpegaudioplayer.h +++ b/src/audio/ffmpegaudioplayer.h @@ -1,5 +1,6 @@ #pragma once +#include "audiocommon.h" #include "../video/ffmpegstream.h" #include "../video/clock.h" @@ -20,6 +21,7 @@ class FFmpegAudioPlayer { virtual void FillAudioBuffers() {}; virtual void Process() {}; virtual void Reinit() {}; + virtual void SetOutput(AudioOutputId output) { Output = output; }; virtual void Stop() {}; virtual void Unload() {}; @@ -27,6 +29,8 @@ class FFmpegAudioPlayer { Video::Clock& GetClock() { return AudioClock; }; protected: + AudioOutputId Output = AO_Main; + uint8_t** AudioBuffer = 0; int AudioLinesize; diff --git a/src/audio/openal/audiobackend.cpp b/src/audio/openal/audiobackend.cpp index 4fe38f06b..9bfafea28 100644 --- a/src/audio/openal/audiobackend.cpp +++ b/src/audio/openal/audiobackend.cpp @@ -2,6 +2,8 @@ #include "../../log.h" +#include + namespace Impacto { namespace Audio { namespace OpenAL { @@ -16,11 +18,35 @@ static LPALCEVENTCONTROLSOFT EventControlFunc = 0; #if ALC_SOFT_reopen_device static LPALCREOPENDEVICESOFT ReopenSoftFunc = 0; #endif -void AL_APIENTRY DefaultDeviceChangedEventCallback( +#if ALC_EXT_thread_local_context +static PFNALCSETTHREADCONTEXTPROC SetThreadContextFunc = 0; +static PFNALCGETTHREADCONTEXTPROC GetThreadContextFunc = 0; +#endif + +struct OutputDevice { + ALCdevice* Device = 0; + ALCcontext* Context = 0; +}; +static std::array Outputs; + +#if ALC_SOFT_system_events +void AL_APIENTRY AudioBackend::SystemEventCallback( ALCenum eventType, ALCenum deviceType, ALCdevice* device, ALCsizei length, const ALCchar* message, void* userParam) { - *(bool*)userParam = true; + if (deviceType != ALC_PLAYBACK_DEVICE_SOFT) return; + + AudioBackend* backend = static_cast(userParam); + switch (eventType) { + case ALC_EVENT_TYPE_DEFAULT_DEVICE_CHANGED_SOFT: + backend->DeviceChanged = true; + break; + case ALC_EVENT_TYPE_DEVICE_ADDED_SOFT: + case ALC_EVENT_TYPE_DEVICE_REMOVED_SOFT: + backend->DeviceListChanged = true; + break; + } } +#endif bool AudioBackend::Init() { AlcDevice = alcOpenDevice(NULL); @@ -57,7 +83,8 @@ bool AudioBackend::Reinit() { } } #endif - Shutdown(); + if (AlcContext) alcDestroyContext(AlcContext); + if (AlcDevice) alcCloseDevice(AlcDevice); AlcDevice = alcOpenDevice(NULL); if (!AlcDevice) { ImpLog(LogLevel::Fatal, LogChannel::Audio, @@ -75,6 +102,9 @@ bool AudioBackend::Reinit() { } void AudioBackend::Shutdown() { + for (int i = 0; i < AO_Count; i++) { + CloseOutput((AudioOutputId)i); + } if (AlcContext) alcDestroyContext(AlcContext); if (AlcDevice) alcCloseDevice(AlcDevice); } @@ -90,21 +120,109 @@ bool AudioBackend::ReopenSupported() { #endif } +std::vector AudioBackend::GetPlaybackDevices() { + ALCenum specifier = ALC_DEVICE_SPECIFIER; +#if ALC_ENUMERATE_ALL_EXT + if (alcIsExtensionPresent(NULL, "ALC_ENUMERATE_ALL_EXT") == AL_TRUE) { + specifier = ALC_ALL_DEVICES_SPECIFIER; + } +#endif + + std::vector devices; + for (const ALCchar* name = alcGetString(NULL, specifier); name && *name; + name += devices.back().size() + 1) { + devices.emplace_back(name); + } + return devices; +} + +bool AudioBackend::DidDeviceListChanged() { + return DeviceListChanged.exchange(false); +} + +bool AudioBackend::OpenOutput(AudioOutputId output, + std::string const& deviceName) { + if (output == AO_Main) return false; + +#if ALC_EXT_thread_local_context + if (!SetThreadContextFunc || !GetThreadContextFunc) { + ImpLog(LogLevel::Error, LogChannel::Audio, + "No thread local context support, cannot open audio output on " + "{:s}\n", + deviceName); + return false; + } + + CloseOutput(output); + OutputDevice& outputDevice = Outputs[output]; + outputDevice.Device = alcOpenDevice(deviceName.c_str()); + if (!outputDevice.Device) { + ImpLog(LogLevel::Error, LogChannel::Audio, + "Could not open OpenAL device {:s}\n", deviceName); + return false; + } + outputDevice.Context = alcCreateContext(outputDevice.Device, NULL); + if (!outputDevice.Context) { + ImpLog(LogLevel::Error, LogChannel::Audio, + "Failed to create OpenAL context on {:s}\n", deviceName); + CloseOutput(output); + return false; + } + return true; +#else + return false; +#endif +} + +void AudioBackend::CloseOutput(AudioOutputId output) { + if (output == AO_Main) return; + + OutputDevice& outputDevice = Outputs[output]; + if (outputDevice.Context) alcDestroyContext(outputDevice.Context); + if (outputDevice.Device) alcCloseDevice(outputDevice.Device); + outputDevice = {}; +} + +bool AudioBackend::IsOutputOpen(AudioOutputId output) { + if (output == AO_Main) return AlcContext != 0; + + OutputDevice const& outputDevice = Outputs[output]; + if (!outputDevice.Context) return false; +#if ALC_EXT_disconnect + ALCint connected = AL_TRUE; + alcGetIntegerv(outputDevice.Device, ALC_CONNECTED, 1, &connected); + return connected == AL_TRUE; +#else + return true; +#endif +} + void AudioBackend::InitAlextFunctions() { +#if ALC_EXT_thread_local_context + if (alcIsExtensionPresent(NULL, "ALC_EXT_thread_local_context") == AL_TRUE) { + SetThreadContextFunc = (PFNALCSETTHREADCONTEXTPROC)alcGetProcAddress( + NULL, "alcSetThreadContext"); + GetThreadContextFunc = (PFNALCGETTHREADCONTEXTPROC)alcGetProcAddress( + NULL, "alcGetThreadContext"); + } +#endif #if ALC_SOFT_system_events if (alcIsExtensionPresent(AlcDevice, "ALC_SOFT_system_events") == AL_TRUE) { EventControlFunc = (LPALCEVENTCONTROLSOFT)alcGetProcAddress( AlcDevice, "alcEventControlSOFT"); if (EventControlFunc) { - const ALCenum eventType{ALC_EVENT_TYPE_DEFAULT_DEVICE_CHANGED_SOFT}; - EventControlFunc(1, &eventType, AL_TRUE); + const std::array eventTypes{ + ALC_EVENT_TYPE_DEFAULT_DEVICE_CHANGED_SOFT, + ALC_EVENT_TYPE_DEVICE_ADDED_SOFT, + ALC_EVENT_TYPE_DEVICE_REMOVED_SOFT, + }; + EventControlFunc((ALCsizei)eventTypes.size(), eventTypes.data(), AL_TRUE); } EventCallBackFunc = (LPALCEVENTCALLBACKSOFT)alcGetProcAddress( AlcDevice, "alcEventCallbackSOFT"); if (EventCallBackFunc) { - EventCallBackFunc((ALCEVENTPROCTYPESOFT)DefaultDeviceChangedEventCallback, - &DeviceChanged); + EventCallBackFunc((ALCEVENTPROCTYPESOFT)SystemEventCallback, this); } #if ALC_SOFT_reopen_device if (alcIsExtensionPresent(AlcDevice, "ALC_SOFT_reopen_device") == @@ -122,6 +240,23 @@ void AudioBackend::InitAlextFunctions() { #endif } +ScopedOutputContext::ScopedOutputContext(AudioOutputId output) { +#if ALC_EXT_thread_local_context + if (!SetThreadContextFunc || !GetThreadContextFunc) return; + + ALCcontext* context = output == AO_Main ? nullptr : Outputs[output].Context; + PreviousContext = GetThreadContextFunc(); + if (context == PreviousContext) return; + Bound = SetThreadContextFunc(context) == AL_TRUE; +#endif +} + +ScopedOutputContext::~ScopedOutputContext() { +#if ALC_EXT_thread_local_context + if (Bound) SetThreadContextFunc(PreviousContext); +#endif +} + }; // namespace OpenAL }; // namespace Audio }; // namespace Impacto diff --git a/src/audio/openal/audiobackend.h b/src/audio/openal/audiobackend.h index 9625fd541..cd021c7fe 100644 --- a/src/audio/openal/audiobackend.h +++ b/src/audio/openal/audiobackend.h @@ -3,15 +3,24 @@ #include "../audiobackend.h" #include "audiocommon.h" +#include + namespace Impacto { namespace Audio { namespace OpenAL { class AudioBackend : public Audio::AudioBackend { private: - bool DeviceChanged = false; + std::atomic DeviceChanged = false; + std::atomic DeviceListChanged = false; void InitAlextFunctions(); +#if ALC_SOFT_system_events + static void AL_APIENTRY + SystemEventCallback(ALCenum eventType, ALCenum deviceType, ALCdevice* device, + ALCsizei length, const ALCchar* message, void* userParam); +#endif + public: bool Init() override; @@ -22,6 +31,29 @@ class AudioBackend : public Audio::AudioBackend { bool DidDeviceChanged() override; bool ReopenSupported() override; + + std::vector GetPlaybackDevices() override; + + bool DidDeviceListChanged() override; + + bool OpenOutput(AudioOutputId output, std::string const& deviceName) override; + + void CloseOutput(AudioOutputId output) override; + + bool IsOutputOpen(AudioOutputId output) override; +}; + +class ScopedOutputContext { + public: + explicit ScopedOutputContext(AudioOutputId output); + ~ScopedOutputContext(); + + ScopedOutputContext(ScopedOutputContext const&) = delete; + ScopedOutputContext& operator=(ScopedOutputContext const&) = delete; + + private: + bool Bound = false; + ALCcontext* PreviousContext = nullptr; }; } // namespace OpenAL diff --git a/src/audio/openal/ffmpegaudioplayer.cpp b/src/audio/openal/ffmpegaudioplayer.cpp index eb1d3478e..2acc6a1dc 100644 --- a/src/audio/openal/ffmpegaudioplayer.cpp +++ b/src/audio/openal/ffmpegaudioplayer.cpp @@ -1,4 +1,5 @@ #include "ffmpegaudioplayer.h" +#include "audiobackend.h" #include "../../video/ffmpegplayer.h" #include "../audiosystem.h" #include @@ -19,6 +20,7 @@ namespace Audio { namespace OpenAL { FFmpegAudioPlayer::~FFmpegAudioPlayer() { + ScopedOutputContext context(Output); alSourceStop(ALSource); av_freep(&AudioBuffer[0]); swr_free(&AudioConvertContext); @@ -26,24 +28,44 @@ FFmpegAudioPlayer::~FFmpegAudioPlayer() { alDeleteSources(1, &ALSource); } -void FFmpegAudioPlayer::Init() { +void FFmpegAudioPlayer::CreateSource() { alGenSources(1, &ALSource); alSourcef(ALSource, AL_PITCH, 1); alSourcef(ALSource, AL_GAIN, 1); alSource3f(ALSource, AL_POSITION, 0, 0, 0); alSource3f(ALSource, AL_VELOCITY, 0, 0, 0); alSourcei(ALSource, AL_LOOPING, AL_FALSE); +} + +void FFmpegAudioPlayer::Init() { + ScopedOutputContext context(Output); + CreateSource(); alGenBuffers(AudioBufferCount, BufferIds); } void FFmpegAudioPlayer::Reinit() { - alGenSources(1, &ALSource); - alSourcef(ALSource, AL_PITCH, 1); - alSourcef(ALSource, AL_GAIN, 1); - alSource3f(ALSource, AL_POSITION, 0, 0, 0); - alSource3f(ALSource, AL_VELOCITY, 0, 0, 0); - alSourcei(ALSource, AL_LOOPING, AL_FALSE); + if (Output != AO_Main) return; + + CreateSource(); + + alGenBuffers(AudioBufferCount, BufferIds); + First = true; +} + +void FFmpegAudioPlayer::SetOutput(AudioOutputId output) { + if (output == Output) return; + + std::lock_guard lock(SourceMutex); + { + ScopedOutputContext context(Output); + alDeleteSources(1, &ALSource); + alDeleteBuffers(AudioBufferCount, BufferIds); + } + + Output = output; + ScopedOutputContext context(Output); + CreateSource(); alGenBuffers(AudioBufferCount, BufferIds); First = true; @@ -68,14 +90,19 @@ void FFmpegAudioPlayer::InitConvertContext(AVCodecContext* codecCtx) { AV_SAMPLE_FMT_S16, 0); } -void FFmpegAudioPlayer::Stop() { alSourceStop(ALSource); } +void FFmpegAudioPlayer::Stop() { + std::lock_guard lock(SourceMutex); + ScopedOutputContext context(Output); + alSourceStop(ALSource); +} void FFmpegAudioPlayer::Unload() { Stop(); + ScopedOutputContext context(Output); alSourcei(ALSource, AL_BUFFER, 0); av_freep(&AudioBuffer[0]); alDeleteSources(1, &ALSource); - alGenSources(1, &ALSource); + CreateSource(); First = true; } @@ -131,6 +158,7 @@ void FFmpegAudioPlayer::FillAudioBuffers() { void FFmpegAudioPlayer::Process() { using namespace std::chrono_literals; + ScopedOutputContext context(Output); float gain = Audio::MasterVolume * Audio::GroupVolumes[Audio::ACG_Movie]; alSourcef(ALSource, AL_GAIN, gain); diff --git a/src/audio/openal/ffmpegaudioplayer.h b/src/audio/openal/ffmpegaudioplayer.h index aa986c9fb..30c949b42 100644 --- a/src/audio/openal/ffmpegaudioplayer.h +++ b/src/audio/openal/ffmpegaudioplayer.h @@ -4,6 +4,8 @@ #include "../ffmpegaudioplayer.h" +#include + namespace Impacto { namespace Audio { namespace OpenAL { @@ -19,12 +21,16 @@ class FFmpegAudioPlayer : public Audio::FFmpegAudioPlayer { void FillAudioBuffers() override; void Process() override; void Reinit() override; + void SetOutput(AudioOutputId output) override; void Stop() override; void Unload() override; private: + void CreateSource(); + ALCuint ALSource; + std::mutex SourceMutex; static int constexpr AudioBufferSize = 256 * 8192; static int constexpr AudioBufferCount = 3; ALuint BufferIds[AudioBufferCount]; diff --git a/src/audio/openal/openalaudiochannel.cpp b/src/audio/openal/openalaudiochannel.cpp index ca9792519..34235f3b2 100644 --- a/src/audio/openal/openalaudiochannel.cpp +++ b/src/audio/openal/openalaudiochannel.cpp @@ -1,4 +1,5 @@ #include "openalaudiochannel.h" +#include "audiobackend.h" #include "../audiosystem.h" #include "../audiostream.h" @@ -25,17 +26,21 @@ OpenALAudioChannel::OpenALAudioChannel(AudioChannelId id, HostBuffer(std::vector(AudioBufferSize, (uint8_t)0)) { BufferStartPositions.fill(0); - alGenBuffers((ALsizei)AudioBuffers.size(), AudioBuffers.data()); - alGenSources(1, &Source); - - alSourcef(Source, AL_PITCH, 1); - alSource3f(Source, AL_POSITION, 0, 0, 0); - alSource3f(Source, AL_VELOCITY, 0, 0, 0); - alSourcei(Source, AL_LOOPING, AL_FALSE); - UpdateGain(); + ScopedOutputContext context(Output); + CreateSource(); } void OpenALAudioChannel::Reinit() { + ScopedOutputContext context(Output); + CreateSource(); +} + +OpenALAudioChannel::~OpenALAudioChannel() { + ScopedOutputContext context(Output); + DeleteSource(); +} + +void OpenALAudioChannel::CreateSource() { alGenBuffers((ALsizei)AudioBuffers.size(), AudioBuffers.data()); alGenSources(1, &Source); @@ -46,13 +51,44 @@ void OpenALAudioChannel::Reinit() { UpdateGain(); } -OpenALAudioChannel::~OpenALAudioChannel() { +void OpenALAudioChannel::DeleteSource() { alDeleteSources(1, &Source); alDeleteBuffers((ALsizei)AudioBuffers.size(), AudioBuffers.data()); } +void OpenALAudioChannel::SetOutput(AudioOutputId output) { + if (output == Output) return; + + size_t position = 0; + { + ScopedOutputContext context(Output); + if (Stream) position = PlaybackPosition(); + DeleteSource(); + } + + Output = output; + ScopedOutputContext context(Output); + CreateSource(); + + FirstQueuedBufferIndex = 0; + BufferStartPositions.fill(0); + PlaybackStarted = false; + if (!Stream) return; + + int samplePosition = (int)position; + if (Looping && samplePosition >= Stream->LoopEnd) { + samplePosition = Stream->LoopStart + (samplePosition - Stream->LoopEnd); + } + if (!Looping && samplePosition >= Stream->Duration) { + EndPlayback(); + return; + } + Stream->Seek(samplePosition); +} + void OpenALAudioChannel::Play(std::unique_ptr stream, bool loop, float fadeInDuration) { + ScopedOutputContext context(Output); EndPlayback(); Stream = std::move(stream); @@ -67,6 +103,7 @@ void OpenALAudioChannel::Play(std::unique_ptr stream, bool loop, void OpenALAudioChannel::Stop(float fadeOutDuration) { if (State == ACS_Stopped) return; + ScopedOutputContext context(Output); if (fadeOutDuration == 0 || State == ACS_Paused) { EndPlayback(); return; @@ -111,17 +148,22 @@ void OpenALAudioChannel::Resume() { State = ACS_Playing; } -float OpenALAudioChannel::PositionInSeconds() const { - if (Stream == nullptr) return 0.0f; - +size_t OpenALAudioChannel::PlaybackPosition() const { int offset; alGetSourcei(Source, AL_SAMPLE_OFFSET, &offset); - size_t position = BufferStartPositions[FirstQueuedBufferIndex] + offset; - return (float)position / Stream->SampleRate; + return BufferStartPositions[FirstQueuedBufferIndex] + offset; +} + +float OpenALAudioChannel::PositionInSeconds() const { + if (Stream == nullptr) return 0.0f; + + ScopedOutputContext context(Output); + return (float)PlaybackPosition() / Stream->SampleRate; } void OpenALAudioChannel::Update(float dt) { + ScopedOutputContext context(Output); UpdateFade(dt); if (State == ACS_Playing || State == ACS_FadingIn || State == ACS_FadingOut) { diff --git a/src/audio/openal/openalaudiochannel.h b/src/audio/openal/openalaudiochannel.h index 36ad9714b..31e58f764 100644 --- a/src/audio/openal/openalaudiochannel.h +++ b/src/audio/openal/openalaudiochannel.h @@ -19,6 +19,7 @@ class OpenALAudioChannel : public Audio::AudioChannel { void Resume() override; void Reinit() override; + void SetOutput(AudioOutputId output) override; void Update(float dt) override; float PositionInSeconds() const override; @@ -40,6 +41,9 @@ class OpenALAudioChannel : public Audio::AudioChannel { bool PlaybackStarted = false; + void CreateSource(); + void DeleteSource(); + size_t PlaybackPosition() const; void EndPlayback(); void UpdateGain(); void UpdateFade(float dt); diff --git a/src/overlay.cpp b/src/overlay.cpp index 09586cb3a..d401fb11d 100644 --- a/src/overlay.cpp +++ b/src/overlay.cpp @@ -505,6 +505,22 @@ static void ShowCommonSettings() { } } +static void ShowControllerAudioSettings() { + if (ImGui::CollapsingHeader("DualShock 4 Audio", + ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::SameLine(); + ImGui::Text("(Played on headphones plugged into the DualShock 4)"); + ImGui::Spacing(); + + auto& controllerAudio = UserConfig::CommonSettings.ControllerAudio; + ImGui::Checkbox("Music", &controllerAudio.BGM); + ImGui::Checkbox("Voices", &controllerAudio.Voice); + ImGui::Checkbox("Sound Effects", &controllerAudio.SE); + ImGui::Checkbox("System Sound Effects", &controllerAudio.SSE); + ImGui::Checkbox("Movies", &controllerAudio.Movie); + } +} + static void ShowSettingsPage(std::string const& selectedGame) { const float footerHeight = ImGui::GetStyle().ItemSpacing.y + ImGui::GetStyle().SeparatorSize + @@ -514,6 +530,8 @@ static void ShowSettingsPage(std::string const& selectedGame) { if (ImGui::BeginChild("##SettingsContent", ImVec2(0, -footerHeight))) { ShowCommonSettings(); ImGui::Spacing(); + ShowControllerAudioSettings(); + ImGui::Spacing(); if (!selectedGame.empty()) { #if !defined(__ANDROID__) && !defined(__SWITCH__) displayChanged |= ShowDisplaySettings(selectedGame); diff --git a/src/userconfig.cpp b/src/userconfig.cpp index 515eac49a..4d17b6238 100644 --- a/src/userconfig.cpp +++ b/src/userconfig.cpp @@ -103,6 +103,14 @@ TOML11_DEFINE_CONVERSION_NON_INTRUSIVE( Display ) +TOML11_DEFINE_CONVERSION_NON_INTRUSIVE( + Impacto::UserConfig::ControllerAudioConfig, + BGM, + Voice, + SE, + SSE, + Movie +) TOML11_DEFINE_CONVERSION_NON_INTRUSIVE( Impacto::UserConfig::Config, ResolutionWidth, @@ -112,7 +120,8 @@ TOML11_DEFINE_CONVERSION_NON_INTRUSIVE( LogChannels, LoggingToConsole, LoggingToFile, - SwapConfirmCancelButtons + SwapConfirmCancelButtons, + ControllerAudio ) TOML11_DEFINE_CONVERSION_NON_INTRUSIVE( diff --git a/src/userconfig.h b/src/userconfig.h index 4b4a709fc..909a065eb 100644 --- a/src/userconfig.h +++ b/src/userconfig.h @@ -45,6 +45,13 @@ struct GameConfig { bool UsePatch; DisplayMode Display = GetDefaultDispMode(); }; +struct ControllerAudioConfig { + bool BGM = false; + bool Voice = false; + bool SE = false; + bool SSE = false; + bool Movie = false; +}; struct Config { int ResolutionWidth = 1280; int ResolutionHeight = 720; @@ -55,6 +62,8 @@ struct Config { bool LoggingToConsole = GetDefaultLogToConsole(); bool LoggingToFile = GetDefaultLogToFile(); bool SwapConfirmCancelButtons = false; + + ControllerAudioConfig ControllerAudio; }; struct AdvancedConfig { RendererType ActiveRenderer = RendererType::OpenGL; diff --git a/src/video/ffmpegplayer.cpp b/src/video/ffmpegplayer.cpp index e162e3726..41f6c249f 100644 --- a/src/video/ffmpegplayer.cpp +++ b/src/video/ffmpegplayer.cpp @@ -105,6 +105,10 @@ void FFmpegPlayer::Init() { void FFmpegPlayer::ReinitAudio() { AudioPlayer->Reinit(); } +void FFmpegPlayer::SetAudioOutput(Audio::AudioOutputId output) { + AudioPlayer->SetOutput(output); +} + AVBufferRef* FFmpegPlayer::HwDecoderInit(const AVCodec* codec) { #if defined(_WIN32) static constexpr auto preferenceOrder = std::to_array({ diff --git a/src/video/ffmpegplayer.h b/src/video/ffmpegplayer.h index af1871a71..7ef58dcea 100644 --- a/src/video/ffmpegplayer.h +++ b/src/video/ffmpegplayer.h @@ -44,6 +44,7 @@ class FFmpegPlayer : public VideoPlayer { void Seek(int64_t pos) override; void ReinitAudio() override; + void SetAudioOutput(Audio::AudioOutputId output) override; void Update(float dt) override; void Render(float videoAlpha) override; diff --git a/src/video/videoplayer.h b/src/video/videoplayer.h index 21564f5c8..e75b3a7ea 100644 --- a/src/video/videoplayer.h +++ b/src/video/videoplayer.h @@ -2,6 +2,7 @@ #include "../impacto.h" #include "../io/stream.h" +#include "../audio/audiocommon.h" #include @@ -18,6 +19,7 @@ class VideoPlayer { virtual void Seek(int64_t pos) {}; virtual void ReinitAudio() {}; + virtual void SetAudioOutput(Audio::AudioOutputId output) {}; virtual void Update(float dt) {}; virtual void Render(float videoAlpha) {}; diff --git a/src/video/videosystem.cpp b/src/video/videosystem.cpp index 814ea9e2d..1dd5e86e4 100644 --- a/src/video/videosystem.cpp +++ b/src/video/videosystem.cpp @@ -57,5 +57,11 @@ void ReinitAudio() { } } +void SetAudioOutput(Audio::AudioOutputId output) { + for (int i = 0; i < VP_Count; i++) { + Players[i]->SetAudioOutput(output); + } +} + } // namespace Video } // namespace Impacto diff --git a/src/video/videosystem.h b/src/video/videosystem.h index e5eb70d54..623aeae7d 100644 --- a/src/video/videosystem.h +++ b/src/video/videosystem.h @@ -15,5 +15,6 @@ void VideoUpdate(float dt); void VideoRender(float videoAlpha); void VideoShutdown(); void ReinitAudio(); +void SetAudioOutput(Audio::AudioOutputId output); } // namespace Video } // namespace Impacto