
国庆长假的最后一天窗外的车流声渐渐恢复了工作日的紧绷。这七天里除了两场 Livehouse 的低音轰鸣我把剩余的所有精力都砸在了这套自研的AI 音乐编曲全流程管线上。市面上很多所谓的“AI 作曲”产品本质上是一个黑盒的端到端音频扩散模型——用户输入一句话吐出一个压制好的 128kbps MP3 文件。这种模式对于真正做音乐工程的人来说毫无实用价值底鼓不能单独挂压限、贝斯不能修剪低频切音、吉他 Solo 无法换成真实软音源重放。只要无法导出可独立编辑的多轨 MIDIStandard MIDI File, Type 1AI 作曲就永远只能停留在“玩具”阶段。经过一周的高强度迭代我们打通了一条从自然语言 Prompt 到工业级多轨 MIDI 导出的结构化前端工程流水线。管线分层拓扑从意图到标准音符字节流这套编曲管线在前端架构上被解耦为四个纯函数式的处理阶段意图拆解与乐理约束层Prompt Parsing Theory Anchor大模型LLM不直接生成二进制而是先解构出音乐骨架元数据调性Key/Scale、速度BPM、小节数Bars、拍号Time Signature以及配器声部规划Instrumentation。符号化记谱与分轨编排层Symbolic Arrangement指导大模型输出结构化 JSON声部明确拆分为Drums通道 10通用打击乐规范 GM、Bass低音线条、Chords键盘/节奏吉他、Lead主音旋律。触弦物理微动态与人类化微移层Humanization Dynamic Modeling消除机械感对量化Quantize过度的音符注入微观摇摆Swing、手指交替力度方差与击弦延迟。SMF 二进制编码与 Blob 导出层Binary MIDI Serializer依据 Standard MIDI File 规范纯前端将多轨数据序列化为Uint8Array并触发下载。结构化编曲中间协议IR设计为了不让大模型在生成音符时产生幻觉我们定义了一套严格的 TypeScript 格式契约Intermediate Representationexport interface MIDINoteIR { pitch: number; // 0-127 (例如 60 为中央 C) startTick: number; // 基于 PPQ 的绝对时间步 durationTicks: number; velocity: number; // 1-127 力度 } export interface MIDITrackIR { name: string; channel: number; // 0-15 (其中 9 对应通道 10打击乐专线) instrumentId: number; // General MIDI 乐器号 (如 33 代表电贝斯指弹) notes: MIDINoteIR[]; } export interface CompositionIR { title: string; bpm: number; timeSignature: [number, number]; // 如 [4, 4] ppq: number; // 每四分音符 Tick 数标准通常为 480 tracks: MIDITrackIR[]; }大模型通过 System Prompt 被严格限制只输出符合该 Schema 的 JSON。由于音符位置完全采用基于四分音符分辨率PPQ 480的整数 Tick彻底消除了浮点数舍入导致的音符错位。浏览器端纯原生 MIDI 序列化引擎很多现成的开源库体积庞大甚至混杂了服务端 Node.js 的Buffer依赖。我们在前端利用现代DataView和Uint8Array编写了一个极简且完全符合 RFC 的 SMF 序列化器。export class SMFType1Serializer { // 将变长数量值 (Variable-Length Quantity, VLQ) 写入字节流 private static writeVLQ(value: number): number[] { let buffer value 0x7f; const bytes: number[] []; while ((value 7)) { buffer 8; buffer | (value 0x7f) | 0x80; } while (true) { bytes.push(buffer 0xff); if (buffer 0x80) buffer 8; else break; } return bytes; } // 构建单个音轨二进制数据块 private static serializeTrack(track: MIDITrackIR): Uint8Array { const trackEvents: { tick: number; bytes: number[] }[] []; // 1. 设置音轨乐器 Program Change trackEvents.push({ tick: 0, bytes: [0xc0 | (track.channel 0x0f), track.instrumentId 0x7f], }); // 2. 音轨元数据Track Name const nameBytes new TextEncoder().encode(track.name); trackEvents.push({ tick: 0, bytes: [0xff, 0x03, ...this.writeVLQ(nameBytes.length), ...Array.from(nameBytes)], }); // 3. 展开 Note On 和 Note Off 事件 for (const note of track.notes) { // Note On trackEvents.push({ tick: note.startTick, bytes: [0x90 | (track.channel 0x0f), note.pitch, note.velocity], }); // Note Off (使用 Note On velocity 0 或 0x80) trackEvents.push({ tick: note.startTick note.durationTicks, bytes: [0x80 | (track.channel 0x0f), note.pitch, 0], }); } // 按时间绝对序重新排序 trackEvents.sort((a, b) a.tick - b.tick); // 4. 将绝对 Tick 转换为相对 Delta-Time 字节流 const rawTrackBytes: number[] []; let lastTick 0; for (const ev of trackEvents) { const delta ev.tick - lastTick; lastTick ev.tick; rawTrackBytes.push(...this.writeVLQ(delta)); rawTrackBytes.push(...ev.bytes); } // 音轨终止符 End of Track (FF 2F 00) rawTrackBytes.push(...this.writeVLQ(0), 0xff, 0x2f, 0x00); // 封装 MTrk 块头: MTrk 4 字节长度 实际数据 const totalLength rawTrackBytes.length; const finalBytes new Uint8Array(8 totalLength); finalBytes.set([0x4d, 0x54, 0x72, 0x6b]); // MTrk const view new DataView(finalBytes.buffer); view.setUint32(4, totalLength, false); // 大端字节序 finalBytes.set(rawTrackBytes, 8); return finalBytes; } // 序列化整首曲目为标准的 Type 1 MIDI 文件 public static exportToBlob(comp: CompositionIR): Blob { const trackBuffers comp.tracks.map((t) this.serializeTrack(t)); const totalTracks trackBuffers.length; // MThd 头部14 字节 const header new Uint8Array(14); header.set([0x4d, 0x54, 0x68, 0x64]); // MThd const view new DataView(header.buffer); view.setUint32(4, 6, false); // 头部长度恒定为 6 view.setUint16(8, 1, false); // Format 1: 多轨同步文件 view.setUint16(10, totalTracks, false); view.setUint16(12, comp.ppq, false); // PPQ 分辨率 // 合并二进制流 const allChunks [header, ...trackBuffers]; return new Blob(allChunks, { type: audio/midi }); } }人类化Humanize算法在编曲管线中的救赎如果不加任何修饰大模型输出的 MIDI 序列直接拖入 Logic Pro 或 Ableton Live就像是 1990 年代的单音蜂鸣器。在序列化之前我们在管线中挂载了一层纯数学的微动态调控鼓组底鼓/军鼓前移微调摇滚风格中军鼓往往比网格刻度慢 5~10ms拖沓感营造深沉的口袋感 Pocket而在放克Funk风格中底鼓必须紧咬网格。力度方差注入利用高斯白噪声给每个八分音符的力度施加 $\pm 6$ 的非对称扰动模拟乐手肌肉发力极限。声相自动化Pan CC10预分配将踩镲Hi-Hat推到左侧 35%贝斯死死焊在绝对中央律动吉他推到右侧 40%直接在 MIDI 层面完成声场拓扑。复盘总结与下一步展望七天长假收官这套管线已经能够根据一段简单的自然语言——“一段 125BPM、E 小调、带有加州朋克风格的快节奏律动贝斯要密集切分主干要清脆的军鼓双击”——在 1.8 秒内解构为合法的 JSON并在浏览器端零开销生成出标准的 Type 1 4 轨 MIDI 文件直接无缝导入专业 DAW 音源库。让 AI 专注于音乐创意的发散而让严谨的前端工程管线负责物理对齐、微动态重塑与二进制交付。技术不是音乐的终结者而是给每个热爱声音的人递上了一把打磨精良的数字乐器。