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}