ホームページ>開発ツール>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は仮想デバイスで、入力にも出力にも現れるから?
また、ダイアログを表示するトリガーとして、入力ノードを取得すればいい(注:他にもあるかもしれないが未検証)ことも分かりました。
(クリックで拡大)
なので、以下の文を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では、上記対応だけではダイアログが表示されないことです。
ヒントは、別のテストプログラムが(似た状況で)異常終了した際に表示されたレポートにありました。
(クリックで拡大。注:ハイライトは当方で付与したもの。)
すなわち、info.plistにNSMicrophoneUsageDescriptionを追加しなければならない、ということです。
これを追加することで、2022r41でも入力ノードを呼び出した際にダイアログが表示され、許可するとChannelMapが拡張されました。
参考サイト(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等にペーストしても、うまくいかない場合があります。
・それでもペーストできない場合は、各項目のカッコ内を適用して下さい。
コードの実装は以上です。引き続き、ファイル関連の処理を実施。
- 前回プロジェクトをベースとする
- 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
- 以下の内容でテキストファイルを作成し、名前をInfo.plistとして保存
注:stringタグの中身がダイアログに表示される。(必要なら、ローカライズの手法を適用する等して下さい。)<?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>
- プロジェクトに、Info.plistをドラッグ&ドロップ
Xojoでの実装(AUAudioUnit版)
- 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
- 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
- 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]