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
AudioResource.cs
2using Microsoft.Extensions.Logging;
3using Newtonsoft.Json;
4
5namespace Gondwana.Audio;
6
11[JsonObject(IsReference = true)]
12public class AudioResource : IDisposable
13{
18 public const float MinimumPlaybackSpeed = 0.25f;
19
23 public const float MaximumPlaybackSpeed = 4.0f;
24
25 private IAudioPlaybackHandle? _playback;
26 private bool _disposed;
27
28 [JsonProperty]
29 private float _volume = 1.0f;
30
31 [JsonProperty]
32 private float _pan;
33
34 [JsonProperty]
35 private float _playbackSpeed = 1.0f;
36
37 [JsonProperty]
38 private bool _isLooping;
39
43 public event EventHandler? PlaybackCompleted;
44
48 public Func<Task>? PlaybackCompletedAsync;
49
53 public event EventHandler? Disposed;
54
55 [JsonConstructor]
56 private AudioResource()
57 {
58 Key = string.Empty;
59 }
60
61 internal AudioResource(
62 string key,
63 IAudioPlaybackHandle playback,
64 float volume = 1.0f,
65 float pan = 0.0f,
66 float playbackSpeed = 1.0f,
67 string? filePathOrExt = null,
68 byte[]? rawBytes = null,
69 AssetsFileIdentifier? assetIdentifier = null)
70 {
71 Key = key;
72 _playback = playback ?? throw new ArgumentNullException(nameof(playback));
73
74 AssetIdentifier = assetIdentifier;
75 SourceFilePath = assetIdentifier is null && !string.IsNullOrWhiteSpace(filePathOrExt) && File.Exists(filePathOrExt)
76 ? filePathOrExt
77 : null;
78
79 var ext = Path.GetExtension(filePathOrExt ?? string.Empty);
80 SourceExtension = string.IsNullOrEmpty(ext) ? null : NormalizeExtension(ext);
81 OriginalBytes = rawBytes;
82
83 Volume = volume;
84 Pan = pan;
85 PlaybackSpeed = playbackSpeed;
86 IsLooping = false;
87
88 _playback.PlaybackCompleted += OnPlaybackCompleted;
89 }
90
92 [JsonProperty]
93 public string Key { get; private set; }
94
96 [JsonIgnore]
97 public byte[]? OriginalBytes { get; private set; }
98
102 [JsonProperty]
103 public string? SourceFilePath { get; private set; }
104
106 [JsonProperty]
107 public AssetsFileIdentifier? AssetIdentifier { get; private set; }
108
110 [JsonProperty]
111 public string? SourceExtension { get; private set; }
112
114 [JsonProperty]
115 public string? SourceUri { get; private set; }
116
120 [JsonIgnore]
121 public string? TempFilePath => _playback?.TemporaryFilePath;
122
126 [JsonIgnore]
127 public bool IsPaused => State == AudioPlaybackState.Paused;
128
132 [JsonIgnore]
133 public bool IsPlaying => State == AudioPlaybackState.Playing;
134
138 [JsonIgnore]
139 public AudioPlaybackState State => _playback?.State ?? AudioPlaybackState.Stopped;
140
145 [JsonIgnore]
146 public TimeSpan CurrentTime
147 {
148 get => _playback?.CurrentTime ?? TimeSpan.Zero;
149 set => Seek(value);
150 }
151
155 [JsonIgnore]
156 public TimeSpan Duration => _playback?.Duration ?? TimeSpan.Zero;
157
159 public bool IsLooping
160 {
161 get => _isLooping;
162 set
163 {
164 _isLooping = value;
165 if (_playback is not null)
166 _playback.IsLooping = value;
167 }
168 }
169
171 public float Volume
172 {
173 get => _volume;
174 set
175 {
176 if (float.IsNaN(value)) throw new ArgumentOutOfRangeException(nameof(value));
177 _volume = Math.Clamp(value, 0f, 1f);
178 if (_playback is not null)
179 _playback.Volume = _volume;
180 }
181 }
182
184 public float Pan
185 {
186 get => _pan;
187 set
188 {
189 if (float.IsNaN(value)) throw new ArgumentOutOfRangeException(nameof(value));
190 _pan = Math.Clamp(value, -1f, 1f);
191 if (_playback is not null)
192 _playback.Pan = _pan;
193 }
194 }
195
199 public float PlaybackSpeed
200 {
201 get => _playbackSpeed;
202 set
203 {
204 if (float.IsNaN(value)) throw new ArgumentOutOfRangeException(nameof(value));
205 _playbackSpeed = Math.Clamp(value, MinimumPlaybackSpeed, MaximumPlaybackSpeed);
206 if (_playback is not null)
207 _playback.PlaybackSpeed = _playbackSpeed;
208 }
209 }
210
215 public void Play(bool fromStart = true) => Playback.Play(fromStart);
216
220 public void Pause() => Playback.Pause();
221
225 public void Resume() => Playback.Resume();
226
231 public void Seek(TimeSpan position) => Playback.Seek(position);
232
236 public void Stop() => Playback.Stop();
237
238 internal void SetSourceUri(string uri)
239 {
240 SourceUri = uri;
241 SourceFilePath = null;
242 SourceExtension = NormalizeExtension(Path.GetExtension(new Uri(uri, UriKind.RelativeOrAbsolute).IsAbsoluteUri
243 ? new Uri(uri).AbsolutePath
244 : uri));
245 }
246
247 internal void SetAssetIdentifier(AssetsFileIdentifier identifier) => AssetIdentifier = identifier;
248
249 internal void CopySourceFrom(AudioResource original)
250 {
253 }
254
259 internal void ReloadIntoManager(bool forceReload = false)
260 {
261 if (string.IsNullOrWhiteSpace(Key))
262 throw new InvalidOperationException("AudioResource has no Key and cannot be reloaded.");
263
264 var manager = AudioResourceManager.Instance;
265
266 if (!forceReload && manager.TryGet(Key, out var existing) && existing is not null)
267 {
268 existing.Volume = Volume;
269 existing.Pan = Pan;
270 existing.PlaybackSpeed = PlaybackSpeed;
271 existing.IsLooping = IsLooping;
272 return;
273 }
274
275 if (forceReload && manager.Contains(Key))
276 manager.Unload(Key);
277
278 AudioResource loaded;
279 if (AssetIdentifier is not null)
280 {
281 using var stream = AssetIdentifier.Data;
282 if (stream is null)
283 throw new InvalidOperationException($"Missing asset data for {Key}.");
284
285 loaded = manager.LoadFromStream(Key, stream, SourceExtension ?? ".wav", Volume, Pan, PlaybackSpeed);
286 loaded.SetAssetIdentifier(AssetIdentifier);
287 }
288 else if (!string.IsNullOrWhiteSpace(SourceFilePath))
289 {
290 loaded = manager.LoadFromFile(Key, SourceFilePath, Volume, Pan, PlaybackSpeed);
291 }
292 else if (!string.IsNullOrWhiteSpace(SourceUri))
293 {
294 loaded = manager.LoadFromUri(Key, SourceUri, Volume, Pan, PlaybackSpeed);
295 }
296 else
297 {
298 throw new InvalidOperationException($"AudioResource '{Key}' has no persisted source.");
299 }
300
301 loaded.IsLooping = IsLooping;
302 }
303
304 private IAudioPlaybackHandle Playback
305 {
306 get
307 {
308 ObjectDisposedException.ThrowIf(_disposed, this);
309 return _playback ?? throw new InvalidOperationException(
310 $"AudioResource '{Key}' is not attached to an audio backend. Configure an audio backend before loading or restoring audio.");
311 }
312 }
313
314 private async void OnPlaybackCompleted(object? sender, EventArgs e)
315 {
316 try
317 {
318 if (PlaybackCompletedAsync is not null)
319 await PlaybackCompletedAsync().ConfigureAwait(false);
320 }
321 catch (Exception ex)
322 {
323 Engine.Logger.LogError(ex, "PlaybackCompletedAsync threw an exception for audio resource: {Key}", Key);
324 }
325 finally
326 {
327 PlaybackCompleted?.Invoke(this, EventArgs.Empty);
328 }
329 }
330
331 private static string? NormalizeExtension(string? extension)
332 {
333 if (string.IsNullOrWhiteSpace(extension))
334 return null;
335
336 return extension.StartsWith('.')
337 ? extension.ToLowerInvariant()
338 : "." + extension.ToLowerInvariant();
339 }
340
345 public void Dispose()
346 {
347 if (_disposed)
348 return;
349
350 if (_playback is not null)
351 {
352 _playback.PlaybackCompleted -= OnPlaybackCompleted;
353 _playback.Dispose();
354 _playback = null;
355 }
356
357 _disposed = true;
358 Disposed?.Invoke(this, EventArgs.Empty);
359 GC.SuppressFinalize(this);
360 }
361}
Represents an identifier for a specific asset within a file, including its type and name.
Stream? Data
Gets the stream containing the asset data.
Represents an audio resource that can be played, paused, resumed, sought, and disposed....
float Pan
Gets or sets stereo pan from -1.0 (left) through 1.0 (right).
string Key
Gets the unique key associated with this audio resource.
EventHandler? Disposed
Event raised when the resource is disposed.
float Volume
Gets or sets output volume in the range 0.0 through 1.0.
bool IsLooping
Gets or sets whether playback loops continuously.
TimeSpan CurrentTime
Gets or sets the current playback position within the resource. Setting this property seeks to the sp...
string? SourceExtension
Normalized file extension used by the configured backend.
float PlaybackSpeed
Gets or sets playback speed. 1.0 is normal speed; supported values are clamped to 0....
bool IsPlaying
Gets a value indicating whether playback is currently active.
string? TempFilePath
Gets the temporary file path used by a backend that requires file-based decoding, if any.
const float MaximumPlaybackSpeed
Maximum allowed playback speed multiplier. Values above this are clamped.
void Dispose()
Disposes the audio resource and releases any underlying playback handle and temporary files....
TimeSpan Duration
Gets the total duration of the resource, or TimeSpan.Zero when unknown.
byte?[] OriginalBytes
Gets the original byte array of the audio data, if available.
bool IsPaused
Gets a value indicating whether playback is currently paused.
void Stop()
Stops playback immediately.
void Pause()
Pauses playback if currently playing.
EventHandler? PlaybackCompleted
Event raised when non-looping playback reaches the end of the resource.
const float MinimumPlaybackSpeed
Minimum allowed playback speed multiplier. Values below this are clamped.
Func< Task >? PlaybackCompletedAsync
Optional asynchronous callback invoked when non-looping playback completes.
string? SourceFilePath
Original file path when the sound was loaded from disk. Null when loaded from an AssetsFile or URI.
AudioPlaybackState State
Gets the current high-level playback state of the resource.
void Play(bool fromStart=true)
Starts playback of the resource.
AssetsFileIdentifier? AssetIdentifier
Asset identifier when the sound was loaded from an AssetsFile.
string? SourceUri
Source URI when the resource was loaded by URI, primarily for browser playback.
void Seek(TimeSpan position)
Seeks to the specified playback position.
void Resume()
Resumes playback if currently paused.
Backend-provided playback handle used by AudioResource. Implements IDisposable to allow releasing any...
AudioPlaybackState
Describes the current playback state of an audio resource without exposing backend-specific playback ...