いろいろあってC++で楽曲から取り出した音声信号の処理をしなければいけなくなったので、その時のメモ

「wavファイル」「C言語」で検索すると、Windowsで使えるAPIを使ったwaveフォーマット処理を行う事例などはそこそこ出てくるが、プラットフォームに依存するところが気になるので却下。

Linux環境で動かすので、ALSAなどのライブラリを使う方法もありましたが、今回はブラックボックスを排除して、泥臭く、プリミティブにコードを書いていくことにしました。

ここでは「file.wave」から情報とデータを抽出して、利用しやすい形に加工するところまで行っています。


waveファイルの入力を受け付けて処理する専用のライブラリなし、APIなしで書いているので、GCCないしはMinGWでも何かしらか環境に対応したコンパイラでコンパイルすれば、Windows環境でもLinux環境でもおそらくMac環境でも動くと思います。



実装環境


CPUIntel Core i7-9700K
MemoryDDR 16GB 2666MHz
OSUbuntu 22.04.2 (wsl2)
Compilergcc version 11.3.0
Option-O2, -mcmodel=medium,
makefile:
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"」というフォーマットの確認を行いファイルエラーを検知する。
sizeには、タグとサイズを合わせた8Byteを除いた以降のファイルの全サイズがバイト単位で入っている。


Formatチャンク


サンプリング周波数や量子化ビット数などの、音声のデジタル信号を扱う上での基本的な情報を得ることができる。
tagの「"f","m","t"," "」の4文字を検知すると、サイズ以下の6項目は固定で、それ以降はデータが圧縮された後などに使われるなどする任意の形式。PCMデータの場合、先頭6項目以降は省略されていることが多い。

今回は先頭の6項目だけを読み取り保存し、それ以降はスキップしている。


Dataチャンク


肝心の音声データの入ったチャンク。tagの「"d","a","t","a"」により判別する。

データサイズはファイルにより違うので、このサイズでメモリ(配列)を動的に確保してデータの読み取り、および、保存を行う。
メモリ領域を確保する際、CDなどに用いられる量子化ビット数16ビットなら、環境にもよるが2Byteのshot型を用いることができる。ハイレゾ音源などの量子化ビット数が24ビットの音源などは8ビットずつchar型で保存するか、構造体を作るか、32ビットで保存するか何かしら工夫が求められる。

ステレオ音源の場合、音のデータが「左右左右...」と格納されているらしいので取り扱いに注意


上記フォーマットに従ってチャンクに分解してデータを整理しながら、入力および整理作業を行っていきます。



コード全体


今回の作成したコードは主に以下を参考にさせていただきました。
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
short型の「buffer_16」に対して、実際の音楽データを格納しています。今回ならステレオ音源なので「左右左右...」とデータが格納されているはずです。抽出したデータは編集するもよしFFT解析するもよし、好きに使ってください。



参考:
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


以上、お疲れさまでした。

このエントリーをはてなブックマークに追加
コメントを閉じる

コメント

コメントフォーム
記事の評価
  • リセット
  • リセット