AI 作曲的高阶结构学习:奏鸣曲式、回旋曲式的形式化表达
一段好听的旋律是灵感,一整首结构完整的奏鸣曲是工程。AI 作曲的瓶颈不在旋律生成,在结构控制。
一、场景痛点
你用 MusicGen 生成了 30 秒的钢琴片段,旋律优美、和声丰富,惊艳到发朋友圈的程度。然后你让 AI 根据这个动机扩展成一首 8 分钟的奏鸣曲——出来的结果是 8 分钟的"好听片段拼接":没有呈示部的主题对比、没有发展部的动机展开、没有再现部的主题回归。
这就是当前 AI 音乐生成的最大短板——局部优秀,全局混乱。模型可以生成令人满意的 30 秒片段,但一旦让它组织超过 2 分钟的完整结构,就暴露出对音乐形式的无知。
人类的作曲家在写奏鸣曲时,脑子里有一个明确的"施工图":
- 呈示部:主部主题(C 大调)+ 副部主题(G 大调)+ 结束部
- 发展部:对两个主题进行变奏、转调、分裂、重组
- 再现部:两个主题回归,但副部主题回到主调(C 大调)
这个结构不是"听起来像"就能模仿的,它需要对调性关系、主题素材、段落功能有精确的形式化理解。
二、底层机制与原理剖析
2.1 奏鸣曲式的形式化建模
核心约束:
- 副部主题在呈示部必须在属调(V),在再现部必须回到主调(I)——这是奏鸣曲式的"宪法"
- 发展部不能简单重复主题,必须做动机展开——这是奏鸣曲式区别于三段曲式的关键
- 连接部的作用不是填充,是调性过渡——呈示部的连接部从 I→V,再现部的从 I→I(修改以避免离开主调)
2.2 不同高阶结构的对比
奏鸣曲式 (Sonata Form): A ─→ B(V) ─→ Development(A,B) ─→ A ─→ B(I) 回旋曲式 (Rondo Form): A ─→ B ─→ A ─→ C ─→ A ─→ B' ─→ A 赋格 (Fugue): Subject → Answer → Counter-subject → Episode → Stretto 变奏曲式 (Theme & Var): Theme → Var1 → Var2 → ... → VarN → Coda2.3 形式化表示:将音乐结构编码为有向图
音乐的宏观结构本质上是一个有向图,节点是段落(Section),边是过渡条件:
Graph G = (V, E) V = {Intro, ThemeA, Transition_AB, ThemeB, Codetta, Dev_Sequence, Dev_Fragmentation, Dev_Modulation, ThemeA_Recap, Transition_AA, ThemeB_Recap, Coda} E = { (Exposition_Start → ThemeA): {key: "C major", tempo: "Allegro", material: "theme_a"}, (ThemeA → Transition_AB): {harmonic_goal: "modulate to V"}, (Transition_AB → ThemeB): {key: "G major", material: "theme_b"}, (ThemeB → Development_Start): {repeat: true, second_ending: "to_development"}, (Development_Start → Dev_Sequence): {sequence_interval: "stepwise_up"}, ... (ThemeA_Recap → Transition_AA): {harmonic_goal: "stay in I", modified: true}, (Transition_AA → ThemeB_Recap): {key: "C major", material: "theme_b_transposed"}, }有了这个形式化表示,我们可以让 AI 在生成音乐时遵循结构约束:在生成到 Transition_AB 时,它"知道"当前应该做从 I 到 V 的转调,而不是随机选择一个和弦进行。
三、生产级代码实现
3.1 音乐结构的形式化引擎
""" AI 音乐结构引擎 将高阶音乐形式(奏鸣曲式、回旋曲式、赋格等)形式化为可执行的生成计划 核心思路: 1. 将音乐结构定义为有向图(节点=段类型, 边=(当前调性, 功能, 素材约束)) 2. 将图展开为线性的 Section 序列 3. 每个 Section 携带生成约束(调性、拍号、主题素材、和声模板) 4. 模型在每个 Section 的约束下生成,保证全局结构完整性 """ from enum import Enum from dataclasses import dataclass, field from typing import Optional import math # ========== 音乐理论基础 ========== class Key: """调性表示""" NOTE_NAMES = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'] def __init__(self, root: int, mode: str = "major"): """ Args: root: 主音的 MIDI 音高 (0=C, 1=C#, ..., 11=B) mode: "major" 或 "minor" """ self.root = root % 12 self.mode = mode @property def name(self) -> str: return f"{self.NOTE_NAMES[self.root]} {'大调' if self.mode == 'major' else '小调'}" def transpose(self, interval: int) -> "Key": """移调""" return Key((self.root + interval) % 12, self.mode) def dominant(self) -> "Key": """属调: 上方纯五度 (7 个半音)""" return Key((self.root + 7) % 12, self.mode) def subdominant(self) -> "Key": """下属调: 上方纯四度 (5 个半音)""" return Key((self.root + 5) % 12, self.mode) def relative_minor(self) -> "Key": """关系小调: 下方小三度""" if self.mode == "major": return Key((self.root - 3) % 12, "minor") else: return Key((self.root + 3) % 12, "major") def __eq__(self, other): return self.root == other.root and self.mode == other.mode def __repr__(self): return self.name class SectionFunction(Enum): """段落功能类型""" # 奏鸣曲式 PRIMARY_THEME = "primary_theme" # 主部主题 SECONDARY_THEME = "secondary_theme" # 副部主题 TRANSITION = "transition" # 连接部 CODETTA = "codetta" # 结束部 DEVELOPMENT = "development" # 发展部 RECAPITULATION = "recapitulation" # 再现部 CODA = "coda" # 尾声 # 回旋曲式 RITORNELLO = "ritornello" # 叠句(A段) EPISODE = "episode" # 插部(B/C段) # 通用 INTRODUCTION = "introduction" # 引子 BRIDGE = "bridge" # 桥段 @dataclass class SectionSpec: """ 段落规格:描述一个音乐段落的所有生成约束 这是 AI 理解"现在该做什么"的核心数据结构 包含了调性、拍号、速度、主题素材、和声模板等所有约束 """ label: str # 段落标签 (如 "呈示部-主部主题") function: SectionFunction # 段落功能 key: Key # 当前调性 time_signature: tuple[int, int] = (4, 4) # 拍号 tempo_bpm: int = 120 # 速度 duration_bars: int = 16 # 长度(小节数) # 主题素材 theme_material: str = "" # 主题素材标识 (如 "theme_a", "theme_b") theme_motif: list[int] = field(default_factory=list) # 动机骨架 (音高级数) # 和声约束 harmonic_template: list[str] = field(default_factory=list) # 和声进行模板 cadence: str = "" # 终止式类型 (PAC/IAC/HC/DC) # 结构约束 repeat: bool = False # 是否重复 repeat_count: int = 1 # 重复次数 dynamic_marking: str = "mf" # 力度标记 def to_generation_prompt(self) -> str: """ 将段落约束转换为给 AI 模型的生成提示 这是模型理解"现在该生成什么内容"的关键 """ prompts = [ f"[段落: {self.label}]", f"功能: {self.function.value}", f"调性: {self.key}", f"长度: {self.duration_bars} 小节 {self.time_signature[0]}/{self.time_signature[1]} 拍", f"速度: {self.tempo_bpm} BPM", f"力度: {self.dynamic_marking}" ] if self.theme_material: prompts.append(f"素材: {self.theme_material}") if self.harmonic_template: prompts.append(f"和声: {' → '.join(self.harmonic_template)}") if self.cadence: prompts.append(f"终止式: {self.cadence}") return " | ".join(prompts) # ========== 结构生成器 ========== class SonataFormGenerator: """ 奏鸣曲式生成器 将抽象的奏鸣曲式定义展开为具体的 Section 序列 古典奏鸣曲式的标准结构: [引子(可选)] → 呈示部 → [反复呈示部] → 发展部 → 再现部 → [尾声] """ def __init__(self, home_key: Key = Key(0, "major"), tempo: int = 132): """ Args: home_key: 主调(如 C 大调) tempo: 速度 (BPM) """ self.home_key = home_key self.tempo = tempo def generate_plan(self, exposition_repeat: bool = True, include_intro: bool = False, coda_bars: int = 16) -> list[SectionSpec]: """ 生成完整的奏鸣曲式执行计划 返回 SectionSpec 列表,每个元素描述了生成阶段需要的所有约束 """ sections: list[SectionSpec] = [] # 主部主题 theme_a_motif = [1, 3, 5, 4, 3, 2, 1] # 一个简单的上行后下行动机 # 副部主题(和主部主题形成对比) theme_b_motif = [5, 6, 5, 3, 2, 1, 5] # 不同的动机骨架 # ========== 引子(可选) ========== if include_intro: sections.append(SectionSpec( label="引子", function=SectionFunction.INTRODUCTION, key=self.home_key, tempo_bpm=self.tempo - 20, # 引子通常稍慢 duration_bars=4, harmonic_template=["I", "V7/IV", "IV", "V7"], cadence="HC" # 半终止,制造期待感 )) # ========== 呈示部 ========== sections.append(SectionSpec( label="呈示部-主部主题", function=SectionFunction.PRIMARY_THEME, key=self.home_key, tempo_bpm=self.tempo, duration_bars=16, theme_material="theme_a", theme_motif=theme_a_motif, harmonic_template=["I", "IV", "V", "I", "vi", "ii", "V7", "I"], cadence="PAC" # 完满终止 )) # 连接部(关键:必须做 I → V 的转调) sections.append(SectionSpec( label="呈示部-连接部", function=SectionFunction.TRANSITION, key=self.home_key, # 从主调开始... tempo_bpm=self.tempo, duration_bars=8, theme_material="transition", harmonic_template=[ "I", "V7/vi", "vi", # 铺垫 "ii", "V7/V", "V", "V7" # 导向属调 ], cadence="HC" # 停在属调的半终止 )) # 副部主题(必须在属调) sections.append(SectionSpec( label="呈示部-副部主题", function=SectionFunction.SECONDARY_THEME, key=self.home_key.dominant(), # ← 关键: 在属调上陈述! tempo_bpm=self.tempo, duration_bars=16, theme_material="theme_b", theme_motif=theme_b_motif, harmonic_template=["I(属调)", "IV", "V7", "I", "ii", "V7/V", "V7", "I"], cadence="PAC" )) # 结束部(巩固副部调性) sections.append(SectionSpec( label="呈示部-结束部", function=SectionFunction.CODETTA, key=self.home_key.dominant(), tempo_bpm=self.tempo, duration_bars=8, theme_material="codetta", harmonic_template=["I", "IV", "V7", "I", "IV", "V7", "I"], cadence="PAC", repeat=exposition_repeat, repeat_count=2 )) # ========== 发展部 ========== # 发展部的核心是"展开",不再是完整陈述主题 # 而是把主题动机拆解、变形、在不同调性上模进 development_keys = [ self.home_key.relative_minor(), # 关系小调 self.home_key.dominant(), # 属调 self.home_key.subdominant().relative_minor(), # 下属小调 Key((self.home_key.root + 3) % 12, "major"), # 上小三度调(远关系) ] for i, dev_key in enumerate(development_keys): sections.append(SectionSpec( label=f"发展部-阶段{i+1}", function=SectionFunction.DEVELOPMENT, key=dev_key, # 频繁转调是发展部的特征 tempo_bpm=self.tempo, duration_bars=16, theme_material="development_from_theme_a", # 以主题 A 为基础展开 theme_motif=theme_a_motif, harmonic_template=[], cadence="" if i < len(development_keys) - 1 else "HC" # 发展部最后用半终止导向再现部 )) # ========== 再现部 ========== # 主部主题(主调) sections.append(SectionSpec( label="再现部-主部主题", function=SectionFunction.PRIMARY_THEME, key=self.home_key, # 回到主调 tempo_bpm=self.tempo, duration_bars=16, theme_material="theme_a", theme_motif=theme_a_motif, harmonic_template=["I", "IV", "V", "I", "vi", "ii", "V7", "I"], cadence="PAC" )) # 连接部(**修改版**: 不能转调到 V,必须停留在 I) sections.append(SectionSpec( label="再现部-连接部(修改版)", function=SectionFunction.TRANSITION, key=self.home_key, # 从主调开始... tempo_bpm=self.tempo, duration_bars=8, theme_material="transition_variant", harmonic_template=[ "I", "IV", "I", # ← 关键修改: 不再强调 V "IV", "ii", "V7", "I" # 停留在主调 ], cadence="HC" )) # 副部主题(**关键**: 回到主调陈述!这是奏鸣曲式的核心原则) sections.append(SectionSpec( label="再现部-副部主题(回归主调)", function=SectionFunction.SECONDARY_THEME, key=self.home_key, # ← 关键: 回到主调,而非属调! tempo_bpm=self.tempo, duration_bars=16, theme_material="theme_b_transposed", theme_motif=theme_b_motif, harmonic_template=["I", "IV", "V7", "I", "ii", "V7/V", "V7", "I(主调)"], cadence="PAC" )) # ========== 尾声 ========== sections.append(SectionSpec( label="尾声", function=SectionFunction.CODA, key=self.home_key, tempo_bpm=self.tempo, duration_bars=coda_bars, theme_material="coda", harmonic_template=["I", "IV", "I", "V7", "I"], cadence="PAC", dynamic_marking="ff" # 尾声通常有力度增强 )) return sections class RondoFormGenerator: """ 回旋曲式生成器 结构: A-B-A-C-A-B'-A (coda) 核心特征: - A 段(叠句/Ritornello)反复出现,始终在主调 - B/C 段(插部/Episode)与 A 形成对比,可以在不同调性 - 最后一次 A 之后通常有一个尾声 """ def __init__(self, home_key: Key = Key(0, "major"), tempo: int = 120): self.home_key = home_key self.tempo = tempo def generate_plan(self) -> list[SectionSpec]: sections = [] # 叠句主题动机 ritornello_motif = [1, 2, 3, 5, 3, 2, 1] # A (Ritornello) - 主调 sections.append(SectionSpec( label="A段 (叠句) 第1次", function=SectionFunction.RITORNELLO, key=self.home_key, duration_bars=16, theme_material="ritornello", theme_motif=ritornello_motif, harmonic_template=["I", "V", "I", "IV", "V7", "I"], cadence="PAC" )) # B (Episode 1) - 属调 sections.append(SectionSpec( label="B段 (插部1)", function=SectionFunction.EPISODE, key=self.home_key.dominant(), duration_bars=12, theme_material="episode_1", harmonic_template=[], dynamic_marking="p" # 插部通常更轻柔,增加对比 )) # A 回归 - 主调 sections.append(SectionSpec( label="A段 (叠句) 第2次", function=SectionFunction.RITORNELLO, key=self.home_key, duration_bars=16, theme_material="ritornello", theme_motif=ritornello_motif, harmonic_template=["I", "V", "I", "IV", "V7", "I"], cadence="PAC" )) # C (Episode 2) - 关系小调,增加对比感 sections.append(SectionSpec( label="C段 (插部2)", function=SectionFunction.EPISODE, key=self.home_key.relative_minor(), duration_bars=16, theme_material="episode_2", harmonic_template=[], dynamic_marking="p" )) # A 第3次 sections.append(SectionSpec( label="A段 (叠句) 第3次", function=SectionFunction.RITORNELLO, key=self.home_key, duration_bars=16, theme_material="ritornello", theme_motif=ritornello_motif, harmonic_template=["I", "V", "I", "IV", "V7", "I"], cadence="PAC" )) # B' (Episode 1 的变体,回到主调) sections.append(SectionSpec( label="B段 (插部1 变体-回归主调)", function=SectionFunction.EPISODE, key=self.home_key, # 变体现在在主调 duration_bars=12, theme_material="episode_1_variant", dynamic_marking="mf" )) # A 最后一次 + Coda sections.append(SectionSpec( label="A段 (叠句) 第4次 + 尾声", function=SectionFunction.RITORNELLO, key=self.home_key, duration_bars=24, theme_material="ritornello_final", theme_motif=ritornello_motif, cadence="PAC" )) return sections # ========== 结构执行器 ========== class StructureAwareComposer: """ 结构感知作曲器 接收形式化结构计划,逐段生成音乐并拼接 生成管线的每一段都知道"自己"在整体结构中的位置、 当前调性、和声约束、主题素材要求 """ def __init__(self, model, sample_rate: int = 32000): self.model = model # AI 音乐生成模型 (MusicGen / AudioLDM / etc.) self.sample_rate = sample_rate def compose(self, plan: list[SectionSpec]) -> bytes: """ 根据结构计划生成完整作品 Args: plan: SectionSpec 序列,定义了完整作品的结构 Returns: 完整音频的 PCM 数据 """ audio_segments = [] total_duration = 0.0 for i, section in enumerate(plan): print(f" [{i+1}/{len(plan)}] 生成: {section.label} ({section.key})") # 构建生成提示 prompt = self._build_generation_prompt(section) # 调用模型生成 audio = self.model.generate( prompt=prompt, duration=section.duration_bars * 2.0, # 假设每小节 2 秒 key=section.key, tempo=section.tempo_bpm, time_signature=section.time_signature ) # 拼接 audio_segments.append(audio) total_duration += len(audio) / self.sample_rate # 如果标记为重复,再生成一次 if section.repeat and section.repeat_count > 1: for _ in range(section.repeat_count - 1): audio_segments.append(audio) # 拼接所有段落 return b''.join(audio_segments) def _build_generation_prompt(self, section: SectionSpec) -> str: """ 构建 AI 模型的生成提示 将形式化的 SectionSpec 转换为模型能理解的自然语言提示 """ prompts = [ f"Compose a {section.duration_bars}-bar section in " f"{section.time_signature[0]}/{section.time_signature[1]} time at {section.tempo_bpm} BPM.", f"Key: {section.key.name}.", f"Function: {section.function.value}.", ] if section.theme_material: prompts.append(f"Musical material: {section.theme_material}.") if section.harmonic_template: prompts.append( f"Harmonic progression: {' → '.join(section.harmonic_template)}." ) if section.cadence: prompts.append(f"End with {section.cadence}.") if section.dynamic_marking: prompts.append(f"Dynamics: {section.dynamic_marking}.") return " ".join(prompts) # ========== 使用示例 ========== if __name__ == "__main__": # 生成一首 C 大调奏鸣曲的结构计划 sonata = SonataFormGenerator( home_key=Key(0, "major"), # C 大调 tempo=132 ) plan = sonata.generate_plan(exposition_repeat=True) print("=== 奏鸣曲式结构计划 ===") for i, section in enumerate(plan): prompt = section.to_generation_prompt() print(f"\n[{i+1}] {prompt}") print(f"\n总段落数: {len(plan)}") print(f"总小节数: {sum(s.duration_bars for s in plan)}") print(f"调性变化: {' → '.join(str(s.key) for s in plan)}") # 验证结构正确性 exposition_theme_b = plan[3] # 呈示部中的副部主题 recap_theme_b = plan[-2] # 再现部中的副部主题 assert exposition_theme_b.key == Key(0, "major").dominant(), \ f"错误: 呈示部副部主题必须在属调,但实际是 {exposition_theme_b.key}" assert recap_theme_b.key == Key(0, "major"), \ f"错误: 再现部副部主题必须在主调,但实际是 {recap_theme_b.key}" print("\n✅ 奏鸣曲式结构验证通过: 呈示部副部主题在属调, 再现部副部主题在主调")四、边界分析与架构权衡
4.1 形式化结构的限制
把音乐结构完全形式化意味着约束了 AI 的创造力。最伟大的奏鸣曲往往突破形式——贝多芬的"华尔斯坦"奏鸣曲直接把发展部融入了再现部,肖邦的叙事曲完全不按奏鸣曲式模板走。
这引出一个根本性的权衡:你要的是一首"正确"的标准奏鸣曲,还是一首"有创造力"的独特作品?
对于大多数应用场景(BGM 生成、教学演示、音乐模板),结构正确性优先。但对于创意作曲,过度约束反而会扼杀 AI 产生惊喜的可能。
4.2 段落边界处的衔接问题
AI 逐段生成最大的问题是段落之间的衔接不自然。虽然调性、主题、速度都正确,但段落的最后一个小节和下一个段落的第一个小节之间往往没有平滑过渡。
解决思路:
- Overlap Generation:每个段落生成时额外生成 2 小节的"重叠区",拼接时做 cross-fade
- Conditional Generation:下一个段落的生成以前一个段落的最后 4 小节为条件
- Post-processing:用音频修复模型平滑段落边界
4.3 模型的选择
| 模型 | 结构控制能力 | 音乐质量 | 生成速度 |
|---|---|---|---|
| MusicGen (Meta) | 弱(需要外部结构注入) | 中 | 快 |
| Suno AI | 中(prompt 可以描述结构) | 高 | 慢 |
| MuseNet (OpenAI) | 强(支持 long-range structure tokens) | 高 | 慢 |
| 自研方案 (形式化 + MusicGen) | 强(形式化结构 + 模型生成局部) | 中 | 中 |
五、总结
让 AI 理解音乐的高阶结构,本质上是一个将隐性知识显性化、将艺术直觉形式化的过程。作曲家在创作时不会想"我现在在连接部,需要从 I 调到 V 调",但他们的耳朵和训练已经内化了这些规则。
我们通过形式化建模把这些规则提取出来,变成 AI 可以理解和执行的结构约束。三个核心要点:
- 结构是第一公民:先定义完整的结构图(SectionSpec 序列),再让模型逐段填充。不要让模型自由发挥超过 16 小节的连续生成。
- 约束比提示重要:用形式化约束(Key、Harmonic Template、Cadence)而不是自然语言提示来控制音乐方向。自然语言太模糊、不可靠。
- 验证结构正确性:生成后自动检查"呈示部副部是否在属调"、"再现部副部是否在主调"等关键约束。
AI 作曲的下一跳不是更逼真的单音色,也不是更长的生成长度,而是真正理解音乐作为一种时间艺术的内在结构逻辑。奏鸣曲式、回旋曲式、赋格——这些人类用 300 年总结出来的音乐结构,是给 AI 最好的"作曲教科书"。