Unity で C# スクリプト：FluidSynth (ネイティブプラグイン)で MIDI を再生：Android ビルド

# Unity で C# スクリプト：FluidSynth (ネイティブプラグイン)で MIDI を再生：Android ビルド

こんにちは、@studio_meowtoon です。今回は Windows 11 の Unity アプリ開発環境にて、C# から FluidSynth をネイティブライブラリとして使用する方法を紹介します。
![csharp_in_unity.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/0a784c1a-f580-0d22-cafc-93c450f28461.png)

## 実現すること

**Unity で実装**する **Android アプリ**に、**FluidSynth ソフトウェアシンセサイザー**をビルドインします。

:::note alert
C# には本来[厳格なコーディング規則](https://learn.microsoft.com/ja-jp/dotnet/csharp/fundamentals/coding-style/coding-conventions)がありますが、この記事では可読性のために、一部規則に沿わない表記方法を使用しています。ご注意ください。
:::

## 開発環境

+ Windows 11 Home 22H2
+ Unity 2021.3.24f1 LTS

## 技術トピック

### [FluidSynth](https://ja.wikipedia.org/wiki/FluidSynth) とは？  

<details>
<summary>こちらを展開してご覧いただけます。</summary>

:::note info
FluidSynth は、ソフトウェアベースの音声合成エンジンです。MIDI データを入力として受け取り、それを実際の音楽や音響イベントに変換します。FluidSynth は、MIDI ファイルを再生するためのツールや、仮想楽器の音色を再現するためのプログラムとして使用されます。また、FluidSynth は、様々なプラットフォーム (Windows、Mac、Linux など) で動作し、さまざまな音声合成の要件に応える柔軟性を持っています。
:::

:::note alert
FluidSynth は LGPL ライセンスで提供されています。今回の記事の例のように FluidSynth をダイナミックリンクライブラリとして利用する場合、利用するソフトウェアは同じ LGPL を適用しなくても構いません。つまり FluidSynth を実行時に動的リンクしてその機能を呼び出す場合、利用する側のソフトウェアのソースコードの公開は必須ではありません。
:::

</details>

## Unity で FluidSynth を再生する手順

### 前提として

:::note warn
**Android モバイルアプリ**を **Windows 11 で開発**するため、両方の環境で動作するアプリを実装します。
:::

### ライブラリのダウンロード

https://github.com/FluidSynth/fluidsynth/releases

|ライブラリ|対応するOS|用途|
|:--|:--|:--|
|[fluidsynth-2.3.3-win10-x64.zip](https://github.com/FluidSynth/fluidsynth/releases/download/v2.3.3/fluidsynth-2.3.3-win10-x64.zip)|Windows|開発機材で使用します。|
|[fluidsynth-2.3.2-android24.zip](https://github.com/FluidSynth/fluidsynth/releases/download/v2.3.2/fluidsynth-2.3.2-android24.zip)|Android|ビルドする実機で使用します。|

### プロジェクトの作成 

ローカルの Unity Hub を起動してシンプルな 2D プロジェクトを作成します。  

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/de21d6c5-5c5b-dd5d-722d-000170a0d227.png)

### ネイティブライブラリの配置

共有ライブラリのファイル形式

|拡張子|対応するOS|概要|
|:--|:--|:--|
|.dll|Windows|ダイナミックリンクライブラリ Windows 系|
|.so|Linux (※含む Android)|ダイナミックリンクライブラリ UNIX 系|

**Assets フォルダの配下**に以下のようにネイティブライブラリを配置しました。

```sh
├── Plugins
│   ├── FluidSynth
│   │   ├── Win10-x64
│   │   │   ├── libfluidsynth-2.dll
│   │   │   ├── libgcc_s_sjlj-1.dll
│   │   │   ├── libglib-2.0-0.dll
│   │   │   ├── libgobject-2.0-0.dll
│   │   │   ├── libgomp-1.dll
│   │   │   ├── libgthread-2.0-0.dll
│   │   │   ├── libinstpatch-2.dll
│   │   │   ├── libintl-8.dll
│   │   │   ├── libsndfile-1.dll
│   │   │   ├── libstdc++-6.dll
│   │   │   └── libwinpthread-1.dll
│   │   └── armeabi-v7a
│   │       ├── libFLAC.so
│   │       ├── libfluidsynth-assetloader.so
│   │       ├── libfluidsynth.so
│   │       ├── libgio-2.0.so
│   │       ├── libglib-2.0.so
│   │       ├── libgmodule-2.0.so
│   │       ├── libgobject-2.0.so
│   │       ├── libgthread-2.0.so
│   │       ├── libinstpatch-1.0.so
│   │       ├── liboboe.so
│   │       ├── libogg.so
│   │       ├── libopus.so
│   │       ├── libpcre.so
│   │       ├── libpcreposix.so
│   │       ├── libsndfile.so
│   │       ├── libvorbis.so
│   │       ├── libvorbisenc.so
│   │       └── libvorbisfile.so
```

**Windows** ネイティブプラグインの設定例
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/d9a4b689-73af-83dd-f43b-87008849e4f6.png)

