AI 正在绞尽脑汁想思路 ING···
AI 摘要
DeepSeek & Kimi

由 DeepSeek V4 Pro、Claude Code 和我共同创作的文章,部分内容由 AI 生成,可能会有错误,请注意甄别。通过改造主题现有代码结构,实现在线运行 Python 代码。

Hello World!JavaScript | MDN
console.log("Hello World!");
Hello World!The Programming Language Lua
print("Hello World!")
Hello World!Silq - What is Silq?
def main() {
    x := 0: 𝔹;
    x := H(x);
    return measure(x);
}
Hello World!Welcome to Python.org
print("Hello World!")
图片测试1
import matplotlib.pyplot as plt
import io, base64
fig, ax = plt.subplots()
ax.plot([1, 2, 3], [1, 4, 9])
buf = io.BytesIO()
fig.savefig(buf, format='png')
buf.seek(0)
img_b64 = base64.b64encode(buf.read()).decode()
f'<img src="data:image/png;base64,{img_b64}" alt="plot">'
图片测试2
import numpy as np
import matplotlib.pyplot as plt
x = np.linspace(0, 2 * np.pi, 100)
y = np.sin(x)
plt.plot(x, y)
plt.show()
图片测试3
import io, base64
import numpy as np
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
x = np.linspace(0, 2 * np.pi, 200)
y = np.sin(x)
y2 = np.cos(x)
fig, ax = plt.subplots(figsize=(8, 4))
ax.plot(x, y, label='sin(x)', linewidth=2)
ax.plot(x, y2, label='cos(x)', linewidth=2, linestyle='--')
ax.set_title('Sine & Cosine Wave', fontsize=14)
ax.set_xlabel('x')
ax.set_ylabel('y')
ax.legend()
ax.grid(True, alpha=0.3)
buf = io.BytesIO()
fig.savefig(buf, format='png', dpi=100, bbox_inches='tight')
buf.seek(0)
img_base64 = base64.b64encode(buf.read()).decode()
f'<img src="data:image/png;base64,{img_base64}" style="max-width:50%;" />'
动图测试
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""Generate an animated GIF via PIL / matplotlib — viewable inline, downloadable."""
import io, base64
import numpy as np
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
from PIL import Image
# ── Set up figure ──
fig, ax = plt.subplots(figsize=(5, 3), facecolor="#00000000")
ax.set_facecolor("#00000000")
ax.axis("off")
fig.subplots_adjust(0, 0, 1, 1)
x = np.linspace(0, 2 * np.pi, 300)
(line1,) = ax.plot(x, np.sin(x), "#27c93f", linewidth=3)
(line2,) = ax.plot(x, np.cos(x) * 0.6, "#ffbd2e", linewidth=1.5, alpha=0.5)
ax.set_xlim(0, 2 * np.pi)
ax.set_ylim(-1.4, 1.4)
# ── 视觉暂留优化:最少帧数 + 无缝循环 ──
# phase_step = 0.35 保持原速度,N = ceil (2π/0.35)=19 刚好越过 2π
# 末帧 (phase≈6.3→sin≈0.017) 与首帧 (phase = 0→sin = 0) 近乎重合,loop 自然闭合
phase_step = 0.35
N = 19
frames = []
for i in range(N):
    phase = i * phase_step
    line1.set_ydata(np.sin(x + phase))
    line2.set_ydata(np.cos(x + phase) * 0.6)
    fig.canvas.draw()
    raw = np.array(fig.canvas.buffer_rgba(), dtype=np.uint8)
    img = Image.fromarray(raw[:, :, :3], "RGB")
    frames.append(img.quantize(method=Image.Quantize.MEDIANCUT))
plt.close(fig)
buf = io.BytesIO()
frames[0].save(
    buf, format="GIF", save_all=True,
    append_images=frames[1:],
    duration=67,   # ms / frame,保持原速度
    loop=0,
)
buf.seek(0)
b64 = base64.b64encode(buf.read()).decode()
total_ms = N * 67
print(f"GIF: {len(b64) // 1024} KB base64, {N} frames, {total_ms}ms")
f'<img src="data:image/gif;base64,{b64}" style="max-width:100%;" />'
视频测试
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Real WebM video + custom styled controls.
Records frames via Canvas+MediaRecorder → WebM blob → <video>.
Controls: custom ▶/⏸ button, range slider, time display.
"""
import io, base64, json, random
import numpy as np
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
from js import document
W, H = 500, 300
FPS = 15
N = 30
# ── Render frames ──
fig, ax = plt.subplots(figsize=(W / 100, H / 100), dpi=100, facecolor="#0c0c0c")
ax.set_facecolor("#0c0c0c")
ax.axis("off")
fig.subplots_adjust(0, 0, 1, 1)
x = np.linspace(0, 2 * np.pi, 300)
(line1,) = ax.plot(x, np.sin(x), "#27c93f", linewidth=3)
(line2,) = ax.plot(x, np.cos(x) * 0.6, "#ffbd2e", linewidth=1.5, alpha=0.5)
ax.set_xlim(0, 2 * np.pi)
ax.set_ylim(-1.4, 1.4)
frame_b64 = []
for i in range(N):
    phase = i * 0.35
    line1.set_ydata(np.sin(x + phase))
    line2.set_ydata(np.cos(x + phase / 2) * 0.6)
    fig.canvas.draw()
    jb = io.BytesIO()
    fig.savefig(jb, format="jpg", dpi=100, bbox_inches="tight", pad_inches=0)
    jb.seek(0)
    frame_b64.append(base64.b64encode(jb.read()).decode())
plt.close(fig)
print(f"Rendered {N} frames @ {FPS}fps")
# ── Inject JS: record video + custom controls ──
uid = random.randint(10000, 99999)
FRAMES = json.dumps(frame_b64)
JS = f"""
setTimeout(function() {{
  var frames = {FRAMES};
  var w = {W}, h = {H}, fps = {FPS}, total = {N}, dur = total / fps;
  var canvas = document.createElement('canvas');
  canvas.width = w; canvas.height = h;
  var ctx = canvas.getContext('2d');
  var stream = canvas.captureStream(fps);
  var mime = MediaRecorder.isTypeSupported('video/webm;codecs=vp9')
    ? 'video/webm;codecs=vp9' : 'video/webm';
  var rec = new MediaRecorder(stream, {{mimeType: mime}});
  var chunks = [];
  rec.ondataavailable = function(e) {{ chunks.push(e.data); }};
  rec.onstop = function() {{
    var blob = new Blob(chunks, {{type: 'video/webm'}});
    var url = URL.createObjectURL(blob);
    var vid = document.getElementById('vp{uid}');
    var load = document.getElementById('vp{uid}_loading');
    var ctrl = document.getElementById('vp{uid}_ctrl');
    var dl   = document.getElementById('vp{uid}_dl');
    var btn  = document.getElementById('vp{uid}_btn');
    var sld  = document.getElementById('vp{uid}_sld');
    var tim  = document.getElementById('vp{uid}_tim');
    vid.src = url;
    vid.style.display = '';
    if (load) load.style.display = 'none';
    if (ctrl) ctrl.style.display = 'flex';
    if (dl) {{ dl.href = url; dl.download = 'pyodide-video.webm'; dl.style.display = ''; }}
    // ── Custom controls ──
    function fmt(t) {{
      t = Math.max(0, t);
      var m = Math.floor(t / 60);
      var s = String(Math.floor(t % 60)).padStart(2, '0');
      return m + ':' + s;
    }}
    vid.addEventListener('timeupdate', function() {{
      if (sld && !sld._dragging) sld.value = vid.currentTime;
      if (tim) tim.textContent = fmt(vid.currentTime) + ' / ' + fmt(dur);
    }});
    vid.addEventListener('ended', function() {{
      if (btn) btn.textContent = '▶';
    }});
    vid.addEventListener('loadedmetadata', function() {{
      if (sld) {{ sld.min = 0; sld.max = dur; sld.step = 0.01; sld.value = 0; }}
    }});
    window['vp{uid}_play'] = function() {{
      if (vid.paused || vid.ended) {{
        if (vid.ended) vid.currentTime = 0;
        vid.play();
        if (btn) btn.textContent = '⏸';
      }} else {{
        vid.pause();
        if (btn) btn.textContent = '▶';
      }}
    }};
    window['vp{uid}_seek'] = function(v) {{
      vid.currentTime = Number(v);
    }};
    sld.addEventListener('mousedown', function() {{ sld._dragging = true; }});
    sld.addEventListener('mouseup', function() {{ sld._dragging = false; }});
    sld.addEventListener('input', function() {{
      if (tim) tim.textContent = fmt(sld.value) + ' / ' + fmt(dur);
    }});
    sld.addEventListener('change', function() {{
      vid.currentTime = Number(sld.value);
    }});
  }};
  rec.start();
  var i = 0;
  var img = new Image();
  function next() {{
    if (i >= total) {{ rec.stop(); return; }}
    img.onload = function() {{
      ctx.clearRect(0, 0, w, h);
      ctx.drawImage(img, 0, 0, w, h);
      i++;
      setTimeout(next, 1000 / fps);
    }};
    img.src = 'data:image/jpeg;base64,' + frames[i];
  }}
  next();
}}, 100);
"""
script = document.createElement("script")
script.textContent = JS
document.body.appendChild(script)
print(f"Video recorder injected (id={uid}), encoding → WebM...")
# ── Return HTML ──
f'''<div class="media-wrapper">
  <span id="vp{uid}_loading" style="display:block;padding:10px 12px;color:#888;font-size:14px;font-family:monospace">Encoding video...</span>
  <video id="vp{uid}" width="{W}" style="display:none;max-width:100%"></video>
  <div id="vp{uid}_ctrl" style="display:none;align-items:center;gap:8px;padding:6px 10px;background:#161616">
    <button id="vp{uid}_btn" onclick="vp{uid}_play()" style="background:none;border:none;color:#27c93f;font-size:14px;cursor:pointer;padding:0 6px;line-height:1" title="Play">▶</button>
    <input id="vp{uid}_sld" type="range" min="0" max="1" value="0" step="0.01" style="flex:1;accent-color:#27c93f;height:4px;cursor:pointer">
    <span id="vp{uid}_tim" style="color:#555;font-size:11px;font-family:monospace;white-space:nowrap">0:00 / 0:00</span>
  </div>
</div>
<a id="vp{uid}_dl" class="btn-media-download" style="display:none">Download</a>'''
标准音高
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import numpy as np
import simpleaudio as sa
import matplotlib.pyplot as plt
frequency = 440  # 标准音高 440Hz
fs = 44100  # 采样率
secends = 3  # 音符持续 3 秒
t = np.linspace(0, secends, secends * fs)
note = np.sin(frequency * t * 2 * np.pi)  # 440Hz 正弦波
plt.plot(note)
plt.show()
audio = note * (2**15 - 1) / np.max(np.abs(note))
audio = audio.astype(np.int16)
play_obj = sa.play_buffer(audio, 1, 2, fs)
play_obj.wait_done()
音阶
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import numpy as np
import simpleaudio as sa
# 获取每个样本的持续时间,T 为音符持续秒数,即周期
sample_rate = 44100
T = 1  # BPM: 60
# T = 0.25  # BPM: 120
t = np.linspace(0, T, int(T * sample_rate), False)
# 计算音符频率
freq_list = [440*2**(i / 12) for i in range(13)]
# 生成正弦波音符
notes = [np.sin(i*t*2*np.pi) for i in freq_list]
# 连环音符
audio = np.hstack(notes)
# 归一化为 16 位范围
audio *= (2 ** 15 - 1) / np.max(np.abs(audio))
# 转换为 16 位数据
audio = audio.astype(np.int16)
# 开始回放
play_obj = sa.play_buffer(audio, 1, 2, sample_rate)
# 退出前等待回放结束
play_obj.wait_done()
小星星
"""小星星 — 简谐波合成,simpleaudio 播放 + <audio> 标签下载"""
import io, base64, wave
import numpy as np
import simpleaudio as sa
from js import window
SAMPLE_RATE = 22050
# 音名 → 频率 (Hz),C4 = 261.63
notes = {"C4":261.63, "D4":293.66, "E4":329.63, "F4":349.23, "G4":392.00, "A4":440.00}
# 小星星旋律:音名 + 时长 (秒)
melody = [
    ("C4",0.4),("C4",0.4),("G4",0.4),("G4",0.4),("A4",0.4),("A4",0.4),("G4",0.8),
    ("F4",0.4),("F4",0.4),("E4",0.4),("E4",0.4),("D4",0.4),("D4",0.4),("C4",0.8),
    ("G4",0.4),("G4",0.4),("F4",0.4),("F4",0.4),("E4",0.4),("E4",0.4),("D4",0.8),
    ("G4",0.4),("G4",0.4),("F4",0.4),("F4",0.4),("E4",0.4),("E4",0.4),("D4",0.8),
    ("C4",0.4),("C4",0.4),("G4",0.4),("G4",0.4),("A4",0.4),("A4",0.4),("G4",0.8),
    ("F4",0.4),("F4",0.4),("E4",0.4),("E4",0.4),("D4",0.4),("D4",0.4),("C4",0.8),
]
# 合成音频
total = sum(d for _, d in melody)
samples = np.zeros(int(SAMPLE_RATE * total), dtype=np.float32)
pos = 0
for name, dur in melody:
    n = int(SAMPLE_RATE * dur)
    t = np.arange(n, dtype=np.float32) / SAMPLE_RATE
    # 基频 + 2 个泛音,ADSR 包络
    sig = 0.6*np.sin(2*np.pi*notes[name]*t) + 0.25*np.sin(4*np.pi*notes[name]*t) + 0.1*np.sin(6*np.pi*notes[name]*t)
    env = np.ones(n, dtype=np.float32)
    attack = n // 20
    release = n // 10
    if attack > 0: env[:attack] = np.linspace(0, 1, attack)
    if release > 0: env[-release:] = np.linspace(1, 0, release)
    samples[pos:pos+n] += (sig * env).astype(np.float32)
    pos += n
