Shogo's Blog

Sep 30, 2012 - 8 minute read - 6さいカンファレンス

6さいカンファレンス 第1回「C言語で作る、はじめてのDAW制作」まとめ

だいぶ時間がたってしまったけど、2012/09/06にくいなちゃんさん主催で開催された6さいカンファレンスのまとめ。 第1回は「C言語で作る、はじめてのDAWソフト制作」です。

勝手にまとめてしまったので、何か問題があれば@shogo82148まで。

BATTLE START!!

くいなちゃん: みなさんは、既にC言語の基本的なところはマスターしていると思いますが、念のために軽く復習しておきましょう。 まずは、このソースコードをご覧ください。 http://kuina.tes.so/6saiconf/a.png(魚拓)

#include <stdio.h>
#include <math.h>

int main(void) {
    return 0;
}

くいなちゃん: 右下の100% は気にしなくてOKです。 このプログラムは、「何もせず終了するだけ」のプログラムとなっています。 ソースコードを順に解説しましょう。

くいなちゃん: まず、1行目と2行目に書かれている #include ですが、これはおまじないです。 プログラムの本質は、4行目からとなります。

くいなちゃん: int main(void) { } が、プログラムの本質となります。 コンピュータがこのプログラムを起動すると、int main(void) { }{} に囲まれた部分を上から順に実行していきます。 } に辿り着いたら終了です。

くいなちゃん: よって、このプログラムは、 return 0; を実行するだけのプログラムとなります。 return 0; とは、おまじないです。 えへへ☆

DAWを作ってみる

くいなちゃん: はい、もう、C言語の基本的なところは全てマスターできましたね。 では、早速 DAWソフトを作ってみましょう。

くいなちゃん: DAWとは、簡単にいいますと、曲を作るソフトです。 それでは、int main(void) { } の中に、DAWっぽいプログラムを書いていきましょう。 とりあえず、int i, j; を書きます。

#include <stdio.h>
#include <math.h>

int main(void) {
    int i, j;
    return 0;
}

くいなちゃん: int i, j; とは、変数 ij を int型で定義するという意味です。 int型とは、整数型という意味です。この ij は、よくループのときに使う名前ですが、ここでも後ほどのループのために定義しておきます。

くいなちゃん: それでは、int i, j; に続いて、次の行を書きましょう。何も考えずに。 unsigned char header[44] = {0x52, 0x49, 0x46, 0x46, 0x34, 0xb1, 0x02, 0x00, 0x57, 0x41, 0x56, 0x45, 0x66, 0x6d, 0x74, 0x20, 0x10, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x44, 0xac, 0x00, 0x00, 0x44, 0xac, 0x00, 0x00, 0x01, 0x00, 0x08, 0x00, 0x64, 0x61, 0x74, 0x61, 0x10, 0xb1, 0x02, 0x00};

くいなちゃん: そして、その次に FILE* fp = fopen("out.wav", "wb"); と書きましょう。 この行は、fopen関数で “out.wav” ファイルを “wb” で開いています。 “wb” というのは、write binary のことで、バイナリ書き込みモードで開くことになります。 そして、そのときにファイルポインタ(ハンドラ) を fp に格納しています。 以後、fp を扱うことで、ファイルが操作できるようになるというわけです。

くいなちゃん: その次は、 double x = 0.0; です。今度は、謎の変数 x を double型で宣言します。 double型とは、小数が扱える型です。

くいなちゃん: そして、本日の目玉、 fwrite(header, 1, 44, fp); をその後に書きましょう! これにより、さっき開いたファイルに、header配列の中身を 44byte分書き込みます。 header配列とは、ちょっと前にみなさんがコピペしたと思われる、謎の16進数の羅列です。

くいなちゃん: 折角なので、ループを作りましょう。変数i、jを定義したのに、ループを作らないなんて損です。

