1using Microsoft.Extensions.Logging;
3using NAudio.Wave.SampleProviders;
11internal sealed class NAudioPlaybackHandle : IAudioPlaybackHandle
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;
19 private PanningSampleProvider? _monoPanProvider;
20 private StereoPanSampleProvider? _stereoPanProvider;
21 private VolumeSampleProvider? _volumeProvider;
23 private bool _stopRequested;
24 private bool? _pendingPlay;
25 private TimeSpan? _pendingPosition;
26 private bool _disposed;
27 private bool _isLooping;
28 private float _volume;
30 private float _playbackSpeed;
42 public NAudioPlaybackHandle(
44 WaveStream waveStream,
45 string? temporaryFilePath,
49 IWavePlayer? outputDevice =
null)
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);
58 var source = _waveStream.ToSampleProvider();
59 _speedProvider =
new VariableSpeedSampleProvider(source)
61 PlaybackSpeed = _playbackSpeed
64 _outputDevice = outputDevice ??
new WaveOutEvent();
68 _outputDevice.Init(BuildAudioGraph(_speedProvider));
72 _outputDevice.Dispose();
76 _outputDevice.PlaybackStopped += OnPlaybackStopped;
82 public event EventHandler? PlaybackCompleted;
92 _ => AudioPlaybackState.Stopped
99 public TimeSpan CurrentTime
101 get { lock (_controlLock)
return _pendingPosition ?? _waveStream.CurrentTime; }
108 public TimeSpan Duration => _waveStream.TotalTime;
113 public string? TemporaryFilePath {
get; }
118 public bool IsLooping
121 set => _isLooping = value;
133 _volume = Math.Clamp(value, 0f, 1f);
134 if (_volumeProvider is not
null)
135 _volumeProvider.Volume = _volume;
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);
160 public float PlaybackSpeed
162 get => _playbackSpeed;
165 _playbackSpeed = Math.Clamp(value, AudioResource.MinimumPlaybackSpeed, AudioResource.MaximumPlaybackSpeed);
166 _speedProvider.PlaybackSpeed = _playbackSpeed;
174 public void Play(
bool fromStart =
true)
183 _pendingPlay = fromStart;
191 _stopRequested =
true;
193 _outputDevice.Stop();
197 _speedProvider.Reset(() => _waveStream.Position = 0);
200 _stopRequested =
false;
202 _outputDevice.Play();
215 _outputDevice.Pause();
228 _outputDevice.Play();
236 public void Seek(TimeSpan position)
241 var clamped = position < TimeSpan.Zero
243 : position > Duration ? Duration : position;
247 _pendingPosition = clamped;
253 _outputDevice.Pause();
255 _speedProvider.Reset(() => _waveStream.CurrentTime = clamped);
258 _outputDevice.Play();
274 _pendingPosition = TimeSpan.Zero;
279 _pendingPosition = TimeSpan.Zero;
280 _stopRequested =
true;
281 _outputDevice.Stop();
284 _speedProvider.Reset(() => _waveStream.Position = 0);
288 private ISampleProvider BuildAudioGraph(ISampleProvider source)
290 ISampleProvider current = source;
291 var channels = current.WaveFormat.Channels;
296 Engine.Logger.LogWarning(
"AudioResource {Key} has invalid channel count: {ChannelCount}", _key, channels);
300 _monoPanProvider =
new PanningSampleProvider(current) { Pan = _pan };
301 current = _monoPanProvider;
305 _stereoPanProvider =
new StereoPanSampleProvider(current);
306 ApplyStereoPan(_stereoPanProvider, _pan);
307 current = _stereoPanProvider;
311 var mux =
new MultiplexingSampleProvider(
new[] { current }, 2);
312 mux.ConnectInputToOutput(0, 0);
313 mux.ConnectInputToOutput(1, 1);
315 _stereoPanProvider =
new StereoPanSampleProvider(current);
316 ApplyStereoPan(_stereoPanProvider, _pan);
317 current = _stereoPanProvider;
321 _volumeProvider =
new VolumeSampleProvider(current) { Volume = _volume };
322 return _volumeProvider;
325 private void OnPlaybackStopped(
object? sender, StoppedEventArgs e)
327 var completed =
false;
333 if (_pendingPosition is { } position)
335 _speedProvider.Reset(() => _waveStream.CurrentTime = position);
336 _pendingPosition =
null;
339 if (e.Exception is not
null)
341 _stopRequested =
false;
343 Engine.Logger.LogError(e.Exception,
"NAudio playback stopped due to an error for audio resource {Key}", _key);
349 _stopRequested =
false;
350 var pendingPlay = _pendingPlay;
352 if (pendingPlay.HasValue)
353 Play(pendingPlay.Value);
357 var reachedEnd = _waveStream.Position >= _waveStream.Length;
358 if (_isLooping && reachedEnd)
364 completed = reachedEnd;
370 PlaybackCompleted?.Invoke(
this, EventArgs.Empty);
373 private static void ApplyStereoPan(StereoPanSampleProvider provider,
float pan)
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);
381 private void ThrowIfDisposed()
383 ObjectDisposedException.ThrowIf(_disposed,
this);
390 public void Dispose()
397 _outputDevice.PlaybackStopped -= OnPlaybackStopped;
400 _stopRequested =
true;
401 _outputDevice.Stop();
408 _outputDevice.Dispose();
409 _waveStream.Dispose();
410 NAudioReaderRegistry.TryDelete(TemporaryFilePath);
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 ...