NFluidsynth と MIDI シーケンサー Domino の連携 ― コード実装編

NFluidsynth ― C# のプロジェクトで LibFluidSynth の機能を利用する。

PC  :dynabook T552/58GKD
OS  :Windows 10(ver 1803)
IDE:Visual Studio 2017 Community

 

2. NFluidsynth と LibFluidSynth のソースコードを編集する

この章では、少しソースコードを編集する。
対象は NFluidsynth の二つのラッパークラスと FluidSynth ソリューションの中の libfluidsynth-OBJ プロジェクトの、いくつかのソースファイルだ。

実行プログラムは、NFluidsynth.Sample プロジェクトの  Sample.cs の中の Main 関数だ。これはラッパークラスのインスタンスを作成して、それを利用するプログラムだ。
Main 関数に実装されているのは、MIDI ファイル再生機能と単音の MIDI イベント発生・再生機能の二つだけなので、これを拡張して MIDI シーケンサー Domino と連携させようというのが、この章の主題になっている。

技術的に高度なことはやっていないので、C と C# の基本的な知識のある人ならば簡単に理解できるだろう。
それと、主要なコードウィンドウが 5 枚もあるのでスクロールが大変だが、サブメニューを使ってうまく読み進めて頂きたい。

スポンサーリンク

参考:

ラッパークラスの基本的知識を学びたい人は、下の参考ドキュメントを読むと良い。
私は学習を始めるにあたって、まずこれを読んだ。

Microsoft Docs:DLL 関数を保持するクラスの作成

以後の説明で使う、マネージ、アンマネージなどという用語も、このドキュメントに関連するドキュメントを読めば、大まかな意味はつかめるはずだ。

1)ソースコードの編集

編集対象のラッパークラスは下のテーブルのとおりだ。
これらに新しいコンストラクタを追加する。追加されるコンストラクタは、LibFluidSynth の 新しい API 関数を必要とするので、やはりそれらも追加する。テーブルには API 関数名しか挙げていないが、対象のソースファイル名はコード例の中で指定している。

wrapper class managed code unmanaged API function
MidiRouter コンストラクタ new_fluid_midi_router_for_csharp
MidiDriver コンストラクタ new_fluid_midi_driver_for_csharp
Program Main 関数

ソースコード編集対象

