Try & Error for Sound Program

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

Sound Shaderの解説 3

infoをオーソドックスに使う方法

前回の解説を書き終えた時点で、自分のsound shaderの解説より先に、単純なシーケンスの仕方を書いた方が良いなって思いました。
なので、一度デモのsound shaderの説明を離れて、今まで書いた事だけでシーケンスをする方法を書いてみようと思います。多分、音楽が出来る人にとっては、そっちの方が良いと思います。
私は音楽が良く解っていないので音楽理論を捏ね回し素人レベルで、それっぽい奴を作るコードを書いてきました。解る人にはウザいかな。
とはいえ、これも何年かの成果なので書きます。今回はひとまず、前回までのコードを使ってshader musicを作る説明にします。

ノート番号

www.asahi-net.or.jp

ピッチ名にはノート番号が対応されています。shaderでノート番号から周波数を出す関数があります。なのでノート番号を使ったシーケンスが可能になります。

float note2freq(int note){
    return 440.*exp2(float(note-69+67)/12.);
}

リンクのページを見ると、0~127とノート番号が振られています。これはshaderで扱うにはちょうど良い数です。7bitです。4和音を想定した時、4X7=28 intで使用できる31bitに収まります。しかもbitが余るので、3和音と4和音の区別をする事ができます。

bit演算を使いintで和音を扱う方法

ビットを7bit事にずらしてintに格納します。そうすると前回のやり方だけで和音を扱えるようになります。

int encodeChord(int pitch1, int pitch2, int pitch3){
    return pitch1 +
        (pitch2 << 7) +
        (pitch3 << 14);
}

int encodeChord(int pitch1, int pitch2, int pitch3, int pitch4){
    return pitch1 +
        (pitch2 << 7) +
        (pitch3 << 14) +
        (pitch4 << 21) +
        (1<<28) ; // 4和音を判別するフラグ
}
ivec4 decodeChord(int chord){
    return ivec4(
        chord & 127,
        chord >> 7 & 127,
        chord >> 14 & 127,
        chord >> 21 & 127
    );
}

int chordLength(int chord){
    return 3 + (chord >> 28 & 1);
}

使用例

これだけ解れば音楽の出来る人なら問題無く使えると思います。まあ書くのは面倒くさいですけど。これでsound shaderで音楽が出来るんじゃないですかね。音符に対しての情報が足りないのなら、INFと同じ仕組みを増やせば良いだけです。

// 音源関係
#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.);
}

float piano(float t,float f){
    float g=0.,a,b=0.;
    f*=t;
    for(float j=1.;j<5.;j++){
        b+=a=exp(-j*.5);
        g+=a*osc_sin(f*j)*exp(-t*1.8/(2.-j*.1));
    }
    g*=1.+1.2*exp(-5.*t);
    return g/b;
}

float lead(float t,float f){
    f *= t;
    return clamp((
        8.0*osc_saw(f)
        + 2.0*osc_sin(f*2.0)
        + osc_tri(f*3.0)
    )/3.0,-1.,1.)*exp(-3.*t);
}

float base(float t,float f){
    f *= t;
    return clamp((
        8.0*osc_sqr(f)
        + 3.0*osc_saw(f*2.0)
        + osc_sin(f*3.0)
    )/2.0,-1.,1.)*exp(-4.*t);
}

// シーケンスのマクロ

#define C3  60
#define Cs3 61
#define D3  62
#define Ds3 63
#define E3  64
#define F3  65
#define Fs3 66
#define G3  67
#define Gs3 68
#define A3  69
#define As3 70
#define B3  71
#define C4  72
#define Cs4 73
#define D4  74
#define Ds4 75
#define E4  76
#define F4  77
#define Fs4 78
#define G4  79
#define Gs4 80
#define A4  81
#define As4 82
#define B4  83

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 Info(a)if(POS==TMP)INF=int[]a[IDX%int[]a.length()];

#define Reset OFFSET=100.;TIME=t;TMP=-1;POS=int(t/barTime);
#define Loop(n)TIME= mod(TIME,barTime*float(n));POS%=n;

float note2freq(int note){
    return 440.*exp2(float(note-69)/12.);
}

int encodeChord(int pitch1, int pitch2, int pitch3){
    return pitch1 +
        (pitch2 << 7) +
        (pitch3 << 14);
}

int encodeChord(int pitch1, int pitch2, int pitch3, int pitch4){
    return pitch1 +
        (pitch2 << 7) +
        (pitch3 << 14) +
        (pitch4 << 21) +
        (1<<28) ; // 4和音を判別するフラグ
}

