"""Bölüm başına tek parça ses miksi: konuşmacı(lar) + seyirci, dengelenmiş, patlamalar kesilmiş.

Eğitim:  eğitmen (Mik 2) sürekli açık; seyirci kaynağı (Mik 1, yoksa seyirci_ayir.py çıktısı) sadece seyirci
         konuşurken açılır (kapalıyken -12 dB; eğitmenin seyirci mikrofonuna sızan sesi yankı yapmasın).
Sohbet:  Mik 1 ve Mik 2 eşit; hangisi konuşuyorsa o açık, öbürü -10 dB. 23:00 öncesi sadece Kamera B sesi.
Her kaynak, konuşma anlarında aynı seviyeye çekilir (yavaş otomatik kazanç, en çok +18 dB).
Son aşama (ffmpeg): 80 Hz alçak kesim, kompresör, limiter, EBU R128 -16 LUFS (iki geçiş), en sonda -2 dB limiter (tepe ≤ -1.5 dBTP).

Kullanım: python3 miks.py [Eğitim|Sohbet]   Çıktı: <Bölüm>/Miks/<Bölüm>_miks.WAV (BWF time_reference ile)
"""
import json, os, re, subprocess, sys
import numpy as np

KOK = "/home/stark/Desktop/Eğitim - 01"
S = 48000; F = 480                       # 10 ms dilim
PARCA = 60.0


def oku(p, bas, sure, sr=S):
    r = subprocess.run(["ffmpeg", "-v", "error", "-nostdin", "-ss", f"{max(bas, 0):.4f}", "-t", f"{sure:.4f}", "-i", p, "-map", "0:a:0",
                        "-ac", "1", "-ar", str(sr), "-f", "f32le", "-"], capture_output=True).stdout
    return np.frombuffer(r, np.float32)


def zarf(yol, sure):
    """10 ms'lik RMS (dB), 16 kHz'de okunarak hızlı."""
    x = oku(yol, 0, sure + 1, 16000); m = len(x) // 160
    return 10 * np.log10(np.mean(x[:m * 160].reshape(m, 160).astype(np.float64) ** 2, axis=1) + 1e-10)


def yumusat(g, atak, birak):
    """Kazanç eğrisi: açılırken 'atak', kapanırken 'birak' sn (10 ms dilimlerde tek kutuplu)."""
    a1 = np.exp(-0.01 / atak); a2 = np.exp(-0.01 / birak); y = np.empty_like(g); v = g[0]
    for i, x in enumerate(g):
        v = (a1 if x > v else a2) * v + (1 - (a1 if x > v else a2)) * x; y[i] = v
    return y


def tutma(m, n):
    """Açık maskeyi n dilim uzat (cümle sonları kesilmesin)."""
    k = np.convolve(m.astype(float), np.ones(n), "full")[:len(m)]
    return k > 0


class Iz:
    def __init__(self, ad, parcalar, b0, n):
        """parcalar: [{yol, bas, sure}] gerçek saatle; zaman çizelgesi b0'dan başlar, n dilim."""
        self.ad = ad; self.parcalar = parcalar
        self.db = np.full(n, np.nan)
        for p in parcalar:
            e = zarf(p["yol"], p["sure"]); i0 = int(round((p["bas"] - b0) * 100))
            j0 = max(0, -i0); j1 = min(len(e), n - i0)
            if j1 > j0: self.db[i0 + j0:i0 + j1] = e[j0:j1]
        self.var = ~np.isnan(self.db)

    def ses(self, t0, sure, b0):
        """Zaman çizelgesinde [t0, t0+sure) (b0'a göreli sn) için ses."""
        out = np.zeros(int(round(sure * S)), np.float32)
        for p in self.parcalar:
            a = max(b0 + t0, p["bas"]); b = min(b0 + t0 + sure, p["bas"] + p["sure"])
            if b <= a: continue
            x = oku(p["yol"], a - p["bas"], b - a); i = int(round((a - b0 - t0) * S))
            L = min(len(x), len(out) - i); out[i:i + L] = x[:L]
        return out


def konusma(db, var):
    """Konuşma anı: gürültü tabanının 12 dB üstü (taban: kaynağın 10. yüzdeliği)."""
    taban = np.nanpercentile(db[var], 10)
    return var & (np.nan_to_num(db, nan=-200) > taban + 12)


