タケノコ畑の置手紙

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

OpenALで立体音響

スマブラとゼノブレ2の追加エピが発表されました!楽しみですね!

ということでここからは応用編になります。
手始めにOpenALの機能を用いて立体音響を再生していきたいと思います。
立体音響は名前の通り立体的で奥行きのある音のことをいいます。

今回は手持ちのwavファイルを読み込んで立体的な音を再現するプログラムです。

 

まずwavを読み込むためには・・・ということで、構造を見ていきましょう。

f:id:TaKeNoKo46:20180622233538p:plain

wavをバイナリエディタで開いたものがこちらです。よくわかりませんね!

でも右の奴と比較してよぉ~く見てみるとなにやら文字が出ていることがわかります。

「wav 構造」とかで検索すればもっと詳しく説明されてるサイトとかありますのでここでは最も単純な構造のものを解説します。上の図をブロックに分けると・・・

52 49 46 46

52 3B AA 11

57 41 56 45

66 6D 74 20

10 00 00 00

01 00

01 00

11 2B 00 00

11 2B 00 00

01 00

08 00

64 61 74 61

00 3B AA 11

00 00 00 00

                   ↓太字が定数

RIFF

データサイズ

WAVE

fmt

情報領域サイズ

formatタグ

チャンネル

標本化周波数

平均転送レート

単位転送数

量子化bit

data

波形部データサイズ

→波形データ

こんな情報が入っています。これをfreadでちまちま読み込んでいけばいいですね。(本当はライブラリとか使えば楽にできるのは内緒 alutとか・・・)

 

 

上記を踏まえてコードに移ります。以下をコピペ!

#define _USE_MATH_DEFINES

#include <windows.h>
#include <stdio.h>
#include <al.h>
#include <alc.h>
#include <math.h>

#pragma comment(lib, "OpenAL32.lib")
//fopenの警告を無視
#pragma warning(disable:4996)

int main() {
	FILE *fp;
	unsigned long memory = 0, length = 0;

	fp = fopen("sound.wav", "rb");
	if (!fp) {
		printf("ファイルを開けない\n");
		return 0;
	}
	//ファイルを開くときの変数
	char type[4];
	DWORD size, chunkSize;
	short formatType, channels;
	DWORD sampleRate, avgBytesPerSec;
	short bytesPerSample, bitsPerSample;
	unsigned long dataSize;

	//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') {
		printf("not 'RIFF'\n");
		return 0;
	}
	fread(&size, sizeof(DWORD), 1, fp);
	fread(type, sizeof(char), 4, fp);
	if (type[0] != 'W' || type[1] != 'A' || type[2] != 'V' || type[3] != 'E') {
		printf("not 'WAVE'\n");
		return 0;
	}
	fread(type, sizeof(char), 4, fp);
	if (type[0] != 'f' || type[1] != 'm' || type[2] != 't' || type[3] != ' ') {
		printf("not 'fmt '\n");
		return 0;
	}
	//ここからchunkSizeバイト分がwavのパラメータ領域
	fread(&chunkSize, sizeof(DWORD), 1, fp);
	//基本的にpcm形式
	fread(&formatType, sizeof(short), 1, fp);	
	//wavのチャンネル数
	fread(&channels, sizeof(short), 1, fp);				
	//サンプリング周波数
	fread(&sampleRate, sizeof(DWORD), 1, fp);				
	//1秒間の平均転送レート(=channels*sampleRate*bitsPerSample / 8)
	fread(&avgBytesPerSec, sizeof(DWORD), 1, fp);			
	//各サンプル数のbyte数(例えば、16bit, 2channelsなら4)
	fread(&bytesPerSample, sizeof(short), 1, fp);			
	//量子化ビット数 (16, 8)
	fread(&bitsPerSample, sizeof(short), 1, fp);			

	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) {
			printf("データがありません。\n");
			return 0;
		}
	}
	//波形データ長
	fread(&dataSize, sizeof(unsigned long), 1, fp);
	//1サンプルの長さ
	memory = bitsPerSample / 8;
	//どのくらい読み込むか
	length = dataSize / memory;
	signed short* wav_data = new signed short[length];
	
	//波形データ
	fread(wav_data, memory, length, fp);
	fclose(fp);

	//いつもの
	ALCdevice* device = alcOpenDevice(nullptr);
	ALCcontext* context = alcCreateContext(device, nullptr);
	alcMakeContextCurrent(context);
	ALuint buffer;
	ALuint source;
	alGenBuffers(1, &buffer);
	alGenSources(1, &source);
	ALuint frequency = sampleRate;

	//ステレオだった場合モノラルに統合
	if (channels == 2) {
		for (int i = 0; i < length; i+=2) {
			//平均値をとる
			wav_data[i / 2] = wav_data[i] / 2 + wav_data[i + 1] / 2;
		}
	}
	//長さもモノラルに直す
	length = length / channels;
	if (bitsPerSample == 8) {
		printf("8bitです。");
		return 0;
	}
	ALuint format = AL_FORMAT_MONO16;
	alBufferData(buffer, format, &wav_data[0], length * sizeof(signed short), sampleRate);
	alSourcei(source, AL_BUFFER, buffer);
	alSourcePlay(source);

	//リスナー(自分)を空間座標に配置
	ALfloat ListenerPos[] = { 0.0, 0.0, 0.0 };   
	alSourcefv(source, AL_POSITION, ListenerPos);
	alSourcei(source, AL_LOOPING, AL_TRUE);
