Gondwana Game Engine Development (master) - 2.6.1
Gondwana is a cross-platform 2.5D game and rendering engine written in C#/.NET 8. It provides fine-grained control over rendering, timing, and scene composition, with built-in support for parallax, z-ordering, pixel overhang, collision detection, and particle effects. Gondwana targets desktop and web platforms using SkiaSharp for graphics and pluggable audio backends for desktop and browser targets.
Loading...
Searching...
No Matches
AudioResourceManager.cs
1using System.Collections.Concurrent;
3using Microsoft.Extensions.Logging;
4
5namespace Gondwana.Audio;
6
10public sealed class AudioResourceManager : IDisposable
11{
12 [Newtonsoft.Json.JsonIgnore]
13 internal DefinitionProvenance? DefinitionProvenance { get; set; }
14
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();
18 private IAudioBackend? _backend;
19 private bool _disposed;
20
25 public event EventHandler<(string Key, AudioResource Resource)>? SoundDisposed;
26
27 private AudioResourceManager() { }
28
32 public static AudioResourceManager Instance => _instance.Value;
33
35 public IAudioBackend? Backend => _backend;
36
38 public bool IsBackendConfigured => _backend is not null;
39
46 public void ConfigureBackend(IAudioBackend backend)
47 {
48 ArgumentNullException.ThrowIfNull(backend);
49
50 lock (_backendLock)
51 {
52 if (ReferenceEquals(_backend, backend))
53 return;
54
55 if (_backend is not null && _soundResources.Count != 0)
56 {
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.");
59 }
60
61 _backend = backend;
62 Engine.Logger.LogInformation("Configured Gondwana audio backend: {AudioBackend}", backend.Name);
63 }
64 }
65
75 public AudioResource LoadFromFile(string key, string filePath, float volume, float pan)
76 => LoadFromFile(key, filePath, volume, pan, 1.0f);
77
88 string key,
89 string filePath,
90 float volume = 1.0f,
91 float pan = 0.0f,
92 float playbackSpeed = 1.0f)
93 {
94 var bytes = File.ReadAllBytes(filePath);
95 return LoadFromBytes(key, bytes, filePath, volume, pan, playbackSpeed);
96 }
97
108 public AudioResource LoadFromStream(string key, Stream input, string fileExt, float volume, float pan)
109 => LoadFromStream(key, input, fileExt, volume, pan, 1.0f);
110
122 string key,
123 Stream input,
124 string fileExt,
125 float volume = 1.0f,
126 float pan = 0.0f,
127 float playbackSpeed = 1.0f)
128 {
129 using var ms = new MemoryStream();
130 input.CopyTo(ms);
131 return LoadFromBytes(key, ms.ToArray(), fileExt, volume, pan, playbackSpeed);
132 }
133
145 string key,
146 string uri,
147 float volume = 1.0f,
148 float pan = 0.0f,
149 float playbackSpeed = 1.0f)
150 {
151 lock (_backendLock)
152 {
153 if (string.IsNullOrWhiteSpace(uri))
154 throw new ArgumentException("Audio URI cannot be empty.", nameof(uri));
155
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);
159 AudioResource? resource = null;
160 try
161 {
162 resource = new AudioResource(key, playback, volume, pan, speed);
163 resource.SetSourceUri(uri);
164 RegisterLoadedSound(key, resource);
165 return resource;
166 }
167 catch
168 {
169 DisposeFailedLoad(resource, playback);
170 throw;
171 }
172 }
173 }
174
183 public List<AudioResource> LoadFromEngineAssetsFile(AssetsFile resourceFile, float defaultVolume, float defaultPan)
184 => LoadFromEngineAssetsFile(resourceFile, defaultVolume, defaultPan, 1.0f);
185
194 public List<AudioResource> LoadFromEngineAssetsFile(
195 AssetsFile resourceFile,
196 float defaultVolume = 1.0f,
197 float defaultPan = 0.0f,
198 float defaultPlaybackSpeed = 1.0f)
199 {
200 List<AudioResource> loadedSounds = [];
201
202 foreach (var entry in resourceFile.GetAllEntries())
203 {
204 if (entry.AssetType != AssetTypes.Audio)
205 continue;
206
207 lock (_backendLock)
208 {
209 if (_soundResources.ContainsKey(entry.AssetName))
210 {
211 Engine.Logger.LogDebug("AudioResource '{Key}' already loaded. Skipping.", entry.AssetName);
212 continue;
213 }
214
215 var stream = resourceFile.Get(entry.AssetType, entry.AssetName);
216 if (stream is null)
217 {
218 Engine.Logger.LogWarning("Failed to retrieve stream for audio resource: {Key}", entry.AssetName);
219 continue;
220 }
221
222 try
223 {
224 using (stream)
225 using (var ms = new MemoryStream())
226 {
227 stream.CopyTo(ms);
228 var sound = LoadFromBytes(
229 entry.AssetName,
230 ms.ToArray(),
231 entry.AssetName,
232 defaultVolume,
233 defaultPan,
234 defaultPlaybackSpeed);
235 sound.SetAssetIdentifier(new AssetsFileIdentifier(resourceFile, AssetTypes.Audio, entry.AssetName));
236 loadedSounds.Add(sound);
237 }
238
239 Engine.Logger.LogInformation("Loaded sound: {Key}", entry.AssetName);
240 }
241 catch (Exception ex)
242 {
243 Engine.Logger.LogError(ex, "Error loading sound from asset file for key: {Key}", entry.AssetName);
244 throw;
245 }
246 }
247 }
248
249 return loadedSounds;
250 }
251
261 public AudioResource? Clone(string key, string? newKey, float? volume, float? pan)
262 => Clone(key, newKey, volume, pan, null);
263
275 string key,
276 string? newKey = null,
277 float? volume = null,
278 float? pan = null,
279 float? playbackSpeed = null)
280 {
281 lock (_backendLock)
282 {
283 if (!_soundResources.TryGetValue(key, out var original))
284 {
285 Engine.Logger.LogWarning("Attempted to clone non-existent AudioResource with key: {Key}", key);
286 return null;
287 }
288
289 newKey ??= $"{key}_clone_{Guid.NewGuid()}";
290 if (_soundResources.ContainsKey(newKey))
291 {
292 Engine.Logger.LogWarning("AudioResource with key '{Key}' already exists. Cannot clone.", newKey);
293 return null;
294 }
295
296 AudioResource clone;
297 if (original.OriginalBytes is not null && !string.IsNullOrEmpty(original.SourceExtension))
298 {
299 using var stream = new MemoryStream(original.OriginalBytes, writable: false);
300 clone = LoadFromStream(
301 newKey,
302 stream,
303 original.SourceExtension,
304 volume ?? original.Volume,
305 pan ?? original.Pan,
306 playbackSpeed ?? original.PlaybackSpeed);
307 }
308 else if (!string.IsNullOrWhiteSpace(original.SourceUri))
309 {
310 clone = LoadFromUri(
311 newKey,
312 original.SourceUri,
313 volume ?? original.Volume,
314 pan ?? original.Pan,
315 playbackSpeed ?? original.PlaybackSpeed);
316 }
317 else
318 {
319 Engine.Logger.LogWarning("Cannot clone AudioResource '{Key}' because its source cannot be recreated.", key);
320 return null;
321 }
322
323 clone.CopySourceFrom(original);
324 clone.IsLooping = original.IsLooping;
325 return clone;
326 }
327 }
328
329 private AudioResource LoadFromBytes(
330 string key,
331 byte[] bytes,
332 string fileHint,
333 float volume,
334 float pan,
335 float playbackSpeed)
336 {
337 lock (_backendLock)
338 {
339 var ext = Path.GetExtension(fileHint);
340 if (string.IsNullOrWhiteSpace(ext))
341 {
342 throw new InvalidOperationException(
343 $"Audio asset '{key}' has no file extension. Ensure audio AssetsFile entries retain their extension.");
344 }
345
346 ValidateSettings(key, volume, pan, playbackSpeed);
347 var speed = ClampPlaybackSpeed(playbackSpeed);
348 var playback = RequireBackend().CreateFromBytes(
349 key,
350 bytes,
351 fileHint,
352 Math.Clamp(volume, 0f, 1f),
353 Math.Clamp(pan, -1f, 1f),
354 speed);
355
356 AudioResource? sound = null;
357 try
358 {
359 sound = new AudioResource(key, playback, volume, pan, speed, fileHint, bytes);
360 RegisterLoadedSound(key, sound);
361 return sound;
362 }
363 catch
364 {
365 DisposeFailedLoad(sound, playback);
366 throw;
367 }
368 }
369 }
370
371 private static void DisposeFailedLoad(AudioResource? resource, IAudioPlaybackHandle playback)
372 {
373 try
374 {
375 if (resource is not null)
376 resource.Dispose();
377 else
378 playback.Dispose();
379 }
380 catch (Exception ex)
381 {
382 // Preserve the original load failure even if a backend also fails to dispose.
383 Engine.Logger.LogError(ex, "Failed to release an audio handle after loading failed.");
384 }
385 }
386
387 private void ReplaceExisting(string key)
388 {
389 if (_soundResources.TryGetValue(key, out var existing))
390 existing.Dispose();
391 }
392
393 private static void ValidateSettings(string key, float volume, float pan, float speed)
394 {
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.");
398 }
399
400 private void RegisterLoadedSound(string key, AudioResource sound)
401 {
402 ReplaceExisting(key);
403 _soundResources[key] = sound;
404 sound.Disposed += (_, _) =>
405 {
406 if (((ICollection<KeyValuePair<string, AudioResource>>)_soundResources).Remove(new(key, sound)))
407 {
408 SoundDisposed?.Invoke(this, (key, sound));
409 }
410 };
411 }
412
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.");
416
417 private static float ClampPlaybackSpeed(float speed) =>
418 Math.Clamp(speed, AudioResource.MinimumPlaybackSpeed, AudioResource.MaximumPlaybackSpeed);
419
424 public void Unload(string key)
425 {
426 lock (_backendLock)
427 {
428 if (_soundResources.TryGetValue(key, out var resource))
429 resource.Dispose();
430 }
431 }
432
436 public void Clear()
437 {
438 DefinitionProvenance = null;
439 lock (_backendLock)
440 {
441 foreach (var resource in _soundResources.Values.ToArray())
442 resource.Dispose();
443
444 _soundResources.Clear();
445 }
446 }
447
454 public bool TryGet(string key, out AudioResource? resource) => _soundResources.TryGetValue(key, out resource);
455
461 public AudioResource? Get(string key) => _soundResources.TryGetValue(key, out var resource) ? resource : null;
462
468 public bool Contains(string key) => _soundResources.ContainsKey(key);
469
474 public IEnumerable<string> GetAllKeys() => _soundResources.Keys;
475
480 public Dictionary<string, AudioResource> GetAll() => new(_soundResources);
481
485 public void Dispose()
486 {
487 if (_disposed)
488 return;
489
490 Clear();
491 _disposed = true;
492 }
493}
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,...
Definition AssetsFile.cs:18
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,...
Definition Engine.cs:36
static ILogger< Engine > Logger
Gets the logger instance used by the engine for diagnostic and informational messages.
Definition Engine.cs:59
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.
Definition AssetTypes.cs:8