サウンド - サスペンド・レジューム2013/11/21

サンプルではBGMが流れ続けますが、一旦サスペンドで音を止めると次にレジュームしても止まったままです。
再開できるように、サウンドのサスペンド・レジューム処理を追加します。

Sound.cpp
/**************
    一時停止
 **************/
void	SoundPlayer::pause(void)
{
	if ( state == PLAYING ) {				// 再生中
		if ( bqPlayerObject ) {
			(*bqPlayerPlay)->SetPlayState(bqPlayerPlay, SL_PLAYSTATE_PAUSED);		// 一時停止状態
		}
		state = PAUSED;
	}
}

/**********
    再開
 **********/
void	SoundPlayer::resume(void)
{
	if ( state == PAUSED ) {				// 一時停止中
		play();
	}
}

再生(SL_PLAYSTATE_PLAYING)や停止(SL_PLAYSTATE_STOPPED)と同様に、SetPlayStateで一時停止(SL_PLAYSTATE_PAUSED)の状態を設定します。
再開は準備からの再生と同じ処理になります。


サンプル
AppMain.cpp
/**************
    一時停止
 **************/
void	pause_app(void)
{
	for (int i = 0; i < 5; i++) {
		sound_player[i].pause();
	}
}

/**********
    再開
 **********/
void	resume_app(void)
{
	for (int i = 0; i < 5; i++) {
		sound_player[i].resume();
	}
}

サスペンド・レジューム時に、それぞれプレイヤーのサスペンド・レジュームを呼んでいる…だけです。


プロジェクト一式は、こちらから。

サウンド - 音量設定2013/11/20

前回サウンドの最低限の処理として音を鳴らせるようにしましたが、今回はサウンドの音量を変更できるようにしてみます。

主な追加・変更点です。
Sound.cpp
	result = (*bqPlayerObject)->GetInterface(bqPlayerObject, SL_IID_VOLUME, &bqPlayerVolume);
	assert(SL_RESULT_SUCCESS == result);									// 音量インタフェース
	set_volume(_vol);														// 音量設定

サウンド再生準備関数 prepare内の処理です。prepareの引数に音量 _volが追加されています。
OpenSLで音量を設定するためのインタフェース bqPlayerVolumeを取得しています。

/*****************************************************
    音量設定
		引数	_vol = 音量(0.0:最小 ~ 1.0:最大)
 *****************************************************/
void	SoundPlayer::set_volume(float _vol)
{
	volume = _vol;
	set_volume();
}

void	SoundPlayer::set_volume(void)
{
	if ( bqPlayerObject ) {
		float		_vol = volume*master_volume;
		SLresult	result;

		result = (*bqPlayerVolume)->SetVolumeLevel(bqPlayerVolume, (_vol >= 1.0f) ? 0 : ((_vol < 0.01f) ? -16000
																							: (SLmillibel)(8000.0f*log10f(_vol))));
		assert(SL_RESULT_SUCCESS == result);
	}
}

音量設定関数です。
音量は 0.0(最小)~1.0(最大)で設定するようにしていますが、SetVolumeLevelはミリベルで指定するようになっていますので常用対数で変換しています。

全体の音量を変更できるように、マスター音量 master_volumeを静的メンバとして持っています。引数の無い方の set_volumeは、マスター音量を変更したときに既に鳴っているサウンドの音量を再設定するために呼び出します。

/*************************************************************
    マスター音量設定
		引数	_vol = マスター音量(0.0:最小 ~ 1.0:最大)
		戻り値	マスター音量が変更されたか
 *************************************************************/
Bool	SoundPlayer::set_master_volume(float _vol)
{
	if ( master_volume != _vol ) {
		master_volume = _vol;
		return	TRUE;
	}
	return	FALSE;
}

マスター音量変更のための静的メンバ関数 set_master_volumeです。
変更がなければ個々のサウンドの再設定は要らないので、変更の有無を戻り値として返すようにしています。


サンプルの変更部分です。
AppMain.cpp
	if ( sys::TouchPanel[0].flag & sys::TouchManager::TRIGGER ) {
		sound_player[se_track].play(sound_data[SOUND_SE], sound_size[SOUND_SE]);			// SE再生
		se_track = ++se_track % 4;
		sound_player[4].set_volume((se_track % 2) ? 0.5f : 1.0f);			// BGM音量変更
	}

