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