1using System.Collections.Concurrent;
3using Microsoft.Extensions.Logging;
10public sealed class AudioResourceManager : IDisposable
12 [Newtonsoft.Json.JsonIgnore]
13 internal DefinitionProvenance? DefinitionProvenance {
get;
set; }
15 private static readonly Lazy<AudioResourceManager> _instance =
new(() =>
new AudioResourceManager());
16 private readonly ConcurrentDictionary<string, AudioResource> _soundResources =
new(StringComparer.Ordinal);
17 private readonly
object _backendLock =
new();
19 private bool _disposed;
27 private AudioResourceManager() { }
32 public static AudioResourceManager
Instance => _instance.Value;
48 ArgumentNullException.ThrowIfNull(backend);
52 if (ReferenceEquals(_backend, backend))
55 if (_backend is not
null && _soundResources.Count != 0)
57 throw new InvalidOperationException(
58 $
"Cannot replace audio backend '{_backend.Name}' with '{backend.Name}' while audio resources are loaded. Clear the audio resource manager first.");
62 Engine.
Logger.LogInformation(
"Configured Gondwana audio backend: {AudioBackend}", backend.
Name);
92 float playbackSpeed = 1.0f)
94 var bytes = File.ReadAllBytes(filePath);
95 return LoadFromBytes(key, bytes, filePath, volume, pan, playbackSpeed);
127 float playbackSpeed = 1.0f)
129 using var ms =
new MemoryStream();
131 return LoadFromBytes(key, ms.ToArray(), fileExt, volume, pan, playbackSpeed);
149 float playbackSpeed = 1.0f)
153 if (
string.IsNullOrWhiteSpace(uri))
154 throw new ArgumentException(
"Audio URI cannot be empty.", nameof(uri));
156 ValidateSettings(key, volume, pan, playbackSpeed);
157 var speed = ClampPlaybackSpeed(playbackSpeed);
158 var playback = RequireBackend().CreateFromUri(key, uri, Math.Clamp(volume, 0f, 1f), Math.Clamp(pan, -1f, 1f), speed);
162 resource =
new AudioResource(key, playback, volume, pan, speed);
163 resource.SetSourceUri(uri);
164 RegisterLoadedSound(key, resource);
169 DisposeFailedLoad(resource, playback);
196 float defaultVolume = 1.0f,
197 float defaultPan = 0.0f,
198 float defaultPlaybackSpeed = 1.0f)
200 List<AudioResource> loadedSounds = [];
202 foreach (var entry
in resourceFile.GetAllEntries())
209 if (_soundResources.ContainsKey(entry.AssetName))
211 Engine.
Logger.LogDebug(
"AudioResource '{Key}' already loaded. Skipping.", entry.AssetName);
215 var stream = resourceFile.Get(entry.AssetType, entry.AssetName);
218 Engine.
Logger.LogWarning(
"Failed to retrieve stream for audio resource: {Key}", entry.AssetName);
225 using (var ms =
new MemoryStream())
228 var sound = LoadFromBytes(
234 defaultPlaybackSpeed);
236 loadedSounds.Add(sound);
239 Engine.
Logger.LogInformation(
"Loaded sound: {Key}", entry.AssetName);
243 Engine.
Logger.LogError(ex,
"Error loading sound from asset file for key: {Key}", entry.AssetName);
262 =>
Clone(key, newKey, volume, pan,
null);
276 string? newKey =
null,
277 float? volume =
null,
279 float? playbackSpeed =
null)
283 if (!_soundResources.TryGetValue(key, out var original))
285 Engine.
Logger.LogWarning(
"Attempted to clone non-existent AudioResource with key: {Key}", key);
289 newKey ??= $
"{key}_clone_{Guid.NewGuid()}";
290 if (_soundResources.ContainsKey(newKey))
292 Engine.
Logger.LogWarning(
"AudioResource with key '{Key}' already exists. Cannot clone.", newKey);
297 if (original.OriginalBytes is not
null && !
string.IsNullOrEmpty(original.SourceExtension))
299 using var stream =
new MemoryStream(original.OriginalBytes, writable:
false);
303 original.SourceExtension,
304 volume ?? original.Volume,
306 playbackSpeed ?? original.PlaybackSpeed);
308 else if (!
string.IsNullOrWhiteSpace(original.SourceUri))
313 volume ?? original.Volume,
315 playbackSpeed ?? original.PlaybackSpeed);
319 Engine.
Logger.LogWarning(
"Cannot clone AudioResource '{Key}' because its source cannot be recreated.", key);
323 clone.CopySourceFrom(original);
324 clone.IsLooping = original.IsLooping;
339 var ext = Path.GetExtension(fileHint);
340 if (
string.IsNullOrWhiteSpace(ext))
342 throw new InvalidOperationException(
343 $
"Audio asset '{key}' has no file extension. Ensure audio AssetsFile entries retain their extension.");
346 ValidateSettings(key, volume, pan, playbackSpeed);
347 var speed = ClampPlaybackSpeed(playbackSpeed);
352 Math.Clamp(volume, 0f, 1f),
353 Math.Clamp(pan, -1f, 1f),
356 AudioResource? sound =
null;
359 sound =
new AudioResource(key, playback, volume, pan, speed, fileHint, bytes);
360 RegisterLoadedSound(key, sound);
365 DisposeFailedLoad(sound, playback);
371 private static void DisposeFailedLoad(AudioResource? resource, IAudioPlaybackHandle playback)
375 if (resource is not
null)
383 Engine.Logger.LogError(ex,
"Failed to release an audio handle after loading failed.");
387 private void ReplaceExisting(
string key)
389 if (_soundResources.TryGetValue(key, out var existing))
393 private static void ValidateSettings(
string key,
float volume,
float pan,
float speed)
395 ArgumentException.ThrowIfNullOrWhiteSpace(key);
396 if (
float.IsNaN(volume) ||
float.IsNaN(pan) ||
float.IsNaN(speed))
397 throw new ArgumentOutOfRangeException(nameof(speed),
"Audio settings must not be NaN.");
400 private void RegisterLoadedSound(
string key, AudioResource sound)
402 ReplaceExisting(key);
403 _soundResources[key] = sound;
404 sound.Disposed += (_, _) =>
406 if (((ICollection<KeyValuePair<string, AudioResource>>)_soundResources).Remove(
new(key, sound)))
413 private IAudioBackend RequireBackend() => _backend
414 ??
throw new InvalidOperationException(
415 "No Gondwana audio backend is configured. Install and initialize a backend such as Gondwana.Audio.NAudio or Gondwana.Audio.Browser before loading audio.");
417 private static float ClampPlaybackSpeed(
float speed) =>
418 Math.Clamp(speed, AudioResource.MinimumPlaybackSpeed, AudioResource.MaximumPlaybackSpeed);
428 if (_soundResources.TryGetValue(key, out var resource))
438 DefinitionProvenance =
null;
441 foreach (var resource
in _soundResources.Values.ToArray())
444 _soundResources.Clear();
454 public bool TryGet(
string key, out
AudioResource? resource) => _soundResources.TryGetValue(key, out resource);
461 public AudioResource?
Get(
string key) => _soundResources.TryGetValue(key, out var resource) ? resource :
null;
468 public bool Contains(
string key) => _soundResources.ContainsKey(key);
474 public IEnumerable<string>
GetAllKeys() => _soundResources.Keys;
480 public Dictionary<string, AudioResource>
GetAll() =>
new(_soundResources);
Represents an identifier for a specific asset within a file, including its type and name.
Represents an asset file used by the engine, providing functionality to load, manage,...
AudioResource? Clone(string key, string? newKey=null, float? volume=null, float? pan=null, float? playbackSpeed=null)
Creates a clone of an existing AudioResource. The clone may inherit or override volume,...
List< AudioResource > LoadFromEngineAssetsFile(AssetsFile resourceFile, float defaultVolume=1.0f, float defaultPan=0.0f, float defaultPlaybackSpeed=1.0f)
Loads all audio entries from the provided AssetsFile and returns the loaded resources.
void ConfigureBackend(IAudioBackend backend)
Configures the backend used for subsequently loaded audio resources. A backend cannot be replaced whi...
AudioResource LoadFromStream(string key, Stream input, string fileExt, float volume=1.0f, float pan=0.0f, float playbackSpeed=1.0f)
Loads an audio resource from the provided Stream.
List< AudioResource > LoadFromEngineAssetsFile(AssetsFile resourceFile, float defaultVolume, float defaultPan)
Compatibility overload that loads audio resources from an AssetsFile. Retains the pre-backend-refacto...
bool Contains(string key)
Determines whether an audio resource with the specified key is loaded.
AudioResource LoadFromUri(string key, string uri, float volume=1.0f, float pan=0.0f, float playbackSpeed=1.0f)
Loads an audio source by URI. This is primarily intended for URI-capable backends such as browser aud...
void Unload(string key)
Unloads and disposes the audio resource with the specified key, if it exists.
void Clear()
Disposes and clears all loaded audio resources.
AudioResource LoadFromStream(string key, Stream input, string fileExt, float volume, float pan)
Compatibility overload that loads an audio resource from a stream. Retains the pre-backend-refactor C...
bool IsBackendConfigured
Gets whether an audio backend has been configured.
IAudioBackend? Backend
Gets the currently configured audio backend, or null if none has been configured.
AudioResource LoadFromFile(string key, string filePath, float volume=1.0f, float pan=0.0f, float playbackSpeed=1.0f)
Loads an audio resource from a file on disk.
void Dispose()
Disposes the manager and all loaded audio resources. This method is idempotent.
EventHandler<(string Key, AudioResource Resource)>? SoundDisposed
Occurs when a loaded AudioResource is disposed and removed from the manager. The event payload contai...
Dictionary< string, AudioResource > GetAll()
Returns a snapshot dictionary of all loaded audio resources.
IEnumerable< string > GetAllKeys()
Returns all loaded audio resource keys.
static AudioResourceManager Instance
Gets the singleton AudioResourceManager instance.
AudioResource? Clone(string key, string? newKey, float? volume, float? pan)
Compatibility overload that creates a clone of an existing audio resource. Retains the pre-backend-re...
bool TryGet(string key, out AudioResource? resource)
Attempts to retrieve a loaded audio resource by key.
AudioResource? Get(string key)
Gets the AudioResource with the specified key, or null if not found.
AudioResource LoadFromFile(string key, string filePath, float volume, float pan)
Compatibility overload that loads an audio resource from a file path. Retains the pre-backend-refacto...
Represents an audio resource that can be played, paused, resumed, sought, and disposed....
Represents the core game engine singleton responsible for managing the main update loop,...
static ILogger< Engine > Logger
Gets the logger instance used by the engine for diagnostic and informational messages.
Creates backend-specific playback handles for the engine's common audio API.
IAudioPlaybackHandle CreateFromBytes(string key, byte[] data, string fileNameOrExtension, float volume, float pan, float playbackSpeed)
Creates a playback handle from raw audio bytes. The implementation is responsible for interpreting th...
string Name
Gets the human-readable name of the backend implementation.
AssetTypes
Defines the types of assets that can be stored and managed by the engine.