1. 为什么 LLM 生成音乐这么难:从“能听懂”到“能演奏”
先聊一个不少做 AI 音乐生成的同学都遇到过的困惑:用大语言模型生成一段钢琴曲,音高对、和弦对、结构也完整,但一播放就感觉“很 MIDI”。旋律听起来像谱子,不像演奏。问题到底出在哪?答案往往不在音高,而在时间。
我们听真人演奏时,耳朵对“细微的时间偏移”极其敏感。同一个四分音符,钢琴家在乐句开头会稍微拖一点,在推向高潮时会赶一点,这些变化在 MIDI 文件里可能只有几十毫秒,却决定了这段音乐是否有“人味儿”。传统符号音乐生成方案通常把音符量化到固定的节拍网格上,模型学到的只是记谱时值,而不是演奏出来的真实时值。于是生成结果听起来就像把乐谱丢进播放器一样,正确但缺乏表现力。
本文要拆解的方案,正是围绕这个问题展开的。项目标题中的三个关键词——Agogic、Performance-Timed Music Tokens、LLM-Native Text-to-Symbolic-Music Generation——分别对应了音乐表演中的核心概念、用于建模它的 Token 设计,以及一套以大语言模型为原生的生成框架。接下来我们会从概念、数据、建模、工程实现到评估做一次完整梳理。
1.1 符号音乐生成与音频音乐生成的区别
在 AI 音乐领域,需要先区分两条技术路线。
音频音乐生成直接生成波形或频谱,最终产物是音频文件。这类方案能保留演奏中的大量细节,但对数据量、算力、编解码器的要求较高,且生成结果难以编辑、难以转换为标准乐谱。
符号音乐生成生成的是结构化的音乐表示,比如 MIDI、MusicXML、ABC 记号,最终可用合成器或音源播放。它的优势是可控性强:音高、节奏、和弦、力度都可以精确修改;劣势是容易丢失演奏层面的微妙变化。真实 MIDI 录制数据里往往带有表演痕迹,但如果预处理时强制量化到网格,这些痕迹就会被抹掉。
Agogic 方案属于后者,但它的特别之处在于:不把表演时值当作噪声抹掉,而是当作信息显式建模。
1.2 记谱时值与表演时值的差距
“记谱时值”是谱面上写出来的值。一个四分音符,谱面上就是 1 拍,在 MIDI 量化后通常对应固定的 tick 数。“表演时值”是演奏时实际发出的音长和音头位置,二者之间往往存在偏差。
这种偏差不是错误,而是音乐表情的一部分。古典钢琴演奏中常见的“rubato”,就是通过整体或局部的速度伸缩来营造情感张力;爵士乐中则常出现音符比节拍稍微靠前或靠后的“laid-back”或“pushed”处理。如果用一套只认识标准时值的模型来生成音乐,它永远无法表达这些演奏技巧。
这里有一个很容易被忽视的细节:静态的演奏时值偏差和动态的速度变化是两个层面的问题。前者是单个音符相对网格的偏移,后者是整段音乐的速度曲线。一个完整的音乐生成模型需要同时处理这两类信息,而 Agogic 方案中的 Performance-Timed Music Tokens 正是为了承载这类信息而设计的。
1.3 为什么“LLM-Native”是一个关键点
当前将大语言模型用于音乐生成,常见的做法是把音乐序列转成 token,然后让模型做自回归预测。这看起来顺理成章,但实际工程中经常出现一种“非原生”的设计:为了补足 LLM 缺失的时间建模能力,开发者在模型外部额外套上时序模块,比如 CRF、条件随机场、后处理量化器。这种做法虽然能提升某些指标,却让整个系统变得复杂,且偏离了大语言模型“统一 token 建模”的设计哲学。
Agogic 方案强调的是 LLM-Native,即尽量让 LLM 用自己最擅长的 next-token prediction 方式,直接完成带有表演细节的音乐生成。核心思路是把音乐演奏信息编码成 token,让模型在训练时就能看到“真实演奏出来的时值”,而不是经过严重量化后的简化版本。这样,模型在推理时也能天然地输出连续的、具有细微时间偏移的音乐序列,不需要额外的时序对齐模块。
2. Agogic 与 Performance-Timed Music Tokens 核心概念
2.1 Agogic 在音乐术语中到底指什么
Agogic(德语 Agogik,源自希腊语 agoge)来自西方音乐理论,传统上指通过细微的时值变化来表现音乐情感的手法。简单来说,它是“演奏者对谱面时值进行动态伸缩”的艺术。
举个例子:同样写的是八分音符,音乐家在演奏一个渐强乐句时,会把每个音稍微拉宽一点,形成一种“往前推进”的听觉感受;在演奏收束句时,则可能把最后一组音稍稍压紧,产生一种“站稳”的结束感。这些变化不写在谱面上,却在真实演奏中无处不在。
在计算机音乐领域,Agogic 这个词以前并不常见。把它引入符号音乐生成的语境中,其实暗示了一个重要转向:音乐 AI 的输出单位不应只是“标准音符”,而应是“带有人类演奏痕迹的音符”。这个转向对生成结果的听感、情感表达和可用性都有直接影响。
2.2 Performance-Timed 与 Notation-Timed 的差别
我们可以用一张对比来理解两种 Token 设计思路:
| 维度 | Notation-Timed(记谱时值) | Performance-Timed(表演时值) |
|---|---|---|
| 音头位置 | 量化到节拍网格 | 保留真实起音偏移 |
| 音长表示 | 标准时值(如 480 tick) | 实际持续时间 |
| 速度处理 | 通常简化为全局 BPM | 支持局部速度变化 |
| 表达力 | 偏机械、可预测 | 更接近真人演奏 |
| 建模难度 | 相对容易 | 需要处理更细粒度信息 |
传统符号音乐生成系统大多是 Notation-Timed 思维。它们把一拍固定切成 480 个 tick,音符起始时间必须是 0、120、240 这样的整数值。这种做法方便了 token 化,却把真人演奏中最宝贵的微时值变化直接丢掉了。
Performance-Timed 思路则更接近演奏者视角:一个音符在 MIDI 里实际从第 125 tick 开始,持续了 372 tick,那就如实记录 125 和 372。这里没有“必须对齐网格”的约束,模型需要学会预测的是演奏者真正弹出来的时间信息。
2.3 Performance-Timed Music Tokens 的设计思路
虽然论文原文我们暂时看不到完整细节,但从标题和现有音乐生成工作的发展脉络来看,这类 Token 序列通常会包含以下几类信息:
- 音符事件:音高、力度(velocity)、声部等基础信息。
- 起始时间偏差:相对节拍网格的偏移量,用于描述“早一点”或“晚一点”起音。
- 时值伸缩信息:实际音长与标准时长的比例或差值,用于描述“拖长”或“缩短”。
- 局部速度事件:可能用速度变化 token 表示某个小节或某个片段内的微速度变化。
这里的关键是:这些信息要能被表示为离散 token,又不能因为量化粒度过粗而丢失表演细节。理想情况下,一个 Performance-Timed Token 序列应该做到:只凭 token 本身,就能还原出与原始 MIDI 基本一致的演奏时序。
3. 数据准备:从 MIDI 文件到带表演时值的 Token 序列
3.1 数据来源有哪些
要用 Performance-Timed 方式训练模型,第一步是拿到“未被过度量化”的 MIDI 数据。常见来源包括:
- 真实乐器录制后转换的 MIDI,比如电子钢琴的 MIDI 输出、DAW 里的演奏录音。
- 带有精细速度信息的古典音乐 MIDI,比如由人逐音符录入、保留踏板和力度变化的数据集。
- 由音频转录得到的 MIDI,这类数据往往保留了大量真实演奏时序,但转录噪声需要额外清洗。
这里要特别提醒:网上很多 MIDI 是经过严格量化后的“乐谱型 MIDI”,用这类数据训练,模型学到的基本是 Notation-Timed 分布,无法体现 Performance-Timed 的优势。因此数据筛选阶段就要检查音符起始时间的分布:如果大量音符精确落在网格上,说明这个 MIDI 没有保留太多真实演奏信息。
3.2 乐谱对齐与表演时值提取
拿到原始 MIDI 后,常见处理流程包括:
- 解析 MIDI 事件:读取每个 track 上的 note_on、note_off、set_tempo 等事件,转换为绝对时间 t(tick 单位)。
- 节拍网格计算:根据 MIDI 的 ticks_per_beat(PPQ)将 beat 映射为网格位置。例如 PPQ = 480 时,一个四分音符对应的 tick 数是 480。
- 音符起始偏差计算:对每个音符,计算其起始 tick 与最近网格位置的差值。
- 时值偏差计算:对比音符实际持续时间和谱面时值之间的差异。
需要说明的是,“谱面时值”在纯 MIDI 中通常并不存在,需要从上下文推断。工程上常用的近似做法是:用局部平均速度或相邻网格长度作为参考值,计算偏差比例。
3.3 数据清洗与标准化
真实 MIDI 数据往往存在各种问题:音符重叠、重复音、超长延音、轨道混乱、速度曲线不连续等。训练前需要做统一清洗:
- 去掉持续时间过短或过长的异常音符;
- 将多轨 MIDI 规范化为固定声部数量,比如钢琴曲统一为左右手两个 track;
- 将速度曲线平滑处理,避免因录制噪声产生跳变;
- 统一 PPQ,比如把不同 MIDI 文件的重采样到同一 ticks_per_beat,便于后续合并训练。
清洗时必须谨慎:过度清洗会把表演时值也抹掉,违背 Performance-Timed 的初衷。建议保留“真实偏差”,只去除“明显错误”。
4. 构建 LLM 文本到符号音乐生成流程
4.1 文本与音乐 Token 的统一序列
LLM-Native 的关键在于:让文本指令和音乐 Token 处在同一个序列中。假设我们要生成一段“忧伤的钢琴曲,行板速度”,那么输入序列可以拼接成:
<BOS> 忧伤的钢琴曲,行板速度 <SEP> <PIANO> <TEMPO_70> <NOTE_60> <VEL_65> <ONSET_0> ... <EOS>这样模型在训练时学习的是“文本条件 → 音乐 Token 序列”的联合分布。生成时只需要输入文本前缀,就能让模型继续输出后续音乐 Token。
这里的 Token 需要覆盖:
- 文本词表:可以是中文、英文或者音乐领域专用词汇;
- 控制标记:如
<PIANO>、<TEMPO_70>、<BOS>、<EOS>; - 音乐事件 token:音高、力度、起始时间、时值等。
4.2 文本指令设计
文本指令的质量会直接影响可控性。常见的指令设计思路有:
- 风格描述:“Baroque”“Romantic”“Jazz Ballad”
- 速度与情绪:“Andante, calm”“Allegro, excited”
- 结构要求:“ABA form”“with a bridge”
- 演奏提示:“with rubato”“slightly behind the beat”
这些指令一方面作为条件输入,另一方面也是评测时验证模型可控性的载体。工程上可以先从简单的组合式模板开始,再逐步过渡到自然语言描述。
4.3 模型训练与推理
训练阶段,将文本和音乐 Token 拼成完整的序列做自回归训练。损失函数是标准的交叉熵,只需要预测序列中每个位置的下一 Token。这里要注意的是,不是所有 Token 都需要同权重参与损失计算;如果音乐 Token 占了绝大多数,文本条件部分的 loss 可能被稀释,可以考虑对不同类别 Token 做加权。
推理阶段,输入文本前缀后,使用自回归解码生成音乐 Token。解码完成后,将 Token 序列还原为 MIDI 事件,再交给合成器渲染音频。如果生成结果有节奏不对齐或音区异常问题,可以在后处理阶段做轻量修正,但不要做密集量化,否则会破坏 Performance-Timed 的效果。
5. 工程实现示例
下面我们用 Python 演示从 MIDI 抽取音符事件、计算量化偏差、并构造带表演时值的 Token 序列。示例以常见环境为准,使用的库为mido,版本需要根据你的项目实际情况调整。
5.1 读取 MIDI 并提取音符事件
先安装依赖:
pip install mido下面的代码读取一个单轨或多轨 MIDI,将 note_on 和 note_off 配对,输出每个音符的绝对起始 tick 和持续时间:
import mido from mido import MidiFile def extract_notes(midi_path: str): """从 MIDI 文件中提取音符事件,返回 (PPQ, note_list)。""" midi = MidiFile(midi_path) ppq = midi.ticks_per_beat # 每四分音符的 tick 数 # 用于暂存未配对的 note_on pending = {} note_events = [] for track in midi.tracks: abs_tick = 0 for msg in track: abs_tick += msg.time if msg.type == "note_on" and msg.velocity > 0: key = (msg.channel, msg.note) pending[key] = (abs_tick, msg.velocity) elif msg.type == "note_off" or (msg.type == "note_on" and msg.velocity == 0): key = (msg.channel, msg.note) if key in pending: start, velocity = pending.pop(key) note_events.append({ "note": msg.note, "velocity": velocity, "start_tick": start, "duration_tick": abs_tick - start, }) note_events.sort(key=lambda x: (x["start_tick"], x["note"])) return ppq, note_events if __name__ == "__main__": ppq, notes = extract_notes("example.mid") print("ticks_per_beat =", ppq) print("note count =", len(notes)) print("first 5 notes =", notes[:5])这段代码把 MIDI 中的时间信息完整保留下来。注意,真实工程的 MIDI 文件可能是多轨多乐器,上面的pending字典以(channel, note)为键,可以避免不同声部互相干扰。
5.2 分析音符时值的量化偏差
拿到音符事件后,可以计算“实际时值与理想网格”的偏差。下面以 16 分音符网格为例,统计平均量化偏差。偏差越大,说明这段 MIDI 保留了越多的表演时值:
import statistics def analyze_timing_outside_grid(ppq: int, notes: list): """ 计算音符起始时间和时长相对 16 分音符网格的偏差。 偏差用 tick 数表示,数值越大说明演奏自由度越高。 """ grid = ppq / 4 # 一个 16 分音符对应的 tick 数 onset_offsets = [] duration_offsets = [] for n in notes: # 起始时间相对最近网格的偏移 start = n["start_tick"] onset_off = start % grid if onset_off > grid / 2: onset_off = grid - onset_off onset_offsets.append(onset_off) # 时值相对最近网格的偏移 dur = n["duration_tick"] dur_off = dur % grid if dur_off > grid / 2: dur_off = grid - dur_off duration_offsets.append(dur_off) avg_onset = statistics.mean(onset_offsets) avg_dur = statistics.mean(duration_offsets) return avg_onset, avg_dur ppq, notes = extract_notes("performance.mid") avg_onset, avg_dur = analyze_timing_outside_grid(ppq, notes) print(f"平均起始偏差: {avg_onset:.2f} tick") print(f"平均时值偏差: {avg_dur:.2f} tick")如果这段 MIDI 是真人演奏录入,平均偏差通常不会等于 0;如果它是量化后的乐谱型 MIDI,平均偏差会非常接近 0。
5.3 构造 Performance-Timed Token 序列
构造 Token 时,最粗糙的做法是直接把原始 tick 数字写进序列,但这会让词表变得非常大。工程上通常将偏差分桶(bucket),比如将 16 分音符网格内的偏移量量化为若干个区间。下面给出一个简化的示例:
def build_performance_token_sequence(ppq: int, notes: list, onset_bucket_size: int = 30): """将音符事件转换为带表演时值的 token 列表。""" tokens = [] for n in notes: note = n["note"] velocity = n["velocity"] start_tick = n["start_tick"] duration_tick = n["duration_tick"] # 起音偏移分桶:把偏移量划分为多个区间 grid = ppq / 4 onset_offset = start_tick % grid bucket = int(onset_offset // onset_bucket_size) onset_token = f"ONSET_OFFSET_{bucket}" # 时值分桶:这里简单以 tick 整数表示,工程上可进一步压缩 dur_token = f"DURATION_{duration_tick}" tokens.append(f"NOTE_{note}") tokens.append(f"VELOCITY_{velocity}") tokens.append(onset_token) tokens.append(dur_token) return tokens ppq, notes = extract_notes("performance.mid") token_seq = build_performance_token_sequence(ppq, notes) # 查看前 20 个 token print(" ".join(token_seq[:20]))这里的ONSET_OFFSET_{bucket}只是一个简化表示。在完整实现中,你还需要考虑速度变化、声部信息、文本控制标记等。关键点是:时间信息不要先量化到网格再去 token 化,而是应该先记录真实时间,再根据精度需要做有界分桶。这样既保留了表演信息,又控制了词表大小。
6. 生成结果评估与应用场景
6.1 客观评估指标
Performance-Timed 音乐生成的效果评估,不能只看“音符正确率”。以下指标更有参考价值:
- 量化偏差分布:生成结果的平均偏移量应与真实演奏数据接近。如果偏移量几乎为 0,说明模型退化成了记谱时值生成。
- 音高准确率:生成序列与目标音高分布的匹配程度。
- 结构重复率:是否存在过度重复或自我抄袭。
- 时值多样性:同一时值音符在不同上下文中的实际时长差异是否足够丰富。
- Token 级别困惑度:在保留测试集上的负对数似然。
客观指标可以快速发现问题,但不足以判断“好不好听”。最终还是要回到主观听感。
6.2 主观听感评估
建议组织小规模 AB 测试,对比以下方案:
- 基线模型:使用标准量化 Token 训练的 LLM;
- Agogic 模型:使用 Performance-Timed Token 训练的 LLM;
- 真实演奏录音。
让听众在“自然度”“情感表达”“结构清晰度”三个维度打分。这里要提醒一点:如果只是让听众判断“像不像 MIDI”,Agogic 的增益会比较明显;但如果听众只关注“旋律是否好听”,差异可能并不大。因为 Agogic 解决的核心问题是表演感,而不是作曲层面的旋律质量。
6.3 可以落地的应用场景
- 音乐教学辅助:生成带有演奏表情的示范 MIDI,让学生听到“谱面上没写但应该这么弹”的细节。
- 伴奏生成:根据主旋律和人声哼唱,生成带有真实演奏感的钢琴伴奏。
- 数字音乐创作:为作曲者快速生成带情感的 MIDI 草稿,再导入 DAW 进行精细修改。
- 游戏与影视配乐:生成符合情绪要求的背景音乐片段,减少人工修音的工作量。
7. 常见问题与排查思路
训练和生成过程中,可能会遇到下面几类问题:
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| 生成结果仍然很机械 | 训练数据本身被过度量化,或 Token 中时间信息被过度分桶 | 检查数据集的偏移量分布,确认是否保留真实演奏信息 |
| 词表过大,训练内存暴涨 | 直接把 tick 数值当作 token,或分桶过细 | 采用有界分桶、相对偏差、速度归一化等方法压缩词表 |
| 训练 loss 正常但生成混乱 | 文本指令与音乐 Token 序列拼接不当 | 检查序列长度控制、特殊标记设置,必要时按 Token 类别加权 |
| 对文本指令不敏感 | 指令只出现在序列开头,被长音乐序列稀释 | 增加指令重复或使用后缀引导,强化条件建模 |
| 生成音符重叠严重 | 没有全局声部上下文约束 | 在 Token 序列中加入声部标识,并设计冲突检测后处理 |
| 推理速度太慢 | 序列过长,自回归生成耗时 | 考虑对音符事件做局部并行化,或用蒸馏小模型加速 |
以“生成结果仍然很机械”为例,排查顺序建议是:
- 先检查训练数据:将原始 MIDI 的起始偏差分布打印出来,看是否集中在 0 附近。
- 检查 Token 化逻辑:看分桶粒度是否过大,比如 30 tick 的分桶在 PPQ=480 时接近 1/16 音符,精度可能不够。
- 检查训练配置:如果同时加入了较强的数据增强或随机量化,可能无意间抹掉了表演时值。
8. 最佳实践与工程建议
8.1 数据质量优先,宁缺毋滥
Performance-Timed 方案的前提是“数据里真的有表演时值”。如果训练集中混入大量量化 MIDI,模型学到的分布会被拉回 Notation-Timed。建议在数据筛选阶段就计算每个文件的量化偏差指标,设定阈值进行过滤,并保留原始 MIDI 备份,方便追溯。
8.2 不要为了词表大小牺牲时间精度
Token 分桶粒度是一个权衡项。分桶过粗,表演细节丢失;分桶过细,词表膨胀,训练效率下降。可以考虑在低 token 量的情况下使用相对偏差编码:不是编码绝对偏移量,而是编码当前音符与前一个音符之间的时间差。这样既能保留时间信息,又能控制词表规模。
8.3 训练时使用混合精度策略
从 LLM 训练的实际经验来看,FP32、FP16、BF16 的精度选择会影响训练稳定性和最终效果。音乐 Token 序列往往较长,显存压力大,适合在训练中使用 BF16 混合精度;但在 loss 波动较大时,可以先切回 FP32 做对比实验,排查是否由精度下降导致。
8.4 评估闭环必须包含听感验证
很多研究者习惯只看客观指标,但这在音乐生成里会有严重盲区。建议每个训练 checkpoint 都固定生成几首测试曲目,同一指令重复生成多次,交给团队或听众做快速主观评估。客观指标用于定位问题,主观听感用于判断是否可用。
8.5 版权与合规意识
训练数据的版权问题必须重视。使用真实演奏 MIDI 时,要确认来源是否允许用于模型训练。生成结果的版权归属也建议在项目中提前定义清楚。发布模型或 demo 时,避免使用受版权保护的原始音频和未授权的乐谱数据。
8.6 从生成到可控生成:RAG 与 Agent 的扩展思路
如果希望音乐生成更可控,可以进一步引入 RAG(检索增强生成)或 Agent 编排。例如,在使用 Performance-Timed Tokens 的基础上,额外加入一个“风格检索”模块,从风格库中检索与目标情绪相近的乐句片段,拼接到文本指令中。这种架构可以复用当前 LLM 生态里的成熟工具链,让音乐生成变得更加灵活。
9. 总结与学习路线
本文从 LLM 生成音乐时“听感机械”的痛点出发,拆解了 Agogic 与 Performance-Timed Music Tokens 的核心思想:把演奏时的真实时值偏差作为建模对象,让 LLM 在原生 token 预测框架下学会表达表演细节。
主要收获可以概括为三点:
- 概念层面:理解 Agogic 术语背后的音乐表演含义,明白 Performance-Timed 与 Notation-Timed 的核心差异。
- 工程层面:掌握从 MIDI 提取音符事件、计算量化偏差、构造表演时值 Token 序列的基本流程。
- 实践层面:了解训练、评估、排查过程中容易踩的坑,以及数据质量、Token 粒度、听感验证等关键原则。
如果接下来想深入,可以按这个顺序继续学习:先熟悉 MIDI 文件格式和mido的底层事件流,再研究现有符号音乐生成模型的 Token 设计方案,然后自己搭建一个小型数据集,试验不同分桶策略对生成效果的影响,最后再尝试引入更复杂的可控生成框架。
音乐生成是 LLM 应用中一个很特别的方向。它既考验模型对结构化序列的建模能力,也考验对艺术表现力的理解。Agogic 这个方向给了一个很好的提醒:在 AI 生成领域,那些看起来微小、容易被量化和归一化掉的“异常值”,往往才是真正决定作品质量的关键信息。