有天突发奇想,能不能让AI直接给我写一首MIDI曲子?这个念头本身平平无奇,真正魔幻的是接下来一整个下午的连环事件。装技能,写歌,发现bug,给开源项目发issue,从网上下了一个老掉牙的MIDI文件,然后被那份陈年数据坑到怀疑人生。最后居然全都解决了,忍不住写下来纪念一下。
#一,装技能,16次安装量的小众神作
需求很直接,AI帮我生成MIDI歌曲文件。我本地装了27个技能,没一个干这活的,于是去技能商店搜“midi”。
搜索结果挺热闹,但点开一看,全是挂羊头卖狗肉的。
midi-synth?那是把已有MIDI渲染成音频的,不是生成。voice-note-to-midi?人声转MIDI,方向反了。- ElevenLabs,Suno?那些是API生成音频文件,跟MIDI不搭边。
真正对口的只有一个,tubone24/midi-agent-skill@midi-generation。安装量16,冷门中的冷门,但描述写的就是“用GM乐器和乐理生成MIDI文件”。工作方式是本地Python脚本加midiutil库生成.mid文件,免费,不用API。
按惯例先过一遍安全审查,SKILL.md内容干净,四个脚本通读一遍没啥恶意,requirements.txt里只有midiutil。装!
冒烟测试一次过,三轨多乐器的小样,228字节,跑起来了。
#二,写ZUN歌,1分20秒
我G:\Downloads里躺着一份主旋律.txt,内容写的是「ZUN节」,车万里那段经典旋律。简谱长这样,-6 2 3 5 3 2循环三遍,再加个-7 1 -7 -5的尾巴。
第一版用的C大调,120BPM,大钢琴两轨(右手旋律,左手分解和弦),引子加主题四遍加尾声,总共40小节,正好80秒。拿mido库验证时长,80.0秒,完美。
然后我开始迭代,翻倍速度到240BPM,40秒播完,再把最后10秒剪到中间当bridge。就是从这一步开始,事情不对劲了。
#三,踩坑,休止符被吞,旋律凭空短了10秒
变速重排之后验证音轨,发现旋律轨30秒就播完了,伴奏轨却还在响。最后10秒只有伴奏孤零零地撑着场面。
排查下来,是技能的一个隐蔽bug,它的Note对象只有pitch和duration两个字段,生成脚本里音符时间是靠time += duration累加出来的,它根本没有表达休止符的能力。AI作曲的时候跳过休止拍,所有音符就全往前挪了,休止拍被吞掉,旋律整体前移。
第一版其实就有这毛病,但那会儿我只验证了总时长,没细听。80秒的文件,旋律早就结束了,我只听了开头觉得还不错,愣是没发现,直到重排结构时才暴露出来。
解决办法很简单,给技能打了个本地补丁,Note增加一个可选字段time,用来指定绝对拍位置,向后兼容。修完重新生成,各轨终于对位了。
#四,迭代,从两轨钢琴到六轨小乐队
第一版只是开始。接下来我折腾了好几轮,
变速加bridge重排。 想听快点,BPM 直接翻倍到 240,让80秒缩到40秒,又嫌结构平淡,再把最后10秒剪到中间当bridge,前20秒铺垫,中间10秒对比段,后面10秒接回主题,三段式结构有了,不过,也顺便埋了上一节那个大坑。
补终止。 剪完发现结尾停在D4上,悬在半空不落地。补了个G-C终止,G和弦分解上行,C琶音上滚,C5长音收尾,四秒搞定。
加音色。 纯钢琴太素了。我加了弦乐群铺和声长音,跟和弦走根音和五音,不抢旋律频段。又加了钢片琴在高八度点缀,只在bridge和终止段出现,带点空灵的味道。四轨了。
换军鼓,加bass。 钢片琴点缀完,总觉得还缺节奏层。换成军鼓,结果发现技能根本没有打击乐通道的概念,它的脚本会特意跳过GM标准的9号通道(打击乐专用)。于是第二次给技能打补丁,Note增加可选channel字段,指定通道就覆盖自动分配。顺手捡了个便宜,军鼓的MIDI音号是38,正好等于音名D2,填"D2"就行,不用改解析器。bass也补上了,根音跟和弦走,低音区撑住。
小号回归。 30秒处小号进,主旋律整体高八度再吹一遍,明亮的音色接回主题,跟钢片琴在终止段撞出一个八度齐奏的亮丽结尾。bass再往后压到20秒才进,让每个声部错峰登场。
最终定格在六轨,大钢琴旋律,大钢琴分解,弦乐pad,军鼓,bass,小号,44秒,240BPM。每一版我都用mido实测验证,时长,音符数,音轨位置全对得上才收工。诺,这里。
ZUN_Festival_Ensemble_Full.mid
#五,发issue,冷门项目也有被认真对待的一天
修完顺手想,这个bug对原项目也是有用的,写个issue报上去吧。
按《提问的智慧》那套流程来,先查仓库有没有现成issue,其实没有,25颗星的小仓库很安静。然后套六要点模板,目标,环境,报错,已做功课,复现步骤,实际与预期。核心事实全是实测数据,160拍的曲子,旋律轨55.5秒结束,伴奏轨79.5秒。附上修复diff,标注了“未验证,可开PR”。
英文项目就写英文。发完。
#六,老MIDI的复仇,2秒起,噪音炸了
issue发完,我转头想继续玩MIDI,从网上下了一份众神眷恋的幻想乡。202秒,3个音轨,931加882个音符。看文件头,GBK中文曲名,乐器名写着“Microsoft GS波表软件合成器”,千禧年代初的。给老资历跪了。
用mido一读,?!直接崩溃!?,data byte must be in range 0..127。
这可不是小问题。MIDI文件里出现了规范外的字节,严格解析器直接拒收。我用耳朵加PotPlayer试听,那噪音楼上张阿姨都听得见。
于是开始跟这份老文件斗智斗勇。先怀疑文件损坏,逐字节检查chunk边界,结构完好,三个音轨的长度字段和文件大小精确吻合。再怀疑音符数据损坏,我手动写了个容错解析器,音符流全干净。那到底在哪崩溃的?
答案藏在两个地方,
第一,track0有16个CC7音量消息,值全是0xFF(255)。 MIDI规范里CC值只允许0-127,255是非法的,严格解析器当场暴毙。微软波表播放器大概见多识广,默默钳位成127继续播。
第二,也是最精彩的,track2有19个CC64延音踏板消息,从1.74秒开始,值从85,86,87……一路递增到104,然后就没然后了,从头到尾没有释放。
延音踏板是开关量,半踏板最多也是固定值。85→104,每秒两三个地递增,这不是踏板,这是别的数据被错标成了踏板,大概率是音量或表情的渐变,被某个老工具映射到了CC64。
而我听到的“从2-3秒开始的大噪音”,正是从1.74秒踏板踩下开始的。track2的每一个音符都被无限延音,持续叠加,两秒之后就混成了一片,越积越浑浊。时间线严丝合缝。
修复倒简单,删掉那19个CC64,把CC7改成127,保存。严格模式读取通过,音符一个不少,时长一分不差。试播,Windows自带播放器,PotPlayer,全都正常了。
#七,事后
一整天下来,其实只干了一件小事,让AI写了一首MIDI。但沿途捡到了这些,
- 一个安装量16的冷门技能(实测能用)
- 一个技能自身的静默bug(已修复,已发issue)
- 一份2000年代老东西的病历(CC64数据污染加非法CC7)
- 一个深刻教训,MIDI文件里最可怕的是控制器数据错。音符错了你听得出来,CC错了它会悄悄毁掉整首歌,还让严格解析器当场去世。
哦对,还有一版修复好的众神眷恋的幻想乡 touhou_fixed.mid 躺在下载目录里。有缘人拿去吧,老曲子的修复版,免费。
评论