Discs and audio output should be less aware

This commit is contained in:
Matt Nadareski
2021-10-05 21:40:41 -07:00
parent 321490bbb4
commit ee0fbc4ccc
12 changed files with 685 additions and 796 deletions

View File

@@ -406,17 +406,17 @@ namespace RedBookPlayer.GUI.ViewModels
/// Initialize the view model with a given image path
/// </summary>
/// <param name="path">Path to the disc image</param>
/// <param name="options">Options to pass to the optical disc factory</param>
/// <param name="repeatMode">RepeatMode for sound output</param>
/// <param name="playerOptions">Options to pass to the player</param>
/// <param name="opticalDiscOptions">Options to pass to the optical disc factory</param>
/// <param name="autoPlay">True if playback should begin immediately, false otherwise</param>
public void Init(string path, OpticalDiscOptions options, RepeatMode repeatMode, bool autoPlay)
public void Init(string path, PlayerOptions playerOptions, OpticalDiscOptions opticalDiscOptions, bool autoPlay)
{
// Stop current playback, if necessary
if(PlayerState != PlayerState.NoDisc)
ExecuteStop();
// Attempt to initialize Player
_player.Init(path, options, repeatMode, autoPlay);
_player.Init(path, playerOptions, opticalDiscOptions, autoPlay);
if(_player.Initialized)
{
_player.PropertyChanged += PlayerStateChanged;
@@ -654,19 +654,24 @@ namespace RedBookPlayer.GUI.ViewModels
{
return await Dispatcher.UIThread.InvokeAsync(() =>
{
OpticalDiscOptions options = new OpticalDiscOptions
PlayerOptions playerOptions = new PlayerOptions
{
DataPlayback = App.Settings.DataPlayback,
GenerateMissingToc = App.Settings.GenerateMissingTOC,
LoadHiddenTracks = App.Settings.PlayHiddenTracks,
RepeatMode = App.Settings.RepeatMode,
SessionHandling = App.Settings.SessionHandling,
};
OpticalDiscOptions opticalDiscOptions = new OpticalDiscOptions
{
GenerateMissingToc = App.Settings.GenerateMissingTOC,
};
// Ensure the context and view model are set
App.PlayerView.DataContext = this;
App.PlayerView.ViewModel = this;
Init(path, options, App.Settings.RepeatMode, App.Settings.AutoPlay);
Init(path, playerOptions, opticalDiscOptions, App.Settings.AutoPlay);
if(Initialized)
App.MainWindow.Title = "RedBookPlayer - " + path.Split('/').Last().Split('\\').Last();

View File

@@ -77,7 +77,7 @@ namespace RedBookPlayer.GUI.ViewModels
/// <summary>
/// Indicates how to repeat tracks
/// </summary>
public RepeatMode RepeatMode { get; set; } = RepeatMode.All;
public RepeatMode RepeatMode { get; set; } = RepeatMode.AllSingleDisc;
/// <summary>
/// Indicates how to handle tracks on different sessions

View File

@@ -27,85 +27,25 @@ namespace RedBookPlayer.Models.Discs
if(_image == null)
return;
// Data tracks only and flag disabled means we can't do anything
if(_image.Tracks.All(t => t.TrackType != TrackType.Audio) && DataPlayback == DataPlayback.Skip)
// Invalid value means we can't do anything
if (value > _image.Tracks.Max(t => t.TrackSequence))
return;
else if (value < _image.Tracks.Min(t => t.TrackSequence))
return;
// Cache the value and the current track number
int cachedValue = value;
int cachedTrackNumber;
// Check if we're incrementing or decrementing the track
bool increment = cachedValue >= _currentTrackNumber;
do
{
// If we're over the last track, wrap around
if(cachedValue > _image.Tracks.Max(t => t.TrackSequence))
{
cachedValue = (int)_image.Tracks.Min(t => t.TrackSequence);
if(cachedValue == 0 && !LoadHiddenTracks)
cachedValue++;
}
// If we're under the first track and we're not loading hidden tracks, wrap around
else if(cachedValue < 1 && !LoadHiddenTracks)
{
cachedValue = (int)_image.Tracks.Max(t => t.TrackSequence);
}
// If we're under the first valid track, wrap around
else if(cachedValue < _image.Tracks.Min(t => t.TrackSequence))
{
cachedValue = (int)_image.Tracks.Max(t => t.TrackSequence);
}
cachedTrackNumber = cachedValue;
// Cache the current track for easy access
Track track = GetTrack(cachedTrackNumber);
Track track = GetTrack(value);
if(track == null)
return;
// Set track flags from subchannel data, if possible
// Set all track flags and values
SetTrackFlags(track);
TotalIndexes = track.Indexes.Keys.Max();
CurrentTrackIndex = track.Indexes.Keys.Min();
CurrentTrackSession = track.TrackSession;
// If the track is playable, just return
if((TrackType == TrackType.Audio || DataPlayback != DataPlayback.Skip)
&& (SessionHandling == SessionHandling.AllSessions || track.TrackSession == 1))
{
break;
}
// If we're not playing the track, skip
if(increment)
cachedValue++;
else
cachedValue--;
}
while(cachedValue != _currentTrackNumber);
// If we looped around, ensure it reloads
if(cachedValue == _currentTrackNumber)
{
this.RaiseAndSetIfChanged(ref _currentTrackNumber, -1);
Track track = GetTrack(cachedValue);
if(track == null)
return;
SetTrackFlags(track);
}
this.RaiseAndSetIfChanged(ref _currentTrackNumber, cachedValue);
Track cachedTrack = GetTrack(cachedValue);
if(cachedTrack == null)
return;
TotalIndexes = cachedTrack.Indexes.Keys.Max();
CurrentTrackIndex = cachedTrack.Indexes.Keys.Min();
CurrentTrackSession = cachedTrack.TrackSession;
// Mark the track as changed
this.RaiseAndSetIfChanged(ref _currentTrackNumber, value);
}
}
@@ -124,14 +64,13 @@ namespace RedBookPlayer.Models.Discs
if(track == null)
return;
// Ensure that the value is valid, wrapping around if necessary
ushort fixedValue = value;
// Invalid value means we can't do anything
if (value > track.Indexes.Keys.Max())
fixedValue = track.Indexes.Keys.Min();
return;
else if (value < track.Indexes.Keys.Min())
fixedValue = track.Indexes.Keys.Max();
return;
this.RaiseAndSetIfChanged(ref _currentTrackIndex, fixedValue);
this.RaiseAndSetIfChanged(ref _currentTrackIndex, value);
// Set new index-specific data
SectionStartSector = (ulong)track.Indexes[CurrentTrackIndex];
@@ -156,19 +95,18 @@ namespace RedBookPlayer.Models.Discs
if(_image == null)
return;
// If the sector is over the end of the image, then loop
ulong tempSector = value;
if(tempSector > _image.Info.Sectors)
tempSector = 0;
else if(tempSector < 0)
tempSector = _image.Info.Sectors - 1;
// Invalid value means we can't do anything
if(value > _image.Info.Sectors)
return;
else if(value < 0)
return;
// Cache the current track for easy access
Track track = GetTrack(CurrentTrackNumber);
if(track == null)
return;
this.RaiseAndSetIfChanged(ref _currentSector, tempSector);
this.RaiseAndSetIfChanged(ref _currentSector, value);
// If the current sector is outside of the last known track, seek to the right one
if(CurrentSector < track.TrackStartSector || CurrentSector > track.TrackEndSector)
@@ -194,6 +132,11 @@ namespace RedBookPlayer.Models.Discs
/// <inheritdoc/>
public override int BytesPerSector => GetTrack(CurrentTrackNumber)?.TrackRawBytesPerSector ?? 0;
/// <summary>
/// Readonly list of all tracks in the image
/// </summary>
public List<Track> Tracks => _image?.Tracks;
/// <summary>
/// Represents the 4CH flag
/// </summary>
@@ -230,21 +173,6 @@ namespace RedBookPlayer.Models.Discs
private set => this.RaiseAndSetIfChanged(ref _trackHasEmphasis, value);
}
/// <summary>
/// Indicate how data tracks should be handled
/// </summary>
public DataPlayback DataPlayback { get; set; } = DataPlayback.Skip;
/// <summary>
/// Indicate if hidden tracks should be loaded
/// </summary>
public bool LoadHiddenTracks { get; set; } = false;
/// <summary>
/// Indicates how tracks on different session should be handled
/// </summary>
public SessionHandling SessionHandling { get; set; } = SessionHandling.AllSessions;
private bool _quadChannel;
private bool _isDataTrack;
private bool _copyAllowed;
@@ -290,13 +218,7 @@ namespace RedBookPlayer.Models.Discs
/// Constructor
/// </summary>
/// <param name="options">Set of options for a new disc</param>
public CompactDisc(OpticalDiscOptions options)
{
DataPlayback = options.DataPlayback;
_generateMissingToc = options.GenerateMissingToc;
LoadHiddenTracks = options.LoadHiddenTracks;
SessionHandling = options.SessionHandling;
}
public CompactDisc(OpticalDiscOptions options) => _generateMissingToc = options.GenerateMissingToc;
/// <inheritdoc/>
public override void Init(string path, IOpticalMediaImage image, bool autoPlay)
@@ -313,8 +235,9 @@ namespace RedBookPlayer.Models.Discs
if(!LoadTOC())
return;
// Load the first track
LoadFirstTrack();
// Load the first track by default
CurrentTrackNumber = 1;
LoadTrack(CurrentTrackNumber);
// Reset total indexes if not in autoplay
if(!autoPlay)
@@ -330,134 +253,32 @@ namespace RedBookPlayer.Models.Discs
Initialized = true;
}
#region Seeking
/// <inheritdoc/>
public override void NextTrack()
{
if(_image == null)
return;
CurrentTrackNumber++;
LoadTrack(CurrentTrackNumber);
}
/// <inheritdoc/>
public override void PreviousTrack()
{
if(_image == null)
return;
CurrentTrackNumber--;
LoadTrack(CurrentTrackNumber);
}
/// <inheritdoc/>
public override bool NextIndex(bool changeTrack)
{
if(_image == null)
return false;
// Cache the current track for easy access
Track track = GetTrack(CurrentTrackNumber);
if(track == null)
return false;
// If the index is greater than the highest index, change tracks if needed
if(CurrentTrackIndex + 1 > track.Indexes.Keys.Max())
{
if(changeTrack)
{
NextTrack();
CurrentSector = (ulong)GetTrack(CurrentTrackNumber).Indexes.Values.Min();
return true;
}
}
// If the next index has an invalid offset, change tracks if needed
else if(track.Indexes[(ushort)(CurrentTrackIndex + 1)] < 0)
{
if(changeTrack)
{
NextTrack();
CurrentSector = (ulong)GetTrack(CurrentTrackNumber).Indexes.Values.Min();
return true;
}
}
// Otherwise, just move to the next index
else
{
CurrentSector = (ulong)track.Indexes[++CurrentTrackIndex];
}
return false;
}
/// <inheritdoc/>
public override bool PreviousIndex(bool changeTrack)
{
if(_image == null)
return false;
// Cache the current track for easy access
Track track = GetTrack(CurrentTrackNumber);
if(track == null)
return false;
// If the index is less than the lowest index, change tracks if needed
if(CurrentTrackIndex - 1 < track.Indexes.Keys.Min())
{
if(changeTrack)
{
PreviousTrack();
CurrentSector = (ulong)GetTrack(CurrentTrackNumber).Indexes.Values.Max();
return true;
}
}
// If the previous index has an invalid offset, change tracks if needed
else if(track.Indexes[(ushort)(CurrentTrackIndex - 1)] < 0)
{
if(changeTrack)
{
PreviousTrack();
CurrentSector = (ulong)GetTrack(CurrentTrackNumber).Indexes.Values.Max();
return true;
}
}
// Otherwise, just move to the previous index
else
{
CurrentSector = (ulong)track.Indexes[--CurrentTrackIndex];
}
return false;
}
#endregion
#region Helpers
/// <inheritdoc/>
public override void ExtractTrackToWav(uint trackNumber, string outputDirectory)
public override void ExtractTrackToWav(uint trackNumber, string outputDirectory) => ExtractTrackToWav(trackNumber, outputDirectory, DataPlayback.Skip);
/// <summary>
/// Extract a track to WAV
/// </summary>
/// <param name="trackNumber">Track number to extract</param>
/// <param name="outputDirectory">Output path to write data to</param>
/// <param name="dataPlayback">DataPlayback value indicating how to handle data tracks</param>
public void ExtractTrackToWav(uint trackNumber, string outputDirectory, DataPlayback dataPlayback)
{
if(_image == null)
return;
// Get the track with that value, if possible
Track track = _image.Tracks.FirstOrDefault(t => t.TrackSequence == trackNumber);
// If the track isn't valid, we can't do anything
if(track == null || !(DataPlayback != DataPlayback.Skip || track.TrackType == TrackType.Audio))
if (track == null)
return;
// Get the number of sectors to read
uint length = (uint)(track.TrackEndSector - track.TrackStartSector);
// Read in the track data to a buffer
byte[] buffer = ReadSectors(track.TrackStartSector, length);
byte[] buffer = ReadSectors(track.TrackStartSector, length, dataPlayback);
// Build the WAV output
string filename = Path.Combine(outputDirectory, $"Track {trackNumber.ToString().PadLeft(2, '0')}.wav");
@@ -471,15 +292,20 @@ namespace RedBookPlayer.Models.Discs
}
}
/// <inheritdoc/>
public override void ExtractAllTracksToWav(string outputDirectory)
/// <summary>
/// Get the track with the given sequence value, if possible
/// </summary>
/// <param name="trackNumber">Track number to retrieve</param>
/// <returns>Track object for the requested sequence, null on error</returns>
public Track GetTrack(int trackNumber)
{
if(_image == null)
return;
foreach(Track track in _image.Tracks)
try
{
ExtractTrackToWav(track.TrackSequence, outputDirectory);
return _image.Tracks.FirstOrDefault(t => t.TrackSequence == trackNumber);
}
catch
{
return null;
}
}
@@ -501,28 +327,49 @@ namespace RedBookPlayer.Models.Discs
}
/// <inheritdoc/>
public override void LoadFirstTrack()
public override void LoadIndex(ushort index)
{
CurrentTrackNumber = 1;
LoadTrack(CurrentTrackNumber);
if(_image == null)
return;
// Cache the current track for easy access
Track track = GetTrack(CurrentTrackNumber);
if (track == null)
return;
// If the index is invalid, just return
if(index < track.Indexes.Keys.Min() || index > track.Indexes.Keys.Max())
return;
// Select the first index that has a sector offset greater than or equal to 0
CurrentSector = (ulong)track.Indexes[index];
}
/// <inheritdoc/>
public override byte[] ReadSectors(uint sectorsToRead) => ReadSectors(CurrentSector, sectorsToRead);
public override byte[] ReadSectors(uint sectorsToRead) => ReadSectors(CurrentSector, sectorsToRead, DataPlayback.Skip);
/// <summary>
/// Read sector data from the base image starting from the specified sector
/// </summary>
/// <param name="sectorsToRead">Current number of sectors to read</param>
/// <param name="dataPlayback">DataPlayback value indicating how to handle data tracks</param>
/// <returns>Byte array representing the read sectors, if possible</returns>
public byte[] ReadSectors(uint sectorsToRead, DataPlayback dataPlayback) => ReadSectors(CurrentSector, sectorsToRead, DataPlayback.Skip);
/// <summary>
/// Read sector data from the base image starting from the specified sector
/// </summary>
/// <param name="startSector">Sector to start at for reading</param>
/// <param name="sectorsToRead">Current number of sectors to read</param>
/// <param name="dataPlayback">DataPlayback value indicating how to handle data tracks</param>
/// <returns>Byte array representing the read sectors, if possible</returns>
private byte[] ReadSectors(ulong startSector, uint sectorsToRead)
private byte[] ReadSectors(ulong startSector, uint sectorsToRead, DataPlayback dataPlayback)
{
if(TrackType == TrackType.Audio || DataPlayback == DataPlayback.Play)
if(TrackType == TrackType.Audio || dataPlayback == DataPlayback.Play)
{
return _image.ReadSectors(startSector, sectorsToRead);
}
else if(DataPlayback == DataPlayback.Blank)
else if(dataPlayback == DataPlayback.Blank)
{
byte[] sectors = _image.ReadSectors(startSector, sectorsToRead);
Array.Clear(sectors, 0, sectors.Length);
@@ -543,23 +390,6 @@ namespace RedBookPlayer.Models.Discs
TotalIndexes = GetTrack(CurrentTrackNumber)?.Indexes.Keys.Max() ?? 0;
}
/// <summary>
/// Get the track with the given sequence value, if possible
/// </summary>
/// <param name="trackNumber">Track number to retrieve</param>
/// <returns>Track object for the requested sequence, null on error</returns>
private Track GetTrack(int trackNumber)
{
try
{
return _image.Tracks.FirstOrDefault(t => t.TrackSequence == trackNumber);
}
catch
{
return null;
}
}
/// <summary>
/// Load TOC for the current disc image
/// </summary>

View File

@@ -103,34 +103,6 @@ namespace RedBookPlayer.Models.Discs
/// <param name="autoPlay">True if playback should begin immediately, false otherwise</param>
public abstract void Init(string path, IOpticalMediaImage image, bool autoPlay);
#region Seeking
/// <summary>
/// Try to move to the next track, wrapping around if necessary
/// </summary>
public abstract void NextTrack();
/// <summary>
/// Try to move to the previous track, wrapping around if necessary
/// </summary>
public abstract void PreviousTrack();
/// <summary>
/// Try to move to the next track index
/// </summary>
/// <param name="changeTrack">True if index changes can trigger a track change, false otherwise</param>
/// <returns>True if the track was changed, false otherwise</returns>
public abstract bool NextIndex(bool changeTrack);
/// <summary>
/// Try to move to the previous track index
/// </summary>
/// <param name="changeTrack">True if index changes can trigger a track change, false otherwise</param>
/// <returns>True if the track was changed, false otherwise</returns>
public abstract bool PreviousIndex(bool changeTrack);
#endregion
#region Helpers
/// <summary>
@@ -140,12 +112,6 @@ namespace RedBookPlayer.Models.Discs
/// <param name="outputDirectory">Output path to write data to</param>
public abstract void ExtractTrackToWav(uint trackNumber, string outputDirectory);
/// <summary>
/// Extract all tracks to WAV
/// </summary>
/// <param name="outputDirectory">Output path to write data to</param>
public abstract void ExtractAllTracksToWav(string outputDirectory);
/// <summary>
/// Load the desired track, if possible
/// </summary>
@@ -153,9 +119,10 @@ namespace RedBookPlayer.Models.Discs
public abstract void LoadTrack(int track);
/// <summary>
/// Load the first valid track in the image
/// Load the desired index, if possible
/// </summary>
public abstract void LoadFirstTrack();
/// <param name="index">Index number to load</param>
public abstract void LoadIndex(ushort index);
/// <summary>
/// Read sector data from the base image starting from the current sector

View File

@@ -4,26 +4,11 @@ namespace RedBookPlayer.Models.Discs
{
#region CompactDisc
/// <summary>
/// Indicate how data tracks should be handled
/// </summary>
public DataPlayback DataPlayback { get; set; } = DataPlayback.Skip;
/// <summary>
/// Indicate if a TOC should be generated if missing
/// </summary>
public bool GenerateMissingToc { get; set; } = false;
/// <summary>
/// Indicate if hidden tracks should be loaded
/// </summary>
public bool LoadHiddenTracks { get; set; } = false;
/// <summary>
/// Indicates how tracks on different session should be handled
/// </summary>
public SessionHandling SessionHandling { get; set; } = SessionHandling.AllSessions;
#endregion
}
}