def seviye_kazanci(db, acik, hedef, en_cok=18.0, pencere=300):
    """Açık anlardaki seviyeyi 'hedef'e çeken yavaş kazanç (dB, ~3 sn pencere)."""
    x = np.where(acik, 10 ** (np.nan_to_num(db, nan=-200) / 10), 0.0); c = acik.astype(float)
    k = np.ones(pencere)
    ort = np.convolve(x, k, "same") / np.maximum(np.convolve(c, k, "same"), 1)
    sev = 10 * np.log10(ort + 1e-10)
    g = np.clip(hedef - sev, -12, en_cok)
    g[np.convolve(c, k, "same") < 10] = 0.0          # yeterli konuşma yoksa dokunma
    return g


def main(bolum):
    z = json.load(open(f"{KOK}/Önizleme/medya/zaman.json"))["bolumler"][bolum]
    b0, b1 = z["bas"], z["son"]; n = int((b1 - b0) * 100) + 1
    ses = z["ses"]
    if bolum == "Eğitim" and os.environ.get("MIKS_MOD", "birlesik") == "birlesik":
        # Kamera A'nın birleşik sesi (alıcı Merged: Mik 1 + Mik 2 canlı karışım) temel ses; hoca ve seyirci doğal
        # dengede, geçiş/açıp kapama yok. Kamera A'nın olmadığı yerde hoca mikrofonu.
        ana = Iz("Kamera A birleşik", ses["Kamera A sesi"], b0, n)
        yedek = Iz("Hoca mik. (yedek)", [p for p in ses["Mikrofon 2"] if p["sure"] > 30], b0, n)
        K_ = konusma(ana.db, ana.var); hedef = np.median(ana.db[K_])
        ana.kapi = np.where(ana.var, 0.0, -90.0); ana.g = seviye_kazanci(ana.db, K_, hedef, en_cok=10, pencere=1000)
        yedek.kapi = yumusat(np.where(ana.var, -90.0, 0.0), 0.03, 0.03)
        Ky = konusma(yedek.db, yedek.var); yedek.g = seviye_kazanci(yedek.db, Ky & ~ana.var, hedef, en_cok=10, pencere=1000)
        izler = [ana, yedek]
        print(f"  Kamera A birleşik ses: kapsam %{ana.var.mean() * 100:.1f}", flush=True)
    elif bolum == "Eğitim":
        ana = Iz("Eğitmen", [p for p in ses["Mikrofon 2"] if p["sure"] > 30], b0, n)
        m1 = [p for p in ses["Mikrofon 1"] if p["sure"] > 30]
        ayr = [p for p in ses.get("Seyirci (ayrılmış)", [])
               if not any(q["bas"] <= p["bas"] + p["sure"] and p["bas"] <= q["bas"] + q["sure"] for q in m1)]
        # seyirci_ayir çıktısı Mik 1'le çakışıyorsa (C1970 / 00050) çakışan kısmı Mik 1'den al
        for p in ses.get("Seyirci (ayrılmış)", []):
            if p not in ayr:
                q = min(m1, key=lambda q: abs(q["bas"] - p["bas"]))
                if p["bas"] < q["bas"]:
                    ayr.append(dict(p, sure=q["bas"] - p["bas"]))
        yan = [Iz("Seyirci Mik 1", m1, b0, n), Iz("Seyirci ayrılmış", ayr, b0, n)]
        # Her an TEK mikrofon açık (iki mikrofon yakınken ikisi birden açıksa yankı olur).
        # 1) Hoca konuşuyor mu: hoca mikrofonundaki konuşmacı ayrımında (pyannote) hocanın etiketi.
        # 2) Hoca konuşmuyorsa ve seyirci kaynağında sızıntının belirgin üstünde ses varsa seyirci mikrofonu.
        # 3) Diğer her durumda (sessizlik, hocanın yanındaki öğrenci) hoca mikrofonu.
        def guc_ort(db, n_):
            p = 10 ** (np.nan_to_num(db, nan=-200) / 10); k = np.ones(n_) / n_
            return 10 * np.log10(np.convolve(p, k, "same") + 1e-20)
        import collections
        hoca = np.zeros(n, bool)
        for p in ana.parcalar:
            ad = os.path.basename(p["yol"])[:-4]; T = json.load(open(f"{KOK}/Önizleme/kurgu/diar/{ad}.json"))
            sure_ = collections.Counter()
            for x, y, k in T: sure_[k] += y - x
            sahip = sure_.most_common(1)[0][0]
            for x, y, k in T:
                if k == sahip:
                    hoca[max(int((p["bas"] + x - b0) * 100) - 10, 0):int((p["bas"] + y - b0) * 100) + 15] = True
        Eg = guc_ort(ana.db, 10)
        s_acik = np.zeros(n, bool)
        for iz in yan:
            if not iz.var.any(): continue
            Sg = guc_ort(iz.db, 10); d = Sg - Eg
            sec = hoca & iz.var & konusma(ana.db, ana.var)
            sizinti = np.median(d[sec])
            taban = np.nanpercentile(iz.db[iz.var], 20)
            ses_var = iz.var & (np.nan_to_num(Sg, nan=-200) > taban + 6)
            # hoca susarken: sızıntının 5 dB üstü yeter; hoca konuşurken: seyirci mikrofonu hocanınkinden açıkça yüksekse
            # sadece insan konuşması: bu kaynağın kendi konuşmacı ayrımındaki (pyannote) konuşma bölgeleri;
            # hışırtı, çarpma, sandalye sesi bunun dışında kalır ve mikrofonu açmaz
            konusma_var = np.zeros(n, bool)
            for p in iz.parcalar:
                yol = f"{KOK}/Önizleme/kurgu/diar/{os.path.basename(p['yol'])[:-4]}.json"
                if not os.path.exists(yol): continue
                for x, y, _k in json.load(open(yol)):
                    if y - x >= 0.3:
                        konusma_var[max(int((p["bas"] + x - b0) * 100) - 5, 0):int((p["bas"] + y - b0) * 100) + 10] = True
            m = ses_var & konusma_var & ((~hoca & (d > sizinti + 5)) | (d > max(sizinti + 14, 2.0)))
            d_ = np.flatnonzero(np.diff(np.concatenate([[0], m.astype(int), [0]])))
            for a_, b_ in zip(d_[::2], d_[1::2]):
                if b_ - a_ < 10: m[a_:b_] = False                     # 100 ms'den kısa: gürültü
            m = np.convolve(m.astype(float), np.ones(56), "full")[15:15 + n] > 0   # 0.15 sn önce aç, 0.4 sn sonra kapa
            m &= iz.var & konusma_var
            print(f"  {iz.ad}: sızıntı {sizinti:+.1f} dB, açık %{m.mean() * 100:.1f}", flush=True)
            s_acik |= m
        # 0.3 sn'den kısa hoca aralarını da seyircide tut (cümle içinde gidip gelmesin)
        d = np.flatnonzero(np.diff(s_acik.astype(int))) + 1; sinir = np.concatenate([[0], d, [n]])
        for a_, b_ in zip(sinir, sinir[1:]):
            if not s_acik[a_] and b_ - a_ < 30 and a_ > 0 and b_ < n: s_acik[a_:b_] = True
        d = np.flatnonzero(np.diff(s_acik.astype(int))) + 1
        yan_var = yan[0].var | yan[1].var
        s_acik &= yan_var                                                # seyirci kaynağı yoksa hoca açık kalır
        ac = lambda m: yumusat(np.where(m, 0.0, -90.0), 0.03, 0.03)      # 30 ms geçiş, kapalıyken tamamen sessiz
        E = konusma(ana.db, ana.var)
        hedef = np.median(ana.db[E & ~s_acik])
        ana.kapi = ac(~s_acik)
        ana.g = seviye_kazanci(ana.db, E & ~s_acik, hedef, en_cok=8, pencere=1000)
        for iz in yan:
            if not iz.var.any(): iz.g = np.zeros(n); iz.kapi = np.full(n, -90.0); continue
            m = s_acik & iz.var
            iz.kapi = ac(m)
            iz.g = seviye_kazanci(iz.db, m & konusma(iz.db, iz.var), hedef, en_cok=18, pencere=1000)
            print(f"  {iz.ad}: açık %{m.mean() * 100:.1f}, ort. kazanç {np.mean(iz.g[m]) if m.any() else 0:+.1f} dB", flush=True)
        np.save(f"{KOK}/Önizleme/kurgu/seyirci_acik.npy", np.packbits(s_acik)); json.dump({"b0": b0, "n": n}, open(f"{KOK}/Önizleme/kurgu/seyirci_acik.json", "w"))
        izler = [ana] + yan
        print(f"  Hoca mikrofonu açık %{(~s_acik).mean() * 100:.1f}; geçiş sayısı {len(d)}", flush=True)
    else:
        a = Iz("Mik 1", [p for p in ses["Mikrofon 1"] if p["sure"] > 30], b0, n)
        b = Iz("Mik 2", [p for p in ses["Mikrofon 2"] if p["sure"] > 30], b0, n)
        kb = Iz("Kamera B", [dict(yol=v["yol"], bas=v["bas"], sure=v["sure"]) for v in z["video"]["B"]
                             if v["bas"] + v["sure"] < min(p["bas"] for p in a.parcalar + b.parcalar) and v["sure"] > 10], b0, n)
        Ka, Kb = konusma(a.db, a.var), konusma(b.db, b.var)
        hedef = np.median(np.concatenate([a.db[Ka], b.db[Kb]]))
        fark = np.nan_to_num(a.db, nan=-200) - np.nan_to_num(b.db, nan=-200)
        for iz, isaret, K in ((a, 1, Ka), (b, -1, Kb)):
            acik = tutma(iz.var & K & (isaret * fark > -3), 25)
            iz.kapi = yumusat(np.where(acik, 0.0, -10.0), 0.01, 0.3)
            iz.g = seviye_kazanci(iz.db, acik & K, hedef, en_cok=15)
        kb.kapi = np.zeros(n); kb.g = seviye_kazanci(kb.db, konusma(kb.db, kb.var), hedef, en_cok=15) if kb.var.any() else np.zeros(n)
        izler = [a, b, kb]

    cikti_dir = f"{KOK}/{bolum}/Miks"; os.makedirs(cikti_dir, exist_ok=True)
    ara = f"{cikti_dir}/.ara.wav"; son = f"{cikti_dir}/{bolum}_miks.WAV"
    ON = "highpass=f=80,acompressor=threshold=-22dB:ratio=3:attack=8:release=200:knee=4:makeup=2,alimiter=limit=0.708:attack=3:release=60:level=false"
    ff = subprocess.Popen(["ffmpeg", "-v", "error", "-nostdin", "-y", "-f", "f32le", "-ar", str(S), "-ac", "1", "-i", "-",
                           "-af", ON, "-c:a", "pcm_f32le", ara], stdin=subprocess.PIPE)
    t = 0.0; sure_top = b1 - b0
    while t < sure_top:
        L = min(PARCA, sure_top - t); mix = np.zeros(int(round(L * S)), np.float32)
        i0 = int(t * 100); i1 = i0 + int(np.ceil(L * 100)) + 1
        for iz in izler:
            if not iz.var[i0:i1].any(): continue
            x = iz.ses(t, L, b0)
            gdb = (iz.g + iz.kapi)[i0:i1]
            g = np.interp(np.arange(len(x)) / S * 100, np.arange(len(gdb)), 10 ** (gdb / 20)).astype(np.float32)
            mix += x * g
        ff.stdin.write(mix.tobytes()); t += PARCA
        print(f"\r  miks {t / sure_top * 100:5.1f}%", end="", flush=True)
    ff.stdin.close(); ff.wait(); print()
    # EBU R128 iki geçiş: önce ölç, sonra doğrusal uygula
    r = subprocess.run(["ffmpeg", "-nostdin", "-hide_banner", "-i", ara, "-af", "loudnorm=I=-16:TP=-1.5:LRA=11:print_format=json", "-f", "null", "-"],
                       capture_output=True, text=True).stderr
    m = json.loads(re.search(r"\{[^{}]*\"input_i\"[^{}]*\}", r, re.S).group(0))
    ln = (f"loudnorm=I=-16:TP=-1.5:LRA=11:measured_I={m['input_i']}:measured_TP={m['input_tp']}:measured_LRA={m['input_lra']}"
          f":measured_thresh={m['input_thresh']}:offset={m['target_offset']}:linear=true")
    subprocess.run(["ffmpeg", "-v", "error", "-nostdin", "-y", "-i", ara, "-af", ln + ",alimiter=limit=0.79:attack=1:release=50:level=false", "-ar", str(S), "-c:a", "pcm_s24le",
                    "-write_bext", "1", "-metadata", f"time_reference={round(b0 * S)}",
                    "-metadata", f"comment={bolum} miks (miks.py)", son], check=True)
    os.remove(ara)
    print(f"  ölçülen {m['input_i']} LUFS → -16 LUFS; yazıldı: {son}")


if __name__ == "__main__":
    for b in (sys.argv[1:] or ["Eğitim", "Sohbet"]):
        print(b, flush=True); main(b)
