#!/usr/bin/env python3
"""
花园风铃 (wind_chimes.py) — 纯标准库过程化风铃合成器。

不依赖 numpy / 任何音频库,只用 wave / math / random / argparse,
在 C 大调五声音阶上合成一段"花园风铃"并输出 16-bit WAV。

声音模型(每一声都是对真实金属风铃管的粗模拟):
  - 每个音 = 基频 + 2.76 倍 + 5.40 倍三个非谐部分音(金属管的特征泛音),
    每个部分音带 ±0.15% 的随机微失谐 → 两管同音时会拍频,像真的风铃在颤;
  - 快速起音(4ms) + 指数衰减(τ 随机 1.4~3.0s);
  - 25% 概率同音双敲(第二下更轻、晚 90ms);
  - 每声带三叠回声(0.42s / 0.71s / 1.13s),模拟走廊或屋檐下的反射;
  - 最后整体归一化到峰值 0.85,结尾 2 秒淡出。

默认种子取当天的日期(如 20260804)——同一阵风每天只吹一次,但可以复现。
用法:
  python3 wind_chimes.py                    # 今天的风,20 秒,写进本站 content/code/
  python3 wind_chimes.py --seed 7 --duration 30 --chimes 40   # 自定义
"""

import argparse
import math
import random
import struct
import sys
import wave
from datetime import date
from pathlib import Path

SR = 22050  # 采样率:22.05 kHz,16-bit 单声道

# C 大调五声音阶(C4..E6),MIDI 号
PENTATONIC = [60, 62, 64, 67, 69, 72, 74, 76, 79, 81, 84, 86, 88]


def midi_to_freq(midi: int) -> float:
    return 440.0 * 2.0 ** ((midi - 69) / 12.0)


def render_chime(freq: float, vel: float, dur: float, rng: random.Random) -> list[float]:
    """合成一声风铃:三个失谐部分音 × 指数衰减。返回浮点采样。"""
    n = int(dur * SR)
    attack = int(0.004 * SR)  # 4ms 起音,避免爆音
    tau = rng.uniform(1.4, 3.0)
    # (倍频比, 相对振幅, 微失谐)
    partials = [(1.0, 1.0, rng.uniform(-0.0015, 0.0015)),
                (2.76, 0.35, rng.uniform(-0.0015, 0.0015)),
                (5.404, 0.12, rng.uniform(-0.0015, 0.0015))]
    freqs = [freq * r * (1.0 + det) for r, _, det in partials]
    amps = [a for _, a, _ in partials]
    sin = math.sin
    buf = [0.0] * n
    for i in range(n):
        t = i / SR
        env = (1.0 if i >= attack else i / attack) * math.exp(-t / tau)
        s = amps[0] * sin(2 * math.pi * freqs[0] * t) \
            + amps[1] * sin(2 * math.pi * freqs[1] * t) \
            + amps[2] * sin(2 * math.pi * freqs[2] * t)
        buf[i] = vel * env * s
    return buf


def main() -> None:
    ap = argparse.ArgumentParser(description="过程化花园风铃合成器(纯标准库)")
    ap.add_argument("--seed", type=int, default=None,
                    help="随机种子;缺省用今天日期(同一阵风每天只吹一次)")
    ap.add_argument("--duration", type=float, default=20.0, help="总时长秒数(默认 20)")
    ap.add_argument("--chimes", type=int, default=30, help="风铃敲击次数(默认 30)")
    ap.add_argument("--out", type=str, default=None,
                    help="输出 WAV 路径(默认:脚本同目录 wind_chimes.wav)")
    args = ap.parse_args()

    seed = args.seed if args.seed is not None else int(date.today().strftime("%Y%m%d"))
    rng = random.Random(seed)

    out_path = Path(args.out) if args.out else Path(__file__).resolve().parent / "wind_chimes.wav"
    total = int(args.duration * SR)
    mix = [0.0] * total

    # 三叠回声参数:(延迟秒, 增益)
    ECHOES = [(0.42, 0.38), (0.71, 0.22), (1.13, 0.12)]
    notes_played = []

    for _ in range(args.chimes):
        t0 = rng.uniform(0.0, max(0.5, args.duration - 2.5))
        freq = midi_to_freq(rng.choice(PENTATONIC))
        vel = rng.uniform(0.3, 1.0)
        dur = min(6.0, args.duration - t0)
        buf = render_chime(freq, vel, dur, rng)
        start = int(t0 * SR)
        for i, v in enumerate(buf):
            if start + i < total:
                mix[start + i] += v
        for delay, gain in ECHOES:
            d = int(delay * SR)
            for i, v in enumerate(buf):
                j = start + d + i
                if j < total:
                    mix[j] += v * gain
        notes_played.append((t0, freq, vel))
        # 25% 概率:同音双敲(第二下更轻、晚 90ms)——真实风铃常被连击两下
        if rng.random() < 0.25:
            d2 = int(0.09 * SR)
            buf2 = render_chime(freq, vel * 0.55, dur * 0.7, rng)
            for i, v in enumerate(buf2):
                j = start + d2 + i
                if j < total:
                    mix[j] += v

    # 结尾 2 秒淡出
    fade = int(2.0 * SR)
    for i in range(fade):
        mix[total - fade + i] *= i / fade

    # 归一化到峰值 0.85
    peak = max(abs(v) for v in mix) or 1.0
    scale = 0.85 / peak
    mix = [v * scale for v in mix]

    # 写 16-bit WAV
    frames = b"".join(struct.pack("<h", int(max(-1.0, min(1.0, v)) * 32767)) for v in mix)
    out_path.parent.mkdir(parents=True, exist_ok=True)
    with wave.open(str(out_path), "wb") as w:
        w.setnchannels(1)
        w.setsampwidth(2)
        w.setframerate(SR)
        w.writeframes(frames)

    # ---- 校验(自己读回自己) ----
    with wave.open(str(out_path), "rb") as w:
        nf = w.getnframes()
        sr = w.getframerate()
        raw = w.readframes(nf)
    vals = struct.unpack(f"<{nf}h", raw)
    rms = math.sqrt(sum(v * v for v in vals) / nf) / 32767.0
    dur = nf / sr

    # ---- ASCII 波形预览(60 格,包络峰值) ----
    bars = 60
    bucket = max(1, nf // bars)
    chars = " .:-=+*#%@"
    env = []
    for b in range(bars):
        seg = vals[b * bucket:(b + 1) * bucket]
        m = max(abs(v) for v in seg) / 32767.0
        env.append(chars[min(len(chars) - 1, int(m * len(chars)))])
    print(f"seed={seed}  时长={dur:.1f}s  采样率={sr}Hz  敲击={len(notes_played)}  "
          f"峰值={max(abs(v) for v in vals) / 32767:.2f}  RMS={rms:.3f}")
    print("".join(env))
    print(f"写出 {out_path} ({out_path.stat().st_size / 1024:.0f} KB)")

    lo, hi = min(f for _, f, _ in notes_played), max(f for _, f, _ in notes_played)
    print(f"音域: {lo:.1f} Hz ~ {hi:.1f} Hz (C 大调五声,{len(PENTATONIC)} 个音)")


if __name__ == "__main__":
    try:
        main()
    except KeyboardInterrupt:
        sys.exit(130)
