国庆长假的最后一天,窗外的车流声渐渐恢复了工作日的紧绷。这七天里,除了两场 Livehouse 的低音轰鸣,我把剩余的所有精力都砸在了这套自研的AI 音乐编曲全流程管线上。
市面上很多所谓的“AI 作曲”产品,本质上是一个黑盒的端到端音频扩散模型——用户输入一句话,吐出一个压制好的 128kbps MP3 文件。这种模式对于真正做音乐工程的人来说毫无实用价值:底鼓不能单独挂压限、贝斯不能修剪低频切音、吉他 Solo 无法换成真实软音源重放。只要无法导出可独立编辑的多轨 MIDI(Standard MIDI File, Type 1),AI 作曲就永远只能停留在“玩具”阶段。
经过一周的高强度迭代,我们打通了一条从自然语言 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 Representation):
export 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 专注于音乐创意的发散,而让严谨的前端工程管线负责物理对齐、微动态重塑与二进制交付。技术不是音乐的终结者,而是给每个热爱声音的人,递上了一把打磨精良的数字乐器。