diff --git a/Source/Engine/Audio/Audio.cpp b/Source/Engine/Audio/Audio.cpp index 3fea63ceb..ee71ec27d 100644 --- a/Source/Engine/Audio/Audio.cpp +++ b/Source/Engine/Audio/Audio.cpp @@ -41,6 +41,8 @@ Array Audio::Sources; Array Audio::Devices; Action Audio::DevicesChanged; Action Audio::ActiveDeviceChanged; +Action Audio::DeviceAdded; +Action Audio::DeviceRemoved; AudioBackend* AudioBackend::Instance = nullptr; namespace @@ -50,6 +52,8 @@ namespace int32 ActiveDeviceIndex = -1; bool MuteOnFocusLoss = true; bool EnableHRTF = true; + StringAnsi ExplicitDeviceName; + StringAnsi SystemDefaultDeviceName; } class AudioService : public EngineService @@ -93,7 +97,22 @@ void AudioSettings::Apply() AudioDevice* Audio::GetActiveDevice() { - return &Devices[ActiveDeviceIndex]; + if (ActiveDeviceIndex >= 0 && ActiveDeviceIndex < Devices.Count()) + return &Devices[ActiveDeviceIndex]; + + if (!SystemDefaultDeviceName.IsEmpty() && Devices.HasItems()) + { + for (int32 i = 0; i < Devices.Count(); i++) + { + if (Devices[i].InternalName == SystemDefaultDeviceName) + return &Devices[i]; + } + } + + if (Devices.HasItems()) + return &Devices[0]; + + return nullptr; } int32 Audio::GetActiveDeviceIndex() @@ -101,9 +120,28 @@ int32 Audio::GetActiveDeviceIndex() return ActiveDeviceIndex; } +int32 Audio::GetActiveDeviceResolvedIndex() +{ + AudioDevice* active = GetActiveDevice(); + if (active != nullptr) + { + for (int32 i = 0; i < Devices.Count(); i++) + { + if (&Devices[i] == active) + return i; + } + } + return -1; +} + void Audio::SetActiveDeviceIndex(int32 index) { index = Math::Clamp(index, -1, Devices.Count() - 1); + if (index >= 0 && index < Devices.Count()) + ExplicitDeviceName = Devices[index].InternalName; + else + ExplicitDeviceName.Clear(); + if (ActiveDeviceIndex == index) return; @@ -114,6 +152,21 @@ void Audio::SetActiveDeviceIndex(int32 index) ActiveDeviceChanged(); } +void Audio::SetActiveDeviceIndexSilent(int32 index) +{ + ActiveDeviceIndex = Math::Clamp(index, -1, Devices.Count() - 1); +} + +const StringAnsi& Audio::GetExplicitDeviceName() +{ + return ExplicitDeviceName; +} + +void Audio::SetSystemDefaultDeviceName(const StringAnsi& name) +{ + SystemDefaultDeviceName = name; +} + float Audio::GetMasterVolume() { return MasterVolume; diff --git a/Source/Engine/Audio/Audio.h b/Source/Engine/Audio/Audio.h index 36b5b3551..f9f03624b 100644 --- a/Source/Engine/Audio/Audio.h +++ b/Source/Engine/Audio/Audio.h @@ -43,6 +43,16 @@ public: /// API_EVENT() static Action ActiveDeviceChanged; + /// + /// Event called when a new audio device is connected to the system. + /// + API_EVENT() static Action DeviceAdded; + + /// + /// Event called when an audio device is disconnected from the system. + /// + API_EVENT() static Action DeviceRemoved; + public: /// /// Gets the active device. @@ -51,17 +61,39 @@ public: API_PROPERTY() static AudioDevice* GetActiveDevice(); /// - /// Gets the index of the active device. + /// Gets the index of the active device. Returns -1 if set to system default mode. /// /// The active device index. API_PROPERTY() static int32 GetActiveDeviceIndex(); + /// + /// Gets the resolved index in Devices array of the device currently playing (resolves system default mode to the actual device index). + /// + /// The resolved active device index. + API_PROPERTY() static int32 GetActiveDeviceResolvedIndex(); + /// /// Sets the index of the active device. /// /// The index. API_PROPERTY() static void SetActiveDeviceIndex(int32 index); + /// + /// Sets the index of the active device without notifying backend. + /// + /// The index. + static void SetActiveDeviceIndexSilent(int32 index); + + /// + /// Gets the internal device name explicitly selected by the user (empty if using dynamic system default device). + /// + static const StringAnsi& GetExplicitDeviceName(); + + /// + /// Sets the system default internal device name. + /// + static void SetSystemDefaultDeviceName(const StringAnsi& name); + public: /// /// Gets the master volume applied to all the audio sources (normalized to range 0-1). diff --git a/Source/Engine/Audio/AudioBackend.h b/Source/Engine/Audio/AudioBackend.h index bd91185a4..53b207854 100644 --- a/Source/Engine/Audio/AudioBackend.h +++ b/Source/Engine/Audio/AudioBackend.h @@ -102,6 +102,9 @@ public: class Source { public: + // Helper accessors for Audio Source playback restoring after audio device change + static void SetStreamingFirstChunk(AudioSource* source, int32 chunkIndex); + FORCE_INLINE static uint32 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) { return Instance->Source_Add(format, position, orientation, volume, pitch, pan, loop, spatial, attenuation, minDistance, doppler); diff --git a/Source/Engine/Audio/AudioSource.cpp b/Source/Engine/Audio/AudioSource.cpp index c8b0f3bab..8629da70e 100644 --- a/Source/Engine/Audio/AudioSource.cpp +++ b/Source/Engine/Audio/AudioSource.cpp @@ -12,6 +12,11 @@ #include "AudioBackend.h" #include "Audio.h" +void AudioBackend::Source::SetStreamingFirstChunk(AudioSource* source, int32 chunkIndex) +{ + source->_streamingFirstChunk = chunkIndex; +} + AudioSource::AudioSource(const SpawnParams& params) : Actor(params) , _velocity(Vector3::Zero) @@ -195,13 +200,14 @@ void AudioSource::Stop() _state = States::Stopped; _isActuallyPlayingSth = false; _streamingFirstChunk = 0; + _savedTime = 0.0f; if (SourceID) AudioBackend::Source::Stop(SourceID); } float AudioSource::GetTime() const { - if (_state == States::Stopped || SourceID == 0 || !Clip->IsLoaded()) + if (_state == States::Stopped || SourceID == 0 || Clip == nullptr || !Clip->IsLoaded()) return 0.0f; float time = AudioBackend::Source::GetCurrentBufferTime(SourceID); @@ -216,6 +222,11 @@ float AudioSource::GetTime() const time = Math::Clamp(time, 0.0f, Clip->GetLength()); + if (time <= 0.0001f && _savedTime > 0.0001f) + { + time = _savedTime; + } + return time; } @@ -233,6 +244,9 @@ void AudioSource::SetTime(float time) Stop(); } + // Restore _savedTime after Stop() resets it + _savedTime = time; + if (UseStreaming()) { // Update the first audio clip chunk to use for streaming and peek the relative time offset from the chunk start @@ -402,16 +416,24 @@ void AudioSource::Update() AudioBackend::Source::VelocityChanged(SourceID, _velocity); } + float curTime = 0.0f; + if (_state != States::Stopped && SourceID && Clip && Clip->IsLoaded()) + { + curTime = GetTime(); + if (curTime > 0.0001f) + _savedTime = curTime; + } + // Reset starting to play value once time is greater than zero - if (_startingToPlay && GetTime() > 0.0f) + if (_startingToPlay && curTime > 0.0f) { _startingToPlay = false; } - if (Math::NearEqual(GetTime(), _startTime) && _isActuallyPlayingSth && _startingToPlay) + if (Math::NearEqual(curTime, _startTime) && _isActuallyPlayingSth && _startingToPlay) ClipStarted(); - if (!UseStreaming() && Math::NearEqual(GetTime(), 0.0f) && _isActuallyPlayingSth && !_startingToPlay) + if (!UseStreaming() && Math::NearEqual(curTime, 0.0f) && _isActuallyPlayingSth && !_startingToPlay) { int32 queuedBuffers; AudioBackend::Source::GetQueuedBuffersCount(SourceID, queuedBuffers); diff --git a/Source/Engine/Audio/AudioSource.h b/Source/Engine/Audio/AudioSource.h index db698f3ba..00adc35e5 100644 --- a/Source/Engine/Audio/AudioSource.h +++ b/Source/Engine/Audio/AudioSource.h @@ -18,6 +18,7 @@ class FLAXENGINE_API AudioSource : public Actor, IAssetReference DECLARE_SCENE_OBJECT(AudioSource); friend class AudioStreamingHandler; friend class AudioClip; + friend class AudioBackend; public: /// diff --git a/Source/Engine/Audio/OpenAL/AudioBackendOAL.cpp b/Source/Engine/Audio/OpenAL/AudioBackendOAL.cpp index f32c34d30..501ebbb66 100644 --- a/Source/Engine/Audio/OpenAL/AudioBackendOAL.cpp +++ b/Source/Engine/Audio/OpenAL/AudioBackendOAL.cpp @@ -3,11 +3,13 @@ #if AUDIO_API_OPENAL #include "AudioBackendOAL.h" +#include "Engine/Platform/Platform.h" #include "Engine/Platform/StringUtils.h" #include "Engine/Core/Log.h" #include "Engine/Core/Collections/Dictionary.h" #include "Engine/Tools/AudioTool/AudioTool.h" #include "Engine/Engine/Units.h" +#include "Engine/Engine/Time.h" #include "Engine/Profiler/ProfilerCPU.h" #include "Engine/Profiler/ProfilerMemory.h" #include "Engine/Audio/Audio.h" @@ -204,23 +206,57 @@ namespace ALC Listener::Rebuild(listener); } - void RebuildSources(const Array& states) + void RebuildSources(const Array& states, float elapsedTime = 0.0f) { for (int32 i = 0; i < states.Count(); i++) { 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() && source->SourceID) + if (source->SourceID) { - // Restore playback state + // Restore playback state and time position without restarting or pausing auto& state = states[i]; if (state.State != AudioSource::States::Stopped) - source->Play(); - if (state.State == AudioSource::States::Paused) - source->Pause(); - if (state.State != AudioSource::States::Stopped) - source->SetTime(state.Time); + { + if (source->Clip && source->Clip->IsLoaded()) + { + float targetTime = state.Time; + if (state.State == AudioSource::States::Playing && elapsedTime > 0.0f) + { + targetTime += elapsedTime; + const float clipLength = source->Clip->GetLength(); + if (clipLength > 0.0001f) + { + if (source->GetIsLooping()) + targetTime = fmodf(targetTime, clipLength); + else + targetTime = Math::Min(targetTime, clipLength); + } + } + + if (source->UseStreaming()) + { + float relativeTime = 0; + const int32 chunkIndex = source->Clip->GetFirstBufferIndex(targetTime, relativeTime); + AudioBackend::Source::SetStreamingFirstChunk(source, chunkIndex); + source->RequestStreamingBuffersUpdate(); + } + else + { + if (source->Clip->Buffers.HasItems() && source->Clip->Buffers[0] != 0) + { + AudioBackend::Source::SetNonStreamingBuffer(source->SourceID, source->Clip->Buffers[0]); + AudioBackend::Source::SetCurrentBufferTime(source->SourceID, targetTime); + } + } + + if (state.State == AudioSource::States::Playing) + source->Play(); + else if (state.State == AudioSource::States::Paused) + source->Pause(); + } + } } } } @@ -658,7 +694,6 @@ void AudioBackendOAL::Base_OnActiveDeviceChanged() { PROFILE_CPU(); PROFILE_MEM(Audio); - // Fast-path on startup if (!ALC::Inited && ALC::Device) { @@ -666,6 +701,8 @@ void AudioBackendOAL::Base_OnActiveDeviceChanged() return; } + const double startTime = Platform::GetTimeSeconds(); + // Cleanup Array states; states.EnsureCapacity(Audio::Sources.Count()); @@ -687,15 +724,41 @@ void AudioBackendOAL::Base_OnActiveDeviceChanged() } // Open device - const StringAnsi& name = Audio::GetActiveDevice()->InternalName; - ALC::Device = alcOpenDevice(name.Get()); + const StringAnsi* namePtr = (Audio::Devices.HasItems() && Audio::GetActiveDeviceIndex() >= 0 && Audio::GetActiveDeviceIndex() < Audio::Devices.Count()) ? &Audio::GetActiveDevice()->InternalName : nullptr; + if (namePtr) + { + ALC::Device = alcOpenDevice(namePtr->Get()); + if (ALC::Device == nullptr) + { + LOG(Warning, "Failed to open active OpenAL device ({0}). Trying default device...", String(*namePtr)); + ALC::Device = alcOpenDevice(nullptr); + } + } + else + { + ALC::Device = alcOpenDevice(nullptr); + } + + if (ALC::Device == nullptr && Audio::Devices.HasItems()) + { + for (int32 i = 0; i < Audio::Devices.Count(); i++) + { + ALC::Device = alcOpenDevice(Audio::Devices[i].InternalName.Get()); + if (ALC::Device != nullptr) + break; + } + } + if (ALC::Device == nullptr) { - LOG(Fatal, "Failed to open OpenAL device ({0}).", String(name)); + LOG(Warning, "Failed to open any OpenAL audio device."); return; } if (ALC::Inited) - LOG(Info, "Changed audio device to: {}", String(Audio::GetActiveDevice()->Name)); + { + const AudioDevice* activeDev = Audio::GetActiveDevice(); + LOG(Info, "Changed audio device to: {}", activeDev ? String(activeDev->Name) : TEXT("Default")); + } // Rebuild context ALC::RebuildContext(); @@ -719,11 +782,16 @@ void AudioBackendOAL::Base_OnActiveDeviceChanged() else { // Reload audio clip - auto assetLock = audioClip->Storage->Lock(); - audioClip->LoadChunk(0); - audioClip->Buffers[0] = AudioBackend::Buffer::Create(); - audioClip->WriteBuffer(0); - + if (audioClip->Storage) + { + auto assetLock = audioClip->Storage->Lock(); + audioClip->LoadChunk(0); + } + if (audioClip->Buffers.HasItems()) + { + audioClip->Buffers[0] = AudioBackend::Buffer::Create(); + audioClip->WriteBuffer(0); + } } } @@ -739,8 +807,9 @@ void AudioBackendOAL::Base_OnActiveDeviceChanged() player.AudioSource = 0; } } + const float elapsedTime = static_cast(Platform::GetTimeSeconds() - startTime); ALC::RebuildListeners(); - ALC::RebuildSources(states); + ALC::RebuildSources(states, elapsedTime); } void AudioBackendOAL::Base_SetDopplerFactor(float value) @@ -786,6 +855,7 @@ bool AudioBackendOAL::Base_Init() const ALCchar* devicesStr = alcGetString(nullptr, ALC_ALL_DEVICES_SPECIFIER); const StringAnsi defaultDeviceName(defaultDevice); + Audio::SetSystemDefaultDeviceName(defaultDeviceName); devices.Clear(); devices.EnsureCapacity(8); @@ -848,13 +918,9 @@ bool AudioBackendOAL::Base_Init() } // Init - 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); + // Create context and open initial device + Base_OnActiveDeviceChanged(); + Audio::SetActiveDeviceIndexSilent(activeDeviceIndex); #ifdef AL_SOFT_source_spatialize if (ALC::IsExtensionSupported("AL_SOFT_source_spatialize")) ALC::Features = EnumAddFlags(ALC::Features, FeatureFlags::SpatialMultiChannel); @@ -875,8 +941,204 @@ bool AudioBackendOAL::Base_Init() return false; } +#ifndef ALC_CONNECTED +#define ALC_CONNECTED 0x313 +#endif + void AudioBackendOAL::Base_Update() { + // Check if active audio device got disconnected or invalidated (WASAPI AUDCLNT_E_DEVICE_INVALIDATED 0x88890004) + bool forceCheck = false; + if (ALC::Device != nullptr) + { + ALCint connected = ALC_TRUE; + alcGetIntegerv(ALC::Device, ALC_CONNECTED, 1, &connected); + if (connected == ALC_FALSE || alcGetError(ALC::Device) != ALC_NO_ERROR) + { + LOG(Warning, "Active audio device disconnected. Swapping active audio device..."); + forceCheck = true; + } + } + +#if ALC_ENUMERATE_ALL_EXT + + // Update audio devices list periodically (throttled every 0.25 seconds / 250ms, or immediately if disconnected) + static float deviceCheckTimer = 0.0f; + static StringAnsi lastDefaultDeviceName; + deviceCheckTimer += Time::Update.UnscaledDeltaTime.GetTotalSeconds(); + if (deviceCheckTimer >= 0.25f || forceCheck) + { + deviceCheckTimer = 0.0f; + if (ALC::IsExtensionSupported("ALC_ENUMERATE_ALL_EXT")) + { + const ALCchar* defaultDevice = alcGetString(nullptr, ALC_DEFAULT_ALL_DEVICES_SPECIFIER); + const StringAnsi defaultDeviceName(defaultDevice); + Audio::SetSystemDefaultDeviceName(defaultDeviceName); + const ALCchar* devicesStr = alcGetString(nullptr, ALC_ALL_DEVICES_SPECIFIER); + Array newDevices; + int32 defaultDeviceIndex = -1; + + while (devicesStr && *devicesStr) + { + const int32 i = newDevices.Count(); + auto& device = newDevices.AddOne(); + device.InternalName = devicesStr; + device.Name = String(device.InternalName).TrimTrailing(); + device.Name.Replace(TEXT("OpenAL Soft on "), TEXT("")); + + if (device.InternalName == defaultDeviceName) + { + defaultDeviceIndex = i; + } + + devicesStr += (device.InternalName.Length() + 1) * sizeof(ALCchar); + } + + if (defaultDeviceIndex == -1 && !newDevices.IsEmpty()) + defaultDeviceIndex = 0; + + bool devicesListChanged = newDevices.Count() != Audio::Devices.Count(); + if (!devicesListChanged) + { + for (int32 i = 0; i < newDevices.Count(); i++) + { + if (newDevices[i].InternalName != Audio::Devices[i].InternalName) + { + devicesListChanged = true; + break; + } + } + } + + const int32 currentActiveIndex = Audio::GetActiveDeviceIndex(); + const StringAnsi& explicitDeviceName = Audio::GetExplicitDeviceName(); + + // Determine the internal name of the currently active device. + // When in System Default Mode (index -1), fall back to the OS default name so + // comparisons correctly identify "no change" and avoid spurious device swaps. + StringAnsi currentActiveInternalName; + if (currentActiveIndex >= 0 && currentActiveIndex < Audio::Devices.Count()) + currentActiveInternalName = Audio::Devices[currentActiveIndex].InternalName; + else if (explicitDeviceName.IsEmpty()) + currentActiveInternalName = defaultDeviceName; // System Default Mode: treat current as OS default + + int32 currentActiveNewIndex = -1; + int32 explicitDeviceNewIndex = -1; + + for (int32 i = 0; i < newDevices.Count(); i++) + { + if (!currentActiveInternalName.IsEmpty() && newDevices[i].InternalName == currentActiveInternalName) + { + currentActiveNewIndex = i; + } + if (!explicitDeviceName.IsEmpty() && newDevices[i].InternalName == explicitDeviceName) + { + explicitDeviceNewIndex = i; + } + } + + bool defaultDeviceChanged = (!lastDefaultDeviceName.IsEmpty() && lastDefaultDeviceName != defaultDeviceName); + lastDefaultDeviceName = defaultDeviceName; + + int32 targetActiveIndex = -1; + if (!explicitDeviceName.IsEmpty()) + { + // Explicit Device Lock Mode: User selected a specific audio device + if (explicitDeviceNewIndex != -1) + { + // Explicit device is available (stay locked or re-claim explicit device) + targetActiveIndex = explicitDeviceNewIndex; + } + else if (forceCheck || currentActiveNewIndex == -1) + { + // Explicit device disconnected -> Fallback to system default + targetActiveIndex = (defaultDeviceIndex != -1) ? defaultDeviceIndex : 0; + } + else + { + targetActiveIndex = currentActiveNewIndex; + } + } + else + { + // Dynamic System Default Mode: Automatically follow OS default device changes + if (forceCheck) + { + targetActiveIndex = (defaultDeviceIndex != -1) ? defaultDeviceIndex : 0; + } + else if (defaultDeviceChanged && defaultDeviceIndex != -1) + { + targetActiveIndex = defaultDeviceIndex; + } + else if (currentActiveNewIndex != -1) + { + targetActiveIndex = currentActiveNewIndex; + } + else if (defaultDeviceIndex != -1) + { + targetActiveIndex = defaultDeviceIndex; + } + } + + if (devicesListChanged) + { + // Fire DeviceAdded for each device in newDevices not present in old list + for (int32 i = 0; i < newDevices.Count(); i++) + { + bool found = false; + for (int32 j = 0; j < Audio::Devices.Count(); j++) + { + if (Audio::Devices[j].InternalName == newDevices[i].InternalName) + { + found = true; + break; + } + } + if (!found) + Audio::DeviceAdded(); + } + + // Fire DeviceRemoved for each device in old list not present in newDevices + for (int32 i = 0; i < Audio::Devices.Count(); i++) + { + bool found = false; + for (int32 j = 0; j < newDevices.Count(); j++) + { + if (newDevices[j].InternalName == Audio::Devices[i].InternalName) + { + found = true; + break; + } + } + if (!found) + Audio::DeviceRemoved(); + } + + Audio::Devices = newDevices; + Audio::DevicesChanged(); + } + + if (targetActiveIndex != -1) + { + StringAnsi targetActiveInternalName = newDevices[targetActiveIndex].InternalName; + if (forceCheck) + { + Audio::SetActiveDeviceIndexSilent(targetActiveIndex); + Base_OnActiveDeviceChanged(); + } + else if (targetActiveInternalName != currentActiveInternalName) + { + Audio::SetActiveDeviceIndexSilent(targetActiveIndex); + Base_OnActiveDeviceChanged(); + } + else + { + Audio::SetActiveDeviceIndexSilent(targetActiveIndex); + } + } + } + } +#endif } void AudioBackendOAL::Base_Dispose()