画面をタッチする度に、BGMの音量を上げたり下げたりしています。


プロジェクト一式は、こちらから。

OpenSLでサウンド再生2013/11/19

OpenSLによるサウンドの再生です。
今回は最低限の処理として、PCMデータの再生を行います。
NDKにあるサンプル native-audioを参考にしていますが、データはWAVファイルから読み込むようにしています。

サウンドプレイヤークラス sys::SoundPlayer
Sound.h
#ifndef	___SOUND_H___
#define	___SOUND_H___

#include "common.h"

#include <SLES/OpenSLES.h>
#include <SLES/OpenSLES_Android.h>


namespace sys
{

/************************
    サウンドプレイヤー
 ************************/
class SoundPlayer
{
public :

	static SLObjectItf	engineObject;					// エンジンオブジェクト
	static SLEngineItf	engineEngine;					// インタフェース

	static void		init_engine(void);					// 初期化
	static void		quit_engine(void);					// 終了


private :

	SLObjectItf						outputMixObject;			// 出力オブジェクト
	SLObjectItf						bqPlayerObject;				// プレイヤーオブジェクト
	SLPlayItf						bqPlayerPlay;				// インタフェース
	SLAndroidSimpleBufferQueueItf	bqPlayerBufferQueue;		// バッファキューインタフェース
	SLVolumeItf						bqPlayerVolume;				// 音量インタフェース

	char*	sound_data;				// サウンドデータ
	u32		sound_size;				// サウンドデータサイズ
	int		sound_loop;				// ループカウンタ

public :

		SoundPlayer(void);				// コンストラクタ
		~SoundPlayer();					// デストラクタ
	void	play(const void*, u32, int _loop = 1);				// 再生
	void	play(void);
	void	prepare(const void*, u32, int _loop = 1);			// 再生準備
	void	stop(void);											// 停止

	void	callback_wav(void);						// 再生コールバック
};

}

#endif
/********************* End of File ********************************/

静的メンバが、サウンドシステム全体として必要な共通部分です。
native-audioを見ると outputMixオブジェクトは Engineオブジェクトを作成するとき一緒に作成していますが、プレイヤー毎に作成しなければいけないようです。

Sound.cpp
/********************
    エンジン初期化
 ********************/
void	SoundPlayer::init_engine(void)
{
	SLresult	result;

	result = slCreateEngine(&engineObject, 0, NULL, 0, NULL, NULL);			// エンジンオブジェクト作成
	assert(SL_RESULT_SUCCESS == result);

	result = (*engineObject)->Realize(engineObject, SL_BOOLEAN_FALSE);		// リアライズ
	assert(SL_RESULT_SUCCESS == result);

	result = (*engineObject)->GetInterface(engineObject, SL_IID_ENGINE, &engineEngine);
	assert(SL_RESULT_SUCCESS == result);									// インタフェース取得
}

/******************
    エンジン終了
 ******************/
void	SoundPlayer::quit_engine(void)
{
	if ( engineObject ) {
		(*engineObject)->Destroy(engineObject);
		engineObject = NULL;
	}
}

サウンドシステム全体で必要なエンジン(Engine)オブジェクトとインタフェースを作成・初期化しています。

/********************
    コンストラクタ
 ********************/
SoundPlayer::SoundPlayer(void)
{
	SLresult	result;

	result = (*engineEngine)->CreateOutputMix(engineEngine, &outputMixObject, 0, NULL, NULL);
	assert(SL_RESULT_SUCCESS == result);									// 出力オブジェクト作成

	result = (*outputMixObject)->Realize(outputMixObject, SL_BOOLEAN_FALSE);
	assert(SL_RESULT_SUCCESS == result);									// リアライズ

	bqPlayerObject = NULL;
}

/******************
    デストラクタ
 ******************/
SoundPlayer::~SoundPlayer()
{
	stop();
	(*outputMixObject)->Destroy(outputMixObject);
}

プレイヤーのコンストラクタ(及び、デストラクタ)です。
エンジンインタフェースから、出力(outputMix)オブジェクトを作成しています。
肝心のプレイヤーオブジェクトはPCMデータのフォーマットが必要なので、再生時に作成します。

