音乐理论基础实战:从十二平均律的频率比到和弦构造、五度圈与对位法则的完整工程链路

音乐理论不是玄学,它是一套建立在频率、比值与整数关系之上的工程系统。只要把"音高"翻译成"频率"、"和弦"翻译成"频率集合"、"调性"翻译成"函数图",整套西方共同写作期(Common Practice Period)的和声规则就能被代码精确复现。本文用可运行的 Python 把声音物理、十二平均律、音程、音阶、三和弦/七和弦、五度圈、罗马数字分析与声部导向(voice leading)逐层落地,并亲手合成一段和弦做 DFT 频谱验证——所有结论都能在你的终端里跑通。

一、声音的物理基础:音高即频率

乐音由周期性振动产生。音高(pitch)唯一对应基频 $f$(单位 Hz)。国际标准音 A4 = 440 Hz 是整座音高体系的锚点。

十二平均律(12-Tone Equal Temperament, 12-TET)把八度(频率翻倍,2:1)均匀切成 12 个半音,相邻半音的频率比是常数 $r = 2^{1/12} \approx 1.059463$。因此任意半音 $n$(相对 A4)的频率为:

$$f(n) = 440 \cdot 2^{n/12}$$

MIDI 标准把这个关系整数化:MIDI 音高号 $m$ 中,A4 = 69,对应频率 $f(m) = 440 \cdot 2^{(m-69)/12}$。


import math

A4 = 440.0
def midi_to_freq(m: int) -> float:
    """MIDI 音高号 -> 频率。A4=69 -> 440Hz。"""
    return A4 * (2.0 ** ((m - 69) / 12.0))

def semis_from_a4(name: str, octave: int) -> int:
    """音名(如 C,D,E...) + 八度 -> 相对 A4 的半音数。"""
    base = {"C": -9, "C#": -8, "Db": -8, "D": -7, "D#": -6, "Eb": -6,
            "E": -5, "F": -4, "F#": -3, "Gb": -3, "G": -2, "G#": -1,
            "Ab": -1, "A": 0, "A#": 1, "Bb": 1, "B": 2}
    return base[name] + (octave - 4) * 12

# 验证:C4(中央 C) 应约为 261.63 Hz
print(f"C4  = {midi_to_freq(60):.2f} Hz")          # 261.63
print(f"A4  = {midi_to_freq(69):.2f} Hz")          # 440.00
print(f"C5  = {midi_to_freq(72):.2f} Hz")          # 523.25 (正好 C4 的两倍)
# 八度关系验证:C5 频率应恰好是 C4 的两倍
assert abs(midi_to_freq(72) / midi_to_freq(60) - 2.0) < 1e-9
print("OK: 八度 = 频率翻倍")

二、平均律 vs 纯律:一个被"平均"掉的误差

平均律的优雅在于转调自由,代价是每个音程都略有偏差。纯律(Just Intonation)用简单整数比定义协和音程,听感更纯净:

音程 纯律比值 平均律比值(2^(n/12)) 偏差(音分)
纯八度 2:1 2.000 0
纯五度 3:2 2.9966 -1.96 音分
纯四度 4:3 2.5198 +1.96 音分
大三度 5:4 2.5198→(4 半音=1.2599) +13.69 音分
小三度 6:5 (3 半音=1.1892) -15.64 音分

音分(cent)是把一个半音再细分为 100 份的对数单位:$\text{cents} = 1200 \cdot \log_2(f_1/f_0)$。


def ratio_to_cents(r: float) -> float:
    return 1200.0 * math.log2(r)

# 纯律大三度 5:4 vs 平均律 4 半音(2^(4/12))
just_major_third = 5.0 / 4.0
et_major_third = 2.0 ** (4.0 / 12.0)
print(f"纯律大三度: {ratio_to_cents(just_major_third):.2f} 音分")
print(f"平均律大三度: {ratio_to_cents(et_major_third):.2f} 音分")
print(f"偏差: {ratio_to_cents(et_major_third/just_major_third):.2f} 音分")  # ≈ +13.69

# 毕达哥拉斯音差: 12 个纯五度(3/2)叠上去 ≠ 7 个八度(2/1)
pyth = (3.0/2.0) ** 12
oct7 = 2.0 ** 7
print(f"毕达哥拉斯音差: {ratio_to_cents(pyth/oct7):.2f} 音分 (≈23.46, 因平均律被抹平)")

