タケノコ畑の置手紙

ちょっとしたプログラムのこととかをにわかなりに書いてみるブログ

手始めにノーマライズ

(ブログの存在自体忘れてたなんて言えない・・・)
暑いですか!暑いですね!外に出たくない病が発症しました!

前回信号処理とか言った手前何すればいいか悩んだ結果これになりました。
ノーマライズとは!
正規化のことである。

はい。以上です。
前の記事かなんかでwavファイルは離散データの集合体だって言ったと思います。
離散値は16bitだと32767~-32768までの値をとって、それが振幅とイコールになります。

もし、機器側のつまみをひねってもなかなか音量が上がらないという現象に遭遇したならば、
それはもともとの音データの振幅が小さい故に・・・っていうことが考えられます。
そんな時はどうしましょう?

そこでこのノーマライズというものを使います。
例えば、1,2,3,4,5というデータがあった場合、ある値(ここでは1)に合わせると
    0.2,0.4,0.6,0.8,1という風に置き換える処理のことをいいます。
音を扱う場合は音の最大値で処理をすればよいので、今回はこれを扱っていきましょう。

音を大きくするという方法ではもう一つ「増幅」があります。
もともとのデータに単純に数値をかけるだけで手軽に音を大きくすることができます。
しかし、これでは音割れを起こしてしまう可能性が非常に高くなるという欠点があります。

ノーマライズと単純な増幅の比較として、wavデータを例にグラフを見てみるとこんなかんじ
f:id:TaKeNoKo46:20180719144702p:plain

グラフで見れば一目瞭然ですね!
ノーマライズは先に最大値を検出してから処理を始めるのでオーバーフロー(値のひっくり返り)は起こりません。
対して増幅だと際限なく大きくできるので、適当に操作してしまうとオーバーフローは断然起こりやすいです。

そんなこんなでプログラムに移りましょう。
今回は音が小さいwavファイルを大きな音に書き換えるプログラムです(ようやく実用的なものが!!!)。
動作としては指定したファイルを読み込んで、ノーマライズした後上書き保存するというものです。
出力する過程で余計な情報(アーティスト名とか)を削って
最も単純な形に上書きするので、そこんところ注意です。
もっとも、mp3には情報付与できますが、wavでやるケースはなかなかないと思いますが・・・
開けないwavファイルもあると思いますが、今回はriff形式に限定しています。

#include <stdio.h>
#include <windows.h>
#include <iostream>

using namespace std;
#pragma warning(disable:4996)

int endWithError(char* msg, int error = 0)
{
	//Display error message in console
	cout << msg << "\n";
	system("PAUSE");
	return error;
}

