Initial commit
338
.gitignore
vendored
Normal file
@@ -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/
|
||||
6
.gitmodules
vendored
Normal file
@@ -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
|
||||
1
Aaru
Submodule
3
README.md
Normal file
@@ -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).
|
||||
34
RedBookPlayer.sln
Normal file
@@ -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
|
||||
8
RedBookPlayer/App.xaml
Normal file
@@ -0,0 +1,8 @@
|
||||
<Application xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
x:Class="RedBookPlayer.App">
|
||||
<Application.Styles>
|
||||
<StyleInclude Source="avares://Avalonia.Themes.Default/DefaultTheme.xaml"/>
|
||||
<StyleInclude Source="avares://Avalonia.Themes.Default/Accents/BaseLight.xaml"/>
|
||||
</Application.Styles>
|
||||
</Application>
|
||||
40
RedBookPlayer/App.xaml.cs
Normal file
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
RedBookPlayer/Assets/-.png
Normal file
|
After Width: | Height: | Size: 357 B |
BIN
RedBookPlayer/Assets/0.png
Normal file
|
After Width: | Height: | Size: 658 B |
BIN
RedBookPlayer/Assets/1.png
Normal file
|
After Width: | Height: | Size: 414 B |
BIN
RedBookPlayer/Assets/2.png
Normal file
|
After Width: | Height: | Size: 604 B |
BIN
RedBookPlayer/Assets/3.png
Normal file
|
After Width: | Height: | Size: 595 B |
BIN
RedBookPlayer/Assets/4.png
Normal file
|
After Width: | Height: | Size: 550 B |
BIN
RedBookPlayer/Assets/5.png
Normal file
|
After Width: | Height: | Size: 601 B |
BIN
RedBookPlayer/Assets/6.png
Normal file
|
After Width: | Height: | Size: 656 B |
BIN
RedBookPlayer/Assets/7.png
Normal file
|
After Width: | Height: | Size: 547 B |
BIN
RedBookPlayer/Assets/8.png
Normal file
|
After Width: | Height: | Size: 693 B |
BIN
RedBookPlayer/Assets/9.png
Normal file
|
After Width: | Height: | Size: 648 B |
BIN
RedBookPlayer/Assets/:.png
Normal file
|
After Width: | Height: | Size: 407 B |
42
RedBookPlayer/DeEmphasisFilter.cs
Normal file
@@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
126
RedBookPlayer/HiResTimer.cs
Normal file
@@ -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<HiResTimerElapsedEventArgs> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
10
RedBookPlayer/MainWindow.xaml
Normal file
@@ -0,0 +1,10 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d"
|
||||
x:Class="RedBookPlayer.MainWindow"
|
||||
Title="RedBookPlayer"
|
||||
SizeToContent="WidthAndHeight">
|
||||
<ContentControl Name="Content"/>
|
||||
</Window>
|
||||
55
RedBookPlayer/MainWindow.xaml.cs
Normal file
@@ -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<ContentControl>("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();
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
435
RedBookPlayer/Player.cs
Normal file
@@ -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<byte[]> 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()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
57
RedBookPlayer/PlayerView.xaml
Normal file
@@ -0,0 +1,57 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d"
|
||||
x:Class="RedBookPlayer.PlayerView"
|
||||
Width="900" Height="300">
|
||||
<StackPanel Margin="16" VerticalAlignment="Center">
|
||||
<Button Click="LoadButton_Click" Margin="32,0,32,16">Load</Button>
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,0,0,16">
|
||||
<Button Click="PlayButton_Click" Width="100" Margin="0,0,16,0">Play</Button>
|
||||
<Button Click="PauseButton_Click" Width="100" Margin="0,0,16,0">Pause</Button>
|
||||
<Button Click="StopButton_Click" Width="100" Margin="0,0,16,0">Stop</Button>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,0,0,16">
|
||||
<Button Click="PreviousTrackButton_Click" Width="100" Margin="0,0,16,0">Previous Track</Button>
|
||||
<Button Click="NextTrackButton_Click" Width="100" Margin="0,0,16,0">Next Track</Button>
|
||||
<Button Click="PreviousIndexButton_Click" Width="100" Margin="0,0,16,0">Previous Index</Button>
|
||||
<Button Click="NextIndexButton_Click" Width="100" Margin="0,0,16,0">Next Index</Button>
|
||||
<RepeatButton Click="RewindButton_Click" Width="100" Margin="0,0,16,0">Rewind</RepeatButton>
|
||||
<RepeatButton Click="FastForwardButton_Click" Width="100">Fast Forward</RepeatButton>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,0,0,16">
|
||||
<StackPanel Margin="0,0,32,0">
|
||||
<TextBlock Margin="0,0,0,4">TRACK</TextBlock>
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
|
||||
<Image Name="TrackDigit1" Width="42" Height="51" Source="/Assets/-.png"></Image>
|
||||
<Image Name="TrackDigit2" Width="42" Height="51" Source="/Assets/-.png"></Image>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
<StackPanel Margin="0,0,32,0">
|
||||
<TextBlock Margin="0,0,0,4">INDEX</TextBlock>
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
|
||||
<Image Name="IndexDigit1" Width="42" Height="51" Source="/Assets/-.png"></Image>
|
||||
<Image Name="IndexDigit2" Width="42" Height="51" Source="/Assets/-.png"></Image>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
<StackPanel>
|
||||
<TextBlock Margin="0,0,0,4">TIME</TextBlock>
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
|
||||
<Image Name="TimeDigit1" Width="42" Height="51" Source="/Assets/-.png"></Image>
|
||||
<Image Name="TimeDigit2" Width="42" Height="51" Source="/Assets/-.png"></Image>
|
||||
<Image Width="11" Height="51" Source="/Assets/:.png"></Image>
|
||||
<Image Name="TimeDigit3" Width="42" Height="51" Source="/Assets/-.png"></Image>
|
||||
<Image Name="TimeDigit4" Width="42" Height="51" Source="/Assets/-.png"></Image>
|
||||
<Image Width="11" Height="51" Source="/Assets/:.png"></Image>
|
||||
<Image Name="TimeDigit5" Width="42" Height="51" Source="/Assets/-.png"></Image>
|
||||
<Image Name="TimeDigit6" Width="42" Height="51" Source="/Assets/-.png"></Image>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
|
||||
<Button Click="EnableDeEmphasisButton_Click" IsVisible="{Binding !PreEmphasis}" Width="200" Margin="0,0,16,0">Enable De-Emphasis</Button>
|
||||
<Button Click="DisableDeEmphasisButton_Click" IsVisible="{Binding PreEmphasis}" Width="200" Margin="0,0,16,0">Disable De-Emphasis</Button>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</UserControl>
|
||||
249
RedBookPlayer/PlayerView.xaml.cs
Normal file
@@ -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<string> GetPath()
|
||||
{
|
||||
OpenFileDialog dialog = new OpenFileDialog();
|
||||
dialog.AllowMultiple = false;
|
||||
|
||||
List<string> knownExtensions = (new Aaru.DiscImages.AaruFormat()).KnownExtensions.ToList();
|
||||
dialog.Filters.Add(new FileDialogFilter()
|
||||
{
|
||||
Name = "Aaru Image Format (*" + string.Join(", *", knownExtensions) + ")",
|
||||
Extensions = knownExtensions.ConvertAll(e => e.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<IAssetLoader>();
|
||||
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<Image>("TrackDigit1");
|
||||
digits[1] = this.FindControl<Image>("TrackDigit2");
|
||||
|
||||
digits[2] = this.FindControl<Image>("IndexDigit1");
|
||||
digits[3] = this.FindControl<Image>("IndexDigit2");
|
||||
|
||||
digits[4] = this.FindControl<Image>("TimeDigit1");
|
||||
digits[5] = this.FindControl<Image>("TimeDigit2");
|
||||
digits[6] = this.FindControl<Image>("TimeDigit3");
|
||||
digits[7] = this.FindControl<Image>("TimeDigit4");
|
||||
digits[8] = this.FindControl<Image>("TimeDigit5");
|
||||
digits[9] = this.FindControl<Image>("TimeDigit6");
|
||||
|
||||
currentTrack = this.FindControl<TextBlock>("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);
|
||||
}
|
||||
}
|
||||
}
|
||||
27
RedBookPlayer/Program.cs
Normal file
@@ -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<App>()
|
||||
.UsePlatformDetect()
|
||||
.LogToDebug();
|
||||
}
|
||||
}
|
||||
38
RedBookPlayer/RedBookPlayer.csproj
Normal file
@@ -0,0 +1,38 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<RuntimeIdentifiers>win-x64;linux-x64</RuntimeIdentifiers>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(RuntimeIdentifier)' == 'win-x64'">
|
||||
<DefineConstants>Windows</DefineConstants>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Compile Update="**\*.xaml.cs">
|
||||
<DependentUpon>%(Filename)</DependentUpon>
|
||||
</Compile>
|
||||
<AvaloniaResource Include="**\*.xaml" Exclude="bin\**;obj\**">
|
||||
<SubType>Designer</SubType>
|
||||
</AvaloniaResource>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Avalonia" Version="0.9.12" />
|
||||
<PackageReference Include="Avalonia.Desktop" Version="0.9.12" />
|
||||
<PackageReference Include="Avalonia.ReactiveUI" Version="0.9.12" />
|
||||
<PackageReference Include="NWaves" Version="0.9.4" />
|
||||
<PackageReference Include="SkiaSharp.NativeAssets.Linux" Version="1.68.3" />
|
||||
<PackageReference Include="Avalonia.Skia.Linux.Natives" Version="1.68.0.2" ExcludeAssets="all" />
|
||||
<PackageReference Include="SkiaSharp" Version="1.68.3" />
|
||||
<PackageReference Include="OpenTK.NetStandard" Version="1.0.5.12" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Aaru\Aaru.Images\Aaru.Images.csproj" />
|
||||
<ProjectReference Include="..\Aaru\Aaru.CommonTypes\Aaru.CommonTypes.csproj" />
|
||||
<ProjectReference Include="..\Aaru\Aaru.Decoders\Aaru.Decoders.csproj" />
|
||||
<ProjectReference Include="..\cscore\CSCore\CSCore.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<AvaloniaResource Include="Assets\*" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
22
RedBookPlayer/ThemeSelectionWindow.xaml
Normal file
@@ -0,0 +1,22 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="RedBookPlayer.ThemeSelectionWindow"
|
||||
Title="Select Theme"
|
||||
Width="450" Height="600">
|
||||
<Border Padding="10">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
<ListBox Grid.Row="0" Grid.Column="0" Name="ThemeList" SelectionMode="Single" Margin="0,0,0,16"/>
|
||||
<Button Grid.Row="1" Grid.Column="0" Name="ApplyButton">Apply</Button>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Window>
|
||||
95
RedBookPlayer/ThemeSelectionWindow.xaml.cs
Normal file
@@ -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<ListBox>("ThemeList");
|
||||
themeList.SelectionChanged += ThemeList_SelectionChanged;
|
||||
|
||||
List<String> items = new List<String>();
|
||||
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<Button>("ApplyButton").Click += ApplyTheme;
|
||||
}
|
||||
}
|
||||
}
|
||||
6
RedBookPlayer/nuget.config
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<packageSources>
|
||||
<add key="AvaloniaCI" value="https://www.myget.org/F/avalonia-ci/api/v2" />
|
||||
</packageSources>
|
||||
</configuration>
|
||||