Browse Source

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.
Sasha Szpakowski 2 months ago
parent
commit
d2d91430ed

+ 4 - 0
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<std::string> &list) const = 0;
+
 	/**
 	 * Sets whether audio from other apps mixes with love.audio or is muted,
 	 * on supported platforms.

+ 15 - 0
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<std::string> &) const
+{
+}
+
 void Audio::pauseContext()
 {
 }

+ 4 - 0
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<std::string> &list) const override;
+
 	void pauseContext();
 	void resumeContext();
 

+ 109 - 8
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<ALint> attribs = computeContextAttribs();
 
 		context = alcCreateContext(device, attribs.data());
 
@@ -257,6 +253,44 @@ Audio::~Audio()
 	alcCloseDevice(device);
 }
 
+std::vector<ALint> Audio::computeContextAttribs()
+{
+	std::vector<ALint> 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<std::string> 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<std::string> &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<ALint> 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<ALint> 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<std::string> &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);

+ 15 - 2
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<std::string> &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<ALint> computeContextAttribs();
 	void initializeEFX();
+
 	// The OpenAL device.
 	ALCdevice *device;
 
@@ -138,7 +145,6 @@ private:
 
 	// The OpenAL context.
 	ALCcontext *context;
-	std::vector<ALCint> attribs;
 
 	// The OpenAL effects
 	struct EffectMapStorage
@@ -149,7 +155,12 @@ private:
 	std::map<std::string, struct EffectMapStorage> effectmap;
 	std::stack<ALuint> 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);

+ 38 - 0
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<std::string> 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 },