用代码做自媒体(三):Claude Code skill 如何合成零版权 BGM(numpy 合成 + fluidsynth 真采样)
本文是「用代码做自媒体」系列第三篇。这条流水线是:① 博客园 MetaWeblog API 自动发布文章 → ② 把博客自动做成讲解视频 → ③ 给视频合成零版权 BGM(本篇) → ④ 命令行自动投稿 B 站。视频有了,总得配个不侵权的背景音乐。
给视频配背景音乐,最容易踩的坑不是技术,是版权:随手扒一首商业曲垫进去,发到公开平台轻则被消音、重则下架。这篇讲怎么用代码在本机合成零版权 BGM——从最朴素的 numpy 正弦波叠加,到 fluidsynth 加载真乐器采样,两条路线我都跑通了,并把过程里几个反直觉的坑(尤其"听感发重怎么量化")一起记下来。
结论先行:numpy 程序化合成胜在零依赖、即时出,但音色假、容易发"闷/重",天花板低;fluidsynth + SoundFont 真采样音色接近真乐器,是想要"好听"的正路。两条都能做到零版权——关键是只仿气质、不抄旋律。

上图是全篇的地图:左边 numpy 程序化、右边 fluidsynth 真采样,底下贯穿全程的四条"零版权 + 调音"方法论(后面第三节细讲)。
环境
- macOS(Apple Silicon),其它平台同理
- Python 3:numpy(合成)、midiutil(写 MIDI)
- ffmpeg / sox:转码、响度统计
- fluidsynth 2.x:SoundFont 真采样渲染(brew install fluid-synth)
- 一个 GM SoundFont(.sf2/.sf3):本文用 MuseScore_General.sf3(~40MB,含全套 GM 乐器)
一、两条合成路线
本机合成 BGM 有两条路线:numpy 程序化加法合成(零依赖、即时,音色偏假)和 fluidsynth + SoundFont 真采样(音色接近真乐器)。先各讲一条。
方案 A:numpy 程序化加法合成(零依赖)
原理就一句话:乐音 = 一组谐波正弦的叠加 × 包络 × 一点混响。基频 f 的钢琴音,大致是 f, 2f, 3f… 这些谐波按递减幅度相加,再乘一个"快起音、慢衰减"的包络模拟敲击。
核心合成一个钢琴单音:
import numpy as np
SR = 44100
def midi_hz(m): # MIDI 音高 → 频率
return 440.0 * 2 ** ((m - 69) / 12.0)
def piano_note(freq, sr=SR, ring=1.4, vel=1.0):
n = int(ring * sr); t = np.arange(n) / sr
sig = np.zeros(n, np.float32)
# 5 个谐波,幅度递减;每个谐波独立指数衰减
for k, amp in enumerate([1.0, 0.5, 0.30, 0.16, 0.08], start=1):
sig += amp * np.exp(-t * (2.6 + 1.1 * k)) * np.sin(2 * np.pi * freq * k * t)
atk = int(0.006 * sr) # 6ms 短 attack,避免"咔"
sig[:atk] *= np.linspace(0, 1, atk)
return (sig / 2.0 * vel).astype(np.float32)
把一段和弦进行 + 旋律按拍铺到一个 buffer 上,最后过一个"廉价混响"(几个衰减延迟抽头叠加)拉开空间感:
def reverb(x, sr=SR):
y = x.copy()
for delay, g in ((0.11, 0.28), (0.19, 0.18), (0.28, 0.10)):
d = int(delay * sr); tap = np.zeros_like(x)
tap[d:] = x[:-d] * g # 延迟 + 衰减
y += tap
return y
写成 wav、ffmpeg 转 mp3 即可。优点:零第三方依赖、一两秒出一首、完全可控。缺点:加法合成的音色"干、假",低次谐波一多就糊成一坨,听感发"重/闷"——这是它的硬天花板,后面专门讲怎么量化和缓解。
方案 B:fluidsynth + SoundFont 真采样(音色好)
真钢琴每个音有几十个泛音、琴弦共鸣、踏板延音、自然衰减,加法合成的 5 个正弦逼近不了。正路是用真乐器采样:把"音高 + 时值"写成 MIDI,交给 fluidsynth 加载 SoundFont(真乐器录音样本库)去发声。整条信号链如下:

