Try & Error for Sound Program

FoxDotを始めました。色々と試してみます。

Sound Shaderの解説 1

初めに

TDFにPC 4K Introに出したsound shaderについて説明していきます。
圧縮前のshaderは

www.shadertoy.com

この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に上げたので遊んでみてください。

www.shadertoy.com

とりあえず、ここまで。まだ続きます。