View File

@@ -64,9 +64,14 @@ namespace RedBookPlayer.Models
Single,
/// <summary>
/// Repeat all tracks
/// Repeat all tracks on a single disc
/// </summary>
All,
AllSingleDisc,
/// <summary>
/// Repeat all tracks on a multiple discs
/// </summary>
AllMultiDisc,
}
/// <summary>

View File

@@ -7,7 +7,7 @@ namespace RedBookPlayer.Models.Hardware.Linux
/// <summary>
/// Sound output instance
/// </summary>
private ALSoundOut _soundOut;
private readonly ALSoundOut _soundOut;
public AudioBackend() { }

View File

@@ -1,52 +0,0 @@
using CSCore.SoundOut;
namespace RedBookPlayer.Models.Hardware.Mac
{
public class AudioBackend : IAudioBackend
{
/// <summary>
/// Sound output instance
/// </summary>
private ALSoundOut _soundOut;
public AudioBackend() { }
public AudioBackend(PlayerSource source)
{
_soundOut = new ALSoundOut(100);
_soundOut.Initialize(source);
}
#region IAudioBackend Implementation
/// <inheritdoc/>
public void Pause() => _soundOut.Pause();
/// <inheritdoc/>
public void Play() => _soundOut.Play();
/// <inheritdoc/>
public void Stop() => _soundOut.Stop();
/// <inheritdoc/>
public PlayerState GetPlayerState()
{
return (_soundOut?.PlaybackState) switch
{
PlaybackState.Paused => PlayerState.Paused,
PlaybackState.Playing => PlayerState.Playing,
PlaybackState.Stopped => PlayerState.Stopped,
_ => PlayerState.NoDisc,
};
}
/// <inheritdoc/>
public void SetVolume(float volume)
{
if (_soundOut != null)
_soundOut.Volume = volume;
}
#endregion
}
}