这正是平均律存在的理由:把 23.46 音分的"毕达哥拉斯音差"摊薄到每个五度上(每五度只差 -1.96 音分),换来任意调性自由往返。

三、音程与协和度

音程 = 两个音的频率比(或十二平均律下的半音数)。协和(consonance)程度近似由频率比的简单程度决定:

  • 完全协和:纯八度(2:1)、纯五度(3:2)、纯四度(4:3)
  • 不完全协和:大/小三度(5:4、6:5)、大/小六度(5:3、8:5)
  • 不协和:二度、七度、三全音(tritone, 6 半音)

# 所有半音音程的"平均律比值"与"纯律理想比值"对照
JUST = {0:(1,1,"纯一度"),1:(16,15,"小二度"),2:(9,8,"大二度"),3:(6,5,"小三度"),
        4:(5,4,"大三度"),5:(4,3,"纯四度"),6:(45,32,"三全音"),7:(3,2,"纯五度"),
        8:(8,5,"小六度"),9:(5,3,"大六度"),10:(9,5,"小七度"),11:(15,8,"大七度"),
        12:(2,1,"纯八度")}

def interval_info(n: int):
    num, den, name = JUST[n]
    et = 2.0 ** (n/12.0)
    just = num/den
    return name, et, just, ratio_to_cents(et/just)

for n in [0,3,4,7,12]:
    name, et, just, dev = interval_info(n)
    print(f"{n:2d} 半音 {name:6s} 平均律={et:.4f} 纯律={just:.4f} 偏差={dev:+.2f}¢")

四、音阶:全半音模式的生成算法

自然大调(Major)的相邻音级间距是 全 全 半 全 全 全 半(W-W-H-W-W-W-H)。自然小调是 全 半 全 全 半 全 全。给定根音 MIDI 号,即可生成整条音阶。


MAJOR = [2, 2, 1, 2, 2, 2, 1]      # 全全半全全全半 (7 步覆盖八度)
NAT_MINOR = [2, 1, 2, 2, 1, 2, 2]  # 全半全全半全全
HARM_MINOR = [2, 1, 2, 2, 1, 3, 1] # 和声小调: 升七级(导音)
MELO_MINOR = [2, 1, 2, 2, 2, 2, 1] # 旋律小调上行

def build_scale(root: int, steps) -> list[int]:
    notes, cur = [root], root
    for s in steps:
        cur += s
        notes.append(cur)
    return notes  # 含八度重复