struct WaveFormat
{
	s8		riff[4];
	u32		total_size;
	s8		fmt[8];
 	u32		fmt_size;
	u16		format;
	u16		channel;
	u32		rate;
	u32		avgbyte;
	u16		block;
	u16		bit;
	s8		data[4];
	u32		data_size;
};

/****************************************************
    再生
		引数	_data = サウンドデータ
				_size = サウンドデータサイズ
				_loop = ループ回数(0:無限ループ)
 ****************************************************/
void	SoundPlayer::prepare(const void* _data, u32 _size, int _loop)
{
	stop();


	SLDataLocator_AndroidSimpleBufferQueue	loc_bufq = {SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, 2};
	SLDataFormat_PCM						format_pcm;
	SLDataSource							audioSrc = {&loc_bufq, &format_pcm};

	switch ( *((u32*)_data) ) {						// データフォーマット
	  case 0x46464952 :			// WAV
		{
			WaveFormat*		_info = (WaveFormat*)_data;

			format_pcm.formatType		= SL_DATAFORMAT_PCM;
			format_pcm.numChannels		= (SLuint32)_info->channel;
			format_pcm.samplesPerSec	= (SLuint32)_info->rate*1000;
			format_pcm.bitsPerSample	= (SLuint32)_info->bit;
			format_pcm.containerSize	= (SLuint32)_info->bit;
			format_pcm.channelMask		= (_info->channel == 1) ? SL_SPEAKER_FRONT_CENTER : (SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT);
			format_pcm.endianness		= SL_BYTEORDER_LITTLEENDIAN;

			sound_data = (char*)((u32)_data + sizeof(WaveFormat));
			sound_size = _info->data_size;
			sound_loop = _loop;
		}
		break;

	  default :
		assert(FALSE);
		break;
	}

	SLDataLocator_OutputMix		loc_outmix = {SL_DATALOCATOR_OUTPUTMIX, outputMixObject};
	SLDataSink					audioSnk = {&loc_outmix, NULL};

	const SLInterfaceID		ids[3] = {SL_IID_PLAY, SL_IID_BUFFERQUEUE, SL_IID_VOLUME};
	const SLboolean			req[3] = {SL_BOOLEAN_TRUE, SL_BOOLEAN_TRUE, SL_BOOLEAN_TRUE};

	SLresult	result;

	result = (*engineEngine)->CreateAudioPlayer(engineEngine, &bqPlayerObject, &audioSrc, &audioSnk, 3, ids, req);
	if ( SL_RESULT_SUCCESS != result ) {									// プレイヤーオブジェクト作成
		bqPlayerObject = NULL;
		return;
	}

	result = (*bqPlayerObject)->Realize(bqPlayerObject, SL_BOOLEAN_FALSE);	// リアライズ
	assert(SL_RESULT_SUCCESS == result);

	result = (*bqPlayerObject)->GetInterface(bqPlayerObject, SL_IID_PLAY, &bqPlayerPlay);
	assert(SL_RESULT_SUCCESS == result);									// インタフェース取得

	result = (*bqPlayerObject)->GetInterface(bqPlayerObject, SL_IID_BUFFERQUEUE, &bqPlayerBufferQueue);
	assert(SL_RESULT_SUCCESS == result);									// バッファキューインタフェース

	result = (*bqPlayerObject)->GetInterface(bqPlayerObject, SL_IID_VOLUME, &bqPlayerVolume);
	assert(SL_RESULT_SUCCESS == result);									// 音量インタフェース

	result = (*bqPlayerBufferQueue)->RegisterCallback(bqPlayerBufferQueue, bqPlayerCallbackWAV, this);
	assert(SL_RESULT_SUCCESS == result);									// 再生コールバック設定

	(*bqPlayerBufferQueue)->Enqueue(bqPlayerBufferQueue, sound_data, (SLuint32)sound_size);
}

void	SoundPlayer::play(void)
{
	if ( bqPlayerObject ) {
		(*bqPlayerPlay)->SetPlayState(bqPlayerPlay, SL_PLAYSTATE_PLAYING);			// 再生開始
	}
}

void	SoundPlayer::play(const void* _data, u32 _size, int _loop)
{
	prepare(_data, _size, _loop);
	play();
}

