C言語でWAVプログラミング

本記事ではC言語を使ったWAV(Waveform Audio File Format)プログラミングについて説明します。

対象となる読者の方は以下です。

  • C言語の基礎的な知識ある方
  • WAVファイルの構造が知りたい方

環境構築

Windows11でC言語の環境構築する方はWSL2(Windows Subsystem for Linux 2)を導入して、Linuxを使えるようにしたほうがよいかと思います。

WSL2の環境構築は以下の方の記事が参考になります。

Qiita

概要 WSL2 を導入する方法と除去する方法などをまとめました. WSL2 が利用可能になるまでの所要時間:約10分 実…

WSL2がインストールできたら、以下のソースコード hello.c を書いて、

#include <stdio.h>

int main(void)
{
    printf("Hello World!\n");
    return 0;
}

下記のコンパイルと実行ができたら、C言語の環境が出来ています。

gcc hello.c -o hello
./hello
Hello World!

WAVについて

WAVのヘッダー情報

WAVは、RIFF (Resource Interchange File Format) と呼ばれるファイルフォーマットとなります。

WAVの大まかなヘッダー情報は以下です。

図:WAVのヘッダー情報
図:WAVのヘッダー情報

まず、RIFFのヘッダーがあり、その中身は以下のようになります。

表:RIFFのヘッダー
オフセット サイズ 内容
0 4 RIFF ID "RIFF"
4 4 ファイルサイズ-8 0xXX 0xXX 0xXX 0xXX
8 4 フォーマット識別子 "WAVE"

RIFF ID、ファイルサイズ-8、フォーマット識別子の順に並んでいます。

フォーマット識別子については、WAVファイルの場合は”WAVE”です。AVI動画の場合は”AVI”、MIDIの場合は"RMID"になります。

次にファイルフォーマットがRIFFの場合、チャンク(小さいブロック)ごとにデータがまとまっており、WAVでは fmt チャンクとdataチャンクが必ずあります

fmtチャンクは以下のような中身となります。

表:fmtチャンクの中身
オフセット サイズ 内容
0 4 チャンクID "fmt "
4 4 チャンクサイズ 16
8 2 AudioFormat 1 (PCM)
10 2 チャンネル数 1 (モノラル)
12 4 サンプルレート 44100
16 4 ByteRate 88200
20 2 BlockAlign 2
22 2 ビット深度 16

チャンクサイズには「チャンクID」と「チャンクサイズ」自身は含まれないです。

ByteRateは1秒間あたりのデータ量(サンプルレートxチャンネル数xビット深度/8)、BlockAlignは1サンプルフレームのバイト数(チャンネル数xビット深度/8)です。

dataチャンクについては以下のような中身となります(2CHの場合)。

表:dataチャンクの中身
オフセット サイズ 内容
0 4 チャンクID "data"
4 4 チャンクサイズ 0xXX 0xXX 0xXX 0xXX
8 2 1CH(L Ch)の1サンプル目 0xXX 0xXX
10 2 2CH(R Ch)の1サンプル目 0xXX 0xXX
12 2 1CH(L Ch)の2サンプル目 0xXX 0xXX
14 2 2CH(R Ch)の2サンプル目 0xXX 0xXX

ステレオの場合、左チャンネルのデータ・右チャンネルのデータという順でデータが詰められます。

WAVに必須ではありませんが、フィールドレコーダーなどの業務放送機器が作成したWAVデータには iXMLチャンクや bextチャンクなどがあり、誰がいつ何のために録音したかのメタデータを含むチャンクがあります。

ヘッダー情報を実際に確認

実際にWAVのヘッダー情報を確認していきます。ヘッダーを確認するためのバイナリエディタはなんでもよいですが、VSCodeで確認するには Hex Editorがおすすめです。

図:HEX EDITORのインストール
図:HEX EDITORのインストール

Audacityで作成したWAVファイルについて確認します。

Hex Editorで確認できるヘッダー情報は以下となります。左側に16進数のバイト列、右にそれをテキスト化したものがあります。

図:実際のヘッダー情報
図:実際のヘッダー情報

RIFFについてはリトルエンディアンとなっており、複数バイトの数値を配置するとき、下位バイトから先に並べるようになっています。例えば、ファイルサイズを見ると、「88 0E 06 00」となっているため、0x00060E88=396936Byte となります。

fmtチャンクも見てみると、フォーマット:「01 00」=1(PCM)、チャンネル数:「01 00」=1(モノラル)、 サンプリングレート:「44 AC 00 00」=44100 Hz、ByteRate:「CC 04 02 00」=132300 Byte、BlockAlign:「03 00」=3byte、ビット深度:「18 00」=24bit となります。

プログラミング

ソースコード

C言語でWAVの音を2倍にするプログラム(wav_write.c)は以下です。

#include <stdio.h>
#include <stdint.h>
#include <string.h>

