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