マネージ(NFluidsynth 側のこと)とアンマネージ (LibFluidSynth 側のこと)という用語は、開発言語の違い(C# と C)と、実行環境の違い(.NET Framework とネイティブ)を強調するのに便利なので、これ以降はこれらの用語を使うことにする。

FluidSynth のバージョンアップで削除された機能の復活について:

FluidSynth 1.1.10 には実装されていた MIDI イベントダンプ用の設定機能が、FluidSynth 2.0.1 では削除されていた。
私はそれを復活して使用しているが、イベントをダンプしないのであれば、あえて復活する必要はない。でもその機能の復活に必要な処理は、とりあえず記事の末尾の方にリストアップしておく。

LibFluidSynth のソースコードの編集:(return)

Visual Studio で FluidSynth.sln を開き、libfluidsynth-OBJ > Header Files フォルダ > midi.h を開く。
同じプロジェクトの Source Files フォルダ > fluid_midi_router.c と fluid_mdriver.c を開いて、下のコードウィンドウにある【Bonkure : For C# project 】というコメントがあるコードを、それぞれのファイルに追加する。

/*	ソースファイル midi.h に宣言を追加	*/
/*	Bonkure: For C# project	*/
FLUIDSYNTH_API fluid_midi_router_t *new_fluid_midi_router_for_csharp(void *settings, void *synth);
FLUIDSYNTH_API fluid_midi_driver_t *new_fluid_midi_driver_for_csharp(void *settings, void *router);


/*	ソースファイル fluid_midi_router.c に関数を追加	*/
/*	Bonkure: For C# project	*/
fluid_midi_router_t *new_fluid_midi_router_for_csharp(void *settings, void *synth)
{
    int dump = 0;
    fluid_settings_t *currentSettings = (fluid_settings_t *)settings;
    
	fluid_settings_getint(currentSettings, "synth.dump", &dump);

    fluid_midi_router_t *router =
    	new_fluid_midi_router(currentSettings, dump == 1 ? 
				fluid_midi_dump_postrouter : fluid_synth_handle_midi_event, synth);
    if (router == NULL) {
        return NULL;
    }
    else {
        return router;
    }
}


/*	ソースファイル fluid_mdriver.c に関数を追加	*/
/* Bonkure: For C# project */
fluid_midi_router_t *new_fluid_midi_router_for_csharp(void *settings, void *synth)
{
    int dump = 0;
    fluid_settings_t *currentSettings = (fluid_settings_t *)settings;
    
	fluid_settings_getint(currentSettings, "synth.dump", &dump);

    fluid_midi_router_t *router =
    new_fluid_midi_router(currentSettings, dump == 1 ? 
				fluid_midi_dump_postrouter : fluid_synth_handle_midi_event, synth);
    if (router == NULL) {
        return NULL;
    }
    else {
        return router;
    }
}

コードの追加が終わったら、ファイルを保存してからビルドを実行する。

  • LibFluidSynth のソースコードを編集した理由

loopMIDI のような MIDI デバイスを使う場合は、MIDI ルーターオブジェクトとMIDI ドライバーオブジェクトを作成する時に、それらのオブジェクトが使用するMIDI イベント処理関数を実行プログラム側で指定する必要がある。
それらの関数が LibFluidSynth 以外のモジュールに実装されているのならば、そうするのは当然だ。
おそらく LibFluidSynth の開発者は、他の MIDI イベント処理モジュールの作成者のためにスペースを空けたのだと思う。

しかし今回のテストのように、マネージコード側の実行プログラムがアンマネージ側の LibFluidSynth の MIDI イベント処理関数を使おうとするならば、【2)LibFluidSynth API 関数とコールバック】で挙げたような、少し奇妙に見える方法を採らざるを得ない

今は必要な MIDI イベント処理関数は  LibFluidSynth の中に全部そろっているのだから、その関数を実行プログラムから指定するのを止めて、初めから LibFluidSynth の中で指定する方が良い。その方が実行プログラムの作成が簡単になる、というのが私の考えだ。

MidiRouter クラスの編集:(return)

NFluidsynth コードパッケージを保存したフォルダを開き、NFluidsynth.sln ファイルをダブルクリックして Visual Studio ソリューションを開く。ソリューションエクスプローラーで NFluidsynth.Shared > NFlidsynth フォルダー を開き、編集対象のファイル MidiRouter.cs を開く。

Bonkure から始まるコメントのコードを、コード例のとおり追加する。using ステートメントは見逃しやすいので注意が必要だ。

using System;
using NFluidsynth.Native;
// Bonkure: using ディレクティブを追加
using System.Runtime.InteropServices;
using fluid_settings_t_ptr = System.IntPtr;
using fluid_midi_router_t_ptr = System.IntPtr;
using fluid_midi_event_t_ptr = System.IntPtr;

namespace NFluidsynth
{
	public delegate int MidiEventHandler (byte[] data, MidiEvent evt);

    public class MidiRouter : FluidsynthObject
	{
        // FLUIDSYNTH_API 関数をインポート
        [DllImport(LibFluidsynth.LibraryName, CallingConvention = CallingConvention.Cdecl)]
        internal static extern fluid_midi_router_t_ptr new_fluid_midi_router_for_csharp(
									                     fluid_settings_t_ptr settings,
									                     IntPtr event_handler_data);        

		// デフォルトコンストラクタ
		public MidiRouter ( 省略 ): base ( 省略 )
		{
		}      
        
        // Bonkure :デリゲートを渡さないコンストラクタを追加          
        public MidiRouter(Settings settings, Synth eventHandlerData)
        : base(new_fluid_midi_router_for_csharp(settings.Handle, eventHandlerData.Handle), true)
        {
        }