第一步,用 midiutil 把旋律/和声写成 MIDI(多声部 + 力度人性化微抖,别让它机械):
import random
from midiutil import MIDIFile
random.seed(11) # 固定种子,可复现
mf = MIDIFile(1, deinterleave=False)
mf.addTempo(0, 0, 148) # BPM
mf.addProgramChange(0, 0, 0, 3) # 通道0 = GM 3 号 Honky-tonk Piano
mf.addProgramChange(0, 1, 0, 105) # 通道1 = GM 105 Banjo
mf.addProgramChange(0, 2, 0, 32) # 通道2 = GM 32 Acoustic Bass
# 通道9 固定是打击乐:36 底鼓 / 38 军鼓 / 42 闭镲 / 39 拍手
def vel(base, spread=8): # 力度抖动,去机械感
return max(1, min(127, int(base + random.uniform(-spread, spread))))
# 往各通道 addNote(track, channel, pitch, time(拍), dur(拍), velocity) ...
第二步,fluidsynth 渲染成 wav(带内置混响),再用 ffmpeg 做规整(淡入淡出、响度标准化、裁长度):
fluidsynth -ni -g 0.85 -R 1 -r 44100 -F out.wav MuseScore_General.sf3 song.mid
ffmpeg -i out.wav -af "afade=t=in:st=0:d=1,afade=t=out:st=70:d=2,\
loudnorm=I=-17:TP=-1.5,atrim=0:72" final.wav
GM 音色表里有现成的 honky-tonk 钢琴、班卓琴、口哨、贝斯、整套鼓——一套西部乡村风的多声部,半小时就能搭出来,音色比 numpy 版高一个档次。
二、四个反直觉的坑
1. 只仿气质,不抄旋律 = 零版权
想要"某首歌那味儿",绝不能复刻它的旋律和声走向——那是对版权曲的派生复制。正确做法是只取情绪/调式/织体,旋律全原创。例如:
- 想要暗黑电影感→ 用 A 小调 + 和声小调的 V 级大三和弦制造哥特张力,自己写一条空灵琶音,加低频脉冲"心跳"。
- 想要温情怀旧→ 用 C 大调 maj7/min7 暖色和弦,写一条留白多、停在中音区的简单旋律。
- 想要欢快西部牛仔→ 大调五声音阶蹦跳旋律 + boom-chick 马蹄贝斯 + 班卓切分。
取的是"调式 + 节奏型 + 配器",不是"那一句旋律"。这样出来的曲子零版权、可公开、可商用。
2. "听感发重"要用 FFT 量化,别凭感觉
合成出来"听着有点重/闷",改完到底是轻了还是更重了?凭耳朵会骗自己,得测。"重"通常等于低频能量占比过高,用 numpy 做个 FFT、算各频段能量占比就能量化:
import numpy as np, wave
def band_ratio(path):
w = wave.open(path, 'rb'); sr = w.getframerate()
d = np.frombuffer(w.readframes(w.getnframes()), np.int16).astype(np.float32)
d = d.reshape(-1, w.getnchannels()).mean(1)
d /= (np.abs(d).max() or 1)
S = np.abs(np.fft.rfft(d * np.hanning(len(d)))) ** 2 # 功率谱
f = np.fft.rfftfreq(len(d), 1/sr); tot = S.sum()
lo = S[f < 150].sum() / tot # <150Hz 轰鸣
mid = S[(f >= 150) & (f < 2000)].sum() / tot # 主体
hi = S[f >= 2000].sum() / tot # 亮度/空气感
return lo*100, mid*100, hi*100
这里有个真踩的坑:我一开始图省事用 sox in.wav -n sinc -t 250 250 stat 想量"<250Hz 低频",结果数字反着走、越改越糊。后来才发现 sox sinc 250 是高通(留 >250Hz),我量的根本是高频,不是低频——拿错频段的数当依据,结论自然全反。换成自己写的 FFT 分频段后,数字才和听感对上:某次配平把 <150Hz 占比从 59.8% 压到 8.3%,浑浊感立刻散了。教训:量化指标本身也要先验证它量的是不是你以为的东西。
3. 多声部要"配平",否则贝斯+鼓糊成一团
多声部合到一起,如果贝斯和底鼓音量给太满、又都在低频区,会把旋律全盖住。实测一版欢快曲,<150Hz 占了 59.8%、主体频段只剩 38%——叮当钢琴和班卓全埋在底下。两步配平:
- 贝斯根音抬高一个八度,从 ~70Hz 的轰鸣区移到 ~130Hz 以上;
- ffmpeg 加一道 EQ:高通切次低频 + 低架衰减 + 高频提亮:
highpass=f=45, equalizer=f=110:t=q:w=1:g=-5, treble=g=3:f=6000
配平后 <150Hz 8.3%、主体 85.6%、高频亮度也上来了——钢琴、班卓、镲都听得清了。
4. 加了个声部,它真的听得见吗?用"相减法"验证
我给曲子加了一段"吆喝"(合成人声做的上行滑音 holler),渲完一听——没声。排查发现两个原因:一是选的 GM 音色(Synth Voice)起音慢,我给的音又短,根本没发出来;二是放在段头重拍,被镲、底鼓、旋律盖住了。
换了更穿透的音色、拉长音、加大力度后,怎么客观确认它"在混音里真的突出来了"?渲染含/不含该声部两版,逐样本相减,得到它在混音里的实际贡献,再和全曲能量比:
# a = 含吆喝版, b = 不含版(同 MIDI、同种子,公共部分逐样本一致)
whoop = a - b # 相减 = 吆喝声部的实际贡献
# 在吆喝出现的时刻,比 rms(whoop) 与 rms(a)
结果:吆喝时刻它占全曲能量的 76%~85%(比整个乐队还响),非吆喝时刻贡献≈0(证明相减干净)。这下才敢说"真听得见",而不是嘴上说响。
三、打包成 Claude Code 技能(blog-to-bgm)
跑通脚本后,把它包成一个 Claude Code 的 skill,以后在对话里直接说一句"做个西部牛仔风的 BGM",Claude 就读说明书、照着调脚本替你跑。承接本系列第二篇"把博客做成视频"的 skill 思路,以下机制依据 Claude Code 官方文档。
1. 先把两条路线收成一个入口
写一个 make_bgm.py 做总调度:按风格名分派到 numpy 或 fluidsynth 路线,默认出 72 秒、产物丢 ~/Downloads。
python3 make_bgm.py 西部牛仔 # 默认 72s → ~/Downloads
python3 make_bgm.py dark 90 # 90 秒暗黑电影感
python3 make_bgm.py 暖心钢琴 60 out.mp3
风格名(中英任写)→ 合成路线的映射:
| 风格(别名) | 路线 | 味道 |
|---|---|---|
| 西部牛仔 / cowboy | fluidsynth 真采样 | 叮当钢琴+班卓+马蹄贝斯+口哨+鼓+吆喝 |
| 暖心钢琴 / warm | fluidsynth 真采样 | 真钢琴 + 弦乐暖底 |
| 暗黑 / dark | numpy 程序化 | 冷弦 pad + 空灵琶音 + 低频心跳 |
| 民谣 / ballad、氛围 / pad | numpy 程序化 | 钢琴民谣 / 氛围铺底 |
2. 建 skill 目录,把脚本放进去
mkdir -p ~/.claude/skills/blog-to-bgm
cp make_bgm.py gen_bgm.py bgm_dark.py gen_cowboy_midi.py gen_warm_midi.py ~/.claude/skills/blog-to-bgm/
# 真采样要的 SoundFont(MuseScore_General,MIT 授权)也放进去(或软链)
skill 目录里放 .py 脚本无限制,脚本路径在 SKILL.md 里用 ${CLAUDE_SKILL_DIR} 引用。
3. 写说明书 SKILL.md(Claude 靠它知道"何时用、怎么用")
在 ~/.claude/skills/blog-to-bgm/SKILL.md 写下 frontmatter(官方推荐至少写 description)+ 用法:
---
name: blog-to-bgm
description: 本机合成零版权背景音乐(BGM),按风格一键出 mp3。当用户说"生成BGM /
给视频配个背景音乐 / 来段西部牛仔风(暗黑、暖心钢琴)的BGM / 做个不侵权的背景音乐"时使用。
---
# 文章/视频配乐 → 零版权 BGM
旋律全原创、只仿气质不抄旋律,零版权可公开可商用。
## 一键出片
python3 ${CLAUDE_SKILL_DIR}/make_bgm.py <风格> [秒数] [输出.mp3]
风格:西部牛仔/cowboy、暖心钢琴/warm(真采样)、暗黑/dark、民谣/ballad、氛围/pad(numpy)
## 两条路线 / 四条调音方法论 / 加新风格
(把本文前面讲的原理、FFT 量化、配平、相减验证、加新风格的步骤都写进去)
说明书写得越细、把你的偏好和踩过的坑都写进去,Claude 跑得越稳——这正是 skill 的价值:把一次性的探索固化成可复用流程。
4. 在对话里用
装了 Claude Code 后,直接说"做个西部牛仔风的 BGM",或打 /blog-to-bgm。Claude 会读 SKILL.md、调 make_bgm.py 出一首零版权 mp3,接着就能喂给 blog-to-video 当背景音乐——整条"博客 → 视频 → 配乐 → 投稿"流水线就接上了。
改
SKILL.md即时生效、不用重启;全新建的 skills 目录才需重启一次 Claude Code。加新风格只要写个合成脚本 + 在make_bgm.py注册一行映射,再到 SKILL.md 补一句风格说明即可。
快速参考
两条路线选型
| numpy 程序化 | fluidsynth + SoundFont | |
|---|---|---|
| 依赖 | 仅 numpy | fluidsynth + .sf2/.sf3 |
| 音色 | 干/假,易发重,天花板低 | 接近真乐器,好听 |
| 适合 | 极简氛围垫底、快速出 | 想要"好听"、多声部编曲 |
零版权铁律:只仿调式/节奏/配器,旋律和声全原创,绝不复刻原曲。
关键命令
# fluidsynth 渲染
fluidsynth -ni -g 0.85 -R 1 -r 44100 -F out.wav voicebank.sf3 song.mid
# 响度标准化 + 淡入淡出 + 裁长
ffmpeg -i out.wav -af "afade=t=in:st=0:d=1,afade=t=out:st=N:d=2,loudnorm=I=-17:TP=-1.5,atrim=0:N" final.wav
# 看响度/峰值(防爆音)
sox final.wav -n stats | grep -E "Pk lev dB|RMS lev dB"
自查清单
- 改完"重不重"用 FFT 分频段量化,别信耳朵,更别用错频段的滤波器去量;
- 多声部先看
<150Hz占比,过高就抬贝斯八度 + EQ; - 新加声部听不见,先查音色起音是否太慢、是否被重拍盖住,再用相减法验证;
- 峰值留余量(Pk lev dB < -1),别削顶爆音。

浙公网安备 33010602011771号