
简介这是一份面向技术开发人员与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 拍、BPM100它很可能按自己的统计习惯输出一段调性模糊的旋律你告诉它从 C4 到 C6但没限定跳进不超过六度它会把两个八度的碎片全部塞进来。另一个常见误区是DeepSeek 只能写文本不能生成 MIDI 文件。这话对但不需要补——文本模型本来就是让你输出文本格式的实际方案是让它输出 JSON你在本地解析。无论你是直接调 DeepSeek 官方 API还是把模型接进自己的代码生成 harness工具链外壳最终要的都是同一件事一段稳定、可解析的 JSON 音符流。2.3 本地依赖与 API 配置OpenAI 兼容接口的一次性准备DeepSeek 的 API 走 OpenAI 兼容协议所以 Python 侧只需要 openai 客户端库和 MIDI 处理库。安装命令如下pip install openai midomido 负责 MIDI 文件的底层读写不依赖任何 DAWopenai 库负责和 DeepSeek API 通信。如果你换成其他 OpenAI 兼容的服务代码基本不用改。拿到 Key 之后通常先在终端导出环境变量避免密钥写死进代码仓库export DEEPSEEK_API_KEYsk-xxxxxxxx之后初始化客户端注意 base_url 要指到 DeepSeek 的接口地址import os from openai import OpenAI client OpenAI( api_keyos.environ[DEEPSEEK_API_KEY], base_urlhttps://api.deepseek.com ) resp client.chat.completions.create( modeldeepseek-chat, messages[{role: user, content: 用一句话介绍 MIDI}], max_tokens100 ) 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/4BPM 100 - 和弦进行C - G - Am - F每个和弦占 2 小节共 8 小节 - 音域C4MIDI 60~ C6MIDI 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_keyos.environ[DEEPSEEK_API_KEY], base_urlhttps://api.deepseek.com ) resp client.chat.completions.create( modeldeepseek-chat, messages[{role: user, content: PROMPT}], response_format{type: json_object}, temperature0.9, max_tokens2048 ) return json.loads(resp.choices[0].message.content) def build_midi(song, output_path, bpm100, tpb480): midi MidiFile(ticks_per_beattpb) track MidiTrack() midi.tracks.append(track) track.append(MetaMessage(set_tempo, tempomido.bpm2tempo(bpm))) track.append(MetaMessage(time_signature, numerator4, denominator4)) 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(keylambda e: (e[0], e[1])) last 0 for tick, _, kind, pitch, vel in events: delta max(0, tick - last) track.append(Message(kind, notepitch, velocityvel, timedelta)) last tick midi.save(output_path) if __name__ __main__: song request_song() build_midi(song, original_idea.mid, bpm100) print(saved: original_idea.mid, notes:, len(song[notes]))运行逻辑分三段request_song负责调用 DeepSeek 并强制输出 JSONbuild_midi把每个音符的 start/duration 先转成绝对 tick再把全部事件按时间轴排成 delta-time 序列写入 MIDI 轨道最后保存文件。tpb480是每拍 tick 数480 是 MIDI 常见精度换算成 16 分音符正好是 120 tick够用且兼容性好。这里有三个参数值得解释。response_format{type: json_object}是确保返回纯 JSON 的关键没它模型可能夹带解释文字temperature0.9是给旋律生成留一点统计随机性太低了旋律容易重复太高了容易散架max_tokens2048对一首八小节钢琴曲足够生成更长的曲子可以提到 4096。把 BPM 写死在提示词里又在 build_midi 里以 bpm100 传给 mido两者保持一致才不会出现提示词说 100文件实际用 120的错位。3.3 三个可调参数BPM、力度、音区的设置边界MIDI 生成效果的差异很多时候就出在你把哪个参数当成提示词约束、哪个参数留给后期渲染。参数控制层推荐初值越界后果BPM提示词 build_midi100与 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_offDAW 会认为某个音符永远不结束整个后续时间轴都乱了。解决先用 Python 自检一遍不要直接拖进 DAWmidi mido.MidiFile(original_idea.mid) for i, track in enumerate(midi.tracks): print(fTrack {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, step0.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 里加一个音符名到数字的映射C460、D462 这样写死即可。千万不要用异常去猜模型输出先看数据再动手。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 文件配合一个 SoundFontSF2 音色库渲染成 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 文件。如果不加-FFluidSynth 会进入实时播放模式没有声音设备时直接报错所以写脚本时务必带上-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 里分轨、调力度再决定要不要留下。这个顺序让我少走了很多弯路。希望帮到你。本文还有配套的精品资源点击获取