windowcapture
исходный код / UI/SoundpadForm.cs

SoundpadForm.cs

1000 строк · 56,722 байт · модуль UI
   1using System;
   2using System.Collections.Generic;
   3using System.Drawing;
   4using System.Drawing.Drawing2D;
   5using System.Drawing.Imaging;
   6using System.IO;
   7using System.IO.MemoryMappedFiles;
   8using System.Runtime.InteropServices;
   9using System.Text;
  10using System.Threading;
  11using System.Windows.Forms;
  12using WindowCapture.Helpers;
  13using WindowCapture.Models;
  14using WindowCapture.Native;
  15
  16namespace WindowCapture.UI
  17{
  18    public class SoundpadForm : Form
  19    {
  20        private const int FormW = 400;
  21        private const int PadSize = 82;
  22        private const int PadGap = 5;
  23        private const int TopBarH = 38;
  24        private const int BottomH = 200;
  25
  26        // ===== Slide =====
  27        private System.Windows.Forms.Timer animTimer;
  28        private float slideProgress, slideTarget;
  29
  30        // ===== Pads =====
  31        private readonly List<SoundPad> pads = new List<SoundPad>();
  32        private int hoverPad = -1, playingPad = -1;
  33        private float playingAnim, playProgress;
  34        private string playingName = "";
  35        private float scrollOffset, scrollTarget;
  36
  37        // ===== Device =====
  38        private List<string> deviceNames;
  39        private int currentDeviceIdx;
  40
  41        // ===== Volume =====
  42        private float masterVolume = 0.85f;
  43
  44        // ===== APO Diagnostics =====
  45        private string[] diagSteps = new string[8];
  46        private int[] diagStatus = new int[8]; // 0=pending, 1=ok, 2=fail
  47        private System.Windows.Forms.Timer diagTimer;
  48
  49        // ===== APO =====
  50        private const string APO_CLSID = "{A1B2C3D4-1234-5678-9ABC-DEF012345678}";
  51        private const string APO_DLL_NAME = "SoundpadAPO.dll";
  52        private const string SHM_NAME = "Global\\WC_Soundpad_Buf";
  53        private const int SHM_HEADER = 5; // floats: volume, readPos, totalSamples, channels, sampleRate
  54        private const int SHM_MAX_SAMPLES = 48000 * 2 * 60; // 60 sec stereo
  55        private MemoryMappedFile mmf;
  56        private MemoryMappedViewAccessor shmAccessor;
  57        private bool apoInstalled;
  58
  59        // ===== GDI =====
  60        private Font fntTitle, fntPad, fntSmall, fntStatus;
  61        private SolidBrush brText, brDim, brAccent, brPadBg, brPadHover, brBarBg, brProgress;
  62        private Pen penEdge, penBorder;
  63
  64        // ===== Noise =====
  65        private static Bitmap noiseTexture;
  66        private static int noiseCachedI = -1;
  67
  68        // ===== P/Invoke =====
  69        [DllImport("kernel32.dll", CharSet = CharSet.Auto)]
  70        private static extern int GetShortPathName(string lpszLong, StringBuilder lpszShort, int cchBuffer);
  71        [DllImport("winmm.dll")]
  72        private static extern int waveOutGetNumDevs();
  73        [DllImport("winmm.dll", CharSet = CharSet.Auto)]
  74        private static extern int waveOutGetDevCaps(IntPtr uDeviceID, ref WAVEOUTCAPS pwoc, int cbwoc);
  75        [DllImport("winmm.dll")]
  76        private static extern int waveInGetNumDevs();
  77        [DllImport("winmm.dll", CharSet = CharSet.Auto)]
  78        private static extern int waveInGetDevCaps(IntPtr uDeviceID, ref WAVEOUTCAPS pwic, int cbwic);
  79        [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
  80        private struct WAVEOUTCAPS { public ushort wMid, wPid; public uint vDriverVersion; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)] public string szPname; public uint dwFormats; public ushort wChannels, wReserved1; public uint dwSupport; }
  81
  82        private string soundsDir;
  83        private class SoundPad { public string FilePath, Name, Hotkey; public Color Accent; public bool IsEmpty; }
  84
  85        /// <summary>Auto-install APO at app startup. Registers COM + binds to default microphone.</summary>
  86        public static void EnsureApoInstalled()
  87        {
  88            try
  89            {
  90                // Quick check: if COM already registered AND DLL exists in System32, skip everything
  91                string dstCheck = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.System), APO_DLL_NAME);
  92                bool comExists = false;
  93                try { using (var k = Microsoft.Win32.Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Classes\CLSID\" + APO_CLSID + @"\InprocServer32")) comExists = (k != null); } catch { }
  94                if (comExists && File.Exists(dstCheck)) { SLog("APO already installed, skipping"); return; }
  95
  96                string srcDll = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "APO", APO_DLL_NAME);
  97                if (!File.Exists(srcDll)) { SLog("APO DLL not found: " + srcDll); return; }
  98
  99                string dstDll = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.System), APO_DLL_NAME);
 100
 101                // 0. Install signing certificate into Trusted Root + TrustedPublisher
 102                string certFile = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "APO", "apo_cert.cer");
 103                if (File.Exists(certFile))
 104                {
 105                    try
 106                    {
 107                        var cert = new System.Security.Cryptography.X509Certificates.X509Certificate2(certFile);
 108                        foreach (string storeName in new[] { "Root", "TrustedPublisher" })
 109                        {
 110                            var store = new System.Security.Cryptography.X509Certificates.X509Store(storeName, System.Security.Cryptography.X509Certificates.StoreLocation.LocalMachine);
 111                            store.Open(System.Security.Cryptography.X509Certificates.OpenFlags.ReadWrite);
 112                            store.Add(cert);
 113                            store.Close();
 114                        }
 115                        SLog("Signing cert installed in Root + TrustedPublisher");
 116                    }
 117                    catch (Exception ex) { SLog("Cert install: " + ex.Message); }
 118                }
 119
 120                // 1. Stop only Audiosrv to release DLL lock (NOT AudioEndpointBuilder — it resets FxProperties!)
 121                RunCmd("net.exe", "stop Audiosrv");
 122                Thread.Sleep(1000);
 123                try { File.Copy(srcDll, dstDll, true); SLog("APO DLL copied to " + dstDll); }
 124                catch (Exception ex) { SLog("APO copy FAILED: " + ex.Message); RunCmd("net.exe", "start Audiosrv"); return; }
 125
 126                // 2. Register COM class
 127                try
 128                {
 129                    using (var key = Microsoft.Win32.Registry.LocalMachine.CreateSubKey(@"SOFTWARE\Classes\CLSID\" + APO_CLSID + @"\InprocServer32"))
 130                    {
 131                        key.SetValue(null, dstDll);
 132                        key.SetValue("ThreadingModel", "Both");
 133                    }
 134                    SLog("APO COM registered");
 135                }
 136                catch (Exception ex) { SLog("APO COM reg FAILED: " + ex.Message); return; }
 137
 138                // 3. Register as AudioProcessingObject
 139                try
 140                {
 141                    using (var key = Microsoft.Win32.Registry.LocalMachine.CreateSubKey(@"SOFTWARE\Classes\AudioEngine\AudioProcessingObjects\" + APO_CLSID))
 142                    {
 143                        key.SetValue("FriendlyName", "Soundpad APO");
 144                        key.SetValue("Copyright", "WindowCapture");
 145                        key.SetValue("MajorVersion", 1, Microsoft.Win32.RegistryValueKind.DWord);
 146                        key.SetValue("MinorVersion", 0, Microsoft.Win32.RegistryValueKind.DWord);
 147                        key.SetValue("Flags", 0xD, Microsoft.Win32.RegistryValueKind.DWord); // same as EQ APO
 148                        key.SetValue("MinInputConnections", 1, Microsoft.Win32.RegistryValueKind.DWord);
 149                        key.SetValue("MaxInputConnections", 1, Microsoft.Win32.RegistryValueKind.DWord);
 150                        key.SetValue("MinOutputConnections", 1, Microsoft.Win32.RegistryValueKind.DWord);
 151                        key.SetValue("MaxOutputConnections", 1, Microsoft.Win32.RegistryValueKind.DWord);
 152                        key.SetValue("MaxInstances", unchecked((int)0xFFFFFFFF), Microsoft.Win32.RegistryValueKind.DWord);
 153                        // CRITICAL: NumAPOInterfaces + APOInterface0 required for audiodg to load APO!
 154                        key.SetValue("NumAPOInterfaces", 1, Microsoft.Win32.RegistryValueKind.DWord);
 155                        key.SetValue("APOInterface0", "{FD7F2B29-24D0-4B5C-B177-592C39F9CA10}"); // IAudioProcessingObject
 156                    }
 157                    SLog("APO AudioEngine registered");
 158                }
 159                catch (Exception ex) { SLog("APO AudioEngine reg FAILED: " + ex.Message); }
 160
 161                // 4. Disable protected audio (allow unsigned APO in audiodg.exe)
 162                try
 163                {
 164                    using (var key = Microsoft.Win32.Registry.LocalMachine.CreateSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Audio"))
 165                        key.SetValue("DisableProtectedAudioDG", 1, Microsoft.Win32.RegistryValueKind.DWord);
 166                    SLog("DisableProtectedAudioDG set");
 167                }
 168                catch { }
 169
 170                // 5-6. Start Audiosrv first, THEN write FxProperties (AudioEndpointBuilder resets them on restart)
 171                // 6. Start Audiosrv (AudioEndpointBuilder should still be running — DON'T restart it, it resets FxProperties)
 172                RunCmd("net.exe", "start Audiosrv");
 173                Thread.Sleep(2000);
 174
 175                // 7. NOW write FxProperties AFTER services are running (AudioEndpointBuilder won't overwrite)
 176                BindApoToDefaultMic();
 177
 178                // 8. Restart ONLY Audiosrv to make audiodg re-read FxProperties with our APO
 179                RunCmd("net.exe", "stop Audiosrv");
 180                Thread.Sleep(500);
 181                RunCmd("net.exe", "start Audiosrv");
 182                Thread.Sleep(1500);
 183
 184                // 9. Open capture stream to trigger audiodg to load APO
 185                TriggerApoLoad();
 186
 187                SLog("APO installation complete");
 188            }
 189            catch (Exception ex) { SLog("EnsureApoInstalled EXCEPTION: " + ex.Message); }
 190        }
 191
 192        /// <summary>Find default microphone endpoint GUID and inject APO CLSID into its FxProperties.
 193        /// MMDevices keys are protected — need to take ownership and grant admin write access first.</summary>
 194        private static void BindApoToDefaultMic()
 195        {
 196            try
 197            {
 198                string basePath = @"SOFTWARE\Microsoft\Windows\CurrentVersion\MMDevices\Audio\Capture";
 199                using (var baseKey = Microsoft.Win32.Registry.LocalMachine.OpenSubKey(basePath))
 200                {
 201                    if (baseKey == null) { SLog("No capture devices in registry"); return; }
 202                    foreach (string endpointGuid in baseKey.GetSubKeyNames())
 203                    {
 204                        using (var epKey = baseKey.OpenSubKey(endpointGuid))
 205                        {
 206                            if (epKey == null) continue;
 207                            object stateVal = epKey.GetValue("DeviceState");
 208                            if (stateVal == null || (int)stateVal != 1) continue;
 209
 210                            SLog("Binding APO to: " + endpointGuid);
 211
 212                            // Write FxProperties with SetValue rights (Administrators have SetValue on MMDevices)
 213                            string fxPath = basePath + "\\" + endpointGuid + "\\FxProperties";
 214                            try
 215                            {
 216                                using (var fxKey = Microsoft.Win32.Registry.LocalMachine.OpenSubKey(fxPath,
 217                                    Microsoft.Win32.RegistryKeyPermissionCheck.ReadWriteSubTree,
 218                                    System.Security.AccessControl.RegistryRights.SetValue))
 219                                {
 220                                    if (fxKey != null)
 221                                    {
 222                                        // ONLY set MFX(,6) to our APO — don't touch other slots (EQ APO may be in SFX)
 223                                        object existingSfx = fxKey.GetValue("{d04e05a6-594b-4fb6-a80d-01af5eed7d1d},5");
 224                                        // Only write SFX if nothing is there yet (don't overwrite EQ APO)
 225                                        if (existingSfx == null || existingSfx.ToString().Length == 0)
 226                                            fxKey.SetValue("{d04e05a6-594b-4fb6-a80d-01af5eed7d1d},5", APO_CLSID);
 227                                        // Always write MFX
 228                                        fxKey.SetValue("{d04e05a6-594b-4fb6-a80d-01af5eed7d1d},6", APO_CLSID);
 229                                        // Processing modes for MFX
 230                                        string defaultMode = "{C18E2F7E-933D-4965-B7D1-1EEF228D2AF3}";
 231                                        fxKey.SetValue("{d3993a3f-99c2-4402-b5ec-a92a0367664b},6", new string[] { defaultMode }, Microsoft.Win32.RegistryValueKind.MultiString);
 232                                        // Ensure effects enabled
 233                                        fxKey.SetValue("{1da5d803-d492-4edd-8c23-e0c0ffee7f0e},5", 1, Microsoft.Win32.RegistryValueKind.DWord);
 234                                        // FX Association (needed for usbaudio2)
 235                                        object existingAssoc = fxKey.GetValue("{d04e05a6-594b-4fb6-a80d-01af5eed7d1d},0");
 236                                        if (existingAssoc == null || existingAssoc.ToString().Contains("00000000"))
 237                                            fxKey.SetValue("{d04e05a6-594b-4fb6-a80d-01af5eed7d1d},0", "{DFF21FE5-F70F-11D0-B917-00A0C9223196}");
 238                                        SLog("  FxProperties: MFX=" + APO_CLSID + " SFX=" + (existingSfx ?? "null") + " for " + endpointGuid);
 239                                    }
 240                                    else SLog("  FxProperties key is null");
 241                                }
 242                            }
 243                            catch (Exception ex) { SLog("  FxProperties write FAILED: " + ex.Message); }
 244                        }
 245                    }
 246                }
 247            }
 248            catch (Exception ex) { SLog("BindApoToDefaultMic EXCEPTION: " + ex.Message); }
 249        }
 250
 251        private static readonly Guid CLSID_MMDevEnum = new Guid("BCDE0395-E52F-467C-8E3D-C4579291692E");
 252        private static readonly Guid IID_IAudioClient = new Guid("1CB9AD4C-DBFA-4C32-B178-C2F568A703B2");
 253
 254        /// <summary>Open default capture device via WASAPI to force audiodg.exe to load APO.</summary>
 255        private static void TriggerApoLoad()
 256        {
 257            try
 258            {
 259                SLog("Triggering APO load via WASAPI capture...");
 260                var enumType = Type.GetTypeFromCLSID(CLSID_MMDevEnum);
 261                var enumerator = (IMMDeviceEnumerator)Activator.CreateInstance(enumType);
 262                IMMDevice device;
 263                enumerator.GetDefaultAudioEndpoint(1 /* eCapture */, 0 /* eConsole */, out device);
 264                if (device == null) { SLog("  No default capture device"); return; }
 265
 266                Guid iidAC = IID_IAudioClient;
 267                object acObj;
 268                device.Activate(ref iidAC, 0x17 /* CLSCTX_ALL */, IntPtr.Zero, out acObj);
 269                var audioClient = (IAudioClient)acObj;
 270                if (audioClient == null) { SLog("  IAudioClient activation failed"); return; }
 271
 272                IntPtr pMixFormat;
 273                audioClient.GetMixFormat(out pMixFormat);
 274                audioClient.Initialize(0 /* AUDCLNT_SHAREMODE_SHARED */, 0, 10000000 /* 1 sec */, 0, pMixFormat, IntPtr.Zero);
 275                audioClient.Start();
 276                SLog("  WASAPI capture started — APO should now be loaded");
 277
 278                // Keep capture open briefly then stop (APO stays loaded as long as audiodg runs)
 279                Thread.Sleep(500);
 280                audioClient.Stop();
 281                Marshal.ReleaseComObject(audioClient);
 282                Marshal.ReleaseComObject(device);
 283                Marshal.ReleaseComObject(enumerator);
 284                SLog("  WASAPI capture stopped");
 285            }
 286            catch (Exception ex) { SLog("TriggerApoLoad FAILED: " + ex.Message); }
 287        }
 288
 289        private static void RestartAudioService()
 290        {
 291            try
 292            {
 293                SLog("Restarting audio services...");
 294                // AudioEndpointBuilder restart cascades to Audiosrv + audiodg
 295                RunCmd("net.exe", "stop AudioEndpointBuilder /y");
 296                Thread.Sleep(1000);
 297                RunCmd("net.exe", "start AudioEndpointBuilder");
 298                Thread.Sleep(500);
 299                RunCmd("net.exe", "start Audiosrv");
 300                SLog("Audio services restarted");
 301            }
 302            catch (Exception ex) { SLog("RestartAudio FAILED: " + ex.Message); }
 303        }
 304
 305        private static void RunCmd(string exe, string args)
 306        {
 307            try
 308            {
 309                var psi = new System.Diagnostics.ProcessStartInfo(exe, args)
 310                { UseShellExecute = false, CreateNoWindow = true };
 311                var p = System.Diagnostics.Process.Start(psi);
 312                if (p != null) p.WaitForExit(5000);
 313                SLog("  RunCmd: " + exe + " " + args.Substring(0, Math.Min(args.Length, 80)) + " exit=" + (p != null ? p.ExitCode.ToString() : "null"));
 314            }
 315            catch (Exception ex) { SLog("  RunCmd FAILED: " + ex.Message); }
 316        }
 317
 318        /// <summary>Write FxProperties by creating a SYSTEM-level scheduled task.
 319        /// Only NT AUTHORITY\SYSTEM can write to MMDevices\Audio\Capture FxProperties.</summary>
 320        private static void WriteFxPropertiesViaPowerShell(string endpointGuid)
 321        {
 322            try
 323            {
 324                string fxPath = @"HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\MMDevices\Audio\Capture\" + endpointGuid + @"\FxProperties";
 325                string apoClsid = APO_CLSID;
 326
 327                // PowerShell script that takes ownership, sets ACL, writes values
 328                string ps = @"
 329$ErrorActionPreference = 'SilentlyContinue'
 330
 331# Enable privileges
 332$signature = @'
 333using System;
 334using System.Runtime.InteropServices;
 335public class TokenPriv {
 336    [DllImport(""advapi32.dll"", SetLastError=true)]
 337    public static extern bool OpenProcessToken(IntPtr h, int acc, ref IntPtr t);
 338    [DllImport(""advapi32.dll"", SetLastError=true)]
 339    public static extern bool LookupPrivilegeValue(string s, string n, ref long l);
 340    [DllImport(""advapi32.dll"", SetLastError=true)]
 341    public static extern bool AdjustTokenPrivileges(IntPtr t, bool d, ref TOKEN_PRIVILEGES p, int b, IntPtr prev, IntPtr ret);
 342    public struct TOKEN_PRIVILEGES { public int Count; public long Luid; public int Attr; }
 343    public static void Enable(string priv) {
 344        IntPtr t = IntPtr.Zero;
 345        OpenProcessToken((IntPtr)(-1), 0x28, ref t);
 346        TOKEN_PRIVILEGES tp = new TOKEN_PRIVILEGES();
 347        tp.Count = 1; tp.Attr = 2;
 348        LookupPrivilegeValue(null, priv, ref tp.Luid);
 349        AdjustTokenPrivileges(t, false, ref tp, 0, IntPtr.Zero, IntPtr.Zero);
 350    }
 351}
 352'@
 353Add-Type -TypeDefinition $signature
 354
 355[TokenPriv]::Enable('SeTakeOwnershipPrivilege')
 356[TokenPriv]::Enable('SeRestorePrivilege')
 357[TokenPriv]::Enable('SeBackupPrivilege')
 358
 359$regPath = '" + fxPath.Replace("'", "''") + @"'
 360$parentPath = $regPath -replace '\\FxProperties$',''
 361
 362# Take ownership of parent key
 363$key = [Microsoft.Win32.Registry]::LocalMachine.OpenSubKey(
 364    $parentPath.Replace('HKLM:\','').Replace('HKLM\\',''),
 365    [Microsoft.Win32.RegistryKeyPermissionCheck]::ReadWriteSubTree,
 366    [System.Security.AccessControl.RegistryRights]::TakeOwnership)
 367if ($key) {
 368    $acl = $key.GetAccessControl()
 369    $admin = [System.Security.Principal.NTAccount]'BUILTIN\Administrators'
 370    $acl.SetOwner($admin)
 371    $key.SetAccessControl($acl)
 372    $acl = $key.GetAccessControl()
 373    $rule = New-Object System.Security.AccessControl.RegistryAccessRule($admin,'FullControl','ContainerInherit,ObjectInherit','None','Allow')
 374    $acl.AddAccessRule($rule)
 375    $key.SetAccessControl($acl)
 376    $key.Close()
 377}
 378
 379# Create FxProperties and take ownership
 380$fxKeyPath = $parentPath.Replace('HKLM:\','').Replace('HKLM\\','') + '\FxProperties'
 381$null = [Microsoft.Win32.Registry]::LocalMachine.CreateSubKey($fxKeyPath)
 382$fxKey = [Microsoft.Win32.Registry]::LocalMachine.OpenSubKey($fxKeyPath,
 383    [Microsoft.Win32.RegistryKeyPermissionCheck]::ReadWriteSubTree,
 384    [System.Security.AccessControl.RegistryRights]::TakeOwnership -bor [System.Security.AccessControl.RegistryRights]::ChangePermissions)
 385if ($fxKey) {
 386    $acl = $fxKey.GetAccessControl()
 387    $admin = [System.Security.Principal.NTAccount]'BUILTIN\Administrators'
 388    $acl.SetOwner($admin)
 389    $fxKey.SetAccessControl($acl)
 390    $acl = $fxKey.GetAccessControl()
 391    $rule = New-Object System.Security.AccessControl.RegistryAccessRule($admin,'FullControl','ContainerInherit,ObjectInherit','None','Allow')
 392    $acl.AddAccessRule($rule)
 393    $fxKey.SetAccessControl($acl)
 394    $fxKey.Close()
 395}
 396
 397# Now write APO values
 398$fxKey2 = [Microsoft.Win32.Registry]::LocalMachine.OpenSubKey($fxKeyPath, $true)
 399if ($fxKey2) {
 400    $fxKey2.SetValue('{d04e05a6-594b-4fb6-a80d-01af5eed7d1d},5', '" + apoClsid + @"')
 401    $fxKey2.SetValue('{d04e05a6-594b-4fb6-a80d-01af5eed7d1d},6', '" + apoClsid + @"')
 402    $fxKey2.SetValue('{d3993a3f-99c2-4402-b5ec-a92a0367664b},5', [string[]]@('{9e90ea20-b493-4fd1-a1a8-7e1361a484fb}'), [Microsoft.Win32.RegistryValueKind]::MultiString)
 403    $fxKey2.SetValue('{d3993a3f-99c2-4402-b5ec-a92a0367664b},6', [string[]]@('{9e90ea20-b493-4fd1-a1a8-7e1361a484fb}'), [Microsoft.Win32.RegistryValueKind]::MultiString)
 404    $fxKey2.Close()
 405    Write-Output 'OK'
 406} else {
 407    Write-Output 'FAIL_OPEN'
 408}
 409";
 410                // Write PS script to temp file
 411                string psFile = Path.Combine(Path.GetTempPath(), "wc_apo_install.ps1");
 412                string logFile = Path.Combine(Path.GetTempPath(), "wc_apo_result.txt");
 413                try { File.Delete(logFile); } catch { }
 414
 415                // Add result logging to script
 416                ps += "\r\n'DONE' | Out-File -FilePath '" + logFile.Replace("'", "''") + "' -Encoding UTF8\r\n";
 417                File.WriteAllText(psFile, ps, Encoding.UTF8);
 418
 419                // Method 1: Run as SYSTEM via schtasks (most reliable)
 420                string taskName = "WC_APO_Install";
 421                string schtasksCreate = string.Format(
 422                    "/Create /TN \"{0}\" /TR \"powershell.exe -NoProfile -ExecutionPolicy Bypass -File '{1}'\" /SC ONCE /ST 00:00 /RU SYSTEM /RL HIGHEST /F",
 423                    taskName, psFile);
 424                string schtasksRun = string.Format("/Run /TN \"{0}\"", taskName);
 425                string schtasksDelete = string.Format("/Delete /TN \"{0}\" /F", taskName);
 426
 427                RunCmd("schtasks.exe", schtasksCreate);
 428                RunCmd("schtasks.exe", schtasksRun);
 429
 430                // Wait for result
 431                for (int wait = 0; wait < 50; wait++) // max 5 sec
 432                {
 433                    Thread.Sleep(100);
 434                    if (File.Exists(logFile)) break;
 435                }
 436
 437                RunCmd("schtasks.exe", schtasksDelete);
 438
 439                string output = File.Exists(logFile) ? File.ReadAllText(logFile).Trim() : "TIMEOUT";
 440                SLog("  SYSTEM task result: " + output);
 441                try { File.Delete(psFile); } catch { }
 442                try { File.Delete(logFile); } catch { }
 443            }
 444            catch (Exception ex) { SLog("  WriteFxProperties PS FAILED: " + ex.Message); }
 445        }
 446
 447        private void RunDiagnostics()
 448        {
 449            // Step 0: DLL exists in System32
 450            diagSteps[0] = "DLL in System32";
 451            diagStatus[0] = File.Exists(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.System), APO_DLL_NAME)) ? 1 : 2;
 452
 453            // Step 1: COM registered
 454            diagSteps[1] = "COM registered";
 455            try { using (var k = Microsoft.Win32.Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Classes\CLSID\" + APO_CLSID + @"\InprocServer32")) diagStatus[1] = (k != null) ? 1 : 2; } catch { diagStatus[1] = 2; }
 456
 457            // Step 2: AudioEngine registered
 458            diagSteps[2] = "AudioEngine APO";
 459            try { using (var k = Microsoft.Win32.Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Classes\AudioEngine\AudioProcessingObjects\" + APO_CLSID)) diagStatus[2] = (k != null) ? 1 : 2; } catch { diagStatus[2] = 2; }
 460
 461            // Step 3: Signing cert trusted
 462            diagSteps[3] = "Cert trusted";
 463            try { var sig = System.Security.Cryptography.X509Certificates.X509Certificate2.CreateFromSignedFile(
 464                Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.System), APO_DLL_NAME));
 465                diagStatus[3] = (sig != null) ? 1 : 2; } catch { diagStatus[3] = File.Exists(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.System), APO_DLL_NAME)) ? 0 : 2; }
 466
 467            // Step 4: FxProperties has our CLSID
 468            diagSteps[4] = "FxProperties bound";
 469            diagStatus[4] = 2;
 470            try {
 471                string basePath = @"SOFTWARE\Microsoft\Windows\CurrentVersion\MMDevices\Audio\Capture";
 472                using (var baseKey = Microsoft.Win32.Registry.LocalMachine.OpenSubKey(basePath))
 473                {
 474                    if (baseKey != null) foreach (string g in baseKey.GetSubKeyNames())
 475                    {
 476                        using (var fxKey = Microsoft.Win32.Registry.LocalMachine.OpenSubKey(basePath + "\\" + g + "\\FxProperties"))
 477                        {
 478                            if (fxKey == null) continue;
 479                            for (int slot = 1; slot <= 7; slot++)
 480                            {
 481                                object val = fxKey.GetValue("{d04e05a6-594b-4fb6-a80d-01af5eed7d1d}," + slot);
 482                                if (val != null && val.ToString() == APO_CLSID) { diagStatus[4] = 1; diagSteps[4] = "FxProperties slot " + slot + " (" + g.Substring(1, 8) + ")"; break; }
 483                            }
 484                        }
 485                        if (diagStatus[4] == 1) break;
 486                    }
 487                }
 488            } catch { }
 489
 490            // Step 5: DLL loaded in audiodg.exe
 491            diagSteps[5] = "Loaded in audiodg";
 492            diagStatus[5] = 2;
 493            try {
 494                var procs = System.Diagnostics.Process.GetProcessesByName("audiodg");
 495                foreach (var p in procs)
 496                    foreach (System.Diagnostics.ProcessModule m in p.Modules)
 497                        if (m.ModuleName.Equals(APO_DLL_NAME, StringComparison.OrdinalIgnoreCase)) { diagStatus[5] = 1; break; }
 498            } catch { diagStatus[5] = 0; }
 499
 500            // Step 6: Native log exists (APO was instantiated)
 501            diagSteps[6] = "APO instantiated";
 502            string nativeLog = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "WindowCapture", "apo_native.log");
 503            diagStatus[6] = File.Exists(nativeLog) ? 1 : 2;
 504
 505            // Step 7: Shared memory active
 506            diagSteps[7] = "Shared memory";
 507            diagStatus[7] = (shmAccessor != null) ? 1 : 2;
 508
 509            apoInstalled = (diagStatus[0] == 1 && diagStatus[1] == 1);
 510            if (!diagTimer.Enabled) diagTimer.Start();
 511            Invalidate();
 512        }
 513
 514        private static void SLog(string msg)
 515        { try { string dir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "WindowCapture"); if (!Directory.Exists(dir)) Directory.CreateDirectory(dir); File.AppendAllText(Path.Combine(dir, "soundpad.log"), DateTime.Now.ToString("HH:mm:ss.fff") + "  " + msg + "\r\n"); } catch { } }
 516
 517        private static string GetShortPath(string p) { var sb = new StringBuilder(300); return GetShortPathName(p, sb, sb.Capacity) > 0 ? sb.ToString() : p; }
 518
 519        public SoundpadForm()
 520        {
 521            var screen = Screen.PrimaryScreen.WorkingArea;
 522            FormBorderStyle = FormBorderStyle.None; ShowInTaskbar = false; TopMost = true;
 523            StartPosition = FormStartPosition.Manual;
 524            BackColor = Color.FromArgb(Settings.BlurTintColor.R, Settings.BlurTintColor.G, Settings.BlurTintColor.B);
 525            DoubleBuffered = true;
 526            SetStyle(ControlStyles.OptimizedDoubleBuffer | ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint, true);
 527            Width = FormW; Height = screen.Height;
 528            Location = new Point(screen.Right, screen.Top);
 529            AllowDrop = true;
 530
 531            soundsDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Sounds");
 532            if (!Directory.Exists(soundsDir)) Directory.CreateDirectory(soundsDir);
 533
 534            slideProgress = 0; slideTarget = 1;
 535            animTimer = new System.Windows.Forms.Timer { Interval = 16 };
 536            animTimer.Tick += AnimTimer_Tick;
 537
 538            Load += (s, e) => { BlurHelper.Apply(Handle); WinApi.TryEnableRoundedCorners(Handle); EnumDevices(); InitSharedMemory(); ReloadSounds(); RunDiagnostics(); };
 539            diagTimer = new System.Windows.Forms.Timer { Interval = 2000 };
 540            diagTimer.Tick += (s2, e2) => { RunDiagnostics(); Invalidate(); };
 541            Shown += (s, e) => { animTimer.Start(); Focus(); };
 542            MouseWheel += (s, e) => {
 543                if ((Control.ModifierKeys & Keys.Control) != 0)
 544                    masterVolume = Math.Max(0f, Math.Min(1f, masterVolume + (e.Delta > 0 ? 0.05f : -0.05f)));
 545                else
 546                { int mx = Math.Max(0, GetGridHeight() - (Height - BottomH - TopBarH)); scrollTarget = Math.Max(0, Math.Min(mx, scrollTarget - e.Delta)); }
 547                Invalidate();
 548            };
 549            DragEnter += (s, e) => { if (e.Data.GetDataPresent(DataFormats.FileDrop)) e.Effect = DragDropEffects.Copy; };
 550            DragDrop += OnDragDrop;
 551        }
 552
 553        private int GetGridHeight() { int cols = Math.Max(1, (Width - 16) / (PadSize + PadGap)); return ((pads.Count + cols - 1) / cols) * (PadSize + PadGap) + 10; }
 554
 555        // ===== Shared Memory =====
 556        private void InitSharedMemory()
 557        {
 558            try
 559            {
 560                // Create shared memory with security that allows audiodg.exe (LocalService) to access
 561                var security = new MemoryMappedFileSecurity();
 562                security.AddAccessRule(new System.Security.AccessControl.AccessRule<MemoryMappedFileRights>(
 563                    new System.Security.Principal.SecurityIdentifier(System.Security.Principal.WellKnownSidType.WorldSid, null),
 564                    MemoryMappedFileRights.ReadWrite, System.Security.AccessControl.AccessControlType.Allow));
 565                mmf = MemoryMappedFile.CreateOrOpen("Global\\WC_Soundpad_Buf", (SHM_HEADER + SHM_MAX_SAMPLES) * 4,
 566                    MemoryMappedFileAccess.ReadWrite, MemoryMappedFileOptions.None, security, HandleInheritability.None);
 567                shmAccessor = mmf.CreateViewAccessor();
 568                shmAccessor.Write(0, 0f); // volume = 0 (stopped)
 569                SLog("Shared memory created OK");
 570            }
 571            catch (Exception ex) { SLog("SharedMem FAILED: " + ex.Message); }
 572        }
 573
 574        private void WriteShmAudio(float[] pcmData, int channels, int sampleRate)
 575        {
 576            if (shmAccessor == null) return;
 577            int totalSamples = Math.Min(pcmData.Length, SHM_MAX_SAMPLES);
 578            shmAccessor.Write(0, masterVolume);            // [0] volume
 579            shmAccessor.Write(4, 0f);                      // [1] readPos
 580            shmAccessor.Write(8, (float)totalSamples);     // [2] totalSamples
 581            shmAccessor.Write(12, (float)channels);        // [3] channels
 582            shmAccessor.Write(16, (float)sampleRate);      // [4] sampleRate
 583            shmAccessor.WriteArray(SHM_HEADER * 4, pcmData, 0, totalSamples);
 584            SLog("SHM written: " + totalSamples + " samples, ch=" + channels + " sr=" + sampleRate);
 585        }
 586
 587        private void StopShm() { if (shmAccessor != null) shmAccessor.Write(0, 0f); }
 588
 589        // ===== APO Management =====
 590        private void CheckApoInstalled()
 591        {
 592            try
 593            {
 594                bool comOk = false, fxOk = false;
 595                using (var key = Microsoft.Win32.Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Classes\CLSID\" + APO_CLSID + @"\InprocServer32"))
 596                    comOk = (key != null);
 597
 598                // Check if bound to any capture device
 599                string basePath = @"SOFTWARE\Microsoft\Windows\CurrentVersion\MMDevices\Audio\Capture";
 600                using (var baseKey = Microsoft.Win32.Registry.LocalMachine.OpenSubKey(basePath))
 601                {
 602                    if (baseKey != null) foreach (string g in baseKey.GetSubKeyNames())
 603                    {
 604                        using (var fxKey = Microsoft.Win32.Registry.LocalMachine.OpenSubKey(basePath + "\\" + g + "\\FxProperties"))
 605                        {
 606                            if (fxKey != null)
 607                            {
 608                                object val = fxKey.GetValue("{d04e05a6-594b-4fb6-a80d-01af5eed7d1d},1");
 609                                if (val == null) val = fxKey.GetValue("{d04e05a6-594b-4fb6-a80d-01af5eed7d1d},5");
 610                                if (val != null && val.ToString() == APO_CLSID) fxOk = true;
 611                            }
 612                        }
 613                        if (fxOk) break;
 614                    }
 615                }
 616
 617                apoInstalled = comOk && fxOk;
 618            }
 619            catch { apoInstalled = false; }
 620            SLog("APO check: installed=" + apoInstalled);
 621        }
 622
 623        private void InstallApo()
 624        {
 625            EnsureApoInstalled();
 626            CheckApoInstalled();
 627            Invalidate();
 628        }
 629
 630        private void UninstallApo()
 631        {
 632            try
 633            {
 634                // Remove FxProperties entries from all capture devices
 635                string basePath = @"SOFTWARE\Microsoft\Windows\CurrentVersion\MMDevices\Audio\Capture";
 636                using (var baseKey = Microsoft.Win32.Registry.LocalMachine.OpenSubKey(basePath))
 637                {
 638                    if (baseKey != null) foreach (string g in baseKey.GetSubKeyNames())
 639                    {
 640                        try
 641                        {
 642                            using (var fxKey = Microsoft.Win32.Registry.LocalMachine.OpenSubKey(basePath + "\\" + g + "\\FxProperties", true))
 643                            {
 644                                if (fxKey != null)
 645                                {
 646                                    fxKey.DeleteValue("{d04e05a6-594b-4fb6-a80d-01af5eed7d1d},5", false);
 647                                    fxKey.DeleteValue("{d04e05a6-594b-4fb6-a80d-01af5eed7d1d},6", false);
 648                                    fxKey.DeleteValue("{d3993a3f-99c2-4402-b5ec-a92a0367664b},5", false);
 649                                    fxKey.DeleteValue("{d3993a3f-99c2-4402-b5ec-a92a0367664b},6", false);
 650                                }
 651                            }
 652                        }
 653                        catch { }
 654                    }
 655                }
 656
 657                // Remove COM and AudioEngine registration
 658                try { Microsoft.Win32.Registry.LocalMachine.DeleteSubKeyTree(@"SOFTWARE\Classes\CLSID\" + APO_CLSID, false); } catch { }
 659                try { Microsoft.Win32.Registry.LocalMachine.DeleteSubKeyTree(@"SOFTWARE\Classes\AudioEngine\AudioProcessingObjects\" + APO_CLSID, false); } catch { }
 660
 661                // Delete DLL
 662                string dstDll = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.System), APO_DLL_NAME);
 663                try { File.Delete(dstDll); } catch { }
 664
 665                SLog("APO uninstalled");
 666                CheckApoInstalled(); Invalidate();
 667            }
 668            catch (Exception ex) { SLog("UninstallAPO EXCEPTION: " + ex.Message); }
 669        }
 670
 671        // ===== Devices =====
 672        private void EnumDevices()
 673        {
 674            deviceNames = new List<string>();
 675            // Input (microphone) devices
 676            try { int c = waveInGetNumDevs(); for (int i = 0; i < c && i < 10; i++) { var caps = new WAVEOUTCAPS(); if (waveInGetDevCaps((IntPtr)i, ref caps, Marshal.SizeOf(caps)) == 0) deviceNames.Add("\uD83C\uDFA4 " + caps.szPname); } } catch { }
 677            // Output (speaker) devices
 678            try { int c = waveOutGetNumDevs(); for (int i = 0; i < c && i < 10; i++) { var caps = new WAVEOUTCAPS(); if (waveOutGetDevCaps((IntPtr)i, ref caps, Marshal.SizeOf(caps)) == 0) deviceNames.Add("\uD83D\uDD0A " + caps.szPname); } } catch { }
 679            if (deviceNames.Count == 0) deviceNames.Add("Default");
 680            SLog("Devices: " + string.Join(", ", deviceNames.ToArray()));
 681        }
 682
 683        // ===== GDI =====
 684        private void InitGdi()
 685        {
 686            if (fntTitle != null) return;
 687            fntTitle = new Font("Segoe UI Semibold", 12f); fntPad = new Font("Segoe UI Semibold", 9f);
 688            fntSmall = new Font("Segoe UI", 7.5f); fntStatus = new Font("Segoe UI", 9f);
 689            Color ac = Color.FromArgb(70, 130, 200);
 690            brText = new SolidBrush(Color.FromArgb(210, 218, 235)); brDim = new SolidBrush(Color.FromArgb(100, 110, 130));
 691            brAccent = new SolidBrush(ac); brPadBg = new SolidBrush(Color.FromArgb(18, 255, 255, 255));
 692            brPadHover = new SolidBrush(Color.FromArgb(35, 255, 255, 255)); brBarBg = new SolidBrush(Color.FromArgb(35, 0, 0, 0));
 693            brProgress = new SolidBrush(Color.FromArgb(60, ac)); penEdge = new Pen(Color.FromArgb(25, ac)); penBorder = new Pen(Color.FromArgb(15, 255, 255, 255));
 694        }
 695
 696        private static Bitmap GetNoise(int i) { if (i <= 0) return null; if (noiseTexture != null && noiseCachedI == i) return noiseTexture; if (noiseTexture != null) noiseTexture.Dispose(); const int sz = 128; var bmp = new Bitmap(sz, sz, PixelFormat.Format32bppArgb); var rng = new Random(42); int aMin = Math.Max(1, i / 2), aMax = Math.Max(aMin + 1, i); var bits = bmp.LockBits(new Rectangle(0, 0, sz, sz), ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb); unsafe { byte* p = (byte*)bits.Scan0; for (int j = 0; j < sz * sz; j++) { byte v = (byte)rng.Next(160, 255); byte a = (byte)rng.Next(aMin, aMax + 1); p[0]=v;p[1]=v;p[2]=v;p[3]=a; p+=4; } } bmp.UnlockBits(bits); noiseTexture = bmp; noiseCachedI = i; return bmp; }
 697
 698        // ===== Sounds =====
 699        private void ReloadSounds()
 700        {
 701            pads.Clear();
 702            string[] exts = { "*.mp3", "*.wav", "*.ogg", "*.flac", "*.m4a" };
 703            var files = new List<string>(); foreach (var ext in exts) files.AddRange(Directory.GetFiles(soundsDir, ext)); files.Sort(StringComparer.OrdinalIgnoreCase);
 704            Color[] colors = { Color.FromArgb(70,130,200), Color.FromArgb(80,180,80), Color.FromArgb(200,160,60), Color.FromArgb(180,100,200), Color.FromArgb(200,80,80), Color.FromArgb(80,200,200), Color.FromArgb(200,120,60), Color.FromArgb(120,200,120) };
 705            for (int i = 0; i < files.Count; i++)
 706                pads.Add(new SoundPad { FilePath = files[i], Name = Path.GetFileNameWithoutExtension(files[i]), Hotkey = i < 9 ? (i+1).ToString() : (i < 12 ? "F"+(i-8) : ""), Accent = colors[i % colors.Length] });
 707            pads.Add(new SoundPad { Name = "+", IsEmpty = true, Accent = Color.FromArgb(60, 70, 80) });
 708            SLog("Loaded " + (pads.Count - 1) + " sounds from " + soundsDir);
 709            Invalidate();
 710        }
 711
 712        private void AddSoundFile()
 713        {
 714            using (var ofd = new OpenFileDialog { Title = "Add Sound", Filter = "Audio|*.mp3;*.wav;*.ogg;*.flac;*.m4a|All|*.*", Multiselect = true })
 715                if (ofd.ShowDialog() == DialogResult.OK) { foreach (var f in ofd.FileNames) try { File.Copy(f, Path.Combine(soundsDir, Path.GetFileName(f)), false); } catch { } ReloadSounds(); }
 716        }
 717
 718        private void OnDragDrop(object s, DragEventArgs e)
 719        {
 720            var files = e.Data.GetData(DataFormats.FileDrop) as string[]; if (files == null) return;
 721            foreach (var f in files) { string ext = Path.GetExtension(f).ToLowerInvariant(); if (ext == ".mp3" || ext == ".wav" || ext == ".ogg" || ext == ".flac" || ext == ".m4a") try { File.Copy(f, Path.Combine(soundsDir, Path.GetFileName(f)), false); } catch { } }
 722            ReloadSounds();
 723        }
 724
 725        // ===== Slide =====
 726        public void SlideIn() { slideTarget = 1; animTimer.Start(); }
 727        public void SlideOut() { slideTarget = 0; animTimer.Start(); }
 728
 729        private void AnimTimer_Tick(object s, EventArgs ev)
 730        {
 731            bool nr = false;
 732            float d = slideTarget - slideProgress;
 733            if (Math.Abs(d) > 0.005f) { slideProgress += d * 0.18f; if (Math.Abs(slideTarget - slideProgress) < 0.005f) slideProgress = slideTarget; if (slideTarget <= 0 && slideProgress <= 0) { animTimer.Stop(); Hide(); return; } var scr = Screen.PrimaryScreen.WorkingArea; float eased = slideProgress * slideProgress * (3f - 2f * slideProgress); Left = scr.Right - (int)(FormW * eased); nr = true; }
 734            float sd = scrollTarget - scrollOffset; if (Math.Abs(sd) > 0.5f) { scrollOffset += sd * 0.22f; nr = true; } else if (Math.Abs(sd) > 0.01f) { scrollOffset = scrollTarget; nr = true; }
 735            if (playingPad >= 0) { playingAnim += 0.12f; nr = true; UpdatePlayProgress(); }
 736            if (nr) Invalidate();
 737        }
 738
 739        // ===== Playback =====
 740        private volatile bool stopRequested;
 741        // Monotonic generation: each PlaySound bumps it so a stale decode thread
 742        // (from a previously-pressed pad) won't clobber the current clip's shared-memory buffer.
 743        private volatile int playGeneration;
 744
 745        private void PlaySound(int idx)
 746        {
 747            if (idx < 0 || idx >= pads.Count || pads[idx].IsEmpty) return;
 748            StopAll();
 749            var pad = pads[idx];
 750            if (!File.Exists(pad.FilePath)) return;
 751
 752            playingPad = idx; playingAnim = 0; playProgress = 0; playingName = pad.Name; stopRequested = false;
 753            int myGen = ++playGeneration;
 754            SLog("PlaySound: " + pad.Name + " file=" + pad.FilePath);
 755
 756            // Play via MCI (to speakers) + write to shared memory (for APO mic injection)
 757            string shortPath = GetShortPath(pad.FilePath);
 758            string alias = "spad";
 759            WinApi.mciSendString("close " + alias, null, 0, IntPtr.Zero);
 760            string cmd = "open \"" + shortPath + "\" type MPEGVideo alias " + alias;
 761            int err = WinApi.mciSendString(cmd, null, 0, IntPtr.Zero);
 762            if (err != 0) { cmd = "open \"" + shortPath + "\" alias " + alias; err = WinApi.mciSendString(cmd, null, 0, IntPtr.Zero); }
 763            SLog("  MCI open err=" + err);
 764            if (err != 0) return;
 765
 766            WinApi.mciSendString("set " + alias + " time format milliseconds", null, 0, IntPtr.Zero);
 767            WinApi.mciSendString("setaudio " + alias + " volume to " + (int)(masterVolume * 1000), null, 0, IntPtr.Zero);
 768            err = WinApi.mciSendString("play " + alias, null, 0, IntPtr.Zero);
 769            SLog("  MCI play err=" + err);
 770
 771            // Also decode to float and write to shared memory for APO
 772            if (shmAccessor != null)
 773            {
 774                new Thread(() => {
 775                    try { DecodeAndWriteShm(pad.FilePath, myGen); } catch (Exception ex) { SLog("SHM decode error: " + ex.Message); }
 776                }) { IsBackground = true }.Start();
 777            }
 778
 779            Invalidate();
 780        }
 781
 782        private void DecodeAndWriteShm(string filePath, int myGen)
 783        {
 784            // Decode via MF to float32 PCM, write to shared memory
 785            try
 786            {
 787                WinApi.CoInitializeEx(IntPtr.Zero, WinApi.COINIT_MULTITHREADED);
 788                WinApi.MFStartup(WinApi.MF_VERSION, 0);
 789
 790                IMFSourceReader reader;
 791                int hr = WinApi.MFCreateSourceReaderFromURL_IntPtr(filePath, IntPtr.Zero, out reader);
 792                if (hr != 0) { SLog("MF SourceReader failed: 0x" + hr.ToString("X8")); return; }
 793
 794                IMFMediaType mt; WinApi.MFCreateMediaType(out mt);
 795                var keyMajor = WinApi.MF_MT_MAJOR_TYPE; var valAudio = WinApi.MFMediaType_Audio;
 796                mt.SetGUID(ref keyMajor, ref valAudio);
 797                var keySub = WinApi.MF_MT_SUBTYPE; var valFloat = new Guid("00000003-0000-0010-8000-00AA00389B71"); // MFAudioFormat_Float
 798                mt.SetGUID(ref keySub, ref valFloat);
 799                var keyCh = WinApi.MF_MT_AUDIO_NUM_CHANNELS; mt.SetUINT32(ref keyCh, 1); // mono for mic
 800                var keySR = WinApi.MF_MT_AUDIO_SAMPLES_PER_SECOND; mt.SetUINT32(ref keySR, 48000);
 801                var keyBPS = WinApi.MF_MT_AUDIO_BITS_PER_SAMPLE; mt.SetUINT32(ref keyBPS, 32);
 802                var keyBA = WinApi.MF_MT_AUDIO_BLOCK_ALIGNMENT; mt.SetUINT32(ref keyBA, 4);
 803                var keyABPS = WinApi.MF_MT_AUDIO_AVG_BYTES_PER_SECOND; mt.SetUINT32(ref keyABPS, 48000 * 4);
 804
 805                reader.SetStreamSelection(WinApi.MF_SOURCE_READER_FIRST_AUDIO_STREAM, true);
 806                hr = reader.SetCurrentMediaType(WinApi.MF_SOURCE_READER_FIRST_AUDIO_STREAM, IntPtr.Zero, mt);
 807                if (hr != 0) { SLog("MF SetMediaType failed"); Marshal.ReleaseComObject(reader); return; }
 808
 809                var samples = new List<float>(48000 * 30); // up to 30 sec
 810                while (!stopRequested && myGen == playGeneration)
 811                {
 812                    uint actual; int flags; long ts; IMFSample sample;
 813                    int rhr = reader.ReadSample(WinApi.MF_SOURCE_READER_FIRST_AUDIO_STREAM, 0, out actual, out flags, out ts, out sample);
 814                    if (rhr != 0 || (flags & WinApi.MF_SOURCE_READERF_ENDOFSTREAM) != 0) break;
 815                    if (sample == null) continue;
 816                    IMFMediaBuffer buf; sample.ConvertToContiguousBuffer(out buf);
 817                    if (buf != null) { IntPtr pData; int max, cur; buf.Lock(out pData, out max, out cur); int nFloats = cur / 4; for (int i = 0; i < nFloats && samples.Count < SHM_MAX_SAMPLES; i++) samples.Add(Marshal.PtrToStructure<float>(pData + i * 4)); buf.Unlock(); Marshal.ReleaseComObject(buf); }
 818                    Marshal.ReleaseComObject(sample);
 819                }
 820                Marshal.ReleaseComObject(reader);
 821                WinApi.MFShutdown(); WinApi.CoUninitialize();
 822
 823                // Only publish to shared memory if this is still the active clip
 824                // (a newer pad press bumps playGeneration and supersedes this decode).
 825                if (samples.Count > 0 && !stopRequested && myGen == playGeneration)
 826                    WriteShmAudio(samples.ToArray(), 1, 48000);
 827                SLog("MF decode done: " + samples.Count + " float samples");
 828            }
 829            catch (Exception ex) { SLog("DecodeShm EXCEPTION: " + ex.Message); }
 830        }
 831
 832        private void UpdatePlayProgress()
 833        {
 834            var sb = new StringBuilder(64);
 835            WinApi.mciSendString("status spad length", sb, 64, IntPtr.Zero);
 836            int totalMs; int.TryParse(sb.ToString().Trim(), out totalMs);
 837            sb.Clear(); WinApi.mciSendString("status spad position", sb, 64, IntPtr.Zero);
 838            int posMs; int.TryParse(sb.ToString().Trim(), out posMs);
 839            if (totalMs > 0) playProgress = (float)posMs / totalMs;
 840            if (totalMs > 0 && posMs >= totalMs - 100) { playingPad = -1; playProgress = 0; playingName = ""; StopShm(); }
 841        }
 842
 843        private void StopAll()
 844        {
 845            stopRequested = true;
 846            WinApi.mciSendString("close spad", null, 0, IntPtr.Zero);
 847            StopShm();
 848            playingPad = -1; playProgress = 0; playingName = "";
 849            Invalidate();
 850        }
 851
 852        // ===== Paint =====
 853        protected override void OnPaintBackground(PaintEventArgs e) { }
 854        protected override void OnPaint(PaintEventArgs e)
 855        {
 856            InitGdi();
 857            var g = e.Graphics; g.SmoothingMode = SmoothingMode.HighSpeed; g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.ClearTypeGridFit;
 858            int w = Width, h = Height;
 859            g.Clear(Color.FromArgb(Settings.BlurTintAlpha, Settings.BlurTintColor));
 860            bool sliding = Math.Abs(slideTarget - slideProgress) > 0.01f;
 861            if (!sliding) { int ni = Settings.NoiseIntensity; if (ni > 0) { var ns = GetNoise(ni); if (ns != null) using (var tb = new TextureBrush(ns, WrapMode.Tile)) g.FillRectangle(tb, 0, 0, w, h); } }
 862            g.DrawLine(penEdge, 0, 0, 0, h);
 863
 864            // Top bar
 865            g.FillRectangle(brBarBg, 0, 0, w, TopBarH);
 866            g.DrawString("SOUNDPAD", fntTitle, brText, 14, 8);
 867            g.DrawString("Vol:" + (int)(masterVolume * 100) + "%", fntSmall, brDim, w - 70, 14);
 868
 869            // APO status
 870            g.DrawString(apoInstalled ? "\u2713 APO" : "\u2717 APO", fntSmall, apoInstalled ? brAccent : brDim, w - 140, 14);
 871
 872            // Pad grid
 873            int gridY = TopBarH + 4, gridH = h - BottomH - gridY;
 874            int cols = Math.Max(1, (w - 16) / (PadSize + PadGap));
 875            int startX = (w - cols * (PadSize + PadGap) + PadGap) / 2;
 876            int iS = (int)scrollOffset;
 877
 878            g.SetClip(new Rectangle(0, gridY, w, gridH));
 879            for (int i = 0; i < pads.Count; i++)
 880            {
 881                var pad = pads[i]; int col = i % cols, row = i / cols;
 882                int px = startX + col * (PadSize + PadGap), py = gridY + row * (PadSize + PadGap) - iS;
 883                if (py + PadSize < gridY || py > gridY + gridH) continue;
 884                bool isHov = (hoverPad == i), isPlay = (playingPad == i);
 885
 886                if (isPlay) { float pulse = 0.6f + 0.4f * (float)Math.Sin(playingAnim); using (var br = new SolidBrush(Color.FromArgb((int)(pulse * 45), pad.Accent))) g.FillRectangle(br, px, py, PadSize, PadSize); g.FillRectangle(brProgress, px, py + PadSize - 4, (int)(PadSize * playProgress), 4); }
 887                else if (isHov) g.FillRectangle(brPadHover, px, py, PadSize, PadSize);
 888                else g.FillRectangle(brPadBg, px, py, PadSize, PadSize);
 889                using (var br = new SolidBrush(Color.FromArgb(isPlay ? 200 : (isHov ? 100 : 40), pad.Accent))) g.FillRectangle(br, px, py, 3, PadSize);
 890                g.DrawRectangle(isHov ? penEdge : penBorder, px, py, PadSize, PadSize);
 891
 892                if (pad.IsEmpty) { g.DrawString("+", fntTitle, brDim, px + PadSize / 2 - 8, py + PadSize / 2 - 12); }
 893                else
 894                {
 895                    string name = pad.Name; if (name.Length > 18) name = name.Substring(0, 17) + "..";
 896                    g.DrawString(name, fntPad, isPlay ? brAccent : brText, px + 6, py + PadSize / 2 - 8);
 897                    if (!string.IsNullOrEmpty(pad.Hotkey)) { using (var br = new SolidBrush(Color.FromArgb(25, pad.Accent))) g.FillRectangle(br, px + PadSize - 24, py + 2, 22, 14); g.DrawString(pad.Hotkey, fntSmall, brDim, px + PadSize - 22, py + 2); }
 898                    g.DrawString(Path.GetExtension(pad.FilePath ?? ""), fntSmall, brDim, px + 6, py + PadSize - 14);
 899                }
 900            }
 901            g.ResetClip();
 902
 903            // Bottom
 904            int botY = h - BottomH;
 905            g.FillRectangle(brBarBg, 0, botY, w, BottomH); g.DrawLine(penEdge, 0, botY, w, botY);
 906
 907            // Playback status
 908            if (playingPad >= 0) {
 909                g.DrawString("\u25B6 " + playingName, fntStatus, brAccent, 14, botY + 4);
 910                g.FillRectangle(brDim, 14, botY + 22, w - 28, 3); g.FillRectangle(brAccent, 14, botY + 22, (int)((w - 28) * playProgress), 3);
 911                g.DrawString("\u25A0 Stop", fntSmall, brText, w - 50, botY + 6);
 912            } else {
 913                g.DrawString("Ready — " + (pads.Count - 1) + " sounds", fntStatus, brDim, 14, botY + 4);
 914            }
 915
 916            // ===== APO Diagnostics =====
 917            int diagY = botY + 30;
 918            g.DrawString("APO Pipeline Status", fntSmall, brText, 14, diagY);
 919            diagY += 14;
 920
 921            Color okColor = Color.FromArgb(80, 200, 80);
 922            Color failColor = Color.FromArgb(200, 80, 80);
 923            Color pendColor = Color.FromArgb(160, 160, 80);
 924
 925            int barW = w - 28;
 926            int okCount = 0;
 927            for (int i = 0; i < 8; i++)
 928            {
 929                if (diagSteps[i] == null) continue;
 930                int dy = diagY + i * 16;
 931                Color sc = diagStatus[i] == 1 ? okColor : (diagStatus[i] == 2 ? failColor : pendColor);
 932                string icon = diagStatus[i] == 1 ? "\u2713" : (diagStatus[i] == 2 ? "\u2717" : "\u25CB");
 933                using (var br = new SolidBrush(sc)) g.DrawString(icon + " " + diagSteps[i], fntSmall, br, 14, dy);
 934                if (diagStatus[i] == 1) okCount++;
 935            }
 936
 937            // Overall progress bar
 938            int totalSteps = 8;
 939            float progress = (float)okCount / totalSteps;
 940            int pbY = diagY + 8 * 16 + 4;
 941            g.FillRectangle(brDim, 14, pbY, barW, 4);
 942            using (var br = new SolidBrush(progress >= 0.9f ? okColor : (progress >= 0.5f ? pendColor : failColor)))
 943                g.FillRectangle(br, 14, pbY, (int)(barW * progress), 4);
 944            g.DrawString((int)(progress * 100) + "%", fntSmall, brDim, w - 40, pbY - 10);
 945
 946            // Help
 947            g.DrawString("Ctrl+Scroll=Vol | Drop files | Esc=Close", fntSmall, brDim, 14, botY + BottomH - 14);
 948        }
 949
 950        // ===== Mouse =====
 951        protected override void OnMouseDown(MouseEventArgs e)
 952        {
 953            base.OnMouseDown(e); if (e.Button != MouseButtons.Left) return;
 954            int botY = Height - BottomH;
 955
 956            // Stop
 957            if (playingPad >= 0 && e.X >= Width - 110 && e.Y >= botY + 50 && e.Y < botY + 70) { StopAll(); return; }
 958            // Device click
 959            if (e.Y >= botY && e.Y < botY + 32) { currentDeviceIdx = (currentDeviceIdx + 1) % (deviceNames != null ? deviceNames.Count : 1); Invalidate(); return; }
 960            // APO install/uninstall
 961            if (e.Y >= botY + 32 && e.Y < botY + 48) { if (apoInstalled) UninstallApo(); else InstallApo(); Invalidate(); return; }
 962            // Pad
 963            int idx = HitTestPad(e.X, e.Y);
 964            if (idx >= 0 && idx < pads.Count) { if (pads[idx].IsEmpty) AddSoundFile(); else PlaySound(idx); }
 965        }
 966
 967        protected override void OnMouseMove(MouseEventArgs e) { base.OnMouseMove(e); int idx = HitTestPad(e.X, e.Y); if (idx != hoverPad) { hoverPad = idx; Invalidate(); } }
 968
 969        private int HitTestPad(int mx, int my)
 970        {
 971            int gridY = TopBarH + 4, gridH = Height - BottomH - gridY;
 972            if (my < gridY || my > gridY + gridH) return -1;
 973            int cols = Math.Max(1, (Width - 16) / (PadSize + PadGap));
 974            int startX = (Width - cols * (PadSize + PadGap) + PadGap) / 2; int iS = (int)scrollOffset;
 975            for (int i = 0; i < pads.Count; i++) { int col = i % cols, row = i / cols; int px = startX + col * (PadSize + PadGap), py = gridY + row * (PadSize + PadGap) - iS; if (mx >= px && mx < px + PadSize && my >= py && my < py + PadSize) return i; }
 976            return -1;
 977        }
 978
 979        protected override void OnKeyDown(KeyEventArgs e)
 980        {
 981            base.OnKeyDown(e);
 982            if (e.KeyCode == Keys.Escape) { StopAll(); SlideOut(); e.Handled = true; return; }
 983            if (e.KeyCode == Keys.Space) { StopAll(); e.Handled = true; return; }
 984            int num = -1;
 985            if (e.KeyCode >= Keys.D1 && e.KeyCode <= Keys.D9) num = (int)e.KeyCode - (int)Keys.D1;
 986            if (e.KeyCode >= Keys.NumPad1 && e.KeyCode <= Keys.NumPad9) num = (int)e.KeyCode - (int)Keys.NumPad1;
 987            if (num >= 0 && num < pads.Count) { PlaySound(num); e.Handled = true; }
 988            if (e.KeyCode >= Keys.F1 && e.KeyCode <= Keys.F3) { int idx = 9 + (int)e.KeyCode - (int)Keys.F1; if (idx < pads.Count) { PlaySound(idx); e.Handled = true; } }
 989        }
 990
 991        protected override void OnDeactivate(EventArgs e) { base.OnDeactivate(e); }
 992        protected override CreateParams CreateParams { get { var cp = base.CreateParams; cp.Style |= unchecked((int)0x80000000); return cp; } }
 993
 994        protected override void Dispose(bool disposing)
 995        {
 996            if (disposing) { stopRequested = true; playGeneration++; if (animTimer != null) { animTimer.Stop(); animTimer.Dispose(); } if (diagTimer != null) { diagTimer.Stop(); diagTimer.Dispose(); } StopAll(); if (shmAccessor != null) shmAccessor.Dispose(); if (mmf != null) mmf.Dispose(); }
 997            base.Dispose(disposing);
 998        }
 999    }
1000}