C言語でシンセサイザー作成入門(その2)

# イントロダクション

TokyoDemoFest2017のCombined Music Compoにて3rdPlaceいただきました。
ありがとうございます。
http://tokyodemofest.jp/2017/results.txt

今回実装したノウハウは基礎部分がメインだったのですが、今後ある程度の発展が見込める部分を確認したので、もうちょっと色々できそうです。

で、実はGUIでデータをいじれるように一から作りたいなーとかで色々調べてるのですが、そんな事やってると全く記事を書かないので、強制的に棚卸しますw

[2017/3/8追記] 内容と微妙にリンクしてるか怪しいですが、一通りの機能を突っ込んであるシンセのソースコードをアップしました。

https://github.com/MachiaWorks/TinySynth

# 今回のお題目
今回は以下の項目について記載します。

* 複数パートの演奏
* 波形の追加

ちとソースコードがないと不便ではあるので、用意はしとこうかと思います。
ただサイズを小さくすることを大前提としているのでかなーり可読性に優れないソースコードしか手元にないのです・・・(最初から小さいサイズにしようとしたので)
なので改めて書き直してからになりますね・・・申し訳ありませんが。

# ①複数パートの追加

前回時点では、同じ音を5秒鳴らす、というソースを書いてたんですが、これだけだと音楽にはなりません。
音楽を音楽たらしめる要素として有名なものは「メロディ・リズム・和音」があります。
音楽を作れるように1要素ずつ追加する形にします。

今回は、「和音」すなわち、**複数音を再生する**、ということにフォーカスを当てます。

和音が鳴るケースでも、データ上はひとつの波形とみなして処理することで綺麗に音が鳴ってくれます。
よって、和音を鳴らす場合は **複数の波形を(データの範囲内で)バッファ同士を加算する事で実装可能** です。

波形の詳しい仕組みは省略しますが、ここらへんは「フーリエ変換」「離散スペクトル」というキーワードで色々調べることができると思います。

実装用のコード書いておきますね。

“` 適当なコード.c
short wav_hz= 44100;// サンプリングレート
unsigned int musictime = wav_hz*5;
float root = 1/1.41421356237;//1/(2^(1/2))

//バッファの確保。
float* wave_data1= (float*)GlobalAlloc(GPTR, sizeof(float)*musictime);
float* wave_data2= (float*)GlobalAlloc(GPTR, sizeof(float)*musictime);
short* wave_result = (short*)GlobalAlloc(GPTR, sizeof(short)*musictime);

float frq1 = 1.f/440.f; //平均律による基準の音、の周波数
float frq2 = 1.f/440.f * root*root;//2度上の音を鳴らす
for( int t=0; t<musictime; t++){
/*
波形をプロットし、値を格納する。
この段階でバッファの対応する値以上になった場合、
波形が断絶し、正常に再生がされないケースあり。
最悪スピーカを破損するケースがあるため、取扱には注意が必要。
*/
wave_data1[t] = (float)(sinf((FLOAT)(2.0f*PI_M*frq1*t/wav_hz ) ) );
wave_data2[t] = (float)(sinf((FLOAT)(2.0f*PI_M*frq2*t/wav_hz ) ) );

wave_result[t] = (short)( 12767.f*( wave_data1[t]+wave_data2[t] / 2.f ) );
}
“`

とりあえずキャストとか適当に書いたコードですが、なんか鳴るかと思います。
何しているかというと、サイン波を2音分バッファ確保して、それを最終的に重ね合わせてるんですね。

上記のソースコードでの注目点は、サイン関数に関して、frq1・frq2にて **別途周波数を指定している** ことです。
周波数が違う場合、その音が振動する周期(何秒で振動がワンセット終わるかを単位化したもの)に相違があります。
違う周波数で鳴っている音がひとつの再生ラインに乗っかることで和音が形成されるわけです。

WAVフォーマットは上記の周期から出力されたサイン波を定期的にプロットする形になります。
ただ、最終的に音にする部分はサウンドカードの方でやってくれるので、プログラマはまずWAVフォーマットに準拠する形でデータを入れればOKです。
ということで、複数の音が含まれたプロットをWAVフォーマットに突っ込んでます。

# ②波形の追加

さて、次はどうしましょうと思いましたが、そもそもの話としてサイン波だけ鳴らしていていいのか?という疑問が浮かんできます。
何故かというと、現実の楽器演奏でも「楽器ごとに違う音が出ている」という事から分析してみましょう。

自分はジャズの楽曲を作成する同人サークルに参加しているのですが、その中で多いのが以下の構成です。

* サックス
* フレットレスベース
* ピアノ
* ドラム

何かっていうと複数の楽器があるわけですが、各楽器ではその構造から鳴り方が全然違うものになっています。
たとえば、ピアノは「音の立ち上がりが鋭く、その後曲線を描くように減衰していく」、ベースは「サイン波に近い形で弦が振動し、徐々に振動が収まっていく」ドラムは「音が複雑に絡んで音程が認識できない」等、鳴り方に特色があります。
これらの分析に時間かけるのは本筋ではないので省略しますが、「物理モデリング音源」「Physical Modeling Synthesis」等で検索すると色々出てきます。(海外の論文とか読むと面白いです)

シンセサイザーも例外ではなく、実装例のようにサイン波を鳴らすだけではなく、 **波形の種類を変更する事で、違う音を出すことが可能** になっています。
勿論上記の楽器のように減衰やドラムのような音も実装可能ですが、今回は単純な波形の変更にとどめておきます。

今回は、ベースとして用いられることが多い波形を実装してみます。

“` 適当なコード.c
short wav_hz= 44100;// サンプリングレート
unsigned int musictime = wav_hz*5;
float root = 1/1.41421356237;//1/(2^(1/2))

//バッファの確保。
float* wave_data1= (float*)GlobalAlloc(GPTR, sizeof(float)*musictime);
float* wave_data2= (float*)GlobalAlloc(GPTR, sizeof(float)*musictime);
short* wave_result = (short*)GlobalAlloc(GPTR, sizeof(short)*musictime);

float frq1 = 1.f/440.f; //平均律による基準の音、の周波数
float frq2 = 1.f/440.f * root*root;//2度上の音を鳴らす
for( int t=0; t<musictime; t++){
/*
波形をプロットし、値を格納する。
この段階でバッファの対応する値以上になった場合、
波形が断絶し、正常に再生がされないケースあり。
最悪スピーカを破損するケースがあるため、取扱には注意が必要。
*/
float osc = 0.f;
for (int k = 1; k < 9; k += 2) {
osc += 1.f / k / k*sinf(PI_M*k / 2.0f) * sinf(2.0f*PI_M*frq1*t/wav_hz );
}
wave_data1[t] = osc;
wave_data2[t] = (float)(sinf((FLOAT)(2.0f*PI_M*frq2*t/wav_hz ) ) );

wave_result[t] = (short)( 12767.f*( wave_data1[t]+wave_data2[t] / 2.f ) );
}
“`

これは何してるかと言うと、和音のうちひとつを三角波に変更したものです。
ソースではサイン波を重ね合わせて波形を作っていますが、勿論非線形的に変更するのもひとつの手法かと思います。(参考書籍等に実装方法が書いてあります)

上記のような実装で、波形を変える事が可能です。
さらなる応用として、波形の強弱をつける・波形の加工を行う等ありますが、それは別途項目を設けようかな、と。
(キーワード:エンベロープ・フィルター処理等)

# 今回はこれで終わりですが

まだネタは存在するので少しずつ書いていこうかと。

では、今後の皆様のシンセサイザー作成ライフに幸あれ。