Edited at

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

More than 1 year has passed since last update.


はじめに

初めまして、Machiaと申します。

働く傍らで、趣味で色々作ってるのですが、

最近になってシンセサイザーをプログラムで作り始めました。

で、参考にした資料とか結構あるのですが、シンセサイザーの作り方としてまとまってる情報は正直少ないのですよ。

枯れてる情報とかあれば、どっかにまとめてあってそれ実装すれば実験し放題でもいいのに、と思ったり。

あと、シンセやエフェクターの基本的な部分って電気回路がベースになっているので、そっちの情報を調べるためプログラムによる実装は多くないです。(実際信号処理というカテゴリで情報は出てくるけど「シンセサイザー」「プログラム」と組み合わせても情報が少ないのが現状かと)

そこで、今回曲りなりに実装したのもあり、こんな知識が必要だよーとか、こんなプログラム書いたよーとかをメモっていこうかと。


対象読者

以下の項目に該当する方は読んで参考になるんじゃないかな。


  • シンセサイザーを自分で作ってみたい

  • サウンドのプログラムって難しそうだけど興味がある

  • 簡単なエフェクターであれば自分で作るのが早いんじゃね?

  • 4kb introとかでシンセを作ることになったグラフィックプログラマw

※4kb introってなんぞって人はまず「デモシーン」でググってみよう!


開発環境・諸注意

プログラム書いてコンパイルできる環境があればよろしいかと。

今回のシンセについては、VisualStudio2015 Express for Desktopで開発してます。

よって音の再生環境はWindowsを想定してます。

VisualStudioの使い方はググってください。

自分が書くより質の高い記事がわんさか出てくるはずです。

あとは、波形を取り扱う関係上、スピーカーから音が出ます。

ソースコードにバグが入ると爆音およびノイズが出る可能性があります。

プログラミング、およびソースコード参照は各自の責任で行った上で、加えて耳とスピーカーの取扱にはご注意ください。

耳の保護対策としてスピーカーから音出す前に波形を表示できると便利かもしれません。

私の環境ではRME BabyfaceとPreSonus StudioOne3で確認を行ってます。

(スペアナ機能を使ってます)


シンセサイザー実装に必要な処理

シンセサイザーって何ができればいいのか、ってことですけど、

定義としては「計算して音を出す装置」だそうで。

上記に対し、今回書いてるプログラムは、「計算して、WAVデータとして波形を書き込んで、再生する」というものです。うむ、ブレてないw

まず今回なにをしてるかと言うと、以下の手順を処理してます。(目次代わり)


  1. 5秒分の波形を書き込むためのバッファを確保する

  2. バッファに波形を書き込む

  3. 波形を再生する

現状Windows対応マザーボードのほぼ全部にサウンドボードが組み込まれており、その機能によって「PCM音声を再生する」ことが可能になっております。

また、WindowsもPCM形式での音声処理に対応しており、Windows使ってる限りはその文法に準拠する形になります。

すなわち、最終的にWindowsで再生するためには、音を「PCM音声として再生できる」形にしてWindowsに情報を渡せば、めでたく再生ができるわけです。

そのためには、「Windowsがプログラムから読み込めるようなデータを作る」ことが必要になります。

(ここらへん別OSでもドライバあれば同じだと思うんですが、説明が煩雑になるので省略します)


1. 波形書き込み用バッファ確保


sample_source.c

short wav_hz= 44100;// サンプリングレート

unsigned int musictime = wav_hz*5;
short* wave_data= (short*)GlobalAlloc(GPTR, sizeof(short)*musictime);

大前提すっ飛ばしますが、まず波形を書き込みするためのバッファを確保しました

配列で最初から持っておくのもいいんですが、ファイルに書き込まれた曲とかDAWみたいなのになると何秒曲が流れるかわからないところがあるので、とりあえずメモリは動的に確保しちゃいます。