View File

@@ -1,5 +1,10 @@
using System;
using System.ComponentModel;
using System.Linq;
using System.Threading.Tasks;
using Aaru.CommonTypes.Enums;
using Aaru.CommonTypes.Structs;
using Avalonia.Threading;
using ReactiveUI;
using RedBookPlayer.Models.Discs;
using RedBookPlayer.Models.Factories;
@@ -36,9 +41,19 @@ namespace RedBookPlayer.Models.Hardware
}
}
/// <summary>
/// Should invoke playback mode changes
/// </summary>
private bool ShouldInvokePlaybackModes
{
get => _shouldInvokePlaybackModes;
set => this.RaiseAndSetIfChanged(ref _shouldInvokePlaybackModes, value);
}
private bool _initialized;
private int _numberOfDiscs;
private int _currentDisc;
private bool _shouldInvokePlaybackModes;
#region OpticalDisc Passthrough
@@ -201,6 +216,15 @@ namespace RedBookPlayer.Models.Hardware
private set => this.RaiseAndSetIfChanged(ref _dataPlayback, value);
}
/// <summary>
/// Indicate if hidden tracks should be loaded
/// </summary>
public bool LoadHiddenTracks
{
get => _loadHiddenTracks;
private set => this.RaiseAndSetIfChanged(ref _loadHiddenTracks, value);
}
/// <summary>
/// Indicates the repeat mode
/// </summary>
@@ -210,6 +234,15 @@ namespace RedBookPlayer.Models.Hardware
private set => this.RaiseAndSetIfChanged(ref _repeatMode, value);
}
/// <summary>
/// Indicates how tracks on different session should be handled
/// </summary>
public SessionHandling SessionHandling
{
get => _sessionHandling;
private set => this.RaiseAndSetIfChanged(ref _sessionHandling, value);
}
/// <summary>
/// Indicates if de-emphasis should be applied
/// </summary>
@@ -230,7 +263,9 @@ namespace RedBookPlayer.Models.Hardware
private PlayerState _playerState;
private DataPlayback _dataPlayback;
private bool _loadHiddenTracks;
private RepeatMode _repeatMode;
private SessionHandling _sessionHandling;
private bool _applyDeEmphasis;
private int _volume;
@@ -241,7 +276,7 @@ namespace RedBookPlayer.Models.Hardware
/// <summary>
/// Sound output handling class
/// </summary>
private readonly SoundOutput[] _soundOutputs;
private readonly SoundOutput _soundOutput;
/// <summary>
/// OpticalDisc object
@@ -253,6 +288,21 @@ namespace RedBookPlayer.Models.Hardware
/// </summary>
private int? _lastVolume = null;
/// <summary>
/// Filtering stage for audio output
/// </summary>
private FilterStage _filterStage;
/// <summary>
/// Current position in the sector for reading
/// </summary>
private int _currentSectorReadPosition = 0;
/// <summary>
/// Lock object for reading track data
/// </summary>
private readonly object _readingImage = new object();
#endregion
/// <summary>
@@ -268,44 +318,51 @@ namespace RedBookPlayer.Models.Hardware
numberOfDiscs = 1;
_numberOfDiscs = numberOfDiscs;
_soundOutputs = new SoundOutput[_numberOfDiscs];
_opticalDiscs = new OpticalDiscBase[numberOfDiscs];
_currentDisc = 0;
for (int i = 0; i < _numberOfDiscs; i++)
{
_soundOutputs[i] = new SoundOutput(defaultVolume);
_soundOutputs[i].SetDeEmphasis(false);
}
_filterStage = new FilterStage();
_soundOutput = new SoundOutput(defaultVolume);
PropertyChanged += HandlePlaybackModes;
}
/// <summary>
/// Initializes player from a given image path
/// </summary>
/// <param name="path">Path to the disc image</param>
/// <param name="options">Options to pass to the optical disc factory</param>
/// <param name="repeatMode">RepeatMode for sound output</param>
/// <param name="playerOptions">Options to pass to the player</param>
/// <param name="opticalDiscOptions">Options to pass to the optical disc factory</param>
/// <param name="autoPlay">True if playback should begin immediately, false otherwise</param>
public void Init(string path, OpticalDiscOptions options, RepeatMode repeatMode, bool autoPlay)
public void Init(string path, PlayerOptions playerOptions, OpticalDiscOptions opticalDiscOptions, bool autoPlay)
{
// Reset initialization
Initialized = false;
// Set player options
DataPlayback = playerOptions.DataPlayback;
LoadHiddenTracks = playerOptions.LoadHiddenTracks;
RepeatMode = playerOptions.RepeatMode;
SessionHandling = playerOptions.SessionHandling;
// Initalize the disc
_opticalDiscs[CurrentDisc] = OpticalDiscFactory.GenerateFromPath(path, options, autoPlay);
_opticalDiscs[CurrentDisc] = OpticalDiscFactory.GenerateFromPath(path, opticalDiscOptions, autoPlay);
if(_opticalDiscs[CurrentDisc] == null || !_opticalDiscs[CurrentDisc].Initialized)
return;
// Add event handling for the optical disc
_opticalDiscs[CurrentDisc].PropertyChanged += OpticalDiscStateChanged;
// Setup de-emphasis filters
_filterStage.SetupFilters();
// Initialize the sound output
_soundOutputs[CurrentDisc].Init(_opticalDiscs[CurrentDisc], repeatMode, autoPlay);
if(_soundOutputs[CurrentDisc] == null || !_soundOutputs[CurrentDisc].Initialized)
_soundOutput.Init(ProviderRead, autoPlay);
if(_soundOutput == null || !_soundOutput.Initialized)
return;
// Add event handling for the sound output
_soundOutputs[CurrentDisc].PropertyChanged += SoundOutputStateChanged;
_soundOutput.PropertyChanged += SoundOutputStateChanged;
// Mark the player as ready
Initialized = true;
@@ -315,7 +372,7 @@ namespace RedBookPlayer.Models.Hardware
SoundOutputStateChanged(this, null);
}
#region Playback
#region Playback (UI)
/// <summary>
/// Begin playback
@@ -324,12 +381,12 @@ namespace RedBookPlayer.Models.Hardware
{
if(_opticalDiscs[CurrentDisc] == null || !_opticalDiscs[CurrentDisc].Initialized)
return;
else if(_soundOutputs[CurrentDisc] == null)
else if(_soundOutput == null)
return;
else if(_soundOutputs[CurrentDisc].PlayerState != PlayerState.Paused && _soundOutputs[CurrentDisc].PlayerState != PlayerState.Stopped)
else if(_soundOutput.PlayerState != PlayerState.Paused && _soundOutput.PlayerState != PlayerState.Stopped)
return;
_soundOutputs[CurrentDisc].Play();
_soundOutput.Play();
_opticalDiscs[CurrentDisc].SetTotalIndexes();
PlayerState = PlayerState.Playing;
}
@@ -341,12 +398,12 @@ namespace RedBookPlayer.Models.Hardware
{
if(_opticalDiscs[CurrentDisc] == null || !_opticalDiscs[CurrentDisc].Initialized)
return;
else if(_soundOutputs[CurrentDisc] == null)
else if(_soundOutput == null)
return;
else if(_soundOutputs[CurrentDisc].PlayerState != PlayerState.Playing)
else if(_soundOutput.PlayerState != PlayerState.Playing)
return;
_soundOutputs[CurrentDisc]?.Pause();
_soundOutput.Pause();
PlayerState = PlayerState.Paused;
}
@@ -378,13 +435,14 @@ namespace RedBookPlayer.Models.Hardware
{
if(_opticalDiscs[CurrentDisc] == null || !_opticalDiscs[CurrentDisc].Initialized)
return;
else if(_soundOutputs[CurrentDisc] == null)
else if(_soundOutput == null)
return;
else if(_soundOutputs[CurrentDisc].PlayerState != PlayerState.Playing && _soundOutputs[CurrentDisc].PlayerState != PlayerState.Paused)
else if(_soundOutput.PlayerState != PlayerState.Playing && _soundOutput.PlayerState != PlayerState.Paused)
return;
_soundOutputs[CurrentDisc].Stop();
_opticalDiscs[CurrentDisc].LoadFirstTrack();
_soundOutput.Stop();
CurrentTrackNumber = 0;
SelectTrack(1);
PlayerState = PlayerState.Stopped;
}
@@ -395,199 +453,47 @@ namespace RedBookPlayer.Models.Hardware
{
if(_opticalDiscs[CurrentDisc] == null || !_opticalDiscs[CurrentDisc].Initialized)
return;
else if(_soundOutputs[CurrentDisc] == null)
else if(_soundOutput == null)
return;
Stop();
_soundOutputs[CurrentDisc].Eject();
_soundOutput.Eject();
_opticalDiscs[CurrentDisc] = null;
PlayerState = PlayerState.NoDisc;
Initialized = false;
}
/// <summary>
/// Select a particular disc by number
/// </summary>
public void SelectDisc(int discNumber)
{
PlayerState wasPlaying = PlayerState;
if (wasPlaying == PlayerState.Playing)
Stop();
CurrentDisc = discNumber;
if (_opticalDiscs[CurrentDisc] != null && _opticalDiscs[CurrentDisc].Initialized)
{
Initialized = true;
OpticalDiscStateChanged(this, null);
SoundOutputStateChanged(this, null);
if(wasPlaying == PlayerState.Playing)
Play();
}
else
{
PlayerState = PlayerState.NoDisc;
Initialized = false;
}
}
/// <summary>
/// Move to the next disc
/// </summary>
public void NextDisc()
{
PlayerState wasPlaying = PlayerState;
if (wasPlaying == PlayerState.Playing)
Stop();
CurrentDisc++;
if (_opticalDiscs[CurrentDisc] != null && _opticalDiscs[CurrentDisc].Initialized)
{
Initialized = true;
OpticalDiscStateChanged(this, null);
SoundOutputStateChanged(this, null);
if(wasPlaying == PlayerState.Playing)
Play();
}
else
{
PlayerState = PlayerState.NoDisc;
Initialized = false;
}
}
public void NextDisc() => SelectDisc(CurrentDisc + 1);
/// <summary>
/// Move to the previous disc
/// </summary>
public void PreviousDisc()
{
PlayerState wasPlaying = PlayerState;
if (wasPlaying == PlayerState.Playing)
Stop();
CurrentDisc--;
if (_opticalDiscs[CurrentDisc] != null && _opticalDiscs[CurrentDisc].Initialized)
{
Initialized = true;
OpticalDiscStateChanged(this, null);
SoundOutputStateChanged(this, null);
if(wasPlaying == PlayerState.Playing)
Play();
}
else
{
PlayerState = PlayerState.NoDisc;
Initialized = false;
}
}
/// <summary>
/// Select a particular track by number
/// </summary>
public void SelectTrack(int trackNumber)
{
if(_opticalDiscs[CurrentDisc] == null || !_opticalDiscs[CurrentDisc].Initialized)
return;
PlayerState wasPlaying = PlayerState;
if(wasPlaying == PlayerState.Playing)
Pause();
if(trackNumber < (HiddenTrack ? 0 : 1) || trackNumber > TotalTracks)
_opticalDiscs[CurrentDisc].LoadFirstTrack();
else
_opticalDiscs[CurrentDisc].LoadTrack(trackNumber);
if(_opticalDiscs[CurrentDisc] is CompactDisc compactDisc)
_soundOutputs[CurrentDisc].SetDeEmphasis(compactDisc.TrackHasEmphasis);
if(wasPlaying == PlayerState.Playing)
Play();
}
public void PreviousDisc() => SelectDisc(CurrentDisc - 1);
/// <summary>
/// Move to the next playable track
/// </summary>
public void NextTrack()
{
if(_opticalDiscs[CurrentDisc] == null || !_opticalDiscs[CurrentDisc].Initialized)
return;
PlayerState wasPlaying = PlayerState;
if(wasPlaying == PlayerState.Playing)
Pause();
_opticalDiscs[CurrentDisc].NextTrack();
if(_opticalDiscs[CurrentDisc] is CompactDisc compactDisc)
_soundOutputs[CurrentDisc].SetDeEmphasis(compactDisc.TrackHasEmphasis);
if(wasPlaying == PlayerState.Playing)
Play();
}
public void NextTrack() => SelectTrack(CurrentTrackNumber + 1);
/// <summary>
/// Move to the previous playable track
/// </summary>
public void PreviousTrack()
{
if(_opticalDiscs[CurrentDisc] == null || !_opticalDiscs[CurrentDisc].Initialized)
return;
PlayerState wasPlaying = PlayerState;
if(wasPlaying == PlayerState.Playing)
Pause();
_opticalDiscs[CurrentDisc].PreviousTrack();
if(_opticalDiscs[CurrentDisc] is CompactDisc compactDisc)
_soundOutputs[CurrentDisc].SetDeEmphasis(compactDisc.TrackHasEmphasis);
if(wasPlaying == PlayerState.Playing)
Play();
}
public void PreviousTrack() => SelectTrack(CurrentTrackNumber - 1);
/// <summary>
/// Move to the next index
/// </summary>
/// <param name="changeTrack">True if index changes can trigger a track change, false otherwise</param>
public void NextIndex(bool changeTrack)
{
if(_opticalDiscs[CurrentDisc] == null || !_opticalDiscs[CurrentDisc].Initialized)
return;
PlayerState wasPlaying = PlayerState;
if(wasPlaying == PlayerState.Playing)
Pause();
_opticalDiscs[CurrentDisc].NextIndex(changeTrack);
if(_opticalDiscs[CurrentDisc] is CompactDisc compactDisc)
_soundOutputs[CurrentDisc].SetDeEmphasis(compactDisc.TrackHasEmphasis);
if(wasPlaying == PlayerState.Playing)
Play();
}
public void NextIndex(bool changeTrack) => SelectIndex((ushort)(CurrentTrackIndex + 1), changeTrack);
/// <summary>
/// Move to the previous index
/// </summary>
/// <param name="changeTrack">True if index changes can trigger a track change, false otherwise</param>
public void PreviousIndex(bool changeTrack)
{
if(_opticalDiscs[CurrentDisc] == null || !_opticalDiscs[CurrentDisc].Initialized)
return;
PlayerState wasPlaying = PlayerState;
if(wasPlaying == PlayerState.Playing)
Pause();
_opticalDiscs[CurrentDisc].PreviousIndex(changeTrack);
if(_opticalDiscs[CurrentDisc] is CompactDisc compactDisc)
_soundOutputs[CurrentDisc].SetDeEmphasis(compactDisc.TrackHasEmphasis);
if(wasPlaying == PlayerState.Playing)
Play();
}
public void PreviousIndex(bool changeTrack) => SelectIndex((ushort)(CurrentTrackIndex - 1), changeTrack);
/// <summary>
/// Fast-forward playback by 75 sectors
@@ -613,6 +519,348 @@ namespace RedBookPlayer.Models.Hardware
#endregion
#region Playback (Internal)
/// <summary>
/// Fill the current byte buffer with playable data
/// </summary>
/// <param name="buffer">Buffer to load data into</param>
/// <param name="offset">Offset in the buffer to load at</param>
/// <param name="count">Number of bytes to load</param>
/// <returns>Number of bytes read</returns>
public int ProviderRead(byte[] buffer, int offset, int count)
{
// If we have an unreadable amount
if (count <= 0)
{
Array.Clear(buffer, offset, count);
return count;
}
// If we have an unreadable track, just return
if(_opticalDiscs[CurrentDisc].BytesPerSector <= 0)
{
Array.Clear(buffer, offset, count);
return count;
}
// Determine how many sectors we can read
DetermineReadAmount(count, out ulong sectorsToRead, out ulong zeroSectorsAmount);
// Get data to return
byte[] audioDataSegment = ReadData(count, sectorsToRead, zeroSectorsAmount);
if(audioDataSegment == null)
{
Array.Clear(buffer, offset, count);
return count;
}
// Write out the audio data to the buffer
Array.Copy(audioDataSegment, 0, buffer, offset, count);
// Set the read position in the sector for easier access
_currentSectorReadPosition += count;
if(_currentSectorReadPosition >= _opticalDiscs[CurrentDisc].BytesPerSector)
{
ulong newSectorValue = _opticalDiscs[CurrentDisc].CurrentSector + (ulong)(_currentSectorReadPosition / _opticalDiscs[CurrentDisc].BytesPerSector);
if(newSectorValue >= _opticalDiscs[CurrentDisc].TotalSectors)
{
ShouldInvokePlaybackModes = true;
}
else if(RepeatMode == RepeatMode.Single && _opticalDiscs[CurrentDisc] is CompactDisc compactDisc)
{
ulong trackEndSector = compactDisc.GetTrack(CurrentTrackNumber).TrackEndSector;
if (newSectorValue > trackEndSector)
{
ShouldInvokePlaybackModes = true;
}
else
{
_opticalDiscs[CurrentDisc].SetCurrentSector(newSectorValue);
_currentSectorReadPosition %= _opticalDiscs[CurrentDisc].BytesPerSector;
}
}
else
{
_opticalDiscs[CurrentDisc].SetCurrentSector(newSectorValue);
_currentSectorReadPosition %= _opticalDiscs[CurrentDisc].BytesPerSector;
}
}
return count;
}
/// <summary>
/// Select a disc by number
/// </summary>
/// <param name="discNumber">Disc number to attempt to load</param>
public void SelectDisc(int discNumber)
{
PlayerState wasPlaying = PlayerState;
if (wasPlaying == PlayerState.Playing)
Stop();
_currentSectorReadPosition = 0;
CurrentDisc = discNumber;
if (_opticalDiscs[CurrentDisc] != null && _opticalDiscs[CurrentDisc].Initialized)
{
Initialized = true;
OpticalDiscStateChanged(this, null);
SoundOutputStateChanged(this, null);
if(wasPlaying == PlayerState.Playing)
Play();
}
else
{
PlayerState = PlayerState.NoDisc;
Initialized = false;
}
}
/// <summary>
/// Select a disc by number
/// </summary>
/// <param name="index">Track index to attempt to load</param>
/// <param name="changeTrack">True if index changes can trigger a track change, false otherwise</param>
public void SelectIndex(ushort index, bool changeTrack)
{
PlayerState wasPlaying = PlayerState;
if (wasPlaying == PlayerState.Playing)
Pause();
// CompactDisc needs special handling of track wraparound
if (_opticalDiscs[CurrentDisc] is CompactDisc compactDisc)
{
// Cache the current track for easy access
Track track = compactDisc.GetTrack(CurrentTrackNumber);
if(track == null)
return;
// Check if we're incrementing or decrementing the track
bool increment = (short)index >= (short)CurrentTrackIndex;
// If the index is greater than the highest index, change tracks if needed
if((short)index > (short)track.Indexes.Keys.Max())
{
if(changeTrack)
NextTrack();
}
// If the index is less than the lowest index, change tracks if needed
else if((short)index < (short)track.Indexes.Keys.Min())
{
if(changeTrack)
{
PreviousTrack();
compactDisc.SetCurrentSector((ulong)compactDisc.GetTrack(CurrentTrackNumber).Indexes.Values.Max());
}
}
// If the next index has an invalid offset, change tracks if needed
else if(track.Indexes[index] < 0)
{
if(changeTrack)
{
if(increment)
{
NextTrack();
}
else
{
PreviousTrack();
compactDisc.SetCurrentSector((ulong)compactDisc.GetTrack(CurrentTrackNumber).Indexes.Values.Min());
}
}
}
// Otherwise, just move to the next index
else
{
compactDisc.SetCurrentSector((ulong)track.Indexes[index]);
}
}
else
{
// TODO: Fill in for non-CD media
}
if(wasPlaying == PlayerState.Playing)
Play();
}
/// <summary>
/// Select a track by number
/// </summary>
/// <param name="trackNumber">Track number to attempt to load</param>
/// <remarks>Changing track with RepeatMode.AllMultiDisc should switch discs<remarks>
public void SelectTrack(int trackNumber)
{
if(_opticalDiscs[CurrentDisc] == null || !_opticalDiscs[CurrentDisc].Initialized)
return;
PlayerState wasPlaying = PlayerState;
if(wasPlaying == PlayerState.Playing)
Pause();
// CompactDisc needs special handling of track wraparound
if (_opticalDiscs[CurrentDisc] is CompactDisc compactDisc)
{
// Cache the value and the current track number
int cachedValue = trackNumber;
int cachedTrackNumber;
// If we have an invalid current track number, set it to the minimum
if(!compactDisc.Tracks.Any(t => t.TrackSequence == _currentTrackNumber))
_currentTrackNumber = (int)compactDisc.Tracks.Min(t => t.TrackSequence);
// Check if we're incrementing or decrementing the track
bool increment = cachedValue >= _currentTrackNumber;
do
{
// If we're over the last track, wrap around
if(cachedValue > compactDisc.Tracks.Max(t => t.TrackSequence))
{
cachedValue = (int)compactDisc.Tracks.Min(t => t.TrackSequence);
if(cachedValue == 0 && !LoadHiddenTracks)
cachedValue++;
}
// If we're under the first track and we're not loading hidden tracks, wrap around
else if(cachedValue < 1 && !LoadHiddenTracks)
{
cachedValue = (int)compactDisc.Tracks.Max(t => t.TrackSequence);
}
// If we're under the first valid track, wrap around
else if(cachedValue < compactDisc.Tracks.Min(t => t.TrackSequence))
{
cachedValue = (int)compactDisc.Tracks.Max(t => t.TrackSequence);
}
cachedTrackNumber = cachedValue;
// Cache the current track for easy access
Track track = compactDisc.GetTrack(cachedTrackNumber);
if(track == null)
return;
// If the track is playable, just return
if((track.TrackType == TrackType.Audio || DataPlayback != DataPlayback.Skip)
&& (SessionHandling == SessionHandling.AllSessions || track.TrackSession == 1))
{
break;
}
// If we're not playing the track, skip
if(increment)
cachedValue++;
else
cachedValue--;
}
while(cachedValue != _currentTrackNumber);
// Load the now-valid value
compactDisc.LoadTrack(cachedTrackNumber);
ApplyDeEmphasis = compactDisc.TrackHasEmphasis;
}
else
{
if(trackNumber >= _opticalDiscs[CurrentDisc].TotalTracks)
trackNumber = 1;
else if(trackNumber < 1)
trackNumber = _opticalDiscs[CurrentDisc].TotalTracks - 1;
_opticalDiscs[CurrentDisc].LoadTrack(trackNumber);
}
if(wasPlaying == PlayerState.Playing)
Play();
}
/// <summary>
/// Determine the number of real and zero sectors to read
/// </summary>
/// <param name="count">Number of requested bytes to read</param>
/// <param name="sectorsToRead">Number of sectors to read</param>
/// <param name="zeroSectorsAmount">Number of zeroed sectors to concatenate</param>
private void DetermineReadAmount(int count, out ulong sectorsToRead, out ulong zeroSectorsAmount)
{
// Attempt to read 10 more sectors than requested
sectorsToRead = ((ulong)count / (ulong)_opticalDiscs[CurrentDisc].BytesPerSector) + 10;
zeroSectorsAmount = 0;
// Avoid overreads by padding with 0-byte data at the end
if(_opticalDiscs[CurrentDisc].CurrentSector + sectorsToRead > _opticalDiscs[CurrentDisc].TotalSectors)
{
ulong oldSectorsToRead = sectorsToRead;
sectorsToRead = _opticalDiscs[CurrentDisc].TotalSectors - _opticalDiscs[CurrentDisc].CurrentSector;
int tempZeroSectorCount = (int)(oldSectorsToRead - sectorsToRead);
zeroSectorsAmount = (ulong)(tempZeroSectorCount < 0 ? 0 : tempZeroSectorCount);
}
}
/// <summary>
/// Read the requested amount of data from an input
/// </summary>
/// <param name="count">Number of bytes to load</param>
/// <param name="sectorsToRead">Number of sectors to read</param>
/// <param name="zeroSectorsAmount">Number of zeroed sectors to concatenate</param>
/// <returns>The requested amount of data, if possible</returns>
private byte[] ReadData(int count, ulong sectorsToRead, ulong zeroSectorsAmount)
{
// If the amount of zeroes being asked for is the same as the sectors, return null
if (sectorsToRead == zeroSectorsAmount)
return null;
// Create padding data for overreads
byte[] zeroSectors = new byte[(int)zeroSectorsAmount * _opticalDiscs[CurrentDisc].BytesPerSector];
byte[] audioData;
// Attempt to read the required number of sectors
var readSectorTask = Task.Run(() =>
{
lock(_readingImage)
{
try
{
if(_opticalDiscs[CurrentDisc] is CompactDisc compactDisc)
return compactDisc.ReadSectors((uint)sectorsToRead, DataPlayback).Concat(zeroSectors).ToArray();
else
return _opticalDiscs[CurrentDisc].ReadSectors((uint)sectorsToRead).Concat(zeroSectors).ToArray();
}
catch { }
return zeroSectors;
}
});
// Wait 100ms at longest for the read to occur
if(readSectorTask.Wait(TimeSpan.FromMilliseconds(100)))
audioData = readSectorTask.Result;
else
return null;
// Load only the requested audio segment
byte[] audioDataSegment = new byte[count];
int copyAmount = Math.Min(count, audioData.Length - _currentSectorReadPosition);
if(Math.Max(0, copyAmount) == 0)
return null;
Array.Copy(audioData, _currentSectorReadPosition, audioDataSegment, 0, copyAmount);
// Apply de-emphasis filtering, only if enabled
if(ApplyDeEmphasis)
_filterStage.ProcessAudioData(audioDataSegment);
return audioDataSegment;
}
#endregion
#region Volume
/// <summary>
@@ -629,7 +877,7 @@ namespace RedBookPlayer.Models.Hardware
/// Set the value for the volume
/// </summary>
/// <param name="volume">New volume value</param>
public void SetVolume(int volume) => _soundOutputs[CurrentDisc]?.SetVolume(volume);
public void SetVolume(int volume) => _soundOutput?.SetVolume(volume);
/// <summary>
/// Temporarily mute playback
@@ -670,59 +918,137 @@ namespace RedBookPlayer.Models.Hardware
/// <summary>
/// Set de-emphasis status
/// </summary>
/// <param name="apply"></param>
private void SetDeEmphasis(bool apply) => _soundOutputs[CurrentDisc]?.SetDeEmphasis(apply);
/// <param name="applyDeEmphasis"></param>
private void SetDeEmphasis(bool applyDeEmphasis) => ApplyDeEmphasis = applyDeEmphasis;
#endregion
#region Helpers
#region Extraction
/// <summary>
/// Extract a single track from the image to WAV
/// </summary>
/// <param name="trackNumber"></param>
/// <param name="outputDirectory">Output path to write data to</param>
public void ExtractSingleTrackToWav(uint trackNumber, string outputDirectory) => _opticalDiscs[CurrentDisc]?.ExtractTrackToWav(trackNumber, outputDirectory);
public void ExtractSingleTrackToWav(uint trackNumber, string outputDirectory)
{
OpticalDiscBase opticalDisc = _opticalDiscs[CurrentDisc];
if(opticalDisc == null || !opticalDisc.Initialized)
return;
if(opticalDisc is CompactDisc compactDisc)
{
// Get the track with that value, if possible
Track track = compactDisc.Tracks.FirstOrDefault(t => t.TrackSequence == trackNumber);
// If the track isn't valid, we can't do anything
if(track == null || !(DataPlayback != DataPlayback.Skip || track.TrackType == TrackType.Audio))
return;
// Extract the track if it's valid
compactDisc.ExtractTrackToWav(trackNumber, outputDirectory, DataPlayback);
}
else
{
opticalDisc?.ExtractTrackToWav(trackNumber, outputDirectory);
}
}
/// <summary>
/// Extract all tracks from the image to WAV
/// </summary>
/// <param name="outputDirectory">Output path to write data to</param>
public void ExtractAllTracksToWav(string outputDirectory) => _opticalDiscs[CurrentDisc]?.ExtractAllTracksToWav(outputDirectory);
public void ExtractAllTracksToWav(string outputDirectory)
{
OpticalDiscBase opticalDisc = _opticalDiscs[CurrentDisc];
if(opticalDisc == null || !opticalDisc.Initialized)
return;
if(opticalDisc is CompactDisc compactDisc)
{
foreach(Track track in compactDisc.Tracks)
{
ExtractSingleTrackToWav(track.TrackSequence, outputDirectory);
}
}
else
{
for(uint i = 0; i < opticalDisc.TotalTracks; i++)
{
ExtractSingleTrackToWav(i, outputDirectory);
}
}
}
#endregion
#region Setters
/// <summary>
/// Set data playback method [CompactDisc only]
/// </summary>
/// <param name="dataPlayback">New playback value</param>
public void SetDataPlayback(DataPlayback dataPlayback)
{
if(_opticalDiscs[CurrentDisc] is CompactDisc compactDisc)
compactDisc.DataPlayback = dataPlayback;
}
public void SetDataPlayback(DataPlayback dataPlayback) => DataPlayback = dataPlayback;
/// <summary>
/// Set the value for loading hidden tracks [CompactDisc only]
/// </summary>
/// <param name="load">True to enable loading hidden tracks, false otherwise</param>
public void SetLoadHiddenTracks(bool load)
{
if(_opticalDiscs[CurrentDisc] is CompactDisc compactDisc)
compactDisc.LoadHiddenTracks = load;
}
/// <param name="loadHiddenTracks">True to enable loading hidden tracks, false otherwise</param>
public void SetLoadHiddenTracks(bool loadHiddenTracks) => LoadHiddenTracks = loadHiddenTracks;
/// <summary>
/// Set repeat mode
/// </summary>
/// <param name="repeatMode">New repeat mode value</param>
public void SetRepeatMode(RepeatMode repeatMode) => _soundOutputs[CurrentDisc]?.SetRepeatMode(repeatMode);
public void SetRepeatMode(RepeatMode repeatMode) => RepeatMode = repeatMode;
/// <summary>
/// Set the value for session handling [CompactDisc only]
/// </summary>
/// <param name="sessionHandling">New session handling value</param>
public void SetSessionHandling(SessionHandling sessionHandling)
public void SetSessionHandling(SessionHandling sessionHandling) => SessionHandling = sessionHandling;
#endregion
#region State Change Event Handlers
/// <summary>
/// Handle special playback modes if we get flagged to
/// </summary>
private async void HandlePlaybackModes(object sender, PropertyChangedEventArgs e)
{
if(_opticalDiscs[CurrentDisc] is CompactDisc compactDisc)
compactDisc.SessionHandling = sessionHandling;
if(e.PropertyName != nameof(ShouldInvokePlaybackModes))
return;
// Always stop before doing anything else
PlayerState wasPlaying = PlayerState;
await Dispatcher.UIThread.InvokeAsync(Stop);
switch(RepeatMode)
{
case RepeatMode.None:
// No-op
break;
case RepeatMode.Single:
_opticalDiscs[CurrentDisc].LoadTrack(CurrentTrackNumber);
break;
case RepeatMode.AllSingleDisc:
SelectTrack(1);
break;
case RepeatMode.AllMultiDisc:
do
{
NextDisc();
}
while(_opticalDiscs[CurrentDisc] != null && !_opticalDiscs[CurrentDisc].Initialized);
SelectTrack(1);
break;
}
_shouldInvokePlaybackModes = false;
if(wasPlaying == PlayerState.Playing)
await Dispatcher.UIThread.InvokeAsync(Play);
}
/// <summary>
@@ -759,10 +1085,8 @@ namespace RedBookPlayer.Models.Hardware
/// </summary>
private void SoundOutputStateChanged(object sender, PropertyChangedEventArgs e)
{
PlayerState = _soundOutputs[CurrentDisc].PlayerState;
RepeatMode = _soundOutputs[CurrentDisc].RepeatMode;
ApplyDeEmphasis = _soundOutputs[CurrentDisc].ApplyDeEmphasis;
Volume = _soundOutputs[CurrentDisc].Volume;
PlayerState = _soundOutput.PlayerState;
Volume = _soundOutput.Volume;
}
#endregion

