From 44d0e24cd9439b0cb0b83483f63793ed730ec6d5 Mon Sep 17 00:00:00 2001 From: watsonb8 Date: Wed, 22 May 2019 10:31:27 -0400 Subject: [PATCH] =?UTF-8?q?Trying=20out=20naudio.=20Doesn=E2=80=99t=20supp?= =?UTF-8?q?ort=20cross=20platform?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Services/PlayerService/PlayerService.cs | 230 ++++++++++++++++++ .../PlayerService/StreamingPlaybackState.cs | 11 + Aurora/Backend/Utils/ReadFullyStream.cs | 101 ++++++++ Aurora/Frontend/Views/Songs/SongsViewModel.cs | 9 + 4 files changed, 351 insertions(+) create mode 100644 Aurora/Backend/Services/PlayerService/PlayerService.cs create mode 100644 Aurora/Backend/Services/PlayerService/StreamingPlaybackState.cs create mode 100644 Aurora/Backend/Utils/ReadFullyStream.cs diff --git a/Aurora/Backend/Services/PlayerService/PlayerService.cs b/Aurora/Backend/Services/PlayerService/PlayerService.cs new file mode 100644 index 0000000..00acda6 --- /dev/null +++ b/Aurora/Backend/Services/PlayerService/PlayerService.cs @@ -0,0 +1,230 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Timers; +using System.Threading; +using Aurora.Backend.Models; +using Aurora.Backend.Utils; +using NAudio.Wave; + +namespace Aurora.Backend.Services.PlayerService +{ + public class PlayerService : BaseService + { + #region Fields + private BufferedWaveProvider _bufferedWaveProvider; + private IWavePlayer _waveOut; + private volatile StreamingPlaybackState _playbackState; + private bool _fullyDownloaded = false; + private System.Timers.Timer _monitorTimer; + private VolumeWaveProvider16 _volumeProvider; + + private bool IsBufferNearlyFull + { + get + { + return _bufferedWaveProvider != null && + _bufferedWaveProvider.BufferLength - _bufferedWaveProvider.BufferedBytes + < _bufferedWaveProvider.WaveFormat.AverageBytesPerSecond / 4; + } + } + + #endregion Fields + + #region Constructor + public PlayerService() + { + _monitorTimer = new System.Timers.Timer(250); + _monitorTimer.Elapsed += OnMonitorTimerTick; + } + + ~PlayerService() + { + _monitorTimer.Elapsed -= OnMonitorTimerTick; + } + + #endregion Constructor + + #region Public Methods + public void Play(BaseSong song) + { + if (_playbackState == StreamingPlaybackState.Stopped) + { + _playbackState = StreamingPlaybackState.Buffering; + _bufferedWaveProvider = null; + ThreadPool.QueueUserWorkItem(Stream, song); + _monitorTimer.Enabled = true; + } + else if (_playbackState == StreamingPlaybackState.Paused) + { + _playbackState = StreamingPlaybackState.Buffering; + } + } + + public void Pause() + { + Pause(); + } + + #endregion Public Methods + + #region Private Methods + private void Stream(object state) + { + BaseSong song = state as BaseSong; + //Load song into stream + song.Load(); + + + //Buffer big enough to hold decompressed song + var buffer = new byte[16384 * 4]; + + IMp3FrameDecompressor decompressor = null; + + try + { + ReadFullyStream rfs = new ReadFullyStream(song.DataStream); + do + { + if (IsBufferNearlyFull) + { + Debug.WriteLine("Buffer getting full, taking a break"); + Thread.Sleep(500); + } + else + { + Mp3Frame frame; + try + { + frame = Mp3Frame.LoadFromStream(rfs); + } + catch (EndOfStreamException) + { + _fullyDownloaded = true; + break; + } + + if (frame == null) + { + break; + } + if (decompressor == null) + { + decompressor = CreateFrameDecompressor(frame); + _bufferedWaveProvider = new BufferedWaveProvider(decompressor.OutputFormat); + _bufferedWaveProvider.BufferDuration = TimeSpan.FromSeconds(20); + + } + + int decompressed = decompressor.DecompressFrame(frame, buffer, 0); + _bufferedWaveProvider.AddSamples(buffer, 0, decompressed); + } + } + while (_playbackState != StreamingPlaybackState.Stopped); + + Debug.WriteLine("Exiting"); + decompressor.Dispose(); + } + finally + { + if (decompressor != null) + { + decompressor.Dispose(); + } + + song.Unload(); + } + } + + + + private void PlayAudio() + { + _waveOut.Play(); + Debug.WriteLine(String.Format("Started playing, waveOut.PlaybackState={0}", _waveOut.PlaybackState)); + _playbackState = StreamingPlaybackState.Playing; + } + + private void PauseAudio() + { + _playbackState = StreamingPlaybackState.Buffering; + _waveOut.Pause(); + Debug.WriteLine(String.Format("Paused to buffer, waveOut.PlaybackState={0}", _waveOut.PlaybackState)); + } + + private void StopAudio() + { + if (_playbackState != StreamingPlaybackState.Stopped) + { + if (!_fullyDownloaded) + { + //End song loading + } + + _playbackState = StreamingPlaybackState.Stopped; + if (_waveOut != null) + { + _waveOut.Stop(); + _waveOut.Dispose(); + _waveOut = null; + } + _monitorTimer.Enabled = false; + // n.b. streaming thread may not yet have exited + Thread.Sleep(500); + } + } + + private void OnPlaybackStopped(object sender, StoppedEventArgs e) + { + Debug.WriteLine("Playback Stopped"); + if (e.Exception != null) + { + //TODO log exception + } + } + + private static IMp3FrameDecompressor CreateFrameDecompressor(Mp3Frame frame) + { + WaveFormat waveFormat = new Mp3WaveFormat(frame.SampleRate, frame.ChannelMode == ChannelMode.Mono ? 1 : 2, + frame.FrameLength, frame.BitRate); + + return new AcmMp3FrameDecompressor(waveFormat); + } + + private void OnMonitorTimerTick(object sender, EventArgs e) + { + if (_playbackState != StreamingPlaybackState.Stopped) + { + //Data available but audio has not been initialized + if (_waveOut == null && _bufferedWaveProvider != null) + { + _waveOut = new WaveOutEvent(); + _waveOut.PlaybackStopped += OnPlaybackStopped; + _volumeProvider = new VolumeWaveProvider16(_bufferedWaveProvider); + _waveOut.Init(_volumeProvider); + + } + else + { + var bufferedSeconds = _bufferedWaveProvider.BufferedDuration.TotalSeconds; + // make it stutter less if we buffer up a decent amount before playing + if (bufferedSeconds < 0.5 && _playbackState == StreamingPlaybackState.Playing && !_fullyDownloaded) + { + PauseAudio(); + } + else if (bufferedSeconds > 4 && _playbackState == StreamingPlaybackState.Buffering) + { + PlayAudio(); + } + else if (_fullyDownloaded && bufferedSeconds == 0) + { + Debug.WriteLine("Reached end of stream"); + StopAudio(); + } + } + } + } + + #endregion Private Methods + } +} diff --git a/Aurora/Backend/Services/PlayerService/StreamingPlaybackState.cs b/Aurora/Backend/Services/PlayerService/StreamingPlaybackState.cs new file mode 100644 index 0000000..a27a3ec --- /dev/null +++ b/Aurora/Backend/Services/PlayerService/StreamingPlaybackState.cs @@ -0,0 +1,11 @@ +using System; +namespace Aurora.Backend.Services.PlayerService +{ + public enum StreamingPlaybackState + { + Stopped, + Playing, + Buffering, + Paused + } +} diff --git a/Aurora/Backend/Utils/ReadFullyStream.cs b/Aurora/Backend/Utils/ReadFullyStream.cs new file mode 100644 index 0000000..0e8edc9 --- /dev/null +++ b/Aurora/Backend/Utils/ReadFullyStream.cs @@ -0,0 +1,101 @@ +using System; +using System.IO; + +namespace Aurora.Backend.Utils +{ + public class ReadFullyStream : Stream + { + private readonly Stream sourceStream; + private long pos; // psuedo-position + private readonly byte[] readAheadBuffer; + private int readAheadLength; + private int readAheadOffset; + + public ReadFullyStream(Stream sourceStream) + { + this.sourceStream = sourceStream; + readAheadBuffer = new byte[4096]; + } + public override bool CanRead + { + get { return true; } + } + + public override bool CanSeek + { + get { return false; } + } + + public override bool CanWrite + { + get { return false; } + } + + public override void Flush() + { + throw new InvalidOperationException(); + } + + public override long Length + { + get { return pos; } + } + + public override long Position + { + get + { + return pos; + } + set + { + throw new InvalidOperationException(); + } + } + + + public override int Read(byte[] buffer, int offset, int count) + { + int bytesRead = 0; + while (bytesRead < count) + { + int readAheadAvailableBytes = readAheadLength - readAheadOffset; + int bytesRequired = count - bytesRead; + if (readAheadAvailableBytes > 0) + { + int toCopy = Math.Min(readAheadAvailableBytes, bytesRequired); + Array.Copy(readAheadBuffer, readAheadOffset, buffer, offset + bytesRead, toCopy); + bytesRead += toCopy; + readAheadOffset += toCopy; + } + else + { + readAheadOffset = 0; + readAheadLength = sourceStream.Read(readAheadBuffer, 0, readAheadBuffer.Length); + //Debug.WriteLine(String.Format("Read {0} bytes (requested {1})", readAheadLength, readAheadBuffer.Length)); + if (readAheadLength == 0) + { + break; + } + } + } + pos += bytesRead; + return bytesRead; + } + + public override long Seek(long offset, SeekOrigin origin) + { + throw new InvalidOperationException(); + } + + public override void SetLength(long value) + { + throw new InvalidOperationException(); + } + + public override void Write(byte[] buffer, int offset, int count) + { + throw new InvalidOperationException(); + } + } +} diff --git a/Aurora/Frontend/Views/Songs/SongsViewModel.cs b/Aurora/Frontend/Views/Songs/SongsViewModel.cs index 18b4506..70a1185 100644 --- a/Aurora/Frontend/Views/Songs/SongsViewModel.cs +++ b/Aurora/Frontend/Views/Songs/SongsViewModel.cs @@ -48,6 +48,15 @@ namespace Aurora.Frontend.Views.Songs SongsList = LibraryService.Instance.GetLibrary(); } + private void PlayExecute() + { + if (_selectedSong != null) + { + PlayerService.Instance.Play(_selectedSong); + } + + } + #endregion Methods }