About WonderWitch 今回のお題:
『サウンド×リソース×x86』


●概要

いや、やっぱり音がないと寂しいかな〜と。

あと、データをリソースにして利用するようにすれば、ファイル転送が楽になったり、 ソースが MML で埋め尽くされて見づらくなることが無くなったり、色々幸せなので。

●内容

音の鳴らし方

簡単に音を鳴らすだけなら、マニュアル P96 (\doc\appendix\soundil.html) に載っているソースほぼそのままで OK です。

#include <sys/bios.h>
#include <il_sound.h>

SoundIL soundIL;
extern BYTE *_heap;     /* 実体はシステムが確保 */

void main()
{
    /* ドライバ (soundIL) が存在しない場合は抜ける */
    if( open_sound_il( &soundIL ) != E_FS_SUCCESS ) return;

    /* この2つでサウンドドライバを初期化 */
    sounddrv_init();
    sound_open();

    /* 渡された文字列 (MML) をリソースに変換し、_heap へ格納 */
    /* 第 3 引数が 0 の時は BGM として変換 */
    parse_mml( _heap, "CDEFG", 0 );

    /* 演奏開始 */
    /* 第 2 引数が PLAY_SINGLE で 1 回、PLAY_LOOP でループ再生 */
    bgm_play( _heap, PLAY_LOOP );
    key_wait();

    /* 演奏停止 */
    bgm_stop();

    /* この 2 つでサウンドドライバを停止・解放 */
    sound_close();
    sounddrv_release();
}

これで「CDEFG」の音階が鳴るわけですな。ドレミファソ。 なお、実行するときには sound.il を /rom0/ に転送しておいて下さい。(当然か)
parse_mml() とか bgm_play() とか、soundIL 内の関数の詳しい仕様はマニュアル (または \doc\appendix\il_sound.html) に載ってます。

_heap ってのはシステムの方でどこかに確保されている領域らしく、 extern 宣言さえしておけば、それ以外は特に意識しなくても大丈夫です。

リソースにして使う

で、上の「MML を実行時に変換して再生」という方法は、サウンドプレイヤーとか、 MML エディタみたいな物を作るときには都合が良いんですが、変換時に CPU パワーを食ったり、 ソースの中に MML が突然出現したりと、気になるところもあるわけです。

そこで、MML をリソースに変換して .fx ファイルにくっつけ、 MML 解析のオーバーヘッドを減らしたりなんだりしてみようと。

まず、sndcnv で MML をリソースに変換します。

> sndcnv <MMLファイル名>

拡張子 .fr のリソースファイルができあがるはずなので、 これをリソースとして .fx ファイルにくっつけます。
.cf ファイルに、

resource: <リソースファイル名>

と書いてやると、指定したファイルを、mkfent が、 .fx ファイルの生成時にリソースとして含めてくれます。
リソースファイルはスペースで区切って複数指定できます。

リソースにアクセスする

リソースにアクセスするコードですが、まず、resource.h を #include します。
で、これまたシステムで指定されているポインタ変数 _resource を extern 宣言します。

extern res_t far *_resource;

これもやっぱり実体はシステムの方で確保されているらしく、 extern 宣言さえすれば後は気にする必要はありません。
res_t 構造体は、resource.h で定義されているので参照して下さい。

この _resource の指す先に、変換済みの MML が格納されているわけですから、 parse_mml() とかは呼ばずに、いきなり

bgm_play( _resource, PLAY_LOOP );

としてやれば BGM が演奏されます。

2 つ以上のリソースを結合させた場合は、

という片方向リスト (のような形) になっているので、順次手繰って参照します。
「_resource->size」というのは、次のリソースまでの segment 数を表します。 ( マニュアル P97 に、「個々のリソースは、mkfent でアペンドする際に パラグラフ境界に先頭が位置するようパディングされますので……」とある )

ちなみに、segment と offset はともに WORD 値で、 segment << 4 + offset で一つの far アドレスが生成されます。
「far アドレス」というのは、20bit で表される 1 つのアドレスというわけです。 「int far *」というふうに宣言された far ポインタで指されます。
far ポインタは DWORD 値で、上位 WORD に segment 値、 下位 WORD に offset 値を格納しています。