def names(scale_midi: list[int]) -> str:
    names_ = ["C","C#","D","D#","E","F","F#","G","G#","A","A#","B"]
    return " ".join(names_[m % 12] + str(m // 12 - 1) for m in scale_midi)

C_major = build_scale(60, MAJOR)
A_minor = build_scale(57, NAT_MINOR)
print("C 大调:", names(C_major))   # C4 D4 E4 F4 G4 A4 B4 C5
print("A 自然小调:", names(A_minor))  # A4 B4 C5 D5 E5 F5 G5 A5
# 关系大小调验证: C 大调与自然 A 小调共享同一组音(比对 7 个音级, 不含八度重复)
assert sorted(m % 12 for m in C_major[:-1]) == sorted(m % 12 for m in A_minor[:-1])
print("OK: C 大调与 A 小调为关系大小调(同音列)")

五、和弦构造:三度堆叠

和弦由"从根音起每隔一个音级堆叠三度"生成。以 C 大三和弦为例:根音 C、三音 E(大三度)、五音 G(纯五度)→ C-E-G。

三和弦 结构(半音) 听感
大三和弦 (maj) 根+4+3 明亮
小三和弦 (min) 根+3+4 暗淡
减三和弦 (dim) 根+3+3 紧张
增三和弦 (aug) 根+4+4 漂浮

七和弦在五音上再叠一个三度(大七/小七/属七等)。


TRIADS = {"maj":(4,3), "min":(3,4), "dim":(3,3), "aug":(4,4)}
SEVENTHS = {"maj7":(4,3,4), "7":(4,3,3), "min7":(3,4,3),
            "min7b5":(3,3,4), "dim7":(3,3,3), "maj7#5":(4,4,3)}

def triad(root: int, kind="maj") -> list[int]:
    a, b = TRIADS[kind]
    return [root, root + a, root + a + b]

def seventh(root: int, kind="7") -> list[int]:
    a, b, c = SEVENTHS[kind]
    return [root, root + a, root + a + b, root + a + b + c]

def freqs(midis: list[int]) -> list[float]:
    return [round(midi_to_freq(m), 2) for m in midis]

print("C 大三和弦:", names(triad(60,"maj")), freqs(triad(60,"maj")))   # C E G
print("A 小三和弦:", names(triad(57,"min")), freqs(triad(57,"min")))   # A C E
print("G7 属七:",   names(seventh(67,"7")),  freqs(seventh(67,"7")))   # G B D F

转位(inversion):把和弦最低音移到最高,得到第一/第二/第三转位。转位不改变音程集合(音名),只改变"哪个音在底部"——这是声部导向优化的基本手段。


def invert(chord: list[int], n: int) -> list[int]:
    c = chord[:]
    for _ in range(n % len(c)):
        c.append(c.pop(0) + 12)
    return c

print("C 大三和弦 原位:",   names(triad(60,"maj")))
print("C 大三和弦 第一转位:", names(invert(triad(60,"maj"),1)))  # E G C
print("C 大三和弦 第二转位:", names(invert(triad(60,"maj"),2)))  # G C E

六、和声功能与五度圈

调性音乐的和弦按功能归类为主(Tonic, 稳定)、下属(Subdominant, 推进)、属(Dominant, 期待解决)。最经典的解决是 V → I(属到主),属和弦中的导音(leading tone,主音下方半音)强烈倾向主音。

五度圈(Circle of Fifths):从 C 起每次叠一个纯五度,顺时针得到升号调(G, D, A, E…),逆时针得到降号调(F, Bb, Eb…)。它是调性关系的总地图——相邻调只差一个音。


# 从 C 出发沿纯五度(7 半音)生成五度圈
fifth = 7
circle = []
m = 60  # C
for _ in range(12):
    circle.append(m % 12)
    m += fifth
order = ["C","G","D","A","E","B","F#","C#","G#","D#","A#","F"]
print("五度圈(顺时针/升号方向):", " ".join(order))
# 验证: 顺时针转 12 次回到 C(模 12)
assert circle[0] == circle[-1] == 0
print("OK: 五度圈闭合回到 C")

# 常见进行 I-V-vi-IV (如 C-G-Am-F),用罗马数字与根音
prog = {"I":(60,"maj"), "V":(67,"maj"), "vi":(57,"min"), "IV":(65,"maj")}
for roman,(root,kind) in prog.items():
    print(f"{roman}: {names(triad(root,kind))}  {freqs(triad(root,kind))}")

七、对位与声部导向:为什么"平行五度"被禁止

当多个声部同时运动时,声部导向(voice leading)决定听感是否平滑。共同写作期的两条铁律:

  1. 禁止平行五度/八度(parallel fifths/octaves):两个声部同时跳进到另一个纯五度/八度,会让人听不出"两个独立声部",声部个性消失。
  2. 导音上行解决:属和弦七音(leading tone)应上行半音到主音;属七和弦的七音应下行二度解决。

平滑导向的量化判据:相邻和弦间各声部移动的总半音数(voice-leading cost)越小越平滑。最优声部导向常用"最小位移"算法求解。


def voice_leading_cost(ch1: list[int], ch2: list[int]) -> int:
    """两和弦同序数声部移动的半音绝对值之和(假设已对齐音级)。"""
    # 把 ch2 每个音映射到 oct 偏移后的最近对应, 简化: 取最小总位移配对
    import itertools
    best = float("inf")
    for perm in itertools.permutations(ch2):
        cost = sum(abs(a - b) for a, b in zip(ch1, perm))
        best = min(best, cost)
    return best

# 原位 I(C E G) -> 原位 V(G B D): 每个声部跳进, cost 高
i_root = triad(60, "maj")   # C E G
v_root = triad(67, "maj")   # G B D
print("I->V 原位 voice-leading cost:", voice_leading_cost(i_root, v_root))
# V 用第一转位(根音在上的"转位 V65 思路): B D G) 让声部更平滑
v_inv = invert(triad(67,"maj"),1)  # E? 实为 B D G
print("I->V(转位) voice-leading cost:", voice_leading_cost(i_root, v_inv))

工程直觉:自动伴奏/四部和声生成器(如 chorale 模型)的本质,就是在"和弦进行图"上做带约束的最短路径搜索——节点是和弦,边权是 voice-leading cost + 功能合理性惩罚。

八、实战验证:合成一段 C 大三和弦并做 DFT 频谱分析

理论必须可证伪。下面用纯 Python(无第三方库)合成 C-E-G 的叠加正弦波,再手算 DFT,验证频谱峰值正好落在三个理论频率上。


import cmath

def synth(chord_midi: list[int], sr=8192, dur=0.25):
    """叠加正弦合成一段音频(返回采样列表)。"""
    n = int(sr * dur)
    sig = [0.0] * n
    for m in chord_midi:
        f = midi_to_freq(m)
        for i in range(n):
            sig[i] += math.sin(2 * math.pi * f * i / sr) / len(chord_midi)
    return sig, sr

def dft(sig):
    """朴素 DFT, 返回 (freqs, mags)。"""
    N = len(sig)
    mags = []
    for k in range(N // 2 + 1):
        re = im = 0.0
        for i, x in enumerate(sig):
            ang = -2 * math.pi * k * i / N
            re += x * math.cos(ang)
            im += x * math.sin(ang)
        mags.append(math.hypot(re, im) / N)
    return mags

sig, sr = synth(triad(60, "maj"))
mags = dft(sig)
# 找能量最高的 3 个峰值对应的频率
peaks = sorted(range(len(mags)), key=lambda k: mags[k], reverse=True)[:3]
theory = sorted(freqs(triad(60, "maj")))
print("理论频率(Hz):", theory)
print("DFT 峰值频率(Hz):", [round(p * sr / len(sig), 1) for p in peaks])
# 每个理论频率应能在 DFT 峰值中找到对应(误差 < 1Hz)
peak_freqs = sorted(round(p * sr / len(sig), 1) for p in peaks)
for t in theory:
    assert any(abs(t - pf) < 1.0 for pf in peak_freqs), f"{t} 未命中"
print("OK: DFT 频谱峰值与 C-E-G 理论频率精确吻合")

运行结果会显示三个峰值 ≈ 261.6、329.6、392.0 Hz(C4、E4、G4),与 midi_to_freq 完全一致——声音物理、平均律与傅里叶分析在同一组数字上闭环。

九、节奏、曲式与工程落点

  • 节奏:拍号(如 4/4)定义每小节 4 个四分音符;节拍=脉冲网格,音符时长=网格的分数(全/半/四/八分音符 = 1, 1/2, 1/4, 1/8 拍)。BPM 把"拍"翻译成秒:每拍秒数 = 60 / BPM。
  • 曲式:乐句(phrase, 通常 4 小节)→ 乐段(period, 问句+答句)→ 一段式/二段式/三段式(ABA)。工程上可用状态机表示曲式结构。
  • 落地场景:MIDI 音序器(音高号↔频率)、自动和声生成(在五度圈上做约束搜索)、音频插件(FFT 调音器/频谱仪)、DAW 中的量化(quantize=把音符吸附到节拍网格)。

def bpm_to_secperbeat(bpm: float) -> float:
    return 60.0 / bpm

def note_duration_sec(beat_fraction: float, bpm: float) -> float:
    """beat_fraction: 1=全音符, 0.25=四分音符 ..."""
    return beat_fraction * 4 * bpm_to_secperbeat(bpm)

print(f"120 BPM 下四分音符时长 = {note_duration_sec(0.25, 120):.3f} s")  # 0.5s

十、结语:音乐理论是一套可执行的约束系统

从 A4=440 的锚点,到 $2^{1/12}$ 把八度摊成 12 份,到三度堆叠定义和弦、五度圈定义调性关系、voice-leading cost 定义声部平滑度——西方音乐理论的每一个规则都有精确的数值对应。把它写进代码,你就能合成和弦、验证频谱、自动生成四部和声,甚至训练一个chorale模型。懂了频率比,就懂了为什么属七要"解决";懂了 voice-leading cost,就懂了为什么平行五度被禁止。这正是工程视角读音乐的乐趣所在。

本文所有代码均不依赖第三方库,复制即可在 Python 3 终端直接运行验证。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿
网站二维码

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部