Try & Error for Sound Program

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

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で表を見つけました。