        protected override void OnDispose ()
		{
			LibFluidsynth.Midi.delete_fluid_midi_router (Handle);
		}

								以下省略

ファイルの編集が終わったら必ずファイルを保存する。

MidiDriver クラスの編集:(return)

MidiRouter.cs の場合と同じ手順で MidiDriver.cs を開き、コードを追加する。

using System;
using NFluidsynth.Native;
// Bonkure: using ディレクティブを追加
using System.Runtime.InteropServices;
using fluid_settings_t_ptr = System.IntPtr;
using fluid_midi_driver_t_ptr = System.IntPtr;


namespace NFluidsynth
{
	public class MidiDriver : FluidsynthObject
	{
        // Bonkure: FLUIDSYNTH_API 関数をインポート
        [DllImport(LibFluidsynth.LibraryName, 
        				CallingConvention = CallingConvention.Cdecl)]
        internal static extern fluid_midi_driver_t_ptr new_fluid_midi_driver_for_csharp(
						                    fluid_settings_t_ptr settings, 
						                    IntPtr event_handler_data);
        
        // デフォルトのコンストラクタ
        public MidiDriver ( 省略 ) :base ( 省略 )
        {
		}

        // Bonkure: デリゲートを渡さないコンストラクタを追加         
        public MidiDriver(Settings settings, MidiRouter handlerData)
           : base(new_fluid_midi_driver_for_csharp(settings.Handle, handlerData.Handle), true) 
		{
        }
        
        						以下省略
	}
}

ファイルの編集が終わったら必ずファイルを保存する。

Program クラスの編集:(return)

これは実行プログラム Main 関数の存在するクラスだ。

Main 関数には一部改編がある。
下のコード例の if (files.Any()) 句より上のコードの一部がそうだ。else  句以下は、デフォルトのコードを削除して、Domino と連携するためのコードを挿入している。この実装では設定ファイルを使っていないので、本格的な応用例にはなっていない。

LoadSoundFont  関数の引数は、各位の環境に合った内容に変更する。
パスの文字列リテラルの中に空白文字(スペース)が含まれる場合、文字列に付加されている  @ マークは省略できない。また、@ マークを使わない場合、バックスラッシュ(日本語環境では “¥” )は “\\” というように 2 文字重ねる必要がある。

public static void Main(string[] args)
{
    using (Settings settings = new Settings(), settings2 = new Settings())
    {
        settings[ConfigurationKeys.SynthAudioChannels].IntValue = 2;
        settings2[ConfigurationKeys.SynthAudioChannels].IntValue = 2;

		// set Midi event dump mode
		settings.DumpMidiEvent = false;
		settings2.DumpMidiEvent = false;
		//settings[ConfigurationKeys.SynthDump].IntValue = 1;
		//settings2[ConfigurationKeys.SynthDump].IntValue = 0;
 
        using (Synth syn = new Synth(settings), syn2 = new Synth(settings2))
        {
            
			省略
            
            if (files.Any())
            {
                
				省略
 
 
            } else {                
                settings[ConfigurationKeys.MidiWinDevice].StringValue = "loopMIDI Port 1";
                syn.LoadSoundFont(@"D:\sf2\A320U.sf2", true);
 
                settings2[ConfigurationKeys.MidiWinDevice].StringValue = "loopMIDI Port 2";
                syn2.LoadSoundFont(@"D:\sf2\KDrum.sf2", true);
 
                //	それぞれ異なるサウンドフォントから異なるプログラムを指定する。 
                syn.ProgramSelect(0, 1, 0, 18);	//	Domino Channel 1
                syn.ProgramSelect(2, 2, 0, 37);	//	Domino Channel 3
                //	2 番目のシンセサイザーにドラムサウンドフォントのプログラムを指定する。
                syn2.ProgramSelect(9, 1, 128, 32);	//	Domino Channel 10
 
                using (MidiRouter router = new MidiRouter(settings, syn),
                                router2 = new MidiRouter(settings2, syn2))
                {
                    using (MidiDriver mdriver = new MidiDriver(settings, router),
                        mdriver2 = new MidiDriver(settings2, router2))
                    using (AudioDriver adriver = new AudioDriver(settings, syn),
                            adriver2 = new AudioDriver(settings2, syn2))
                    {
                        for (; ; )
                        {
                            if (Console.ReadKey(true).Key.ToString() == "Q") { break; }
                        }
                    }
                }
            }
        }
    }
}

ファイルの編集が終わったら必ずファイルを保存する。

2)LibFluidSynth API 関数とコールバック(return)

このセクションでは、参考情報として 異なるプラットフォーム間、具体的には LibFluidSynth と NFluidsynth の間の関数コールバックの実例を紹介する。NFluidsynth 側のコールバック関数として LibFluidSynth API 関数を指定するので、MIDI イベントに対するレスポンスが【1)ソースコードの編集】で例示した方法に劣ることはないと思う。

