由 DeepSeek V4 Pro、Claude Code 和我共同创作的文章,部分内容由 AI 生成,可能会有错误,请注意甄别。通过改造主题现有代码结构,实现在线运行 Python 代码。
console.log("Hello World!"); |
print("Hello World!") |
def main() { | |
x := 0: 𝔹; | |
x := H(x); | |
return measure(x); | |
} |
print("Hello World!") |
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">' |
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() |
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) — 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, 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>' |
""" | |
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>''' | |