ホームページ>開発ツール>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)は複数入力に対応しているので、各シーケンサーをミキサーに繋げば出力を一本化できるかと思ったのですが、繋ぎ方がよく分からなかったため、楽器ごとにエンジン&シーケンサーを割り付けてそれらを同時にスタート/ストップするという、原始的な方法をとりました。
GAINとPANは、当初ミキサーの入力で設定しようとしたのですが、これではダメで、シーケンサーの出力に設定したらOKでした。MusicDeviceを複数生成してミキサーに繋いだエンジンを使ってシーケンサーを複数生成すると、全ての音が鳴ることは鳴るが、どうも一つのMusicDeviceに全シーケンサーが接続されてしまうエンジンを一つ(即ちミキサーも一つ)生成してチャンネル数分のMusicDeviceを接続し、このエンジンを用いてシーケンサーも複数生成すると、どうも最初に接続したMusicDeviceと最後に生成したシーケンサーが繋がるだけ(他のMusicDeviceは無効、残りのシーケンサーは直接出力)?のようで、個別に制御しようとすると各MusicDeviceと各シーケンサーを紐付ける必要がありそうなのだが、(できるかどうかも含めて)その方法(または別のアプローチ)が現時点では不明。
エフェクターのうち、ディレイは4つのパラメーターのうちwetDryMixとdelayTime、リバーブは全パラメータ(factoryPresetとwetDryMix)を変更可能とします。
レベルメーターは、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となってしまいます。
これをそのままリピート再生すると他の楽器とズレてくるので、整数化(切り上げ)することで対応します。(処理はソフトウェア側で行い、データは加工しません。)
以上を踏まえ、(残りの)仕様は以下の通りとしました。
- 各チャンネルに関わる部分は、1チャンネル分をコンテナコントロールに実装し、インスタンスはダイナミックに生成する。
- プレイと停止、マスターボリュームとレベルメータは、メインウィンドウに実装する。
- 数値の指定には、目盛付きのスライダーコントロールを用いる(XojoのSliderをCocoaのnumberOfTickMarksでカスタマイズ)。
- スライダーを操作すると、操作中はコンテナ下部に数値を表示する。
- (ドラムスのところでも触れたが、)リピート再生とする。
- MIDIデータは先頭のトラックのみを対象とする。
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等にペーストしても、うまくいかない場合があります。
・それでもペーストできない場合は、各項目のカッコ内を適用して下さい。
実行してみたところ、指定したMIDIファイルデータがシーケンサーによって同時演奏されることを確認しました。
- Xojoで新規プロジェクトを作成(64bitビルド)
- Window1に、BevelButton2個(Name:BevelButton1、Name:BevelButton2)、Canvas(Name:Canvas1)、Label(Name:Label1)、Separator(Name:Separator1)、Slider(Name:Slider1)、Timer(Name:Timer1)を置く
- 以下をBevelButton1にペースト(できなければ、Sub - Endの間をActionイベントに記述)
Sub Action() Handles Action PlayStart() End Sub
- 以下をBevelButton2にペースト(できなければ、Sub - Endの間をActionイベントに記述)
Sub Action() Handles Action PlayStop() // プレイボタンを戻す BevelButton1.Value=false End Sub
- 以下を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
- 以下を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
- 以下を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
- 以下をWindow1にペースト
Sub Open() Handles Open InitWin() End Sub
- 以下を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
- 以下を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
- 以下をWindow1にペースト
Protected Sub DrawIndicator(vL As Single, vR As Single) // 値を保持 pVolL=vL pVolR=vR // 再描画 Canvas1.Refresh End Sub
- 以下を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
- 以下をWindow1にペースト
Protected Sub PlayStart() // 全チャンネルをプレイ状態にセット for i As Integer = 0 to pChannel.Ubound pChannel(i).MusicPlay() next Timer1.Mode=2 End Sub
- 以下をWindow1にペースト
Protected Sub PlayStop() // 全チャンネルを停止状態にセット for i As Integer = 0 to pChannel.Ubound pChannel(i).MusicStop() next // 最小値を送った後にタイマー停止 DrawIndicator(-120,-120) Timer1.Mode=0 End Sub
- 以下をWindow1にペースト(できなければプロパティに、名前:pChannel(-1)、データ型:ContainerControl1、を追加)
Protected Property pChannel(-1) as ContainerControl1
- 以下をWindow1にペースト(できなければプロパティに、名前:pVolL、データ型:Single、標準値:-120.0、を追加)
Protected Property pVolL as Single = -120.0
- 以下をWindow1にペースト(できなければプロパティに、名前:pVolR、データ型:Single、標準値:-120.0、を追加)
Protected Property pVolR as Single = -120.0
- 新規ContainerControlを作成(名前は、ここではデフォルトの「ContainerControl1」とした。)
(注:IDEの右ペイン(ライブラリ)にあるContainerControlを、左ペイン(ナビゲーター)にドラッグ&ドロップする。)- 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を適宜追加。配置はスクリーンショットを参照)
- 以下を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
- 以下を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
- 以下を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
- 以下をSlider1にペースト(できなければ、Sub - Endの間をValueChangedイベントに記述)
Sub ValueChanged() Handles ValueChanged // 対応するスライダーの値をゲインにセット Dim v1 As Single=(me.Value)/100 SetGain(v1) // 値を表示 ShowValue(v1) End Sub
- 以下を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
- 以下を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
- 以下を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
- 以下を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
- 以下をTimer1にペースト(できなければ、Sub - Endの間をActionイベントに記述)
Sub Action() Handles Action // 値をクリア Label2.Text="---" End Sub
- 以下を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
- 以下を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
- 以下を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
- 以下を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
- 以下を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
- 以下を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
- 以下をContainerControl1にペースト
Public Sub MusicStop() // シーケンサー停止 Declare Sub stop Lib "Cocoa" selector "stop" (class_id As Ptr) stop(pSequencer) End Sub
- 以下を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
- 以下を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
- 以下を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
- 以下を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
- 以下を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
- 以下を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
- 以下を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
- 以下を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
- 以下を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
- 以下をContainerControl1にペースト
Protected Sub ShowValue(vv As Single) Timer1.Mode=0 // まず、タイマーを停止モードに Label2.Text=Str(vv) // 値をセット Timer1.Mode=1 // タイマー始動 End Sub
- 以下をContainerControl1にペースト(できなければプロパティに、名前:pSequencer、データ型:Ptr、を追加)
Protected Property pSequencer as Ptr
- 以下をContainerControl1にペースト(できなければプロパティに、名前:pReverb、データ型:Ptr、を追加)
Protected Property pReverb as Ptr
- 以下をContainerControl1にペースト(できなければプロパティに、名前:pDelay、データ型:Ptr、を追加)
Protected Property pDelay as Ptr
- 以下をContainerControl1にペースト(できなければプロパティに、名前:pMixer、データ型:Ptr、を追加)
Protected Property pMixer as Ptr
- 以下をContainerControl1にペースト(できなければプロパティに、名前:pInst、データ型:Ptr、を追加)
Protected Property pInst as Ptr
- 以下をContainerControl1にペースト
Protected Structure AudioComponentDescription componentType As UInt32 componentSubType As UInt32 componentManufacturer As UInt32 componentFlags As UInt32 componentFlagsMask As UInt32 End Structure
- 以下をContainerControl1にペースト
Protected Structure AVBeatRange startBeat As Double lengthInBeats As Double End Structure
- 以下をContainerControl1にペースト
(注:元はlong型のため、64bitビルドではInt64、32bitビルドではInt32にする。)Protected Structure ComponentInstanceRecord data As Int64 End Structure
- Sliderのカスタムクラスを作成(名前は、ここではデフォルトの「CustomSlider」とした。)
(注:IDEの右ペイン(ライブラリ)にあるSliderを、左ペイン(ナビゲーター)にドラッグ&ドロップする。)- CustomSliderにイベント定義を追加し、イベント名をOpenにする
- 以下を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
- ContainerControl1のSlider1〜5のSuperをCustomSliderに設定
おわりに
プレイ中に停止して再びプレイすると、止まったところから再開するのですが(これ自体はシステムの仕様)、どうも不安定です。
チャンネルが独立していることが原因である可能性が考えられるので、(本来あるべき?)一つのミキサーに全て繋ぐ方法を模索した方がいいかもしれません。
あと、(レベルメーターと同じアプローチになるが)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]