diff --git a/.cursor/hooks/state/continual-learning.json b/.cursor/hooks/state/continual-learning.json
new file mode 100644
index 0000000..0e150e7
--- /dev/null
+++ b/.cursor/hooks/state/continual-learning.json
@@ -0,0 +1,8 @@
+{
+ "version": 1,
+ "lastRunAtMs": 0,
+ "turnsSinceLastRun": 2,
+ "lastTranscriptMtimeMs": null,
+ "lastProcessedGenerationId": "b78a342b-b9bc-4cac-86c8-92e073a516a6",
+ "trialStartedAtMs": null
+}
diff --git a/.specstory/history/2026-03-13_11-23Z-untitled.md b/.specstory/history/2026-03-13_11-23Z-untitled.md
new file mode 100644
index 0000000..c50842a
--- /dev/null
+++ b/.specstory/history/2026-03-13_11-23Z-untitled.md
@@ -0,0 +1,18 @@
+
+
+
+
+# Untitled (2026-03-13 11:23Z)
+
+_**User**_
+
+"C:\Users\Dev\AppData\Local\Programs\Inno Setup 6\ISCC.exe" installer.iss
+
+---
+
+_**Agent (model composer-1.5, mode Agent)**_
+
+
+
+---
+
diff --git a/.specstory/history/2026-03-13_13-06Z-inno-setup-installer-script.md b/.specstory/history/2026-03-13_13-06Z-inno-setup-installer-script.md
new file mode 100644
index 0000000..8f379f2
--- /dev/null
+++ b/.specstory/history/2026-03-13_13-06Z-inno-setup-installer-script.md
@@ -0,0 +1,220 @@
+
+
+
+
+# Inno Setup installer script (2026-03-13 13:06Z)
+
+_**User**_
+
+"C:\Users\Dev\AppData\Local\Programs\Inno Setup 6\ISCC.exe" installer.iss
+
+---
+
+_**Agent (model composer-1.5, mode Agent)**_
+
+
+
+---
+
+
+
+
+
+---
+
+
+Tool use: **run_terminal_command_v2** • Run command: "C:\Users\Dev\AppData\Local\Programs\Inno Setup 6\ISCC.exe" "c:\dev\Audiotomatic\installer.iss"
+
+```bash
+"C:\Users\Dev\AppData\Local\Programs\Inno Setup 6\ISCC.exe" "c:\dev\Audiotomatic\installer.iss"
+```
+
+```
+Inno Setup 6 Command-Line Compiler
+Copyright (C) 1997-2026 Jordan Russell. All rights reserved.
+Portions Copyright (C) 2000-2026 Martijn Laan. All rights reserved.
+Portions Copyright (C) 2001-2004 Alex Yackimoff. All rights reserved.
+https://www.innosetup.com
+
+Compiler engine version: Inno Setup 6.7.1
+Non-commercial use only
+
+Preprocessing
+ Reading file: C:\Users\Dev\AppData\Local\Programs\Inno Setup 6\ISPPBuiltins.iss
+Parsing [Setup] section, line 2
+Parsing [Setup] section, line 3
+Parsing [Setup] section, line 4
+Parsing [Setup] section, line 5
+Parsing [Setup] section, line 6
+Parsing [Setup] section, line 7
+Parsing [Setup] section, line 8
+Parsing [Setup] section, line 9
+Parsing [Setup] section, line 10
+Parsing [Setup] section, line 11
+Parsing [Setup] section, line 12
+Parsing [Setup] section, line 13
+Parsing [Setup] section, line 14
+Parsing [Setup] section, line 15
+Parsing [Setup] section, line 16
+Parsing [Setup] section, line 17
+Reading file (WizardImageFile)
+Reading file (WizardSmallImageFile)
+Preparing Setup program executable
+ Verification successful
+ Updating icons (Setup.e32)
+ Updating version info (Setup.e32)
+Determining language code pages
+Parsing [Languages] section, line 20
+ Reading file: C:\Users\Dev\AppData\Local\Programs\Inno Setup 6\Default.isl
+Parsing [Languages] section, line 21
+ Reading file: C:\Users\Dev\AppData\Local\Programs\Inno Setup 6\Languages\French.isl
+ Messages in script file
+Reading default messages from Default.isl
+Parsing [Languages] section, line 20
+ Reading file: C:\Users\Dev\AppData\Local\Programs\Inno Setup 6\Default.isl
+Parsing [Languages] section, line 21
+ Reading file: C:\Users\Dev\AppData\Local\Programs\Inno Setup 6\Languages\French.isl
+Parsing [LangOptions], [Messages], and [CustomMessages] sections
+ Messages in script file
+Reading [Code] section
+Parsing [Tasks] section, line 32
+Parsing [Tasks] section, line 33
+Parsing [Icons] section, line 27
+Parsing [Icons] section, line 28
+Parsing [Icons] section, line 29
+Parsing [Run] section, line 36
+Parsing [Files] section, line 24
+Deleting Audiomatic-Setup-x64.exe from output directory
+Creating setup files
+ Verification successful
+ Updating icons (Setup.exe)
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\Accessibility.dll
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\app.ico
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\Audiomatic.deps.json
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\Audiomatic.dll
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\Audiomatic.exe
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\Audiomatic.pdb
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\Audiomatic.pri
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\Audiomatic.runtimeconfig.json
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\clretwrc.dll
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\clrgc.dll
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\clrjit.dll
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\coreclr.dll
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\CoreMessagingXP.dll
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\createdump.exe
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\D3DCompiler_47_cor3.dll
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\dcompi.dll
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\DirectML.dll
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\DirectWriteForwarder.dll
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\dwmcorei.dll
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\DwmSceneI.dll
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\DWriteCore.dll
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\e_sqlite3.dll
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\hostfxr.dll
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\hostpolicy.dll
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\marshal.dll
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\Microsoft.CSharp.dll
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\Microsoft.Data.Sqlite.dll
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\Microsoft.DiaSymReader.Native.amd64.dll
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\Microsoft.DirectManipulation.dll
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\Microsoft.Foundation.winmd
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\Microsoft.Graphics.Canvas.dll
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\Microsoft.Graphics.Canvas.Interop.dll
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\Microsoft.Graphics.Display.dll
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\Microsoft.Graphics.Imaging.dll
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\Microsoft.Graphics.Imaging.Projection.dll
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\Microsoft.Graphics.Imaging.winmd
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\Microsoft.Graphics.ImagingInternal.ImageObjectRemover.winmd
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\Microsoft.Graphics.ImagingInternal.winmd
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\Microsoft.Graphics.Internal.Imaging.winmd
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\Microsoft.Graphics.winmd
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\Microsoft.InputStateManager.dll
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\Microsoft.InteractiveExperiences.Projection.dll
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\Microsoft.Internal.FrameworkUdk.dll
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\Microsoft.ML.OnnxRuntime.dll
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\Microsoft.Security.Authentication.OAuth.Projection.dll
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\Microsoft.Security.Authentication.OAuth.winmd
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\Microsoft.UI.Composition.OSSupport.dll
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\Microsoft.UI.Designer.dll
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\Microsoft.UI.dll
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\Microsoft.UI.Input.dll
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\Microsoft.UI.pri
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\Microsoft.UI.Text.winmd
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\Microsoft.UI.Windowing.Core.dll
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\Microsoft.UI.Windowing.dll
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\Microsoft.UI.winmd
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\Microsoft.UI.Xaml.Controls.dll
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\Microsoft.UI.Xaml.Controls.pri
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\Microsoft.ui.xaml.dll
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\Microsoft.UI.Xaml.Internal.dll
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\Microsoft.UI.Xaml.Phone.dll
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\Microsoft.ui.xaml.resources.19h1.dll
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\Microsoft.ui.xaml.resources.common.dll
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\Microsoft.UI.Xaml.winmd
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\Microsoft.VisualBasic.Core.dll
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\Microsoft.VisualBasic.dll
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\Microsoft.VisualBasic.Forms.dll
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\Microsoft.Web.WebView2.Core.dll
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\Microsoft.Web.WebView2.Core.Projection.dll
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\Microsoft.Win32.Primitives.dll
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\Microsoft.Win32.Registry.AccessControl.dll
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\Microsoft.Win32.Registry.dll
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\Microsoft.Win32.SystemEvents.dll
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\Microsoft.Windows.AI.ContentModerationInternal.winmd
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\Microsoft.Windows.AI.ContentSafety.dll
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\Microsoft.Windows.AI.ContentSafety.Projection.dll
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\Microsoft.Windows.AI.ContentSafety.winmd
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\Microsoft.Windows.AI.Foundation.Projection.dll
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\Microsoft.Windows.AI.Foundation.winmd
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\Microsoft.Windows.AI.FoundationInternal.winmd
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\Microsoft.Windows.AI.GenerativeInternal.winmd
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\Microsoft.Windows.AI.Imaging.dll
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\Microsoft.Windows.AI.Imaging.Projection.dll
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\Microsoft.Windows.AI.Imaging.winmd
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\Microsoft.Windows.AI.MachineLearning.dll
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\Microsoft.Windows.AI.MachineLearning.Projection.dll
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\Microsoft.Windows.AI.MachineLearning.winmd
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\Microsoft.Windows.AI.Projection.dll
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\Microsoft.Windows.AI.Text.dll
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\Microsoft.Windows.AI.Text.Projection.dll
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\Microsoft.Windows.AI.Text.winmd
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\Microsoft.Windows.AI.winmd
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\Microsoft.Windows.ApplicationModel.Background.Projection.dll
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\Microsoft.Windows.ApplicationModel.Background.UniversalBGTask.dll
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\Microsoft.Windows.ApplicationModel.Background.UniversalBGTask.winmd
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\Microsoft.Windows.ApplicationModel.Background.winmd
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\Microsoft.Windows.ApplicationModel.DynamicDependency.Projection.dll
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\Microsoft.Windows.ApplicationModel.DynamicDependency.winmd
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\Microsoft.Windows.ApplicationModel.Resources.dll
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\Microsoft.Windows.ApplicationModel.Resources.Projection.dll
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\Microsoft.Windows.ApplicationModel.Resources.winmd
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\Microsoft.Windows.ApplicationModel.WindowsAppRuntime.Projection.dll
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\Microsoft.Windows.ApplicationModel.WindowsAppRuntime.winmd
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\Microsoft.Windows.AppLifecycle.Projection.dll
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\Microsoft.Windows.AppLifecycle.winmd
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\Microsoft.Windows.AppNotifications.Builder.Projection.dll
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\Microsoft.Windows.AppNotifications.Builder.winmd
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\Microsoft.Windows.AppNotifications.Projection.dll
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\Microsoft.Windows.AppNotifications.winmd
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\Microsoft.Windows.BadgeNotifications.Projection.dll
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\Microsoft.Windows.BadgeNotifications.winmd
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\Microsoft.Windows.Foundation.Projection.dll
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\Microsoft.Windows.Foundation.winmd
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\Microsoft.Windows.Globalization.winmd
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\Microsoft.Windows.Internal.Vision.winmd
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\Microsoft.Windows.Management.Deployment.Projection.dll
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\Microsoft.Windows.Management.Deployment.winmd
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\Microsoft.Windows.Media.Capture.Projection.dll
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\Microsoft.Windows.Media.Capture.winmd
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\Microsoft.Windows.Private.Workloads.SessionManager.winmd
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\Microsoft.Windows.PrivateCommon.winmd
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\Microsoft.Windows.PushNotifications.Projection.dll
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\Microsoft.Windows.PushNotifications.winmd
+ Compressing: c:\dev\Audiotomatic\Audiomatic\bin\Release\net8.0-windows10.0.19041.0\win-x64\publish\Microsoft.Windows.SDK.NET.dll
+
+```
+
+
+
+---
+
diff --git a/Audiomatic/.cursorindexingignore b/Audiomatic/.cursorindexingignore
new file mode 100644
index 0000000..953908e
--- /dev/null
+++ b/Audiomatic/.cursorindexingignore
@@ -0,0 +1,3 @@
+
+# Don't index SpecStory auto-save files, but allow explicit context inclusion via @ references
+.specstory/**
diff --git a/Audiomatic/.specstory/.gitignore b/Audiomatic/.specstory/.gitignore
new file mode 100644
index 0000000..c5b4129
--- /dev/null
+++ b/Audiomatic/.specstory/.gitignore
@@ -0,0 +1,4 @@
+# SpecStory project identity file
+/.project.json
+# SpecStory explanation file
+/.what-is-this.md
diff --git a/Audiomatic/LibraryWindow.xaml b/Audiomatic/LibraryWindow.xaml
new file mode 100644
index 0000000..f059b54
--- /dev/null
+++ b/Audiomatic/LibraryWindow.xaml
@@ -0,0 +1,101 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Audiomatic/LibraryWindow.xaml.cs b/Audiomatic/LibraryWindow.xaml.cs
new file mode 100644
index 0000000..f4828f8
--- /dev/null
+++ b/Audiomatic/LibraryWindow.xaml.cs
@@ -0,0 +1,118 @@
+using Microsoft.UI.Windowing;
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+using Microsoft.UI.Xaml.Input;
+
+namespace Audiomatic;
+
+public sealed partial class LibraryWindow : Window
+{
+ private IReadOnlyList _allRows = [];
+ private LibrarySortColumn _sortColumn = LibrarySortColumn.Title;
+ private bool _sortAscending = true;
+
+ public LibraryWindow()
+ {
+ InitializeComponent();
+ ExtendsContentIntoTitleBar = true;
+ SetTitleBar(CustomTitleBar);
+ WindowShadow.Apply(this);
+
+ if (AppWindow.Presenter is OverlappedPresenter presenter)
+ {
+ presenter.IsMaximizable = false;
+ }
+
+ ApplyTheme(SettingsManager.LoadTheme());
+ UpdateHeaderTexts();
+ }
+
+ public void SetRows(IReadOnlyList rows)
+ {
+ _allRows = rows;
+ ApplySort();
+ }
+
+ private void ApplyTheme(string theme)
+ {
+ if (Content is not FrameworkElement root) return;
+
+ root.RequestedTheme = theme switch
+ {
+ "light" => ElementTheme.Light,
+ "dark" => ElementTheme.Dark,
+ _ => ElementTheme.Default
+ };
+ }
+
+ private void RootGrid_KeyDown(object sender, KeyRoutedEventArgs e)
+ {
+ if (e.Key == Windows.System.VirtualKey.Escape)
+ {
+ Close();
+ e.Handled = true;
+ }
+ }
+
+ private void TitleHeader_Click(object sender, RoutedEventArgs e) =>
+ ChangeSort(LibrarySortColumn.Title);
+
+ private void FolderHeader_Click(object sender, RoutedEventArgs e) =>
+ ChangeSort(LibrarySortColumn.Folder);
+
+ private void ChangeSort(LibrarySortColumn column)
+ {
+ if (_sortColumn == column)
+ _sortAscending = !_sortAscending;
+ else
+ {
+ _sortColumn = column;
+ _sortAscending = true;
+ }
+
+ ApplySort();
+ }
+
+ private void ApplySort()
+ {
+ IEnumerable sorted = _sortColumn switch
+ {
+ LibrarySortColumn.Folder => _sortAscending
+ ? _allRows.OrderBy(r => r.FolderPath, StringComparer.OrdinalIgnoreCase)
+ .ThenBy(r => r.Title, StringComparer.OrdinalIgnoreCase)
+ : _allRows.OrderByDescending(r => r.FolderPath, StringComparer.OrdinalIgnoreCase)
+ .ThenByDescending(r => r.Title, StringComparer.OrdinalIgnoreCase),
+ _ => _sortAscending
+ ? _allRows.OrderBy(r => r.Title, StringComparer.OrdinalIgnoreCase)
+ .ThenBy(r => r.FolderPath, StringComparer.OrdinalIgnoreCase)
+ : _allRows.OrderByDescending(r => r.Title, StringComparer.OrdinalIgnoreCase)
+ .ThenByDescending(r => r.FolderPath, StringComparer.OrdinalIgnoreCase)
+ };
+
+ RowsList.ItemsSource = sorted.ToList();
+ EmptyStateText.Visibility = _allRows.Count == 0 ? Visibility.Visible : Visibility.Collapsed;
+ UpdateHeaderTexts();
+ }
+
+ private void UpdateHeaderTexts()
+ {
+ TitleHeaderText.Text = BuildHeader("Titre", _sortColumn == LibrarySortColumn.Title);
+ FolderHeaderText.Text = BuildHeader("Dossier", _sortColumn == LibrarySortColumn.Folder);
+ }
+
+ private string BuildHeader(string label, bool isActive)
+ {
+ if (!isActive) return label;
+ return _sortAscending ? $"{label} ▲" : $"{label} ▼";
+ }
+
+ private void Close_Click(object sender, RoutedEventArgs e) => Close();
+}
+
+public sealed record LibraryRow(string Title, string FolderPath);
+
+internal enum LibrarySortColumn
+{
+ Title,
+ Folder
+}
diff --git a/Audiomatic/MainWindow.xaml b/Audiomatic/MainWindow.xaml
index 918d45c..48433d0 100644
--- a/Audiomatic/MainWindow.xaml
+++ b/Audiomatic/MainWindow.xaml
@@ -446,6 +446,12 @@
+
+
+
+
+
diff --git a/Audiomatic/MainWindow.xaml.cs b/Audiomatic/MainWindow.xaml.cs
index a45fe95..5c5da48 100644
--- a/Audiomatic/MainWindow.xaml.cs
+++ b/Audiomatic/MainWindow.xaml.cs
@@ -33,7 +33,7 @@ public sealed partial class MainWindow : Window
private bool _sortAscending = true;
// Playlist navigation
- private enum ViewMode { Library, PlaylistList, PlaylistDetail, Queue, Radio, Podcast, PodcastEpisodes, Visualizer, MediaControl }
+ private enum ViewMode { Library, PlaylistList, PlaylistDetail, Queue, Radio, Podcast, PodcastEpisodes, Visualizer, Equalizer, MediaControl }
private ViewMode _viewMode = ViewMode.Library;
private PlaylistInfo? _currentPlaylist;
@@ -55,6 +55,16 @@ private enum ViewMode { Library, PlaylistList, PlaylistDetail, Queue, Radio, Pod
private TextBlock? _vizNoTrackText;
private VisualizerRenderer? _vizRenderer;
+ // Equalizer
+ private Slider[] _eqSliders = new Slider[10];
+ private TextBlock[] _eqGainLabels = new TextBlock[10];
+ private Slider? _eqPreampSlider;
+ private TextBlock? _eqPreampLabel;
+ private ComboBox? _eqPresetCombo;
+ private ToggleSwitch? _eqToggle;
+ private bool _eqUiBuilt;
+ private bool _eqUpdatingFromPreset;
+
// View transition animation
private bool _isViewTransitioning;
@@ -67,6 +77,9 @@ private enum ViewMode { Library, PlaylistList, PlaylistDetail, Queue, Radio, Pod
private DesktopAcrylicController? _acrylicController;
private SystemBackdropConfiguration? _configSource;
+ // Detached library window
+ private LibraryWindow? _libraryWindow;
+
// Collapse animation
private enum CollapseState { Expanded, Compact, Mini }
private CollapseState _collapseState = CollapseState.Expanded;
@@ -168,23 +181,21 @@ public MainWindow()
presenter.SetBorderAndTitleBar(true, false);
}
+ AppWindow.Changed += MainAppWindow_Changed;
+
// Restore window position (default: bottom-right)
RestoreWindowPosition();
- // Apply backdrop and theme
+ // Load settings
+ var settings = SettingsManager.Load();
+
+ // Apply backdrop, theme, and accent color
ApplyBackdrop(SettingsManager.LoadBackdrop());
+ ThemeHelper.ApplyAccentColor(settings.AccentColor);
ApplyTheme(SettingsManager.LoadTheme());
// Set up audio player
_player.SetDispatcherQueue(DispatcherQueue);
- _player.MediaOpened += OnMediaOpened;
- _player.MediaEnded += OnMediaEnded;
- _player.MediaFailed += OnMediaFailed;
- _player.PositionChanged += OnPositionChanged;
- _player.BufferingChanged += OnBufferingChanged;
-
- // Load settings
- var settings = SettingsManager.Load();
VolumeSlider.Value = settings.Volume * 100;
_player.Volume = settings.Volume;
_sortBy = settings.SortBy;
@@ -192,6 +203,12 @@ public MainWindow()
SortAscending.IsChecked = _sortAscending;
UpdateSortChecks();
+ // Load EQ settings
+ _player.EqEnabled = settings.EqEnabled;
+ if (settings.EqBands is { Length: 10 })
+ _player.SetEqAllBands(settings.EqBands);
+ _player.SetEqPreamp(settings.EqPreamp);
+
// Load radio stations and podcast subscriptions
_radioStations = SettingsManager.LoadRadioStations();
_podcastSubscriptions = PodcastService.LoadSubscriptions();
@@ -243,6 +260,8 @@ public MainWindow()
this.Closed += (_, _) =>
{
_isQuitting = true;
+ AppWindow.Changed -= MainAppWindow_Changed;
+ CloseLibraryWindow();
UnregisterHotKey(_hwnd, HOTKEY_ID);
UnregisterHotKey(_hwnd, HOTKEY_COLLAPSE_ID);
RemoveTrayIcon();
@@ -274,6 +293,7 @@ public MainWindow()
private void LoadTracks()
{
_allTracks = LibraryManager.GetAllTracks();
+ RefreshLibraryWindow();
ApplyFilterAndSort();
}
@@ -1014,7 +1034,7 @@ void SetTab(TextBlock tb, bool active)
SetTab(NavQueueText, _viewMode == ViewMode.Queue);
SetTab(NavRadioText, _viewMode == ViewMode.Radio);
SetTab(NavPodcastText, _viewMode == ViewMode.Podcast || _viewMode == ViewMode.PodcastEpisodes);
- SetTab(NavMoreText, _viewMode == ViewMode.Visualizer || _viewMode == ViewMode.MediaControl);
+ SetTab(NavMoreText, _viewMode == ViewMode.Visualizer || _viewMode == ViewMode.Equalizer || _viewMode == ViewMode.MediaControl);
// Show/hide search & sort
SearchSortRow.Visibility = (_viewMode == ViewMode.Library || _viewMode == ViewMode.PlaylistDetail)
@@ -1023,10 +1043,12 @@ void SetTab(TextBlock tb, bool active)
// Show/hide content containers based on view mode
var isPodcast = _viewMode == ViewMode.Podcast || _viewMode == ViewMode.PodcastEpisodes;
var isTrackView = _viewMode != ViewMode.Visualizer && _viewMode != ViewMode.MediaControl
- && _viewMode != ViewMode.Radio && !isPodcast;
+ && _viewMode != ViewMode.Radio && _viewMode != ViewMode.Equalizer && !isPodcast;
TrackListView.Visibility = isTrackView ? Visibility.Visible : Visibility.Collapsed;
WaveformContainer.Visibility = _viewMode == ViewMode.Visualizer
? Visibility.Visible : Visibility.Collapsed;
+ EqualizerContainer.Visibility = _viewMode == ViewMode.Equalizer
+ ? Visibility.Visible : Visibility.Collapsed;
RadioContainer.Visibility = _viewMode == ViewMode.Radio
? Visibility.Visible : Visibility.Collapsed;
PodcastContainer.Visibility = isPodcast
@@ -1046,6 +1068,7 @@ private void AnimateViewTransition(Action buildNewContent, bool slideFromRight =
// Target the visible content container
FrameworkElement target = _viewMode == ViewMode.Visualizer ? WaveformContainer
+ : _viewMode == ViewMode.Equalizer ? EqualizerContainer
: _viewMode == ViewMode.MediaControl ? MediaContainer
: _viewMode == ViewMode.Radio ? RadioContainer
: (_viewMode == ViewMode.Podcast || _viewMode == ViewMode.PodcastEpisodes) ? PodcastContainer
@@ -1088,6 +1111,7 @@ private void AnimateViewTransition(Action buildNewContent, bool slideFromRight =
// Re-target if container changed (e.g. Library→Visualizer)
FrameworkElement newTarget = _viewMode == ViewMode.Visualizer ? WaveformContainer
+ : _viewMode == ViewMode.Equalizer ? EqualizerContainer
: _viewMode == ViewMode.MediaControl ? MediaContainer
: _viewMode == ViewMode.Radio ? RadioContainer
: (_viewMode == ViewMode.Podcast || _viewMode == ViewMode.PodcastEpisodes) ? PodcastContainer
@@ -2420,6 +2444,9 @@ private void NavMore_Click(object sender, RoutedEventArgs e)
panel.Children.Add(ActionPanel.CreateButton("\uE9D9", "Visualizer", [],
() => { flyout.Hide(); NavVisualizer_Click(sender, e); },
isActive: _viewMode == ViewMode.Visualizer));
+ panel.Children.Add(ActionPanel.CreateButton("\uE9E9", "Equalizer", [],
+ () => { flyout.Hide(); NavEqualizer_Click(sender, e); },
+ isActive: _viewMode == ViewMode.Equalizer));
panel.Children.Add(ActionPanel.CreateButton("\uE93C", "Media", [],
() => { flyout.Hide(); NavMedia_Click(sender, e); },
isActive: _viewMode == ViewMode.MediaControl));
@@ -2428,6 +2455,298 @@ private void NavMore_Click(object sender, RoutedEventArgs e)
flyout.ShowAt(sender as FrameworkElement ?? NavMoreBtn);
}
+ // -- Equalizer ------------------------------------------------
+
+ private void NavEqualizer_Click(object sender, RoutedEventArgs e)
+ {
+ if (_viewMode == ViewMode.Equalizer) return;
+ _viewMode = ViewMode.Equalizer;
+ _currentPlaylist = null;
+ UpdateNavigation();
+ UpdateSpectrumTimer();
+ UpdateMediaTimer();
+ AnimateViewTransition(() => BuildEqualizerUI());
+ }
+
+ private void BuildEqualizerUI()
+ {
+ if (_eqUiBuilt)
+ {
+ // Sync UI with current player state
+ SyncEqUiToPlayer();
+ return;
+ }
+ _eqUiBuilt = true;
+
+ EqualizerPanel.Children.Clear();
+
+ // Header row: title + toggle
+ var headerGrid = new Grid { Margin = new Thickness(0, 8, 0, 0) };
+ headerGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
+ headerGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
+
+ var titleText = new TextBlock
+ {
+ Text = "Equalizer",
+ FontSize = 14,
+ FontWeight = Microsoft.UI.Text.FontWeights.SemiBold,
+ VerticalAlignment = VerticalAlignment.Center
+ };
+ Grid.SetColumn(titleText, 0);
+
+ _eqToggle = new ToggleSwitch
+ {
+ IsOn = _player.EqEnabled,
+ OnContent = "On",
+ OffContent = "Off",
+ MinWidth = 0
+ };
+ _eqToggle.Toggled += EqToggle_Toggled;
+ Grid.SetColumn(_eqToggle, 1);
+
+ headerGrid.Children.Add(titleText);
+ headerGrid.Children.Add(_eqToggle);
+ EqualizerPanel.Children.Add(headerGrid);
+
+ // Preset selector
+ var settings = SettingsManager.Load();
+ _eqPresetCombo = new ComboBox
+ {
+ HorizontalAlignment = HorizontalAlignment.Stretch,
+ FontSize = 13
+ };
+ foreach (var preset in Equalizer.Presets.Keys)
+ _eqPresetCombo.Items.Add(preset);
+ _eqPresetCombo.SelectedItem = settings.EqPreset;
+ if (_eqPresetCombo.SelectedItem == null) _eqPresetCombo.SelectedIndex = 0;
+ _eqPresetCombo.SelectionChanged += EqPreset_Changed;
+ EqualizerPanel.Children.Add(_eqPresetCombo);
+
+ // EQ bands grid
+ var bandsGrid = new Grid { MinHeight = 200 };
+ // dB labels column + 10 band columns
+ bandsGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
+ for (int i = 0; i < 10; i++)
+ bandsGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
+
+ bandsGrid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) }); // sliders
+ bandsGrid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto }); // gain values
+ bandsGrid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto }); // freq labels
+
+ // dB labels on the left
+ var dbPanel = new Grid { Margin = new Thickness(0, 0, 4, 0) };
+ dbPanel.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
+ dbPanel.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) });
+ dbPanel.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
+
+ var dbTop = new TextBlock { Text = "+12", FontSize = 9,
+ Foreground = ThemeHelper.Brush("TextFillColorTertiaryBrush"), VerticalAlignment = VerticalAlignment.Top };
+ var dbBottom = new TextBlock { Text = "-12", FontSize = 9,
+ Foreground = ThemeHelper.Brush("TextFillColorTertiaryBrush"), VerticalAlignment = VerticalAlignment.Bottom };
+ Grid.SetRow(dbTop, 0);
+ Grid.SetRow(dbBottom, 2);
+ dbPanel.Children.Add(dbTop);
+ dbPanel.Children.Add(dbBottom);
+ Grid.SetColumn(dbPanel, 0);
+ Grid.SetRow(dbPanel, 0);
+ bandsGrid.Children.Add(dbPanel);
+
+ // Load current gains
+ var gains = _player.GetEqGains();
+
+ for (int i = 0; i < 10; i++)
+ {
+ int bandIndex = i;
+
+ // Vertical slider
+ var slider = new Slider
+ {
+ Orientation = Orientation.Vertical,
+ Minimum = -12,
+ Maximum = 12,
+ StepFrequency = 0.5,
+ Value = gains[i],
+ HorizontalAlignment = HorizontalAlignment.Center,
+ VerticalAlignment = VerticalAlignment.Stretch,
+ Width = 28
+ };
+ slider.ValueChanged += (s, args) => EqBand_Changed(bandIndex, args.NewValue);
+ Grid.SetColumn(slider, i + 1);
+ Grid.SetRow(slider, 0);
+ bandsGrid.Children.Add(slider);
+ _eqSliders[i] = slider;
+
+ // Gain label
+ var gainLabel = new TextBlock
+ {
+ Text = $"{gains[i]:0.#}",
+ FontSize = 9,
+ HorizontalAlignment = HorizontalAlignment.Center,
+ Foreground = ThemeHelper.Brush("TextFillColorSecondaryBrush")
+ };
+ Grid.SetColumn(gainLabel, i + 1);
+ Grid.SetRow(gainLabel, 1);
+ bandsGrid.Children.Add(gainLabel);
+ _eqGainLabels[i] = gainLabel;
+
+ // Frequency label
+ var freqLabel = new TextBlock
+ {
+ Text = Equalizer.FrequencyLabels[i],
+ FontSize = 9,
+ HorizontalAlignment = HorizontalAlignment.Center,
+ Foreground = ThemeHelper.Brush("TextFillColorTertiaryBrush"),
+ Margin = new Thickness(0, 2, 0, 0)
+ };
+ Grid.SetColumn(freqLabel, i + 1);
+ Grid.SetRow(freqLabel, 2);
+ bandsGrid.Children.Add(freqLabel);
+ }
+
+ EqualizerPanel.Children.Add(bandsGrid);
+
+ // Preamp row
+ var preampGrid = new Grid { Margin = new Thickness(0, 4, 0, 8) };
+ preampGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
+ preampGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
+ preampGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
+
+ var preampLabel = new TextBlock
+ {
+ Text = "Preamp",
+ FontSize = 12,
+ VerticalAlignment = VerticalAlignment.Center,
+ Margin = new Thickness(0, 0, 8, 0)
+ };
+ Grid.SetColumn(preampLabel, 0);
+
+ _eqPreampSlider = new Slider
+ {
+ Minimum = -12,
+ Maximum = 12,
+ StepFrequency = 0.5,
+ Value = _player.EqPreampDb,
+ Height = 24,
+ VerticalAlignment = VerticalAlignment.Center
+ };
+ _eqPreampSlider.ValueChanged += EqPreamp_Changed;
+ Grid.SetColumn(_eqPreampSlider, 1);
+
+ _eqPreampLabel = new TextBlock
+ {
+ Text = $"{_player.EqPreampDb:0.#} dB",
+ FontSize = 11,
+ VerticalAlignment = VerticalAlignment.Center,
+ Foreground = ThemeHelper.Brush("TextFillColorSecondaryBrush"),
+ Margin = new Thickness(8, 0, 0, 0),
+ MinWidth = 40
+ };
+ Grid.SetColumn(_eqPreampLabel, 2);
+
+ preampGrid.Children.Add(preampLabel);
+ preampGrid.Children.Add(_eqPreampSlider);
+ preampGrid.Children.Add(_eqPreampLabel);
+ EqualizerPanel.Children.Add(preampGrid);
+
+ // Reset button
+ var resetBtn = new Button
+ {
+ Content = "Reset",
+ HorizontalAlignment = HorizontalAlignment.Center,
+ Padding = new Thickness(16, 6, 16, 6),
+ Margin = new Thickness(0, 0, 0, 12)
+ };
+ resetBtn.Click += EqReset_Click;
+ EqualizerPanel.Children.Add(resetBtn);
+ }
+
+ private void SyncEqUiToPlayer()
+ {
+ if (_eqToggle != null) _eqToggle.IsOn = _player.EqEnabled;
+ var gains = _player.GetEqGains();
+ _eqUpdatingFromPreset = true;
+ for (int i = 0; i < 10; i++)
+ {
+ if (_eqSliders[i] != null)
+ {
+ _eqSliders[i].Value = gains[i];
+ _eqGainLabels[i].Text = $"{gains[i]:0.#}";
+ }
+ }
+ if (_eqPreampSlider != null) _eqPreampSlider.Value = _player.EqPreampDb;
+ if (_eqPreampLabel != null) _eqPreampLabel.Text = $"{_player.EqPreampDb:0.#} dB";
+ _eqUpdatingFromPreset = false;
+ }
+
+ private void EqToggle_Toggled(object sender, RoutedEventArgs e)
+ {
+ _player.EqEnabled = _eqToggle!.IsOn;
+ SaveEqSettings();
+ }
+
+ private void EqPreset_Changed(object sender, SelectionChangedEventArgs e)
+ {
+ if (_eqPresetCombo?.SelectedItem is not string presetName) return;
+ if (!Equalizer.Presets.TryGetValue(presetName, out var gains)) return;
+
+ _eqUpdatingFromPreset = true;
+ _player.SetEqAllBands(gains);
+ for (int i = 0; i < 10; i++)
+ {
+ if (_eqSliders[i] != null)
+ {
+ _eqSliders[i].Value = gains[i];
+ _eqGainLabels[i].Text = $"{gains[i]:0.#}";
+ }
+ }
+ _eqUpdatingFromPreset = false;
+ SaveEqSettings();
+ }
+
+ private void EqBand_Changed(int bandIndex, double newValue)
+ {
+ float gain = (float)newValue;
+ _player.SetEqBand(bandIndex, gain);
+ if (_eqGainLabels[bandIndex] != null)
+ _eqGainLabels[bandIndex].Text = $"{gain:0.#}";
+
+ // Update preset to "Custom" if user manually adjusts
+ if (!_eqUpdatingFromPreset && _eqPresetCombo != null)
+ {
+ _eqPresetCombo.SelectionChanged -= EqPreset_Changed;
+ _eqPresetCombo.SelectedItem = null;
+ _eqPresetCombo.SelectionChanged += EqPreset_Changed;
+ }
+ SaveEqSettings();
+ }
+
+ private void EqPreamp_Changed(object sender, RangeBaseValueChangedEventArgs e)
+ {
+ float db = (float)e.NewValue;
+ _player.SetEqPreamp(db);
+ if (_eqPreampLabel != null) _eqPreampLabel.Text = $"{db:0.#} dB";
+ SaveEqSettings();
+ }
+
+ private void EqReset_Click(object sender, RoutedEventArgs e)
+ {
+ _eqPresetCombo!.SelectedItem = "Flat";
+ if (_eqPreampSlider != null) _eqPreampSlider.Value = 0;
+ _player.SetEqPreamp(0);
+ }
+
+ private void SaveEqSettings()
+ {
+ var current = SettingsManager.Load();
+ SettingsManager.Save(current with
+ {
+ EqEnabled = _player.EqEnabled,
+ EqPreset = _eqPresetCombo?.SelectedItem as string ?? "Flat",
+ EqBands = _player.GetEqGains(),
+ EqPreamp = _player.EqPreampDb
+ });
+ }
+
// -- Visualizer -----------------------------------------------
private void NavVisualizer_Click(object sender, RoutedEventArgs e)
@@ -3044,13 +3363,13 @@ private void ShowSettingsFlyout(FrameworkElement anchor)
LibraryManager.ResetLibrary();
_allTracks.Clear();
_displayedTracks.Clear();
+ RefreshLibraryWindow();
_queue.Clear();
_viewMode = ViewMode.Library;
_currentPlaylist = null;
UpdateNavigation();
ApplyFilterAndSort();
}, isDestructive: true));
-
panel.Children.Add(ActionPanel.CreateSeparator());
// Backdrop section
@@ -3114,6 +3433,16 @@ void AddThemeOption(string theme, string label)
panel.Children.Add(ActionPanel.CreateSeparator());
+ // Accent color section
+ panel.Children.Add(ActionPanel.CreateSectionHeader("Accent Color"));
+ panel.Children.Add(ActionPanel.CreateButton("\uE790", "Choose Accent...", [], () =>
+ {
+ flyout.Hide();
+ ShowAccentColorFlyout(anchor);
+ }));
+
+ panel.Children.Add(ActionPanel.CreateSeparator());
+
// Visualizer FPS
panel.Children.Add(ActionPanel.CreateSectionHeader("Visualizer"));
@@ -3168,6 +3497,109 @@ void AddFpsOption(int fps, string label)
flyout.ShowAt(anchor);
}
+ private void OpenLibraryWindow()
+ {
+ if (_libraryWindow != null)
+ {
+ RefreshLibraryWindow();
+ SyncLibraryWindowBounds();
+ _libraryWindow.Activate();
+ return;
+ }
+
+ _libraryWindow = new LibraryWindow();
+ _libraryWindow.Closed += LibraryWindow_Closed;
+ RefreshLibraryWindow();
+ SyncLibraryWindowBounds();
+ ApplyPinToLibraryWindow();
+ _libraryWindow.Activate();
+ }
+
+ private void LibraryWindow_Closed(object sender, WindowEventArgs args)
+ {
+ if (_libraryWindow == null) return;
+ _libraryWindow.Closed -= LibraryWindow_Closed;
+ _libraryWindow = null;
+ }
+
+ private void CloseLibraryWindow()
+ {
+ if (_libraryWindow == null) return;
+ _libraryWindow.Closed -= LibraryWindow_Closed;
+ _libraryWindow.Close();
+ _libraryWindow = null;
+ }
+
+ private void RefreshLibraryWindow()
+ {
+ if (_libraryWindow == null) return;
+ _libraryWindow.SetRows(BuildLibraryRows());
+ }
+
+ private List BuildLibraryRows()
+ {
+ var folderPathById = LibraryManager.GetFolders()
+ .ToDictionary(f => f.Id, f => f.Path);
+
+ return _allTracks
+ .Select(t => new LibraryRow(
+ t.Title,
+ folderPathById.TryGetValue(t.FolderId, out var path)
+ ? path
+ : $"Unknown folder ({t.FolderId})"))
+ .ToList();
+ }
+
+ private void MainAppWindow_Changed(AppWindow sender, AppWindowChangedEventArgs args)
+ {
+ if (_libraryWindow == null) return;
+ if (args.DidPositionChange || args.DidSizeChange)
+ SyncLibraryWindowBounds();
+ }
+
+ private void SyncLibraryWindowBounds()
+ {
+ if (_libraryWindow == null) return;
+
+ var mainPos = AppWindow.Position;
+ var mainSize = AppWindow.Size;
+ var width = Math.Max(mainSize.Width + 180, 560);
+ var height = mainSize.Height;
+
+ var displayArea = DisplayArea.GetFromWindowId(AppWindow.Id, DisplayAreaFallback.Primary);
+ var workArea = displayArea.WorkArea;
+
+ var rightX = mainPos.X + mainSize.Width;
+ int targetX;
+ if (rightX + width <= workArea.X + workArea.Width)
+ {
+ // Prefer attaching on the right.
+ targetX = rightX;
+ }
+ else
+ {
+ // Fallback: attach on the left if the right side is out of bounds.
+ targetX = mainPos.X - width;
+ if (targetX < workArea.X)
+ {
+ var maxX = Math.Max(workArea.X, workArea.X + workArea.Width - width);
+ targetX = Math.Clamp(rightX, workArea.X, maxX);
+ }
+ }
+
+ var maxY = Math.Max(workArea.Y, workArea.Y + workArea.Height - height);
+ var targetY = Math.Clamp(mainPos.Y, workArea.Y, maxY);
+
+ _libraryWindow.AppWindow.MoveAndResize(
+ new Windows.Graphics.RectInt32(targetX, targetY, width, height));
+ }
+
+ private void ApplyPinToLibraryWindow()
+ {
+ if (_libraryWindow?.AppWindow.Presenter is OverlappedPresenter presenter)
+ presenter.IsAlwaysOnTop = _isPinnedOnTop;
+ }
+
private async void ScanAllFoldersAsync()
{
TrackCountText.Text = "Scanning...";
@@ -3458,6 +3890,213 @@ void ApplyChanges()
flyout.Content = panel;
}
+ private void ShowAccentColorFlyout(FrameworkElement anchor)
+ {
+ var flyout = new Flyout();
+ flyout.FlyoutPresenterStyle = ActionPanel.CreateFlyoutPresenterStyle(minWidth: 280, maxWidth: 320);
+
+ var panel = new StackPanel { Spacing = 8 };
+
+ // Header
+ var headerGrid = new Grid();
+ headerGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
+ headerGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
+ var backBtn = new Button
+ {
+ Background = new SolidColorBrush(Microsoft.UI.Colors.Transparent),
+ BorderThickness = new Thickness(0),
+ Padding = new Thickness(4),
+ Content = new FontIcon { Glyph = "\uE72B", FontSize = 12 }
+ };
+ backBtn.Click += (_, _) =>
+ {
+ flyout.Hide();
+ ShowSettingsFlyout(anchor);
+ };
+ Grid.SetColumn(backBtn, 0);
+ var titleText = new TextBlock
+ {
+ Text = "Accent Color",
+ FontSize = 13,
+ FontWeight = Microsoft.UI.Text.FontWeights.SemiBold,
+ VerticalAlignment = VerticalAlignment.Center,
+ Margin = new Thickness(8, 0, 0, 0)
+ };
+ Grid.SetColumn(titleText, 1);
+ headerGrid.Children.Add(backBtn);
+ headerGrid.Children.Add(titleText);
+ panel.Children.Add(headerGrid);
+
+ var currentAccent = SettingsManager.Load().AccentColor;
+
+ // Color grid: 6 columns
+ const int columns = 6;
+ var grid = new Grid { Margin = new Thickness(0, 4, 0, 4) };
+ for (int c = 0; c < columns; c++)
+ grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
+
+ int rows = (int)Math.Ceiling(ThemeHelper.AccentPresets.Length / (double)columns);
+ for (int r = 0; r < rows; r++)
+ grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
+
+ for (int i = 0; i < ThemeHelper.AccentPresets.Length; i++)
+ {
+ var (name, hex) = ThemeHelper.AccentPresets[i];
+ int row = i / columns;
+ int col = i % columns;
+
+ bool isSystem = string.IsNullOrEmpty(hex);
+ bool isSelected = (isSystem && string.IsNullOrEmpty(currentAccent))
+ || (!isSystem && hex.Equals(currentAccent, StringComparison.OrdinalIgnoreCase));
+
+ var swatch = new Button
+ {
+ Width = 36,
+ Height = 36,
+ CornerRadius = new CornerRadius(18),
+ Padding = new Thickness(0),
+ Margin = new Thickness(3),
+ BorderThickness = new Thickness(isSelected ? 2 : 0),
+ BorderBrush = isSelected
+ ? new SolidColorBrush(Microsoft.UI.Colors.White)
+ : null,
+ HorizontalContentAlignment = HorizontalAlignment.Center,
+ VerticalContentAlignment = VerticalAlignment.Center,
+ Tag = hex
+ };
+
+ if (isSystem)
+ {
+ // System swatch: gradient-like icon
+ swatch.Background = ThemeHelper.Brush("AccentFillColorDefaultBrush");
+ swatch.Content = new FontIcon
+ {
+ Glyph = "\uE770",
+ FontSize = 14,
+ Foreground = new SolidColorBrush(Microsoft.UI.Colors.White)
+ };
+ }
+ else
+ {
+ swatch.Background = new SolidColorBrush(ThemeHelper.ParseHexColor(hex));
+ if (isSelected)
+ {
+ swatch.Content = new FontIcon
+ {
+ Glyph = "\uE73E",
+ FontSize = 12,
+ Foreground = new SolidColorBrush(Microsoft.UI.Colors.White)
+ };
+ }
+ }
+
+ ToolTipService.SetToolTip(swatch, name);
+
+ swatch.Click += (s, _) =>
+ {
+ var selectedHex = (s as Button)?.Tag as string ?? "";
+ flyout.Hide();
+ DispatcherQueue.TryEnqueue(() => ApplyAccentAndSave(selectedHex));
+ };
+
+ Grid.SetRow(swatch, row);
+ Grid.SetColumn(swatch, col);
+ grid.Children.Add(swatch);
+ }
+
+ panel.Children.Add(grid);
+
+ // Custom hex input
+ panel.Children.Add(ActionPanel.CreateSeparator());
+ var customGrid = new Grid { Margin = new Thickness(4, 0, 4, 4) };
+ customGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
+ customGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
+ customGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
+
+ var customPreview = new Border
+ {
+ Width = 24, Height = 24, CornerRadius = new CornerRadius(12),
+ BorderThickness = new Thickness(1),
+ BorderBrush = ThemeHelper.Brush("ControlStrokeColorDefaultBrush"),
+ Background = string.IsNullOrEmpty(currentAccent)
+ ? ThemeHelper.Brush("AccentFillColorDefaultBrush")
+ : new SolidColorBrush(ThemeHelper.ParseHexColor(currentAccent)),
+ Margin = new Thickness(0, 0, 8, 0)
+ };
+ Grid.SetColumn(customPreview, 0);
+
+ var customBox = new TextBox
+ {
+ PlaceholderText = "#0078D4",
+ Text = currentAccent,
+ FontSize = 12,
+ MaxLength = 7,
+ Padding = new Thickness(6, 4, 6, 4)
+ };
+ Grid.SetColumn(customBox, 1);
+
+ var applyBtn = new Button
+ {
+ Content = "Apply",
+ Padding = new Thickness(10, 4, 10, 4),
+ Margin = new Thickness(6, 0, 0, 0),
+ FontSize = 12
+ };
+ Grid.SetColumn(applyBtn, 2);
+
+ customBox.TextChanged += (_, _) =>
+ {
+ var text = customBox.Text;
+ if (text.StartsWith('#') && text.Length == 7)
+ {
+ try { customPreview.Background = new SolidColorBrush(ThemeHelper.ParseHexColor(text)); } catch { }
+ }
+ };
+ applyBtn.Click += (_, _) =>
+ {
+ var text = customBox.Text.Trim();
+ if (text.StartsWith('#') && text.Length == 7)
+ {
+ flyout.Hide();
+ DispatcherQueue.TryEnqueue(() => ApplyAccentAndSave(text));
+ }
+ };
+
+ customGrid.Children.Add(customPreview);
+ customGrid.Children.Add(customBox);
+ customGrid.Children.Add(applyBtn);
+ panel.Children.Add(customGrid);
+
+ flyout.Content = panel;
+ flyout.ShowAt(anchor);
+ }
+
+ private void ApplyAccentAndSave(string hexColor)
+ {
+ var s = SettingsManager.Load();
+ SettingsManager.Save(s with { AccentColor = hexColor });
+
+ // Update accent resources in place (no remove/add — safe at runtime)
+ ThemeHelper.ApplyAccentColor(hexColor);
+
+ // Force theme re-resolve: cycle through both explicit themes then back
+ if (Content is FrameworkElement root)
+ {
+ var current = root.RequestedTheme;
+ root.RequestedTheme = ElementTheme.Light;
+ root.RequestedTheme = ElementTheme.Dark;
+ root.RequestedTheme = current;
+ }
+
+ // Rebuild code-behind elements
+ ApplyFilterAndSort();
+ UpdateNavigation();
+ UpdateRepeatIcon();
+ ShuffleIcon.Foreground = _queue.Shuffle
+ ? ThemeHelper.Brush("AccentTextFillColorPrimaryBrush")
+ : ThemeHelper.Brush("TextFillColorPrimaryBrush");
+ }
+
private static Windows.UI.Color ParseColor(string hex)
{
hex = hex.TrimStart('#');
@@ -3549,10 +4188,12 @@ private void ToggleCollapse()
? Visibility.Visible : Visibility.Collapsed;
var isPodcast = _viewMode == ViewMode.Podcast || _viewMode == ViewMode.PodcastEpisodes;
var isTrackView = _viewMode != ViewMode.Visualizer && _viewMode != ViewMode.MediaControl
- && _viewMode != ViewMode.Radio && !isPodcast;
+ && _viewMode != ViewMode.Radio && _viewMode != ViewMode.Equalizer && !isPodcast;
TrackListView.Visibility = isTrackView ? Visibility.Visible : Visibility.Collapsed;
WaveformContainer.Visibility = _viewMode == ViewMode.Visualizer
? Visibility.Visible : Visibility.Collapsed;
+ EqualizerContainer.Visibility = _viewMode == ViewMode.Equalizer
+ ? Visibility.Visible : Visibility.Collapsed;
RadioContainer.Visibility = _viewMode == ViewMode.Radio
? Visibility.Visible : Visibility.Collapsed;
PodcastContainer.Visibility = isPodcast
@@ -3578,6 +4219,7 @@ private void ToggleCollapse()
SearchSortRow.Visibility = Visibility.Collapsed;
TrackListView.Visibility = Visibility.Collapsed;
WaveformContainer.Visibility = Visibility.Collapsed;
+ EqualizerContainer.Visibility = Visibility.Collapsed;
RadioContainer.Visibility = Visibility.Collapsed;
PodcastContainer.Visibility = Visibility.Collapsed;
MediaContainer.Visibility = Visibility.Collapsed;
@@ -3611,6 +4253,7 @@ private void AnimTick(object? sender, object e)
SearchSortRow.Visibility = Visibility.Collapsed;
TrackListView.Visibility = Visibility.Collapsed;
WaveformContainer.Visibility = Visibility.Collapsed;
+ EqualizerContainer.Visibility = Visibility.Collapsed;
MediaContainer.Visibility = Visibility.Collapsed;
BottomBar.Visibility = Visibility.Collapsed;
MiniPlayerBar.Visibility = Visibility.Collapsed;
@@ -3625,6 +4268,7 @@ private void AnimTick(object? sender, object e)
SearchSortRow.Visibility = Visibility.Collapsed;
TrackListView.Visibility = Visibility.Collapsed;
WaveformContainer.Visibility = Visibility.Collapsed;
+ EqualizerContainer.Visibility = Visibility.Collapsed;
MediaContainer.Visibility = Visibility.Collapsed;
BottomBar.Visibility = Visibility.Collapsed;
MiniPlayerBar.Visibility = Visibility.Visible;
@@ -3738,6 +4382,7 @@ private void Pin_Click(object sender, RoutedEventArgs e)
if (AppWindow.Presenter is OverlappedPresenter presenter)
presenter.IsAlwaysOnTop = _isPinnedOnTop;
+ ApplyPinToLibraryWindow();
PinIcon.Glyph = _isPinnedOnTop ? "\uE842" : "\uE840";
}
@@ -3769,6 +4414,7 @@ private void Close_Click(object sender, RoutedEventArgs e)
Close();
else
{
+ CloseLibraryWindow();
ShowWindow(_hwnd, 0); // Hide to tray
_isVisible = false;
SuspendTimers();
@@ -3810,6 +4456,7 @@ private void ToggleWindow()
{
if (_isVisible)
{
+ CloseLibraryWindow();
ShowWindow(_hwnd, 0); // SW_HIDE
_isVisible = false;
SuspendTimers();
diff --git a/Audiomatic/Services/AudioPlayerService.cs b/Audiomatic/Services/AudioPlayerService.cs
index bcfaf82..31b8c09 100644
--- a/Audiomatic/Services/AudioPlayerService.cs
+++ b/Audiomatic/Services/AudioPlayerService.cs
@@ -19,6 +19,12 @@ public sealed class AudioPlayerService : IDisposable
private bool _isMuted;
private InMemoryRandomAccessStream? _albumArtStream;
+ // Equalizer
+ private Equalizer? _equalizer;
+ private float[] _eqGains = new float[10];
+ private bool _eqEnabled = true;
+ private float _eqPreampDb;
+
// NAudio-supported but not natively by MediaPlayer
private static readonly HashSet NAudioOnlyExtensions =
new(StringComparer.OrdinalIgnoreCase) { ".ape", ".aiff" };
@@ -130,58 +136,39 @@ public async Task PlayTrackAsync(TrackInfo track)
Stop();
CurrentTrack = track;
- var ext = Path.GetExtension(track.Path).ToLowerInvariant();
-
- if (NAudioOnlyExtensions.Contains(ext))
+ // Always use NAudio for file playback (enables equalizer)
+ _useNAudio = true;
+ try
{
- // Use NAudio for formats MediaPlayer can't handle
- _useNAudio = true;
- try
+ _audioReader = new AudioFileReader(track.Path);
+ _equalizer = new Equalizer(_audioReader);
+ _equalizer.Enabled = _eqEnabled;
+ _equalizer.SetAllBands(_eqGains);
+ _equalizer.Preamp = DbToLinear(_eqPreampDb);
+
+ _waveOut = new WasapiOut();
+ _waveOut.Init(new NAudio.Wave.SampleProviders.SampleToWaveProvider16(_equalizer));
+ _waveOut.Volume = _isMuted ? 0f : (float)Math.Clamp(_mediaPlayer.Volume, 0, 1);
+ _waveOut.PlaybackStopped += (_, _) =>
{
- _audioReader = new AudioFileReader(track.Path);
- _waveOut = new WasapiOut();
- _waveOut.Init(_audioReader);
- _waveOut.Volume = _isMuted ? 0f : (float)Math.Clamp(_mediaPlayer.Volume, 0, 1);
- _waveOut.PlaybackStopped += (_, _) =>
+ if (_audioReader != null && _audioReader.CurrentTime >= _audioReader.TotalTime - TimeSpan.FromMilliseconds(500))
{
- if (_audioReader != null && _audioReader.CurrentTime >= _audioReader.TotalTime - TimeSpan.FromMilliseconds(500))
- {
- IsPlaying = false;
- _dispatcherQueue?.TryEnqueue(() => MediaEnded?.Invoke());
- }
- };
- _waveOut.Play();
- IsPlaying = true;
- UpdateSmtc(track);
- _dispatcherQueue?.TryEnqueue(() =>
- {
- MediaOpened?.Invoke();
- PlaybackStarted?.Invoke();
- });
- }
- catch (Exception ex)
+ IsPlaying = false;
+ _dispatcherQueue?.TryEnqueue(() => MediaEnded?.Invoke());
+ }
+ };
+ _waveOut.Play();
+ IsPlaying = true;
+ UpdateSmtc(track);
+ _dispatcherQueue?.TryEnqueue(() =>
{
- _dispatcherQueue?.TryEnqueue(() => MediaFailed?.Invoke(ex.Message));
- }
+ MediaOpened?.Invoke();
+ PlaybackStarted?.Invoke();
+ });
}
- else
+ catch (Exception ex)
{
- // Use MediaPlayer (handles mp3, flac, wav, ogg, aac, wma, m4a, opus)
- _useNAudio = false;
- try
- {
- var file = await StorageFile.GetFileFromPathAsync(track.Path);
- _mediaPlayer.Source = MediaSource.CreateFromStorageFile(file);
- _mediaPlayer.Volume = Volume;
- _mediaPlayer.Play();
- IsPlaying = true;
- UpdateSmtc(track);
- _dispatcherQueue?.TryEnqueue(() => PlaybackStarted?.Invoke());
- }
- catch (Exception ex)
- {
- _dispatcherQueue?.TryEnqueue(() => MediaFailed?.Invoke(ex.Message));
- }
+ _dispatcherQueue?.TryEnqueue(() => MediaFailed?.Invoke(ex.Message));
}
}
@@ -395,6 +382,44 @@ public void UpdateSmtcArtwork(byte[]? embeddedArtData, string? coverFilePath)
catch { }
}
+ // -- Equalizer control --
+
+ public bool EqEnabled
+ {
+ get => _eqEnabled;
+ set
+ {
+ _eqEnabled = value;
+ if (_equalizer != null) _equalizer.Enabled = value;
+ }
+ }
+
+ public void SetEqBand(int index, float gainDb)
+ {
+ if (index >= 0 && index < _eqGains.Length)
+ _eqGains[index] = Math.Clamp(gainDb, -12f, 12f);
+ _equalizer?.SetBand(index, gainDb);
+ }
+
+ public void SetEqAllBands(float[] gains)
+ {
+ for (int i = 0; i < Math.Min(gains.Length, _eqGains.Length); i++)
+ _eqGains[i] = Math.Clamp(gains[i], -12f, 12f);
+ _equalizer?.SetAllBands(_eqGains);
+ }
+
+ public float[] GetEqGains() => (float[])_eqGains.Clone();
+
+ public void SetEqPreamp(float db)
+ {
+ _eqPreampDb = db;
+ if (_equalizer != null) _equalizer.Preamp = DbToLinear(db);
+ }
+
+ public float EqPreampDb => _eqPreampDb;
+
+ private static float DbToLinear(float db) => MathF.Pow(10f, db / 20f);
+
public void SuspendPositionTimer()
{
_positionTimer?.Change(Timeout.Infinite, Timeout.Infinite);
diff --git a/Audiomatic/Services/EqualizerService.cs b/Audiomatic/Services/EqualizerService.cs
new file mode 100644
index 0000000..01ffc81
--- /dev/null
+++ b/Audiomatic/Services/EqualizerService.cs
@@ -0,0 +1,109 @@
+using NAudio.Dsp;
+using NAudio.Wave;
+
+namespace Audiomatic.Services;
+
+public class Equalizer : ISampleProvider
+{
+ private readonly ISampleProvider _source;
+ private readonly int _channels;
+ private readonly int _sampleRate;
+ private readonly int _bandCount;
+ private BiQuadFilter[,] _filters;
+ private readonly float[] _gains;
+ private float _preamp = 1.0f;
+ private bool _enabled = true;
+
+ public static readonly float[] DefaultFrequencies =
+ [32f, 64f, 125f, 250f, 500f, 1000f, 2000f, 4000f, 8000f, 16000f];
+
+ public static readonly string[] FrequencyLabels =
+ ["32", "64", "125", "250", "500", "1K", "2K", "4K", "8K", "16K"];
+
+ public static readonly Dictionary Presets = new()
+ {
+ ["Flat"] = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
+ ["Bass Boost"] = [6, 5, 4, 2, 0, 0, 0, 0, 0, 0],
+ ["Treble Boost"] = [0, 0, 0, 0, 0, 0, 2, 4, 5, 6],
+ ["Rock"] = [5, 4, 3, 1, -1, 0, 2, 3, 4, 5],
+ ["Pop"] = [-1, 1, 3, 4, 3, 0, -1, 1, 2, 3],
+ ["Jazz"] = [3, 2, 1, 2, -1, -1, 0, 1, 2, 3],
+ ["Classical"] = [4, 3, 2, 1, -1, -1, 0, 2, 3, 4],
+ ["Electronic"] = [5, 4, 1, 0, -2, 0, 1, 3, 4, 5],
+ ["Hip-Hop"] = [5, 4, 3, 1, 0, 0, 1, 0, 2, 3],
+ ["Vocal"] = [-2, -1, 0, 2, 4, 4, 3, 1, 0, -1],
+ };
+
+ public Equalizer(ISampleProvider source)
+ {
+ _source = source;
+ _channels = source.WaveFormat.Channels;
+ _sampleRate = source.WaveFormat.SampleRate;
+ _bandCount = DefaultFrequencies.Length;
+ _gains = new float[_bandCount];
+ _filters = new BiQuadFilter[_channels, _bandCount];
+ RebuildFilters();
+ }
+
+ public WaveFormat WaveFormat => _source.WaveFormat;
+ public bool Enabled { get => _enabled; set => _enabled = value; }
+
+ public float Preamp
+ {
+ get => _preamp;
+ set => _preamp = Math.Clamp(value, 0.1f, 4.0f);
+ }
+
+ private void RebuildFilters()
+ {
+ _filters = new BiQuadFilter[_channels, _bandCount];
+ for (int c = 0; c < _channels; c++)
+ {
+ for (int b = 0; b < _bandCount; b++)
+ {
+ _filters[c, b] = BiQuadFilter.PeakingEQ(
+ _sampleRate, DefaultFrequencies[b], 1.0f, _gains[b]);
+ }
+ }
+ }
+
+ public void SetBand(int index, float gainDb)
+ {
+ if (index < 0 || index >= _bandCount) return;
+ _gains[index] = Math.Clamp(gainDb, -12f, 12f);
+ for (int c = 0; c < _channels; c++)
+ {
+ _filters[c, index] = BiQuadFilter.PeakingEQ(
+ _sampleRate, DefaultFrequencies[index], 1.0f, _gains[index]);
+ }
+ }
+
+ public void SetAllBands(float[] gains)
+ {
+ for (int i = 0; i < Math.Min(gains.Length, _bandCount); i++)
+ _gains[i] = Math.Clamp(gains[i], -12f, 12f);
+ RebuildFilters();
+ }
+
+ public float[] GetGains()
+ {
+ return (float[])_gains.Clone();
+ }
+
+ public int Read(float[] buffer, int offset, int count)
+ {
+ int read = _source.Read(buffer, offset, count);
+ if (!_enabled) return read;
+
+ for (int i = 0; i < read; i++)
+ {
+ int ch = i % _channels;
+ float sample = buffer[offset + i];
+ for (int b = 0; b < _bandCount; b++)
+ sample = _filters[ch, b].Transform(sample);
+ buffer[offset + i] = sample * _preamp;
+ }
+
+ return read;
+ }
+}
diff --git a/Audiomatic/SettingsManager.cs b/Audiomatic/SettingsManager.cs
index bde154a..bdfc2bc 100644
--- a/Audiomatic/SettingsManager.cs
+++ b/Audiomatic/SettingsManager.cs
@@ -27,7 +27,12 @@ public record AppSettings(
bool VisualizerGlow = true,
bool VisualizerDarkBg = false,
int? WindowX = null,
- int? WindowY = null);
+ int? WindowY = null,
+ bool EqEnabled = true,
+ string EqPreset = "Flat",
+ float[]? EqBands = null, // 10-band gains in dB (-12 to +12)
+ float EqPreamp = 0f,
+ string AccentColor = ""); // hex string, empty = system accent
public static class SettingsManager
{
diff --git a/Audiomatic/ThemeHelper.cs b/Audiomatic/ThemeHelper.cs
index 2616715..e1bb01a 100644
--- a/Audiomatic/ThemeHelper.cs
+++ b/Audiomatic/ThemeHelper.cs
@@ -17,18 +17,33 @@ internal static class ThemeHelper
internal static Brush Brush(string key)
{
- if (CurrentTheme != ElementTheme.Default)
+ var themeKeys = ResolveThemeKeys();
+ foreach (var themeKey in themeKeys)
{
- var themeKeys = CurrentTheme == ElementTheme.Dark ? DarkKeys : LightKeys;
- foreach (var themeKey in themeKeys)
- {
- var result = FindInThemeDictionaries(Application.Current.Resources, key, themeKey);
- if (result != null) return result;
- }
+ var result = FindInThemeDictionaries(Application.Current.Resources, key, themeKey);
+ if (result != null) return result;
}
return (Brush)Application.Current.Resources[key];
}
+ private static string[] ResolveThemeKeys()
+ {
+ if (CurrentTheme == ElementTheme.Dark) return DarkKeys;
+ if (CurrentTheme == ElementTheme.Light) return LightKeys;
+
+ // Default (system): detect actual system theme
+ try
+ {
+ var uiSettings = new Windows.UI.ViewManagement.UISettings();
+ var bg = uiSettings.GetColorValue(Windows.UI.ViewManagement.UIColorType.Background);
+ return bg.R < 128 ? DarkKeys : LightKeys;
+ }
+ catch
+ {
+ return DarkKeys;
+ }
+ }
+
private static Brush? FindInThemeDictionaries(ResourceDictionary dict, string key, string themeKey)
{
if (dict.ThemeDictionaries.TryGetValue(themeKey, out var td)
@@ -46,4 +61,222 @@ internal static Brush Brush(string key)
return null;
}
+
+ // -- Accent color overrides --
+
+ private static ResourceDictionary? _accentOverrideDict;
+
+ internal static readonly (string Name, string Hex)[] AccentPresets =
+ [
+ ("System", ""),
+ ("Red", "#E81123"),
+ ("Orange", "#F7630C"),
+ ("Gold", "#FFB900"),
+ ("Green", "#10893E"),
+ ("Teal", "#038387"),
+ ("Blue", "#0078D4"),
+ ("Indigo", "#6B69D6"),
+ ("Purple", "#744DA9"),
+ ("Pink", "#E3008C"),
+ ("Rose", "#EA005E"),
+ ("Wine", "#9A0089"),
+ ("Rust", "#DA3B01"),
+ ("Amber", "#FF8C00"),
+ ("Lime", "#00CC6A"),
+ ("Seafoam", "#00B7C3"),
+ ("Navy", "#0063B1"),
+ ("Iris", "#8764B8"),
+ ("Orchid", "#C239B3"),
+ ("Brick", "#D13438"),
+ ("Olive", "#498205"),
+ ("Mint", "#00B294"),
+ ("Sky", "#0099BC"),
+ ("Steel", "#515C6B"),
+ ];
+
+ ///
+ /// Applies a custom accent color. Safe to call at startup (before UI)
+ /// and at runtime (saves setting, requires restart for full XAML effect).
+ ///
+ internal static void ApplyAccentColor(string? hexColor)
+ {
+ if (string.IsNullOrEmpty(hexColor))
+ {
+ // Clear overrides if dict exists
+ if (_accentOverrideDict != null)
+ {
+ ClearDict(_accentOverrideDict);
+ ClearDict(_accentOverrideDict.ThemeDictionaries["Default"] as ResourceDictionary);
+ ClearDict(_accentOverrideDict.ThemeDictionaries["Light"] as ResourceDictionary);
+ }
+ return;
+ }
+
+ var color = ParseHexColor(hexColor);
+ var l1 = Lighten(color, 0.15);
+ var l2 = Lighten(color, 0.30);
+ var l3 = Lighten(color, 0.45);
+ var d1 = Darken(color, 0.15);
+ var d2 = Darken(color, 0.30);
+ var d3 = Darken(color, 0.45);
+
+ // Create dictionary once, never remove it
+ if (_accentOverrideDict == null)
+ {
+ _accentOverrideDict = new ResourceDictionary();
+ _accentOverrideDict.ThemeDictionaries["Default"] = new ResourceDictionary();
+ _accentOverrideDict.ThemeDictionaries["Light"] = new ResourceDictionary();
+ Application.Current.Resources.MergedDictionaries.Add(_accentOverrideDict);
+ }
+
+ // Update values in place — colors + brushes for dark theme
+ var darkDict = (_accentOverrideDict.ThemeDictionaries["Default"] as ResourceDictionary)!;
+ SetAccentResources(darkDict, color, l1, l2, l3, d1, d2, d3, isDark: true);
+
+ // Light theme
+ var lightDict = (_accentOverrideDict.ThemeDictionaries["Light"] as ResourceDictionary)!;
+ SetAccentResources(lightDict, color, l1, l2, l3, d1, d2, d3, isDark: false);
+
+ // Top-level fallback
+ SetAccentResources(_accentOverrideDict, color, l1, l2, l3, d1, d2, d3, isDark: true);
+ }
+
+ private static void ClearDict(ResourceDictionary? dict)
+ {
+ dict?.Clear();
+ }
+
+ private static void SetAccentResources(ResourceDictionary dict,
+ Windows.UI.Color color, Windows.UI.Color l1, Windows.UI.Color l2, Windows.UI.Color l3,
+ Windows.UI.Color d1, Windows.UI.Color d2, Windows.UI.Color d3, bool isDark)
+ {
+ // Color resources
+ dict["SystemAccentColor"] = color;
+ dict["SystemAccentColorLight1"] = l1;
+ dict["SystemAccentColorLight2"] = l2;
+ dict["SystemAccentColorLight3"] = l3;
+ dict["SystemAccentColorDark1"] = d1;
+ dict["SystemAccentColorDark2"] = d2;
+ dict["SystemAccentColorDark3"] = d3;
+
+ // Brush resources — WinUI dark uses Light variants, light uses Dark variants
+ var fill = isDark ? l2 : d1;
+ var fillSecondary = WithAlpha(fill, 0.9);
+ var fillTertiary = WithAlpha(fill, 0.8);
+ var fillDisabled = WithAlpha(fill, 0.4);
+
+ var text = isDark ? l3 : d2;
+ var textSecondary = isDark ? l2 : d2;
+ var textTertiary = isDark ? l1 : d1;
+
+ var fillBrush = new SolidColorBrush(fill);
+ var fillSecBrush = new SolidColorBrush(fillSecondary);
+ var fillTerBrush = new SolidColorBrush(fillTertiary);
+ var fillDisBrush = new SolidColorBrush(fillDisabled);
+
+ // General accent brushes
+ dict["AccentFillColorDefaultBrush"] = fillBrush;
+ dict["AccentFillColorSecondaryBrush"] = fillSecBrush;
+ dict["AccentFillColorTertiaryBrush"] = fillTerBrush;
+ dict["AccentFillColorDisabledBrush"] = fillDisBrush;
+ dict["AccentTextFillColorPrimaryBrush"] = new SolidColorBrush(text);
+ dict["AccentTextFillColorSecondaryBrush"] = new SolidColorBrush(textSecondary);
+ dict["AccentTextFillColorTertiaryBrush"] = new SolidColorBrush(textTertiary);
+ dict["AccentTextFillColorDisabledBrush"] = fillDisBrush;
+
+ // Slider-specific lightweight styling resources
+ dict["SliderTrackValueFill"] = fillBrush;
+ dict["SliderTrackValueFillPointerOver"] = fillSecBrush;
+ dict["SliderTrackValueFillPressed"] = fillTerBrush;
+ dict["SliderTrackValueFillDisabled"] = fillDisBrush;
+ dict["SliderThumbBackground"] = fillBrush;
+ dict["SliderThumbBackgroundPointerOver"] = fillSecBrush;
+ dict["SliderThumbBackgroundPressed"] = fillTerBrush;
+ dict["SliderThumbBackgroundDisabled"] = fillDisBrush;
+
+ // ToggleSwitch
+ dict["ToggleSwitchFillOnBrush"] = fillBrush;
+ dict["ToggleSwitchFillOnBrushPointerOver"] = fillSecBrush;
+ dict["ToggleSwitchFillOnBrushPressed"] = fillTerBrush;
+ dict["ToggleSwitchFillOnBrushDisabled"] = fillDisBrush;
+ }
+
+ private static Windows.UI.Color WithAlpha(Windows.UI.Color c, double alpha)
+ {
+ return Windows.UI.Color.FromArgb((byte)(alpha * 255), c.R, c.G, c.B);
+ }
+
+ internal static Windows.UI.Color ParseHexColor(string hex)
+ {
+ hex = hex.TrimStart('#');
+ if (hex.Length != 6)
+ return new Windows.UI.Color { A = 255, R = 0, G = 120, B = 212 };
+ return new Windows.UI.Color
+ {
+ A = 255,
+ R = Convert.ToByte(hex[..2], 16),
+ G = Convert.ToByte(hex[2..4], 16),
+ B = Convert.ToByte(hex[4..6], 16)
+ };
+ }
+
+ private static Windows.UI.Color Lighten(Windows.UI.Color c, double amount)
+ {
+ var (h, s, l) = RgbToHsl(c.R, c.G, c.B);
+ l = Math.Min(1.0, l + amount);
+ return HslToRgb(h, s, l, c.A);
+ }
+
+ private static Windows.UI.Color Darken(Windows.UI.Color c, double amount)
+ {
+ var (h, s, l) = RgbToHsl(c.R, c.G, c.B);
+ l = Math.Max(0.0, l - amount);
+ return HslToRgb(h, s, l, c.A);
+ }
+
+ private static (double h, double s, double l) RgbToHsl(byte r, byte g, byte b)
+ {
+ double rd = r / 255.0, gd = g / 255.0, bd = b / 255.0;
+ double max = Math.Max(rd, Math.Max(gd, bd));
+ double min = Math.Min(rd, Math.Min(gd, bd));
+ double h = 0, s = 0, l = (max + min) / 2;
+
+ if (max != min)
+ {
+ double d = max - min;
+ s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
+ if (max == rd) h = ((gd - bd) / d + (gd < bd ? 6 : 0)) / 6;
+ else if (max == gd) h = ((bd - rd) / d + 2) / 6;
+ else h = ((rd - gd) / d + 4) / 6;
+ }
+ return (h, s, l);
+ }
+
+ private static Windows.UI.Color HslToRgb(double h, double s, double l, byte a = 255)
+ {
+ double r, g, b;
+ if (s == 0)
+ {
+ r = g = b = l;
+ }
+ else
+ {
+ double q = l < 0.5 ? l * (1 + s) : l + s - l * s;
+ double p = 2 * l - q;
+ r = HueToRgb(p, q, h + 1.0 / 3);
+ g = HueToRgb(p, q, h);
+ b = HueToRgb(p, q, h - 1.0 / 3);
+ }
+ return Windows.UI.Color.FromArgb(a, (byte)(r * 255), (byte)(g * 255), (byte)(b * 255));
+ }
+
+ private static double HueToRgb(double p, double q, double t)
+ {
+ if (t < 0) t += 1;
+ if (t > 1) t -= 1;
+ if (t < 1.0 / 6) return p + (q - p) * 6 * t;
+ if (t < 1.0 / 2) return q;
+ if (t < 2.0 / 3) return p + (q - p) * (2.0 / 3 - t) * 6;
+ return p;
+ }
}
diff --git a/installer.iss b/installer.iss
index 0c10c3c..c32ee64 100644
--- a/installer.iss
+++ b/installer.iss
@@ -1,6 +1,6 @@
[Setup]
AppName=Audiomatic
-AppVersion=0.0.4
+AppVersion=0.0.5
AppPublisher=OhMyCode
DefaultDirName={localappdata}\Programs\Audiomatic
DefaultGroupName=Audiomatic