From 2b546eb4b34f4add9d245ba22189f9f94ab7ac42 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 17 Feb 2026 18:43:52 +0100 Subject: [PATCH] Add OpenAL audio to Web --- Source/Engine/Audio/Audio.Build.cs | 5 +- Source/Engine/Audio/Audio.cpp | 10 +++ Source/Engine/Audio/AudioBackend.h | 2 + .../Engine/Audio/OpenAL/AudioBackendOAL.cpp | 64 +++++++++++++------ .../Build/NativeCpp/Builder.NativeCpp.cs | 3 + 5 files changed, 62 insertions(+), 22 deletions(-) diff --git a/Source/Engine/Audio/Audio.Build.cs b/Source/Engine/Audio/Audio.Build.cs index d2e628957..8a616b309 100644 --- a/Source/Engine/Audio/Audio.Build.cs +++ b/Source/Engine/Audio/Audio.Build.cs @@ -61,7 +61,7 @@ public class Audio : EngineModule useOpenAL = true; break; case TargetPlatform.Web: - // TODO: audio playback on Web (OpenAL) + useOpenAL = true; break; default: throw new InvalidPlatformException(options.Platform.Target); } @@ -102,6 +102,9 @@ public class Audio : EngineModule options.Libraries.Add("CoreAudio.framework"); options.Libraries.Add("AudioToolbox.framework"); break; + case TargetPlatform.Web: + options.LinkEnv.CustomArgs.Add("-lopenal"); + break; default: throw new InvalidPlatformException(options.Platform.Target); } } diff --git a/Source/Engine/Audio/Audio.cpp b/Source/Engine/Audio/Audio.cpp index 5574919c4..0441e27a0 100644 --- a/Source/Engine/Audio/Audio.cpp +++ b/Source/Engine/Audio/Audio.cpp @@ -145,6 +145,11 @@ void Audio::SetEnableHRTF(bool value) { if (EnableHRTF == value) return; + if (value && EnumHasNoneFlags(AudioBackend::Features(), AudioBackend::FeatureFlags::HRTF)) + { + LOG(Warning, "HRTF audio is not supported."); + return; + } EnableHRTF = value; AudioBackend::Listener::ReinitializeAll(); } @@ -203,6 +208,11 @@ bool AudioService::Init() { LOG(Warning, "Failed to initialize audio backend."); } + if (EnableHRTF && EnumHasNoneFlags(AudioBackend::Features(), AudioBackend::FeatureFlags::HRTF)) + { + LOG(Warning, "HRTF audio is not supported."); + EnableHRTF = false; + } Engine::Pause.Bind(&OnEnginePause); Engine::Unpause.Bind(&OnEngineUnpause); diff --git a/Source/Engine/Audio/AudioBackend.h b/Source/Engine/Audio/AudioBackend.h index 7d3d087a7..bd91185a4 100644 --- a/Source/Engine/Audio/AudioBackend.h +++ b/Source/Engine/Audio/AudioBackend.h @@ -20,6 +20,8 @@ public: None = 0, // Supports multi-channel (incl. stereo) audio playback for spatial sources (3D), otherwise 3d audio needs to be in mono format. SpatialMultiChannel = 1, + // Supports HRTF audio. + HRTF = 2, }; static AudioBackend* Instance; diff --git a/Source/Engine/Audio/OpenAL/AudioBackendOAL.cpp b/Source/Engine/Audio/OpenAL/AudioBackendOAL.cpp index 715ede74f..68ad4007f 100644 --- a/Source/Engine/Audio/OpenAL/AudioBackendOAL.cpp +++ b/Source/Engine/Audio/OpenAL/AudioBackendOAL.cpp @@ -136,7 +136,11 @@ namespace ALC { ASSERT_LOW_LAYER(sourceID == 0); alGenSources(1, &sourceID); - ASSERT_LOW_LAYER(sourceID != 0); + if (sourceID == 0) + { + ALC_CHECK_ERROR(alGenSources); + return; + } alSourcef(sourceID, AL_GAIN, volume); alSourcef(sourceID, AL_PITCH, pitch); @@ -174,14 +178,23 @@ namespace ALC if (Device == nullptr) return; +#if PLATFORM_WEB + ALCint* attrList = nullptr; +#else ALCint attrList[] = { ALC_HRTF_SOFT, ALC_FALSE }; if (Audio::GetEnableHRTF()) { LOG(Info, "Enabling OpenAL HRTF"); attrList[1] = ALC_TRUE; } +#endif Context = alcCreateContext(Device, attrList); + if (Context == nullptr) + { + LOG(Error, "Failed to create OpenAL context."); + return; + } alcMakeContextCurrent(Context); } @@ -198,7 +211,7 @@ namespace ALC AudioSource* source = Audio::Sources[i]; Source::Rebuild(source->SourceID, source->GetPosition(), source->GetOrientation(), source->GetVolume(), source->GetPitch(), source->GetPan(), source->GetIsLooping() && !source->UseStreaming(), source->Is3D(), source->GetAttenuation(), source->GetMinDistance(), source->GetDopplerFactor()); - if (states.HasItems()) + if (states.HasItems() && source->SourceID) { // Restore playback state auto& state = states[i]; @@ -337,18 +350,18 @@ void AudioBackendOAL::Listener_ReinitializeAll() uint32 AudioBackendOAL::Source_Add(const AudioDataInfo& format, const Vector3& position, const Quaternion& orientation, float volume, float pitch, float pan, bool loop, bool spatial, float attenuation, float minDistance, float doppler) { PROFILE_MEM(Audio); - uint32 sourceID = 0; ALC::Source::Rebuild(sourceID, position, orientation, volume, pitch, pan, loop, spatial, attenuation, minDistance, doppler); - - // Cache audio data format assigned on source (used in Source_GetCurrentBufferTime) - ALC::Locker.Lock(); - auto& data = ALC::SourcesData[sourceID]; - data.Format = format; - data.Spatial = spatial; - data.Pan = pan; - ALC::Locker.Unlock(); - + if (sourceID) + { + // Cache audio data format assigned on source (used in Source_GetCurrentBufferTime) + ALC::Locker.Lock(); + auto& data = ALC::SourcesData[sourceID]; + data.Format = format; + data.Spatial = spatial; + data.Pan = pan; + ALC::Locker.Unlock(); + } return sourceID; } @@ -646,6 +659,13 @@ void AudioBackendOAL::Base_OnActiveDeviceChanged() PROFILE_CPU(); PROFILE_MEM(Audio); + // Fast-path on startup + if (!ALC::Inited && ALC::Device) + { + ALC::RebuildContext(); + return; + } + // Cleanup Array states; states.EnsureCapacity(Audio::Sources.Count()); @@ -788,7 +808,6 @@ bool AudioBackendOAL::Base_Init() devicesStr += (device.InternalName.Length() + 1) * sizeof(ALCchar); } - if (activeDeviceIndex == -1) { LOG(Warning, "Failed to pick a default device"); @@ -829,18 +848,21 @@ bool AudioBackendOAL::Base_Init() } // Init - Base_SetDopplerFactor(AudioSettings::Get()->DopplerFactor); - alDistanceModel(AL_INVERSE_DISTANCE_CLAMPED); // Default attenuation model - int32 clampedIndex = Math::Clamp(activeDeviceIndex, -1, Audio::Devices.Count() - 1); - if (clampedIndex == Audio::GetActiveDeviceIndex()) - { - ALC::RebuildContext(true); - } - Audio::SetActiveDeviceIndex(activeDeviceIndex); #ifdef AL_SOFT_source_spatialize if (ALC::IsExtensionSupported("AL_SOFT_source_spatialize")) ALC::Features = EnumAddFlags(ALC::Features, FeatureFlags::SpatialMultiChannel); #endif +#if !PLATFORM_WEB + ALC::Features = EnumAddFlags(ALC::Features, FeatureFlags::HRTF); +#endif + Base_SetDopplerFactor(AudioSettings::Get()->DopplerFactor); + alDistanceModel(AL_INVERSE_DISTANCE_CLAMPED); // Default attenuation model + if (Audio::GetActiveDeviceIndex() == Math::Clamp(activeDeviceIndex, -1, Audio::Devices.Count() - 1)) + { + // Manually create context if SetActiveDeviceIndex won't call it + Base_OnActiveDeviceChanged(); + } + Audio::SetActiveDeviceIndex(activeDeviceIndex); ALC::Inited = true; // Log service info diff --git a/Source/Tools/Flax.Build/Build/NativeCpp/Builder.NativeCpp.cs b/Source/Tools/Flax.Build/Build/NativeCpp/Builder.NativeCpp.cs index fb07b0886..daa2b5062 100644 --- a/Source/Tools/Flax.Build/Build/NativeCpp/Builder.NativeCpp.cs +++ b/Source/Tools/Flax.Build/Build/NativeCpp/Builder.NativeCpp.cs @@ -429,6 +429,7 @@ namespace Flax.Build var dependencyModule = buildData.Rules.GetModule(moduleName); if (dependencyModule != null && buildData.Modules.TryGetValue(dependencyModule, out var dependencyOptions)) { + moduleOptions.LinkEnv.CustomArgs.AddRange(dependencyOptions.LinkEnv.CustomArgs); moduleOptions.LinkEnv.InputFiles.AddRange(dependencyOptions.OutputFiles); moduleOptions.DependencyFiles.AddRange(dependencyOptions.DependencyFiles); moduleOptions.OptionalDependencyFiles.AddRange(dependencyOptions.OptionalDependencyFiles); @@ -445,6 +446,7 @@ namespace Flax.Build var dependencyModule = buildData.Rules.GetModule(moduleName); if (dependencyModule != null && buildData.Modules.TryGetValue(dependencyModule, out var dependencyOptions)) { + moduleOptions.LinkEnv.CustomArgs.AddRange(dependencyOptions.LinkEnv.CustomArgs); moduleOptions.LinkEnv.InputFiles.AddRange(dependencyOptions.OutputFiles); moduleOptions.DependencyFiles.AddRange(dependencyOptions.DependencyFiles); moduleOptions.OptionalDependencyFiles.AddRange(dependencyOptions.OptionalDependencyFiles); @@ -940,6 +942,7 @@ namespace Flax.Build var moduleOptions = BuildModule(buildData, module); // Merge module into target environment + buildData.TargetOptions.LinkEnv.CustomArgs.AddRange(moduleOptions.LinkEnv.CustomArgs); buildData.TargetOptions.LinkEnv.InputFiles.AddRange(moduleOptions.OutputFiles); buildData.TargetOptions.DependencyFiles.AddRange(moduleOptions.DependencyFiles); buildData.TargetOptions.OptionalDependencyFiles.AddRange(moduleOptions.OptionalDependencyFiles);