**Android** ネイティブプラグインの設定例
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/3aa8fd63-170f-9f18-49da-61d1e6e06321.png)

### アンマネージド DLL 関数の作成

FluidSynth ネイティブプラグインを C# (Unity) から呼び出すために、**アンマネージド DLL 関数**として Fluidsynth.cs ファイルを作成します。

<details>
<summary>コードの全体を表示する</summary>

```csharp:Assets\Scripts\Fluidsynth.cs
using System.Runtime.InteropServices;
using void_ptr = System.IntPtr;
using fluid_settings_t = System.IntPtr;
using fluid_synth_t = System.IntPtr;
using fluid_audio_driver_t = System.IntPtr;
using fluid_player_t = System.IntPtr;
using fluid_midi_event_t = System.IntPtr;

namespace GameDev {
    /// <summary>
    /// class for Fluidsynth API definitions.
    /// </summary>
    internal static class Fluidsynth {
#nullable enable

#if UNITY_ANDROID
        const string LIBLARY = "libfluidsynth.so";
#elif UNITY_STANDALONE_WIN
        const string LIBLARY = "libfluidsynth-2.dll";
#endif
        const UnmanagedType LP_Str = UnmanagedType.LPStr;
        internal const int FLUID_OK = 0;
        internal const int FLUID_FAILED = -1;
        [DllImport(LIBLARY)] internal static extern fluid_settings_t new_fluid_settings();
        [DllImport(LIBLARY)] internal static extern void delete_fluid_settings(fluid_settings_t settings);
        [DllImport(LIBLARY)] internal static extern fluid_synth_t new_fluid_synth(fluid_settings_t settings);
        [DllImport(LIBLARY)] internal static extern void delete_fluid_synth(fluid_synth_t synth);
        [DllImport(LIBLARY)] internal static extern fluid_audio_driver_t new_fluid_audio_driver(fluid_settings_t settings, fluid_synth_t synth);
        [DllImport(LIBLARY)] internal static extern void delete_fluid_audio_driver(fluid_audio_driver_t driver);
        [DllImport(LIBLARY)] internal static extern int fluid_synth_sfload(fluid_synth_t synth, [MarshalAs(LP_Str)] string filename, bool reset_presets);
        [DllImport(LIBLARY)] internal static extern int fluid_is_soundfont([MarshalAs(LP_Str)] string filename);
        [DllImport(LIBLARY)] internal static extern int fluid_synth_noteon(fluid_synth_t synth, int chan, int key, int vel);
        [DllImport(LIBLARY)] internal static extern int fluid_synth_noteoff(fluid_synth_t synth, int chan, int key);
        [DllImport(LIBLARY)] internal static extern void fluid_synth_set_gain(fluid_synth_t synth, float gain);
        [DllImport(LIBLARY)] internal static extern fluid_player_t new_fluid_player(fluid_synth_t synth);
        [DllImport(LIBLARY)] internal static extern int delete_fluid_player(fluid_player_t player);
        [DllImport(LIBLARY)] internal static extern int fluid_player_add(fluid_player_t player, [MarshalAs(LP_Str)] string midifile);
        [DllImport(LIBLARY)] internal static extern int fluid_is_midifile([MarshalAs(LP_Str)] string filename);
        [DllImport(LIBLARY)] internal static extern int fluid_player_play(fluid_player_t player);
        [DllImport(LIBLARY)] internal static extern int fluid_player_join(fluid_player_t player);
        [DllImport(LIBLARY)] internal static extern int fluid_player_stop(fluid_player_t player);
        internal delegate int handle_midi_event_func_t(void_ptr data, fluid_midi_event_t evt);
        [DllImport(LIBLARY)] internal static extern int fluid_player_set_playback_callback(fluid_player_t player, handle_midi_event_func_t handler, void_ptr handler_data);
        [DllImport(LIBLARY)] internal static extern int fluid_synth_handle_midi_event(void_ptr data, fluid_midi_event_t evt);
        [DllImport(LIBLARY)] internal static extern int fluid_synth_program_change(fluid_synth_t synth, int chan, int program);
        [DllImport(LIBLARY)] internal static extern int fluid_synth_cc(fluid_synth_t synth, int chan, int ctrl, int val);
        [DllImport(LIBLARY)] internal static extern int fluid_midi_event_get_type(fluid_midi_event_t evt);
        [DllImport(LIBLARY)] internal static extern int fluid_midi_event_get_channel(fluid_midi_event_t evt);
        [DllImport(LIBLARY)] internal static extern int fluid_midi_event_get_key(fluid_midi_event_t evt);
        [DllImport(LIBLARY)] internal static extern int fluid_midi_event_get_velocity(fluid_midi_event_t evt);
        [DllImport(LIBLARY)] internal static extern int fluid_midi_event_get_control(fluid_midi_event_t evt);
        [DllImport(LIBLARY)] internal static extern int fluid_midi_event_get_value(fluid_midi_event_t evt);
        [DllImport(LIBLARY)] internal static extern int fluid_midi_event_get_program(fluid_midi_event_t evt);
    }
}
```

