#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Bank bed musik instrumental (bebas royalti, dibangkitkan sendiri) — variasi dari
tipe yang sudah disetujui Bos: pad hangat + arpeggio pluck + bass lembut (kalem) dan
versi lebih hidup dengan bass jalan + perkusi ringan (ceria).

Tujuan: bukan satu lagu diulang terus, tapi beberapa "saudara" yang rasanya sama.

Keluaran WAV 44.1 kHz stereo, loopable, 24 detik:
  musik/kalem_hangat_24s.wav   82 BPM  Am-F-C-G   pad + pluck naik
  musik/kalem_lembut_24s.wav   76 BPM  C-G-Am-F   music box, arpeggio 8
  musik/kalem_malam_24s.wav    88 BPM  Am-Em-F-C   pluck panjang + delay
  musik/ceria_ringan_24s.wav  106 BPM  F-C-G-Am   bass tiap ketuk, hit 2 & 4
  musik/ceria_jalan_24s.wav   112 BPM  Am-F-C-G   bass jalan, shaker 8
  musik/ceria_cerah_24s.wav   100 BPM  C-Am-F-G   mayor, arp naik-turun

Pakai:
  /usr/bin/python3 musik_bank.py                      # semua, 24 dtk
  /usr/bin/python3 musik_bank.py kalem_hangat ceria_cerah