ivec4 decodeChord(int chord){
    return ivec4(
        chord & 127,
        chord >> 7 & 127,
        chord >> 14 & 127,
        chord >> 21 & 127
    );
}

int chordLength(int chord){
    return 3 + (chord >> 28 & 1);
}

vec2 mainSound(int s,float t){
    float bpm=120.;
    float barTime = 240./bpm;
    float TIME;
    int INF;
    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))
    Info((1,1,2,1))
    gain +=  .7*hihat(TIME)*float(INF)*.5;
    
    Reset;
    Loop(4)
    Rhythm(1,(1)) Info((encodeChord(C3,E3,G3)))
    Rhythm(1,(1)) Info((encodeChord(F3,A4,C4)))
    Rhythm(1,(1)) Info((encodeChord(A3,C4,E4)))
    Rhythm(2,(1)) Info((encodeChord(G3,B4,D4,F4), encodeChord(D3,F3,A3)))
    
    ivec4 chord = decodeChord(INF);
    int size = chordLength(INF);
    
    for(int i=0;i<size;i++){
        gain += .6*piano(TIME,note2freq(chord[i]))/float(size);
    }
    
    Reset;
    Loop(1)
    Rhythm(8,(1))
    Info((chord[0]-24,chord[0]-12))
    gain+= .2*base(TIME,note2freq(INF));
      
    Reset;
    Loop(1)
    Rhythm(4,(1,1,1,0))
    Info((C3,E3,G3))
    gain+= .2*lead(TIME,note2freq(INF));
    
    return .5*clamp(1.5*vec2(gain),-1.,1.);
}

www.shadertoy.com

終わりに

長いちゃんとした曲に仕上げるならLoop()を外してズラズラと書けば良いだけです。
これを書き終わり、自分の偏った音楽理論の使い方を説明するのも、何か違うかなって思えてきました。一旦、これで区切りを付けて、別記事で自分のやり方とか書いてみようと思います。音源については、説明って程のことも余りないので、また別記事で思いつく事があったら書いてみます。
何か質問がありましたら、遠慮なくどうぞ。

Sound Shaderの解説 2

現在の音符の情報取得

前回は、リズムの情報から音源に送る時間の取得でした。今回は音符の情報の扱いになります。ベロシティー、音階、和音の情報が扱えます。一見やっている事は複雑そうですが、やっている事は現在のintの情報だけです。これをbit演算を使い加工して色々な要素に振り分けます。デモのshaderではintで、やり取りしています。bit演算なので、ちゃんとuintを使った方が良いと思うのですが、マクロでの書き込みが、分かりずらくなるのでintの31bitで遣り繰りしてます。

マクロでの表記法

マクロを使うので、入力表記部分は、なるべく分かりやすい書き方をしていきます。ここは自由に決めれます。後はマクロの方で水面下の白鳥の足の様に足掻きます。さて書き方ですが、前回のRhythmの後に続けてかきます。

Rhythm(8,(1))
Info((1,1,2,1))

こんな感じが直感に近いと思います。これはハイハットのアクセントです。Infoの引数はマクロによってint[](1,1,2,1)と内部では変換されています。Rhythmのマクロの中で現在のインデックスがIDXとして代入されているのでint[](1,1,2,1)[IDX]として取得されます。これをINFに代入します。
これでマクロの基本部分は終わりです。後は応用だけ。マクロを使って使いやすくしていくだけです。

Reset;
Loop(1)
Rhythm(8,(1))
Info((1,1,2,1))
gain +=  hihat(TIME)*float(INF)*.5;

こんな感じで使います。 Info((1,1,2,1))は内部でINF=int[](1,1,2,1)[IDX%int[](1,1,2,1).length()];なので、こんな省略形で構いません。
サンプルをshadertoyに置きました。

www.shadertoy.com

次回は取得したintをbit演算を使いピッチを出したり和音を構成したりする方法を書きます。
5年間sound shaderでシーケンスを弄ってきて固まってきたのが、こんなにシンプルになりました。結局、これだけなんですね。後は応用。音楽理論とか絡んできます。使い方が個人の好みにチューニングできるかな。ここから先は個人の音楽理論に対するアプローチになります。

前回の記事は

gaziya.hateblo.jp

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

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

FM音源の実装