int main(void) {

	char wav_name[64];
	char memory;
	char type[4];
	DWORD parameter, chunkSize;
	short formatType, channels;
	DWORD sampleRate, avgBytesPerSec;
	short bytesPerSample, bitsPerSample;
	unsigned long dataSize, WAV_LENGTH;
	signed short* wav_data;

	char file[64];
	FILE *fp = NULL;

	//ファイルの読み込み
	cout << "読み込むファイル名を入力(拡張子なし) -> ";
	scanf("%s", wav_name);

	sprintf(file, "%s.wav", wav_name);

	fp = fopen(file, "rb");
	if (!fp) return endWithError("Failed to open the wav file");

	//Check that the WAVE file is OK
	fread(type, sizeof(char), 4, fp);
	if (type[0] != 'R' || type[1] != 'I' || type[2] != 'F' || type[3] != 'F')
		return endWithError("No RIFF");

	fread(&parameter, sizeof(DWORD), 1, fp);
	fread(type, sizeof(char), 4, fp);
	if (type[0] != 'W' || type[1] != 'A' || type[2] != 'V' || type[3] != 'E')
		return endWithError("not WAVE");

	fread(type, sizeof(char), 4, fp);
	if (type[0] != 'f' || type[1] != 'm' || type[2] != 't' || type[3] != ' ')
		return endWithError("not fmt ");

	fread(&chunkSize, sizeof(DWORD), 1, fp);				//ここからchunkSizeバイト分がwavのパラメータ領域
	fread(&formatType, sizeof(short), 1, fp);				//基本的にpcm形式
	fread(&channels, sizeof(short), 1, fp);					//wavのチャンネル数
	fread(&sampleRate, sizeof(DWORD), 1, fp);				//サンプリング周波数
	fread(&avgBytesPerSec, sizeof(DWORD), 1, fp);			//1秒間の平均転送レート(=channels*sampleRate*bitsPerSample / 8)
	fread(&bytesPerSample, sizeof(short), 1, fp);			//各サンプル数のbyte数(例えば、16bit, 2channelsなら4)
	fread(&bitsPerSample, sizeof(short), 1, fp);			//量子化ビット数 (16, 8)

	//16bit以外は処理が増えるんですすみませぬ・・・
	if (bitsPerSample != 16) {
		endWithError("not 16bit");
	}

	for (int i = 0; i < 256; i++) {
		fread(type, sizeof(char), 1, fp);
		if (type[0] == 'd') {
			fread(type, sizeof(char), 1, fp);
			if (type[0] == 'a') {
				fread(type, sizeof(char), 1, fp);
				if (type[0] == 't') {
					fread(type, sizeof(char), 1, fp);
					if (type[0] == 'a') {
						break;
					}
				}
			}
		}
		if (i == 255) {
			return 0;
		}
	}

	fread(&dataSize, sizeof(unsigned long), 1, fp);		//上下bit,左右chがばらばらの長さ

	//一度に読み込む長さ(ex.16bitなら2byteずつ)
	memory = bitsPerSample / 8;
	//波形データ長
	WAV_LENGTH = dataSize / memory;
	//読み込んだデータを入れる
	wav_data = new signed short[WAV_LENGTH];
	fread(wav_data, memory, WAV_LENGTH, fp);

	fclose(fp);

	// ここからノーマライズ処理
	// 1ループ目は最大値の検出
	int max = 0;
	for (int i = 0; i < WAV_LENGTH; i++) {
		if (max < abs(wav_data[i])) {
			max = abs(wav_data[i]);
		}
	}
	// 2ループ目は全体をノーマライズ
	for (int i = 0; i < WAV_LENGTH; i++) {
		wav_data[i] = (double)wav_data[i] / max*(pow(2, 15) - 1);          //2^15 - 1は16bit wavの最大値(32767)
	}
	//処理終わり

	//dataSize + 付加情報分 + 36
	parameter = dataSize + 36;
	//元の形式を無視して最も付加情報が少ないファイルに変換するため、最小の16byte
	chunkSize = 16;
	//書き出し
	fp = fopen(file, "wb");
	fwrite("RIFF", sizeof(char), 4, fp);
	fwrite(&parameter, sizeof(DWORD), 1, fp);
	fwrite("WAVE", sizeof(char), 4, fp);
	fwrite("fmt ", sizeof(char), 4, fp);
	fwrite(&chunkSize, sizeof(DWORD), 1, fp);
	fwrite(&formatType, sizeof(short), 1, fp);
	fwrite(&channels, sizeof(short), 1, fp);
	fwrite(&sampleRate, sizeof(DWORD), 1, fp);
	fwrite(&avgBytesPerSec, sizeof(DWORD), 1, fp);
	fwrite(&bytesPerSample, sizeof(short), 1, fp);
	fwrite(&bitsPerSample, sizeof(short), 1, fp);
	fwrite("data", sizeof(char), 4, fp);
	fwrite(&dataSize, sizeof(unsigned long), 1, fp);
	fwrite(&wav_data[0], memory, WAV_LENGTH, fp);

	fclose(fp);
	return 0;
}

解説はコメントを参照してください。
16bitに限定している理由ですが、
8bitではとる値が0~255かつ現在は音質的な意味であまり使われないので省いています。
24bitは3byteデータ型がないので書き込むときに少々手間がかかってしまうのと、
ハイレゾでは他のデータ形式がよく使われているのであんまり需要ないかなと・・・
要は手間に対してリターンが少ないからです(言い訳)

あと読み込みたいwavはコンパイル時に生成されたexeと同じフォルダに入れといてください。

sprintf(file, "WAVE/%s.wav", wav_name);

こんな感じにすればWAVEフォルダの中身を読み込めます。

今回は以上です。
なんかあったらコメントとかくれればわかる範囲でお答えしますので気軽にどうぞ~

それではまた次回の置手紙でお会いしましょう・・・