for (i = 0; i < 16; i++)
{
    int key;
    scanf("%d", &key);
    for (j = 0; j < 44100 / 4; j++)
    {
        unsigned char a = (unsigned char)(sin(x) * 127.0 + 128.0);
        fwrite(&a, 1, 1, fp);
        x += 441.0 * pow(2.0, (double)key / 12.0) * 2.0 * 3.14159265358979 / 44100.0;
    }
}

くいなちゃん: 最後に、この行を追加します。 return 0; の直前ですね。 fclose(fp); これにより、先ほど開いたファイルを、安全に閉じます。

くいなちゃん: はい、みなさん、こんな感じになりましたでしょうか。 http://kuina.tes.so/6saiconf/img0.png(魚拓)

#include <stdio.h>
#include <math.h>

/* 7 6 7 6 7 2 5 3 0 0 0 0 0 0 0 0 Für Elise*/
/* 9 9 7 7 6 6 6 6 4 6 7 4 2 2 2 2 Biene */

int main(void) {
    int i, j;
    unsigned char header[44] = {0x52, 0x49, 0x46, 0x46, 0x34, 0xb1, 0x02, 0x00, 0x57, 0x41, 0x56, 0x45, 0x66, 0x6d, 0x74, 0x20, 0x10, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x44, 0xac, 0x00, 0x00, 0x44, 0xac , 0x00, 0x00, 0x01, 0x00, 0x08, 0x00, 0x64, 0x61, 0x74, 0x61, 0x10, 0xb1, 0x02, 0x00};
    FILE *fp = fopen("out.wav", "wb");
    double x = 0.0;
    fwrite(header, 1, 44, fp);
    for (i = 0; i < 16; i++)
    {
        int key;
        scanf("%d", &key);
        for (j = 0; j < 44100 / 4; j++)
        {
            unsigned char a = (unsigned char)(sin(x) * 127.0 + 128.0);
            fwrite(&a, 1, 1, fp);
            x += 441.0 * pow(2.0, (double)key / 12.0) * 2.0 * 3.14159265358979 / 44100.0;
        }
    }
    fclose(fp);
    return 0;
}

くいなちゃん: それでは、実行してみましょう! 実行したら、 7 6 7 6 7 2 5 3 0 0 0 0 0 0 0 0 と打ってください。 0 は 8個です。

くいなちゃん: すると、out.wav というファイルが作成されていると思います。 再生すると…

くいなちゃん: はい、何か鳴りましたね。 それでは、ループの中身を軽く説明しましょう。 えっ、曲名解らない人いますか? いませんね!

くいなちゃん: for (i = 0; i < 16; i++) では、16個の入力を取得しています。 int key; scanf("%d", &key); というのが、取得したデータを 変数key に保持しているところですね。 入力というのは、先ほど実行時に入力していただいた、 7 6 7 6 7 2 5 3 0 0 0 0 0 0 0 0 という16個の数値のことです。

くいなちゃん: scanf 関数は、キーボードからの入力を取得する関数です。 scanf("%d", &変数名); で、int型の入力を受け取り、変数に格納します。 double型の場合は、"%lf" とします。

くいなちゃん: その次の for(j = 0; j < 44100 / 4; j++) では、1/4秒分の音声波形データを書き込みます。 44100 というのは、1秒間あたりの音声データ量[byte] だと思ってください。 44100byte 書き込むと、1秒になるわけです。

くいなちゃん: unsigned char a = (unsigned char)(sin(x) * 127.0 + 128.0); では、波形を生成しているわけですが、ここでみなさんが大好きな三角関数が登場します。 sin(x) で、サイン波を作り出し、 * 127.0 + 128.0 により、.wavデータの規格に沿ったデータへと補正しています。 つまり、サイン波の音が書きこまれるわけですね。

くいなちゃん: signed char ではなく、符号なしの unsigned char である理由は、.wavデータの規格がそうなっているからです。

くいなちゃん: で、fwrite(&a, 1, 1, fp); で 今作った波形(変数aに入っている)を書き込み、x += の行で、しかるべき角速度に従って、波形 sin(x) の x を進めています。 この長い式が、ドレミファソラシドの音階の周波数ごとの角速度を計算しています。