サウンドの再生部分です。
再生されるまでそこそこ処理があるので、準備(prepare)と再生(play)の関数を分けています。

まず124~146行で WAVデータのヘッダから PCMのフォーマットを設定します。
そして sound_data・sound_sizeに PCMデータ本体のアドレスとサイズを格納しておきます。

次に156行でプレイヤーオブジェクトを作成して、175行までいろいろ OpenSLとして必要な設定を行います(今回音量の変更は行っていませんが、とりあえずインタフェースの取得だけ行っています)。

177行の Enqueueでサウンドデータをキューに送ります。
その後、183行で状態を SL_PLAYSTATE_PLAYINGに設定することによって再生が開始されます。

/**********************
    再生コールバック
 **********************/
static
void	bqPlayerCallbackWAV(SLAndroidSimpleBufferQueueItf, void* context)
{
	((SoundPlayer*)context)->callback_wav();
}

void	SoundPlayer::callback_wav(void)
{
	if ( sound_loop == 1 ) {
		(*bqPlayerPlay)->SetPlayState(bqPlayerPlay, SL_PLAYSTATE_STOPPED);						// 停止
		return;
	}
	if ( sound_loop > 1 ) {
		sound_loop--;
	}
	(*bqPlayerBufferQueue)->Enqueue(bqPlayerBufferQueue, sound_data, (SLuint32)sound_size);		// 再生開始
}

再生コールバックです。
ループ回数が残っていれば、再びデータをキューに送ります。
無ければ SL_PLAYSTATE_STOPPEDで停止状態にします。

/**********
    停止
 **********/
void	SoundPlayer::stop(void)
{
	if ( bqPlayerObject ) {					// 再生中
		(*bqPlayerPlay)->SetPlayState(bqPlayerPlay, SL_PLAYSTATE_STOPPED);			// 停止状態
		(*bqPlayerObject)->Destroy(bqPlayerObject);
		bqPlayerObject = NULL;
	}
}

停止処理です。
停止状態にしたうえで、プレイヤーオブジェクトを削除しておきます。

※再生コールバックの停止でも削除を試してみましたが、その後の処理が行われませんでした。Destroyした時点で、コールバック処理自体が切られてまうようです。


最後にサンプルプログラムです。
AppMain.cpp

#include "common.h"
#include "Sprite.h"
#include "TouchPanel.h"
#include "Sound.h"


// スプライト
enum
{
	SPR_PHOTO = 0,			// 背景
	SPR_MAX,
};

static sys::Sprite*	sprite;		// スプライト


// サウンドデータ
enum
{
	SOUND_BGM = 0,			// BGM
	SOUND_SE,				// SE
	SOUND_MAX,
};

static sys::SoundPlayer*	sound_player;				// サウンドプレイヤー
static char*				sound_data[SOUND_MAX];		// サウンドデータ
static u32					sound_size[SOUND_MAX];		// サウンドデータサイズ
static int					se_track;					// SEトラック番号


/************
    初期化
 ************/
void	init_app(void)
{
	struct SprDef
	{
		const char*		tex_name;		// テクスチャファイル名
		SRect			coord;			// UV座標
	};

	static const
	SprDef	spr_def[SPR_MAX] =
			{
				{"photo.pkm",		{  0,   0, 640, 960}},		// 背景
			};

	sprite = new sys::Sprite[SPR_MAX];				// スプライト初期化
	for (int i = 0; i < SPR_MAX; i++) {
		sprite[i].set(sys::TexCache::RES_ASSET, spr_def[i].tex_name, &spr_def[i].coord);
	}


	sound_player = new sys::SoundPlayer[5];													// サウンドプレイヤー
	sound_data[SOUND_BGM] = (char*)sys::load_asset("bgm.wav", &sound_size[SOUND_BGM]);		// サウンドデータ読み込み
	sound_data[SOUND_SE] = (char*)sys::load_asset("se.wav", &sound_size[SOUND_SE]);
	se_track = 0;

	sound_player[4].play(sound_data[SOUND_BGM], sound_size[SOUND_BGM], 0);					// BGM再生開始
}

/**********
    終了
 **********/
void	quit_app(void)
{
	delete[]	sound_player;
	for (int i = 0; i < SOUND_MAX; i++) {
		free(sound_data[i]);
	}

	delete[]	sprite;
}

