作曲:从 brief 到一份原创配乐
风格是一套词汇。它给你:
- 声音:音色、电平和效果;
- 律动家族;
- 一种和声语言;
- 旋律规则;
- 一套编配语法。
它永远不给你音符。每一个音符都由你来写:
- 和弦及其声部排布;
- 低音线;
- 动机和旋律线;
- 曲式。
引擎拒绝填补空缺。缺了和声、旋律、律动或曲式,都是一个错误,错误会告诉你该去写什么。然后引擎用代码确定性地演奏、混音、做母带,并在你要求的地方播放真实乐器的录音。
绝不复用已交付的素材。 演示曲(
node tools/music.mjs list)和我们自己的影片配乐是试听参照,也是新颖度语料库。它们永远不能当影片的配乐,也不能当模板。不要复制它们,不要把它们移调,不要给它们换节奏。一份配乐只要听起来像任何一首已交付的曲子,或者引用了其中一首——一个 6 音片段(移调与否都算),或者任意调、任意拍号、任意节奏下的一个 8 音旋律形状——node tools/music.mjs novelty就判它不通过。惯用语没问题。 ii-V-I、变格的「阿门」终止、两和弦循环的 vamp、I-bVII-IV 都是词汇。风格页面把它们列出来,就是为了让你用。不行的是复用同一段音乐:一个多小节的和弦进行连同它的节奏、律动、轮廓和曲式一起搬过来,或者一段被引用的旋律。
为什么有这条规矩:有一个「新」示例曾经改掉了发布片配乐的调、和弦和音高,却保留了它的节奏、轮廓、鼓和曲式。人一听就听出是同一首歌。新颖度指纹称量的恰恰就是这些东西。
工作流程
- 把 brief 读成数字。 写下:
- 用途(发布片、讲解片、网站上的循环动画、游戏、广告);
- 时长(秒),以及是否必须循环;
- 3-5 个画面拍点和它们的时间(揭晓、主张、切到片尾卡);
- 对方描述那种感觉时用的词。
-
选一个风格和一个情绪。 风格:
细节见 styles/vocabularies.md。情绪是tables.ts里的一行(见本目录 index.md 里的情绪列表)。然后设定五个情绪控制量,每个 0..1,0.5 = 校准好的默认调色板:控制量 它调动什么 energy律动密度、鼓的电平、侧链深度、力度 warmth滤波器更暗,磁带过载和抖晃(wow)更多,低音稍多一点 brightness滤波器和拨弦的明亮度打开,踩镲提高 tension铺底失谐更宽,弦乐起音更短,主奏衰减更短 space混响发送量、延迟混合比、铺底的释放时间 控制量只改变声音。和声上的紧张感要你自己写(第 4 步)。要选出能让一种感觉被读出来的速度、调式、音区和密度,见 theory/mood.md:速度和调式是最强的线索,几条线索指向同一个方向,情绪就不会被认错。 3. 在写任何和弦之前,先勾出弧线和动机。 写下三样东西: - 紧张度曲线。 每段一个 0..1 的数字:它上升,在峰值之前稍稍回落(一次换气或 breakdown),在画面的关键帧上达到顶峰,然后释放。它会变成每一段的
energy、它的层次和它的和声(回家之前的倾向和弦)。 - 高潮。 在哪一小节、全曲的最高音是哪个,以及它下面的和弦(主和弦,所有层次齐上)。 - 动机。 它的节奏(避开哪些拍,长音落在哪里),它的轮廓(一次跳进,然后级进回来),以及它将如何在整个曲式里发展:陈述、模进、在铺垫段里被碎片化、在高潮处扩大、在结尾说最后一句话(theory/melody.md)。 4. 写和声。 在风格的和声语言之内工作:它的调式、和弦性质、倾向和声部排布规则。根据情绪而不是习惯来决定调和调式。 - 写一个为这份 brief 而写的和弦进行。有意识地选择它的长度(2、3、4、5、6 或 8 小节)和它的和声节奏(每小节一个和弦、每小节两个,或者每两小节一个)。 - 每个和弦的声部排布都自己来。声部级进移动,保留共同音。根音归低音。 - 按风格的低音惯用写法,给每个和弦写它的低音线(一小节):长音根音、根音加五音、行走低音、反拍八分音符、切分的 808 音型。节奏要是你自己的。 - 把紧张与释放当成一条曲线。 稳定(主和弦),离开(下属、关系调、借用和弦),倾向(属和弦、挂留、持续音),回家。把回家放在画面的关键拍上。中间的段落以半终止或阻碍终止结束,完满终止留到最后(theory/harmony.md)。 - 低处展开,高处收紧。 大约 C3 以下不用密集音程(theory/sound.md)。 5. 写动机。 动机是 2-5 个音,带有自己的节奏。先发明节奏: - 它避开哪些拍? - 它从弱拍开始吗? - 长音在哪里?然后才是音高:轮廓,一次跳进,然后级进回来。
要发展动机。不要复印它:
- 重复,然后变化:改最后一个音程、错位一个八分音符、延长它;
- 模进:同样的形状,高一个音级或一个三度;
- 呼应:问句结束在主音之外,答句落在主音上,或者由另一件乐器来回答;
- 在铺垫段里把它碎片化,在高潮处把它扩大(更慢、更长的音);
- 把它放进低音里制造威胁感,或者在结尾放得又高又轻。
乐句的最高点放在它的后半段。一个乐句 2 或 4 小节;一个乐段是两个乐句(问、答)。旋律线必须遵守风格的音域和密度。强拍上放和弦音,把感觉放进以级进解决的倚音和挂留音里。 6. 选择或编写律动。 从风格的列表里挑一个家族(
- 它打印旋律、和声、节奏、紧张度和情绪的得分(0..1),按段落和按小节的紧张度曲线,以及每一段的终止式。 - 每一条bounce、trap、brushes、hand……)。设定density0..1、variation0..1 和fill(在段落最后一小节加花)。种子让每一小节都有变化,所以没有两首曲子共用同一个循环。你也可以自己用记谱法写鼓的小节,每条轨一小节或多小节:kick、snare、ghost、hat、perc。用第二个律动做对比(half、build)。保持底鼓和反拍稳定,在它们上面做切分(theory/rhythm.md)。 7. 写曲式。 段落是{ kind, bars, harmony, lead, counter, arp, comp, groove, bass, chordVel, energy, repeat, stretch }。 - kind 决定哪些层可以发声:intro、verse、groove、hook、build、half、breakdown、drop、bridge、swell、breath、outro。见 vocabularies.md 里的编配语法。 - 旋律线只在你写了的地方发声。 - 让每一次段落切换都落在一个画面拍点上。在bpm下,一小节持续beatsPerBar x 60 / bpm秒,在 30 fps 下一拍是1800 / bpm帧。 - 把一个段落标成stretch: true:影片适配时会重复或删掉它,以达到精确的长度,并且仍然结束在你的尾奏上。 - 必须卡准画面时刻的音乐(在 16.0 s 的揭晓处来一个 drop):按长度作曲(小节数 x 每小节拍数 x 60 / bpm),并且不带--fit渲染,或者用发布片所用的拍点网格模式(launchTemplate.ts里的gridScore:配乐严格按影片的 bpm 播放,剪辑点落在它的强拍上)。当曲式本来就在目标长度的 max(0.5 s, 一拍) 之内结束时,--fit会保持你的速度,由尾音吸收差值。超出这个范围,它就会改变速度,每个卡点都跟着移动。 - 歌曲和 cue 的曲式、铺垫段,以及配器槽位:见 theory/form.md。 8. 渲染之前,先按手艺修改。craft在几毫秒内读完音符:CRAFT行都点明它看到了什么、在哪里(小节和拍),以及一个修法。 - 循环:读这些发现,修改音符,再跑一次craft。直到剩下的每条发现都是你有意为之的(悬疑片的开放式结尾、摇滚里的平行五度)才停。在给对方的说明里讲清楚是哪几条。 - 把打印出来的紧张度曲线和你在第 3 步勾的草图对比。如果峰值不在你计划的地方,错的是编配,不是草图。曲线下面那一行说明是什么在推动每一段(响度、密度、音区、和声):响度和密度权重最大,所以一个主和弦上的全奏或一个 drop 就是峰值(theory/harmony.md 第 8 节)。- 调/调式问题和作曲警告(例如一条旋律线比它所在的段落短);
- 母带:响度与目标的对比、真峰值,以及当 -1 dBTP 天花板让增益提前停下时的一条提示;
- ghost、reverb 和 masking 守卫;
- 分轨;
- craft(第 8 步的报告,明亮度从渲染结果中测量;只有不可演奏的音域才判不通过);
- 新颖度。
任何一项失败,它都以非零状态退出。
render打印同样的报告(带--stems时包括分轨),只是不含新颖度。两者都把每个音色只合成一次,用上除一个以外的全部 CPU 核心,并在混音的同时并行渲染守卫和分轨:音频与串行渲染逐位相同(render --verify会再串行渲染一遍并比较;ANIDOODLE_THREADS=1关掉线程池)。- 分轨: 任何被标出偏离超过 3 dB 的声部,用
levels修正。levels是每个槽位的 dB 偏移量(+2 = 响两 dB),叠加在校准增益、修剪量和情绪控制量的偏移之上,和你自己音色的gainDb用的是同一把尺子。只看 LUFS 曾经掩盖过一个偏热 7-10 dB 的次低音。 - 守卫和分轨意见不一致时,守卫说了算。 如果 masking 不通过(旋律听不出来),就把主奏调高。为此,分轨测量允许主奏比它的容差再高出最多 +3 dB。你也可以把落在旋律音区里的东西调低或者削薄。情绪控制量会随声音一起移动分轨目标(例如 brightness 会抬高踩镲,也抬高踩镲的目标);打印出来的目标已经把这一点算进去了。
- 母带没到目标: 柔和母带(-16 LUFS)永远不压缩。如果一个峰值挡住了增益,整首曲子都会保持更小的音量,而
check会说出差多少、峰值在哪里(小节、拍和秒),以及哪些声部在那里叠在一起(从最响的开始,附上它们正在演奏的音符)。就在那个位置的音符里修:把那个强拍下的低音错开,把那个大和弦琶音化,不要加倍高潮的那个音。 -
新颖度: 如果不通过,改数字指向的那些东西。改调什么都修不好。每一项特征是一袋 n 元组,用余弦相似度比较(0 = 毫无共同之处,1 = 完全相同);得分是它们的加权平均,高于 0.5 就不通过:
特征 权重 它统计什么 melodyRhythm0.20 主奏在每小节里的起音位置,以及它的起音间隔三元组 melodyContour0.15 主奏的 上 / 下 / 同 四元组 melodyIntervals0.10 主奏的音程三元组,以半音计(与移调无关) lineRhythm0.15 对位旋律线的节奏加上低音的节奏(各自的小节位置和起音间隔三元组) drums0.15 每个鼓声部每小节的击打位置 chords0.10 和弦功能三元组:每个根音相对于调的位置,连同它的和弦性质 qualities0.07 和弦性质三元组(maj7、min、dom、sus……) form0.08 段落类型的顺序,两两成对 主奏是旋律声部每次起音时的最高音。小节位置取决于拍号;起音间隔三元组、轮廓、音程和两道引用闸门都与拍号无关。有两道闸门不管得分多少都直接判不通过:已交付旋律的一个 6 音片段(它的音程和时值,任意调),以及一个 8 音旋律形状(只看它的音程,任意调、任意拍号、任意节奏:改成 5/4 重新划小节或者拉长了的引用,仍然是那支曲子)。只有有辨识度的形状才算(三种或更多不同的音程、一个三度或更大的跳进、最多三个重复音),所以一段音阶跑动永远不算引用。 - 试听: 用之前要让对方听。测量只能证明你没有明显出错。只有耳朵能说对不对。循环动画要交付
.wav:mp3 会加上编码器填充,所以在循环点会有咔哒声或空隙。
录音乐器
装了音源包时,八个原声声部会播放真实乐器的录音:piano(以及带 variant: "upright" 的 piano)、harp、marimba、vibes、glockenspiel、带 variant: "tubular" 的 bell,还有 timpani。弦乐、吉他、贝斯、铜管、木管、合唱、钢片琴、八音盒、电钢琴和原声套鼓无论如何都保持建模,而且没有音源包时每一份配乐照样能播放。
有四件事决定你写什么:
- 待在音域之内,否则渲染会抛错。 三角钢琴能低到 A0;定音鼓止于 B3;颤音琴从 F3 开始,钟琴从 G5 开始。完整的表在 index.md。
- 录音的音按那件乐器自己的方式衰减。 三角钢琴的一个低音会响上几十秒。不要在它下面写一层密集的织体,还指望尾音会自己让开。
- 钢琴有真正的踏板。
sus区在制音器抬起时发声,所以perform.ts本来就写在每次和声变化之后的换踏板都听得见,而一个和弦留在下一个和弦底下继续鸣响,正是真正的演奏者会做的事。opts: { pedal: false }让踏板停着不踩。 - 真实马林巴的低音很丰满。 D2 到 C4 的分量和它的最高处一样足。让低音线避开旋律的音区,而不是往下够一个放不到曲调下面去的音。
两个选项写在声部的 opts 里:
| 选项 | 作用 |
|---|---|
variant: "upright" / variant: "tubular" | 选用这件乐器的第二份录音 |
sampled: false | 不管装了什么,这个声部都保持建模音色 |
node tools/music.mjs check 会打印划分结果,所以在相信乐器表之前先读它:
一个你以为会用录音、结果却是建模的声部,要么超出了音域,要么选择了退出。两种情况都要你来修;哪一种都不是悄无声息的回退。
素材格式
把配乐写在影片自己的文件里,放在影片模块旁边:在脚手架生成的项目里是 src/canvas-core/<film>Score.ts(在 skill 内部是 engine/src/canvas-core/<film>Score.ts),然后导出它:
import type { Material } from "./music";
export const score = (): Material => ({
style: "<style id>", title: "<this film's score>", seed: <any integer>, mood: "<mood id>",
bpm: <tempo in the style's range>, key: "<tonic>", mode: "<mode id>", meter: "<meter>",
moodControls: { energy: <0..1>, warmth: <0..1>, brightness: <0..1>, tension: <0..1>, space: <0..1> },
swing: <0.5 straight .. 0.67 hard>, dyn: [<start 0..1>, <end 0..1>], tail: <seconds of ring-out>,
chords: { "<name>": { voicing: "[<note> <note> <note>]", bass: "<one bar of notation>" }, /* ... */ },
motifs: { "<motif>": "<one or more bars of notation>", /* your variations are motifs too */ },
grooves: { main: { family: "<family>", density: <0..1>, variation: <0..1> }, half: { family: "<family>" } },
sections: [
{ kind: "intro", bars: <n>, harmony: ["<chord>", /* per bar, cycled */], chordVel: <0..1> },
{ kind: "hook", bars: <n>, harmony: [/* ... */], lead: ["<motif>", "<motif variant>", "r:<beats>"], stretch: true, energy: <0..1> },
{ kind: "groove", bars: <n>, harmony: [/* ... */], lead: ["<2-bar motif>"], loopLines: true },
{ kind: "verse", bars: <n>, harmony: ["<chord>", "~"], comp: ["x:1 r:.5 x:.5 r:2"] }, // held over the bar line; a comped rhythm
{ kind: "bridge", bars: <n>, key: "<new tonic>", mode: "<mode>", harmony: [/* in the new key */], lead: [/* ... */] },
/* ... */
{ kind: "outro", bars: <n>, harmony: ["<home chord>"], lead: ["<the motif's last word>"] },
],
});
影片用 canvas-core/score.ts 里的 filmScore(score(), fps, frames) 来播放它。它会把你的曲式适配到精确的长度。score.ts 是一个可以复制的空骨架。
- 音色:
voices: { lead: "<alternate name>" }把一个槽位换成风格的某个备选音色。或者给出你自己的{ inst, role, opts }(乐器见 index.md 里的表),带上该槽位的角色:chords和arp是"accomp",lead是"melody",counter是"color",bass是"bass",鼓轨是"drum"。电平是同一把尺子。 调色板里的音色、备选音色和你自己的音色,都放在该槽位的分轨目标上(styles/vocabularies.md 里的那一列,修正值也算在内):备选音色带一个实测的修剪量,而你自己的音色根据它的乐器在你的音符所在音区里的实测电平来摆放。所以别写你音色的gainDb(0 = 正好在目标上),只在需要偏移时像levels一样设它:gainDb: 2就是高出两 dB。然后读check的分轨结果,按它打印的 dB 来调。选项(attack、bright、drive)会让电平稍微移动一点;钢琴弹到很高的音区时,天然就是安静的。variant和sampled是两个会改变你得到哪种声音的选项,它们在上面的录音乐器里。 - 循环:
loop: true,曲式的长度就是循环的长度。render会让它无缝衔接。 -
拆分小节:
harmony: ["<chord> <chord>"]在拍号的划分处拆分一小节,而不是在算术上的中点。给那一段一个bass覆盖值来配合它。拍号 2 个和弦 3 个和弦 2/4 1 + 1 平分 3/4 2 + 1(在第 3 拍换) 1 + 1 + 1 4/4 2 + 2 2 + 1 + 1 5/4 3 + 2 2 + 1 + 2 6/8 1 + 1(3 + 3 个八分音符) 平分 7/8 2 + 1.5(4 + 3 个八分音符) 1 + 1 + 1.5(2 + 2 + 3) 9/8 2 + 1 1 + 1 + 1 12/8 2 + 2 2 + 1 + 1 拍指的是拍号的拍(在 6/8、9/8 和 12/8 里是附点四分音符)。想用另一种方式感受小节,就设定这首曲子的
grouping(例如grouping: [2, 3]表示按 2 + 3 感受的 5/4):拆成和分组一样多的和弦时,拆分会跟随分组。或者给每个和弦写上它的拍数:"Dm9:1 G13:2"。 - 延留的和弦:"~"把它前面的和弦延续过小节线,不重新敲击,所以铺底音色永远不会在小节线处掉下去:harmony: ["Cmaj9", "~", "Am9", "~"]是两个各占两小节的和弦。"~ G13"先延续进这一小节,然后再换。在延留的和弦下面,低音保持自己的线条(和弦的低音线会再奏一次)。要让低音也连起来,把这一段bass里这一小节写成"~"。 - 和弦伴奏节奏: 段落的comp给和弦声部一个节奏,每一项一小节,在段落内循环。x:DUR敲出那一刻正在发声的和弦,r:DUR休止,x:DUR@0.7加重音,一小节还可以以~:DUR开头,把上一次敲击延续过小节线(一个提前的和弦):comp: ["x:1 r:.5 x:.5 r:1 x:1", "r:3.5 x:.5", "~:1 r:1 x:2"]。comp 的每一小节加起来必须等于一整小节。没有comp时,每个和弦在它的整个跨度内发声。 - 无声的和弦:voicing: "r"让和弦声部休止,而这个和弦仍然指明和声:调性检查、craft、新颖度和低音都会读它。用于由低音或琶音单独陈述的小节,或者 stop-time 式的停顿。 - 旋律线只奏一次。lead、counter和arp从段落的第一小节开始,只奏一次,即使harmony在循环。较短的线条会留下休止,check会警告。设loopLines: true让线条重复,直到填满段落。repeat: n重放整个段落,旋律线也一起重放。 - 段落的energy(0..1,0.5 = 按写的来)设定段落里每个声部的力度,手写的旋律线也算在内。它把力度缩放到 0.55 倍到 1.45 倍,大约 -5 到 +3 dB。它也设定生成律动的密度。 -dyn是整首曲子的力度级别[start, end],渐变过渡(默认[0.62, 0.66])。swing和tail的默认值分别是风格摇摆范围的下限和 3.2 s。 - 转调: 给一个段落它自己的key和mode。调性检查随后会分别读每个调性区域。最后一遍副歌升一个全音没问题,只要那一段声明了新调。 - 调性检查重视主音。 当调式的音阶对这些音符的解释不逊于任何其他读法,并且主音听得见(至少占音符时长的 8 %)时,它接受你声明的主音和调式。一段 lydian 或 dorian 的音乐,如果音符倚重另一个音级,就会被读成那个音级的大调。在段落开头和终止处把低音放在主音上,并亮出调式的色彩音。 - 写出来的鼓: -LiteralGroove按轨给出记谱的小节(kick、snare、ghost、hat、perc)。 - 这些小节从每一段的第一小节开始循环;cycle: "piece"改为按整首曲子的小节计数来循环。 - 无音高的鼓忽略写下的音高:写C4就行。 - 鼓轨里的定音鼓(orchestral、choral、suspense、world)会调到key的主音上,生成的律动也一样(epic)。
记谱法(逐小节校验;加起来不对的小节会抛错,最后一小节也一样:在小节中间结束的线条要写出它的休止,C2:3 r:1,绝不留无声的空缺):
- 一个记号是
NOTE:DUR[@VEL](带八度的音名,例如F#4)、[NOTE NOTE ...]:DUR(一个和弦)或r:DUR(一个休止)。 - 小节之间用
|分隔,或者作为数组的各项给出。 - 时值以拍计。在 6/8、9/8 和 12/8 里一拍是附点四分音符,所以一个八分音符是
1/3。在 7/8 里一拍是四分音符,所以一个八分音符是.5,一小节是 3.5。 @VEL缩放这条线的力度(0..1)。- 在手写的 piece 里,落在任意拍上的一小节 sting 要这样声明:
line(t, "G6:1", { ..., hit: true })。
是什么让它动听(说它完成之前先检查)
- [ ] 一个想法,加以发展。 一个动机,听到 6-12 次,每次都不一样。第二个想法回答它,或者与它形成对比。
- [ ] 一条你能唱出来的线条。 以级进为主,跳进之后折回,拱形,每个乐句组在后半段有一个最高点。
- [ ] 问与答。 乐句以 2 和 4 小节为单位。问句开放结束,答句回到家。
- [ ] 感觉在不协和里。 拍点上放和弦音。画面需要一阵酸楚的地方放一个倚音或挂留音,向下级进解决。
- [ ] 平滑移动的和声。 保持共同音,声部级进,低音与曲调反向进行,低音区展开。
- [ ] 会呼吸的律动。 稳住的底鼓和反拍,上面加切分,旋律里有休止,只在段落结尾加花。
- [ ] 一道弧线。 按段落在密度、音区、和声节奏和律动上形成对比。峰值之前一次回落,峰值落在关键帧上,然后释放。
- [ ] 终止式落在画面拍点上。 主张、揭晓和 logo 各自得到一个结构性的强拍。
- [ ] 结束在一个乐句上: 一个延长的主和弦加上动机的最后一句话、一个收尾短音(button),或者(只限悬疑)一个不解决的延长。
- [ ] 情绪线索一致。 速度、调式、音区、密度、演奏法和明亮度都指向对方用的那些词。如果听的人说出了另一种情绪,错的是选择,不是听的人。
- [ ] 广度。 跨项目地变换拍号、和声节奏、律动家族和曲式长度。给两个产品写的两份配乐不应该是兄弟姐妹。
什么检查什么
| 检查 | 在哪里运行 | 什么时候不通过 |
|---|---|---|
调/调式(planProblems) | check、render | 某个调性区域的音符撑不起它声明的主音和调式 |
| 作曲警告 | check、render | (只警告)一条旋律线比它的段落短,…… |
| 母带 | check、render | (只提示)峰值天花板让增益在达到目标之前停下 |
| ghost 守卫 | check、render | 4 小节窗口没有形状(起音点少、持续、湿、没有终止式) |
| reverb 守卫 | check、render | 后期尾音落在干声 10 dB 以内 |
| masking 守卫 | check、render | 旋律在 500 Hz-4 kHz 上没有压过其他声部 |
| 分轨 | check、render --stems、stems | 某个声部偏离目标超过 3 dB(主奏:允许高出最多 +6 dB);对未校准的风格只作参考 |
| craft | craft、check | (参考性的得分和修法)旋律、和声、节奏、紧张度、情绪契合度;只在出现原声乐器奏不出的音时不通过 |
| 新颖度 | check、novelty | 与某首已交付的曲子(或彼此之间)相似度高于 0.5,复用了一个 6 音旋律片段,或者在任意调、任意拍号、任意节奏下出现同一个 8 音旋律形状 |
tools/music-unit.mjs 让引擎保持诚实:确定性、没有默认值、每套词汇都能渲染。