Open4

Swift: Cのstruct内の可変長バイト列を扱う

kabeyakabeya

Swiftで、C言語の構造体を扱おうとしたときに、以下のような奴に出くわすことがあります。

typedef struct MusicEventUserData
{
    UInt32    length;
    UInt8      data[1];
} MusicEventUserData;

このdataというのが曲者で、C言語上では、配列の宣言上のサイズ1を超えてlengthまでアクセスできるようにメモリが確保されているというようなパターンです。

Swiftではこれが以下のように定義されています。

public struct MusicEventUserData {
    public var length: UInt32
    public var data: (UInt8)
}

SwiftとCの自動生成ブリッジは、Cの固定長配列をタプルに変換するので、Swift側は要素が1個のタプルになっています。
(もしたとえばC側がUInt8 data[4]だったならSwift側はvar data: (UInt8,UInt8,UInt8,UInt8)だったでしょう)

ともかく、これをどう扱うかなんです。

kabeyakabeya

C言語では例えばどういう使い方をするのかというと、別のデータ構造を作って、キャストします。

typedef struct MyUserData
{
    UInt32    length;
    UInt32    value1;
    UInt32    value2;
} MyUserData;

MyUserData data;
data.length = sizeof(MyUserData);
data.value1 = 3;
data.value2 = 5;
// MusicEventUserData*を必要とするAPIに渡すとき
MusicEventUserData* dataPtrToAPI = (MusicEventUserData*)&data;
// APIからMusicEventUserData*が渡ってきたとき
MyUserData* dataPtr = (MyUserData*)dataPtrFromAPI;
// dataPtr->value1でアクセス

Swiftでも同じようなことをするんですね。

kabeyakabeya

Swiftも、C言語の例にならって別の構造体を定義します。単純なキャストは怒られるので、ちょっと書き方を工夫します。

struct MyUserData {
    var length: UInt32 = 0
    var value1: UInt32 = 0
    var value2: UInt32 = 0
}

var data = MyUserData()
data.length = UInt32(MemoryLayout<MyUserData>.stride)
data.value1 = 3
data.value2 = 5
// UnsafePointer<MusicEventUserData>を必要とするAPIに渡すとき
withUnsafePointer(to: &data) { originalPtr in
    originalPtr.withMemoryRebound(to: MusicEventUserData.self, capacity: 1) { dataPtrToAPI in
          // APIに渡す処理
    }
}
// APIからUnsafePointer<MusicEventUserData>が渡ってきたとき
// dataPtrFromAPI: UnsafePointer<MusicEventUserData>とします。
let _ = dataPtrFromAPI.withMemoryRebound(to: MyUserData.self, capacity: 1) { dataPtr in
    // dataPtr.pointee.value1でアクセス
}

渡すときは、withUnsafePointerでポインタを取得して、さらにwithMemoryReboundでキャストのようなことをします。withMemoryReboundの代わりにunsafeBitCastでも動作しますが、ちょっとコンパイラに注意されます(良くないよ、そういうの…、みたいな)。

渡ってきたときも同様にwithMemoryReboundで逆のことをします。

kabeyakabeya

もともとこの話の調査のきっかけはこうです。
AudioToolboxのMusicPlayerMusicPlayerStartしても、再生が終わったタイミングが分からないのです。
しかもMusicPlayerIsPlayingは、曲が再生中ならtrueを返す、のではなくて、明示的にMusicPlayerStopを呼ばない限りtrueを返すという凶悪な仕様です。

で、MusicTrackNewUserEventを使って曲終わりにユーザイベントを置くと、MusicSequenceSetUserCallbackで設定したコールバックが呼ばれる、という話ですね。

曲終わりのイベントというだけなら、コールバックが呼ばれた場合=曲終わり、ということなので別にMusicEventUserDataに何かデータを入れる必要もなくキャストも要らないのですが、後から別のタイミングでイベントを入れるとすると、そのイベントが何かを区別できたほうがいいし、そこに必要なデータも入っていて欲しい、ということで調べた、ということなんです。

ちなみに、最初、ユーザイベントを最初のトラック(テンポトラック)に設定したのですが、これだとコールバックが呼ばれませんでした。
曲の中の最後に再生されるMIDIイベントのあるトラックに置く必要があるようです。

曲終わりの検出ですが、全トラックを調べてMusicTrackGetPropertykSequenceTrackProperty_TrackLengthを指定してトラックの長さを取得します。
取得されるのは最後のMIDIイベントのタイミング(MusicTimeStamp)です。もっとも長いトラックの終わり=曲の終わり、とみなします。

ただ、こいつも完全には正確ではないんですね。
MIDIイベント自体はノートオフ&エンドオブトラックで終わっていても、サスティンとか効いているとノートオフ後も音が鳴っているというケースがあります。
でも取れるのはあくまで最後のMIDIイベントのタイミング。
音の鳴り終わりではないんです。

音の鳴り終わりを取得する方法は…
分かりませんでした。