1.はじめに
タイトルの通り、C言語でギターエフェクターを作成しました。第一回はC言語で音声ファイルを扱う方法についてです。wavファイルの音声データをC言語の配列として取り込みます。その方法について勉強した内容をまとめます。
2.wavファイルのフォーマット
プログラミングでファイルを扱おうと思った時、そのファイルのフォーマット(決まり事)を知らないといけません。wavファイルはRIFFという形式に則っていて、この表のように大きく三つのブロックに分かれています。
チャンク名 |
パラメータ |
Byte数 |
内容 |
RIFFヘッダ |
ID |
4 |
“RIFF” |
size |
4 |
datasize + 36 |
type |
4 |
“WAVE” |
フォーマット チャンク |
ID |
4 |
“fmt␣” |
size |
4 |
16 |
format |
2 |
1(下記参照) |
channel |
2 |
モノラル:1, ステレオ:2 |
samples per sec |
4 |
サンプリング周波数 |
bytes per sec |
4 |
一秒あたりのバイト数 |
block size |
2 |
8bitモノラルで1 |
bits per sample |
2 |
量子化ビット数 |
データチャンク |
ID |
4 |
“data” |
size |
4 |
datasize |
data |
datasize |
実際の音データ |
チャンクとはデータの塊であり、最初にそのチャンクが何を示すチャンクであるかを示すIDと、そのチャンクのバイト数を書き込み、その後ろに実際のデータを書き込みます。wavファイルは
- フォーマットチャンク:音データの詳細情報を記録する
- データチャンク:実際の音データを記録する
という二つのチャンクで構成されています。
RIFFヘッダ
ID:「このファイルがRIFF形式に沿っていますよ」という事を示します。
size:音データ+36(RIFFのtypeパラメータ~データチャンクのsizeパラメータの総サイズ)
type:そのファイルの識別子を示します。
フォーマットチャンク
ID:「このチャンクがフォーマットチャンクですよ」という事を示しています。
size:フォーマットチャンクのサイズを書き込みます。拡張機能を利用しなければ16です。
format:そのファイルの形式を示します。通常のwavファイルの場合は1を書き込みます。
channel:その音声データのチャンネル数を示します。3ch以上も可能だと思いますが、再生環境がないのでわからないのとエフェクターを作るという目標なのでモノラルしか使用してません。
samples per sec:サンプリング周波数を示します。
bytes per sec:一秒あたりのバイト数=サンプリング周波数×量子化ビット数×チャンネル数/8
block size:1ブロックのサイズ=量子化ビット数/8×チャンネル数
bits per sample:量子化ビット数
データチャンク
ID:「このチャンクがデータチャンクですよ」という事を示しています。
size:実際に格納される音データのサイズを書き込みます。
data:実際の音データを書き込みます。音データがどのようなデジタルデータになっているかはこのサイトがわかりやすいと思います。
参考:Illustrator基礎講座-デジタル
3.wavファイルの読み込み/書き込み
wavファイルの入出力を行う関数は今後も使い続けるので、ヘッダファイル化しました。そのヘッダファイル<audioio.h>には
- 音データのパラメータを格納するWAV_PRM構造体
- wavファイルの取り込みを行うaudio_read関数
- wavファイルの出力を行うaudio_write関数
を記述しました。
追記:ヘッダファイルの中に直接関数を書く事は推奨されていません。この方がわかりやすいですが、ちゃんとヘッダファイルと関数を記述するソースファイルに分けましょう。
WAV_PRM構造体
typedef struct
{
int fs; //サンプリング周波数
int L; //データ長
} WAV_PRM;
取り込んだデータの各パラメータはこれらのメンバに格納されます。フォーマットチャンクで取り込み可能なパラメータならメンバを増やす事も可能です。
読み込みと書き込みの流れは以下のようになります。
wavファイルをaudio_readで読み込み、サンプリング周波数などのパラメータはWAV_PRM構造体に、音データはdouble型配列に格納して、プログラムに渡します。ここで好きなエフェクトをかけて、処理後のパラメータと音データをaudio_writeを使ってwavファイルに書き込みます。
3.1 wavファイルの読み込み
audio_read関数
double *audio_read(WAV_PRM *prm, char *filename)
{
//変数宣言
FILE *fp;
int n;
double *data;
char header_ID[4];
long header_size;
char header_type[4];
char fmt_ID[4];
long fmt_size;
short fmt_format;
short fmt_channel;
long fmt_samples_per_sec;
long fmt_bytes_per_sec;
short fmt_block_size;
short fmt_bits_per_sample;
char data_ID[4];
long data_size;
short data_data;
//wavファイルオープン
fp = fopen(filename, "rb");
//wavデータ読み込み
fread(header_ID, 1, 4, fp);
fread(&header_size, 4, 1, fp);
fread(header_type, 1, 4, fp);
fread(fmt_ID, 1, 4, fp);
fread(&fmt_size, 4, 1, fp);
fread(&fmt_format, 2, 1, fp);
fread(&fmt_channel, 2, 1, fp);
fread(&fmt_samples_per_sec, 4, 1, fp);
fread(&fmt_bytes_per_sec, 4, 1, fp);
fread(&fmt_block_size, 2, 1, fp);
fread(&fmt_bits_per_sample, 2, 1, fp);
fread(data_ID, 1, 4, fp);
fread(&data_size, 4, 1, fp);
//パラメータ代入
prm->fs = fmt_samples_per_sec;
prm->bits = fmt_bits_per_sample;
prm->L = data_size / 2;
//音声データ代入
data = calloc(prm->L,sizeof(double));
for (n = 0; n < prm->L; n++) {
fread(&data_data, 2, 1, fp);
data[n] = (double)data_data / 32768.0;
}
fclose(fp);
return data;
}
単純にfread関数を用いて順々にパラメータを読み込んでいき、利用したいパラメータはポインタ渡しを用いてWAV_PRM構造体変数に代入します。
読み込んだ音データは16bitの場合-32768から32767の整数値を持っています。なのでshort型がぴったりです。freadで読み込んだ音データは32768で割って、-1から1までの値になり、double型に変換して配列に格納します。これを戻り値としてプログラムに渡します。
3.2 wavファイルの書き込み
audio_write関数
void audio_write(double *data, WAV_PRM *prm, char *filename)
{
//変数宣言
FILE *fp;
int n;
char header_ID[4];
long header_size;
char header_type[4];
char fmt_ID[4];
long fmt_size;
short fmt_format;
short fmt_channel;
long fmt_samples_per_sec;
long fmt_bytes_per_sec;
short fmt_block_size;
short fmt_bits_per_sample;
char data_ID[4];
long data_size;
short data_data;
//ファイルオープン
fp = fopen(filename, "wb");
//ヘッダー書き込み
header_ID[0] = 'R';
header_ID[1] = 'I';
header_ID[2] = 'F';
header_ID[3] = 'F';
header_size = 36 + prm->L * 2;
header_type[0] = 'W';
header_type[1] = 'A';
header_type[2] = 'V';
header_type[3] = 'E';
fwrite(header_ID, 1, 4, fp);
fwrite(&header_size, 4, 1, fp);
fwrite(header_type, 1, 4, fp);
//フォーマット書き込み
fmt_ID[0] = 'f';
fmt_ID[1] = 'm';
fmt_ID[2] = 't';
fmt_ID[3] = ' ';
fmt_size = 16;
fmt_format = 1;
fmt_channel = 1;
fmt_samples_per_sec = prm->fs;
fmt_bytes_per_sec = prm->fs * prm->bits / 8;
fmt_block_size = prm->bits / 8;
fmt_bits_per_sample = prm->bits;
fwrite(fmt_ID, 1, 4, fp);
fwrite(&fmt_size, 4, 1, fp);
fwrite(&fmt_format, 2, 1, fp);
fwrite(&fmt_channel, 2, 1, fp);
fwrite(&fmt_samples_per_sec, 4, 1, fp);
fwrite(&fmt_bytes_per_sec, 4, 1, fp);
fwrite(&fmt_block_size, 2, 1, fp);
fwrite(&fmt_bits_per_sample, 2, 1, fp);
//データ書き込み
data_ID[0] = 'd';
data_ID[1] = 'a';
data_ID[2] = 't';
data_ID[3] = 'a';
data_size = prm->L * 2;
fwrite(data_ID, 1, 4, fp);
fwrite(&data_size, 4, 1, fp);
//音声データ書き込み
fp = fopen(filename, "wb");
for (n = 0; n < prm->L; n++) {
//リミッター
if (data[n] > 1) {
data_data = 32767;
} else if (data[n] < -1) {
data_data = -32767;
} else {
data_data = (short)(data[n] * 32767.0);
}
fwrite(&data_data, 2, 1, fp);
}
fclose(fp);
}
wavファイルのフォーマットに合うように1バイトずつ値を入れていきます。最後にデータを書き込んでいきますが、short型の値は-32768から32767までの値しか扱えません。よって書き込みたいデータがオーバーフローしないように、リミッターをかけています。
4.実行
以下のようなテストコードで実行した所、入力した音源と同じ音源が出力されました。今後はこのプログラムを基本としてエフェクトの部分を書き込んでいきます。
#include <stdio.h>
#include <stdlib.h>
#include "audioio.h"
int main(int argc, char *argv[])
{
//変数宣言
WAV_PRM prm_in, prm_out;
double *data_in, *data_out;
int n;
char filename[64];
if(argc != 2){
printf("引数が違います\n");
exit( 1 );
}
//出力ファイル名入力
printf("output file name : ");
scanf("%s", filename);
//wavファイルの読み込み
data_in = audio_read(&prm_in, argv[1]);
//パラメータコピー
prm_out.fs = prm_in.fs;
prm_out.bits = prm_in.bits;
prm_out.L = prm_in.L;
//データコピー(実際にはこの代わりにエフェクト処理をかける)
data_out = calloc(prm_out.L, sizeof(double)); //メモリの確保
for (n = 0;n < prm_out.L;n++){
data_out[n] = data_in[n];
}
//書き込み
audio_write(data_out, &prm_out, filename);
//メモリ解放
free(data_in);
free(data_out);
return 0;
}
入力するファイル名はコマンドライン引数として入力する。エフェクトのパラメータと出力ファイル名はscanfで入力する(今回はパラメータ入力はなし)。
5.まとめ
単純にfreadとfwriteを使ってwavファイルを読み込み、音声データをC言語で扱えるようにしました。
今回のプログラム、量子化bit数が取り出せるようにはしていますが、データ型の関係上16bitのデータしか扱えません。if文とかで読み込んだ量子化bit数に合わせて場合分けして対応すればいいのですが、正直16bitのデータ以外扱っていくつもりがないのでこのままでいいかなと。。。
実はwavファイルには拡張機能もあって、違うチャンクが割り込んで来る事もあります。今回のように単純に順番に読んでいくだけだと、気づいたら全然違うチャンクを読んでいた!なんて事になりかねません。自分の用途には今の所問題ないけど、量子化bitの問題と合わせてそのうち改善しようかな。