ホームページ開発ツール>Xojo / Real Studio Trial and Error・CocoaのDeclareでMIDIを鳴らす

 Xojo / Real Studio Trial and Error

CocoaのDeclareでMIDIを鳴らす

目次
 はじめに

 以下は、Xojo Cocoaビルドについての話題です。

 MIDIを使って音楽を演奏することができないか、調べてみました。

 なお検証には、Xojo 2017 Release 1.1を用いています。(Mac mini 2018 + macOS 10.14.6 Mojave)


 方針

 Xojoは、REALbasicの頃からMIDI(Note Player)に対応しています。
 ただしこれは、楽器の種類を指定して発音するというシンプルなもので、あまり凝ったことはできません。
 ここでは、MIDIファイル(ループ)をシーケンサーで演奏するものを考えてみます。

 楽器単独で鳴らしても面白みがないので、ギター、ベース、キーボード、ドラムスの4つの楽器(チャンネル)を同時演奏することとします。
 各チャンネルではそれぞれ、GAIN(ここでは入力側の音量の意)、PAN、エフェクター(ディレイとリバーブ)の操作が可能とします。
 また、全体としてはプレイと停止、マスターボリューム(出力側の音量)とレベルメーターを装備します。

 さて、MIDI演奏ソフト自体は以前、iOSを意識してXcodeで試作したことがあったのですが、この時はオーディオ用のAPIとしてAUGraphを用いていました。
 今回もこれでいこうと思ったのですが、チェックしたらAUGraph(Audio Unit Processing Graph Servicesの各API)は10.14まででDeprecatedになっていました。
 暫くは大丈夫な気もしますが、折角なので(笑)、代替を探すことにしました。
 ググってみると、AVAudioEngineを使うのがポピュラーなようで、いくつか解説を読むと、基本的な考え方や処理の流れは一緒のようなので、これにしました。

 参考サイト(1):AVAudioEngineを使ってみる - Qiita

 とは云うものの、AVAudioEngineとMIDIシーケンサー(AVAudioSequencer)を繋ぐ方法が分かりません。
 情報が少なくて苦戦しましたが、結局、ノードをMusicDeviceとしたAVAudioUnitMIDIInstrumentを生成してAVAudioEngineに接続し、このエンジンを用いてAVAudioSequencerを生成することで、両者を接続できました。(注:音を鳴らすだけならAVAudioEngineは不要で、AVAudioSequencerだけあればよい。)

 AVAudioEngineのミキサー(mainMixerNode)は複数入力に対応しているので、各シーケンサーをミキサーに繋げば出力を一本化できるかと思ったのですが、繋ぎ方がよく分からなかったため、楽器ごとにエンジン&シーケンサーを割り付けてそれらを同時にスタート/ストップするという、原始的な方法をとりました。
