らくとあいすの備忘録

twitter : lactoice251

GLSLで音を作るために 番外編

こんにちは。らくとあいすです。 「GLSLで音を作るために」第二回の前にしてさっそく番外編となってしまいました。 今回は5/30に twigl.app及びVRChat上のクラブで配信したGLSLサウンドを用いたDJについて、記憶の新しいうちに書いておきます。

性質上、ここでは1mixしか聴けませんがライブ時に使用したURLを載せておきます。 これ自体を解読することはあまりオススメしませんが...。 twigl.app

やったこと

GLSLを用いて、僕の好きな曲や自分の曲を作りこみ、そしてそれをライブで繋いだり混ぜたりするDJのような形式で発表しました*1。 これは、1. 各トラックの作りこみ、2. 作ったトラックを使ったDJ風プレイの大きく二つの工程に分けられます。1については、書くことが膨大過ぎる上にケースバイケースなので、本記事では概略に触れるに留めます*2。2については、GLSL特有の面白く、わかりやすい部分が結構あるので、少し具体的なコードも交えつつ解説したいと思います。

各トラックの作りこみ

トラックの作りこみはおおよそ次のようなフローで行いました:

  1. 楽曲の聴きこみ
  2. 要素の取捨選択と代替
  3. 音色の作りこみ
  4. 音程とリズムの構成
  5. 展開の構成

楽曲の聴きこみは、まずさっと一通り聴いてから集中して聴く箇所を絞ります。各パートをザクっと紙に書き起こした後、それぞれのパートに集中して聞き返しながらディティールを写しとっていきます。 この時、パートをしっかりと最小単位まで分解しきることが恐らく後の作業をスムーズにするように思います。

次に取捨選択ですが、GLSLサウンドでやる都合上、明らかにコストがかかりすぎる音があったりします。例えば歌や、複雑な構造の生楽器などはなかなか再現が難しいものです。 そこで、それらの要素を落として曲が成立するか吟味した上で、それを引き算した時に空いた空間を別の要素で補填します。 生楽器演奏でも、同じ曲を表現するのにピアノで弾くのとトランペットで吹くのでは、再解釈が必要となります。GLSLサウンドへの変換でも同様に、原曲を損なわないために原曲と表現を変えるということが必要だと思っていて、ここが一番のポイントのような気がしています。

音色の作りこみは、感覚と測定の両面で進めていきます。まず、各楽器を聴いたときの感覚で、この音を表す最もシンプルだと思われる波形を数式に表します。ここでは、サインなのか、のこぎりなのか、倍音の多いFMなのか、といったぐらいの粒度です。次に生成した波形と原曲の波形を、Audacity等の波形編集ソフト上で比較します。また、スペクトログラムも比較していきます。ここで、おおよそ、どんな倍音成分が必要かがわかってくるので、試行錯誤して所望の波形に近づけていきます。最後に、音を聴きなおして原曲に合う合わないによらず、単体として良い音になるように感覚で微調整しておおよその音作りを終えます。

音程とリズムの構成は、ピアノロールに打ち込むように、音を譜面に落とし込んでいく作業です。ただ、コーディングベースのサウンド全般に言えるかもしれませんが不規則な動きや、一部分だけ連符になっていたりする楽譜に弱いので、ここでも再解釈が必要になることがあります。その不規則性が、曲の中でどうしても必要な要素と解釈されれば、コード量を尽くして再現しますが、別の表現で代替可能と思えば書きやすいように表現を少し変えたりします。このあたりは、ケースバイケースですね。

展開の構成は、曲の展開の中で使いたい部分を再現したり、実際に回すときに変化をつけやすいように曲を分解する作業です。 今回は、各曲をひとつの関数にまとめたので、そのインプットとして通常実時間のみを取れば良いのですが、展開切り替え用にintを一つ変数にとっています。 このint変数の値によって、組み合わせるパートを変化させて、曲の展開を作っています。

このように、音色の作りこみ方法を除けば、おおよそは通常のDAWや生楽器を使用した楽曲のカバーと、同じようなフローになっているように思います*3

