Sound Shaderの解説 1
初めに
TDFにPC 4K Introに出したsound shaderについて説明していきます。
圧縮前のshaderは
このshaderは自分独自のformatのスクリプトを作り、それをGLSLにコンバートしています。 なので余計に難読になっていると思います。shaderに沿って説明してもわからない事が出てくると思うので、shaderのコードから、外れて説明することになります。
外れて説明するので、shaderには1文字のマクロ名を使ってますが、分かり易く長めのマクロ名に変更します。
音楽を作るのには、音源とシーケンサーが必要になります。最初はシーケンサーの説明からしますが、その前にGLSLでは使えない可変長配列をマクロを使ってfakeで実装するtrickを紹介します。これが肝です。これが出来ないとシーケンスを仕込むのは難しすぎです。
GLSLで可変長配列??
音楽で1小節の中に現れる音符の数は一定ではありません。これをシーケンスするのだから可変長配列が欲しくなります。しかしGLSLにはありません。で、アイデアです。定数として定義しないで、そのまま使う方法です。
int[](1,2,3,4,5)[1];
こんな感じです。でも、これだと何回もint[](1,2,3,4,5)
を書かないとなりません。そこでマクロの登場です。
#define ARR(a) int[]a ARR((1,2,3,4,5))
みたいな感じで使います。カッコの中にpythonのタプルの様に運用します。
配列のサイズは
int[](1,2,3,4,5).length()
これで取得できます。このlength()
の存在を知ってから、一気に道が開けました。後はマクロで何とかするの力技です。
シーケンスについて
まずBPMを決定します。このシーケンサーは途中でBPMを変更できません。まあ、そんなレアケースは切り捨てます。並列処理の為、他にもADSRのサスティーンタイムも固定です。
普通はbeatを基準にシーケンスをすると思いますが、これは1小節を基準にシーケンスします。これだと何が良いかと言うと、途中で拍子を変更できる。変拍子にも対応できる事です。今回はnimifyの為に変拍子対応を外しましたが、後で別枠で変拍子対応についても書きます。
という事で、このシーケンサーは、小節の中の時間と何小節目かで、管理します。必要になるのは1小節の時間。それは
float bpm=120.; float barTime = 240./bpm;
これで取得します。
次に小節の中に音符をどう並べるかです。ここは最初から音符を詰めていく方法をやめました。
まず、拍子を決めます。次に拍子を何等分するか決める。という方法を取りました。慣れないとピンと来ないかもしれませんが、簡単に3連符とか制御できます。
//♩♩♩♩ Rhythm(4,(1,1,1,1)) //♩♫♩♩ Rhythm(4,(1,2,1,1)) //♩♫三連符♩ Rhythm(4,(1,2,3,1))
こんな感じの表記になります。pythonでいうタプルの中が配列です。
スネアのような裏打ちの場合は
//休符♩休符♩ Rhythm(4,(0,1,0,1))
ゼロ除算になりますがイメージ優先で例外を充てました。基本、休符は拍子全部が休符のみで使用にしました。拍子の中で裏拍を入れる場合は4拍子なら8拍子に変更して対応します。音符の後の休符の場合は
//♩♪休符♩♪休符 Rhythm(4,(1,1,1,1))
こういう風に扱います。どうせ音をカットする事ができないので同じ事です。
同じ事の繰り返しは省略出来るようにしてあります。
//♩♩♩♩ Rhythm(4,(1)) //休符♩休符♩ Rhythm(4,(0,1))
こんな風に手抜きができます。
リズムの扱いは、こんなところです。シーケンサーの都合上、音源を変える度にリセットをかけます。
それと何小節をループさせるかという事が必要になります。それを
Reset
Loop()
とします。以上のルールでドラムのシーケンスのコードを書きます。
音源にTIME
の引数を渡していますが、このシーケンサーが吐き出す音源に渡す時間です。単純に使ってください。
float OFFSET; int TMP,POS,IDX; #define EXTRACT(m,s)\ {\ TIME=mod(TIME,barTime);\ float a,b;\ IDX=-1;\ for(int i=0;i<m;i++){\ a=float(int[]s[i%int[]s.length()]);\ b=barTime/float(m);\ if(a>0.)OFFSET=0.;\ if(TIME<b)break;\ IDX+=int(a);\ TIME-=b;OFFSET+=b/max(1.,a);\ }\ if(a>0.)IDX+=int(TIME/b*a)+1;TIME=mod(TIME,b/max(1.,a));TIME+=OFFSET;\ } #define Rhythm(m,s)TMP++;if(POS==TMP)EXTRACT(m,s) #define Reset OFFSET=100.;TIME=t;TMP=-1;POS=int(t/barTime); #define Loop(n)TIME= mod(TIME,barTime*float(n));POS%=n; #define PI acos(-1.) #define PIH (PI*.5) #define TAU (PI*2.) #define osc_sin(x)sin((x)*TAU) #define osc_saw(x)(1.-fract(x)*2.) #define osc_sqr(x)sign(0.5-fract(x)) #define osc_tri(x)(asin(sin((x)*PI))/PIH) float noize(float t, float a, float b){ float g=fract(cos(t*exp2(a))*exp2(b))+6.0; return exp(-0.08*g*g) *40.0-1.; } float kick(float t) { return clamp(1.5*asin(cos(320.0*t-30.0*exp(-40.0*t))),-1.,1.)*exp(-4.*t); } float snare(float t){ float n=noize(t,10.,10.)*exp(-t*10.), f=t*160.,a=.3*osc_tri(1.2*f)*exp(-t*3.); float b=.05*osc_sin(10.*f+a)*exp(-t*3.); float c=osc_sin(f+b)*exp(-t*15.), z; return n+c; } float hihat(float t){ return noize(t,10.,18.)*exp(-t*15.); } vec2 mainSound(int s,float t){ float bpm=120.; float barTime = 240./bpm; float TIME; float gain =0.; Reset; Loop(1) Rhythm(2,(1,1)) gain += .5*kick(TIME); Reset; Loop(4) for(int i=0;i<3;i++){ Rhythm(4,(0,1,0,1)) } Rhythm(4,(0,1,0,2)) gain += .6*snare(TIME); Reset; Loop(1) Rhythm(8,(1)) gain += 1.*hihat(TIME)*.5; return .5*clamp(1.5*vec2(gain),-1.,1.); }
マクロの方は説明は辛いので(正確に言えば、もう触りたくは無い)、読み解くか、そのままお使いください。もう少し根性が出たら説明するかも。音源はデモで使った音源と同じです。リズムを変えてドラムマシーンとして使えるとおもいます。shadertoyに上げたので遊んでみてください。
とりあえず、ここまで。まだ続きます。