using System; using System.Collections.Generic; 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.Audio; using RedBookPlayer.Models.Discs; using RedBookPlayer.Models.Factories; namespace RedBookPlayer.Models.Hardware { public class Player : ReactiveObject { /// /// Indicate if the player is ready to be used /// public bool Initialized { get => _initialized; private set => this.RaiseAndSetIfChanged(ref _initialized, value); } #region Playback Passthrough /// /// Currently selected disc /// public int CurrentDisc { get => _currentDisc; private set { int temp = value; if(temp < 0) temp = _numberOfDiscs - 1; else if(temp >= _numberOfDiscs) temp = 0; this.RaiseAndSetIfChanged(ref _currentDisc, temp); } } /// /// Indicates how to deal with multiple discs /// public DiscHandling DiscHandling { get => _discHandling; private set => this.RaiseAndSetIfChanged(ref _discHandling, value); } /// /// Indicates how to handle playback of data tracks /// public DataPlayback DataPlayback { get => _dataPlayback; private set => this.RaiseAndSetIfChanged(ref _dataPlayback, value); } /// /// Indicate if hidden tracks should be loaded /// public bool LoadHiddenTracks { get => _loadHiddenTracks; private set => this.RaiseAndSetIfChanged(ref _loadHiddenTracks, value); } /// /// Indicates the repeat mode /// public RepeatMode RepeatMode { get => _repeatMode; private set => this.RaiseAndSetIfChanged(ref _repeatMode, value); } /// /// Indicates how tracks on different session should be handled /// public SessionHandling SessionHandling { get => _sessionHandling; private set => this.RaiseAndSetIfChanged(ref _sessionHandling, value); } /// /// Indicates if de-emphasis should be applied /// public bool ApplyDeEmphasis { get => _applyDeEmphasis; private set => this.RaiseAndSetIfChanged(ref _applyDeEmphasis, value); } /// /// Should invoke playback mode changes /// private bool ShouldInvokePlaybackModes { get => _shouldInvokePlaybackModes; set => this.RaiseAndSetIfChanged(ref _shouldInvokePlaybackModes, value); } private bool _initialized; private int _numberOfDiscs; private int _currentDisc; private DiscHandling _discHandling; private bool _loadHiddenTracks; private DataPlayback _dataPlayback; private RepeatMode _repeatMode; private SessionHandling _sessionHandling; private bool _applyDeEmphasis; private bool _shouldInvokePlaybackModes; #endregion #region OpticalDisc Passthrough /// /// Path to the disc image /// public string ImagePath { get => _imagePath; private set => this.RaiseAndSetIfChanged(ref _imagePath, value); } /// /// Current track number /// public int CurrentTrackNumber { get => _currentTrackNumber; private set => this.RaiseAndSetIfChanged(ref _currentTrackNumber, value); } /// /// Current track index /// public ushort CurrentTrackIndex { get => _currentTrackIndex; private set => this.RaiseAndSetIfChanged(ref _currentTrackIndex, value); } /// /// Current track session /// public ushort CurrentTrackSession { get => _currentTrackSession; private set => this.RaiseAndSetIfChanged(ref _currentTrackSession, value); } /// /// Current sector number /// public ulong CurrentSector { get => _currentSector; private set => this.RaiseAndSetIfChanged(ref _currentSector, value); } /// /// Represents the sector starting the section /// public ulong SectionStartSector { get => _sectionStartSector; protected set => this.RaiseAndSetIfChanged(ref _sectionStartSector, value); } /// /// Represents if the disc has a hidden track /// public bool HiddenTrack { get => _hasHiddenTrack; private set => this.RaiseAndSetIfChanged(ref _hasHiddenTrack, value); } /// /// Represents the 4CH flag [CompactDisc only] /// public bool QuadChannel { get => _quadChannel; private set => this.RaiseAndSetIfChanged(ref _quadChannel, value); } /// /// Represents the DATA flag [CompactDisc only] /// public bool IsDataTrack { get => _isDataTrack; private set => this.RaiseAndSetIfChanged(ref _isDataTrack, value); } /// /// Represents the DCP flag [CompactDisc only] /// public bool CopyAllowed { get => _copyAllowed; private set => this.RaiseAndSetIfChanged(ref _copyAllowed, value); } /// /// Represents the PRE flag [CompactDisc only] /// public bool TrackHasEmphasis { get => _trackHasEmphasis; private set => this.RaiseAndSetIfChanged(ref _trackHasEmphasis, value); } /// /// Represents the total tracks on the disc /// public int TotalTracks => _opticalDiscs[CurrentDisc]?.TotalTracks ?? 0; /// /// Represents the total indices on the disc /// public int TotalIndexes => _opticalDiscs[CurrentDisc]?.TotalIndexes ?? 0; /// /// Total sectors in the image /// public ulong TotalSectors => _opticalDiscs[CurrentDisc]?.TotalSectors ?? 0; /// /// Represents the time adjustment offset for the disc /// public ulong TimeOffset => _opticalDiscs[CurrentDisc]?.TimeOffset ?? 0; /// /// Represents the total playing time for the disc /// public ulong TotalTime => _opticalDiscs[CurrentDisc]?.TotalTime ?? 0; private string _imagePath; private int _currentTrackNumber; private ushort _currentTrackIndex; private ushort _currentTrackSession; private ulong _currentSector; private ulong _sectionStartSector; private bool _hasHiddenTrack; private bool _quadChannel; private bool _isDataTrack; private bool _copyAllowed; private bool _trackHasEmphasis; #endregion #region SoundOutput Passthrough /// /// Indicates the current player state /// public PlayerState PlayerState { get => _playerState; private set => this.RaiseAndSetIfChanged(ref _playerState, value); } /// /// Current playback volume /// public int Volume { get => _volume; private set => this.RaiseAndSetIfChanged(ref _volume, value); } private PlayerState _playerState; private int _volume; #endregion #region Private State Variables /// /// Sound output handling class /// private readonly SoundOutput _soundOutput; /// /// OpticalDisc objects /// private readonly OpticalDiscBase[] _opticalDiscs; /// /// List of available tracks organized by disc /// private Dictionary> _availableTrackList; /// /// Current track playback order /// private List> _trackPlaybackOrder; /// /// Current track in playback order list /// private int _currentTrackInOrder; /// /// Last volume for mute toggling /// private int? _lastVolume = null; /// /// Filtering stage for audio output /// private readonly FilterStage _filterStage; /// /// Current position in the sector for reading /// private int _currentSectorReadPosition = 0; /// /// Lock object for reading track data /// private readonly object _readingImage = new object(); #endregion /// /// Constructor /// /// Number of discs to allow loading /// Default volume between 0 and 100 to use when starting playback public Player(int numberOfDiscs, int defaultVolume) { Initialized = false; if(numberOfDiscs <= 0) numberOfDiscs = 1; _numberOfDiscs = numberOfDiscs; _opticalDiscs = new OpticalDiscBase[numberOfDiscs]; _currentDisc = 0; _filterStage = new FilterStage(); _soundOutput = new SoundOutput(ProviderRead, defaultVolume); _soundOutput.PropertyChanged += SoundOutputStateChanged; _availableTrackList = new Dictionary>(); for(int i = 0; i < _numberOfDiscs; i++) { _availableTrackList.Add(i, new List()); } _trackPlaybackOrder = new List>(); _currentTrackInOrder = 0; PropertyChanged += HandlePlaybackModes; // Force a refresh of the state information SoundOutputStateChanged(this, null); } /// /// Initializes player from a given image path /// /// Path to the disc image /// Options to pass to the player /// Options to pass to the optical disc factory /// True if playback should begin immediately, false otherwise public void Init(string path, PlayerOptions playerOptions, OpticalDiscOptions opticalDiscOptions, bool autoPlay) { // Reset initialization Initialized = false; // Set player options DataPlayback = playerOptions.DataPlayback; DiscHandling = playerOptions.DiscHandling; LoadHiddenTracks = playerOptions.LoadHiddenTracks; RepeatMode = playerOptions.RepeatMode; SessionHandling = playerOptions.SessionHandling; // Initalize the disc _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 _soundOutput.Init(autoPlay); if(_soundOutput == null || !_soundOutput.Initialized) return; // Load in the track list for the current disc LoadTrackList(); // Mark the player as ready Initialized = true; // Force a refresh of the state information OpticalDiscStateChanged(this, null); SoundOutputStateChanged(this, null); } /// /// Load the track list into the track dictionary /// private void LoadTrackList() { OpticalDiscBase opticalDisc = _opticalDiscs[CurrentDisc]; // If the disc exists, add it to the dictionary if(_opticalDiscs[CurrentDisc] != null) { if(opticalDisc is CompactDisc compactDisc) _availableTrackList[CurrentDisc] = compactDisc.Tracks.Select(t => (int)t.TrackSequence).OrderBy(s => s).ToList(); else _availableTrackList[CurrentDisc] = Enumerable.Range(1, opticalDisc.TotalTracks).ToList(); } // If the disc is null, then make sure it's removed else { _availableTrackList[CurrentDisc] = new List(); } // Repopulate the playback order _trackPlaybackOrder = new List>(); if(DiscHandling == DiscHandling.SingleDisc) { List availableTracks = _availableTrackList[CurrentDisc]; if(availableTracks != null && availableTracks.Count > 0) _trackPlaybackOrder.AddRange(availableTracks.Select(t => new KeyValuePair(CurrentDisc, t))); } else if(DiscHandling == DiscHandling.MultiDisc) { for(int i = 0; i < _numberOfDiscs; i++) { List availableTracks = _availableTrackList[i]; if(availableTracks != null && availableTracks.Count > 0) _trackPlaybackOrder.AddRange(availableTracks.Select(t => new KeyValuePair(i, t))); } } // Try to get back to the last loaded track SetTrackOrderIndex(); } /// /// Set the current track order index, if possible /// private void SetTrackOrderIndex() { int currentFoundTrack = 0; if(_trackPlaybackOrder == null || _trackPlaybackOrder.Count == 0) { _currentTrackInOrder = 0; return; } else if(_trackPlaybackOrder.Any(kvp => kvp.Key == CurrentDisc)) { currentFoundTrack = _trackPlaybackOrder.FindIndex(kvp => kvp.Key == CurrentDisc && kvp.Value == CurrentTrackNumber); if(currentFoundTrack == -1) currentFoundTrack = _trackPlaybackOrder.FindIndex(kvp => kvp.Key == CurrentDisc && kvp.Value == _trackPlaybackOrder.Min(kvp => kvp.Value)); } else { int lowestDiscNumber = _trackPlaybackOrder.Min(kvp => kvp.Key); currentFoundTrack = _trackPlaybackOrder.FindIndex(kvp => kvp.Key == lowestDiscNumber && kvp.Value == _trackPlaybackOrder.Min(kvp => kvp.Value)); } CurrentDisc = _trackPlaybackOrder[currentFoundTrack].Key; CurrentTrackNumber = _trackPlaybackOrder[currentFoundTrack].Value; _currentTrackInOrder = currentFoundTrack; } #region Playback (UI) /// /// Begin playback /// public void Play() { if(_opticalDiscs[CurrentDisc] == null || !_opticalDiscs[CurrentDisc].Initialized) return; else if(_soundOutput == null) return; else if(_soundOutput.PlayerState != PlayerState.Paused && _soundOutput.PlayerState != PlayerState.Stopped) return; _soundOutput.Play(); _opticalDiscs[CurrentDisc].SetTotalIndexes(); PlayerState = PlayerState.Playing; } /// /// Pause current playback /// public void Pause() { if(_opticalDiscs[CurrentDisc] == null || !_opticalDiscs[CurrentDisc].Initialized) return; else if(_soundOutput == null) return; else if(_soundOutput.PlayerState != PlayerState.Playing) return; _soundOutput.Pause(); PlayerState = PlayerState.Paused; } /// /// Toggle current playback /// public void TogglePlayback() { switch(PlayerState) { case PlayerState.NoDisc: break; case PlayerState.Stopped: Play(); break; case PlayerState.Paused: Play(); break; case PlayerState.Playing: Pause(); break; } } /// /// Shuffle the current track order /// public void ShuffleTracks() { List> newPlaybackOrder = new List>(); Random random = new Random(); while(_trackPlaybackOrder.Count > 0) { int next = random.Next(0, _trackPlaybackOrder.Count - 1); newPlaybackOrder.Add(_trackPlaybackOrder[next]); _trackPlaybackOrder.RemoveAt(next); } _trackPlaybackOrder = newPlaybackOrder; switch(PlayerState) { case PlayerState.Stopped: _currentTrackInOrder = 0; break; case PlayerState.Paused: case PlayerState.Playing: SetTrackOrderIndex(); break; } } /// /// Stop current playback /// public void Stop() { if(_opticalDiscs[CurrentDisc] == null || !_opticalDiscs[CurrentDisc].Initialized) return; else if(_soundOutput == null) return; else if(_soundOutput.PlayerState != PlayerState.Playing && _soundOutput.PlayerState != PlayerState.Paused) return; _soundOutput.Stop(); SelectRelativeTrack(0); PlayerState = PlayerState.Stopped; } /// /// Eject the currently loaded disc /// public void Eject() { if(_opticalDiscs[CurrentDisc] == null || !_opticalDiscs[CurrentDisc].Initialized) return; else if(_soundOutput == null) return; Stop(); _opticalDiscs[CurrentDisc] = null; LoadTrackList(); // Force a refresh of the state information OpticalDiscStateChanged(this, null); SoundOutputStateChanged(this, null); // Only de-initialize the player if all discs are ejected if(_opticalDiscs.All(d => d == null || !d.Initialized)) { _soundOutput.Eject(); PlayerState = PlayerState.NoDisc; Initialized = false; } else { PlayerState = PlayerState.Stopped; } } /// /// Move to the next disc /// public void NextDisc() => SelectDisc(CurrentDisc + 1); /// /// Move to the previous disc /// public void PreviousDisc() => SelectDisc(CurrentDisc - 1); /// /// Move to the next playable track /// /// TODO: This should follow the track playback order public void NextTrack() => SelectRelativeTrack(_currentTrackInOrder + 1); /// /// Move to the previous playable track /// /// TODO: This should follow the track playback order public void PreviousTrack() => SelectRelativeTrack(_currentTrackInOrder - 1); /// /// Move to the next index /// /// True if index changes can trigger a track change, false otherwise public void NextIndex(bool changeTrack) => SelectIndex((ushort)(CurrentTrackIndex + 1), changeTrack); /// /// Move to the previous index /// /// True if index changes can trigger a track change, false otherwise public void PreviousIndex(bool changeTrack) => SelectIndex((ushort)(CurrentTrackIndex - 1), changeTrack); /// /// Fast-forward playback by 75 sectors /// public void FastForward() { if(_opticalDiscs[CurrentDisc] == null || !_opticalDiscs[CurrentDisc].Initialized) return; _opticalDiscs[CurrentDisc].SetCurrentSector(_opticalDiscs[CurrentDisc].CurrentSector + 75); } /// /// Rewind playback by 75 sectors /// public void Rewind() { if(_opticalDiscs[CurrentDisc] == null || !_opticalDiscs[CurrentDisc].Initialized) return; _opticalDiscs[CurrentDisc].SetCurrentSector(_opticalDiscs[CurrentDisc].CurrentSector - 75); } #endregion #region Playback (Internal) /// /// Fill the current byte buffer with playable data /// /// Buffer to load data into /// Offset in the buffer to load at /// Number of bytes to load /// Number of bytes read 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(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) { int previousTrack = CurrentTrackNumber; 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; } // If we are supposed to change tracks, get the next one from the list if(CurrentTrackNumber != previousTrack && !ShouldInvokePlaybackModes) Dispatcher.UIThread.InvokeAsync(NextTrack).ConfigureAwait(false).GetAwaiter().GetResult(); } return count; } /// /// Select a disc by number /// /// Disc number to attempt to load public void SelectDisc(int discNumber) { // If the disc didn't change, don't do anything if(_currentDisc == discNumber) return; PlayerState wasPlaying = PlayerState; if(wasPlaying == PlayerState.Playing) Stop(); if(discNumber >= _numberOfDiscs) discNumber = 0; else if(discNumber < 0) discNumber = _numberOfDiscs - 1; _currentSectorReadPosition = 0; CurrentDisc = discNumber; // If we're in single disc mode, we need to reload the full track list if(DiscHandling == DiscHandling.SingleDisc) LoadTrackList(); if(_opticalDiscs[CurrentDisc] != null && _opticalDiscs[CurrentDisc].Initialized) { Initialized = true; SelectTrack(1); OpticalDiscStateChanged(this, null); SoundOutputStateChanged(this, null); if(wasPlaying == PlayerState.Playing) Play(); } else { PlayerState = PlayerState.NoDisc; Initialized = false; } } /// /// Select a disc by number /// /// Track index to attempt to load /// True if index changes can trigger a track change, false otherwise public void SelectIndex(ushort index, bool changeTrack) { if(_opticalDiscs[CurrentDisc] == null || !_opticalDiscs[CurrentDisc].Initialized) return; // If the index didn't change, don't do anything if(_currentTrackIndex == index) return; 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(); } /// /// Select a track by number within a disc /// /// Track number to attempt to load /// True if the track was changed, false otherwise public bool SelectTrack(int trackNumber) { if(_opticalDiscs[CurrentDisc] == null || !_opticalDiscs[CurrentDisc].Initialized) return false; 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 false; // 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); } SetTrackOrderIndex(); if(wasPlaying == PlayerState.Playing) Play(); return true; } /// /// Determine the number of real and zero sectors to read /// /// Number of sectors to read /// Number of zeroed sectors to concatenate private void DetermineReadAmount(out ulong sectorsToRead, out ulong zeroSectorsAmount) { // Always attempt to read one second of data sectorsToRead = 75; 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); } } /// /// Read the requested amount of data from an input /// /// Number of bytes to load /// Number of sectors to read /// Number of zeroed sectors to concatenate /// The requested amount of data, if possible 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) { //byte[] subchannelData = compactDisc.ReadSubchannels((uint)sectorsToRead); 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; } /// /// Select a track in the relative track list by number /// /// Relative track number to attempt to load private void SelectRelativeTrack(int relativeTrackNumber) { if(_trackPlaybackOrder == null || _trackPlaybackOrder.Count == 0) return; PlayerState wasPlaying = PlayerState; if(wasPlaying == PlayerState.Playing) Pause(); if(relativeTrackNumber < 0) relativeTrackNumber = _trackPlaybackOrder.Count - 1; else if(relativeTrackNumber >= _trackPlaybackOrder.Count) relativeTrackNumber = 0; do { _currentTrackInOrder = relativeTrackNumber; KeyValuePair discTrackPair = _trackPlaybackOrder[relativeTrackNumber]; SelectDisc(discTrackPair.Key); if(SelectTrack(discTrackPair.Value)) break; relativeTrackNumber++; if(relativeTrackNumber < 0) relativeTrackNumber = _trackPlaybackOrder.Count - 1; else if(relativeTrackNumber >= _trackPlaybackOrder.Count) relativeTrackNumber = 0; } while(true); if(wasPlaying == PlayerState.Playing) Play(); } #endregion #region Volume /// /// Increment the volume value /// public void VolumeUp() => SetVolume(Volume + 1); /// /// Decrement the volume value /// public void VolumeDown() => SetVolume(Volume - 1); /// /// Set the value for the volume /// /// New volume value public void SetVolume(int volume) => _soundOutput?.SetVolume(volume); /// /// Temporarily mute playback /// public void ToggleMute() { if(_lastVolume == null) { _lastVolume = Volume; SetVolume(0); } else { SetVolume(_lastVolume.Value); _lastVolume = null; } } #endregion #region Emphasis /// /// Enable de-emphasis /// public void EnableDeEmphasis() => SetDeEmphasis(true); /// /// Disable de-emphasis /// public void DisableDeEmphasis() => SetDeEmphasis(false); /// /// Toggle de-emphasis /// public void ToggleDeEmphasis() => SetDeEmphasis(!ApplyDeEmphasis); /// /// Set de-emphasis status /// /// private void SetDeEmphasis(bool applyDeEmphasis) => ApplyDeEmphasis = applyDeEmphasis; #endregion #region Extraction /// /// Extract a single track from the image to WAV /// /// /// Output path to write data to 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); } } /// /// Extract all tracks from the image to WAV /// /// Output path to write data to 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 /// /// Set data playback method [CompactDisc only] /// /// New playback value public void SetDataPlayback(DataPlayback dataPlayback) => DataPlayback = dataPlayback; /// /// Set disc handling method /// /// New playback value public void SetDiscHandling(DiscHandling discHandling) { DiscHandling = discHandling; LoadTrackList(); } /// /// Set the value for loading hidden tracks [CompactDisc only] /// /// True to enable loading hidden tracks, false otherwise public void SetLoadHiddenTracks(bool loadHiddenTracks) => LoadHiddenTracks = loadHiddenTracks; /// /// Set repeat mode /// /// New repeat mode value public void SetRepeatMode(RepeatMode repeatMode) => RepeatMode = repeatMode; /// /// Set the value for session handling [CompactDisc only] /// /// New session handling value public void SetSessionHandling(SessionHandling sessionHandling) => SessionHandling = sessionHandling; #endregion #region State Change Event Handlers /// /// Handle special playback modes if we get flagged to /// private async void HandlePlaybackModes(object sender, PropertyChangedEventArgs e) { if(e.PropertyName != nameof(ShouldInvokePlaybackModes)) return; // Always pause before doing anything else PlayerState wasPlaying = PlayerState; await Dispatcher.UIThread.InvokeAsync(Pause); switch(RepeatMode) { case RepeatMode.None: await Dispatcher.UIThread.InvokeAsync(Stop); break; case RepeatMode.Single: _opticalDiscs[CurrentDisc].LoadTrack(CurrentTrackNumber); break; case RepeatMode.All: NextTrack(); break; } _shouldInvokePlaybackModes = false; if(wasPlaying == PlayerState.Playing) await Dispatcher.UIThread.InvokeAsync(Play); } /// /// Update the player from the current OpticalDisc /// private void OpticalDiscStateChanged(object sender, PropertyChangedEventArgs e) { if(_opticalDiscs[CurrentDisc] == null) { ImagePath = null; CurrentTrackNumber = 1; return; } ImagePath = _opticalDiscs[CurrentDisc].ImagePath; CurrentTrackNumber = _opticalDiscs[CurrentDisc].CurrentTrackNumber; CurrentTrackIndex = _opticalDiscs[CurrentDisc].CurrentTrackIndex; CurrentSector = _opticalDiscs[CurrentDisc].CurrentSector; SectionStartSector = _opticalDiscs[CurrentDisc].SectionStartSector; HiddenTrack = TimeOffset > 150; if(_opticalDiscs[CurrentDisc] is CompactDisc compactDisc) { QuadChannel = compactDisc.QuadChannel; IsDataTrack = compactDisc.IsDataTrack; CopyAllowed = compactDisc.CopyAllowed; TrackHasEmphasis = compactDisc.TrackHasEmphasis; } else { QuadChannel = false; IsDataTrack = _opticalDiscs[CurrentDisc].TrackType != TrackType.Audio; CopyAllowed = false; TrackHasEmphasis = false; } } /// /// Update the player from the current SoundOutput /// private void SoundOutputStateChanged(object sender, PropertyChangedEventArgs e) { PlayerState = _soundOutput.PlayerState; Volume = _soundOutput.Volume; } #endregion #region Helpers /// /// Parse multiple subchannels into object data /// /// Raw subchannel data to format /// List of subchannel object data private List ParseSubchannels(byte[] subchannelData) { if(subchannelData == null || subchannelData.Length % 96 != 0) return null; // Create the list of objects to return var parsedSubchannelData = new List(); // Read in 96-byte chunks int modValue = subchannelData.Length / 96; for(int i = 0; i < modValue; i++) { byte[] buffer = new byte[96]; Array.Copy(subchannelData, i * 96, buffer, 0, 96); var singleSubchannel = new SubchannelData(buffer); parsedSubchannelData.Add(singleSubchannel); } return parsedSubchannelData; } /// /// Reformat raw subchannel data for multiple sectors /// /// Raw subchannel data to format /// Dictionary mapping subchannel to formatted data private Dictionary ConvertSubchannels(byte[] subchannelData) { if(subchannelData == null || subchannelData.Length % 96 != 0) return null; // Parse the subchannel data, if possible var parsedSubchannelData = ParseSubchannels(subchannelData); return ConvertSubchannels(parsedSubchannelData); } /// /// Reformat subchannel object data for multiple sectors /// /// Subchannel object data to format /// Dictionary mapping subchannel to formatted data private Dictionary ConvertSubchannels(List subchannelData) { if(subchannelData == null) return null; // Prepare the output formatted data Dictionary formattedData = new Dictionary { ['P'] = new byte[8 * subchannelData.Count], ['Q'] = new byte[8 * subchannelData.Count], ['R'] = new byte[8 * subchannelData.Count], ['S'] = new byte[8 * subchannelData.Count], ['T'] = new byte[8 * subchannelData.Count], ['U'] = new byte[8 * subchannelData.Count], ['V'] = new byte[8 * subchannelData.Count], ['W'] = new byte[8 * subchannelData.Count], }; // Read in each object for(int i = 0; i < subchannelData.Count; i++) { if(subchannelData[i] == null) continue; Dictionary singleData = subchannelData[i].ConvertData(); if(singleData == null) continue; Array.Copy(singleData['P'], 0, formattedData['P'], 8 * i, 8); Array.Copy(singleData['Q'], 0, formattedData['Q'], 8 * i, 8); Array.Copy(singleData['R'], 0, formattedData['R'], 8 * i, 8); Array.Copy(singleData['S'], 0, formattedData['S'], 8 * i, 8); Array.Copy(singleData['T'], 0, formattedData['T'], 8 * i, 8); Array.Copy(singleData['U'], 0, formattedData['U'], 8 * i, 8); Array.Copy(singleData['V'], 0, formattedData['V'], 8 * i, 8); Array.Copy(singleData['W'], 0, formattedData['W'], 8 * i, 8); } return formattedData; } #endregion } }