using Engine.Audio.Contracts;
using Engine.Kernel.Diagnostics;
using Engine.Kernel.World;
namespace Engine.Audio;
///
/// Owns the one native miniaudio engine this plugin creates, and the
/// GameObject <-> loaded-sound handle mapping. loads a
/// clip the first time it sees a GameObject's AudioSource, applies
/// Volume/Loop changes without re-touching the native side when nothing
/// changed, fires PlayOnAwake exactly once, and — same no-destruction-event
/// problem PhysicsWorld has — unloads sounds for GameObjects that no
/// longer have an AudioSource.
///
/// useNullBackend exists for exactly one reason: Engine.Audio.Tests uses
/// it. Real gameplay always gets a real device (see AudioPlugin) — nothing
/// about the null backend belongs in a shipped build's own choice.
///
internal sealed class AudioWorld : IDisposable
{
private readonly ILogger _log;
private readonly Dictionary _handles = [];
private readonly Dictionary _lastSynced = [];
private readonly HashSet _warnedFailedToLoad = [];
public AudioWorld(ILogger log, bool useNullBackend)
{
_log = log;
if (Native.Lingua_Audio_Init(useNullBackend ? 1 : 0) == 0)
_log.Error("Failed to initialize the audio engine — no sounds will play.");
}
public void Sync(IWorld world)
{
var live = new HashSet();
foreach (var go in world.Query())
{
live.Add(go);
var src = go.GetComponent()!;
if (_warnedFailedToLoad.Contains(go))
continue;
if (!_handles.TryGetValue(go, out var handle))
{
handle = Native.Lingua_Audio_LoadSound(src.ClipPath);
if (handle < 0)
{
_log.Warn($"'{go.Name}' AudioSource failed to load '{src.ClipPath}'.");
_warnedFailedToLoad.Add(go);
continue;
}
_handles[go] = handle;
Native.Lingua_Audio_SetVolume(handle, src.Volume);
Native.Lingua_Audio_SetLooping(handle, src.Loop);
_lastSynced[go] = (src.Volume, src.Loop);
if (src.PlayOnAwake)
Native.Lingua_Audio_Play(handle);
continue;
}
var last = _lastSynced[go];
if (last.Volume != src.Volume)
Native.Lingua_Audio_SetVolume(handle, src.Volume);
if (last.Loop != src.Loop)
Native.Lingua_Audio_SetLooping(handle, src.Loop);
_lastSynced[go] = (src.Volume, src.Loop);
}
foreach (var stale in _handles.Keys.Where(go => !live.Contains(go)).ToList())
{
Native.Lingua_Audio_UnloadSound(_handles[stale]);
_handles.Remove(stale);
_lastSynced.Remove(stale);
}
_warnedFailedToLoad.RemoveWhere(go => !live.Contains(go));
}
public void Play(GameObject go)
{
if (_handles.TryGetValue(go, out var handle))
Native.Lingua_Audio_Play(handle);
}
public void Stop(GameObject go)
{
if (_handles.TryGetValue(go, out var handle))
Native.Lingua_Audio_Stop(handle);
}
public bool IsPlaying(GameObject go) =>
_handles.TryGetValue(go, out var handle) && Native.Lingua_Audio_IsPlaying(handle);
public void Dispose()
{
foreach (var handle in _handles.Values)
Native.Lingua_Audio_UnloadSound(handle);
_handles.Clear();
_lastSynced.Clear();
Native.Lingua_Audio_Shutdown();
}
}