</details>

以下、ポイントを説明していきます。

```csharp:Assets\Scripts\Fluidsynth.cs ※抜粋
#if UNITY_ANDROID
    const string LIBLARY = "libfluidsynth.so";
#elif UNITY_STANDALONE_WIN
    const string LIBLARY = "libfluidsynth-2.dll";
#endif
```

:::note info
**Unity** の**プラットフォーム依存コンパイル**機能により、開発機の Windows とビルド先の実機 Android で参照するネイティブライブラリを切り替えます。
:::

```csharp:Assets\Scripts\Fluidsynth.cs ※抜粋
[DllImport(LIBLARY)] internal static extern fluid_settings_t new_fluid_settings();
[DllImport(LIBLARY)] internal static extern void delete_fluid_settings(fluid_settings_t settings);
[DllImport(LIBLARY)] internal static extern fluid_synth_t new_fluid_synth(fluid_settings_t settings);
[DllImport(LIBLARY)] internal static extern void delete_fluid_synth(fluid_synth_t synth);
```

:::note info
C# (Unity) のマネージド コードから C++ で実装されたライブラリ関数を呼び出すには、呼び出す関数ごとにマネージド プロトタイプを実装します。 
:::

:::note info
ここまでの手順で、FluidSynth ( ネイティブプラグイン)を C# (Unity) から利用する準備が出来ました😋
:::

### Synth クラスの実装

