diff --git a/RedBookPlayer.sln b/RedBookPlayer.sln
index 3d1090b..a4fee71 100644
--- a/RedBookPlayer.sln
+++ b/RedBookPlayer.sln
@@ -1,31 +1,37 @@
Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio 15
-VisualStudioVersion = 15.0.26124.0
+# Visual Studio Version 16
+VisualStudioVersion = 16.0.31321.278
MinimumVisualStudioVersion = 15.0.26124.0
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RedBookPlayer", "RedBookPlayer\RedBookPlayer.csproj", "{94944959-0352-4ABF-9C5C-19FF33747ECE}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RedBookPlayer", "RedBookPlayer\RedBookPlayer.csproj", "{94944959-0352-4ABF-9C5C-19FF33747ECE}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CSCore", "cscore\CSCore\CSCore.csproj", "{C97C30F9-30F3-4BA2-9A02-CF8644DEB616}"
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "cscore", "cscore", "{9A371299-4C59-4E46-9C3B-4FE024017491}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aaru.CommonTypes", "Aaru\Aaru.CommonTypes\Aaru.CommonTypes.csproj", "{F2B84194-26EB-4227-B1C5-6602517E85AE}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CSCore", "cscore\CSCore\CSCore.csproj", "{C81E7637-D25A-4545-8E27-4D83D973F4DC}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aaru.Decoders", "Aaru\Aaru.Decoders\Aaru.Decoders.csproj", "{0BEB3088-B634-4289-AE17-CDF2D25D00D5}"
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Aaru", "Aaru", "{BAC4D43B-B6B0-495F-A147-1D4761D60134}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aaru.Images", "Aaru\Aaru.Images\Aaru.Images.csproj", "{74032CBC-339B-42F3-AF6F-E96C261F3E6A}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aaru.CommonTypes", "Aaru\Aaru.CommonTypes\Aaru.CommonTypes.csproj", "{F2B84194-26EB-4227-B1C5-6602517E85AE}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aaru.Checksums", "Aaru\Aaru.Checksums\Aaru.Checksums.csproj", "{CC48B324-A532-4A45-87A6-6F91F7141E8D}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aaru.Decoders", "Aaru\Aaru.Decoders\Aaru.Decoders.csproj", "{0BEB3088-B634-4289-AE17-CDF2D25D00D5}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aaru.Console", "Aaru\Aaru.Console\Aaru.Console.csproj", "{CCAA7AFE-C094-4D82-A66D-630DE8A3F545}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aaru.Helpers", "Aaru\Aaru.Helpers\Aaru.Helpers.csproj", "{F8BDF57B-1571-4CD0-84B3-B422088D359A}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aaru.Helpers", "Aaru\Aaru.Helpers\Aaru.Helpers.csproj", "{F8BDF57B-1571-4CD0-84B3-B422088D359A}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aaru.Images", "Aaru\Aaru.Images\Aaru.Images.csproj", "{74032CBC-339B-42F3-AF6F-E96C261F3E6A}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aaru.Compression", "Aaru\Aaru.Compression\Aaru.Compression.csproj", "{858398D1-7321-4763-8BAB-56BBFEC74E29}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aaru.Console", "Aaru\Aaru.Console\Aaru.Console.csproj", "{CCAA7AFE-C094-4D82-A66D-630DE8A3F545}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aaru.Filters", "Aaru\Aaru.Filters\Aaru.Filters.csproj", "{D571B8EF-903D-4353-BDD5-B834F9F029EF}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aaru.Checksums", "Aaru\Aaru.Checksums\Aaru.Checksums.csproj", "{CC48B324-A532-4A45-87A6-6F91F7141E8D}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CUETools.Codecs", "Aaru\cuetools.net\CUETools.Codecs\CUETools.Codecs.csproj", "{1E8EB4FB-C16D-437F-B54C-0026D32E9230}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aaru.Compression", "Aaru\Aaru.Compression\Aaru.Compression.csproj", "{858398D1-7321-4763-8BAB-56BBFEC74E29}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CUETools.Codecs.Flake", "Aaru\cuetools.net\CUETools.Codecs.Flake\CUETools.Codecs.Flake.csproj", "{6089CEBC-C88F-4A4C-8717-676F7CC427B9}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aaru.Filters", "Aaru\Aaru.Filters\Aaru.Filters.csproj", "{D571B8EF-903D-4353-BDD5-B834F9F029EF}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "cuetools.net", "cuetools.net", "{7B180FCA-A2BB-48C8-AF36-2300C033C476}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CUETools.Codecs", "Aaru\cuetools.net\CUETools.Codecs\CUETools.Codecs.csproj", "{8F4AD79C-D5E5-44C4-9F03-B0DF4EE8BBA0}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CUETools.Codecs.Flake", "Aaru\cuetools.net\CUETools.Codecs.Flake\CUETools.Codecs.Flake.csproj", "{ED8E11B7-786F-4EFF-9E4C-B937B7A2DE89}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -36,9 +42,6 @@ Global
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
- GlobalSection(SolutionProperties) = preSolution
- HideSolutionNode = FALSE
- EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{94944959-0352-4ABF-9C5C-19FF33747ECE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{94944959-0352-4ABF-9C5C-19FF33747ECE}.Debug|Any CPU.Build.0 = Debug|Any CPU
@@ -52,18 +55,18 @@ Global
{94944959-0352-4ABF-9C5C-19FF33747ECE}.Release|x64.Build.0 = Release|Any CPU
{94944959-0352-4ABF-9C5C-19FF33747ECE}.Release|x86.ActiveCfg = Release|Any CPU
{94944959-0352-4ABF-9C5C-19FF33747ECE}.Release|x86.Build.0 = Release|Any CPU
- {C97C30F9-30F3-4BA2-9A02-CF8644DEB616}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {C97C30F9-30F3-4BA2-9A02-CF8644DEB616}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {C97C30F9-30F3-4BA2-9A02-CF8644DEB616}.Debug|x64.ActiveCfg = Debug|Any CPU
- {C97C30F9-30F3-4BA2-9A02-CF8644DEB616}.Debug|x64.Build.0 = Debug|Any CPU
- {C97C30F9-30F3-4BA2-9A02-CF8644DEB616}.Debug|x86.ActiveCfg = Debug|Any CPU
- {C97C30F9-30F3-4BA2-9A02-CF8644DEB616}.Debug|x86.Build.0 = Debug|Any CPU
- {C97C30F9-30F3-4BA2-9A02-CF8644DEB616}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {C97C30F9-30F3-4BA2-9A02-CF8644DEB616}.Release|Any CPU.Build.0 = Release|Any CPU
- {C97C30F9-30F3-4BA2-9A02-CF8644DEB616}.Release|x64.ActiveCfg = Release|Any CPU
- {C97C30F9-30F3-4BA2-9A02-CF8644DEB616}.Release|x64.Build.0 = Release|Any CPU
- {C97C30F9-30F3-4BA2-9A02-CF8644DEB616}.Release|x86.ActiveCfg = Release|Any CPU
- {C97C30F9-30F3-4BA2-9A02-CF8644DEB616}.Release|x86.Build.0 = Release|Any CPU
+ {C81E7637-D25A-4545-8E27-4D83D973F4DC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {C81E7637-D25A-4545-8E27-4D83D973F4DC}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C81E7637-D25A-4545-8E27-4D83D973F4DC}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {C81E7637-D25A-4545-8E27-4D83D973F4DC}.Debug|x64.Build.0 = Debug|Any CPU
+ {C81E7637-D25A-4545-8E27-4D83D973F4DC}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {C81E7637-D25A-4545-8E27-4D83D973F4DC}.Debug|x86.Build.0 = Debug|Any CPU
+ {C81E7637-D25A-4545-8E27-4D83D973F4DC}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {C81E7637-D25A-4545-8E27-4D83D973F4DC}.Release|Any CPU.Build.0 = Release|Any CPU
+ {C81E7637-D25A-4545-8E27-4D83D973F4DC}.Release|x64.ActiveCfg = Release|Any CPU
+ {C81E7637-D25A-4545-8E27-4D83D973F4DC}.Release|x64.Build.0 = Release|Any CPU
+ {C81E7637-D25A-4545-8E27-4D83D973F4DC}.Release|x86.ActiveCfg = Release|Any CPU
+ {C81E7637-D25A-4545-8E27-4D83D973F4DC}.Release|x86.Build.0 = Release|Any CPU
{F2B84194-26EB-4227-B1C5-6602517E85AE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F2B84194-26EB-4227-B1C5-6602517E85AE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F2B84194-26EB-4227-B1C5-6602517E85AE}.Debug|x64.ActiveCfg = Debug|Any CPU
@@ -88,42 +91,6 @@ Global
{0BEB3088-B634-4289-AE17-CDF2D25D00D5}.Release|x64.Build.0 = Release|Any CPU
{0BEB3088-B634-4289-AE17-CDF2D25D00D5}.Release|x86.ActiveCfg = Release|Any CPU
{0BEB3088-B634-4289-AE17-CDF2D25D00D5}.Release|x86.Build.0 = Release|Any CPU
- {74032CBC-339B-42F3-AF6F-E96C261F3E6A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {74032CBC-339B-42F3-AF6F-E96C261F3E6A}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {74032CBC-339B-42F3-AF6F-E96C261F3E6A}.Debug|x64.ActiveCfg = Debug|Any CPU
- {74032CBC-339B-42F3-AF6F-E96C261F3E6A}.Debug|x64.Build.0 = Debug|Any CPU
- {74032CBC-339B-42F3-AF6F-E96C261F3E6A}.Debug|x86.ActiveCfg = Debug|Any CPU
- {74032CBC-339B-42F3-AF6F-E96C261F3E6A}.Debug|x86.Build.0 = Debug|Any CPU
- {74032CBC-339B-42F3-AF6F-E96C261F3E6A}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {74032CBC-339B-42F3-AF6F-E96C261F3E6A}.Release|Any CPU.Build.0 = Release|Any CPU
- {74032CBC-339B-42F3-AF6F-E96C261F3E6A}.Release|x64.ActiveCfg = Release|Any CPU
- {74032CBC-339B-42F3-AF6F-E96C261F3E6A}.Release|x64.Build.0 = Release|Any CPU
- {74032CBC-339B-42F3-AF6F-E96C261F3E6A}.Release|x86.ActiveCfg = Release|Any CPU
- {74032CBC-339B-42F3-AF6F-E96C261F3E6A}.Release|x86.Build.0 = Release|Any CPU
- {CC48B324-A532-4A45-87A6-6F91F7141E8D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {CC48B324-A532-4A45-87A6-6F91F7141E8D}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {CC48B324-A532-4A45-87A6-6F91F7141E8D}.Debug|x64.ActiveCfg = Debug|Any CPU
- {CC48B324-A532-4A45-87A6-6F91F7141E8D}.Debug|x64.Build.0 = Debug|Any CPU
- {CC48B324-A532-4A45-87A6-6F91F7141E8D}.Debug|x86.ActiveCfg = Debug|Any CPU
- {CC48B324-A532-4A45-87A6-6F91F7141E8D}.Debug|x86.Build.0 = Debug|Any CPU
- {CC48B324-A532-4A45-87A6-6F91F7141E8D}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {CC48B324-A532-4A45-87A6-6F91F7141E8D}.Release|Any CPU.Build.0 = Release|Any CPU
- {CC48B324-A532-4A45-87A6-6F91F7141E8D}.Release|x64.ActiveCfg = Release|Any CPU
- {CC48B324-A532-4A45-87A6-6F91F7141E8D}.Release|x64.Build.0 = Release|Any CPU
- {CC48B324-A532-4A45-87A6-6F91F7141E8D}.Release|x86.ActiveCfg = Release|Any CPU
- {CC48B324-A532-4A45-87A6-6F91F7141E8D}.Release|x86.Build.0 = Release|Any CPU
- {CCAA7AFE-C094-4D82-A66D-630DE8A3F545}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {CCAA7AFE-C094-4D82-A66D-630DE8A3F545}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {CCAA7AFE-C094-4D82-A66D-630DE8A3F545}.Debug|x64.ActiveCfg = Debug|Any CPU
- {CCAA7AFE-C094-4D82-A66D-630DE8A3F545}.Debug|x64.Build.0 = Debug|Any CPU
- {CCAA7AFE-C094-4D82-A66D-630DE8A3F545}.Debug|x86.ActiveCfg = Debug|Any CPU
- {CCAA7AFE-C094-4D82-A66D-630DE8A3F545}.Debug|x86.Build.0 = Debug|Any CPU
- {CCAA7AFE-C094-4D82-A66D-630DE8A3F545}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {CCAA7AFE-C094-4D82-A66D-630DE8A3F545}.Release|Any CPU.Build.0 = Release|Any CPU
- {CCAA7AFE-C094-4D82-A66D-630DE8A3F545}.Release|x64.ActiveCfg = Release|Any CPU
- {CCAA7AFE-C094-4D82-A66D-630DE8A3F545}.Release|x64.Build.0 = Release|Any CPU
- {CCAA7AFE-C094-4D82-A66D-630DE8A3F545}.Release|x86.ActiveCfg = Release|Any CPU
- {CCAA7AFE-C094-4D82-A66D-630DE8A3F545}.Release|x86.Build.0 = Release|Any CPU
{F8BDF57B-1571-4CD0-84B3-B422088D359A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F8BDF57B-1571-4CD0-84B3-B422088D359A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F8BDF57B-1571-4CD0-84B3-B422088D359A}.Debug|x64.ActiveCfg = Debug|Any CPU
@@ -136,6 +103,42 @@ Global
{F8BDF57B-1571-4CD0-84B3-B422088D359A}.Release|x64.Build.0 = Release|Any CPU
{F8BDF57B-1571-4CD0-84B3-B422088D359A}.Release|x86.ActiveCfg = Release|Any CPU
{F8BDF57B-1571-4CD0-84B3-B422088D359A}.Release|x86.Build.0 = Release|Any CPU
+ {74032CBC-339B-42F3-AF6F-E96C261F3E6A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {74032CBC-339B-42F3-AF6F-E96C261F3E6A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {74032CBC-339B-42F3-AF6F-E96C261F3E6A}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {74032CBC-339B-42F3-AF6F-E96C261F3E6A}.Debug|x64.Build.0 = Debug|Any CPU
+ {74032CBC-339B-42F3-AF6F-E96C261F3E6A}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {74032CBC-339B-42F3-AF6F-E96C261F3E6A}.Debug|x86.Build.0 = Debug|Any CPU
+ {74032CBC-339B-42F3-AF6F-E96C261F3E6A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {74032CBC-339B-42F3-AF6F-E96C261F3E6A}.Release|Any CPU.Build.0 = Release|Any CPU
+ {74032CBC-339B-42F3-AF6F-E96C261F3E6A}.Release|x64.ActiveCfg = Release|Any CPU
+ {74032CBC-339B-42F3-AF6F-E96C261F3E6A}.Release|x64.Build.0 = Release|Any CPU
+ {74032CBC-339B-42F3-AF6F-E96C261F3E6A}.Release|x86.ActiveCfg = Release|Any CPU
+ {74032CBC-339B-42F3-AF6F-E96C261F3E6A}.Release|x86.Build.0 = Release|Any CPU
+ {CCAA7AFE-C094-4D82-A66D-630DE8A3F545}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {CCAA7AFE-C094-4D82-A66D-630DE8A3F545}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {CCAA7AFE-C094-4D82-A66D-630DE8A3F545}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {CCAA7AFE-C094-4D82-A66D-630DE8A3F545}.Debug|x64.Build.0 = Debug|Any CPU
+ {CCAA7AFE-C094-4D82-A66D-630DE8A3F545}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {CCAA7AFE-C094-4D82-A66D-630DE8A3F545}.Debug|x86.Build.0 = Debug|Any CPU
+ {CCAA7AFE-C094-4D82-A66D-630DE8A3F545}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {CCAA7AFE-C094-4D82-A66D-630DE8A3F545}.Release|Any CPU.Build.0 = Release|Any CPU
+ {CCAA7AFE-C094-4D82-A66D-630DE8A3F545}.Release|x64.ActiveCfg = Release|Any CPU
+ {CCAA7AFE-C094-4D82-A66D-630DE8A3F545}.Release|x64.Build.0 = Release|Any CPU
+ {CCAA7AFE-C094-4D82-A66D-630DE8A3F545}.Release|x86.ActiveCfg = Release|Any CPU
+ {CCAA7AFE-C094-4D82-A66D-630DE8A3F545}.Release|x86.Build.0 = Release|Any CPU
+ {CC48B324-A532-4A45-87A6-6F91F7141E8D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {CC48B324-A532-4A45-87A6-6F91F7141E8D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {CC48B324-A532-4A45-87A6-6F91F7141E8D}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {CC48B324-A532-4A45-87A6-6F91F7141E8D}.Debug|x64.Build.0 = Debug|Any CPU
+ {CC48B324-A532-4A45-87A6-6F91F7141E8D}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {CC48B324-A532-4A45-87A6-6F91F7141E8D}.Debug|x86.Build.0 = Debug|Any CPU
+ {CC48B324-A532-4A45-87A6-6F91F7141E8D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {CC48B324-A532-4A45-87A6-6F91F7141E8D}.Release|Any CPU.Build.0 = Release|Any CPU
+ {CC48B324-A532-4A45-87A6-6F91F7141E8D}.Release|x64.ActiveCfg = Release|Any CPU
+ {CC48B324-A532-4A45-87A6-6F91F7141E8D}.Release|x64.Build.0 = Release|Any CPU
+ {CC48B324-A532-4A45-87A6-6F91F7141E8D}.Release|x86.ActiveCfg = Release|Any CPU
+ {CC48B324-A532-4A45-87A6-6F91F7141E8D}.Release|x86.Build.0 = Release|Any CPU
{858398D1-7321-4763-8BAB-56BBFEC74E29}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{858398D1-7321-4763-8BAB-56BBFEC74E29}.Debug|Any CPU.Build.0 = Debug|Any CPU
{858398D1-7321-4763-8BAB-56BBFEC74E29}.Debug|x64.ActiveCfg = Debug|Any CPU
@@ -160,29 +163,49 @@ Global
{D571B8EF-903D-4353-BDD5-B834F9F029EF}.Release|x64.Build.0 = Release|Any CPU
{D571B8EF-903D-4353-BDD5-B834F9F029EF}.Release|x86.ActiveCfg = Release|Any CPU
{D571B8EF-903D-4353-BDD5-B834F9F029EF}.Release|x86.Build.0 = Release|Any CPU
- {1E8EB4FB-C16D-437F-B54C-0026D32E9230}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {1E8EB4FB-C16D-437F-B54C-0026D32E9230}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {1E8EB4FB-C16D-437F-B54C-0026D32E9230}.Debug|x64.ActiveCfg = Debug|Any CPU
- {1E8EB4FB-C16D-437F-B54C-0026D32E9230}.Debug|x64.Build.0 = Debug|Any CPU
- {1E8EB4FB-C16D-437F-B54C-0026D32E9230}.Debug|x86.ActiveCfg = Debug|Any CPU
- {1E8EB4FB-C16D-437F-B54C-0026D32E9230}.Debug|x86.Build.0 = Debug|Any CPU
- {1E8EB4FB-C16D-437F-B54C-0026D32E9230}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {1E8EB4FB-C16D-437F-B54C-0026D32E9230}.Release|Any CPU.Build.0 = Release|Any CPU
- {1E8EB4FB-C16D-437F-B54C-0026D32E9230}.Release|x64.ActiveCfg = Release|Any CPU
- {1E8EB4FB-C16D-437F-B54C-0026D32E9230}.Release|x64.Build.0 = Release|Any CPU
- {1E8EB4FB-C16D-437F-B54C-0026D32E9230}.Release|x86.ActiveCfg = Release|Any CPU
- {1E8EB4FB-C16D-437F-B54C-0026D32E9230}.Release|x86.Build.0 = Release|Any CPU
- {6089CEBC-C88F-4A4C-8717-676F7CC427B9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {6089CEBC-C88F-4A4C-8717-676F7CC427B9}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {6089CEBC-C88F-4A4C-8717-676F7CC427B9}.Debug|x64.ActiveCfg = Debug|Any CPU
- {6089CEBC-C88F-4A4C-8717-676F7CC427B9}.Debug|x64.Build.0 = Debug|Any CPU
- {6089CEBC-C88F-4A4C-8717-676F7CC427B9}.Debug|x86.ActiveCfg = Debug|Any CPU
- {6089CEBC-C88F-4A4C-8717-676F7CC427B9}.Debug|x86.Build.0 = Debug|Any CPU
- {6089CEBC-C88F-4A4C-8717-676F7CC427B9}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {6089CEBC-C88F-4A4C-8717-676F7CC427B9}.Release|Any CPU.Build.0 = Release|Any CPU
- {6089CEBC-C88F-4A4C-8717-676F7CC427B9}.Release|x64.ActiveCfg = Release|Any CPU
- {6089CEBC-C88F-4A4C-8717-676F7CC427B9}.Release|x64.Build.0 = Release|Any CPU
- {6089CEBC-C88F-4A4C-8717-676F7CC427B9}.Release|x86.ActiveCfg = Release|Any CPU
- {6089CEBC-C88F-4A4C-8717-676F7CC427B9}.Release|x86.Build.0 = Release|Any CPU
+ {8F4AD79C-D5E5-44C4-9F03-B0DF4EE8BBA0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {8F4AD79C-D5E5-44C4-9F03-B0DF4EE8BBA0}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {8F4AD79C-D5E5-44C4-9F03-B0DF4EE8BBA0}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {8F4AD79C-D5E5-44C4-9F03-B0DF4EE8BBA0}.Debug|x64.Build.0 = Debug|Any CPU
+ {8F4AD79C-D5E5-44C4-9F03-B0DF4EE8BBA0}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {8F4AD79C-D5E5-44C4-9F03-B0DF4EE8BBA0}.Debug|x86.Build.0 = Debug|Any CPU
+ {8F4AD79C-D5E5-44C4-9F03-B0DF4EE8BBA0}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {8F4AD79C-D5E5-44C4-9F03-B0DF4EE8BBA0}.Release|Any CPU.Build.0 = Release|Any CPU
+ {8F4AD79C-D5E5-44C4-9F03-B0DF4EE8BBA0}.Release|x64.ActiveCfg = Release|Any CPU
+ {8F4AD79C-D5E5-44C4-9F03-B0DF4EE8BBA0}.Release|x64.Build.0 = Release|Any CPU
+ {8F4AD79C-D5E5-44C4-9F03-B0DF4EE8BBA0}.Release|x86.ActiveCfg = Release|Any CPU
+ {8F4AD79C-D5E5-44C4-9F03-B0DF4EE8BBA0}.Release|x86.Build.0 = Release|Any CPU
+ {ED8E11B7-786F-4EFF-9E4C-B937B7A2DE89}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {ED8E11B7-786F-4EFF-9E4C-B937B7A2DE89}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {ED8E11B7-786F-4EFF-9E4C-B937B7A2DE89}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {ED8E11B7-786F-4EFF-9E4C-B937B7A2DE89}.Debug|x64.Build.0 = Debug|Any CPU
+ {ED8E11B7-786F-4EFF-9E4C-B937B7A2DE89}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {ED8E11B7-786F-4EFF-9E4C-B937B7A2DE89}.Debug|x86.Build.0 = Debug|Any CPU
+ {ED8E11B7-786F-4EFF-9E4C-B937B7A2DE89}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {ED8E11B7-786F-4EFF-9E4C-B937B7A2DE89}.Release|Any CPU.Build.0 = Release|Any CPU
+ {ED8E11B7-786F-4EFF-9E4C-B937B7A2DE89}.Release|x64.ActiveCfg = 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.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {C81E7637-D25A-4545-8E27-4D83D973F4DC} = {9A371299-4C59-4E46-9C3B-4FE024017491}
+ {F2B84194-26EB-4227-B1C5-6602517E85AE} = {BAC4D43B-B6B0-495F-A147-1D4761D60134}
+ {0BEB3088-B634-4289-AE17-CDF2D25D00D5} = {BAC4D43B-B6B0-495F-A147-1D4761D60134}
+ {F8BDF57B-1571-4CD0-84B3-B422088D359A} = {BAC4D43B-B6B0-495F-A147-1D4761D60134}
+ {74032CBC-339B-42F3-AF6F-E96C261F3E6A} = {BAC4D43B-B6B0-495F-A147-1D4761D60134}
+ {CCAA7AFE-C094-4D82-A66D-630DE8A3F545} = {BAC4D43B-B6B0-495F-A147-1D4761D60134}
+ {CC48B324-A532-4A45-87A6-6F91F7141E8D} = {BAC4D43B-B6B0-495F-A147-1D4761D60134}
+ {858398D1-7321-4763-8BAB-56BBFEC74E29} = {BAC4D43B-B6B0-495F-A147-1D4761D60134}
+ {D571B8EF-903D-4353-BDD5-B834F9F029EF} = {BAC4D43B-B6B0-495F-A147-1D4761D60134}
+ {7B180FCA-A2BB-48C8-AF36-2300C033C476} = {BAC4D43B-B6B0-495F-A147-1D4761D60134}
+ {8F4AD79C-D5E5-44C4-9F03-B0DF4EE8BBA0} = {7B180FCA-A2BB-48C8-AF36-2300C033C476}
+ {ED8E11B7-786F-4EFF-9E4C-B937B7A2DE89} = {7B180FCA-A2BB-48C8-AF36-2300C033C476}
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {6BBF5459-C634-4145-961C-04EBE90FE66B}
EndGlobalSection
EndGlobal
diff --git a/RedBookPlayer/DeEmphasisFilter.cs b/RedBookPlayer/DeEmphasisFilter.cs
index e9122ab..87d3f66 100644
--- a/RedBookPlayer/DeEmphasisFilter.cs
+++ b/RedBookPlayer/DeEmphasisFilter.cs
@@ -3,14 +3,17 @@ using NWaves.Filters.BiQuad;
namespace RedBookPlayer
{
+ ///
+ /// Filter for applying de-emphasis to audio
+ ///
public class DeEmphasisFilter : BiQuadFilter
{
- static readonly double B0;
- static readonly double B1;
- static readonly double B2;
- static readonly double A0;
- static readonly double A1;
- static readonly double A2;
+ private static readonly double B0;
+ private static readonly double B1;
+ private static readonly double B2;
+ private static readonly double A0;
+ private static readonly double A1;
+ private static readonly double A2;
static DeEmphasisFilter()
{
diff --git a/RedBookPlayer/HiResTimer.cs b/RedBookPlayer/HiResTimer.cs
index 1c2a5ef..d5c8307 100644
--- a/RedBookPlayer/HiResTimer.cs
+++ b/RedBookPlayer/HiResTimer.cs
@@ -4,12 +4,15 @@ using System.Threading;
namespace RedBookPlayer
{
+ ///
+ /// Recurring timer wrapper with a high degree of accuracy
+ ///
public class HiResTimer
{
static readonly float tickFrequency = 1000f / Stopwatch.Frequency;
- volatile float interval;
- volatile bool isRunning;
+ volatile float _interval;
+ volatile bool _isRunning;
public HiResTimer() : this(1f) {}
@@ -19,24 +22,25 @@ namespace RedBookPlayer
float.IsNaN(interval))
throw new ArgumentOutOfRangeException(nameof(interval));
- this.interval = interval;
+ _interval = interval;
}
public float Interval
{
- get => interval;
+ get => _interval;
set
{
if(value < 0f ||
float.IsNaN(value))
throw new ArgumentOutOfRangeException(nameof(value));
- interval = value;
+ _interval = value;
}
}
public bool Enabled
{
+ get => _isRunning;
set
{
if(value)
@@ -44,23 +48,22 @@ namespace RedBookPlayer
else
Stop();
}
- get => isRunning;
}
public event EventHandler Elapsed;
public void Start()
{
- if(isRunning)
+ if(_isRunning)
return;
- isRunning = true;
+ _isRunning = true;
var thread = new Thread(ExecuteTimer);
thread.Priority = ThreadPriority.Highest;
thread.Start();
}
- public void Stop() => isRunning = false;
+ public void Stop() => _isRunning = false;
void ExecuteTimer()
{
@@ -69,9 +72,9 @@ namespace RedBookPlayer
var stopwatch = new Stopwatch();
stopwatch.Start();
- while(isRunning)
+ while(_isRunning)
{
- nextTrigger += interval;
+ nextTrigger += _interval;
float elapsed;
while(true)
@@ -91,7 +94,7 @@ namespace RedBookPlayer
else
Thread.Sleep(10);
- if(!isRunning)
+ if(!_isRunning)
return;
}
@@ -110,11 +113,4 @@ namespace RedBookPlayer
static float ElapsedHiRes(Stopwatch stopwatch) => stopwatch.ElapsedTicks * tickFrequency;
}
-
- public class HiResTimerElapsedEventArgs : EventArgs
- {
- internal HiResTimerElapsedEventArgs(float delay) => Delay = delay;
-
- public float Delay { get; }
- }
}
\ No newline at end of file
diff --git a/RedBookPlayer/HiResTimerElapsedEventArgs.cs b/RedBookPlayer/HiResTimerElapsedEventArgs.cs
new file mode 100644
index 0000000..1abbc91
--- /dev/null
+++ b/RedBookPlayer/HiResTimerElapsedEventArgs.cs
@@ -0,0 +1,11 @@
+using System;
+
+namespace RedBookPlayer
+{
+ public class HiResTimerElapsedEventArgs : EventArgs
+ {
+ internal HiResTimerElapsedEventArgs(float delay) => Delay = delay;
+
+ public float Delay { get; }
+ }
+}
\ No newline at end of file
diff --git a/RedBookPlayer/MainWindow.xaml.cs b/RedBookPlayer/MainWindow.xaml.cs
index 14b37c8..f6166cc 100644
--- a/RedBookPlayer/MainWindow.xaml.cs
+++ b/RedBookPlayer/MainWindow.xaml.cs
@@ -19,34 +19,37 @@ namespace RedBookPlayer
InitializeComponent();
}
+ ///
+ /// Apply a custom theme to the player
+ ///
+ /// Path to the theme under the themes directory
public static void ApplyTheme(string theme)
{
- if((theme ?? "") == "")
- {
+ // If no theme path is provided, we can ignore
+ if(string.IsNullOrWhiteSpace(theme))
return;
- }
- if(theme == "default")
+ // If the theme name is "default", we assume the internal theme is used
+ if(theme.Equals("default", StringComparison.CurrentCultureIgnoreCase))
{
Instance.ContentControl.Content = new PlayerView();
}
else
{
- string themeDirectory = Directory.GetCurrentDirectory() + "/themes/" + theme;
- string xamlPath = themeDirectory + "/view.xaml";
+ string themeDirectory = $"{Directory.GetCurrentDirectory()}/themes/{theme}";
+ string xamlPath = $"{themeDirectory}/view.xaml";
if(!File.Exists(xamlPath))
{
Console.WriteLine("Warning: specified theme doesn't exist, reverting to default");
-
return;
}
try
{
- Instance.ContentControl.Content =
- new PlayerView(File.ReadAllText(xamlPath).
- Replace("Source=\"", $"Source=\"file://{themeDirectory}/"));
+ string xaml = File.ReadAllText(xamlPath);
+ xaml = xaml.Replace("Source=\"", $"Source=\"file://{themeDirectory}/");
+ Instance.ContentControl.Content = new PlayerView(xaml);
}
catch(XmlException ex)
{
diff --git a/RedBookPlayer/Player.cs b/RedBookPlayer/Player.cs
index 564f035..40f4f09 100644
--- a/RedBookPlayer/Player.cs
+++ b/RedBookPlayer/Player.cs
@@ -1,270 +1,392 @@
using System;
+using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Aaru.CommonTypes.Enums;
using Aaru.CommonTypes.Structs;
using Aaru.DiscImages;
using Aaru.Helpers;
-using CSCore;
using CSCore.SoundOut;
using NWaves.Audio;
using NWaves.Filters.BiQuad;
using static Aaru.Decoders.CD.FullTOC;
-using WaveFormat = CSCore.WaveFormat;
namespace RedBookPlayer
{
public class Player
{
- public enum TrackType
- {
- Audio, Data
- }
+ #region Public Fields
- readonly object readingImage = new object();
+ ///
+ /// Indicate if the player is ready to be used
+ ///
+ public bool Initialized { get; private set; } = false;
- ushort currentIndex = 1;
- ulong currentSector;
- int currentSectorReadPosition;
- int currentTrack;
- BiQuadFilter deEmphasisFilterLeft;
- BiQuadFilter deEmphasisFilterRight;
- public bool Initialized;
- ALSoundOut soundOut;
- PlayerSource source;
- CDFullTOC toc;
- int volume = 100;
+ ///
+ /// Currently loaded disc image
+ ///
+ public AaruFormat Image { get; private set; }
+ ///
+ /// Current track number
+ ///
public int CurrentTrack
{
- get => currentTrack;
-
+ get => _currentTrack;
private set
{
+ // Unset image means we can't do anything
if(Image == null)
return;
+ // If the value is the same, don't do anything
+ if(value == _currentTrack)
+ return;
+
+ // Check if we're incrementing or decrementing the track
+ bool increment = value > _currentTrack;
+
+ // Ensure that the value is valid, wrapping around if necessary
if(value >= Image.Tracks.Count)
- currentTrack = 0;
+ _currentTrack = 0;
else if(value < 0)
- currentTrack = Image.Tracks.Count - 1;
+ _currentTrack = Image.Tracks.Count - 1;
else
- currentTrack = value;
+ _currentTrack = value;
- byte[] flagsData =
- Image.ReadSectorTag(Image.Tracks[CurrentTrack].TrackSequence, SectorTagType.CdTrackFlags);
+ // Cache the current track for easy access
+ Track track = Image.Tracks[CurrentTrack];
+ // Set new track-specific data
+ byte[] flagsData = Image.ReadSectorTag(track.TrackSequence, SectorTagType.CdTrackFlags);
ApplyDeEmphasis = ((CdFlags)flagsData[0]).HasFlag(CdFlags.PreEmphasis);
- byte[] subchannel = Image.ReadSectorTag(Image.Tracks[CurrentTrack].TrackStartSector,
- SectorTagType.CdSectorSubchannel);
+ try
+ {
+ byte[] subchannel = Image.ReadSectorTag(track.TrackStartSector, SectorTagType.CdSectorSubchannel);
- if(!ApplyDeEmphasis)
- ApplyDeEmphasis = (subchannel[3] & 0b01000000) != 0;
+ if(!ApplyDeEmphasis)
+ ApplyDeEmphasis = (subchannel[3] & 0b01000000) != 0;
- CopyAllowed = (subchannel[2] & 0b01000000) != 0;
- TrackType_ = (subchannel[1] & 0b01000000) != 0 ? TrackType.Data : TrackType.Audio;
+ CopyAllowed = (subchannel[2] & 0b01000000) != 0;
+ TrackType = (subchannel[1] & 0b01000000) != 0 ? Aaru.CommonTypes.Enums.TrackType.Data : Aaru.CommonTypes.Enums.TrackType.Audio;
+ }
+ catch(ArgumentException)
+ {
+ TrackType = track.TrackType;
+ }
TrackHasEmphasis = ApplyDeEmphasis;
- TotalIndexes = Image.Tracks[CurrentTrack].Indexes.Keys.Max();
- CurrentIndex = Image.Tracks[CurrentTrack].Indexes.Keys.Min();
+ TotalIndexes = track.Indexes.Keys.Max();
+ CurrentIndex = track.Indexes.Keys.Min();
+
+ // If we're not playing data tracks, skip
+ if(!App.Settings.PlayDataTracks && TrackType != Aaru.CommonTypes.Enums.TrackType.Audio)
+ {
+ if(increment)
+ NextTrack();
+ else
+ PreviousTrack();
+ }
}
}
+ ///
+ /// Current track index
+ ///
public ushort CurrentIndex
{
- get => currentIndex;
-
+ get => _currentIndex;
private set
{
- currentIndex = value;
-
- SectionStartSector = (ulong)Image.Tracks[CurrentTrack].Indexes[CurrentIndex];
- TotalTime = Image.Tracks[CurrentTrack].TrackEndSector - Image.Tracks[CurrentTrack].TrackStartSector;
- }
- }
-
- public ulong CurrentSector
- {
- get => currentSector;
-
- private set
- {
- currentSector = value;
-
+ // Unset image means we can't do anything
if(Image == null)
return;
- if((CurrentTrack < Image.Tracks.Count - 1 &&
- CurrentSector >= Image.Tracks[CurrentTrack + 1].TrackStartSector) ||
- (CurrentTrack > 0 && CurrentSector < Image.Tracks[CurrentTrack].TrackStartSector))
+ // If the value is the same, don't do anything
+ if(value == _currentIndex)
+ return;
+
+ // Cache the current track for easy access
+ Track track = Image.Tracks[CurrentTrack];
+
+ // Ensure that the value is valid, wrapping around if necessary
+ if(value > track.Indexes.Keys.Max())
+ _currentIndex = 0;
+ else if(value < 0)
+ _currentIndex = track.Indexes.Keys.Max();
+ else
+ _currentIndex = value;
+
+ // Set new index-specific data
+ SectionStartSector = (ulong)track.Indexes[CurrentIndex];
+ TotalTime = track.TrackEndSector - track.TrackStartSector;
+ }
+ }
+
+ ///
+ /// Current sector number
+ ///
+ public ulong CurrentSector
+ {
+ get => _currentSector;
+ private set
+ {
+ // Unset image means we can't do anything
+ if(Image == null)
+ return;
+
+ // If the value is the same, don't do anything
+ if(value == _currentSector)
+ return;
+
+ // Cache the current track for easy access
+ Track track = Image.Tracks[CurrentTrack];
+
+ _currentSector = value;
+
+ if((CurrentTrack < Image.Tracks.Count - 1 && CurrentSector >= Image.Tracks[CurrentTrack + 1].TrackStartSector)
+ || (CurrentTrack > 0 && CurrentSector < track.TrackStartSector))
{
- foreach(Track track in Image.Tracks.ToArray().Reverse())
+ foreach(Track trackData in Image.Tracks.ToArray().Reverse())
{
- if(CurrentSector < track.TrackStartSector)
- continue;
-
- CurrentTrack = (int)track.TrackSequence - 1;
-
- break;
+ if(CurrentSector >= trackData.TrackStartSector)
+ {
+ CurrentTrack = (int)trackData.TrackSequence - 1;
+ break;
+ }
}
}
- foreach((ushort key, int i) in Image.Tracks[CurrentTrack].Indexes.Reverse())
+ foreach((ushort key, int i) in track.Indexes.Reverse())
{
- if((int)CurrentSector < i)
- continue;
-
- CurrentIndex = key;
-
- return;
+ if((int)CurrentSector >= i)
+ {
+ CurrentIndex = key;
+ return;
+ }
}
CurrentIndex = 0;
}
}
- public bool TrackHasEmphasis { get; private set; }
- public bool ApplyDeEmphasis { get; private set; }
- public bool CopyAllowed { get; private set; }
- public TrackType? TrackType_ { get; private set; }
- public ulong SectionStartSector { get; private set; }
- public int TotalTracks { get; private set; }
- public int TotalIndexes { get; private set; }
- public ulong TimeOffset { get; private set; }
- public ulong TotalTime { get; private set; }
+ ///
+ /// Represents the pre-emphasis flag
+ ///
+ public bool TrackHasEmphasis { get; private set; } = false;
+ ///
+ /// Indicates if de-emphasis should be applied
+ ///
+ public bool ApplyDeEmphasis { get; private set; } = false;
+
+ ///
+ /// Represents the copy allowed flag
+ ///
+ public bool CopyAllowed { get; private set; } = false;
+
+ ///
+ /// Represents the track type
+ ///
+ public TrackType? TrackType { get; private set; }
+
+ ///
+ /// Represents the sector starting the section
+ ///
+ public ulong SectionStartSector { get; private set; }
+
+ ///
+ /// Represents the total tracks on the disc
+ ///
+ public int TotalTracks { get; private set; } = 0;
+
+ ///
+ /// Represents the total indices on the disc
+ ///
+ public int TotalIndexes { get; private set; } = 0;
+
+ ///
+ /// Represents the time adjustment offset for the disc
+ ///
+ public ulong TimeOffset { get; private set; } = 0;
+
+ ///
+ /// Represents the total playing time for the disc
+ ///
+ public ulong TotalTime { get; private set; } = 0;
+
+ ///
+ /// Represents the current play volume between 0 and 100
+ ///
public int Volume
{
- get => volume;
-
+ get => _volume;
set
{
- if(volume >= 0 &&
- volume <= 100)
- volume = value;
+ if(value >= 0 &&
+ value <= 100)
+ _volume = value;
}
}
- public AaruFormat Image { get; private set; }
+ #endregion
+ #region Private State Variables
+
+ ///
+ /// Current track number
+ ///
+ private int _currentTrack = 0;
+
+ ///
+ /// Current track index
+ ///
+ private ushort _currentIndex = 0;
+
+ ///
+ /// Current sector number
+ ///
+ private ulong _currentSector = 0;
+
+ ///
+ /// Current position in the sector
+ ///
+ private int _currentSectorReadPosition = 0;
+
+ ///
+ /// Current play volume between 0 and 100
+ ///
+ private int _volume = 100;
+
+ ///
+ /// Current disc table of contents
+ ///
+ private CDFullTOC _toc;
+
+ ///
+ /// Data provider for sound output
+ ///
+ private PlayerSource _source;
+
+ ///
+ /// Sound output instance
+ ///
+ private ALSoundOut _soundOut;
+
+ ///
+ /// Left channel de-emphasis filter
+ ///
+ private BiQuadFilter _deEmphasisFilterLeft;
+
+ ///
+ /// Right channel de-emphasis filter
+ ///
+ private BiQuadFilter _deEmphasisFilterRight;
+
+ ///
+ /// Lock object for reading track data
+ ///
+ private readonly object _readingImage = new object();
+
+ #endregion
+
+ ///
+ /// Initialize the player with a given image
+ ///
+ /// Aaruformat image to load for playback
+ /// True if playback should begin immediately, false otherwise
public async void Init(AaruFormat image, bool autoPlay = false)
{
+ // If the image is null, we can't do anything
+ if(image == null)
+ return;
+
+ // Set the current disc image
Image = image;
- if(await Task.Run(() => image.Info.ReadableMediaTags?.Contains(MediaTagType.CD_FullTOC)) != true)
- {
- Console.WriteLine("Full TOC not found");
-
+ // Attempt to load the TOC
+ if(!await LoadTOC())
return;
- }
- byte[] tocBytes = await Task.Run(() => image.ReadDiskTag(MediaTagType.CD_FullTOC));
+ // Setup the de-emphasis filters
+ SetupFilters();
- if((tocBytes?.Length ?? 0) == 0)
- {
- Console.WriteLine("Error reading TOC from disc image");
-
- return;
- }
-
- if(Swapping.Swap(BitConverter.ToUInt16(tocBytes, 0)) + 2 != tocBytes.Length)
- {
- byte[] tmp = new byte[tocBytes.Length + 2];
- Array.Copy(tocBytes, 0, tmp, 2, tocBytes.Length);
- tmp[0] = (byte)((tocBytes.Length & 0xFF00) >> 8);
- tmp[1] = (byte)(tocBytes.Length & 0xFF);
- tocBytes = tmp;
- }
-
- CDFullTOC? nullableToc = await Task.Run(() => Decode(tocBytes));
-
- if(nullableToc == null)
- {
- Console.WriteLine("Error decoding TOC");
-
- return;
- }
-
- toc = nullableToc.Value;
-
- Console.WriteLine(Prettify(toc));
-
- if(deEmphasisFilterLeft == null)
- {
- deEmphasisFilterLeft = new DeEmphasisFilter();
- deEmphasisFilterRight = new DeEmphasisFilter();
- }
- else
- {
- deEmphasisFilterLeft.Reset();
- deEmphasisFilterRight.Reset();
- }
-
- if(source == null)
- {
- source = new PlayerSource(ProviderRead);
-
- soundOut = new ALSoundOut(100);
- soundOut.Initialize(source);
- }
- else
- soundOut.Stop();
+ // Setup the audio output
+ SetupAudio();
+ // Load the first track
CurrentTrack = 0;
LoadTrack(0);
+ // Initialize playback, if necessary
if(autoPlay)
- soundOut.Play();
+ _soundOut.Play();
else
TotalIndexes = 0;
+ // Set the internal disc state
TotalTracks = image.Tracks.Count;
- TrackDataDescriptor firstTrack = toc.TrackDescriptors.First(d => d.ADR == 1 && d.POINT == 1);
+ TrackDataDescriptor firstTrack = _toc.TrackDescriptors.First(d => d.ADR == 1 && d.POINT == 1);
TimeOffset = (ulong)((firstTrack.PMIN * 60 * 75) + (firstTrack.PSEC * 75) + firstTrack.PFRAME);
- TotalTime = TimeOffset + image.Tracks.Last().TrackEndSector;
+ TotalTime = TimeOffset + image.Tracks.Last().TrackEndSector;
+ // Set the output volume from settings
Volume = App.Settings.Volume;
+ // Mark the player as ready
Initialized = true;
- source.Start();
+ // Begin loading data
+ _source.Start();
}
+ ///
+ /// Fill the current byte buffer with playable data
+ ///
+ /// Buffer to load data into
+ /// Offset in the buffer to load at
+ /// Number of bytes to load
+ /// Number of bytes read
public int ProviderRead(byte[] buffer, int offset, int count)
{
- soundOut.Volume = (float)Volume / 100;
+ // Set the current volume
+ _soundOut.Volume = (float)Volume / 100;
+ // Determine how many sectors we can read
ulong sectorsToRead;
ulong zeroSectorsAmount;
-
do
{
- sectorsToRead = ((ulong)count / 2352) + 2;
+ // Attempt to read 2 more sectors than requested
+ sectorsToRead = ((ulong)count / 2352) + 2;
zeroSectorsAmount = 0;
+ // Avoid overreads by padding with 0-byte data at the end
if(CurrentSector + sectorsToRead > Image.Info.Sectors)
{
ulong oldSectorsToRead = sectorsToRead;
- sectorsToRead = Image.Info.Sectors - CurrentSector;
- zeroSectorsAmount = oldSectorsToRead - sectorsToRead;
+ sectorsToRead = Image.Info.Sectors - CurrentSector;
+ zeroSectorsAmount = oldSectorsToRead - sectorsToRead;
}
- if(sectorsToRead > 0)
- continue;
-
- LoadTrack(0);
- currentSectorReadPosition = 0;
+ // TODO: Figure out when this value could be negative
+ if(sectorsToRead <= 0)
+ {
+ LoadTrack(0);
+ _currentSectorReadPosition = 0;
+ }
} while(sectorsToRead <= 0);
+ // Create padding data for overreads
byte[] zeroSectors = new byte[zeroSectorsAmount * 2352];
- Array.Clear(zeroSectors, 0, zeroSectors.Length);
byte[] audioData;
- Task task = Task.Run(() =>
+ // Attempt to read the required number of sectors
+ var readSectorTask = Task.Run(() =>
{
- lock(readingImage)
+ lock(_readingImage)
{
try
{
@@ -273,34 +395,27 @@ namespace RedBookPlayer
catch(ArgumentOutOfRangeException)
{
LoadTrack(0);
-
return Image.ReadSectors(CurrentSector, (uint)sectorsToRead).Concat(zeroSectors).ToArray();
}
}
});
- if(task.Wait(TimeSpan.FromMilliseconds(100)))
+ // Wait 100ms at longest for the read to occur
+ if(readSectorTask.Wait(TimeSpan.FromMilliseconds(100)))
{
- audioData = task.Result;
+ audioData = readSectorTask.Result;
}
else
{
Array.Clear(buffer, offset, count);
-
return count;
}
- Task.Run(() =>
- {
- lock(readingImage)
- {
- Image.ReadSector(CurrentSector + 375);
- }
- });
-
+ // Load only the requested audio segment
byte[] audioDataSegment = new byte[count];
- Array.Copy(audioData, currentSectorReadPosition, audioDataSegment, 0, count);
+ Array.Copy(audioData, _currentSectorReadPosition, audioDataSegment, 0, Math.Min(count, audioData.Length - _currentSectorReadPosition));
+ // Apply de-emphasis filtering, only if enabled
if(ApplyDeEmphasis)
{
float[][] floatAudioData = new float[2][];
@@ -310,75 +425,79 @@ namespace RedBookPlayer
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]);
+ 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
Array.Copy(audioDataSegment, 0, buffer, offset, count);
- currentSectorReadPosition += count;
-
- if(currentSectorReadPosition < 2352)
- return count;
-
- CurrentSector += (ulong)currentSectorReadPosition / 2352;
- currentSectorReadPosition %= 2352;
+ // Set the read position in the sector for easier access
+ _currentSectorReadPosition += count;
+ if(_currentSectorReadPosition >= 2352)
+ {
+ CurrentSector += (ulong)_currentSectorReadPosition / 2352;
+ _currentSectorReadPosition %= 2352;
+ }
return count;
}
- public void LoadTrack(int index)
- {
- bool oldRun = source.Run;
- source.Stop();
-
- CurrentSector = (ulong)Image.Tracks[index].Indexes[1];
-
- source.Run = oldRun;
- }
+ #region Player Controls
+ ///
+ /// Start audio playback
+ ///
public void Play()
{
if(Image == null)
return;
- soundOut.Play();
+ _soundOut.Play();
TotalIndexes = Image.Tracks[CurrentTrack].Indexes.Keys.Max();
}
+ ///
+ /// Pause the current audio playback
+ ///
public void Pause()
{
if(Image == null)
return;
- soundOut.Stop();
+ _soundOut.Stop();
}
+ ///
+ /// Stop the current audio playback
+ ///
public void Stop()
{
if(Image == null)
return;
- soundOut.Stop();
+ _soundOut.Stop();
LoadTrack(CurrentTrack);
}
+ ///
+ /// Try to move to the next track, wrapping around if necessary
+ ///
public void NextTrack()
{
if(Image == null)
return;
- if(CurrentTrack + 1 >= Image.Tracks.Count)
- CurrentTrack = 0;
- else
- CurrentTrack++;
-
+ CurrentTrack++;
LoadTrack(CurrentTrack);
}
+ ///
+ /// Try to move to the previous track, wrapping around if necessary
+ ///
public void PreviousTrack()
{
if(Image == null)
@@ -386,22 +505,19 @@ namespace RedBookPlayer
if(CurrentSector < (ulong)Image.Tracks[CurrentTrack].Indexes[1] + 75)
{
- if(App.Settings.AllowSkipHiddenTrack &&
- CurrentTrack == 0 &&
- CurrentSector >= 75)
+ if(App.Settings.AllowSkipHiddenTrack && CurrentTrack == 0 && CurrentSector >= 75)
CurrentSector = 0;
else
- {
- if(CurrentTrack - 1 < 0)
- CurrentTrack = Image.Tracks.Count - 1;
- else
- CurrentTrack--;
- }
+ CurrentTrack--;
}
LoadTrack(CurrentTrack);
}
+ ///
+ /// Try to move to the next track index
+ ///
+ /// True if index changes can trigger a track change, false otherwise
public void NextIndex(bool changeTrack)
{
if(Image == null)
@@ -409,16 +525,22 @@ namespace RedBookPlayer
if(CurrentIndex + 1 > Image.Tracks[CurrentTrack].Indexes.Keys.Max())
{
- if(!changeTrack)
- return;
-
- NextTrack();
- CurrentSector = (ulong)Image.Tracks[CurrentTrack].Indexes.Values.Min();
+ if(changeTrack)
+ {
+ NextTrack();
+ CurrentSector = (ulong)Image.Tracks[CurrentTrack].Indexes.Values.Min();
+ }
}
else
+ {
CurrentSector = (ulong)Image.Tracks[CurrentTrack].Indexes[++CurrentIndex];
+ }
}
+ ///
+ /// Try to move to the previous track index
+ ///
+ /// True if index changes can trigger a track change, false otherwise
public void PreviousIndex(bool changeTrack)
{
if(Image == null)
@@ -426,16 +548,21 @@ namespace RedBookPlayer
if(CurrentIndex - 1 < Image.Tracks[CurrentTrack].Indexes.Keys.Min())
{
- if(!changeTrack)
- return;
-
- PreviousTrack();
- CurrentSector = (ulong)Image.Tracks[CurrentTrack].Indexes.Values.Max();
+ if(changeTrack)
+ {
+ PreviousTrack();
+ CurrentSector = (ulong)Image.Tracks[CurrentTrack].Indexes.Values.Max();
+ }
}
else
+ {
CurrentSector = (ulong)Image.Tracks[CurrentTrack].Indexes[--CurrentIndex];
+ }
}
+ ///
+ /// Fast-forward playback by 75 sectors, if possible
+ ///
public void FastForward()
{
if(Image == null)
@@ -444,6 +571,9 @@ namespace RedBookPlayer
CurrentSector = Math.Min(Image.Info.Sectors - 1, CurrentSector + 75);
}
+ ///
+ /// Rewind playback by 75 sectors, if possible
+ ///
public void Rewind()
{
if(Image == null)
@@ -453,46 +583,298 @@ namespace RedBookPlayer
CurrentSector -= 75;
}
- public void EnableDeEmphasis() => ApplyDeEmphasis = true;
+ ///
+ /// Toggle de-emphasis processing
+ ///
+ /// True to apply de-emphasis, false otherwise
+ public void ToggleDeEmphasis(bool enable) => ApplyDeEmphasis = enable;
- public void DisableDeEmphasis() => ApplyDeEmphasis = false;
- }
+ #endregion
- public class PlayerSource : IWaveSource
- {
- public delegate int ReadFunction(byte[] buffer, int offset, int count);
+ #region Helpers
- readonly ReadFunction read;
-
- public bool Run = true;
-
- public PlayerSource(ReadFunction read) => this.read = read;
-
- public WaveFormat WaveFormat => new WaveFormat();
- bool IAudioSource.CanSeek => throw new NotImplementedException();
-
- public long Position
+ ///
+ /// Generate a CDFullTOC object from the current image
+ ///
+ /// CDFullTOC object, if possible
+ /// Copied from
+ private bool GenerateTOC()
{
- get => throw new NotImplementedException();
- set => throw new NotImplementedException();
+ // Invalid image means we can't generate anything
+ if(Image == null)
+ return false;
+
+ _toc = new CDFullTOC();
+ Dictionary _trackFlags = new Dictionary();
+ Dictionary sessionEndingTrack = new Dictionary();
+ _toc.FirstCompleteSession = byte.MaxValue;
+ _toc.LastCompleteSession = byte.MinValue;
+ List trackDescriptors = new List();
+ byte currentTrack = 0;
+
+ foreach(Track track in Image.Tracks.OrderBy(t => t.TrackSession).ThenBy(t => t.TrackSequence))
+ {
+ byte[] trackFlags = Image.ReadSectorTag(track.TrackStartSector + 1, SectorTagType.CdTrackFlags);
+ if(trackFlags != null)
+ _trackFlags.Add((byte)track.TrackStartSector, trackFlags[0]);
+ }
+
+ foreach(Track track in Image.Tracks.OrderBy(t => t.TrackSession).ThenBy(t => t.TrackSequence))
+ {
+ if(track.TrackSession < _toc.FirstCompleteSession)
+ _toc.FirstCompleteSession = (byte)track.TrackSession;
+
+ if(track.TrackSession <= _toc.LastCompleteSession)
+ {
+ currentTrack = (byte)track.TrackSequence;
+
+ continue;
+ }
+
+ if(_toc.LastCompleteSession > 0)
+ sessionEndingTrack.Add(_toc.LastCompleteSession, currentTrack);
+
+ _toc.LastCompleteSession = (byte)track.TrackSession;
+ }
+
+ byte currentSession = 0;
+
+ foreach(Track track in Image.Tracks.OrderBy(t => t.TrackSession).ThenBy(t => t.TrackSequence))
+ {
+ _trackFlags.TryGetValue((byte)track.TrackSequence, out byte trackControl);
+
+ if(trackControl == 0 &&
+ track.TrackType != Aaru.CommonTypes.Enums.TrackType.Audio)
+ trackControl = (byte)CdFlags.DataTrack;
+
+ // Lead-Out
+ if(track.TrackSession > currentSession &&
+ currentSession != 0)
+ {
+ (byte minute, byte second, byte frame) leadoutAmsf = LbaToMsf(track.TrackStartSector - 150);
+
+ (byte minute, byte second, byte frame) leadoutPmsf =
+ LbaToMsf(Image.Tracks.OrderBy(t => t.TrackSession).ThenBy(t => t.TrackSequence).Last().
+ TrackStartSector);
+
+ // Lead-out
+ trackDescriptors.Add(new TrackDataDescriptor
+ {
+ SessionNumber = currentSession,
+ POINT = 0xB0,
+ ADR = 5,
+ CONTROL = 0,
+ HOUR = 0,
+ Min = leadoutAmsf.minute,
+ Sec = leadoutAmsf.second,
+ Frame = leadoutAmsf.frame,
+ PHOUR = 2,
+ PMIN = leadoutPmsf.minute,
+ PSEC = leadoutPmsf.second,
+ PFRAME = leadoutPmsf.frame
+ });
+
+ // This seems to be constant? It should not exist on CD-ROM but CloneCD creates them anyway
+ // Format seems like ATIP, but ATIP should not be as 0xC0 in TOC...
+ //trackDescriptors.Add(new TrackDataDescriptor
+ //{
+ // SessionNumber = currentSession,
+ // POINT = 0xC0,
+ // ADR = 5,
+ // CONTROL = 0,
+ // Min = 128,
+ // PMIN = 97,
+ // PSEC = 25
+ //});
+ }
+
+ // Lead-in
+ if(track.TrackSession > currentSession)
+ {
+ currentSession = (byte)track.TrackSession;
+ sessionEndingTrack.TryGetValue(currentSession, out byte endingTrackNumber);
+
+ (byte minute, byte second, byte frame) leadinPmsf =
+ LbaToMsf(Image.Tracks.FirstOrDefault(t => t.TrackSequence == endingTrackNumber)?.TrackEndSector ??
+ 0 + 1);
+
+ // Starting track
+ trackDescriptors.Add(new TrackDataDescriptor
+ {
+ SessionNumber = currentSession,
+ POINT = 0xA0,
+ ADR = 1,
+ CONTROL = trackControl,
+ PMIN = (byte)track.TrackSequence
+ });
+
+ // Ending track
+ trackDescriptors.Add(new TrackDataDescriptor
+ {
+ SessionNumber = currentSession,
+ POINT = 0xA1,
+ ADR = 1,
+ CONTROL = trackControl,
+ PMIN = endingTrackNumber
+ });
+
+ // Lead-out start
+ trackDescriptors.Add(new TrackDataDescriptor
+ {
+ SessionNumber = currentSession,
+ POINT = 0xA2,
+ ADR = 1,
+ CONTROL = trackControl,
+ PHOUR = 0,
+ PMIN = leadinPmsf.minute,
+ PSEC = leadinPmsf.second,
+ PFRAME = leadinPmsf.frame
+ });
+ }
+
+ (byte minute, byte second, byte frame) pmsf = LbaToMsf(track.TrackStartSector);
+
+ // Track
+ trackDescriptors.Add(new TrackDataDescriptor
+ {
+ SessionNumber = (byte)track.TrackSession,
+ POINT = (byte)track.TrackSequence,
+ ADR = 1,
+ CONTROL = trackControl,
+ PHOUR = 0,
+ PMIN = pmsf.minute,
+ PSEC = pmsf.second,
+ PFRAME = pmsf.frame
+ });
+ }
+
+ _toc.TrackDescriptors = trackDescriptors.ToArray();
+ return true;
}
- public long Length => throw new NotImplementedException();
+ ///
+ /// Convert the sector to LBA values
+ ///
+ /// Sector to convert
+ /// LBA values for the sector number
+ /// Copied from
+ private (byte minute, byte second, byte frame) LbaToMsf(ulong sector) =>
+ ((byte)((sector + 150) / 75 / 60), (byte)((sector + 150) / 75 % 60), (byte)((sector + 150) % 75));
- public int Read(byte[] buffer, int offset, int count)
+ ///
+ /// Load TOC for the current disc image
+ ///
+ /// True if the TOC could be loaded, false otherwise
+ private async Task LoadTOC()
{
- if(Run)
- return read(buffer, offset, count);
+ if(await Task.Run(() => Image.Info.ReadableMediaTags?.Contains(MediaTagType.CD_FullTOC)) != true)
+ {
+ // Only generate the TOC if we have it set
+ if(!App.Settings.GenerateMissingTOC)
+ {
+ Console.WriteLine("Full TOC not found");
+ return false;
+ }
- Array.Clear(buffer, offset, count);
+ Console.WriteLine("Attempting to generate TOC");
+ if(GenerateTOC())
+ {
+ Console.WriteLine(Prettify(_toc));
+ return true;
+ }
+ else
+ {
+ Console.WriteLine("Full TOC not found or generated");
+ return false;
+ }
+ }
- return count;
+ byte[] tocBytes = await Task.Run(() => Image.ReadDiskTag(MediaTagType.CD_FullTOC));
+ if(tocBytes == null || tocBytes.Length == 0)
+ {
+ Console.WriteLine("Error reading TOC from disc image");
+ return false;
+ }
+
+ if(Swapping.Swap(BitConverter.ToUInt16(tocBytes, 0)) + 2 != tocBytes.Length)
+ {
+ byte[] tmp = new byte[tocBytes.Length + 2];
+ Array.Copy(tocBytes, 0, tmp, 2, tocBytes.Length);
+ tmp[0] = (byte)((tocBytes.Length & 0xFF00) >> 8);
+ tmp[1] = (byte)(tocBytes.Length & 0xFF);
+ tocBytes = tmp;
+ }
+
+ var nullableToc = await Task.Run(() => Decode(tocBytes));
+ if(nullableToc == null)
+ {
+ Console.WriteLine("Error decoding TOC");
+ return false;
+ }
+
+ _toc = nullableToc.Value;
+ Console.WriteLine(Prettify(_toc));
+ return true;
}
- public void Dispose() {}
+ ///
+ /// Load the track for a given track number, if possible
+ ///
+ /// Track number to load
+ private void LoadTrack(int index)
+ {
+ // Save if audio is currently playing
+ bool oldRun = _source.Run;
- public void Start() => Run = true;
+ // Stop playback if necessary
+ _source.Stop();
- public void Stop() => Run = false;
+ // If it is a valid index, seek to the first, non-negative sectored index for the track
+ if(index >= 0 && index < Image.Tracks.Count)
+ {
+ ushort firstIndex = Image.Tracks[index].Indexes.Keys.Min();
+ int firstSector = Image.Tracks[index].Indexes[firstIndex];
+ CurrentSector = (ulong)(firstSector >= 0 ? firstSector : Image.Tracks[index].Indexes[1]);
+ }
+
+ // Reset the playing state
+ _source.Run = oldRun;
+ }
+
+ ///
+ /// Sets or resets the de-emphasis filters
+ ///
+ private void SetupFilters()
+ {
+ if(_deEmphasisFilterLeft == null)
+ {
+ _deEmphasisFilterLeft = new DeEmphasisFilter();
+ _deEmphasisFilterRight = new DeEmphasisFilter();
+ }
+ else
+ {
+ _deEmphasisFilterLeft.Reset();
+ _deEmphasisFilterRight.Reset();
+ }
+ }
+
+ ///
+ /// Sets or resets the audio playback objects
+ ///
+ private void SetupAudio()
+ {
+ if(_source == null)
+ {
+ _source = new PlayerSource(ProviderRead);
+ _soundOut = new ALSoundOut(100);
+ _soundOut.Initialize(_source);
+ }
+ else
+ {
+ _soundOut.Stop();
+ }
+ }
+
+ #endregion
}
}
\ No newline at end of file
diff --git a/RedBookPlayer/PlayerSource.cs b/RedBookPlayer/PlayerSource.cs
new file mode 100644
index 0000000..3b73f5a
--- /dev/null
+++ b/RedBookPlayer/PlayerSource.cs
@@ -0,0 +1,44 @@
+using System;
+using CSCore;
+using WaveFormat = CSCore.WaveFormat;
+
+namespace RedBookPlayer
+{
+ public class PlayerSource : IWaveSource
+ {
+ public delegate int ReadFunction(byte[] buffer, int offset, int count);
+
+ readonly ReadFunction _read;
+
+ public bool Run = true;
+
+ public PlayerSource(ReadFunction read) => _read = read;
+
+ public WaveFormat WaveFormat => new WaveFormat();
+ bool IAudioSource.CanSeek => throw new NotImplementedException();
+
+ public long Position
+ {
+ get => throw new NotImplementedException();
+ set => throw new NotImplementedException();
+ }
+
+ public long Length => throw new NotImplementedException();
+
+ public int Read(byte[] buffer, int offset, int count)
+ {
+ if(Run)
+ return _read(buffer, offset, count);
+
+ Array.Clear(buffer, offset, count);
+
+ return count;
+ }
+
+ public void Dispose() {}
+
+ public void Start() => Run = true;
+
+ public void Stop() => Run = false;
+ }
+}
\ No newline at end of file
diff --git a/RedBookPlayer/PlayerView.xaml.cs b/RedBookPlayer/PlayerView.xaml.cs
index 3a23296..9e48786 100644
--- a/RedBookPlayer/PlayerView.xaml.cs
+++ b/RedBookPlayer/PlayerView.xaml.cs
@@ -4,7 +4,7 @@ using System.IO;
using System.Linq;
using System.Threading.Tasks;
using System.Timers;
-using Aaru.CommonTypes.Interfaces;
+using Aaru.CommonTypes.Enums;
using Aaru.DiscImages;
using Aaru.Filters;
using Avalonia;
@@ -14,62 +14,269 @@ using Avalonia.Markup.Xaml;
using Avalonia.Media.Imaging;
using Avalonia.Platform;
using Avalonia.Threading;
-using ReactiveUI;
namespace RedBookPlayer
{
public class PlayerView : UserControl
{
+ ///
+ /// Player representing the internal state and loaded image
+ ///
public static Player Player = new Player();
- TextBlock currentTrack;
- Image[] digits;
- Timer updateTimer;
+
+ ///
+ /// Set of images representing the digits for the UI
+ ///
+ ///
+ /// TODO: Does it make sense to have this as an array?
+ ///
+ private Image[] _digits;
+
+ ///
+ /// Timer for performing UI updates
+ ///
+ private Timer _updateTimer;
public PlayerView() => InitializeComponent(null);
public PlayerView(string xaml) => InitializeComponent(xaml);
- public async void LoadButton_Click(object sender, RoutedEventArgs e)
- {
- string path = await GetPath();
-
- if(path == null)
- {
- return;
- }
-
- await Task.Run(() =>
- {
- var image = new AaruFormat();
- IFilter filter = new ZZZNoFilter();
- filter.Open(path);
- image.Open(filter);
-
- Player.Init(image, App.Settings.AutoPlay);
- });
-
- await Dispatcher.UIThread.InvokeAsync(() =>
- {
- MainWindow.Instance.Title = "RedBookPlayer - " + path.Split('/').Last().Split('\\').Last();
- });
- }
+ #region Helpers
+ ///
+ /// Generate a path selection dialog box
+ ///
+ /// User-selected path, if possible
public async Task GetPath()
{
- var dialog = new OpenFileDialog();
- dialog.AllowMultiple = false;
-
+ var dialog = new OpenFileDialog { AllowMultiple = false };
List knownExtensions = new AaruFormat().KnownExtensions.ToList();
-
- dialog.Filters.Add(new FileDialogFilter
+ dialog.Filters.Add(new FileDialogFilter()
{
Name = "Aaru Image Format (*" + string.Join(", *", knownExtensions) + ")",
- Extensions = knownExtensions.ConvertAll(e => e.Substring(1))
+ Extensions = knownExtensions.ConvertAll(e => e.TrimStart('.'))
});
return (await dialog.ShowAsync((Window)Parent.Parent))?.FirstOrDefault();
}
+ ///
+ /// Generate the digit string to be interpreted by the UI
+ ///
+ /// String representing the digits for the player
+ private string GenerateDigitString()
+ {
+ // If the player isn't initialized, return all '-' characters
+ if (!Player.Initialized)
+ return string.Empty.PadLeft(20, '-');
+
+ // Otherwise, take the current time into account
+ ulong sectorTime = Player.CurrentSector;
+ if (Player.SectionStartSector != 0)
+ sectorTime -= Player.SectionStartSector;
+ else
+ sectorTime += Player.TimeOffset;
+
+ int[] numbers = new int[]
+ {
+ Player.CurrentTrack + 1,
+ Player.CurrentIndex,
+
+ (int)(sectorTime / (75 * 60)),
+ (int)(sectorTime / 75 % 60),
+ (int)(sectorTime % 75),
+
+ Player.TotalTracks,
+ Player.TotalIndexes,
+
+ (int)(Player.TotalTime / (75 * 60)),
+ (int)(Player.TotalTime / 75 % 60),
+ (int)(Player.TotalTime % 75),
+ };
+
+ return string.Join("", numbers.Select(i => i.ToString().PadLeft(2, '0').Substring(0, 2)));
+ }
+
+ ///
+ /// Load the png image for a given character based on the theme
+ ///
+ /// Character to load the image for
+ /// Bitmap representing the loaded image
+ ///
+ /// TODO: Currently assumes that an image must always exist
+ ///
+ private Bitmap GetBitmap(char character)
+ {
+ if(App.Settings.SelectedTheme == "default")
+ {
+ IAssetLoader assets = AvaloniaLocator.Current.GetService();
+
+ 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);
+ }
+ }
+
+ ///
+ /// Initialize the displayed digits array
+ ///
+ private void Initialize()
+ {
+ _digits = new Image[]
+ {
+ this.FindControl("TrackDigit1"),
+ this.FindControl("TrackDigit2"),
+
+ this.FindControl("IndexDigit1"),
+ this.FindControl("IndexDigit2"),
+
+ this.FindControl("TimeDigit1"),
+ this.FindControl("TimeDigit2"),
+ this.FindControl("TimeDigit3"),
+ this.FindControl("TimeDigit4"),
+ this.FindControl("TimeDigit5"),
+ this.FindControl("TimeDigit6"),
+
+ this.FindControl("TotalTracksDigit1"),
+ this.FindControl("TotalTracksDigit2"),
+
+ this.FindControl("TotalIndexesDigit1"),
+ this.FindControl("TotalIndexesDigit2"),
+
+ this.FindControl("TotalTimeDigit1"),
+ this.FindControl("TotalTimeDigit2"),
+ this.FindControl("TotalTimeDigit3"),
+ this.FindControl("TotalTimeDigit4"),
+ this.FindControl("TotalTimeDigit5"),
+ this.FindControl("TotalTimeDigit6"),
+ };
+ }
+
+ ///
+ /// Initialize the UI based on the currently selected theme
+ ///
+ /// XAML data representing the theme, null for default
+ private void InitializeComponent(string xaml)
+ {
+ DataContext = new PlayerViewModel();
+
+ if (xaml != null)
+ new AvaloniaXamlLoader().Load(xaml, null, this);
+ else
+ AvaloniaXamlLoader.Load(this);
+
+ Initialize();
+
+ _updateTimer = new Timer(1000 / 60);
+
+ _updateTimer.Elapsed += (sender, e) =>
+ {
+ try
+ {
+ UpdateView(sender, e);
+ }
+ catch(Exception ex)
+ {
+ Console.WriteLine(ex);
+ }
+ };
+
+ _updateTimer.AutoReset = true;
+ _updateTimer.Start();
+ }
+
+ ///
+ /// Indicates if the image is considered "playable" or not
+ ///
+ /// Aaruformat image file
+ /// True if the image is playble, false otherwise
+ private bool IsPlayableImage(AaruFormat image)
+ {
+ // Invalid images can't be played
+ if (image == null)
+ return false;
+
+ // Tape images are not supported
+ if (image.IsTape)
+ return false;
+
+ // Determine based on media type
+ // TODO: Can we be more granular with sub types?
+ (string type, string _) = Aaru.CommonTypes.Metadata.MediaType.MediaTypeToString(image.Info.MediaType);
+ return type switch
+ {
+ "Compact Disc" => true,
+ "GD" => true, // Requires TOC generation
+ _ => false,
+ };
+ }
+
+ ///
+ /// Update the UI with the most recent information from the Player
+ ///
+ private void UpdateView(object sender, ElapsedEventArgs e)
+ {
+ Dispatcher.UIThread.InvokeAsync(() =>
+ {
+ string digitString = GenerateDigitString();
+ for (int i = 0; i < _digits.Length; i++)
+ {
+ if (_digits[i] != null)
+ _digits[i].Source = GetBitmap(digitString[i]);
+ }
+
+ if (Player.Initialized)
+ {
+ PlayerViewModel dataContext = (PlayerViewModel)DataContext;
+ dataContext.HiddenTrack = Player.TimeOffset > 150;
+ dataContext.ApplyDeEmphasis = Player.ApplyDeEmphasis;
+ dataContext.TrackHasEmphasis = Player.TrackHasEmphasis;
+ dataContext.CopyAllowed = Player.CopyAllowed;
+ dataContext.IsAudioTrack = Player.TrackType == TrackType.Audio;
+ dataContext.IsDataTrack = Player.TrackType != TrackType.Audio;
+ }
+ });
+ }
+
+ #endregion
+
+ #region Event Handlers
+
+ public async void LoadButton_Click(object sender, RoutedEventArgs e)
+ {
+ string path = await GetPath();
+ if (path == null)
+ return;
+
+ bool result = await Task.Run(() =>
+ {
+ var image = new AaruFormat();
+ var filter = new ZZZNoFilter();
+ filter.Open(path);
+ image.Open(filter);
+
+ if (IsPlayableImage(image))
+ {
+ Player.Init(image, App.Settings.AutoPlay);
+ return true;
+ }
+ else
+ return false;
+ });
+
+ if (result)
+ {
+ await Dispatcher.UIThread.InvokeAsync(() =>
+ {
+ MainWindow.Instance.Title = "RedBookPlayer - " + path.Split('/').Last().Split('\\').Last();
+ });
+ }
+ }
+
public void PlayButton_Click(object sender, RoutedEventArgs e) => Player.Play();
public void PauseButton_Click(object sender, RoutedEventArgs e) => Player.Pause();
@@ -80,208 +287,18 @@ namespace RedBookPlayer
public void PreviousTrackButton_Click(object sender, RoutedEventArgs e) => Player.PreviousTrack();
- public void NextIndexButton_Click(object sender, RoutedEventArgs e) =>
- Player.NextIndex(App.Settings.IndexButtonChangeTrack);
+ public void NextIndexButton_Click(object sender, RoutedEventArgs e) => Player.NextIndex(App.Settings.IndexButtonChangeTrack);
- public void PreviousIndexButton_Click(object sender, RoutedEventArgs e) =>
- Player.PreviousIndex(App.Settings.IndexButtonChangeTrack);
+ public void PreviousIndexButton_Click(object sender, RoutedEventArgs e) => Player.PreviousIndex(App.Settings.IndexButtonChangeTrack);
public void FastForwardButton_Click(object sender, RoutedEventArgs e) => Player.FastForward();
public void RewindButton_Click(object sender, RoutedEventArgs e) => Player.Rewind();
- public void EnableDeEmphasisButton_Click(object sender, RoutedEventArgs e) => Player.EnableDeEmphasis();
+ public void EnableDeEmphasisButton_Click(object sender, RoutedEventArgs e) => Player.ToggleDeEmphasis(true);
- public void DisableDeEmphasisButton_Click(object sender, RoutedEventArgs e) => Player.DisableDeEmphasis();
+ public void DisableDeEmphasisButton_Click(object sender, RoutedEventArgs e) => Player.ToggleDeEmphasis(false);
- void UpdateView(object sender, ElapsedEventArgs e)
- {
- if(Player.Initialized)
- {
- ulong sectorTime = Player.CurrentSector;
-
- if(Player.SectionStartSector != 0)
- {
- sectorTime -= Player.SectionStartSector;
- }
- else
- {
- sectorTime += Player.TimeOffset;
- }
-
- int[] numbers =
- {
- Player.CurrentTrack + 1, Player.CurrentIndex, (int)(sectorTime / (75 * 60)),
- (int)(sectorTime / 75 % 60), (int)(sectorTime % 75), Player.TotalTracks, Player.TotalIndexes,
- (int)(Player.TotalTime / (75 * 60)), (int)(Player.TotalTime / 75 % 60), (int)(Player.TotalTime % 75)
- };
-
- string digitString = string.Join("", numbers.Select(i => i.ToString().PadLeft(2, '0').Substring(0, 2)));
-
- Dispatcher.UIThread.InvokeAsync(() =>
- {
- for(int i = 0; i < digits.Length; i++)
- {
- if(digits[i] != null)
- {
- digits[i].Source = GetBitmap(digitString[i]);
- }
- }
-
- var dataContext = (PlayerViewModel)DataContext;
- dataContext.HiddenTrack = Player.TimeOffset > 150;
- dataContext.ApplyDeEmphasis = Player.ApplyDeEmphasis;
- dataContext.TrackHasEmphasis = Player.TrackHasEmphasis;
- dataContext.CopyAllowed = Player.CopyAllowed;
- dataContext.IsAudioTrack = Player.TrackType_ == Player.TrackType.Audio;
- dataContext.IsDataTrack = Player.TrackType_ == Player.TrackType.Data;
- });
- }
- else
- {
- Dispatcher.UIThread.InvokeAsync(() =>
- {
- foreach(Image digit in digits)
- {
- if(digit != null)
- {
- digit.Source = GetBitmap('-');
- }
- }
- });
- }
- }
-
- Bitmap GetBitmap(char character)
- {
- if(App.Settings.SelectedTheme == "default")
- {
- IAssetLoader assets = AvaloniaLocator.Current.GetService();
-
- return new Bitmap(assets.Open(new Uri($"avares://RedBookPlayer/Assets/{character}.png")));
- }
-
- string themeDirectory = Directory.GetCurrentDirectory() + "/themes/" + App.Settings.SelectedTheme;
- Bitmap bitmap;
-
- using(FileStream stream = File.Open(themeDirectory + $"/{character}.png", FileMode.Open))
- {
- bitmap = new Bitmap(stream);
- }
-
- return bitmap;
- }
-
- public void Initialize()
- {
- digits = new Image[20];
-
- digits[0] = this.FindControl("TrackDigit1");
- digits[1] = this.FindControl("TrackDigit2");
-
- digits[2] = this.FindControl("IndexDigit1");
- digits[3] = this.FindControl("IndexDigit2");
-
- digits[4] = this.FindControl("TimeDigit1");
- digits[5] = this.FindControl("TimeDigit2");
- digits[6] = this.FindControl("TimeDigit3");
- digits[7] = this.FindControl("TimeDigit4");
- digits[8] = this.FindControl("TimeDigit5");
- digits[9] = this.FindControl("TimeDigit6");
-
- digits[10] = this.FindControl("TotalTracksDigit1");
- digits[11] = this.FindControl("TotalTracksDigit2");
-
- digits[12] = this.FindControl("TotalIndexesDigit1");
- digits[13] = this.FindControl("TotalIndexesDigit2");
-
- digits[14] = this.FindControl("TotalTimeDigit1");
- digits[15] = this.FindControl("TotalTimeDigit2");
- digits[16] = this.FindControl("TotalTimeDigit3");
- digits[17] = this.FindControl("TotalTimeDigit4");
- digits[18] = this.FindControl("TotalTimeDigit5");
- digits[19] = this.FindControl("TotalTimeDigit6");
-
- currentTrack = this.FindControl("CurrentTrack");
- }
-
- void InitializeComponent(string xaml)
- {
- DataContext = new PlayerViewModel();
-
- if(xaml != null)
- {
- new AvaloniaXamlLoader().Load(xaml, null, this);
- }
- else
- {
- AvaloniaXamlLoader.Load(this);
- }
-
- Initialize();
-
- updateTimer = new Timer(1000 / 60);
-
- updateTimer.Elapsed += (sender, e) =>
- {
- try
- {
- UpdateView(sender, e);
- }
- catch(Exception ex)
- {
- Console.WriteLine(ex);
- }
- };
-
- updateTimer.AutoReset = true;
- updateTimer.Start();
- }
- }
-
- public class PlayerViewModel : ReactiveObject
- {
- bool applyDeEmphasis;
- bool copyAllowed;
- bool hiddenTrack;
- bool isAudioTrack;
- bool isDataTrack;
- bool trackHasEmphasis;
-
- public bool ApplyDeEmphasis
- {
- get => applyDeEmphasis;
- set => this.RaiseAndSetIfChanged(ref applyDeEmphasis, value);
- }
-
- public bool TrackHasEmphasis
- {
- get => trackHasEmphasis;
- set => this.RaiseAndSetIfChanged(ref trackHasEmphasis, value);
- }
-
- public bool HiddenTrack
- {
- get => hiddenTrack;
- set => this.RaiseAndSetIfChanged(ref hiddenTrack, value);
- }
-
- public bool CopyAllowed
- {
- get => copyAllowed;
- set => this.RaiseAndSetIfChanged(ref copyAllowed, value);
- }
-
- public bool IsAudioTrack
- {
- get => isAudioTrack;
- set => this.RaiseAndSetIfChanged(ref isAudioTrack, value);
- }
-
- public bool IsDataTrack
- {
- get => isDataTrack;
- set => this.RaiseAndSetIfChanged(ref isDataTrack, value);
- }
+ #endregion
}
}
\ No newline at end of file
diff --git a/RedBookPlayer/PlayerViewModel.cs b/RedBookPlayer/PlayerViewModel.cs
new file mode 100644
index 0000000..a44dcc4
--- /dev/null
+++ b/RedBookPlayer/PlayerViewModel.cs
@@ -0,0 +1,49 @@
+using ReactiveUI;
+
+namespace RedBookPlayer
+{
+ public class PlayerViewModel : ReactiveObject
+ {
+ private bool _applyDeEmphasis;
+ public bool ApplyDeEmphasis
+ {
+ get => _applyDeEmphasis;
+ set => this.RaiseAndSetIfChanged(ref _applyDeEmphasis, 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);
+ }
+
+ private bool _copyAllowed;
+ public bool CopyAllowed
+ {
+ get => _copyAllowed;
+ set => this.RaiseAndSetIfChanged(ref _copyAllowed, value);
+ }
+
+ private bool _isAudioTrack;
+ public bool IsAudioTrack
+ {
+ get => _isAudioTrack;
+ set => this.RaiseAndSetIfChanged(ref _isAudioTrack, value);
+ }
+
+ private bool _isDataTrack;
+ public bool IsDataTrack
+ {
+ get => _isDataTrack;
+ set => this.RaiseAndSetIfChanged(ref _isDataTrack, value);
+ }
+ }
+}
\ No newline at end of file
diff --git a/RedBookPlayer/Program.cs b/RedBookPlayer/Program.cs
index 64edf12..111558d 100644
--- a/RedBookPlayer/Program.cs
+++ b/RedBookPlayer/Program.cs
@@ -1,4 +1,7 @@
-using Avalonia;
+#if Windows
+using System.Runtime.InteropServices;
+#endif
+using Avalonia;
using Avalonia.Logging.Serilog;
namespace RedBookPlayer
diff --git a/RedBookPlayer/Settings.cs b/RedBookPlayer/Settings.cs
index 0411c1d..8640e43 100644
--- a/RedBookPlayer/Settings.cs
+++ b/RedBookPlayer/Settings.cs
@@ -6,18 +6,55 @@ namespace RedBookPlayer
{
public class Settings
{
- string filePath;
+ ///
+ /// Indicates if discs should start playing on load
+ ///
+ public bool AutoPlay { get; set; } = false;
+
+ ///
+ /// Indicates if an index change can trigger a track change
+ ///
+ public bool IndexButtonChangeTrack { get; set; } = false;
+
+ ///
+ /// Indicates if the index 0 of track 1 is treated like a hidden track
+ ///
+ public bool AllowSkipHiddenTrack { get; set; } = false;
+
+ ///
+ /// Indicates if data tracks should be played like old, non-compliant players
+ ///
+ public bool PlayDataTracks { get; set; } = false;
+
+ ///
+ /// Generate a TOC if the disc is missing one
+ ///
+ public bool GenerateMissingTOC { get; set; } = true;
+
+ ///
+ /// Indicates the default playback volume
+ ///
+ public int Volume { get; set; } = 100;
+
+ ///
+ /// Indicates the currently selected theme
+ ///
+ public string SelectedTheme { get; set; } = "default";
+
+ ///
+ /// Path to the settings file
+ ///
+ private string _filePath;
public Settings() {}
- public Settings(string filePath) => this.filePath = filePath;
-
- public bool AutoPlay { get; set; }
- public bool IndexButtonChangeTrack { get; set; }
- public bool AllowSkipHiddenTrack { get; set; }
- public int Volume { get; set; } = 100;
- public string SelectedTheme { get; set; } = "default";
+ public Settings(string filePath) => _filePath = filePath;
+ ///
+ /// Load settings from a file
+ ///
+ /// Path to the settings JSON file
+ /// Settings derived from the input file, if possible
public static Settings Load(string filePath)
{
if(File.Exists(filePath))
@@ -25,7 +62,7 @@ namespace RedBookPlayer
try
{
Settings settings = JsonSerializer.Deserialize(File.ReadAllText(filePath));
- settings.filePath = filePath;
+ settings._filePath = filePath;
MainWindow.ApplyTheme(settings.SelectedTheme);
@@ -42,6 +79,9 @@ namespace RedBookPlayer
return new Settings(filePath);
}
+ ///
+ /// Save settings to a file
+ ///
public void Save()
{
var options = new JsonSerializerOptions
@@ -50,7 +90,7 @@ namespace RedBookPlayer
};
string json = JsonSerializer.Serialize(this, options);
- File.WriteAllText(filePath, json);
+ File.WriteAllText(_filePath, json);
}
}
}
\ No newline at end of file
diff --git a/RedBookPlayer/SettingsWindow.xaml b/RedBookPlayer/SettingsWindow.xaml
index 0760065..3f2ce4c 100644
--- a/RedBookPlayer/SettingsWindow.xaml
+++ b/RedBookPlayer/SettingsWindow.xaml
@@ -17,6 +17,14 @@
Treat index 0 of track 1 as track 0 (hidden track)
+
+
+ Play data tracks like old, non-compliant players
+
+
+
+ Generate a TOC if the disc is missing one
+
Volume
diff --git a/RedBookPlayer/SettingsWindow.xaml.cs b/RedBookPlayer/SettingsWindow.xaml.cs
index 4873089..abc880f 100644
--- a/RedBookPlayer/SettingsWindow.xaml.cs
+++ b/RedBookPlayer/SettingsWindow.xaml.cs
@@ -8,49 +8,47 @@ namespace RedBookPlayer
{
public class SettingsWindow : Window
{
- readonly Settings settings;
- string selectedTheme;
- ListBox themeList;
+ private readonly Settings _settings;
+ private string _selectedTheme;
+ private ListBox _themeList;
public SettingsWindow() {}
public SettingsWindow(Settings settings)
{
- DataContext = this.settings = settings;
+ DataContext = _settings = settings;
InitializeComponent();
}
public void ThemeList_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
- if(e.AddedItems.Count == 0)
- {
+ if (e.AddedItems.Count == 0)
return;
- }
- selectedTheme = (string)e.AddedItems[0];
+ _selectedTheme = (string)e.AddedItems[0];
}
public void ApplySettings(object sender, RoutedEventArgs e)
{
- if((selectedTheme ?? "") != "")
+ if (!string.IsNullOrWhiteSpace(_selectedTheme))
{
- settings.SelectedTheme = selectedTheme;
- MainWindow.ApplyTheme(selectedTheme);
+ _settings.SelectedTheme = _selectedTheme;
+ MainWindow.ApplyTheme(_selectedTheme);
}
- PlayerView.Player.Volume = settings.Volume;
+ PlayerView.Player.Volume = _settings.Volume;
- settings.Save();
+ _settings.Save();
}
- public void UpdateView() => this.FindControl("VolumeLabel").Text = settings.Volume.ToString();
+ public void UpdateView() => this.FindControl("VolumeLabel").Text = _settings.Volume.ToString();
void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
- themeList = this.FindControl("ThemeList");
- themeList.SelectionChanged += ThemeList_SelectionChanged;
+ _themeList = this.FindControl("ThemeList");
+ _themeList.SelectionChanged += ThemeList_SelectionChanged;
List items = new List();
items.Add("default");
@@ -61,16 +59,14 @@ namespace RedBookPlayer
{
string themeName = dir.Split('/')[1];
- if(!File.Exists($"themes/{themeName}/view.xaml"))
- {
+ if (!File.Exists($"themes/{themeName}/view.xaml"))
continue;
- }
items.Add(themeName);
}
}
- themeList.Items = items;
+ _themeList.Items = items;
this.FindControl