1using System; 2using System.Collections.Generic; 3using System.Drawing; 4using System.Drawing.Drawing2D; 5using System.Drawing.Imaging; 6using System.Drawing.Text; 7using System.IO; 8using System.Runtime.InteropServices; 9using System.Text; 10using System.Threading; 11using System.Windows.Forms; 12using WindowCapture.Models; 13using WindowCapture.Native; 14 15namespace WindowCapture.UI 16{ 17 public class AudioPlayerForm : Form, IMessageFilter 18 { 19 // ===== Custom Fonts ===== 20 private static PrivateFontCollection _fonts; 21 private static FontFamily _bebasFont; 22 23 private static void InitFonts() 24 { 25 if (_fonts != null) return; 26 try 27 { 28 _fonts = new PrivateFontCollection(); 29 30 var assembly = typeof(AudioPlayerForm).Assembly; 31 string resourceName = "WindowCapture.Bebas_Neue_Bold.ttf"; 32 33 foreach (string name in assembly.GetManifestResourceNames()) 34 { 35 if (name.EndsWith("Bebas Neue Bold.ttf", StringComparison.OrdinalIgnoreCase)) 36 { 37 resourceName = name; 38 break; 39 } 40 } 41 42 using (Stream fontStream = assembly.GetManifestResourceStream(resourceName)) 43 { 44 if (fontStream != null) 45 { 46 byte[] fontData = new byte[fontStream.Length]; 47 fontStream.Read(fontData, 0, (int)fontStream.Length); 48 49 IntPtr data = Marshal.AllocCoTaskMem(fontData.Length); 50 Marshal.Copy(fontData, 0, data, fontData.Length); 51 _fonts.AddMemoryFont(data, fontData.Length); 52 Marshal.FreeCoTaskMem(data); 53 54 if (_fonts.Families.Length > 0) 55 _bebasFont = _fonts.Families[0]; 56 } 57 } 58 } 59 catch (Exception ex) { Log("InitFonts error: " + ex.Message); } 60 } 61 62 // ===== Debug log ===== 63 private static readonly string LogPath = Path.Combine( 64 Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), 65 "WindowCapture", "audioplayer.log"); 66 67 private static void Log(string msg) 68 { 69 try 70 { 71 string dir = Path.GetDirectoryName(LogPath); 72 if (!Directory.Exists(dir)) Directory.CreateDirectory(dir); 73 File.AppendAllText(LogPath, 74 DateTime.Now.ToString("HH:mm:ss.fff") + " " + msg + "\r\n"); 75 } 76 catch { } 77 } 78 79 // ===== Playback ===== 80 private string currentFilePath; 81 private string currentTrackTitle; 82 private string currentTrackArtist; 83 private string tempWavPath; 84 private string[] folderFiles; 85 private int currentFileIndex; 86 private bool isPlaying; 87 private double currentPos; 88 private double duration; 89 private float volume = Settings.AudioVolume; 90 private bool mciOpen; 91 private const string MciAlias = "aptrack"; 92 private static readonly string[] AudioExts = 93 { ".mp3", ".wav", ".flac", ".ogg", ".m4a", ".aac", ".opus", ".wma" }; 94 95 // ===== UI layout ===== 96 private Bitmap albumArt; 97 private Bitmap blurredStrip; 98 private Color accentColor = Color.FromArgb(0, 188, 212); 99 private int artH; 100 private const int StripH = 50; 101 102 // ===== Waveform ===== 103 private volatile float[] waveformData; 104 private float waveLoadAnim; 105 private float specAnim; 106 private float playStateFactor; 107 private double lastPollPos; 108 private DateTime lastPollTime; 109 private float smoothBass, smoothTreble; 110 private PointF[] specPointsTop; 111 private float trackSwitchAnim; 112 private float trackInfoAlpha = 1.0f; 113 private float hoverFadeFactor = 0.0f; 114 private float leftArrowAlpha = 0.0f; 115 private float rightArrowAlpha = 0.0f; 116 private float arrowPulseAnim = 0.0f; 117 private Color leftEdgeColor = Color.White; 118 private Color rightEdgeColor = Color.White; 119 private volatile int cancelToken; 120 121 // ===== Timers ===== 122 private System.Windows.Forms.Timer pollTimer; 123 private System.Windows.Forms.Timer paintTimer; 124 private System.Windows.Forms.Timer volFadeTimer; 125 126 // ===== Mouse gestures ===== 127 private bool lmbDown, rmbDown; 128 private int lmbStartX, rmbStartX; 129 private bool lmbDragging, rmbSwitched; 130 private double seekStartPos; 131 private bool closeBtnHover, minBtnHover, maxBtnHover; 132 private float winButtonsAlpha = 0.0f; 133 private DateTime lastMciSeekTime = DateTime.MinValue; 134 private DateTime lastSeekTime = DateTime.MinValue; 135 136 // ===== Volume indicator ===== 137 private float volIndicatorAlpha; 138 private DateTime volLastChange; 139 140 // ===== Window resize (right edge) ===== 141 private const int ResizeEdge = 8; 142 private bool resizing; 143 private int resizeStartX, resizeStartW; 144 145 // ===== WndProc constants ===== 146 private const int WM_NCHITTEST = 0x84; 147 private const int WM_NCACTIVATE = 0x86; 148 private const int WM_NCCALCSIZE = 0x83; 149 private const int WM_MOUSEWHEEL = 0x020A; 150 private const int HTCLIENT = 1; 151 private const int HTCAPTION = 2; 152 private const int HTRIGHT = 11; 153 154 [DllImport("winmm.dll")] 155 private static extern int waveOutSetVolume(IntPtr hwo, uint dwVolume); 156 157 // ===== Constructor ===== 158 public AudioPlayerForm(string filePath) 159 { 160 InitFonts(); 161 currentFilePath = Path.GetFullPath(filePath); 162 ScanFolder(currentFilePath); 163 SetupForm(); 164 LoadTrack(currentFilePath, true); 165 } 166 167 // ===== Form setup ===== 168 private void SetupForm() 169 { 170 FormBorderStyle = FormBorderStyle.None; 171 BackColor = Color.FromArgb(18, 18, 22); 172 DoubleBuffered = true; 173 SetStyle(ControlStyles.OptimizedDoubleBuffer | 174 ControlStyles.AllPaintingInWmPaint | 175 ControlStyles.UserPaint, true); 176 ShowInTaskbar = true; 177 Width = Math.Max(300, Settings.AudioPlayerW); 178 Height = StripH; 179 StartPosition = FormStartPosition.CenterScreen; 180 181 pollTimer = new System.Windows.Forms.Timer { Interval = 50 }; 182 paintTimer = new System.Windows.Forms.Timer { Interval = 16 }; 183 volFadeTimer = new System.Windows.Forms.Timer { Interval = 16 }; 184 pollTimer.Tick += PollTimer_Tick; 185 paintTimer.Tick += (s, e) => { 186 waveLoadAnim += 0.05f; 187 specAnim += 0.15f; 188 189 if (isPlaying) { if (playStateFactor < 1f) playStateFactor = Math.Min(1f, playStateFactor + 0.08f); } 190 else { if (playStateFactor > 0f) playStateFactor = Math.Max(0f, playStateFactor - 0.05f); } 191 192 if (trackSwitchAnim > 0) { 193 trackSwitchAnim -= 0.04f; 194 if (trackSwitchAnim < 0) trackSwitchAnim = 0; 195 } 196 197 Point cp = PointToClient(Cursor.Position); 198 bool inArea = cp.Y >= (Height - StripH) && cp.X >= 0 && cp.X <= Width && cp.Y <= Height; 199 if (inArea) { 200 if (hoverFadeFactor < 1.0f) { 201 hoverFadeFactor += 0.12f; 202 if (hoverFadeFactor > 1.0f) hoverFadeFactor = 1.0f; 203 } 204 } else { 205 if (hoverFadeFactor > 0.0f) { 206 hoverFadeFactor -= 0.08f; 207 if (hoverFadeFactor < 0.0f) hoverFadeFactor = 0.0f; 208 } 209 } 210 211 bool nearWinButtons = cp.X > Width * 0.6f && cp.Y < Height * 0.4f && ClientRectangle.Contains(cp); 212 if (nearWinButtons) { 213 if (winButtonsAlpha < 1.0f) winButtonsAlpha = Math.Min(1.0f, winButtonsAlpha + 0.1f); 214 } else { 215 if (winButtonsAlpha > 0.0f) winButtonsAlpha = Math.Max(0.0f, winButtonsAlpha - 0.07f); 216 } 217 218 int artAreaH = Height - StripH; 219 int arrowZoneW = 70; 220 bool nearLeft = cp.X >= 0 && cp.X <= arrowZoneW && cp.Y >= 0 && cp.Y < artAreaH && ClientRectangle.Contains(cp); 221 bool nearRight = cp.X >= Width - arrowZoneW && cp.X <= Width && cp.Y >= 0 && cp.Y < artAreaH && ClientRectangle.Contains(cp); 222 223 if (nearLeft) { if (leftArrowAlpha < 1.0f) leftArrowAlpha = Math.Min(1.0f, leftArrowAlpha + 0.1f); } 224 else { if (leftArrowAlpha > 0.0f) leftArrowAlpha = Math.Max(0.0f, leftArrowAlpha - 0.08f); } 225 226 if (nearRight) { if (rightArrowAlpha < 1.0f) rightArrowAlpha = Math.Min(1.0f, rightArrowAlpha + 0.1f); } 227 else { if (rightArrowAlpha > 0.0f) rightArrowAlpha = Math.Max(0.0f, rightArrowAlpha - 0.08f); } 228 229 arrowPulseAnim += 0.06f; 230 231 if (trackInfoAlpha < 1.0f) { 232 trackInfoAlpha += 0.05f; 233 if (trackInfoAlpha > 1.0f) trackInfoAlpha = 1.0f; 234 } 235 236 Invalidate(); 237 }; 238 volFadeTimer.Tick += VolFadeTimer_Tick; 239 240 Shown += (s, e) => { 241 WinApi.TryEnableRoundedCorners(Handle); 242 ApplyBlur(); 243 pollTimer.Start(); 244 paintTimer.Start(); 245 Application.AddMessageFilter(this); 246 }; 247 FormClosing += (s, e) => { 248 // Stop timers and cancel background work BEFORE tearing down MCI/handle, 249 // otherwise the 16ms paint timer or a decode callback fires on a disposed form. 250 cancelToken++; 251 if (pollTimer != null) { pollTimer.Stop(); pollTimer.Dispose(); } 252 if (paintTimer != null) { paintTimer.Stop(); paintTimer.Dispose(); } 253 if (volFadeTimer != null) { volFadeTimer.Stop(); volFadeTimer.Dispose(); } 254 Application.RemoveMessageFilter(this); 255 Settings.AudioPlayerW = Width; 256 Settings.Save(); 257 MciClose(); 258 DeleteTempWav(); 259 }; 260 } 261 262 public bool PreFilterMessage(ref Message m) 263 { 264 if (m.Msg == WM_MOUSEWHEEL) 265 { 266 if (IsDisposed) return false; 267 Point p = PointToClient(Cursor.Position); 268 if (ClientRectangle.Contains(p)) 269 { 270 int delta = (short)((m.WParam.ToInt64() >> 16) & 0xFFFF); 271 AdjustVol(delta > 0 ? 0.05f : -0.05f); 272 return true; 273 } 274 } 275 return false; 276 } 277 278 private void ApplyBlur() 279 { 280 Color tint = Settings.BlurTintColor; 281 int a = Settings.BlurTintAlpha; 282 int abgr = (a << 24) | (tint.B << 16) | (tint.G << 8) | tint.R; 283 if (Settings.BlurMode == 1) 284 WinApi.EnableAcrylicBlur(Handle, abgr); 285 else 286 WinApi.EnableBlurBehind(Handle, abgr); 287 } 288 289 // ===== Track loading ===== 290 private void LoadTrack(string path, bool autoPlay = true) 291 { 292 MciClose(); 293 DeleteTempWav(); 294 295 currentFilePath = Path.GetFullPath(path); 296 LoadMetadata(currentFilePath); 297 waveformData = null; 298 299 cancelToken++; 300 trackSwitchAnim = 1.0f; 301 Log("LoadTrack: " + currentFilePath); 302 303 if (albumArt != null) { albumArt.Dispose(); albumArt = null; } 304 if (blurredStrip != null) { blurredStrip.Dispose(); blurredStrip = null; } 305 306 albumArt = LoadAlbumArt(currentFilePath); 307 accentColor = GetDominantColor(albumArt); 308 309 if (albumArt != null) 310 { 311 leftEdgeColor = GetEdgeColor(albumArt, true); 312 rightEdgeColor = GetEdgeColor(albumArt, false); 313 } 314 else 315 { 316 leftEdgeColor = rightEdgeColor = Color.FromArgb(100, 100, 110); 317 } 318 319 if (albumArt != null) 320 { 321 int w = Width; 322 int h = ComputeArtHeight(); 323 int cropH = StripH + 20; 324 int cropY = Math.Max(0, albumArt.Height - (int)((float)cropH / h * albumArt.Height)); 325 Rectangle cropRect = new Rectangle(0, cropY, albumArt.Width, albumArt.Height - cropY); 326 327 blurredStrip = WindowCapture.Effects.ImageEffects.CopyRegion(albumArt, cropRect); 328 if (blurredStrip != null) 329 { 330 WindowCapture.Effects.ImageEffects.ApplyBlur(blurredStrip, 331 new Rectangle(0, 0, blurredStrip.Width, blurredStrip.Height), 15); 332 } 333 } 334 335 artH = albumArt != null ? ComputeArtHeight() : 0; 336 Height = Math.Max(StripH, artH); 337 338 currentFileIndex = Array.FindIndex(folderFiles, f => 339 string.Equals(f, currentFilePath, StringComparison.OrdinalIgnoreCase)); 340 if (currentFileIndex < 0) currentFileIndex = 0; 341 342 string cmd = string.Format("open \"{0}\" alias {1}", currentFilePath, MciAlias); 343 int err = WinApi.mciSendString(cmd, null, 0, IntPtr.Zero); 344 Log("MCI auto open err=" + err); 345 if (err != 0) 346 { 347 cmd = string.Format("open \"{0}\" type MPEGVideo alias {1}", currentFilePath, MciAlias); 348 err = WinApi.mciSendString(cmd, null, 0, IntPtr.Zero); 349 Log("MCI MPEGVideo open err=" + err); 350 } 351 mciOpen = (err == 0); 352 353 if (mciOpen) 354 { 355 WinApi.mciSendString("set " + MciAlias + " time format milliseconds", null, 0, IntPtr.Zero); 356 duration = MciGetLength(); 357 MciSetVolume(volume); 358 currentPos = 0; 359 isPlaying = false; 360 Log("MCI opened OK, duration=" + duration + "s, autoPlay=" + autoPlay); 361 if (autoPlay) MciPlay(); 362 StartWaveformDecode(currentFilePath); 363 } 364 else 365 { 366 Log("MCI failed for both types, trying MF decode to WAV..."); 367 string src = currentFilePath; 368 bool ap = autoPlay; 369 int tok = cancelToken; 370 var t = new Thread(() => { 371 try 372 { 373 WinApi.CoInitializeEx(IntPtr.Zero, WinApi.COINIT_MULTITHREADED); 374 try 375 { 376 string wav = DecodeToPcmWav(src); 377 Log("DecodeToPcmWav returned: " + (wav ?? "null") + 378 (tok != cancelToken ? " (cancelled)" : "")); 379 if (wav != null && tok == cancelToken) 380 { 381 BeginInvoke((Action)(() => { 382 if (tok != cancelToken) { try { File.Delete(wav); } catch {} return; } 383 tempWavPath = wav; 384 string c2 = string.Format("open \"{0}\" alias {1}", wav, MciAlias); 385 int e2 = WinApi.mciSendString(c2, null, 0, IntPtr.Zero); 386 Log("MCI open temp WAV err=" + e2 + " path=" + wav); 387 mciOpen = (e2 == 0); 388 if (mciOpen) 389 { 390 WinApi.mciSendString("set " + MciAlias + " time format milliseconds", null, 0, IntPtr.Zero); 391 duration = MciGetLength(); 392 MciSetVolume(volume); 393 currentPos = 0; 394 isPlaying = false; 395 Log("Temp WAV opened OK, duration=" + duration + "s"); 396 if (ap) MciPlay(); 397 } 398 Invalidate(); 399 })); 400 } 401 else if (wav != null) 402 { 403 try { File.Delete(wav); } catch {} 404 } 405 else 406 { 407 Log("DecodeToPcmWav FAILED — returned null"); 408 } 409 } 410 finally { WinApi.CoUninitialize(); } 411 } 412 catch (Exception ex) { Log("MFDecode thread exception: " + ex); } 413 }) { IsBackground = true, Name = "MFDecode" }; 414 t.Start(); 415 StartWaveformDecode(src); 416 } 417 418 Invalidate(); 419 } 420 421 private void DeleteTempWav() 422 { 423 if (tempWavPath != null && File.Exists(tempWavPath)) 424 { 425 try { File.Delete(tempWavPath); } catch { } 426 tempWavPath = null; 427 } 428 } 429 430 // ===== MF waveform decode (background thread) ===== 431 private void StartWaveformDecode(string path) 432 { 433 waveformData = null; 434 435 int token = cancelToken; 436 Log("StartWaveformDecode: " + path); 437 var t = new Thread(() => { 438 try 439 { 440 WinApi.CoInitializeEx(IntPtr.Zero, WinApi.COINIT_MULTITHREADED); 441 try 442 { 443 float[] data = ComputeWaveformViaMF(path, 4096, token); 444 if (token == cancelToken) 445 { 446 waveformData = data; 447 } 448 Log("Waveform result: " + (data != null ? data.Length + " cols" : "null") + 449 (token != cancelToken ? " (cancelled)" : "")); 450 } 451 finally { WinApi.CoUninitialize(); } 452 } 453 catch (Exception ex) { Log("WaveformDecode thread exception: " + ex); } 454 }) { IsBackground = true, Name = "WaveformDecode" }; 455 t.Start(); 456 } 457 458 private float[] ComputeWaveformViaMF(string path, int numCols, int token) 459 { 460 int hr; 461 hr = WinApi.MFStartup(WinApi.MF_VERSION, 0); 462 Log("Waveform MFStartup hr=0x" + hr.ToString("X8")); 463 if (hr != 0) return null; 464 try 465 { 466 IMFSourceReader reader; 467 hr = WinApi.MFCreateSourceReaderFromURL_IntPtr(path, IntPtr.Zero, out reader); 468 Log("Waveform MFCreateSourceReaderFromURL hr=0x" + hr.ToString("X8")); 469 if (hr != 0) return null; 470 471 IMFMediaType mt; 472 hr = WinApi.MFCreateMediaType(out mt); 473 474 var keyMajor = WinApi.MF_MT_MAJOR_TYPE; 475 var valAudio = WinApi.MFMediaType_Audio; 476 mt.SetGUID(ref keyMajor, ref valAudio); 477 478 var keySub = WinApi.MF_MT_SUBTYPE; 479 var valPCM = WinApi.MFAudioFormat_PCM; 480 mt.SetGUID(ref keySub, ref valPCM); 481 482 var keyCh = WinApi.MF_MT_AUDIO_NUM_CHANNELS; 483 mt.SetUINT32(ref keyCh, 1); 484 485 var keySR = WinApi.MF_MT_AUDIO_SAMPLES_PER_SECOND; 486 mt.SetUINT32(ref keySR, 16000); 487 488 var keyBPS = WinApi.MF_MT_AUDIO_BITS_PER_SAMPLE; 489 mt.SetUINT32(ref keyBPS, 16); 490 491 var keyBA = WinApi.MF_MT_AUDIO_BLOCK_ALIGNMENT; 492 mt.SetUINT32(ref keyBA, 2); 493 494 var keyABPS = WinApi.MF_MT_AUDIO_AVG_BYTES_PER_SECOND; 495 mt.SetUINT32(ref keyABPS, 32000); 496 497 reader.SetStreamSelection(WinApi.MF_SOURCE_READER_FIRST_AUDIO_STREAM, true); 498 hr = reader.SetCurrentMediaType(WinApi.MF_SOURCE_READER_FIRST_AUDIO_STREAM, IntPtr.Zero, mt); 499 Log("Waveform SetCurrentMediaType hr=0x" + hr.ToString("X8")); 500 if (hr != 0) { Marshal.ReleaseComObject(reader); return null; } 501 502 const int blockSamples = 160; 503 var peaks = new List<float>(4096); 504 float curPeak = 0f; 505 int curCnt = 0; 506 int sampleCount = 0; 507 int nullCount = 0; 508 509 while (token == cancelToken) 510 { 511 uint actualIdx; 512 int flags; 513 long ts; 514 IMFSample sample; 515 int rhr = reader.ReadSample( 516 WinApi.MF_SOURCE_READER_FIRST_AUDIO_STREAM, 517 0, out actualIdx, out flags, out ts, out sample); 518 if (rhr != 0) { Log("Waveform ReadSample err=0x" + rhr.ToString("X8")); break; } 519 if ((flags & WinApi.MF_SOURCE_READERF_ENDOFSTREAM) != 0) break; 520 if (sample == null) { nullCount++; if (nullCount > 200) break; continue; } 521 nullCount = 0; 522 sampleCount++; 523 524 IMFMediaBuffer buf; 525 sample.ConvertToContiguousBuffer(out buf); 526 if (buf == null) { Marshal.ReleaseComObject(sample); continue; } 527 528 IntPtr pData; 529 int maxLen, curLen; 530 buf.Lock(out pData, out maxLen, out curLen); 531 532 int numShorts = curLen / 2; 533 for (int i = 0; i < numShorts; i++) 534 { 535 short s = Marshal.ReadInt16(pData, i * 2); 536 float v = Math.Abs(s / 32768f); 537 if (v > curPeak) curPeak = v; 538 curCnt++; 539 if (curCnt >= blockSamples) 540 { 541 peaks.Add(curPeak); 542 curPeak = 0f; 543 curCnt = 0; 544 } 545 } 546 buf.Unlock(); 547 Marshal.ReleaseComObject(buf); 548 Marshal.ReleaseComObject(sample); 549 } 550 if (curCnt > 0) peaks.Add(curPeak); 551 Marshal.ReleaseComObject(reader); 552 Log("Waveform decode done: " + sampleCount + " samples, " + peaks.Count + " peaks"); 553 554 if (peaks.Count == 0) return null; 555 556 var result = new float[numCols]; 557 for (int c = 0; c < numCols; c++) 558 { 559 int i0 = c * peaks.Count / numCols; 560 int i1 = (c + 1) * peaks.Count / numCols; 561 if (i1 >= peaks.Count) i1 = peaks.Count - 1; 562 float pk = 0f; 563 for (int i = i0; i <= i1; i++) 564 if (peaks[i] > pk) pk = peaks[i]; 565 result[c] = pk; 566 } 567 568 float maxPk = 0f; 569 foreach (var p in result) if (p > maxPk) maxPk = p; 570 if (maxPk > 0.001f) 571 for (int i = 0; i < result.Length; i++) result[i] /= maxPk; 572 573 return result; 574 } 575 catch (Exception ex) { Log("Waveform EXCEPTION: " + ex); return null; } 576 finally { WinApi.MFShutdown(); } 577 } 578 579 // ===== MF decode to temp WAV ===== 580 private static string DecodeToPcmWav(string path) 581 { 582 int hr; 583 Log("DecodeToPcmWav START: " + path); 584 hr = WinApi.MFStartup(WinApi.MF_VERSION, 0); 585 Log("Decode MFStartup hr=0x" + hr.ToString("X8")); 586 if (hr != 0) return null; 587 try 588 { 589 IMFSourceReader reader; 590 hr = WinApi.MFCreateSourceReaderFromURL_IntPtr(path, IntPtr.Zero, out reader); 591 Log("Decode MFCreateSourceReaderFromURL hr=0x" + hr.ToString("X8")); 592 if (hr != 0) return null; 593 594 IMFMediaType mt; 595 hr = WinApi.MFCreateMediaType(out mt); 596 Log("Decode MFCreateMediaType hr=0x" + hr.ToString("X8")); 597 598 var keyMajor = WinApi.MF_MT_MAJOR_TYPE; 599 var valAudio = WinApi.MFMediaType_Audio; 600 mt.SetGUID(ref keyMajor, ref valAudio); 601 602 var keySub = WinApi.MF_MT_SUBTYPE; 603 var valPCM = WinApi.MFAudioFormat_PCM; 604 mt.SetGUID(ref keySub, ref valPCM); 605 606 var keyCh = WinApi.MF_MT_AUDIO_NUM_CHANNELS; 607 mt.SetUINT32(ref keyCh, 2); 608 609 var keySR = WinApi.MF_MT_AUDIO_SAMPLES_PER_SECOND; 610 mt.SetUINT32(ref keySR, 44100); 611 612 var keyBPS = WinApi.MF_MT_AUDIO_BITS_PER_SAMPLE; 613 mt.SetUINT32(ref keyBPS, 16); 614 615 var keyBA = WinApi.MF_MT_AUDIO_BLOCK_ALIGNMENT; 616 mt.SetUINT32(ref keyBA, 4); 617 618 var keyABPS = WinApi.MF_MT_AUDIO_AVG_BYTES_PER_SECOND; 619 mt.SetUINT32(ref keyABPS, 44100 * 4); 620 621 hr = reader.SetStreamSelection(WinApi.MF_SOURCE_READER_FIRST_AUDIO_STREAM, true); 622 Log("Decode SetStreamSelection hr=0x" + hr.ToString("X8")); 623 hr = reader.SetCurrentMediaType(WinApi.MF_SOURCE_READER_FIRST_AUDIO_STREAM, IntPtr.Zero, mt); 624 Log("Decode SetCurrentMediaType hr=0x" + hr.ToString("X8")); 625 if (hr != 0) { Log("Decode FAILED at SetCurrentMediaType"); Marshal.ReleaseComObject(reader); return null; } 626 627 string tmpPath = Path.Combine(Path.GetTempPath(), "wc_flac_" + Guid.NewGuid().ToString("N").Substring(0, 8) + ".wav"); 628 Log("Decode temp path: " + tmpPath); 629 using (var fs = new FileStream(tmpPath, FileMode.Create, FileAccess.Write)) 630 using (var bw = new BinaryWriter(fs)) 631 { 632 const int sampleRate = 44100, channels = 2, bitsPerSample = 16; 633 int blockAlign = channels * bitsPerSample / 8; 634 int avgBytesPerSec = sampleRate * blockAlign; 635 bw.Write(new byte[44]); 636 637 long dataBytes = 0; 638 int sampleCount = 0; 639 640 while (true) 641 { 642 uint actualIdx; int flags; long ts; 643 IMFSample sample; 644 int rhr = reader.ReadSample( 645 WinApi.MF_SOURCE_READER_FIRST_AUDIO_STREAM, 646 0, out actualIdx, out flags, out ts, out sample); 647 if (rhr != 0) { Log("Decode ReadSample err=0x" + rhr.ToString("X8")); break; } 648 if ((flags & WinApi.MF_SOURCE_READERF_ENDOFSTREAM) != 0) break; 649 if (sample == null) continue; 650 sampleCount++; 651 652 IMFMediaBuffer buf; 653 sample.ConvertToContiguousBuffer(out buf); 654 if (buf != null) 655 { 656 IntPtr pData; int maxLen, curLen; 657 buf.Lock(out pData, out maxLen, out curLen); 658 byte[] chunk = new byte[curLen]; 659 Marshal.Copy(pData, chunk, 0, curLen); 660 bw.Write(chunk); 661 dataBytes += curLen; 662 buf.Unlock(); 663 Marshal.ReleaseComObject(buf); 664 } 665 Marshal.ReleaseComObject(sample); 666 } 667 Marshal.ReleaseComObject(reader); 668 Log("Decode done: " + sampleCount + " samples, " + dataBytes + " bytes"); 669 670 if (dataBytes == 0) 671 { 672 Log("Decode produced 0 bytes — deleting temp"); 673 fs.Close(); 674 try { File.Delete(tmpPath); } catch { } 675 return null; 676 } 677 678 fs.Seek(0, SeekOrigin.Begin); 679 bw.Write(new byte[] { (byte)'R',(byte)'I',(byte)'F',(byte)'F' }); 680 bw.Write((int)(36 + dataBytes)); 681 bw.Write(new byte[] { (byte)'W',(byte)'A',(byte)'V',(byte)'E' }); 682 bw.Write(new byte[] { (byte)'f',(byte)'m',(byte)'t',(byte)' ' }); 683 bw.Write(16); 684 bw.Write((short)1); 685 bw.Write((short)channels); 686 bw.Write(sampleRate); 687 bw.Write(avgBytesPerSec); 688 bw.Write((short)blockAlign); 689 bw.Write((short)bitsPerSample); 690 bw.Write(new byte[] { (byte)'d',(byte)'a',(byte)'t',(byte)'a' }); 691 bw.Write((int)dataBytes); 692 } 693 Log("Decode WAV written OK: " + tmpPath); 694 return tmpPath; 695 } 696 catch (Exception ex) { Log("Decode EXCEPTION: " + ex); return null; } 697 finally { WinApi.MFShutdown(); } 698 } 699 700 // ===== Folder scan ===== 701 private void ScanFolder(string filePath) 702 { 703 string dir = Path.GetDirectoryName(filePath); 704 try 705 { 706 var result = new List<string>(); 707 foreach (var f in Directory.GetFiles(dir)) 708 if (Array.IndexOf(AudioExts, Path.GetExtension(f).ToLowerInvariant()) >= 0) 709 result.Add(f); 710 result.Sort(StringComparer.OrdinalIgnoreCase); 711 folderFiles = result.ToArray(); 712 } 713 catch { folderFiles = new[] { filePath }; } 714 } 715 716 private void BrowseFile(int dir) 717 { 718 if (folderFiles == null || folderFiles.Length == 0) return; 719 int idx = currentFileIndex + dir; 720 if (idx < 0) idx = folderFiles.Length - 1; 721 if (idx >= folderFiles.Length) idx = 0; 722 LoadTrack(folderFiles[idx], true); 723 } 724 725 // ===== MCI helpers ===== 726 private void MciPlay() 727 { 728 if (!mciOpen) return; 729 WinApi.mciSendString("play " + MciAlias, null, 0, IntPtr.Zero); 730 isPlaying = true; 731 } 732 private void MciPause() 733 { 734 if (!mciOpen) return; 735 WinApi.mciSendString("pause " + MciAlias, null, 0, IntPtr.Zero); 736 isPlaying = false; 737 } 738 private void MciToggle() { if (isPlaying) MciPause(); else MciPlay(); } 739 740 private void MciSeek(int ms) 741 { 742 if (!mciOpen) return; 743 WinApi.mciSendString(string.Format("seek {0} to {1}", MciAlias, ms), null, 0, IntPtr.Zero); 744 if (isPlaying) MciPlay(); 745 currentPos = ms / 1000.0; 746 lastSeekTime = DateTime.Now; 747 } 748 749 private void MciSetVolume(float v) 750 { 751 volume = v; 752 // Keep the value in memory; it is persisted once in FormClosing. 753 // (Previously Settings.Save() rewrote the whole ini file on every wheel/arrow tick → UI stutter + disk thrash.) 754 Settings.AudioVolume = v; 755 756 uint vol16 = (uint)(v * 0xFFFF); 757 uint combined = vol16 | (vol16 << 16); 758 waveOutSetVolume(IntPtr.Zero, combined); 759 760 if (!mciOpen) return; 761 int vol = (int)(v * 1000); 762 WinApi.mciSendString(string.Format("setaudio {0} volume to {1}", MciAlias, vol), null, 0, IntPtr.Zero); 763 WinApi.mciSendString(string.Format("setaudio {0} left volume to {1}", MciAlias, vol), null, 0, IntPtr.Zero); 764 WinApi.mciSendString(string.Format("setaudio {0} right volume to {1}", MciAlias, vol), null, 0, IntPtr.Zero); 765 } 766 767 private double MciGetLength() 768 { 769 if (!mciOpen) return 0; 770 var sb = new StringBuilder(64); 771 WinApi.mciSendString("status " + MciAlias + " length", sb, 64, IntPtr.Zero); 772 int ms; 773 return int.TryParse(sb.ToString().Trim(), out ms) ? ms / 1000.0 : 0; 774 } 775 776 private double MciGetPosition() 777 { 778 if (!mciOpen) return 0; 779 var sb = new StringBuilder(64); 780 WinApi.mciSendString("status " + MciAlias + " position", sb, 64, IntPtr.Zero); 781 int ms; 782 return int.TryParse(sb.ToString().Trim(), out ms) ? ms / 1000.0 : 0; 783 } 784 785 private void MciClose() 786 { 787 if (!mciOpen) return; 788 WinApi.mciSendString("stop " + MciAlias, null, 0, IntPtr.Zero); 789 WinApi.mciSendString("close " + MciAlias, null, 0, IntPtr.Zero); 790 mciOpen = false; 791 isPlaying = false; 792 } 793 794 // ===== Timers ===== 795 private void PollTimer_Tick(object sender, EventArgs e) 796 { 797 if (!mciOpen) return; 798 double p = MciGetPosition(); 799 if (Math.Abs(p - currentPos) > 0.001) { 800 lastPollPos = p; 801 lastPollTime = DateTime.Now; 802 } 803 currentPos = p; 804 if (isPlaying && duration > 1 && currentPos >= duration - 0.15) 805 BrowseFile(1); 806 } 807 808 private void VolFadeTimer_Tick(object sender, EventArgs e) 809 { 810 double elapsed = (DateTime.Now - volLastChange).TotalMilliseconds; 811 if (elapsed > 1200) 812 { 813 float t = (float)((elapsed - 1200) / 600.0); 814 volIndicatorAlpha = Math.Max(0f, 1f - t); 815 Invalidate(); 816 if (volIndicatorAlpha <= 0f) volFadeTimer.Stop(); 817 } 818 } 819 820 // ===== Album art ===== 821 private int ComputeArtHeight() 822 { 823 if (albumArt == null) return 0; 824 return (int)((float)albumArt.Height / albumArt.Width * Width); 825 } 826 827 private Bitmap LoadAlbumArt(string path) 828 { 829 try 830 { 831 string ext = Path.GetExtension(path).ToLowerInvariant(); 832 byte[] data = null; 833 if (ext == ".mp3") data = ExtractId3v2Art(path); 834 else if (ext == ".flac") data = ExtractFlacArt(path); 835 else if (ext == ".m4a" || ext == ".aac") data = ExtractM4aArt(path); 836 if (data != null && data.Length > 4) 837 using (var ms = new MemoryStream(data)) 838 return new Bitmap(ms); 839 } 840 catch { } 841 return null; 842 } 843 844 private byte[] ExtractId3v2Art(string path) 845 { 846 using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read)) 847 { 848 var hdr = new byte[10]; 849 if (fs.Read(hdr, 0, 10) < 10 || hdr[0] != 'I' || hdr[1] != 'D' || hdr[2] != '3') return null; 850 int tagSize = ((hdr[6]&0x7F)<<21)|((hdr[7]&0x7F)<<14)|((hdr[8]&0x7F)<<7)|(hdr[9]&0x7F); 851 long tagEnd = 10 + tagSize; 852 while (fs.Position + 10 < tagEnd) 853 { 854 var fh = new byte[10]; 855 fs.Read(fh, 0, 10); 856 string id = Encoding.ASCII.GetString(fh, 0, 4); 857 int sz = (fh[4]<<24)|(fh[5]<<16)|(fh[6]<<8)|fh[7]; 858 if (sz <= 0 || sz > 5*1024*1024) break; 859 if (id == "APIC") 860 { 861 var d = new byte[sz]; fs.Read(d, 0, sz); 862 int p = 1; 863 while (p < d.Length && d[p] != 0) p++; p++; 864 if (p >= d.Length) return null; 865 p++; 866 while (p < d.Length && d[p] != 0) p++; p++; 867 if (p >= d.Length) return null; 868 var img = new byte[d.Length - p]; 869 Array.Copy(d, p, img, 0, img.Length); 870 return img; 871 } 872 else fs.Seek(sz, SeekOrigin.Current); 873 } 874 } 875 return null; 876 } 877 878 private byte[] ExtractFlacArt(string path) 879 { 880 using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read)) 881 { 882 var mark = new byte[4]; 883 if (fs.Read(mark, 0, 4) < 4 || 884 mark[0]!='f'||mark[1]!='L'||mark[2]!='a'||mark[3]!='C') return null; 885 while (fs.Position < fs.Length) 886 { 887 var bh = new byte[4]; 888 if (fs.Read(bh, 0, 4) < 4) break; 889 bool last = (bh[0] & 0x80) != 0; 890 int type = bh[0] & 0x7F; 891 int len = (bh[1]<<16)|(bh[2]<<8)|bh[3]; 892 if (len <= 0) break; 893 if (type == 6) 894 { 895 var blk = new byte[len]; fs.Read(blk, 0, len); 896 int pos = 4; 897 int mimeLen = (blk[pos]<<24)|(blk[pos+1]<<16)|(blk[pos+2]<<8)|blk[pos+3]; pos+=4+mimeLen; 898 int descLen = (blk[pos]<<24)|(blk[pos+1]<<16)|(blk[pos+2]<<8)|blk[pos+3]; pos+=4+descLen; 899 pos += 16; 900 int dLen = (blk[pos]<<24)|(blk[pos+1]<<16)|(blk[pos+2]<<8)|blk[pos+3]; pos+=4; 901 if (dLen > 0 && pos+dLen <= blk.Length) 902 { 903 var img = new byte[dLen]; Array.Copy(blk, pos, img, 0, dLen); 904 return img; 905 } 906 } 907 else fs.Seek(len, SeekOrigin.Current); 908 if (last) break; 909 } 910 } 911 return null; 912 } 913 914 private byte[] ExtractM4aArt(string path) 915 { 916 using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read)) 917 { 918 var buf = new byte[(int)Math.Min(fs.Length, 2*1024*1024)]; 919 fs.Read(buf, 0, buf.Length); 920 for (int i = 0; i < buf.Length - 8; i++) 921 if (buf[i]=='c'&&buf[i+1]=='o'&&buf[i+2]=='v'&&buf[i+3]=='r') 922 { 923 int s = i+4; 924 if (s+8 < buf.Length && buf[s+4]=='d'&&buf[s+5]=='a'&&buf[s+6]=='t'&&buf[s+7]=='a') 925 { 926 int dboxSz = (buf[s]<<24)|(buf[s+1]<<16)|(buf[s+2]<<8)|buf[s+3]; 927 int imgStart = s+8+8; 928 int imgLen = dboxSz-16; 929 if (imgLen > 0 && imgStart+imgLen <= buf.Length) 930 { 931 var img = new byte[imgLen]; 932 Array.Copy(buf, imgStart, img, 0, imgLen); 933 return img; 934 } 935 } 936 } 937 } 938 return null; 939 } 940 941 // ===== Window buttons rects ===== 942 private Rectangle CloseBtnRect { get { return new Rectangle(Width - 28, 5, 18, 18); } } 943 private Rectangle MaxBtnRect { get { return new Rectangle(Width - 28 - 24, 5, 18, 18); } } 944 private Rectangle MinBtnRect { get { return new Rectangle(Width - 28 - 48, 5, 18, 18); } } 945 946 private void PaintWindowButtons(Graphics g) 947 { 948 if (winButtonsAlpha <= 0.01f) return; 949 float groupA = winButtonsAlpha; 950 951 DrawWinButton(g, CloseBtnRect, closeBtnHover, "close", groupA); 952 DrawWinButton(g, MaxBtnRect, maxBtnHover, "max", groupA); 953 DrawWinButton(g, MinBtnRect, minBtnHover, "min", groupA); 954 } 955 956 private void DrawWinButton(Graphics g, Rectangle r, bool hover, string type, float groupAlpha) 957 { 958 float finalAlpha = groupAlpha; 959 960 if (hover) 961 { 962 Color highlightColor = (type == "close") 963 ? Color.FromArgb((int)(finalAlpha * 180), 180, 40, 40) 964 : Color.FromArgb((int)(finalAlpha * 60), 255, 255, 255); 965 966 using (var br = new SolidBrush(highlightColor)) 967 g.FillRectangle(br, r); 968 } 969 970 using (var pen = new Pen(Color.FromArgb((int)(finalAlpha * (hover ? 255 : 180)), 255, 255, 255), 1.2f)) 971 { 972 int p = 6; 973 if (type == "close") { 974 g.DrawLine(pen, r.Left + p, r.Top + p, r.Right - p, r.Bottom - p); 975 g.DrawLine(pen, r.Right - p, r.Top + p, r.Left + p, r.Bottom - p); 976 } else if (type == "max") { 977 g.DrawRectangle(pen, r.Left + p, r.Top + p, r.Width - p * 2, r.Height - p * 2); 978 } else if (type == "min") { 979 g.DrawLine(pen, r.Left + p, r.Bottom - p - 1, r.Right - p, r.Bottom - p - 1); 980 } 981 } 982 } 983 984 // ===== Painting ===== 985 protected override void OnPaint(PaintEventArgs e) 986 { 987 var g = e.Graphics; 988 g.SmoothingMode = SmoothingMode.AntiAlias; 989 g.InterpolationMode = InterpolationMode.HighQualityBicubic; 990 int w = Width, h = Height; 991 int stripY = h - StripH; 992 993 using (var br = new SolidBrush(Color.FromArgb(20, 0, 0, 0))) 994 g.FillRectangle(br, 0, 0, w, h); 995 996 if (albumArt != null) 997 { 998 g.DrawImage(albumArt, new Rectangle(0, 0, w, h)); 999 1000 using (var br = new LinearGradientBrush( 1001 new Point(0, h - StripH), new Point(0, h), 1002 Color.Transparent, Color.FromArgb(160, 0, 0, 0))) 1003 { 1004 g.FillRectangle(br, 0, h - StripH, w, StripH); 1005 } 1006 } 1007 1008 stripY = h - StripH; 1009 PaintSpectrogramSlider(g, w, stripY); 1010 if (volIndicatorAlpha > 0.01f) PaintVolumeIndicator(g, w, h); 1011 1012 PaintArrows(g, w, h); 1013 PaintWindowButtons(g); 1014 } 1015 1016 private Color GetEdgeColor(Bitmap bmp, bool left) 1017 { 1018 if (bmp == null) return Color.White; 1019 try { 1020 long r=0, g=0, b=0, count=0; 1021 int xStart = left ? 0 : bmp.Width - 10; 1022 int xEnd = left ? 10 : bmp.Width; 1023 for (int x = xStart; x < xEnd; x += 2) { 1024 for (int y = 0; y < bmp.Height; y += 5) { 1025 Color c = bmp.GetPixel(x, y); 1026 r += c.R; g += c.G; b += c.B; count++; 1027 } 1028 } 1029 if (count == 0) return Color.White; 1030 return Color.FromArgb((int)(r/count), (int)(g/count), (int)(b/count)); 1031 } catch { return Color.White; } 1032 } 1033 1034 private void PaintArrows(Graphics g, int w, int h) 1035 { 1036 int artAreaH = h - StripH; 1037 if (artAreaH <= 0) return; 1038 1039 Point cp = PointToClient(Cursor.Position); 1040 float pulse = 0.85f + 0.15f * (float)Math.Sin(arrowPulseAnim); 1041 1042 if (leftArrowAlpha > 0.01f) 1043 { 1044 int zoneW = 200; 1045 using (var path = new GraphicsPath()) { 1046 path.AddEllipse(-zoneW, cp.Y - 150, zoneW * 2, 300); 1047 using (var pgb = new PathGradientBrush(path)) { 1048 pgb.CenterPoint = new PointF(0, cp.Y); 1049 Color c = Color.FromArgb((int)(leftArrowAlpha * 110 * pulse), leftEdgeColor); 1050 pgb.CenterColor = c; 1051 pgb.SurroundColors = new Color[] { Color.Transparent }; 1052 pgb.FocusScales = new PointF(0.1f, 0.1f); 1053 g.FillRectangle(pgb, 0, 0, zoneW, artAreaH); 1054 } 1055 } 1056 DrawArrow(g, "\u2039", new Rectangle(0, 0, 70, artAreaH), leftArrowAlpha); 1057 } 1058 1059 if (rightArrowAlpha > 0.01f) 1060 { 1061 int zoneW = 200; 1062 using (var path = new GraphicsPath()) { 1063 path.AddEllipse(w - zoneW, cp.Y - 150, zoneW * 2, 300); 1064 using (var pgb = new PathGradientBrush(path)) { 1065 pgb.CenterPoint = new PointF(w, cp.Y); 1066 Color c = Color.FromArgb((int)(rightArrowAlpha * 110 * pulse), rightEdgeColor); 1067 pgb.CenterColor = c; 1068 pgb.SurroundColors = new Color[] { Color.Transparent }; 1069 pgb.FocusScales = new PointF(0.1f, 0.1f); 1070 g.FillRectangle(pgb, w - zoneW, 0, zoneW, artAreaH); 1071 } 1072 } 1073 DrawArrow(g, "\u203A", new Rectangle(w - 70, 0, 70, artAreaH), rightArrowAlpha); 1074 } 1075 } 1076 1077 private void DrawArrow(Graphics g, string text, Rectangle rect, float alpha) 1078 { 1079 g.TextRenderingHint = TextRenderingHint.AntiAlias; 1080 using (var font = new Font(_bebasFont ?? FontFamily.GenericSansSerif, 42f, FontStyle.Regular, GraphicsUnit.Pixel)) 1081 { 1082 var sz = g.MeasureString(text, font); 1083 float tx = rect.X + (rect.Width - sz.Width) / 2; 1084 float ty = rect.Y + (rect.Height - sz.Height) / 2; 1085 1086 using (var bShad = new SolidBrush(Color.FromArgb((int)(alpha * 100), 0, 0, 0))) 1087 g.DrawString(text, font, bShad, tx + 1, ty + 1); 1088 1089 using (var br = new SolidBrush(Color.FromArgb((int)(alpha * 220), 255, 255, 255))) 1090 g.DrawString(text, font, br, tx, ty); 1091 } 1092 } 1093 1094 private void PaintSpectrogramSlider(Graphics g, int w, int stripY) 1095 { 1096 int spectY = stripY; 1097 int spectH = StripH; 1098 int centerY = spectY + spectH / 2; 1099 int curX = (duration > 0) ? (int)(currentPos / duration * w) : 0; 1100 1101 double interpPos = currentPos; 1102 if (isPlaying && lastPollTime != DateTime.MinValue) { 1103 double elapsed = (DateTime.Now - lastPollTime).TotalSeconds; 1104 interpPos = lastPollPos + elapsed; 1105 if (interpPos > duration) interpPos = duration; 1106 } 1107 int smoothCurX = (duration > 0) ? (int)(interpPos / duration * w) : 0; 1108 1109 float[] wd = waveformData; 1110 if (wd != null) 1111 { 1112 string trackInfo = currentTrackTitle; 1113 if (!string.IsNullOrEmpty(currentTrackArtist)) 1114 trackInfo = currentTrackArtist + " \u2014 " + trackInfo; 1115 1116 string timeStr = ""; 1117 bool showTime = lmbDragging || (DateTime.Now - lastSeekTime).TotalSeconds < 2.5; 1118 if (showTime) 1119 timeStr = " [" + FormatTime(currentPos) + " / " + FormatTime(duration) + "]"; 1120 1121 FontFamily ff = _bebasFont; 1122 if (ff == null) 1123 { 1124 string[] fontFamilies = { "Bebas", "Bebas Neue", "BebasNeue", "Bahnschrift", "Segoe UI" }; 1125 foreach (var name_f in fontFamilies) 1126 { 1127 try { ff = new FontFamily(name_f); break; } catch { } 1128 } 1129 } 1130 1131 g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAlias; 1132 using (var path = new GraphicsPath()) 1133 { 1134 float fontSize = 32f; 1135 var sf = StringFormat.GenericTypographic; 1136 path.AddString(trackInfo, ff, (int)FontStyle.Regular, fontSize, new PointF(0, 0), sf); 1137 1138 if (!string.IsNullOrEmpty(timeStr)) 1139 { 1140 using (var ffTime = new FontFamily("Segoe UI")) 1141 { 1142 var bInfo = path.GetBounds(); 1143 path.AddString(timeStr, ffTime, (int)FontStyle.Regular, fontSize * 0.45f, 1144 new PointF(bInfo.Right + 5, fontSize * 0.2f), sf); 1145 } 1146 } 1147 1148 var bounds = path.GetBounds(); 1149 if (bounds.Width > 0 && bounds.Height > 0) 1150 { 1151 float targetW = w * 0.98f; 1152 float targetH = spectH * 0.92f; 1153 float finalScale = Math.Min(targetW / bounds.Width, targetH / bounds.Height); 1154 float tx = (w - bounds.Width * finalScale) / 2 - bounds.Left * finalScale; 1155 float ty = spectY + (spectH - bounds.Height * finalScale) / 2 - bounds.Top * finalScale; 1156 1157 var matrix = new Matrix(); 1158 matrix.Scale(finalScale, finalScale); 1159 matrix.Translate(tx, ty, MatrixOrder.Append); 1160 path.Transform(matrix); 1161 1162 int N = wd.Length; 1163 1164 if (isPlaying && !lmbDragging && lastPollTime != DateTime.MinValue) { 1165 double elapsed = (DateTime.Now - lastPollTime).TotalSeconds; 1166 interpPos = lastPollPos + elapsed; 1167 if (interpPos > duration) interpPos = duration; 1168 } 1169 1170 double syncOffset = 0.04; 1171 int curIdx = (duration > 0) ? (int)((interpPos + (lmbDragging ? 0 : syncOffset)) / duration * N) : 0; 1172 if (curIdx < 0) curIdx = 0; if (curIdx >= N) curIdx = N - 1; 1173 1174 float bezierPlay = playStateFactor * playStateFactor * (3.0f - 2.0f * playStateFactor); 1175 float globalEnergyFactor = 0.22f + 0.78f * bezierPlay; 1176 1177 float rawBass = 0; 1178 int bassWin = Math.Max(1, N / 100); 1179 for (int i = curIdx - 2; i <= curIdx + bassWin; i++) { 1180 if (i >= 0 && i < N) rawBass = Math.Max(rawBass, wd[i]); 1181 } 1182 1183 float rawTreble = 0; 1184 int trebleWin = Math.Max(1, N / 400); 1185 for (int i = curIdx; i <= curIdx + trebleWin; i++) { 1186 if (i >= 0 && i + 1 < N) rawTreble = Math.Max(rawTreble, Math.Abs(wd[i+1] - wd[i])); 1187 } 1188 rawTreble *= 6.0f; 1189 1190 if (rawBass > smoothBass) smoothBass = rawBass; 1191 else smoothBass = smoothBass * 0.85f + rawBass * 0.15f; 1192 1193 smoothTreble = smoothTreble * 0.7f + rawTreble * 0.3f; 1194 1195 float punchyBass = (float)Math.Pow(smoothBass, 1.3) * 1.8f * bezierPlay; 1196 float punchyTreble = (float)Math.Pow(smoothTreble, 1.1) * 2.5f * bezierPlay; 1197 1198 int polyCount = w * 2; 1199 if (specPointsTop == null || specPointsTop.Length != polyCount) { 1200 specPointsTop = new PointF[polyCount]; 1201 } 1202 1203 float effectRange = w * 0.25f; 1204 int maxHalfH = spectH / 2 - 2; 1205 1206 for (int x = 0; x < w; x++) 1207 { 1208 int idx = x * N / w; if (idx >= N) idx = N - 1; 1209 float peak = wd[idx]; 1210 1211 float dist = Math.Abs(x - smoothCurX); 1212 float localFactor = (dist < effectRange) ? (float)Math.Pow(1.0 - (dist / effectRange), 2) : 0f; 1213 1214 float localEnergy = (punchyBass * (1.0f - (float)x/w*0.5f) + punchyTreble * ((float)x/w*0.5f)); 1215 float pulseFactor = 1.0f + localEnergy * localFactor * 1.8f; 1216 1217 float desiredH = peak * maxHalfH * pulseFactor * globalEnergyFactor; 1218 1219 float hh; 1220 if (desiredH < maxHalfH * 0.7f) { 1221 hh = desiredH; 1222 } else { 1223 float overflow = desiredH - (maxHalfH * 0.7f); 1224 hh = (maxHalfH * 0.7f) + (maxHalfH * 0.3f) * (float)Math.Atan(overflow / (maxHalfH * 0.3f)) / (float)(Math.PI / 2); 1225 } 1226 1227 if (hh < 1) hh = 1; 1228 1229 specPointsTop[x] = new PointF(x, centerY - hh); 1230 specPointsTop[polyCount - 1 - x] = new PointF(x, centerY + hh); 1231 } 1232 1233 float brightness = 0.85f + (punchyBass * 0.4f + punchyTreble * 0.4f); 1234 int r = (int)Math.Min(255, accentColor.R * brightness); 1235 int g_comp = (int)Math.Min(255, accentColor.G * brightness); 1236 int b = (int)Math.Min(255, accentColor.B * brightness); 1237 1238 using (var brPlayed = new SolidBrush(Color.FromArgb(r, g_comp, b))) 1239 using (var brUnplayed = new SolidBrush(Color.FromArgb(100, 100, 100, 110))) 1240 { 1241 var oldClip = g.Clip; 1242 g.SetClip(new Rectangle(0, spectY, smoothCurX, spectH)); 1243 g.FillPolygon(brPlayed, specPointsTop); 1244 1245 g.SetClip(new Rectangle(smoothCurX, spectY, w - smoothCurX, spectH)); 1246 g.FillPolygon(brUnplayed, specPointsTop); 1247 1248 g.Clip = oldClip; 1249 } 1250 1251 Point cp = PointToClient(Cursor.Position); 1252 bool hovered = cp.Y >= (Height - StripH) && cp.X >= 0 && cp.X <= Width && cp.Y <= Height; 1253 float baseA = trackInfoAlpha; 1254 1255 using (var bText = new LinearGradientBrush(new RectangleF(0, 0, w, 1), Color.White, Color.White, 0f)) 1256 { 1257 ColorBlend cb = new ColorBlend(); 1258 int stopCount = 25; 1259 cb.Positions = new float[stopCount]; 1260 cb.Colors = new Color[stopCount]; 1261 1262 float fadeRadius = 300f; 1263 float deadZone = 100f; 1264 1265 for (int i = 0; i < stopCount; i++) 1266 { 1267 float pos = (float)i / (stopCount - 1); 1268 float xPos = pos * w; 1269 cb.Positions[i] = pos; 1270 1271 float distToCursor = Math.Abs(xPos - cp.X); 1272 float targetAlpha = 1.0f; 1273 1274 if (distToCursor < deadZone) 1275 { 1276 targetAlpha = 0; 1277 } 1278 else if (distToCursor < fadeRadius) 1279 { 1280 float t = (distToCursor - deadZone) / (fadeRadius - deadZone); 1281 targetAlpha = (t * t) * 0.2f; 1282 } 1283 else 1284 { 1285 targetAlpha = 0.2f; 1286 } 1287 1288 float localAlpha = 1.0f - (1.0f - targetAlpha) * hoverFadeFactor; 1289 1290 int finalA = (int)(baseA * localAlpha * 255); 1291 cb.Colors[i] = Color.FromArgb(finalA, 255, 255, 255); 1292 } 1293 bText.InterpolationColors = cb; 1294 1295 g.CompositingQuality = CompositingQuality.HighQuality; 1296 g.FillPath(bText, path); 1297 } 1298 } 1299 } 1300 1301 using (var pen = new Pen(Color.FromArgb(180, 255, 255, 255), 1.5f)) 1302 g.DrawLine(pen, smoothCurX, spectY, smoothCurX, spectY + spectH); 1303 } 1304 else 1305 { 1306 float phase = waveLoadAnim; 1307 for (int i = 0; i < w; i += 6) 1308 { 1309 float amp = 0.1f + 0.1f * (float)Math.Sin(phase + i * 0.1f); 1310 int hh = (int)(amp * spectH); 1311 using (var br = new SolidBrush(Color.FromArgb(100, accentColor.R, accentColor.G, accentColor.B))) 1312 g.FillRectangle(br, i, centerY - hh / 2, 3, hh); 1313 } 1314 } 1315 } 1316 1317 // ===== Volume indicator ===== 1318 private void PaintVolumeIndicator(Graphics g, int w, int h) 1319 { 1320 double elapsed = (DateTime.Now - volLastChange).TotalMilliseconds; 1321 if (elapsed > 1200) 1322 { 1323 float t = (float)((elapsed - 1200) / 600.0); 1324 volIndicatorAlpha = Math.Max(0f, 1f - t); 1325 } 1326 if (volIndicatorAlpha <= 0.01f) return; 1327 float a = volIndicatorAlpha; 1328 1329 int barW = 4, barH = 140; 1330 int bx = w / 2 - barW / 2; 1331 int by = h / 2 - barH / 2; 1332 1333 g.CompositingQuality = CompositingQuality.HighQuality; 1334 g.SmoothingMode = SmoothingMode.AntiAlias; 1335 1336 using (var brBg = new SolidBrush(Color.FromArgb((int)(a * 50), 0, 0, 0))) 1337 g.FillRectangle(brBg, bx, by, barW, barH); 1338 1339 int fillH = (int)(barH * volume); 1340 if (fillH > 0) 1341 { 1342 using (var brFill = new SolidBrush(Color.FromArgb((int)(a * 255), 255, 255, 255))) 1343 g.FillRectangle(brFill, bx, by + barH - fillH, barW, fillH); 1344 1345 using (var brPoint = new SolidBrush(Color.FromArgb((int)(a * 255), 255, 255, 255))) 1346 g.FillEllipse(brPoint, bx - 3, by + barH - fillH - 5, 10, 10); 1347 } 1348 1349 string volStr = (int)(volume * 100) + "%"; 1350 using (var font = new Font(_bebasFont ?? FontFamily.GenericSansSerif, 24f, FontStyle.Regular, GraphicsUnit.Pixel)) 1351 { 1352 g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAlias; 1353 var sz = g.MeasureString(volStr, font); 1354 using (var brText = new SolidBrush(Color.FromArgb((int)(a * 255), 255, 255, 255))) 1355 g.DrawString(volStr, font, brText, w / 2 - sz.Width / 2, by + barH + 15); 1356 } 1357 } 1358 1359 private string FormatTime(double s) 1360 { 1361 if (double.IsNaN(s) || s < 0) s = 0; 1362 int ts = (int)s; 1363 return (ts / 60) + ":" + (ts % 60).ToString("D2"); 1364 } 1365 1366 // ===== Mouse ===== 1367 protected override void OnMouseEnter(EventArgs e) 1368 { 1369 base.OnMouseEnter(e); 1370 Focus(); 1371 } 1372 1373 protected override void OnKeyDown(KeyEventArgs e) 1374 { 1375 base.OnKeyDown(e); 1376 1377 if (e.KeyCode == Keys.Space) 1378 { 1379 MciToggle(); 1380 e.Handled = true; 1381 } 1382 else if (e.KeyCode == Keys.Left) 1383 { 1384 int newPos = (int)(currentPos * 1000) - 5000; 1385 MciSeek(Math.Max(0, newPos)); 1386 e.Handled = true; 1387 } 1388 else if (e.KeyCode == Keys.Right) 1389 { 1390 int newPos = (int)(currentPos * 1000) + 5000; 1391 MciSeek(Math.Min((int)(duration * 1000), newPos)); 1392 e.Handled = true; 1393 } 1394 else if (e.KeyCode == Keys.Up) 1395 { 1396 volume = Math.Min(1f, volume + 0.05f); 1397 MciSetVolume(volume); 1398 ShowVol(); 1399 e.Handled = true; 1400 } 1401 else if (e.KeyCode == Keys.Down) 1402 { 1403 volume = Math.Max(0f, volume - 0.05f); 1404 MciSetVolume(volume); 1405 ShowVol(); 1406 e.Handled = true; 1407 } 1408 else if (e.KeyCode == Keys.Escape) 1409 { 1410 Close(); 1411 } 1412 } 1413 1414 private void ShowVol() 1415 { 1416 volIndicatorAlpha = 1f; 1417 volLastChange = DateTime.Now; 1418 if (!volFadeTimer.Enabled) volFadeTimer.Start(); 1419 Invalidate(); 1420 } 1421 1422 private void LoadMetadata(string path) 1423 { 1424 currentTrackTitle = Path.GetFileNameWithoutExtension(path); 1425 currentTrackArtist = ""; 1426 try 1427 { 1428 string ext = Path.GetExtension(path).ToLowerInvariant(); 1429 if (ext == ".mp3") ExtractId3Metadata(path); 1430 } 1431 catch { } 1432 } 1433 1434 private void ExtractId3Metadata(string path) 1435 { 1436 try { 1437 using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read)) 1438 { 1439 var hdr = new byte[10]; 1440 if (fs.Read(hdr, 0, 10) < 10 || hdr[0] != 'I' || hdr[1] != 'D' || hdr[2] != '3') return; 1441 int tagSize = ((hdr[6] & 0x7F) << 21) | ((hdr[7] & 0x7F) << 14) | ((hdr[8] & 0x7F) << 7) | (hdr[9] & 0x7F); 1442 long tagEnd = 10 + tagSize; 1443 while (fs.Position + 10 < tagEnd) 1444 { 1445 var fh = new byte[10]; 1446 if (fs.Read(fh, 0, 10) < 10) break; 1447 string id = Encoding.ASCII.GetString(fh, 0, 4); 1448 int sz = (fh[4] << 24) | (fh[5] << 16) | (fh[6] << 8) | fh[7]; 1449 if (sz <= 0 || sz > 10 * 1024 * 1024) break; 1450 if (id == "TIT2") currentTrackTitle = ReadId3String(fs, sz); 1451 else if (id == "TPE1") currentTrackArtist = ReadId3String(fs, sz); 1452 else fs.Seek(sz, SeekOrigin.Current); 1453 } 1454 } 1455 } catch { } 1456 } 1457 1458 private string ReadId3String(FileStream fs, int sz) 1459 { 1460 if (sz <= 1) { fs.Seek(sz, SeekOrigin.Current); return ""; } 1461 var d = new byte[sz]; fs.Read(d, 0, sz); 1462 byte enc = d[0]; 1463 string res = ""; 1464 try { 1465 if (enc == 1) res = Encoding.Unicode.GetString(d, 1, d.Length - 1); 1466 else if (enc == 2) res = Encoding.BigEndianUnicode.GetString(d, 1, d.Length - 1); 1467 else if (enc == 3) res = Encoding.UTF8.GetString(d, 1, d.Length - 1); 1468 else res = Encoding.Default.GetString(d, 1, d.Length - 1); 1469 } catch { } 1470 return res.Trim('\0', ' ', '\r', '\n'); 1471 } 1472 1473 protected override void OnMouseDown(MouseEventArgs e) 1474 { 1475 base.OnMouseDown(e); 1476 1477 if (e.Button == MouseButtons.Left) 1478 { 1479 if (CloseBtnRect.Contains(e.Location)) { Close(); return; } 1480 if (MaxBtnRect.Contains(e.Location)) { 1481 WindowState = (WindowState == FormWindowState.Maximized) ? FormWindowState.Normal : FormWindowState.Maximized; 1482 return; 1483 } 1484 if (MinBtnRect.Contains(e.Location)) { WindowState = FormWindowState.Minimized; return; } 1485 } 1486 1487 if (e.Button == MouseButtons.Left) 1488 { 1489 int artAreaH = Height - StripH; 1490 lmbStartX = e.X; 1491 lmbDown = true; 1492 lmbDragging = false; 1493 seekStartPos = currentPos; 1494 1495 if (e.X >= Width - ResizeEdge) 1496 { 1497 resizing = true; 1498 resizeStartX = e.X + Left; 1499 resizeStartW = Width; 1500 Capture = true; 1501 return; 1502 } 1503 1504 if (e.Y >= artAreaH) 1505 { 1506 Capture = true; 1507 return; 1508 } 1509 1510 if (e.X < 70 || e.X > Width - 70) 1511 { 1512 return; 1513 } 1514 1515 Capture = true; 1516 } 1517 else if (e.Button == MouseButtons.Right) 1518 { 1519 rmbDown = true; 1520 rmbStartX = e.X; 1521 rmbSwitched = false; 1522 } 1523 } 1524 1525 protected override void OnMouseMove(MouseEventArgs e) 1526 { 1527 base.OnMouseMove(e); 1528 1529 if (resizing) 1530 { 1531 int newW = Math.Max(260, resizeStartW + (e.X + Left - resizeStartX)); 1532 if (newW != Width) 1533 { 1534 Width = newW; 1535 artH = albumArt != null ? ComputeArtHeight() : 0; 1536 Height = artH + StripH; 1537 Invalidate(); 1538 } 1539 return; 1540 } 1541 1542 bool onClose = CloseBtnRect.Contains(e.Location); 1543 if (onClose != closeBtnHover) { closeBtnHover = onClose; Invalidate(); } 1544 1545 bool onMax = MaxBtnRect.Contains(e.Location); 1546 if (onMax != maxBtnHover) { maxBtnHover = onMax; Invalidate(); } 1547 1548 bool onMin = MinBtnRect.Contains(e.Location); 1549 if (onMin != minBtnHover) { minBtnHover = onMin; Invalidate(); } 1550 1551 Cursor = (e.X >= Width - ResizeEdge) ? Cursors.SizeWE : Cursors.Default; 1552 1553 if (lmbDown) 1554 { 1555 int dx = e.X - lmbStartX; 1556 if (!lmbDragging && (Math.Abs(dx) > 5 || Math.Abs(e.Y - (Height - StripH/2)) > 5)) 1557 lmbDragging = true; 1558 1559 if (lmbDragging) 1560 { 1561 int artAreaH = Height - StripH; 1562 if (e.Y >= artAreaH) 1563 { 1564 if (duration > 0) 1565 { 1566 double newPos = Math.Max(0, Math.Min(duration, seekStartPos + (double)dx / Width * duration)); 1567 currentPos = newPos; 1568 if ((DateTime.Now - lastMciSeekTime).TotalMilliseconds > 25) 1569 { 1570 MciSeek((int)(newPos * 1000)); 1571 lastMciSeekTime = DateTime.Now; 1572 } 1573 } 1574 } 1575 else if (e.X >= 70 && e.X <= Width - 70) 1576 { 1577 WinApi.ReleaseCapture(); 1578 WinApi.SendMessage(Handle, 0xA1, 2, 0); 1579 lmbDown = false; 1580 } 1581 Invalidate(); 1582 } 1583 } 1584 1585 if (rmbDown) 1586 { 1587 int dx = e.X - rmbStartX; 1588 if (Math.Abs(dx) > (rmbSwitched ? 80 : 55)) 1589 { 1590 rmbSwitched = true; 1591 BrowseFile(dx > 0 ? 1 : -1); 1592 rmbStartX = e.X; 1593 } 1594 } 1595 } 1596 1597 protected override void OnMouseUp(MouseEventArgs e) 1598 { 1599 base.OnMouseUp(e); 1600 if (e.Button == MouseButtons.Left) 1601 { 1602 if (resizing) { resizing = false; Capture = false; return; } 1603 1604 if (!lmbDragging) 1605 { 1606 int artAreaH = Height - StripH; 1607 if (e.Y < artAreaH) 1608 { 1609 if (e.X < 70) { BrowseFile(-1); } 1610 else if (e.X > Width - 70) { BrowseFile(1); } 1611 else { MciToggle(); } 1612 } 1613 else 1614 { 1615 MciToggle(); 1616 } 1617 } 1618 1619 lmbDown = false; 1620 lmbDragging = false; 1621 Capture = false; 1622 Invalidate(); 1623 } 1624 else if (e.Button == MouseButtons.Right) 1625 { 1626 rmbDown = false; 1627 } 1628 } 1629 1630 private void AdjustVol(float diff) 1631 { 1632 volume = Math.Max(0f, Math.Min(1f, volume + diff)); 1633 MciSetVolume(volume); 1634 volIndicatorAlpha = 1f; 1635 volLastChange = DateTime.Now; 1636 if (!volFadeTimer.Enabled) volFadeTimer.Start(); 1637 Invalidate(); 1638 } 1639 1640 protected override void OnMouseWheel(MouseEventArgs e) 1641 { 1642 base.OnMouseWheel(e); 1643 AdjustVol(e.Delta > 0 ? 0.05f : -0.05f); 1644 } 1645 1646 // ===== WndProc (borderless + drag zone) ===== 1647 protected override void WndProc(ref Message m) 1648 { 1649 switch (m.Msg) 1650 { 1651 case WM_MOUSEWHEEL: 1652 int delta = (short)((m.WParam.ToInt64() >> 16) & 0xFFFF); 1653 AdjustVol(delta > 0 ? 0.05f : -0.05f); 1654 m.Result = IntPtr.Zero; 1655 return; 1656 1657 case WM_NCCALCSIZE: 1658 m.Result = IntPtr.Zero; 1659 return; 1660 1661 case WM_NCACTIVATE: 1662 m.Result = new IntPtr(1); 1663 return; 1664 1665 case WM_NCHITTEST: 1666 m.Result = new IntPtr(HTCLIENT); 1667 return; 1668 } 1669 base.WndProc(ref m); 1670 } 1671 1672 private void GetAlbumColors(Bitmap bmp, out Color primary, out Color secondary) 1673 { 1674 primary = Color.FromArgb(0, 188, 212); 1675 secondary = Color.FromArgb(255, 255, 255); 1676 if (bmp == null) return; 1677 1678 try 1679 { 1680 var samples = new List<Color>(); 1681 int stepX = Math.Max(1, bmp.Width / 15); 1682 int stepY = Math.Max(1, bmp.Height / 15); 1683 for (int x = 0; x < bmp.Width; x += stepX) 1684 { 1685 for (int y = 0; y < bmp.Height; y += stepY) 1686 { 1687 samples.Add(bmp.GetPixel(x, y)); 1688 } 1689 } 1690 1691 var sorted = samples.FindAll(c => { 1692 float h, s, v; 1693 ColorToHSV(c, out h, out s, out v); 1694 return s > 0.2f && v > 0.2f; 1695 }); 1696 1697 if (sorted.Count < 2) 1698 { 1699 if (samples.Count > 0) primary = samples[samples.Count / 2]; 1700 return; 1701 } 1702 1703 primary = sorted[0]; 1704 1705 float h1, s1, v1; 1706 ColorToHSV(primary, out h1, out s1, out v1); 1707 1708 secondary = primary; 1709 foreach (var c in sorted) 1710 { 1711 float h2, s2, v2; 1712 ColorToHSV(c, out h2, out s2, out v2); 1713 if (Math.Abs(h2 - h1) > 30) 1714 { 1715 secondary = c; 1716 break; 1717 } 1718 } 1719 1720 primary = BoostColor(primary); 1721 secondary = BoostColor(secondary); 1722 } 1723 catch { } 1724 } 1725 1726 private Color BoostColor(Color c) 1727 { 1728 float h, s, v; 1729 ColorToHSV(c, out h, out s, out v); 1730 if (s < 0.5f) s = 0.75f; 1731 if (v < 0.6f) v = 0.9f; 1732 return ColorFromHSV(h, s, v); 1733 } 1734 1735 private Color GetDominantColor(Bitmap bmp) 1736 { 1737 Color p, s; 1738 GetAlbumColors(bmp, out p, out s); 1739 return p; 1740 } 1741 1742 private void ColorToHSV(Color color, out float hue, out float saturation, out float value) 1743 { 1744 hue = color.GetHue(); 1745 int max = Math.Max(color.R, Math.Max(color.G, color.B)); 1746 int min = Math.Min(color.R, Math.Min(color.G, color.B)); 1747 saturation = (max == 0) ? 0 : 1f - (1f * min / max); 1748 value = max / 255f; 1749 } 1750 1751 private Color ColorFromHSV(double hue, double saturation, double value) 1752 { 1753 int hi = Convert.ToInt32(Math.Floor(hue / 60)) % 6; 1754 double f = hue / 60 - Math.Floor(hue / 60); 1755 value = value * 255; 1756 int v = Convert.ToInt32(value); 1757 int p = Convert.ToInt32(value * (1 - saturation)); 1758 int q = Convert.ToInt32(value * (1 - f * saturation)); 1759 int t = Convert.ToInt32(value * (1 - (1 - f) * saturation)); 1760 if (hi == 0) return Color.FromArgb(255, v, t, p); 1761 else if (hi == 1) return Color.FromArgb(255, q, v, p); 1762 else if (hi == 2) return Color.FromArgb(255, p, v, t); 1763 else if (hi == 3) return Color.FromArgb(255, p, q, v); 1764 else if (hi == 4) return Color.FromArgb(255, t, p, v); 1765 else return Color.FromArgb(255, v, p, q); 1766 } 1767 1768 protected override CreateParams CreateParams 1769 { 1770 get 1771 { 1772 var cp = base.CreateParams; 1773 cp.Style |= unchecked((int)0x80000000); // WS_POPUP 1774 return cp; 1775 } 1776 } 1777 } 1778}