MIDI データを再生するシンセサイザークラスを作成します。

<details>
<summary>コードの全体を表示する</summary>

```csharp:Assets\Scripts\Synth.cs
using System;
using System.ComponentModel;

using static GameDev.Fluidsynth;
using void_ptr = System.IntPtr;
using fluid_settings_t = System.IntPtr;
using fluid_synth_t = System.IntPtr;
using fluid_audio_driver_t = System.IntPtr;
using fluid_player_t = System.IntPtr;
using fluid_midi_event_t = System.IntPtr;

namespace GameDev {
    /// <summary>
    /// the synth class.
    /// </summary>
    public class Synth {
#nullable enable

        // Const [nouns]
        const float SYNTH_GAIN = 0.5f;
        const int NOTE_ON = 144;
        const int NOTE_OFF = 128;

        // static Fields [nouns, noun phrases]
        static fluid_settings_t _setting = IntPtr.Zero;
        static fluid_synth_t _synth = IntPtr.Zero;
        static fluid_player_t _player = IntPtr.Zero;
        static fluid_audio_driver_t _adriver = IntPtr.Zero;
        static handle_midi_event_func_t? _event_callback;
        static Func<IntPtr, IntPtr, int>? _on_playbacking;
        static Action? _on_started;
        static Action? _on_ended;
        static PropertyChangedEventHandler? _on_updated;
        static string _sound_font_path = string.Empty;
        static string _midi_file_path = string.Empty;
        static bool _ready = false;
        static bool _stopping = false;

        // static Constructor
        static Synth() {
            _event_callback += (void_ptr data, fluid_midi_event_t evt) => { return HandleEvent(data, evt); };
            _on_playbacking += (void_ptr data, fluid_midi_event_t evt) => {
                int type = fluid_midi_event_get_type(evt);
                int channel = fluid_midi_event_get_channel(evt);
                int control = fluid_midi_event_get_control(evt);
                int value = fluid_midi_event_get_value(evt);
                int program = fluid_midi_event_get_program(evt);
                if (type != NOTE_ON && type != NOTE_OFF) { // not note on or note off
                    //Log.Debug($"type: {type} channel: {channel} control: {control} value: {value} program: {program}");
                }
                return fluid_synth_handle_midi_event(data, evt);
            };
        }

        // static Properties [noun, noun phrase, adjective] 
        public static string SoundFontPath {
            get => _sound_font_path;
            set {
                _sound_font_path = value;
                Log.Info("Synth set soundFontPath.");
            }
        }
        public static string MidiFilePath {
            get => _midi_file_path;
            set {
                _midi_file_path = value;
                Log.Info("Synth set midiFilePath.");
            }
        }
        public static bool Playing {
            get => _ready;
        }

        // static Events [verb, verb phrase] 
        public static event Func<IntPtr, IntPtr, int> Playbacking {
            add {
                _on_playbacking += value;
                _event_callback = new handle_midi_event_func_t(_on_playbacking);
            }
            remove => _on_playbacking -= value;
        }
        public static event Action Started {
            add => _on_started += value;
            remove => _on_started -= value;
        }
        public static event Action Ended {
            add => _on_ended += value;
            remove => _on_ended -= value;
        }
        public static event PropertyChangedEventHandler Updated {
            add => _on_updated += value;
            remove => _on_updated -= value;
        }

        // public static Methods [verb, verb phrases]
        public static void Init() {
            try {
                if (!SoundFontPath.HasValue() || !MidiFilePath.HasValue()) {
                    Log.Warn("no sound font or no midi file.");
                    return;
                }
                _setting = new_fluid_settings();
                _synth = new_fluid_synth(_setting);
                fluid_synth_set_gain(_synth, SYNTH_GAIN);
                _player = new_fluid_player(_synth);
                Log.Info($"try to load the sound font: {SoundFontPath}");
                if (fluid_is_soundfont(SoundFontPath) != 1) {
                    Log.Error("not a sound font.");
                    return;
                }
                if (_event_callback is not null) {
                    fluid_player_set_playback_callback(_player, _event_callback, _synth);
                }
                int sfont_id = fluid_synth_sfload(_synth, SoundFontPath, true);
                if (sfont_id == FLUID_FAILED) {
                    Log.Error("failed to load the sound font.");
                    return;
                } else {
                    Log.Info($"loaded the sound font: {SoundFontPath}");
                }
                Log.Info($"try to load the midi file: {MidiFilePath}");
                if (fluid_is_midifile(MidiFilePath) != 1) {
                    Log.Error("not a midi file.");
                    return;
                }
                int result = fluid_player_add(_player, MidiFilePath);
                if (result == FLUID_FAILED) {
                    Log.Error("failed to load the midi file.");
                    return;
                } else {
                    Log.Info($"loaded the midi file: {MidiFilePath}");
                }
                _adriver = new_fluid_audio_driver(_setting, _synth);
                _ready = true;
                Log.Info("init :)");
            } catch (Exception ex) {
                Log.Error(ex.Message);
            }
        }
        public static void Start() {
            try {
                if (!_ready) {
                    Init();
                    if (!_ready) {
                        Log.Error("failed to init.");
                        return;
                    }
                }
                fluid_player_play(_player);
                Log.Info("start :)");
                _on_started?.Invoke();
                fluid_player_join(_player);
                Log.Info("end :D");
                if (_stopping == false) {
                    _on_ended?.Invoke();
                }
            } catch (Exception ex) {
                Log.Error(ex.Message);
            }
        }
        public static void Stop() {
            try {
                if (!_player.IsZero()) {
                    _stopping = true;
                    fluid_player_stop(_player);
                }
                final();
                Log.Info("stop :|");
                GC.Collect();
                Log.Info("GC.Collect.");
            } catch (Exception ex) {
                Log.Error(ex.Message);
            }
        }
        public static int HandleEvent(IntPtr data, IntPtr evt) {
            return fluid_synth_handle_midi_event(data, evt);
        }

        // private static Methods [verb, verb phrases]
        static void final() {
            try {
                delete_fluid_audio_driver(_adriver);
                delete_fluid_player(_player);
                delete_fluid_synth(_synth);
                delete_fluid_settings(_setting);
                _adriver = IntPtr.Zero;
                _player = IntPtr.Zero;
                _synth = IntPtr.Zero;
                _setting = IntPtr.Zero;
                Log.Info("final :|");
            } catch (Exception ex) {
                Log.Error(ex.Message);
            } finally {
                _ready = false;
                _stopping = false;
            }
        }
        static void onPropertyChanged(object sender, PropertyChangedEventArgs e) {
            _on_updated?.Invoke(sender, e);
        }
    }
}
```