/******************************
    稼働
		戻り値	アプリ続行か
 ******************************/
Bool	update_app(void)
{
	sprite[SPR_PHOTO].draw(0.0f, 0.0f);			// 背景

	if ( sys::TouchPanel[0].flag & sys::TouchManager::TRIGGER ) {
		sound_player[se_track].play(sound_data[SOUND_SE], sound_size[SOUND_SE]);			// SE再生
		se_track = ++se_track % 4;
	}

	return	TRUE;
}

/**************
    一時停止
 **************/
void	pause_app(void)
{
	for (int i = 0; i < 5; i++) {
		sound_player[i].stop();
	}
}

/**********
    再開
 **********/
void	resume_app(void)
{
}

/**************** End of File *************************************************/

サウンドプレイヤー:sound_player
4:BGM用
0~3:SE用
BGMは初期化時、SEは画面タッチしたときに再生します。


プロジェクト一式は、こちらから。

Android NDK サウンド再生指針2013/11/18

さて、Android NDKプログラミングもそろそろサウンドの再生に取り掛かりたいと思います。

サウンドデータの管理ですが、
・音を鳴らす度にファイルから読み込むのでは重い
・PCMデータをメモリに置いておくのは容量を食う
ということで、
「メモリ上の圧縮フォーマットデータをデコードしながら再生」
ができるようにしたいです(アプリの規模や状態で常にそれがベストとは限りませんが、少なくとも対応しておきたい)。

しかし、Androidのサウンドは
・SDKの SoundPoolは圧縮フォーマットをリソースにできるけど、デコードしてメモリに常駐
・SDKの MediaPlayerはファイル指定のみ
・NDKの OpenSLは、圧縮ファイルの再生はファイル指定のみ
と、なかなか一筋縄ではいかないようです。

仕方が無いので、圧縮データ(問題なさそうなOgg Vorbisフォーマット)をデコードしつつ、OpenSLのPCM再生を使用するという方法で何とか進めてみたいと思います。
ああ、面倒くさい。

タッチパネル入力 その22013/11/17

前回に引き続きタッチパネルからの入力、今回は native側の処理を行います。

タッチパネル入力管理クラス sys::TouchManagerです。

TouchPanel.h
/********************************

		タッチパネル

 ********************************/

#ifndef	___TOUCH_PANEL_H___
#define	___TOUCH_PANEL_H___

#include "common.h"


namespace sys
{

/**********************
    タッチパネル管理
 **********************/
class TouchManager
{
public :

	static void		init_manager(void);					// 管理システム初期化
	static void		quit_manager(void);					// 管理システム終了
	static void		update_manager(short const*);		// 管理システム稼働

private :

	int		repeat_cnt;			// リピート用カウンタ
	int		prev_x, prev_y;		// 前回のタッチ位置

public :

static const int	TOUCH_MAX = 5;						// マルチタッチ数

enum
{
	TOUCH	= (1 << 0),			// タッチ
	TRIG	= (1 << 1),			// 初回タッチ
	REPEAT	= (1 << 2),			// リピート
	RELEASE	= (1 << 3),			// リリース
};

	u32		flag;				// タッチ状態フラグ
	int		x, y;				// タッチ位置
	int		move_x, move_y;		// 移動距離

		TouchManager(void)				// コンストラクタ
		{
			flag = 0x00;
		}
	void	update(short const*);		// 稼働
};

extern TouchManager*	TouchPanel;

}

#endif
/*************** End of File ****************************************/

TouchPanel.cpp
/***********************************

		タッチパネル管理

 ***********************************/

#include	"TouchPanel.h"
#include	"Renderer.h"