MusicDeviceを複数生成してミキサーに繋いだエンジンを使ってシーケンサーを複数生成すると、全ての音が鳴ることは鳴るが、どうも一つのMusicDeviceに全シーケンサーが接続されてしまう エンジンを一つ(即ちミキサーも一つ)生成してチャンネル数分のMusicDeviceを接続し、このエンジンを用いてシーケンサーも複数生成すると、どうも最初に接続したMusicDeviceと最後に生成したシーケンサーが繋がるだけ(他のMusicDeviceは無効、残りのシーケンサーは直接出力)?のようで、個別に制御しようとすると各MusicDeviceと各シーケンサーを紐付ける必要がありそうなのだが、(できるかどうかも含めて)その方法(または別のアプローチ)が現時点では不明。
 GAINとPANは、当初ミキサーの入力で設定しようとしたのですが、これではダメで、シーケンサーの出力に設定したらOKでした。
 エフェクターのうち、ディレイは4つのパラメーターのうちwetDryMixdelayTime、リバーブは全パラメータ(factoryPresetwetDryMix)を変更可能とします。
 レベルメーターは、AUGraphとは異なり、installTapOnBus:bufferSize:format:block:を使うのが作法のようでした。

 参考サイト(2):ios - Level Metering with AVAudioEngine - Stack Overflow

 このメソッドは事後処理にBlocksを使うため、以前やった方法で試してみましたが、うまくいきませんでした。以前と異なり、Delegateである上に定期的に繰り返し呼ばれることが影響しているのかもしれません。(試しに、2019r2から追加されたObjCBlockも使ってみたが、StackOverflowExceptionで止まってしまった。)
 で、解決策は意外なところにありました。それは参考サイト(2)の下の方にあった別回答で、ミキサーのノードからaudioUnitを取得すると、後はAUGraphと同じ手法が使えるというもので、実際やってみたら動きました。
 なお、インジケータでの表示値は各チャンネルの合計値としたい訳ですが、(探し方が悪かったのか)この計算式が見つからなかったので、以下の騒音の計算式を使わせて頂きました。

 参考サイト(3):騒音の単位 dB(デシベル)の足し算の簡単な計算方法!!

 あと、ドラムスのループに関して注意するのは、ループの終端(AVMusicTrackのloopRangeで取得)が小節の終端と一致しない(場合がある?)点です。
 今回使用したデータは4/4拍子・4小節分なのですが、ファイルを読み込んで長さを取得すると、4*4=16にはならず、15.61667となってしまいます。
 これをそのままリピート再生すると他の楽器とズレてくるので、整数化(切り上げ)することで対応します。(処理はソフトウェア側で行い、データは加工しません。)

 以上を踏まえ、(残りの)仕様は以下の通りとしました。

 MIDIファイルについて

 本稿では入力のMIDIデータとして、ギター、ベース、キーボード、ドラムスの4種類のファイルを使用しています。
 それぞれはフリー素材または自作したもので、詳細は以下の通りです。

 ギター
 以下のページで公開されているものを使わせて頂きました。
 ギター打ち込み(簡単なコードストローク>こんな感じ、にリンクあり)
 なお、本データには先頭に空白の小節(セットアップ小節、というらしい)がありますが、今回のケースでは無くても問題なさそうだったのでカットしました。
 以下のソースコードでは、この、カットしたものが前提となっています。(カットしないと他の楽器とズレます。)

 ベース/キーボード
 ギターに合わせて自作したものです。(ダウンロードはこちらから:rb_tipsU.zip

 ドラムス
 実は、どこのサイトからダウンロードしたものか、判然としません(苦)。
 ファイル名はgroove_monkee_free_midi_gm.zipなので、検索すると以下が公式サイトのようですが、別のダウンロードサイトだったかもしれません。
 Groove Monkee | Best MIDI Drum Loops and Bass Loops for Songwriting
 ドラムスは他の楽器ほど相互依存性が高くないので、8ビート120テンポの4または8小節のループであれば代替は可能(の筈)。

 下準備
 1. ベース/キーボードをダウンロードし、解凍(MIDIフォルダが復元されます)。
 2. ギターをダウンロードし、MIDI編集ソフト(例えばAria Maestosa)で先頭小節をカットする。その後、MIDIフォルダ内に移動。
 3. ドラムスをダウンロードし、Rock Essentials 1>8th Rock>120 Rock 10 8ths Basic.midをMIDIフォルダ内に移動。
 4. MIDIフォルダを、後述するプロジェクトと同じフォルダ内に移動。


 Xojoでの実装
【ソースコードのコピー&ペーストについて】
・ソースコード(グレー背景部分の全文)をコピーし、指定のオブジェクトにペーストすると、(新規作成して名前等を個別にコピー&ペーストしなくても)復元されます。
・ペーストはオブジェクトに行って下さい。オブジェクト内のEvent Handlers/Methods/Properties等にペーストしても、うまくいかない場合があります。
・それでもペーストできない場合は、各項目のカッコ内を適用して下さい。
  1. Xojoで新規プロジェクトを作成(64bitビルド)
  2. Window1に、BevelButton2個(Name:BevelButton1、Name:BevelButton2)、Canvas(Name:Canvas1)、Label(Name:Label1)、Separator(Name:Separator1)、Slider(Name:Slider1)、Timer(Name:Timer1)を置く
  3. 以下をBevelButton1にペースト(できなければ、Sub - Endの間をActionイベントに記述)
    Sub Action() Handles Action
      PlayStart()
    End Sub
    
  4. 以下をBevelButton2にペースト(できなければ、Sub - Endの間をActionイベントに記述)
    Sub Action() Handles Action
      PlayStop()
      
      // プレイボタンを戻す
      BevelButton1.Value=false
    End Sub
    
  5. 以下をCanvas1にペースト(できなければ、Sub - Endの間をPaintイベントに記述)
    Sub Paint(g As Graphics, areas() As REALbasic.Rect) Handles Paint
      // 外枠
      g.ForeColor=RGB(191,191,191)
      g.DrawRect 0,0,Canvas1.Width,Canvas1.Height
      
      // 左右チャンネル名
      g.ForeColor=RGB(147,147,0)
      g.TextSize=9
      g.DrawString "L",5,12
      g.DrawString "R",5,25
      
      // 出力レベルは、-120dB ~ +30dBの範囲にあるので、Canvas1.Width-30にマッピングする
      g.ForeColor=RGB(0,191,0)
      Dim wL As Integer = ((pVolL+120)*(Canvas1.Width-30)) / 150
      g.FillRect 15,7,wL,5
      Dim wR As Integer = ((pVolR+120)*(Canvas1.Width-30)) / 150
      g.FillRect 15,18,wR,5
    End Sub
    
  6. 以下をSlider1にペースト(できなければ、Sub - Endの間をValueChangedイベントに記述)
    Sub ValueChanged() Handles ValueChanged
      // 全チャンネルの音量を同時に操作
      Dim v1 As Single=(me.Value)/100
      for i As Integer = 0 to pChannel.Ubound
        pChannel(i).SetVolume(v1)
      next
    End Sub
    
  7. 以下をTimer1にペースト(できなければ、Sub - Endの間をActionイベントに記述)
    Sub Action() Handles Action
      Dim vL, vLD, vLA(-1) As Single
      Dim vR, vRD, vRA(-1) As Single
      
      ReDim vLA(pChannel.Ubound)
      ReDim vRA(pChannel.Ubound)
      
      // チャンネル毎の左右の出力レベルを取得
      for i As Integer = 0 to pChannel.Ubound
        vLA(i) = pChannel(i).GetPostAveragePowerL()+120
        vRA(i) = pChannel(i).GetPostAveragePowerR()+120
      next
      
      // 左右それぞれ出力レベルを合計
      vL=vLA(0)
      vR=vRA(0)
      for i As Integer = 1 to pChannel.Ubound
        vLD=DecibelTable(Abs(vL-vLA(i)))
        if vL>vLA(i) then
          vL=vL+vLD
        else
          vL=vLA(i)+vLD
        end if
        
        vRD=DecibelTable(Abs(vR-vRA(i)))
        if vR>vRA(i) then
          vR=vR+vRD
        else
          vR=vRA(i)+vRD
        end if
      next
      
      // インジケーターを描画
      Window1.DrawIndicator(vL-120,vR-120)
    End Sub
    
  8. 以下をWindow1にペースト
    Sub Open() Handles Open
      InitWin()
    End Sub
    
  9. 以下をWindow1にペースト
    Protected Sub AddChannel(f As FolderItem)
      // 新規チャンネルコンテナを生成して、配列に追加
      Dim plyr As new ContainerControl1
      pChannel.Append plyr
      
      // チャンネルコンテナの配置位置(左端)を既存の次にセット
      Dim lft As Integer = pChannel.Ubound*plyr.Width
      
      // チャンネルコンテナを配置
      plyr.EmbedWithin(Self,lft,50,plyr.Width,plyr.Height)
      
      // チャンネルコンテナを初期化
      plyr.InitChannel(f)
    End Sub
    
  10. 以下をWindow1にペースト
    Protected Function DecibelTable(vv As Single) as Single
      Dim vvd As Integer
      
      // 四捨五入して整数化
      vvd = Round(vv)
      
      // 対応する値を返す
      select case vvd
      case 0,1
        return 3
      case 2,3,4
        return 2
      case 5,6,7,8,9
        return 1
      case 10
        return 0
      else
        return 0
      end select
    End Function
    
  11. 以下をWindow1にペースト
    Protected Sub DrawIndicator(vL As Single, vR As Single)
      // 値を保持
      pVolL=vL
      pVolR=vR
      
      // 再描画
      Canvas1.Refresh
    End Sub
    
  12. 以下をWindow1にペースト
    Protected Sub InitWin()
      Dim f As FolderItem
      
      f=GetFolderItem("").Child("MIDI").Child("120 Rock 10 8ths Basic.mid")
      AddChannel(f)
      
      f=GetFolderItem("").Child("MIDI").Child("s-bass_1.mid")
      AddChannel(f)
      
      f=GetFolderItem("").Child("MIDI").Child("s-gt_1.mid")
      AddChannel(f)
      
      f=GetFolderItem("").Child("MIDI").Child("s-piano_1.mid")
      AddChannel(f)
      
    End Sub
    
  13. 以下をWindow1にペースト
    Protected Sub PlayStart()
      // 全チャンネルをプレイ状態にセット
      for i As Integer = 0 to pChannel.Ubound
        pChannel(i).MusicPlay()
      next
      
      Timer1.Mode=2
    End Sub
    
  14. 以下をWindow1にペースト
    Protected Sub PlayStop()
      // 全チャンネルを停止状態にセット
      for i As Integer = 0 to pChannel.Ubound
        pChannel(i).MusicStop()
      next
      
      // 最小値を送った後にタイマー停止
      DrawIndicator(-120,-120)
      Timer1.Mode=0
    End Sub
    
  15. 以下をWindow1にペースト(できなければプロパティに、名前:pChannel(-1)、データ型:ContainerControl1、を追加)
    Protected Property pChannel(-1) as ContainerControl1
    
  16. 以下をWindow1にペースト(できなければプロパティに、名前:pVolL、データ型:Single、標準値:-120.0、を追加)
    Protected Property pVolL as Single = -120.0
    
  17. 以下をWindow1にペースト(できなければプロパティに、名前:pVolR、データ型:Single、標準値:-120.0、を追加)
    Protected Property pVolR as Single = -120.0
    
  18. 新規ContainerControlを作成(名前は、ここではデフォルトの「ContainerControl1」とした。)
    (注:IDEの右ペイン(ライブラリ)にあるContainerControlを、左ペイン(ナビゲーター)にドラッグ&ドロップする。)
  19. ContainerControl1に、BevelButton2個(Name:BevelButton1、Name:BevelButton2)、PopupMenu(Name:PopupMenu1)、Slider5個(Name:Slider1、Name:Slider2、Name:Slider3、Name:Slider4、Name:Slider5)、Timer(Name:Timer1)、Label2個(Name:Label1、Name:Label2)を置く(他に、必要ならLabel、Separatorを適宜追加。配置はスクリーンショットを参照)
  20. 以下をBevelButton1にペースト(できなければ、Sub - Endの間をActionイベントに記述)
    Sub Action() Handles Action
      if me.Value then
        // バイパスをオフにして、対応するスライダーの値をディレイにセット
        SetDelayBypass(false)
        Dim v4 As Single=Slider3.Value
        Dim v5 As Single=Slider4.Value/50
        SetDelay(v4,v5,50,15000)  // feedbackとlowPassCutoffは標準値をセット
      else
        // バイパスをオンに
        SetDelayBypass(true)
      end if
    End Sub
    
  21. 以下をBevelButton2にペースト(できなければ、Sub - Endの間をActionイベントに記述)
    Sub Action() Handles Action
      if me.Value then
        // バイパスをオフにして、対応するポップアップとスライダーの値をリバーブにセット
        SetReverbBypass(false)
        Dim p1 As Integer = PopupMenu1.ListIndex
        Dim v7 As Single=Slider5.Value
        SetReverb(p1,v7)
      else
        // バイパスをオンに
        SetReverbBypass(true)
      end if
    End Sub
    
  22. 以下をPopupMenu1にペースト(できなければ、Sub - Endの間をChangeイベントに記述)
    Sub Change() Handles Change
      // 対応するポップアップとスライダーの値をリバーブにセット
      Dim p1 As Integer = me.ListIndex
      Dim v7 As Single=Slider5.Value
      SetReverb(p1,v7)
    End Sub
    
  23. 以下をSlider1にペースト(できなければ、Sub - Endの間をValueChangedイベントに記述)
    Sub ValueChanged() Handles ValueChanged
      // 対応するスライダーの値をゲインにセット
      Dim v1 As Single=(me.Value)/100
      SetGain(v1)
      
      // 値を表示
      ShowValue(v1)
    End Sub
    
  24. 以下をSlider2にペースト(できなければ、Sub - Endの間をValueChangedイベントに記述)
    Sub ValueChanged() Handles ValueChanged
      // 対応するスライダーの値をパンにセット
      Dim v3 As Single=((me.Value-50)/100)*2
      SetPan(v3)  // -1.0 〜 1.0
      
      // 値を表示
      ShowValue(v3)
    End Sub
    
  25. 以下をSlider3にペースト(できなければ、Sub - Endの間をValueChangedイベントに記述)
    Sub ValueChanged() Handles ValueChanged
      // 対応するスライダーの値をディレイにセット
      Dim v4 As Single=me.Value
      Dim v5 As Single=Slider4.Value/50
      SetDelay(v4,v5,50,15000)
      
      // 値を表示
      ShowValue(v4)
    End Sub
    
  26. 以下をSlider4にペースト(できなければ、Sub - Endの間をValueChangedイベントに記述)
    Sub ValueChanged() Handles ValueChanged
      // 対応するスライダーの値をディレイにセット
      Dim v4 As Single=Slider3.Value
      Dim v5 As Single=me.Value/50
      SetDelay(v4,v5,50,15000)
      
      // 値を表示
      ShowValue(v5)
    End Sub
    
  27. 以下をSlider5にペースト(できなければ、Sub - Endの間をValueChangedイベントに記述)
    Sub ValueChanged() Handles ValueChanged
      // 対応するスライダーの値をディレイにセット
      Dim p1 As Integer = PopupMenu1.ListIndex
      Dim v7 As Single=me.Value
      SetReverb(p1,v7)
      
      // 値を表示
      ShowValue(v7)
    End Sub
    
  28. 以下をTimer1にペースト(できなければ、Sub - Endの間をActionイベントに記述)
    Sub Action() Handles Action
      // 値をクリア
      Label2.Text="---"
    End Sub
    
  29. 以下をContainerControl1にペースト
    Protected Function CreateAudioEngine() as Ptr
      Dim error As Ptr
      
      // 文字列を指定してクラスオブジェクトを取得する。最初に一回宣言しておけばよい。
      Declare Function NSClassFromString Lib "Cocoa" (aClassName As CFStringRef) As Ptr
      
      Declare Function alloc Lib "Cocoa" selector "alloc" (class_id As Ptr) As Ptr
      Declare Function init Lib "Cocoa" selector "init" (obj_id As Ptr) As Ptr
      
      // エンジン生成
      Dim engine As Ptr = NSClassFromString("AVAudioEngine")
      engine = init(alloc(engine))
      
      // ノード設定オプションをMusicDeviceとしてセット
      Dim cd As AudioComponentDescription
      cd.componentManufacturer = &h6170706c // kAudioUnitManufacturer_Apple = 'appl'
      cd.componentFlags = 0
      cd.componentFlagsMask = 0
      cd.componentType = &h61756d75  // kAudioUnitType_MusicDevice = 'aumu'
      cd.componentSubType = &h646c7320  // kAudioUnitSubType_DLSSynth = 'dls '
      
      // MIDIデバイス生成
      pInst = NSClassFromString("AVAudioUnitMIDIInstrument")
      Declare Function initWithAudioComponentDescription Lib "Cocoa" selector "initWithAudioComponentDescription:" (obj_id As Ptr, desc As AudioComponentDescription) As Ptr
      pInst = initWithAudioComponentDescription(alloc(pInst), cd)
      
      // エフェクター生成
      pDelay = NSClassFromString("AVAudioUnitDelay")
      pDelay = init(alloc(pDelay))
      pReverb = NSClassFromString("AVAudioUnitReverb")
      pReverb = init(alloc(pReverb))
      
      // MIDIデバイスとエフェクターをエンジンに接続
      Declare Sub attachNode Lib "Cocoa" selector "attachNode:" (class_id As Ptr, node As Ptr)
      attachNode(engine, pInst)
      attachNode(engine, pDelay)
      attachNode(engine, pReverb)
      
      // ビルトインノードを取得
      Declare Function mainMixerNode Lib "Cocoa" selector "mainMixerNode" (class_id As Ptr) As Ptr
      pMixer = mainMixerNode(engine)
      
      // mixerからformatを取得
      Declare Function outputFormatForBus Lib "Cocoa" selector "outputFormatForBus:" (class_id As Ptr, idx As UInteger) As Ptr
      Dim format As Ptr = outputFormatForBus(pMixer, 0)
      
      // ノードの接続順を指定
      Declare Sub connecttoformat Lib "Cocoa" selector "connect:to:format:" (class_id As Ptr, src As Ptr, dest As Ptr, format As Ptr)
      connecttoformat(engine, pInst, pDelay, format)
      connecttoformat(engine, pDelay, pReverb, format)
      connecttoformat(engine, pReverb, pMixer, format)
      
      // エンジン実行
      Declare Sub startAndReturnError Lib "Cocoa" selector "startAndReturnError:" (class_id As Ptr, byRef err As Ptr)
      startAndReturnError(engine, error)
      
      // エンジンを返す
      return engine
    End Function
    
  30. 以下をContainerControl1にペースト
    Public Sub ExtractBodyAndExtn(st As String, byRef body As String, byRef ext As String)
      Dim i, cnt As Integer
      
      // ピリオドの数をカウント
      cnt=CountFields(st,".")
      
      // 拡張子がなければ入力をbodyとして返す
      if cnt<=1 then
        body=st
        return
      end if
      
      // 最後のピリオド以降を拡張子とみなす
      ext=NthField(st,".",cnt)
      
      // 最後のピリオド以前をbodyとみなす
      body=""
      for i=1 to CountFields(st,".")-2
        body=body+NthField(st,".",i)+"."
      next
      body=body+NthField(st,".",cnt-1)
    End Sub
    
  31. 以下をContainerControl1にペースト
    Public Function GetPostAveragePowerL() as Single
      // ミキサーの取得
      Dim value As Single = -120.0
      Dim vol As Single = (Window1.Slider1.Value)/100
      Declare Function audioUnit Lib "Cocoa" selector "audioUnit" (class_id As Ptr) As ComponentInstanceRecord
      Dim unit As ComponentInstanceRecord = audioUnit(pMixer)
      
      // 左チャンネルの出力レベルを取得
      Declare Function AudioUnitGetParameter Lib "AudioUnit" (inUnit As ComponentInstanceRecord, inID As UInt32, inScope As UInt32, inElement As UInt32, byRef outValue As Single) As Integer
      Dim result As Integer = AudioUnitGetParameter(unit, 3000, 2, 0, value)  // kMultiChannelMixerParam_PostAveragePower = 3000 (3000 = Left Channel)
      if result<>0 then msgBox "GetPostAveragePowerL "+Str(result)
      
      return ((value + 120.0) * vol) - 120.0
    End Function
    
  32. 以下をContainerControl1にペースト
    Public Function GetPostAveragePowerR() as Single
      // ミキサーの取得
      Dim value As Single = -120.0
      Dim vol As Single = (Window1.Slider1.Value)/100
      Declare Function audioUnit Lib "Cocoa" selector "audioUnit" (class_id As Ptr) As ComponentInstanceRecord
      Dim unit As ComponentInstanceRecord = audioUnit(pMixer)
      
      // 右チャンネルの出力レベルを取得
      Declare Function AudioUnitGetParameter Lib "AudioUnit" (inUnit As ComponentInstanceRecord, inID As UInt32, inScope As UInt32, inElement As UInt32, byRef outValue As Single) As Integer
      Dim result As Integer = AudioUnitGetParameter(unit, 3000+1, 2, 0, value)  // kMultiChannelMixerParam_PostAveragePower = 3000 (3000+1 = Right Channel)
      if result<>0 then msgBox "GetPostAveragePowerR "+Str(result)
      
      return ((value + 120.0) * vol) - 120.0
    End Function
    
  33. 以下をContainerControl1にペースト
    Public Sub InitChannel(f As FolderItem)
      // ファイル名を表示
      Dim body, extn As String
      ExtractBodyAndExtn(f.Name,body,extn)
      Label1.Text=body
      
      // AudioEngineを生成して起動
      Dim engine As Ptr = CreateAUDioEngine()
      
      // Sequencerを生成してMIDIファイルを読み込む
      SetSequencer(f,engine)
      
      // ゲイン設定
      Dim v1 As Single=(Slider1.Value)/100
      SetGain(v1)
      
      // 音量設定
      Dim v2 As Single=(Window1.Slider1.Value)/100
      SetVolume(v2)
      
      // パン(定位)設定
      Dim v3 As Single=((Slider3.Value-50)/100)*2
      SetPan(v3)
      
      // ディレイ設定(初期値は無効)
      SetDelayBypass(true)
      
      // リバーブ設定(初期値は無効)
      SetReverbBypass(true)
      
      // MeteringModeをオンに
      SetMeteringMode(1)
    End Sub
    
  34. 以下をContainerControl1にペースト
    Public Sub MusicPlay()
      // シーケンサー実行
      Dim error As Ptr
      Declare Sub startAndReturnError Lib "Cocoa" selector "startAndReturnError:" (class_id As Ptr, byRef err As Ptr)
      startAndReturnError(pSequencer, error)
    End Sub
    
  35. 以下をContainerControl1にペースト
    Public Sub MusicStop()
      // シーケンサー停止
      Declare Sub stop Lib "Cocoa" selector "stop" (class_id As Ptr)
      stop(pSequencer)
    End Sub
    
  36. 以下をContainerControl1にペースト
    Protected Sub SetDelay(wetdryVal As Single, timeVal As Single, fbackVal As Single, lpcutVal As Single)
      // delayの設定
      Declare Sub setWetDryMix Lib "Cocoa" selector "setWetDryMix:" (class_id As Ptr, val As Single)
      setWetDryMix(pDelay, wetdryVal)
      Declare Sub setDelayTime Lib "Cocoa" selector "setDelayTime:" (class_id As Ptr, val As Single)
      setDelayTime(pDelay, timeVal)
      Declare Sub setFeedback Lib "Cocoa" selector "setFeedback:" (class_id As Ptr, val As Single)
      setFeedback(pDelay, fbackVal)
      Declare Sub setLowPassCutoff Lib "Cocoa" selector "setLowPassCutoff:" (class_id As Ptr, val As Single)
      setLowPassCutoff(pDelay, lpcutVal)
    End Sub
    
  37. 以下をContainerControl1にペースト
    Protected Sub SetDelayBypass(flg As Boolean)
      // delayのバイパス設定
      Declare Sub setBypass Lib "Cocoa" selector "setBypass:" (class_id As Ptr, val As Boolean)
      setBypass(pDelay, flg)
    End Sub
    
  38. 以下をContainerControl1にペースト
    Protected Sub SetGain(inVal As Single)
      // 入力側をゲインと見做す
      Declare Sub setVolume Lib "Cocoa" selector "setVolume:" (class_id As Ptr, val As Single)
      setVolume(pInst, inVal)
    End Sub
    
  39. 以下をContainerControl1にペースト
    Protected Sub SetMeteringMode(allowMetering As UInt32)
      // audioUnitを取得
      Declare Function audioUnit Lib "Cocoa" selector "audioUnit" (class_id As Ptr) As ComponentInstanceRecord
      Dim unit As ComponentInstanceRecord = audioUnit(pMixer)
      
      // メーターの使用有無をセット
      Declare Function AudioUnitSetProperty Lib "AudioUnit" (inUnit As ComponentInstanceRecord, inID As UInt32, inScope As UInt32, inElement As UInt32, byRef inData As UInt32, inDataSize As UInt32) As Integer
      Dim result As Integer = AudioUnitSetProperty(unit, 3007, 2, 0, allowMetering, 4)  // kAudioUnitProperty_MeteringMode = 3007
      if result<>0 then msgBox "SetMeteringMode "+Str(result)
    End Sub
    
  40. 以下をContainerControl1にペースト
    Protected Sub SetPan(panVal As Single)
      // panの設定
      Declare Sub setPan Lib "Cocoa" selector "setPan:" (class_id As Ptr, val As Single)
      setPan(pInst, panVal)
    End Sub
    
  41. 以下をContainerControl1にペースト
    Protected Sub SetReverb(preset As Integer, wetdryVal As Single)
      // reverbの設定
      Declare Sub loadFactoryPreset Lib "Cocoa" selector "loadFactoryPreset:" (class_id As Ptr, pre As Integer)
      loadFactoryPreset(pReverb, preset)
      Declare Sub setWetDryMix Lib "Cocoa" selector "setWetDryMix:" (class_id As Ptr, val As Single)
      setWetDryMix(pReverb, wetdryVal)
    End Sub
    
  42. 以下をContainerControl1にペースト
    Protected Sub SetReverbBypass(flg As Boolean)
      // reverbのバイパス設定
      Declare Sub setBypass Lib "Cocoa" selector "setBypass:" (class_id As Ptr, val As Boolean)
      setBypass(pReverb, flg)
    End Sub
    
  43. 以下をContainerControl1にペースト
    Protected Sub SetSequencer(f As FolderItem, engine As Ptr)
      Dim error As Ptr
      
      // 文字列を指定してクラスオブジェクトを取得する。最初に一回宣言しておけばよい。
      Declare Function NSClassFromString Lib "Cocoa" (aClassName As CFStringRef) As Ptr
      
      Declare Function alloc Lib "Cocoa" selector "alloc" (class_id As Ptr) As Ptr
      Declare Function init Lib "Cocoa" selector "init" (obj_id As Ptr) As Ptr
      
      // エンジンを使ってシーケンサー生成
      Dim sequencer As Ptr = NSClassFromString("AVAudioSequencer")
      Declare Function initWithAudioEngine Lib "Cocoa" selector "initWithAudioEngine:" (obj_id As Ptr, engn As Ptr) As Ptr
      sequencer = initWithAudioEngine(alloc(sequencer), engine)
      
      // MIDIファイル読み込み
      Dim url As Ptr = NSClassFromString("NSURL")  // クラスメソッドなので、まずNSURLクラスを取得
      Declare Function fileURLWithPath Lib "Cocoa" Selector "fileURLWithPath:" (receiver As Ptr, path As CFStringRef) As Ptr
      url = fileURLWithPath(url, f.NativePath)
      Declare Sub loadFromURL Lib "Cocoa" selector "loadFromURL:options:error:" (class_id As Ptr, node As Ptr, typ As Integer, byRef err As Ptr)
      loadFromURL(sequencer, url, 1, error)  // options = kMusicSequenceLoadSMF_ChannelsToTracks = (1 << 0)
      
      // トラックを取得
      Declare Function tracks Lib "Cocoa" selector "tracks" (obj_id As Ptr) As Ptr
      Dim ary As Ptr = tracks(sequencer)
      Declare Function objectAtIndex Lib "Cocoa" selector "objectAtIndex:" (obj_id As Ptr, idx As Integer) As Ptr
      Dim track As Ptr = objectAtIndex(ary, 0)
      
      // リピート再生(回数はデフォルトでForeverになってるので、フラグオンのみ)
      Declare Sub setLoopingEnabled Lib "Cocoa" selector "setLoopingEnabled:" (obj_id As Ptr, flg As Boolean)
      setLoopingEnabled(track, true)
      
      // トラック(特にドラムス?)によってはループの終端が小節の終端と一致していない場合があるので、一致させる
      Declare Function loopRange Lib "Cocoa" selector "loopRange" (obj_id As Ptr) As AVBeatRange
      Dim range As AVBeatRange = loopRange(track)
      range.lengthInBeats=Ceil(range.lengthInBeats)  // 切り上げて整数化(元々整数の場合は同じ値のまま)
      Declare Sub setLoopRange Lib "Cocoa" selector "setLoopRange:" (obj_id As Ptr, rng As AVBeatRange)
      setLoopRange(track, range)
      
      // シーケンサーを保持
      pSequencer=sequencer
    End Sub
    
  44. 以下をContainerControl1にペースト
    Public Sub SetVolume(outVal As Single)
      // 音量の設定
      Declare Sub setVolume Lib "Cocoa" selector "setOutputVolume:" (class_id As Ptr, val As Single)
      setVolume(pMixer, outVal)
    End Sub
    
  45. 以下をContainerControl1にペースト
    Protected Sub ShowValue(vv As Single)
      Timer1.Mode=0  // まず、タイマーを停止モードに
      
      Label2.Text=Str(vv)  // 値をセット
      
      Timer1.Mode=1  // タイマー始動
    End Sub
    
  46. 以下をContainerControl1にペースト(できなければプロパティに、名前:pSequencer、データ型:Ptr、を追加)
    Protected Property pSequencer as Ptr
    
  47. 以下をContainerControl1にペースト(できなければプロパティに、名前:pReverb、データ型:Ptr、を追加)
    Protected Property pReverb as Ptr
    
  48. 以下をContainerControl1にペースト(できなければプロパティに、名前:pDelay、データ型:Ptr、を追加)
    Protected Property pDelay as Ptr
    
  49. 以下をContainerControl1にペースト(できなければプロパティに、名前:pMixer、データ型:Ptr、を追加)
    Protected Property pMixer as Ptr
    
  50. 以下をContainerControl1にペースト(できなければプロパティに、名前:pInst、データ型:Ptr、を追加)
    Protected Property pInst as Ptr
    
  51. 以下をContainerControl1にペースト
    Protected Structure AudioComponentDescription
      componentType As UInt32
      componentSubType As UInt32
      componentManufacturer As UInt32
      componentFlags As UInt32
      componentFlagsMask As UInt32
    End Structure
    
  52. 以下をContainerControl1にペースト
    Protected Structure AVBeatRange
      startBeat As Double
      lengthInBeats As Double
    End Structure
    
  53. 以下をContainerControl1にペースト
    Protected Structure ComponentInstanceRecord
      data As Int64
    End Structure
    
    (注:元はlong型のため、64bitビルドではInt64、32bitビルドではInt32にする。)
  54. Sliderのカスタムクラスを作成(名前は、ここではデフォルトの「CustomSlider」とした。)
    (注:IDEの右ペイン(ライブラリ)にあるSliderを、左ペイン(ナビゲーター)にドラッグ&ドロップする。)
  55. CustomSliderにイベント定義を追加し、イベント名をOpenにする
  56. 以下をCustomSliderにペースト
    Sub Open() Handles Open
      // スライダーの目盛数を11に
      Declare Sub numberOfTickMarks Lib "Cocoa" selector "setNumberOfTickMarks:" (receiver As Integer, mk As Integer)
      numberOfTickMarks(me.Handle, 11)
      
      // インスタンスのOpenイベントをレイズ
      RaiseEvent Open()
    End Sub
    
  57. ContainerControl1のSlider1〜5のSuperをCustomSliderに設定
 実行してみたところ、指定したMIDIファイルデータがシーケンサーによって同時演奏されることを確認しました。
S Shot1


 おわりに

 プレイ中に停止して再びプレイすると、止まったところから再開するのですが(これ自体はシステムの仕様)、どうも不安定です。
 チャンネルが独立していることが原因である可能性が考えられるので、(本来あるべき?)一つのミキサーに全て繋ぐ方法を模索した方がいいかもしれません。

 あと、(レベルメーターと同じアプローチになるが)AudioUnitからエフェクターにアクセスすると、AUGraphを用いる時と同様の、より詳細なパラメーターの設定ができるようで、このあたりも面白いかも。

 参考サイト(4):AVAudioUnitのパラメータ詳細(応用編) - Qiita


 お世話になったサイト

 貴重な情報をご提供頂いている皆様に、お礼申し上げます。(以下、順不同)

 参考サイト(1):AVAudioEngineを使ってみる - Qiita
 参考サイト(2):ios - Level Metering with AVAudioEngine - Stack Overflow
 参考サイト(3):騒音の単位 dB(デシベル)の足し算の簡単な計算方法!!
 参考サイト(4):AVAudioUnitのパラメータ詳細(応用編) - Qiita


 更新履歴

 2020.06.04 方針のインデント内の記述に誤りがあったので訂正。
 2020.05.25 新規作成


[Home]  [MacSoft]  [Donation]  [History]  [Privacy Policy]  [Affiliate Policy]