//バッファに格納したので消してよい delete[] wav_data; printf("再生中"); int i = 0; while (1) { //ソース(音源)を空間座標中でぐるぐる回す ALfloat SourcePos[] = { cos(2 * M_PI*i / 360), 0.0, sin(2 * M_PI*i / 360) }; //設定を適用 alSourcefv(source, AL_POSITION, SourcePos); Sleep(30); i++; } system("PAUSE"); alDeleteBuffers(1, &buffer); alDeleteSources(1, &source); //OpenALの後始末 alcMakeContextCurrent(nullptr); alcDestroyContext(context); alcCloseDevice(device); return 0; }

読み込み部分は先ほど説明した通り、上から順番にデータを読み込んでいきます。

それを用いた配列の確保、パラメータの設定をするのでここをミスるとうまくいかない場合が多いです。

 

//リスナー(自分)を空間座標に配置

ALfloat ListenerPos = { 0.0, 0.0, 0.0 };

alSourcefv(source, AL_POSITION, ListenerPos);

while (1) {

    //ソース(音源)を空間座標中でぐるぐる回す

    ALfloat SourcePos = { cos(2 * M_PI*i / 360), 0.0,

                 sin(2 * M_PI*i / 360) };

    //設定を適用

    alSourcefv(source, AL_POSITION, SourcePos);

    Sleep(30);

    i++;

 正直解説するところが少ないですが、強いて言うならここでしょう。

無限ループ内で30 msごとに音源の位置を1度ずつ変化させていきます。

こうすることで自分を中心に音源がぐるぐる回る音が再生されると思います。ヘッドフォン推奨。

ちなみに~Pos関数{x座標, y座標, z座標}が引数となっています。横、高さ、奥行きって感じですね。数学の教科書とかだとyとzが反対になってたりしますが、個人的にはこちらの表記が好きです。(自分語り)

 

 

以上!   ・・・というのも味気ないのでモノラル変換について

wavファイルの波形データは、ステレオならL1,R1,L2,R2,L3,R3・・・のように左右の音データが交互に並んでいます。例えば左chのみを抽出したいときは、配列の0番目、2番目、4番目・・・という風に読み込んで間を詰めればOKです。

これを応用してL1とR1の平均、L2とR2の平均、・・・というような処理がこれです↓

for (int i = 0; i < length; i+=2) {

    //平均値をとる

    wav_data[i / 2] = wav_data[i] / 2 + wav_data[i + 1] / 2;

 間を詰めるので、データ長は半分になります。

なぜこのようなことをするのかというと、音源位置を指定するOpenALの機能はモノラル音源しか対応していないためです。感覚的にもお分かりいただけるでしょう。

 

割とマジでこれくらいしか書くことないです。あとは自分で改変して練習してみましょう!

次回からは…信号処理でも…

音を読み込んでそれになにかしら変化を与えるための処理をやっていきます。多分。

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