ホームページ開発ツール>Xojo / Real Studio Trial and Error・CocoaのDeclareでMIDIを鳴らす・録音する(不具合対応)

 Xojo / Real Studio Trial and Error

CocoaのDeclareでMIDIを鳴らす・録音する(不具合対応)

目次
 はじめに

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

 MIDI演奏時の録音ができなくなっていた問題を、調べてみました。

 なお検証には、Xojo 2018 Release 2/2022 Release 4.1を用いています。(Mac mini 2018 + macOS 15.4.1 Sequoia)


 経緯

 本プロジェクトは、macOS 15 Sequoiaにおいて、Xojo 2018 Release 2/2022 Release 4.1で動作しないことが確認できています。
注:当方ではXojo 2018 Release 2(以下、2018r2)と2022 Release 4.1(以下、2022r41)を常用しているため、この二つを取り上げている。両者の間のどこで、以下に示す違いが出るのかは、 検証していないので不明。また、macOS 10.14 Mojave(以下、Mojave)以降、macOS 15 Sequoia(以下、Sequoia)迄のどのバージョンから以下の状況が現出するのかも、未検証。
 具体的には、
 1. 2018r2:システム設定>サウンドで出力をスピーカーにすると録音できない。SoundFlowerにすれば、録音は可能だがスピーカーから音は出ない。
 2. 2022r41:何をやっても録音できない。
 問題は二つあって、一つは両者共通なので、まずは2018r2から見ていきます。

 本プロジェクトでは、ContainerControl1>CreateAudioEngine()で出力のChannelMapを設定しているのですが、Mojaveでは、SoundFlowerがインストールされていれば、出力が2系統(左右あるので、チャンネル数は4)に増え、取得すると{-1,-1,-1,-1}になっているので、{0,1,0,1}で上書きすれば事は足りました。
 それがSequoiaでは、同状況で{0,1}となって、選択中の出力にしかアクセス出来ない状態となっています。

 あれこれ調べた結果、Sequoiaで出力が2系統に拡張されるためには、マイクアクセスのダイアログで許可が指定されなければならない、ということが分かりました。(ちなみに、ダイアログ自体はMojaveでも表示はされるが、録音開始直前で、それでも動作はする。)
デフォルト値が、(現在の出力選択によって){0,1,-1,-1}または{-1,-1,0,1}になる、といった違いがある。
出力の拡張に、なぜ入力であるマイクの許可が必要?という点には疑問が残らないでもないが、SoundFlowerは仮想デバイスで、入力にも出力にも現れるから?
S Shot2
(クリックで拡大)
 また、ダイアログを表示するトリガーとして、入力ノードを取得すればいい(注:他にもあるかもしれないが未検証)ことも分かりました。
 なので、以下の文をChannelMap設定前に挿入すれば、2018r2では動作することが確認できました。
  // インプットノードを取得(マイクアクセスのダイアログが表示。許可するとChannelMapにSoundflowerが追加される。ただし非アクティブなので、あとでChannelMapを書き換える。)
  Declare Function inputNode Lib "Cocoa" selector "inputNode" (class_id As Ptr) As Ptr
  Dim input As Ptr = inputNode(engine)

 もう一つの問題は、2022r41では、上記対応だけではダイアログが表示されないことです。
 ヒントは、別のテストプログラムが(似た状況で)異常終了した際に表示されたレポートにありました。
S Shot2
(クリックで拡大。注:ハイライトは当方で付与したもの。)

 すなわち、info.plistにNSMicrophoneUsageDescriptionを追加しなければならない、ということです。
 これを追加することで、2022r41でも入力ノードを呼び出した際にダイアログが表示され、許可するとChannelMapが拡張されました。
