From 85c948fd8da17a57385a4c88e09953b67cbb5d48 Mon Sep 17 00:00:00 2001 From: RoosterDragon Date: Fri, 16 Jun 2017 20:06:45 +0100 Subject: [PATCH] Add a streaming audio playback interface. This allows audio to be streamed, rather than needed to be fully loaded into memory. --- OpenRA.Game/Sound/Sound.cs | 48 ++- OpenRA.Game/Sound/SoundDevice.cs | 6 +- .../Traits/Sound/AmbientSound.cs | 2 +- .../Widgets/Logic/MusicPlayerLogic.cs | 5 +- OpenRA.Platforms.Default/OpenAlSoundEngine.cs | 383 ++++++++++++++---- 5 files changed, 337 insertions(+), 107 deletions(-) diff --git a/OpenRA.Game/Sound/Sound.cs b/OpenRA.Game/Sound/Sound.cs index f05c1d7d7d..0d01f76fbf 100644 --- a/OpenRA.Game/Sound/Sound.cs +++ b/OpenRA.Game/Sound/Sound.cs @@ -37,10 +37,10 @@ namespace OpenRA public sealed class Sound : IDisposable { readonly ISoundEngine soundEngine; + ISoundLoader[] loaders; + IReadOnlyFileSystem fileSystem; Cache sounds; ISoundSource rawSource; - Func getSoundSource; - ISoundSource musicSource; ISound music; ISound video; MusicInfo currentMusic; @@ -54,12 +54,12 @@ namespace OpenRA MuteAudio(); } - ISoundSource LoadSound(ISoundLoader[] loaders, IReadOnlyFileSystem fileSystem, string filename) + T LoadSound(string filename, Func loadFormat) { if (!fileSystem.Exists(filename)) { Log.Write("sound", "LoadSound, file does not exist: {0}", filename); - return null; + return default(T); } using (var stream = fileSystem.Open(filename)) @@ -70,8 +70,7 @@ namespace OpenRA stream.Position = 0; if (loader.TryParseSound(stream, out soundFormat)) { - var source = soundEngine.AddSoundSourceFromMemory( - soundFormat.GetPCMInputStream().ReadAllBytes(), soundFormat.Channels, soundFormat.SampleBits, soundFormat.SampleRate); + var source = loadFormat(soundFormat); soundFormat.Dispose(); return source; } @@ -84,15 +83,18 @@ namespace OpenRA public void Initialize(ISoundLoader[] loaders, IReadOnlyFileSystem fileSystem) { StopMusic(); - soundEngine.ReleaseSourcePool(); + soundEngine.StopAllSounds(); if (sounds != null) foreach (var soundSource in sounds.Values) if (soundSource != null) soundSource.Dispose(); - getSoundSource = s => LoadSound(loaders, fileSystem, s); - sounds = new Cache(getSoundSource); + this.loaders = loaders; + this.fileSystem = fileSystem; + Func loadIntoMemory = soundFormat => soundEngine.AddSoundSourceFromMemory( + soundFormat.GetPCMInputStream().ReadAllBytes(), soundFormat.Channels, soundFormat.SampleBits, soundFormat.SampleRate); + sounds = new Cache(filename => LoadSound(filename, loadIntoMemory)); currentSounds = new Dictionary(); video = null; } @@ -172,7 +174,7 @@ namespace OpenRA public void Tick() { // Song finished - if (MusicPlaying && !music.Playing) + if (MusicPlaying && music.Complete) { StopMusic(); onMusicComplete(); @@ -204,11 +206,17 @@ namespace OpenRA StopMusic(); - musicSource = getSoundSource(m.Filename); - if (musicSource == null) - return; + Func stream = soundFormat => soundEngine.Play2DStream( + soundFormat.GetPCMInputStream(), soundFormat.Channels, soundFormat.SampleBits, soundFormat.SampleRate, + false, true, WPos.Zero, MusicVolume); + music = LoadSound(m.Filename, stream); + + if (music == null) + { + onMusicComplete = null; + return; + } - music = soundEngine.Play2D(musicSource, false, true, WPos.Zero, MusicVolume, false); currentMusic = m; MusicPlaying = true; } @@ -233,14 +241,9 @@ namespace OpenRA if (music != null) { soundEngine.StopSound(music); - soundEngine.ReleaseSound(music); + music = null; } - if (musicSource != null) - musicSource.Dispose(); - - music = null; - musicSource = null; currentMusic = null; MusicPlaying = false; } @@ -405,6 +408,11 @@ namespace OpenRA public void Dispose() { + StopAudio(); + if (sounds != null) + foreach (var soundSource in sounds.Values) + if (soundSource != null) + soundSource.Dispose(); soundEngine.Dispose(); } } diff --git a/OpenRA.Game/Sound/SoundDevice.cs b/OpenRA.Game/Sound/SoundDevice.cs index 3a96b09868..e1f9074cce 100644 --- a/OpenRA.Game/Sound/SoundDevice.cs +++ b/OpenRA.Game/Sound/SoundDevice.cs @@ -10,6 +10,7 @@ #endregion using System; +using System.IO; namespace OpenRA { @@ -18,6 +19,7 @@ namespace OpenRA SoundDevice[] AvailableDevices(); ISoundSource AddSoundSourceFromMemory(byte[] data, int channels, int sampleBits, int sampleRate); ISound Play2D(ISoundSource sound, bool loop, bool relative, WPos pos, float volume, bool attenuateVolume); + ISound Play2DStream(Stream stream, int channels, int sampleBits, int sampleRate, bool loop, bool relative, WPos pos, float volume); float Volume { get; set; } void PauseSound(ISound sound, bool paused); void StopSound(ISound sound); @@ -25,8 +27,6 @@ namespace OpenRA void StopAllSounds(); void SetListenerPosition(WPos position); void SetSoundVolume(float volume, ISound music, ISound video); - void ReleaseSourcePool(); - void ReleaseSound(ISound sound); } public class SoundDevice @@ -47,7 +47,7 @@ namespace OpenRA { float Volume { get; set; } float SeekPosition { get; } - bool Playing { get; } + bool Complete { get; } void SetPosition(WPos pos); } } diff --git a/OpenRA.Mods.Common/Traits/Sound/AmbientSound.cs b/OpenRA.Mods.Common/Traits/Sound/AmbientSound.cs index b9826d5d7c..31232a14a3 100644 --- a/OpenRA.Mods.Common/Traits/Sound/AmbientSound.cs +++ b/OpenRA.Mods.Common/Traits/Sound/AmbientSound.cs @@ -50,7 +50,7 @@ namespace OpenRA.Mods.Common.Traits.Sound if (IsTraitDisabled) return; - currentSounds.RemoveWhere(s => s == null || !s.Playing); + currentSounds.RemoveWhere(s => s == null || s.Complete); var pos = self.CenterPosition; if (pos != cachedPosition) diff --git a/OpenRA.Mods.Common/Widgets/Logic/MusicPlayerLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/MusicPlayerLogic.cs index 243fd41748..1e70aafa21 100644 --- a/OpenRA.Mods.Common/Widgets/Logic/MusicPlayerLogic.cs +++ b/OpenRA.Mods.Common/Widgets/Logic/MusicPlayerLogic.cs @@ -87,8 +87,9 @@ namespace OpenRA.Mods.Common.Widgets.Logic if (currentSong == null || musicPlaylist.CurrentSongIsBackground) return ""; - var minutes = (int)Game.Sound.MusicSeekPosition / 60; - var seconds = (int)Game.Sound.MusicSeekPosition % 60; + var seek = Game.Sound.MusicSeekPosition; + var minutes = (int)seek / 60; + var seconds = (int)seek % 60; var totalMinutes = currentSong.Length / 60; var totalSeconds = currentSong.Length % 60; diff --git a/OpenRA.Platforms.Default/OpenAlSoundEngine.cs b/OpenRA.Platforms.Default/OpenAlSoundEngine.cs index 8162c02de6..7657f86cd0 100644 --- a/OpenRA.Platforms.Default/OpenAlSoundEngine.cs +++ b/OpenRA.Platforms.Default/OpenAlSoundEngine.cs @@ -11,9 +11,12 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Runtime.InteropServices; using System.Text; +using System.Threading; +using System.Threading.Tasks; using OpenAL; namespace OpenRA.Platforms.Default @@ -37,7 +40,8 @@ namespace OpenRA.Platforms.Default public int FrameStarted; public WPos Pos; public bool IsRelative; - public ISoundSource Sound; + public OpenAlSoundSource SoundSource; + public OpenAlSound Sound; } const int MaxInstancesPerFrame = 3; @@ -45,7 +49,7 @@ namespace OpenRA.Platforms.Default const int GroupDistanceSqr = GroupDistance * GroupDistance; const int PoolSize = 32; - readonly Dictionary sourcePool = new Dictionary(); + readonly Dictionary sourcePool = new Dictionary(PoolSize); float volume = 1f; IntPtr device; IntPtr context; @@ -100,6 +104,14 @@ namespace OpenRA.Platforms.Default return new string[] { }; } + internal static int MakeALFormat(int channels, int bits) + { + if (channels == 1) + return bits == 16 ? AL10.AL_FORMAT_MONO16 : AL10.AL_FORMAT_MONO8; + else + return bits == 16 ? AL10.AL_FORMAT_STEREO16 : AL10.AL_FORMAT_STEREO8; + } + public OpenAlSoundEngine(string deviceName) { if (deviceName != null) @@ -137,23 +149,27 @@ namespace OpenRA.Platforms.Default bool TryGetSourceFromPool(out uint source) { - foreach (var kvp in sourcePool) + foreach (var kv in sourcePool) { - if (!kvp.Value.IsActive) + if (!kv.Value.IsActive) { - sourcePool[kvp.Key].IsActive = true; - source = kvp.Key; + sourcePool[kv.Key].IsActive = true; + source = kv.Key; return true; } } var freeSources = new List(); - foreach (var key in sourcePool.Keys) + foreach (var kv in sourcePool) { - int state; - AL10.alGetSourcei(key, AL10.AL_SOURCE_STATE, out state); - if (state != AL10.AL_PLAYING && state != AL10.AL_PAUSED) - freeSources.Add(key); + var sound = kv.Value.Sound; + if (sound != null && sound.Complete) + { + var freeSource = kv.Key; + freeSources.Add(freeSource); + AL10.alSourceRewind(freeSource); + AL10.alSourcei(freeSource, AL10.AL_BUFFER, 0); + } } if (freeSources.Count == 0) @@ -162,12 +178,16 @@ namespace OpenRA.Platforms.Default return false; } - foreach (var i in freeSources) - sourcePool[i].IsActive = false; - - sourcePool[freeSources[0]].IsActive = true; + foreach (var freeSource in freeSources) + { + var slot = sourcePool[freeSource]; + slot.SoundSource = null; + slot.Sound = null; + slot.IsActive = false; + } source = freeSources[0]; + sourcePool[source].IsActive = true; return true; } @@ -176,14 +196,16 @@ namespace OpenRA.Platforms.Default return new OpenAlSoundSource(data, channels, sampleBits, sampleRate); } - public ISound Play2D(ISoundSource sound, bool loop, bool relative, WPos pos, float volume, bool attenuateVolume) + public ISound Play2D(ISoundSource soundSource, bool loop, bool relative, WPos pos, float volume, bool attenuateVolume) { - if (sound == null) + if (soundSource == null) { Log.Write("sound", "Attempt to Play2D a null `ISoundSource`"); return null; } + var alSoundSource = (OpenAlSoundSource)soundSource; + var currFrame = Game.LocalTick; var atten = 1f; @@ -199,7 +221,7 @@ namespace OpenRA.Platforms.Default continue; ++activeCount; - if (s.Sound != sound) + if (s.SoundSource != alSoundSource) continue; if (currFrame - s.FrameStarted >= 5) continue; @@ -225,9 +247,27 @@ namespace OpenRA.Platforms.Default var slot = sourcePool[source]; slot.Pos = pos; slot.FrameStarted = currFrame; - slot.Sound = sound; slot.IsRelative = relative; - return new OpenAlSound(source, ((OpenAlSoundSource)sound).Buffer, loop, relative, pos, volume * atten); + slot.SoundSource = alSoundSource; + slot.Sound = new OpenAlSound(source, loop, relative, pos, volume * atten, alSoundSource.SampleRate, alSoundSource.Buffer); + return slot.Sound; + } + + public ISound Play2DStream(Stream stream, int channels, int sampleBits, int sampleRate, bool loop, bool relative, WPos pos, float volume) + { + var currFrame = Game.LocalTick; + + uint source; + if (!TryGetSourceFromPool(out source)) + return null; + + var slot = sourcePool[source]; + slot.Pos = pos; + slot.FrameStarted = currFrame; + slot.IsRelative = relative; + slot.SoundSource = null; + slot.Sound = new OpenAlStreamingSound(source, loop, relative, pos, volume, channels, sampleBits, sampleRate, stream); + return slot.Sound; } public float Volume @@ -241,25 +281,25 @@ namespace OpenRA.Platforms.Default if (sound == null) return; - var key = ((OpenAlSound)sound).Source; + var source = ((OpenAlSound)sound).Source; int state; - AL10.alGetSourcei(key, AL10.AL_SOURCE_STATE, out state); + AL10.alGetSourcei(source, AL10.AL_SOURCE_STATE, out state); if (state == AL10.AL_PLAYING && paused) - AL10.alSourcePause(key); + AL10.alSourcePause(source); else if (state == AL10.AL_PAUSED && !paused) - AL10.alSourcePlay(key); + AL10.alSourcePlay(source); } public void SetAllSoundsPaused(bool paused) { - foreach (var key in sourcePool.Keys) + foreach (var source in sourcePool.Keys) { int state; - AL10.alGetSourcei(key, AL10.AL_SOURCE_STATE, out state); + AL10.alGetSourcei(source, AL10.AL_SOURCE_STATE, out state); if (state == AL10.AL_PLAYING && paused) - AL10.alSourcePause(key); + AL10.alSourcePause(source); else if (state == AL10.AL_PAUSED && !paused) - AL10.alSourcePlay(key); + AL10.alSourcePlay(source); } } @@ -283,22 +323,14 @@ namespace OpenRA.Platforms.Default if (sound == null) return; - var key = ((OpenAlSound)sound).Source; - int state; - AL10.alGetSourcei(key, AL10.AL_SOURCE_STATE, out state); - if (state == AL10.AL_PLAYING || state == AL10.AL_PAUSED) - AL10.alSourceStop(key); + ((OpenAlSound)sound).Stop(); } public void StopAllSounds() { - foreach (var key in sourcePool.Keys) - { - int state; - AL10.alGetSourcei(key, AL10.AL_SOURCE_STATE, out state); - if (state == AL10.AL_PLAYING || state == AL10.AL_PAUSED) - AL10.alSourceStop(key); - } + foreach (var slot in sourcePool.Values) + if (slot.Sound != null) + slot.Sound.Stop(); } public void SetListenerPosition(WPos position) @@ -311,26 +343,6 @@ namespace OpenRA.Platforms.Default AL10.alListenerf(EFX.AL_METERS_PER_UNIT, .01f); } - public void ReleaseSourcePool() - { - foreach (var slot in sourcePool) - if (slot.Value.Sound != null) - ReleaseSound(slot.Key); - } - - public void ReleaseSound(ISound sound) - { - var openAlSound = sound as OpenAlSound; - if (openAlSound != null) - ReleaseSound(openAlSound.Source); - } - - void ReleaseSound(uint source) - { - AL10.alSourceStop(source); - AL10.alSourcei(source, AL10.AL_BUFFER, 0); - } - ~OpenAlSoundEngine() { Dispose(false); @@ -344,6 +356,8 @@ namespace OpenRA.Platforms.Default void Dispose(bool disposing) { + StopAllSounds(); + if (context != IntPtr.Zero) { ALC10.alcMakeContextCurrent(IntPtr.Zero); @@ -365,19 +379,13 @@ namespace OpenRA.Platforms.Default bool disposed; public uint Buffer { get { return buffer; } } - - static int MakeALFormat(int channels, int bits) - { - if (channels == 1) - return bits == 16 ? AL10.AL_FORMAT_MONO16 : AL10.AL_FORMAT_MONO8; - else - return bits == 16 ? AL10.AL_FORMAT_STEREO16 : AL10.AL_FORMAT_STEREO8; - } + public int SampleRate { get; private set; } public OpenAlSoundSource(byte[] data, int channels, int sampleBits, int sampleRate) { + SampleRate = sampleRate; AL10.alGenBuffers(new IntPtr(1), out buffer); - AL10.alBufferData(buffer, MakeALFormat(channels, sampleBits), data, new IntPtr(data.Length), new IntPtr(sampleRate)); + AL10.alBufferData(buffer, OpenAlSoundEngine.MakeALFormat(channels, sampleBits), data, new IntPtr(data.Length), new IntPtr(sampleRate)); } protected virtual void Dispose(bool disposing) @@ -404,48 +412,54 @@ namespace OpenRA.Platforms.Default class OpenAlSound : ISound { public readonly uint Source; - float volume; + protected readonly float SampleRate; - public OpenAlSound(uint source, uint buffer, bool looping, bool relative, WPos pos, float volume) + public OpenAlSound(uint source, bool looping, bool relative, WPos pos, float volume, int sampleRate, uint buffer) + : this(source, looping, relative, pos, volume, sampleRate) + { + AL10.alSourcei(source, AL10.AL_BUFFER, (int)buffer); + AL10.alSourcePlay(source); + } + + protected OpenAlSound(uint source, bool looping, bool relative, WPos pos, float volume, int sampleRate) { Source = source; + SampleRate = sampleRate; Volume = volume; AL10.alSourcef(source, AL10.AL_PITCH, 1f); AL10.alSource3f(source, AL10.AL_POSITION, pos.X, pos.Y, pos.Z); AL10.alSource3f(source, AL10.AL_VELOCITY, 0f, 0f, 0f); - AL10.alSourcei(source, AL10.AL_BUFFER, (int)buffer); AL10.alSourcei(source, AL10.AL_LOOPING, looping ? 1 : 0); AL10.alSourcei(source, AL10.AL_SOURCE_RELATIVE, relative ? 1 : 0); AL10.alSourcef(source, AL10.AL_REFERENCE_DISTANCE, 6826); AL10.alSourcef(source, AL10.AL_MAX_DISTANCE, 136533); - AL10.alSourcePlay(source); } public float Volume { - get { return volume; } - set { AL10.alSourcef(Source, AL10.AL_GAIN, volume = value); } + get { float volume; AL10.alGetSourcef(Source, AL10.AL_GAIN, out volume); return volume; } + set { AL10.alSourcef(Source, AL10.AL_GAIN, value); } } - public float SeekPosition + public virtual float SeekPosition { get { - int pos; - AL10.alGetSourcei(Source, AL11.AL_SAMPLE_OFFSET, out pos); - return pos / 22050f; + int sampleOffset; + AL10.alGetSourcei(Source, AL11.AL_SAMPLE_OFFSET, out sampleOffset); + return sampleOffset / SampleRate; } } - public bool Playing + public virtual bool Complete { get { int state; AL10.alGetSourcei(Source, AL10.AL_SOURCE_STATE, out state); - return state == AL10.AL_PLAYING; + return state == AL10.AL_STOPPED; } } @@ -453,5 +467,212 @@ namespace OpenRA.Platforms.Default { AL10.alSource3f(Source, AL10.AL_POSITION, pos.X, pos.Y, pos.Z); } + + protected void StopSource() + { + int state; + AL10.alGetSourcei(Source, AL10.AL_SOURCE_STATE, out state); + if (state == AL10.AL_PLAYING || state == AL10.AL_PAUSED) + AL10.alSourceStop(Source); + } + + public virtual void Stop() + { + StopSource(); + AL10.alSourcei(Source, AL10.AL_BUFFER, 0); + } + } + + class OpenAlStreamingSound : OpenAlSound + { + const int BufferCount = 3; + const int BufferSizeInSecs = 1; + readonly object bufferDequeueLock = new object(); + readonly CancellationTokenSource cts = new CancellationTokenSource(); + readonly Task streamTask; + readonly Stack freeBuffers = new Stack(BufferCount); + int totalSamplesPlayed; + + public OpenAlStreamingSound(uint source, bool looping, bool relative, WPos pos, float volume, int channels, int sampleBits, int sampleRate, Stream stream) + : base(source, looping, relative, pos, volume, sampleRate) + { + streamTask = Task.Run(async () => + { + var format = OpenAlSoundEngine.MakeALFormat(channels, sampleBits); + var bytesPerSample = sampleBits / 8; + + var buffers = new uint[BufferCount]; + AL10.alGenBuffers(new IntPtr(buffers.Length), buffers); + try + { + foreach (var buffer in buffers) + freeBuffers.Push(buffer); + var data = new byte[sampleRate * bytesPerSample * BufferSizeInSecs]; + var streamEnd = false; + + while (!streamEnd && !cts.IsCancellationRequested) + { + // Fill the data array as fully as possible. + var count = await ReadFillingBuffer(stream, data); + streamEnd = count < data.Length; + + // Fill a buffer and queue it for playback. + var nextBuffer = freeBuffers.Pop(); + AL10.alBufferData(nextBuffer, format, data, new IntPtr(count), new IntPtr(sampleRate)); + AL10.alSourceQueueBuffers(source, new IntPtr(1), ref nextBuffer); + + lock (cts) + { + if (!cts.IsCancellationRequested) + { + // Once we have at least one buffer filled, we can actually start. + // If streaming fell behind, the source will have stopped, + // we also need to play it in this case to resume audio. + int state; + AL10.alGetSourcei(source, AL10.AL_SOURCE_STATE, out state); + if (state == AL10.AL_INITIAL || state == AL10.AL_STOPPED) + { + // If we resume playback from the stopped state, it resets to the beginning! + // To avoid replaying the same audio, we need to dequeue the processed buffers first. + if (state == AL10.AL_STOPPED) + DequeueBuffers(); + + AL10.alSourcePlay(source); + } + } + } + + // Try and dequeue buffers as they become available to be reused. + // When the stream ends, wait for all the buffers to be processed + while (freeBuffers.Count < (streamEnd ? buffers.Length : 1)) + { + await Task.Delay(TimeSpan.FromSeconds(BufferSizeInSecs), cts.Token).ConfigureAwait(false); + DequeueBuffers(); + } + } + } + catch (TaskCanceledException) + { + // Streaming has been cancelled, we'll need to perform some cleanup. + } + finally + { + // If we never actually started the source, we need to start it and then stop it. + // Otherwise it is left in the initial state and never returned to the source pool. + int state; + AL10.alGetSourcei(Source, AL10.AL_SOURCE_STATE, out state); + if (state == AL10.AL_INITIAL) + AL10.alSourcePlay(Source); + + // Ensure the source is stopped, which will mark all buffers as processed. + // Dequeue these, so they can then be freed. + AL10.alSourceStop(Source); + lock (bufferDequeueLock) + { + int buffersProcessed; + AL10.alGetSourcei(source, AL10.AL_BUFFERS_PROCESSED, out buffersProcessed); + for (var i = 0; i < buffersProcessed; i++) + { + var dequeued = 0U; + AL10.alSourceUnqueueBuffers(source, new IntPtr(1), ref dequeued); + } + } + + AL10.alDeleteBuffers(new IntPtr(buffers.Length), buffers); + stream.Dispose(); + } + }).ContinueWith(task => + { + Game.RunAfterTick(() => + { + throw new Exception("Failed to stream a sound.", task.Exception); + }); + }, TaskContinuationOptions.OnlyOnFaulted); + } + + async Task ReadFillingBuffer(Stream stream, byte[] buffer) + { + var offset = 0; + int count; + while (offset < buffer.Length && + (count = await stream.ReadAsync(buffer, offset, buffer.Length - offset, cts.Token).ConfigureAwait(false)) > 0) + offset += count; + return offset; + } + + void DequeueBuffers() + { + lock (bufferDequeueLock) + { + // Check for any processed buffers, and dequeue them for reuse. + int buffersProcessed; + AL10.alGetSourcei(Source, AL10.AL_BUFFERS_PROCESSED, out buffersProcessed); + for (var i = 0; i < buffersProcessed; i++) + { + // Dequeue a processed buffer, so we can reuse it. + var dequeued = 0U; + AL10.alSourceUnqueueBuffers(Source, new IntPtr(1), ref dequeued); + freeBuffers.Push(dequeued); + + // When we remove a buffer, we need to account for this to calculate the overall seek position. + // The final buffer in the track may be shorter then BufferSizeInSecs, so we'll need to check how + // many bytes are actually in each buffer to avoid adding too much at the end. + int byteSize; + AL10.alGetBufferi(dequeued, AL10.AL_SIZE, out byteSize); + + int bitRate; + AL10.alGetBufferi(dequeued, AL10.AL_BITS, out bitRate); + + totalSamplesPlayed += byteSize / (bitRate / 8); + } + } + } + + public override void Stop() + { + lock (cts) + { + StopSource(); + cts.Cancel(); + } + + try + { + streamTask.Wait(); + } + catch (AggregateException) + { + } + } + + public override float SeekPosition + { + get + { + // Stop buffers being dequeued whilst we calculate the seek position. + lock (bufferDequeueLock) + { + int sampleOffset; + AL10.alGetSourcei(Source, AL11.AL_SAMPLE_OFFSET, out sampleOffset); + + int state; + AL10.alGetSourcei(Source, AL10.AL_SOURCE_STATE, out state); + + // If the source is not stopped, add the current offset to the total offset and return that. + if (state != AL10.AL_STOPPED) + return (sampleOffset + totalSamplesPlayed) / SampleRate; + + // If the source stopped, the current offset will have been reset to 0. + // We'll need to dequeue any buffers played first and then return the total. + DequeueBuffers(); + return totalSamplesPlayed / SampleRate; + } + } + } + + public override bool Complete + { + get { return streamTask.IsCompleted; } + } } }