GlobalAlloc()関数はWindows組み込みの関数ですね。

ここで、何も説明せずに数値を入れちゃっていますが、覚えてもらいたいのはひとつ。

44100 Hz = 1秒あたりのサンプリング周波数(になることが多い)

サンプリング周波数っていうのは文字通り、サンプリングする周波数、すなわち、単位時間あたりこのくらいのデータ量で標本化するよってのを定めたものです。

この値が出てきたら、1秒あたりのデータ数が44100個ある!ってくらいの覚え方でいいと思います。

この規格も昔の製品の名残で決められたみたいで、今後レートが変更されることは十分に考えられます。

ただ現在Windowsが乗っかる機器付属のサウンドカードであれば、基本的には44100Hzのデータの再生に対応してるみたいです。

よって、今のところは決め打ちで問題ないですし、将来的に単位が変更になっても該当部分を変更すればいいのです。

実際、お高いオーディオIFではここらへん自由に変更できるせいなのか、巷の音楽ソフトにはサンプリングレート変える機能が実装されたりしてますね。


2. 波形書き込み

それではお待ちかね・・・なのかわからんけど、波形書き込みです。

けど、そこまで複雑なことしてもしょうがないので、適当に書いちゃいます。

今回はこのまま確保したメモリには、適当に値入れてしまいましょう。


sample_source.c

        float frq = 1.f/440.f;  //平均律による基準の音、の周波数

for( int t=0; t<musictime; t++){
/*
波形をプロットし、値を格納する。
この段階でバッファの対応する値以上になった場合、
波形が断絶し、正常に再生がされないケースあり。
最悪スピーカを破損するケースがあるため、取扱には注意が必要。
*/

wave_data[t]
= (short)(12767*sinf((FLOAT)(2.0f*PI_M*frq*t/wav_hz ) ) );
}

結果、波形としてサイン波が入っております。これで波形の書き込みはおしまい。

例えば、アナログシンセの場合、上記で生成した音をオシレータとして、アンプ・フィルタ・エフェクタ等を重ねて、最終的にデータをバッファに格納すればいいわけですね。

あと、実際のところこのプロセスにおいて音の周波数を変更することで、みなさんおなじみの鍵盤の音の高さで鳴らすことができるわけです。

ピッチの高さは波の周期によって変わります。

現在のところ周波数は決め打ちにしておりますが、

この値を鍵盤に合わせる形で変更すれば、音程は変更可能です。

「平均律」でググってもらうと計算方法がでてきます。


3. 波形の再生

Windowsで波形を再生するにはいくつか手続きがあるので、一番簡単な実装を書いておきます。

waveOut系関数はすっごいレガシーなAPIですが、現行OSであるWindows10でも問題なく動作しますし、基本的な考えは他のSDK・APIでも同様なので、今回使用します。

あと実装も簡単です。

実際にやることとしては、WindowsでWAVフォーマットのデータを再生するのに必要な設定を書き込んで、それらの情報をベースにWavデータを再生します

ちょっと長ったらしいですが、基本的に一回書いてしまえばあとは使いまわすだけです。


sample_source.c

WAVEFORMATEX wf; //WAVEFORMATEX 構造体
wf.wFormatTag = WAVE_FORMAT_PCM; //変更しなくてもいいや
wf.nChannels = 1; //モノラルは'1' ステレオなら'2'。
wf.nSamplesPerSec = 44100; //44100Hz
wf.wBitsPerSample = 16; //16ビット
wf.nBlockAlign = wf.nChannels*wf.wBitsPerSample / 8; //計算
wf.nAvgBytesPerSec = wf.nSamplesPerSec*wf.nBlockAlign; //計算
wf.cbSize = 0; //計算
HWAVEOUT hWOut;
//コールバックにも対応しているが、今回は実装しない
waveOutOpen(&hWOut, WAVE_MAPPER, &wf, 0, 0, CALLBACK_NULL);

