# -*- coding: utf-8 -*-
"""MIDI → lofi（2026-08-25）：Gymnopédie No.1 lofi 化
流程：bitmidi MIDI → mido 解析 → 旋律轨(7)/伴奏轨(5,6,8) → 拍量化 → lofi 合成
3/4 拍、放慢到 76 BPM、全套 lofi 处理（低通 pad/低音/swing/vinyl/wobble/位深劣化）。
"""
import math, random, wave, struct
import mido
from pathlib import Path

SR = 11025
random.seed(42)
MID = Path(__file__).resolve().parent / "gymnopedie1.mid"
BPM = 76                                            # 原曲 91，lofi 放慢
TPB = None

def midi_to_notes():
    m = mido.MidiFile(str(MID))
    tpb = m.ticks_per_beat; globals()["TPB"] = tpb
    tempo = 500000
    # tempo 事件扫描
    abs_t = 0
    for tr in m.tracks:
        for msg in tr:
            abs_t += msg.time
            if msg.type == "set_tempo":
                tempo = msg.tempo; break
        if tempo != 500000: break
    print(f"原 BPM {round(mido.tempo2bpm(tempo))}，tpb {tpb}")
    def beats(tick): return tick / tpb
    mel, pad = [], []                               # [(pitch, start_beat, dur_beats)]
    for i, tr in enumerate(m.tracks):
        if i < 5: continue
        abs_t = 0; pending = {}
        is_mel = (i == 7)
        for msg in tr:
            abs_t += msg.time
            if msg.type == "note_on" and msg.velocity > 0:
                pending.setdefault(msg.note, abs_t)
            elif msg.type in ("note_off", "note_on") :
                if msg.type == "note_off" or msg.velocity == 0:
                    if msg.note in pending:
                        st = pending.pop(msg.note)
                        dur = max(1, beats(abs_t - st))
                        (mel if is_mel else pad).append((msg.note, beats(st), dur))
    return mel, pad

def f_of(p): return 440.0 * (2 ** ((p - 69) / 12.0))

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 main():
    mel, pad = midi_to_notes()
    mel.sort(key=lambda n: n[1]); pad.sort(key=lambda n: n[1])
    print(f"旋律 {len(mel)} 音，伴奏 {len(pad)} 音，旋律结束拍 {mel[-1][1]+mel[-1][2]:.1f}")
    total_beats = (int((mel[-1][1]+mel[-1][2]) / 3) + 1) * 3     # 3/4 对齐
    beat = 60.0 / BPM
    def S(b): return beat * b
    total = int(S(total_beats) * SR)
    buf = [0.0]*total
    def add(src, st_beats):
        st = int(S(st_beats)*SR)
        for i, v in enumerate(src):
            if 0 <= st+i < total: buf[st+i] += v
    # 伴奏音符 → 低通 pad（音量小、加混响）
    for p, sb, du in pad:
        add(lowpass(comb(tone(f_of(p), du*beat, amp=0.038, harm=0.4))), sb)
    # 低音：每小节最低伴奏音低八度
    by_bar = {}
    for p, sb, du in pad:
        bar = int(sb // 3)
        by_bar.setdefault(bar, []).append((p, sb, du))
    for bar, notes in by_bar.items():
        low = min(notes, key=lambda n: n[0])
        add(comb(tone(f_of(low[0])-0, low[2]*beat, amp=0.10, harm=0.1)), low[1])
    # 鼓（3/4 waltz-lofi：kick 小节头，hat 每拍）
    for bar in range(int(total_beats // 3)):
        add(kick(), bar*3)
        for b3 in range(3):
            add(hat(amp=0.09 if b3 == 0 else 0.05), bar*3 + b3)
    # 主旋律（高亮 + 混响 + 采样错位）
    for p, sb, du in mel:
        m = comb(tone(f_of(p), du*beat+0.3, amp=random.uniform(0.88, 1.0)))
        add(m, sb + random.randint(-int(0.008*SR), int(0.006*SR))/SR)
    # wobble + sidechain + vinyl + 位深劣化
    beat_s = S(1)*SR
    out2 = []
    wob = [math.sin(2*math.pi*0.35*(i/SR)) for i in range(total)]
    for i in range(total):
        pump = 1.0 - 0.22*max(0.0, math.sin(math.pi*(i % int(beat_s))/beat_s))
        j = max(0, min(total-1, int(i + 0.0015*wob[i]*SR)))
        v = buf[j]
        out2.append(v*pump)
    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" / "lofi_satie.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}  {len(out2)/SR:.1f}s  {len(out.read_bytes())//1024}KB")

main()