LibFluidSynth のソースコードを変更せずに NFluidsynth のラッパークラスを利用するには、ここで例示している仕様を実装するのが、おそらく一番簡単だ。コード例は【コールバック関数の使用例】の項にあるので参考にして頂きたい。

MIDI イベント処理の関数チェーン:
  • LibFluidSynth 内部の関数チェーン

Domino から、loopMIDI のような MIDI デバイス経由で MIDI データを受信する場合、 LibFluidSynth の内部では 1 個の MIDI イベントごとに下のフローに示す順番で関数呼び出しが発生する。

MIDI イベントコールバック関数 > router 関数 > synthesizer  関数 >…> Audio

MIDI イベントコールバック関数は、Windowsマルチメディア 開発の mmeapi .h  の中で宣言されている midiInOpen 関数に渡される関数だ。Windows では fluid_winmidi_callback  が渡される。このコールバックは異なるプラットフォーム間のコールバックではない。router 関数と synthesizer 関数は実行プログラム側から指定する

  • LibFluidSynth + NFluidsynth の関数チェーン

上のフローを少しアレンジしてみよう。
黒色表記は LibFuidSynth 側の関数で、赤色表記が 実行プログラム側から指定される NFluidsynth 側の関数だ。

fluid_winmidi_callback > dump ?  MidiRouter 1 : MidiRouter 2 >dump ? Synthesizer1 : Synthesizer2 >…> Audio

赤色表記の関数が、アンマネージ側から呼び出されるマネージコード側のコールバック関数だ。
コールバック関数には、
マネージコード側で利用可能な関数ならば、シグネチャーさえ合っていればどのような関数でも指定できる。関数本体が実装されているモジュールは何であっても構わない。

今は、 Domino から受け取った MIDI データを音響データに変換して再生するという明確な目標があるので、MidiRouter 関数と Synthesizer 関数として LibFluidSynth の router 関数と synthesizer 関数を使う。

この二つの関数は両方ともマネージコード側にインポートされているが、実行環境自体はアンマネージ側だ。しかし MidiRouter 関数からの Synthesizer 関数に対する呼び出しが関数ポインタによる呼び出しなので、少し奇妙に見えるかも知れないが、これはやはり異なるプラットフォーム間のコールバックになる。

コールバック関数の使用例:
  • API 関数のインポートとコールバック

下のコードウィンドウに、実際にテストした実装例を示す。コールバック関数の指定は実行プログラムの Main 関数ではなく、各クラスの中で行っている。

マネージコード側コールバック関数のポインタをアンマネージ側に渡すには、デリゲート定義を使用する。
デリゲートとコンストラクタの実装方法はコード例で確認して頂きたい。
デリゲート型変数は必ずしも必要ではないが、変数を使った方がコードが分かりやすくなる。

  • デリゲートの属性指定

デリゲート定義の属性指定は重要だ。
もしこれを指定しなければ、MidiRouter >Synthesizer の関数ポインタ呼び出しの箇所で、必ず呼び出し規約違反の例外が発生する。

