简介:这是一份面向技术开发人员与AI音乐创作者的实战型PDF文档,讲解如何借助DeepSeek模型与MIDI数据处理实现原创音乐生成。内容从AI作曲技术背景、DeepSeek模型特点、MIDI文件结构与解析入手,逐步覆盖数据预处理、模型架构设计、训练调优、音乐生成流程及完整代码示例,并给出游戏配乐、影视配乐等应用案例,适合对AI音乐创作感兴趣的开发者系统学习。资源压缩包为单个PDF文件,大小约1.97MB,共26页,目录完整、图文与代码展示清晰,便于按章节查阅。目前已有378人学习浏览,文档结合理论与实践,既能帮助读者理解AI作曲的基本原理,也能直接参照其中的代码和流程进行工程实现,有效提升音乐生成项目的落地效率。
1. DeepSeek 生成 MIDI 而不是完整音频:AI 作曲实战的第一个判断
把结论放在前面:想让 DeepSeek 做 AI 作曲,最稳的做法不是让它"生成一个音频文件",而是让它按约定格式输出 MIDI 音符事件——音高、起始时间、时值、力度——随后你在本地用合成器把这些事件渲染成声音。这套链路的最大好处是生成结果可编辑、可量化、可搬进 DAW 重新配器,而不是一个改不了的黑匣子。这篇笔记会从提示词模板讲到 .mid 落地,再讲到合成音频的完整路径,全程覆盖我在实际跑这个方向时踩过的坑。适合想给 AI 工具链加入作曲能力的开发者,也适合从零起步、想搞清楚 MIDI 和音频差在哪的音乐爱好者。你只需要一台能跑 Python 的电脑,外加一个可用的 DeepSeek API Key。
2. MIDI 为什么接得住 DeepSeek 的输出:原理与四件套环境准备
2.1 MIDI 是事件序列不是声谱图:面向 LLM 的格式优势
MIDI 文件本质上不是一个"声音文件",它是一张时刻表:某时刻按下哪个键、力度多大、持续多久、何时松开、变速和踏板变化怎么走。整个信息量远小于等长音频,以 120BPM、30 秒的钢琴独奏为例,如果用 44.1kHz 16bit 采样,音频要大约 5MB;同一段内容写成 MIDI,常常不到 10KB。这个差距直接决定了模型输出的可行性——LLM 生成的 token 是离散的、有穷的,音符事件天然满足这一约束,而波形采样是连续浮点,很难让语言模型稳定生成。
所以做"文本生成音乐"时,主流路线不是让大模型直接吐音频,而是先生成 MIDI / MusicXML 这类结构化乐谱,再交给合成器。DeepSeek 作为一个文本模型,真正擅长的是把"乐理要求"翻译成一串有规则的 JSON 事件;把 MIDI 的 note_on、note_off、velocity 当成类似代码结构来理解,它就不容易跑偏。这也是为什么在这条链路里,MIDI 不是妥协,而是最适合语言模型出力的中间表示。
2.2 DeepSeek 在这一环节的真正职责:自然语言翻译成结构化 MIDI 指令
直接说结论:DeepSeek 不适合给你"凭空哼一段还带混响的成品",它适合给你"高信息密度的乐谱草案"。你把任务想成——你说自然语言,它说 MIDI 事件。
我在实际工作中会把 DeepSeek 当作一个"乐理翻译层"来用:给它固定调式、和弦进行、节奏密度、音区范围,它输出符合这些约束的音符数组。核心难点不是模型能不能作曲,而是提示词把"音乐常识"变成"数据契约"。比如不告诉它"C 大调、4/4 拍、BPM=100",它很可能按自己的统计习惯输出一段调性模糊的旋律;你告诉它"从 C4 到 C6"但没限定"跳进不超过六度",它会把两个八度的碎片全部塞进来。
另一个常见误区是:DeepSeek 只能写文本,不能生成 MIDI 文件。这话对,但不需要补——文本模型本来就是让你输出文本格式的,实际方案是让它输出 JSON,你在本地解析。无论你是直接调 DeepSeek 官方 API,还是把模型接进自己的代码生成 harness(工具链外壳),最终要的都是同一件事:一段稳定、可解析的 JSON 音符流。
2.3 本地依赖与 API 配置:OpenAI 兼容接口的一次性准备
DeepSeek 的 API 走 OpenAI 兼容协议,所以 Python 侧只需要 openai 客户端库和 MIDI 处理库。安装命令如下:
pip install openai midomido 负责 MIDI 文件的底层读写,不依赖任何 DAW;openai 库负责和 DeepSeek API 通信。如果你换成其他 OpenAI 兼容的服务,代码基本不用改。
拿到 Key 之后,通常先在终端导出环境变量,避免密钥写死进代码仓库:
export DEEPSEEK_API_KEY=sk-xxxxxxxx之后初始化客户端,注意 base_url 要指到 DeepSeek 的接口地址:
import os from openai import OpenAI client = OpenAI( api_key=os.environ["DEEPSEEK_API_KEY"], base_url="https://api.deepseek.com" ) resp = client.chat.completions.create( model="deepseek-chat", messages=[{"role": "user", "content": "用一句话介绍 MIDI"}], max_tokens=100 ) print(resp.choices[0].message.content)这里的 model 用 deepseek-chat 即可完成音乐 JSON 生成任务;如果你想做本地化部署,也可以用 vLLM 在内网把 DeepSeek 服务拉起,base_url 换成你自己的地址,请求结构完全一样。对于普通 MIDI 生成来说,云端 API 单价很低,一次生成一首带有几十个音符的小曲,大约在几分钱到一毛钱人民币的量级,本地部署主要是为了隐私和调试频率。建议第一次先跑通云端 API,确认输出效果后再决定是否折腾内网推理服务。如果响应时间比较长,优先检查网络和 max_tokens,不要一上来就调温度。
3. 用 DeepSeek 生成 MIDI 的最小可运行流程:从提示词模板到 .mid 文件
3.1 提示词模板:一首钢琴流行曲的完整乐理约束
这个方案最关键的部分在提示词。给 DeepSeek 的提示词要写三件事:音乐风格、乐理规格、JSON 格式。缺任何一件,产出的稳定性都会明显下降。
你现在是一个 MIDI 作曲助手。请按下面的乐理规格生成一段原创钢琴流行旋律,只输出 JSON,不要输出任何解释文字。 规格: - 调性:C 大调 - 拍号:4/4,BPM = 100 - 和弦进行:C - G - Am - F,每个和弦占 2 小节,共 8 小节 - 音域:C4(MIDI 60)~ C6(MIDI 84) - 风格:钢琴流行,主旋律与简单分解和弦伴奏 JSON 格式: { "notes": [ {"pitch": 60, "start": 0.0, "duration": 1.0, "velocity": 85} ] } 要求: - start 和 duration 都以四分之一拍(四分音符)为单位 - 每个四分音符内最多出现 2 个音符 - 旋律跨小节时优先落在和弦进行标记的重拍上 - 相邻旋律音跳进不超过六度,除非该音是和弦分解音 - 加入 2~3 处休止,避免密密麻麻看到这个模板你可能已经意识到:乐理约束写清楚之后,模型自由发挥的余地其实小了很多。这是好事——只写"帮我写首歌",模型大概率输出一段在统计上平均、在听觉上平庸的东西。和弦进行、音域和节拍密度才是它值得被信任的脚手架。
提示词里还有两个细节容易被忽略。一是"每个四分音符内最多出现 2 个音符",这防止旋律在极短时值上堆成噪音;二是"加入 2~3 处休止",这保证了句子呼吸感。实际生成时如果旋律太平滑,可以把温度调到 0.9 左右;如果完全失控、音符散乱,先把 BPM 和和弦进行固定回来。
3.2 端到端代码:调用 API、解析 JSON、落盘 MIDI 事件
拿到 DeepSeek 返回的 JSON 之后,下一步就是转成标准 MIDI 文件。下面这段代码可以直接存成deepseek_to_midi.py运行:
import os import json import mido from openai import OpenAI from mido import MidiFile, MidiTrack, Message, MetaMessage PROMPT = """ (把上面 3.1 节的提示词原文贴到这里) """ def request_song(): client = OpenAI( api_key=os.environ["DEEPSEEK_API_KEY"], base_url="https://api.deepseek.com" ) resp = client.chat.completions.create( model="deepseek-chat", messages=[{"role": "user", "content": PROMPT}], response_format={"type": "json_object"}, temperature=0.9, max_tokens=2048 ) return json.loads(resp.choices[0].message.content) def build_midi(song, output_path, bpm=100, tpb=480): midi = MidiFile(ticks_per_beat=tpb) track = MidiTrack() midi.tracks.append(track) track.append(MetaMessage('set_tempo', tempo=mido.bpm2tempo(bpm))) track.append(MetaMessage('time_signature', numerator=4, denominator=4)) events = [] for item in song["notes"]: start = int(round(item["start"] * tpb)) end = int(round((item["start"] + item["duration"]) * tpb)) events.append((start, 1, 'note_on', item["pitch"], item["velocity"])) events.append((end, 0, 'note_off', item["pitch"], 0)) # 同一 tick 上先把 note_off 排在前面,避免旧 off 干扰新 on events.sort(key=lambda e: (e[0], e[1])) last = 0 for tick, _, kind, pitch, vel in events: delta = max(0, tick - last) track.append(Message(kind, note=pitch, velocity=vel, time=delta)) last = tick midi.save(output_path) if __name__ == "__main__": song = request_song() build_midi(song, "original_idea.mid", bpm=100) print("saved: original_idea.mid, notes:", len(song["notes"]))运行逻辑分三段:request_song负责调用 DeepSeek 并强制输出 JSON;build_midi把每个音符的 start/duration 先转成绝对 tick,再把全部事件按时间轴排成 delta-time 序列写入 MIDI 轨道;最后保存文件。tpb=480是每拍 tick 数,480 是 MIDI 常见精度,换算成 16 分音符正好是 120 tick,够用且兼容性好。
这里有三个参数值得解释。response_format={"type": "json_object"}是确保返回纯 JSON 的关键,没它模型可能夹带解释文字;temperature=0.9是给旋律生成留一点统计随机性,太低了旋律容易重复,太高了容易散架;max_tokens=2048对一首八小节钢琴曲足够,生成更长的曲子可以提到 4096。把 BPM 写死在提示词里,又在 build_midi 里以 bpm=100 传给 mido,两者保持一致才不会出现"提示词说 100,文件实际用 120"的错位。
3.3 三个可调参数:BPM、力度、音区的设置边界
MIDI 生成效果的差异,很多时候就出在你把哪个参数当成"提示词约束"、哪个参数留给"后期渲染"。
| 参数 | 控制层 | 推荐初值 | 越界后果 |
|---|---|---|---|
| BPM | 提示词 + build_midi | 100 | 与 DAW 不同步,整曲拉长或压缩 |
| velocity | 提示词给范围 | 主 85-100 / 伴 60-75 | 力度扁平,听感机械 |
| 音域 | 提示词 | C4-C6 | 音域过宽,跳进失控 |
BPM:建议在提示词阶段固定。模型在生成音符的 start 和 duration 时会以拍为单位计算,如果 BPM 不在提示词里固定,模型会自己脑补一个速度,输出结果与你的要求脱节。后期用 DAW 随时能改快慢,但源头模糊会让大量训练样本的平均错觉渗入生成。
力度(velocity):提示词里只给范围即可,比如"主旋律 85~100、伴奏 60~75"。最终文件里的具体数值靠后期渲染工具按层分配。实际经验是,AI 输出的力度往往过于均匀,你不加约束,它会默认一批清一色 80 左右的音,听感非常机械。
音区:限制越死,调性越稳。C4~C6 适合钢琴流行主旋律;如果做低音伴奏,把范围缩到 C2~C4。注意,不要在同一提示词里同时要求"音域很宽"和"跳进很小",这两者天然冲突,模型只能顾一头。先让窄音域跑通,再逐步放宽,比一次给满自由度更容易得到能用的曲子。
4. AI 作曲实战的常见问题排查:格式、乐感与拍子都在这份清单里
这一章写的每一条都是从实际操作里捞出来的,按"现象 → 原因 → 解决"给你过一遍。
4.1 生成的 .mid 在 DAW 里打不开
现象:把 DeepSeek 生成的 JSON 转出.mid后,拖进 Cubase / FL Studio / Reaper 时软件报错,或者导入后音轨里全是空事件。
原因:最常见的是你把数据写进了带文本内容的文件,而不是标准二进制 MIDI。另一个高频问题是事件顺序异常——轨道里只有 note_on 没有匹配的 note_off,DAW 会认为某个音符永远不结束,整个后续时间轴都乱了。
解决:先用 Python 自检一遍,不要直接拖进 DAW:
midi = mido.MidiFile("original_idea.mid") for i, track in enumerate(midi.tracks): print(f"Track {i}: {len(track)} events") for msg in track: print(msg)如果能看到成对的 note_on / note_off,而且 delta time 非负,文件结构基本没问题。然后把文件扩展名、保存路径都检查掉,尤其注意误存成.mid.txt的情况。如果确实缺失 note_off,在 build_midi 里对每个音符强制生成 off 事件即可(上面的代码已经做了);再把 tpb 统一成 480,多数 DAW 对 480 兼容性最好。
4.2 旋律像随机数:原因几乎都在提示词漏了乐理约束
现象:生成的旋律在音高跨度上乱跳,一会儿高音区一会儿低音区,完全听不出主次关系。
原因:没有在提示词里给"旋律骨架"。LLM 是根据 token 概率生成音高的,不给定可依循的和弦进行、不给重拍落点约束,它就会平均地使用每一个音阶音,听起来就是随机音符。这不是模型不行,而是约束不够。
解决:把提示词里的"和弦进行"和"重拍优先落在和弦音"当作强制条件。比如 C 大调 4/4 拍下,第 1 拍最好落在 C-E-G 中的某个音,第 3 拍可以落经过音,经过音和下一音之间的音程尽量不超过二度。你也可以在提示词里补一句"先列出每小节的重拍音高,再生成旋律",引导模型在结构层先思考,而不是直接逐音生成。
4.3 音符时长对不上拍子:单位约定与量化冲突
现象:在 DAW 导入后,所有音符时长都歪了 50% 上下,四分音符变成八分音符,或整曲拉长了一倍。
原因:提示词里写的是"start 和 duration 以四分音符为单位",但模型在训练文本里见过的 MIDI 输出往往混用"秒"和"拍"。它可能在部分音符上用了秒的单位,部分又用了拍,导致数据内部不一致。这不是代码 bug,而是数据契约没有写死。
解决:两种手段并用。一是提示词里补一句"不要使用秒作为单位,所有数值必须是四分音符的倍数或小数";二是代码侧加一层量化保护,把 start 和 duration 按 0.25 拍取整。配套函数长这样:
def quantize_notes(notes, step=0.25): out = [] for n in notes: start = round(n["start"] / step) * step dur = max(step, round(n["duration"] / step) * step) out.append({"pitch": n["pitch"], "start": start, "duration": dur, "velocity": n["velocity"]}) return out量化步长 0.25 拍相当于 16 分音符,对这个场景够用;如果要保留更自由的散板风格,就把 step 调到 0.125,代价是和声对位的稳定性下降。
4.4 返回的 JSON 解析失败:Markdown 围栏、注释与非法字段
现象:json.loads直接抛 JSONDecodeError,查看 raw 输出发现开头是```json,结尾是```,或者中间夹带了类似// 注释的行。
原因:模型没有严格执行"只输出 JSON",温度偏高时更容易夹带解释性文字。response_format能降低概率但不能完全消除。另一个坑是模型偶尔把 pitch 写成C4而不是60,字符串跑进 JSON 后你的解析器会报类型错误。
解决:在解析前先做一次清洗,把常见的围栏、空白、非 JSON 前缀都剥掉:
import re import json def extract_json(raw): raw = raw.strip() m = re.search(r"```(?:json)?\s*(.*?)\s*```", raw, re.S) if m: raw = m.group(1) try: return json.loads(raw) except json.JSONDecodeError: raw = re.sub(r"//[^\n]*", "", raw) # 去掉行注释再试一次 return json.loads(raw)注意:如果 DeepSeek 版本不支持 response_format 参数,可以先去掉它,再用上面的 extract_json 兜底,成功率依然可观。
加上这个函数后,解析失败率会明显下降。如果仍然失败,把 raw 内容打印出来看,凡是 pitch 字段出现"C4"这类字符串,就在 build_midi 里加一个音符名到数字的映射,C4=60、D4=62 这样写死即可。千万不要用异常去猜模型输出,先看数据再动手。
4.5 渲染出来像廉价电子琴:音色库和动态的锅
现象:MIDI 文件在 DAW 里播放时,像是几百块的电子琴默认音色,声音干、平、没有任何起伏。
原因:MIDI 文件本身不携带音色,最终音色取决于 DAW 或合成器里的 SoundFont / VST。系统默认 GM 音色库往往是最粗糙的那一版,同时 AI 输出的 velocity 集中在 80 附近,缺少力度强弱,听感机械。
解决:换音色库,并在导入 DAW 后对力度做微调。合成器侧方案见下一章;这里先给一个成本极低的做法,在 build_midi 里给 velocity 加一个人性化扰动:
import random note["velocity"] = max(1, min(127, note["velocity"] + random.randint(-6, 6)))这个 ±6 的抖动不会改变整体动态层,但听感会从"全键盘一个力度"变成"有一点人味"。想要更精细,在 DAW 中按旋律和伴奏分两条轨,分别给不同力度范围。
5. 把 .mid 渲染成可听的原创音乐:FluidSynth 命令行与 DAW 微调
5.1 用 FluidSynth 把 MIDI 转 WAV 的最小命令
写完.mid后,最直接、可以无脑出结果的渲染路径是 FluidSynth。它能把 MIDI 文件配合一个 SoundFont(SF2 音色库)渲染成 WAV。
fluidsynth -ni /usr/share/sounds/sf2/FluidR3_GM.sf2 \ -F original_idea.wav -r 44100 original_idea.mid各参数含义:-ni是"不交互、一次渲染完退出";-F指定输出 WAV;-r设置采样率,44100 是 CD 标准;最后一个参数是输入 MIDI 文件。如果不加-F,FluidSynth 会进入实时播放模式,没有声音设备时直接报错,所以写脚本时务必带上-F。
FluidSynth 找不到默认音色库时,可以先跑fluidsynth看启动日志,确认它加载了哪个目录下的 SoundFont。没有现成音色库时,从网上找一个通用 GM 音色库放在固定路径,比如~/.soundfonts/,然后在命令里写绝对路径。第一次渲染完成后,打开生成的 WAV 听一遍;如果 WAV 是静音,先检查.mid是不是空的——用第 4.1 节的方式确认事件数量。
5.2 导入 DAW 的微调清单:力度、CC1、踏板与段落
FluidSynth 输出适合快速验证,但要做成真正能用的原创音乐,我建议把.mid导入 DAW 做一轮人工微调。这块与其说是技术,不如说是流程纪律。
第一,把 AI 生成的轨道按声部分离。如果提示词要求的是"主旋律 + 分解和弦伴奏",DeepSeek 返回的 JSON 往往单轨混合。在 DAW 里按音高把 C4 以上的音切到主旋律轨、C4 以下的音切到伴奏轨,再分别分配不同音色(钢琴主旋律、柔和弦乐打底)。很多"AI 味"实际上来自单轨全部堆在一个音色上。
第二,处理力度曲线。选中旋律轨,把 velocity 整体提高到 90~105,伴奏轨压到 50~70,再把每个乐句第一拍的力度加强 5~10%。不要全局调,分轨做才能保留声部层次。
第三,加 CC1 表情和 sustain 踏板。CC1 控制器控制音量表情变化,在 DAW 里画一条缓慢起伏的 CC1 曲线,比静态力度自然得多。钢琴段落开头用踏板,句尾放开,会让 4.1 里那种"廉价电子琴"感觉消失大半。这一步不能在 DeepSeek 侧完成,因为模型感知不到音频实际反馈——只有你是真人,能听出边界在哪。
5.3 选音色库:SF2 与 SoundFont 的几个现实取舍
SF2 是一整包波形采样加音量映射,加载一个就能覆盖钢琴、弦乐、鼓这类 GM 全音色。选音色库时优先看两点:体积和采样层数。16MB 级别的通用库在钢琴上普遍干瘪;500MB 以上的三角钢琴库会好不少,但会拉高磁盘占用量和加载时间。实际工作流里,我一般维护两个库:一个 32MB 的通用 GM 库用于流程验证,一个 200MB 左右的钢琴/弦乐专项库用于最终渲染。
还有一个容易忽略的点:不同 SoundFont 对 velocity 层的响应差异很大,同一个力度在 A 库正常、在 B 库可能过爆。换库后不要偷懒,先把主旋律的力度范围重新听一遍再批量导出。
6. 三个进阶技巧:把 DeepSeek 写的东西从"能听"打磨成"想用"
花过前面那些功夫之后,你手里已经有一条能跑通的 DeepSeek + MIDI 管线。最后给你三个我一直在用的进阶动作。
第一个动作是批量候选加筛选。不要只生成一次,让 DeepSeek 用同一套约束采样 3~5 次,然后用一个简单的打分脚本挑:计算旋律相邻音跳进的平均值(最好在 2~5 度之间)、低音区占比(建议低于 30%)、有效休止比例(5%~20% 为佳)。这种统计筛选不是玄学,它是在对抗 LLM 的概率均匀化——把最接近人类写作习惯的候选挑出来。
第二个动作是"二次投喂"迭代。拿到第一版 MIDI 后不要急着当成品,把它的和弦进行摘出来,连同旋律一起压缩成摘要文本,再让 DeepSeek 写第二版变奏。提示词里加一句"保持和弦进行,但把第一段的旋律装饰音换成邻音,并增加一个上行模进",比直接重写更容易得到和最初版本有关联的素材。
第三个动作是给自己留一版"纯 MIDI 存档"。原始 JSON、中间 .mid、最终 WAV 分目录存好,尤其要标注提示词版本。AI 作曲的产物迁移性很差,过一周你可能已经忘了当初那首曲子的约束是怎么写的;把提示词和结果绑在一起,下一次迭代才有后悔药可吃。
把这个管线跑熟后,你会发现最耗时间的不是 API 调用,而是听、挑、改这三件事。我养成的习惯是:每次投喂提示词前先写死 BPM 与和弦进行,拿到 MIDI 后先花两分钟在 DAW 里分轨、调力度,再决定要不要留下。这个顺序让我少走了很多弯路。希望帮到你。
本文还有配套的精品资源,点击获取