From 4c712677e0586f10f1552def1bbeb5ff4408a2a5 Mon Sep 17 00:00:00 2001 From: Matt Nadareski Date: Mon, 5 Jul 2021 22:13:00 -0700 Subject: [PATCH] Fix rendering of hidden tracks --- RedBookPlayer.Common/Discs/CompactDisc.cs | 154 +++++++++++++++--- RedBookPlayer.Common/Discs/OpticalDisc.cs | 33 +--- .../Discs/OpticalDiscFactory.cs | 10 +- RedBookPlayer.Common/Hardware/Player.cs | 21 ++- RedBookPlayer.Common/PlayerViewModel.cs | 17 +- RedBookPlayer.GUI/PlayerView.xaml.cs | 16 +- RedBookPlayer.GUI/Settings.cs | 9 +- RedBookPlayer.GUI/SettingsWindow.xaml | 4 +- 8 files changed, 182 insertions(+), 82 deletions(-) diff --git a/RedBookPlayer.Common/Discs/CompactDisc.cs b/RedBookPlayer.Common/Discs/CompactDisc.cs index eb51c0c..8f123ef 100644 --- a/RedBookPlayer.Common/Discs/CompactDisc.cs +++ b/RedBookPlayer.Common/Discs/CompactDisc.cs @@ -34,16 +34,30 @@ namespace RedBookPlayer.Common.Discs 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; + // 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 = _image.Tracks[cachedTrackNumber]; + Track track = GetTrack(cachedTrackNumber); // Set track flags from subchannel data, if possible SetTrackFlags(track); @@ -78,7 +92,7 @@ namespace RedBookPlayer.Common.Discs return; // Cache the current track for easy access - Track track = _image.Tracks[CurrentTrackNumber]; + Track track = GetTrack(CurrentTrackNumber); // Ensure that the value is valid, wrapping around if necessary ushort fixedValue = value; @@ -106,18 +120,18 @@ namespace RedBookPlayer.Common.Discs return; // Cache the current track for easy access - Track track = _image.Tracks[CurrentTrackNumber]; + Track track = GetTrack(CurrentTrackNumber); this.RaiseAndSetIfChanged(ref _currentSector, value); - if((CurrentTrackNumber < _image.Tracks.Count - 1 && CurrentSector >= _image.Tracks[CurrentTrackNumber + 1].TrackStartSector) + if((CurrentTrackNumber < _image.Tracks.Count - 1 && CurrentSector >= GetTrack(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; + CurrentTrackNumber = (int)trackData.TrackSequence; break; } } @@ -136,6 +150,9 @@ namespace RedBookPlayer.Common.Discs } } + /// + public override int BytesPerSector => GetTrack(CurrentTrackNumber).TrackRawBytesPerSector; + /// /// Represents the 4CH flag /// @@ -206,6 +223,11 @@ namespace RedBookPlayer.Common.Discs /// private bool _loadDataTracks = false; + /// + /// Indicate if hidden tracks should be loaded + /// + private bool _loadHiddenTracks = false; + /// /// Current disc table of contents /// @@ -217,10 +239,12 @@ namespace RedBookPlayer.Common.Discs /// Constructor /// /// Generate a TOC if the disc is missing one + /// Load hidden tracks for playback /// Load data tracks for playback - public CompactDisc(bool generateMissingToc, bool loadDataTracks) + public CompactDisc(bool generateMissingToc, bool loadHiddenTracks, bool loadDataTracks) { _generateMissingToc = generateMissingToc; + _loadHiddenTracks = loadHiddenTracks; _loadDataTracks = loadDataTracks; } @@ -257,47 +281,101 @@ namespace RedBookPlayer.Common.Discs #region Seeking + /// + public override void NextTrack() + { + if(_image == null) + return; + + CurrentTrackNumber++; + LoadTrack(CurrentTrackNumber); + } + + /// + public override void PreviousTrack() + { + if(_image == null) + return; + + CurrentTrackNumber--; + LoadTrack(CurrentTrackNumber); + } + /// public override bool NextIndex(bool changeTrack) { if(_image == null) return false; - if(CurrentTrackIndex + 1 > _image.Tracks[CurrentTrackNumber].Indexes.Keys.Max()) + // Cache the current track for easy access + Track track = GetTrack(CurrentTrackNumber); + + // 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)_image.Tracks[CurrentTrackNumber].Indexes.Values.Min(); + 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)_image.Tracks[CurrentTrackNumber].Indexes[++CurrentTrackIndex]; + CurrentSector = (ulong)track.Indexes[++CurrentTrackIndex]; } return false; } /// - public override bool PreviousIndex(bool changeTrack, bool playHiddenTrack) + public override bool PreviousIndex(bool changeTrack) { if(_image == null) return false; - if(CurrentTrackIndex - 1 < _image.Tracks[CurrentTrackNumber].Indexes.Keys.Min()) + // Cache the current track for easy access + Track track = GetTrack(CurrentTrackNumber); + + // If the index is less than the lowest index, change tracks if needed + if(CurrentTrackIndex - 1 < track.Indexes.Keys.Min()) { if(changeTrack) { - PreviousTrack(playHiddenTrack); - CurrentSector = (ulong)_image.Tracks[CurrentTrackNumber].Indexes.Values.Max(); + 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)_image.Tracks[CurrentTrackNumber].Indexes[--CurrentTrackIndex]; + CurrentSector = (ulong)track.Indexes[--CurrentTrackIndex]; } return false; @@ -310,7 +388,7 @@ namespace RedBookPlayer.Common.Discs /// public override void LoadFirstTrack() { - CurrentTrackNumber = 0; + CurrentTrackNumber = 1; LoadTrack(CurrentTrackNumber); } @@ -320,6 +398,12 @@ namespace RedBookPlayer.Common.Discs /// True to enable loading data tracks, false otherwise public void SetLoadDataTracks(bool load) => _loadDataTracks = load; + /// + /// Set the value for loading hidden tracks + /// + /// True to enable loading hidden tracks, false otherwise + public void SetLoadHiddenTracks(bool load) => _loadHiddenTracks = load; + /// public override void SetTotalIndexes() { @@ -330,17 +414,37 @@ namespace RedBookPlayer.Common.Discs } /// - protected override void LoadTrack(int track) + protected override void LoadTrack(int trackNumber) { if(_image == null) return; - if(track < 0 || track >= _image.Tracks.Count) + // If the track number is invalid, just return + if(trackNumber < _image.Tracks.Min(t => t.TrackSequence) || trackNumber > _image.Tracks.Max(t => t.TrackSequence)) 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]); + // Cache the current track for easy access + Track track = GetTrack(trackNumber); + + // Select the first index that has a sector offset greater than or equal to 0 + CurrentSector = (ulong)(track?.Indexes.OrderBy(kvp => kvp.Key).First(kvp => kvp.Value >= 0).Value ?? 0); + } + + /// + /// Get the track with the given sequence value, if possible + /// + /// Track number to retrieve + /// Track object for the requested sequence, null on error + private Track GetTrack(int trackNumber) + { + try + { + return _image.Tracks.FirstOrDefault(t => t.TrackSequence == trackNumber); + } + catch + { + return null; + } } /// diff --git a/RedBookPlayer.Common/Discs/OpticalDisc.cs b/RedBookPlayer.Common/Discs/OpticalDisc.cs index 985b741..b522574 100644 --- a/RedBookPlayer.Common/Discs/OpticalDisc.cs +++ b/RedBookPlayer.Common/Discs/OpticalDisc.cs @@ -40,7 +40,7 @@ namespace RedBookPlayer.Common.Discs /// /// Number of bytes per sector for the current track /// - public int BytesPerSector => _image.Tracks[CurrentTrackNumber].TrackRawBytesPerSector; + public abstract int BytesPerSector { get; } /// /// Represents the track type @@ -97,36 +97,12 @@ namespace RedBookPlayer.Common.Discs /// /// Try to move to the next track, wrapping around if necessary /// - public void NextTrack() - { - if(_image == null) - return; - - CurrentTrackNumber++; - LoadTrack(CurrentTrackNumber); - } + public abstract void NextTrack(); /// /// Try to move to the previous track, wrapping around if necessary /// - /// True to play the hidden track, if it exists - public void PreviousTrack(bool playHiddenTrack) - { - if(_image == null) - return; - - if(CurrentSector < (ulong)_image.Tracks[CurrentTrackNumber].Indexes[1] + 75) - { - if(playHiddenTrack && CurrentTrackNumber == 0 && CurrentSector >= 75) - CurrentSector = 0; - else - CurrentTrackNumber--; - } - else - CurrentTrackNumber--; - - LoadTrack(CurrentTrackNumber); - } + public abstract void PreviousTrack(); /// /// Try to move to the next track index @@ -139,9 +115,8 @@ namespace RedBookPlayer.Common.Discs /// Try to move to the previous track index /// /// True if index changes can trigger a track change, false otherwise - /// True to play the hidden track, if it exists /// True if the track was changed, false otherwise - public abstract bool PreviousIndex(bool changeTrack, bool playHiddenTrack); + public abstract bool PreviousIndex(bool changeTrack); #endregion diff --git a/RedBookPlayer.Common/Discs/OpticalDiscFactory.cs b/RedBookPlayer.Common/Discs/OpticalDiscFactory.cs index 416cbe4..52ad9d8 100644 --- a/RedBookPlayer.Common/Discs/OpticalDiscFactory.cs +++ b/RedBookPlayer.Common/Discs/OpticalDiscFactory.cs @@ -13,10 +13,11 @@ namespace RedBookPlayer.Common.Discs /// /// Path to load the image from /// Generate a TOC if the disc is missing one [CompactDisc only] + /// Load hidden tracks for playback [CompactDisc only] /// Load data tracks for playback [CompactDisc only] /// True if the image should be playable immediately, false otherwise /// Instantiated OpticalDisc, if possible - public static OpticalDisc GenerateFromPath(string path, bool generateMissingToc, bool loadDataTracks, bool autoPlay) + public static OpticalDisc GenerateFromPath(string path, bool generateMissingToc, bool loadHiddenTracks, bool loadDataTracks, bool autoPlay) { try { @@ -32,7 +33,7 @@ namespace RedBookPlayer.Common.Discs image.Open(filter); // Generate and instantiate the disc - return GenerateFromImage(image, generateMissingToc, loadDataTracks, autoPlay); + return GenerateFromImage(image, generateMissingToc, loadHiddenTracks, loadDataTracks, autoPlay); } catch { @@ -46,10 +47,11 @@ namespace RedBookPlayer.Common.Discs /// /// IOpticalMediaImage to create from /// Generate a TOC if the disc is missing one [CompactDisc only] + /// Load hidden tracks for playback [CompactDisc only] /// Load data tracks for playback [CompactDisc only] /// True if the image should be playable immediately, false otherwise /// Instantiated OpticalDisc, if possible - public static OpticalDisc GenerateFromImage(IOpticalMediaImage image, bool generateMissingToc, bool loadDataTracks, bool autoPlay) + public static OpticalDisc GenerateFromImage(IOpticalMediaImage image, bool generateMissingToc, bool loadHiddenTracks, bool loadDataTracks, bool autoPlay) { // If the image is not usable, we don't do anything if(!IsUsableImage(image)) @@ -63,7 +65,7 @@ namespace RedBookPlayer.Common.Discs { case "Compact Disc": case "GD": - opticalDisc = new CompactDisc(generateMissingToc, loadDataTracks); + opticalDisc = new CompactDisc(generateMissingToc, loadHiddenTracks, loadDataTracks); break; default: opticalDisc = null; diff --git a/RedBookPlayer.Common/Hardware/Player.cs b/RedBookPlayer.Common/Hardware/Player.cs index 6a4d20e..0fdc4d4 100644 --- a/RedBookPlayer.Common/Hardware/Player.cs +++ b/RedBookPlayer.Common/Hardware/Player.cs @@ -188,10 +188,11 @@ namespace RedBookPlayer.Common.Hardware /// /// Path to the disc image /// Generate a TOC if the disc is missing one [CompactDisc only] + /// Load hidden tracks for playback [CompactDisc only] /// Load data tracks for playback [CompactDisc only] /// True if playback should begin immediately, false otherwise /// Default volume between 0 and 100 to use when starting playback - public Player(string path, bool generateMissingToc, bool loadDataTracks, bool autoPlay, int defaultVolume) + public Player(string path, bool generateMissingToc, bool loadHiddenTracks, bool loadDataTracks, bool autoPlay, int defaultVolume) { // Set the internal state for initialization Initialized = false; @@ -199,7 +200,7 @@ namespace RedBookPlayer.Common.Hardware _soundOutput.SetDeEmphasis(false); // Initalize the disc - _opticalDisc = OpticalDiscFactory.GenerateFromPath(path, generateMissingToc, loadDataTracks, autoPlay); + _opticalDisc = OpticalDiscFactory.GenerateFromPath(path, generateMissingToc, loadHiddenTracks, loadDataTracks, autoPlay); if(_opticalDisc == null || !_opticalDisc.Initialized) return; @@ -295,8 +296,7 @@ namespace RedBookPlayer.Common.Hardware /// /// Move to the previous playable track /// - /// True to play the hidden track, if it exists - public void PreviousTrack(bool playHiddenTrack) + public void PreviousTrack() { if(_opticalDisc == null || !_opticalDisc.Initialized) return; @@ -304,7 +304,7 @@ namespace RedBookPlayer.Common.Hardware bool? wasPlaying = Playing; if(wasPlaying == true) Pause(); - _opticalDisc.PreviousTrack(playHiddenTrack); + _opticalDisc.PreviousTrack(); if(_opticalDisc is CompactDisc compactDisc) _soundOutput.SetDeEmphasis(compactDisc.TrackHasEmphasis); @@ -334,8 +334,7 @@ namespace RedBookPlayer.Common.Hardware /// Move to the previous index /// /// True if index changes can trigger a track change, false otherwise - /// True to play the hidden track, if it exists - public void PreviousIndex(bool changeTrack, bool playHiddenTrack) + public void PreviousIndex(bool changeTrack) { if(_opticalDisc == null || !_opticalDisc.Initialized) return; @@ -343,7 +342,7 @@ namespace RedBookPlayer.Common.Hardware bool? wasPlaying = Playing; if(wasPlaying == true) Pause(); - _opticalDisc.PreviousIndex(changeTrack, playHiddenTrack); + _opticalDisc.PreviousIndex(changeTrack); if(_opticalDisc is CompactDisc compactDisc) _soundOutput.SetDeEmphasis(compactDisc.TrackHasEmphasis); @@ -389,6 +388,12 @@ namespace RedBookPlayer.Common.Hardware /// True to enable loading data tracks, false otherwise public void SetLoadDataTracks(bool load) => (_opticalDisc as CompactDisc)?.SetLoadDataTracks(load); + /// + /// Set the value for loading hidden tracks [CompactDisc only] + /// + /// True to enable loading hidden tracks, false otherwise + public void SetLoadHiddenTracks(bool load) => (_opticalDisc as CompactDisc)?.SetLoadHiddenTracks(load); + /// /// Set the value for the volume /// diff --git a/RedBookPlayer.Common/PlayerViewModel.cs b/RedBookPlayer.Common/PlayerViewModel.cs index 635b4a4..04c62a2 100644 --- a/RedBookPlayer.Common/PlayerViewModel.cs +++ b/RedBookPlayer.Common/PlayerViewModel.cs @@ -186,16 +186,17 @@ namespace RedBookPlayer.Common /// /// Path to the disc image /// Generate a TOC if the disc is missing one [CompactDisc only] + /// Load hidden tracks for playback [CompactDisc only] /// Load data tracks for playback [CompactDisc only] /// True if playback should begin immediately, false otherwise /// Default volume between 0 and 100 to use when starting playback - public void Init(string path, bool generateMissingToc, bool loadDataTracks, bool autoPlay, int defaultVolume) + public void Init(string path, bool generateMissingToc, bool loadHiddenTracks, bool loadDataTracks, bool autoPlay, int defaultVolume) { // Stop current playback, if necessary if(Playing != null) Playing = null; // Create and attempt to initialize new Player - _player = new Player(path, generateMissingToc, loadDataTracks, autoPlay, defaultVolume); + _player = new Player(path, generateMissingToc, loadHiddenTracks, loadDataTracks, autoPlay, defaultVolume); if(Initialized) { _player.PropertyChanged += PlayerStateChanged; @@ -228,8 +229,7 @@ namespace RedBookPlayer.Common /// /// Move to the previous playable track /// - /// True to play the hidden track, if it exists - public void PreviousTrack(bool playHiddenTrack) => _player?.PreviousTrack(playHiddenTrack); + public void PreviousTrack() => _player?.PreviousTrack(); /// /// Move to the next index @@ -241,8 +241,7 @@ namespace RedBookPlayer.Common /// Move to the previous index /// /// True if index changes can trigger a track change, false otherwise - /// True to play the hidden track, if it exists - public void PreviousIndex(bool changeTrack, bool playHiddenTrack) => _player?.PreviousIndex(changeTrack, playHiddenTrack); + public void PreviousIndex(bool changeTrack) => _player?.PreviousIndex(changeTrack); /// /// Fast-forward playback by 75 sectors, if possible @@ -270,6 +269,12 @@ namespace RedBookPlayer.Common /// True to enable loading data tracks, false otherwise public void SetLoadDataTracks(bool load) => _player?.SetLoadDataTracks(load); + /// + /// Set the value for loading hidden tracks [CompactDisc only] + /// + /// True to enable loading hidden tracks, false otherwise + public void SetLoadHiddenTracks(bool load) => _player?.SetLoadHiddenTracks(load); + /// /// Set the value for the volume /// diff --git a/RedBookPlayer.GUI/PlayerView.xaml.cs b/RedBookPlayer.GUI/PlayerView.xaml.cs index ef7d3b9..16f43cc 100644 --- a/RedBookPlayer.GUI/PlayerView.xaml.cs +++ b/RedBookPlayer.GUI/PlayerView.xaml.cs @@ -55,7 +55,7 @@ namespace RedBookPlayer.GUI { return await Dispatcher.UIThread.InvokeAsync(() => { - PlayerViewModel.Init(path, App.Settings.GenerateMissingTOC, App.Settings.PlayDataTracks, App.Settings.AutoPlay, App.Settings.Volume); + PlayerViewModel.Init(path, App.Settings.GenerateMissingTOC, App.Settings.PlayHiddenTracks, App.Settings.PlayDataTracks, App.Settings.AutoPlay, App.Settings.Volume); if (PlayerViewModel.Initialized) MainWindow.Instance.Title = "RedBookPlayer - " + path.Split('/').Last().Split('\\').Last(); @@ -66,7 +66,11 @@ namespace RedBookPlayer.GUI /// /// Update the view model with new settings /// - public void UpdateViewModel() => PlayerViewModel.SetLoadDataTracks(App.Settings.PlayDataTracks); + public void UpdateViewModel() + { + PlayerViewModel.SetLoadDataTracks(App.Settings.PlayDataTracks); + PlayerViewModel.SetLoadHiddenTracks(App.Settings.PlayHiddenTracks); + } /// /// Generate the digit string to be interpreted by the frontend @@ -83,7 +87,7 @@ namespace RedBookPlayer.GUI int[] numbers = new int[] { - PlayerViewModel.CurrentTrackNumber + 1, + PlayerViewModel.CurrentTrackNumber, PlayerViewModel.CurrentTrackIndex, (int)(sectorTime / (75 * 60)), @@ -138,7 +142,7 @@ namespace RedBookPlayer.GUI ulong sectorTime = PlayerViewModel.CurrentSector; if(PlayerViewModel.SectionStartSector != 0) sectorTime -= PlayerViewModel.SectionStartSector; - else + else if (PlayerViewModel.CurrentTrackNumber > 0) sectorTime += PlayerViewModel.TimeOffset; return sectorTime; @@ -261,11 +265,11 @@ namespace RedBookPlayer.GUI public void NextTrackButton_Click(object sender, RoutedEventArgs e) => PlayerViewModel.NextTrack(); - public void PreviousTrackButton_Click(object sender, RoutedEventArgs e) => PlayerViewModel.PreviousTrack(App.Settings.AllowSkipHiddenTrack); + public void PreviousTrackButton_Click(object sender, RoutedEventArgs e) => PlayerViewModel.PreviousTrack(); public void NextIndexButton_Click(object sender, RoutedEventArgs e) => PlayerViewModel.NextIndex(App.Settings.IndexButtonChangeTrack); - public void PreviousIndexButton_Click(object sender, RoutedEventArgs e) => PlayerViewModel.PreviousIndex(App.Settings.IndexButtonChangeTrack, App.Settings.AllowSkipHiddenTrack); + public void PreviousIndexButton_Click(object sender, RoutedEventArgs e) => PlayerViewModel.PreviousIndex(App.Settings.IndexButtonChangeTrack); public void FastForwardButton_Click(object sender, RoutedEventArgs e) => PlayerViewModel.FastForward(); diff --git a/RedBookPlayer.GUI/Settings.cs b/RedBookPlayer.GUI/Settings.cs index 2329e7c..855b2c8 100644 --- a/RedBookPlayer.GUI/Settings.cs +++ b/RedBookPlayer.GUI/Settings.cs @@ -21,9 +21,14 @@ namespace RedBookPlayer.GUI public bool IndexButtonChangeTrack { get; set; } = false; /// - /// Indicates if the index 0 of track 1 is treated like a hidden track + /// Indicates if hidden tracks should be played /// - public bool AllowSkipHiddenTrack { get; set; } = false; + /// + /// Hidden tracks can be one of the following: + /// - TrackSequence == 0 + /// - Larget pregap of track 1 (> 150 sectors) + /// + public bool PlayHiddenTracks { get; set; } = false; /// /// Indicates if data tracks should be played like old, non-compliant players diff --git a/RedBookPlayer.GUI/SettingsWindow.xaml b/RedBookPlayer.GUI/SettingsWindow.xaml index ba416ba..158ba9e 100644 --- a/RedBookPlayer.GUI/SettingsWindow.xaml +++ b/RedBookPlayer.GUI/SettingsWindow.xaml @@ -17,8 +17,8 @@ Index navigation can change track - - Treat index 0 of track 1 as track 0 (hidden track) + + Play hidden tracks