くいなちゃん: はい、おめでとうございます、あなたは全て理解されました。

音色を変えてみる

くいなちゃん: はい、では、ピロピロした音では頼りないので、しっかりした音色に変更してみましょう。 unsigned char a = (unsigned char)(sin(x) * 127.0 + 128.0); の行を、 unsigned char a = (unsigned char)((sin(x) >= 0.0 ? 1.0 : -1.0) * 127.0 + 128.0); と書き換えてみてください。

くいなちゃん: http://kuina.tes.so/6saiconf/img1.png(魚拓) この黄色くなった行が、変更点です。

#include <stdio.h>
#include <math.h>

/* 7 6 7 6 7 2 5 3 0 0 0 0 0 0 0 0 Für Elise*/
/* 9 9 7 7 6 6 6 6 4 6 7 4 2 2 2 2 Biene */

int main(void) {
    int i, j;
    unsigned char header[44] = {0x52, 0x49, 0x46, 0x46, 0x34, 0xb1, 0x02, 0x00, 0x57, 0x41, 0x56, 0x45, 0x66, 0x6d, 0x74, 0x20, 0x10, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x44, 0xac, 0x00, 0x00, 0x44, 0xac , 0x00, 0x00, 0x01, 0x00, 0x08, 0x00, 0x64, 0x61, 0x74, 0x61, 0x10, 0xb1, 0x02, 0x00};
    FILE *fp = fopen("out.wav", "wb");
    double x = 0.0;
    fwrite(header, 1, 44, fp);
    for (i = 0; i < 16; i++)
    {
        int key;
        scanf("%d", &key);
        for (j = 0; j < 44100 / 4; j++)
        {
            unsigned char a = (unsigned char)((sin(x) >= 0.0 ? 1.0 : -1.0) * 127.0 + 128.0);
            fwrite(&a, 1, 1, fp);
            x += 441.0 * pow(2.0, (double)key / 12.0) * 2.0 * 3.14159265358979 / 44100.0;
        }
    }
    fclose(fp);
    return 0;
}

くいなちゃん: 今までは、sin(x) でサイン波の波形の音を鳴らしていましたが、今度は矩形波です。 sin(x) >= 0.0 のとき、1.0になり、それ以外は -1.0 となります。

くいなちゃん: 鳴らしてみてください。 実行時に 9 9 7 7 6 6 6 6 4 6 7 4 2 2 2 2 と入れると、別の曲が楽しめます。

ビブラートをかけてみる

くいなちゃん: 今度は、音にビブラートをかけてみましょう。下記のように 3行変更してみてください。 http://kuina.tes.so/6saiconf/img2.png(魚拓)

#include <stdio.h>
#include <math.h>

/* 7 6 7 6 7 2 5 3 0 0 0 0 0 0 0 0 Für Elise*/
/* 9 9 7 7 6 6 6 6 4 6 7 4 2 2 2 2 Biene */

int main(void) {
    int i, j;
    unsigned char header[44] = {0x52, 0x49, 0x46, 0x46, 0x34, 0xb1, 0x02, 0x00, 0x57, 0x41, 0x56, 0x45, 0x66, 0x6d, 0x74, 0x20, 0x10, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x44, 0xac, 0x00, 0x00, 0x44, 0xac , 0x00, 0x00, 0x01, 0x00, 0x08, 0x00, 0x64, 0x61, 0x74, 0x61, 0x10, 0xb1, 0x02, 0x00};
    FILE *fp = fopen("out.wav", "wb");
    double x = 0.0;
    double y = 0.0;
    fwrite(header, 1, 44, fp);
    for (i = 0; i < 16; i++)
    {
        int key;
        scanf("%d", &key);
        for (j = 0; j < 44100 / 4; j++)
        {
            unsigned char a = (unsigned char)((sin(x) >= 0.0 ? 1.0 : -1.0) * 127.0 + 128.0);
            fwrite(&a, 1, 1, fp);
            x += (441.0 + sin(y) * 10) * pow(2.0, (double)key / 12.0) * 2.0 * 3.14159265358979 / 44100.0;
            y += 5.0 * 2.0 * 3.14159265358979 / 44100.0;
        }
    }
    fclose(fp);
    return 0;
}

