いろいろあってC++で楽曲から取り出した音声信号の処理をしなければいけなくなったので、その時のメモ
「wavファイル」「C言語」で検索すると、Windowsで使えるAPIを使ったwaveフォーマット処理を行う事例などはそこそこ出てくるが、プラットフォームに依存するところが気になるので却下。
Linux環境で動かすので、ALSAなどのライブラリを使う方法もありましたが、今回はブラックボックスを排除して、泥臭く、プリミティブにコードを書いていくことにしました。
ここでは「file.wave」から情報とデータを抽出して、利用しやすい形に加工するところまで行っています。
waveファイルの入力を受け付けて処理する専用のライブラリなし、APIなしで書いているので、GCCないしはMinGWでも何かしらか環境に対応したコンパイラでコンパイルすれば、Windows環境でもLinux環境でもおそらくMac環境でも動くと思います。
実装環境
main: main.o
g++ -o main -O2 -mcmodel=medium main.o
main.o: main.cpp
g++ -c -O2 -mcmodel=medium main.cpp
音のデジタル化
基本的なデジタルで表現された音声について、経験の無い方向けに多少補足しておきます。詳細にはここでは触れないので、詳しくはデジタル信号処理系の書籍でも読めば最初のほうのページに載ってると思います。
wavファイル(音声ファイル形式)
概要:- デジタルミュージックの最も基本となるファイル形式
- 「WAV」であったり「WAVE」であったりするが同じもの
- パルス符号変調(PCM)
- 音声信号をデジタルデータにサンプリングした形式
- 符号化方式についての規定はない(任意)
- PCM(無圧縮)ADPCM(圧縮)対応
- Mpeg-1 Audio Layer-3(MP3),Indeo Audioなど圧縮方式利用可能
連続したアナログな音の波形をデジタルな不連続なデータに変換(D/A変換)するには、「標本化」「量子化」「符号化」という手順を取る。
A/D変換
図 AD変換の例
アナログデータを一定の時間で区切り、ある時間ごとの信号をアナログ波形の標本として取り出す。サンプリング周波数44.1kHzなら、1秒あたり44,100の区間に分割する。
量子化:
信号レベルを何段階で表現するかを決めて、標本化したデータをその段階のレベルに当てはめることで、整数値に置き換える。量子化ビット数16bitなら、2の16乗個の段階を取る。
符号化:
量子化した値を2進数で表した数値に変換する。
音声ファイル(.wav)フォーマット
フォーマットに関しても、たくさんの方がわかりやすくまとめているので、必要なだけの理解に留めてここでは簡単に触れるだけにします。
waveファイルには以下のようにいくつかのチャンクが含まれており、それぞれに情報を持っています。今回は、その中でも特によく使われる情報を持っている3つのチャンクだけに着目しています。
図 waveフォーマット
RIFFチャンク
Resource Interchange File Formatという汎用のファイル形式のチャンクがある。配列の先頭にあるので「"R","I","F","F"」というタグの確認と、今回なら「"W","A","V","E"」というフォーマットの確認を行いファイルエラーを検知する。
Formatチャンク
サンプリング周波数や量子化ビット数などの、音声のデジタル信号を扱う上での基本的な情報を得ることができる。
今回は先頭の6項目だけを読み取り保存し、それ以降はスキップしている。
Dataチャンク
肝心の音声データの入ったチャンク。tagの「"d","a","t","a"」により判別する。
データサイズはファイルにより違うので、このサイズでメモリ(配列)を動的に確保してデータの読み取り、および、保存を行う。
ステレオ音源の場合、音のデータが「左右左右...」と格納されているらしいので取り扱いに注意
上記フォーマットに従ってチャンクに分解してデータを整理しながら、入力および整理作業を行っていきます。
コード全体
今回の作成したコードは主に以下を参考にさせていただきました。
https://necotech.org/archives/657
環境の違いで動かなかった箇所の修正と自分流にカスタマイズしたものが以下になります。
makefileなども加えたコード全体は以下GitHubに置いています。
https://github.com/sukima-log/wave_data_extract
ライブラリやAPI等のブラックボックスとなりやすいものは排除して処理することを意識したため、冗長なコードになってる感は否めません。
#include <iostream>
#include <stdio.h>
#include <string.h>
#define PATH 255 // ファイルパスの最大長
#define FILE_NAME "./wav/adventurers.WAV" // 入力ファイルパス
// 各チャンク先頭の「tag」と「size」を表す構造体
typedef struct _chunk {
char id[4]; // tag 4文字
unsigned int size; // Chunk size
} ChunkHead;
/* ------ Riff Chunk ------ */
// 「tag="R","I","F","F"」「size=4」
typedef struct _riffChunk {
ChunkHead head; // 共通Chunkヘッダー
char format[4]; // 「format="W""A""V""E"」
} RiffChunk;
// Wave Format Chunk 固定基本情報
// size以降6つの要素は長さ固定、それ以降は読み込んだsizeに従う
typedef struct _wavFmt {
unsigned short audioFormat; // Wavフォーマット
unsigned short channels; // チャンネル数
unsigned int samplePerSecond;// サンプリング周波数
unsigned int bytesPerSecond; // データ量/秒
unsigned short blockAlign; // 単位バイト幅
unsigned short bitsPerSample; // 量子化ビット数
} WaveFileFormat;
/* ------ Wave Format Chunk ------ */
// 「tag="f","m","t"," "」「size=不定」
typedef struct _wavFmtChunk {
ChunkHead chunk; // 共通Chunkヘッダー
WaveFileFormat format; // 楽曲データ 固定基本情報
} WaveFormatChunk;
/* ------ Wave Data Chunk ------ */
// 「tag="d","a","t","a"」「size=不定」
// 「Header+データ内容」をメモリ領域に確保
int main (void) {
/* ファイル入力 */
char filename_in[PATH]; // 入力ファイル名
FILE *file = NULL;
sprintf(filename_in, FILE_NAME);
file = fopen(filename_in, "rb");
// ファイルオープン失敗チェック
if (file == NULL) {
printf("※ファイルオープン失敗\n");
fclose(file);
return 1;
}
/* 音楽データ読み込み */
size_t read_size;
// RIFF Chunk読み込み(ヘッダーチェック)
RiffChunk riff;
read_size = fread(&riff, sizeof(RiffChunk), 1, file);
if (read_size != 1) return 1;
if (strncmp(riff.head.id, "RIFF", 4) != 0) {
fclose(file);
printf("※ファイルフォーマットの異常\n");
return 1;
}
if (strncmp(riff.format, "WAVE", 4) != 0) {
fclose(file);
printf("※ファイルフォーマットの異常\n");
return 1;
}
ChunkHead chunk;
WaveFileFormat format;
unsigned long cu_pos = ftell(file); // ファイル現在地
// ここでは量子化ビット数16bitのものを扱う
if (sizeof(short) != 2) printf("buffer_16のデータ型を2byteのものに書き換え\n");
short *buffer_16;
// ファイル内全検索(終点到達まで)
while (cu_pos < riff.head.size + sizeof(ChunkHead)) {
read_size = fread(&chunk, sizeof(ChunkHead), 1, file);
if (read_size != 1) return 1;
if (chunk.size < 0) break; // フォーマットエラー検知
// fmt Chunk読み込み
if (strncmp(chunk.id, "fmt ", 4) == 0) {
read_size = fread(&format, std::min(chunk.size, (unsigned int)sizeof(WaveFileFormat)), 1, file);
if (read_size != 1) return 1;
fseek(file, chunk.size - (unsigned int)sizeof(WaveFileFormat), SEEK_CUR);
}
// data Chunk読み込み
else if (strncmp(chunk.id, "data", 4) == 0) {
if (format.bitsPerSample == 16) {
// データ格納配列動的確保 (chunk.size/2 : バイトを16bit型で取得するため)
buffer_16 = (short *)calloc(chunk.size/2, sizeof(short));
read_size = fread(buffer_16, sizeof(short), chunk.size/2, file);
if (read_size != chunk.size/2) return 1;
} else {
fclose(file);
printf("量子化ビット数 16bit以外\n");
return 1;
}
} else {
fseek(file, chunk.size, SEEK_CUR); // 認識できないChunkのSkip
}
cu_pos = ftell(file); // 現在のファイル位置取得
}
fclose(file); // ファイルを閉じる
printf("サンプリング周波数 : %d Hz\n", format.samplePerSecond);
printf("量子化ビット数 : %d bit\n", format.bitsPerSample);
printf("ステレオ「2」/ モノラル「1」 : %d\n", format.channels);
return 0;
}
サンプリング周波数 : 44100 Hz
量子化ビット数 : 16 bit
ステレオ「2」/ モノラル「1」 : 2
参考:
https://necotech.org/archives/657
https://shop.cqpub.co.jp/hanbai/books/44/44731/44731.pdf
http://fftest33.blog.fc2.com/blog-entry-98.html
音楽素材:
https://www.music-note.jp/bgm/fantasy.html
以上、お疲れさまでした。