プログラム講座 上級編1
- AIFFサウンドのループ演奏 -
 上級編1です。今回は指定したAIFFサウンドをBGMとして演奏させるプログラムを作成してみます。AIFFサウンドの同期演奏については初級編7で解説してありますので、先にそちらを一読した方がよいでしょう。
 今回は「コールバック」について解説します。
- 注意:
- 今回のプログラムは、必ずFuture BASIC II英語版か日本語版でコンパイルして実行してください。
◆コールバックルーチンとは?
 今回のプログラムには今までにないものが出てきます。それは「コールバックルーチン」(コールバック)と呼ばれるものです。通常ツールボックスを呼び出すと、指定した引数(値など)によって処理されて、呼び出したプログラムに戻ってきます。
 ところがコールバックルーチンの場合、次のような感じで処理が行われます。
- プログラムからToolBoxを呼び出す
- ある条件の場合、ユーザーに問い合わせる処理があればユーザーのプログラムを呼び出す
- 渡された引数(値)を元に処理を行いToolBoxに戻す
といった流れを繰り返します。今回のサウンドの場合、演奏が終了した場合にユーザーのプログラムを呼び出すという「コールバックルーチン」を定義する事ができます。ループ演奏する場合は、このコールバックルーチンに自分のプログラムを呼び出すようにしておけばいいわけです。
 Macのアプリケーションでは、結構多くコールバックルーチンが使用されています。ファイルをオープンする時に一度選択したファイルは表示されないといった類のものです。当然コールバックルーチンが使用できるかどうかは、呼び出すツールボックス命令によって異なります。コールバックルーチンが使用できるものは、あまり多くありません。
◆コールバックルーチンの制限
 便利そうなコールバックルーチンですが、かなり制限があります。PRINT文などは使用できませんし、その他Future BASICのほとんどの命令は使用できません。直接ツールボックスを呼び出すか、グローバル変数の値を設定するか、与えられた引数によって、返す値を設定するかどうかといった程度です。また、Future BASICの場合、先頭にコールバックルーチン(最初のセグメント)に記述しておく必要があります。
 今回は比較的楽なサウンドループのチェックだけです。とは言うものの上記のような制限があるため、演奏終了したから、再度演奏するプログラムを呼び出そう、という事はできません。このような事を行うと暴走するか、バスエラー等で停止してしまいます。
 結局の所演奏完了フラグを用意しておき、演奏が終了したらフラグをセット、それをメインルーチンで毎時チェックしていてフラグが設定されていたら、演奏させるプログラムを呼び出す、といった具合になります。
◆サウンドチャンネル
 サウンドチャネルという呼び方をする人もいるようですが、ここではサウンドチャンネルにしておきます。
サウンドチャンネルはサウンドを演奏するチャンネルの事です、って名前そのままの説明ですみません。サウンドを演奏するには、このサウンドチャンネルにモノラル出力、ステレオ出力、コールバックルーチンなどを設定します。この設定を省略した場合は、初級編7のサンプルのようになります。
 サウンドチャンネルの設定は以下のようになっています。
osErr% = FN SNDNEWCHANNEL(設定するチャンネル,サウンド形式,出力方法,コールバックルーチンのアドレス)
 設定するチャンネルは
