Planefillerの音作り
8k Introとは、映像・音楽のすべてが8KBの実行ファイル(.exe)から生成される映像作品のことを指します。 https://youtu.be/-OT-wFXbjk4?t=96
以下の1ループに焦点を合わせて解説します。
https://scrapbox.io/files/675f0781db0bb7b6e466c678.ogg
Wavenerdのコードエディタに放り込んで Ctrl+R 叩いたら鳴らせます。
対象読者
あなたのための記事です。シェーダーを使った音作りのノウハウをいろいろ共有できると思います。 オススメです!Grooveboxやモジュラーシンセと近い感覚の作曲プロセスについて話します。
ただし、これが4k・8kにおける唯一の音楽の作り方ではないことを念頭に置いて読み進めてください。
シェーダで絵を作る方
絵で使うシェーダとは全く違う世界観が垣間見れると思います。
とにかく、音楽の作り方を知りたい方
どのようなパート構成で音楽が作られているかは解説できると思います。
そもそも、Planefillerはどういう環境で作られているの?
MinimalGLを使えば、なんと映像用のGLSLコードと音楽用のGLSLコードを用意するだけで4k Introが作れます。自分でC++を書く必要は一切ありません。
なんでみんな4kやらないの!こんなに簡単なのに!!
https://gyazo.com/13dfe9151379c5f1a2f4ee2fd0520b78
つまり、MinimalGLで作られたIntroの音楽はGLSLで作られているということですね。
すなわち、Planefillerの音楽もGLSLで作られています。
Shader Musicについて
0b5vrは、デモを作る際は基本的にシェーダで音楽を作ることが多いです。
通常のDAWで当たり前に扱えるようなシーケンサー・シンセ・エフェクターなんてものは全て自分で定義しなきゃいけないですし、リバーブやフィルタといった極めて基本的なエフェクターも扱うことができません。 また、今回はとりわけIntro向けの音楽なので、一切のサンプルを利用することもできません。
決して万人にオススメできる作曲手段ではないものの、一から自分でシーケンサーやシンセを作って、自分だけの出音を目指したい人はぜひ試してみてください。
音楽のコンセプト
そもそも前提として、PlanefillerはCompofillerであり、割と手癖で適当に作っている側面が強い作品です。 そんな中でも、とりあえず映像・音楽全体のディレクションとしては「パーティー」「レイヴ」「ダンスフロア」「Banger」みたいな要素を意識しました。 https://youtu.be/m3GMlorAQYY
https://youtu.be/6wilYaYaj3E
音楽のパート構成
Planefillerの音楽のパート構成は以下のとおりです:
ドラム: kick, hihat, open hihat, fm perc, rim, ride, clap, snare909, crash
シンセ: bass, choir, arp
ここからは順にパートごとの解説を行っていきます。
ドラム
ドラム全体のリズムは以下のような感じです。
あまりひねりのない素直な打ち込み方だと思います。
code:rhythm
kick: x...x...x...x...
hihat: xxxxxxxxxxxxxxxx
open hihat: ..x...x...x...x.
fm perc: xxxxxxxxxxxxxxxx
rim: .xx.xx.xx.xx.xxx
ride: x.x.x.x.x.x.x.x.
clap: ....x.......x...
スウィング
Swingは、16分の間隔のリズムの偶数を少し後ろにずらす手法です。 リズムが「ハネ」ることにより、グルーヴ感が出ます。
こちらが、8分音符を50:50で分割した場合。
https://scrapbox.io/files/675f96c97398d8bfd7d4fff5.ogg
こちらが、8分音符を64:36で分割した場合。
https://scrapbox.io/files/675f96cdc5a2b20f6dd0921d.ogg
リズムの印象がグッと変わるのがわかると思います。
実はいままでShader Musicでのスウィングの打ち込み方をいろいろ試行錯誤していたのですが、今回やっと t2sSwing ・ s2tSwing という関数にパッケージングすることができました。 → Swing サイドチェイン/ダッキング
ダッキング(サイドチェイン)は、キックの音に合わせて他のパートの音を抑制し、キックの音を目立たせて出音全体にメリハリを付ける手法です。 本来はコンプレッサーなどのエフェクターを用いて実現する技術ですが、私はキックのタイミングで制御する sidechain という変数を用いて、キック以外のパートを抑制しています。 (最近は、これは「サイドチェイン」というより「ダッキング」のほうが意味合いとして正しいことに気づき、また変数名としてもより親しみやすいことから、 duck を代わりに使うように変えています。)
こちらがダッキングがない場合
https://scrapbox.io/files/675f077c7fd8bf9a6faac5b5.ogg
こちらがダッキングのある場合
https://scrapbox.io/files/675f0781db0bb7b6e466c678.ogg
こちらも、グルーヴ感を演出するのに大変重宝する手法です。
キック kick
https://scrapbox.io/files/675f059c8fff2684e0975f49.ogg
基本的には、サイン波を用いて、ピッチエンベロープでピッチを高いところから50Hz付近に落とすのがセオリーです。 ただし、シェーダにおいては周波数の変化をエンベロープとしてそのまま使うことはできず、代わりに位相の変化でサイン波を制御することになりますのでご注意を。積分ってやつです。 まあ怖がらず、適当にガチャガチャやっていい感じの音を探しましょう。
今回は3つの exp2 を用いて、高いところから40Hzまでピッチを落としています。
exp でなく exp2 なのは圧縮効率を考えてのことで、出音上の意味はありません。
また、サイン波はtanhを使って歪ませています。 tanh は歪み系エフェクトです。 code:glsl
vec2 phase = vec2(40.0 * t); // ベースは40Hz
phase -= 9.0 * exp2(-25.0 * t); // 緩やかなピッチエンベロープ
phase -= 3.0 * exp2(-50.0 * t); // 気持ちタイトめのピッチエンベロープ
phase -= 3.0 * exp2(-500.0 * t); // とても鋭いピッチエンベロープ
wave += sin(TAU * phase); // サイン波生成する
dest += 0.5 * env * tanh(1.3 * wave); // tanhで歪ませたうえでエンベロープと掛け算
また、ブレイク部分では、エンベロープを短くすることで低周波を鳴らさない、HPFに近いことを行っています。 コード内 hi-pass like と書かれている箇所ですね。
ドロップに差し掛かった時に低域が鳴るように戻し、ドカンと曲を盛り上げる役割があります。
https://scrapbox.io/files/675f05a3ca955400c685a595.ogg
ハイハット hihat
https://scrapbox.io/files/675f06147398d8bfd7d0c0eb.ogg
とりあえず高い周波数のサイン波をたくさん鳴らせばハイハットっぽい音が鳴るだろう、というのが基本的なコンセプトですね。最近使うハイハットはほぼこれです。
code:glsl
vec2 shotgun(float t, float spread) {
vec2 sum = vec2(0.0);
repeat(i, 64) {
vec3 dice = hash3f(float(i) + vec3(7, 1, 3)); // 乱数
sum += vec2(sin(TAU * t * exp2(spread * dice.x))) // ランダムな周波数のサイン波を生成
* rotate2D(TAU * dice.y); // rotate2Dで音に広がりをもたせる
}
return sum / 64.0;
}
こちらも、私がハイハットを鳴らす際に乱用している手法ですね。
今回はfract step velocityに加えて、4つの16分音符のリズムのうち3発目を長めのエンベロープで鳴るようにしています。
code:glsl
float vel = fract(seq.x * 0.38); // fract step velocity
float env = exp2(-exp2(6.0 - 1.0 * vel - float(mod(seq.x, 4.0) == 2.0)) * t); // ベロシティと裏のタイミングを使ってエンベロープを制御
オープンハイハット open hihat
8分音符の偶数で鳴る長めのハイハットです。リズムの裏を強調し、グルーヴ感を高める役割を持っています。 https://scrapbox.io/files/675f061a67be7450e559e9ff.ogg
これは比較的最近獲得した新手法なのですが、FM変調で作った高周波帯の音をユニゾン8本くらいで鳴らすことで、ハイハットやシンバルなどの金物らしい音が鳴ってくれます。 code:glsl
repeat(i, 8) {
vec3 dice = hash3f(vec3(i)); // 乱数
vec3 dice2 = hash3f(dice); // もうひと乱数
vec2 wave = vec2(0.0);
wave = 4.5 * exp2(-5.0 * t) * sin(wave + exp2(13.30 + 0.1 * dice.x) * t + dice2.xy);
wave = 3.2 * exp2(-1.0 * t) * sin(wave + exp2(11.78 + 0.3 * dice.y) * t + dice2.yz);
wave = 1.0 * exp2(-5.0 * t) * sin(wave + exp2(14.92 + 0.2 * dice.z) * t + dice2.zx);
sum += wave;
}
以下の音は、この repeat の繰り返し回数を 1 から 8 まで変化させながら鳴らしたもの。1音では無機質な高周波音が、ユニゾンの本数を増やしていくにつれてアコースティックな鳴りに変化していくのがわかると思います。
https://scrapbox.io/files/67618d634a19f79f9f0a10fd.ogg
FMパーカッション fm perc
FM変調を用いた不思議な音が鳴るパーカッションです。 役割としてはタムタムに近いかも。リズムと出音にスパイスを加える役割があるようです。 https://scrapbox.io/files/675f061dd0b1cc3c7de3f1c4.ogg
ステップごとに乱数でモジュレータの振幅や周波数を制御しつつ鳴らしています。
割と秘伝のタレ的に使っているため、あんまり意味や根拠は説明できないコードです。FM変調難しいもんね。 code:glsl
float freq = exp2(9.0 + 2.0 * dice.x);
float env = exp2(-exp2(3.0 + 5.0 * dice.y) * t) * smoothstep(0.0, 0.01, q);
float fm = env * exp2(2.0 + 4.0 * dice.z) * sin(freq * exp2(-t));
float wave = sin(fm);
SESSIONSで作品が上映されたあとに即席Write-upをしていたら、ukonpowerがこの音を聞いて「0b5vrの顔が浮き上がってくるよう」と話していました。 また、 dest に音を追加する際、 rotate2D(seq.x) のように、回転行列を掛けています。 これは、音の定位を常にLR中央にせず、色々な定位から鳴るようにして、音に広がりを持たせるためのものです。 → 音に回転行列 リムショット rim
こちらも、リズムにスパイスを加える役割のものですね。
https://scrapbox.io/files/675f0620ca955400c685a7d3.ogg
このリムショット音も多くの作品で乱用していますね。
ジェネレータは三角波が2本、それを足してtanhを使って歪ませています。 code:glsl
float wave = tanh(4.0 * ( // tanhで歪ませる
+ tri(t * 400.0 - 0.5 * env) // 三角波1個目 400Hz
+ tri(t * 1500.0 - 0.5 * env) // 三角波2個目 1500Hz
));
ライドシンバル ride
8分のリズムで静かに鳴るライドシンバルの音です。ミックス全体の高周波帯を埋める役割があります。 https://scrapbox.io/files/675f062632038d023d7cd65e.ogg
オープンハイハットと同様、FM変調とユニゾンを用いた手法を利用しています。
まだTR-909のライドと張れるかというと微妙ですが、十分に倍音が豊かなライドサウンドが出せるようになって嬉しいです。 code:glsl
repeat(i, 8) {
vec3 dice = hash3f(vec3(i));
vec3 dice2 = hash3f(dice);
vec2 wave = vec2(0.0);
wave = 2.9 * env * sin(wave + exp2(13.10 + 0.4 * dice.x) * t + dice2.xy);
wave = 2.8 * env * sin(wave + exp2(14.97 + 0.4 * dice.y) * t + dice2.yz);
wave = 1.0 * env * sin(wave + exp2(14.09 + 1.0 * dice.z) * t + dice2.zx);
sum += wave;
}
ハンドクラップ clap
さっきから「グルーヴ感」と何度も言っていますが、僕はグルーヴがなんだかわかりません。ノリで音楽をやっています。
https://scrapbox.io/files/675f06290cc1c7087ac9ce09.ogg
code:glsl
float env = mix(
exp2(-80.0 * t), // 後ろの残響
exp2(-500.0 * mod(t, 0.012)), // 最初の短いエンベロープ連打
exp2(-100.0 * max(0.0, t - 0.02)) // 最初は連打、その後残響
);
vec2 wave = cyclic(vec3(4.0 * cis(800.0 * t), 840.0 * t), 0.5, 2.0).xy; // ノイズ
dest += 0.15 * tanh(20.0 * env * wave);
スネアロール snare909
https://scrapbox.io/files/675f062fc57375e88e673c5f.ogg
ピッチエンベロープを掛けたサイン波にノイズを混ぜ、 tanh で歪ませればそれっぽくなります。
code:glsl
vec2 wave = (
cyclic(vec3(cis(4000.0 * t), 4000.0 * t), 1.0, 2.0).xy // ノイズ
+ sin(1400.0 * t - 40.0 * exp2(-t * 200.0)) // 鋭く1400Hzに落ちていくサイン波
);
クラッシュシンバル crash
https://scrapbox.io/files/675f0633e208da6f5f5c1fda.ogg
今回はチャラい曲なので、気持ちチャラめのクラッシュシンバルです。チャラいクラッシュシンバルってなに。
シンセ
ここからはシンセパートですね。
Chord (和音)
使用しているChordは以下で、名前にするとm7(11)とかになるのでしょうか。
マイナーコードの平行移動はRave系音楽で頻出する鳴らし方ですね。
https://gyazo.com/8d92088827081064df47aa59b4c290a7
Chordは int 型の配列 CHORD として、根音からの相対的な音階を整数で定義します。 code:glsl
const int N_CHORD = 8;
0, 7, 10, 12, 15, 17, 19, 22
);
code:glsl
#define p2f(i) (exp2(((i)-69.)/12.)*440.) ベース bass
ベースは楽曲全体の低音を補うのと同時に、Chordの根音を強調して響きを安定させる音です。
https://scrapbox.io/files/675f063ad028136dffe13a3d.ogg
コードの根音を取り出し、シンプルにtanhでちょっと歪ませたサイン波を鳴らしています。
code:glsl
float note = 24.0 + trans + float(CHORD0); // コードの基底音を取り出し、ベースのピッチを決定する float freq = p2f(note); // ピッチを周波数に変換
float phase = freq * t; // 周波数と時間から位相を計算する
float wave = tanh(2.0 * sin(TAU * phase)); // tanhでちょっと歪ませたサイン波
クワイア風シンセ choir
本楽曲の最も目立つパートであるクワイア(合唱)風Rave Stabです。リアルな人声というよりは、古いRoland音源のシンセクワイア的なサウンドですね。 Chordの構成音8つを一斉に鳴らしています。
https://scrapbox.io/files/675f063e117e89704ede5dc7.ogg
ユニゾンは、同じ音階の音を少しずつ周波数をずらしながら複数鳴らすことによって、音に厚みをもたせる手法です。
また、ノイズをWavetableとして利用するとDCオフセットが混入してしまい問題が生じてしまいますが、ユニゾンさせたり和音で同時に鳴らしている複数の波形にそれぞれ別の回転行列を掛けることによって、ノイズ同士がDCオフセットを打ち消し合っています。ついでに音に広がりも加わって一石二鳥。 code:glsl
repeat(i, 64) {
float fi = float(i);
vec3 dice = hash3f(float(i) + vec3(8, 4, 2)); // 乱数を生成
float note = 48.0 + trans + float(CHORDi % N_CHORD); // iに応じてChordの構成音を順に取り出す float freq = p2f(note) * exp2(0.016 * tan(2.0 * dice.y - 1.0)); // ピッチを周波数に変換、乱数でdetuneさせる
float phase = lofi(t * freq, 1.0 / 16.0); // lofiでレトロなPCMっぽい鳴りに
vec3 d = vec3(2.0, -3.0, -8.0);
vec2 wave = cyclic(fract(phase) * d, 0.5, 2.0).xy; // Cyclic NoiseをWavetableとしてサンプルする
sum += vec2(wave) * rotate2D(fi); // 回転行列でDC除去 + 音に広がりをもたせる
}
WavenerdにもVectorscopeを見ることができる機能があります。設定から探してみてください。
https://gyazo.com/1b3aa002928cb111df783a5a5e47f535
左はDCオフセットがあり良くない状態。右は良い状態
前半はこのようにエンベロープをやや短く鳴らしています。エンベロープに残響っぽい成分がありリバーブが掛かっているように聞こえますよね。
https://scrapbox.io/files/675f0648c223d36cd79ad287.ogg
code:glsl
env *= mix(
smoothstep(0.6 * l, 0.4 * l, t), // メインのエンベロープ
exp2(-5.0 * t), // 残響音用のエンベロープ
0.1 // どのくらい残響音混ぜますか
);
アルペジオ arp
クワイアの後ろで静かに鳴らしているアルペジオです。クワイアの音をちょっと可愛く派手にする算段です。 https://scrapbox.io/files/675f0641b4e9d4cbdd3f07e3.ogg
こちらもクワイア同様、Cyclic NoiseをWavetableとして鳴らしています。
取り出すコードの構成音を高速で変えつつ鳴らします。
code:glsl
int iarp = int(16.0 * t / B2T); // 1拍あたり16のペースで増加していく
float note = 48.0 + trans + float(CHORDiarp % N_CHORD) + 12.0 * float((iarp % 3) / 2); // iarpを用いてコードの構成音を取り出す float freq = p2f(note); // ピッチを周波数に
float phase = TAU * lofi(t * freq, 1.0 / 16.0); // lofiでレトロなPCMっぽい鳴りに
vec2 wave = cyclic(vec3(cis(phase), iarp), 0.5, 2.0).xy * rotate2D(time.w); // Cyclic NoiseをWavetableとしてサンプルする。iarpでも音色を変える
Choirといっしょに鳴らすとこんな感じ。極めて隠し味的ですが、やや派手な鳴りになりますよね。
https://scrapbox.io/files/675f06455efff43a4079de2f.ogg