Unity で C# スクリプト:FluidSynth (ネイティブプラグイン)で MIDI を再生:Android ビルド
こんにちは、@studio_meowtoon です。今回は Windows 11 の Unity アプリ開発環境にて、C# から FluidSynth をネイティブライブラリとして使用する方法を紹介します。
実現すること
Unity で実装する Android アプリに、FluidSynth ソフトウェアシンセサイザーをビルドインします。
C# には本来厳格なコーディング規則がありますが、この記事では可読性のために、一部規則に沿わない表記方法を使用しています。ご注意ください。
開発環境
- Windows 11 Home 22H2
- Unity 2021.3.24f1 LTS
技術トピック
FluidSynth とは?
こちらを展開してご覧いただけます。
FluidSynth は、ソフトウェアベースの音声合成エンジンです。MIDI データを入力として受け取り、それを実際の音楽や音響イベントに変換します。FluidSynth は、MIDI ファイルを再生するためのツールや、仮想楽器の音色を再現するためのプログラムとして使用されます。また、FluidSynth は、様々なプラットフォーム (Windows、Mac、Linux など) で動作し、さまざまな音声合成の要件に応える柔軟性を持っています。
FluidSynth は LGPL ライセンスで提供されています。今回の記事の例のように FluidSynth をダイナミックリンクライブラリとして利用する場合、利用するソフトウェアは同じ LGPL を適用しなくても構いません。つまり FluidSynth を実行時に動的リンクしてその機能を呼び出す場合、利用する側のソフトウェアのソースコードの公開は必須ではありません。
Unity で FluidSynth を再生する手順
前提として
Android モバイルアプリを Windows 11 で開発するため、両方の環境で動作するアプリを実装します。
ライブラリのダウンロード
ライブラリ | 対応するOS | 用途 |
---|---|---|
fluidsynth-2.3.3-win10-x64.zip | Windows | 開発機材で使用します。 |
fluidsynth-2.3.2-android24.zip | Android | ビルドする実機で使用します。 |
プロジェクトの作成
ローカルの Unity Hub を起動してシンプルな 2D プロジェクトを作成します。
ネイティブライブラリの配置
共有ライブラリのファイル形式
拡張子 | 対応するOS | 概要 |
---|---|---|
.dll | Windows | ダイナミックリンクライブラリ Windows 系 |
.so | Linux (※含む Android) | ダイナミックリンクライブラリ UNIX 系 |
Assets フォルダの配下に以下のようにネイティブライブラリを配置しました。
├── 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
アンマネージド DLL 関数の作成
FluidSynth ネイティブプラグインを C# (Unity) から呼び出すために、アンマネージド DLL 関数として 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);
}
}
以下、ポイントを説明していきます。
#if UNITY_ANDROID
const string LIBLARY = "libfluidsynth.so";
#elif UNITY_STANDALONE_WIN
const string LIBLARY = "libfluidsynth-2.dll";
#endif
Unity のプラットフォーム依存コンパイル機能により、開発機の Windows とビルド先の実機 Android で参照するネイティブライブラリを切り替えます。
[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);
C# (Unity) のマネージド コードから C++ で実装されたライブラリ関数を呼び出すには、呼び出す関数ごとにマネージド プロトタイプを実装します。
ここまでの手順で、FluidSynth ( ネイティブプラグイン)を C# (Unity) から利用する準備が出来ました😋
Synth クラスの実装
MIDI データを再生するシンセサイザークラスを作成します。
コードの全体を表示する
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);
}
}
}
以下、ポイントを説明していきます。
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);
}
}
FluidSynth ソフトウェアシンセサイザーを初期化し、サウンドフォントと MIDI ファイルをロードして再生するための処理を行っています。
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);
}
}
再生を開始するための Start メソッドと、再生を停止するための Stop メソッドを定義しています。
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;
}
}
FluidSynth の終了処理を行う final メソッドを定義しています。
ここまでの手順で、メインの C# コードから使用する、FluidSynth ラッパークラスの準備が出来ました😋
UI の作成
Unity の UI エディタを使用して以下の UI を作成しました。
コントロール | 表示 | 内容 |
---|---|---|
Button | Start | MIDI 再生を開始します。 |
Button | Stop | MIDI 再生を停止します。 |
Text | Debug Text | 動作のログを表示します。 |
メインプログラムの作成
コードの全体を表示する
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());
}
}
}
以下、ポイントを説明していきます。
#if UNITY_EDITOR
DATA_PATH = Application.dataPath + "/Resources";
#else
DATA_PATH = Application.persistentDataPath + "/Resources";
#endif
リソースとなる 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 実機 |
static async void playSong() {
await Task.Run(() => Synth.Start());
}
static async void stopSong() {
await Task.Run(() => Synth.Stop());
}
非同期処理で再生の開始、停止を実行しています。
while (_message_queue.Count > 0) {
string message = _message_queue.Dequeue();
_text_debug.text += $"\n{message}";
}
UI コントロールはメインスレッドからしか触れないので、ログ処理はキューに貯めておき、Update メソッドで一括表示しています。
Android にビルドして実行
ここまでの手順で、Unity で実装する Android アプリに FluidSynth をビルドインすることが出来ました!😆
まとめ
- C# (Unity) から C++ で実装されたネイティブライブラリを使用することができました。
- FluidSynth は MIDI シーケンサーを内蔵しているので、今後、音声/音楽が必要である様々な開発シーンで利用できる可能性を秘めていると思います。
どうでしたか? Window 11 の Unity で、Android アプリを開発する環境を手軽に構築することができます、ぜひお試しください。今後も Unity / C# の開発トピックなどを紹介していきますので、ぜひお楽しみにしてください。
参考資料