Agogic表演时序Token:让LLM生成音乐更有‘人味儿’

📅 2026/8/27 4:53:38
Agogic表演时序Token:让LLM生成音乐更有‘人味儿’
如果你做过文本到音乐的生成任务大概率遇到过一种奇怪的感觉模型生成的旋律音高是对的和弦进行也算顺畅但听感就是“很机械”“像节拍器在弹琴”。这时候很多人会去调模型结构、换解码策略、加对抗训练但问题往往不在模型本身而在音乐被表示成 token 的那一刻就丢了东西。Agogic 这个研究方向之所以值得关注是因为它把矛头指向了音乐生成里一个长期被低估的环节表演时序performance timing。它提出了一种“面向 LLM 原生的音乐 token 设计”目标不是让模型多学几个音符而是让模型在生成符号音乐时能够直接输出带有人类演奏时序感的精细控制信息。本文会用技术拆解的方式来聊清楚几个问题为什么传统 token 化方案天生表达不了“人的节奏感”Agogic 这类“表演时序音乐 token”到底解决了什么如果你想在自己的音乐生成项目里尝试这种思路最小实现长什么样、坑在哪里、怎么验证效果。1. 为什么 LLM 生成音乐时总差一点“人味儿”先看一个几乎所有音乐生成项目都会遇到的真实现象。你让 LLM 生成一段 8 小节的钢琴旋律模型输出的是离散的 MIDI 音符事件哪个时刻按下哪个音、按多久、力度多少。听起来应该没问题对吧但实际听感是每个音都精确落在 16 分音符的网格上每个音的时值都是整齐的倍数关系力度变化很平缓整体像一台没有感情的合成器在播放乐谱。问题出在哪里出在符号音乐的 token 化方式。主流的符号音乐 token 方案比如 MIDI-Like 和 REMI本质上都是把 MIDI 事件按时间顺序切碎再映射成离散 token。它们能表达“第几个位置存在一个音符”但很难表达“这个音比标准位置晚了 30 毫秒”“这个音稍微弹长了一点”“这个和弦的力度比前一个强了 15%”。这些细微偏差在乐谱里通常看不到但在演奏里恰恰是“人味儿”的主要来源。换句话说传统方案把音乐当成了一张精确到网格的图纸而演奏家真正需要的是一份动态的、带时间弹性的演出指示。Agogic 想要补的正是这一层信息。这个点对做 AI 音乐生成的开发者来说非常关键你后续加再多后处理、做再多采样器渲染都无法弥补 token 化阶段丢失的时间细节。如果输入表征里没有“表演时间”这个维度模型就不可能凭空生成它。2. 符号音乐生成的核心问题从“音符正确”到“表演到位”要理解 Agogic需要先回顾符号音乐生成这个领域是怎么一步步走到今天的。2.1 第一代把 MIDI 当成普通序列最早的方案很简单把 MIDI 文件里的音符事件线性展开。每一条消息记录成一个 token比如Note-On(C4, velocity100)Time-Shift(120 ticks)Note-Off(C4)模型只需要做一件事预测下一个 token。这就是 MIDI-Like 方案。设计简单但问题也明显序列太长长程依赖差而且时间刻度完全取决于 MIDI 文件的分辨率不同文件之间很难统一。2.2 第二代结构化位置编码后来出现了 REMI 这类方案把音乐按小节、位置、乐器、音符结构化成嵌套事件用相对位置替代绝对时间。比如Bar(0)Position(0)Tempo(120)Note-On(C4, velocity80)这种方案让模型更容易学到音乐的结构规律生成结果在“乐理正确性”上提升非常明显。但 REMI 的位置依然是固定的量化网格每个 Position 通常是 16 分音符或 32 分音符。你可以在网格上选择放不放音符但音符“不能离开网格”。这带来一个隐藏问题量化网格一旦固定表演中的微时值偏差expressive timing就被过滤掉了。人类演奏者不会严格按网格演奏。旋律的高潮处会稍微拖一点快速的华彩段会越弹越急左手伴奏的某个和弦会略微靠后让右手旋律更有“呼吸感”。这些偏差是表演意图的一部分不是噪音。2.3 第三代从音频 token 到符号与表现的结合再往后音乐生成开始分叉。一条路是直接生成音频 token比如用 EnCodec 这类神经编解码器把音频压成离散 token再交给 LLM 生成。好处是保留了完整的音色和表演细节坏处是可控性差你很难告诉模型“这里改成大三和弦”“这段改成 4/4 拍”。另一条路就是 Agogic 所代表的继续留在符号音乐的世界里但对 token 词典做重新设计把表现层面的连续信息离散化后放进 token 序列让 LLM 能同时学到“音符结构”和“表演时序”。这才是“Performance-Timed Music Tokens”的核心主张不是换模型而是换 token 粒度。3. Agogic 到底是什么Performance-Timed Music Tokens 拆解先从词源说起。Agogic 来自音乐术语 agogic accent缓急重音指的是通过延长或缩短音符时值来制造重音效果而不是靠力度强调。这个命名其实很准确项目想解决的正是传统符号表示里最容易被忽略的“时间弹性”。3.1 什么是 Performance-Timed Music Tokens我的理解是它是一类在传统符号音乐 token 之外额外编码“音符相对标准位置的时间偏移”和“音符实际时值相对记谱时值的偏移”的 token 设计。传统 REMI 里一个 Position(2) 就表示“这一拍的第二份”。但表演者实际按下琴键的时刻可能比这个位置早 20 毫秒或晚 30 毫秒。Agogic 这类方案会把偏移量也量化进 tokenOnset-Delta(-30ms)Note(C4, velocity92, duration-offset45ms)这样模型不仅知道“什么时候按”还能知道“按得早了一点还是晚了一点早了多久”。从建模角度看这相当于把连续时间域上的微小偏差变成了一系列离散类别。LLM 本质上是一个离散 token 的概率模型只要偏差被合理量化成 token模型就能学习它在音乐语境中的分布规律。3.2 与 MIDI-Like、REMI 的核心差异可以用一张表来对比维度MIDI-LikeREMIPerformance-Timed TokensAgogic 思路时间表示绝对 tick 偏移小节/位置量化网格量化网格 微时序偏移能否表达细微速度变化弱基本不能强能否表达音符微早/微晚不能不能能序列长度长中等中等偏长对 LLM 的友好度一般较好较好但词表更复杂可控性弱中中高这里有一个容易误解的点加时间偏移 token 不等于放弃结构化。Agogic 大概率仍然会保留小节、位置、和弦这类结构信息只是在这些维度之上增加了“表演执行层”的描述。生成时LLM 先决定“和声与旋律骨架”再决定“具体怎么弹”。这种分层天然更适合音乐这种“结构 表现”双轨信息。4. Agogic 的适用场景与边界任何 token 设计都是成本和收益的权衡。Performance-Timed Music Tokens 不是银弹它适合的任务和不适合的任务都很明确。4.1 适合的场景需要生成 MIDI 后再交给采样器或音源渲染的音乐项目。这类项目对节奏的人性化要求较高而传统网格化 token 输出往往需要额外写一套“人性化后处理”脚本。研究演奏风格、解释性时序分析。比如想分析不同演奏者对同一首曲子的速度曲线差异这类 token 能保留可用的时间信息。LLM 原生可控生成。如果你希望用自然语言提示控制“这里慢一点”“这段更自由”模型必须有能力在输出里表达这些微观时间变化。需要从符号音乐还原表演细节的音频渲染流水线。4.2 不适合的场景对最终音频质量要求极高、不需要符号中间态的项目。直接生成音频 token 可能更合适。资源敏感的边缘设备。加了偏移 token 后序列长度和词表都会变大推理开销会上升。简单教学演示。如果只是生成“正确的乐谱”传统 REMI 已经完全够用没有必要引入额外的复杂度。没有高质量表演数据支撑的场景。这类方案依赖带有真实演奏信息的 MIDI 数据如果训练数据本身是量化后的乐谱学不到有用的表演时序信息。所以选不选 Agogic 这类思路核心判断依据只有一条你的产品是否需要“有表现力的符号音乐”作为中间表示。5. 环境准备与最小工程结构前面聊了很多背景从这一节开始进入动手环节。为了把原理讲透我下面用一个极简的 tokenizer 演示“如何把 MIDI 变成带表演时序的 token 序列”。这不是 Agogic 官方的 API具体实现细节以项目实际发布为准但核心设计思想是一致的在传统符号 token 旁边加上时间偏移和时值偏移。5.1 环境准备建议准备以下环境Python 3.10 以上pretty_midi 或 mido用于解析 MIDItransformers、torch用于后续的 LLM 训练或推理一个带表演信息非量化的 MIDI 文件做测试你可以只装前两个跑通原理pip install mido pretty_midi如果打算继续做训练实验再把深度学习框架加上pip install torch transformers版本不需要完全对齐本文以你本机实际兼容情况为准。5.2 工程结构建议一个可扩展的实验结构大致如下agogic_demo/ ├── data/ │ └── example.mid ├── src/ │ ├── tokenizer.py # token 设计与编码解码 │ ├── dataset.py # 构建训练数据集 │ ├── generate.py # 生成入口 │ └── decode_to_midi.py # 从 token 恢复 MIDI ├── configs/ │ └── train.yaml └── README.md6. 完整示例把 MIDI 变成表演时序 Token这一节给出可运行的最小代码分三步解析 MIDI、量化事件、组装 token 序列。6.1 定义 token 词表为了表达表演时序我定义三类额外 tokenTIME_SHIFT用于表示网格层的时间推进和 REMI 里的 Position 类似。ONSET_DEV表示音符起始时间相对网格位置的偏移用毫秒量化。DUR_DEV表示音符时值相对标准时值的偏移用毫秒量化。# 文件路径agogic_demo/src/tokenizer.py from collections import OrderedDict import pretty_midi import numpy as np # 配置网格分辨率32 分音符、偏移量化粒度 GRID 32 # 每拍 32 份 ONSET_CLASSES 9 # -20ms ~ 20ms步长 5ms DUR_CLASSES 9 # -40ms ~ 40ms步长 10ms class PerformanceTokenizer: def __init__(self): self.vocab self._build_vocab() def _build_vocab(self): vocab OrderedDict() vocab[PAD] 0 vocab[BOS] 1 vocab[EOS] 2 # 音符事件包含音高、力度、偏移类别 # 这里用 NOTE_pitch_vel_onsetCls_durCls 形式 note_tokens [] for pitch in range(0, 128): for vel in range(0, 128, 8): # 力度量化到 16 档 for onset_cls in range(ONSET_CLASSES): for dur_cls in range(DUR_CLASSES): note_tokens.append( fNOTE_{pitch}_{vel}_{onset_cls}_{dur_cls} ) for i, token in enumerate(note_tokens, startlen(vocab)): vocab[token] i # 时间推进 token for pos in range(GRID * 8): # 假设最多 8 拍内推进 vocab[fTIME_SHIFT_{pos}] len(vocab) return vocab property def vocab_size(self): return len(self.vocab) def token_to_id(self, token: str) - int: return self.vocab[token] def id_to_token(self, token_id: int) - str: for token, idx in self.vocab.items(): if idx token_id: return token raise KeyError(token_id)这段代码的关键点不是词表内容本身而是它在设计上明确区分了两类信息结构信息音高、力度、时间网格位置。表演信息起始偏移、时值偏移。实际项目中词表会比这个复杂得多通常还包括乐器、和弦、速度等 token。但核心分层思想是一致的。6.2 从 MIDI 抽取事件并编码接下来把 MIDI 文件解析成 token 序列。重点在于同一音符除了记录音高和力度还要计算它与标准网格位置的偏差。# 文件路径agogic_demo/src/tokenizer.py续 def midi_to_performance_tokens(midi_path: str, tokenizer: PerformanceTokenizer) - list[int]: midi pretty_midi.PrettyMIDI(midi_path) # 假设只处理第一个乐器且按 120 BPM 把时间映射到“网格刻度” # 实际项目需要根据曲目速度动态计算 bpm 120.0 seconds_per_beat 60.0 / bpm seconds_per_grid seconds_per_beat / GRID events [] for note in midi.instruments[0].notes: # 音符起始时间在整个小节里的相对位置按拍取整 start_in_beats note.start / seconds_per_beat quantized_index round(start_in_beats * GRID) # 计算起始时间相对于网格的偏差毫秒 grid_time quantized_index * seconds_per_grid onset_offset_ms (note.start - grid_time) * 1000.0 # 计算时值偏移 expected_duration round(note.end / seconds_per_grid - quantized_index) * seconds_per_grid dur_offset_ms (note.end - note.start - expected_duration) * 1000.0 # 量化偏差 onset_cls int(np.clip(round(onset_offset_ms / 5.0) ONSET_CLASSES // 2, 0, ONSET_CLASSES - 1)) dur_cls int(np.clip(round(dur_offset_ms / 10.0) DUR_CLASSES // 2, 0, DUR_CLASSES - 1)) # 力度量化到 16 档 vel_quantized (note.velocity // 8) * 8 vel_quantized min(max(vel_quantized, 0), 127) token_name fNOTE_{int(note.pitch)}_{vel_quantized}_{onset_cls}_{dur_cls} events.append((quantized_index, token_name)) events.sort(keylambda x: x[0]) # 组装成 token 序列按位置推进插入 TIME_SHIFT tokens [tokenizer.token_to_id(BOS)] last_pos 0 for pos, token_name in events: if pos last_pos: tokens.append(tokenizer.token_to_id(fTIME_SHIFT_{pos - last_pos})) last_pos pos tokens.append(tokenizer.token_to_id(token_name)) tokens.append(tokenizer.token_to_id(EOS)) return tokens运行下面这段代码可以看到一个 MIDI 文件被转成带偏移信息的 token 序列。if __name__ __main__: tok PerformanceTokenizer() seq midi_to_performance_tokens(data/example.mid, tok) print(vocab size:, tok.vocab_size) print(token length:, len(seq)) print([tok.id_to_token(t) for t in seq[:20]])预期输出类似vocab size: 117154 token length: 342 [BOS, TIME_SHIFT_3, NOTE_60_64_4_4, NOTE_64_72_5_3, ...]真正的 Agogic 词表不会是这种简单拼接方式通常会把偏移做成独立的维度或者采用更紧凑的事件类型。但“结构 偏移”的组合思路是相通的。7. 文本到符号音乐的生成链路设计有了 token 序列下一步就是让 LLM 把文本提示映射成这类 token 序列。这是典型的“文本到符号音乐生成”任务。7.1 把文本提示变成生成前缀可以直接用一段文本描述作为生成条件。为了让模型理解得更稳定建议把提示标准化成模板再把模板和 token 序列拼接在一起训练。# 文件路径agogic_demo/src/generate.py PROMPT_TEMPLATE Generate a piano phrase with the following requirements: - Mood: {mood} - Tempo: {tempo} BPM - Style: {style} - Form: {form} - Expressive timing: {expression} Music tokens: def build_prompt(moodcalm, tempo80, styleclassical, form8 bars, expressionrubato): return PROMPT_TEMPLATE.format( moodmood, tempotempo, stylestyle, formform, expressionexpression, )在实际训练中文本部分会被 tokenize 成文本 token音乐部分会被 tokenize 成音乐 token两者拼接成一个序列。推理时模型根据文本前缀自回归生成音乐 token。7.2 生成并解码回 MIDI假设你已经有一个训练好的模型推理逻辑大致如下# 文件路径agogic_demo/src/generate.py续 import torch from transformers import AutoModelForCausalLM, AutoTokenizer def generate_music_tokens(text_prompt: str, model_path: str, max_new_tokens: int 1024): model AutoModelForCausalLM.from_pretrained(model_path) text_tokenizer AutoTokenizer.from_pretrained(model_path) inputs text_tokenizer(text_prompt, return_tensorspt) with torch.no_grad(): outputs model.generate( **inputs, max_new_tokensmax_new_tokens, do_sampleTrue, temperature0.9, top_p0.95, ) # 这里需要根据你自己的词表设计来确定如何切分文本部分和音乐部分 return outputs[0].tolist()生成出来的 token 序列要解码回 MIDI。注意这一步要把 ONSET_DEV 和 DUR_DEV 还原成真实的起始时间和时值才能让 MIDI 带有表演感。# 文件路径agogic_demo/src/decode_to_midi.py import pretty_midi def performance_tokens_to_midi(token_ids: list[int], tokenizer, output_path: str): midi pretty_midi.PrettyMIDI() instrument pretty_midi.Instrument(program0) bpm 120.0 seconds_per_beat 60.0 / bpm seconds_per_grid seconds_per_beat / GRID current_grid_pos 0 notes_sorted [] for token_id in token_ids: token tokenizer.id_to_token(token_id) if token.startswith(TIME_SHIFT): shift int(token.split(_)[2]) current_grid_pos shift elif token.startswith(NOTE): parts token.split(_) pitch int(parts[1]) vel int(parts[2]) onset_cls int(parts[3]) dur_cls int(parts[4]) # 偏移还原 onset_offset_ms (onset_cls - ONSET_CLASSES // 2) * 5.0 dur_offset_ms (dur_cls - DUR_CLASSES // 2) * 10.0 start_time current_grid_pos * seconds_per_grid onset_offset_ms / 1000.0 duration seconds_per_grid dur_offset_ms / 1000.0 notes_sorted.append((start_time, pitch, vel, duration)) for start, pitch, vel, duration in sorted(notes_sorted, keylambda x: x[0]): note pretty_midi.Note( velocityvel, pitchpitch, startstart, endstart duration ) instrument.notes.append(note) midi.instruments.append(instrument) midi.write(output_path) print(saved to, output_path)这是一个最小可用的闭环文本提示 → LLM → 音乐 token → 带表演时序的 MIDI。8. 效果评估怎么判断“生成得好”很多团队做到这一步发现最难的不是训练而是评估。判断一个音乐生成系统好不好不能只看 loss 降了多少。8.1 结构指标先看乐理正确性音符密度是否合理。和弦进行是否符合基本调性。节奏型是否与提示匹配。是否出现了大量不自然的跨度过大的跳进。这类指标可以用程序统计比如统计 token 里的音高分布、时值分布、终止式出现频率。如果与训练集分布差异过大说明生成开始“失控”了。8.2 表演时序指标这是 Agogic 这类方案最应该评估的地方ONSET_DEV token 的分布是否接近真实演奏数据DUR_DEV token 的分布是否接近真实演奏数据速度曲线是否符合情感提示比如提示“rubato”时速度变化范围应该更大。如果发现 ONSET_DEV 基本都集中在中间档位说明模型没有学会使用偏移能力或者训练数据里根本没有足够的表演信息。8.3 听感评估最终还是要靠听。建议组织小规模主观评测请评测者听生成的 MIDI 渲染音频从“音乐性”“节奏自然度”“与提示一致性”三个维度打分。一个实用的经验是对比实验永远比绝对打分有价值。把同一段文本提示分别用传统 token 方案和 Performance-Timed token 方案生成再让评测者做 AB 对比结论会清晰得多。9. 常见问题与排查方法在实际复现类似项目时最容易出问题的几个点如下整理成排查表供收藏。问题现象可能原因排查方式解决方案生成的音符全部对齐到网格没有偏移训练数据本身是量化 MIDI统计训练集 ONSET_DEV 分布换用带真实演奏数据的 MIDI或对训练数据做随机偏移增强序列过长训练显存溢出偏移 token 增加了序列长度检查 token 序列的平均长度增大 GRID 粒度、减少偏移类别数或用更长序列的训练框架文本提示生成了错误的调性/速度提示模板与音乐 token 之间缺乏对齐检查提示文本与目标序列是否拼接正确在提示里加入更明确的拍号、速度、调性字段生成的 MIDI 起点大量为负值偏移解码逻辑错误打印 start_time 最小值检查 ONSET_DEV 还原公式确保不超过当前网格位置偏移分布集中在中心档位偏移量化粒度过粗或 Loss 权重过低打印偏移 token 的分布直方图调整量化粒度或对偏移 token 设置更大的 Loss 权重生成结果结构混乱、小节错乱缺少小节/位置的结构约束检查是否保留了 TIME_SHIFT 和结构化位置信息不要完全去掉网格层结构 token 和偏移 token 要共存10. 工程实践与最佳建议从工程角度看把这类方向落地到产品里有几条建议值得提前记下来。10.1 永远保留“结构层”与“表演层”的分层这是最重要的原则。不要把所有的时序信息混在同一个 token 里。推荐的层次是结构层小节、位置、和弦、音高 → 执行层力度、偏移、踏板 → 渲染层音源、混响。这样带来的好处是可以单独控制结构稳定性和表现力。可以针对不同层次设计不同的 Loss 权重。生成后可以后处理调整表演层而不破坏结构层。更容易做可控生成比如指定“结构和声不变只改变速度曲线”。10.2 训练数据质量比模型结构更重要很多团队在 Performance-Timed token 上效果不佳原因不是模型不够强而是训练数据里的 MIDI 大多是量化过的根本没有表演信息。建议从以下来源筛选数据真实演奏录制的 MIDI而不是在 DAW 里鼠标点的 MIDI。带有速度轨tempo track和 CC 控制信息的 MIDI。不同演奏者对同一曲目的多次录音这类数据对学习表演风格差异非常宝贵。如果数据不足可以先做数据增强在量化 MIDI 上加入符合人类演奏习惯的随机偏移比如旋律音稍微提前、长音稍微延长、重拍音略微靠后。这类增强虽然不如真实演奏数据但能帮模型建立“偏移与音乐语境”的基本关联。10.3 偏移量化粒度要按场景调偏移量化的步长直接影响两件事表现力上限和模型学习难度。如果步长太粗比如 20ms 一档相邻档位听感差异不明显模型容易学成一个模糊的中间值如果步长太细比如 1ms 一档词表爆炸而且训练数据很难覆盖到每一档。更务实的做法是起始偏移用 5ms 或 8ms 一档时值偏移用 10ms 一档覆盖范围控制在 ±40ms 左右。这个范围已经能覆盖大多数人类演奏的自然偏差。10.4 用生成策略控制表现力同一个模型通过不同的采样参数就能得到完全不同的表现力温度低生成结果偏向中位数偏移听感稳定但机械。温度高偏移分布更宽听感有惊喜但也可能失控。对偏移 token 使用独立 temperature如果实现对 token 类型有区分可以给偏移 token 单独设一个较高的 temperature让结构层保持稳定表演层更有变化。这个技巧在实际项目中非常实用因为它不需要重新训练模型就能调节“稳定度”和“表现力”的平衡。10.5 关注数据版权与授权边界音乐生成项目的训练数据授权问题不能忽视。使用公开 MIDI 数据集时要确认数据集的授权协议是否允许训练和商用。如果涉及真实演奏家的录音还需要考虑表演者权益。建议建立数据来源清单标注每条数据的授权类型在生产环境上线前做版权审查。11. 总结这篇文章从问题出发梳理了符号音乐生成里一个容易被忽略的维度表演时序。我们看到了传统 MIDI-Like 和 REMI 方案在表达“人的节奏感”上的天然短板也拆解了 Performance-Timed Music Tokens 的核心思路在结构化音乐 token 之外增加起始偏移和时值偏移让 LLM 在生成时有机会直接输出表演层面的细节。Agogic 的价值不在于提出了一个新的模型结构而在于它重新审视了最底层的 token 设计。对于做 AI 音乐应用和 LLM 应用开发的读者来说这是一个非常值得记住的视角当生成结果不够好时先检查表征再检查模型。如果你想往这个方向继续深入建议下一步做三件事找一批真实演奏 MIDI写一个统计脚本统计 ONSET_DEV 和 DUR_DEV 的分布对“人的演奏偏差”建立直觉。用文中极简 tokenizer 跑通 MIDI → token → MIDI 的闭环确认偏移信息没有在转换过程中丢损。在小型 LLM 上做一次对比实验同一个模型分别用传统 token 和带表演时序的 token 训练用 AB 评测验证效果差异。真正做到第三步你会对“表征设计决定生成质量上限”这句话有非常切身的体会。