Merge branch 'Tryibion-audio-device-change'

This commit is contained in:
2026-10-01 10:13:46 +02:00
6 changed files with 405 additions and 32 deletions
+54 -1
View File
@@ -41,6 +41,8 @@ Array<AudioSource*> Audio::Sources;
Array<AudioDevice> 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;
+33 -1
View File
@@ -43,6 +43,16 @@ public:
/// </summary>
API_EVENT() static Action ActiveDeviceChanged;
/// <summary>
/// Event called when a new audio device is connected to the system.
/// </summary>
API_EVENT() static Action DeviceAdded;
/// <summary>
/// Event called when an audio device is disconnected from the system.
/// </summary>
API_EVENT() static Action DeviceRemoved;
public:
/// <summary>
/// Gets the active device.
@@ -51,17 +61,39 @@ public:
API_PROPERTY() static AudioDevice* GetActiveDevice();
/// <summary>
/// Gets the index of the active device.
/// Gets the index of the active device. Returns -1 if set to system default mode.
/// </summary>
/// <returns>The active device index.</returns>
API_PROPERTY() static int32 GetActiveDeviceIndex();
/// <summary>
/// Gets the resolved index in Devices array of the device currently playing (resolves system default mode to the actual device index).
/// </summary>
/// <returns>The resolved active device index.</returns>
API_PROPERTY() static int32 GetActiveDeviceResolvedIndex();
/// <summary>
/// Sets the index of the active device.
/// </summary>
/// <param name="index">The index.</param>
API_PROPERTY() static void SetActiveDeviceIndex(int32 index);
/// <summary>
/// Sets the index of the active device without notifying backend.
/// </summary>
/// <param name="index">The index.</param>
static void SetActiveDeviceIndexSilent(int32 index);
/// <summary>
/// Gets the internal device name explicitly selected by the user (empty if using dynamic system default device).
/// </summary>
static const StringAnsi& GetExplicitDeviceName();
/// <summary>
/// Sets the system default internal device name.
/// </summary>
static void SetSystemDefaultDeviceName(const StringAnsi& name);
public:
/// <summary>
/// Gets the master volume applied to all the audio sources (normalized to range 0-1).
+3
View File
@@ -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);
+26 -4
View File
@@ -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);
+1
View File
@@ -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:
/// <summary>
+288 -26
View File
@@ -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<AudioSourceState>& states)
void RebuildSources(const Array<AudioSourceState>& 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<ALC::AudioSourceState> 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<float>(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<AudioDevice> 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()