くいなちゃん: 今までは、441.0 という値(実は"ラ"の音) を中心に音を作っていたんですが、その中心音を揺るがすことで、ビブラートをかけています。 y がその揺るぎを生成させるもので、5Hz で振動させています。

くいなちゃん: なんとなく、プログラム眺めてたら解るでしょう。

和音を鳴らしてみる

くいなちゃん: はい、次は、和音を鳴らしてみます。 主旋律の完全5度上に、パートを合成してみましょう。 http://kuina.tes.so/6saiconf/img3.png(魚拓) のように、3行変更してください。 右が切れている行は、下記のようになります z += (441.0 + sin(y) * 10.0) * pow(2.0, (double)(key + 7) / 12.0) * 2.0 * 3.14159265358979 / 44100.0;

#include <stdio.h>
#include <math.h>

int main(void) {
    int i, j;
    unsigned char header[44] = {0x52, 0x49, 0x46, 0x46, 0x34, 0xb1, 0x02, 0x00, 0x57, 0x41, 0x56, 0x45, 0x66, 0x6d, 0x74, 0x20, 0x10, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x44, 0xac, 0x00, 0x00, 0x44, 0xac , 0x00, 0x00, 0x01, 0x00, 0x08, 0x00, 0x64, 0x61, 0x74, 0x61, 0x10, 0xb1, 0x02, 0x00};
    FILE *fp = fopen("out.wav", "wb");
    double x = 0.0;
    double y = 0.0;
    double z = 0.0;
    fwrite(header, 1, 44, fp);
    for (i = 0; i < 16; i++)
    {
        int key;
        scanf("%d", &key);
        for (j = 0; j < 44100 / 4; j++)
        {
            unsigned char a = (unsigned char)((sin(x) >= 0.0 ? 1.0 : -1.0) * 64.0 + sin(z) * 63.0 + 128.0);
            fwrite(&a, 1, 1, fp);
            x += (441.0 + sin(y) * 10) * pow(2.0, (double)key / 12.0) * 2.0 * 3.14159265358979 / 44100.0;
            y += 5.0 * 2.0 * 3.14159265358979 / 44100.0;
            z += (441.0 + sin(y) * 10) * pow(2.0, (double)(key + 7) / 12.0) * 2.0 * 3.14159265358979 / 44100.0;
        }
    }
    fclose(fp);
    return 0;
}

くいなちゃん: プログラムを眺めたら解ると思いますが、新たな変数 z を定義し、さっき作った矩形派に加算する形で sin(z) が合成されています。 今まで、 * 127.0 + 128.0 となっていましたが、2つの音を合成するので、 * 127.0 を、*64.0*63.0 の2つに分割しています。

くいなちゃん: 適宜、鳴らしてみてくださいね

ディレイを実装しちゃう

くいなちゃん: はい、最後にラスボス、「ディレイ」を実装しちゃいましょう。 これは非常に手ごわい相手ですが、音がかなり幻想的な雰囲気になるので、挑戦しちゃいましょう!

くいなちゃん: http://kuina.tes.so/6saiconf/img4.png(魚拓) のように、計5行を変更してください。 消えている部分は、 unsigned char a = ((unsigned char)((sin(x) >= 0.0 ? 1.0 : -1.0) * 64.0 + sin(z) * 63.0 + 128.0) + buff[p] * 3) / 4; です!

#include <stdio.h>
#include <math.h>

/* 7 6 7 6 7 2 5 3 0 0 0 0 0 0 0 0 Für Elise*/
/* 9 9 7 7 6 6 6 6 4 6 7 4 2 2 2 2 Biene */