また、同様に、far 宣言されていないポインタで指されるのが「near アドレス」です。 これは 16bit で表されるアドレス値です。 名前の通り、far アドレスに比べて狭い領域 (〜 64KB) までしか表せません。
多分、offset 値に相当するんだろうと思います。

ちなみにこの segment と offset という概念、intel の特許です。どうでも良いんですが。

リソースにアクセスするコードは以下の通り。

#include <resource.h>
extern res_t far    *_resource;     /* 実体はシステムが確保 */
static res_t far    *rsc = NULL;    /* リソース取得位置保存用 */

/* リソースの取得位置を初期化 */
void    init_resource( void )
{
    rsc = _resource;    /* _resource って const にすべきだと思う */
}

/* リソースを取得 */
/*
 * type: 取得すべきリソースの種類 (RESID_〜 マクロ (resource.h) )
 * RESID_IMAGE (0x4D42): 'BM': 画像
 * RESID_MUSIC (0x444D): 'MD': MML
 * RESID_SE    (0x4553): 'SE': SE
 * RESID_INST  (0x4953): 'SI': 音色
 * RESID_VOICE (0x4F56): 'VO': PCM
 */
res_t far   *get_resource( WORD type )
{
    res_t far *rslt;
    while ( rsc != NULL )
    {
        /* リソースでなかったら抜ける */
        /* RESID: リソースのマジックナンバー ('FR'): resource.h) */
        if ( rsc->magic != RESID )
            break;

        /* 現在のリソースを指している */
        rslt = rsc;
        /* 次のリソースを指させる */
        rsc = MK_FP( FP_SEG( rsc ) + rsc->size, FP_OFF( rsc ) );
        if ( rslt->type == type )   /* 指定したリソースのタイプと合致 */
        {
            /* segment 境界のバグ対策 */
            rslt = MK_FP( FP_SEG( rslt ) + ( FP_OFF( rslt ) >> 4 ),
                            FP_OFF( rslt ) & 0x00f );
            return rslt;
        }
    }
    return NULL;
}

……殆どすまうぐさんのページからのパクりです。 御免なさい(苦笑

最初に init_resource() を呼んでおいて、次に get_resource() を、 欲しいリソースの種類を指定して呼んでやると、 そのリソースを指している far ポインタが返ってきます。
結合したリソースの順番が分かっていれば、もう少しコードを省略できますが、 こっちの方が何となく安心なのでそのまま採用です。

セグメントとオフセットの話

で、「segment 境界のバグ対策」というのが気になるわけです。

まず、offset と segment で 1 つの far アドレスを指すことは上で説明しましたが、これで考えると、

i) segment = 1FFF, offset = 000F

  segment 1FFF
+) offset  000F
----------------
  address 1FFFF
ii) segment = 1000, offset = FFFF

  segment 1000
+) offset  FFFF
----------------
  address 1FFFF

と、segment 値と offset 値が異なっても、同じアドレスを指すことが出来ます。

さらに、offset 値は near ポインタの値として使われ ( 多分 ) ます。 で、offset 値が大きな値になっている状態でデータを読もうとすると、 値がオーバーフローを起こして上手く動作しなかったりします。
つまり、どこかにリソースを near ポインタで参照するコードがあったときに、 誤動作する可能性が出てくるわけですな。

これを回避するため、offset 値の上位 12bit を segment 値の方に移してしまい、 offset 値 ( near ポインタ ) がオーバーフローしないようにすることにします。
これが「segment 境界のバグ対策」というところでやっている内容です。

near ポインタは 16bit ですから、64KB 範囲のメモリを参照することが出来ます。
64KB を越えるファイルは WW に転送できません ( の筈 ) から、これで大丈夫なわけです。

●サンプル

サンプルです。 BGM とリソースのサンプル
実行すると BGM として「ドレミファソラシド」( 1 番目のリソース ) が流れます。
どれかキーを押すと「ドシラソファミレド」( 2 番目のリソース ) に変わります。
.cf ファイルとか、色々弄ってみてやって下さい。

●備考

何だか難しそうな感じだったサウンド・リソース関係ですが、 すまうぐさんのページのお陰でかなりはかどりました。
# というか、このページを見ないと出来なかったと思う(苦笑
分かり易い解説に感謝です。


戻る