S Shot2
(クリックで拡大)
 参考サイト(1):ios - The app's Info.plist must contain an NSMicrophoneUsageDescription key with a string value explaining to the user how the app uses this data - Stack Overflow

 以下は、追加情報です。
 これまで、ChannelMapにアクセスするために、audioUnitを取得していましたが、AUAudioUnitを使う方法もあることが分かりました。
 AUAudioUnitはaudioUnitのラッパーで、audioUnitがC/C++ベースなのに対し、AUAudioUnitはObjective-C/Swiftベースなので、よりスマートに記述できる、ということのようです。 (Xojoでは却って手間が増えますが…)
 こちらも動作することは確認できましたので、その時のサンプルも載せておきます。

 参考サイト(2):swift - What is the difference between AUAudioUnit and AudioUnit - Stack Overflow

 Xojoでの実装
注:以下の実装では、一部Xojo 2022r4.1ではDeprecatedな機能が使われています。必要なら推奨される機能に置き換えて下さい。
【ソースコードのコピー&ペーストについて】
・ソースコード(グレー背景部分の全文)をコピーし、指定のオブジェクトにペーストすると、(新規作成して名前等を個別にコピー&ペーストしなくても)復元されます。
・ペーストはオブジェクトに行って下さい。オブジェクト内のEvent Handlers/Methods/Properties等にペーストしても、うまくいかない場合があります。
・それでもペーストできない場合は、各項目のカッコ内を適用して下さい。
  1. 前回プロジェクトをベースとする
  2. ContainerControl1のCreateAudioEngineを、以下に置き替え
    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)
      
      // アウトプットノードを取得(通常の発音時はなくてもいいが、ChannelMapはアウトプットにしか設定できないため)
      Declare Function outputNode Lib "Cocoa" selector "outputNode" (class_id As Ptr) As Ptr
      Dim output As Ptr = outputNode(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)
      connecttoformat(engine, pMixer, output, format)
      
      // 録音が有効ならChannelMapをセット
      if Window1.pEnableRec then
        
        // インプットノードを取得(マイクアクセスのダイアログが表示。許可するとChannelMapにSoundflowerが追加される。ただし非アクティブなので、ChannelMapを書き換える。)
        Declare Function inputNode Lib "Cocoa" selector "inputNode" (class_id As Ptr) As Ptr
        Dim input As Ptr = inputNode(engine)
        
        // アウトプットノードのaudioUnitを取得
        Declare Function audioUnit Lib "Cocoa" selector "audioUnit" (class_id As Ptr) As ComponentInstanceRecord
        Dim unit As ComponentInstanceRecord = audioUnit(output)
        
        // アウトプットノードのaudioUnitにChannelMapをセット
        Declare Function AudioUnitSetProperty Lib "AudioUnit" (inUnit As ComponentInstanceRecord, inID As UInt32, inScope As UInt32, inElement As UInt32, inData As Ptr, inDataSize As UInt32) As Integer
        Dim result As Int32 = AudioUnitSetProperty(unit, 2002, 0, 1, Window1.pChannelMap, Window1.pChannelMap.Size)  // 2002 = kAudioOutputUnitProperty_ChannelMap
        if result<>0 then msgBox "SetChannelMap 3 "+Str(result)
        
      end if
      
      // エンジン実行
      Declare Sub startAndReturnError Lib "Cocoa" selector "startAndReturnError:" (class_id As Ptr, byRef err As Ptr)
      startAndReturnError(engine, error)
      
      // エンジンを返す
      return engine
      
    End Function
    
 コードの実装は以上です。引き続き、ファイル関連の処理を実施。
  1. 以下の内容でテキストファイルを作成し、名前をInfo.plistとして保存
    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
    <plist version="1.0">
    <dict>
    	<key>NSMicrophoneUsageDescription</key>
    	<string>録音するには許可が必要です。</string>
    </dict>
    </plist>
    
     注:stringタグの中身がダイアログに表示される。(必要なら、ローカライズの手法を適用する等して下さい。)
  2. プロジェクトに、Info.plistをドラッグ&ドロップ

 Xojoでの実装(AUAudioUnit版)
  1. Window1のInitChannelMapを、以下に置き替え
    Protected Sub InitChannelMap()
      
      // 文字列を指定してクラスオブジェクトを取得する。最初に一回宣言しておけばよい。
      Declare Function NSClassFromString Lib "Cocoa" (aClassName As CFStringRef) As Ptr
      
      Declare Function numberWithUnsignedInteger Lib "Cocoa" Selector "numberWithUnsignedInteger:" (receiver As Ptr, val As Integer) As Ptr
      
      // 0と1のNSNumberを生成
      Dim num0 As Ptr = NSClassFromString("NSNumber")  // クラスメソッドなので、まずNSNumberクラスを取得
      num0 = numberWithUnsignedInteger(num0, 0)  // 0
      Dim num1 As Ptr = NSClassFromString("NSNumber")  // クラスメソッドなので、まずNSNumberクラスを取得
      num1 = numberWithUnsignedInteger(num1, 1)  // 1
      
      // 配列を生成
      pChannelMap = NSClassFromString("NSMutableArray")  // クラスメソッドなので、まずNSMutableArrayクラスを取得
      Declare Function getArray Lib "Cocoa" Selector "array" (receiver As Ptr) As Ptr  // Return Array*
      pChannelMap=getArray(pChannelMap)
      
      // 出力チャンネルの指定
      Declare Sub addObject Lib "Cocoa" Selector "addObject:" (receiver As Ptr, obj As Ptr)
      addObject(pChannelMap, num0)  // Soundflower 2ch Lに送られる
      addObject(pChannelMap, num1)  // Soundflower 2ch Rに送られる
      addObject(pChannelMap, num0)  // 内蔵スピーカー Lに送られる
      addObject(pChannelMap, num1)  // 内蔵スピーカー Rに送られる
      
    End Sub
    
  2. ContainerControl1のCreateAudioEngineを、以下に置き替え
    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)
      
      // アウトプットノードを取得(通常の発音時はなくてもいいが、ChannelMapはアウトプットにしか設定できないため)
      Declare Function outputNode Lib "Cocoa" selector "outputNode" (class_id As Ptr) As Ptr
      Dim output As Ptr = outputNode(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)
      connecttoformat(engine, pMixer, output, format)
      
      // 録音が有効ならChannelMapをセット
      if Window1.pEnableRec then
        
        // インプットノードを取得(マイクアクセスのダイアログが表示。許可するとChannelMapにSoundflowerが追加される。ただし非アクティブなので、ChannelMapを書き換える。)
        Declare Function inputNode Lib "Cocoa" selector "inputNode" (class_id As Ptr) As Ptr
        Dim input As Ptr = inputNode(engine)
        
        // アウトプットノードのAUAudioUnitを取得
        Declare Function AUAudioUnit Lib "Cocoa" selector "AUAudioUnit" (class_id As Ptr) As Ptr
        Dim unit As Ptr = AUAudioUnit(output)
        
        // AUAudioUnitにChannelMapをセット
        Declare Sub setChannelMap Lib "Cocoa" selector "setChannelMap:" (class_id As Ptr, data As Ptr)
        setChannelMap(unit, Window1.pChannelMap)
        
      end if
      
      // エンジン実行
      Declare Sub startAndReturnError Lib "Cocoa" selector "startAndReturnError:" (class_id As Ptr, byRef err As Ptr)
      startAndReturnError(engine, error)
      
      // エンジンを返す
      return engine
      
    End Function
    
  3. Window1のpChannelMapプロパティの型を、Ptrに書き換える

 おわりに

 分かってしまえば、修正箇所は僅かなのですが、ここに至るまでは結構時間がかかりました。やれやれ。

 なお、SoundFlowerはApple silicon Macでは動作しないため、代替としてBlackHoleを使うのがトレンドのようですが、当方ではSoundFlowerが動作していることもあって、ここではBlackHoleによる検証は行なっていません。


 お世話になったサイト

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

 参考サイト(1):ios - The app's Info.plist must contain an NSMicrophoneUsageDescription key with a string value explaining to the user how the app uses this data - Stack Overflow
 参考サイト(2):swift - What is the difference between AUAudioUnit and AudioUnit - Stack Overflow


 更新履歴

 2025.05.15 新規作成


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