namespace sys
{

TouchManager*	TouchPanel;

/************
    初期化
 ************/
void	TouchManager::init_manager(void)
{
	TouchPanel = new TouchManager[TOUCH_MAX];
}

/**********
    終了
 **********/
void	TouchManager::quit_manager(void)
{
	delete[]	TouchPanel;
}

/***********************************************
    稼働
		引数	_status[0] = タッチしているか
				_status[1] = X座標
				_status[2] = Y座標
 ***********************************************/
void	TouchManager::update_manager(short const* _status)
{
	for (int i = 0; i < TOUCH_MAX; i++) {
		TouchPanel[i].update(_status);
		_status += 3;
	}
}

void	TouchManager::update(short const* _status)
{
	if ( _status[0] ) {						// タッチ中
		// タッチ位置補正
		x = (_status[1] - sys::Renderer::screen_rect.x)*SCREEN_WIDTH/sys::Renderer::screen_rect.w - SCREEN_WIDTH/2;
		y = (_status[2] - sys::Renderer::screen_rect.y)*SCREEN_HEIGHT/sys::Renderer::screen_rect.h - SCREEN_HEIGHT/2;
		if ( !(flag & TOUCH) ) {					// 初回
			flag = TOUCH | TRIG | REPEAT;
			repeat_cnt = TOUCH_REPEAT1;
			move_x = 0;
			move_y = 0;
			prev_x = x;
			prev_y = y;
		}
		else {										// 継続
			flag = TOUCH;
			if ( --repeat_cnt == 0 ) {				// リピート
				flag |= REPEAT;
				repeat_cnt = TOUCH_REPEAT2;
			}
			move_x	= x - prev_x;
			move_y	= y - prev_y;
			prev_x	= x;
			prev_y	= y;
		}
	}
	else {									// 非タッチ
		flag = (flag & TOUCH) ? RELEASE : 0;
	}
}

}

/*************** End of File *****************************************/

マルチタッチ対応のため、配列 TouchPanelで複数のインスタンスを処理します。

フレーム毎の処理 updateでは javaで取得した状態を受け取り、各メンバに格納します。
タッチしているときは、flagのビットTOUCHを立てます。
タッチ位置座標は画面描画と同じ、(-SCREEN_WIDTH/2, -SCREEN_HEIGHT/2) - (SCREEN_WIDTH/2, SCREEN_HEIGHT/2)になるように変換します。

基本的にはこれだけですが、他によく使う状態を計算しておきます。

flagのビット
・TRIG         離した状態からタッチしたとき
・RELEASE   タッチした状態から離したとき
・REPEAT    TRIGと同じですが、押しっぱなしのとき一定間隔でビットが立ちます

move_x, move_y
タッチしているときの前フレームからの移動距離


次に、TouchManagerを呼び出すシステム部分です。

SysMain.cpp
/**********
    稼働
 **********/
JNIEXPORT jboolean JNICALL	Java_sys_BaseActivity_updateNative(JNIEnv* env, jobject, jbyteArray touch)
{
	Renderer::update();							// 描画管理稼働

	jbyte*	dst = (jbyte*)env->GetPrimitiveArrayCritical(touch, NULL);

	TouchManager::update_manager((short*)dst);	// タッチパネル管理稼働
	env->ReleasePrimitiveArrayCritical(touch, dst, 0);

	if ( update_app() ) {						// アプリメイン稼働
		Renderer::draw();						// 描画後処理
		return	JNI_TRUE;
	}
	init_flag = FALSE;
	return	JNI_FALSE;							// アプリ終了
}

初期化・終了部分は、それぞれ init_managerと quit_managerを呼んでいるだけなので省略しています。
稼働部分では javaから受け取った jbyteArrayから GetPrimitiveArrayCriticalで配列のポインタを取得して TouchManagerに渡しています。

※javaから配列を渡すときはいくつか方法があり、GetPrimitiveArrayCriticalはそのままポインタを渡す(らしい)ので高速なのですが、その分いろいろと制限がありますので使うときは気をつけてください。


サンプルのタッチパネルテスト部分です。
タッチしたところにビー玉の絵が表示されます。

AppMain.cpp
	for (int i = 0; i < sys::TouchManager::TOUCH_MAX; i++) {			// タッチパネルテスト
		if ( sys::TouchPanel[i].flag & sys::TouchManager::TOUCH ) {
			sprite[SPR_BALL_BLUE + (i % 4)].draw(sys::TouchPanel[i].x, sys::TouchPanel[i].y, 3.0f + sinf((cnt % 60)*M_PI*2/60)*2.0f);
		}
	}
	cnt++;


タッチパネルテスト


※ビー玉の画像は「かわいいフリー素材集 いらすとや」から使わせていただきました。どうもありがとうございます。

プロジェクト一式は、こちらから。