/* RIFFヘッダー */
typedef struct {
    char     riff[4];
    uint32_t file_size;
    char     wave[4];
} RiffHeader;

/* チャンクのヘッダー */
typedef struct {
    char id[4];
    uint32_t size;
} ChunkHeader;

/* fmtチャンク */
typedef struct{
    uint16_t audio_format;
    uint16_t num_channels;
    uint32_t sample_rate;
    uint32_t byte_rate;
    uint16_t block_align;
    uint16_t bits_per_sample;
} FmtChunk;

/* データ処理関数 */
void datachunk_proc(FILE* fp_in, FILE* fp_out, FmtChunk fmt, uint32_t datasize);

int main(void)
{
    RiffHeader riffheader;
    ChunkHeader chunk;
    FmtChunk fmt;
    uint8_t data;
    int32_t i;

    /* ファイルオープン Input */
    FILE *fp_in = fopen("input.wav", "rb");
    if (fp_in == NULL) {
        perror("fopen");
        return 1;
    }

    /* ファイルオープン Output */
    FILE *fp_out = fopen("output.wav", "wb");
    if (fp_out == NULL) {
        perror("fopen");
        return 1;
    }

    /* RIFFヘッダーの読み込み */
    if (fread(&riffheader, sizeof(RiffHeader), 1, fp_in) != 1){
        fprintf(stderr, "Header read error\n");
        fclose(fp_in);
        return 1;
    }

    /* RIFFヘッダー書き込み */
    fwrite(&riffheader, sizeof(RiffHeader), 1, fp_out);

    while ( 1 ){
        /* チャンクID を読みだす */
        if (fread(&chunk, sizeof(ChunkHeader), 1, fp_in) != 1){
            /* ファイル終端ならば */
            if (feof(fp_in)) {
                printf("End of file\n");
                break;      /* 正常終了 */
            }
            else{
                printf("size=%ld\n", sizeof(ChunkHeader));
                fprintf(stderr, "ChunkId read error\n");
                fclose(fp_in);
                return 1;            
            }
        }

        /* チャンクヘッダー書き込む */
        fwrite(&chunk, sizeof(ChunkHeader), 1, fp_out);

        /* fmtチャンクの場合 */
        if (memcmp(chunk.id, "fmt ", 4) == 0){
            /* FMTチャンク情報を読み出す */
            fread(&fmt, sizeof(FmtChunk), 1, fp_in);

            /* FMTチャンク書き込み */
            fwrite(&fmt, sizeof(FmtChunk), 1, fp_out);
        }
        /* dataチャンクの場合 */
        else if (memcmp(chunk.id, "data", 4) == 0){
            /* dataチャンクの処理 */
            datachunk_proc(fp_in, fp_out, fmt, chunk.size);
            printf("data process complete!!\n");
        }
        /* その他のチャンク */
        else{
            /* チャンクをコピー */
            for ( i = 0; i < chunk.size; i++ ){
                fread(&data, sizeof(uint8_t), 1, fp_in);
                fwrite(&data, sizeof(uint8_t), 1, fp_out);
            }
        }
    }

    /* ファイル閉じる */
    fclose(fp_in);
    fclose(fp_out);
    return 0;
}

void datachunk_proc(FILE* fp_in, FILE* fp_out, FmtChunk fmt, uint32_t datasize){
    uint32_t i, j;
    uint32_t ch = 0;
    uint32_t shift;
    int32_t data = 0;
    /* サンプル数を計算 */
    uint32_t num_samples = datasize / fmt.block_align;
    /* バイト数計算 */
    uint32_t byte = fmt.bits_per_sample / 8;
    uint8_t in[byte];
    uint8_t out[byte];

    printf("Channels        : %u\n", fmt.num_channels);
    printf("Sample Rate     : %u\n", fmt.sample_rate);
    printf("Bits Per Sample : %u\n", fmt.bits_per_sample);
    printf("Sample Num      : %u\n", num_samples);

    for ( i = 0; i < num_samples; i++ ){
        for ( ch = 0; ch < fmt.num_channels; ch++ ){
            /* データ読み込み */
            fread(in, sizeof(uint8_t), byte, fp_in);

            /* 入力データ作成 */
            data = 0;
            for ( j = 0; j < byte; j++){
                shift = 8*j;
                data |= in[j] << shift;
            }     

            /* 信号処理を記述 */
            data = data * 2;    /* 2倍(+6dB)  */

            /* 出力データ作成 */
            for ( j = 0; j < byte; j++){
                shift = 8*j;
                out[j] = (data >> shift) & 0xFF;
            }

            /* データ書き込み */
            fwrite(out, sizeof(uint8_t), byte, fp_out);
        }
    }
}

ソースコード解説

ファイルを開く

    /* ファイルオープン Input */
    FILE *fp_in = fopen("input.wav", "rb");
    if (fp_in == NULL) {
        perror("fopen");
        return 1;
    }

    /* ファイルオープン Output */
    FILE *fp_out = fopen("output.wav", "wb");
    if (fp_out == NULL) {
        perror("fopen");
        return 1;
    }

