3
.gitmodules
vendored
@@ -3,4 +3,5 @@
|
|||||||
url = https://github.com/aaru-dps/Aaru.git
|
url = https://github.com/aaru-dps/Aaru.git
|
||||||
[submodule "cscore"]
|
[submodule "cscore"]
|
||||||
path = cscore
|
path = cscore
|
||||||
url = https://github.com/filoe/cscore.git
|
url = https://github.com/sk-zk/cscore.git
|
||||||
|
branch = netstandard
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ using Avalonia.Controls;
|
|||||||
using Avalonia.Controls.ApplicationLifetimes;
|
using Avalonia.Controls.ApplicationLifetimes;
|
||||||
using Avalonia.Markup.Xaml;
|
using Avalonia.Markup.Xaml;
|
||||||
using RedBookPlayer.GUI;
|
using RedBookPlayer.GUI;
|
||||||
|
using RedBookPlayer.GUI.Views;
|
||||||
|
|
||||||
namespace RedBookPlayer
|
namespace RedBookPlayer
|
||||||
{
|
{
|
||||||
|
Before Width: | Height: | Size: 357 B After Width: | Height: | Size: 357 B |
|
Before Width: | Height: | Size: 658 B After Width: | Height: | Size: 658 B |
|
Before Width: | Height: | Size: 414 B After Width: | Height: | Size: 414 B |
|
Before Width: | Height: | Size: 604 B After Width: | Height: | Size: 604 B |
|
Before Width: | Height: | Size: 595 B After Width: | Height: | Size: 595 B |
|
Before Width: | Height: | Size: 550 B After Width: | Height: | Size: 550 B |
|
Before Width: | Height: | Size: 601 B After Width: | Height: | Size: 601 B |
|
Before Width: | Height: | Size: 656 B After Width: | Height: | Size: 656 B |
|
Before Width: | Height: | Size: 547 B After Width: | Height: | Size: 547 B |
|
Before Width: | Height: | Size: 693 B After Width: | Height: | Size: 693 B |
|
Before Width: | Height: | Size: 648 B After Width: | Height: | Size: 648 B |
|
Before Width: | Height: | Size: 407 B After Width: | Height: | Size: 407 B |
@@ -5,7 +5,7 @@ using System.Runtime.InteropServices;
|
|||||||
using Avalonia;
|
using Avalonia;
|
||||||
using Avalonia.Logging.Serilog;
|
using Avalonia.Logging.Serilog;
|
||||||
|
|
||||||
namespace RedBookPlayer
|
namespace RedBookPlayer.GUI
|
||||||
{
|
{
|
||||||
internal class Program
|
internal class Program
|
||||||
{
|
{
|
||||||
@@ -2,8 +2,8 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>WinExe</OutputType>
|
<OutputType>WinExe</OutputType>
|
||||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
|
||||||
<RuntimeIdentifiers>win-x64;linux-x64</RuntimeIdentifiers>
|
<RuntimeIdentifiers>win-x64;linux-x64</RuntimeIdentifiers>
|
||||||
|
<RuntimeIdentifier>linux-x64</RuntimeIdentifier>
|
||||||
<DebugType>embedded</DebugType>
|
<DebugType>embedded</DebugType>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<PropertyGroup Condition="'$(RuntimeIdentifier)|$(Configuration)' == 'win-x64|Debug'">
|
<PropertyGroup Condition="'$(RuntimeIdentifier)|$(Configuration)' == 'win-x64|Debug'">
|
||||||
@@ -21,17 +21,9 @@
|
|||||||
<PackageReference Include="Avalonia" Version="0.9.12" />
|
<PackageReference Include="Avalonia" Version="0.9.12" />
|
||||||
<PackageReference Include="Avalonia.Desktop" Version="0.9.12" />
|
<PackageReference Include="Avalonia.Desktop" Version="0.9.12" />
|
||||||
<PackageReference Include="Avalonia.ReactiveUI" Version="0.9.12" />
|
<PackageReference Include="Avalonia.ReactiveUI" Version="0.9.12" />
|
||||||
<PackageReference Include="NWaves" Version="0.9.4" />
|
|
||||||
<PackageReference Include="SkiaSharp.NativeAssets.Linux" Version="1.68.3" />
|
|
||||||
<PackageReference Include="Avalonia.Skia.Linux.Natives" Version="1.68.0.2" ExcludeAssets="all" />
|
|
||||||
<PackageReference Include="SkiaSharp" Version="1.68.3" />
|
|
||||||
<PackageReference Include="OpenTK.NetStandard" Version="1.0.5.12" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\Aaru\Aaru.Images\Aaru.Images.csproj" />
|
<ProjectReference Include="..\RedBookPlayer.Models\RedBookPlayer.Models.csproj" />
|
||||||
<ProjectReference Include="..\Aaru\Aaru.CommonTypes\Aaru.CommonTypes.csproj" />
|
|
||||||
<ProjectReference Include="..\Aaru\Aaru.Decoders\Aaru.Decoders.csproj" />
|
|
||||||
<ProjectReference Include="..\cscore\CSCore\CSCore.csproj" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<AvaloniaResource Include="Assets\*" />
|
<AvaloniaResource Include="Assets\*" />
|
||||||
@@ -2,9 +2,9 @@ using System;
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using Avalonia.Input;
|
using Avalonia.Input;
|
||||||
using RedBookPlayer.GUI;
|
using RedBookPlayer.GUI.Views;
|
||||||
|
|
||||||
namespace RedBookPlayer
|
namespace RedBookPlayer.GUI
|
||||||
{
|
{
|
||||||
public class Settings
|
public class Settings
|
||||||
{
|
{
|
||||||
@@ -21,9 +21,14 @@ namespace RedBookPlayer
|
|||||||
public bool IndexButtonChangeTrack { get; set; } = false;
|
public bool IndexButtonChangeTrack { get; set; } = false;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Indicates if the index 0 of track 1 is treated like a hidden track
|
/// Indicates if hidden tracks should be played
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool AllowSkipHiddenTrack { get; set; } = false;
|
/// <remarks>
|
||||||
|
/// Hidden tracks can be one of the following:
|
||||||
|
/// - TrackSequence == 0
|
||||||
|
/// - Larget pregap of track 1 (> 150 sectors)
|
||||||
|
/// </remarks>
|
||||||
|
public bool PlayHiddenTracks { get; set; } = false;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Indicates if data tracks should be played like old, non-compliant players
|
/// Indicates if data tracks should be played like old, non-compliant players
|
||||||
672
RedBookPlayer.GUI/ViewModels/PlayerViewModel.cs
Normal file
@@ -0,0 +1,672 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reactive;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Media.Imaging;
|
||||||
|
using Avalonia.Platform;
|
||||||
|
using Avalonia.Threading;
|
||||||
|
using ReactiveUI;
|
||||||
|
using RedBookPlayer.GUI.Views;
|
||||||
|
using RedBookPlayer.Models.Hardware;
|
||||||
|
|
||||||
|
namespace RedBookPlayer.GUI.ViewModels
|
||||||
|
{
|
||||||
|
public class PlayerViewModel : ReactiveObject
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Player representing the internal state
|
||||||
|
/// </summary>
|
||||||
|
private Player _player;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Set of images representing the digits for the UI
|
||||||
|
/// </summary>
|
||||||
|
private Image[] _digits;
|
||||||
|
|
||||||
|
#region Player Passthrough
|
||||||
|
|
||||||
|
#region OpticalDisc Passthrough
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Current track number
|
||||||
|
/// </summary>
|
||||||
|
public int CurrentTrackNumber
|
||||||
|
{
|
||||||
|
get => _currentTrackNumber;
|
||||||
|
private set => this.RaiseAndSetIfChanged(ref _currentTrackNumber, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Current track index
|
||||||
|
/// </summary>
|
||||||
|
public ushort CurrentTrackIndex
|
||||||
|
{
|
||||||
|
get => _currentTrackIndex;
|
||||||
|
private set => this.RaiseAndSetIfChanged(ref _currentTrackIndex, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Current sector number
|
||||||
|
/// </summary>
|
||||||
|
public ulong CurrentSector
|
||||||
|
{
|
||||||
|
get => _currentSector;
|
||||||
|
private set => this.RaiseAndSetIfChanged(ref _currentSector, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the sector starting the section
|
||||||
|
/// </summary>
|
||||||
|
public ulong SectionStartSector
|
||||||
|
{
|
||||||
|
get => _sectionStartSector;
|
||||||
|
private set => this.RaiseAndSetIfChanged(ref _sectionStartSector, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents if the disc has a hidden track
|
||||||
|
/// </summary>
|
||||||
|
public bool HiddenTrack
|
||||||
|
{
|
||||||
|
get => _hasHiddenTrack;
|
||||||
|
private set => this.RaiseAndSetIfChanged(ref _hasHiddenTrack, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the 4CH flag [CompactDisc only]
|
||||||
|
/// </summary>
|
||||||
|
public bool QuadChannel
|
||||||
|
{
|
||||||
|
get => _quadChannel;
|
||||||
|
private set => this.RaiseAndSetIfChanged(ref _quadChannel, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the DATA flag [CompactDisc only]
|
||||||
|
/// </summary>
|
||||||
|
public bool IsDataTrack
|
||||||
|
{
|
||||||
|
get => _isDataTrack;
|
||||||
|
private set => this.RaiseAndSetIfChanged(ref _isDataTrack, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the DCP flag [CompactDisc only]
|
||||||
|
/// </summary>
|
||||||
|
public bool CopyAllowed
|
||||||
|
{
|
||||||
|
get => _copyAllowed;
|
||||||
|
private set => this.RaiseAndSetIfChanged(ref _copyAllowed, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the PRE flag [CompactDisc only]
|
||||||
|
/// </summary>
|
||||||
|
public bool TrackHasEmphasis
|
||||||
|
{
|
||||||
|
get => _trackHasEmphasis;
|
||||||
|
private set => this.RaiseAndSetIfChanged(ref _trackHasEmphasis, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the total tracks on the disc
|
||||||
|
/// </summary>
|
||||||
|
public int TotalTracks => _player.TotalTracks;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the total indices on the disc
|
||||||
|
/// </summary>
|
||||||
|
public int TotalIndexes => _player.TotalIndexes;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Total sectors in the image
|
||||||
|
/// </summary>
|
||||||
|
public ulong TotalSectors => _player.TotalSectors;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the time adjustment offset for the disc
|
||||||
|
/// </summary>
|
||||||
|
public ulong TimeOffset => _player.TimeOffset;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the total playing time for the disc
|
||||||
|
/// </summary>
|
||||||
|
public ulong TotalTime => _player.TotalTime;
|
||||||
|
|
||||||
|
private int _currentTrackNumber;
|
||||||
|
private ushort _currentTrackIndex;
|
||||||
|
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
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Indicate if the model is ready to be used
|
||||||
|
/// </summary>
|
||||||
|
public bool Initialized => _player?.Initialized ?? false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Indicate if the output is playing
|
||||||
|
/// </summary>
|
||||||
|
public bool? Playing
|
||||||
|
{
|
||||||
|
get => _playing;
|
||||||
|
private set => this.RaiseAndSetIfChanged(ref _playing, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Indicates if de-emphasis should be applied
|
||||||
|
/// </summary>
|
||||||
|
public bool ApplyDeEmphasis
|
||||||
|
{
|
||||||
|
get => _applyDeEmphasis;
|
||||||
|
private set => this.RaiseAndSetIfChanged(ref _applyDeEmphasis, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Current playback volume
|
||||||
|
/// </summary>
|
||||||
|
public int Volume
|
||||||
|
{
|
||||||
|
get => _volume;
|
||||||
|
private set => this.RaiseAndSetIfChanged(ref _volume, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool? _playing;
|
||||||
|
private bool _applyDeEmphasis;
|
||||||
|
private int _volume;
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Commands
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Command for loading a disc
|
||||||
|
/// </summary>
|
||||||
|
public ReactiveCommand<Unit, Unit> LoadCommand { get; }
|
||||||
|
|
||||||
|
#region Playback
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Command for beginning playback
|
||||||
|
/// </summary>
|
||||||
|
public ReactiveCommand<Unit, Unit> PlayCommand { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Command for pausing playback
|
||||||
|
/// </summary>
|
||||||
|
public ReactiveCommand<Unit, Unit> PauseCommand { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Command for pausing playback
|
||||||
|
/// </summary>
|
||||||
|
public ReactiveCommand<Unit, Unit> TogglePlayPauseCommand { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Command for stopping playback
|
||||||
|
/// </summary>
|
||||||
|
public ReactiveCommand<Unit, Unit> StopCommand { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Command for moving to the next track
|
||||||
|
/// </summary>
|
||||||
|
public ReactiveCommand<Unit, Unit> NextTrackCommand { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Command for moving to the previous track
|
||||||
|
/// </summary>
|
||||||
|
public ReactiveCommand<Unit, Unit> PreviousTrackCommand { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Command for moving to the next index
|
||||||
|
/// </summary>
|
||||||
|
public ReactiveCommand<Unit, Unit> NextIndexCommand { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Command for moving to the previous index
|
||||||
|
/// </summary>
|
||||||
|
public ReactiveCommand<Unit, Unit> PreviousIndexCommand { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Command for fast forwarding
|
||||||
|
/// </summary>
|
||||||
|
public ReactiveCommand<Unit, Unit> FastForwardCommand { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Command for rewinding
|
||||||
|
/// </summary>
|
||||||
|
public ReactiveCommand<Unit, Unit> RewindCommand { get; }
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Volume
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Command for incrementing volume
|
||||||
|
/// </summary>
|
||||||
|
public ReactiveCommand<Unit, Unit> VolumeUpCommand { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Command for decrementing volume
|
||||||
|
/// </summary>
|
||||||
|
public ReactiveCommand<Unit, Unit> VolumeDownCommand { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Command for toggling mute
|
||||||
|
/// </summary>
|
||||||
|
public ReactiveCommand<Unit, Unit> ToggleMuteCommand { get; }
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Emphasis
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Command for enabling de-emphasis
|
||||||
|
/// </summary>
|
||||||
|
public ReactiveCommand<Unit, Unit> EnableDeEmphasisCommand { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Command for disabling de-emphasis
|
||||||
|
/// </summary>
|
||||||
|
public ReactiveCommand<Unit, Unit> DisableDeEmphasisCommand { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Command for toggling de-emphasis
|
||||||
|
/// </summary>
|
||||||
|
public ReactiveCommand<Unit, Unit> ToggleDeEmphasisCommand { get; }
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Constructor
|
||||||
|
/// </summary>
|
||||||
|
public PlayerViewModel()
|
||||||
|
{
|
||||||
|
LoadCommand = ReactiveCommand.Create(ExecuteLoad);
|
||||||
|
|
||||||
|
PlayCommand = ReactiveCommand.Create(ExecutePlay);
|
||||||
|
PauseCommand = ReactiveCommand.Create(ExecutePause);
|
||||||
|
TogglePlayPauseCommand = ReactiveCommand.Create(ExecuteTogglePlayPause);
|
||||||
|
StopCommand = ReactiveCommand.Create(ExecuteStop);
|
||||||
|
NextTrackCommand = ReactiveCommand.Create(ExecuteNextTrack);
|
||||||
|
PreviousTrackCommand = ReactiveCommand.Create(ExecutePreviousTrack);
|
||||||
|
NextIndexCommand = ReactiveCommand.Create(ExecuteNextIndex);
|
||||||
|
PreviousIndexCommand = ReactiveCommand.Create(ExecutePreviousIndex);
|
||||||
|
FastForwardCommand = ReactiveCommand.Create(ExecuteFastForward);
|
||||||
|
RewindCommand = ReactiveCommand.Create(ExecuteRewind);
|
||||||
|
|
||||||
|
VolumeUpCommand = ReactiveCommand.Create(ExecuteVolumeUp);
|
||||||
|
VolumeDownCommand = ReactiveCommand.Create(ExecuteVolumeDown);
|
||||||
|
ToggleMuteCommand = ReactiveCommand.Create(ExecuteToggleMute);
|
||||||
|
|
||||||
|
EnableDeEmphasisCommand = ReactiveCommand.Create(ExecuteEnableDeEmphasis);
|
||||||
|
DisableDeEmphasisCommand = ReactiveCommand.Create(ExecuteDisableDeEmphasis);
|
||||||
|
ToggleDeEmphasisCommand = ReactiveCommand.Create(ExecuteToggleDeEmphasis);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initialize the view model with a given image path
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="path">Path to the disc image</param>
|
||||||
|
/// <param name="generateMissingToc">Generate a TOC if the disc is missing one [CompactDisc only]</param>
|
||||||
|
/// <param name="loadHiddenTracks">Load hidden tracks for playback [CompactDisc only]</param>
|
||||||
|
/// <param name="loadDataTracks">Load data tracks for playback [CompactDisc only]</param>
|
||||||
|
/// <param name="autoPlay">True if playback should begin immediately, false otherwise</param>
|
||||||
|
/// <param name="defaultVolume">Default volume between 0 and 100 to use when starting playback</param>
|
||||||
|
public void Init(string path, bool generateMissingToc, bool loadHiddenTracks, bool loadDataTracks, bool autoPlay, int defaultVolume)
|
||||||
|
{
|
||||||
|
// Stop current playback, if necessary
|
||||||
|
if(Playing != null) ExecuteStop();
|
||||||
|
|
||||||
|
// Create and attempt to initialize new Player
|
||||||
|
_player = new Player(path, generateMissingToc, loadHiddenTracks, loadDataTracks, autoPlay, defaultVolume);
|
||||||
|
if(Initialized)
|
||||||
|
{
|
||||||
|
_player.PropertyChanged += PlayerStateChanged;
|
||||||
|
PlayerStateChanged(this, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Playback
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Begin playback
|
||||||
|
/// </summary>
|
||||||
|
public void ExecutePlay() => _player?.Play();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Pause current playback
|
||||||
|
/// </summary>
|
||||||
|
public void ExecutePause() => _player?.Pause();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Toggle playback
|
||||||
|
/// </summary>
|
||||||
|
public void ExecuteTogglePlayPause() => _player?.TogglePlayback();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Stop current playback
|
||||||
|
/// </summary>
|
||||||
|
public void ExecuteStop() => _player?.Stop();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Move to the next playable track
|
||||||
|
/// </summary>
|
||||||
|
public void ExecuteNextTrack() => _player?.NextTrack();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Move to the previous playable track
|
||||||
|
/// </summary>
|
||||||
|
public void ExecutePreviousTrack() => _player?.PreviousTrack();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Move to the next index
|
||||||
|
/// </summary>
|
||||||
|
public void ExecuteNextIndex() => _player?.NextIndex(App.Settings.IndexButtonChangeTrack);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Move to the previous index
|
||||||
|
/// </summary>
|
||||||
|
public void ExecutePreviousIndex() => _player?.PreviousIndex(App.Settings.IndexButtonChangeTrack);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fast-forward playback by 75 sectors, if possible
|
||||||
|
/// </summary>
|
||||||
|
public void ExecuteFastForward() => _player?.FastForward();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Rewind playback by 75 sectors, if possible
|
||||||
|
/// </summary>
|
||||||
|
public void ExecuteRewind() => _player?.Rewind();
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Volume
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Increment the volume value
|
||||||
|
/// </summary>
|
||||||
|
public void ExecuteVolumeUp() => _player?.VolumeUp();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Decrement the volume value
|
||||||
|
/// </summary>
|
||||||
|
public void ExecuteVolumeDown() => _player?.VolumeDown();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Set the value for the volume
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="volume">New volume value</param>
|
||||||
|
public void ExecuteSetVolume(int volume) => _player?.SetVolume(volume);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Temporarily mute playback
|
||||||
|
/// </summary>
|
||||||
|
public void ExecuteToggleMute() => _player?.ToggleMute();
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Emphasis
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Enable de-emphasis
|
||||||
|
/// </summary>
|
||||||
|
public void ExecuteEnableDeEmphasis() => _player?.EnableDeEmphasis();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Disable de-emphasis
|
||||||
|
/// </summary>
|
||||||
|
public void ExecuteDisableDeEmphasis() => _player?.DisableDeEmphasis();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Toggle de-emphasis
|
||||||
|
/// </summary>
|
||||||
|
public void ExecuteToggleDeEmphasis() => _player?.ToggleDeEmphasis();
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Helpers
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Load a disc image from a selection box
|
||||||
|
/// </summary>
|
||||||
|
public async void ExecuteLoad()
|
||||||
|
{
|
||||||
|
string path = await GetPath();
|
||||||
|
if(path == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
await LoadImage(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initialize the displayed digits array
|
||||||
|
/// </summary>
|
||||||
|
public void InitializeDigits()
|
||||||
|
{
|
||||||
|
PlayerView playerView = MainWindow.Instance.ContentControl.Content as PlayerView;
|
||||||
|
|
||||||
|
_digits = new Image[]
|
||||||
|
{
|
||||||
|
playerView.FindControl<Image>("TrackDigit1"),
|
||||||
|
playerView.FindControl<Image>("TrackDigit2"),
|
||||||
|
|
||||||
|
playerView.FindControl<Image>("IndexDigit1"),
|
||||||
|
playerView.FindControl<Image>("IndexDigit2"),
|
||||||
|
|
||||||
|
playerView.FindControl<Image>("TimeDigit1"),
|
||||||
|
playerView.FindControl<Image>("TimeDigit2"),
|
||||||
|
playerView.FindControl<Image>("TimeDigit3"),
|
||||||
|
playerView.FindControl<Image>("TimeDigit4"),
|
||||||
|
playerView.FindControl<Image>("TimeDigit5"),
|
||||||
|
playerView.FindControl<Image>("TimeDigit6"),
|
||||||
|
|
||||||
|
playerView.FindControl<Image>("TotalTracksDigit1"),
|
||||||
|
playerView.FindControl<Image>("TotalTracksDigit2"),
|
||||||
|
|
||||||
|
playerView.FindControl<Image>("TotalIndexesDigit1"),
|
||||||
|
playerView.FindControl<Image>("TotalIndexesDigit2"),
|
||||||
|
|
||||||
|
playerView.FindControl<Image>("TotalTimeDigit1"),
|
||||||
|
playerView.FindControl<Image>("TotalTimeDigit2"),
|
||||||
|
playerView.FindControl<Image>("TotalTimeDigit3"),
|
||||||
|
playerView.FindControl<Image>("TotalTimeDigit4"),
|
||||||
|
playerView.FindControl<Image>("TotalTimeDigit5"),
|
||||||
|
playerView.FindControl<Image>("TotalTimeDigit6"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Load an image from the path
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="path">Path to the image to load</param>
|
||||||
|
public async Task<bool> LoadImage(string path)
|
||||||
|
{
|
||||||
|
return await Dispatcher.UIThread.InvokeAsync(() =>
|
||||||
|
{
|
||||||
|
Init(path, App.Settings.GenerateMissingTOC, App.Settings.PlayHiddenTracks, App.Settings.PlayDataTracks, App.Settings.AutoPlay, App.Settings.Volume);
|
||||||
|
if(Initialized)
|
||||||
|
MainWindow.Instance.Title = "RedBookPlayer - " + path.Split('/').Last().Split('\\').Last();
|
||||||
|
|
||||||
|
return Initialized;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Set the value for loading data tracks [CompactDisc only]
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="load">True to enable loading data tracks, false otherwise</param>
|
||||||
|
public void SetLoadDataTracks(bool load) => _player?.SetLoadDataTracks(load);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Set the value for loading hidden tracks [CompactDisc only]
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="load">True to enable loading hidden tracks, false otherwise</param>
|
||||||
|
public void SetLoadHiddenTracks(bool load) => _player?.SetLoadHiddenTracks(load);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Generate the digit string to be interpreted by the frontend
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>String representing the digits for the frontend</returns>
|
||||||
|
private string GenerateDigitString()
|
||||||
|
{
|
||||||
|
// If the disc isn't initialized, return all '-' characters
|
||||||
|
if(Initialized != true)
|
||||||
|
return string.Empty.PadLeft(20, '-');
|
||||||
|
|
||||||
|
int usableTrackNumber = CurrentTrackNumber;
|
||||||
|
if(usableTrackNumber < 0)
|
||||||
|
usableTrackNumber = 0;
|
||||||
|
else if(usableTrackNumber > 99)
|
||||||
|
usableTrackNumber = 99;
|
||||||
|
|
||||||
|
// Otherwise, take the current time into account
|
||||||
|
ulong sectorTime = GetCurrentSectorTime();
|
||||||
|
|
||||||
|
int[] numbers = new int[]
|
||||||
|
{
|
||||||
|
usableTrackNumber,
|
||||||
|
CurrentTrackIndex,
|
||||||
|
|
||||||
|
(int)(sectorTime / (75 * 60)),
|
||||||
|
(int)(sectorTime / 75 % 60),
|
||||||
|
(int)(sectorTime % 75),
|
||||||
|
|
||||||
|
TotalTracks,
|
||||||
|
TotalIndexes,
|
||||||
|
|
||||||
|
(int)(TotalTime / (75 * 60)),
|
||||||
|
(int)(TotalTime / 75 % 60),
|
||||||
|
(int)(TotalTime % 75),
|
||||||
|
};
|
||||||
|
|
||||||
|
return string.Join("", numbers.Select(i => i.ToString().PadLeft(2, '0').Substring(0, 2)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Load the png image for a given character based on the theme
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="character">Character to load the image for</param>
|
||||||
|
/// <returns>Bitmap representing the loaded image</returns>
|
||||||
|
private Bitmap GetBitmap(char character)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if(App.Settings.SelectedTheme == "default")
|
||||||
|
{
|
||||||
|
IAssetLoader assets = AvaloniaLocator.Current.GetService<IAssetLoader>();
|
||||||
|
|
||||||
|
return new Bitmap(assets.Open(new Uri($"avares://RedBookPlayer/Assets/{character}.png")));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
string themeDirectory = $"{Directory.GetCurrentDirectory()}/themes/{App.Settings.SelectedTheme}";
|
||||||
|
using FileStream stream = File.Open($"{themeDirectory}/{character}.png", FileMode.Open);
|
||||||
|
return new Bitmap(stream);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get current sector time, accounting for offsets
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>ulong representing the current sector time</returns>
|
||||||
|
private ulong GetCurrentSectorTime()
|
||||||
|
{
|
||||||
|
ulong sectorTime = CurrentSector;
|
||||||
|
if(SectionStartSector != 0)
|
||||||
|
sectorTime -= SectionStartSector;
|
||||||
|
else if(CurrentTrackNumber > 0)
|
||||||
|
sectorTime += TimeOffset;
|
||||||
|
|
||||||
|
return sectorTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Generate a path selection dialog box
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>User-selected path, if possible</returns>
|
||||||
|
private async Task<string> GetPath()
|
||||||
|
{
|
||||||
|
return await Dispatcher.UIThread.InvokeAsync(async () =>
|
||||||
|
{
|
||||||
|
var dialog = new OpenFileDialog { AllowMultiple = false };
|
||||||
|
List<string> knownExtensions = new Aaru.DiscImages.AaruFormat().KnownExtensions.ToList();
|
||||||
|
dialog.Filters.Add(new FileDialogFilter()
|
||||||
|
{
|
||||||
|
Name = "Aaru Image Format (*" + string.Join(", *", knownExtensions) + ")",
|
||||||
|
Extensions = knownExtensions.ConvertAll(e => e.TrimStart('.'))
|
||||||
|
});
|
||||||
|
|
||||||
|
return (await dialog.ShowAsync(MainWindow.Instance))?.FirstOrDefault();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Update the view-model from the Player
|
||||||
|
/// </summary>
|
||||||
|
private void PlayerStateChanged(object sender, PropertyChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if(_player?.Initialized != true)
|
||||||
|
return;
|
||||||
|
|
||||||
|
CurrentTrackNumber = _player.CurrentTrackNumber;
|
||||||
|
CurrentTrackIndex = _player.CurrentTrackIndex;
|
||||||
|
CurrentSector = _player.CurrentSector;
|
||||||
|
SectionStartSector = _player.SectionStartSector;
|
||||||
|
|
||||||
|
HiddenTrack = _player.HiddenTrack;
|
||||||
|
|
||||||
|
QuadChannel = _player.QuadChannel;
|
||||||
|
IsDataTrack = _player.IsDataTrack;
|
||||||
|
CopyAllowed = _player.CopyAllowed;
|
||||||
|
TrackHasEmphasis = _player.TrackHasEmphasis;
|
||||||
|
|
||||||
|
Playing = _player.Playing;
|
||||||
|
ApplyDeEmphasis = _player.ApplyDeEmphasis;
|
||||||
|
Volume = _player.Volume;
|
||||||
|
|
||||||
|
UpdateDigits();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Update UI
|
||||||
|
/// </summary>
|
||||||
|
private void UpdateDigits()
|
||||||
|
{
|
||||||
|
Dispatcher.UIThread.Post(() =>
|
||||||
|
{
|
||||||
|
string digitString = GenerateDigitString() ?? string.Empty.PadLeft(20, '-');
|
||||||
|
for(int i = 0; i < _digits.Length; i++)
|
||||||
|
{
|
||||||
|
Bitmap digitImage = GetBitmap(digitString[i]);
|
||||||
|
if(_digits[i] != null && digitImage != null)
|
||||||
|
_digits[i].Source = digitImage;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<Window xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
<Window xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d"
|
||||||
x:Class="RedBookPlayer.GUI.MainWindow" Title="RedBookPlayer" SizeToContent="WidthAndHeight"
|
x:Class="RedBookPlayer.GUI.Views.MainWindow" Title="RedBookPlayer" SizeToContent="WidthAndHeight"
|
||||||
DragDrop.AllowDrop="True">
|
DragDrop.AllowDrop="True">
|
||||||
<ContentControl Name="Content" />
|
<ContentControl Name="Content" />
|
||||||
</Window>
|
</Window>
|
||||||
@@ -5,8 +5,9 @@ using System.Xml;
|
|||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Input;
|
using Avalonia.Input;
|
||||||
using Avalonia.Markup.Xaml;
|
using Avalonia.Markup.Xaml;
|
||||||
|
using RedBookPlayer.GUI.ViewModels;
|
||||||
|
|
||||||
namespace RedBookPlayer.GUI
|
namespace RedBookPlayer.GUI.Views
|
||||||
{
|
{
|
||||||
public class MainWindow : Window
|
public class MainWindow : Window
|
||||||
{
|
{
|
||||||
@@ -30,10 +31,13 @@ namespace RedBookPlayer.GUI
|
|||||||
if(string.IsNullOrWhiteSpace(theme))
|
if(string.IsNullOrWhiteSpace(theme))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
// If we already have a view, cache the view model
|
||||||
|
PlayerViewModel pvm = ((PlayerView)Instance.ContentControl.Content).PlayerViewModel;
|
||||||
|
|
||||||
// If the theme name is "default", we assume the internal theme is used
|
// If the theme name is "default", we assume the internal theme is used
|
||||||
if(theme.Equals("default", StringComparison.CurrentCultureIgnoreCase))
|
if(theme.Equals("default", StringComparison.CurrentCultureIgnoreCase))
|
||||||
{
|
{
|
||||||
Instance.ContentControl.Content = new PlayerView();
|
Instance.ContentControl.Content = new PlayerView(pvm);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -50,17 +54,19 @@ namespace RedBookPlayer.GUI
|
|||||||
{
|
{
|
||||||
string xaml = File.ReadAllText(xamlPath);
|
string xaml = File.ReadAllText(xamlPath);
|
||||||
xaml = xaml.Replace("Source=\"", $"Source=\"file://{themeDirectory}/");
|
xaml = xaml.Replace("Source=\"", $"Source=\"file://{themeDirectory}/");
|
||||||
Instance.ContentControl.Content = new PlayerView(xaml);
|
Instance.ContentControl.Content = new PlayerView(xaml, pvm);
|
||||||
}
|
}
|
||||||
catch(XmlException ex)
|
catch(XmlException ex)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"Error: invalid theme XAML ({ex.Message}), reverting to default");
|
Console.WriteLine($"Error: invalid theme XAML ({ex.Message}), reverting to default");
|
||||||
Instance.ContentControl.Content = new PlayerView();
|
Instance.ContentControl.Content = new PlayerView(pvm);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Instance.Width = ((PlayerView)Instance.ContentControl.Content).Width;
|
Instance.Width = ((PlayerView)Instance.ContentControl.Content).Width;
|
||||||
Instance.Height = ((PlayerView)Instance.ContentControl.Content).Height;
|
Instance.Height = ((PlayerView)Instance.ContentControl.Content).Height;
|
||||||
|
|
||||||
|
pvm.InitializeDigits();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -78,6 +84,8 @@ namespace RedBookPlayer.GUI
|
|||||||
|
|
||||||
ContentControl.Content = new PlayerView();
|
ContentControl.Content = new PlayerView();
|
||||||
|
|
||||||
|
((PlayerView)ContentControl.Content).PlayerViewModel.InitializeDigits();
|
||||||
|
|
||||||
CanResize = false;
|
CanResize = false;
|
||||||
|
|
||||||
KeyDown += OnKeyDown;
|
KeyDown += OnKeyDown;
|
||||||
@@ -90,7 +98,7 @@ namespace RedBookPlayer.GUI
|
|||||||
|
|
||||||
Closing += (e, f) =>
|
Closing += (e, f) =>
|
||||||
{
|
{
|
||||||
((PlayerView)ContentControl.Content).StopButton_Click(this, null);
|
((PlayerView)ContentControl.Content).PlayerViewModel.ExecuteStop();
|
||||||
};
|
};
|
||||||
|
|
||||||
AddHandler(DragDrop.DropEvent, MainWindow_Drop);
|
AddHandler(DragDrop.DropEvent, MainWindow_Drop);
|
||||||
@@ -107,7 +115,7 @@ namespace RedBookPlayer.GUI
|
|||||||
IEnumerable<string> fileNames = e.Data.GetFileNames();
|
IEnumerable<string> fileNames = e.Data.GetFileNames();
|
||||||
foreach(string filename in fileNames)
|
foreach(string filename in fileNames)
|
||||||
{
|
{
|
||||||
bool loaded = await playerView.LoadImage(filename);
|
bool loaded = await playerView?.PlayerViewModel?.LoadImage(filename);
|
||||||
if(loaded)
|
if(loaded)
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -121,61 +129,62 @@ namespace RedBookPlayer.GUI
|
|||||||
if(e.Key == App.Settings.OpenSettingsKey)
|
if(e.Key == App.Settings.OpenSettingsKey)
|
||||||
{
|
{
|
||||||
settingsWindow = new SettingsWindow(App.Settings);
|
settingsWindow = new SettingsWindow(App.Settings);
|
||||||
|
settingsWindow.Closed += OnSettingsClosed;
|
||||||
settingsWindow.Show();
|
settingsWindow.Show();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load image
|
// Load image
|
||||||
else if (e.Key == App.Settings.LoadImageKey)
|
else if (e.Key == App.Settings.LoadImageKey)
|
||||||
{
|
{
|
||||||
playerView?.LoadButton_Click(this, null);
|
playerView?.PlayerViewModel?.ExecuteLoad();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Toggle playback
|
// Toggle playback
|
||||||
else if(e.Key == App.Settings.TogglePlaybackKey || e.Key == Key.MediaPlayPause)
|
else if(e.Key == App.Settings.TogglePlaybackKey || e.Key == Key.MediaPlayPause)
|
||||||
{
|
{
|
||||||
playerView?.PlayPauseButton_Click(this, null);
|
playerView?.PlayerViewModel?.ExecuteTogglePlayPause();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stop playback
|
// Stop playback
|
||||||
else if(e.Key == App.Settings.StopPlaybackKey || e.Key == Key.MediaStop)
|
else if(e.Key == App.Settings.StopPlaybackKey || e.Key == Key.MediaStop)
|
||||||
{
|
{
|
||||||
playerView?.StopButton_Click(this, null);
|
playerView?.PlayerViewModel?.ExecuteStop();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Next Track
|
// Next Track
|
||||||
else if(e.Key == App.Settings.NextTrackKey || e.Key == Key.MediaNextTrack)
|
else if(e.Key == App.Settings.NextTrackKey || e.Key == Key.MediaNextTrack)
|
||||||
{
|
{
|
||||||
playerView?.NextTrackButton_Click(this, null);
|
playerView?.PlayerViewModel?.ExecuteNextTrack();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Previous Track
|
// Previous Track
|
||||||
else if(e.Key == App.Settings.PreviousTrackKey || e.Key == Key.MediaPreviousTrack)
|
else if(e.Key == App.Settings.PreviousTrackKey || e.Key == Key.MediaPreviousTrack)
|
||||||
{
|
{
|
||||||
playerView?.PreviousTrackButton_Click(this, null);
|
playerView?.PlayerViewModel?.ExecutePreviousTrack();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Next Index
|
// Next Index
|
||||||
else if(e.Key == App.Settings.NextIndexKey)
|
else if(e.Key == App.Settings.NextIndexKey)
|
||||||
{
|
{
|
||||||
playerView?.NextIndexButton_Click(this, null);
|
playerView?.PlayerViewModel?.ExecuteNextIndex();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Previous Index
|
// Previous Index
|
||||||
else if(e.Key == App.Settings.PreviousIndexKey)
|
else if(e.Key == App.Settings.PreviousIndexKey)
|
||||||
{
|
{
|
||||||
playerView?.PreviousIndexButton_Click(this, null);
|
playerView?.PlayerViewModel?.ExecutePreviousIndex();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fast Foward
|
// Fast Foward
|
||||||
else if(e.Key == App.Settings.FastForwardPlaybackKey)
|
else if(e.Key == App.Settings.FastForwardPlaybackKey)
|
||||||
{
|
{
|
||||||
playerView?.FastForwardButton_Click(this, null);
|
playerView?.PlayerViewModel?.ExecuteFastForward();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rewind
|
// Rewind
|
||||||
else if(e.Key == App.Settings.RewindPlaybackKey)
|
else if(e.Key == App.Settings.RewindPlaybackKey)
|
||||||
{
|
{
|
||||||
playerView?.RewindButton_Click(this, null);
|
playerView?.PlayerViewModel?.ExecuteRewind();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Volume Up
|
// Volume Up
|
||||||
@@ -188,7 +197,7 @@ namespace RedBookPlayer.GUI
|
|||||||
increment *= 5;
|
increment *= 5;
|
||||||
|
|
||||||
if(playerView?.PlayerViewModel?.Volume != null)
|
if(playerView?.PlayerViewModel?.Volume != null)
|
||||||
playerView.PlayerViewModel.Volume += increment;
|
playerView.PlayerViewModel.ExecuteSetVolume(playerView.PlayerViewModel.Volume + increment);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Volume Down
|
// Volume Down
|
||||||
@@ -201,22 +210,28 @@ namespace RedBookPlayer.GUI
|
|||||||
decrement *= 5;
|
decrement *= 5;
|
||||||
|
|
||||||
if (playerView?.PlayerViewModel?.Volume != null)
|
if (playerView?.PlayerViewModel?.Volume != null)
|
||||||
playerView.PlayerViewModel.Volume -= decrement;
|
playerView.PlayerViewModel.ExecuteSetVolume(playerView.PlayerViewModel.Volume - decrement);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mute Toggle
|
// Mute Toggle
|
||||||
else if(e.Key == App.Settings.ToggleMuteKey || e.Key == Key.VolumeMute)
|
else if(e.Key == App.Settings.ToggleMuteKey || e.Key == Key.VolumeMute)
|
||||||
{
|
{
|
||||||
playerView?.MuteToggleButton_Click(this, null);
|
playerView?.PlayerViewModel?.ExecuteToggleMute();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Emphasis Toggle
|
// Emphasis Toggle
|
||||||
else if(e.Key == App.Settings.ToggleDeEmphasisKey)
|
else if(e.Key == App.Settings.ToggleDeEmphasisKey)
|
||||||
{
|
{
|
||||||
playerView?.EnableDisableDeEmphasisButton_Click(this, null);
|
playerView?.PlayerViewModel?.ExecuteToggleDeEmphasis();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void OnSettingsClosed(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
PlayerView playerView = ContentControl.Content as PlayerView;
|
||||||
|
playerView?.UpdateViewModel();
|
||||||
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,21 +1,21 @@
|
|||||||
<UserControl xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
<UserControl xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d"
|
||||||
x:Class="RedBookPlayer.GUI.PlayerView" Width="900" Height="400">
|
x:Class="RedBookPlayer.GUI.Views.PlayerView" Width="900" Height="400">
|
||||||
<StackPanel Margin="16" VerticalAlignment="Center">
|
<StackPanel Margin="16" VerticalAlignment="Center">
|
||||||
<Button Click="LoadButton_Click" Margin="32,0,32,16">Load</Button>
|
<Button Command="{Binding LoadCommand}" Margin="32,0,32,16">Load</Button>
|
||||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,0,0,16">
|
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,0,0,16">
|
||||||
<Button Click="PlayButton_Click" Width="100" Margin="0,0,16,0">Play</Button>
|
<Button Command="{Binding PlayCommand}" Width="100" Margin="0,0,16,0">Play</Button>
|
||||||
<Button Click="PauseButton_Click" Width="100" Margin="0,0,16,0">Pause</Button>
|
<Button Command="{Binding PauseCommand}" Width="100" Margin="0,0,16,0">Pause</Button>
|
||||||
<Button Click="StopButton_Click" Width="100" Margin="0,0,16,0">Stop</Button>
|
<Button Command="{Binding StopCommand}" Width="100" Margin="0,0,16,0">Stop</Button>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,0,0,16">
|
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,0,0,16">
|
||||||
<Button Click="PreviousTrackButton_Click" Width="100" Margin="0,0,16,0">Previous Track</Button>
|
<Button Command="{Binding PreviousTrackCommand}" Width="100" Margin="0,0,16,0">Previous Track</Button>
|
||||||
<Button Click="NextTrackButton_Click" Width="100" Margin="0,0,16,0">Next Track</Button>
|
<Button Command="{Binding NextTrackCommand}" Width="100" Margin="0,0,16,0">Next Track</Button>
|
||||||
<Button Click="PreviousIndexButton_Click" Width="100" Margin="0,0,16,0">Previous Index</Button>
|
<Button Command="{Binding PreviousIndexCommand}" Width="100" Margin="0,0,16,0">Previous Index</Button>
|
||||||
<Button Click="NextIndexButton_Click" Width="100" Margin="0,0,16,0">Next Index</Button>
|
<Button Command="{Binding NextIndexCommand}" Width="100" Margin="0,0,16,0">Next Index</Button>
|
||||||
<RepeatButton Click="RewindButton_Click" Width="100" Margin="0,0,16,0">Rewind</RepeatButton>
|
<RepeatButton Command="{Binding RewindCommand}" Width="100" Margin="0,0,16,0">Rewind</RepeatButton>
|
||||||
<RepeatButton Click="FastForwardButton_Click" Width="100">Fast Forward</RepeatButton>
|
<RepeatButton Command="{Binding FastForwardCommand}" Width="100">Fast Forward</RepeatButton>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,0,0,16">
|
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,0,0,16">
|
||||||
<StackPanel Margin="0,0,32,0">
|
<StackPanel Margin="0,0,32,0">
|
||||||
@@ -76,11 +76,11 @@
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,0,0,16">
|
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,0,0,16">
|
||||||
<Button Click="EnableDeEmphasisButton_Click" IsVisible="{Binding !ApplyDeEmphasis}" Width="200"
|
<Button Command="{Binding EnableDeEmphasisCommand}" IsVisible="{Binding !ApplyDeEmphasis}" Width="200"
|
||||||
Margin="0,0,16,0">
|
Margin="0,0,16,0">
|
||||||
Enable De-Emphasis
|
Enable De-Emphasis
|
||||||
</Button>
|
</Button>
|
||||||
<Button Click="DisableDeEmphasisButton_Click" IsVisible="{Binding ApplyDeEmphasis}" Width="200"
|
<Button Command="{Binding DisableDeEmphasisCommand}" IsVisible="{Binding ApplyDeEmphasis}" Width="200"
|
||||||
Margin="0,0,16,0">
|
Margin="0,0,16,0">
|
||||||
Disable De-Emphasis
|
Disable De-Emphasis
|
||||||
</Button>
|
</Button>
|
||||||
@@ -98,6 +98,7 @@
|
|||||||
<TextBlock Margin="0,0,16,0" IsVisible="{Binding QuadChannel}">4CH</TextBlock>
|
<TextBlock Margin="0,0,16,0" IsVisible="{Binding QuadChannel}">4CH</TextBlock>
|
||||||
<TextBlock Margin="0,0,16,0" Foreground="LightGray" IsVisible="{Binding !HiddenTrack}">HIDDEN</TextBlock>
|
<TextBlock Margin="0,0,16,0" Foreground="LightGray" IsVisible="{Binding !HiddenTrack}">HIDDEN</TextBlock>
|
||||||
<TextBlock Margin="0,0,16,0" IsVisible="{Binding HiddenTrack}">HIDDEN</TextBlock>
|
<TextBlock Margin="0,0,16,0" IsVisible="{Binding HiddenTrack}">HIDDEN</TextBlock>
|
||||||
|
<TextBlock Margin="0,0,16,0" Text="{Binding Volume}"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</UserControl>
|
</UserControl>
|
||||||
73
RedBookPlayer.GUI/Views/PlayerView.xaml.cs
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Markup.Xaml;
|
||||||
|
using RedBookPlayer.GUI.ViewModels;
|
||||||
|
|
||||||
|
namespace RedBookPlayer.GUI.Views
|
||||||
|
{
|
||||||
|
public class PlayerView : UserControl
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Read-only access to the view model
|
||||||
|
/// </summary>
|
||||||
|
public PlayerViewModel PlayerViewModel => DataContext as PlayerViewModel;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initialize the UI based on the default theme
|
||||||
|
/// </summary>
|
||||||
|
public PlayerView() : this(null, null) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initialize the UI based on the default theme with an existing view model
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="xaml">XAML data representing the theme, null for default</param>
|
||||||
|
/// <param name="playerViewModel">Existing PlayerViewModel to load in instead of creating a new one</param>
|
||||||
|
public PlayerView(PlayerViewModel playerViewModel) : this(null, playerViewModel) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initialize the UI based on the currently selected theme
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="xaml">XAML data representing the theme, null for default</param>
|
||||||
|
/// <param name="playerViewModel">Existing PlayerViewModel to load in instead of creating a new one</param>
|
||||||
|
public PlayerView(string xaml, PlayerViewModel playerViewModel)
|
||||||
|
{
|
||||||
|
LoadTheme(xaml);
|
||||||
|
|
||||||
|
if(playerViewModel != null)
|
||||||
|
DataContext = playerViewModel;
|
||||||
|
else
|
||||||
|
DataContext = new PlayerViewModel();
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Helpers
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Update the view model with new settings
|
||||||
|
/// </summary>
|
||||||
|
public void UpdateViewModel()
|
||||||
|
{
|
||||||
|
PlayerViewModel.SetLoadDataTracks(App.Settings.PlayDataTracks);
|
||||||
|
PlayerViewModel.SetLoadHiddenTracks(App.Settings.PlayHiddenTracks);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Load the theme from a XAML, if possible
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="xaml">XAML data representing the theme, null for default</param>
|
||||||
|
private void LoadTheme(string xaml)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if(xaml != null)
|
||||||
|
new AvaloniaXamlLoader().Load(xaml, null, this);
|
||||||
|
else
|
||||||
|
AvaloniaXamlLoader.Load(this);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
AvaloniaXamlLoader.Load(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<Window xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
<Window xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" d:DesignWidth="800"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" d:DesignWidth="800"
|
||||||
d:DesignHeight="450" x:Class="RedBookPlayer.GUI.SettingsWindow" Title="Settings" SizeToContent="WidthAndHeight">
|
d:DesignHeight="450" x:Class="RedBookPlayer.GUI.Views.SettingsWindow" Title="Settings" SizeToContent="WidthAndHeight">
|
||||||
<StackPanel>
|
<StackPanel>
|
||||||
<TabControl>
|
<TabControl>
|
||||||
<TabItem Header="UI Settings">
|
<TabItem Header="UI Settings">
|
||||||
@@ -17,8 +17,8 @@
|
|||||||
<TextBlock VerticalAlignment="Center">Index navigation can change track</TextBlock>
|
<TextBlock VerticalAlignment="Center">Index navigation can change track</TextBlock>
|
||||||
</WrapPanel>
|
</WrapPanel>
|
||||||
<WrapPanel Margin="0,0,0,16">
|
<WrapPanel Margin="0,0,0,16">
|
||||||
<CheckBox IsChecked="{Binding AllowSkipHiddenTrack}" Margin="0,0,8,0" />
|
<CheckBox IsChecked="{Binding PlayHiddenTracks}" Margin="0,0,8,0" />
|
||||||
<TextBlock VerticalAlignment="Center">Treat index 0 of track 1 as track 0 (hidden track)</TextBlock>
|
<TextBlock VerticalAlignment="Center">Play hidden tracks</TextBlock>
|
||||||
</WrapPanel>
|
</WrapPanel>
|
||||||
<WrapPanel Margin="0,0,0,16">
|
<WrapPanel Margin="0,0,0,16">
|
||||||
<CheckBox IsChecked="{Binding PlayDataTracks}" Margin="0,0,8,0"/>
|
<CheckBox IsChecked="{Binding PlayDataTracks}" Margin="0,0,8,0"/>
|
||||||
@@ -6,7 +6,7 @@ using Avalonia.Input;
|
|||||||
using Avalonia.Interactivity;
|
using Avalonia.Interactivity;
|
||||||
using Avalonia.Markup.Xaml;
|
using Avalonia.Markup.Xaml;
|
||||||
|
|
||||||
namespace RedBookPlayer.GUI
|
namespace RedBookPlayer.GUI.Views
|
||||||
{
|
{
|
||||||
public class SettingsWindow : Window
|
public class SettingsWindow : Window
|
||||||
{
|
{
|
||||||
@@ -6,11 +6,12 @@ using Aaru.CommonTypes.Interfaces;
|
|||||||
using Aaru.CommonTypes.Structs;
|
using Aaru.CommonTypes.Structs;
|
||||||
using Aaru.Decoders.CD;
|
using Aaru.Decoders.CD;
|
||||||
using Aaru.Helpers;
|
using Aaru.Helpers;
|
||||||
|
using ReactiveUI;
|
||||||
using static Aaru.Decoders.CD.FullTOC;
|
using static Aaru.Decoders.CD.FullTOC;
|
||||||
|
|
||||||
namespace RedBookPlayer.Discs
|
namespace RedBookPlayer.Models.Discs
|
||||||
{
|
{
|
||||||
public class CompactDisc : OpticalDisc
|
public class CompactDisc : OpticalDiscBase, IReactiveObject
|
||||||
{
|
{
|
||||||
#region Public Fields
|
#region Public Fields
|
||||||
|
|
||||||
@@ -24,35 +25,52 @@ namespace RedBookPlayer.Discs
|
|||||||
if(_image == null)
|
if(_image == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
// Data tracks only and flag disabled means we can't do anything
|
||||||
|
if(_image.Tracks.All(t => t.TrackType != TrackType.Audio) && !LoadDataTracks)
|
||||||
|
return;
|
||||||
|
|
||||||
// Cache the value and the current track number
|
// Cache the value and the current track number
|
||||||
int cachedValue = value;
|
int cachedValue = value;
|
||||||
int cachedTrackNumber = _currentTrackNumber;
|
int cachedTrackNumber;
|
||||||
|
|
||||||
// Check if we're incrementing or decrementing the track
|
// Check if we're incrementing or decrementing the track
|
||||||
bool increment = cachedValue >= _currentTrackNumber;
|
bool increment = cachedValue >= _currentTrackNumber;
|
||||||
|
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
// Ensure that the value is valid, wrapping around if necessary
|
// If we're over the last track, wrap around
|
||||||
if(cachedValue >= _image.Tracks.Count)
|
if(cachedValue > _image.Tracks.Max(t => t.TrackSequence))
|
||||||
cachedValue = 0;
|
{
|
||||||
else if(cachedValue < 0)
|
cachedValue = (int)_image.Tracks.Min(t => t.TrackSequence);
|
||||||
cachedValue = _image.Tracks.Count - 1;
|
if(cachedValue == 0 && !LoadHiddenTracks)
|
||||||
|
cachedValue++;
|
||||||
|
}
|
||||||
|
|
||||||
_currentTrackNumber = 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
|
// Cache the current track for easy access
|
||||||
Track track = _image.Tracks[_currentTrackNumber];
|
Track track = GetTrack(cachedTrackNumber);
|
||||||
|
if(track == null)
|
||||||
|
return;
|
||||||
|
|
||||||
// Set track flags from subchannel data, if possible
|
// Set track flags from subchannel data, if possible
|
||||||
SetTrackFlags(track);
|
SetTrackFlags(track);
|
||||||
|
|
||||||
TotalIndexes = track.Indexes.Keys.Max();
|
|
||||||
CurrentTrackIndex = track.Indexes.Keys.Min();
|
|
||||||
|
|
||||||
// If the track is playable, just return
|
// If the track is playable, just return
|
||||||
if(TrackType == TrackType.Audio || App.Settings.PlayDataTracks)
|
if(TrackType == TrackType.Audio || LoadDataTracks)
|
||||||
return;
|
break;
|
||||||
|
|
||||||
// If we're not playing the track, skip
|
// If we're not playing the track, skip
|
||||||
if(increment)
|
if(increment)
|
||||||
@@ -60,7 +78,28 @@ namespace RedBookPlayer.Discs
|
|||||||
else
|
else
|
||||||
cachedValue--;
|
cachedValue--;
|
||||||
}
|
}
|
||||||
while(cachedValue != cachedTrackNumber);
|
while(cachedValue != _currentTrackNumber);
|
||||||
|
|
||||||
|
// If we looped around, ensure it reloads
|
||||||
|
if(cachedValue == _currentTrackNumber)
|
||||||
|
{
|
||||||
|
this.RaiseAndSetIfChanged(ref _currentTrackNumber, -1);
|
||||||
|
|
||||||
|
Track track = GetTrack(cachedValue);
|
||||||
|
if(track == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
SetTrackFlags(track);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.RaiseAndSetIfChanged(ref _currentTrackNumber, cachedValue);
|
||||||
|
|
||||||
|
Track cachedTrack = GetTrack(cachedValue);
|
||||||
|
if(cachedTrack == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
TotalIndexes = cachedTrack.Indexes.Keys.Max();
|
||||||
|
CurrentTrackIndex = cachedTrack.Indexes.Keys.Min();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,15 +114,18 @@ namespace RedBookPlayer.Discs
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
// Cache the current track for easy access
|
// Cache the current track for easy access
|
||||||
Track track = _image.Tracks[CurrentTrackNumber];
|
Track track = GetTrack(CurrentTrackNumber);
|
||||||
|
if(track == null)
|
||||||
|
return;
|
||||||
|
|
||||||
// Ensure that the value is valid, wrapping around if necessary
|
// Ensure that the value is valid, wrapping around if necessary
|
||||||
|
ushort fixedValue = value;
|
||||||
if(value > track.Indexes.Keys.Max())
|
if(value > track.Indexes.Keys.Max())
|
||||||
_currentTrackIndex = track.Indexes.Keys.Min();
|
fixedValue = track.Indexes.Keys.Min();
|
||||||
else if(value < track.Indexes.Keys.Min())
|
else if(value < track.Indexes.Keys.Min())
|
||||||
_currentTrackIndex = track.Indexes.Keys.Max();
|
fixedValue = track.Indexes.Keys.Max();
|
||||||
else
|
|
||||||
_currentTrackIndex = value;
|
this.RaiseAndSetIfChanged(ref _currentTrackIndex, fixedValue);
|
||||||
|
|
||||||
// Set new index-specific data
|
// Set new index-specific data
|
||||||
SectionStartSector = (ulong)track.Indexes[CurrentTrackIndex];
|
SectionStartSector = (ulong)track.Indexes[CurrentTrackIndex];
|
||||||
@@ -95,25 +137,27 @@ namespace RedBookPlayer.Discs
|
|||||||
public override ulong CurrentSector
|
public override ulong CurrentSector
|
||||||
{
|
{
|
||||||
get => _currentSector;
|
get => _currentSector;
|
||||||
set
|
protected set
|
||||||
{
|
{
|
||||||
// Unset image means we can't do anything
|
// Unset image means we can't do anything
|
||||||
if(_image == null)
|
if(_image == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// Cache the current track for easy access
|
// Cache the current track for easy access
|
||||||
Track track = _image.Tracks[CurrentTrackNumber];
|
Track track = GetTrack(CurrentTrackNumber);
|
||||||
|
if(track == null)
|
||||||
|
return;
|
||||||
|
|
||||||
_currentSector = value;
|
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 ?? 0))
|
||||||
|| (CurrentTrackNumber > 0 && CurrentSector < track.TrackStartSector))
|
|| (CurrentTrackNumber > 0 && CurrentSector < track.TrackStartSector))
|
||||||
{
|
{
|
||||||
foreach(Track trackData in _image.Tracks.ToArray().Reverse())
|
foreach(Track trackData in _image.Tracks.ToArray().Reverse())
|
||||||
{
|
{
|
||||||
if(CurrentSector >= trackData.TrackStartSector)
|
if(CurrentSector >= trackData.TrackStartSector)
|
||||||
{
|
{
|
||||||
CurrentTrackNumber = (int)trackData.TrackSequence - 1;
|
CurrentTrackNumber = (int)trackData.TrackSequence;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -132,25 +176,59 @@ namespace RedBookPlayer.Discs
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override int BytesPerSector => GetTrack(CurrentTrackNumber)?.TrackRawBytesPerSector ?? 0;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents the 4CH flag
|
/// Represents the 4CH flag
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool QuadChannel { get; private set; } = false;
|
public bool QuadChannel
|
||||||
|
{
|
||||||
|
get => _quadChannel;
|
||||||
|
private set => this.RaiseAndSetIfChanged(ref _quadChannel, value);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents the DATA flag
|
/// Represents the DATA flag
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool IsDataTrack => TrackType != TrackType.Audio;
|
public bool IsDataTrack
|
||||||
|
{
|
||||||
|
get => _isDataTrack;
|
||||||
|
private set => this.RaiseAndSetIfChanged(ref _isDataTrack, value);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents the DCP flag
|
/// Represents the DCP flag
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool CopyAllowed { get; private set; } = false;
|
public bool CopyAllowed
|
||||||
|
{
|
||||||
|
get => _copyAllowed;
|
||||||
|
private set => this.RaiseAndSetIfChanged(ref _copyAllowed, value);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents the PRE flag
|
/// Represents the PRE flag
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool TrackHasEmphasis { get; private set; } = false;
|
public bool TrackHasEmphasis
|
||||||
|
{
|
||||||
|
get => _trackHasEmphasis;
|
||||||
|
private set => this.RaiseAndSetIfChanged(ref _trackHasEmphasis, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Indicate if data tracks should be loaded
|
||||||
|
/// </summary>
|
||||||
|
public bool LoadDataTracks { get; set; } = false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Indicate if hidden tracks should be loaded
|
||||||
|
/// </summary>
|
||||||
|
public bool LoadHiddenTracks { get; set; } = false;
|
||||||
|
|
||||||
|
private bool _quadChannel;
|
||||||
|
private bool _isDataTrack;
|
||||||
|
private bool _copyAllowed;
|
||||||
|
private bool _trackHasEmphasis;
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
@@ -171,6 +249,11 @@ namespace RedBookPlayer.Discs
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private ulong _currentSector = 0;
|
private ulong _currentSector = 0;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Indicate if a TOC should be generated if missing
|
||||||
|
/// </summary>
|
||||||
|
private readonly bool _generateMissingToc = false;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Current disc table of contents
|
/// Current disc table of contents
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -178,8 +261,21 @@ namespace RedBookPlayer.Discs
|
|||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Constructor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="generateMissingToc">Generate a TOC if the disc is missing one</param>
|
||||||
|
/// <param name="loadHiddenTracks">Load hidden tracks for playback</param>
|
||||||
|
/// <param name="loadDataTracks">Load data tracks for playback</param>
|
||||||
|
public CompactDisc(bool generateMissingToc, bool loadHiddenTracks, bool loadDataTracks)
|
||||||
|
{
|
||||||
|
_generateMissingToc = generateMissingToc;
|
||||||
|
LoadHiddenTracks = loadHiddenTracks;
|
||||||
|
LoadDataTracks = loadDataTracks;
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public override void Init(IOpticalMediaImage image, bool autoPlay = false)
|
public override void Init(IOpticalMediaImage image, bool autoPlay)
|
||||||
{
|
{
|
||||||
// If the image is null, we can't do anything
|
// If the image is null, we can't do anything
|
||||||
if(image == null)
|
if(image == null)
|
||||||
@@ -200,7 +296,7 @@ namespace RedBookPlayer.Discs
|
|||||||
TotalIndexes = 0;
|
TotalIndexes = 0;
|
||||||
|
|
||||||
// Set the internal disc state
|
// Set the internal disc state
|
||||||
TotalTracks = _image.Tracks.Count;
|
TotalTracks = (int)_image.Tracks.Max(t => t.TrackSequence);
|
||||||
TrackDataDescriptor firstTrack = _toc.TrackDescriptors.First(d => d.ADR == 1 && d.POINT == 1);
|
TrackDataDescriptor firstTrack = _toc.TrackDescriptors.First(d => d.ADR == 1 && d.POINT == 1);
|
||||||
TimeOffset = (ulong)((firstTrack.PMIN * 60 * 75) + (firstTrack.PSEC * 75) + firstTrack.PFRAME);
|
TimeOffset = (ulong)((firstTrack.PMIN * 60 * 75) + (firstTrack.PSEC * 75) + firstTrack.PFRAME);
|
||||||
TotalTime = TimeOffset + _image.Tracks.Last().TrackEndSector;
|
TotalTime = TimeOffset + _image.Tracks.Last().TrackEndSector;
|
||||||
@@ -211,24 +307,63 @@ namespace RedBookPlayer.Discs
|
|||||||
|
|
||||||
#region Seeking
|
#region Seeking
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override void NextTrack()
|
||||||
|
{
|
||||||
|
if(_image == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
CurrentTrackNumber++;
|
||||||
|
LoadTrack(CurrentTrackNumber);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override void PreviousTrack()
|
||||||
|
{
|
||||||
|
if(_image == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
CurrentTrackNumber--;
|
||||||
|
LoadTrack(CurrentTrackNumber);
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public override bool NextIndex(bool changeTrack)
|
public override bool NextIndex(bool changeTrack)
|
||||||
{
|
{
|
||||||
if(_image == null)
|
if(_image == null)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
if(CurrentTrackIndex + 1 > _image.Tracks[CurrentTrackNumber].Indexes.Keys.Max())
|
// Cache the current track for easy access
|
||||||
|
Track track = GetTrack(CurrentTrackNumber);
|
||||||
|
if(track == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// If the index is greater than the highest index, change tracks if needed
|
||||||
|
if(CurrentTrackIndex + 1 > track.Indexes.Keys.Max())
|
||||||
{
|
{
|
||||||
if(changeTrack)
|
if(changeTrack)
|
||||||
{
|
{
|
||||||
NextTrack();
|
NextTrack();
|
||||||
CurrentSector = (ulong)_image.Tracks[CurrentTrackNumber].Indexes.Values.Min();
|
CurrentSector = (ulong)GetTrack(CurrentTrackNumber).Indexes.Values.Min();
|
||||||
return true;
|
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
|
else
|
||||||
{
|
{
|
||||||
CurrentSector = (ulong)_image.Tracks[CurrentTrackNumber].Indexes[++CurrentTrackIndex];
|
CurrentSector = (ulong)track.Indexes[++CurrentTrackIndex];
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
@@ -240,18 +375,37 @@ namespace RedBookPlayer.Discs
|
|||||||
if(_image == null)
|
if(_image == null)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
if(CurrentTrackIndex - 1 < _image.Tracks[CurrentTrackNumber].Indexes.Keys.Min())
|
// Cache the current track for easy access
|
||||||
|
Track track = GetTrack(CurrentTrackNumber);
|
||||||
|
if(track == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// If the index is less than the lowest index, change tracks if needed
|
||||||
|
if(CurrentTrackIndex - 1 < track.Indexes.Keys.Min())
|
||||||
{
|
{
|
||||||
if(changeTrack)
|
if(changeTrack)
|
||||||
{
|
{
|
||||||
PreviousTrack();
|
PreviousTrack();
|
||||||
CurrentSector = (ulong)_image.Tracks[CurrentTrackNumber].Indexes.Values.Max();
|
CurrentSector = (ulong)GetTrack(CurrentTrackNumber).Indexes.Values.Max();
|
||||||
return true;
|
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
|
else
|
||||||
{
|
{
|
||||||
CurrentSector = (ulong)_image.Tracks[CurrentTrackNumber].Indexes[--CurrentTrackIndex];
|
CurrentSector = (ulong)track.Indexes[--CurrentTrackIndex];
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
@@ -264,7 +418,7 @@ namespace RedBookPlayer.Discs
|
|||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public override void LoadFirstTrack()
|
public override void LoadFirstTrack()
|
||||||
{
|
{
|
||||||
CurrentTrackNumber = 0;
|
CurrentTrackNumber = 1;
|
||||||
LoadTrack(CurrentTrackNumber);
|
LoadTrack(CurrentTrackNumber);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -274,21 +428,41 @@ namespace RedBookPlayer.Discs
|
|||||||
if(_image == null)
|
if(_image == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
TotalIndexes = _image.Tracks[CurrentTrackNumber].Indexes.Keys.Max();
|
TotalIndexes = GetTrack(CurrentTrackNumber)?.Indexes.Keys.Max() ?? 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
protected override void LoadTrack(int track)
|
protected override void LoadTrack(int trackNumber)
|
||||||
{
|
{
|
||||||
if(_image == null)
|
if(_image == null)
|
||||||
return;
|
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;
|
return;
|
||||||
|
|
||||||
ushort firstIndex = _image.Tracks[track].Indexes.Keys.Min();
|
// Cache the current track for easy access
|
||||||
int firstSector = _image.Tracks[track].Indexes[firstIndex];
|
Track track = GetTrack(trackNumber);
|
||||||
CurrentSector = (ulong)(firstSector >= 0 ? firstSector : _image.Tracks[track].Indexes[1]);
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the track with the given sequence value, if possible
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="trackNumber">Track number to retrieve</param>
|
||||||
|
/// <returns>Track object for the requested sequence, null on error</returns>
|
||||||
|
private Track GetTrack(int trackNumber)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return _image.Tracks.FirstOrDefault(t => t.TrackSequence == trackNumber);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -304,7 +478,7 @@ namespace RedBookPlayer.Discs
|
|||||||
if(_image.Info.ReadableMediaTags?.Contains(MediaTagType.CD_FullTOC) != true)
|
if(_image.Info.ReadableMediaTags?.Contains(MediaTagType.CD_FullTOC) != true)
|
||||||
{
|
{
|
||||||
// Only generate the TOC if we have it set
|
// Only generate the TOC if we have it set
|
||||||
if(!App.Settings.GenerateMissingTOC)
|
if(!_generateMissingToc)
|
||||||
{
|
{
|
||||||
Console.WriteLine("Full TOC not found");
|
Console.WriteLine("Full TOC not found");
|
||||||
return false;
|
return false;
|
||||||
@@ -369,8 +543,9 @@ namespace RedBookPlayer.Discs
|
|||||||
/// <param name="track">Track object to read from</param>
|
/// <param name="track">Track object to read from</param>
|
||||||
private void SetDefaultTrackFlags(Track track)
|
private void SetDefaultTrackFlags(Track track)
|
||||||
{
|
{
|
||||||
QuadChannel = false;
|
|
||||||
TrackType = track.TrackType;
|
TrackType = track.TrackType;
|
||||||
|
QuadChannel = false;
|
||||||
|
IsDataTrack = track.TrackType != TrackType.Audio;
|
||||||
CopyAllowed = false;
|
CopyAllowed = false;
|
||||||
TrackHasEmphasis = false;
|
TrackHasEmphasis = false;
|
||||||
}
|
}
|
||||||
@@ -390,9 +565,11 @@ namespace RedBookPlayer.Discs
|
|||||||
byte flags = (byte)(descriptor.CONTROL & 0x0D);
|
byte flags = (byte)(descriptor.CONTROL & 0x0D);
|
||||||
TrackHasEmphasis = (flags & (byte)TocControl.TwoChanPreEmph) == (byte)TocControl.TwoChanPreEmph;
|
TrackHasEmphasis = (flags & (byte)TocControl.TwoChanPreEmph) == (byte)TocControl.TwoChanPreEmph;
|
||||||
CopyAllowed = (flags & (byte)TocControl.CopyPermissionMask) == (byte)TocControl.CopyPermissionMask;
|
CopyAllowed = (flags & (byte)TocControl.CopyPermissionMask) == (byte)TocControl.CopyPermissionMask;
|
||||||
TrackType = (flags & (byte)TocControl.DataTrack) == (byte)TocControl.DataTrack ? TrackType.Data : TrackType.Audio;
|
IsDataTrack = (flags & (byte)TocControl.DataTrack) == (byte)TocControl.DataTrack;
|
||||||
QuadChannel = (flags & (byte)TocControl.FourChanNoPreEmph) == (byte)TocControl.FourChanNoPreEmph;
|
QuadChannel = (flags & (byte)TocControl.FourChanNoPreEmph) == (byte)TocControl.FourChanNoPreEmph;
|
||||||
|
|
||||||
|
TrackType = IsDataTrack ? TrackType.Data : TrackType.Audio;
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
catch(Exception)
|
catch(Exception)
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
using Aaru.CommonTypes.Enums;
|
using Aaru.CommonTypes.Enums;
|
||||||
using Aaru.CommonTypes.Interfaces;
|
using Aaru.CommonTypes.Interfaces;
|
||||||
|
using ReactiveUI;
|
||||||
|
|
||||||
namespace RedBookPlayer.Discs
|
namespace RedBookPlayer.Models.Discs
|
||||||
{
|
{
|
||||||
public abstract class OpticalDisc
|
public abstract class OpticalDiscBase : ReactiveObject
|
||||||
{
|
{
|
||||||
#region Public Fields
|
#region Public Fields
|
||||||
|
|
||||||
@@ -25,17 +26,21 @@ namespace RedBookPlayer.Discs
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Current sector number
|
/// Current sector number
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract ulong CurrentSector { get; set; }
|
public abstract ulong CurrentSector { get; protected set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents the sector starting the section
|
/// Represents the sector starting the section
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ulong SectionStartSector { get; protected set; }
|
public ulong SectionStartSector
|
||||||
|
{
|
||||||
|
get => _sectionStartSector;
|
||||||
|
protected set => this.RaiseAndSetIfChanged(ref _sectionStartSector, value);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Number of bytes per sector for the current track
|
/// Number of bytes per sector for the current track
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int BytesPerSector => _image.Tracks[CurrentTrackNumber].TrackBytesPerSector;
|
public abstract int BytesPerSector { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents the track type
|
/// Represents the track type
|
||||||
@@ -55,7 +60,7 @@ namespace RedBookPlayer.Discs
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Total sectors in the image
|
/// Total sectors in the image
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ulong TotalSectors => _image.Info.Sectors;
|
public ulong TotalSectors => _image?.Info.Sectors ?? 0;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents the time adjustment offset for the disc
|
/// Represents the time adjustment offset for the disc
|
||||||
@@ -67,6 +72,8 @@ namespace RedBookPlayer.Discs
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public ulong TotalTime { get; protected set; } = 0;
|
public ulong TotalTime { get; protected set; } = 0;
|
||||||
|
|
||||||
|
private ulong _sectionStartSector;
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Protected State Variables
|
#region Protected State Variables
|
||||||
@@ -83,42 +90,19 @@ namespace RedBookPlayer.Discs
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="image">Aaruformat image to load</param>
|
/// <param name="image">Aaruformat image to load</param>
|
||||||
/// <param name="autoPlay">True if playback should begin immediately, false otherwise</param>
|
/// <param name="autoPlay">True if playback should begin immediately, false otherwise</param>
|
||||||
public abstract void Init(IOpticalMediaImage image, bool autoPlay = false);
|
public abstract void Init(IOpticalMediaImage image, bool autoPlay);
|
||||||
|
|
||||||
#region Seeking
|
#region Seeking
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Try to move to the next track, wrapping around if necessary
|
/// Try to move to the next track, wrapping around if necessary
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void NextTrack()
|
public abstract void NextTrack();
|
||||||
{
|
|
||||||
if(_image == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
CurrentTrackNumber++;
|
|
||||||
LoadTrack(CurrentTrackNumber);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Try to move to the previous track, wrapping around if necessary
|
/// Try to move to the previous track, wrapping around if necessary
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void PreviousTrack()
|
public abstract 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Try to move to the next track index
|
/// Try to move to the next track index
|
||||||
@@ -155,6 +139,12 @@ namespace RedBookPlayer.Discs
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract void SetTotalIndexes();
|
public abstract void SetTotalIndexes();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Set the current sector
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="sector">New sector number to use</param>
|
||||||
|
public void SetCurrentSector(ulong sector) => CurrentSector = sector;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Load the desired track, if possible
|
/// Load the desired track, if possible
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -1,31 +1,72 @@
|
|||||||
|
using System.IO;
|
||||||
using Aaru.CommonTypes.Interfaces;
|
using Aaru.CommonTypes.Interfaces;
|
||||||
using Aaru.CommonTypes.Metadata;
|
using Aaru.CommonTypes.Metadata;
|
||||||
|
using Aaru.DiscImages;
|
||||||
|
using Aaru.Filters;
|
||||||
|
using RedBookPlayer.Models.Discs;
|
||||||
|
|
||||||
namespace RedBookPlayer.Discs
|
namespace RedBookPlayer.Models.Factories
|
||||||
{
|
{
|
||||||
public static class OpticalDiscFactory
|
public static class OpticalDiscFactory
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Generate an OpticalDisc from an input path
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="path">Path to load the image from</param>
|
||||||
|
/// <param name="generateMissingToc">Generate a TOC if the disc is missing one [CompactDisc only]</param>
|
||||||
|
/// <param name="loadHiddenTracks">Load hidden tracks for playback [CompactDisc only]</param>
|
||||||
|
/// <param name="loadDataTracks">Load data tracks for playback [CompactDisc only]</param>
|
||||||
|
/// <param name="autoPlay">True if the image should be playable immediately, false otherwise</param>
|
||||||
|
/// <returns>Instantiated OpticalDisc, if possible</returns>
|
||||||
|
public static OpticalDiscBase GenerateFromPath(string path, bool generateMissingToc, bool loadHiddenTracks, bool loadDataTracks, bool autoPlay)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Validate the image exists
|
||||||
|
if(string.IsNullOrWhiteSpace(path) || !File.Exists(path))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
// Load the disc image to memory
|
||||||
|
// TODO: Assumes Aaruformat right now for all
|
||||||
|
var image = new AaruFormat();
|
||||||
|
var filter = new ZZZNoFilter();
|
||||||
|
filter.Open(path);
|
||||||
|
image.Open(filter);
|
||||||
|
|
||||||
|
// Generate and instantiate the disc
|
||||||
|
return GenerateFromImage(image, generateMissingToc, loadHiddenTracks, loadDataTracks, autoPlay);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// All errors mean an invalid image in some way
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Generate an OpticalDisc from an input IOpticalMediaImage
|
/// Generate an OpticalDisc from an input IOpticalMediaImage
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="image">IOpticalMediaImage to create from</param>
|
/// <param name="image">IOpticalMediaImage to create from</param>
|
||||||
|
/// <param name="generateMissingToc">Generate a TOC if the disc is missing one [CompactDisc only]</param>
|
||||||
|
/// <param name="loadHiddenTracks">Load hidden tracks for playback [CompactDisc only]</param>
|
||||||
|
/// <param name="loadDataTracks">Load data tracks for playback [CompactDisc only]</param>
|
||||||
/// <param name="autoPlay">True if the image should be playable immediately, false otherwise</param>
|
/// <param name="autoPlay">True if the image should be playable immediately, false otherwise</param>
|
||||||
/// <returns>Instantiated OpticalDisc, if possible</returns>
|
/// <returns>Instantiated OpticalDisc, if possible</returns>
|
||||||
public static OpticalDisc GenerateFromImage(IOpticalMediaImage image, bool autoPlay)
|
public static OpticalDiscBase GenerateFromImage(IOpticalMediaImage image, bool generateMissingToc, bool loadHiddenTracks, bool loadDataTracks, bool autoPlay)
|
||||||
{
|
{
|
||||||
// If the image is not usable, we don't do anything
|
// If the image is not usable, we don't do anything
|
||||||
if(!IsUsableImage(image))
|
if(!IsUsableImage(image))
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
// Create the output object
|
// Create the output object
|
||||||
OpticalDisc opticalDisc;
|
OpticalDiscBase opticalDisc;
|
||||||
|
|
||||||
// Create the proper disc type
|
// Create the proper disc type
|
||||||
switch(GetMediaType(image))
|
switch(GetMediaType(image))
|
||||||
{
|
{
|
||||||
case "Compact Disc":
|
case "Compact Disc":
|
||||||
case "GD":
|
case "GD":
|
||||||
opticalDisc = new CompactDisc();
|
opticalDisc = new CompactDisc(generateMissingToc, loadHiddenTracks, loadDataTracks);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
opticalDisc = null;
|
opticalDisc = null;
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using NWaves.Filters.BiQuad;
|
using NWaves.Filters.BiQuad;
|
||||||
|
|
||||||
namespace RedBookPlayer.Hardware
|
namespace RedBookPlayer.Models.Hardware
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Filter for applying de-emphasis to audio
|
/// Filter for applying de-emphasis to audio
|
||||||
518
RedBookPlayer.Models/Hardware/Player.cs
Normal file
@@ -0,0 +1,518 @@
|
|||||||
|
using System;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using Aaru.CommonTypes.Enums;
|
||||||
|
using ReactiveUI;
|
||||||
|
using RedBookPlayer.Models.Discs;
|
||||||
|
using RedBookPlayer.Models.Factories;
|
||||||
|
|
||||||
|
namespace RedBookPlayer.Models.Hardware
|
||||||
|
{
|
||||||
|
public class Player : ReactiveObject
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Indicate if the player is ready to be used
|
||||||
|
/// </summary>
|
||||||
|
public bool Initialized { get; private set; } = false;
|
||||||
|
|
||||||
|
#region OpticalDisc Passthrough
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Current track number
|
||||||
|
/// </summary>
|
||||||
|
public int CurrentTrackNumber
|
||||||
|
{
|
||||||
|
get => _currentTrackNumber;
|
||||||
|
private set => this.RaiseAndSetIfChanged(ref _currentTrackNumber, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Current track index
|
||||||
|
/// </summary>
|
||||||
|
public ushort CurrentTrackIndex
|
||||||
|
{
|
||||||
|
get => _currentTrackIndex;
|
||||||
|
private set => this.RaiseAndSetIfChanged(ref _currentTrackIndex, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Current sector number
|
||||||
|
/// </summary>
|
||||||
|
public ulong CurrentSector
|
||||||
|
{
|
||||||
|
get => _currentSector;
|
||||||
|
private set => this.RaiseAndSetIfChanged(ref _currentSector, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the sector starting the section
|
||||||
|
/// </summary>
|
||||||
|
public ulong SectionStartSector
|
||||||
|
{
|
||||||
|
get => _sectionStartSector;
|
||||||
|
protected set => this.RaiseAndSetIfChanged(ref _sectionStartSector, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents if the disc has a hidden track
|
||||||
|
/// </summary>
|
||||||
|
public bool HiddenTrack
|
||||||
|
{
|
||||||
|
get => _hasHiddenTrack;
|
||||||
|
private set => this.RaiseAndSetIfChanged(ref _hasHiddenTrack, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the 4CH flag [CompactDisc only]
|
||||||
|
/// </summary>
|
||||||
|
public bool QuadChannel
|
||||||
|
{
|
||||||
|
get => _quadChannel;
|
||||||
|
private set => this.RaiseAndSetIfChanged(ref _quadChannel, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the DATA flag [CompactDisc only]
|
||||||
|
/// </summary>
|
||||||
|
public bool IsDataTrack
|
||||||
|
{
|
||||||
|
get => _isDataTrack;
|
||||||
|
private set => this.RaiseAndSetIfChanged(ref _isDataTrack, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the DCP flag [CompactDisc only]
|
||||||
|
/// </summary>
|
||||||
|
public bool CopyAllowed
|
||||||
|
{
|
||||||
|
get => _copyAllowed;
|
||||||
|
private set => this.RaiseAndSetIfChanged(ref _copyAllowed, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the PRE flag [CompactDisc only]
|
||||||
|
/// </summary>
|
||||||
|
public bool TrackHasEmphasis
|
||||||
|
{
|
||||||
|
get => _trackHasEmphasis;
|
||||||
|
private set => this.RaiseAndSetIfChanged(ref _trackHasEmphasis, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the total tracks on the disc
|
||||||
|
/// </summary>
|
||||||
|
public int TotalTracks => _opticalDisc.TotalTracks;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the total indices on the disc
|
||||||
|
/// </summary>
|
||||||
|
public int TotalIndexes => _opticalDisc.TotalIndexes;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Total sectors in the image
|
||||||
|
/// </summary>
|
||||||
|
public ulong TotalSectors => _opticalDisc.TotalSectors;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the time adjustment offset for the disc
|
||||||
|
/// </summary>
|
||||||
|
public ulong TimeOffset => _opticalDisc.TimeOffset;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the total playing time for the disc
|
||||||
|
/// </summary>
|
||||||
|
public ulong TotalTime => _opticalDisc.TotalTime;
|
||||||
|
|
||||||
|
private int _currentTrackNumber;
|
||||||
|
private ushort _currentTrackIndex;
|
||||||
|
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
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Indicate if the output is playing
|
||||||
|
/// </summary>
|
||||||
|
public bool? Playing
|
||||||
|
{
|
||||||
|
get => _playing;
|
||||||
|
private set => this.RaiseAndSetIfChanged(ref _playing, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Indicates if de-emphasis should be applied
|
||||||
|
/// </summary>
|
||||||
|
public bool ApplyDeEmphasis
|
||||||
|
{
|
||||||
|
get => _applyDeEmphasis;
|
||||||
|
private set => this.RaiseAndSetIfChanged(ref _applyDeEmphasis, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Current playback volume
|
||||||
|
/// </summary>
|
||||||
|
public int Volume
|
||||||
|
{
|
||||||
|
get => _volume;
|
||||||
|
private set => this.RaiseAndSetIfChanged(ref _volume, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool? _playing;
|
||||||
|
private bool _applyDeEmphasis;
|
||||||
|
private int _volume;
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Private State Variables
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sound output handling class
|
||||||
|
/// </summary>
|
||||||
|
private readonly SoundOutput _soundOutput;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// OpticalDisc object
|
||||||
|
/// </summary>
|
||||||
|
private readonly OpticalDiscBase _opticalDisc;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Last volume for mute toggling
|
||||||
|
/// </summary>
|
||||||
|
private int? _lastVolume = null;
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a new Player from a given image path
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="path">Path to the disc image</param>
|
||||||
|
/// <param name="generateMissingToc">Generate a TOC if the disc is missing one [CompactDisc only]</param>
|
||||||
|
/// <param name="loadHiddenTracks">Load hidden tracks for playback [CompactDisc only]</param>
|
||||||
|
/// <param name="loadDataTracks">Load data tracks for playback [CompactDisc only]</param>
|
||||||
|
/// <param name="autoPlay">True if playback should begin immediately, false otherwise</param>
|
||||||
|
/// <param name="defaultVolume">Default volume between 0 and 100 to use when starting playback</param>
|
||||||
|
public Player(string path, bool generateMissingToc, bool loadHiddenTracks, bool loadDataTracks, bool autoPlay, int defaultVolume)
|
||||||
|
{
|
||||||
|
// Set the internal state for initialization
|
||||||
|
Initialized = false;
|
||||||
|
_soundOutput = new SoundOutput();
|
||||||
|
_soundOutput.SetDeEmphasis(false);
|
||||||
|
|
||||||
|
// Initalize the disc
|
||||||
|
_opticalDisc = OpticalDiscFactory.GenerateFromPath(path, generateMissingToc, loadHiddenTracks, loadDataTracks, autoPlay);
|
||||||
|
if(_opticalDisc == null || !_opticalDisc.Initialized)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Add event handling for the optical disc
|
||||||
|
_opticalDisc.PropertyChanged += OpticalDiscStateChanged;
|
||||||
|
|
||||||
|
// Initialize the sound output
|
||||||
|
_soundOutput.Init(_opticalDisc, autoPlay, defaultVolume);
|
||||||
|
if(_soundOutput == null || !_soundOutput.Initialized)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Add event handling for the sound output
|
||||||
|
_soundOutput.PropertyChanged += SoundOutputStateChanged;
|
||||||
|
|
||||||
|
// Mark the player as ready
|
||||||
|
Initialized = true;
|
||||||
|
|
||||||
|
// Force a refresh of the state information
|
||||||
|
OpticalDiscStateChanged(this, null);
|
||||||
|
SoundOutputStateChanged(this, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Playback
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Begin playback
|
||||||
|
/// </summary>
|
||||||
|
public void Play()
|
||||||
|
{
|
||||||
|
if(_opticalDisc == null || !_opticalDisc.Initialized)
|
||||||
|
return;
|
||||||
|
else if(_soundOutput == null)
|
||||||
|
return;
|
||||||
|
else if(_soundOutput.Playing)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_soundOutput.Play();
|
||||||
|
_opticalDisc.SetTotalIndexes();
|
||||||
|
Playing = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Pause current playback
|
||||||
|
/// </summary>
|
||||||
|
public void Pause()
|
||||||
|
{
|
||||||
|
if(_opticalDisc == null || !_opticalDisc.Initialized)
|
||||||
|
return;
|
||||||
|
else if(_soundOutput == null)
|
||||||
|
return;
|
||||||
|
else if(!_soundOutput.Playing)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_soundOutput?.Stop();
|
||||||
|
Playing = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Toggle current playback
|
||||||
|
/// </summary>
|
||||||
|
public void TogglePlayback()
|
||||||
|
{
|
||||||
|
if(Playing == true)
|
||||||
|
Pause();
|
||||||
|
else
|
||||||
|
Play();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Stop current playback
|
||||||
|
/// </summary>
|
||||||
|
public void Stop()
|
||||||
|
{
|
||||||
|
if(_opticalDisc == null || !_opticalDisc.Initialized)
|
||||||
|
return;
|
||||||
|
else if(_soundOutput == null)
|
||||||
|
return;
|
||||||
|
else if(!_soundOutput.Playing)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_soundOutput?.Stop();
|
||||||
|
_opticalDisc.LoadFirstTrack();
|
||||||
|
Playing = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Move to the next playable track
|
||||||
|
/// </summary>
|
||||||
|
public void NextTrack()
|
||||||
|
{
|
||||||
|
if(_opticalDisc == null || !_opticalDisc.Initialized)
|
||||||
|
return;
|
||||||
|
|
||||||
|
bool? wasPlaying = Playing;
|
||||||
|
if(wasPlaying == true) Pause();
|
||||||
|
|
||||||
|
_opticalDisc.NextTrack();
|
||||||
|
if(_opticalDisc is CompactDisc compactDisc)
|
||||||
|
_soundOutput.SetDeEmphasis(compactDisc.TrackHasEmphasis);
|
||||||
|
|
||||||
|
if(wasPlaying == true) Play();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Move to the previous playable track
|
||||||
|
/// </summary>
|
||||||
|
public void PreviousTrack()
|
||||||
|
{
|
||||||
|
if(_opticalDisc == null || !_opticalDisc.Initialized)
|
||||||
|
return;
|
||||||
|
|
||||||
|
bool? wasPlaying = Playing;
|
||||||
|
if(wasPlaying == true) Pause();
|
||||||
|
|
||||||
|
_opticalDisc.PreviousTrack();
|
||||||
|
if(_opticalDisc is CompactDisc compactDisc)
|
||||||
|
_soundOutput.SetDeEmphasis(compactDisc.TrackHasEmphasis);
|
||||||
|
|
||||||
|
if(wasPlaying == true) Play();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Move to the next index
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="changeTrack">True if index changes can trigger a track change, false otherwise</param>
|
||||||
|
public void NextIndex(bool changeTrack)
|
||||||
|
{
|
||||||
|
if(_opticalDisc == null || !_opticalDisc.Initialized)
|
||||||
|
return;
|
||||||
|
|
||||||
|
bool? wasPlaying = Playing;
|
||||||
|
if(wasPlaying == true) Pause();
|
||||||
|
|
||||||
|
_opticalDisc.NextIndex(changeTrack);
|
||||||
|
if(_opticalDisc is CompactDisc compactDisc)
|
||||||
|
_soundOutput.SetDeEmphasis(compactDisc.TrackHasEmphasis);
|
||||||
|
|
||||||
|
if(wasPlaying == true) Play();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Move to the previous index
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="changeTrack">True if index changes can trigger a track change, false otherwise</param>
|
||||||
|
public void PreviousIndex(bool changeTrack)
|
||||||
|
{
|
||||||
|
if(_opticalDisc == null || !_opticalDisc.Initialized)
|
||||||
|
return;
|
||||||
|
|
||||||
|
bool? wasPlaying = Playing;
|
||||||
|
if(wasPlaying == true) Pause();
|
||||||
|
|
||||||
|
_opticalDisc.PreviousIndex(changeTrack);
|
||||||
|
if(_opticalDisc is CompactDisc compactDisc)
|
||||||
|
_soundOutput.SetDeEmphasis(compactDisc.TrackHasEmphasis);
|
||||||
|
|
||||||
|
if(wasPlaying == true) Play();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fast-forward playback by 75 sectors, if possible
|
||||||
|
/// </summary>
|
||||||
|
public void FastForward()
|
||||||
|
{
|
||||||
|
if(_opticalDisc == null || !_opticalDisc.Initialized)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_opticalDisc.SetCurrentSector(Math.Min(_opticalDisc.TotalSectors, _opticalDisc.CurrentSector + 75));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Rewind playback by 75 sectors, if possible
|
||||||
|
/// </summary>
|
||||||
|
public void Rewind()
|
||||||
|
{
|
||||||
|
if(_opticalDisc == null || !_opticalDisc.Initialized)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if(_opticalDisc.CurrentSector >= 75)
|
||||||
|
_opticalDisc.SetCurrentSector(_opticalDisc.CurrentSector - 75);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Volume
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Increment the volume value
|
||||||
|
/// </summary>
|
||||||
|
public void VolumeUp() => SetVolume(Volume + 1);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Decrement the volume value
|
||||||
|
/// </summary>
|
||||||
|
public void VolumeDown() => SetVolume(Volume + 1);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Set the value for the volume
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="volume">New volume value</param>
|
||||||
|
public void SetVolume(int volume) => _soundOutput?.SetVolume(volume);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Temporarily mute playback
|
||||||
|
/// </summary>
|
||||||
|
public void ToggleMute()
|
||||||
|
{
|
||||||
|
if(_lastVolume == null)
|
||||||
|
{
|
||||||
|
_lastVolume = Volume;
|
||||||
|
SetVolume(0);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
SetVolume(_lastVolume.Value);
|
||||||
|
_lastVolume = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Emphasis
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Enable de-emphasis
|
||||||
|
/// </summary>
|
||||||
|
public void EnableDeEmphasis() => SetDeEmphasis(true);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Disable de-emphasis
|
||||||
|
/// </summary>
|
||||||
|
public void DisableDeEmphasis() => SetDeEmphasis(false);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Toggle de-emphasis
|
||||||
|
/// </summary>
|
||||||
|
public void ToggleDeEmphasis() => SetDeEmphasis(!ApplyDeEmphasis);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Set de-emphasis status
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="apply"></param>
|
||||||
|
private void SetDeEmphasis(bool apply) => _soundOutput?.SetDeEmphasis(apply);
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Helpers
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Set the value for loading data tracks [CompactDisc only]
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="load">True to enable loading data tracks, false otherwise</param>
|
||||||
|
public void SetLoadDataTracks(bool load)
|
||||||
|
{
|
||||||
|
if(_opticalDisc is CompactDisc compactDisc)
|
||||||
|
compactDisc.LoadDataTracks = load;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Set the value for loading hidden tracks [CompactDisc only]
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="load">True to enable loading hidden tracks, false otherwise</param>
|
||||||
|
public void SetLoadHiddenTracks(bool load)
|
||||||
|
{
|
||||||
|
if(_opticalDisc is CompactDisc compactDisc)
|
||||||
|
compactDisc.LoadHiddenTracks = load;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Update the player from the current OpticalDisc
|
||||||
|
/// </summary>
|
||||||
|
private void OpticalDiscStateChanged(object sender, PropertyChangedEventArgs e)
|
||||||
|
{
|
||||||
|
CurrentTrackNumber = _opticalDisc.CurrentTrackNumber;
|
||||||
|
CurrentTrackIndex = _opticalDisc.CurrentTrackIndex;
|
||||||
|
CurrentSector = _opticalDisc.CurrentSector;
|
||||||
|
SectionStartSector = _opticalDisc.SectionStartSector;
|
||||||
|
|
||||||
|
HiddenTrack = TimeOffset > 150;
|
||||||
|
|
||||||
|
if(_opticalDisc is CompactDisc compactDisc)
|
||||||
|
{
|
||||||
|
QuadChannel = compactDisc.QuadChannel;
|
||||||
|
IsDataTrack = compactDisc.IsDataTrack;
|
||||||
|
CopyAllowed = compactDisc.CopyAllowed;
|
||||||
|
TrackHasEmphasis = compactDisc.TrackHasEmphasis;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
QuadChannel = false;
|
||||||
|
IsDataTrack = _opticalDisc.TrackType != TrackType.Audio;
|
||||||
|
CopyAllowed = false;
|
||||||
|
TrackHasEmphasis = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Update the player from the current SoundOutput
|
||||||
|
/// </summary>
|
||||||
|
private void SoundOutputStateChanged(object sender, PropertyChangedEventArgs e)
|
||||||
|
{
|
||||||
|
Playing = _soundOutput.Playing;
|
||||||
|
ApplyDeEmphasis = _soundOutput.ApplyDeEmphasis;
|
||||||
|
Volume = _soundOutput.Volume;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@ using System;
|
|||||||
using CSCore;
|
using CSCore;
|
||||||
using WaveFormat = CSCore.WaveFormat;
|
using WaveFormat = CSCore.WaveFormat;
|
||||||
|
|
||||||
namespace RedBookPlayer.Hardware
|
namespace RedBookPlayer.Models.Hardware
|
||||||
{
|
{
|
||||||
public class PlayerSource : IWaveSource
|
public class PlayerSource : IWaveSource
|
||||||
{
|
{
|
||||||
@@ -4,11 +4,12 @@ using System.Threading.Tasks;
|
|||||||
using CSCore.SoundOut;
|
using CSCore.SoundOut;
|
||||||
using NWaves.Audio;
|
using NWaves.Audio;
|
||||||
using NWaves.Filters.BiQuad;
|
using NWaves.Filters.BiQuad;
|
||||||
using RedBookPlayer.Discs;
|
using ReactiveUI;
|
||||||
|
using RedBookPlayer.Models.Discs;
|
||||||
|
|
||||||
namespace RedBookPlayer.Hardware
|
namespace RedBookPlayer.Models.Hardware
|
||||||
{
|
{
|
||||||
public class SoundOutput
|
public class SoundOutput : ReactiveObject
|
||||||
{
|
{
|
||||||
#region Public Fields
|
#region Public Fields
|
||||||
|
|
||||||
@@ -17,15 +18,23 @@ namespace RedBookPlayer.Hardware
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public bool Initialized { get; private set; } = false;
|
public bool Initialized { get; private set; } = false;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Indicates if de-emphasis should be applied
|
|
||||||
/// </summary>
|
|
||||||
public bool ApplyDeEmphasis { get; set; } = false;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Indicate if the output is playing
|
/// Indicate if the output is playing
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool Playing => _soundOut.PlaybackState == PlaybackState.Playing;
|
public bool Playing
|
||||||
|
{
|
||||||
|
get => _playing;
|
||||||
|
private set => this.RaiseAndSetIfChanged(ref _playing, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Indicates if de-emphasis should be applied
|
||||||
|
/// </summary>
|
||||||
|
public bool ApplyDeEmphasis
|
||||||
|
{
|
||||||
|
get => _applyDeEmphasis;
|
||||||
|
private set => this.RaiseAndSetIfChanged(ref _applyDeEmphasis, value);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Current playback volume
|
/// Current playback volume
|
||||||
@@ -33,38 +42,33 @@ namespace RedBookPlayer.Hardware
|
|||||||
public int Volume
|
public int Volume
|
||||||
{
|
{
|
||||||
get => _volume;
|
get => _volume;
|
||||||
set
|
private set
|
||||||
{
|
{
|
||||||
|
int tempVolume = value;
|
||||||
if(value > 100)
|
if(value > 100)
|
||||||
_volume = 100;
|
tempVolume = 100;
|
||||||
else if(value < 0)
|
else if(value < 0)
|
||||||
_volume = 0;
|
tempVolume = 0;
|
||||||
else
|
|
||||||
_volume = value;
|
this.RaiseAndSetIfChanged(ref _volume, tempVolume);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool _playing;
|
||||||
|
private bool _applyDeEmphasis;
|
||||||
|
private int _volume;
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Private State Variables
|
#region Private State Variables
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Current position in the sector
|
|
||||||
/// </summary>
|
|
||||||
private int _currentSectorReadPosition = 0;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// OpticalDisc from the parent player for easy access
|
/// OpticalDisc from the parent player for easy access
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// TODO: Can we remove the need for a local reference to OpticalDisc?
|
/// TODO: Can we remove the need for a local reference to OpticalDisc?
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
private OpticalDisc _opticalDisc;
|
private OpticalDiscBase _opticalDisc;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Internal value for the volume
|
|
||||||
/// </summary>
|
|
||||||
private int _volume;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Data provider for sound output
|
/// Data provider for sound output
|
||||||
@@ -86,6 +90,11 @@ namespace RedBookPlayer.Hardware
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private BiQuadFilter _deEmphasisFilterRight;
|
private BiQuadFilter _deEmphasisFilterRight;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Current position in the sector
|
||||||
|
/// </summary>
|
||||||
|
private int _currentSectorReadPosition = 0;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Lock object for reading track data
|
/// Lock object for reading track data
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -99,7 +108,7 @@ namespace RedBookPlayer.Hardware
|
|||||||
/// <param name="opticalDisc">OpticalDisc to load from</param>
|
/// <param name="opticalDisc">OpticalDisc to load from</param>
|
||||||
/// <param name="autoPlay">True if playback should begin immediately, false otherwise</param>
|
/// <param name="autoPlay">True if playback should begin immediately, false otherwise</param>
|
||||||
/// <param name="defaultVolume">Default volume between 0 and 100 to use when starting playback</param>
|
/// <param name="defaultVolume">Default volume between 0 and 100 to use when starting playback</param>
|
||||||
public void Init(OpticalDisc opticalDisc, bool autoPlay = false, int defaultVolume = 100)
|
public void Init(OpticalDiscBase opticalDisc, bool autoPlay = false, int defaultVolume = 100)
|
||||||
{
|
{
|
||||||
// If we have an unusable disc, just return
|
// If we have an unusable disc, just return
|
||||||
if(opticalDisc == null || !opticalDisc.Initialized)
|
if(opticalDisc == null || !opticalDisc.Initialized)
|
||||||
@@ -144,32 +153,15 @@ namespace RedBookPlayer.Hardware
|
|||||||
// Set the current volume
|
// Set the current volume
|
||||||
_soundOut.Volume = (float)Volume / 100;
|
_soundOut.Volume = (float)Volume / 100;
|
||||||
|
|
||||||
// Determine how many sectors we can read
|
// If we have an unreadable track, just return
|
||||||
ulong sectorsToRead;
|
if(_opticalDisc.BytesPerSector <= 0)
|
||||||
ulong zeroSectorsAmount;
|
|
||||||
do
|
|
||||||
{
|
{
|
||||||
// Attempt to read 2 more sectors than requested
|
Array.Clear(buffer, offset, count);
|
||||||
sectorsToRead = ((ulong)count / (ulong)_opticalDisc.BytesPerSector) + 2;
|
return count;
|
||||||
zeroSectorsAmount = 0;
|
}
|
||||||
|
|
||||||
// Avoid overreads by padding with 0-byte data at the end
|
// Determine how many sectors we can read
|
||||||
if(_opticalDisc.CurrentSector + sectorsToRead > _opticalDisc.TotalSectors)
|
DetermineReadAmount(count, out ulong sectorsToRead, out ulong zeroSectorsAmount);
|
||||||
{
|
|
||||||
ulong oldSectorsToRead = sectorsToRead;
|
|
||||||
sectorsToRead = _opticalDisc.TotalSectors - _opticalDisc.CurrentSector;
|
|
||||||
|
|
||||||
int tempZeroSectorCount = (int)(oldSectorsToRead - sectorsToRead);
|
|
||||||
zeroSectorsAmount = (ulong)(tempZeroSectorCount < 0 ? 0 : tempZeroSectorCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Figure out when this value could be negative
|
|
||||||
if(sectorsToRead <= 0)
|
|
||||||
{
|
|
||||||
_opticalDisc.LoadFirstTrack();
|
|
||||||
_currentSectorReadPosition = 0;
|
|
||||||
}
|
|
||||||
} while(sectorsToRead <= 0);
|
|
||||||
|
|
||||||
// Create padding data for overreads
|
// Create padding data for overreads
|
||||||
byte[] zeroSectors = new byte[(int)zeroSectorsAmount * _opticalDisc.BytesPerSector];
|
byte[] zeroSectors = new byte[(int)zeroSectorsAmount * _opticalDisc.BytesPerSector];
|
||||||
@@ -220,20 +212,7 @@ namespace RedBookPlayer.Hardware
|
|||||||
|
|
||||||
// Apply de-emphasis filtering, only if enabled
|
// Apply de-emphasis filtering, only if enabled
|
||||||
if(ApplyDeEmphasis)
|
if(ApplyDeEmphasis)
|
||||||
{
|
ProcessDeEmphasis(audioDataSegment);
|
||||||
float[][] floatAudioData = new float[2][];
|
|
||||||
floatAudioData[0] = new float[audioDataSegment.Length / 4];
|
|
||||||
floatAudioData[1] = new float[audioDataSegment.Length / 4];
|
|
||||||
ByteConverter.ToFloats16Bit(audioDataSegment, floatAudioData);
|
|
||||||
|
|
||||||
for(int i = 0; i < floatAudioData[0].Length; i++)
|
|
||||||
{
|
|
||||||
floatAudioData[0][i] = _deEmphasisFilterLeft.Process(floatAudioData[0][i]);
|
|
||||||
floatAudioData[1][i] = _deEmphasisFilterRight.Process(floatAudioData[1][i]);
|
|
||||||
}
|
|
||||||
|
|
||||||
ByteConverter.FromFloats16Bit(floatAudioData, audioDataSegment);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write out the audio data to the buffer
|
// Write out the audio data to the buffer
|
||||||
Array.Copy(audioDataSegment, 0, buffer, offset, count);
|
Array.Copy(audioDataSegment, 0, buffer, offset, count);
|
||||||
@@ -242,7 +221,7 @@ namespace RedBookPlayer.Hardware
|
|||||||
_currentSectorReadPosition += count;
|
_currentSectorReadPosition += count;
|
||||||
if(_currentSectorReadPosition >= _opticalDisc.BytesPerSector)
|
if(_currentSectorReadPosition >= _opticalDisc.BytesPerSector)
|
||||||
{
|
{
|
||||||
_opticalDisc.CurrentSector += (ulong)(_currentSectorReadPosition / _opticalDisc.BytesPerSector);
|
_opticalDisc.SetCurrentSector(_opticalDisc.CurrentSector + (ulong)(_currentSectorReadPosition / _opticalDisc.BytesPerSector));
|
||||||
_currentSectorReadPosition %= _opticalDisc.BytesPerSector;
|
_currentSectorReadPosition %= _opticalDisc.BytesPerSector;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -254,22 +233,105 @@ namespace RedBookPlayer.Hardware
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Start audio playback
|
/// Start audio playback
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void Play() => _soundOut.Play();
|
public void Play()
|
||||||
|
{
|
||||||
|
if (_soundOut.PlaybackState != PlaybackState.Playing)
|
||||||
|
_soundOut.Play();
|
||||||
|
|
||||||
|
Playing = _soundOut.PlaybackState == PlaybackState.Playing;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Pause audio playback
|
||||||
|
/// </summary>
|
||||||
|
public void Pause()
|
||||||
|
{
|
||||||
|
if(_soundOut.PlaybackState != PlaybackState.Paused)
|
||||||
|
_soundOut.Pause();
|
||||||
|
|
||||||
|
Playing = _soundOut.PlaybackState == PlaybackState.Playing;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Stop audio playback
|
/// Stop audio playback
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void Stop() => _soundOut.Stop();
|
public void Stop()
|
||||||
|
{
|
||||||
|
if(_soundOut.PlaybackState != PlaybackState.Stopped)
|
||||||
|
_soundOut.Stop();
|
||||||
|
|
||||||
|
Playing = _soundOut.PlaybackState == PlaybackState.Playing;
|
||||||
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Helpers
|
#region Helpers
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Toggle de-emphasis processing
|
/// Set de-emphasis status
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="enable">True to apply de-emphasis, false otherwise</param>
|
/// <param name="apply"></param>
|
||||||
public void SetDeEmphasis(bool enable) => ApplyDeEmphasis = enable;
|
public void SetDeEmphasis(bool apply) => ApplyDeEmphasis = apply;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Set the value for the volume
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="volume">New volume value</param>
|
||||||
|
public void SetVolume(int volume) => Volume = volume;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determine the number of real and zero sectors to read
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="count">Number of requested bytes to read</param>
|
||||||
|
/// <param name="sectorsToRead">Number of sectors to read</param>
|
||||||
|
/// <param name="zeroSectorsAmount">Number of zeroed sectors to concatenate</param>
|
||||||
|
private void DetermineReadAmount(int count, out ulong sectorsToRead, out ulong zeroSectorsAmount)
|
||||||
|
{
|
||||||
|
do
|
||||||
|
{
|
||||||
|
// Attempt to read 2 more sectors than requested
|
||||||
|
sectorsToRead = ((ulong)count / (ulong)_opticalDisc.BytesPerSector) + 2;
|
||||||
|
zeroSectorsAmount = 0;
|
||||||
|
|
||||||
|
// Avoid overreads by padding with 0-byte data at the end
|
||||||
|
if(_opticalDisc.CurrentSector + sectorsToRead > _opticalDisc.TotalSectors)
|
||||||
|
{
|
||||||
|
ulong oldSectorsToRead = sectorsToRead;
|
||||||
|
sectorsToRead = _opticalDisc.TotalSectors - _opticalDisc.CurrentSector;
|
||||||
|
|
||||||
|
int tempZeroSectorCount = (int)(oldSectorsToRead - sectorsToRead);
|
||||||
|
zeroSectorsAmount = (ulong)(tempZeroSectorCount < 0 ? 0 : tempZeroSectorCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we're reading past the last sector of the disc, wrap around
|
||||||
|
// TODO: Have past-end reads looping back controlled by a flag instead (Repeat? Repeat All?)
|
||||||
|
if(sectorsToRead <= 0)
|
||||||
|
{
|
||||||
|
_opticalDisc.LoadFirstTrack();
|
||||||
|
_currentSectorReadPosition = 0;
|
||||||
|
}
|
||||||
|
} while(sectorsToRead <= 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Process de-emphasis of audio data
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="audioData">Audio data to process</param>
|
||||||
|
private void ProcessDeEmphasis(byte[] audioData)
|
||||||
|
{
|
||||||
|
float[][] floatAudioData = new float[2][];
|
||||||
|
floatAudioData[0] = new float[audioData.Length / 4];
|
||||||
|
floatAudioData[1] = new float[audioData.Length / 4];
|
||||||
|
ByteConverter.ToFloats16Bit(audioData, floatAudioData);
|
||||||
|
|
||||||
|
for(int i = 0; i < floatAudioData[0].Length; i++)
|
||||||
|
{
|
||||||
|
floatAudioData[0][i] = _deEmphasisFilterLeft.Process(floatAudioData[0][i]);
|
||||||
|
floatAudioData[1][i] = _deEmphasisFilterRight.Process(floatAudioData[1][i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
ByteConverter.FromFloats16Bit(floatAudioData, audioData);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Sets or resets the de-emphasis filters
|
/// Sets or resets the de-emphasis filters
|
||||||
19
RedBookPlayer.Models/RedBookPlayer.Models.csproj
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||||
|
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Avalonia.ReactiveUI" Version="0.9.12" />
|
||||||
|
<PackageReference Include="NWaves" Version="0.9.4" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Aaru\Aaru.Images\Aaru.Images.csproj" />
|
||||||
|
<ProjectReference Include="..\Aaru\Aaru.CommonTypes\Aaru.CommonTypes.csproj" />
|
||||||
|
<ProjectReference Include="..\Aaru\Aaru.Decoders\Aaru.Decoders.csproj" />
|
||||||
|
<ProjectReference Include="..\cscore\CSCore\CSCore.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
6
RedBookPlayer.Models/nuget.config
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<configuration>
|
||||||
|
<packageSources>
|
||||||
|
<add key="AvaloniaCI" value="https://www.myget.org/F/avalonia-ci/api/v2" />
|
||||||
|
</packageSources>
|
||||||
|
</configuration>
|
||||||
@@ -3,7 +3,7 @@ Microsoft Visual Studio Solution File, Format Version 12.00
|
|||||||
# Visual Studio Version 16
|
# Visual Studio Version 16
|
||||||
VisualStudioVersion = 16.0.31321.278
|
VisualStudioVersion = 16.0.31321.278
|
||||||
MinimumVisualStudioVersion = 15.0.26124.0
|
MinimumVisualStudioVersion = 15.0.26124.0
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RedBookPlayer", "RedBookPlayer\RedBookPlayer.csproj", "{94944959-0352-4ABF-9C5C-19FF33747ECE}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RedBookPlayer.GUI", "RedBookPlayer.GUI\RedBookPlayer.GUI.csproj", "{94944959-0352-4ABF-9C5C-19FF33747ECE}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "cscore", "cscore", "{9A371299-4C59-4E46-9C3B-4FE024017491}"
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "cscore", "cscore", "{9A371299-4C59-4E46-9C3B-4FE024017491}"
|
||||||
EndProject
|
EndProject
|
||||||
@@ -40,6 +40,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
|
|||||||
README.md = README.md
|
README.md = README.md
|
||||||
EndProjectSection
|
EndProjectSection
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RedBookPlayer.Models", "RedBookPlayer.Models\RedBookPlayer.Models.csproj", "{462A3B8E-A5D4-4539-8469-1647B47AB2A8}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
@@ -194,6 +196,18 @@ Global
|
|||||||
{ED8E11B7-786F-4EFF-9E4C-B937B7A2DE89}.Release|x64.Build.0 = Release|Any CPU
|
{ED8E11B7-786F-4EFF-9E4C-B937B7A2DE89}.Release|x64.Build.0 = Release|Any CPU
|
||||||
{ED8E11B7-786F-4EFF-9E4C-B937B7A2DE89}.Release|x86.ActiveCfg = Release|Any CPU
|
{ED8E11B7-786F-4EFF-9E4C-B937B7A2DE89}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
{ED8E11B7-786F-4EFF-9E4C-B937B7A2DE89}.Release|x86.Build.0 = Release|Any CPU
|
{ED8E11B7-786F-4EFF-9E4C-B937B7A2DE89}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
{462A3B8E-A5D4-4539-8469-1647B47AB2A8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{462A3B8E-A5D4-4539-8469-1647B47AB2A8}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{462A3B8E-A5D4-4539-8469-1647B47AB2A8}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{462A3B8E-A5D4-4539-8469-1647B47AB2A8}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{462A3B8E-A5D4-4539-8469-1647B47AB2A8}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{462A3B8E-A5D4-4539-8469-1647B47AB2A8}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{462A3B8E-A5D4-4539-8469-1647B47AB2A8}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{462A3B8E-A5D4-4539-8469-1647B47AB2A8}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{462A3B8E-A5D4-4539-8469-1647B47AB2A8}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{462A3B8E-A5D4-4539-8469-1647B47AB2A8}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{462A3B8E-A5D4-4539-8469-1647B47AB2A8}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{462A3B8E-A5D4-4539-8469-1647B47AB2A8}.Release|x86.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|||||||
@@ -1,248 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using System.Timers;
|
|
||||||
using Avalonia;
|
|
||||||
using Avalonia.Controls;
|
|
||||||
using Avalonia.Interactivity;
|
|
||||||
using Avalonia.Markup.Xaml;
|
|
||||||
using Avalonia.Media.Imaging;
|
|
||||||
using Avalonia.Platform;
|
|
||||||
using Avalonia.Threading;
|
|
||||||
|
|
||||||
namespace RedBookPlayer.GUI
|
|
||||||
{
|
|
||||||
public class PlayerView : UserControl
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Read-only access to the view model
|
|
||||||
/// </summary>
|
|
||||||
public PlayerViewModel PlayerViewModel => DataContext as PlayerViewModel;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Set of images representing the digits for the UI
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// TODO: Does it make sense to have this as an array?
|
|
||||||
/// </remarks>
|
|
||||||
private Image[] _digits;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Timer for performing UI updates
|
|
||||||
/// </summary>
|
|
||||||
private Timer _updateTimer;
|
|
||||||
|
|
||||||
public PlayerView() => InitializeComponent(null);
|
|
||||||
|
|
||||||
public PlayerView(string xaml) => InitializeComponent(xaml);
|
|
||||||
|
|
||||||
#region Helpers
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Generate a path selection dialog box
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>User-selected path, if possible</returns>
|
|
||||||
public async Task<string> GetPath()
|
|
||||||
{
|
|
||||||
var dialog = new OpenFileDialog { AllowMultiple = false };
|
|
||||||
List<string> knownExtensions = new Aaru.DiscImages.AaruFormat().KnownExtensions.ToList();
|
|
||||||
dialog.Filters.Add(new FileDialogFilter()
|
|
||||||
{
|
|
||||||
Name = "Aaru Image Format (*" + string.Join(", *", knownExtensions) + ")",
|
|
||||||
Extensions = knownExtensions.ConvertAll(e => e.TrimStart('.'))
|
|
||||||
});
|
|
||||||
|
|
||||||
return (await dialog.ShowAsync((Window)Parent.Parent))?.FirstOrDefault();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Load an image from the path
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="path">Path to the image to load</param>
|
|
||||||
public async Task<bool> LoadImage(string path)
|
|
||||||
{
|
|
||||||
bool result = await Dispatcher.UIThread.InvokeAsync(() =>
|
|
||||||
{
|
|
||||||
PlayerViewModel.Init(path, App.Settings.AutoPlay, App.Settings.Volume);
|
|
||||||
return PlayerViewModel.Initialized;
|
|
||||||
});
|
|
||||||
|
|
||||||
if(result)
|
|
||||||
{
|
|
||||||
await Dispatcher.UIThread.InvokeAsync(() =>
|
|
||||||
{
|
|
||||||
MainWindow.Instance.Title = "RedBookPlayer - " + path.Split('/').Last().Split('\\').Last();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Load the png image for a given character based on the theme
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="character">Character to load the image for</param>
|
|
||||||
/// <returns>Bitmap representing the loaded image</returns>
|
|
||||||
/// <remarks>
|
|
||||||
/// TODO: Currently assumes that an image must always exist
|
|
||||||
/// </remarks>
|
|
||||||
private Bitmap GetBitmap(char character)
|
|
||||||
{
|
|
||||||
if(App.Settings.SelectedTheme == "default")
|
|
||||||
{
|
|
||||||
IAssetLoader assets = AvaloniaLocator.Current.GetService<IAssetLoader>();
|
|
||||||
|
|
||||||
return new Bitmap(assets.Open(new Uri($"avares://RedBookPlayer/Assets/{character}.png")));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
string themeDirectory = $"{Directory.GetCurrentDirectory()}/themes/{App.Settings.SelectedTheme}";
|
|
||||||
using FileStream stream = File.Open($"{themeDirectory}/{character}.png", FileMode.Open);
|
|
||||||
return new Bitmap(stream);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initialize the UI based on the currently selected theme
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="xaml">XAML data representing the theme, null for default</param>
|
|
||||||
private void InitializeComponent(string xaml)
|
|
||||||
{
|
|
||||||
DataContext = new PlayerViewModel();
|
|
||||||
|
|
||||||
// Load the theme
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if(xaml != null)
|
|
||||||
new AvaloniaXamlLoader().Load(xaml, null, this);
|
|
||||||
else
|
|
||||||
AvaloniaXamlLoader.Load(this);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
AvaloniaXamlLoader.Load(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
InitializeDigits();
|
|
||||||
|
|
||||||
_updateTimer = new Timer(1000 / 60);
|
|
||||||
|
|
||||||
_updateTimer.Elapsed += (sender, e) =>
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
UpdateView(sender, e);
|
|
||||||
}
|
|
||||||
catch(Exception ex)
|
|
||||||
{
|
|
||||||
Console.WriteLine(ex);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
_updateTimer.AutoReset = true;
|
|
||||||
_updateTimer.Start();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initialize the displayed digits array
|
|
||||||
/// </summary>
|
|
||||||
private void InitializeDigits()
|
|
||||||
{
|
|
||||||
_digits = new Image[]
|
|
||||||
{
|
|
||||||
this.FindControl<Image>("TrackDigit1"),
|
|
||||||
this.FindControl<Image>("TrackDigit2"),
|
|
||||||
|
|
||||||
this.FindControl<Image>("IndexDigit1"),
|
|
||||||
this.FindControl<Image>("IndexDigit2"),
|
|
||||||
|
|
||||||
this.FindControl<Image>("TimeDigit1"),
|
|
||||||
this.FindControl<Image>("TimeDigit2"),
|
|
||||||
this.FindControl<Image>("TimeDigit3"),
|
|
||||||
this.FindControl<Image>("TimeDigit4"),
|
|
||||||
this.FindControl<Image>("TimeDigit5"),
|
|
||||||
this.FindControl<Image>("TimeDigit6"),
|
|
||||||
|
|
||||||
this.FindControl<Image>("TotalTracksDigit1"),
|
|
||||||
this.FindControl<Image>("TotalTracksDigit2"),
|
|
||||||
|
|
||||||
this.FindControl<Image>("TotalIndexesDigit1"),
|
|
||||||
this.FindControl<Image>("TotalIndexesDigit2"),
|
|
||||||
|
|
||||||
this.FindControl<Image>("TotalTimeDigit1"),
|
|
||||||
this.FindControl<Image>("TotalTimeDigit2"),
|
|
||||||
this.FindControl<Image>("TotalTimeDigit3"),
|
|
||||||
this.FindControl<Image>("TotalTimeDigit4"),
|
|
||||||
this.FindControl<Image>("TotalTimeDigit5"),
|
|
||||||
this.FindControl<Image>("TotalTimeDigit6"),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Update the UI with the most recent information from the Player
|
|
||||||
/// </summary>
|
|
||||||
private void UpdateView(object sender, ElapsedEventArgs e)
|
|
||||||
{
|
|
||||||
Dispatcher.UIThread.InvokeAsync(() =>
|
|
||||||
{
|
|
||||||
string digitString = PlayerViewModel.GenerateDigitString();
|
|
||||||
for (int i = 0; i < _digits.Length; i++)
|
|
||||||
{
|
|
||||||
if (_digits[i] != null)
|
|
||||||
_digits[i].Source = GetBitmap(digitString[i]);
|
|
||||||
}
|
|
||||||
|
|
||||||
PlayerViewModel?.UpdateView();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Event Handlers
|
|
||||||
|
|
||||||
public async void LoadButton_Click(object sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
string path = await GetPath();
|
|
||||||
if (path == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
await LoadImage(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void PlayButton_Click(object sender, RoutedEventArgs e) => PlayerViewModel.Playing = true;
|
|
||||||
|
|
||||||
public void PauseButton_Click(object sender, RoutedEventArgs e) => PlayerViewModel.Playing = false;
|
|
||||||
|
|
||||||
public void PlayPauseButton_Click(object sender, RoutedEventArgs e) => PlayerViewModel.Playing = !(PlayerViewModel.Playing ?? false);
|
|
||||||
|
|
||||||
public void StopButton_Click(object sender, RoutedEventArgs e) => PlayerViewModel.Playing = null;
|
|
||||||
|
|
||||||
public void NextTrackButton_Click(object sender, RoutedEventArgs e) => PlayerViewModel.NextTrack();
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
public void FastForwardButton_Click(object sender, RoutedEventArgs e) => PlayerViewModel.FastForward();
|
|
||||||
|
|
||||||
public void RewindButton_Click(object sender, RoutedEventArgs e) => PlayerViewModel.Rewind();
|
|
||||||
|
|
||||||
public void VolumeUpButton_Click(object sender, RoutedEventArgs e) => PlayerViewModel.Volume++;
|
|
||||||
|
|
||||||
public void VolumeDownButton_Click(object sender, RoutedEventArgs e) => PlayerViewModel.Volume--;
|
|
||||||
|
|
||||||
public void MuteToggleButton_Click(object sender, RoutedEventArgs e) => PlayerViewModel.ToggleMute();
|
|
||||||
|
|
||||||
public void EnableDeEmphasisButton_Click(object sender, RoutedEventArgs e) => PlayerViewModel.ApplyDeEmphasis = true;
|
|
||||||
|
|
||||||
public void DisableDeEmphasisButton_Click(object sender, RoutedEventArgs e) => PlayerViewModel.ApplyDeEmphasis = false;
|
|
||||||
|
|
||||||
public void EnableDisableDeEmphasisButton_Click(object sender, RoutedEventArgs e) => PlayerViewModel.ApplyDeEmphasis = !PlayerViewModel.ApplyDeEmphasis;
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,270 +0,0 @@
|
|||||||
using System.Linq;
|
|
||||||
using Aaru.CommonTypes.Enums;
|
|
||||||
using ReactiveUI;
|
|
||||||
using RedBookPlayer.Discs;
|
|
||||||
using RedBookPlayer.Hardware;
|
|
||||||
|
|
||||||
namespace RedBookPlayer.GUI
|
|
||||||
{
|
|
||||||
public class PlayerViewModel : ReactiveObject
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Player representing the internal state
|
|
||||||
/// </summary>
|
|
||||||
private Player _player;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Last volume for mute toggling
|
|
||||||
/// </summary>
|
|
||||||
private int? _lastVolume = null;
|
|
||||||
|
|
||||||
#region Player Status
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Indicate if the model is ready to be used
|
|
||||||
/// </summary>
|
|
||||||
public bool Initialized => _player?.Initialized ?? false;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Indicate the player state
|
|
||||||
/// </summary>
|
|
||||||
public bool? Playing
|
|
||||||
{
|
|
||||||
get => _player?.Playing ?? false;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
if(_player != null)
|
|
||||||
_player.Playing = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Indicate the current playback volume
|
|
||||||
/// </summary>
|
|
||||||
public int Volume
|
|
||||||
{
|
|
||||||
get => _player?.Volume ?? 100;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
if(_player != null)
|
|
||||||
_player.Volume = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Indicates if de-emphasis should be applied
|
|
||||||
/// </summary>
|
|
||||||
public bool ApplyDeEmphasis
|
|
||||||
{
|
|
||||||
get => _player?.ApplyDeEmphasis ?? false;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
if(_player != null)
|
|
||||||
_player.ApplyDeEmphasis = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Model-Provided Playback Information
|
|
||||||
|
|
||||||
private ulong _currentSector;
|
|
||||||
public ulong CurrentSector
|
|
||||||
{
|
|
||||||
get => _currentSector;
|
|
||||||
set => this.RaiseAndSetIfChanged(ref _currentSector, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public int CurrentFrame => (int)(_currentSector / (75 * 60));
|
|
||||||
public int CurrentSecond => (int)(_currentSector / 75 % 60);
|
|
||||||
public int CurrentMinute => (int)(_currentSector % 75);
|
|
||||||
|
|
||||||
private ulong _totalSectors;
|
|
||||||
public ulong TotalSectors
|
|
||||||
{
|
|
||||||
get => _totalSectors;
|
|
||||||
set => this.RaiseAndSetIfChanged(ref _totalSectors, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public int TotalFrames => (int)(_totalSectors / (75 * 60));
|
|
||||||
public int TotalSeconds => (int)(_totalSectors / 75 % 60);
|
|
||||||
public int TotalMinutes => (int)(_totalSectors % 75);
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Disc Flags
|
|
||||||
|
|
||||||
private bool _quadChannel;
|
|
||||||
public bool QuadChannel
|
|
||||||
{
|
|
||||||
get => _quadChannel;
|
|
||||||
set => this.RaiseAndSetIfChanged(ref _quadChannel, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool _isDataTrack;
|
|
||||||
public bool IsDataTrack
|
|
||||||
{
|
|
||||||
get => _isDataTrack;
|
|
||||||
set => this.RaiseAndSetIfChanged(ref _isDataTrack, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool _copyAllowed;
|
|
||||||
public bool CopyAllowed
|
|
||||||
{
|
|
||||||
get => _copyAllowed;
|
|
||||||
set => this.RaiseAndSetIfChanged(ref _copyAllowed, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool _trackHasEmphasis;
|
|
||||||
public bool TrackHasEmphasis
|
|
||||||
{
|
|
||||||
get => _trackHasEmphasis;
|
|
||||||
set => this.RaiseAndSetIfChanged(ref _trackHasEmphasis, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool _hiddenTrack;
|
|
||||||
public bool HiddenTrack
|
|
||||||
{
|
|
||||||
get => _hiddenTrack;
|
|
||||||
set => this.RaiseAndSetIfChanged(ref _hiddenTrack, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initialize the view model with a given image path
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="path">Path to the disc image</param>
|
|
||||||
/// <param name="autoPlay">True if playback should begin immediately, false otherwise</param>
|
|
||||||
/// <param name="defaultVolume">Default volume between 0 and 100 to use when starting playback</param>
|
|
||||||
public void Init(string path, 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, autoPlay, defaultVolume);
|
|
||||||
if(Initialized)
|
|
||||||
UpdateView();
|
|
||||||
}
|
|
||||||
|
|
||||||
#region Playback
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Move to the next playable track
|
|
||||||
/// </summary>
|
|
||||||
public void NextTrack() => _player?.NextTrack();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Move to the previous playable track
|
|
||||||
/// </summary>
|
|
||||||
public void PreviousTrack() => _player?.PreviousTrack();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Move to the next index
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="changeTrack">True if index changes can trigger a track change, false otherwise</param>
|
|
||||||
public void NextIndex(bool changeTrack) => _player?.NextIndex(changeTrack);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Move to the previous index
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="changeTrack">True if index changes can trigger a track change, false otherwise</param>
|
|
||||||
public void PreviousIndex(bool changeTrack) => _player?.PreviousIndex(changeTrack);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Fast-forward playback by 75 sectors, if possible
|
|
||||||
/// </summary>
|
|
||||||
public void FastForward() => _player?.FastForward();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Rewind playback by 75 sectors, if possible
|
|
||||||
/// </summary>
|
|
||||||
public void Rewind() => _player?.Rewind();
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Helpers
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Generate the digit string to be interpreted by the frontend
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>String representing the digits for the frontend</returns>
|
|
||||||
public string GenerateDigitString()
|
|
||||||
{
|
|
||||||
// If the disc isn't initialized, return all '-' characters
|
|
||||||
if(_player?.OpticalDisc == null || !_player.OpticalDisc.Initialized)
|
|
||||||
return string.Empty.PadLeft(20, '-');
|
|
||||||
|
|
||||||
// Otherwise, take the current time into account
|
|
||||||
ulong sectorTime = _player.GetCurrentSectorTime();
|
|
||||||
|
|
||||||
int[] numbers = new int[]
|
|
||||||
{
|
|
||||||
_player.OpticalDisc.CurrentTrackNumber + 1,
|
|
||||||
_player.OpticalDisc.CurrentTrackIndex,
|
|
||||||
|
|
||||||
(int)(sectorTime / (75 * 60)),
|
|
||||||
(int)(sectorTime / 75 % 60),
|
|
||||||
(int)(sectorTime % 75),
|
|
||||||
|
|
||||||
_player.OpticalDisc.TotalTracks,
|
|
||||||
_player.OpticalDisc.TotalIndexes,
|
|
||||||
|
|
||||||
(int)(_player.OpticalDisc.TotalTime / (75 * 60)),
|
|
||||||
(int)(_player.OpticalDisc.TotalTime / 75 % 60),
|
|
||||||
(int)(_player.OpticalDisc.TotalTime % 75),
|
|
||||||
};
|
|
||||||
|
|
||||||
return string.Join("", numbers.Select(i => i.ToString().PadLeft(2, '0').Substring(0, 2)));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Temporarily mute playback
|
|
||||||
/// </summary>
|
|
||||||
public void ToggleMute()
|
|
||||||
{
|
|
||||||
if(_lastVolume == null)
|
|
||||||
{
|
|
||||||
_lastVolume = Volume;
|
|
||||||
Volume = 0;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Volume = _lastVolume.Value;
|
|
||||||
_lastVolume = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Update the UI from the internal player
|
|
||||||
/// </summary>
|
|
||||||
public void UpdateView()
|
|
||||||
{
|
|
||||||
if(_player?.Initialized != true)
|
|
||||||
return;
|
|
||||||
|
|
||||||
CurrentSector = _player.GetCurrentSectorTime();
|
|
||||||
TotalSectors = _player.OpticalDisc.TotalTime;
|
|
||||||
|
|
||||||
HiddenTrack = _player.OpticalDisc.TimeOffset > 150;
|
|
||||||
|
|
||||||
if(_player.OpticalDisc is CompactDisc compactDisc)
|
|
||||||
{
|
|
||||||
QuadChannel = compactDisc.QuadChannel;
|
|
||||||
IsDataTrack = compactDisc.IsDataTrack;
|
|
||||||
CopyAllowed = compactDisc.CopyAllowed;
|
|
||||||
TrackHasEmphasis = compactDisc.TrackHasEmphasis;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
QuadChannel = false;
|
|
||||||
IsDataTrack = _player.OpticalDisc.TrackType != TrackType.Audio;
|
|
||||||
CopyAllowed = false;
|
|
||||||
TrackHasEmphasis = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,258 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.IO;
|
|
||||||
using Aaru.DiscImages;
|
|
||||||
using Aaru.Filters;
|
|
||||||
using RedBookPlayer.Discs;
|
|
||||||
|
|
||||||
namespace RedBookPlayer.Hardware
|
|
||||||
{
|
|
||||||
public class Player
|
|
||||||
{
|
|
||||||
#region Public Fields
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Indicate if the player is ready to be used
|
|
||||||
/// </summary>
|
|
||||||
public bool Initialized { get; private set; } = false;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// OpticalDisc object
|
|
||||||
/// </summary>
|
|
||||||
public OpticalDisc OpticalDisc { get; private set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Indicate if the disc is playing
|
|
||||||
/// </summary>
|
|
||||||
public bool? Playing
|
|
||||||
{
|
|
||||||
get => _soundOutput?.Playing;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
if(OpticalDisc == null || !OpticalDisc.Initialized)
|
|
||||||
return;
|
|
||||||
|
|
||||||
// If the playing state has not changed, do nothing
|
|
||||||
if(value == _soundOutput?.Playing)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if(value == true)
|
|
||||||
{
|
|
||||||
_soundOutput.Play();
|
|
||||||
OpticalDisc.SetTotalIndexes();
|
|
||||||
}
|
|
||||||
else if(value == false)
|
|
||||||
{
|
|
||||||
_soundOutput.Stop();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_soundOutput.Stop();
|
|
||||||
OpticalDisc.LoadFirstTrack();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Indicate the current playback volume
|
|
||||||
/// </summary>
|
|
||||||
public int Volume
|
|
||||||
{
|
|
||||||
get => _soundOutput?.Volume ?? 100;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
if(_soundOutput != null)
|
|
||||||
_soundOutput.Volume = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Indicates if de-emphasis should be applied
|
|
||||||
/// </summary>
|
|
||||||
public bool ApplyDeEmphasis
|
|
||||||
{
|
|
||||||
get => _soundOutput?.ApplyDeEmphasis ?? false;
|
|
||||||
set => _soundOutput?.SetDeEmphasis(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Private State Variables
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Sound output handling class
|
|
||||||
/// </summary>
|
|
||||||
public SoundOutput _soundOutput;
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Create a new Player from a given image path
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="path">Path to the disc image</param>
|
|
||||||
/// <param name="autoPlay">True if playback should begin immediately, false otherwise</param>
|
|
||||||
/// <param name="defaultVolume">Default volume between 0 and 100 to use when starting playback</param>
|
|
||||||
public Player(string path, bool autoPlay = false, int defaultVolume = 100)
|
|
||||||
{
|
|
||||||
// Set the internal state for initialization
|
|
||||||
Initialized = false;
|
|
||||||
_soundOutput = new SoundOutput();
|
|
||||||
_soundOutput.ApplyDeEmphasis = false;
|
|
||||||
OpticalDisc = null;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Validate the image exists
|
|
||||||
if(string.IsNullOrWhiteSpace(path) || !File.Exists(path))
|
|
||||||
return;
|
|
||||||
|
|
||||||
// Load the disc image to memory
|
|
||||||
var image = new AaruFormat();
|
|
||||||
var filter = new ZZZNoFilter();
|
|
||||||
filter.Open(path);
|
|
||||||
image.Open(filter);
|
|
||||||
|
|
||||||
// Generate and instantiate the disc
|
|
||||||
OpticalDisc = OpticalDiscFactory.GenerateFromImage(image, autoPlay);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
// All errors mean an invalid image in some way
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize the sound output
|
|
||||||
_soundOutput.Init(OpticalDisc, autoPlay, defaultVolume);
|
|
||||||
if(_soundOutput == null || !_soundOutput.Initialized)
|
|
||||||
return;
|
|
||||||
|
|
||||||
// Mark the player as ready
|
|
||||||
Initialized = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
#region Playback
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Move to the next playable track
|
|
||||||
/// </summary>
|
|
||||||
public void NextTrack()
|
|
||||||
{
|
|
||||||
if(OpticalDisc == null || !OpticalDisc.Initialized)
|
|
||||||
return;
|
|
||||||
|
|
||||||
bool? wasPlaying = Playing;
|
|
||||||
if(wasPlaying == true) Playing = false;
|
|
||||||
|
|
||||||
OpticalDisc.NextTrack();
|
|
||||||
if(OpticalDisc is CompactDisc compactDisc)
|
|
||||||
_soundOutput.ApplyDeEmphasis = compactDisc.TrackHasEmphasis;
|
|
||||||
|
|
||||||
if(wasPlaying == true) Playing = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Move to the previous playable track
|
|
||||||
/// </summary>
|
|
||||||
public void PreviousTrack()
|
|
||||||
{
|
|
||||||
if(OpticalDisc == null || !OpticalDisc.Initialized)
|
|
||||||
return;
|
|
||||||
|
|
||||||
bool? wasPlaying = Playing;
|
|
||||||
if(wasPlaying == true) Playing = false;
|
|
||||||
|
|
||||||
OpticalDisc.PreviousTrack();
|
|
||||||
if(OpticalDisc is CompactDisc compactDisc)
|
|
||||||
_soundOutput.ApplyDeEmphasis = compactDisc.TrackHasEmphasis;
|
|
||||||
|
|
||||||
if(wasPlaying == true) Playing = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Move to the next index
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="changeTrack">True if index changes can trigger a track change, false otherwise</param>
|
|
||||||
public void NextIndex(bool changeTrack)
|
|
||||||
{
|
|
||||||
if(OpticalDisc == null || !OpticalDisc.Initialized)
|
|
||||||
return;
|
|
||||||
|
|
||||||
bool? wasPlaying = Playing;
|
|
||||||
if(wasPlaying == true) Playing = false;
|
|
||||||
|
|
||||||
OpticalDisc.NextIndex(changeTrack);
|
|
||||||
if(OpticalDisc is CompactDisc compactDisc)
|
|
||||||
_soundOutput.ApplyDeEmphasis = compactDisc.TrackHasEmphasis;
|
|
||||||
|
|
||||||
if(wasPlaying == true) Playing = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Move to the previous index
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="changeTrack">True if index changes can trigger a track change, false otherwise</param>
|
|
||||||
public void PreviousIndex(bool changeTrack)
|
|
||||||
{
|
|
||||||
if(OpticalDisc == null || !OpticalDisc.Initialized)
|
|
||||||
return;
|
|
||||||
|
|
||||||
bool? wasPlaying = Playing;
|
|
||||||
if(wasPlaying == true) Playing = false;
|
|
||||||
|
|
||||||
OpticalDisc.PreviousIndex(changeTrack);
|
|
||||||
if(OpticalDisc is CompactDisc compactDisc)
|
|
||||||
_soundOutput.ApplyDeEmphasis = compactDisc.TrackHasEmphasis;
|
|
||||||
|
|
||||||
if(wasPlaying == true) Playing = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Fast-forward playback by 75 sectors, if possible
|
|
||||||
/// </summary>
|
|
||||||
public void FastForward()
|
|
||||||
{
|
|
||||||
if(OpticalDisc == null || !OpticalDisc.Initialized)
|
|
||||||
return;
|
|
||||||
|
|
||||||
OpticalDisc.CurrentSector = Math.Min(OpticalDisc.TotalSectors, OpticalDisc.CurrentSector + 75);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Rewind playback by 75 sectors, if possible
|
|
||||||
/// </summary>
|
|
||||||
public void Rewind()
|
|
||||||
{
|
|
||||||
if(OpticalDisc == null || !OpticalDisc.Initialized)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if(OpticalDisc.CurrentSector >= 75)
|
|
||||||
OpticalDisc.CurrentSector -= 75;
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Helpers
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get current sector time, accounting for offsets
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>ulong representing the current sector time</returns>
|
|
||||||
public ulong GetCurrentSectorTime()
|
|
||||||
{
|
|
||||||
ulong sectorTime = OpticalDisc.CurrentSector;
|
|
||||||
if (OpticalDisc.SectionStartSector != 0)
|
|
||||||
sectorTime -= OpticalDisc.SectionStartSector;
|
|
||||||
else
|
|
||||||
sectorTime += OpticalDisc.TimeOffset;
|
|
||||||
|
|
||||||
return sectorTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Set if de-emphasis should be applied
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="apply">True to enable, false to disable</param>
|
|
||||||
public void SetDeEmphasis(bool apply) => _soundOutput?.SetDeEmphasis(apply);
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
}
|
|
||||||