int main(void) {
    int i, j;
    unsigned char header[44] = {0x52, 0x49, 0x46, 0x46, 0x34, 0xb1, 0x02, 0x00, 0x57, 0x41, 0x56, 0x45, 0x66, 0x6d, 0x74, 0x20, 0x10, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x44, 0xac, 0x00, 0x00, 0x44, 0xac , 0x00, 0x00, 0x01, 0x00, 0x08, 0x00, 0x64, 0x61, 0x74, 0x61, 0x10, 0xb1, 0x02, 0x00};
    FILE *fp = fopen("out.wav", "wb");
    double x = 0.0;
    double y = 0.0;
    double z = 0.0;
    unsigned char buf[44100 / 4] = {0};
    int p = 0;
    fwrite(header, 1, 44, fp);
    for (i = 0; i < 16; i++)
    {
        int key;
        scanf("%d", &key);
        for (j = 0; j < 44100 / 4; j++)
        {
            unsigned char a = ((unsigned char)((sin(x) >= 0.0 ? 1.0 : -1.0) * 64.0 + sin(z) * 63.0 + 128.0) + buf[p] * 3) / 4;
            buf[p] = a;
            p = (p + 1) % (44100 / 4);
            fwrite(&a, 1, 1, fp);
            x += (441.0 + sin(y) * 10) * pow(2.0, (double)key / 12.0) * 2.0 * 3.14159265358979 / 44100.0;
            y += 5.0 * 2.0 * 3.14159265358979 / 44100.0;
            z += (441.0 + sin(y) * 10) * pow(2.0, (double)(key + 7) / 12.0) * 2.0 * 3.14159265358979 / 44100.0;
        }
    }
    fclose(fp);
    return 0;
}

くいなちゃん: 15行目で、あらかじめディレイ用のバッファを作成します。 unsigned char buff[44100 / 4] = {0}; により、 44100 / 4、すなわち、1/4秒間のバッファを作成し、 {0} により、全て 0 で初期化します。

くいなちゃん: 16行目の int p = 0; は、buff を円環状にするための、位置を保持する変数です。 buff内の、p (初期は0) の部分にデータを書き込み、p を 1ずつ加え、順にデータを書き込んでいきます。 で、pが buffの終端に辿り着いたら p を 0 に戻し、また先頭からデータを書き込むようにします。

くいなちゃん: 今言った動作をしている部分が、25, 26 行目になります。 buff[p] = a; で、本来wavファイルに出力するデータを buff にも書き込み、p = (p + 1) % (44100 / 4); で、p を 1ずつ進めています。 %演算子は、終端に辿り着いたときに 0 に戻す役割をしています。 割り算の余りを利用して、ネ!

くいなちゃん: で、24行目の出力波形を計算する部分で、buffに残っている、「1/4秒前に出力した波形」 を上から合成しています。 これにより、残響音の効果が得られ、ディレイとなるわけです。

くいなちゃん: 鳴らしてみてください。 お風呂場みたいな雰囲気? 7 6 7 6 7 2 5 3 0 0 0 0 0 0 0 0

くいなちゃん: ディレイの効果が判りやすいように、本来の音1 : ディレイ3 の割合で計算しています。 が、実際の音楽を作るときは、もっと ディレイを押さえるべきですね>ameri1341さん

改造してみる

第1回 6さいカンファレンスは以上です。 カンファレンス内で出てきたプログラムをお手軽に実行できるようJavascriptを組んでみました。 自分で改造して遊んでみましょう!

正規表現で置換してJavascriptにしているだけなので、いろいろ制限付き。

  • プリプロセッサは解釈しない
  • グローバル変数は使えない
  • 型は解釈しない(全部浮動小数点。7/2=3.5になる。)
  • 数字に対するキャスト((double)10とか)はできない。
  • 入力できるのは空白区切りの数字だけ

うまく行かなかったら諦めてコンパイラをインストールしてください。