Carbon の技法: asynchronous sound 再生

はじめに

'snd 'リソースの sound を SndPlay() 関数で再生する方法を論じます。 asynchronous 再生というのは、アニメーションと同時に音を鳴らせるやりかたで、 ゲームなどには欠かせない手法です。

SndPlay() 関数についての Apple の Document (文献1)を見るたびにいつも困惑させられます。
カバーしている範囲があまりにも広いため、とかく不必要な部分までコーディングしてしまいがちです。
また、Sound Manager の章が執筆された時期が古く(68K CPU向け、Pascal ソース)、それ以来根本的な改訂がされていないため、 なにがsound 再生に本質的なのかが非常にわかり難くなっています
そこで本文では、目的とする部分だけに絞り込み、 自分のためのメモのつもりで執筆しました。参考になる方がおられましたら幸いです。

前提条件

'snd 'リソースが次の2条件を同時に満足していること。
(1)形式1(format 1) のサウンドリソースであること。
(2)リソースに含まれるコマンドの数が128以下であること。

これは ResEdit や Resorcerer を使って簡単に調べることができます。
(1)最初の2バイトが0x0001であればよい。
(2)11バイト12バイト目の値がコマンドの数を表す。たいていは0x0001である。とにかくこの値が 0x0080以下であればよい。

サブルーチン群

ソースコードを別のファイルに用意したので、以下の説明を対照させながらお読みください。
関数の名前は Sound Manager の解説にある Pascal code のそれと一致させた。したがって関数が果たす役割もそれと同じです。

SndChannelPtr MyCreateSndChannel( SndCallBackUPP userRoutine )
SndPlay() 関数を実行する前には必ず sound channel を用意しなければならない。これを作成するのがこの関数の目的です。
mySndChan = nil にしてから SndNewChannel() を呼んでいるので、
sound channel で使用する長さ128のキューを含むメモリがシステムヒープの中に自動的に用意されます。
前提条件(2)により、これで十分ということです。
なお Pascal code では自前でsound channel のメモリを割り付けていますが、この方法にはバグがあると文献2には書かれています。

OSErr MyInstallCallback( SndChannelPtr mySndChan )
キューの最後尾に callBackCmd を挿入するのがこの関数の目的です。
callBackCmd と kSoundComplete の役割は Pascal code と同じですが、param2 の値が異なっています。
68K CPU 以外には SetCurrentA5 は意味がありません。かわりに param2 にグローバル変数のアドレスを直接埋め込み、 sound channel が不用になったことを示すフラグを取り出せるようにしました。(逆にこの方法は68K CPU でも使えます)
kWait = false にした理由は、文献2による。

pascal void MyCallback( SndChannelPtr channel, SndCommand * theCmd )
この関数は音が鳴っている途中でにシステムから繰り返し呼ばれ、キューから取り出したコマンドを調べます。
もしこれが、自分でインストールしておいたcallBackCmd であることが param1 により確認できれば、
グローバルフラグ gCallbackPerformed をセットします。
これは sound cannnel は不用になったという合図です。

void MyCheckSndChan( void )
sound channel が不用になったという合図を受けて、それを廃棄するための関数です。
メモリリークをおこさないようにするため、Handle で保持されている sound もここで廃棄します。
廃棄する順番が大事で、まず sound channel を廃棄し、次に soud の Handle を廃棄します。
Pascal code では順序が逆になっていますが、これはバグを誘発する恐れがあります。
詳しくはここを見てください。

void MyStopPlaying( void )
鳴っている音を強制的に停止させるための関数です。
通常は'snd 'リソースの音が最後まで鳴らされると、MyCallback() でこれを検出して後始末(MyCheckSndChan)をします。
これに対して、なんらかの理由で現在鳴っている音を中断するときには外部からこの関数を呼びます。

void MyStartPlaying( short mySndID )
発音('snd 'リソースの音を鳴らす)を開始するための関数です。
音を鳴らすために外部から呼ばれるのは、もっぱらこの関数です。

構成はPascal code とほぼ同じ形になりました。
(1) sound channel を作成します。
(2) 'snd 'リソースを取り出して Handle に取り付け、そのアドレスをuseInfoフィールドに入れます。
(3) SndPlay() 関数を呼んで発音を開始します。
(4) MyInstallCallback() を呼びます。
しかしその中身はこれまで述べてきたように微妙に変わっています。

使い方

準備として、次の関数のprototype宣言をしておいてください。
void MyCheckSndChan( void ); void MyStopPlaying( void ); void MyStartPlaying( short mySndID ); 音が必要になった箇所で MyStartPlaying( kSndID ) を呼んでください。ただし kSndID は'snd 'リソースのID 番号です。
(この関数は一度に一つのリソースしか再生できません。連続して MyStartPlaying() を呼ぶと、最初の音が一瞬鳴った後、すぐに2番目の音が鳴り始めます。)

また、MyStopPlaying() を呼ぶことで、鳴っている音を途中で止めることもできます。

自然に鳴り終った音の後始末をするため、MyCheckSndChan() を定期的に呼び出す必要があります。
(1)Carbon Event Manager を使用している場合
RunApplicationEventLoop() を呼ぶ前に timer event をインストールしてDoIdle()関数を定期的に呼び出します。
(一例)0.5秒(30 ticks)に一回 timer 割込みをする

InstallEventLoopTimer(GetCurrentEventLoop(),0,TicksToEventTime(30), NewEventLoopTimerUPP( (EventLoopTimerProcPtr)DoIdle ), NULL,NULL); 次にその DoIdle() の中で MyCheckSndChan() を呼びます。
(一例)
void DoIdle( void ) {   MyCheckSndChan(); // terminate sound channel }

(2)旧式の Event Loop を使用している場合。
main event loop の中にDoIdle()をインストールしておきます。
(一例)

if ( WaitNextEvent( everyEvent, &event, kSleep, nil ) ) DoEvent( &event ); // normal event processing else DoIdle( &event ); DoIdle() の中で MyCheckSndChan() を呼ぶのは(1)と同様。

注意

Mac OS 9 で CarbonLib をインストールして Carbon Application を make , run する場合、CarbonLib のバグのため MyCallback() が正しく動作しません(CarbonLib J1-1.6 で確認しました)。
回避方法はこちらをみてください。

参考文献

文献1: QuickTime Audio : Sound manager の章
文献2:「Macintoshプロフェッショナルプログラミング」デーブ・マーク編、トッパン、ISBN4-8101-8941-4(1995)の第4章「サウンドを処理する」written by Jim Reekes


ホームページへ戻る