Kick & YouTube Audio Maximizer

Boosts quiet sounds, compresses loud peaks.

// ==UserScript==
// @name        Kick & YouTube Audio Maximizer
// @name:tr     Kick & YouTube Ses Dengeleyici
// @description:tr  Sessiz sesleri yükseltir, yüksek sesleri bastırır.
// @description  Boosts quiet sounds, compresses loud peaks.
// @namespace   http://tampermonkey.net/
// @version     1.6
// @author      baris
// @match       *://*.kick.com/*
// @match       *://kick.com/*
// @match       *://*.youtube.com/*
// @match       *://youtube.com/*
// @match       *://youtu.be/*
// @grant       none
// @license     GNU GPLv3
// ==/UserScript==

(() => {
  // --- DEĞİŞİKLİK 1: Arayüz ve Ses Bağlamını global değişkenlerde tutalım ---
  // Bu değişkenler sayesinde UI ve AudioContext sadece bir kez oluşturulacak.
  let ui = null;
  let audioCtx = null;

  // Yeni video yüklendiğinde sadece bir kez init et
  async function initVideo() {
    const video = document.querySelector('video');
    // Eğer video yoksa veya bu videoya zaten işlem uygulandıysa çık
    if (!video || video.dataset.maxApplied) return;
    video.dataset.maxApplied = '1';

    // --- DEĞİŞİKLİK 2: AudioContext'i sadece bir kez oluştur ---
    if (!audioCtx) {
      audioCtx = new AudioContext();
    }

    // Kullanıcı etkileşimi olmadan ses başlamazsa, devam ettirmek için dinleyici ekle
    if (audioCtx.state === 'suspended') {
      const resume = () => { audioCtx.resume(); window.removeEventListener('click', resume); };
      window.addEventListener('click', resume);
    }

    const src = audioCtx.createMediaElementSource(video);
    const comp = audioCtx.createDynamicsCompressor();
    comp.threshold.value = -50;
    comp.knee.value      = 30;
    comp.ratio.value     = 20;
    comp.attack.value    = 0.002;
    comp.release.value   = 0.10;

    const gain = audioCtx.createGain();
    gain.gain.value = 2.5;

    const ana = audioCtx.createAnalyser();
    ana.fftSize = 512;
    const buf = new Uint8Array(ana.fftSize);

    src.connect(comp);
    comp.connect(gain);
    gain.connect(ana);
    ana.connect(audioCtx.destination);

    // --- DEĞİŞİKLİK 3: Arayüzü oluştur veya mevcut olanı getir ---
    // Arayüz daha önce oluşturulmadıysa createUI ile oluşturulacak.
    const g = await createUI();
    const W = g.canvas.width, H = g.canvas.height;
    let smooth = 0, peak = 0, peakT = 0;
    const lerp = (a, b, f) => a + (b - a) * f;

    (function draw() {
      // Eğer video elementi sayfadan kaldırıldıysa (örneğin başka sayfaya geçildi),
      // çizim döngüsünü durdur. Bu, gereksiz işlemci kullanımını önler.
      if (!video.isConnected) {
        return;
      }

      requestAnimationFrame(draw);
      ana.getByteTimeDomainData(buf);
      const rms = Math.sqrt(buf.reduce((s, v) => s + ((v - 128) / 128) ** 2, 0) / buf.length);
      const level = Math.min(rms * gain.gain.value, 1);
      smooth = lerp(smooth, level, 0.18);

      const now = performance.now();
      if (smooth > peak || now - peakT > 2000) { peak = smooth; peakT = now; }
      if (now - peakT > 2000) peak = lerp(peak, smooth, 0.05);

      const hue = lerp(220, 0, smooth);
      g.clearRect(0, 0, W, H);
      g.fillStyle = `hsl(${hue} 80% 50%)`;
      g.fillRect(0, 0, smooth * W, H);
      g.fillStyle = '#fff';
      g.fillRect(peak * W - 1, 0, 2, H);
    })();
  }

  // --- DEĞİŞİKLİK 4: Arayüzü "get-or-create" (varsa getir, yoksa oluştur) mantığıyla yeniden yapılandır ---
  // Bu fonksiyon artık UI'yi sadece bir kez oluşturur ve her zaman aynı context'i döndürür.
  function createUI() {
    return new Promise(res => {
      // Eğer UI zaten oluşturulmuşsa, mevcut context'i hemen döndür.
      if (ui) {
        return res(ui.g);
      }

      const waitDOM = () => document.body
        ? Promise.resolve()
        : new Promise(inner => {
            const id = setInterval(() => {
              if (document.body) {
                clearInterval(id);
                inner();
              }
            }, 200);
          });

      waitDOM().then(() => {
        const wrap = Object.assign(document.createElement('div'), {
          style: `
            position:fixed;
            bottom:80px;
            left:20px;
            z-index:2147483647;
            background:rgba(0,0,0,0.6);
            padding:6px 8px;
            border-radius:8px;
            pointer-events:none;
          `.replace(/\s+/g, ' ')
        });
        wrap.innerHTML = `
          <div style="color:#fff;font-family:monospace;margin-bottom:4px">
            🔊 Maximizer by Barış
          </div>`;
        const canvas = Object.assign(document.createElement('canvas'), {
          width:300, height:20,
          style:'background:#222;border-radius:6px;display:block;'
        });
        wrap.appendChild(canvas);
        document.body.appendChild(wrap);

        // Oluşturulan UI elemanlarını ve context'i global değişkende sakla
        ui = {
          wrap: wrap,
          canvas: canvas,
          g: canvas.getContext('2d')
        };
        // Promise'i çözerek context'i döndür
        res(ui.g);
      });
    });
  }

  // Başlangıç ve dinleyiciler
  (async () => {
    // Sayfa ilk yüklendiğinde dene
    await initVideo();

    // YouTube/Kick SPA navigasyonunda yeniden init et
    let lastUrl = location.href;
    const observerCallback = () => {
      if (location.href !== lastUrl) {
        lastUrl = location.href;
        // Sayfa geçişlerinde videonun yüklenmesi için kısa bir gecikme eklemek faydalıdır
        setTimeout(initVideo, 500);
      }
    };

    // URL değişimlerini hem aralıkla hem de MutationObserver ile takip etmek daha sağlamdır.
    setInterval(observerCallback, 1000);

    // DOM değişimlerinde video yeniden yüklendiyse init et (örneğin, video oynatıcı ilk kez yüklendiğinde)
    new MutationObserver(initVideo).observe(document.body, {
      childList: true,
      subtree: true
    });
  })();
})();