#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Bed musik instrumental untuk reel IG StudioSoft — dibangkitkan sendiri (bebas royalti).

Keluaran: WAV 44.1 kHz stereo, loopable (jumlah bar bulat), siap di-mix ke reel oleh
`ss_reel_musik.py`. Dua rasa:
  kalem  — pad hangat + arpeggio lembut (cocok untuk konten edukasi)
  ceria  — tempo lebih cepat + bass jalan + perkusi ringan (untuk konten tips/promo)

Pakai:  /usr/bin/python3 musik_bed.py kalem 24 /home/mantapman/.hermes/profiles/atun/workspace/musik/kalem_24s.wav
"""
import sys
import wave

import numpy as np

SR = 44100


def nada(nama, oktaf):
    """Frekuensi nada dari nama + oktaf (A4 = 440)."""
    dasar = {"C": -9, "C#": -8, "D": -7, "D#": -6, "E": -5, "F": -4,
             "F#": -3, "G": -2, "G#": -1, "A": 0, "A#": 1, "B": 2}
    return 440.0 * 2 ** ((dasar[nama] + 12 * (oktaf - 4)) / 12.0)


def pluck(f, dur, amp=0.5, harmonik=(1.0, 0.5, 0.25, 0.12)):
    """Nada petik: beberapa harmonik dengan peluruhan eksponensial."""
    n = int(SR * dur)
    t = np.arange(n) / SR
    x = np.zeros(n)
    for i, a in enumerate(harmonik, start=1):
        x += a * np.sin(2 * np.pi * f * i * t)
    env = np.exp(-3.2 * t) * (1 - np.exp(-400 * t))
    return amp * x * env


def pad(f, dur, amp=0.18):
    """Pad hangat: dua osilator sedikit detune + attack lambat."""
    n = int(SR * dur)
    t = np.arange(n) / SR
    x = (np.sin(2 * np.pi * f * t) +
         0.6 * np.sin(2 * np.pi * f * 1.003 * t) +
         0.3 * np.sin(2 * np.pi * f * 0.5 * t))
    env = np.minimum(1.0, t / 0.6) * np.minimum(1.0, (dur - t) / 0.6)
    return amp * x * np.clip(env, 0, 1)


def bass(f, dur, amp=0.35):
    n = int(SR * dur)
    t = np.arange(n) / SR
    env = np.exp(-1.6 * t) * (1 - np.exp(-300 * t))
    return amp * np.sin(2 * np.pi * f * t) * env


def hit(dur=0.25, amp=0.12):
    """Perkusi ringan: derau yang diredam."""
    n = int(SR * dur)
    t = np.arange(n) / SR
    rng = np.random.default_rng(7)
    noise = rng.normal(0, 1, n)
    # low-pass sederhana (rata-rata bergerak)
    k = np.ones(28) / 28
    noise = np.convolve(noise, k, mode="same")
    return amp * noise * np.exp(-22 * t)


def buat_bed(rasa, durasi):
    """Bangun larik stereo `durasi` detik."""
    if rasa == "ceria":
        bpm, arp_gaya, amp_arp = 104, "jalan", 0.34
    else:
        bpm, arp_gaya, amp_arp = 84, "naik", 0.30

    ketuk = 60.0 / bpm
    bar = 4 * ketuk
    progresi = [("A", 3), ("F", 3), ("C", 4), ("G", 3)]  # Am - F - C - G
    n_total = int(SR * durasi)
    L = np.zeros(n_total)
    R = np.zeros(n_total)

    def tempel(sig, mulai_detik, kanal=None, pan=0.0, amp=1.0):
        i = int(mulai_detik * SR)
        if i >= n_total:
            return
        s = sig[: n_total - i] * amp
        l_gain = np.sqrt(0.5 * (1 - pan))
        r_gain = np.sqrt(0.5 * (1 + pan))
        if kanal in (None, "L"):
            L[i:i + len(s)] += s * (l_gain if kanal is None else 1.0)
        if kanal in (None, "R"):
            R[i:i + len(s)] += s * (r_gain if kanal is None else 1.0)

    n_bar = int(np.ceil(durasi / bar))
    for b in range(n_bar):
        t0 = b * bar
        nama, ok = progresi[b % len(progresi)]
        akar = nada(nama, ok)
        # pad satu bar
        for f, pan in ((akar, -0.25), (akar * 1.5, 0.25), (akar * 2, 0.0)):
            tempel(pad(f, bar * 1.05), t0, pan=pan)
        # bass tiap 2 ketuk
        if rasa == "ceria":
            for k in range(4):
                tempel(bass(nada(nama, ok - 1), ketuk * 0.9), t0 + k * ketuk)
            tempel(hit(), t0)
            tempel(hit(), t0 + 2 * ketuk)
        else:
            tempel(bass(nada(nama, ok - 1), ketuk * 1.8), t0)

        # arpeggio: nada akor dibunyikan berurutan
        akor = [akar, akar * 1.25, akar * 1.5, akar * 2]
        if arp_gaya == "naik":
            urut = [(nada_, k * ketuk) for k, nada_ in enumerate(akor[:3] + akor[1:2])]
        else:
            urut = [(a, k * ketuk / 1.0) for k, a in
                    enumerate([akar * 2, akar, akar * 1.5, akar * 1.25])]
        for f, t_off in urut:
            tempel(pluck(f, min(1.6, bar)), t0 + t_off, pan=0.2 * np.sin(2 * np.pi * t_off / bar),
                   amp=amp_arp)

    # pinggir halus: fade 0,6 dtk awal/akhir supaya loop tidak "jedug"
    fade = int(SR * 0.6)
    for ch in (L, R):
        ch[:fade] *= np.linspace(0, 1, fade)
        ch[-fade:] *= np.linspace(1, 0, fade)

    st = np.stack([L, R], axis=1)
    puncak = np.max(np.abs(st)) or 1.0
    st = np.tanh(1.25 * st / puncak) * 0.86  # lembutkan puncak, bukan hard clip
    return st


def tulis_wav(path, st):
    pcm = np.clip(st, -1, 1)
    pcm = (pcm * 32767).astype("<i2")
    with wave.open(path, "wb") as w:
        w.setnchannels(2)
        w.setsampwidth(2)
        w.setframerate(SR)
        w.writeframes(pcm.tobytes())


def main():
    rasa = sys.argv[1] if len(sys.argv) > 1 else "kalem"
    durasi = float(sys.argv[2]) if len(sys.argv) > 2 else 24.0
    out = sys.argv[3] if len(sys.argv) > 3 else f"/tmp/bed_{rasa}_{int(durasi)}s.wav"
    tulis_wav(out, buat_bed(rasa, durasi))
    import os
    print(f"OK {out} ({os.path.getsize(out)//1024} KB, {durasi:.0f} dtk, rasa={rasa})")


if __name__ == "__main__":
    main()