</details>
 
以下、ポイントを説明していきます。

```csharp:Assets\Scripts\Synth.cs ※抜粋
public static void Init() {
    try {
        if (!SoundFontPath.HasValue() || !MidiFilePath.HasValue()) {
            Log.Warn("no sound font or no midi file.");
            return;
        }
        _setting = new_fluid_settings();
        _synth = new_fluid_synth(_setting);
        fluid_synth_set_gain(_synth, SYNTH_GAIN);
        _player = new_fluid_player(_synth);
        Log.Info($"try to load the sound font: {SoundFontPath}");
        if (fluid_is_soundfont(SoundFontPath) != 1) {
            Log.Error("not a sound font.");
            return;
        }
        if (_event_callback is not null) {
            fluid_player_set_playback_callback(_player, _event_callback, _synth);
        }
        int sfont_id = fluid_synth_sfload(_synth, SoundFontPath, true);
        if (sfont_id == FLUID_FAILED) {
            Log.Error("failed to load the sound font.");
            return;
        } else {
            Log.Info($"loaded the sound font: {SoundFontPath}");
        }
        Log.Info($"try to load the midi file: {MidiFilePath}");
        if (fluid_is_midifile(MidiFilePath) != 1) {
            Log.Error("not a midi file.");
            return;
        }
        int result = fluid_player_add(_player, MidiFilePath);
        if (result == FLUID_FAILED) {
            Log.Error("failed to load the midi file.");
            return;
        } else {
            Log.Info($"loaded the midi file: {MidiFilePath}");
        }
        _adriver = new_fluid_audio_driver(_setting, _synth);
        _ready = true;
        Log.Info("init :)");
    } catch (Exception ex) {
        Log.Error(ex.Message);
    }
}
```

