jackdaw開発日記(1) C言語でwavファイルを作る

jackdawというプロジェクトを開発しているのですが、後で自分の書いたコードを見返したときに分からなくならないように、いくつか書き残しておこうと思います。

今回はまずC言語でwavファイルを作る方法について書きます。

出力先ファイルを作る

まず空のファイルを作ります。open関数を使います。

#include <fcntl.h>

int open(const char *path, int oflag, ...);

第1引数 path にはファイル名を指定します。新しくファイルを作り、かつこの後プログラム内でファイルに対する読み込みと書き込みを行うので、第2引数 oflag には O_CREAT | O_RDWR を指定します(同様にファイルを作るcreat関数がありますが、O_RDWR の代わりに O_WRONLY が指定されてしまうため使えません)。第2引数 oflag が O_CREAT を含むとき、第3引数でファイルのアクセス権限を指定することができます。後でwavファイルを再生したり削除したりできるように S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH としましょう。返り値のファイルディスクリプタはこの後使うので変数に格納しておきます。エラーの場合 -1 が返ります。

ファイルを閉じるときにはclose関数を使います。

#include <unistd.h>

int close(int fildes);

第1引数 fildes にはファイルディスクリプタを渡します。

ここまでをまとめると以下のようになります。

#include <fcntl.h>
#include <unistd.h>

int main(void){
    char *out_filename = "a.wav"; //ファイル名は今のところこうしておきます
    int out = open(out_filename, O_CREAT | O_RDWR, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
    if(out == -1) return -1;
    close(out);
}

空のファイルが作られます。ls -lで-rw-r--r--となっていることを確認して下さい。

マップする

次に、作ったファイルに対して書き込みを行う準備をします。このためには事前にファイルの大きさを知っておく必要があります。単位はバイトです。ビット深度/8 × チャンネル数 × サンプリング周波数 × 曲全体の秒数 + ヘッダサイズで計算できます。ヘッダサイズはこの後44バイトになります。

int samplerate;
short bitdepth, channel;
double length;
int filesize = bitdepth / 8 * channel * samplerate * length + 44;

現在ファイルは空であるため、中身を表すための領域がディスク上に確保されていません。まずこれを確保する必要があります。ftruncate関数を使います。

#include <unistd.h>

int ftruncate(int fildes, off_t length);

第1引数 fildes にファイルディスクリプタ、第2引数 length にファイルサイズを渡します。成功すると0、失敗すると-1が返ります。

次に、データを書き込むためにファイルをマップします。mmap関数を使います。

#include <sys/mman.h>

void *mmap(void *addr, size_t len, int prot, int flags, int fileds, off_t off);

第1引数 addr にはNULL、第2引数 len にはファイルサイズ、第3引数 prot には PROT_WRITE、第4引数 flags にはMAP_SHARED、第5引数 fildesにはファイルディスクリプタ、第6引数には 0 を渡します。返り値はマップした領域のアドレスです。何らかのポインタ型変数に格納します。今回はビット深度に合わせてshort *型にします。エラーの場合 MAP_FAILED が返ります。

ここまでをまとめると以下のようになります。サンプリング周波数は44100、ビット深度は16、チャンネルは2(ステレオ)とし、秒数は今のところ3秒にしておきます。

#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>

int main(void){
    char *out_filename = "a.wav";
    int out = open(out_filename, O_CREAT | O_RDWR, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
    if(out == -1) return -1;

    int samplerate = 44100;
    short bitdepth = 16, channel = 2;
    double length = 3.;
    int filesize = bitdepth / 8 * channel * samplerate * length + 44;

    if(ftruncate(out, filesize) == -1) return -1;

    short *map = mmap(NULL, filesize, PROT_WRITE, MAP_SHARED, out, 0);
    if(map == MAP_FAILED) return -1;

    /* ここでデータを書き込む */

    close(out);
    munmap(map, filesize);
}

これで、map の参照先に代入した値がそのまま出力ファイルに書き込まれるようになります。

ヘッダーを書き込む

ヘッダーの中身はググると出てきます(私はwav ファイルフォーマットを見ました)
初めの4バイトは文字列"RIFF"
次の4バイトはファイルサイズ(filesize - 8)
次の4バイトは文字列"WAVE"
次の4バイトは文字列"fmt "
次の4バイトはヘッダーサイズ(16)
次の2バイトは波形フォーマット(1でいいらしい)
次の2バイトはチャンネル数(channel)
次の4バイトはサンプリング周波数(samplerate)
次の4バイトはデータ速度(samplerate * bitdepth / 8 * channel)
次の2バイトはブロックサイズ(bitdepth / 8 * channel)
次の2バイトはビット深度(bitdepth)
次の4バイトは文字列"data"
次の4バイトはデータサイズ(filesize - 44)
文字列の書き込みにはstring.hのmemcpy関数を使います。

memcpy(map, "RIFF", 4);
((unsigned int *)map)[1] = filesize - 8;
memcpy(map + 4, "WAVEfmt ", 8);
((unsigned int *)map)[4] = 16;
((unsigned short *)map)[10] = 1;
((unsigned short *)map)[11] = channel;
((unsigned int *)map)[6] = samplerate;
((unsigned int *)map)[7] = samplerate * bitdepth / 8 * channel;
((unsigned short *)map)[16] = channel * bitdepth / 8;
((unsigned short *)map)[17] = bitdepth;
memcpy(map + 18, "data", 4);
((unsigned int *)map)[10] = filesize - 44;

中身を書き込む

ここでは例として、440Hzの正弦波を書き込むことにします。math.hのsin関数を使います。コンパイル時に-lmを付けるのを忘れないで下さい。

for(int i = 0; i < samplerate * length; ++i)
    map[i * 2 + 22] =
    map[i * 2 + 23] =
        sin(2 * M_PI * 440 * i / samplerate) * (1 << 15);

プログラム全体は以下のようになります。

#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <string.h>
#include <math.h>

int main(void){
    char *out_filename = "a.wav";
    int out = open(out_filename, O_CREAT | O_RDWR, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
    if(out == -1) return -1;

    int samplerate = 44100;
    short bitdepth = 16, channel = 2;
    double length = 3.;
    int filesize = bitdepth / 8 * channel * samplerate * length + 44;

    if(ftruncate(out, filesize) == -1) return -1;

    short *map = mmap(NULL, filesize, PROT_WRITE, MAP_SHARED, out, 0);
    if(map == MAP_FAILED) return -1;

    memcpy(map, "RIFF", 4);
    ((unsigned int *)map)[1] = filesize - 8;
    memcpy(map + 4, "WAVEfmt ", 8);
    ((unsigned int *)map)[4] = 16;
    ((unsigned short *)map)[10] = 1;
    ((unsigned short *)map)[11] = channel;
    ((unsigned int *)map)[6] = samplerate;
    ((unsigned int *)map)[7] = samplerate * bitdepth / 8 * channel;
    ((unsigned short *)map)[16] = channel * bitdepth / 8;
    ((unsigned short *)map)[17] = bitdepth;
    memcpy(map + 18, "data", 4);
    ((unsigned int *)map)[10] = filesize - 44;

    for(int i = 0; i < samplerate * length; ++i) map[i * 2 + 22] = map[i * 2 + 23] = sin(2 * M_PI * 440 * i / samplerate) * (1 << 15);

    close(out);
    munmap(map, filesize);
}

実行すると440Hzの正弦波が3秒間流れるwavファイルが作られます。

今回はここまでです。
この記事があなたの役に立てば幸いです。