简介:面向AI音乐创作开发者的实战指南,聚焦DeepSeek与MIDI技术的融合应用,系统讲解从MIDI数据采集、清洗、特征提取,到模型架构设计、训练调优,再到音乐参数生成与MIDI文件输出的完整链路。文档共26页,以“理论—预处理—建模—生成—评估—应用”为脉络,详细介绍了MIDI文件结构与解析、音符/节奏/和弦特征提取、DeepSeek输入所需的文本化表示与分词方法,并给出训练循环、超参数调优、损失函数选择等关键实现细节,以及游戏配乐、影视配乐、个性化音乐推荐等多个实战案例;同时覆盖环境搭建、模型加载、输入处理与推理流程,附有可迁移的完整代码示例,各章节层层递进,便于按需查阅。资源为单个PDF文件,约1.97MB,目录完整清晰,目前已吸引377人学习下载,适合具备一定编程基础、希望借助AI提升作曲效率的开发者参考,帮助快速掌握AI作曲的实操方法。
1. AI作曲实战:DeepSeek+MIDI这条路线,值得认真做
AI作曲实战这个标题,最容易让人误会的地方在于:它既不需要你会乐理,也不需要你懂音频合成,但你必须先接受一个反直觉的结论:DeepSeek这类大模型生成的其实是乐谱文本,真正让音符发声的是MIDI文件。MIDI不直接发音,它只记录“什么时候、用什么音高、以多大力气、弹多久”。恰恰是这种“不发音”的属性,让AI作曲变得可控——你可以换音色、改速度、抽走一段旋律重新编曲,而不是抱着一整段不可编辑的音频干瞪眼。对做游戏音频、短视频配乐、编曲Demo的人来说,这是一条成本最低、最容易被验证的落地路线:用DeepSeek写出结构和乐理正确的乐谱,再转成MIDI,剩下的交给DAW去渲染。
2. 先把AI作曲的工程链路对齐:DeepSeek负责作曲,MIDI负责记谱
2.1 为什么不干脆让AI直接出音频
你如果让DeepSeek直接“生成一段音频”,它做不到,它是文本模型,只能输出文本。市面上那些AI音乐产品(比如Suno)走的是“文本生成音频”的闭源链路,你拿不到中间产物,也就没法在吉他换钢琴、把120BPM改成90BPM、把副歌剪掉两小节。用DeepSeek+MIDI的方案,本质上是把“创作”和“渲染”拆开:创作交给大模型、渲染交给DAW或音源库。
这个拆法的好处有三个。第一,可控性:MIDI里的每个音符都可以单独编辑,错了就改一个数字,不用整段重录。第二,可复现性:同一个提示词、同一个seed,能得到完全相同的结果,这对批量生成配乐素材很重要。第三,成本极低:一次请求生成几十个音符也就是几K的token量,比音频生成模型动辄按秒计费便宜得多。代价是你需要自己做一次“乐谱文本→MIDI文件”的转换,这也是这个标题里最核心的工程步骤,后面几章会详细展开。
2.2 MIDI到底在记录什么
MIDI文件(SMF,Standard MIDI File)本质上是一串带时间戳的事件流。你不需要把每个字节都背下来,但需要理解四类核心事件:
| 事件类型 | 作用 | 常见参数 |
|---|---|---|
| Note On | 音符开始发声 | 通道、音高(0-127)、力度(0-127) |
| Note Off | 音符结束 | 通道、音高、释放力度 |
| Program Change | 切换乐器音色 | 通道、音色编号(对应GM音色表) |
| Control Change | 控制表情或效果 | 控制器编号(如CC1调制轮、CC7主音量) |
音高编号和钢琴键的对应关系是:中央C(C4)= 60,往上一个半音加1,A4 = 69。力度(Velocity)则决定这个音弹得多响。理解这四类事件之后,你就知道“AI作曲”实际上要产出什么——无非是一串按时间排列的“音高+起始时间+持续时间+力度”四元组。怎么让大模型稳定输出这组数据,是第3章要解决的问题。
2.3 DeepSeek做这件事的定位:乐理助手,不是音频引擎
DeepSeek真正擅长的是音乐理论知识——调式、和弦进行、终止式、节奏型。它见过海量乐谱文本和乐理教程,能写出结构完整的旋律和和声编排。所以正确的用法是把它当作“一个懂乐理的搭档”,而不是“一键出歌机器”。你需要用提示词约束它输出结构化乐谱,而不是让它自由发挥一段散文式描述。
这里我最常用的是DeepSeek的API接口,底模用deepseek-chat就够,不需要本地部署。原因后面第5章会细说,根本问题是显存和推理速度。API调用方式跟OpenAI SDK兼容,最小调用代码长这样:
import os from openai import OpenAI client = OpenAI( api_key=os.getenv("DEEPSEEK_API_KEY"), base_url="https://api.deepseek.com" ) resp = client.chat.completions.create( model="deepseek-chat", messages=[ {"role": "system", "content": "你是资深的作曲编曲助手,只输出结构化的乐谱文本。"}, {"role": "user", "content": "写一段4/4拍、C大调、8小节的钢琴旋律,速度90BPM。"} ], temperature=0.8, max_tokens=1024, # seed=None # 不固定seed时每次结果都不同 ) print(resp.choices[0].message.content)这段代码的思路是:先用system角色锁死输出边界,再用user角色把创作要求说清楚。base_url必须指向DeepSeek的服务地址,模型名用deepseek-chat,这个对应的是公开的对话模型,费用和上下文长度以官网说明为准。温度参数temperature=0.8是我在“稳定”和“创意”之间取的折中,低于0.4旋律容易重复,高于1.2结构会散。max_tokens=1024对一段8小节旋律绰绰有余。
注意:
seed参数在DeepSeek API上不一定每次都能精确复现,但固定种子能显著缩小随机范围。要精确复现同一段旋律,最可靠的做法是把你满意的那段乐谱文本保存下来,不做二次生成。
2.4 别急着挂Agent或Harness
看到“AI作曲”这个需求,有人第一反应是上Agent编排框架,给大模型挂一堆工具——一个智能体负责写旋律、一个负责配和弦、一个负责导出MIDI。我在这个标题下的实践经验是:过度设计。作曲这个任务目前的核心瓶颈不在“多智能体协作”,而在于“单次生成结果的正确性”。先让DeepSeek在一次对话里稳定输出一份能通过解析的乐谱,比什么都重要。等你真的碰到“需要先听完第一段再决定第二段怎么写”的场景,再考虑用harness之类的东西做多轮编排也不迟。现在这一章,先把链路跑通。
3. 乐谱格式选型:DeepSeek输出“MiniABC”,脚本负责转MIDI
3.1 为什么不让DeepSeek直接输出MIDI二进制
最自然的想法是“让DeepSeek直接生成MIDI文件”,但这条路走不通。MIDI文件是二进制格式,大模型输出文本时很容易产生幻觉,直接生成二进制字节串几乎必然损坏。更理智的做法是:让DeepSeek输出一种“人可读、结构严格、容易解析”的乐谱文本。
我选了两种格式:第一种叫MiniABC,借鉴了ABC记谱法的行头风格,但音符部分改用“音名+八度数字+时值数字”的显式记法。它最大的优点是对大模型友好——音符信息一目了然,不会像标准ABC那样出现大量省略规则和音高歧义。第二种是JSON音符数组,适合需要程序化处理精确节奏的场景,但提示词写起来更啰嗦。日常首选MiniABC。
3.2 MiniABC的语法约定
一份MiniABC乐谱长这样:
X:1 T:demo M:4/4 L:1/4 K:C C4 D4 E4 G4 | A4 G4 E4 D4 | C2 E2 D2 F2 | G2 G4 z2 |逐行解释:X:是曲子编号,T:是标题,M:是拍号,L:是默认音符时值,K:是调号。音符部分我从第6行开始写,C4表示中央C(MIDI 60),D4表示中央D(62),数字4表示四分音符;C2是二分音符;G8是八分音符;竖线|是小节线,解析时忽略;z2表示二分休止符;[CEG]4表示四分音符的和弦,同时发声。八度处理用数字直接标,C4是中央C,C5就是高一个八度,C3是低一个八度。
提示:MiniABC不是标准ABC,只是一个我为了绕开大模型幻觉设计的简化方言。它的价值在于“让DeepSeek能稳定输出,让脚本好解析”。如果你之后要跟其他音乐软件互通,建议用Python把MiniABC转成标准MIDI再导入。
3.3 解析MiniABC到MIDI:mido方案
下面这个脚本用mido库把MiniABC文本转成MIDI文件,是整套流程的承重墙。
import re from mido import MidiFile, MidiTrack, Message, MetaMessage NOTE_MAP = { 'C': 0, 'D': 2, 'E': 4, 'F': 5, 'G': 7, 'A': 9, 'B': 11 } def note_to_midi(note_str: str) -> int: """把 C4 这样的记号转成 MIDI 音高编号,C4=60。""" if note_str == 'z': return None m = re.match(r'^([A-G])(\d)$', note_str) if not m: raise ValueError(f"非法音符: {note_str}") letter, octave = m.group(1), int(m.group(2)) # C4 是中央C(60),C3 是 48,每升降八度差12 return NOTE_MAP[letter] + (octave + 1) * 12 def note_to_ticks(note_str: str, ticks_per_beat: int) -> int: """把时值数字转成 MIDI tick 数。""" if note_str == 'z': return ticks_per_beat * 2 # z2 由调用方处理 m = re.search(r'(\d+)$', note_str) if not m: return ticks_per_beat # 缺省当四分音符 return ticks_per_beat * 4 // int(m.group(1)) def miniabc_to_midi(text: str, output_path: str, bpm: int = 90): ticks_per_beat = 480 mid = MidiFile(ticks_per_beat=ticks_per_beat) track = MidiTrack() mid.tracks.append(track) # 设置速度和拍号 track.append(MetaMessage('set_tempo', tempo=int(60_000_000 / bpm), time=0)) track.append(MetaMessage('time_signature', numerator=4, denominator=4, time=0)) current_tick = 0 lines = text.strip().splitlines() for line in lines: line = line.strip() if not line or line[0] in 'XTLMK': continue # 音符区:处理和弦与单音 tokens = re.findall(r'\[[A-Gz0-9]+\]?\d*|[A-Gz]\d*', line) for token in tokens: if token == '|': continue if token.startswith('['): # 和弦,例如 [CEG]4 inner = re.search(r'\[([A-G]+)\](\d*)', token) notes_str = inner.group(1) dur_str = inner.group(2) or '4' duration = ticks_per_beat * 4 // int(dur_str) for ch in notes_str: midi_note = note_to_midi(ch + '4') # 和弦默认在中央八度 track.append(Message('note_on', note=midi_note, velocity=72, time=current_tick if midi_note else 0)) track.append(Message('note_off', note=midi_note, velocity=72, time=duration)) current_tick = 0 else: note_str = token if note_str == 'z': duration = ticks_per_beat * 4 // int(token[1:] or '4') current_tick += duration continue midi_note = note_to_midi(note_str) duration = note_to_ticks(note_str, ticks_per_beat) if midi_note is not None: track.append(Message('note_on', note=midi_note, velocity=72, time=0)) track.append(Message('note_off', note=midi_note, velocity=72, time=duration)) else: current_tick += duration mid.save(output_path) print(f"已保存: {output_path}")这段代码的核心逻辑分三步。第一步,用正则把每行音符区拆成独立token,支持单音、和弦和休止符。第二步,把C4这类音高记号映射到MIDI音高编号,这里的关键是(octave + 1) * 12这个公式——C4对应 (4+1)*12=60,C3对应48,保证中央C的映射正确。第三步,把时值数字换算成tick数,tick是MIDI内部的时间单位,ticks_per_beat=480表示一个四分音符占480个tick,E8就是480*4//8=240个tick。
这段代码刻意省略了节奏修正逻辑,也就是说,它假设DeepSeek输出的每个小节内时值总和刚好等于拍号要求。现实是它会算错,所以第5章会专门讲怎么应对。第4章先把完整链路跑起来。
3.4 备选方案:JSON音符数组什么时候用
当旋律包含大量切分音、三连音、附点时,MiniABC会变得很难写。比如三连音在MiniABC里没有原生的语法糖,硬写会牺牲可读性。这时候我会切换成JSON方案:
[ {"note": 60, "start": 0, "duration": 480, "velocity": 72}, {"note": 62, "start": 480, "duration": 240, "velocity": 72}, {"note": 64, "start": 720, "duration": 240, "velocity": 80} ]JSON方案的好处是每个音符的时间信息都显式给出,解析逻辑更简单,DeepSeek输出这种结构的鲁棒性也不错。缺点是提示词会长很多,你得把“start和duration都用tick表示,四分音符=480”写清楚。我的实践经验是:80%的旋律场景用MiniABC就够了,遇到诡异节奏再上JSON,不要一开始就搞得过于复杂。
4. 完整跑通一个例子:从一句中文提示词到一首原创MIDI
4.1 提示词模板,直接抄
下面这个模板是我反复调过之后比较稳定的版本,核心是把“音乐风格、结构、调式、节奏”拆成四个约束点,并强制要求输出MiniABC格式:
你是资深作曲编曲助理。请根据以下要求创作一段原创钢琴旋律,并严格按照MiniABC格式输出: - 调式:C大调 - 拍号:4/4 - 速度:90 BPM - 结构:8小节,前4小节为主题,后4小节为主题变化 - 情绪:明亮、舒缓,适合旅行短片 MiniABC输出要求: 1. 第一行X:1,第二行T:标题,第三行M:4/4,第四行L:1/4,第五行K:C 2. 从第六行开始写音符,每行写4小节,小节之间用竖线|分隔 3. 音符格式:音名+八度数字+时值数字,例如C4表示四分音符的中央C,A2表示二分音符的A3 4. 允许使用的音符时值:1、2、4、8,分别对应全音符、二分音符、四分音符、八分音符 5. 休止符用z加时值表示,例如z4 6. 每个小节音符时值总和必须等于4个四分音符 7. 只用MiniABC输出,不要附加任何解释关键点在第6条“每个小节音符时值总和必须等于4个四分音符”,这是语义层面的约束,也是DeepSeek最容易出错的地方。没有这句话,它输出的节奏会对不上拍号,下文会详谈。另外,如果你想要的和弦是“带伴奏的主旋律”,要在提示词里显式说明“左手低音声部每小节一个根音,用C2/G2这种时值较长的音符”,否则DeepSeek会默认只写单音旋律。
4.2 用脚本把“API调用→MiniABC→MIDI”串起来
这一节把第2章的API调用和第3章的解析器合并成一个完整脚本,一次执行直接产出MIDI文件:
import os from openai import OpenAI from miniabc import miniabc_to_midi # 假设你把上一章的解析器存成 miniabc.py PROMPT = """(这里放4.1节的完整提示词)""" client = OpenAI( api_key=os.getenv("DEEPSEEK_API_KEY"), base_url="https://api.deepseek.com" ) resp = client.chat.completions.create( model="deepseek-chat", messages=[{"role": "user", "content": PROMPT}], temperature=0.8, max_tokens=2048, ) abc_text = resp.choices[0].message.content # 把DeepSeek可能包在代码块里的内容剥出来 if "```" in abc_text: abc_text = abc_text.split("```")[1] if abc_text.startswith("miniabc"): abc_text = abc_text[7:].strip() abc_text = abc_text.strip() with open("output/song.abc", "w", encoding="utf-8") as f: f.write(abc_text) miniabc_to_midi(abc_text, "output/song.mid", bpm=90) print("完成:output/song.mid")这段脚本的工程要点有两个。第一个是“代码块剥离”:DeepSeek有时会把MiniABC文本包在Markdown代码块里,如果不剥离就传给解析器,解析器会在X:1之前看见三个反引号,直接报错。我这边用if "```" in abc_text做了防御。第二个是中间产物落盘:song.abc这个文本文件是后悔药——如果在MIDI试听环节发现问题,你可以直接改这个文本重新生成,不用再花一次API调用。
4.3 拿到MIDI之后的验证:转简谱做快速预览
MIDI文件靠肉眼看不出问题,最直接的验证方式是转成简谱文本打印出来,快速检查有没有离谱的音高跳跃或节奏错误:
from mido import MidiFile def midi_to_jianpu(mid_path: str): mid = MidiFile(mid_path) notes = [] for track in mid.tracks: tick = 0 for msg in track: tick += msg.time if msg.type == 'note_on' and msg.velocity > 0: # MIDI 60 = C4,转成简谱相对唱名 semitone = msg.note - 60 scale_degree = (semitone % 12) names = ['1', '#1', '2', '#2', '3', '4', '#4', '5', '#5', '6', '#6', '7'] note_name = names[scale_degree] octave_offset = semitone // 12 if octave_offset > 0: note_name += "'" * octave_offset elif octave_offset < 0: note_name += "," * (-octave_offset) notes.append((tick, note_name)) for t, n in notes: print(f"{t:6d} {n}") midi_to_jianpu("output/song.mid")这段输出的效果是:每一行“tick值+简谱唱名”,1是do,3是mi,带'是高音,带,是低音。你不需要懂五线谱,扫一眼旋律走向就能发现大问题——比如相邻两个音符差了12个半音,那大概率是DeepSeek把八度写错了。如果手边有DAW小样或音源库,把这个MIDI拖进去换钢琴音色听一遍,是最快的最终验证。
4.4 三个必调参数的优先级
| 参数 | 优先度 | 作用 | 我的经验取值 |
|---|---|---|---|
| temperature | 最高 | 控制旋律的随机程度 | 0.7-0.9,追求稳定用0.6 |
| max_tokens | 高 | 防止长曲子被截断 | 8小节2048,16小节4096 |
| 提示词中的“小节时值约束” | 高 | 让拍号正确 | 必须显式写“每小节时值总和=4” |
max_tokens这个参数容易被忽略。DeepSeek的输出长度有限,如果生成16小节但max_tokens只给1024,旋律会在第10小节戛然而止,不会向你报错,你只会得到一份残缺乐谱。我的经验是:8小节给2048,16小节给4096,留足余量。
5. AI作曲的6个翻车现场:常见问题与排查
5.1 API报错:messages tool calls need immediate results
现象:代码跑到client.chat.completions.create这一步,控制台抛出以messages tool calls need immediate results开头的一长串错误,连续几次都一样。
原因:这个标题下,最常见的触发场景不是曲子本身,而是你在同一个messages数组里混入了工具调用消息但没提供对应的工具结果。如果你把上一轮带tool_calls的助手消息原样塞进下一轮请求,而缺失了role: "tool"的返回消息,API会直接拒绝后续生成。DeepSeek在对话接口上对消息序列校验很严格。
解决:调用前过滤掉消息历史里所有finish_reason="tool_calls"的助手消息,或者补齐对应的工具结果。就这个项目而言,根本不需要工具调用,把多轮对话拆成每次独立的API请求,只保留system和user两条消息,是最干净的规避方式。
5.2 乐谱时值总和总对不上拍号
现象:生成的MIDI导入DAW后,有的小节长了半拍,有的小节短了一拍,整段旋律的节拍像喝了酒。
原因:DeepSeek在生成过程中对数字计算不敏感,它可能在小节里写了一个二分音符加两个四分音符,再加一个八分音符,合计3.5拍,而拍号是4/4。提示词里第6条约束只能降低出错概率,不能彻底消除。
解决:第一道防线是在解析器里加时值校验:每个小节音符时值求和,不等于拍号时就打印警告。第二道防线是让DeepSeek做自检——在提示词末尾加“请检查每个小节的时值总和是否为4拍,如有错误立即修正”。第三道防线是人工检查转好的简谱,跳过那些时值异常的段落,手动改song.abc文本里的音符时值,重新生成MIDI。最后这步往往比重新调API更快。
5.3 旋律能听,但和声终止式总是“悬空”
现象:旋律走向没问题,但结尾缺乏结束感,像是话说到一半被人打断。
原因:DeepSeek理解的“结束”可能是音符偏高或者停在非主音上。在C大调里,最稳的终止式是停在C和弦的根音或三音上,但大模型可能会把这个位置交给E、G或者B,制造出一种未解决的不安感。
解决:在提示词里显式指定终止式,例如“最后两小节使用V-I终止式,落在C4上”。V-I终止式就是G和弦接C和弦,这是和声学里最经典的完满终止。指定了之后,绝大多数情况下结尾会变得干净利落。
5.4 力度变化像过山车,或像电子节拍器
现象:MIDI播放时,有的音符音量突然大得吓人,有的软得像被棉花捂住,整段听起来忽大忽小。
原因:大模型生成的力度值容易走极端,它不知道“弱起”应该给多轻、“重音”应该给多重。而这会让音源库渲染出来的声音非常不稳定。
解决:解析时对velocity做归一化处理。我的做法是把所有力度值约束在56到88之间,旋律重音位给80以上,经过音给60左右。公式很简单:velocity = max(56, min(88, note_velocity))。这个区间内大部分音源都能保持自然响应,不会出现爆音或断音。
5.5 风格越来越“AI味”,几首曲子听着都像一个人写的
现象:不同提示词生成的旋律,听完之后总觉得有相似的味道,尤其是节奏型和音程走向。
原因:这是temperature低和提示词里的风格词太泛的双重结果。你写“流行风格”,DeepSeek会调到它见过最常出现的流行旋律模板,本质上是在复读高频模式。
解决:把风格描述从“形容词”改成“音型约束”。比如“使用附点节奏”“旋律以三度跳进为主”“避免连续三次同方向级进”,这类乐理层面的约束会让旋律跳出模板化。另外把温度调到0.9以上,会给随机性留出空间。但注意温度太高结构会松散,我的上限是1.1。
5.6 本地部署DeepSeek反而卡在显存和速度上
现象:有人为了省钱或数据隔离,把DeepSeek系列的开源权重拉到本地,结果生成一段8小节的旋律要等两分钟,女生笔记本风扇狂转。
原因:作曲任务实际生成几百个token,用API一次往返也就几秒钟。本地部署小模型质量不够,大模型显存要求高,推理速度又慢,属于两头不讨好。
解决:这标题下直接用API接口,省下的时间用来调提示词比什么都值。如果你确实有离网需求,优先选DeepSeek蒸馏系列里7B/14B级别的量化版本,4bit量化后显存占用能压到8GB以内。但你需要接受一个现实:小模型的乐理能力和格式遵循度会明显下降,MiniABC的报错率会翻倍,得不偿失。
6. 进阶用法:把单旋律变成多轨编曲,以及我的验证习惯
当单旋律能稳定产出之后,下一步是让DeepSeek帮你编曲。我一般会让它分别输出三个声部的MiniABC:主旋律、和弦铺垫、低音根音,然后通过脚本合并成多轨MIDI。提示词里加一句“主旋律用八分音符,和弦声部每小节一个柱式和弦,低音声部每小节一个二分音符根音”,就能得到层次分明的三轨结构。合并时给每个音轨分配不同的MIDI通道和Program Change音色编号,钢琴配80、贝斯配32、弦乐配48,一轨抒情钢琴曲立刻变成有空间感的小编制乐队。
但脚本能自动化的始终有限,我的验证习惯永远是“先机器检查,再人耳听”。机器检查分三步:读回MIDI文件看音符总数是否和预期一致;转简谱扫一眼音域有没有超过C3-C6;检查有没有单音符长达两拍以上的“长音断层”。人耳听则固定在MIDI播放器里循环三遍——第一遍听旋律走向,第二遍听节奏稳定,第三遍闭眼检查情绪是否跟提示词的方向一致。
这个标题做下来,我最深的一条教训是:AI作曲90%的时间不是在“作曲”,而是在调提示词的格式约束和解析器的防御逻辑。DeepSeek永远不会在第一次生成时就给你一份完美的乐谱,但只要你把“格式约束前置、错误处理兜底”,它产出的旋律骨架确实能让普通人跨过从0到1的创作门槛。希望帮到你。
本文还有配套的精品资源,点击获取