Compare commits
1 Commits
master
...
feature/na
Author | SHA1 | Date | |
---|---|---|---|
|
44d0e24cd9 |
230
Aurora/Backend/Services/PlayerService/PlayerService.cs
Normal file
230
Aurora/Backend/Services/PlayerService/PlayerService.cs
Normal file
@ -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<PlayerService>
|
||||
{
|
||||
#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
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
using System;
|
||||
namespace Aurora.Backend.Services.PlayerService
|
||||
{
|
||||
public enum StreamingPlaybackState
|
||||
{
|
||||
Stopped,
|
||||
Playing,
|
||||
Buffering,
|
||||
Paused
|
||||
}
|
||||
}
|
101
Aurora/Backend/Utils/ReadFullyStream.cs
Normal file
101
Aurora/Backend/Utils/ReadFullyStream.cs
Normal file
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
|
||||
}
|
||||
|
Reference in New Issue
Block a user