# -*- coding: utf-8 -*-
"""lofi 合成器 v3（2026-08-25）：乐谱 → 低保真 WAV
lofi 制作技术全套落地：七和弦、低通 pad、sidechain 泵感、位深劣化、
磁带 wobble、vinyl 噼啪噪点、swing、采样错位。
（《送别》准确谱待用户拍照供给；现保留三首：songbie_v2 尽力版/joy/aurora）
"""
import math, random, wave, struct
from pathlib import Path

SR = 22050
random.seed(42)
DO4 = 349.23
_step = {1: 0, 2: 2, 3: 4, 4: 5, 5: 7, 6: 9, 7: 11}
def frq(deg, oct_=0):
    return DO4 * (2 ** ((_step[deg] + 12 * oct_) / 12.0))

SONGS = {
  "songbie_v2": {
    "bpm": 76, "amp": 1.1,
    "prog": [[(1,0),(3,0),(5,0),(7,-1)],[(5,0),(7,0),(2,1),(4,1)],
             [(2,0),(4,0),(6,0),(1,1)],[(7,-1),(2,0),(4,0),(6,0)]],
    "mel": [
      (5,0,1.5),(6,0,0.5),(1,1,1),(5,0,1),(3,0,2),
      (5,0,1),(3,0,1),(2,0,1),(1,-1,1),(6,0,2),
      (5,0,1.5),(6,0,0.5),(1,1,1),(5,0,1),(3,0,1),(5,0,1),
      (6,0,1),(1,1,1),(2,1,1),(1,1,1),(5,0,2),
      (5,0,1.5),(6,0,0.5),(1,1,1),(5,0,1),(3,0,2),
      (5,0,1),(3,0,1),(2,0,1),(1,-1,1),(6,0,2),
      (5,0,1.5),(6,0,0.5),(1,1,1),(5,0,1),(3,0,1),(5,0,1),
      (6,0,1),(1,1,1),(2,1,1),(1,1,1),(5,0,2),
    ],
  },
  "joy": {
    "bpm": 82, "amp": 1.1,
    "prog": [[(1,0),(3,0),(5,0),(7,-1)],[(4,0),(6,0),(1,1),(3,1)],
             [(5,0),(7,0),(2,1),(4,1)],[(1,0),(3,0),(5,0),(7,-1)]],
    "mel": [
      (3,0,1),(3,0,1),(4,0,1),(5,0,1),(5,0,1),(4,0,1),(3,0,1),(2,0,1),
      (1,0,1),(1,0,1),(2,0,1),(3,0,1),(3,0,1.5),(2,0,0.5),(2,0,2),
      (3,0,1),(3,0,1),(4,0,1),(5,0,1),(5,0,1),(4,0,1),(3,0,1),(2,0,1),
      (1,0,1),(1,0,1),(2,0,1),(3,0,1),(2,0,1.5),(1,0,0.5),(1,0,2),
    ],
  },
  "aurora": {
    "bpm": 70, "amp": 1.1,
    "prog": [[(6,0),(1,1),(3,1),(5,1)],[(4,0),(6,0),(1,1),(3,1)],
             [(1,0),(3,0),(5,0),(7,-1)],[(5,0),(7,0),(2,1),(4,1)]],
    "mel": [
      (3,1,1),(5,1,1),(6,1,2),(3,1,1),(2,1,1),(1,1,2),
      (6,0,1),(1,1,1),(3,1,1),(2,1,1),(1,1,2),
      (3,1,1),(5,1,1),(6,1,2),(1,1,0.5),(7,0,0.5),(6,0,1),(5,0,1),(3,0,2),
      (3,1,1),(2,1,1),(1,1,2),(6,0,1),(7,0,1),(1,1,2),
    ],
  },
}

def tone(f, dur, amp=0.5, harm=0.25):
    n = int(dur * SR); out = []
    for i in range(n):
        t = i / SR
        v = math.sin(2*math.pi*f*t) + harm*math.sin(4*math.pi*f*t)*1.03
        env = min(1, t/0.012) * min(1, (dur-t)/0.10)
        out.append(v*amp*env)
    return out

