commit abe06fcdb998fe6283e63d203cc9c02db496366c Author: deagahelio <16138291+deagahelio@users.noreply.github.com> Date: Fri Mar 19 17:07:27 2021 -0300 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..abf36a6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,338 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +## Visual Studio Code specific files and folder +.vscode +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.jsons + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ +**/Properties/launchSettings.json + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush +.cr/ + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..361306b --- /dev/null +++ b/.gitmodules @@ -0,0 +1,6 @@ +[submodule "Aaru"] + path = Aaru + url = https://github.com/aaru-dps/Aaru.git +[submodule "cscore"] + path = cscore + url = https://github.com/deagahelio/cscore.git diff --git a/Aaru b/Aaru new file mode 160000 index 0000000..65739fa --- /dev/null +++ b/Aaru @@ -0,0 +1 @@ +Subproject commit 65739fa9660fd4637204288898b80779484b359f diff --git a/README.md b/README.md new file mode 100644 index 0000000..885b934 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# RedBookPlayer + +[Audio CD](https://en.wikipedia.org/wiki/Compact_Disc_Digital_Audio) player for [Aaru format](https://github.com/aaru-dps/Aaru). \ No newline at end of file diff --git a/RedBookPlayer.sln b/RedBookPlayer.sln new file mode 100644 index 0000000..7578d18 --- /dev/null +++ b/RedBookPlayer.sln @@ -0,0 +1,34 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.26124.0 +MinimumVisualStudioVersion = 15.0.26124.0 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RedBookPlayer", "RedBookPlayer\RedBookPlayer.csproj", "{94944959-0352-4ABF-9C5C-19FF33747ECE}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + 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 + {94944959-0352-4ABF-9C5C-19FF33747ECE}.Debug|x64.ActiveCfg = Debug|Any CPU + {94944959-0352-4ABF-9C5C-19FF33747ECE}.Debug|x64.Build.0 = Debug|Any CPU + {94944959-0352-4ABF-9C5C-19FF33747ECE}.Debug|x86.ActiveCfg = Debug|Any CPU + {94944959-0352-4ABF-9C5C-19FF33747ECE}.Debug|x86.Build.0 = Debug|Any CPU + {94944959-0352-4ABF-9C5C-19FF33747ECE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {94944959-0352-4ABF-9C5C-19FF33747ECE}.Release|Any CPU.Build.0 = Release|Any CPU + {94944959-0352-4ABF-9C5C-19FF33747ECE}.Release|x64.ActiveCfg = Release|Any CPU + {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 + EndGlobalSection +EndGlobal diff --git a/RedBookPlayer/App.xaml b/RedBookPlayer/App.xaml new file mode 100644 index 0000000..911230d --- /dev/null +++ b/RedBookPlayer/App.xaml @@ -0,0 +1,8 @@ + + + + + + diff --git a/RedBookPlayer/App.xaml.cs b/RedBookPlayer/App.xaml.cs new file mode 100644 index 0000000..efc805a --- /dev/null +++ b/RedBookPlayer/App.xaml.cs @@ -0,0 +1,40 @@ +using System; +using System.IO; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Markup.Xaml; + +namespace RedBookPlayer +{ + public class App : Application + { + public static string CurrentTheme = "default"; + + static App() + { + Directory.SetCurrentDirectory(Path.GetDirectoryName(System.Diagnostics.Process.GetCurrentProcess().MainModule.FileName)); + } + + public override void Initialize() + { + AppDomain.CurrentDomain.UnhandledException += (e, f) => + { + Console.WriteLine(((Exception)f.ExceptionObject).ToString()); + }; + + AvaloniaXamlLoader.Load(this); + } + + public override void OnFrameworkInitializationCompleted() + { + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + desktop.MainWindow = new MainWindow(); + desktop.ShutdownMode = ShutdownMode.OnMainWindowClose; + } + + base.OnFrameworkInitializationCompleted(); + } + } +} \ No newline at end of file diff --git a/RedBookPlayer/Assets/-.png b/RedBookPlayer/Assets/-.png new file mode 100644 index 0000000..4e81634 Binary files /dev/null and b/RedBookPlayer/Assets/-.png differ diff --git a/RedBookPlayer/Assets/0.png b/RedBookPlayer/Assets/0.png new file mode 100644 index 0000000..5980b5f Binary files /dev/null and b/RedBookPlayer/Assets/0.png differ diff --git a/RedBookPlayer/Assets/1.png b/RedBookPlayer/Assets/1.png new file mode 100644 index 0000000..b8ca63d Binary files /dev/null and b/RedBookPlayer/Assets/1.png differ diff --git a/RedBookPlayer/Assets/2.png b/RedBookPlayer/Assets/2.png new file mode 100644 index 0000000..b1e2c46 Binary files /dev/null and b/RedBookPlayer/Assets/2.png differ diff --git a/RedBookPlayer/Assets/3.png b/RedBookPlayer/Assets/3.png new file mode 100644 index 0000000..dfd97e4 Binary files /dev/null and b/RedBookPlayer/Assets/3.png differ diff --git a/RedBookPlayer/Assets/4.png b/RedBookPlayer/Assets/4.png new file mode 100644 index 0000000..900b473 Binary files /dev/null and b/RedBookPlayer/Assets/4.png differ diff --git a/RedBookPlayer/Assets/5.png b/RedBookPlayer/Assets/5.png new file mode 100644 index 0000000..2ae7c1b Binary files /dev/null and b/RedBookPlayer/Assets/5.png differ diff --git a/RedBookPlayer/Assets/6.png b/RedBookPlayer/Assets/6.png new file mode 100644 index 0000000..524d153 Binary files /dev/null and b/RedBookPlayer/Assets/6.png differ diff --git a/RedBookPlayer/Assets/7.png b/RedBookPlayer/Assets/7.png new file mode 100644 index 0000000..d782ea2 Binary files /dev/null and b/RedBookPlayer/Assets/7.png differ diff --git a/RedBookPlayer/Assets/8.png b/RedBookPlayer/Assets/8.png new file mode 100644 index 0000000..02326ac Binary files /dev/null and b/RedBookPlayer/Assets/8.png differ diff --git a/RedBookPlayer/Assets/9.png b/RedBookPlayer/Assets/9.png new file mode 100644 index 0000000..011312e Binary files /dev/null and b/RedBookPlayer/Assets/9.png differ diff --git a/RedBookPlayer/Assets/:.png b/RedBookPlayer/Assets/:.png new file mode 100644 index 0000000..006ed17 Binary files /dev/null and b/RedBookPlayer/Assets/:.png differ diff --git a/RedBookPlayer/DeEmphasisFilter.cs b/RedBookPlayer/DeEmphasisFilter.cs new file mode 100644 index 0000000..fd5c8bd --- /dev/null +++ b/RedBookPlayer/DeEmphasisFilter.cs @@ -0,0 +1,42 @@ +using System; +using NWaves.Filters.BiQuad; + +namespace RedBookPlayer +{ + public class DeEmphasisFilter : BiQuadFilter + { + static double B0, B1, B2, A0, A1, A2; + + static DeEmphasisFilter() + { + double fc = 5277; + double slope = 0.4850; + double gain = -9.465; + + double w0 = 2 * Math.PI * fc / 44100; + double A = Math.Exp(gain / 40 * Math.Log(10)); + double alpha = Math.Sin(w0) / 2 * Math.Sqrt((A + 1 / A) * (1 / slope - 1) + 2); + + double cs = Math.Cos(w0); + double v = 2 * Math.Sqrt(A) * alpha; + + B0 = A * ((A + 1) + (A - 1) * cs + v); + B1 = -2 * A * ((A - 1) + (A + 1) * cs); + B2 = A * ((A + 1) + (A - 1) * cs - v); + A0 = (A + 1) - (A - 1) * cs + v; + A1 = 2 * ((A - 1) - (A + 1) * cs); + A2 = (A + 1) - (A - 1) * cs - v; + + B2 /= A0; + B1 /= A0; + B0 /= A0; + A2 /= A0; + A1 /= A0; + A0 = 1; + } + + public DeEmphasisFilter() : base(B0, B1, B2, A0, A1, A2) + { + } + } +} \ No newline at end of file diff --git a/RedBookPlayer/HiResTimer.cs b/RedBookPlayer/HiResTimer.cs new file mode 100644 index 0000000..ea05e59 --- /dev/null +++ b/RedBookPlayer/HiResTimer.cs @@ -0,0 +1,126 @@ +using System; +using System.Diagnostics; +using System.Threading; + +namespace RedBookPlayer { + public class HiResTimer + { + private static readonly float tickFrequency = 1000f / Stopwatch.Frequency; + + public event EventHandler Elapsed; + + private volatile float interval; + private volatile bool isRunning; + + public HiResTimer() : this(1f) + { + } + + public HiResTimer(float interval) + { + if (interval < 0f || Single.IsNaN(interval)) + throw new ArgumentOutOfRangeException(nameof(interval)); + this.interval = interval; + } + + public float Interval + { + get { return interval; } + set + { + if (value < 0f || Single.IsNaN(value)) + throw new ArgumentOutOfRangeException(nameof(value)); + interval = value; + } + } + + public bool Enabled + { + set + { + if (value) + Start(); + else + Stop(); + } + get { return isRunning; } + } + + public void Start() + { + if (isRunning) + return; + + isRunning = true; + Thread thread = new Thread(ExecuteTimer); + thread.Priority = ThreadPriority.Highest; + thread.Start(); + } + + public void Stop() + { + isRunning = false; + } + + private void ExecuteTimer() + { + float nextTrigger = 0f; + + Stopwatch stopwatch = new Stopwatch(); + stopwatch.Start(); + + while (isRunning) + { + nextTrigger += interval; + float elapsed; + + while (true) + { + elapsed = ElapsedHiRes(stopwatch); + float diff = nextTrigger - elapsed; + if (diff <= 0f) + break; + + if (diff < 1f) + Thread.SpinWait(10); + else if (diff < 5f) + Thread.SpinWait(100); + else if (diff < 15f) + Thread.Sleep(1); + else + Thread.Sleep(10); + + if (!isRunning) + return; + } + + + float delay = elapsed - nextTrigger; + Elapsed?.Invoke(this, new HiResTimerElapsedEventArgs(delay)); + + if (stopwatch.Elapsed.TotalHours >= 1d) + { + stopwatch.Restart(); + nextTrigger = 0f; + } + } + + stopwatch.Stop(); + } + + private static float ElapsedHiRes(Stopwatch stopwatch) + { + return stopwatch.ElapsedTicks * tickFrequency; + } + } + + public class HiResTimerElapsedEventArgs : EventArgs + { + public float Delay { get; } + + internal HiResTimerElapsedEventArgs(float delay) + { + Delay = delay; + } + } +} \ No newline at end of file diff --git a/RedBookPlayer/MainWindow.xaml b/RedBookPlayer/MainWindow.xaml new file mode 100644 index 0000000..9b7f264 --- /dev/null +++ b/RedBookPlayer/MainWindow.xaml @@ -0,0 +1,10 @@ + + + diff --git a/RedBookPlayer/MainWindow.xaml.cs b/RedBookPlayer/MainWindow.xaml.cs new file mode 100644 index 0000000..ffa29cb --- /dev/null +++ b/RedBookPlayer/MainWindow.xaml.cs @@ -0,0 +1,55 @@ +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using Avalonia.Input; + +namespace RedBookPlayer +{ + public class MainWindow : Window + { + public static MainWindow Instance; + public ContentControl ContentControl; + public Window themeSelectionWindow; + + public MainWindow() + { + Instance = this; + InitializeComponent(); + } + + public void OnKeyDown(object sender, KeyEventArgs e) + { + if (e.Key == Key.F1) + { + themeSelectionWindow = new ThemeSelectionWindow(); + themeSelectionWindow.Show(); + } + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + + ContentControl = this.FindControl("Content"); + ContentControl.Content = new PlayerView(); + + MainWindow.Instance.MaxWidth = ((PlayerView)MainWindow.Instance.ContentControl.Content).Width; + MainWindow.Instance.MaxHeight = ((PlayerView)MainWindow.Instance.ContentControl.Content).Height; + + ContentControl.Content = new PlayerView(); + + this.CanResize = false; + + this.KeyDown += OnKeyDown; + this.Closing += (s, e) => + { + themeSelectionWindow?.Close(); + themeSelectionWindow = null; + }; + + this.Closing += (e, f) => + { + PlayerView.player.Shutdown(); + }; + } + } +} \ No newline at end of file diff --git a/RedBookPlayer/Player.cs b/RedBookPlayer/Player.cs new file mode 100644 index 0000000..bda2e69 --- /dev/null +++ b/RedBookPlayer/Player.cs @@ -0,0 +1,435 @@ +using System; +using System.Threading.Tasks; +using Aaru.CommonTypes.Enums; +using Aaru.Decoders.CD; +using Aaru.DiscImages; +using Aaru.Helpers; +using System.Linq; +using CSCore.SoundOut; +using CSCore; +using NWaves.Audio; +using NWaves.Filters.BiQuad; + +namespace RedBookPlayer +{ + public class Player + { + public bool Initialized = false; + private int currentTrack = 0; + public int CurrentTrack + { + get + { + return currentTrack; + } + + private set + { + if (Image != null) + { + if (value >= Image.Tracks.Count) + { + currentTrack = 0; + } + else if (value < 0) + { + currentTrack = Image.Tracks.Count - 1; + } + else + { + currentTrack = value; + } + + byte[] flagsData = Image.ReadSectorTag(Image.Tracks[CurrentTrack].TrackSequence, SectorTagType.CdTrackFlags); + HasPreEmphasis = ((CdFlags)flagsData[0]).HasFlag(CdFlags.PreEmphasis); + + CurrentIndex = Image.Tracks[CurrentTrack].Indexes.Keys.GetEnumerator().Current; + } + } + } + public ushort CurrentIndex { get; private set; } = 1; + private ulong currentSector = 0; + private int currentSectorReadPosition = 0; + public ulong CurrentSector + { + get + { + return currentSector; + } + + private set + { + currentSector = value; + + if (Image != null) + { + if (CurrentTrack < Image.Tracks.Count - 1 && CurrentSector >= Image.Tracks[CurrentTrack + 1].TrackStartSector) + { + CurrentTrack++; + } + else if (CurrentTrack > 0 && CurrentSector < Image.Tracks[CurrentTrack].TrackStartSector) + { + CurrentTrack--; + } + + foreach (var item in Image.Tracks[CurrentTrack].Indexes.Reverse()) + { + if ((int)CurrentSector >= item.Value) + { + CurrentIndex = item.Key; + return; + } + } + CurrentIndex = 0; + } + } + } + public bool HasPreEmphasis { get; private set; } = false; + public AaruFormat Image { get; private set; } + FullTOC.CDFullTOC toc; + PlayerSource source; + ALSoundOut soundOut; + BiQuadFilter deEmphasisFilterLeft; + BiQuadFilter deEmphasisFilterRight; + bool readingImage = false; + + public async void Init(AaruFormat image) + { + this.Image = image; + + if (await Task.Run(() => image.Info.ReadableMediaTags?.Contains(MediaTagType.CD_FullTOC)) != true) + { + Console.WriteLine("Full TOC not found"); + return; + } + + byte[] tocBytes = await Task.Run(() => image.ReadDiskTag(MediaTagType.CD_FullTOC)); + + 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; + } + + FullTOC.CDFullTOC? nullableToc = await Task.Run(() => FullTOC.Decode(tocBytes)); + + if (nullableToc == null) + { + Console.WriteLine("Error decoding TOC"); + return; + } + + toc = nullableToc.Value; + + Console.WriteLine(FullTOC.Prettify(toc)); + + CurrentTrack = 0; + CurrentSector = 0; + + deEmphasisFilterLeft = new DeEmphasisFilter(); + deEmphasisFilterRight = new DeEmphasisFilter(); + source = new PlayerSource(ProviderRead); + + soundOut?.Dispose(); + soundOut = new ALSoundOut(50); + soundOut.Initialize(source); + soundOut.Play(); + + LoadTrack(CurrentTrack); + + Initialized = true; + + source.Start(); + } + + public int ProviderRead(byte[] buffer, int offset, int count) + { + ulong sectorsToRead; + ulong zeroSectorsAmount; + + do + { + sectorsToRead = (ulong)count / 2352 + 2; + zeroSectorsAmount = 0; + + if (CurrentSector + sectorsToRead > Image.Info.Sectors) + { + ulong oldSectorsToRead = sectorsToRead; + sectorsToRead = Image.Info.Sectors - CurrentSector; + zeroSectorsAmount = oldSectorsToRead - sectorsToRead; + } + + if (sectorsToRead <= 0) + { + CurrentSector = 0; + currentSectorReadPosition = 0; + } + } while (sectorsToRead <= 0); + + byte[] zeroSectors = new Byte[zeroSectorsAmount * 2352]; + Array.Clear(zeroSectors, 0, zeroSectors.Length); + byte[] audioData; + + Task task = Task.Run(() => + { + try + { + return Image.ReadSectors(CurrentSector, (uint)sectorsToRead).Concat(zeroSectors).ToArray(); + } + catch (System.ArgumentOutOfRangeException) + { + CurrentSector = 0; + return Image.ReadSectors(CurrentSector, (uint)sectorsToRead).Concat(zeroSectors).ToArray(); + } + }); + + if (task.Wait(TimeSpan.FromMilliseconds(100))) + { + audioData = task.Result; + } + else + { + Array.Clear(buffer, offset, count); + return count; + } + + Task.Run(() => + { + if (!readingImage) + { + readingImage = true; + Image.ReadSector(CurrentSector + 375); + readingImage = false; + } + }); + + byte[] audioDataSegment = new byte[count]; + Array.Copy(audioData, currentSectorReadPosition, audioDataSegment, 0, count); + + if (HasPreEmphasis) + { + float[][] floatAudioData = new float[2][]; + floatAudioData[0] = new float[audioDataSegment.Length / 4]; + floatAudioData[1] = new float[audioDataSegment.Length / 4]; + ByteConverter.ToFloats16Bit(audioDataSegment, floatAudioData); + + for (int i = 0; i < floatAudioData[0].Length; i++) + { + floatAudioData[0][i] = deEmphasisFilterLeft.Process(floatAudioData[0][i]); + floatAudioData[1][i] = deEmphasisFilterRight.Process(floatAudioData[1][i]); + } + + ByteConverter.FromFloats16Bit(floatAudioData, audioDataSegment); + } + + Array.Copy(audioDataSegment, 0, buffer, offset, count); + + 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 = Image.Tracks[index].TrackStartSector; + + source.Run = oldRun; + } + + public void Play() + { + if (Image == null) + { + return; + } + + soundOut.Play(); + } + + public void Pause() + { + if (Image == null) + { + return; + } + + soundOut.Stop(); + } + + public void Stop() + { + if (Image == null) + { + return; + } + + soundOut.Stop(); + LoadTrack(CurrentTrack); + } + + public void NextTrack() + { + if (Image == null) + { + return; + } + + if (++CurrentTrack >= Image.Tracks.Count) + { + CurrentTrack = 0; + } + + LoadTrack(CurrentTrack); + } + + public void PreviousTrack() + { + if (Image == null) + { + return; + } + + if (--CurrentTrack < 0) + { + CurrentTrack = Image.Tracks.Count - 1; + } + + LoadTrack(CurrentTrack); + } + + public void NextIndex() + { + if (Image == null) + { + return; + } + + if (++CurrentIndex > Image.Tracks[CurrentTrack].Indexes.Keys.Max()) + { + NextTrack(); + CurrentSector = (ulong)Image.Tracks[CurrentTrack].Indexes[1]; + } + else + { + CurrentSector = (ulong)Image.Tracks[CurrentTrack].Indexes[CurrentIndex]; + } + } + + public void PreviousIndex() + { + if (Image == null) + { + return; + } + + if (CurrentIndex <= 1 || --CurrentIndex < Image.Tracks[CurrentTrack].Indexes.Keys.Min()) + { + PreviousTrack(); + CurrentSector = (ulong)Image.Tracks[CurrentTrack].Indexes.Values.Max(); + } + else + { + CurrentSector = (ulong)Image.Tracks[CurrentTrack].Indexes[CurrentIndex]; + } + } + + public void FastForward() + { + if (Image == null) + { + return; + } + + CurrentSector = Math.Min(Image.Info.Sectors - 1, CurrentSector + 75); + } + + public void Rewind() + { + if (Image == null) + { + return; + } + + if (CurrentSector >= 75) + CurrentSector -= 75; + } + + public void EnableDeEmphasis() + { + HasPreEmphasis = true; + } + + public void DisableDeEmphasis() + { + HasPreEmphasis = false; + } + + public void Shutdown() + { + soundOut?.Stop(); + source?.Stop(); + } + } + + public class PlayerSource : IWaveSource + { + public CSCore.WaveFormat WaveFormat => new CSCore.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 bool Run = true; + private ReadFunction read; + + public delegate int ReadFunction(byte[] buffer, int offset, int count); + + public PlayerSource(ReadFunction read) + { + this.read = read; + } + + public int Read(byte[] buffer, int offset, int count) + { + if (!Run) + { + Array.Clear(buffer, offset, count); + return count; + } + else + { + return read(buffer, offset, count); + } + } + + public void Start() + { + Run = true; + } + + public void Stop() + { + Run = false; + } + + public void Dispose() + { + } + } +} diff --git a/RedBookPlayer/PlayerView.xaml b/RedBookPlayer/PlayerView.xaml new file mode 100644 index 0000000..541581c --- /dev/null +++ b/RedBookPlayer/PlayerView.xaml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + Rewind + Fast Forward + + + + TRACK + + + + + + + INDEX + + + + + + + TIME + + + + + + + + + + + + + + + + + + diff --git a/RedBookPlayer/PlayerView.xaml.cs b/RedBookPlayer/PlayerView.xaml.cs new file mode 100644 index 0000000..5e705d2 --- /dev/null +++ b/RedBookPlayer/PlayerView.xaml.cs @@ -0,0 +1,249 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using System.Timers; +using Aaru.CommonTypes.Interfaces; +using Aaru.DiscImages; +using Aaru.Filters; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Markup.Xaml; +using Avalonia.Media.Imaging; +using Avalonia.Platform; +using Avalonia.Threading; +using ReactiveUI; + +namespace RedBookPlayer +{ + public class PlayerView : UserControl + { + public PlayerView() + { + InitializeComponent(null); + } + + public PlayerView(string xaml) + { + InitializeComponent(xaml); + } + + public static Player player = new Player(); + TextBlock currentTrack; + Image[] digits; + Timer updateTimer; + + public async void LoadButton_Click(object sender, RoutedEventArgs e) + { + string path = await GetPath(); + + if (path == null) + { + return; + } + + await Task.Run(() => + { + AaruFormat image = new AaruFormat(); + IFilter filter = new ZZZNoFilter(); + filter.Open(path); + image.Open(filter); + + player?.Shutdown(); + player = new Player(); + player.Init(image); + }); + + await Dispatcher.UIThread.InvokeAsync(() => + { + MainWindow.Instance.Title = "RedBookPlayer - " + path.Split('/').Last().Split('\\').Last(); + }); + } + + public async Task GetPath() + { + OpenFileDialog dialog = new OpenFileDialog(); + dialog.AllowMultiple = false; + + List knownExtensions = (new Aaru.DiscImages.AaruFormat()).KnownExtensions.ToList(); + dialog.Filters.Add(new FileDialogFilter() + { + Name = "Aaru Image Format (*" + string.Join(", *", knownExtensions) + ")", + Extensions = knownExtensions.ConvertAll(e => e.Substring(1)) + } + ); + + return (await dialog.ShowAsync((Window)this.Parent.Parent))?.FirstOrDefault(); + } + + public void PlayButton_Click(object sender, RoutedEventArgs e) + { + player.Play(); + } + + public void PauseButton_Click(object sender, RoutedEventArgs e) + { + player.Pause(); + } + + public void StopButton_Click(object sender, RoutedEventArgs e) + { + player.Stop(); + } + + public void NextTrackButton_Click(object sender, RoutedEventArgs e) + { + player.NextTrack(); + } + + public void PreviousTrackButton_Click(object sender, RoutedEventArgs e) + { + player.PreviousTrack(); + } + + public void NextIndexButton_Click(object sender, RoutedEventArgs e) + { + player.NextIndex(); + } + + public void PreviousIndexButton_Click(object sender, RoutedEventArgs e) + { + player.PreviousIndex(); + } + + 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 DisableDeEmphasisButton_Click(object sender, RoutedEventArgs e) + { + player.DisableDeEmphasis(); + } + + private void UpdateView(object sender, ElapsedEventArgs e) + { + if (player.Initialized) + { + string track = (player.CurrentTrack + 1).ToString().PadLeft(2, '0'); + string index = (player.CurrentIndex).ToString().PadLeft(2, '0'); + string frames = (player.CurrentSector % 75).ToString().PadLeft(2, '0'); + string seconds = ((player.CurrentSector / 75) % 60).ToString().PadLeft(2, '0'); + string minutes = ((player.CurrentSector / (75 * 60)) % 60).ToString().PadLeft(2, '0'); + + Dispatcher.UIThread.InvokeAsync(() => + { + int i = 0; + foreach (char digit in track + index + minutes + seconds + frames) + if (digits[i] != null) + digits[i++].Source = GetBitmap(digit); + + ((PlayerViewModel)DataContext).PreEmphasis = player.HasPreEmphasis; + }); + } + else + { + Dispatcher.UIThread.InvokeAsync(() => + { + foreach (Image digit in digits) + { + if (digit != null) + digit.Source = GetBitmap('-'); + } + }); + } + } + + private Bitmap GetBitmap(char character) + { + if (App.CurrentTheme == "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.CurrentTheme; + Bitmap bitmap; + using (FileStream stream = File.Open(themeDirectory + $"/{character}.png", FileMode.Open)) + { + bitmap = new Bitmap(stream); + } + return bitmap; + } + } + + public void Initialize() + { + digits = new Image[10]; + + 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"); + + currentTrack = this.FindControl("CurrentTrack"); + } + + 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(); + } + } + + public class PlayerViewModel : ReactiveObject + { + private bool preEmphasis; + public bool PreEmphasis + { + get => preEmphasis; + set => this.RaiseAndSetIfChanged(ref preEmphasis, value); + } + } +} \ No newline at end of file diff --git a/RedBookPlayer/Program.cs b/RedBookPlayer/Program.cs new file mode 100644 index 0000000..fbc45d8 --- /dev/null +++ b/RedBookPlayer/Program.cs @@ -0,0 +1,27 @@ +using Avalonia; +using Avalonia.Logging.Serilog; + +namespace RedBookPlayer +{ + class Program + { + public static void Main(string[] args) + { +#if Windows + AllocConsole(); +#endif + BuildAvaloniaApp().StartWithClassicDesktopLifetime(args); + } + +#if Windows + [DllImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + static extern bool AllocConsole(); +#endif + + public static AppBuilder BuildAvaloniaApp() + => AppBuilder.Configure() + .UsePlatformDetect() + .LogToDebug(); + } +} diff --git a/RedBookPlayer/RedBookPlayer.csproj b/RedBookPlayer/RedBookPlayer.csproj new file mode 100644 index 0000000..fc4f6b7 --- /dev/null +++ b/RedBookPlayer/RedBookPlayer.csproj @@ -0,0 +1,38 @@ + + + WinExe + netcoreapp3.1 + true + win-x64;linux-x64 + + + Windows + + + + %(Filename) + + + Designer + + + + + + + + + + + + + + + + + + + + + + diff --git a/RedBookPlayer/ThemeSelectionWindow.xaml b/RedBookPlayer/ThemeSelectionWindow.xaml new file mode 100644 index 0000000..c8d9ce2 --- /dev/null +++ b/RedBookPlayer/ThemeSelectionWindow.xaml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + diff --git a/RedBookPlayer/ThemeSelectionWindow.xaml.cs b/RedBookPlayer/ThemeSelectionWindow.xaml.cs new file mode 100644 index 0000000..860d84d --- /dev/null +++ b/RedBookPlayer/ThemeSelectionWindow.xaml.cs @@ -0,0 +1,95 @@ +using System; +using System.Collections.Generic; +using System.IO; +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Markup.Xaml; + +namespace RedBookPlayer +{ + public class ThemeSelectionWindow : Window + { + ListBox themeList; + string selectedTheme; + + public ThemeSelectionWindow() + { + InitializeComponent(); + } + + public void ThemeList_SelectionChanged(object sender, SelectionChangedEventArgs e) + { + if (e.AddedItems.Count == 0) + { + return; + } + + selectedTheme = (string)e.AddedItems[0]; + } + + public void ApplyTheme(object sender, RoutedEventArgs e) + { + if (selectedTheme == "" || selectedTheme == null) + { + return; + } + + if (selectedTheme == "default") + { + MainWindow.Instance.ContentControl.Content = new PlayerView(); + } + else + { + string themeDirectory = Directory.GetCurrentDirectory() + "/themes/" + selectedTheme; + string xamlPath = themeDirectory + "/view.xaml"; + + try + { + MainWindow.Instance.ContentControl.Content = new PlayerView( + File.ReadAllText(xamlPath).Replace("Source=\"", $"Source=\"file://{themeDirectory}/") + ); + } + catch (System.Xml.XmlException ex) + { + Console.WriteLine($"Error: invalid theme XAML ({ex.Message}), reverting to default"); + MainWindow.Instance.ContentControl.Content = new PlayerView(); + } + } + + MainWindow.Instance.Width = ((PlayerView)MainWindow.Instance.ContentControl.Content).Width; + MainWindow.Instance.Height = ((PlayerView)MainWindow.Instance.ContentControl.Content).Height; + + App.CurrentTheme = selectedTheme; + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + + themeList = this.FindControl("ThemeList"); + themeList.SelectionChanged += ThemeList_SelectionChanged; + + List items = new List(); + items.Add("default"); + + if (Directory.Exists("themes/")) + { + foreach (string dir in Directory.EnumerateDirectories("themes/")) + { + string themeName = dir.Split('/')[1]; + + if (!File.Exists($"themes/{themeName}/view.xaml")) + { + continue; + } + + items.Add(themeName); + } + } + + themeList.Items = items; + + this.FindControl