作ったトラックを使ったDJ風プレイ

まず、突飛に聞こえるかもしれませんが、レコードを回すこととGLSLサウンドで曲を作った関数を呼び出すことは、本質的に同じだと思っています。 レコードは、事前に音の波形を溝の深さとして記録しており、それをターンテーブル上で回すことで、針が指す時刻の溝の深さを次々と読み取って音に変えていきます。 GLSLサウンドも同様で、各トラックを作りこんだ関数はレコードに相当し、そして針の代わりに時刻 (time変数) を入力することで返り値として、溝の深さに相当する波形の値を読み取ることが出来るわけです。

このような、気持ちで持ってコメントを付けたGLSL Sound DJの最小コードは次のようなものが考えられると思います:

#define bpm 132.0
#define pi2 6.2831
//フェードイン関数(a拍目~b拍目)
#define fin(a,b) smoothstep(a,b,time*bpm/60.0)
//フェードアウト関数(a拍目~b拍目)
#define fout(a,b) (1.0-smoothstep(a,b,time*bpm/60.0))

//A(440Hz)からの相対周波数計算関数
float calf(float i){
  return pow(2.0,i/12.0);
}
//一つ目のレコード(内容は一例です)
vec2 recordA(float time){
  float t = time*bpm/60.0;
  float bd = sin(3e2*time)*fract(-t);
  float hh = fract(sin(time*12.1)*4e5)*pow(fract(-t+0.5),20.0);
  return vec2(bd+hh)*0.5;
}
//二つ目のレコード(内容は一例です)
vec2 recordB(float time){
  float t = time*bpm/60.0;
  vec4 sl = vec4(0.,7.,9.,14.);  
  int st = int(mod(floor(t*2.0),4.0));
  float seq = sin(pi2*time*440.0*calf(sl[st]))*fract(-t*2.0);
  return vec2(seq)*0.5;
}
vec2 mainSound(float time){
  float t = time*bpm/60.0;
  //ターンテーブルを逆回転
  float itime = -time;
  //ターンテーブルを行き来(スクラッチ)
  float sctime = sin(pi2*t)*0.5;
  //ターンテーブルの回転速度を指数関数的に加速させる
  float etime = exp(time);
  //ターンテーブルを何回か引き戻す(スタッター)
  float stime=(t<2.0)?mod(time,pow(2.0,-floor(t))*15.0/bpm):time-120.0/bpm;
  //********************************************//
  vec2 table1 = vec2(0.0);//空のターンテーブル1
  vec2 table2 = vec2(0.0);//空のターンテーブル2
  table1 = recordA(time);//ターンテーブル1にレコードAを乗せる
  table2 = recordB(time);//ターンテーブル2にレコードBを乗せる
  vec2 master = table1 + table2;//masterアウトにmix
  return master;
}

以下は、上記のコードについてかいつまんで解説します。是非 twigl.app で実際に触りつつ読み進めてもらえればと思います。

twiglの基本操作のおさらい

念のため基本操作をおさらいします。まず、twiglにアクセスした後、SoundShaderを有効にし、上記コードをSoundShader用のエディタにコピー。次にalt+Enterを押すことでコンパイルされ、再生が始まります。alt+ctrl+Enterで再生ストップです。何か変更を加えた場合はalt+Enterで再コンパイルすることで、反映されます。

GLSL Sound DJ最小コードの解説

まず、ここでは二枚のレコードを作成します (recordA, recordB)。本来は各レコードについて前節でお話したような方法で作りこんでいきますが、ここではごく簡単なサンプルを入れてあります。 次に、メイン関数の下6行ですが、ここでレコードをターンテーブルに載せて再生・ミックスに相当する操作を行います。このサンプルでは、シンプルに二つのターンテーブルにレコードを載せてそのまま再生しています。BPMについては、#defineで指定しているので、元々合うようになっています。

DJにおいて、必須ともいえるフェーダーの再現は、#defineで冒頭に定義されているfin及びfoutを用いて行うことが出来ます*4。これを使うと例えば次のようにrecordAとrecordBをクロスフェードできます:

