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

AudioPlayerForm.cs

1778 строк · 73,756 байт · модуль UI
   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}