2012年8月11日土曜日

(1)C言語で音声合成もどき ~WAVファイルを生成する~


この記事は素人が音声合成で遊んでいるだけの記事です。完全に行き当たりばったりなので、紹介している内容の保証はできません。
また、記事の内容を予告なく変更することがあります。


「C言語で音声合成もどき」ということで、このシリーズではフォルマント合成と呼ばれる手法を使った音声合成プログラムをC言語で作っていきます。

本題の音声合成に入る前に、まずは肩慣らしとして音声ファイルを作るプログラムをつくります。

今回は 8bit, 44100Hz, 1ch WAV形式のファイルを作成するプログラムを書いてみます。
(この記事では、比較的実装が簡単なWAV形式を扱います。ひょっとするとMP3形式等のほうが馴染み深いかもしれませんが、MP3ではデータ圧縮にやや複雑なアルゴリズムを使うことになります。)

なお、WAVファイルの構造については
WAVファイルフォーマット
WAVE file format
を参考にしました。


さて、WAVファイル生成ルーチンを実装していきます。
このプログラムを実行すると、1秒間無音が流れるWAVファイルを test.wav という名前で作成します。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// 44100Hz, 8bit のWAVデータ
#define SMPL 44100
#define BIT  8

void wav_write(const char *filename, unsigned char *buffer, size_t size)
{
    size_t filesize;
    unsigned char head[44];

    FILE *fp = fopen(filename, "wb");
    if (fp == NULL) return;

    /* RIFFヘッダ (8バイト) */
    memcpy(head, "RIFF", 4);
    filesize = sizeof(head) + size;
    head[4] = (filesize - 8) >> 0  & 0xff;
    head[5] = (filesize - 8) >> 8  & 0xff;
    head[6] = (filesize - 8) >> 16 & 0xff;
    head[7] = (filesize - 8) >> 24 & 0xff;

    /* WAVEヘッダ (4バイト) */
    memcpy(head + 8, "WAVE", 4);

    /* fmtチャンク (24バイト) */
    memcpy(head + 12, "fmt ", 4);
    head[16] = 16;
    head[17] = 0;
    head[18] = 0;
    head[19] = 0;
    head[20] = 1;
    head[21] = 0;
    head[22] = 1;
    head[23] = 0;
    head[24] = SMPL >> 0  & 0xff;
    head[25] = SMPL >> 8  & 0xff;
    head[26] = SMPL >> 16 & 0xff;
    head[27] = SMPL >> 24 & 0xff;
    head[28] = (SMPL * (BIT / 8)) >> 0  & 0xff;
    head[29] = (SMPL * (BIT / 8)) >> 8  & 0xff;
    head[30] = (SMPL * (BIT / 8)) >> 16 & 0xff;
    head[31] = (SMPL * (BIT / 8)) >> 24 & 0xff;
    head[32] = (BIT / 8) >> 0 & 0xff;
    head[33] = (BIT / 8) >> 8 & 0xff;
    head[34] = BIT >> 0 & 0xff;
    head[35] = BIT >> 8 & 0xff;

    /* dataチャンク (8 + size バイト) */
    memcpy(head + 36, "data", 4);
    head[40] = size >> 0  & 0xff;
    head[41] = size >> 8  & 0xff;
    head[42] = size >> 16 & 0xff;
    head[43] = size >> 24 & 0xff;

    /* 書き出し */
    fwrite(head, sizeof(head), 1, fp);
    fwrite(buffer, size, 1, fp);
    fclose(fp);
}

int main(void)
{
    size_t size = 1 * (SMPL * (BIT/8));    /* 1秒間 */
    unsigned char *buf = (char *) malloc(size);

    memset(buf, 0x80, size);

    wav_write("test.wav", buf, size);

    free(buf);
    return 0;
}

少しだけコードの解説をしておきます。

main関数内の変数 buf は、8bit の音声データの格納先を指します。
音声データのバイト数を計算して、malloc() でメモリを確保しています。

その後に続く
memset(buf, 0x80, size);
でメモリの中身を 0x80 で埋め尽くしています。

8bit量子化では、量子化データの値と振幅の対応は下表のようになります。
8bit量子化での値
(0x00~0xff)
信号の振幅
(-1~+1)
0 (0x00)
-1
128 (0x80)
0
255 (0xff)
+1
無信号、つまり振幅 0 は 0x80 (=128) に相当します。
buf 内の音声データを0x80で埋め尽くすわけですから、出力音声は無音になります。

ちなみに memset(~); の部分を以下のように書き換えれば、440Hzのノコギリ波を生成できます(このコードのしくみは 基本波形の生成 で説明しています)。
ノコギリ波を一旦 -1~+1 の範囲で生成したあと、0~255にスケーリングして buf に書き込んでいます。
int i;
double sig, p = 0;
for (i = 0; i < size; i++) {
    /* ノコギリ波生成 */
    p += 440.0 / SMPL;
    p -= floor(p);
    sig = 2 * (p - 0.5);

    /* 8bit量子化してバッファへ */
    buf[i] = (unsigned char)(127 * sig) + 128;
}

こうして用意した音声データ (buf) を wav_write 関数に渡します。
wav_write 関数は、音声データの先頭にヘッダ情報を追加して、バイナリファイルとして書き出します。

wav_write関数内では、この形式のコードが何度か出現します。
    data[0] = value >> 0  & 0xff;
    data[1] = value >> 8  & 0xff;
    data[2] = value >> 16 & 0xff;
    data[3] = value >> 24 & 0xff;
これは整数 value を8ビットごとに分解して配列に突っ込む操作です(Nビット右シフトして 0b11111111 (= 0xff = 255) でマスクすれば、Nビット目から8ビット分の値を取得できます)。
int型のような4バイト整数を char型/unsigned char型 の配列に展開するときによく使います。

WAV形式のヘッダ構造等については(説明すると長くなるので)省きます。
上の参考ページとコードを照らしあわせながら見てみてください。



次回は、波形を加工して声っぽくするプログラムを作ります。

次回>>

2 件のコメント:

  1. 8bit量子化でsignedになるとありますが、128が無信号なのだとすればそれはunsignedというべきだと思います。

    返信削除
    返信
    1. ご指摘ありがとうございます。
      その通りですね、WAV形式の8bit量子化ではunsigned char型として扱うことになっています。
      ミスを修正して加筆しました。

      削除