DIM sndChannnel&
のようにしておきFN SNDNEWCHANNEL(sndChannnel&・・・というように指定します。sndChannel&にはサウンドを演奏するために必要な情報がレコード形式(C言語では構造体)で格納されます。
 サウンド形式ですが、これは以下のようになっています。
| _squareWaveSynth | 矩形波サウンド | 
| _waveTableSynth | FMサウンド | 
| _sampledSynth | サンプリングサウンド | 
 AIFFはサンプリングされたものなので_sampledSynthになります。
 次に出力方法ですが、これは以下のようになっています。
| _initChanLeft | 左スピーカーから出力 | 
| _initChanRight | 右スピーカーから出力 | 
| _initMono | モノラル出力 | 
| _initStereo | ステレオ出力 | 
| _initMACE3 | 1:3 MACE 圧縮形式 | 
| _initMACE6 | 1:6 MACE 圧縮形式 | 
 他にもいくつもありますが、通常はステレオ、モノラルで十分でしょう。
最後のコールバックルーチンについては、次の項で説明します。
◆コールバックルーチンの指定方法
 通常Future BASICで関数のアドレスを指定する場合、以下のようにします。
LOCAL FN myCallBack
  LoopFlag% = _zTrue
END FN
Adrs& = @FN myCallBack
 これを
osErr% = FN SNDNEWCHANNEL(sndChanPtr&,_sampledSynth,_initMono,@FN myCallBack)
 と指定するとシステムエラーで停止するか、うまくすれば何も起こりません。通常の場合、このような@FNの指定でよいのですが、コールバックルーチンでは、この方法は使用する事ができません。コールバックルーチンに関しては専用の命令を使わなければなりません。その命令は
ENTERPROC
EXITPROC
 です。他に同様のものとしてENTERPROC%〜EXITPROC%がありますが、こちらは今回は使用しません。
 専用の命令はわかりましたが、今度はこの関数の「アドレス」(マシン語に変換された時の場所)を求めなければなりません。というのもコールバックルーチンは関数の場所を指定する必要があるからです。Future BASICには、指定場所のアドレスを求める命令が用意されています。指定位置のアドレスを求めるには次のようにします。
"MyAddress"
ENTERPROC
EXITPROC
Adrs& = LINE "MyAdrress"
 こうする事でEXTERPROCの先頭アドレスがAdrs&に入ります。後はこのアドレスをコールバックルーチンの飛び先として指定すればできあがりです。つまり、以下のようになります。
osErr% = FN SNDNEWCHANNEL(sndChanPtr&,_sampledSynth,_initMono,Adrs&)
 今回は2つコールバックルーチンがありますが、片方は使用していません。どちらも指定の仕方は同じですのでプログラムを見ながら研究してみてください。
◆コールバックルーチンとFuture BASIC
 これで終われば良いのですが、Future BASICの都合上ENTERPROC命令が見つかると、いきなり実行してしまいます。通常、コールバックルーチンは一番先頭に記述してあるので、コンパイルして実行するといきなりコールバックルーチンだけが実行され終わってしまいます。
 結局の所、これを防ぐためにGOTO "Main"のように本当の実行先に処理を移してやる必要があります。基本的に以下のような構成になっていないと駄目だという事です。
DIM 変数定義
GOTO "Main"
"CallBackLabel"
ENTERPROC
  処理
EXITPROC
"Main"
LoopFlag% = _false
END GLOBALS
DO
  HANDLEEVENTS
UNTIL _quit_flag
 このため、変数はグロバール変数になってしまうので、一番最初に定義しておかないと、コンパイルエラーもしくは正常に実行されなくなります。
 さらに注意事項として、コールバックルーチンに渡される引数があります。この引数は
EXTERPROC(引数1, 引数2,...)
 のように指定します。この引数の数を間違えるとシステムエラーが発生してしまいます。今回のサウンド演奏のコールバックルーチンに戻り値はありませんが、戻り値が必要なものはEXITPROCの所に、きちんと戻り値を書かないとこれまたシステムエラーの原因になります。
◆エラーについて
 Future BASICに限らず多くの場合、エラーチェックは厳しくしておいた方が最終的にはエラー発生率が低くなり開発時間も短縮することができます。今回のプログラムでは変数の初期化が行われていないためエラーで動かなかったというのがありました。通常はLOCAL FN〜と書いていましたが、今回はCLEAR LOCAL〜といった具合になっています。
 普通のアプリケーションの場合は、あまり関係ないのかもしれませんが、プラグインやコールバックルーチンなどを作成する場合は、エラーチェックを厳しくしておいた方がよいでしょう。特にこの講座では、なるべくプログラムの見通しをよくして、わかりやすく解説しようとしているため、エラー処理等を省略してある場合があります。
◆終わりに
 とにかくプログラムは一応動けば後はなんとかなるものです。ところが最初のステップが結構厳しいと、なかなか進めなくなってしまい引き返してしまう事もあります。特にMac関係の書籍などは、わかりやすい解説書が少ないので、なおさらです。ホームページでMac関係のプログラム関係を探しても、意外と少なく英語だったり難易度が高くて???だったり様々です。
 腕のいいプログラマが必ずしも、わかりやすい講座を書けるとは限りませんが、もう少しわかりやすい書籍やページがあってもいいように思います。
 今回のプログラムは、何かキーを押すと終了するようになっています。powerMacでは相性が悪いソフトがあるかもしれません。一応動作チェックはしましたが・・・。また、演奏するファイルはオープンしたままにしておかないと、指定されたバッファに読み込んだ分しか演奏しませんので注意が必要です。
 あと、今回のプログラムはよくわからなかったのでNiftyServe - HomeParty上のFMACHELLのベンさんにいろいろ教えてもらいました。この場を借りて感謝いたします。
 ところで、次のネタは何がよろしいでしょうか?
◆今回のプログラムリスト
'
'       "AIFF サウンドファイルを読み込んで演奏する(非同期)"
'
COMPILE 0,_dimmedVarsOnly
DIM sndChanPtr&:                                  ' "サウンドチャンネルポインタ"
DIM soundRefNum%:                                 ' "サウンドファイルのファイル参照番号"
DIM loopFlag%:                                    ' "ループフラグ"
DIM sndPtr&:                                      ' "コールバックルーチンに渡されるサウンドポインタ"
DIM sndCmd&:                                      ' "サウンドコマンド"
DIM LoopAdrs&:                                    ' "コールバックルーチンのアドレス"
DIM CmdLoopAdrs&:                                 ' "コールバックルーチンのアドレス(サウンドコマンド)"
DIM osErr%:                                       ' "MacOSエラー"
END GLOBALS
GOTO "nextStep":                                  ' "メインループ先へジャンプ"
' "サウンドループチェック"
"LoopCheck"
ENTERPROC(sndPtr&)
  loopFlag% = _zTrue:                             ' "ループフラグをオンにする"
EXITPROC
' "サウンドループチェック(サウンドコマンド)"
' "今回はサウンドチャンネルへのコマンドは送信していない"
"LoopCmdCheck"
ENTERPROC(sndPtr&,sndCmd&)
  loopFlag% = _zTrue:                             ' "ループフラグをオンにする"
EXITPROC
' "ファイルを選択してサウンドを演奏する"
CLEAR LOCAL FN playAIFF
  DIM soundFilePBPtr&,vol%
  DIM 64 filename$
  filename$ = FILES$ (_fOpen, "AIFF", , vol%)
  LONG IF filename$ <>""
    OPEN "I", #1, filename$, , vol%
    soundFilePBPtr& = USR _fileAddr (1)
    LONG IF soundFilePBPtr& <> 0
      soundRefNum% = soundFilePBPtr&.ioRefNum%:   ' "ファイル参照番号(ボリューム番号ではありません)"
      osErr% = FN SNDSTARTFILEPLAY (sndChanPtr&, soundRefNum%, 0, 20*1024, 0, 0,LoopAdrs&, _zTrue)
      PRINT "play Err = ";osErr%
    END IF
  END IF
END FN
' "サウンドをループさせる"
CLEAR LOCAL FN soundLoop
  LONG IF loopFlag% = _zTrue
    osErr% = FN SNDSTARTFILEPLAY (sndChanPtr&, soundRefNum%, 0, 20*1024, 0, 0,LoopAdrs&, _zTrue)
    loopFlag% = _false:                           ' "ループフラグをオフにする"
    PRINT "SoundLoop!"
  END IF
END FN
' "ここからがメインルーチン"
"nextStep"
LoopAdrs& = LINE "LoopCheck":                     ' "コールバックルーチンのアドレスを求める"
CmdLoopAdrs& = LINE "LoopCmdCheck":               ' "コールバックルーチンのアドレスを求める"
soundRefNum% = 0:                                 ' "ファイル参照番号初期設定"
loopFlag% = _false:                               ' "ループフラグをオフ"
' "サウンドチャンネルを設定する"
' "モノラルではなくステレオにする場合は、_initMonoを_initStreoにする"
osErr% = FN SNDNEWCHANNEL(sndChanPtr&,_sampledSynth,_initMono,CmdLoopAdrs&)
PRINT "newChannel = ";osErr%
FN playAIFF:                                      ' "サウンドを演奏する"
DO
  HANDLEEVENTS
  FN soundLoop:                                   ' "ループチェック"
UNTIL INKEY$ <>""
CLOSE #1
osErr% = FN SNDDISPOSECHANNEL(sndChanPtr&,_zTRUE)