らくとあいすの備忘録

twitter : lactoice251

第1回 GLSLで音を作るために

こんにちは。らくとあいすです。 以前「GLSLで音を作る」という記事を書いたところ、ありがたいことに色々と反響をいただきました。 raku-phys.hatenablog.com せっかくなので、より詳しい記事を書いていこうと思ったのですが、私も始めたばかりということもあって日々色々なやり方が変わっていきます。 なので、長編の記事として、「これを読めばわかる」といったようなものが書きづらいといった状況にあります。 そこで今回から、GLSLサウンドに関連したちょっとしたお話を何回かに分けて(思いついたときに...)書いていこうと思います。(続くことは保証しません) 例によって今回も twigl.appサウンドシェーダーを開きつつ読み進んでいただければと思います。

GLSLサウンドにおける時間の捉え方

GLSLサウンドでは、あらゆる部分が時間の関数となります。 GLSLで絵を作る場合は、縦横と時間の3軸、立体的な絵の場合は4軸考えたりすることもあると思いますが、サウンドは基本的に一軸です。 ただし、その一軸はいくつかの階層的な構造を持っていると、私は考えています。

ここでは、私なりに考えたGLSLサウンドにおける時間軸の階層性を、次の例で説明します。

#define pi2 6.2631
#define bpm 118.0
float fm(float t,float f,float i,float r){
  return sin(pi2*f*t+i*sin(pi2*f*t*r));
}
float calf(float i){
  return pow(2.0,i/12.0);
}
vec2 mainSound(float time){
  float tbpm = time*bpm/60.0;
  float[8] seq_line = float[](0.,7.,5.,10.,7.,3.,5.,-2.);
  float seq_time = mod(floor(tbpm),8.0);
  float seq_freq = 440.0*calf(seq_line[int(seq_time)]);
  float seq = fm(time,seq_freq,0.5,1.0)*fract(-tbpm);
  return vec2(seq*0.5);
}

上記のコードで生成した音を、次の再生ボタンから聴いてみて下さい:

8つの音が連続して流れたのがわかると思います。実際、上記のコードはこの8つの音を連続して繰り返すだけのシンプルなものです。 この8つの音のカタマリが一つめの時間の階層です。 フレーズ、パターン、リフ、シーケンスなど色々言い方はあると思いますが、このような数秒程度の音のカタマリは音楽を構成する上で良く使われます。 さて、この8つの音の波形は、次のようになっています。

f:id:Raku_Phys:20200520212015p:plain
8つの音の波形

8つの音の音程の変化は(波形上はささいな変化なので)この波形から読み取ることは難しいですが、各8音が規則的な音量変化をしていることがわかると思います。 この各音の時間変化が、二つ目の時間の階層です。一般に音は、鳴り始めから鳴り終わりまでの間に音色が変化します。 例えば、ピアノではハンマーが弦を叩く初めの瞬間、そして弦が強く振動し最後には次第に弱まっていきます。太鼓では、叩いた瞬間の鋭い打撃音の後に、太鼓の内部での音の響きが残ります。 このような、1音1音の持っている音の時間変化や音量の時間変化は、音色を決める重要な要素となっています。 さて、この波形をさらにクローズアップして見てみると次のようになっています。

f:id:Raku_Phys:20200520212340p:plain
拡大した波形

丸まったノコギリの歯のような形の波形の繰り返しが見えると思います。この一つ一つの波形が、三つ目の時間の階層です。 この波形が音を取り扱う上での(ひとまずの)時間的の最小単位で、音色を決めるもっとも根本的な要素になっています。

実装

時間の階層構造は捉え方の話ではありますが、私の場合実装上でも意識的に時間の階層を取り扱っています。

まず、1つ目と2つ目の階層の時間を扱う上で重要となるものがBeats Per Minute (BPM) です。 おそらくこの記事を読んでいる読者の方々であればBPMという言葉自体はご存じかと思いますが、その名の通り1分間あたりのbeatの数です。 では、このbeatがどのように決まっているかというと、実際のところ厳密な定義があるわけではありません。 とはいえ、そうそう意見が割れることもなく、例えば4つ打ちのダンスミュージックであればバスドラムがbeatとなることが多いですし、8beatであれば (その名の通り) 8分音符で刻まれるハイハットの倍の長さがbeatとなります。ざっくりとは体でノれるリズムの単位がbeatで、その1分あたりの数がBPMと思っておいて良いかと思います。 そして、曲を構成するあらゆる音はこのBPMを単位として、その1/2や1/4のの上におおよそ乗ることになります。 したがって、その1音1音の音の変化や、それらが集まったフレーズはこのBPMを単位とした時間で扱うと色々と都合が良いです。 そこで、まずはじめに、実時間 (time) をbpmを基準とした時間 (tbpm) に次のように変換します:

float tbpm = time*bpm/60.0;

mainSound関数の入力として与えられる時間は秒を単位としているので、そのまま1秒をbeatとするとBPM=60となります。そこで、その実時間を60で割った後bpmを書けることでBPMを基準とする時間に変換することが出来ます。

このBPMを基準とした時間 (tbpm) を用いて1つ目の階層 (フレーズ) の時間は次のように表しています:

 float seq_time = mod(floor(tbpm),8.0);

フレーズの時間は連続的な変化をするものではないので、まずはfloorで離散化し、そのあとmodで折り返すことで8つの音の繰り返しを作っています。

2つ目の階層 (1音の変化) の時間は次のように表しています:

fract(-tbpm)

音の変化 (ここでは音量のエンベロープ) は連続的なのでここでは、floorでつぶさずに連続的なまま扱うことにし、fractを使って1beatごとに折り返すことにします。

最後に波形を扱うための三つ目の時間としては、mainSoundの入力として与えられる実時間 (time) をそのまま使います。これは各波形の繰り返しの速さ (=周波数) はBPMなどに関係する量ではないからです。 コード上では、fm波形を生成するための関数の入力として実時間 (time) をそのまま与えています。

おわりに

今回はGLSLサウンドを書く上で普段意識している、時間の捉え方について書きました。 色々とごちゃごちゃと書いてしまった気がしますが...要は目的に応じて使いやすいように時間を加工しておくと便利ということだけ知ってもられば良いのかもしれません。 コード内には今回解説していない要素も色々と含まれていますが、今後もしかすると解説するかもしれません。 また、質問等あればお気軽に投げていただければと思います。 それでは、お読み頂きありがとうございました!