diff --git a/.gitmodules b/.gitmodules index 361306b..38b3a5b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -3,4 +3,4 @@ url = https://github.com/aaru-dps/Aaru.git [submodule "cscore"] path = cscore - url = https://github.com/deagahelio/cscore.git + url = https://github.com/filoe/cscore.git diff --git a/Aaru b/Aaru index 65739fa..b41b167 160000 --- a/Aaru +++ b/Aaru @@ -1 +1 @@ -Subproject commit 65739fa9660fd4637204288898b80779484b359f +Subproject commit b41b1679117927df188b2f14bfaa5c2190af05d1 diff --git a/RedBookPlayer/PlayableDisc.cs b/RedBookPlayer/PlayableDisc.cs new file mode 100644 index 0000000..f030fa9 --- /dev/null +++ b/RedBookPlayer/PlayableDisc.cs @@ -0,0 +1,699 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Aaru.CommonTypes.Enums; +using Aaru.CommonTypes.Interfaces; +using Aaru.CommonTypes.Structs; +using Aaru.Decoders.CD; +using Aaru.Helpers; +using static Aaru.Decoders.CD.FullTOC; + +namespace RedBookPlayer +{ + public class PlayableDisc + { + #region Public Fields + + /// + /// Indicate if the disc is ready to be used + /// + public bool Initialized { get; private set; } = false; + + /// + /// Current track number + /// + public int CurrentTrackNumber + { + get => _currentTrackNumber; + set + { + // Unset image means we can't do anything + if(_image == null) + return; + + // Cache the value and the current track number + int cachedValue = value; + int cachedTrackNumber = _currentTrackNumber; + + // Check if we're incrementing or decrementing the track + bool increment = cachedValue >= _currentTrackNumber; + + do + { + // Ensure that the value is valid, wrapping around if necessary + if(cachedValue >= _image.Tracks.Count) + cachedValue = 0; + else if(cachedValue < 0) + cachedValue = _image.Tracks.Count - 1; + + _currentTrackNumber = cachedValue; + + // Cache the current track for easy access + Track track = _image.Tracks[_currentTrackNumber]; + + // Set track flags from subchannel data, if possible + SetTrackFlags(track); + + ApplyDeEmphasis = TrackHasEmphasis; + + TotalIndexes = track.Indexes.Keys.Max(); + CurrentTrackIndex = track.Indexes.Keys.Min(); + + // If the track is playable, just return + if(TrackType == TrackType.Audio || App.Settings.PlayDataTracks) + return; + + // If we're not playing the track, skip + if(increment) + cachedValue++; + else + cachedValue--; + } + while(cachedValue != cachedTrackNumber); + } + } + + /// + /// Current track index + /// + public ushort CurrentTrackIndex + { + get => _currentTrackIndex; + set + { + // Unset image means we can't do anything + if(_image == null) + return; + + // Cache the current track for easy access + Track track = _image.Tracks[CurrentTrackNumber]; + + // Ensure that the value is valid, wrapping around if necessary + if(value > track.Indexes.Keys.Max()) + _currentTrackIndex = 0; + else if(value < 0) + _currentTrackIndex = track.Indexes.Keys.Max(); + else + _currentTrackIndex = value; + + // Set new index-specific data + SectionStartSector = (ulong)track.Indexes[CurrentTrackIndex]; + TotalTime = track.TrackEndSector - track.TrackStartSector; + } + } + + /// + /// Current sector number + /// + public ulong CurrentSector + { + get => _currentSector; + set + { + // Unset image means we can't do anything + if(_image == null) + return; + + // Cache the current track for easy access + Track track = _image.Tracks[CurrentTrackNumber]; + + _currentSector = value; + + if((CurrentTrackNumber < _image.Tracks.Count - 1 && CurrentSector >= _image.Tracks[CurrentTrackNumber + 1].TrackStartSector) + || (CurrentTrackNumber > 0 && CurrentSector < track.TrackStartSector)) + { + foreach(Track trackData in _image.Tracks.ToArray().Reverse()) + { + if(CurrentSector >= trackData.TrackStartSector) + { + CurrentTrackNumber = (int)trackData.TrackSequence - 1; + break; + } + } + } + + foreach((ushort key, int i) in track.Indexes.Reverse()) + { + if((int)CurrentSector >= i) + { + CurrentTrackIndex = key; + return; + } + } + + CurrentTrackIndex = 0; + } + } + + /// + /// Represents the PRE flag + /// + public bool TrackHasEmphasis { get; private set; } = false; + + /// + /// Indicates if de-emphasis should be applied + /// + public bool ApplyDeEmphasis { get; private set; } = false; + + /// + /// Represents the DCP flag + /// + public bool CopyAllowed { get; private set; } = false; + + /// + /// Represents the track type + /// + public TrackType TrackType { get; private set; } + + /// + /// Represents the 4CH flag + /// + public bool QuadChannel { get; private set; } = false; + + /// + /// Represents the sector starting the section + /// + public ulong SectionStartSector { get; private set; } + + /// + /// Represents the total tracks on the disc + /// + public int TotalTracks { get; private set; } = 0; + + /// + /// Represents the total indices on the disc + /// + public int TotalIndexes { get; private set; } = 0; + + /// + /// Total sectors in the image + /// + public ulong TotalSectors => _image.Info.Sectors; + + /// + /// Represents the time adjustment offset for the disc + /// + public ulong TimeOffset { get; private set; } = 0; + + /// + /// Represents the total playing time for the disc + /// + public ulong TotalTime { get; private set; } = 0; + + #endregion + + #region Private State Variables + + /// + /// Currently loaded disc image + /// + private IOpticalMediaImage _image; + + /// + /// Current track number + /// + private int _currentTrackNumber = 0; + + /// + /// Current track index + /// + private ushort _currentTrackIndex = 0; + + /// + /// Current sector number + /// + private ulong _currentSector = 0; + + /// + /// Current disc table of contents + /// + private CDFullTOC _toc; + + #endregion + + /// + /// Initialize the disc with a given image + /// + /// Aaruformat image to load + /// True if playback should begin immediately, false otherwise + public void Init(IOpticalMediaImage image, bool autoPlay = false) + { + // If the image is null, we can't do anything + if(image == null) + return; + + // Set the current disc image + _image = image; + + // Attempt to load the TOC + if(!LoadTOC()) + return; + + // Load the first track + LoadFirstTrack(); + + // Reset total indexes if not in autoplay + if(!autoPlay) + TotalIndexes = 0; + + // Set the internal disc state + TotalTracks = _image.Tracks.Count; + TrackDataDescriptor firstTrack = _toc.TrackDescriptors.First(d => d.ADR == 1 && d.POINT == 1); + TimeOffset = (ulong)((firstTrack.PMIN * 60 * 75) + (firstTrack.PSEC * 75) + firstTrack.PFRAME); + TotalTime = TimeOffset + _image.Tracks.Last().TrackEndSector; + + // Mark the disc as ready + Initialized = true; + } + + #region Seeking + + /// + /// Try to move to the next track, wrapping around if necessary + /// + public void NextTrack() + { + if(_image == null) + return; + + CurrentTrackNumber++; + LoadTrack(CurrentTrackNumber); + } + + /// + /// Try to move to the previous track, wrapping around if necessary + /// + public void PreviousTrack() + { + if(_image == null) + return; + + if(CurrentSector < (ulong)_image.Tracks[CurrentTrackNumber].Indexes[1] + 75) + { + if(App.Settings.AllowSkipHiddenTrack && CurrentTrackNumber == 0 && CurrentSector >= 75) + CurrentSector = 0; + else + CurrentTrackNumber--; + } + else + CurrentTrackNumber--; + + LoadTrack(CurrentTrackNumber); + } + + /// + /// Try to move to the next track index + /// + /// True if index changes can trigger a track change, false otherwise + /// True if the track was changed, false otherwise + public bool NextIndex(bool changeTrack) + { + if(_image == null) + return false; + + if(CurrentTrackIndex + 1 > _image.Tracks[CurrentTrackNumber].Indexes.Keys.Max()) + { + if(changeTrack) + { + NextTrack(); + CurrentSector = (ulong)_image.Tracks[CurrentTrackNumber].Indexes.Values.Min(); + return true; + } + } + else + { + CurrentSector = (ulong)_image.Tracks[CurrentTrackNumber].Indexes[++CurrentTrackIndex]; + } + + return false; + } + + /// + /// Try to move to the previous track index + /// + /// True if index changes can trigger a track change, false otherwise + /// True if the track was changed, false otherwise + public bool PreviousIndex(bool changeTrack) + { + if(_image == null) + return false; + + if(CurrentTrackIndex - 1 < _image.Tracks[CurrentTrackNumber].Indexes.Keys.Min()) + { + if(changeTrack) + { + PreviousTrack(); + CurrentSector = (ulong)_image.Tracks[CurrentTrackNumber].Indexes.Values.Max(); + return true; + } + } + else + { + CurrentSector = (ulong)_image.Tracks[CurrentTrackNumber].Indexes[--CurrentTrackIndex]; + } + + return false; + } + + /// + /// Fast-forward playback by 75 sectors, if possible + /// + public void FastForward() + { + if(_image == null) + return; + + CurrentSector = Math.Min(_image.Info.Sectors - 1, CurrentSector + 75); + } + + /// + /// Rewind playback by 75 sectors, if possible + /// + public void Rewind() + { + if(_image == null) + return; + + if(CurrentSector >= 75) + CurrentSector -= 75; + } + + /// + /// Toggle de-emphasis processing + /// + /// True to apply de-emphasis, false otherwise + public void ToggleDeEmphasis(bool enable) => ApplyDeEmphasis = enable; + + #endregion + + #region Helpers + + /// + /// Load the first valid track in the image + /// + public void LoadFirstTrack() + { + CurrentTrackNumber = 0; + LoadTrack(CurrentTrackNumber); + } + + /// + /// Read sector data from the base image starting from the current sector + /// + /// Current number of sectors to read + /// Byte array representing the read sectors, if possible + public byte[] ReadSectors(uint sectorsToRead) => _image.ReadSectors(CurrentSector, sectorsToRead); + + /// + /// Set the total indexes from the current track + /// + public void SetTotalIndexes() + { + if(_image == null) + return; + + TotalIndexes = _image.Tracks[CurrentTrackNumber].Indexes.Keys.Max(); + } + + /// + /// Generate a CDFullTOC object from the current image + /// + /// CDFullTOC object, if possible + /// Copied from + private bool GenerateTOC() + { + // Invalid image means we can't generate anything + if(_image == null) + return false; + + _toc = new CDFullTOC(); + Dictionary _trackFlags = new Dictionary(); + Dictionary sessionEndingTrack = new Dictionary(); + _toc.FirstCompleteSession = byte.MaxValue; + _toc.LastCompleteSession = byte.MinValue; + List trackDescriptors = new List(); + byte currentTrack = 0; + + foreach(Track track in _image.Tracks.OrderBy(t => t.TrackSession).ThenBy(t => t.TrackSequence)) + { + byte[] trackFlags = _image.ReadSectorTag(track.TrackStartSector + 1, SectorTagType.CdTrackFlags); + if(trackFlags != null) + _trackFlags.Add((byte)track.TrackStartSector, trackFlags[0]); + } + + foreach(Track track in _image.Tracks.OrderBy(t => t.TrackSession).ThenBy(t => t.TrackSequence)) + { + if(track.TrackSession < _toc.FirstCompleteSession) + _toc.FirstCompleteSession = (byte)track.TrackSession; + + if(track.TrackSession <= _toc.LastCompleteSession) + { + currentTrack = (byte)track.TrackSequence; + + continue; + } + + if(_toc.LastCompleteSession > 0) + sessionEndingTrack.Add(_toc.LastCompleteSession, currentTrack); + + _toc.LastCompleteSession = (byte)track.TrackSession; + } + + byte currentSession = 0; + + foreach(Track track in _image.Tracks.OrderBy(t => t.TrackSession).ThenBy(t => t.TrackSequence)) + { + _trackFlags.TryGetValue((byte)track.TrackSequence, out byte trackControl); + + if(trackControl == 0 && + track.TrackType != Aaru.CommonTypes.Enums.TrackType.Audio) + trackControl = (byte)CdFlags.DataTrack; + + // Lead-Out + if(track.TrackSession > currentSession && + currentSession != 0) + { + (byte minute, byte second, byte frame) leadoutAmsf = LbaToMsf(track.TrackStartSector - 150); + + (byte minute, byte second, byte frame) leadoutPmsf = + LbaToMsf(_image.Tracks.OrderBy(t => t.TrackSession).ThenBy(t => t.TrackSequence).Last(). + TrackStartSector); + + // Lead-out + trackDescriptors.Add(new TrackDataDescriptor + { + SessionNumber = currentSession, + POINT = 0xB0, + ADR = 5, + CONTROL = 0, + HOUR = 0, + Min = leadoutAmsf.minute, + Sec = leadoutAmsf.second, + Frame = leadoutAmsf.frame, + PHOUR = 2, + PMIN = leadoutPmsf.minute, + PSEC = leadoutPmsf.second, + PFRAME = leadoutPmsf.frame + }); + + // This seems to be constant? It should not exist on CD-ROM but CloneCD creates them anyway + // Format seems like ATIP, but ATIP should not be as 0xC0 in TOC... + //trackDescriptors.Add(new TrackDataDescriptor + //{ + // SessionNumber = currentSession, + // POINT = 0xC0, + // ADR = 5, + // CONTROL = 0, + // Min = 128, + // PMIN = 97, + // PSEC = 25 + //}); + } + + // Lead-in + if(track.TrackSession > currentSession) + { + currentSession = (byte)track.TrackSession; + sessionEndingTrack.TryGetValue(currentSession, out byte endingTrackNumber); + + (byte minute, byte second, byte frame) leadinPmsf = + LbaToMsf(_image.Tracks.FirstOrDefault(t => t.TrackSequence == endingTrackNumber)?.TrackEndSector ?? + 0 + 1); + + // Starting track + trackDescriptors.Add(new TrackDataDescriptor + { + SessionNumber = currentSession, + POINT = 0xA0, + ADR = 1, + CONTROL = trackControl, + PMIN = (byte)track.TrackSequence + }); + + // Ending track + trackDescriptors.Add(new TrackDataDescriptor + { + SessionNumber = currentSession, + POINT = 0xA1, + ADR = 1, + CONTROL = trackControl, + PMIN = endingTrackNumber + }); + + // Lead-out start + trackDescriptors.Add(new TrackDataDescriptor + { + SessionNumber = currentSession, + POINT = 0xA2, + ADR = 1, + CONTROL = trackControl, + PHOUR = 0, + PMIN = leadinPmsf.minute, + PSEC = leadinPmsf.second, + PFRAME = leadinPmsf.frame + }); + } + + (byte minute, byte second, byte frame) pmsf = LbaToMsf(track.TrackStartSector); + + // Track + trackDescriptors.Add(new TrackDataDescriptor + { + SessionNumber = (byte)track.TrackSession, + POINT = (byte)track.TrackSequence, + ADR = 1, + CONTROL = trackControl, + PHOUR = 0, + PMIN = pmsf.minute, + PSEC = pmsf.second, + PFRAME = pmsf.frame + }); + } + + _toc.TrackDescriptors = trackDescriptors.ToArray(); + return true; + } + + /// + /// Convert the sector to LBA values + /// + /// Sector to convert + /// LBA values for the sector number + /// Copied from + private (byte minute, byte second, byte frame) LbaToMsf(ulong sector) => + ((byte)((sector + 150) / 75 / 60), (byte)((sector + 150) / 75 % 60), (byte)((sector + 150) % 75)); + + /// + /// Load TOC for the current disc image + /// + /// True if the TOC could be loaded, false otherwise + private bool LoadTOC() + { + if(_image.Info.ReadableMediaTags?.Contains(MediaTagType.CD_FullTOC) != true) + { + // Only generate the TOC if we have it set + if(!App.Settings.GenerateMissingTOC) + { + Console.WriteLine("Full TOC not found"); + return false; + } + + Console.WriteLine("Attempting to generate TOC"); + if(GenerateTOC()) + { + Console.WriteLine(Prettify(_toc)); + return true; + } + else + { + Console.WriteLine("Full TOC not found or generated"); + return false; + } + } + + byte[] tocBytes = _image.ReadDiskTag(MediaTagType.CD_FullTOC); + if(tocBytes == null || tocBytes.Length == 0) + { + Console.WriteLine("Error reading TOC from disc image"); + return false; + } + + if(Swapping.Swap(BitConverter.ToUInt16(tocBytes, 0)) + 2 != tocBytes.Length) + { + byte[] tmp = new byte[tocBytes.Length + 2]; + Array.Copy(tocBytes, 0, tmp, 2, tocBytes.Length); + tmp[0] = (byte)((tocBytes.Length & 0xFF00) >> 8); + tmp[1] = (byte)(tocBytes.Length & 0xFF); + tocBytes = tmp; + } + + var nullableToc = Decode(tocBytes); + if(nullableToc == null) + { + Console.WriteLine("Error decoding TOC"); + return false; + } + + _toc = nullableToc.Value; + Console.WriteLine(Prettify(_toc)); + return true; + } + + /// + /// Load the desired track, if possible + /// + /// Track number to load + private void LoadTrack(int track) + { + if(_image == null) + return; + + if(track < 0 || track >= _image.Tracks.Count) + return; + + ushort firstIndex = _image.Tracks[track].Indexes.Keys.Min(); + int firstSector = _image.Tracks[track].Indexes[firstIndex]; + CurrentSector = (ulong)(firstSector >= 0 ? firstSector : _image.Tracks[track].Indexes[1]); + } + + /// + /// Set default track flags for the current track + /// + /// Track object to read from + private void SetDefaultTrackFlags(Track track) + { + QuadChannel = false; + TrackType = track.TrackType; + CopyAllowed = false; + TrackHasEmphasis = false; + } + + /// + /// Set track flags from the current track + /// + /// Track object to read from + private void SetTrackFlags(Track track) + { + try + { + // Get the track descriptor from the TOC + TrackDataDescriptor descriptor = _toc.TrackDescriptors.First(d => d.POINT == track.TrackSequence); + + // Set the track flags from TOC data + byte flags = (byte)(descriptor.CONTROL & 0x0D); + TrackHasEmphasis = (flags & (byte)TocControl.TwoChanPreEmph) == (byte)TocControl.TwoChanPreEmph; + CopyAllowed = (flags & (byte)TocControl.CopyPermissionMask) == (byte)TocControl.CopyPermissionMask; + TrackType = (flags & (byte)TocControl.DataTrack) == (byte)TocControl.DataTrack ? TrackType.Data : TrackType.Audio; + QuadChannel = (flags & (byte)TocControl.FourChanNoPreEmph) == (byte)TocControl.FourChanNoPreEmph; + + return; + } + catch(Exception) + { + SetDefaultTrackFlags(track); + } + } + + #endregion + } +} diff --git a/RedBookPlayer/Player.cs b/RedBookPlayer/Player.cs index 40f4f09..6ef92d4 100644 --- a/RedBookPlayer/Player.cs +++ b/RedBookPlayer/Player.cs @@ -1,15 +1,9 @@ using System; -using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using Aaru.CommonTypes.Enums; -using Aaru.CommonTypes.Structs; -using Aaru.DiscImages; -using Aaru.Helpers; using CSCore.SoundOut; using NWaves.Audio; using NWaves.Filters.BiQuad; -using static Aaru.Decoders.CD.FullTOC; namespace RedBookPlayer { @@ -23,247 +17,23 @@ namespace RedBookPlayer public bool Initialized { get; private set; } = false; /// - /// Currently loaded disc image + /// Indicate if the disc is playing /// - public AaruFormat Image { get; private set; } - - /// - /// Current track number - /// - public int CurrentTrack - { - get => _currentTrack; - private set - { - // Unset image means we can't do anything - if(Image == null) - return; - - // If the value is the same, don't do anything - if(value == _currentTrack) - return; - - // Check if we're incrementing or decrementing the track - bool increment = value > _currentTrack; - - // Ensure that the value is valid, wrapping around if necessary - if(value >= Image.Tracks.Count) - _currentTrack = 0; - else if(value < 0) - _currentTrack = Image.Tracks.Count - 1; - else - _currentTrack = value; - - // Cache the current track for easy access - Track track = Image.Tracks[CurrentTrack]; - - // Set new track-specific data - byte[] flagsData = Image.ReadSectorTag(track.TrackSequence, SectorTagType.CdTrackFlags); - ApplyDeEmphasis = ((CdFlags)flagsData[0]).HasFlag(CdFlags.PreEmphasis); - - try - { - byte[] subchannel = Image.ReadSectorTag(track.TrackStartSector, SectorTagType.CdSectorSubchannel); - - if(!ApplyDeEmphasis) - ApplyDeEmphasis = (subchannel[3] & 0b01000000) != 0; - - CopyAllowed = (subchannel[2] & 0b01000000) != 0; - TrackType = (subchannel[1] & 0b01000000) != 0 ? Aaru.CommonTypes.Enums.TrackType.Data : Aaru.CommonTypes.Enums.TrackType.Audio; - } - catch(ArgumentException) - { - TrackType = track.TrackType; - } - - TrackHasEmphasis = ApplyDeEmphasis; - - TotalIndexes = track.Indexes.Keys.Max(); - CurrentIndex = track.Indexes.Keys.Min(); - - // If we're not playing data tracks, skip - if(!App.Settings.PlayDataTracks && TrackType != Aaru.CommonTypes.Enums.TrackType.Audio) - { - if(increment) - NextTrack(); - else - PreviousTrack(); - } - } - } - - /// - /// Current track index - /// - public ushort CurrentIndex - { - get => _currentIndex; - private set - { - // Unset image means we can't do anything - if(Image == null) - return; - - // If the value is the same, don't do anything - if(value == _currentIndex) - return; - - // Cache the current track for easy access - Track track = Image.Tracks[CurrentTrack]; - - // Ensure that the value is valid, wrapping around if necessary - if(value > track.Indexes.Keys.Max()) - _currentIndex = 0; - else if(value < 0) - _currentIndex = track.Indexes.Keys.Max(); - else - _currentIndex = value; - - // Set new index-specific data - SectionStartSector = (ulong)track.Indexes[CurrentIndex]; - TotalTime = track.TrackEndSector - track.TrackStartSector; - } - } - - /// - /// Current sector number - /// - public ulong CurrentSector - { - get => _currentSector; - private set - { - // Unset image means we can't do anything - if(Image == null) - return; - - // If the value is the same, don't do anything - if(value == _currentSector) - return; - - // Cache the current track for easy access - Track track = Image.Tracks[CurrentTrack]; - - _currentSector = value; - - if((CurrentTrack < Image.Tracks.Count - 1 && CurrentSector >= Image.Tracks[CurrentTrack + 1].TrackStartSector) - || (CurrentTrack > 0 && CurrentSector < track.TrackStartSector)) - { - foreach(Track trackData in Image.Tracks.ToArray().Reverse()) - { - if(CurrentSector >= trackData.TrackStartSector) - { - CurrentTrack = (int)trackData.TrackSequence - 1; - break; - } - } - } - - foreach((ushort key, int i) in track.Indexes.Reverse()) - { - if((int)CurrentSector >= i) - { - CurrentIndex = key; - return; - } - } - - CurrentIndex = 0; - } - } - - /// - /// Represents the pre-emphasis flag - /// - public bool TrackHasEmphasis { get; private set; } = false; - - /// - /// Indicates if de-emphasis should be applied - /// - public bool ApplyDeEmphasis { get; private set; } = false; - - /// - /// Represents the copy allowed flag - /// - public bool CopyAllowed { get; private set; } = false; - - /// - /// Represents the track type - /// - public TrackType? TrackType { get; private set; } - - /// - /// Represents the sector starting the section - /// - public ulong SectionStartSector { get; private set; } - - /// - /// Represents the total tracks on the disc - /// - public int TotalTracks { get; private set; } = 0; - - /// - /// Represents the total indices on the disc - /// - public int TotalIndexes { get; private set; } = 0; - - /// - /// Represents the time adjustment offset for the disc - /// - public ulong TimeOffset { get; private set; } = 0; - - /// - /// Represents the total playing time for the disc - /// - public ulong TotalTime { get; private set; } = 0; - - /// - /// Represents the current play volume between 0 and 100 - /// - public int Volume - { - get => _volume; - set - { - if(value >= 0 && - value <= 100) - _volume = value; - } - } + public bool Playing => _soundOut.PlaybackState == PlaybackState.Playing; #endregion #region Private State Variables - /// - /// Current track number - /// - private int _currentTrack = 0; - - /// - /// Current track index - /// - private ushort _currentIndex = 0; - - /// - /// Current sector number - /// - private ulong _currentSector = 0; - /// /// Current position in the sector /// private int _currentSectorReadPosition = 0; /// - /// Current play volume between 0 and 100 + /// PlaybableDisc object /// - private int _volume = 100; - - /// - /// Current disc table of contents - /// - private CDFullTOC _toc; + private PlayableDisc _playableDisc; /// /// Data provider for sound output @@ -295,20 +65,16 @@ namespace RedBookPlayer /// /// Initialize the player with a given image /// - /// Aaruformat image to load for playback + /// Initialized disc image /// True if playback should begin immediately, false otherwise - public async void Init(AaruFormat image, bool autoPlay = false) + public void Init(PlayableDisc disc, bool autoPlay = false) { - // If the image is null, we can't do anything - if(image == null) + // If the disc is not initalized, we can't do anything + if(!disc.Initialized) return; - // Set the current disc image - Image = image; - - // Attempt to load the TOC - if(!await LoadTOC()) - return; + // Set the internal reference to the disc + _playableDisc = disc; // Setup the de-emphasis filters SetupFilters(); @@ -316,24 +82,9 @@ namespace RedBookPlayer // Setup the audio output SetupAudio(); - // Load the first track - CurrentTrack = 0; - LoadTrack(0); - // Initialize playback, if necessary if(autoPlay) _soundOut.Play(); - else - TotalIndexes = 0; - - // Set the internal disc state - TotalTracks = image.Tracks.Count; - TrackDataDescriptor firstTrack = _toc.TrackDescriptors.First(d => d.ADR == 1 && d.POINT == 1); - TimeOffset = (ulong)((firstTrack.PMIN * 60 * 75) + (firstTrack.PSEC * 75) + firstTrack.PFRAME); - TotalTime = TimeOffset + image.Tracks.Last().TrackEndSector; - - // Set the output volume from settings - Volume = App.Settings.Volume; // Mark the player as ready Initialized = true; @@ -352,7 +103,7 @@ namespace RedBookPlayer public int ProviderRead(byte[] buffer, int offset, int count) { // Set the current volume - _soundOut.Volume = (float)Volume / 100; + _soundOut.Volume = (float)App.Settings.Volume / 100; // Determine how many sectors we can read ulong sectorsToRead; @@ -364,17 +115,17 @@ namespace RedBookPlayer zeroSectorsAmount = 0; // Avoid overreads by padding with 0-byte data at the end - if(CurrentSector + sectorsToRead > Image.Info.Sectors) + if(_playableDisc.CurrentSector + sectorsToRead > _playableDisc.TotalSectors) { ulong oldSectorsToRead = sectorsToRead; - sectorsToRead = Image.Info.Sectors - CurrentSector; + sectorsToRead = _playableDisc.TotalSectors - _playableDisc.CurrentSector; zeroSectorsAmount = oldSectorsToRead - sectorsToRead; } // TODO: Figure out when this value could be negative if(sectorsToRead <= 0) { - LoadTrack(0); + _playableDisc.LoadFirstTrack(); _currentSectorReadPosition = 0; } } while(sectorsToRead <= 0); @@ -390,12 +141,12 @@ namespace RedBookPlayer { try { - return Image.ReadSectors(CurrentSector, (uint)sectorsToRead).Concat(zeroSectors).ToArray(); + return _playableDisc.ReadSectors((uint)sectorsToRead).Concat(zeroSectors).ToArray(); } catch(ArgumentOutOfRangeException) { - LoadTrack(0); - return Image.ReadSectors(CurrentSector, (uint)sectorsToRead).Concat(zeroSectors).ToArray(); + _playableDisc.LoadFirstTrack(); + return _playableDisc.ReadSectors((uint)sectorsToRead).Concat(zeroSectors).ToArray(); } } }); @@ -416,7 +167,7 @@ namespace RedBookPlayer Array.Copy(audioData, _currentSectorReadPosition, audioDataSegment, 0, Math.Min(count, audioData.Length - _currentSectorReadPosition)); // Apply de-emphasis filtering, only if enabled - if(ApplyDeEmphasis) + if(_playableDisc.ApplyDeEmphasis) { float[][] floatAudioData = new float[2][]; floatAudioData[0] = new float[audioDataSegment.Length / 4]; @@ -439,25 +190,25 @@ namespace RedBookPlayer _currentSectorReadPosition += count; if(_currentSectorReadPosition >= 2352) { - CurrentSector += (ulong)_currentSectorReadPosition / 2352; + _playableDisc.CurrentSector += (ulong)_currentSectorReadPosition / 2352; _currentSectorReadPosition %= 2352; } return count; } - #region Player Controls + #region Playback /// /// Start audio playback /// public void Play() { - if(Image == null) + if(!_playableDisc.Initialized) return; _soundOut.Play(); - TotalIndexes = Image.Tracks[CurrentTrack].Indexes.Keys.Max(); + _playableDisc.SetTotalIndexes(); } /// @@ -465,7 +216,7 @@ namespace RedBookPlayer /// public void Pause() { - if(Image == null) + if(!_playableDisc.Initialized) return; _soundOut.Stop(); @@ -476,371 +227,17 @@ namespace RedBookPlayer /// public void Stop() { - if(Image == null) + if(!_playableDisc.Initialized) return; _soundOut.Stop(); - LoadTrack(CurrentTrack); + _playableDisc.LoadFirstTrack(); } - /// - /// Try to move to the next track, wrapping around if necessary - /// - public void NextTrack() - { - if(Image == null) - return; - - CurrentTrack++; - LoadTrack(CurrentTrack); - } - - /// - /// Try to move to the previous track, wrapping around if necessary - /// - public void PreviousTrack() - { - if(Image == null) - return; - - if(CurrentSector < (ulong)Image.Tracks[CurrentTrack].Indexes[1] + 75) - { - if(App.Settings.AllowSkipHiddenTrack && CurrentTrack == 0 && CurrentSector >= 75) - CurrentSector = 0; - else - CurrentTrack--; - } - - LoadTrack(CurrentTrack); - } - - /// - /// Try to move to the next track index - /// - /// True if index changes can trigger a track change, false otherwise - public void NextIndex(bool changeTrack) - { - if(Image == null) - return; - - if(CurrentIndex + 1 > Image.Tracks[CurrentTrack].Indexes.Keys.Max()) - { - if(changeTrack) - { - NextTrack(); - CurrentSector = (ulong)Image.Tracks[CurrentTrack].Indexes.Values.Min(); - } - } - else - { - CurrentSector = (ulong)Image.Tracks[CurrentTrack].Indexes[++CurrentIndex]; - } - } - - /// - /// Try to move to the previous track index - /// - /// True if index changes can trigger a track change, false otherwise - public void PreviousIndex(bool changeTrack) - { - if(Image == null) - return; - - if(CurrentIndex - 1 < Image.Tracks[CurrentTrack].Indexes.Keys.Min()) - { - if(changeTrack) - { - PreviousTrack(); - CurrentSector = (ulong)Image.Tracks[CurrentTrack].Indexes.Values.Max(); - } - } - else - { - CurrentSector = (ulong)Image.Tracks[CurrentTrack].Indexes[--CurrentIndex]; - } - } - - /// - /// Fast-forward playback by 75 sectors, if possible - /// - public void FastForward() - { - if(Image == null) - return; - - CurrentSector = Math.Min(Image.Info.Sectors - 1, CurrentSector + 75); - } - - /// - /// Rewind playback by 75 sectors, if possible - /// - public void Rewind() - { - if(Image == null) - return; - - if(CurrentSector >= 75) - CurrentSector -= 75; - } - - /// - /// Toggle de-emphasis processing - /// - /// True to apply de-emphasis, false otherwise - public void ToggleDeEmphasis(bool enable) => ApplyDeEmphasis = enable; - #endregion #region Helpers - /// - /// Generate a CDFullTOC object from the current image - /// - /// CDFullTOC object, if possible - /// Copied from - private bool GenerateTOC() - { - // Invalid image means we can't generate anything - if(Image == null) - return false; - - _toc = new CDFullTOC(); - Dictionary _trackFlags = new Dictionary(); - Dictionary sessionEndingTrack = new Dictionary(); - _toc.FirstCompleteSession = byte.MaxValue; - _toc.LastCompleteSession = byte.MinValue; - List trackDescriptors = new List(); - byte currentTrack = 0; - - foreach(Track track in Image.Tracks.OrderBy(t => t.TrackSession).ThenBy(t => t.TrackSequence)) - { - byte[] trackFlags = Image.ReadSectorTag(track.TrackStartSector + 1, SectorTagType.CdTrackFlags); - if(trackFlags != null) - _trackFlags.Add((byte)track.TrackStartSector, trackFlags[0]); - } - - foreach(Track track in Image.Tracks.OrderBy(t => t.TrackSession).ThenBy(t => t.TrackSequence)) - { - if(track.TrackSession < _toc.FirstCompleteSession) - _toc.FirstCompleteSession = (byte)track.TrackSession; - - if(track.TrackSession <= _toc.LastCompleteSession) - { - currentTrack = (byte)track.TrackSequence; - - continue; - } - - if(_toc.LastCompleteSession > 0) - sessionEndingTrack.Add(_toc.LastCompleteSession, currentTrack); - - _toc.LastCompleteSession = (byte)track.TrackSession; - } - - byte currentSession = 0; - - foreach(Track track in Image.Tracks.OrderBy(t => t.TrackSession).ThenBy(t => t.TrackSequence)) - { - _trackFlags.TryGetValue((byte)track.TrackSequence, out byte trackControl); - - if(trackControl == 0 && - track.TrackType != Aaru.CommonTypes.Enums.TrackType.Audio) - trackControl = (byte)CdFlags.DataTrack; - - // Lead-Out - if(track.TrackSession > currentSession && - currentSession != 0) - { - (byte minute, byte second, byte frame) leadoutAmsf = LbaToMsf(track.TrackStartSector - 150); - - (byte minute, byte second, byte frame) leadoutPmsf = - LbaToMsf(Image.Tracks.OrderBy(t => t.TrackSession).ThenBy(t => t.TrackSequence).Last(). - TrackStartSector); - - // Lead-out - trackDescriptors.Add(new TrackDataDescriptor - { - SessionNumber = currentSession, - POINT = 0xB0, - ADR = 5, - CONTROL = 0, - HOUR = 0, - Min = leadoutAmsf.minute, - Sec = leadoutAmsf.second, - Frame = leadoutAmsf.frame, - PHOUR = 2, - PMIN = leadoutPmsf.minute, - PSEC = leadoutPmsf.second, - PFRAME = leadoutPmsf.frame - }); - - // This seems to be constant? It should not exist on CD-ROM but CloneCD creates them anyway - // Format seems like ATIP, but ATIP should not be as 0xC0 in TOC... - //trackDescriptors.Add(new TrackDataDescriptor - //{ - // SessionNumber = currentSession, - // POINT = 0xC0, - // ADR = 5, - // CONTROL = 0, - // Min = 128, - // PMIN = 97, - // PSEC = 25 - //}); - } - - // Lead-in - if(track.TrackSession > currentSession) - { - currentSession = (byte)track.TrackSession; - sessionEndingTrack.TryGetValue(currentSession, out byte endingTrackNumber); - - (byte minute, byte second, byte frame) leadinPmsf = - LbaToMsf(Image.Tracks.FirstOrDefault(t => t.TrackSequence == endingTrackNumber)?.TrackEndSector ?? - 0 + 1); - - // Starting track - trackDescriptors.Add(new TrackDataDescriptor - { - SessionNumber = currentSession, - POINT = 0xA0, - ADR = 1, - CONTROL = trackControl, - PMIN = (byte)track.TrackSequence - }); - - // Ending track - trackDescriptors.Add(new TrackDataDescriptor - { - SessionNumber = currentSession, - POINT = 0xA1, - ADR = 1, - CONTROL = trackControl, - PMIN = endingTrackNumber - }); - - // Lead-out start - trackDescriptors.Add(new TrackDataDescriptor - { - SessionNumber = currentSession, - POINT = 0xA2, - ADR = 1, - CONTROL = trackControl, - PHOUR = 0, - PMIN = leadinPmsf.minute, - PSEC = leadinPmsf.second, - PFRAME = leadinPmsf.frame - }); - } - - (byte minute, byte second, byte frame) pmsf = LbaToMsf(track.TrackStartSector); - - // Track - trackDescriptors.Add(new TrackDataDescriptor - { - SessionNumber = (byte)track.TrackSession, - POINT = (byte)track.TrackSequence, - ADR = 1, - CONTROL = trackControl, - PHOUR = 0, - PMIN = pmsf.minute, - PSEC = pmsf.second, - PFRAME = pmsf.frame - }); - } - - _toc.TrackDescriptors = trackDescriptors.ToArray(); - return true; - } - - /// - /// Convert the sector to LBA values - /// - /// Sector to convert - /// LBA values for the sector number - /// Copied from - private (byte minute, byte second, byte frame) LbaToMsf(ulong sector) => - ((byte)((sector + 150) / 75 / 60), (byte)((sector + 150) / 75 % 60), (byte)((sector + 150) % 75)); - - /// - /// Load TOC for the current disc image - /// - /// True if the TOC could be loaded, false otherwise - private async Task LoadTOC() - { - if(await Task.Run(() => Image.Info.ReadableMediaTags?.Contains(MediaTagType.CD_FullTOC)) != true) - { - // Only generate the TOC if we have it set - if(!App.Settings.GenerateMissingTOC) - { - Console.WriteLine("Full TOC not found"); - return false; - } - - Console.WriteLine("Attempting to generate TOC"); - if(GenerateTOC()) - { - Console.WriteLine(Prettify(_toc)); - return true; - } - else - { - Console.WriteLine("Full TOC not found or generated"); - return false; - } - } - - byte[] tocBytes = await Task.Run(() => Image.ReadDiskTag(MediaTagType.CD_FullTOC)); - if(tocBytes == null || tocBytes.Length == 0) - { - Console.WriteLine("Error reading TOC from disc image"); - return false; - } - - if(Swapping.Swap(BitConverter.ToUInt16(tocBytes, 0)) + 2 != tocBytes.Length) - { - byte[] tmp = new byte[tocBytes.Length + 2]; - Array.Copy(tocBytes, 0, tmp, 2, tocBytes.Length); - tmp[0] = (byte)((tocBytes.Length & 0xFF00) >> 8); - tmp[1] = (byte)(tocBytes.Length & 0xFF); - tocBytes = tmp; - } - - var nullableToc = await Task.Run(() => Decode(tocBytes)); - if(nullableToc == null) - { - Console.WriteLine("Error decoding TOC"); - return false; - } - - _toc = nullableToc.Value; - Console.WriteLine(Prettify(_toc)); - return true; - } - - /// - /// Load the track for a given track number, if possible - /// - /// Track number to load - private void LoadTrack(int index) - { - // Save if audio is currently playing - bool oldRun = _source.Run; - - // Stop playback if necessary - _source.Stop(); - - // If it is a valid index, seek to the first, non-negative sectored index for the track - if(index >= 0 && index < Image.Tracks.Count) - { - ushort firstIndex = Image.Tracks[index].Indexes.Keys.Min(); - int firstSector = Image.Tracks[index].Indexes[firstIndex]; - CurrentSector = (ulong)(firstSector >= 0 ? firstSector : Image.Tracks[index].Indexes[1]); - } - - // Reset the playing state - _source.Run = oldRun; - } - /// /// Sets or resets the de-emphasis filters /// diff --git a/RedBookPlayer/PlayerView.xaml b/RedBookPlayer/PlayerView.xaml index 7b0f528..2213eba 100644 --- a/RedBookPlayer/PlayerView.xaml +++ b/RedBookPlayer/PlayerView.xaml @@ -94,6 +94,8 @@ EMPHASIS COPY COPY + 4CH + 4CH HIDDEN HIDDEN diff --git a/RedBookPlayer/PlayerView.xaml.cs b/RedBookPlayer/PlayerView.xaml.cs index 9e48786..72dc4b3 100644 --- a/RedBookPlayer/PlayerView.xaml.cs +++ b/RedBookPlayer/PlayerView.xaml.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Threading.Tasks; using System.Timers; using Aaru.CommonTypes.Enums; +using Aaru.CommonTypes.Interfaces; using Aaru.DiscImages; using Aaru.Filters; using Avalonia; @@ -20,10 +21,15 @@ namespace RedBookPlayer public class PlayerView : UserControl { /// - /// Player representing the internal state and loaded image + /// Player representing the internal state /// public static Player Player = new Player(); + /// + /// Disc representing the loaded image + /// + public static PlayableDisc PlayableDisc = new PlayableDisc(); + /// /// Set of images representing the digits for the UI /// @@ -66,32 +72,32 @@ namespace RedBookPlayer /// String representing the digits for the player private string GenerateDigitString() { - // If the player isn't initialized, return all '-' characters - if (!Player.Initialized) + // If the disc or player aren't initialized, return all '-' characters + if (!PlayableDisc.Initialized) return string.Empty.PadLeft(20, '-'); // Otherwise, take the current time into account - ulong sectorTime = Player.CurrentSector; - if (Player.SectionStartSector != 0) - sectorTime -= Player.SectionStartSector; + ulong sectorTime = PlayableDisc.CurrentSector; + if (PlayableDisc.SectionStartSector != 0) + sectorTime -= PlayableDisc.SectionStartSector; else - sectorTime += Player.TimeOffset; + sectorTime += PlayableDisc.TimeOffset; int[] numbers = new int[] { - Player.CurrentTrack + 1, - Player.CurrentIndex, + PlayableDisc.CurrentTrackNumber + 1, + PlayableDisc.CurrentTrackIndex, (int)(sectorTime / (75 * 60)), (int)(sectorTime / 75 % 60), (int)(sectorTime % 75), - Player.TotalTracks, - Player.TotalIndexes, + PlayableDisc.TotalTracks, + PlayableDisc.TotalIndexes, - (int)(Player.TotalTime / (75 * 60)), - (int)(Player.TotalTime / 75 % 60), - (int)(Player.TotalTime % 75), + (int)(PlayableDisc.TotalTime / (75 * 60)), + (int)(PlayableDisc.TotalTime / 75 % 60), + (int)(PlayableDisc.TotalTime % 75), }; return string.Join("", numbers.Select(i => i.ToString().PadLeft(2, '0').Substring(0, 2))); @@ -194,16 +200,12 @@ namespace RedBookPlayer /// /// Aaruformat image file /// True if the image is playble, false otherwise - private bool IsPlayableImage(AaruFormat image) + private bool IsPlayableImage(IOpticalMediaImage image) { // Invalid images can't be played if (image == null) return false; - // Tape images are not supported - if (image.IsTape) - return false; - // Determine based on media type // TODO: Can we be more granular with sub types? (string type, string _) = Aaru.CommonTypes.Metadata.MediaType.MediaTypeToString(image.Info.MediaType); @@ -215,6 +217,43 @@ namespace RedBookPlayer }; } + /// + /// Load an image from the path + /// + /// Path to the image to load + private async void LoadImage(string path) + { + bool result = await Task.Run(() => + { + var image = new AaruFormat(); + var filter = new ZZZNoFilter(); + filter.Open(path); + image.Open(filter); + + if(IsPlayableImage(image)) + { + PlayableDisc.Init(image, App.Settings.AutoPlay); + if(PlayableDisc.Initialized) + { + Player.Init(PlayableDisc, App.Settings.AutoPlay); + return true; + } + + return false; + } + else + return false; + }); + + if(result) + { + await Dispatcher.UIThread.InvokeAsync(() => + { + MainWindow.Instance.Title = "RedBookPlayer - " + path.Split('/').Last().Split('\\').Last(); + }); + } + } + /// /// Update the UI with the most recent information from the Player /// @@ -232,12 +271,13 @@ namespace RedBookPlayer if (Player.Initialized) { PlayerViewModel dataContext = (PlayerViewModel)DataContext; - dataContext.HiddenTrack = Player.TimeOffset > 150; - dataContext.ApplyDeEmphasis = Player.ApplyDeEmphasis; - dataContext.TrackHasEmphasis = Player.TrackHasEmphasis; - dataContext.CopyAllowed = Player.CopyAllowed; - dataContext.IsAudioTrack = Player.TrackType == TrackType.Audio; - dataContext.IsDataTrack = Player.TrackType != TrackType.Audio; + dataContext.HiddenTrack = PlayableDisc.TimeOffset > 150; + dataContext.ApplyDeEmphasis = PlayableDisc.ApplyDeEmphasis; + dataContext.TrackHasEmphasis = PlayableDisc.TrackHasEmphasis; + dataContext.CopyAllowed = PlayableDisc.CopyAllowed; + dataContext.QuadChannel = PlayableDisc.QuadChannel; + dataContext.IsAudioTrack = PlayableDisc.TrackType == TrackType.Audio; + dataContext.IsDataTrack = PlayableDisc.TrackType != TrackType.Audio; } }); } @@ -252,29 +292,7 @@ namespace RedBookPlayer if (path == null) return; - bool result = await Task.Run(() => - { - var image = new AaruFormat(); - var filter = new ZZZNoFilter(); - filter.Open(path); - image.Open(filter); - - if (IsPlayableImage(image)) - { - Player.Init(image, App.Settings.AutoPlay); - return true; - } - else - return false; - }); - - if (result) - { - await Dispatcher.UIThread.InvokeAsync(() => - { - MainWindow.Instance.Title = "RedBookPlayer - " + path.Split('/').Last().Split('\\').Last(); - }); - } + LoadImage(path); } public void PlayButton_Click(object sender, RoutedEventArgs e) => Player.Play(); @@ -283,21 +301,45 @@ namespace RedBookPlayer public void StopButton_Click(object sender, RoutedEventArgs e) => Player.Stop(); - public void NextTrackButton_Click(object sender, RoutedEventArgs e) => Player.NextTrack(); + public void NextTrackButton_Click(object sender, RoutedEventArgs e) + { + bool wasPlaying = Player.Playing; + if(wasPlaying) Player.Pause(); + PlayableDisc.NextTrack(); + if(wasPlaying) Player.Play(); + } - public void PreviousTrackButton_Click(object sender, RoutedEventArgs e) => Player.PreviousTrack(); + public void PreviousTrackButton_Click(object sender, RoutedEventArgs e) + { + bool wasPlaying = Player.Playing; + if(wasPlaying) Player.Pause(); + PlayableDisc.PreviousTrack(); + if(wasPlaying) Player.Play(); + } - public void NextIndexButton_Click(object sender, RoutedEventArgs e) => Player.NextIndex(App.Settings.IndexButtonChangeTrack); + public void NextIndexButton_Click(object sender, RoutedEventArgs e) + { + bool wasPlaying = Player.Playing; + if(wasPlaying) Player.Pause(); + PlayableDisc.NextIndex(App.Settings.IndexButtonChangeTrack); + if(wasPlaying) Player.Play(); + } - public void PreviousIndexButton_Click(object sender, RoutedEventArgs e) => Player.PreviousIndex(App.Settings.IndexButtonChangeTrack); + public void PreviousIndexButton_Click(object sender, RoutedEventArgs e) + { + bool wasPlaying = Player.Playing; + if(wasPlaying) Player.Pause(); + PlayableDisc.PreviousIndex(App.Settings.IndexButtonChangeTrack); + if(wasPlaying) Player.Play(); + } - public void FastForwardButton_Click(object sender, RoutedEventArgs e) => Player.FastForward(); + public void FastForwardButton_Click(object sender, RoutedEventArgs e) => PlayableDisc.FastForward(); - public void RewindButton_Click(object sender, RoutedEventArgs e) => Player.Rewind(); + public void RewindButton_Click(object sender, RoutedEventArgs e) => PlayableDisc.Rewind(); - public void EnableDeEmphasisButton_Click(object sender, RoutedEventArgs e) => Player.ToggleDeEmphasis(true); + public void EnableDeEmphasisButton_Click(object sender, RoutedEventArgs e) => PlayableDisc.ToggleDeEmphasis(true); - public void DisableDeEmphasisButton_Click(object sender, RoutedEventArgs e) => Player.ToggleDeEmphasis(false); + public void DisableDeEmphasisButton_Click(object sender, RoutedEventArgs e) => PlayableDisc.ToggleDeEmphasis(false); #endregion } diff --git a/RedBookPlayer/PlayerViewModel.cs b/RedBookPlayer/PlayerViewModel.cs index a44dcc4..17e058f 100644 --- a/RedBookPlayer/PlayerViewModel.cs +++ b/RedBookPlayer/PlayerViewModel.cs @@ -32,6 +32,13 @@ namespace RedBookPlayer set => this.RaiseAndSetIfChanged(ref _copyAllowed, value); } + private bool _quadChannel; + public bool QuadChannel + { + get => _quadChannel; + set => this.RaiseAndSetIfChanged(ref _quadChannel, value); + } + private bool _isAudioTrack; public bool IsAudioTrack { diff --git a/RedBookPlayer/SettingsWindow.xaml.cs b/RedBookPlayer/SettingsWindow.xaml.cs index abc880f..ae39c0f 100644 --- a/RedBookPlayer/SettingsWindow.xaml.cs +++ b/RedBookPlayer/SettingsWindow.xaml.cs @@ -36,8 +36,6 @@ namespace RedBookPlayer MainWindow.ApplyTheme(_selectedTheme); } - PlayerView.Player.Volume = _settings.Volume; - _settings.Save(); }