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
NAudioPlaybackHandle.cs
1using Microsoft.Extensions.Logging;
2using NAudio.Wave;
3using NAudio.Wave.SampleProviders;
4
6
11internal sealed class NAudioPlaybackHandle : IAudioPlaybackHandle
12{
13 private readonly string _key;
14 private readonly object _controlLock = new();
15 private readonly WaveStream _waveStream;
16 private readonly IWavePlayer _outputDevice;
17 private readonly VariableSpeedSampleProvider _speedProvider;
18
19 private PanningSampleProvider? _monoPanProvider;
20 private StereoPanSampleProvider? _stereoPanProvider;
21 private VolumeSampleProvider? _volumeProvider;
22
23 private bool _stopRequested;
24 private bool? _pendingPlay;
25 private TimeSpan? _pendingPosition;
26 private bool _disposed;
27 private bool _isLooping;
28 private float _volume;
29 private float _pan;
30 private float _playbackSpeed;
31
42 public NAudioPlaybackHandle(
43 string key,
44 WaveStream waveStream,
45 string? temporaryFilePath,
46 float volume,
47 float pan,
48 float playbackSpeed,
49 IWavePlayer? outputDevice = null)
50 {
51 _key = key;
52 _waveStream = waveStream;
53 TemporaryFilePath = temporaryFilePath;
54 _volume = Math.Clamp(volume, 0f, 1f);
55 _pan = Math.Clamp(pan, -1f, 1f);
56 _playbackSpeed = Math.Clamp(playbackSpeed, AudioResource.MinimumPlaybackSpeed, AudioResource.MaximumPlaybackSpeed);
57
58 var source = _waveStream.ToSampleProvider();
59 _speedProvider = new VariableSpeedSampleProvider(source)
60 {
61 PlaybackSpeed = _playbackSpeed
62 };
63
64 _outputDevice = outputDevice ?? new WaveOutEvent();
65
66 try
67 {
68 _outputDevice.Init(BuildAudioGraph(_speedProvider));
69 }
70 catch
71 {
72 _outputDevice.Dispose();
73 throw;
74 }
75
76 _outputDevice.PlaybackStopped += OnPlaybackStopped;
77 }
78
82 public event EventHandler? PlaybackCompleted;
83
88 public AudioPlaybackState State => _outputDevice.PlaybackState switch
89 {
90 PlaybackState.Playing => AudioPlaybackState.Playing,
91 PlaybackState.Paused => AudioPlaybackState.Paused,
92 _ => AudioPlaybackState.Stopped
93 };
94
99 public TimeSpan CurrentTime
100 {
101 get { lock (_controlLock) return _pendingPosition ?? _waveStream.CurrentTime; }
102 }
103
108 public TimeSpan Duration => _waveStream.TotalTime;
109
113 public string? TemporaryFilePath { get; }
114
118 public bool IsLooping
119 {
120 get => _isLooping;
121 set => _isLooping = value;
122 }
123
128 public float Volume
129 {
130 get => _volume;
131 set
132 {
133 _volume = Math.Clamp(value, 0f, 1f);
134 if (_volumeProvider is not null)
135 _volumeProvider.Volume = _volume;
136 }
137 }
138
143 public float Pan
144 {
145 get => _pan;
146 set
147 {
148 _pan = Math.Clamp(value, -1f, 1f);
149 if (_monoPanProvider is not null)
150 _monoPanProvider.Pan = _pan;
151 else if (_stereoPanProvider is not null)
152 ApplyStereoPan(_stereoPanProvider, _pan);
153 }
154 }
155
160 public float PlaybackSpeed
161 {
162 get => _playbackSpeed;
163 set
164 {
165 _playbackSpeed = Math.Clamp(value, AudioResource.MinimumPlaybackSpeed, AudioResource.MaximumPlaybackSpeed);
166 _speedProvider.PlaybackSpeed = _playbackSpeed;
167 }
168 }
169
174 public void Play(bool fromStart = true)
175 {
176 lock (_controlLock)
177 {
178 ThrowIfDisposed();
179
180 // WaveOutEvent.Stop completes asynchronously. Restart only after its worker exits.
181 if (_stopRequested)
182 {
183 _pendingPlay = fromStart;
184 return;
185 }
186
187 if (fromStart)
188 {
189 if (State != AudioPlaybackState.Stopped)
190 {
191 _stopRequested = true;
192 _pendingPlay = true;
193 _outputDevice.Stop();
194 return;
195 }
196
197 _speedProvider.Reset(() => _waveStream.Position = 0);
198 }
199
200 _stopRequested = false;
201 if (State != AudioPlaybackState.Playing)
202 _outputDevice.Play();
203 }
204 }
205
209 public void Pause()
210 {
211 lock (_controlLock)
212 {
213 ThrowIfDisposed();
214 if (State == AudioPlaybackState.Playing)
215 _outputDevice.Pause();
216 }
217 }
218
222 public void Resume()
223 {
224 lock (_controlLock)
225 {
226 ThrowIfDisposed();
227 if (State == AudioPlaybackState.Paused)
228 _outputDevice.Play();
229 }
230 }
231
236 public void Seek(TimeSpan position)
237 {
238 lock (_controlLock)
239 {
240 ThrowIfDisposed();
241 var clamped = position < TimeSpan.Zero
242 ? TimeSpan.Zero
243 : position > Duration ? Duration : position;
244
245 if (_stopRequested)
246 {
247 _pendingPosition = clamped;
248 return;
249 }
250
251 var wasPlaying = State == AudioPlaybackState.Playing;
252 if (wasPlaying)
253 _outputDevice.Pause();
254
255 _speedProvider.Reset(() => _waveStream.CurrentTime = clamped);
256
257 if (wasPlaying)
258 _outputDevice.Play();
259 }
260 }
261
266 public void Stop()
267 {
268 lock (_controlLock)
269 {
270 ThrowIfDisposed();
271 _pendingPlay = null;
272 if (_stopRequested)
273 {
274 _pendingPosition = TimeSpan.Zero;
275 return;
276 }
277 if (State != AudioPlaybackState.Stopped)
278 {
279 _pendingPosition = TimeSpan.Zero;
280 _stopRequested = true;
281 _outputDevice.Stop();
282 }
283 else
284 _speedProvider.Reset(() => _waveStream.Position = 0);
285 }
286 }
287
288 private ISampleProvider BuildAudioGraph(ISampleProvider source)
289 {
290 ISampleProvider current = source;
291 var channels = current.WaveFormat.Channels;
292
293 switch (channels)
294 {
295 case <= 0:
296 Engine.Logger.LogWarning("AudioResource {Key} has invalid channel count: {ChannelCount}", _key, channels);
297 break;
298
299 case 1:
300 _monoPanProvider = new PanningSampleProvider(current) { Pan = _pan };
301 current = _monoPanProvider;
302 break;
303
304 case 2:
305 _stereoPanProvider = new StereoPanSampleProvider(current);
306 ApplyStereoPan(_stereoPanProvider, _pan);
307 current = _stereoPanProvider;
308 break;
309
310 default:
311 var mux = new MultiplexingSampleProvider(new[] { current }, 2);
312 mux.ConnectInputToOutput(0, 0);
313 mux.ConnectInputToOutput(1, 1);
314 current = mux;
315 _stereoPanProvider = new StereoPanSampleProvider(current);
316 ApplyStereoPan(_stereoPanProvider, _pan);
317 current = _stereoPanProvider;
318 break;
319 }
320
321 _volumeProvider = new VolumeSampleProvider(current) { Volume = _volume };
322 return _volumeProvider;
323 }
324
325 private void OnPlaybackStopped(object? sender, StoppedEventArgs e)
326 {
327 var completed = false;
328 lock (_controlLock)
329 {
330 if (_disposed)
331 return;
332
333 if (_pendingPosition is { } position)
334 {
335 _speedProvider.Reset(() => _waveStream.CurrentTime = position);
336 _pendingPosition = null;
337 }
338
339 if (e.Exception is not null)
340 {
341 _stopRequested = false;
342 _pendingPlay = null;
343 Engine.Logger.LogError(e.Exception, "NAudio playback stopped due to an error for audio resource {Key}", _key);
344 return;
345 }
346
347 if (_stopRequested)
348 {
349 _stopRequested = false;
350 var pendingPlay = _pendingPlay;
351 _pendingPlay = null;
352 if (pendingPlay.HasValue)
353 Play(pendingPlay.Value);
354 return;
355 }
356
357 var reachedEnd = _waveStream.Position >= _waveStream.Length;
358 if (_isLooping && reachedEnd)
359 {
360 Play(true);
361 return;
362 }
363
364 completed = reachedEnd;
365 }
366
367 // User callbacks may unload through the manager; never invoke them under
368 // the device control lock (manager disposal takes these locks in reverse).
369 if (completed)
370 PlaybackCompleted?.Invoke(this, EventArgs.Empty);
371 }
372
373 private static void ApplyStereoPan(StereoPanSampleProvider provider, float pan)
374 {
375 var value = Math.Clamp(pan, -1f, 1f);
376 var angle = (value + 1f) * 0.5f * (float)(Math.PI / 2);
377 provider.LeftVolume = MathF.Cos(angle);
378 provider.RightVolume = MathF.Sin(angle);
379 }
380
381 private void ThrowIfDisposed()
382 {
383 ObjectDisposedException.ThrowIf(_disposed, this);
384 }
385
390 public void Dispose()
391 {
392 lock (_controlLock)
393 {
394 if (_disposed)
395 return;
396
397 _outputDevice.PlaybackStopped -= OnPlaybackStopped;
398 try
399 {
400 _stopRequested = true;
401 _outputDevice.Stop();
402 }
403 catch
404 {
405 // Best effort during teardown.
406 }
407
408 _outputDevice.Dispose();
409 _waveStream.Dispose();
410 NAudioReaderRegistry.TryDelete(TemporaryFilePath);
411 _disposed = true;
412 }
413 }
414}
void Resume()
Resume playback after a pause.
void Pause()
Pause playback. Calling Resume should continue from the paused position.
void Stop()
Stop playback and reset the current position to the start.
AudioPlaybackState
Describes the current playback state of an audio resource without exposing backend-specific playback ...