"""
import os
import sys
import wave

import numpy as np

SR = 44100
WS = os.path.dirname(os.path.abspath(__file__))
OUT = os.path.join(WS, "musik")

NADA = {"C": -9, "C#": -8, "D": -7, "D#": -6, "E": -5, "F": -4,
        "F#": -3, "G": -2, "G#": -1, "A": 0, "A#": 1, "B": 2}


def f0(nama, oktaf):
    return 440.0 * 2 ** ((NADA[nama] + 12 * (oktaf - 4)) / 12.0)


def env_ad(t, attack=0.01, decay=3.0):
    return np.exp(-decay * t) * (1 - np.exp(-t / max(attack, 1e-4)))


def pluck(f, dur, amp=0.5, decay=3.2, bright=1.0, detune=0.0):
    n = int(SR * dur)
    t = np.arange(n) / SR
    x = np.zeros(n)
    for i, a in enumerate((1.0, 0.5 * bright, 0.25 * bright, 0.12 * bright), start=1):
        x += a * np.sin(2 * np.pi * f * i * (1 + detune) * t)
    # sedikit "petik" kayu: transient pendek di harmonik tinggi
    x += 0.15 * bright * np.sin(2 * np.pi * f * 6 * t) * np.exp(-40 * t)
    return amp * x * env_ad(t, 0.002, decay)


def musicbox(f, dur, amp=0.4):
    n = int(SR * dur)
    t = np.arange(n) / SR
    x = (np.sin(2 * np.pi * f * t) + 0.28 * np.sin(2 * np.pi * f * 3.01 * t)
         + 0.12 * np.sin(2 * np.pi * f * 5.02 * t))
    return amp * x * env_ad(t, 0.004, 4.2)


def pad(f, dur, amp=0.17, warna=1.0):
    n = int(SR * dur)
    t = np.arange(n) / SR
    x = (np.sin(2 * np.pi * f * t)
         + 0.55 * np.sin(2 * np.pi * f * 1.004 * t + 0.7)
         + 0.28 * np.sin(2 * np.pi * f * 0.5 * t))
    if warna > 0:
        x += warna * 0.10 * np.sin(2 * np.pi * f * 2 * t + 1.1)
    env = np.minimum(1.0, t / 0.8) * np.minimum(1.0, np.maximum(0.0, (dur - t) / 0.8))
    return amp * x * np.clip(env, 0, 1)


def bass(f, dur, amp=0.34):
    n = int(SR * dur)
    t = np.arange(n) / SR
    x = np.sin(2 * np.pi * f * t) + 0.12 * np.sin(2 * np.pi * f * 2 * t)
    return amp * np.tanh(1.4 * x) * env_ad(t, 0.01, 1.7)


def kick(dur=0.22, amp=0.30):
    n = int(SR * dur)
    t = np.arange(n) / SR
    fr = 110 * np.exp(-26 * t) + 46
    x = np.sin(2 * np.pi * np.cumsum(fr) / SR)
    return amp * x * np.exp(-12 * t)


def shaker(dur=0.09, amp=0.09, seed=3):
    n = int(SR * dur)
    t = np.arange(n) / SR
    rng = np.random.default_rng(seed)
    x = rng.normal(0, 1, n)
    # high-pass kasar: beda dengan versi sebelumnya (yang low-pass = "tup")
    x = np.diff(np.concatenate([[0.0], x]))
    return amp * x * np.exp(-38 * t)


def rim(dur=0.12, amp=0.13, seed=11):
    n = int(SR * dur)
    t = np.arange(n) / SR
    rng = np.random.default_rng(seed)
    x = rng.normal(0, 1, n)
    k = np.ones(6) / 6
    x = np.convolve(x, k, mode="same")
    x += 0.6 * np.sin(2 * np.pi * 1650 * t)
    return amp * x * np.exp(-34 * t)


def reverb(x, detik=1.3, amp=0.22, seed=5):
    """Reverb sederhana: konvolusi dengan IR derau yang meluruh (FFT, cepat)."""
    n_ir = int(SR * detik)
    rng = np.random.default_rng(seed)
    ir = rng.normal(0, 1, n_ir) * np.exp(-4.5 * np.arange(n_ir) / n_ir)
    ir[: int(SR * 0.01)] *= np.linspace(0, 1, int(SR * 0.01))  # pre-delay halus
    ir /= np.max(np.abs(ir)) or 1.0
    n = len(x) + n_ir - 1
    nfft = 1 << (n - 1).bit_length()
    y = np.fft.irfft(np.fft.rfft(x, nfft) * np.fft.rfft(ir, nfft), nfft)[: len(x)]
    return x + amp * y / (np.max(np.abs(y)) or 1.0)


VARIASI = {
    # nama: (bpm, progresi, gaya)
    "kalem_hangat": (82, [("A", 3), ("F", 3), ("C", 4), ("G", 3)], "pad_pluck"),
    "kalem_lembut": (76, [("C", 4), ("G", 3), ("A", 3), ("F", 3)], "musicbox"),
    "kalem_malam": (88, [("A", 3), ("E", 3), ("F", 3), ("C", 4)], "pluck_delay"),
    "ceria_ringan": (106, [("F", 3), ("C", 4), ("G", 3), ("A", 3)], "bass_hit"),
    "ceria_jalan": (112, [("A", 3), ("F", 3), ("C", 4), ("G", 3)], "bass_jalan"),
    "ceria_cerah": (100, [("C", 4), ("A", 3), ("F", 3), ("G", 3)], "arp_penuh"),
}


def buat(nama, durasi=24.0):
    bpm, progresi, gaya = VARIASI[nama]
    ketuk = 60.0 / bpm
    bar = 4 * ketuk
    n_total = int(SR * durasi)
    L = np.zeros(n_total)
    R = np.zeros(n_total)

    def tempel(sig, mulai, pan=0.0):
        i = int(mulai * SR)
        if i >= n_total:
            return
        s = sig[: n_total - i]
        L[i:i + len(s)] += s * np.sqrt(0.5 * (1 - pan))
        R[i:i + len(s)] += s * np.sqrt(0.5 * (1 + pan))

    n_bar = int(np.ceil(durasi / bar))
    for b in range(n_bar):
        t0 = b * bar
        nm, ok = progresi[b % len(progresi)]
        akar = f0(nm, ok)
        akor = [akar, akar * 1.25, akar * 1.5, akar * 2]

        # ---- lapisan pad (semua gaya)
        for f, pan in ((akar, -0.3), (akar * 1.5, 0.3), (akar * 2, 0.0)):
            tempel(pad(f, bar * 1.06, amp=0.16 if "ceria" in nama else 0.19), t0, pan)

        # ---- bass & ritme
        if gaya == "bass_hit":
            for k in range(4):
                tempel(bass(f0(nm, ok - 1), ketuk * 0.92, amp=0.36), t0 + k * ketuk)
            for k in (0, 2):
                tempel(kick(amp=0.26), t0 + k * ketuk)
            for k in (1, 3):
                tempel(rim(amp=0.12, seed=11 + b + k), t0 + k * ketuk, pan=0.15)
            for k in range(8):
                tempel(shaker(amp=0.045, seed=20 + b * 8 + k), t0 + k * ketuk / 2, pan=-0.2)
        elif gaya == "bass_jalan":
            # bass jalan: akar, kwint, oktaf, kwint
            pola = [akar / 2, akar / 2 * 1.5, akar, akar / 2 * 1.25]
            for k, f in enumerate(pola):
                tempel(bass(f, ketuk * 0.8, amp=0.33), t0 + k * ketuk)
            for k in (0, 2):
                tempel(kick(amp=0.24), t0 + k * ketuk)
            for k in range(8):
                tempel(shaker(amp=0.06, seed=50 + b * 8 + k), t0 + k * ketuk / 2,
                       pan=0.25 if k % 2 else -0.25)
        elif gaya == "arp_penuh":
            tempel(bass(akar / 2, ketuk * 1.9, amp=0.34), t0)
            tempel(bass(akar / 2 * 1.5, ketuk * 1.8, amp=0.30), t0 + 2 * ketuk)
            for k in (0, 2):
                tempel(kick(amp=0.22), t0 + k * ketuk)
            for k in (1, 3):
                tempel(rim(amp=0.10, seed=31 + b + k), t0 + k * ketuk, pan=-0.1)
        else:
            tempel(bass(akar / 2, ketuk * 1.85, amp=0.32), t0)
            if gaya == "pluck_delay":
                tempel(bass(akar / 2 * 1.5, ketuk, amp=0.20), t0 + 2 * ketuk)

        # ---- arpeggio / melodi
        if gaya == "musicbox":
            urut = [(akor[i], k * ketuk / 2) for k, i in enumerate([0, 2, 1, 3, 2, 0, 1, 2])]
            for f, off in urut:
                tempel(musicbox(f * 2, min(2.0, bar), amp=0.17), t0 + off, pan=0.25)
        elif gaya == "pluck_delay":
            urut = [(akor[i], k * ketuk) for k, i in enumerate([0, 1, 2, 3])]
            for f, off in urut:
                tempel(pluck(f, min(2.2, bar), amp=0.26, decay=1.9, bright=0.6), t0 + off, pan=0.2)
        elif gaya == "arp_penuh":
            seq = [akor[i] for i in [0, 1, 2, 3, 2, 1, 0, 1]]
            for k, f in enumerate(seq):
                tempel(pluck(f * 2, 0.9, amp=0.20, decay=4.5, bright=1.2), t0 + k * ketuk / 2,
                       pan=0.35 * np.sin(np.pi * k / 4))
        elif gaya == "bass_hit":
            seq = [akor[i] for i in [3, 2, 1, 2]]
            for k, f in enumerate(seq):
                tempel(pluck(f, 1.2, amp=0.22, decay=3.4, bright=1.1), t0 + k * ketuk, pan=0.3)
        elif gaya == "bass_jalan":
            seq = [akor[i] for i in [0, 2, 1, 2, 3, 2, 1, 2]]
            for k, f in enumerate(seq):
                tempel(pluck(f * 2, 0.8, amp=0.17, decay=5.0, bright=1.4), t0 + k * ketuk / 2,
                       pan=0.3 * np.cos(np.pi * k / 4))
        else:  # pad_pluck
            urut = [(akor[i], k * ketuk) for k, i in enumerate([0, 1, 2, 1])]
            for f, off in urut:
                tempel(pluck(f, min(1.8, bar), amp=0.28, decay=2.8, bright=0.9), t0 + off, pan=0.2)

    # ---- ruang: reverb + stereo sedikit beda per kanal
    L = reverb(L, 1.25, 0.20, seed=5)
    R = reverb(R, 1.35, 0.22, seed=9)
    # delay halus untuk gaya pluck_delay
    if gaya == "pluck_delay":
        d = int(ketuk * SR)
        L[d:] += 0.18 * L[:-d]
        R[d:] += 0.16 * R[:-d]

    # ---- fade ujung supaya loop tidak jedug
    fade = int(SR * 0.7)
    for ch in (L, R):
        ch[:fade] *= np.linspace(0, 1, fade)
        ch[-fade:] *= np.linspace(1, 0, fade)
    # ---- loudness: RMS ~0.10, puncak lembut
    st = np.stack([L, R], axis=1)
    rms = np.sqrt(np.mean(st ** 2)) or 1e-9
    st *= 0.105 / rms
    puncak = np.max(np.abs(st)) or 1.0
    st = np.tanh(1.15 * st / puncak) * 0.90
    return st


def tulis(path, st):
    pcm = (np.clip(st, -1, 1) * 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():
    minta = sys.argv[1:] or list(VARIASI)
    durasi = 24.0
    os.makedirs(OUT, exist_ok=True)
    for nama in minta:
        if nama not in VARIASI:
            print("❌ tidak dikenal:", nama)
            continue
        st = buat(nama, durasi)
        path = os.path.join(OUT, f"{nama}_{int(durasi)}s.wav")
        tulis(path, st)
        rms = np.sqrt(np.mean(st ** 2))
        print(f"✔ {nama:14s} {os.path.getsize(path)//1024:5d} KB  peak {np.max(np.abs(st)):.2f}  rms {rms:.3f}")


if __name__ == "__main__":
    main()