# 归一化 + 转 int16
peak = float(np.max(np.abs(samples)))
if peak > 1e-12: samples *= (0.85 / peak)
samples_i16 = (samples * 32767).astype(np.int16)
del samples
# ── 打包 WAV → base64 ──
buf = io.BytesIO()
with wave.open(buf, "wb") as w:
    w.setnchannels(1)
    w.setsampwidth(2)
    w.setframerate(SAMPLE_RATE)
    w.writeframes(samples_i16.tobytes())
buf.seek(0)
audio_b64 = base64.b64encode(buf.read()).decode()
# 暴露给 JS,用于 audio 标签 ↔ simpleaudio 切换
window._pyodideAudio = dict(base64=audio_b64, channels=1, sample_width=2, sample_rate=SAMPLE_RATE)
# 后台播放(非阻塞)
print(f"Playing Twinkle Twinkle Little Star ({total:.0f}s)...")
play_obj = sa.play_buffer(samples_i16, 1, 2, SAMPLE_RATE)
window._pyodidePlayObj = play_obj
print("Done!")
f'<audio controls src="data:audio/wav;base64,{audio_b64}" style="max-width:100%;"></audio>'
国际歌
"""L'Internationale — 简谐波合成,simpleaudio 播放 + <audio> 标签下载"""
import io, base64, wave
import numpy as np
import simpleaudio as sa
from js import window
SAMPLE_RATE = 22050
BPM = 104
beat = 60.0 / BPM  # 一拍 ≈ 0.577s
# 音名 → 频率 (Hz),C4 = 261.63
notes = {
    "C4":261.63, "D4":293.66, "E4":329.63, "F4":349.23,
    "G4":392.00, "A4":440.00, "B4":493.88, "C5":523.25,
}
# 国际歌旋律:(音名, 拍数)  — 修复版,见下方注释
#  起来,饥寒交迫的奴隶 | 起来,全世界受苦的人
#  满腔的热血已经沸腾 | 要为真理而斗争
#
# 修复说明:
# 1. 开头 C→E 改为 C→C→E(属音→主音,主音需重复才符合原曲)
# 2. 上行线补入 G→A→G→F(原版漏了 A 前的 G,跳过大跳)
# 3. 副歌经核对已正确(上行琶音 C - E-G - C' + 下行 A - G-F + 收束 D - E-C)
melody = [
    # 前奏 pickup(属音 G 弱起)
    ("G4",0.5),
    # 第一乐句 "起来,饥寒交迫的奴隶"
    ("C4",0.5),("C4",0.5),("E4",0.5),("F4",0.5),   # C C E F — 主音重复后级进上行
    ("G4",0.5),("A4",0.75),("G4",0.25),("F4",0.5),  # G A G F — 级进到 A 后折返
    # "起来,全世界受苦的人"
    ("E4",0.75),("C4",0.25),("D4",0.5),("E4",0.25),("F4",0.25),("E4",0.25),("D4",0.25),("C4",1.5),
    # 第二乐句 "满腔的热血已经沸腾"
    ("G4",0.5),
    ("C4",0.5),("C4",0.5),("E4",0.5),("F4",0.5),   # C C E F
    ("G4",0.5),("A4",0.75),("G4",0.25),("F4",0.5),  # G A G F
    # "要为真理而斗争"(结尾缩短,E→D→C 收束)
    ("E4",0.5),("D4",0.5),("C4",2.0),
    # 副歌:这是最后的斗争,团结起来到明天
    ("C4",0.5),("E4",0.5),("G4",0.5),("C5",0.5),
    ("A4",1.0),("G4",0.5),("F4",0.5),
    ("E4",0.75),("D4",0.25),("E4",0.5),("F4",0.5),
    ("G4",1.5),("F4",0.25),("E4",0.25),
    ("D4",0.5),("E4",0.5),("C4",2.0),
    # 副歌重复:英特纳雄耐尔就一定要实现(末句 G→F→E 改为 G→G→F→E)
    ("C4",0.5),("E4",0.5),("G4",0.5),("C5",0.5),
    ("A4",1.0),("G4",0.5),("F4",0.5),
    ("E4",0.75),("D4",0.25),("E4",0.5),("F4",0.5),
    ("G4",0.5),("G4",0.5),("F4",0.5),("E4",0.5),
    ("D4",0.5),("E4",0.5),("C4",2.0),
]
# 合成音频
total_beats = sum(d for _, d in melody)
total_sec = total_beats * beat
samples = np.zeros(int(SAMPLE_RATE * total_sec) + SAMPLE_RATE//4, dtype=np.float32)
pos = 0
for name, dur_beat in melody:
    dur = dur_beat * beat
    n = int(SAMPLE_RATE * dur)
    if n < 1: continue
    t = np.arange(n, dtype=np.float32) / SAMPLE_RATE
    f = notes[name]
    # 基频 + 3 个泛音
    sig = (0.55*np.sin(2*np.pi*f*t)
           + 0.25*np.sin(4*np.pi*f*t)
           + 0.12*np.sin(6*np.pi*f*t)
           + 0.05*np.sin(8*np.pi*f*t))
    # ADSR 包络
    env = np.ones(n, dtype=np.float32)
    a = min(n//10, int(0.02*SAMPLE_RATE))
    r = min(n//8,  int(0.06*SAMPLE_RATE))
    if a>0: env[:a] = np.linspace(0,1,a)
    if r>0: env[-r:] = np.linspace(1,0,r)
    samples[pos:pos+n] += (sig * env * 0.75).astype(np.float32)
    pos += n
samples = samples[:pos]
# 归一化 + 转 int16
peak = float(np.max(np.abs(samples)))
if peak > 1e-12: samples *= (0.88 / peak)
samples_i16 = (samples * 32767).astype(np.int16)
del samples
# ── WAV → base64 ──
buf = io.BytesIO()
with wave.open(buf, "wb") as w:
    w.setnchannels(1)
    w.setsampwidth(2)
    w.setframerate(SAMPLE_RATE)
    w.writeframes(samples_i16.tobytes())
buf.seek(0)
audio_b64 = base64.b64encode(buf.read()).decode()
window._pyodideAudio = dict(base64=audio_b64, channels=1, sample_width=2, sample_rate=SAMPLE_RATE)
print(f"Playing L'Internationale ({total_sec:.0f}s)...")
play_obj = sa.play_buffer(samples_i16, 1, 2, SAMPLE_RATE)
window._pyodidePlayObj = play_obj
print("Done!")
f'<audio controls src="data:audio/wav;base64,{audio_b64}" style="max-width:100%;"></audio>'
白色相簿
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Music Synthesizer for Pyodide — renders, plays via simpleaudio, returns <audio> element.
"""
import numpy as np
import simpleaudio as sa
import io, base64, wave
# ============================================================
#  Music Data (unchanged from original)
# ============================================================
timbre = [[1, 0.005, 0.01, 0.00, 0.00, 0.00, 0, 0],
          [0.7, 0.4, 0.35, 0.3, 0.2, 0.1, 0.1, 0.1],
          [1, 0.2, 0.2, 0, 0, 0, 0, 0],
          [1, 0.2, 0.3, 0, 0, 0, 0, 0]]
rhythm_piano1 = [[4, 5, 6, 3, 1, 2, 1, 3, 4, 5, 6, 3, 1, 6, 2, 2, 1, 3, 4, 5, 6, 3, 1, 6, 2, 1, 3, 4, 5, 6, 3, 1, 6, 2, 2, 3, 3],
                 [0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 0, 1, 1, 1, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 1, 1, 1, 0],
                 [0.5, 0.5, 0.5, 1.5, 1, 1.5, 2, 0.5, 0.5, 0.5, 0.5, 1, 1, 0.5, 0.75, 0.75, 2, 0.5, 0.5, 0.5, 0.5, 1, 1, 0.5, 1.5, 2,
                  0.5, 0.5, 0.5, 0.5, 1, 1, 0.5, 0.75, 0.75, 2, 0.5]]
rhythm_piano2 = [[4, 5, 6, 3, 1, 6, 2, 1, 1, 2, 3, 1, 6, 5, 6, 6, 3, 1, 2, 6, 5, 3, 2, 1, 2, 3, 4, 3, 4, 3, 2, 1, 3, 6],
                 [0, 0, 0, 1, 1, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
                 [0.5, 0.5, 0.5, 1, 1, 0.5, 1.5, 1, 0.5, 0.5, 2, 2, 0.5, 3.5, 0.5, 1, 0.5, 0.25, 1.75, 0.5, 1, 0.5, 0.25, 1.25, 0.5,
                  0.5, 0.5, 0.25, 0.75, 0.5, 0.75, 0.75, 4, 0.5]]
rhythm_piano3 = [[6, 3, 1, 2, 2, 6, 5, 5, 3, 2, 1, 3, 4, 5, 3, 2, 3, 1, 5, 5, 4, 3, 4, 5, 6, 1, 1, 5, 5, 4, 3, 4, 5, 1],
                 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
                 [1, 0.5, 0.25, 1.25, 0.5, 0.5, 0.5, 0.5, 0.5, 0.25, 1.75, 0.5, 1.5, 0.5, 1, 1, 3.5, 0.5, 2, 1, 1, 0.5,
                  0.25, 0.75, 0.5, 1.5, 0.5, 2, 1, 1, 0.5, 0.25, 2.25, 1]]
rhythm_piano4 = [[5, 5, 4, 3, 4, 5, 6, 1, 1, 18, 18, 16, 5, 4, 5, 3, 4, 5, 6, 3, 1, 7, 1, 2, 1, 3, 4, 5, 6, 3, 1, 6, 2, 2, 3, 3],
                 [0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 1, 0, 0, 0, 0, 1, 1, 0, 1, 1, 1, 0],
                 [2, 1, 1, 0.5, 0.5, 0.5, 2, 1, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 4, 0.5, 0.5, 0.5, 0.5, 1.5, 1, 0.5, 0.5, 0.5, 2, 0.5,
                  0.5, 0.5, 0.5, 1, 1, 0.5, 0.75, 0.75, 2, 0.5]]
rhythm_piano5 = [[4, 5, 6, 3, 1, 7, 1, 2, 1, 1, 2, 3, 6, 1, 7, 1, 2, 3, 4, 5, 6, 3, 1, 7, 1, 2, 1, 3, 4, 5, 6, 3, 1, 6, 2, 2, 3, 3],
                 [0, 0, 0, 1, 1, 0, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 1, 1, 1, 0, 0, 0, 0, 1, 1, 0, 1, 1, 1, 0],
                 [0.5, 0.5, 0.5, 1.5, 1, 0.5, 0.5, 0.5, 1, 0.5, 0.5, 4, 0.5, 1, 1, 0.5, 1, 0.5, 0.5, 0.5, 0.5, 1.5, 1, 0.5, 0.5, 0.5,
                  2, 0.5, 0.5, 0.5, 0.5, 1, 1, 0.5, 0.75, 0.75, 2, 0.5]]
rhythm_piano6 = [[4, 5, 6, 3, 1, 7, 1, 2, 1, 1, 2, 3, 6, 4, 3, 1, 2, 1, 6, 6, 3, 1, 2, 6, 5, 3, 2, 1, 2, 3, 4, 3, 4, 3, 2, 1, 3, 6],
                 [0, 0, 0, 1, 1, 0, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
                 [0.5, 0.5, 0.5, 1.5, 1, 0.5, 0.5, 0.5, 1, 0.5, 0.5, 4, 0.5, 1, 1, 0.5, 1, 16, 0.5, 1, 0.5, 0.25, 1.75, 0.5, 1, 0.5,
                  0.25, 1.25, 0.5, 0.5, 0.5, 0.25, 0.75, 0.5, 0.75, 0.75, 4, 0.5]]
rhythm_piano7 = [rhythm_piano3, rhythm_piano4, rhythm_piano5]
rhythm_piano8 = [[4, 5, 6, 3, 1, 7, 1, 2, 1, 1, 2, 3, 6, 4, 3, 1, 2, 1, 3, 4, 5, 6, 3, 1, 7, 1, 2, 1, 3, 4, 5, 6, 3, 1, 6, 2, 2, 3, 3],
                 [0, 0, 0, 1, 1, 0, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 0, 1, 1, 1, 0, 0, 0, 0, 1, 1, 0, 1, 1, 1, 0],
                 [0.5, 0.5, 0.5, 1.5, 1, 0.5, 0.5, 0.5, 1, 0.5, 0.5, 4, 0.5, 1, 1, 0.5, 1, 32, 0.5, 0.5, 0.5, 0.5, 1.5, 1, 0.5, 0.5,
                  0.5, 2, 0.5, 0.5, 0.5, 0.5, 1, 1, 0.5, 0.75, 0.75, 2, 0.5]]
rhythm_piano9 = [[4, 5, 6, 3, 1, 7, 1, 2, 1, 1, 2, 3, 6, 4, 3, 1, 2, 1],
                 [0, 0, 0, 1, 1, 0, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1],
                 [0.5, 0.5, 0.5, 1.5, 1, 0.5, 0.5, 0.5, 1, 0.5, 0.5, 4, 0.5, 1, 1, 0.5, 1, 40.5]]
y1 = [rhythm_piano1, rhythm_piano2, rhythm_piano3, rhythm_piano4, rhythm_piano5, rhythm_piano6, rhythm_piano7,
      rhythm_piano8, rhythm_piano5, rhythm_piano9]
rhythm_sax1 = [[0, 1, 5, 7, 1, 2, 7, 5, 3, 7, 5, 6, 5],
               [1, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0],
               [319.5, 0.5, 10.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.5, 0.25, 6]]
rhythm_sax2 = [[5, 6, 1, 3, 4, 5, 6, 3, 4, 5, 4, 3, 4, 5, 6, 1, 7, 1, 2],
               [0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1],
               [0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 4.75, 0.25, 0.25, 0.5, 0.5, 0.5, 0.5, 1, 1, 1, 8]]
rhythm_sax3 = [[0, 1, 6],
               [0, 0, 0],
               [56.5, 0.25, 0.25]]
rhythm_sax4 = [[5, 1, 1, 6, 5, 1, 1, 2, 3, 3, 2, 1, 6, 5, 1, 1, 6, 5, 1, 1, 2, 1, 7, 5, 3, 6, 6],
               [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0],
               [0.5, 0.25, 0.75, 0.5, 0.5, 0.25, 2.25, 0.5, 0.5, 0.5, 0.25, 0.75, 0.5, 0.5, 0.25, 0.75, 0.5, 0.5, 0.25,
                1.75, 0.25, 0.75, 0.5, 0.5, 0.5, 0.75, 0.25]]
rhythm_sax5 = [[5, 1, 1, 6, 5, 1, 1, 3, 3, 4, 3, 2, 1, 7, 1, 6, 7, 1, 2, 3, 2, 3, 4, 5, 6, 7, 1, 7, 2, 1, 7, 1, 7],
               [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, -1, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 1, 0],
               [0.5, 0.25, 0.75, 0.5, 0.5, 0.25, 1.25, 0.5, 0.5, 0.25, 0.25, 0.25, 1.75, 0.25, 0.25, 0.5, 0.5, 0.5, 0.5,
                0.5, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 1, 1, 1, 1, 0.25, 0.25, 7.5]]
y2 = [rhythm_sax1, rhythm_sax2, rhythm_sax3, rhythm_sax4, rhythm_sax5]
rhythm_drum1 = [[1, 4, 1, 1, 4],
                [-3, -3, -3, -3, -3],
                [1, 0.75, 0.75, 0.5, 1]]
rhythm_organ1 = [[0, 3, 4, 5, 6, 3, 1, 6, 2, 1, 3, 4, 5, 6, 3, 1, 6, 2, 2, 3, 3],
                 [0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 1, 1, 1, 0],
                 [15.5, 0.5, 0.5, 0.5, 0.5, 1, 1, 0.5, 1.5, 2, 0.5, 0.5, 0.5, 0.5, 1, 1, 0.5, 0.75, 0.75, 2, 0.5]]
rhythm_organ2 = [[4, 5, 6, 3, 1, 6, 2, 1, 1, 2, 3, 1, 6, 5, 0, 5, 5, 6, 6, 0],
                 [0, 0, 0, 1, 1, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0],
                 [0.5, 0.5, 0.5, 1, 1, 0.5, 1.5, 1, 0.5, 0.5, 2, 2, 0.5, 4, 60, 1, 1, 0.5, 0.5, 33]]
rhythm_organ3 = [[0, 3, 4, 5, 6, 3, 1, 6, 2, 2, 1, 1, 2, 3, 1, 6, 5, 0, 0],
                 [0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0],
                 [31.5, 0.5, 0.5, 0.5, 0.5, 1, 1, 0.5, 1, 0.5, 1, 0.5, 0.5, 2, 2, 0.5, 4, 112, 152]]
y3 = [rhythm_organ1, rhythm_organ2, rhythm_organ3]
# ============================================================
#  Synthesis Engine
# ============================================================
SAMPLE_RATE = 11025
TEMPO = 80  # BPM
# C major scale frequencies (octave 4 base)
_SCALE = {
    1: 261.63, 2: 293.66, 3: 329.63, 4: 349.23,
    5: 392.00, 6: 440.00, 7: 493.88,
}
def _freq(note, octave):
    """Convert scale degree + octave shift to Hz."""
    if note == 0:
        return 0.0
    base = _SCALE.get(abs(note), 440.0)
    return base * (2.0 ** octave)
def _render_note(freq, dur_sec, t_row):
    """Generate a single note's waveform with envelope."""
    n = max(int(SAMPLE_RATE * dur_sec), 1)
    if freq <= 0:
        return np.zeros(n, dtype=np.float32)
    t = np.arange(n, dtype=np.float32) / SAMPLE_RATE
    sig = np.zeros(n, dtype=np.float32)
    for h, amp in enumerate(t_row):
        if amp > 0:
            sig += amp * np.sin(2.0 * np.pi * freq * (h + 1.0) * t)
    peak = float(np.max(np.abs(sig)))
    if peak > 1e-12:
        sig *= (1.0 / peak)
    # ADSR envelope
    env = np.full(n, 0.7, dtype=np.float32)
    a = int(0.01 * SAMPLE_RATE)   # 10ms attack
    d = int(0.03 * SAMPLE_RATE)   # 30ms decay
    r = int(0.08 * SAMPLE_RATE)   # 80ms release
    if a > 0 and a <= n:
        env[:a] = np.linspace(0.0, 1.0, a, dtype=np.float32)
    d_end = min(a + d, n)
    if d_end > a:
        env[a:d_end] = np.linspace(1.0, 0.7, d_end - a, dtype=np.float32)
    rel_start = max(0, n - r)
    if rel_start < n:
        env[rel_start:] = np.linspace(0.7, 0.0, n - rel_start, dtype=np.float32)
    return (sig * env * 0.7).astype(np.float32)
def _render_drum(dur_sec, kind=1):
    """Generate a drum hit (noise burst with fast decay)."""
    n = max(int(SAMPLE_RATE * dur_sec), 1)
    noise = np.random.uniform(-1.0, 1.0, n).astype(np.float32)
    # Different drum "kinds": adjust decay and low-pass
    if kind == 1:
        decay = np.exp(-np.arange(n, dtype=np.float32) / (SAMPLE_RATE * 0.04))
    else:
        decay = np.exp(-np.arange(n, dtype=np.float32) / (SAMPLE_RATE * 0.08))
    return (noise * decay * 0.5).astype(np.float32)
def _merge_polyphonic(rhythms):
    """Merge multiple simultaneous rhythms into one, time-ordered."""
    events = []
    for r in rhythms:
        t = 0.0
        for i in range(len(r[0])):
            events.append((t, r[0][i], r[1][i], r[2][i]))
            t += r[2][i]
    events.sort(key=lambda x: x[0])
    notes, octs, durs = [], [], []
    prev = 0.0
    for start, n_val, octv, dur in events:
        gap = start - prev
        if gap > 1e-6:
            notes.append(0)
            octs.append(0)
            durs.append(gap)
            prev = start
        notes.append(n_val)
        octs.append(octv)
        durs.append(dur)
        prev = start + dur
    return [notes, octs, durs]
def _is_polyphonic(item):
    """Check if item is a polyphonic group (list of rhythms)."""
    if not (isinstance(item, list) and len(item) > 0 and isinstance(item[0], list)):
        return False
    if len(item[0]) == 0:
        return False
    return isinstance(item[0][0], list)
def _flatten_voice(voice):
    """Flatten a voice: merge polyphonic sections, keep normal rhythms."""
    flat = []
    for item in voice:
        if _is_polyphonic(item):
            flat.append(_merge_polyphonic(item))
        else:
            flat.append(item)
    return flat
def _voice_duration(voice):
    """Total duration of a flattened voice in beats."""
    return sum(float(d) for r in voice for d in r[2])
def _render_voice(voice, timbre_idx):
    """Render a flattened voice to a numpy float32 array."""
    beat_sec = 60.0 / TEMPO
    total_beats = _voice_duration(voice)
    total_smp = int(total_beats * beat_sec * SAMPLE_RATE) + SAMPLE_RATE
    out = np.zeros(total_smp, dtype=np.float32)
    pos = 0
    for rhythm in voice:
        notes, octaves, durations = rhythm[0], rhythm[1], rhythm[2]
        for i in range(len(notes)):
            dur_sec = float(durations[i]) * beat_sec
            note = notes[i]
            octave = octaves[i]
            n_smp = max(int(dur_sec * SAMPLE_RATE), 1)
            if octave < -2:
                # Drum / noise
                wav = _render_drum(dur_sec, abs(note))
            else:
                f = _freq(note, octave)
                wav = _render_note(f, dur_sec, timbre[timbre_idx])
            fill = min(len(wav), total_smp - pos)
            if fill > 0:
                out[pos:pos + fill] += wav[:fill]
            pos += n_smp
    return out[:pos]
# ============================================================
#  Render & Output
# ============================================================
print("Flattening voices...")
pf = _flatten_voice(y1)
sf = _flatten_voice(y2)
of = _flatten_voice(y3)
print(f"  Piano: {_voice_duration(pf):.0f} beats, {len(pf)} sections")
print(f"  Sax:   {_voice_duration(sf):.0f} beats, {len(sf)} sections")
print(f"  Organ: {_voice_duration(of):.0f} beats, {len(of)} sections")
print("Rendering piano...")
piano = _render_voice(pf, 0)  # timbre[0]
print("Rendering sax...")
sax = _render_voice(sf, 1)    # timbre[1]
print("Rendering organ...")
organ = _render_voice(of, 2)  # timbre[2]
# Pad to max length
max_len = max(len(piano), len(sax), len(organ))
def _pad(arr, length):
    if len(arr) < length:
        return np.pad(arr, (0, length - len(arr)), mode='constant').astype(np.float32)
    return arr.astype(np.float32)
piano = _pad(piano, max_len)
sax = _pad(sax, max_len)
organ = _pad(organ, max_len)
# Mix with volume levels
mixed = piano * 0.75 + sax * 0.5 + organ * 0.55
# Master normalize
peak = float(np.max(np.abs(mixed)))
if peak > 1e-12:
    mixed *= (0.9 / peak)
# Convert to 16-bit PCM
samples_int16 = (mixed * 32767.0).astype(np.int16)
del mixed, piano, sax, organ  # free memory
# ── 打包 WAV → <audio> 标签 ──
buf = io.BytesIO()
with wave.open(buf, 'wb') as w:
    w.setnchannels(1)
    w.setsampwidth(2)
    w.setframerate(SAMPLE_RATE)
    w.writeframes(samples_int16.tobytes())
buf.seek(0)
audio_b64 = base64.b64encode(buf.read()).decode()
# 把回放所需数据暴露给 JS,用于 audio 标签 ↔ simpleaudio 双向切换
from js import window
window._pyodideAudio = dict(
    base64=audio_b64,
    channels=1,
    sample_width=2,
    sample_rate=SAMPLE_RATE,
)
# Play via simpleaudio(非阻塞,后台播放)
print("Playing...")
play_obj = sa.play_buffer(samples_int16, 1, 2, SAMPLE_RATE)
window._pyodidePlayObj = play_obj
total_sec = max_len / SAMPLE_RATE
print(f"Total {total_sec:.1f}s — 点击下方 audio 控件可切换播放源")
f'<audio controls src="data:audio/wav;base64,{audio_b64}" style="max-width:100%;"></audio>'
Love Story — Taylor Swift
"""Love Story (Taylor Swift) — D大调 BPM 119,完整简谱合成,多轨混音,<audio> 标签下载"""
import io, base64, wave, math
import numpy as np
import simpleaudio as sa
from js import window
SAMPLE_RATE = 44100
BPM = 119
beat = 60.0 / BPM                   # 一拍 ≈ 0.504 s
bar_beats = 4.0                     # 4 / 4 拍
total_bars = 64                     # 64 小节 ≈ 2:09
# 音名 → 频率 (Hz),D 大调(含 F#、C#),C4 = 261.63
notes = {
    "C#3": 138.59, "D3": 146.83, "E3": 164.81, "F#3": 185.00,
    "G3": 196.00, "A3": 220.00, "B3": 246.94,
    "C#4": 277.18, "D4": 293.66, "E4": 329.63, "F#4": 369.99,
    "G4": 392.00, "A4": 440.00, "B4": 493.88, "C#5": 554.37,
    "D5": 587.33, "E5": 659.25, "F#5": 739.99, "G5": 783.99, "A5": 880.00,
    "R": 0,                         # 休止符(频率 0 = 无声)
}
def tone(freq, dur, harmonics, adsr=(0.02, 0.08)):
    """生成单音:基频 + 泛音 + ADSR 包络"""
    n = int(SAMPLE_RATE * dur)
    if n < 1: return np.zeros(0, dtype=np.float32)
    t = np.arange(n, dtype=np.float32) / SAMPLE_RATE
    sig = np.zeros(n, dtype=np.float32)
    for i, amp in enumerate(harmonics):
        sig += amp * np.sin(2 * np.pi * freq * (i + 1) * t)
    attack_n = int(adsr[0] * SAMPLE_RATE)
    release_n = int(adsr[1] * SAMPLE_RATE)
    env = np.ones(n, dtype=np.float32)
    if attack_n > 0 and attack_n < n:
        env[:attack_n] = np.linspace(0, 1, attack_n)
    if release_n > 0 and release_n < n:
        env[-release_n:] = np.linspace(1, 0, release_n)
    return (sig * env).astype(np.float32)
def render_track(sequence, harmonics, adsr=(0.01, 0.05)):
    """将 (音名, 拍数) 序列渲染为音频数组;遇 R 输出静音段"""
    total_beats = sum(d for _, d in sequence)
    total_samples = int(SAMPLE_RATE * total_beats * beat)
    buf = np.zeros(total_samples, dtype=np.float32)
    pos = 0
    for name, dur_beat in sequence:
        dur = dur_beat * beat
        n = int(SAMPLE_RATE * dur)
        if n < 1: continue
        chunk = tone(notes[name], dur, harmonics, adsr)
        if pos + len(chunk) <= len(buf):
            buf[pos:pos + len(chunk)] += chunk
        pos += n
    return buf, total_beats
# ═══════════════════════════════════════════════════
# 1. Bass — D–A–Bm–G(I–V–vi–IV in D)
# ═══════════════════════════════════════════════════
bass_seq = []
for _ in range(total_bars // 2):
    bass_seq += [("D3", 2.0), ("A3", 2.0), ("B3", 2.0), ("G3", 2.0)]
bass_track, _ = render_track(bass_seq, [0.85, 0.15])
# ═══════════════════════════════════════════════════
# 2. Chords — D / A / Bm / G 三音 Pad,每和弦 2 拍
# ═══════════════════════════════════════════════════
chord_triads = {
    "D":  ["D4", "F#4", "A4"],
    "A":  ["A3", "C#4", "E4"],
    "Bm": ["B3", "D4", "F#4"],
    "G":  ["G3", "B3", "D4"],
}
chord_seq_roots = []
for _ in range(total_bars // 2):
    chord_seq_roots += ["D", "A", "Bm", "G"]
chord_beats = len(chord_seq_roots) * 2.0
chord_len = int(SAMPLE_RATE * chord_beats * beat)
chord_track = np.zeros(chord_len, dtype=np.float32)
pos = 0
for root in chord_seq_roots:
    dur = 2.0 * beat
    n = int(SAMPLE_RATE * dur)
    if n < 1: continue
    chord_sig = np.zeros(n, dtype=np.float32)
    for note_name in chord_triads[root]:
        freq = notes[note_name]
        t = np.arange(n, dtype=np.float32) / SAMPLE_RATE
        chord_sig += 0.12 * np.sin(2 * np.pi * freq * t)
    attack_n = int(0.03 * SAMPLE_RATE)
    release_n = int(0.10 * SAMPLE_RATE)
    env = np.ones(n, dtype=np.float32)
    if attack_n < n: env[:attack_n] = np.linspace(0, 1, attack_n)
    if release_n < n: env[-release_n:] = np.linspace(1, 0, release_n)
    if pos + n <= chord_len:
        chord_track[pos:pos + n] += (chord_sig * env).astype(np.float32)
    pos += n
# ═══════════════════════════════════════════════════
# 3. Melody — 完整简谱(1 = D 4 / 4 ♪=119,全曲 64 小节)
# ═══════════════════════════════════════════════════
# 简谱→音名:1 = D4 2 = E4 3 = F#4 4 = G4 5 = A4 6 = B4 7 = C#5
#            1,=D3 … 7,=C#4    .1=D5 .2=E5 .3=F#5 .4=G5 .5=A5
# _= 八分 (0.5 拍)  无_= 四分 (1 拍)  -= 延 1 拍  R = 休止  |= 小节线
melody_seq = [
    # ══════ Intro 前奏(bars 1–4, jianpu lines 3–10)══════
    # 前奏仅伴奏,旋律休止 4 bars = 16 beats
    ("R",16.0),
    # ══════ Verse 1(bars 5–12, 8 bars, lines 12–28)══════
    # Bar 5 ─ "We were both young when I first saw you" ────
    #   7,_1_ 1 1. 1_ | 7,_1_ 2 1 1_
    ("C#4",0.5),("D4",0.5),("D4",1.0),("D5",1.0),("D4",0.5),
    ("C#4",0.5),("D4",0.5),("E4",1.0),("D4",1.0),("D4",1.0),
    # Bar 6 ─ "I close my eyes and the flash-back starts" ──
    #   7,_.1_ .1 .1_ 1_ | 7,_1_ 2_ 1_ 7,_1_
    ("C#4",0.5),("D5",0.5),("D5",1.0),("D5",0.5),("D4",0.5),
    ("C#4",0.5),("D4",0.5),("E4",0.5),("D4",0.5),("C#4",0.5),("D4",1.0),
    # Bar 7 ─ "I'm stan-ding there / On a bal-co-ny" ──────
    #   6,-- 0 0 | 0 1_1_1_ 7,_7,_5,_
    ("B3",2.0),("R",2.0),
    ("R",0.5),("D4",0.5),("D4",0.5),("D4",0.5),("C#4",0.5),("C#4",0.5),("A3",0.5),
    # Bar 8 ─ "in sum-mer air" ─────────────────────────────
    #   5,_5,_6,- 0 |
    ("A3",0.5),("A3",0.5),("B3",2.0),("R",1.0),
    # Bar 9 ─ "See the lights, see the par-ty, the" ────────
    #   0 0 0 0_ | 7,_.1_ .1 - 1_1_
    ("R",3.5),("D4",0.5),
    ("C#4",0.5),("D5",0.5),("D5",2.0),("D4",0.5),("D4",0.5),
    # Bar 10 ─ "ball gowns / See you make your way" ────────
    #   7,_7,_1_2_ 1. | 7,_1_1_1_ 1_
    ("C#4",0.5),("C#4",0.5),("D4",0.5),("E4",0.5),("D5",1.0),
    ("C#4",0.5),("D4",0.5),("D4",0.5),("D4",0.5),("D4",1.0),
    # Bar 11 ─ "through the crowd / And say Hel-lo" ────────
    #   7,_1_2_1_ 7,_1_ | 6,-- 0 0
    ("C#4",0.5),("D4",0.5),("E4",1.0),("D4",0.5),("C#4",0.5),("D4",1.0),
    ("B3",2.0),("R",2.0),
    # Bar 12 ─ "Lit-tle did I know" (pickup to pre) ────────
    #   0 0 1_7,_5,_5,_ | - 6,- 0
    ("R",1.5),("D4",0.5),("C#4",0.5),("A3",0.5),("A3",0.5),("B3",2.0),("R",2.0),
    ("R",3.5),("D4",0.5),
    # ══════ Pre-Chorus 1(bars 13–16, 4 bars, lines 29–36)══════
    # Bar 13 ─ "That you were Ro-me-o, you were throw-ing" ──
    #   7,_1_1_1_ 1_1_1_ | 7,_1_2_ 1_1_1_
    ("C#4",0.5),("D4",0.5),("D4",0.5),("D4",0.5),("D4",0.5),("D4",0.5),("D4",0.5),("D4",0.5),
    # Bar 14 ─ "peb-bles / And my dad-dy said, Stay a-way" ──
    #   1_1_2_1_ 1_1_ | 5_4_3_3 2_3_2_
    ("D4",0.5),("D4",0.5),("E4",0.5),("D4",0.5),("D4",0.5),("D4",0.5),("D4",0.5),("E4",0.5),
    ("A4",0.5),("G4",0.5),("F#4",0.5),("F#4",1.0),("E4",0.5),("F#4",0.5),("E4",0.5),
    # Bar 15 ─ "from Ju-li-et / And I was cry-ing on the" ──
    #   3_2_3_2_ 3 1 | 1_2_2_2_ 1 3_
    ("F#4",0.5),("E4",0.5),("F#4",0.5),("E4",0.5),("F#4",1.0),("D4",1.0),
    ("D4",0.5),("E4",0.5),("E4",0.5),("E4",0.5),("D4",1.0),("F#4",1.0),
    # Bar 16 ─ "stair-case / Beg-ging you please don't go," ─
    #   4_3.-- | 0 3_3 1 . =||
    ("G4",2.0),("F#4",2.0),
    ("R",0.5),("F#4",0.5),("F#4",1.0),("D4",1.0),("R",1.0),
    # ══════ Chorus 1(bars 17–24, 8 bars, lines 38–45)══════
    # Bar 17 ─ "Ro-me-o, take me some-where we can be" ──────
    #   1_1_ 4 3 1 | 1_2_2_1_ 3_1_2
    ("D4",0.5),("D4",0.5),("G4",1.0),("F#4",1.0),("D4",1.0),
    ("D4",0.5),("E4",0.5),("E4",0.5),("D4",0.5),("F#4",0.5),("D4",0.5),("E4",1.0),
    # Bar 18 ─ "a-lone / I'll be wai-ting, all there's" ─────
    #   1 2 3 2 | 1_2_2_1_ 3_1_2
    ("D4",1.0),("E4",1.0),("F#4",1.0),("E4",1.0),
    ("D4",0.5),("E4",0.5),("E4",0.5),("D4",0.5),("F#4",0.5),("D4",0.5),("E4",1.0),
    # Bar 19 ─ "left to do is run / You'll be the prince" ───
    #   1 2_1_ 3 2 | 1 2_1_ 3 2
    ("D4",1.0),("E4",0.5),("D4",0.5),("F#4",1.0),("E4",1.0),
    ("D4",1.0),("E4",0.5),("D4",0.5),("F#4",1.0),("E4",1.0),
    # Bar 20 ─ "and I'll be the prin-cess / It's a love" ────
    #   1_1_ 2 3_2_ 3 | 3_2_ 3 3 1
    ("D4",0.5),("D4",0.5),("E4",1.0),("F#4",0.5),("E4",0.5),("F#4",1.0),
    ("F#4",0.5),("E4",0.5),("F#4",1.0),("F#4",1.0),("D4",1.0),
    # Bar 21 ─ "sto-ry, ba-by, just say ""Yes"" / Ro-me-o," ──
    #   1_1_ 4 3 1_1_ | 1_2_2_1_ 3_1_2
    ("D4",0.5),("D4",0.5),("G4",1.0),("F#4",1.0),("D4",0.5),("D4",0.5),
    ("D4",0.5),("E4",0.5),("E4",0.5),("D4",0.5),("F#4",0.5),("D4",0.5),("E4",1.0),
    # Bar 22 ─ "save me, they're try-ing to tell me how to" ──
    #   1_1_ 4 3 1_1_ | 0 1_2_1_ 3 3_2_
    ("D4",0.5),("D4",0.5),("G4",1.0),("F#4",1.0),("D4",0.5),("D4",0.5),
    ("R",0.5),("D4",0.5),("E4",0.5),("D4",0.5),("F#4",1.0),("F#4",0.5),("E4",0.5),
    # Bar 23 ─ "feel / This love is dif-fi-cult, but it's" ──
    #   0 1_2_1_ 5 2 | 1 2_1_ 3 2
    ("R",0.5),("D4",0.5),("E4",0.5),("D4",0.5),("A4",1.0),("E4",1.0),
    ("D4",1.0),("E4",0.5),("D4",0.5),("F#4",1.0),("E4",1.0),
    # Bar 24 ─ "real / Don't be a-fraid, we'll make it out" ──
    #   1_2_2_1_ 3 2 | 1_1_ 2 3_2_3_2_
    ("D4",0.5),("E4",0.5),("E4",0.5),("D4",0.5),("F#4",1.0),("E4",1.0),
    ("D4",0.5),("D4",0.5),("E4",1.0),("F#4",0.5),("E4",0.5),("F#4",0.5),("F#4",0.5),
    # ══════ Verse 2(bars 25–32, 8 bars, lines 47–54)══════
    # Bar 25 ─ "of this mess / It's a love sto-ry, ba-by," ──
    #   3_2_ 3 3 1 | - 0 0 0_ 3_2_
    ("F#4",0.5),("E4",0.5),("F#4",1.0),("F#4",1.0),("D4",1.0),
    ("R",3.5),("F#4",0.5),("E4",0.5),
    # Bar 26 ─ "just say ""Yes"" / Oh, oh, oh" ───────────────
    #   - 3.-- 0 | 0.5,_ 1_2_ - 3.--
    ("D5",1.0),("D4",1.0),("R",2.0),
    ("R",0.5),("A3",0.5),("D4",0.5),("E4",0.5),("D5",2.0),("D4",1.0),
    # Bar 27 ─ " / So I sneak out to the gar-den to" ────────
    #   0 0 0 0_1_ | 7,_1_1.1_ 1_7,_7,_
    ("R",3.5),("D4",0.5),
    ("C#4",0.5),("D4",0.5),("D4",0.5),("D5",0.5),("D4",0.5),("D4",0.5),("C#4",0.5),("C#4",0.5),
    # Bar 28 ─ "see you / We keep qui-et, 'cause we're" ─────
    #   1_2_1. | 7,_1_1.1_ 1_7,_7,_
    ("D4",0.5),("E4",0.5),("D5",1.0),
    ("C#4",0.5),("D4",0.5),("D4",0.5),("D5",0.5),("D4",0.5),("C#4",0.5),("C#4",0.5),("C#4",0.5),
    # Bar 29 ─ "dead if they knew / So close your eyes /" ────
    #   1_2_1_ 7,_7,_ | 6,-- 0 0
    ("D4",0.5),("E4",0.5),("D5",0.5),("C#4",0.5),("C#4",0.5),("C#4",0.5),
    ("B3",2.0),("R",2.0),
    # Bar 30 ─ "Es-cape this town for a lit-tle while," ─────
    #   0 1_1_1_ 1_7,_5,_ | 5,_5,_6,-.7,_
    ("R",0.5),("D4",0.5),("D4",0.5),("D4",0.5),("C#4",0.5),("A3",0.5),
    ("A3",0.5),("A3",0.5),("B3",1.5),("C#4",0.5),
    # Bar 31 ─ "oh, oh / 'Cause you were Ro-me-o, I was" ────
    #   -.3_ 0_3_2_1_ | 1_1_1_1_ 1_1_1_
    ("F#4",1.0),("R",0.5),("F#4",0.5),("E4",0.5),("D4",0.5),
    ("D4",0.5),("D4",0.5),("D4",0.5),("D4",0.5),("D4",0.5),("D4",0.5),("D4",0.5),("D4",0.5),
    # Bar 32 ─ "a scar-let let-ter / And my dad-dy said," ────
    #   7,_1_2_1_ 1_1_ | 1_1_2_1_ 1_1_1_
    ("C#4",0.5),("D4",0.5),("E4",0.5),("D4",0.5),("D4",0.5),("D4",0.5),("D4",0.5),("D4",0.5),
    ("D4",0.5),("D4",0.5),("E4",0.5),("D4",0.5),("D4",0.5),("D4",0.5),("D4",0.5),("E4",0.5),
    # ══════ Pre-Chorus 2(bars 33–36, lines 56–63)══════
    # Bar 33 ─ "Stay a-way from Ju-li-et / But you were" ─────
    #   5_4_3_ 3_2_3_3_ | 3_3_3_3_ 2_2_1_
    ("A4",0.5),("G4",0.5),("F#4",0.5),("E4",0.5),("F#4",0.5),("F#4",0.5),("F#4",0.5),("F#4",0.5),
    # Bar 34 ─ "ev-ery-thing to me / I was beg-ging you," ────
    #   2_2_2_2_ 1 3_ | 4_3.-- 0 3_3
    ("F#4",0.5),("E4",0.5),("F#4",0.5),("E4",0.5),("F#4",0.5),("D4",0.5),("D4",0.5),("F#4",0.5),
    ("E4",0.5),("E4",0.5),("E4",0.5),("E4",0.5),("D4",1.0),("F#4",1.0),
    # Bar 35 ─ "please don't go, and I said" ────────────────
    #   1 = 2 = - ||
    ("G4",2.0),("F#4",1.0),("R",0.5),("F#4",0.5),("F#4",1.0),("D4",1.0),
    # Bar 36 ─ (instrumental pickup to chorus 2) ────────────
    ("R",4.0),
    # ══════ Chorus 2(bars 37–48, 12 bars, lines 66–83)══════
    # Bar 37 ─ "Ro-me-o, take me some-where we can be" ──────
    ("D4",0.5),("D4",0.5),("G4",1.0),("F#4",1.0),("D4",1.0),
    ("D4",0.5),("E4",0.5),("E4",0.5),("D4",0.5),("F#4",0.5),("D4",0.5),("E4",1.0),
    # Bar 38 ─ "a-lone / I'll be wai-ting, all there's" ─────
    ("D4",1.0),("E4",1.0),("F#4",1.0),("E4",1.0),
    ("D4",0.5),("E4",0.5),("E4",0.5),("D4",0.5),("F#4",0.5),("D4",0.5),("E4",1.0),
    # Bar 39 ─ "left to do is run / You'll be the prince" ───
    ("D4",1.0),("E4",0.5),("D4",0.5),("F#4",1.0),("E4",1.0),
    ("D4",1.0),("E4",0.5),("D4",0.5),("F#4",1.0),("E4",1.0),
    # Bar 40 ─ "and I'll be the prin-cess / It's a love" ────
    ("D4",0.5),("D4",0.5),("E4",1.0),("F#4",0.5),("E4",0.5),("F#4",1.0),
    ("F#4",0.5),("E4",0.5),("F#4",1.0),("F#4",1.0),("D4",1.0),
    # Bar 41 ─ "sto-ry, ba-by, just say ""Yes"" / Ro-me-o," ──
    ("D4",0.5),("D4",0.5),("G4",1.0),("F#4",1.0),("D4",0.5),("D4",0.5),
    ("D4",0.5),("E4",0.5),("E4",0.5),("D4",0.5),("F#4",0.5),("D4",0.5),("E4",1.0),
    # Bar 42 ─ "save me, they're try-ing to tell me how to" ──
    ("D4",0.5),("D4",0.5),("G4",1.0),("F#4",1.0),("D4",0.5),("D4",0.5),
    ("R",0.5),("D4",0.5),("E4",0.5),("D4",0.5),("F#4",0.5),("F#4",0.5),("E4",0.5),("E4",0.5),
    # Bar 43 ─ "feel / This love is dif-fi-cult, but it's" ──
    ("R",0.5),("D4",0.5),("E4",0.5),("D4",0.5),("A4",1.0),("E4",1.0),
    ("D4",1.0),("E4",0.5),("D4",0.5),("F#4",1.0),("E4",1.0),
    # Bar 44 ─ "real / Don't be a-fraid, we'll make it out" ──
    ("D4",0.5),("E4",0.5),("E4",0.5),("D4",0.5),("F#4",1.0),("E4",1.0),
    ("D4",0.5),("D4",0.5),("E4",1.0),("F#4",0.5),("E4",0.5),("F#4",0.5),("F#4",0.5),
    # Bar 45 ─ "of this mess / It's a love sto-ry, ba-by," ──
    ("F#4",0.5),("E4",0.5),("F#4",1.0),("F#4",1.0),("D4",1.0),
    ("R",3.5),("F#4",0.5),
    # Bar 46 ─ "just say ""Yes"" / Oh, oh," ──────────────────
    ("D5",1.0),("D4",1.0),("R",2.0),
    ("R",0.5),("A3",0.5),("D4",0.5),("E4",0.5),("D5",2.0),("D4",1.0),
    # Bar 47 ─ "oh / Oh, oh," ───────────────────────────────
    ("R",1.0),("D5",2.0),("D4",1.0),
    ("R",0.5),("A3",0.5),("F#4",0.5),("E4",0.5),("D5",1.0),("D4",1.0),
    # Bar 48 ─ (segue to bridge) ────────────────────────────
    ("R",4.0),
    # ══════ Bridge(bars 49–56, 8 bars, lines 89–97)══════
    # Bar 49 ─ "But I got tired of wai-ting / Won-der-ing" ──
    #   5,_4,_4,_5,_ 3_2_1_7,_ | 7,.1.-
    ("A3",0.5),("G3",0.5),("G3",0.5),("A3",0.5),("F#3",0.5),("E3",0.5),("D3",0.5),("C#4",0.5),
    ("C#4",1.0),("D4",0.5),("D4",1.0),("R",0.5),
    # Bar 50 ─ "if you were e-ver com-ing a-round / My faith" ──
    #   0 0 6,_1_1_1_ | 2_3_3_3_ 3_4_3_2_
    ("R",0.5),("R",0.5),("B3",0.5),("D4",0.5),("D4",0.5),("D4",0.5),
    ("E4",0.5),("F#4",0.5),("F#4",0.5),("F#4",0.5),("F#4",0.5),("G4",0.5),("F#4",0.5),("E4",0.5),
    # Bar 51 ─ "in you was fa-ding / When I met you on the" ──
    #   - 0 1_3_1_1_7,_ | -.1_2.1_
    ("R",1.0),("D4",0.5),("F#4",0.5),("D4",0.5),("D4",0.5),("C#4",0.5),
    ("C#4",0.5),("D5",0.5),("E5",1.0),("D5",1.0),("R",0.5),
    # Bar 52 ─ "out-skirts of town, and I said" ─────────────
    #   - 0 1_1_ | 2_3_3_3_ 5_.2_.2_
    ("R",1.0),("D4",0.5),("D4",0.5),
    ("E4",0.5),("F#4",0.5),("F#4",0.5),("F#4",0.5),("A4",0.5),("E5",0.5),("E5",1.0),
    # Bar 53 ─ "Ro-me-o, save me, I've been feel-ing so" ────
    #   1_1_4_3_2_ 1 | 1_2_2_1_ 3_1_2
    ("D4",0.5),("D4",0.5),("G4",0.5),("F#4",0.5),("E4",0.5),("D4",1.0),
    ("D4",0.5),("E4",0.5),("E4",0.5),("D4",0.5),("F#4",0.5),("D4",0.5),("E4",1.0),
    # Bar 54 ─ "a-lone / I keep wai-ting for you, but you" ──
    #   1 2 3 2 | 1_2_2_1_ 3_1_2_2_
    ("D4",1.0),("E4",1.0),("F#4",1.0),("E4",1.0),
    ("D4",0.5),("E4",0.5),("E4",0.5),("D4",0.5),("F#4",0.5),("D4",0.5),("E4",0.5),("E4",0.5),
    # Bar 55 ─ "ne-ver come / Is this in my head? I don't" ──
    #   1 2_1_3_2_ 1_ | 1 2_1_3_2_ 1_
    ("D4",1.0),("E4",0.5),("D4",0.5),("F#4",0.5),("E4",0.5),("D4",0.5),
    ("D4",1.0),("E4",0.5),("D4",0.5),("F#4",0.5),("E4",0.5),("D4",0.5),
    # Bar 56 ─ "know what to think / He knelt to the ground" ──
    #   1 2_1_ 3 2 | 1 2_1_3_3_ 2
    ("D4",1.0),("E4",0.5),("D4",0.5),("F#4",1.0),("E4",1.0),
    ("D4",1.0),("E4",0.5),("D4",0.5),("F#4",0.5),("F#4",0.5),("E4",1.0),
    # ══════ Final Chorus 升调(bars 57–62, 6 bars, lines 107–116)══════
    # Note: 简谱标记 (+2key) 从 D→E,因 Python 实现限制,此处仍在 D 大调演唱
    #   实际音高应整体升 2 个半音,当前以 D 大调近似
    # Bar 57 ─ "and pulled out a ring and said, Mar-ry me," ──
    #   1_1_4_3_2_2_1_ | 1_2_2_1_ 3_1_2
    ("D4",0.5),("D4",0.5),("G4",0.5),("F#4",0.5),("E4",0.5),("E4",0.5),("D4",0.5),
    ("D4",0.5),("E4",0.5),("E4",0.5),("D4",0.5),("F#4",0.5),("D4",0.5),("E4",1.0),
    # Bar 58 ─ "Ju-li-et, you'll ne-ver have to be a-lone" ──
    #   1 2 3 2 | 5_3_2_2_ 1_1_1_
    ("D4",1.0),("E4",1.0),("F#4",1.0),("E4",1.0),
    ("A4",0.5),("F#4",0.5),("E4",0.5),("E4",0.5),("D4",0.5),("D4",0.5),("D4",0.5),
    # Bar 59 ─ "I love you and that's all I real-ly know" ───
    #   1 2_1_3_2_ | 1_2_2_3_2_
    ("D4",1.0),("E4",0.5),("D4",0.5),("F#4",0.5),("E4",0.5),
    ("D4",0.5),("E4",0.5),("E4",0.5),("F#4",0.5),("E4",1.0),
    # Bar 60 ─ "I talked to your dad, go pick out a white" ──
    #   1_1_4_3_2_ | 1_2_2_1_ 3_1_2
    ("D4",0.5),("D4",0.5),("G4",0.5),("F#4",0.5),("E4",0.5),
    ("D4",0.5),("E4",0.5),("E4",0.5),("D4",0.5),("F#4",0.5),("D4",0.5),("E4",1.0),
    # Bar 61 ─ "dress / It's a love sto-ry, ba-by, just" ────
    #   1_1_2_ 3_2_3_ | 3_2_3_3_2. 0
    ("D4",0.5),("D4",0.5),("E4",0.5),("F#4",0.5),("E4",0.5),("F#4",0.5),
    ("F#4",0.5),("E4",0.5),("F#4",0.5),("F#4",0.5),("E4",0.5),("R",1.0),
    # Bar 62 ─ "say, ""Yes"" / Oh, oh, oh" ──────────────────
    #   1.-- 0 | 0 .5,_1_2_ - 3.--
    ("D5",1.0),("D4",1.0),("R",1.0),("D4",1.0),
    ("R",0.5),("A3",0.5),("D4",0.5),("E4",0.5),("D5",2.0),("D4",1.0),
    # ══════ Outro(bars 63–64, 2 bars, lines 117–121)══════
    # Bar 63 ─ "Oh, oh, oh, oh / 'Cause we were both young" ──
    #   0 .5,_3_2_ - 6,-- | 0 0 0 0_1_
    ("R",0.5),("A3",0.5),("F#4",0.5),("E4",0.5),("B4",1.0),("B4",1.0),
    ("R",3.5),("D4",0.5),
    # Bar 64 ─ "when I first saw you" ───────────────────────
    #   7,_1_1_1.7,_ | 7,_1_2_ - 1 - -
    ("C#4",0.5),("D4",0.5),("D4",0.5),("D5",0.5),("C#4",0.5),
    ("C#4",0.5),("D4",0.5),("E4",0.5),("D4",1.0),("D4",2.0),
]
melody_track, _ = render_track(melody_seq, [0.40, 0.25, 0.12, 0.05], adsr=(0.005, 0.06))
# ═══════════════════════════════════════════════════
# 4. Drums — 频率扫描 Kick + 分层 Snare + 重音 HH + Crash / Ride
# ═══════════════════════════════════════════════════
bar_sec = bar_beats * beat
total_sec = total_bars * bar_sec
drum_len = int(SAMPLE_RATE * total_sec)
drum_track = np.zeros(drum_len, dtype=np.float32)
# ── Drum synth helpers ──
def drum_kick(dur, sr):
    """Kick:频率 150→55 Hz 扫描 + 瞬态 click"""
    n = int(sr * dur)
    if n < 1: return np.zeros(0, dtype=np.float32)
    t = np.arange(n, dtype=np.float32) / sr
    f0 = 150.0 - 95.0 * np.clip(t / max(dur, 0.001), 0, 1)
    amp = np.exp(-t * 22)
    click = 0.20 * np.exp(-t * 180)
    return (0.85 * np.sin(2 * np.pi * f0 * t) * amp + click * amp).astype(np.float32)
def drum_snare(dur, sr):
    """Snare:200 Hz 体音 + 噪声 crack + 瞬态"""
    n = int(sr * dur)
    if n < 1: return np.zeros(0, dtype=np.float32)
    t = np.arange(n, dtype=np.float32) / sr
    body = 0.28 * np.sin(2 * np.pi * 200 * t)
    noise = np.random.uniform(-1, 1, n).astype(np.float32)
    # 简易高通:噪声乘高频载波 → 偏亮的 crack
    noise_bright = noise * (0.5 + 0.5 * np.sin(2 * np.pi * 2400 * t))
    amp = np.exp(-t * 14)
    return ((body + 0.38 * noise_bright) * amp).astype(np.float32)
def drum_hh(dur, sr, accent=1.0):
    """Hi‑hat:有色噪声 + 快衰减,accent 控制力度"""
    n = int(sr * dur)
    if n < 1: return np.zeros(0, dtype=np.float32)
    t = np.arange(n, dtype=np.float32) / sr
    noise = np.random.uniform(-1, 1, n).astype(np.float32)
    # 高频强调
    noise_bright = noise * (0.4 + 0.6 * np.sin(2 * np.pi * 3500 * t))
    amp = np.exp(-t * 55) * accent
    return (0.16 * noise_bright * amp).astype(np.float32)
def drum_crash(dur, sr):
    """Crash cymbal:宽带噪声 + 中长衰减"""
    n = int(sr * dur)
    if n < 1: return np.zeros(0, dtype=np.float32)
    t = np.arange(n, dtype=np.float32) / sr
    noise = np.random.uniform(-1, 1, n).astype(np.float32)
    amp = np.exp(-t * 5)
    return (0.30 * noise * amp).astype(np.float32)
def drum_ride(dur, sr):
    """Ride bell:高音调金属感"""
    n = int(sr * dur)
    if n < 1: return np.zeros(0, dtype=np.float32)
    t = np.arange(n, dtype=np.float32) / sr
    # 多个高频泛音模拟金属铃
    sig = (0.10 * np.sin(2 * np.pi * 3200 * t)
           + 0.06 * np.sin(2 * np.pi * 4800 * t)
           + 0.03 * np.sin(2 * np.pi * 6200 * t))
    noise = np.random.uniform(-0.5, 0.5, n).astype(np.float32)
    amp = np.exp(-t * 18)
    return ((sig + 0.08 * noise) * amp).astype(np.float32)
# ── 逐小节编排 ──
for bar in range(total_bars):
    bar_start = int(bar * bar_sec * SAMPLE_RATE)
    is_chorus = (17 <= bar <= 24) or (37 <= bar <= 44)  # 副歌小节
    is_final  = (57 <= bar <= 62)                        # 终副歌小节
    for beat_idx in range(4):
        t_beat = bar_start + int(beat_idx * beat * SAMPLE_RATE)
        # Kick on 1, 3 — 副歌 / 终副歌加力度
        if beat_idx in (0, 2):
            dur = 0.15
            n = int(SAMPLE_RATE * dur)
            kick = drum_kick(dur, SAMPLE_RATE)
            if is_chorus or is_final:
                kick *= 1.10   # 副歌 kick 更重
            if t_beat + n <= drum_len:
                drum_track[t_beat:t_beat + n] += kick
        # Snare on 2, 4 — 强力 backbeat
        if beat_idx in (1, 3):
            dur = 0.20
            n = int(SAMPLE_RATE * dur)
            snare = drum_snare(dur, SAMPLE_RATE)
            if is_chorus or is_final:
                snare *= 1.12   # 副歌 snare 更 crack
            if t_beat + n <= drum_len:
                drum_track[t_beat:t_beat + n] += snare
        # Hi‑hat:八分音符,正拍 accent 1.0 / 弱拍 0.55
        for eighth in (0, 1):
            t_hh = bar_start + int((beat_idx + eighth * 0.5) * beat * SAMPLE_RATE)
            dur_hh = 0.06
            n_hh = int(SAMPLE_RATE * dur_hh)
            accent = 1.0 if eighth == 0 else 0.55
            hh = drum_hh(dur_hh, SAMPLE_RATE, accent)
            if t_hh + n_hh <= drum_len:
                drum_track[t_hh:t_hh + n_hh] += hh
    # Crash:副歌 / 终副歌第一拍开头
    if is_chorus or is_final:
        t_crash = bar_start
        dur_c = 0.60
        n_c = int(SAMPLE_RATE * dur_c)
        crash = drum_crash(dur_c, SAMPLE_RATE)
        if t_crash + n_c <= drum_len:
            drum_track[t_crash:t_crash + n_c] += crash
    # Ride bell:副歌第 2、4 拍后半拍
    if is_chorus:
        for bi in (1, 3):
            t_ride = bar_start + int((bi + 0.5) * beat * SAMPLE_RATE)
            dur_r = 0.12
            n_r = int(SAMPLE_RATE * dur_r)
            ride = drum_ride(dur_r, SAMPLE_RATE)
            if t_ride + n_r <= drum_len:
                drum_track[t_ride:t_ride + n_r] += ride * 0.7
# 归一化鼓轨
d_peak = float(np.max(np.abs(drum_track)))
if d_peak > 1e-12:
    drum_track *= (0.78 / d_peak)
# ═══════════════════════════════════════════════════
# 5. 混音 — 对齐 + 加权叠加
# ═══════════════════════════════════════════════════
target_len = drum_len
def pad_to(buf, length):
    if len(buf) < length:
        return np.pad(buf, (0, length - len(buf)))
    return buf[:length]
bass_arr   = pad_to(bass_track,   target_len)
chord_arr  = pad_to(chord_track,  target_len)
melody_arr = pad_to(melody_track, target_len)
mix = (bass_arr * 0.55 + chord_arr * 0.25 + melody_arr * 0.58 + drum_track * 0.48)
peak = float(np.max(np.abs(mix)))
if peak > 1e-12:
    mix *= (0.82 / peak)
samples_i16 = (mix * 32767).astype(np.int16)
del mix
# ── WAV → base64 ──
buf = io.BytesIO()
with wave.open(buf, "wb") as w:
    w.setnchannels(1)
    w.setsampwidth(2)
    w.setframerate(SAMPLE_RATE)
    w.writeframes(samples_i16.tobytes())
buf.seek(0)
audio_b64 = base64.b64encode(buf.read()).decode()
window._pyodideAudio = dict(base64=audio_b64, channels=1, sample_width=2, sample_rate=SAMPLE_RATE)
print(f"Love Story (Taylor Swift) — D major, 64 bars @ {BPM} BPM ({total_sec:.0f} s)")
play_obj = sa.play_buffer(samples_i16, 1, 2, SAMPLE_RATE)
window._pyodidePlayObj = play_obj
print("Done!")
f'<audio controls src="data:audio/wav;base64,{audio_b64}" style="max-width:100%;"></audio>'
Style — Taylor Swift
"""Style (Taylor Swift, 1989) — Verse + Chorus 多轨合成,<audio> 标签下载"""
import io, base64, wave, math
import numpy as np
import simpleaudio as sa
from js import window
SAMPLE_RATE = 44100
BPM = 95                               # Style 原速 ≈ 95 BPM
beat = 60.0 / BPM                      # 一拍 ≈ 0.632 s
bar_beats = 4.0
total_bars = 16
notes = {
    "C3": 130.81, "D3": 146.83, "E3": 164.81, "F3": 174.61,
    "G3": 196.00, "A3": 220.00, "B3": 246.94,
    "C4": 261.63, "D4": 293.66, "E4": 329.63, "F4": 349.23,
    "G4": 392.00, "A4": 440.00, "B4": 493.88, "C5": 523.25,
}
def tone(freq, dur, harmonics, adsr=(0.02, 0.08)):
    n = int(SAMPLE_RATE * dur)
    if n < 1: return np.zeros(0, dtype=np.float32)
    t = np.arange(n, dtype=np.float32) / SAMPLE_RATE
    sig = np.zeros(n, dtype=np.float32)
    for i, amp in enumerate(harmonics):
        sig += amp * np.sin(2 * np.pi * freq * (i + 1) * t)
    attack_n = int(adsr[0] * SAMPLE_RATE)
    release_n = int(adsr[1] * SAMPLE_RATE)
    env = np.ones(n, dtype=np.float32)
    if attack_n > 0 and attack_n < n: env[:attack_n] = np.linspace(0, 1, attack_n)
    if release_n > 0 and release_n < n: env[-release_n:] = np.linspace(1, 0, release_n)
    return (sig * env).astype(np.float32)
def render_track(sequence, harmonics, adsr=(0.01, 0.05)):
    total_beats = sum(d for _, d in sequence)
    total_samples = int(SAMPLE_RATE * total_beats * beat)
    buf = np.zeros(total_samples, dtype=np.float32)
    pos = 0
    for name, dur_beat in sequence:
        dur = dur_beat * beat
        n = int(SAMPLE_RATE * dur)
        if n < 1: continue
        chunk = tone(notes[name], dur, harmonics, adsr)
        if pos + len(chunk) <= len(buf):
            buf[pos:pos + len(chunk)] += chunk
        pos += n
    return buf, total_beats
# ═══════════════════════════════════════════════════
# 1. Bass — I-vi-IV-V(Style 的 funk‑pop 和声)
# ═══════════════════════════════════════════════════
bass_seq = []
for _ in range(total_bars // 2):         # 每轮 4 和弦 × 2 拍 = 2 小节
    bass_seq += [("C3", 2.0), ("A3", 2.0), ("F3", 2.0), ("G3", 2.0)]
bass_track, _ = render_track(bass_seq, [0.80, 0.20])
# ═══════════════════════════════════════════════════
# 2. Chords — 三音 Pad
# ═══════════════════════════════════════════════════
chord_triads = {
    "C":  ["C4", "E4", "G4"],
    "Am": ["A3", "C4", "E4"],
    "F":  ["F3", "A3", "C4"],
    "G":  ["G3", "B3", "D4"],
}
chord_seq_roots = []
for _ in range(total_bars // 2):
    chord_seq_roots += ["C", "Am", "F", "G"]
chord_beats = len(chord_seq_roots) * 2.0
chord_len = int(SAMPLE_RATE * chord_beats * beat)
chord_track = np.zeros(chord_len, dtype=np.float32)
pos = 0
for root in chord_seq_roots:
    dur = 2.0 * beat
    n = int(SAMPLE_RATE * dur)
    if n < 1: continue
    chord_sig = np.zeros(n, dtype=np.float32)
    for note_name in chord_triads[root]:
        freq = notes[note_name]
        t = np.arange(n, dtype=np.float32) / SAMPLE_RATE
        chord_sig += 0.12 * np.sin(2 * np.pi * freq * t)
    attack_n = int(0.03 * SAMPLE_RATE)
    release_n = int(0.10 * SAMPLE_RATE)
    env = np.ones(n, dtype=np.float32)
    if attack_n < n: env[:attack_n] = np.linspace(0, 1, attack_n)
    if release_n < n: env[-release_n:] = np.linspace(1, 0, release_n)
    if pos + n <= chord_len:
        chord_track[pos:pos + n] += (chord_sig * env).astype(np.float32)
    pos += n
# ═══════════════════════════════════════════════════
# 3. Melody — "Style" (C 大调,Verse + Pre‑Chorus + Chorus)
# ═══════════════════════════════════════════════════
verse = [
    # Bar 1 ─ "Midnight, you come and pick me up, no" ──
    ("G4", 0.5), ("G4", 0.5), ("G4", 0.5), ("G4", 0.5),
    ("G4", 0.5), ("A4", 0.5), ("G4", 0.5), ("E4", 0.5),
    # Bar 2 ─ "headlights" ────────────────────────────
    ("D4", 0.5), ("C4", 0.5), ("C4", 1.5),
    ("D4", 0.5), ("C4", 1.0),
    # Bar 3 ─ "Long drive, could end in burning" ──────
    ("G4", 0.5), ("G4", 0.5), ("G4", 0.5), ("G4", 0.5),
    ("A4", 0.5), ("G4", 0.5), ("E4", 0.5), ("D4", 0.5),
    # Bar 4 ─ "flames or paradise" ─────────────────────
    ("C4", 0.5), ("C4", 0.5), ("C4", 1.5),
    ("D4", 0.5), ("C4", 1.0),
]
pre_chorus = [
    # Bar 5 ─ "You got that James Dean daydream" ──────
    ("C4", 0.5), ("D4", 0.5), ("E4", 0.75), ("E4", 0.25),
    ("D4", 0.5), ("C4", 0.5), ("C4", 0.5), ("D4", 0.5),
    # Bar 6 ─ "look in your eye" ──────────────────────
    ("E4", 0.5), ("G4", 0.5), ("E4", 0.5), ("D4", 0.5),
    ("C4", 1.5), ("D4", 0.5),
]
chorus = [
    # Bar 7 ─ "And I got that red lip classic" ────────
    ("E4", 0.5), ("G4", 0.5), ("A4", 0.5), ("G4", 0.5),
    ("E4", 0.5), ("D4", 0.5), ("C4", 0.5), ("C4", 0.5),
    # Bar 8 ─ "thing that you like" ───────────────────
    ("D4", 0.5), ("E4", 0.5), ("G4", 0.5), ("G4", 0.5),
    ("E4", 0.5), ("D4", 0.5), ("C4", 0.5), ("C4", 0.5),
    # Bar 9 ─ "And when we go crashing down" ──────────
    ("E4", 0.5), ("G4", 0.5), ("A4", 0.5), ("G4", 0.5),
    ("E4", 0.5), ("D4", 0.5), ("C4", 0.5), ("D4", 0.5),
    # Bar 10 ─ "we come back every time" ──────────────
    ("G4", 0.5), ("E4", 0.5), ("D4", 0.5), ("C4", 0.5),
    ("D4", 0.5), ("C4", 0.5), ("A4", 0.5), ("G4", 0.5),
]
outro = [
    # Bar 11 ─ "'Cause we never go out of style," ─────
    ("G4", 0.5), ("E4", 0.5), ("D4", 0.5), ("C4", 0.5),
    ("D4", 0.5), ("C4", 0.5), ("G4", 0.5), ("E4", 0.5),
    # Bar 12 ─ "we never go out of style" ─────────────
    ("D4", 0.5), ("C4", 0.5), ("D4", 0.5), ("E4", 0.5),
    ("C4", 2.0),
    # Bar 13 ─ repeat "'Cause we never go out" ────────
    ("G4", 0.5), ("E4", 0.5), ("D4", 0.5), ("C4", 0.5),
    ("D4", 0.5), ("C4", 0.5), ("A4", 0.5), ("G4", 0.5),
    # Bar 14 ─ "of style" (held) ──────────────────────
    ("E4", 0.5), ("D4", 0.5), ("C4", 0.5), ("D4", 0.5),
    ("C4", 2.0),
    # Bar 15-16 ─ instrumental fade ───────────────────
    ("G4", 0.5), ("E4", 0.5), ("D4", 0.5), ("C4", 0.5),
    ("D4", 0.5), ("C4", 0.5), ("C4", 1.0),
    ("D4", 0.5), ("C4", 2.5), ("C4", 1.0),
]
melody_seq = verse + pre_chorus + chorus + outro  # 4+2+4+6 = 16 bars
melody_track, _ = render_track(melody_seq, [0.38, 0.24, 0.10, 0.05], adsr=(0.005, 0.06))
# ═══════════════════════════════════════════════════
# 4. Drums — Kick + Snare + Hi‑hat (funk‑pop groove)
# ═══════════════════════════════════════════════════
bar_sec = bar_beats * beat
total_sec = total_bars * bar_sec
drum_len = int(SAMPLE_RATE * total_sec)
drum_track = np.zeros(drum_len, dtype=np.float32)
for bar in range(total_bars):
    bar_start = int(bar * bar_sec * SAMPLE_RATE)
    for beat_idx in range(4):
        t_beat = bar_start + int(beat_idx * beat * SAMPLE_RATE)
        # Kick on 1, 3 —
        if beat_idx in (0, 2):
            n = int(SAMPLE_RATE * 0.14)
            t = np.arange(n, dtype=np.float32) / SAMPLE_RATE
            env = np.exp(-t * 30)
            kick = (0.85 * np.sin(2 * np.pi * 55 * t) * env).astype(np.float32)
            if t_beat + n <= drum_len:
                drum_track[t_beat:t_beat + n] += kick
        # Snare on 2, 4 — 加重 backbeat
        if beat_idx in (1, 3):
            n = int(SAMPLE_RATE * 0.18)
            t = np.arange(n, dtype=np.float32) / SAMPLE_RATE
            env = np.exp(-t * 15)
            noise = np.random.uniform(-1, 1, n).astype(np.float32)
            body = 0.22 * np.sin(2 * np.pi * 200 * t)
            snare = ((0.38 * noise + body) * env).astype(np.float32)
            if t_beat + n <= drum_len:
                drum_track[t_beat:t_beat + n] += snare
        # Hi‑hat on eighth notes
        for eighth in (0, 1):
            t_hh = bar_start + int((beat_idx + eighth * 0.5) * beat * SAMPLE_RATE)
            n_hh = int(SAMPLE_RATE * 0.05)
            t = np.arange(n_hh, dtype=np.float32) / SAMPLE_RATE
            env_hh = np.exp(-t * 55)
            hh_noise = np.random.uniform(-1, 1, n_hh).astype(np.float32)
            hh = (0.13 * hh_noise * env_hh).astype(np.float32)
            if t_hh + n_hh <= drum_len:
                drum_track[t_hh:t_hh + n_hh] += hh
d_peak = float(np.max(np.abs(drum_track)))
if d_peak > 1e-12:
    drum_track *= (0.72 / d_peak)
# ═══════════════════════════════════════════════════
# 5. 混音
# ═══════════════════════════════════════════════════
target_len = drum_len
def pad_to(buf, length):
    if len(buf) < length:
        return np.pad(buf, (0, length - len(buf)))
    return buf[:length]
bass_arr   = pad_to(bass_track,   target_len)
chord_arr  = pad_to(chord_track,  target_len)
melody_arr = pad_to(melody_track, target_len)
mix = (bass_arr * 0.50 + chord_arr * 0.22 + melody_arr * 0.55 + drum_track * 0.48)
peak = float(np.max(np.abs(mix)))
if peak > 1e-12:
    mix *= (0.85 / peak)
samples_i16 = (mix * 32767).astype(np.int16)
del mix
# ── WAV → base64 ──
buf = io.BytesIO()
with wave.open(buf, "wb") as w:
    w.setnchannels(1)
    w.setsampwidth(2)
    w.setframerate(SAMPLE_RATE)
    w.writeframes(samples_i16.tobytes())
buf.seek(0)
audio_b64 = base64.b64encode(buf.read()).decode()
window._pyodideAudio = dict(base64=audio_b64, channels=1, sample_width=2, sample_rate=SAMPLE_RATE)
print(f"Style (Taylor Swift) — {total_bars} bars @ {BPM} BPM ({total_sec:.0f} s)")
play_obj = sa.play_buffer(samples_i16, 1, 2, SAMPLE_RATE)
window._pyodidePlayObj = play_obj
print("Done!")
f'<audio controls src="data:audio/wav;base64,{audio_b64}" style="max-width:100%;"></audio>'

这里方便渲染引用的链接[1][2][3]

[1]
"""
General Numerical Solver for the 1D Time-Dependent Schrodinger's equation.
author: Jake Vanderplas
email: vanderplas@astro.washington.edu
website: http://jakevdp.github.io
license: BSD
Please feel free to use and modify this, but keep the above information. Thanks!
"""
import numpy as np
from matplotlib import pyplot as pl
from matplotlib import animation
from scipy.fftpack import fft,ifft
class Schrodinger(object):
    """
    Class which implements a numerical solution of the time-dependent
    Schrodinger equation for an arbitrary potential
    """
    def __init__(self, x, psi_x0, V_x,
                 k0 = None, hbar=1, m=1, t0=0.0):
        """
        Parameters
        ----------
        x : array_like, float
            length-N array of evenly spaced spatial coordinates
        psi_x0 : array_like, complex
            length-N array of the initial wave function at time t0
        V_x : array_like, float
             length-N array giving the potential at each x
        k0 : float
            the minimum value of k.  Note that, because of the workings of the
            fast fourier transform, the momentum wave-number will be defined
            in the range
              k0 < k < 2*pi / dx
            where dx = x[1]-x[0].  If you expect nonzero momentum outside this
            range, you must modify the inputs accordingly.  If not specified,
            k0 will be calculated such that the range is [-k0,k0]
        hbar : float
            value of planck's constant (default = 1)
        m : float
            particle mass (default = 1)
        t0 : float
            initial tile (default = 0)
        """
        # Validation of array inputs
        self.x, psi_x0, self.V_x = map(np.asarray, (x, psi_x0, V_x))
        N = self.x.size
        assert self.x.shape == (N,)
        assert psi_x0.shape == (N,)
        assert self.V_x.shape == (N,)
        # Set internal parameters
        self.hbar = hbar
        self.m = m
        self.t = t0
        self.dt_ = None
        self.N = len(x)
        self.dx = self.x[1] - self.x[0]
        self.dk = 2 * np.pi / (self.N * self.dx)
        # set momentum scale
        if k0 == None:
            self.k0 = -0.5 * self.N * self.dk
        else:
            self.k0 = k0
        self.k = self.k0 + self.dk * np.arange(self.N)
        self.psi_x = psi_x0
        self.compute_k_from_x()
        # variables which hold steps in evolution of the
        self.x_evolve_half = None
        self.x_evolve = None
        self.k_evolve = None
        # attributes used for dynamic plotting
        self.psi_x_line = None
        self.psi_k_line = None
        self.V_x_line = None
    def _set_psi_x(self, psi_x):
        self.psi_mod_x = (psi_x * np.exp(-1j * self.k[0] * self.x)
                          * self.dx / np.sqrt(2 * np.pi))
    def _get_psi_x(self):
        return (self.psi_mod_x * np.exp(1j * self.k[0] * self.x)
                * np.sqrt(2 * np.pi) / self.dx)
    def _set_psi_k(self, psi_k):
        self.psi_mod_k = psi_k * np.exp(1j * self.x[0]
                                        * self.dk * np.arange(self.N))
    def _get_psi_k(self):
        return self.psi_mod_k * np.exp(-1j * self.x[0] * 
                                        self.dk * np.arange(self.N))
    
    def _get_dt(self):
        return self.dt_
    def _set_dt(self, dt):
        if dt != self.dt_:
            self.dt_ = dt
            self.x_evolve_half = np.exp(-0.5 * 1j * self.V_x
                                         / self.hbar * dt )
            self.x_evolve = self.x_evolve_half * self.x_evolve_half
            self.k_evolve = np.exp(-0.5 * 1j * self.hbar /
                                    self.m * (self.k * self.k) * dt)
    
    psi_x = property(_get_psi_x, _set_psi_x)
    psi_k = property(_get_psi_k, _set_psi_k)
    dt = property(_get_dt, _set_dt)
    def compute_k_from_x(self):
        self.psi_mod_k = fft(self.psi_mod_x)
    def compute_x_from_k(self):
        self.psi_mod_x = ifft(self.psi_mod_k)
    def time_step(self, dt, Nsteps = 1):
        """
        Perform a series of time-steps via the time-dependent
        Schrodinger Equation.
        Parameters
        ----------
        dt : float
            the small time interval over which to integrate
        Nsteps : float, optional
            the number of intervals to compute.  The total change
            in time at the end of this method will be dt * Nsteps.
            default is N = 1
        """
        self.dt = dt
        if Nsteps > 0:
            self.psi_mod_x *= self.x_evolve_half
        for i in range(Nsteps - 1):
            self.compute_k_from_x()
            self.psi_mod_k *= self.k_evolve
            self.compute_x_from_k()
            self.psi_mod_x *= self.x_evolve
        self.compute_k_from_x()
        self.psi_mod_k *= self.k_evolve
        self.compute_x_from_k()
        self.psi_mod_x *= self.x_evolve_half
        self.compute_k_from_x()
        self.t += dt * Nsteps
######################################################################
# Helper functions for gaussian wave-packets
def gauss_x(x, a, x0, k0):
    """
    a gaussian wave packet of width a, centered at x0, with momentum k0
    """ 
    return ((a * np.sqrt(np.pi)) ** (-0.5)
            * np.exp(-0.5 * ((x - x0) * 1. / a) ** 2 + 1j * x * k0))
def gauss_k(k,a,x0,k0):
    """
    analytical fourier transform of gauss_x(x), above
    """
    return ((a / np.sqrt(np.pi))**0.5
            * np.exp(-0.5 * (a * (k - k0)) ** 2 - 1j * (k - k0) * x0))
######################################################################
# Utility functions for running the animation
def theta(x):
    """
    theta function :
      returns 0 if x<=0, and 1 if x>0
    """
    x = np.asarray(x)
    y = np.zeros(x.shape)
    y[x > 0] = 1.0
    return y
def square_barrier(x, width, height):
    return height * (theta(x) - theta(x - width))
######################################################################
# Create the animation
# specify time steps and duration
dt = 0.01
N_steps = 50
t_max = 120
frames = int(t_max / float(N_steps * dt))
# specify constants
hbar = 1.0   # planck's constant
m = 1.9      # particle mass
# specify range in x coordinate
N = 2 ** 11
dx = 0.1
x = dx * (np.arange(N) - 0.5 * N)
# specify potential
V0 = 1.5
L = hbar / np.sqrt(2 * m * V0)
a = 3 * L
x0 = -60 * L
V_x = square_barrier(x, a, V0)
V_x[x < -98] = 1E6
V_x[x > 98] = 1E6
# specify initial momentum and quantities derived from it
p0 = np.sqrt(2 * m * 0.2 * V0)
dp2 = p0 * p0 * 1./80
d = hbar / np.sqrt(2 * dp2)
k0 = p0 / hbar
v0 = p0 / m
psi_x0 = gauss_x(x, d, x0, k0)
# define the Schrodinger object which performs the calculations
S = Schrodinger(x=x,
                psi_x0=psi_x0,
                V_x=V_x,
                hbar=hbar,
                m=m,
                k0=-28)
######################################################################
# Set up plot
fig = pl.figure()
# plotting limits
xlim = (-100, 100)
klim = (-5, 5)
# top axes show the x-space data
ymin = 0
ymax = V0
ax1 = fig.add_subplot(211, xlim=xlim,
                      ylim=(ymin - 0.2 * (ymax - ymin),
                            ymax + 0.2 * (ymax - ymin)))
psi_x_line, = ax1.plot([], [], c='r', label=r'$|\psi(x)|$')
V_x_line, = ax1.plot([], [], c='k', label=r'$V(x)$')
center_line = ax1.axvline(0, c='k', ls=':',
                          label = r"$x_0 + v_0t$")
title = ax1.set_title("")
ax1.legend(prop=dict(size=12))
ax1.set_xlabel('$x$')
ax1.set_ylabel(r'$|\psi(x)|$')
# bottom axes show the k-space data
ymin = abs(S.psi_k).min()
ymax = abs(S.psi_k).max()
ax2 = fig.add_subplot(212, xlim=klim,
                      ylim=(ymin - 0.2 * (ymax - ymin),
                            ymax + 0.2 * (ymax - ymin)))
psi_k_line, = ax2.plot([], [], c='r', label=r'$|\psi(k)|$')
p0_line1 = ax2.axvline(-p0 / hbar, c='k', ls=':', label=r'$\pm p_0$')
p0_line2 = ax2.axvline(p0 / hbar, c='k', ls=':')
mV_line = ax2.axvline(np.sqrt(2 * V0) / hbar, c='k', ls='--',
                      label=r'$\sqrt{2mV_0}$')
ax2.legend(prop=dict(size=12))
ax2.set_xlabel('$k$')
ax2.set_ylabel(r'$|\psi(k)|$')
######################################################################
# Animate plot
def init():
    # 初始帧:波函数为空,势能和参考线已就位
    psi_x_line.set_data([], [])
    V_x_line.set_data(S.x, S.V_x)
    center_line.set_data(2 * [x0], [0, 1])
    psi_k_line.set_data([], [])
    title.set_text("t = 0.00")
    return (psi_x_line, V_x_line, center_line, psi_k_line, title)
def animate(i):
    S.time_step(dt, N_steps)
    psi_x_line.set_data(S.x, 4 * abs(S.psi_x))
    # V_x 是静态的,已在 init () 中设置,无需每帧重复
    center_line.set_data(2 * [x0 + S.t * p0 / m], [0, 1])
    psi_k_line.set_data(S.k, abs(S.psi_k))
    title.set_text("t = %.2f" % S.t)
    return (psi_x_line, V_x_line, center_line, psi_k_line, title)
# call the animator.  blit=True means only re-draw the parts that have changed.
#anim = animation.FuncAnimation(fig, animate, init_func=init, frames=frames, interval=30, blit=True)
# uncomment the following line to save the video in mp4 format.  This
# requires either mencoder or ffmpeg to be installed on your system
#anim.save('schrodinger_barrier.mp4', fps=15, extra_args=['-vcodec', 'libx264'])
#pl.show()
# ── 手动渲染动画帧为 HTML / JS 播放器 ──
# matplotlib 的 to_jshtml () 在 Pyodide(Emscripten)中有兼容性 bug:
# HTMLWriter 内部将 BytesIO 传给只接受文件路径的函数,抛出
#   TypeError: argument should be a str or an os.PathLike object … not 'BytesIO'
# anim.save(buf, writer="pillow") 同理(都走 MovieWriter 体系)。
# 绕过方案:手动逐帧调用 init ()/animate (i) + fig.savefig(BytesIO)
# (该路径已验证可行 ——runner.js 的 plt.show() monkey-patch 即用此法),
# 然后内联复刻 matplotlib HTMLWriter 的 JS 播放器模板。
import base64 as _b64, io as _io, random as _random, string as _string
# 初始化动画状态
init()
# 逐帧渲染为 base64 PNG
_frames_b64 = []
for _i in range(frames):
    animate(_i)
    _buf = _io.BytesIO()
    fig.savefig(_buf, format="png", bbox_inches="tight")
    _buf.seek(0)
    _frames_b64.append(_b64.b64encode(_buf.read()).decode())
# 关闭图形释放 Emscripten 内存
pl.close(fig)
# 生成唯一 ID(同一页面可能嵌入多个动画)
_id = "".join(_random.choices(_string.hexdigits.lower(), k=32))
# 帧数据注入
_frames_js = "\n  ".join(
    f'frames[{i}] = "data:image/png;base64,{b64}";'
    for i, b64 in enumerate(_frames_b64)
)
# 播放器 HTML + JS(复刻 matplotlib HTMLWriter 输出,去掉 FA 4.4.0 CDN link)
f'''<style>
.animation {{ display:inline-block; text-align:center; }}
input[type=range].anim-slider {{ width:374px; margin-left:auto; margin-right:auto; }}
.anim-buttons {{ margin:8px 0; }}
.anim-buttons button {{ padding:0; width:36px; }}
.anim-state label {{ margin-right:8px; }}
.anim-state input {{ margin:0; vertical-align:middle; }}
</style>
<div class="animation">
  <img id="_anim_img{_id}">
  <div class="anim-controls">
    <input id="_anim_slider{_id}" type="range" class="anim-slider"
      min="0" max="{frames - 1}" step="1" value="0"
      oninput="anim{_id}.set_frame(parseInt(this.value));">
    <div class="anim-buttons">
      <button title="Decrease speed" aria-label="Decrease speed" onclick="anim{_id}.slower()"><i class="fa fa-minus"></i></button>
      <button title="First frame" aria-label="First frame" onclick="anim{_id}.first_frame()"><i class="fa fa-fast-backward"></i></button>
      <button title="Previous frame" aria-label="Previous frame" onclick="anim{_id}.previous_frame()"><i class="fa fa-step-backward"></i></button>
      <button title="Play backwards" aria-label="Play backwards" onclick="anim{_id}.reverse_animation()"><i class="fa fa-play fa-flip-horizontal"></i></button>
      <button title="Pause" aria-label="Pause" onclick="anim{_id}.pause_animation()"><i class="fa fa-pause"></i></button>
      <button title="Play" aria-label="Play" onclick="anim{_id}.play_animation()"><i class="fa fa-play"></i></button>
      <button title="Next frame" aria-label="Next frame" onclick="anim{_id}.next_frame()"><i class="fa fa-step-forward"></i></button>
      <button title="Last frame" aria-label="Last frame" onclick="anim{_id}.last_frame()"><i class="fa fa-fast-forward"></i></button>
      <button title="Increase speed" aria-label="Increase speed" onclick="anim{_id}.faster()"><i class="fa fa-plus"></i></button>
    </div>
    <form title="Repetition mode" aria-label="Repetition mode" action="#n" name="_anim_loop_select{_id}" class="anim-state">
      <input type="radio" name="state" value="once" id="_anim_radio1_{_id}"><label for="_anim_radio1_{_id}">Once</label>
      <input type="radio" name="state" value="loop" id="_anim_radio2_{_id}" checked><label for="_anim_radio2_{_id}">Loop</label>
      <input type="radio" name="state" value="reflect" id="_anim_radio3_{_id}"><label for="_anim_radio3_{_id}">Reflect</label>
    </form>
  </div>
</div>
<script>
(function() {{
  var ua = navigator.userAgent;
  var isIE = ua.indexOf("MSIE ") > -1 || ua.indexOf("Trident/") > -1;
  function Animation(frames, img_id, slider_id, interval, loop_select_id) {{
    this.img_id = img_id;
    this.slider_id = slider_id;
    this.loop_select_id = loop_select_id;
    this.interval = interval;
    this.current_frame = 0;
    this.direction = 0;
    this.timer = null;
    this.frames = new Array(frames.length);
    for (var i = 0; i < frames.length; i++) {{
      this.frames[i] = new Image();
      this.frames[i].src = frames[i];
    }}
    var slider = document.getElementById(this.slider_id);
    slider.max = this.frames.length - 1;
    if (isIE) {{
      slider.setAttribute("onchange", slider.getAttribute("oninput"));
      slider.setAttribute("oninput", null);
    }}
    this.set_frame(this.current_frame);
  }}
  Animation.prototype.get_loop_state = function() {{
    var btn = document[this.loop_select_id].state;
    for (var i = 0; i < btn.length; i++) if (btn[i].checked) return btn[i].value;
    return undefined;
  }};
  Animation.prototype.set_frame = function(frame) {{
    this.current_frame = frame;
    document.getElementById(this.img_id).src = this.frames[this.current_frame].src;
    document.getElementById(this.slider_id).value = this.current_frame;
  }};
  Animation.prototype.next_frame = function() {{ this.set_frame(Math.min(this.frames.length - 1, this.current_frame + 1)); }};
  Animation.prototype.previous_frame = function() {{ this.set_frame(Math.max(0, this.current_frame - 1)); }};
  Animation.prototype.first_frame = function() {{ this.set_frame(0); }};
  Animation.prototype.last_frame = function() {{ this.set_frame(this.frames.length - 1); }};
  Animation.prototype.slower = function() {{ this.interval /= 0.7; if (this.direction > 0) {{ this.play_animation(); }} else if (this.direction < 0) {{ this.reverse_animation(); }} }};
  Animation.prototype.faster = function() {{ this.interval *= 0.7; if (this.direction > 0) {{ this.play_animation(); }} else if (this.direction < 0) {{ this.reverse_animation(); }} }};
  Animation.prototype.anim_step_forward = function() {{
    this.current_frame += 1;
    if (this.current_frame < this.frames.length) {{ this.set_frame(this.current_frame); }}
    else {{
      var s = this.get_loop_state();
      if (s == "loop") this.first_frame();
      else if (s == "reflect") {{ this.last_frame(); this.reverse_animation(); }}
      else {{ this.pause_animation(); this.last_frame(); }}
    }}
  }};
  Animation.prototype.anim_step_reverse = function() {{
    this.current_frame -= 1;
    if (this.current_frame >= 0) {{ this.set_frame(this.current_frame); }}
    else {{
      var s = this.get_loop_state();
      if (s == "loop") this.last_frame();
      else if (s == "reflect") {{ this.first_frame(); this.play_animation(); }}
      else {{ this.pause_animation(); this.first_frame(); }}
    }}
  }};
  Animation.prototype.pause_animation = function() {{ this.direction = 0; if (this.timer) {{ clearInterval(this.timer); this.timer = null; }} }};
  Animation.prototype.play_animation = function() {{
    this.pause_animation(); this.direction = 1;
    var t = this;
    if (!this.timer) this.timer = setInterval(function() {{ t.anim_step_forward(); }}, this.interval);
  }};
  Animation.prototype.reverse_animation = function() {{
    this.pause_animation(); this.direction = -1;
    var t = this;
    if (!this.timer) this.timer = setInterval(function() {{ t.anim_step_reverse(); }}, this.interval);
  }};
  var frames = new Array({frames});
  {_frames_js}
  window.anim{_id} = new Animation(frames, "_anim_img{_id}", "_anim_slider{_id}", 30, "_anim_loop_select{_id}");
}})();
</script>'''

§ 参考链接


  1. Quantum Python: Animating the Schrodinger Equation ↩︎

  2. 有限差分数值求解一维无限深势阱的薛定谔方程 ↩︎

  3. 使用数值方法求解单电子 Schrodinger 方程 ↩︎

更新于 阅读次数

请我喝[茶]~( ̄▽ ̄)~*

星旅人 微信支付

微信支付

星旅人 支付宝

支付宝


在提问之前,你应该学会如何提问