* Sat Mar 26 19:35:00 JST 2005 Naoyuki Sawa
- ストリーム再生のまとめ#1


 P/ECEのサウンドAPIは、低水準すぎず多機能すぎず、適度なバランスが取れていて、ゲーム効果音用のサウンドAPIとして、とても使い易い仕様だと思います。が、ストリーム再生に使おうとすると、ちょっと難しい部分があります。原因は二つあって、ひとつはカーネルのサウンドルーチンの単純なバグ、もうひとつはサウンド処理の設計仕様に起因する少し根の深い問題です。そこで、今回より何回かに分けて、P/ECEのサウンドAPIを使ってストリーム再生を行う場合の注意点について、記録しておこうと思います。


 まずは基礎編です。P/ECEでのストリーム再生プログラムについて、既にご理解なさっている方は、読み飛ばしてください。

 PCMデータ再生のためのサウンドAPIを使って、音楽など長時間の音声データを再生する場合には、一般に「ストリーム再生」と呼ばれる方法が採られます。あらかじめ全てのPCMデータをメモリ上に用意してから再生を開始するのではなく、まず少しのPCMデータを用意してそれを再生開始し、再生が完了するまでの時間を使って続きのPCMデータを用意します。この手順を繰り返すことによって、長時間の音声データを再生し続けることを、ストリーム再生と呼びます。ハードディスク上の大きな音楽ファイルを再生したり、インターネット放送をダウンロードしながら再生するのも、ストリーム再生の一種です。

 それではさっそく、P/ECEのサウンドAPIを使って、ストリーム再生を行ってみましょう。再生する音声データは音楽ではなく、単純な440Hzの矩形波データを生成しながら再生することにします。この程度ならば、あえてストリーム再生を行う必要はなく、あらかじめ短い矩形波データを作成しておいて、それをループ再生するだけでも事足りるので、あまり実践的な例ではないのですけれど、サンプルプログラムを簡単にするため、ということでご理解ください。


	/////////////////////////////////////////////////////////////////////////////

	#include < piece.h >

	/****************************************************************************
	 *	グローバル変数、関数宣言など
	 ****************************************************************************/

	unsigned char vbuff[DISP_X * DISP_Y]; /* 仮想画面バッファ */

	/* サウンド関数の宣言 */
	void sndplay();
	void sndstop();
	void fillbuf(short wbuff[/*BUFLEN*/]);
	void endproc(PCEWAVEINFO* pwi);

	/****************************************************************************
	 *	基本処理
	 ****************************************************************************/

	void
	pceAppInit()
	{
		/* 一般的な初期化。 */
		pceAppSetProcPeriod(100);
		pceLCDSetBuffer(vbuff);
		pceLCDDispStart();

		/* サウンド初期化。 */
		sndplay();
	}

	void
	pceAppProc(int count)
	{
		/* なにかボタンが押されたら終了します。 */
		if(pcePadGet() & (PAD_A | PAD_B | PAD_SELECT)) {
			pceAppReqExit(0);
		}

		/* メッセージ表示。 */
		memset(vbuff, 0, sizeof vbuff);
		pceFontSetPos(0, 0);
		pceFontPrintf("なにかボタンを押すと\n終了します。");
		pceLCDTrans();
	}

	void
	pceAppExit()
	{
		/* サウンド停止。 */
		sndstop();
	}

	/****************************************************************************
	 *	サウンド処理
	 ****************************************************************************/

	#define CH 0 /* ストリーム再生用チャネル番号(変更可) */

	#define BUFLEN 64 /* PCEWAVEINFO当りのPCMデータサンプル数 */

	PCEWAVEINFO wi[2]; /* PCMWAVEINFO。二つを交互に使います。 */

	/* ストリーム再生を開始します。 */
	void
	sndplay()
	{
		int i;

		/* ストリーム再生用チャネルの音量を最大にします。 */
		pceWaveSetChAtt(CH, 0);

		/* 二つのPCEWAVEINFOを初期化します。 */
		memset(wi, 0, sizeof wi);
		for(i = 0; i < 2; i++) {
			/* 16bit PCM形式、連続出力。 */
			wi[i].type = PW_TYPE_16BITPCM | PW_TYPE_CONT;
			/* PCMデータ領域を確保します。 */
			wi[i].pData = pceHeapAlloc(sizeof(short) * BUFLEN);
			/* PCMデータのサンプル数を格納します。 */
			wi[i].len = BUFLEN;
			/* 再生完了時に呼び出される関数を登録します。 */
			wi[i].pfEndProc = endproc;
			/* 最初(i=0)とその次(i=1)のPCMデータを作成します。 */
			fillbuf((short*)wi[i].pData);
		}

		/* 一つめのPCEWAVEINFOの再生を開始します。 */
		pceWaveDataOut(CH, &wi[0]);

		/* 二つめのPCEWAVEINFOの再生を予約します。 */
		pceWaveDataOut(CH, &wi[1]);
	}

	/* PCMWAVEINFO一つ分のPCMデータを生成します。 */
	void
	fillbuf(short wbuff[/*BUFLEN*/])
	{
		static unsigned f = 0;
		int i;

		/* 440Hzの矩形波データを作成します。 */
		for(i = 0; i < BUFLEN; i++) {
			f = (f + 1) % (16000/440);
			wbuff[i] = (f < (16000/440/2)) ? -32768 : 32767;
		}
	}

	/* 一つのPCMWAVEINFOが再生完了したときに呼び出される関数です。 */
	void
	endproc(PCEWAVEINFO* pwi)
	{
		/* 次のPCMデータを作成します。 */
		fillbuf((short*)pwi->pData);

		/* このPCMWAVEINFOの再生を予約します。 */
		pceWaveDataOut(CH, pwi);
	}

	/* ストリーム再生を停止します。 */
	void
	sndstop()
	{
		/* サウンドを停止します。 */
		pceWaveAbort(CH);
	}

	/////////////////////////////////////////////////////////////////////////////

 前半部分はP/ECEプログラムの定石どおりですので、後半の“サウンド処理”以降だけを説明することにします。  sndplay()は、ストリーム再生を開始するための関数です。まず、再生用チャネルの音量を最大にします。ストリーム再生とは直接関係ありませんが、前のアプリケーションのチャネル音量設定がそのまま残っているので、定石としてやっておいた方がいいです。


	pceWaveSetChAtt(CH, 0);
 次に、ストリーム再生用に二つのPCEWAVEINFOを初期化し、最初のPCMデータを生成します。複数のPCEWAVEINFOの連続再生を予約するために、typeフィールドにPW_TYPE_CONTフラグを指定していることに注目してください。

	memset(wi, 0, sizeof wi);
	for(i = 0; i < 2; i++) {
		wi[i].type = PW_TYPE_16BITPCM | PW_TYPE_CONT;
		wi[i].pData = pceHeapAlloc(sizeof(short) * BUFLEN);
		wi[i].len = BUFLEN;
		wi[i].pfEndProc = endproc;
		fillbuf((short*)wi[i].pData);
	}
 二つのPCEWAVEINFOが用意できたら、まず一つめのPCEWAVEINFOの再生を開始します。続けて二つめのPCEWAVEINFOの再生を予約します。このように再生を予約しておくと、一つめのPCEWAVEINFOの再生が完了した後、自動的に二つめのPCEWAVEINFOの再生が開始されます。

	pceWaveDataOut(CH, &wi[0]);
	pceWaveDataOut(CH, &wi[1]);
 fillbuf()関数は、PCMWAVEINFO一つ分のPCMデータを生成するための関数です。上の方で、PCMWAVEINFO一つ当りのPCMデータのサンプル数を64サンプルと定義しています。

	#define BUFLEN 64
 従って、fillbuf()関数は、64サンプル分のPCMデータを生成します。前述のとおり、このサンプルプログラムでは、音声データとして、単純な440Hzの矩形波データを生成します。

	for(i = 0; i < BUFLEN; i++) {
		f = (f + 1) % (16000/440);
		wbuff[i] = (f < (16000/440/2)) ? -32768 : 32767;
	}
 endproc()関数は、PCEWAVEINFOの再生完了通知を受け取るための関数です。sndplay()関数のところで、PCEWAVEINFO.pfEndProcにこの関数のポインタを登録しました。P/ECEカーネルのサウンドルーチンは、一つのPCEWAVEINFOの再生が完了したときに、そのPCEWAVEINFOのpfEndProcフィールドを調べて、NULLでなければアプリケーション定義の通知関数へのポインタと見なして、通知関数をコールバックします。コールバック引数として、再生完了したPCEWAVEINFOへのポインタが渡されます。再生完了したPCEWAVEINFOは、もうカーネルのサウンドルーチンの管理を離れていますので、自由に再利用が可能です。そこでendproc()関数は、再生完了したPCEWAVEINFOのPCMデータ領域に、次のPCMデータを生成しています。

	fillbuf((short*)pwi->pData);
 PCMデータを生成したら、再び、このPCEWAVEINFOの再生を予約します。

	pceWaveDataOut(CH, pwi);
 最後に、アプリケーション終了時に呼ばれるsndstop()関数を説明します。ストリーム再生に限らず、音を鳴らしたままアプリケーションを終了すると、P/ECEがハングアップしたり異常動作する場合があります。音が鳴っている可能性のあるチャネルを、確実に停止しておくことが重要です。これも、P/ECEプログラムの定石です。

	pceWaveAbort(CH);
 今回は、指定したチャネルだけを停止するpceWaveAbort() APIを使いましたが、全てのチャネルをまとめて停止するpceWaveStop() APIを使っても良いです。一つのチャネルを停止するのがpceWaveAbort()で、全チャネルを停止するのがpceWaveStop()。ちょっと名前と機能の対応がまぎらわしいので、注意が必要です。

 以上、基礎編でした。これだけを見ると、P/ECEのサウンドAPIを使って、何の問題も無くストリーム再生ができていますが・・・平和なのはここまでです(^^; それでは、一つめの問題の再現へ移って行きたいと思います。


 先のサンプルプログラムでは、PCEWAVEINFO一つ当りのPCMサンプル数を64と定義していました。64という数に特に意味は無く、適当に決めた値です。P/ECEのスピーカー出力周波数は16000Hz(※注)なので、64サンプルは250分の1秒分に相当します。PCEWAVEINFO一つ分のPCMデータを250分の1秒で再生完了するわけですから、毎秒250回の頻度で、endproc()関数がコールバックされることになります。毎秒250回のコールバックというのは、ちょっと頻度が高すぎな気がしますね。コールバックの頻度が高いほど、CPUパワーを多く消費し、アプリケーションプログラムの動作速度が遅くなってしまいます。
(※注1: 厳密には、16000Hzのデータをカーネル内部で補間して、32000Hzで出力しているのですけれど、実質16000Hzと考えて問題ありません。)

 コールバックの頻度を下げるには、PCEWAVEINFO一つ当りのPCMサンプル数を多くすることです。あまり多くしすぎると、その分メモリ消費量も増えてしまうので、加減が難しいところですが、とりあえず64→100ぐらいに増やしてみましょう。


	#define BUFLEN 64
	 ↓変更
	#define BUFLEN 100
 プログラムを変更後、コンパイル、実行してみると・・・ノイズっぽい音が鳴った後、すぐに発声が停止してしまいました。PCEWAVEINFO一つ当りのPCMサンプル数を変えただけなのに!これが、一つめの問題点です。

 PCEWAVEINFO一つ当りのPCMサンプル数を100にすると正しく動作しなくなったので、もう少し別の値で試してみましょう。64ならば正しく動作していたのですから、その倍の128ではどうでしょうか?


	#define BUFLEN 100
	 ↓変更
	#define BUFLEN 128
 今度は、正しく動作しました。実は、P/ECEのサウンドAPIを使ってストリーム再生を行う場合には、次のような制限があります。
  PCEWAVEINFO一つ当りのPCMサンプル数は、64の倍数でなければならない。
 64の倍数でないPCMサンプル数でストリーム再生を行うと、さきほどの例のようにすぐに発声が停止してしまったり、運の悪いときにはハングアップしてしまったりと、予測のできない結果になります。

 この制限はあくまで“ストリーム再生を行う場合”のみですから、単発の効果音を鳴らす場合には、効果音のPCMサンプル数が64の倍数である必要はありません。多くのP/ECEアプリケーションは、効果音にのみサウンドAPIを用い、BGM再生には音楽ライブラリを利用しているので、直接サウンドAPIを使ってストリーム再生を行うことは稀です。だから、あまり問題になっていないのだと思います。ちなみに音楽ライブラリも、その内部ではサウンドAPIを使ってストリーム再生を行っていて、PCMサンプル数を128と定義しています。偶然にも64の倍数だったために、音楽ライブラリでは問題が発現せず、P/ECE開発時にサウンドAPIの問題が発覚しなかったのだと思います。

 この制限は、P/ECEカーネルの仕様ではなく、単純なバグによるものです。問題の場所は、次のとおり:


	\usr\PIECE\sysdev\pcekn\snd.c 352行目 (PIECE KERNEL : Ver 1.20 の場合)

	setpwi( wp, wp->pwi, pwi = pwi->next ); ・・・誤り (現状)
	 ↓
	setpwi( wp, pwi = wp->pwi, pwi->next ); ・・・正しくは、こうです
 誤りの内容を簡単に説明すると、本当はカーネル内部のワークエリアを指さなければいけないポインタ(pwi)が、アプリケーションのワークエリアを指してしまっています。PCMデータのサンプル数が64の倍数の場合に限り、偶然にもこのポインタ(pwi)が利用されないため、問題が発現しないだけなのです。


 今回は、P/ECEでのストリーム再生の基礎と、一つめの問題点の再現方法までを説明しました。次回は、カーネルのサウンドルーチンの動作を追いかけながら、この問題点の内容をもう少し詳しく見ていこうと思います。

(続きます...)


nsawa@piece-me.org