View File

@@ -0,0 +1,25 @@
namespace RedBookPlayer.Models.Discs
{
public class PlayerOptions
{
/// <summary>
/// Indicate how data tracks should be handled
/// </summary>
public DataPlayback DataPlayback { get; set; } = DataPlayback.Skip;
/// <summary>
/// Indicate if hidden tracks should be loaded
/// </summary>
public bool LoadHiddenTracks { get; set; } = false;
/// <summary>
/// Indicates the repeat mode
/// </summary>
public RepeatMode RepeatMode { get; set; } = RepeatMode.None;
/// <summary>
/// Indicates how tracks on different session should be handled
/// </summary>
public SessionHandling SessionHandling { get; set; } = SessionHandling.AllSessions;
}
}

View File

@@ -1,9 +1,5 @@
using System;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using ReactiveUI;
using RedBookPlayer.Models.Discs;
namespace RedBookPlayer.Models.Hardware
{
@@ -29,24 +25,6 @@ namespace RedBookPlayer.Models.Hardware
private set => this.RaiseAndSetIfChanged(ref _playerState, value);
}
/// <summary>
/// Indicates the repeat mode
/// </summary>
public RepeatMode RepeatMode
{
get => _repeatMode;
private set => this.RaiseAndSetIfChanged(ref _repeatMode, value);
}
/// <summary>
/// Indicates if de-emphasis should be applied
/// </summary>
public bool ApplyDeEmphasis
{
get => _applyDeEmphasis;
private set => this.RaiseAndSetIfChanged(ref _applyDeEmphasis, value);
}
/// <summary>
/// Current playback volume
/// </summary>
@@ -67,22 +45,12 @@ namespace RedBookPlayer.Models.Hardware
private bool _initialized;
private PlayerState _playerState;
private RepeatMode _repeatMode;
private bool _applyDeEmphasis;
private int _volume;
#endregion
#region Private State Variables
/// <summary>
/// OpticalDisc from the parent player for easy access
/// </summary>
/// <remarks>
/// TODO: Can we remove the need for a local reference to OpticalDisc?
/// </remarks>
private OpticalDiscBase _opticalDisc;
/// <summary>
/// Data provider for sound output
/// </summary>
@@ -93,60 +61,26 @@ namespace RedBookPlayer.Models.Hardware
/// </summary>
private IAudioBackend _soundOut;
/// <summary>
/// Filtering stage for audio output
/// </summary>
private FilterStage _filterStage;
/// <summary>
/// Current position in the sector
/// </summary>
private int _currentSectorReadPosition = 0;
/// <summary>
/// Lock object for reading track data
/// </summary>
private readonly object _readingImage = new object();
#endregion
/// <summary>
/// Constructor
/// </summary>
/// <param name="defaultVolume">Default volume between 0 and 100 to use when starting playback</param>
public SoundOutput(int defaultVolume = 100)
{
Volume = defaultVolume;
_filterStage = new FilterStage();
}
public SoundOutput(int defaultVolume = 100) => Volume = defaultVolume;
/// <summary>
/// Initialize the output with a given image
/// </summary>
/// <param name="opticalDisc">OpticalDisc to load from</param>
/// <param name="repeatMode">RepeatMode for sound output</param>
/// <param name="read">ReadFunction to use during decoding</param>
/// <param name="autoPlay">True if playback should begin immediately, false otherwise</param>
public void Init(OpticalDiscBase opticalDisc, RepeatMode repeatMode, bool autoPlay)
public void Init(PlayerSource.ReadFunction read, bool autoPlay)
{
// If we have an unusable disc, just return
if(opticalDisc == null || !opticalDisc.Initialized)
return;
// Save a reference to the disc
_opticalDisc = opticalDisc;
// Enable de-emphasis for CDs, if necessary
if(opticalDisc is CompactDisc compactDisc)
ApplyDeEmphasis = compactDisc.TrackHasEmphasis;
// Setup de-emphasis filters
_filterStage.SetupFilters();
// Reset initialization
Initialized = false;
// Setup the audio output
SetupAudio();
// Setup the repeat mode
RepeatMode = repeatMode;
SetupAudio(read);
// Initialize playback, if necessary
if(autoPlay)
@@ -166,61 +100,10 @@ namespace RedBookPlayer.Models.Hardware
public void Reset()
{
_soundOut.Stop();
_opticalDisc = null;
Initialized = false;
PlayerState = PlayerState.NoDisc;
}
/// <summary>
/// Fill the current byte buffer with playable data
/// </summary>
/// <param name="buffer">Buffer to load data into</param>
/// <param name="offset">Offset in the buffer to load at</param>
/// <param name="count">Number of bytes to load</param>
/// <returns>Number of bytes read</returns>
public int ProviderRead(byte[] buffer, int offset, int count)
{
// Set the current volume
_soundOut.SetVolume((float)Volume / 100);
// If we have an unreadable track, just return
if(_opticalDisc.BytesPerSector <= 0)
{
Array.Clear(buffer, offset, count);
return count;
}
// Determine how many sectors we can read
DetermineReadAmount(count, out ulong sectorsToRead, out ulong zeroSectorsAmount);
// Get data to return
byte[] audioDataSegment = ReadData(count, sectorsToRead, zeroSectorsAmount);
if(audioDataSegment == null)
{
Array.Clear(buffer, offset, count);
return count;
}
// Write out the audio data to the buffer
Array.Copy(audioDataSegment, 0, buffer, offset, count);
// Set the read position in the sector for easier access
_currentSectorReadPosition += count;
if(_currentSectorReadPosition >= _opticalDisc.BytesPerSector)
{
int currentTrack = _opticalDisc.CurrentTrackNumber;
_opticalDisc.SetCurrentSector(_opticalDisc.CurrentSector + (ulong)(_currentSectorReadPosition / _opticalDisc.BytesPerSector));
_currentSectorReadPosition %= _opticalDisc.BytesPerSector;
if(RepeatMode == RepeatMode.None && _opticalDisc.CurrentTrackNumber < currentTrack)
Stop();
else if(RepeatMode == RepeatMode.Single && _opticalDisc.CurrentTrackNumber != currentTrack)
_opticalDisc.LoadTrack(currentTrack);
}
return count;
}
#region Playback
/// <summary>
@@ -265,108 +148,25 @@ namespace RedBookPlayer.Models.Hardware
#region Helpers
/// <summary>
/// Set de-emphasis status
/// </summary>
/// <param name="apply">New de-emphasis status</param>
public void SetDeEmphasis(bool apply) => ApplyDeEmphasis = apply;
/// <summary>
/// Set repeat mode
/// </summary>
/// <param name="repeatMode">New repeat mode value</param>
public void SetRepeatMode(RepeatMode repeatMode) => RepeatMode = repeatMode;
/// <summary>
/// Set the value for the volume
/// </summary>
/// <param name="volume">New volume value</param>
public void SetVolume(int volume) => Volume = volume;
/// <summary>
/// Determine the number of real and zero sectors to read
/// </summary>
/// <param name="count">Number of requested bytes to read</param>
/// <param name="sectorsToRead">Number of sectors to read</param>
/// <param name="zeroSectorsAmount">Number of zeroed sectors to concatenate</param>
private void DetermineReadAmount(int count, out ulong sectorsToRead, out ulong zeroSectorsAmount)
public void SetVolume(int volume)
{
// Attempt to read 10 more sectors than requested
sectorsToRead = ((ulong)count / (ulong)_opticalDisc.BytesPerSector) + 10;
zeroSectorsAmount = 0;
// Avoid overreads by padding with 0-byte data at the end
if(_opticalDisc.CurrentSector + sectorsToRead > _opticalDisc.TotalSectors)
{
ulong oldSectorsToRead = sectorsToRead;
sectorsToRead = _opticalDisc.TotalSectors - _opticalDisc.CurrentSector;
int tempZeroSectorCount = (int)(oldSectorsToRead - sectorsToRead);
zeroSectorsAmount = (ulong)(tempZeroSectorCount < 0 ? 0 : tempZeroSectorCount);
}
}
/// <summary>
/// Read the requested amount of data from an input
/// </summary>
/// <param name="count">Number of bytes to load</param>
/// <param name="sectorsToRead">Number of sectors to read</param>
/// <param name="zeroSectorsAmount">Number of zeroed sectors to concatenate</param>
/// <returns>The requested amount of data, if possible</returns>
private byte[] ReadData(int count, ulong sectorsToRead, ulong zeroSectorsAmount)
{
// Create padding data for overreads
byte[] zeroSectors = new byte[(int)zeroSectorsAmount * _opticalDisc.BytesPerSector];
byte[] audioData;
// Attempt to read the required number of sectors
var readSectorTask = Task.Run(() =>
{
lock(_readingImage)
{
for(int i = 0; i < 4; i++)
{
try
{
return _opticalDisc.ReadSectors((uint)sectorsToRead).Concat(zeroSectors).ToArray();
}
catch { }
}
return zeroSectors;
}
});
// Wait 100ms at longest for the read to occur
if(readSectorTask.Wait(TimeSpan.FromMilliseconds(100)))
audioData = readSectorTask.Result;
else
return null;
// Load only the requested audio segment
byte[] audioDataSegment = new byte[count];
int copyAmount = Math.Min(count, audioData.Length - _currentSectorReadPosition);
if(Math.Max(0, copyAmount) == 0)
return null;
Array.Copy(audioData, _currentSectorReadPosition, audioDataSegment, 0, copyAmount);
// Apply de-emphasis filtering, only if enabled
if(ApplyDeEmphasis)
_filterStage.ProcessAudioData(audioDataSegment);
return audioDataSegment;
Volume = volume;
_soundOut.SetVolume((float)Volume / 100);
}
/// <summary>
/// Sets or resets the audio playback objects
/// </summary>
private void SetupAudio()
/// <param name="read">ReadFunction to use during decoding</param>
private void SetupAudio(PlayerSource.ReadFunction read)
{
if(_source == null)
{
_source = new PlayerSource(ProviderRead);
_source = new PlayerSource(read);
if(RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
_soundOut = new Linux.AudioBackend(_source);
else if(RuntimeInformation.IsOSPlatform(OSPlatform.Windows))

View File

@@ -7,7 +7,7 @@ namespace RedBookPlayer.Models.Hardware.Windows
/// <summary>
/// Sound output instance
/// </summary>
private ALSoundOut _soundOut;
private readonly ALSoundOut _soundOut;
public AudioBackend() { }