table1 = recordA(time)*fout(0.,16.);
table2 = recordB(time)*fin(0.,16.);

二つの因数は、フェードの開始と終了です。これを使って、2つのトラックをパキっと入れ替えることもできます:

table1 = recordA(time)*fout(7.5,8.5);
table2 = recordB(time)*fin(7.5,8.5);

多分、DJの方はフェーダーを上げ下げする速度も気を使っていると思いますが、そこまでパラメータを含めるとやはりまた煩雑になるので、ここではGLSLのsmoothstep関数で統一しています。 どうしてもやりたくなれば、その都度書けば良いという話ではあります。

次に、DJと言えばお皿を触って色々するイメージ(ド素人)なので、愚直に実装してみたのがmain関数の初めの数行 (itime,stime,etime,sctime) です。 レコードを触ること、すなわち相対的には針の位置を変えることは、GLSLにおいては時間 (time) を加工することに相当します。 ターンテーブルの逆回転は、time = -time と、時間を反転させることで再現されます:

table1 = recordA(time);//そのまま再生
table2 = recordB(itime);//逆再生

ターンテーブルをごしごしとその場でいったり来たりさせるスクラッチは、sin等の関数を使って時間をいったり来たりさせれば実装出来ます:

table1 = recordA(sctime);//スクラッチ
table2 = recordB(sctime);//スクラッチ

スタートから数拍だけ時間を折り返すと、スタッターと呼ばれる効果を作ることが出来ます(物理お皿でやることがあるのかはわかりません):

table1 = recordA(stime);//スタッター
table2 = recordB(stime);//スタッター

お皿をどんどん加速させることも、時間を指数関数などでどんどん早く進めることで実現可能です:

table1 = recordA(etime);//指数関数的にお皿を加速
table2 = recordB(time);//そのまま再生

他にも色々な操作をtimeに加えて、それを各レコードを読みだす時間 (針) に入れて上げることで、面白い効果を得ることが出来ます。 是非色々と組み合わせて遊んでみて下さい。

最後に

GLSL Sound DJはたしかにハードルが高くお気軽にオススメ出来るようなものではないのですが、作りこめば作りこむほど音になって返ってきてくれる楽しさがあります。 簡単なレコードを作って混ぜ合わせるところから、是非トライしてみて下さい。わからないことや間違えがあれば、Twitterかコメントにお願いします。

謝辞

GLSL Sound DJは、doxasさん(h_doxas (@h_doxas) | Twitter) の開発された、twiglのGLSLライブ配信環境に依拠するところが非常に大きいです。 維持、開発してくださり本当にありがとうございます。twiglを用いて度々セッションしてくれているブタジエンさん (ブタジエン (@butadiene121) | Twitter )、いつもありがとうございます。 ブタジエンさんの作る、Visualがアイデアのかなりの部分を支えてくれています。最後に、今回のDJで楽曲を使用させて頂いた方々にお礼の意を込めてセットリストとリンクを載せて終えたいと思います。

  • バーチャルユーチューバーのいのち (キヌ)

www.youtube.com

  • O.D.E.N (obake)

digitalghost.booth.pm

www.youtube.com

  • Tell Your World (kz)

www.youtube.com

  • TECHNO VINEGAR (obake)

www.youtube.com

  • Cloud Identifier: 集合知に遍在する残滓による自意識の再構成 (memex)

www.youtube.com

  • さざなみ (Sound: らくとあいす/VJ ブタジエン)

twigl.app

*1:僕はDJをやったことも、コントローラーに手を振れたことも、物理現実で見たこともないので、あくまでDJの"ような"ですが...。

*2:実際これは準備が相当に大変でゴールデンウィークの代休を利用して、木曜の夜から土曜の夜までぶっ続けで書き続けてようやく間に合った感じでした。

*3:DAWを使用した楽曲のカバーをしたことはないですが...

*4:普通の関数で書いても良かったのですが、ライブでやる都合上time変数を代入する文字数を書くのが大変なので、defineを使って省略しています。