:::note info
FluidSynth ソフトウェアシンセサイザーを初期化し、サウンドフォントと MIDI ファイルをロードして再生するための処理を行っています。
:::

```csharp:Assets\Scripts\Synth.cs ※抜粋
public static void Start() {
    try {
        if (!_ready) {
            Init();
            if (!_ready) {
                Log.Error("failed to init.");
                return;
            }
        }
        fluid_player_play(_player);
        Log.Info("start :)");
        _on_started?.Invoke();
        fluid_player_join(_player);
        Log.Info("end :D");
        if (_stopping == false) {
            _on_ended?.Invoke();
        }
    } catch (Exception ex) {
        Log.Error(ex.Message);
    }
}
public static void Stop() {
    try {
        if (!_player.IsZero()) {
            _stopping = true;
            fluid_player_stop(_player);
        }
        final();
        Log.Info("stop :|");
        GC.Collect();
        Log.Info("GC.Collect.");
    } catch (Exception ex) {
        Log.Error(ex.Message);
    }
}
```

:::note info
再生を開始するための Start メソッドと、再生を停止するための Stop メソッドを定義しています。
:::

```csharp:Assets\Scripts\Synth.cs ※抜粋
static void final() {
    try {
        delete_fluid_audio_driver(_adriver);
        delete_fluid_player(_player);
        delete_fluid_synth(_synth);
        delete_fluid_settings(_setting);
        _adriver = IntPtr.Zero;
        _player = IntPtr.Zero;
        _synth = IntPtr.Zero;
        _setting = IntPtr.Zero;
        Log.Info("final :|");
    } catch (Exception ex) {
        Log.Error(ex.Message);
    } finally {
        _ready = false;
        _stopping = false;
    }
}
```

:::note info
FluidSynth の終了処理を行う final メソッドを定義しています。
:::

:::note info
ここまでの手順で、メインの C# コードから使用する、FluidSynth ラッパークラスの準備が出来ました😋
:::

### UI の作成

Unity の UI エディタを使用して以下の UI を作成しました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/ef184b83-78bb-c0bb-96e5-bfc365931322.png)

|コントロール|表示|内容|
|:--|:--|:--|
|Button|Start|MIDI 再生を開始します。|
|Button|Stop|MIDI 再生を停止します。|
|Text|Debug Text|動作のログを表示します。|

### メインプログラムの作成

<details>
<summary>コードの全体を表示する</summary>