FM音源の説明とかは他に情報があるので省略します。
実装について書いていきます。これについて情報があまりなかったので、FM音源ICの情報を探し回って何とか形にしました。 作って実際出ている音とかICの音と違ってました。intで作るsin波と違いがあるかもしれません。なのでFM音源ICをエミュレートしているわけではありません。アルゴリズムだけ合わせてる感じです。確かめるすべがないのでアルゴリズムはあっているとは思っていても確証が持てないでもいます。

https://ja.wikipedia.org/wiki/FM%E9%9F%B3%E6%BA%90
ここより数式だけ拾いだします。

f:id:gaziya:20210628123423p:plain

FM音源はオペレーターの関数が特定できれば、アルゴリズムという繋ぎ方のパターンが存在してるだけです。 こちらも参考させてもらいました。

qiita.com

FM音源を数式からだけでは実装に無理がありました。パラメータが足りませんでした。FM音源ICのマニュアルとか見て、デチューンとADSRが必要でした。他にも有るみたいだったのですが、そこは諦めました。エンベロープでADSRを使うのもやり過ぎ感があったので、エンベロープはアタックとディケイだけにしました。
オペレーターに使うパラメータを4つに絞りました。ボリュームレベル、周波数マルチプル、デチューン、エンベロープ。これにオペレーターからの入力が必要になります。パラメータを4つに4OPなのでmat4()を使ってパラメータの簡略化も出来ました。
デチューンの関数は

float detune(float freq, float n){
  return exp2(log2(freq)+n/12.);
}

これは周波数をlog2()に入れてピッチにしてピッチを12等分したもので上下させてexp2()で又、周波数に戻す関数になります。
エンベロープはアタックとディケイだけを採用

float env_d(float t, float d)
{
    return exp(-t*5./d);
}

float env_ad(float t, float a, float d)
{
    return  min(t/max(1e-5,a),env_d(t-a,d));
}

引数は実時間、アタック時間、ディケイ時間としました。秒単位です。 オペレーターの関数は、ボリュームレベル、周波数マルチプル、デチューン、エンベロープをvec4で扱います。それとオペレーターの出力を入力として使います。

#define osc_sin(x) sin((x)*6.2832)
// ボリュームレベル * osc_sin( 周波数マルチプル * detune(周波数 , デチューン) * 実時間 + オペレーター出力) * エンベロープ
#define OP(d,o)d.x*osc_sin(d.y*detune(f,d.z)*t+o)*d.w

となります。これはマクロで書いてあります。この後にアルゴリズムというFM音源の作法が絡んでくるのを簡略化する為にしてあります。ちょっとわかりずらいです。 あと、f,tという変数が登場しますが f 周波数、t 実時間です。これもその後の処理を簡略化する為にパブリック変数みたいな扱いにしてあります。
これがオペレーターの関数で、アルゴリズムが登場して更に解りづらくなります。説明が辛くなってきたので、後はコードで勘弁してください。追々、理解の進みと共に加筆します。先に説明したデチューンとエンベロープは簡略化の為にマクロに組み込みました。

// OSC
#define osc_sin(x) sin((x)*6.2832)

float note2freq(int n){
  return 440.*exp2((float(n)-69.)/12.);
}

// Note numbers
#define C  60
// Scale
#define Major_scale int[](0,2,4,5,7,9,11)
#define P(n) C + Major_scale[n%7] + n/7*12

// FM data
#define FM mat4 DT=mat4
// envelope
#define EN(a,d) min(t/max(1e-5,a),exp(-(t-a)*5./d))
// FM operator -> vec4(Level,Maltiple,Detune,Envelope),operator
#define OP(a,i) a.x*osc_sin(a.y*exp2(log2(f)+a.z/12.)*t+i)*a.w

#define O1(i) OP(DT[0], i)
#define O2(i) OP(DT[1], i)
#define O3(i) OP(DT[2], i)
#define O4(i) OP(DT[3], i)
    
#define A0(F) O4(O3(O2(O1(F*O1(0.)))))
#define A1(F) O4(O3(O2(0.)+O1(F*O1(0.))))
#define A2(F) O4(O3(O2(0.))+O1(F*O1(0.)))
#define A3(F) O4(O3(0.)+O2(O1(F*O1(0.))))
#define A4(F) O4(O3(0.))+O2(O1(F*O1(0.)))
#define A5(F) O4(O1(F*O1(0.)))+O3(O1(F*O1(0.)))+O2(O1(F*O1(0.)))
#define A6(F) O4(0.)+O3(0.)+O2(O1(F*O1(0.)))
#define A7(F) O4(0.)+O3(0.)+O2(0.)+O1(F*O1(0.))