WAVEHDR wh;
wh.lpData = (LPSTR)wave_data; // データを書き込む
wh.dwBufferLength = sizeof(short)*wf.nChannels*musictime;//再生時間
wh.dwFlags = 0;
wh.dwLoops = 1;//1回だけ再生
wh.dwBytesRecorded = 0;
wh.dwUser = 0;
wh.lpNext = NULL;
wh.reserved = 0;

// 再生
waveOutPrepareHeader(hWOut, &wh, sizeof(WAVEHDR));
waveOutWrite(hWOut, &wh, sizeof(WAVEHDR));


ということであとは再生するのを待つだけです。

実際のところ処理を待つまでもなく、実行したらすぐ再生されます。


次は何しましょうか

さて、今回は以下のものを作成しました。(前掲より引用)

1. 5秒分の波形を書き込むためのデータ領域を確保する

2. 波形を書き込む
3. 波形を再生する

次のプロセスとして、以下のものを実装してみるのはいかがでしょうか。


  • 複数パートで演奏させる

  • 曲の長さを変更する(譜面の長さに対応して自動で変更できるようにする)

  • 音の加工(音の高さを自由に変更できるようにする、オシレータとしてサイン波・三角波を追加、フィルタやエンベロープを追加する等)

  • 今風のAPI使って再生してみる(XAudio2、WASAPI等)

どうです?こんな感じで項目を分解していくと自分でもできそう!ってなりません?

今回その1を書いたので、その2以降で上記の改造案を実装していこうかと思います。

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


補足

あと、実装でちょっと気になる人はいたんじゃないかと思うんですが、

ひとつ補足としてお伝えする方が良さそうなこと。

現在の実装では、音の高さを最初から決め打ちで指定して波形を書き込み、

それを再生させています。

例えばリアルタイムで演奏する場合、どんな音を演奏するかなんて押した鍵盤次第なので、最初から決め打ちするわけにはいかず、この方法は使えません。

すなわち、

音の高さ等を入力して、すぐに波形生成はできないのか?

という部分です。

実際VSTi使ってるとリアルタイム演奏とかありますよねー。

自分もよく使ってますがアレ便利ですよーw

上記のような実装は、結論から言うと可能です。

ただ、前述の通り譜面等を最初から準備しているわけじゃないので、以下のような流れになります。


  • 入力された鍵盤の音の高さを検出

  • それに対し波形生成を実施

  • 最終的に出力する

これを解決するためのひとつの手段として「コールバック関数を用いてバッファを少しずつ書き込む」という方法があります。

実装が間に合えばここらへんも記述してみようかと。


参考資料

英語じゃないものを主に記載してます。

Demoのつくりかた(主に4k/64kIntro用)

http://kioku.sys-k.net/tips/

kiokuさんのサイトにある情報。

SOUND

http://gyabo.sakura.ne.jp/progsnd.html

ぎゃぼさんとこの情報。

サウンドプログラミング入門 音響合成の基本とC言語による実装

http://floor13.sakura.ne.jp/book06/book06.html

書籍。比較的新しい本。

シンセサイザ実装するためのとっかかりとしてはアリかと。

C言語ではじめる音のプログラミング

http://floor13.sakura.ne.jp/book03/book03.html

上記書籍と同じ作者の本。

もうちょっと理論よりな印象。とは言うものの実装例も存在するので、参考資料に見るのはアリ。

ちなみにもう8刷まで出てるみたいで、まだAmazonで購入可能。

(シンセのエフェクト実装するのに参考にしてます)

サウンドエフェクトのプログラミング Cによる音の加工と音源合成

http://shop.ohmsha.co.jp/shopdetail/000000001934/

書籍。

自分みたいに理工学でもなく音響も学んでないようなヤツにピッタリな本。

実装部分に関する記述が比較的詳しく書かれてる印象。

ちなみに物理モデリング音源の実装とかも書かれてるのはありがたい。