def lowpass(src, alpha=0.25):                    # 一阶低通（垫底柔化）
    out, l = [], 0.0
    for v in src:
        l += alpha * (v - l)
        out.append(l)
    return out

def comb(src, delay_s=0.16, fb=0.35):
    d = int(delay_s*SR); out = src[:]
    for i in range(d, len(out)):
        out[i] += out[i-d]*fb
    return out

def kick(dur=0.32, amp=0.5):
    return [math.sin(2*math.pi*(118-82*(i/SR)/dur)*(i/SR))*amp*math.exp(-(i/SR)*13)
            for i in range(int(dur*SR))]

def hat(dur=0.05, amp=0.08):
    return [random.uniform(-1,1)*amp*math.exp(-t*70) for t in [i/SR for i in range(int(dur*SR))]]

def render(name, cfg):
    beat = 60.0 / cfg["bpm"]
    def S(b): return beat * b
    total = int(S(32) * SR)
    buf = [0.0]*total
    for bar in range(8):                          # 七和弦 pad（低通+混响）+ 低音
        chord = cfg["prog"][bar % 4]
        for deg, oc in chord:
            p = lowpass(comb(tone(frq(deg, oc), S(4), amp=0.055, harm=0.4)))
            st = int(S(bar*4)*SR) - SR//20
            for i, v in enumerate(p):
                if st+i < total: buf[st+i] += v
        root = chord[0]
        b = comb(tone(frq(root[0], root[1]-1), S(4), amp=0.15, harm=0.1))
        st = int(S(bar*4)*SR)
        for i, v in enumerate(b):
            if st+i < total: buf[st+i] += v
    for bar in range(8):                          # 鼓（swing）
        b0 = bar*4
        for off in (0, 2):
            k = kick()
            st = int(S(b0+off)*SR)
            for i, v in enumerate(k):
                if st+i < total: buf[st+i] += v
        for s8 in range(8):
            h = hat(amp=0.10 if s8 % 2 == 0 else 0.05)
            st = int(S(b0 + s8*0.5 + (0.06 if s8 % 2 else 0))*SR)
            for i, v in enumerate(h):
                if st+i < total: buf[st+i] += v
    t = 0.0                                       # 主旋律（采样错位+力度随机）
    for deg, oc, beats in cfg["mel"]:
        m = comb(tone(frq(deg, oc), beats*beat+0.3,
                      amp=random.uniform(0.66, 0.85)*cfg["amp"]))
        st = int(S(t)*SR) + random.randint(-int(0.010*SR), int(0.006*SR))
        for i, v in enumerate(m):
            if 0 <= st+i < total: buf[st+i] += v
        t += beats
    out2 = []                                     # wobble + 泵感 + vinyl 噼啪
    wob = [math.sin(2*math.pi*0.35*(i/SR)) for i in range(total)]
    beat_s = S(1)*SR
    for i in range(total):
        pump = 1.0 - 0.25*max(0.0, math.sin(math.pi*(i % int(beat_s)) / beat_s))
        j = int(i + 0.0015*wob[i]*SR)
        j = max(0, min(total-1, j))
        v = buf[j]
        if random.random() < 0.0013:
            v += random.uniform(-0.5, 0.5)*0.06
        out2.append(v*pump)
    q = 0.03125                                   # 位深劣化（8 位感颗粒）
    out2 = [round(v/q)*q for v in out2]
    peak = max(1.0, max(abs(v) for v in out2))*1.08
    out2 = [max(-1, min(1, v/peak)) for v in out2]
    out = Path(__file__).resolve().parent / "preview" / f"lofi_{name}.wav"
    with wave.open(str(out), "wb") as w:
        w.setnchannels(1); w.setsampwidth(2); w.setframerate(SR)
        w.writeframes(b"".join(struct.pack("<h", int(v*32767)) for v in out2))
    print(f"[OK] {out.name}  {len(out2)/SR:.1f}s  {len(out.read_bytes())//1024}KB")

for name, cfg in SONGS.items():
    render(name, cfg)