Swift: Cのstruct内の可変長バイト列を扱う
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)
だったでしょう)
ともかく、これをどう扱うかなんです。
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でも同じようなことをするんですね。
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
で逆のことをします。
もともとこの話の調査のきっかけはこうです。
AudioToolboxのMusicPlayer
でMusicPlayerStart
しても、再生が終わったタイミングが分からないのです。
しかもMusicPlayerIsPlaying
は、曲が再生中ならtrue
を返す、のではなくて、明示的にMusicPlayerStop
を呼ばない限りtrue
を返すという凶悪な仕様です。
で、MusicTrackNewUserEvent
を使って曲終わりにユーザイベントを置くと、MusicSequenceSetUserCallback
で設定したコールバックが呼ばれる、という話ですね。
曲終わりのイベントというだけなら、コールバックが呼ばれた場合=曲終わり、ということなので別にMusicEventUserData
に何かデータを入れる必要もなくキャストも要らないのですが、後から別のタイミングでイベントを入れるとすると、そのイベントが何かを区別できたほうがいいし、そこに必要なデータも入っていて欲しい、ということで調べた、ということなんです。
ちなみに、最初、ユーザイベントを最初のトラック(テンポトラック)に設定したのですが、これだとコールバックが呼ばれませんでした。
曲の中の最後に再生されるMIDIイベントのあるトラックに置く必要があるようです。
曲終わりの検出ですが、全トラックを調べてMusicTrackGetProperty
でkSequenceTrackProperty_TrackLength
を指定してトラックの長さを取得します。
取得されるのは最後のMIDIイベントのタイミング(MusicTimeStamp
)です。もっとも長いトラックの終わり=曲の終わり、とみなします。
ただ、こいつも完全には正確ではないんですね。
MIDIイベント自体はノートオフ&エンドオブトラックで終わっていても、サスティンとか効いているとノートオフ後も音が鳴っているというケースがあります。
でも取れるのはあくまで最後のMIDIイベントのタイミング。
音の鳴り終わりではないんです。
音の鳴り終わりを取得する方法は…
分かりませんでした。