A Day In The Life

とあるプログラマの備忘録

AVAudioSequencerでMIDIファイルを再生して曲の終わりにコールバックを設定する方法

AVAudioSequencerでMIDIファイルを再生して曲の終わりにコールバックを設定する方法です。AVAudioSequencer 自体にそのようなコールバックがないので CoreMIDI のコールバックを使います。CoreMIDIはSwiftのブロックではなくてC言語の関数ポインタでコールバックを取らないといけないケースがあって大変です。

public class Sequencer {

  let callBack: @convention(c) (UnsafeMutablePointer<Void>, MusicSequence, MusicTrack, MusicTimeStamp, UnsafePointer<MusicEventUserData>, MusicTimeStamp, MusicTimeStamp) -> Void = {
    (obj, seq, mt, timestamp, userData, timestamp2, timestamp3) in
    // Cタイプ関数なのでselfを使えません
    let mySelf: Sequencer = unsafeBitCast(obj, Sequencer.self)
    :
    : 曲の終了後に行う処理
    :
  }
    
  var sequencer: AVAudioSequencer

  public func playWithMidiURL(midiFileUrl: NSURL, audioFiles: [NSURL]) {
    // サンプラーとオーディオエンジンの初期化
    let audioEngine = AVAudioEngine()
    let samplerNode = AVAudioUnitSampler()

    // サンプラーとオーディオエンジンをつなげる
    audioEngine.attachNode(samplerNode)
    audioEngine.connect(samplerNode,
      to: audioEngine.mainMixerNode,
      format: samplerNode.outputFormatForBus(0))

    // オーディオエンジンをスタートする
    do {
      try samplerNode.loadAudioFilesAtURLs(audioFiles)
      try audioEngine.start()
    } catch {
            
    }

    // シーケンサーの初期化
    self.sequencer = AVAudioSequencer(audioEngine: audioEngine)
    let musicSequence = audioEngine.musicSequence
        
    // MIDIファイル読み込み
    do {
      try sequencer.loadFromURL(midiFileUrl, options: .SMF_ChannelsToTracks)
    } catch {
      print("Error load MIDI file")
    }
    // シーケンサスタート
    do {
      sequencer.prepareToPlay()
      try sequencer.start()
    } catch {
      print("Error play MIDI file")
    }
        
    // 曲の長さを取得
    var musicLengthInBeats: NSTimeInterval = 0.0
    for track in sequencer.tracks {
      let lengthInBeats = track.lengthInBeats
      if musicLengthInBeats < lengthInBeats {
        musicLengthInBeats = lengthInBeats
      }
    }
    // 曲の最後にコールバックを仕込む
    MusicSequenceSetUserCallback(musicSequence, callBack, unsafeBitCast(self, UnsafeMutablePointer<Void>.self))
    var musicTrack: MusicTrack = nil
    MusicSequenceGetIndTrack(musicSequence, 0, &musicTrack)
    let userData: UnsafeMutablePointer<MusicEventUserData> = UnsafeMutablePointer.alloc(1)
    MusicTrackNewUserEvent(musicTrack, ceil(musicLengthInBeats), userData)
  }
}

曲の長さを調べてタイマーでやれば良いと思われるかもしれませんが、それだと曲のポーズに対応できないのでこのような方法が必要になります。

参考記事

ソースコードはこちら

動作確認済みのソースコードはこちらに置いてあります。 - glassonion1/R9MIDISequencer