float sound(float t, float f)
{
    FM(
      .5, 2., 0., EN(.05, 2.),
      .4, 2., 1., EN(.01, 3.),
      .5, 2., 1., EN(.01, 3.),
      .7, 1., 0., EN(.01, 3.)
    );
    return A4(.5);
}

vec2 mainSound( int samp, float time )
{
    float beat=60./120.;
    float T = mod(time,beat);
    int N = int(time/beat)%8;
    return .5*vec2(sound(T, note2freq(P(N))));
}

www.shadertoy.com

FM音源を表す書き方は、下にあります。ここにある数字は全部パラメータ。数字のみ弄れます。あとアルゴリズムが、A0からA7まで。良い悪いは、置いておいて、数字を弄れば音は変わります。 上のリンクからshadertoyに行けば試せます。BPM120で4分の1音符がドレミファソラシドを繰り返してます。キーのノート番号は60 C4です。

float sound(float t, float f)
{
    FM(
      // ボリュームレベル、周波数マルチプル、デチューン、エンベロープ(attack, decay)
      .5, 1., 0., EN(.01, 2.), // OP1 のパラメータ
      .4, 2., 0., EN(.01, 3.), // OP2 のパラメータ
      .5, 2., 1., EN(.01, 3.), // OP3 のパラメータ
      .7, 2., 0., EN(.01, 3.)  // OP4 のパラメータ
    );
    return A6(.2); // アルゴリズム 引数はフィードバック
}

アルゴリズムについてtwitterで表を見つけました。

以前に書いたGLSL soundについての記事

GLSL soundについては5年くらい、あれこれやっている。以前書いた記事のリンクを貼りました。今とスタイルが違ってたりしますが参考にはなると思います。 qiita.com

qiita.com

qiita.com

qiita.com

qiita.com

これは一番最初に書いた記事。マクロでのシーケンス。これでマクロの妙味に気が付きました。今の書き方とは、ちょっと離れていますが、基本部分は変わらない所も有ります。

qiita.com

しかし、俺は何と戦ているのだろう。全然ゴールが見える気配が無いな。もう一山超えればお終いにしようとして、どれだけ経ったんですかね。まだ、sound shaderには面白さを見出してはいない。デモに音楽必須なんて無ければいいのに。

GLSLで扱う音源について

FM音源の話に入る前に音源について少し書いておきます。
大雑把に言えば、sound programは、音源とシーケンスだけです。今回は音源の基礎部分だけ書いて行きます。 音源には基本波形が有ります。 sin波、ノコギリ波、矩形波三角波が有ります。これらを材料として音を作っていきます。その他にノイズを加工するという方法も有りますが、GPUだとフィルターを作る事については不向きです。フィルターについては、良くわかりません。でも別のやり方でノイズを使ってたりはします。ノイズについては、いずれ纏めないとは思ってます。基本波形はオシレーター(OSC)として使います。以後、オシレーターはOSCと表記していきます。
OSCの関数です。入力は時間と周波数を乗算したモノ。OSCには、これを引数にした方が便利だったりします。

// OSC
#define osc_sin(x) sin((x)*6.2832)
#define osc_saw(x) (1.0-fract(x)*2.0)
#define osc_sqr(x) sign(0.5-fract(x))
#define osc_tri(x) asin(sin((x)*6.2832))/1.5708

音源には、もう一つ大事な要素が有ります。それがエンベロープ。音の衰減です。
大抵のshaderを見れば

exp(-time*3.0)

とかで済ましてます。これはディケイだけのエンベロープです。ADSRとかも作れますが、使い道とかが良くわかってません。大抵はアタックとディケイだけのエンベロープを使ってます。これも参考に書いておきます。

float env_adsr(float t, vec4 e, float sLv)
{  
  float a=t/max(1e-5,e.x);
  float d=(t<e.x+e.y)?1.-(t-e.x)*sLv/max(1e-5,e.y):0.;
  float s=1.-sLv;
  float r=(1.-sLv)-(t-e.x-e.y-e.z)*(1.-sLv)/max(1e-5,e.w);
  return max(0.,min(a,max(d,min(s,r))));
}

float env_d(float t, float d)
{
    return exp(-t*5./d);
}

float env_ad(float t, float a, float d)
{
    return  min(t/max(1e-5,a),env_d(t-a,d));
}

この辺りは単なる道具なので、こんな説明です。
sound programは構造が面倒なのですが、マクロを使うと簡潔に書ける事が多いので、マクロを多用しています。癖でマクロで無くて良い奴もマクロを使っていますが、気にしないでください。