[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public delegate int LibMidiEventHandler(IntPtr data, fluid_midi_event_t_ptr evt);

参考リンクの中の Microsoft Doc の方にはガベージコレクション(GC)についての注意事項が載っているが、今回のテストでは、プログラムの実行中に GC の回収対象になるようなクラスインスタンスは一つも発生しないので、その配慮は不要だ。

  • コード例
  • (1)11~12 行目はデリゲートの定義。
  • (2)19 行目の第 2 引数がデリゲート。
  • (3)24~35 行目がコールバック関数。
  • (4)38 行目以下が実装例になっている。
using System;
using NFluidsynth.Native;
// Bonkure
using System.Runtime.InteropServices;
using fluid_settings_t_ptr = System.IntPtr;
using fluid_midi_event_t_ptr = System.IntPtr;
using fluid_midi_driver_t_ptr = System.IntPtr;

// namespace 内で定義
// Test: delegate
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public delegate int LibMidiEventHandler(IntPtr data, fluid_midi_event_t_ptr evt);

// MidiRouter クラス内で定義
// Test: use in test constructor
[DllImport(LibFluidsynth.LibraryName, CallingConvention = CallingConvention.Cdecl)]
internal static extern fluid_midi_router_t_ptr new_fluid_midi_router(
                              fluid_settings_t_ptr settings, 
                              LibMidiEventHandler handler, 
                              IntPtr event_handler_data);

// Test: signature matches test delegate LibMidiEventHandler
// Callback from [fluid_winmidi_callback] in unmanaged dll
[DllImport(LibFluidsynth.LibraryName, CallingConvention = CallingConvention.Cdecl)]
[return: MarshalAs(UnmanagedType.I4)] internal static extern int fluid_synth_handle_midi_event(
                              IntPtr data, 
                              fluid_midi_event_t_ptr evt);

// Test: signature matches test delegate LibMidiEventHandler
// Callback from [fluid_winmidi_callback] in unmanaged code dll to dump MIDI events 
[DllImport(LibFluidsynth.LibraryName, CallingConvention = CallingConvention.Cdecl)]
[return: MarshalAs(UnmanagedType.I4)]
internal static extern int fluid_midi_dump_postrouter(
		                      IntPtr data,
		                      fluid_midi_event_t_ptr evt);

// Callback をデリゲートに登録
internal static LibMidiEventHandler rhandler = fluid_synth_handle_midi_event;
internal static LibMidiEventHandler rdumphandler = fluid_midi_dump_postrouter;


// Test: Constructor      
public MidiRouter(Settings settings, Synth eventHandlerData)
            : base(new_fluid_midi_router(
							settings.Handle,
                			settings[ConfigurationKeys.SynthDump].IntValue==1 ? 
							rdumphandler : rhandler, 
							eventHandlerData.Handle), true)
        {
        }

protected override void OnDispose ()
{
    rhandler = null;
    rdumphandler = null;
    LibFluidsynth.Midi.delete_fluid_midi_router (Handle);
}


// MidiDriver クラス内で定義

// Test: use in test constructor
[DllImport(LibFluidsynth.LibraryName, CallingConvention = CallingConvention.Cdecl)]
internal static extern fluid_midi_driver_t_ptr new_fluid_midi_driver(
							fluid_settings_t_ptr settings,
							LibMidiEventHandler handler, 
							IntPtr event_handler_data);

// Test: signature matches test delegate LibMidiEventHandler
[DllImport(LibFluidsynth.LibraryName, CallingConvention = CallingConvention.Cdecl)]
[return: MarshalAs(UnmanagedType.I4)] 
internal static extern int fluid_midi_router_handle_midi_event(
							IntPtr data, fluid_midi_event_t_ptr evt);

// Test: signature matches test delegate LibMidiEventHandler
[DllImport(LibFluidsynth.LibraryName, CallingConvention = CallingConvention.Cdecl)]
[return: MarshalAs(UnmanagedType.I4)] 
internal static extern int fluid_midi_dump_prerouter(
							IntPtr data, fluid_midi_event_t_ptr evt);


internal static LibMidiEventHandler mdhandler = fluid_midi_router_handle_midi_event;
internal static LibMidiEventHandler mdumpdhandler = fluid_midi_dump_prerouter;

// Test: Constructor

public MidiDriver(Settings settings, MidiRouter handlerData)
: base(new_fluid_midi_driver(
							settings.Handle,
                			settings[ConfigurationKeys.SynthDump].IntValue == 1 ? 
							mdumpdhandler :  mdhandler, 
							handlerData.Handle), true)
        {
        }

protected override void OnDispose ()
{
    mdhandler = null;
    mdumpdhandler = null;
    LibFluidsynth.Midi.delete_fluid_midi_driver (Handle);
}

このテストでは、MidiRouter クラスと MidiDriver クラスのデフォルトのコンストラクタは使わなかった。デフォルトのデリゲート定義の使い方を、私が理解できなかったからだ。

NFluidsynth の作成者である atsushieno 氏の興味の中心は、主にモバイルデバイスの方にあるようなので、もしかしたらそちらの実装に適した仕様なのかも知れない。何にしても、私には難しすぎた。

参考リンク:

Microsoft Doc:コールバック メソッドとしてのデリゲートのマーシャ リング
aharisuのごみ箱:アンマネージコードに C# のデリゲートを渡す

コールバック関数の呼び出し規約の所でかなり悩んだが、2 番目の記事のおかげで解決できた。オーナーの aharisu さんとコメント投稿者 shima さんには、この場を借りてお礼を申し上げたい。

FluidSynth のバージョンアップで削除された機能の復活:(return)

FluidSynth のバージョンアップで、MIDI イベントダンプモードの設定機能が削除されたことと、私が v1.1.10 の設定機能を v2.0.1 の中に復活して使用していることは、この章の最初の方で言った通りだ。
下のテーブルにその内容を挙げるので、参考にして頂きたい。

ConfigurationKeys.cs は NFluidsynth.Shared プロジェクト> generator フォルダの中にある。他のファイルは libfluidsyht-OBJ プロジェクト > Header Files か Source Files フォルダの中だ。

ソースファイル 追加コード
ConfigurationKeys.cs 24 public const string SynthDump = “synth.dump”;
fluid_synth.h 114 int dump;
fluid_synth.c 199 fluid_settings_register_int(settings, “synth.dump”, 0, 0, 1, FLUID_HINT_TOGGLED);
同上 637 fluid_settings_getint(settings, “synth.dump”, &synth->dump);

これらを復活したのは、一つのプロセスの中で 16 個以下の Synthesizer オブジェクトを操作するようなテストを考えているからだ。そのためには、設定を個々のオブジェクト内部に保存する仕組みがある方が、何かと都合が良い。うまく行くかどうかは分からないけど・・・。

おわりに:

私が LibFluidSynth の機能を追及してみようと思ったのは、FluidSynth のソースコードを数か月間黙ってながめ続けた後のことだ。最初はプログラミングを再開するつもりはなかったが、NFluidsynth を見つけたことで、少しやる気が出てきた。

しかし、何よりも強く興味をひかれたのは、FluidSynth を Domino と連携させた時の音質の良さだ。

Domino と Reaper v0.999 や Cubase LE5 などの DAW を連携させる場合は、オーディオドライバーとして低レイテンシーの ASIO を使わざるを得ない。そうしなければ、ドラムスのビートを正確に再生することできないのだ。だが FluidSynth では、いわゆるダイレクトサウンドで再生しているにも関わらず、ビート落ちが発生しないのだ。

これまでのテストでは、FluidSynth のプロセスを二つ走らせるケースと、一つのプロセスの中で二つの独立したシンセサイザーのインスタンスを走らせるケースは確認できた。この次は、そのインスタンスを大幅に増やした場合、音質がどうなるのかを確認したいと考えている。

スポンサーリンク

スポンサーリンク

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です