入力WAVファイルと出力WAVファイルを開きます。


ヘッダー読み書き

    /* RIFFヘッダーの読み込み */
    if (fread(&riffheader, sizeof(RiffHeader), 1, fp_in) != 1){
        fprintf(stderr, "Header read error\n");
        fclose(fp_in);
        return 1;
    }

    /* RIFFヘッダー書き込み */
    fwrite(&riffheader, sizeof(RiffHeader), 1, fp_out);

入力WAVファイルのRIFFヘッダーを読み込んで、それを出力WAVファイルにコピーします。


チャンクIDを読み出す

    while ( 1 ){
        /* チャンクID を読みだす */
        if (fread(&chunk, sizeof(ChunkHeader), 1, fp_in) != 1){
            /* ファイル終端ならば */
            if (feof(fp_in)) {
                printf("End of file\n");
                break;      /* 正常終了 */
            }
            else{
                printf("size=%ld\n", sizeof(ChunkHeader));
                fprintf(stderr, "ChunkId read error\n");
                fclose(fp_in);
                return 1;            
            }
        }

        /* チャンクヘッダー書き込む */
        fwrite(&chunk, sizeof(ChunkHeader), 1, fp_out);

チャンクIDとチャンクサイズを読み出して、チャンクIDごとに処理の仕方を変更します。読み出せなくてファイル終端の場合は、breakでwhile文の中から抜けます。

読みだしたチャンクIDとチャンクサイズは出力ファイルにコピーします。


チャンクごとに処理

        /* fmtチャンクの場合 */
        if (memcmp(chunk.id, "fmt ", 4) == 0){
            /* FMTチャンク情報を読み出す */
            fread(&fmt, sizeof(FmtChunk), 1, fp_in);

            /* FMTチャンク書き込み */
            fwrite(&fmt, sizeof(FmtChunk), 1, fp_out);
        }
        /* dataチャンクの場合 */
        else if (memcmp(chunk.id, "data", 4) == 0){
            /* dataチャンクの処理 */
            datachunk_proc(fp_in, fp_out, fmt, chunk.size);
            printf("data process complete!!\n");
        }
        /* その他のチャンク */
        else{
            /* チャンクをコピー */
            for ( i = 0; i < chunk.size; i++ ){
                fread(&data, sizeof(uint8_t), 1, fp_in);
                fwrite(&data, sizeof(uint8_t), 1, fp_out);
            }
        }

読み出したチャンクIDがdataチャンク以外の場合、チャンク情報を出力WAVファイルにコピーします。dataチャンクの場合、fmtチャンクの情報を基にデータ処理を行います。


データ読み出し

    for ( i = 0; i < num_samples; i++ ){
        for ( ch = 0; ch < fmt.num_channels; ch++ ){
            /* データ読み込み */
            fread(in, sizeof(uint8_t), byte, fp_in);

            /* 入力データ作成 */
            data = 0;
            for ( j = 0; j < byte; j++){
                shift = 8*j;
                data |= in[j] << shift;
            }     

datachunk_proc でデータ処理を行います。freadでデータを読み込み、リトルエンディアンであることを考慮して取得した値をビットシフトして足し合わせ、入力データを作成します。


信号処理

            /* 信号処理を記述 */
            data = data * 2;    /* 2倍(+6dB)  */

ここに信号処理を記述します。注意点として、上記の書き方だとオーバーフローしたときに狂った値になってしまうので、クリップ処理はしたほうがよいです。ソースコードの見やすさを重視したため、上記のコードはクリップ処理を記述していないです。


データ書き出し

            /* 出力データ作成 */
            for ( j = 0; j < byte; j++){
                shift = 8*j;
                out[j] = (data >> shift) & 0xFF;
            }

            /* データ書き込み */
            fwrite(out, sizeof(uint8_t), byte, fp_out);

出力するデータもリトルエンディアンを考慮してビットシフト後に配列outに格納して、fwriteでデータを書き込みます。

出力結果の確認

入力WAVファイルをinput.wav としてwav_write.cと同じフォルダに入れて、以下のコマンドを実行することでコンパイルと実行ができます。

gcc wav_write.c -o wav_write
./wav_write
Channels        : 1
Sample Rate     : 44100
Bits Per Sample : 24
Sample Num      : 132300
data process complete!!
End of file

Audacityなどのソフトで入力WAVファイルと出力WAVファイルを比較すると、以下のように波形の大きさが2倍になっていることを確認できます。

図:出力結果
図:出力結果

おわりに

C言語によるWAVプログラミングについて解説しました。C言語でWAVファイルを読み書きするブログがあまり見当たらなかったため、記事を書きましたがどうでしょうか?

わかりやすく解説したつもりですが、解説を省いている点もあるため、わかりにくい点についてはコメントしていただければ幸いです。