```csharp:Assets\Scripts\Program.cs
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

using UnityEngine;
using UnityEngine.UI;
using TMPro;

namespace GameDev {
    /// <summary>
    /// the main program.
    /// </summary>
    public class Program : MonoBehaviour {
#nullable enable
        string DATA_PATH;
        [SerializeField] Button _button_start, _button_stop;
        [SerializeField] TextMeshProUGUI _text_debug;
        Queue<string> _message_queue = new();
        void Awake() {
            try {
#if UNITY_EDITOR
                DATA_PATH = Application.dataPath + "/Resources";
#else
                DATA_PATH = Application.persistentDataPath + "/Resources";
#endif
                _button_start.onClick.AddListener(() => buttonStart());
                _button_stop.onClick.AddListener(() => buttonStop());
                Log.OnError += (sender, e) => _message_queue.Enqueue($"[Error] {e.PropertyName}");
                Log.OnWarn += (sender, e) => _message_queue.Enqueue($"[Warn] {e.PropertyName}");
                Log.OnInfo += (sender, e) => _message_queue.Enqueue($"[Info] {e.PropertyName}");
            }
            catch (Exception ex) {
                Log.Error(ex.Message);
            }
        }
        void Start() {
            try {
                Synth.SoundFontPath = DATA_PATH + "/SoundFont/GeneralUser GS v1.47.sf2";
                Synth.MidiFilePath = DATA_PATH + "/MIDI/New_Song.mid";
            }
            catch (Exception ex) {
                Log.Error(ex.Message);
            }
        }
        void Update() {
            try {
                while (_message_queue.Count > 0) {
                    string message = _message_queue.Dequeue();
                    _text_debug.text += $"\n{message}";
                }
            }
            catch (Exception ex) {
                Log.Error(ex.Message);
            }
        }
        static void buttonStart() {
            try {
                Log.Info("start button clicked!");
                playSong();
            }
            catch (Exception ex) {
                Log.Error(ex.Message);
            }
        }
        static void buttonStop() {
            try {
                Log.Info("stop button clicked!");
                stopSong();
            }
            catch (Exception ex) {
                Log.Error(ex.Message);
            }
        }
        static async void playSong() {
            await Task.Run(() => Synth.Start());
        }
        static async void stopSong() {
            await Task.Run(() => Synth.Stop());
        }
    }
}
```

</details>

以下、ポイントを説明していきます。

```csharp:Assets\Scripts\Program.cs ※抜粋
#if UNITY_EDITOR
    DATA_PATH = Application.dataPath + "/Resources";
#else
    DATA_PATH = Application.persistentDataPath + "/Resources";
#endif
```

:::note info

リソースとなる MIDI ファイル、サウンドフォントファイルは以下のフォルダに別途配置します。

|値|動作OS|実際のパス|概要|
|:--|:--|:--|:--|
|Application.dataPath|Windows|/Assets|開発時|
|Application.persistentDataPath|Windows|C:/Users/{user}/AppData/LocalLow/{company-name}/{product-name}|Windows ビルド後|
|Application.persistentDataPath|Android|/Android/data/{package-name}/files|Android 実機|

:::

```csharp:Assets\Scripts\Program.cs ※抜粋
static async void playSong() {
    await Task.Run(() => Synth.Start());
}
static async void stopSong() {
    await Task.Run(() => Synth.Stop());
}
```

:::note info
非同期処理で再生の開始、停止を実行しています。
:::

```csharp:Assets\Scripts\Program.cs ※抜粋
while (_message_queue.Count > 0) {
    string message = _message_queue.Dequeue();
    _text_debug.text += $"\n{message}";
}
```

:::note info
UI コントロールはメインスレッドからしか触れないので、ログ処理はキューに貯めておき、Update メソッドで一括表示しています。
:::

### Android にビルドして実行

![unnamed.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/98df0f56-9d82-a588-349f-8a8bdac82b1c.png)

:::note info
ここまでの手順で、**Unity で実装する Android アプリ**に **FluidSynth をビルドイン**することが出来ました！😆
:::

## まとめ

+ C# (Unity) から C++ で実装されたネイティブライブラリを使用することができました。
+ FluidSynth は MIDI シーケンサーを内蔵しているので、今後、音声/音楽が必要である様々な開発シーンで利用できる可能性を秘めていると思います。

どうでしたか？ Window 11 の Unity で、Android アプリを開発する環境を手軽に構築することができます、ぜひお試しください。今後も Unity / C# の開発トピックなどを紹介していきますので、ぜひお楽しみにしてください。

## 参考資料

https://www.fluidsynth.org/api/UsageGuide.html