
简介资源是一份面向开发者与AI音乐爱好者的实战型PDF文档围绕DeepSeek与MIDI技术结合系统讲解从模型调用、MIDI解析与清洗到生成原创音乐全流程的落地方法。文档不仅覆盖AI作曲背景、DeepSeek模型特点、MIDI文件结构等基础内容还重点给出文本化预处理、模型构建训练、音乐生成代码示例与效果评估调优策略适合希望通过AI提升创作效率、探索自动作曲方案的读者。包体为1个PDF文件整体大小约1.97MB内容共26页章节结构完整、便于按需翻阅。已有377人学习下载适合作为入门到进阶的参考资料。1. AI作曲不是玄学DeepSeek写乐谱MIDI当翻译官朋友递给我一个MIDI键盘问我能不能用DeepSeek把脑子里哼的旋律变成一首完整的歌。我的回答是方向反了。AI作曲真正能落地的路径不是让大模型直接输出音频而是让DeepSeek生成结构化的音符与编曲指令再用脚本转成MIDI文件最后交给音源渲染成声音。这篇笔记就按这条路线展开从DeepSeek API的提示词设计、JSON结构约定到pretty_midi落地、多轨编曲扩展、常见翻车现场和批量变体生成。适合想快速出音乐Demo、又不想碰模型训练的音乐人、开发者和AI应用爱好者照着做就能拿到第一个能响的.mid文件。2. 先立住选型为什么MIDI是DeepSeek生成音乐的最佳中间格式2.1 MIDI不是音频是乐手的“演奏开关表”很多人第一次接触MIDI会误以为它存的是声音。实际上一个标准MIDI文件里存的是“什么时候按下哪个键、按多久、按多大力”这类离散指令音色由播放端的合成器决定。这个特性让它天然适合大模型参与创作LLM最擅长的就是生成离散符号序列而不是连续声波。一条常见的MIDI音符事件包含四个核心字段pitch音高0到127的整数、velocity力度0到127、start起始时刻和end结束时刻。这四个字段恰好能映射成JSON里的四个数字。DeepSeek不需要理解“声音的物理性质”它只需要学会“C大调里E音比C音高四度”这类符号规则就能产出合理的音符序列。这就是为什么我说MIDI是大模型音乐生成的“最佳中间格式”。另一个好处是可验证。文本生成错误最多是语句不通音乐生成错误会直接表现为难听的刺耳音。MIDI因为是符号化的我们可以在写入文件之前做音域检查、和声内音检查和时值检查把不合格的音符合理化处理。这个“先校验再渲染”的流程能过滤掉大模型相当一部分幻觉输出。2.2 DeepSeek在流水线里的三个输出角色在一个完整的AI作曲工作流里DeepSeek不负责“生成波形文件”它负责三件事。第一件事是生成和弦进行。你给它一个调性、风格和段落长度它返回一组和弦序列比如C自然大调下的Cmaj7、Am7、Dm7、G7。这类输出非常稳定因为和弦进行本质上是乐理规则的有序排列大模型在训练数据里见过海量类似结构。第二件事是生成旋律骨架。旋律比和弦难一些因为它需要“创意”但DeepSeek在给定和弦约束和音域范围后产出的旋律至少是“语法正确”的。注意我用的是“语法正确”而不是“好听”——好不好听是主观的需要人工筛选。第三件事是生成编曲规则。比如“主歌部分用柱式和弦铺底副歌改成琶音”、“第5到第8小节贝斯切分”。这类输出适合以自然语言或JSON规则传给后处理脚本而不是直接生成几千个音符让程序无脑落盘。把这三件事拆开做比让DeepSeek一次性输出“整首曲子”可靠得多。原因后面避坑章会详细说核心在于单次生成的长度有限越长越容易结构崩坏。2.3 API和本地部署怎么选稳定性与成本的权衡在实际项目中DeepSeek有两种接入方式调官方API或者本地部署。我的建议是先跑通API再考虑本地部署。官方API的好处是模型能力强JSON格式稳定性高而且支持结构化输出模式。接入方式也很简单因为DeepSeek的API兼容OpenAI的SDK只需要把base_url换成官方API地址模型名填deepseek-chat。对于“生成音乐指令”这种中等复杂度的任务API调用的响应速度和稳定性都足够。本地部署的好处是数据不出内网、按量成本可控适合需要批量生成几百首曲子做数据集的公司。但要注意本地部署小参数模型时JSON格式的稳定性会明显下降——模型经常在输出过程中夹杂解释性文字或者把花括号写丢。如果你非要本地部署建议用deepseek系列里参数规模较大的版本并且在后处理里写一个JSON提取函数兜底。还有个成本细节生成音符列表这种任务输出tokens比输入多得多所以计费主要看输出量。我一般会让DeepSeek输出“压缩谱式”的JSON——和弦表加旋律表而不是让它把每个轨道的每个音符都描述一遍。这样输出量能省一半以上。提示不要一上来就追求“全自动作曲”。把DeepSeek定位成“懂乐理的助手”而不是“音乐家”把最终决定权留给人耳工作流的容错率会高很多。3. 跑通最小工作流DeepSeek API pretty_midi 生成第一个MIDI文件3.1 约定JSON结构先定翻译表再写提示词想让DeepSeek稳定产出可解析的音乐数据第一步不是写提示词而是定义JSON结构。我见过太多人直接让模型“写一段旋律”结果返回的是自然语言描述或者Markdown代码块解析全靠猜。正确做法是把输出结构钉死让模型像填表一样返回数据。我常用的结构分两层顶层是tempo和chords第二层是melody事件列表。和弦用“起始小节持续小节数和弦名”表示旋律用“起始拍音高时值力度”表示。用拍而不是秒做单位是因为拍在音乐逻辑里更自然换算成秒的工作交给代码做。{ tempo: 120, chords: [ {start_bar: 0, bars: 4, chord: Cmaj7}, {start_bar: 4, bars: 4, chord: Am7} ], melody: [ {start: 0, pitch: 72, duration: 1, velocity: 92}, {start: 1, pitch: 74, duration: 0.5, velocity: 88} ] }这套结构的关键点在于和弦和旋律是分开的各自独立成数组。这样后处理时可以分别控制比如把和弦整体降八度做铺底或者把旋律改写成琶音都不需要改动JSON解析逻辑。3.2 调DeepSeek APIJSON模式下的提示词与调用代码DeepSeek的API兼容OpenAI SDK所以调用代码非常短。关键是要把system提示词写清楚只输出JSON、不要解释、严格遵循结构。from openai import OpenAI import json client OpenAI( api_keysk-你的密钥, base_urlhttps://api.deepseek.com ) def compose_music(prompt_text): resp client.chat.completions.create( modeldeepseek-chat, response_format{type: json_object}, messages[ { role: system, content: 你是作曲助手。严格按用户要求输出JSON不要输出任何解释文字。 }, {role: user, content: prompt_text} ], temperature0.4, max_tokens4096 ) return json.loads(resp.choices[0].message.content)这段代码里有几个参数值得解释。temperature设0.4是我在“格式稳定”和“素材多样性”之间试出的折中点低于0.2会让旋律千篇一律高于0.7容易在JSON括号上翻车。response_format强制返回JSON对象这是DeepSeek官方支持的能力能过滤掉大部分“好的我来为你创作”这类废输出。提示词模板我一般这样写PROMPT_TEMPLATE 请创作一段8小节、C自然大调、BPM120的温柔流行风格音乐。 合法和弦列表只能从这些里选 Cmaj7, Dm7, Em7, Fmaj7, G7, Am7, Bm7b5 JSON字段说明 - chords: start_bar 从0开始计数bars 为和弦持续的小节数 - melody: start 以拍为单位从0计数pitch 为MIDI音高整数(范围60到84)duration 为拍数velocity 为力度(1到127) 输出结构参照以下但旋律内容请原创重新编排 { tempo: 120, chords: [ {start_bar: 0, bars: 2, chord: Cmaj7}, {start_bar: 2, bars: 2, chord: Dm7} ], melody: [ {start: 0, pitch: 72, duration: 1, velocity: 92} ] } 把合法和弦列表写死进提示词是我强烈建议的做法。如果不限定DeepSeek会自由发挥出各种奇怪的扩展和弦比如Cadd9、G/A这类后处理代码每遇到一个就得加一条解析规则非常被动。3.3 JSON转MIDIpretty_midi最小落地脚本拿到JSON之后用pretty_midi库落地成.mid文件。这个过程只有两步把拍换算成秒把JSON字段映射成Note对象。import pretty_midi from pretty_midi import Note, Instrument, PrettyMIDI NOTE_MAP { C: 60, C#: 61, D: 62, D#: 63, E: 64, F: 65, F#: 66, G: 67, G#: 68, A: 69, A#: 70, B: 71 } CHORD_INTERVALS { : [0, 4, 7], m: [0, 3, 7], maj7: [0, 4, 7, 11], 7: [0, 4, 7, 10], m7: [0, 3, 7, 10], m7b5: [0, 3, 6, 10] } def parse_chord(chord_name): name chord_name.strip().replace(M7, maj7).replace(△, maj7) for root in sorted(NOTE_MAP.keys(), keylen, reverseTrue): if name.startswith(root): suffix name[len(root):] if suffix not in CHORD_INTERVALS: raise ValueError(f未支持的和弦后缀: {suffix}) return NOTE_MAP[root], CHORD_INTERVALS[suffix] raise ValueError(f无法识别的和弦: {chord_name})parse_chord函数把“Cmaj7”这类字符串拆成根音音高60和音程列表[0, 4, 7, 11]然后转成实际音符。def json_to_midi(music, output_pathoutput.mid): midi PrettyMIDI(initial_tempomusic[tempo]) sec_per_beat 60.0 / music[tempo] piano Instrument(program0) for ch in music[chords]: root, intervals parse_chord(ch[chord]) sec_start ch[start_bar] * 4 * sec_per_beat sec_dur ch[bars] * 4 * sec_per_beat for iv in intervals: piano.notes.append(Note( velocity70, pitchroot iv, startsec_start, endsec_start sec_dur )) for n in music[melody]: vel max(1, min(127, int(n[velocity]))) pitch max(21, min(108, int(n[pitch]))) sec_start n[start] * sec_per_beat sec_dur n[duration] * sec_per_beat piano.notes.append(Note( velocityvel, pitchpitch, startsec_start, endsec_start sec_dur )) midi.instruments.append(piano) midi.write(output_path) return output_path这段代码有两个必须理解的参数换算。第一PrettyMIDI的initial_tempo只负责告诉播放器“这首歌每分钟多少拍”而Note对象里的start和end字段单位是秒所以必须用sec_per_beat 60.0 / tempo把拍换成秒。第二和弦轨的start_bar要先乘以4变成拍再乘sec_per_beat变成秒因为一小节默认四拍。跑完后你会得到一个可播放的.mid文件。我用市面上的DAW或者免费MIDI播放器打开试听都能直接出声。需要注意的是生成的MIDI只有钢琴轨听起来像一个人在弹电子琴这是正常的下一步就是加编曲。4. 把单轨Demo扩展成完整编曲鼓、贝斯、和弦铺底的生成分工4.1 为什么要分轨调用单次输出长度与上下文稳定性第一个MIDI文件跑通之后最自然的想法是让DeepSeek一次性生成一整首编曲钢琴、贝斯、鼓、弦乐全给它。我劝你别这么做。原因很现实大模型单次输出tokens有限一首完整的四轨编曲如果全用事件表描述光鼓轨可能就几百个事件输出很容易在中途截断而且模型在超长生成时结构一致性会崩——开头还在C大调结尾已经跑到降E大调去了。所以我的做法是分轨调用和弦进行一轮、旋律一轮、贝斯规则一轮、鼓不调模型用脚本规则生成。每轮调用之间靠“上一轮的输出”做衔接。比如第二轮生成旋律时把第一轮的和弦JSON原样放进提示词让模型“看着和弦写旋律”。这样每一轮任务都足够轻模型翻车概率大幅下降。4.2 分轨生成的角色提示词贝斯、铺底、旋律各干各的既然分工提示词也要分工。生成旋律的提示词聚焦在和弦约束上生成贝斯的提示词则聚焦在根音走向和节奏型上。BASS_PROMPT 下面是这首歌的和弦进行JSON {c和弦JSON} 请设计一条贝斯线要求 1. 以和弦根音为骨干第1拍必须落在根音上 2. 可以加经过音但经过音只能来自当前和弦音或其邻近半音 3. 输出格式为melody事件数组字段与之前相同 只输出JSON。 注意这里我用了“第1拍必须落在根音上”这类硬约束而不是“要弹得好听”。大模型理解“好听”是玄学但理解“第1拍根音”是明确的规则约束。把艺术判断留给自己把规则执行交给模型这是分轨生成的核心思路。贝斯落地时我从和弦JSON里直接抽根音生成一个保守版本作为基线再决定要不要用DeepSeek的版本替代。这样即使模型生成的贝斯不合意程序也不会中断。def build_bass_track(chords, bass_tune, tempo): sec_per_beat 60.0 / tempo bass Instrument(program34) for n in bass_tune[melody]: sec_start n[start] * sec_per_beat sec_dur n[duration] * sec_per_beat bass.notes.append(Note( velocitymax(1, min(127, int(n[velocity]))), pitchmax(21, min(60, int(n[pitch]))), startsec_start, endsec_start sec_dur )) return bassprogram34是通用MIDI音色表里的电贝斯pitch上限压到60是为了让贝斯保持在低音区不和旋律抢高频空间。这段代码里的关键参数是音域clip——贝斯音高限制在21到60之间超出就强制拉回这是防止模型乱来的第二道保险。4.3 鼓轨不靠LLM猜规则轮换加一点偏移鼓轨是我唯一不建议用大模型生成的轨道。原因是鼓点的“好听”高度依赖风格套路而套路用规则写比让模型猜更稳定。常见EDM或流行曲的底鼓、军鼓、闭镲轮换用几行代码就能表达清楚。def build_rock_drum(total_bars, tempo): sec_per_beat 60.0 / tempo drum Instrument(program0, is_drumTrue) for bar in range(total_bars): for beat in range(4): if beat 0: drum.notes.append(Note( velocity100, pitch36, start(bar * 4 beat) * sec_per_beat, end(bar * 4 beat 0.15) * sec_per_beat )) if beat 2: drum.notes.append(Note( velocity95, pitch38, start(bar * 4 beat) * sec_per_beat, end(bar * 4 beat 0.12) * sec_per_beat )) if beat % 2 0: drum.notes.append(Note( velocity72, pitch42, start(bar * 4 beat) * sec_per_beat, end(bar * 4 beat 0.08) * sec_per_beat )) return drum这段代码里pitch36是GM音色表的底鼓38是军鼓42是闭镲。is_drumTrue告诉播放器这条轨道走打击乐通道不需要音高旋律。end的计算是start加一个小数值模拟打击乐短促的衰减感。这比生成音符时值更接近真实演奏。如果你想在此基础上加“人性化”就在每次start上叠加一个随机的±10毫秒偏移同时让velocity在上下5的范围内浮动。这个技巧能明显削弱机械感音频行业叫它humanize。4.4 让DeepSeek给变奏规则用规则作曲而不是音符作曲编曲做到这一步曲子的骨架和肢体都有了但音乐不能从头到尾重复一个动机需要变奏。这里我给DeepSeek的任务不是“生成新旋律”而是“生成变奏规则”。VARIATION_PROMPT 这是主题旋律的JSON {m旋律JSON} 请给出3条变奏规则要求 1. 每条规则必须是可编程的比如“第3-4小节整体移高纯四度” 2. 每条规则必须说明作用于哪个小节的哪个声部 3. 只输出JSON格式为{variations: [{target_bars: [3,4], operation: transpose, value: 5, track: melody}]} 这个思路是把“创作”拆成“素材生成”和“规则生成”两步。素材可能平庸但好的变换规则能让平庸素材变得可用。比如“把副歌旋律倒影处理”“把和弦织体从柱式改成琶音”这些规则落到代码里就是几行数组操作但音乐效果立竿见影。这套方案跑顺之后你会发现自己不是在“调教AI作曲”而是在“设计一套作曲流水线”。5. DeepSeekMIDI落地避坑清单5个让程序当场翻车的坑5.1 JSON被截断或混入散文解析失败怎么兜底现象调用DeepSeek后json.loads直接抛异常检查返回内容发现要么是“好的下面是您需要的旋律”开头要么是JSON写到一半戛然而止。原因有两个提示词没锁死输出格式或者生成的JSON超过单次输出tokens上限被截断。解决第一启用response_format的JSON模式并在system里强调“不要解释”第二写一个提取函数从返回文本里抓取最后一个看起来像完整JSON的片段再解析。import re def extract_json(text): candidates re.findall(r\{.*\}, text, re.DOTALL) for candidate in reversed(candidates): try: return json.loads(candidate) except json.JSONDecodeError: continue raise ValueError(未找到合法JSON)这段代码的trick在于reversed——从最长的候选开始尝试因为截断场景下后半段往往残缺前半段完整JSON能先被命中。我把这个函数挂在所有DeepSeek调用后面相当于给模型输出上了一道保险。这不是最优解但在本地部署小模型时几乎必不可少。5.2 播放速度是预期的两倍拍和秒的换算混用现象生成的MIDI文件导入DAW后播放速度飞快或者音符时长全部错乱。原因pretty_midi里Note对象的start和end单位是秒而DeepSeek输出的是拍。直接拿拍当秒填进去120BPM的歌会变成每分钟120拍但每拍只有1秒——实际应该是0.5秒。反直觉的是这个错误发生时看起来“很有规律”很难一眼发现。解决全项目统一用sec_per_beat 60.0 / tempo做转换不允许任何地方裸写数字。我自己的习惯是在项目里定义一个工具函数beat_to_sec(beat, tempo)所有时间计算都走它后期调BPM只需要改一处。这个习惯帮我在批量生成场景里省掉过好几个小时的排错时间。5.3 和弦解析崩溃模型无视合法列表输出花式和弦现象代码运行到parse_chord时抛出“未支持的和弦后缀: M7”或者更常见的“add9”、“sus2”这些提示词里根本没列过的和弦。原因模型有时觉得“Cadd9”比“Cmaj7”更丰富自作主张使用了列表外的和弦。解决除了解析函数里加归一化和跳过逻辑更重要的是在提示词里把合法列表用枚举式写法强调并在和弦字段后加例——“只能从以下列表选择Cmaj7, Dm7...”。还有一个血泪经验如果某次生成的音乐听起来和弦很怪别急着怀疑代码先打印出DeepSeek实际返回的和弦列表看一眼——九成是模型用了列表外的怪和弦。5.4 音高和力度越界刺耳音与爆音现象渲染出的MIDI里偶尔出现一个极其刺耳的高音或者播放时某个音有明显破音感。原因模型生成的pitch或velocity超出了合理范围比如pitch112、velocity130。这类错误在单轨试听时不算致命但一旦铺满四轨就会在副歌处突然炸一下。解决写一个清洗函数在写入Note对象前统一做clip。def sanitize_note(note, min_pitch21, max_pitch108): return { pitch: max(min_pitch, min(max_pitch, int(note[pitch]))), velocity: max(1, min(127, int(note[velocity]))), start: max(0, float(note[start])), duration: max(0.1, float(note[duration])) }注意duration还做了下限处理——如果模型生成了0时值的音符播放器可能直接忽略导致旋律空缺。我一般把最小值设为一拍的十分之一既不改变节奏感又能保住音符存在感。5.5 API调用报错base_url和上下文窗口的隐患现象用OpenAI的SDK调DeepSeek时报404或Invalid URL或者在长提示词场景下返回空内容。原因base_url填错了。DeepSeek的接口兼容OpenAI但base_url要用官方API地址本身不需要在末尾加/v1也不需要拼多余的路径。把OpenAI的默认地址直接换成DeepSeek的API地址即可。另一个隐患是提示词过长——把整首歌的和弦表、旋律、贝斯全部塞进一轮调用会挤占输出空间甚至让模型忽略后面的字段约束。解决保持每轮调用的输入简短只带上当前任务最需要的上下文。注意如果你在本机部署的是小参数版本模型JSON模式不稳定是常态不要因此怀疑API代码写错了。我在小模型上的做法是提示词里明确要求“只输出JSON不要Markdown”然后在后处理里用extract_json兜底。6. 进阶玩法让DeepSeek写生成器批量生产风格变体把工作流跑通后我推荐你换一种用法不再让DeepSeek直接生成音符列表而是让它生成“生成音符的函数”。这个思路的转变在于函数本身就是规则规则写对一次就能批量复用还能让本地代码掌控随机性。import random SCALE_C_MAJOR [60, 62, 64, 65, 67, 69, 71, 72] def melody_from_seed(seed, bars8): rng random.Random(seed) note rng.choice([60, 64, 67, 72]) beat 0 melody [] while beat bars * 4: dur rng.choice([0.5, 1, 1, 1, 2]) melody.append({ pitch: note, start: beat, duration: dur, velocity: rng.randint(70, 100) }) beat dur step rng.choice([-2, -1, -1, 1, 1, 2, 3]) idx SCALE_C_MAJOR.index(note) note SCALE_C_MAJOR[(idx step) % len(SCALE_C_MAJOR)] return melody你可以让DeepSeek生成类似的函数体只要提示词里讲清楚“用音阶内随机游走、保证音符在C大调音阶内、返回事件字典列表”它能写出比这个示例更复杂的版本。然后你在本地循环跑100个seed生成100首变体批量转MIDI、批量试听。对一个Demo项目来说这个量级的素材池已经足够支撑选曲。批量生成之后验证逻辑不能省。我现在的做法是跑一个校验脚本循环三件事检查所有音符pitch是否在21到108之间、检查旋律音符是否落在当前和弦内音上、检查总时长是否和预期小节数一致。这三项过了才导入DAW试听。原创性方面经典和弦进行比如I-V-vi-IV很容易撞梗我的习惯是让DeepSeek在变奏规则里加入“替换第3个和弦为同功能代理和弦”再人工过一遍旋律线条。我很少拿AI生成的第一版直接交差通常会让它生成三版变体自己挑一个动机然后手工改两处节奏。这个习惯帮我躲过很多“听起来很AI”的尴尬时刻。这套路子走顺之后你手头就有了一个能持续产出素材的作曲流水线希望帮到你。本文还有配套的精品资源点击获取