From d2d91430ed38ebd18dd3cd7dcbf88657612e137f Mon Sep 17 00:00:00 2001 From: Sasha Szpakowski Date: Tue, 6 May 2025 22:41:12 -0300 Subject: [PATCH] Add support for toggling audio output spatialization (HRTF). Disabled by default. Add love.audio.setOutputSpatialization(enable [, filtername]). Add enabled, filtername = love.audio.getOutputSpatialization(). Add filterlist = love.audio.getOutputSpatializationFilters(). Additional filters (HRTFs) can currently be added via the data files and search paths OpenAL Soft uses. Resolves #1699. --- src/modules/audio/Audio.h | 4 + src/modules/audio/null/Audio.cpp | 15 ++++ src/modules/audio/null/Audio.h | 4 + src/modules/audio/openal/Audio.cpp | 117 +++++++++++++++++++++++++++-- src/modules/audio/openal/Audio.h | 17 ++++- src/modules/audio/wrap_Audio.cpp | 38 ++++++++++ 6 files changed, 185 insertions(+), 10 deletions(-) diff --git a/src/modules/audio/Audio.h b/src/modules/audio/Audio.h index 8b6eae670..1cdf4d050 100644 --- a/src/modules/audio/Audio.h +++ b/src/modules/audio/Audio.h @@ -282,6 +282,10 @@ public: */ virtual bool isEFXsupported() const = 0; + virtual bool setOutputSpatialization(bool enable, const char *filter = nullptr) = 0; + virtual bool getOutputSpatialization(const char *&filter) const = 0; + virtual void getOutputSpatializationFilters(std::vector &list) const = 0; + /** * Sets whether audio from other apps mixes with love.audio or is muted, * on supported platforms. diff --git a/src/modules/audio/null/Audio.cpp b/src/modules/audio/null/Audio.cpp index 29f814be5..490f69564 100644 --- a/src/modules/audio/null/Audio.cpp +++ b/src/modules/audio/null/Audio.cpp @@ -199,6 +199,21 @@ bool Audio::isEFXsupported() const return false; } +bool Audio::setOutputSpatialization(bool, const char *) +{ + return false; +} + +bool Audio::getOutputSpatialization(const char *&filter) const +{ + filter = nullptr; + return false; +} + +void Audio::getOutputSpatializationFilters(std::vector &) const +{ +} + void Audio::pauseContext() { } diff --git a/src/modules/audio/null/Audio.h b/src/modules/audio/null/Audio.h index 8d38d4523..2188c46a8 100644 --- a/src/modules/audio/null/Audio.h +++ b/src/modules/audio/null/Audio.h @@ -83,6 +83,10 @@ public: int getMaxSourceEffects() const; bool isEFXsupported() const; + bool setOutputSpatialization(bool enable, const char *filter = nullptr) override; + bool getOutputSpatialization(const char *&filter) const override; + void getOutputSpatializationFilters(std::vector &list) const override; + void pauseContext(); void resumeContext(); diff --git a/src/modules/audio/openal/Audio.cpp b/src/modules/audio/openal/Audio.cpp index 1aa0783a0..d206f2db5 100644 --- a/src/modules/audio/openal/Audio.cpp +++ b/src/modules/audio/openal/Audio.cpp @@ -113,9 +113,6 @@ Audio::Audio() , poolThread(nullptr) , distanceModel(DISTANCE_INVERSE_CLAMPED) { - attribs.push_back(0); - attribs.push_back(0); - // Before opening new device, check if recording // is requested. if (getRequestRecordingPermission()) @@ -137,10 +134,9 @@ Audio::Audio() if (device == nullptr) throw love::Exception("Could not open device."); -#ifdef ALC_EXT_EFX - attribs.insert(attribs.begin(), ALC_MAX_AUXILIARY_SENDS); - attribs.insert(attribs.begin() + 1, MAX_SOURCE_EFFECTS); -#endif + hasHRTFExtension = alcIsExtensionPresent(device, "ALC_SOFT_HRTF") == AL_TRUE; + + std::vector attribs = computeContextAttribs(); context = alcCreateContext(device, attribs.data()); @@ -257,6 +253,44 @@ Audio::~Audio() alcCloseDevice(device); } +std::vector Audio::computeContextAttribs() +{ + std::vector attribs; + +#ifdef ALC_EXT_EFX + attribs.push_back(ALC_MAX_AUXILIARY_SENDS); + attribs.push_back(MAX_REQUESTED_SOURCE_EFFECTS); +#endif + +#ifdef ALC_SOFT_HRTF + if (hasHRTFExtension) + { + attribs.push_back(ALC_HRTF_SOFT); + attribs.push_back(requestEnableHRTF ? AL_TRUE : AL_FALSE); + if (!requestedHRTFFilter.empty()) + { + std::vector filters; + getOutputSpatializationFilters(filters); + + for (size_t i = 0; i < filters.size(); i++) + { + if (filters[i] == requestedHRTFFilter) + { + attribs.push_back(ALC_HRTF_ID_SOFT); + attribs.push_back((int)i); + break; + } + } + } + } +#endif + + attribs.push_back(0); + attribs.push_back(0); + + return attribs; +} + love::audio::Source *Audio::newSource(love::sound::Decoder *decoder) { return new Source(pool, decoder); @@ -386,7 +420,7 @@ void Audio::getPlaybackDevices(std::vector &list) } } -void Audio::setPlaybackDevice(const char* name) +void Audio::setPlaybackDevice(const char *name) { #ifndef ALC_SOFT_reopen_device typedef ALCboolean (ALC_APIENTRY*LPALCREOPENDEVICESOFT)(ALCdevice *device, @@ -404,6 +438,8 @@ void Audio::setPlaybackDevice(const char* name) return; } + std::vector attribs = computeContextAttribs(); + if (alcReopenDeviceSOFT(device, (const ALCchar *) name, attribs.data()) == ALC_FALSE) throw love::Exception("Cannot set output device: %s", alcGetString(device, alcGetError(device))); } @@ -707,6 +743,71 @@ bool Audio::isEFXsupported() const #endif } +bool Audio::setOutputSpatialization(bool enable, const char *filter) +{ + requestEnableHRTF = enable; + if (filter != nullptr) + requestedHRTFFilter = filter; + else + requestedHRTFFilter.clear(); + +#ifdef ALC_SOFT_HRTF + if (hasHRTFExtension) + { + static auto alcResetDeviceSOFT = (LPALCRESETDEVICESOFT)alcGetProcAddress(device, "alcResetDeviceSOFT"); + if (alcResetDeviceSOFT == nullptr) + return false; + + std::vector attribs = computeContextAttribs(); + return alcResetDeviceSOFT(device, attribs.data()) != AL_FALSE; + } +#endif + + return false; +} + +bool Audio::getOutputSpatialization(const char *&filter) const +{ +#ifdef ALC_SOFT_HRTF + if (hasHRTFExtension) + { + ALCint enabled = 0; + alcGetIntegerv(device, ALC_HRTF_SOFT, 1, &enabled); + if (enabled != 0) + filter = alcGetString(device, ALC_HRTF_SPECIFIER_SOFT); + else + filter = nullptr; + return enabled != 0; + } +#endif + + filter = nullptr; + return false; +} + +void Audio::getOutputSpatializationFilters(std::vector &list) const +{ +#ifdef ALC_SOFT_HRTF + if (!hasHRTFExtension) + return; + + static auto alcGetStringiSOFT = (LPALCGETSTRINGISOFT)alcGetProcAddress(device, "alcGetStringiSOFT"); + if (alcGetStringiSOFT == nullptr) + return; + + ALCint count = 0; + alcGetIntegerv(device, ALC_NUM_HRTF_SPECIFIERS_SOFT, 1, &count); + for (int i = 0; i < count; i++) + { + const char *specifier = alcGetStringiSOFT(device, ALC_HRTF_SPECIFIER_SOFT, i); + if (specifier != nullptr) + list.push_back(specifier); + } +#else + LOVE_UNUSED(list); +#endif +} + bool Audio::getEffectID(const char *name, ALuint &id) { auto iter = effectmap.find(name); diff --git a/src/modules/audio/openal/Audio.h b/src/modules/audio/openal/Audio.h index 5dce39609..ae265b1d1 100644 --- a/src/modules/audio/openal/Audio.h +++ b/src/modules/audio/openal/Audio.h @@ -122,6 +122,10 @@ public: int getMaxSourceEffects() const; bool isEFXsupported() const; + bool setOutputSpatialization(bool enable, const char *filter = nullptr) override; + bool getOutputSpatialization(const char *&filter) const override; + void getOutputSpatializationFilters(std::vector &list) const override; + bool getEffectID(const char *name, ALuint &id); std::string getPlaybackDevice(); @@ -129,7 +133,10 @@ public: void setPlaybackDevice(const char *name); private: + + std::vector computeContextAttribs(); void initializeEFX(); + // The OpenAL device. ALCdevice *device; @@ -138,7 +145,6 @@ private: // The OpenAL context. ALCcontext *context; - std::vector attribs; // The OpenAL effects struct EffectMapStorage @@ -149,7 +155,12 @@ private: std::map effectmap; std::stack slotlist; int MAX_SCENE_EFFECTS = 64; - int MAX_SOURCE_EFFECTS = 64; + int MAX_REQUESTED_SOURCE_EFFECTS = 64; + int MAX_SOURCE_EFFECTS = 0; + + // Disable HRTF output by default. + bool requestEnableHRTF = false; + std::string requestedHRTFFilter; // The Pool. Pool *pool; @@ -179,6 +190,8 @@ private: DistanceModel distanceModel; //float metersPerUnit = 1.0; + bool hasHRTFExtension = false; + #ifdef LOVE_ANDROID # ifndef ALC_SOFT_pause_device typedef void (ALC_APIENTRY*LPALCDEVICEPAUSESOFT)(ALCdevice *device); diff --git a/src/modules/audio/wrap_Audio.cpp b/src/modules/audio/wrap_Audio.cpp index 0541be892..c354bdb70 100644 --- a/src/modules/audio/wrap_Audio.cpp +++ b/src/modules/audio/wrap_Audio.cpp @@ -540,6 +540,41 @@ int w_isEffectsSupported(lua_State *L) return 1; } +int w_setOutputSpatialization(lua_State *L) +{ + bool enable = luax_checkboolean(L, 1); + const char *filter = luaL_optstring(L, 2, nullptr); + bool success = instance()->setOutputSpatialization(enable, filter); + luax_pushboolean(L, success); + return 1; +} + +int w_getOutputSpatialization(lua_State *L) +{ + const char *filter = nullptr; + bool enabled = instance()->getOutputSpatialization(filter); + luax_pushboolean(L, enabled); + if (filter != nullptr) + lua_pushstring(L, filter); + else + lua_pushnil(L); + return 2; +} + +int w_getOutputSpatializationFilters(lua_State *L) +{ + std::vector filters; + instance()->getOutputSpatializationFilters(filters); + + lua_createtable(L, (int)filters.size(), 0); + for (int i = 0; i < (int)filters.size(); i++) + { + luax_pushstring(L, filters[i]); + lua_rawseti(L, -2, i + 1); + } + return 1; +} + int w_setMixWithSystem(lua_State *L) { luax_pushboolean(L, Audio::setMixWithSystem(luax_checkboolean(L, 1))); @@ -622,6 +657,9 @@ static const luaL_Reg functions[] = { "getMaxSceneEffects", w_getMaxSceneEffects }, { "getMaxSourceEffects", w_getMaxSourceEffects }, { "isEffectsSupported", w_isEffectsSupported }, + { "setOutputSpatialization", w_setOutputSpatialization }, + { "getOutputSpatialization", w_getOutputSpatialization }, + { "getOutputSpatializationFilters", w_getOutputSpatializationFilters }, { "setMixWithSystem", w_setMixWithSystem }, { "getPlaybackDevice", w_getPlaybackDevice }, { "getPlaybackDevices", w_getPlaybackDevices },