.NET CoreとGtkSharpでGUIアプリを作ってみる[SDLでオーディオ]
前回の記事で.NET CoreとGtkSharpで何かしらGUIアプリを作ってみたわけですが、アラームがならないキッチンタイマーというどうしようのないものでした。
geeksinhitachiprovince.hatenablog.comというわけで、SDL2の補助ライブラリ、SDL2_mixerを使って音楽ファイルを演奏できるようにし、それをアラームということにしてみます。
SDLについては本家ページの他、Wikipediaをご覧ください。
SDLを使用可能にする
Cライブラリのインストール
最近のFedoraの場合
sudo dnf install SDL2_mixer-devel
Windowsの場合
前回の記事同様MSYS2を利用します。
pacman -S mingw-w64-x86_64-SDL2_mixer
.NET CoreアプリからSDL2_mixerを使えるようにする
NuGetでSDL2のC#バインディングを探したのですが、Mono/.NET Framework向けだったり、mixerが使えるのかわからなかったりしたので、自分でバインディングを書くことにしました。今回必要な関数は少ないので簡単です。
SDL2
namespace GIHPLib { public class SDL2 { public const uint /* Constants for init */ SDL_INIT_TIMER = 0x00000001u, SDL_INIT_AUDIO = 0x00000010u, SDL_INIT_VIDEO = 0x00000020u /**< SDL_INIT_VIDEO implies SDL_INIT_EVENTS */, SDL_INIT_JOYSTICK = 0x00000200u /**< SDL_INIT_JOYSTICK implies SDL_INIT_EVENTS */, SDL_INIT_HAPTIC = 0x00001000u, SDL_INIT_GAMECONTROLLER = 0x00002000u /**< SDL_INIT_GAMECONTROLLER implies SDL_INIT_JOYSTICK */, SDL_INIT_EVENTS = 0x00004000u, SDL_INIT_SENSOR = 0x00008000u, SDL_INIT_NOPARACHUTE = 0x00100000u /**< compatibility; this flag is ignored. */, SDL_INIT_EVERYTHING = SDL_INIT_TIMER | SDL_INIT_AUDIO | SDL_INIT_VIDEO | SDL_INIT_EVENTS | SDL_INIT_JOYSTICK | SDL_INIT_HAPTIC | SDL_INIT_GAMECONTROLLER | SDL_INIT_SENSOR ; public const int /* Constants for audio format */ AUDIO_U8 = 0x0008 /**< Unsigned 8-bit samples */, AUDIO_S8 = 0x8008 /**< Signed 8-bit samples */, AUDIO_U16LSB = 0x0010 /**< Unsigned 16-bit samples */, AUDIO_S16LSB = 0x8010 /**< Signed 16-bit samples */, AUDIO_U16MSB = 0x1010 /**< As above, but big-endian byte order */, AUDIO_S16MSB = 0x9010 /**< As above, but big-endian byte order */, AUDIO_U16 = AUDIO_U16LSB, AUDIO_S16 = AUDIO_S16LSB, AUDIO_S32LSB = 0x8020 /**< 32-bit integer samples */, AUDIO_S32MSB = 0x9020 /**< As above, but big-endian byte order */, AUDIO_S32 = AUDIO_S32LSB, AUDIO_F32LSB = 0x8120 /**< 32-bit floating point samples */, AUDIO_F32MSB = 0x9120 /**< As above, but big-endian byte order */, AUDIO_F32 = AUDIO_F32LSB ; public static readonly int AUDIO_U16SYS, AUDIO_S16SYS, AUDIO_S32SYS, AUDIO_F32SYS ; static SDL2(){ if(System.BitConverter.IsLittleEndian){ AUDIO_U16SYS = AUDIO_U16LSB; AUDIO_S16SYS = AUDIO_S16LSB; AUDIO_S32SYS = AUDIO_S32LSB; AUDIO_F32SYS = AUDIO_F32LSB; } else{ AUDIO_U16SYS = AUDIO_U16MSB; AUDIO_S16SYS = AUDIO_S16MSB; AUDIO_S32SYS = AUDIO_S32MSB; AUDIO_F32SYS = AUDIO_F32MSB; } } [System.Runtime.InteropServices.DllImport("SDL2", EntryPoint="SDL_Init")] public static extern int Init(uint flags); [System.Runtime.InteropServices.DllImport("SDL2", EntryPoint="SDL_GetError")] public static extern System.IntPtr GetError(); [System.Runtime.InteropServices.DllImport("SDL2", EntryPoint="SDL_Quit")] public static extern void Quit(); } }
今回実際に使ったのはGetError()だけでした。
SDL2_mixer
namespace GIHPLib { using Mix = SDL2_mixer; using Mix_Music = System.IntPtr; public class SDL2_mixer { public enum MIX_InitFlags { MIX_INIT_FLAC = 0x00000001, MIX_INIT_MOD = 0x00000002, MIX_INIT_MP3 = 0x00000008, MIX_INIT_OGG = 0x00000010, MIX_INIT_MID = 0x00000020 } public const int MIX_MAX_VOLUME = 128 ; public delegate void PlayingFinishedHandler(); /* mixer初期化 */ [System.Runtime.InteropServices.DllImport("SDL2_mixer", EntryPoint="Mix_Init")] public static extern int Init(int flags); [System.Runtime.InteropServices.DllImport("SDL2_mixer", EntryPoint="Mix_Quit")] public static extern void Quit(); /* audio初期化 */ [System.Runtime.InteropServices.DllImport("SDL2_mixer", EntryPoint="Mix_OpenAudio")] public static extern int OpenAudio(int frequency, System.UInt16 format, int channels, int chunksize); [System.Runtime.InteropServices.DllImport("SDL2_mixer", EntryPoint="Mix_CloseAudio")] public static extern void CloseAudio(); /* 確保、開放 */ [System.Runtime.InteropServices.DllImport("SDL2_mixer", EntryPoint="Mix_LoadMUS")] public static extern Mix_Music LoadMUS(string file_name); [System.Runtime.InteropServices.DllImport("SDL2_mixer", EntryPoint="Mix_FreeMusic")] public static extern void FreeMusic(Mix_Music music); /* 設定 */ [System.Runtime.InteropServices.DllImport("SDL2_mixer", EntryPoint="Mix_HookMusicFinished")] public static extern void HookMusicFinished(PlayingFinishedHandler handler); [System.Runtime.InteropServices.DllImport("SDL2_mixer", EntryPoint="Mix_VolumeMusic")] public static extern int VolumeMusic(int volume); /* 操作 */ [System.Runtime.InteropServices.DllImport("SDL2_mixer", EntryPoint="Mix_PlayMusic")] public static extern int PlayMusic(Mix_Music music, int loop); [System.Runtime.InteropServices.DllImport("SDL2_mixer", EntryPoint="Mix_HaltMusic")] public static extern int HaltMusic(); /* 状態取得 */ [System.Runtime.InteropServices.DllImport("SDL2_mixer", EntryPoint="Mix_PlayingMusic")] public static extern int PlayingMusic(); } }
こちらも、全部は使ってないです。
オーディオファイルを演奏する手順
大雑把な流れです。
GIHPLib.SDL2.Init(GIHPLib.SDL2.SDL_INIT_AUDIO);/* 何故か呼ばなくても動きます。実際、今回のアプリでは呼んでません。 */ GIHPLib.SDL2_mixer.OpenAudio(44100, (ushort)GIHPLib.SDL2.AUDIO_S16SYS, 2, 4096); music = GIHPLib.SDL2_mixer.LoadMUS(filepath);/* flacやmp3等などが開けます。 */ GIHPLib.SDL2_mixer.PlayMusic(music, loopcount);/* 再生を始めます。再生は別スレッドで行われるので、この呼び出しはすぐに帰ってきます。 */ GIHPLib.SDL2_mixer.HaltMusic();/* 再生を停止します。これを呼ばなくてもFreeMusicの呼び出しで停止します。 */ GIHPLib.SDL2_mixer.FreeMusic(music);/* メモリ等のリソースを開放します。 */ GIHPLib.SDL2_mixer.CloseAudio(); GIHPLib.SDL2.Quit();/* Initが要らなかったので、これもいらないです。 */
Mix_OpenAudioのドキュメントによると、先にSDL_Init(SDL_INIT_AUDIO)を呼ばなければならないのですが、これを呼ばずにいきなりSDL2_mixer.OpenAudio(...)を呼んでも問題なかったので、このアプリではSDL2.InitとSDL2.Quitは省略しています。
SDL2_mixerでアラームを鳴らせるようキッチンタイマーを改造
SDL2_mixerの初期化と後処理
まず次のフィールドを追加します。
//クラスMainWindowのフィールド IntPtr music; bool counting/*これは前回からありました*/, sdl_init_success;
コンストラクタでSDL2_mixer.OpenAudioを呼び出します。
//MainWindowのコンストラクタ sdl_init_success = false; music = IntPtr.Zero; if(GIHPLib.SDL2_mixer.OpenAudio(44100, (ushort)GIHPLib.SDL2.AUDIO_S16SYS, 2, 4096) != 0) { var md = new MessageDialog(this, DialogFlags.Modal, MessageType.Error, ButtonsType.Ok, "SDL_mixerの初期化に失敗しました。\nSDLからの報告内容:" + System.Runtime.InteropServices.Marshal.PtrToStringAnsi(GIHPLib.SDL2.GetError())); md.Run(); md.Destroy(); } else{ sdl_init_success = true; }
メソッドWindow_DeleteEventでSDL2_mixer.CloseAudioを呼び出します。
//メソッドMainWindow.Window_DeleteEvent内 if(sdl_init_success) { if(music != IntPtr.Zero) { GIHPLib.SDL2_mixer.HaltMusic(); GIHPLib.SDL2_mixer.FreeMusic(music); } GIHPLib.SDL2_mixer.CloseAudio(); }
アラーム関連の操作ができるようボタンを追加
GladeでGUIを再設計し、こんな感じにしました。
ID割当は、
- 表示用ラベル:time_display(前回から変更)
- 分+ボタン:m_up_buton
- 分-ボタン:m_down_buton
- 秒+ボタン:s_up_buton
- 秒-ボタン:s_down_buton
- アラーム停止ボタン:alarm_stop_button
- アラーム選択ボタン:alarm_choose_button
このようになりました。
前回、表示用ラベルのID _label1はプロジェクト作成時に生成されたものそのままでした。アンダーバーから始まっていて統一感が無く気持ち悪かったので今回変更しました。
追加したボタンにイベント設定
アラーム停止ボタンとアラーム選択ボタンにイベントハンドラを登録します。
まずはフィールド宣言です。前回のように[UI]属性を付け、IDと同じ名前にします。
//クラスMainWindowのフィールド [UI] private Button alarm_stop_button = null; [UI] private Button alarm_choose_button = null;
次にイベントハンドラを登録します。
//MainWindowのコンストラクタ
alarm_stop_button.ButtonReleaseEvent += stopAlarm;
alarm_choose_button.ButtonReleaseEvent += chooseAlarm;
イベントハンドラの本体を書きます。
//MainWindowメンバメソッド void stopAlarm(object sender, EventArgs e) { try { GIHPLib.SDL2_mixer.HaltMusic(); } catch(System.Exception err) { System.Console.Error.WriteLine(err.Message); } } void chooseAlarm(object sender, EventArgs e) { if(sdl_init_success) { FileChooserDialog fc = new FileChooserDialog( "報知音に使用する音楽ファイルの選択", this, FileChooserAction.Open, "取り消し", ResponseType.Cancel, "開く", ResponseType.Accept ); FileFilter ff; ff = new FileFilter(); ff.AddMimeType("audio/flac"); ff.AddPattern("*.flac"); ff.Name = "flacオーディオ"; fc.AddFilter(ff); ff = new FileFilter(); ff.AddMimeType("audio/wav"); ff.AddPattern("*.wav"); ff.Name = "wavオーディオ"; fc.AddFilter(ff); ff = new FileFilter(); ff.AddMimeType("audio/x_mpg"); ff.AddMimeType("audio/x_mp3"); ff.AddPattern("*.mp3"); ff.Name = "MPEG1 Audio Layer3"; fc.AddFilter(ff); ff = new FileFilter(); ff.AddMimeType("audio/ogg"); ff.AddPattern("*.ogg"); ff.AddPattern("*.oga"); ff.Name = "Ogg Vorbis"; fc.AddFilter(ff); if(fc.Run() == (int)ResponseType.Accept) { if(music != IntPtr.Zero) { GIHPLib.SDL2_mixer.HaltMusic(); GIHPLib.SDL2_mixer.FreeMusic(music); } music = GIHPLib.SDL2_mixer.LoadMUS(fc.Filename); if(music == IntPtr.Zero) { MessageDialog md = new MessageDialog(this, DialogFlags.Modal, MessageType.Error, ButtonsType.Ok, "SDL_mixerでファイルを開くことに失敗しました。\nSDLからの報告内容:" + System.Runtime.InteropServices.Marshal.PtrToStringAnsi(GIHPLib.SDL2.GetError()) + "\nなお、Windows版SDL_mixerはASCIIのみのファイル名しか扱えないようです。\nまたLinux版SDL_mixerはディストリビューションによってはmp3は再生できないようです。"); md.Run(); md.Destroy(); } } fc.Destroy(); } else { MessageDialog md = new MessageDialog(this, DialogFlags.Modal, MessageType.Error, ButtonsType.Ok, "SDL初期化に失敗しています。報知音を鳴らせません。"); md.Run(); md.Destroy(); } }
タイマースレッドからアラーム再生イベントを受け付ける
TimerMainクラスにデリゲートとそのイベントフィールドを追加します。
//クラスTimerMainのフィールド public delegate void Alarmer(); public event Alarmer alarm;
カウントダウンで残り0秒になった瞬間イベントを発生させます。
//メソッドTimerMain.startの0秒以下条件分岐部 if(left_sec <= 0) { pause = true; if(alarm != null) { alarm(); } }
MainWindow側にイベントハンドラを用意します。
//MainWindowのコンストラクタ
tm.alarm += alarm;
//MainWindowメンバメソッド void alarm() { Application.Invoke(delegate { if(sdl_init_success) { if(music != IntPtr.Zero) { if(GIHPLib.SDL2_mixer.PlayMusic(music, -1) == -1) { MessageDialog md = new MessageDialog(this, DialogFlags.Modal, MessageType.Error, ButtonsType.Ok, "SDL_mixerでの報知音の再生に失敗しました。\nSDLからの報告内容:" + System.Runtime.InteropServices.Marshal.PtrToStringAnsi(GIHPLib.SDL2.GetError())); md.Run(); md.Destroy(); } } } else { MessageDialog md = new MessageDialog(this, DialogFlags.Modal, MessageType.Error, ButtonsType.Ok, "SDL初期化に失敗しています。報知音を鳴らせません。"); md.Run(); md.Destroy(); } }); }
これで完成
これでとりあえず何かしらをアラームとして鳴らせるようになりました。
Choose alarmボタンを押すとファイル選択ダイアログが表示されアラームとしてflacやoga、mp3などが開けます。以降の0秒カウント時に鳴らされます。
MainWindow.csの全文も載せます。
using System; using Gtk; using UI = Gtk.Builder.ObjectAttribute; namespace KitchenTimer { class MainWindow: Window { [UI] private Label time_display = null; [UI] private Button m_up_button = null; [UI] private Button m_down_button = null; [UI] private Button s_up_button = null; [UI] private Button s_down_button = null; [UI] private Button start_stop_button = null; [UI] private Button alarm_stop_button = null; [UI] private Button alarm_choose_button = null; TimerMain tm; IntPtr music; bool counting, sdl_init_success; public MainWindow() : this(new Builder("MainWindow.glade")) { } private MainWindow(Builder builder) : base(builder.GetObject("MainWindow").Handle) { builder.Autoconnect(this); tm = new TimerMain(); tm.updateLeftTime += updateLabelText; tm.setStat += setStat; tm.alarm += alarm; start_stop_button.ButtonReleaseEvent += startTimer; DeleteEvent += Window_DeleteEvent; counting = false; m_up_button.ButtonReleaseEvent += incM; m_down_button.ButtonReleaseEvent += decM; s_up_button.ButtonReleaseEvent += incS; s_down_button.ButtonReleaseEvent += decS; alarm_stop_button.ButtonReleaseEvent += stopAlarm; alarm_choose_button.ButtonReleaseEvent += chooseAlarm; updateLabelText(); sdl_init_success = false; music = IntPtr.Zero; if(GIHPLib.SDL2_mixer.OpenAudio(44100, (ushort)GIHPLib.SDL2.AUDIO_S16SYS, 2, 4096) != 0) { var md = new MessageDialog(this, DialogFlags.Modal, MessageType.Error, ButtonsType.Ok, "SDL_mixerの初期化に失敗しました。\nSDLからの報告内容:" + System.Runtime.InteropServices.Marshal.PtrToStringAnsi(GIHPLib.SDL2.GetError())); md.Run(); md.Destroy(); } else{ sdl_init_success = true; } } private void Window_DeleteEvent(object sender, DeleteEventArgs a) { if(sdl_init_success) { if(music != IntPtr.Zero) { GIHPLib.SDL2_mixer.HaltMusic(); GIHPLib.SDL2_mixer.FreeMusic(music); } GIHPLib.SDL2_mixer.CloseAudio(); } tm.exitThread(); Application.Quit(); } private void updateLabelText() { Application.Invoke(delegate { int sec = tm.getLeftTime(), show_min = sec / 60, show_sec = sec - (60 * show_min) ; time_display.Markup = "<span font='32.0' weight='bold'>" + show_min.ToString() + "分\t" + show_sec.ToString() + "秒" + "</span>"; }); } void startTimer(object sender, EventArgs e) { if(counting) { tm.pauseCountdown(); } else { tm.resumeCountdown(); } } void setStat(bool stat) { Application.Invoke(delegate { counting = stat; if(stat) { m_up_button.Sensitive = false; m_down_button.Sensitive = false; s_up_button.Sensitive = false; s_down_button.Sensitive = false; } else { m_up_button.Sensitive = true; m_down_button.Sensitive = true; s_up_button.Sensitive = true; s_down_button.Sensitive = true; } }); } void incM(object sender, EventArgs e) { tm.setLeftTime(tm.getLeftTime() + 60); updateLabelText(); } void decM(object sender, EventArgs e) { int sec = tm.getLeftTime(); sec -= 60; if(sec < 0) { sec += 60; } tm.setLeftTime(sec); updateLabelText(); } void incS(object sender, EventArgs e) { tm.setLeftTime(tm.getLeftTime() + 1); updateLabelText(); } void decS(object sender, EventArgs e) { int sec = tm.getLeftTime(); sec -= 1; if(sec < 0) { sec = 0; } tm.setLeftTime(sec); updateLabelText(); } void alarm() { Application.Invoke(delegate { if(sdl_init_success) { if(music != IntPtr.Zero) { if(GIHPLib.SDL2_mixer.PlayMusic(music, -1) == -1) { MessageDialog md = new MessageDialog(this, DialogFlags.Modal, MessageType.Error, ButtonsType.Ok, "SDL_mixerでの報知音の再生に失敗しました。\nSDLからの報告内容:" + System.Runtime.InteropServices.Marshal.PtrToStringAnsi(GIHPLib.SDL2.GetError())); md.Run(); md.Destroy(); } } } else { MessageDialog md = new MessageDialog(this, DialogFlags.Modal, MessageType.Error, ButtonsType.Ok, "SDL初期化に失敗しています。報知音を鳴らせません。"); md.Run(); md.Destroy(); } }); } void stopAlarm(object sender, EventArgs e) { try { GIHPLib.SDL2_mixer.HaltMusic(); } catch(System.Exception err) { System.Console.Error.WriteLine(err.Message); } } void chooseAlarm(object sender, EventArgs e) { if(sdl_init_success) { FileChooserDialog fc = new FileChooserDialog( "報知音に使用する音楽ファイルの選択", this, FileChooserAction.Open, "取り消し", ResponseType.Cancel, "開く", ResponseType.Accept ); FileFilter ff; ff = new FileFilter(); ff.AddMimeType("audio/flac"); ff.AddPattern("*.flac"); ff.Name = "flacオーディオ"; fc.AddFilter(ff); ff = new FileFilter(); ff.AddMimeType("audio/wav"); ff.AddPattern("*.wav"); ff.Name = "wavオーディオ"; fc.AddFilter(ff); ff = new FileFilter(); ff.AddMimeType("audio/x_mpg"); ff.AddMimeType("audio/x_mp3"); ff.AddPattern("*.mp3"); ff.Name = "MPEG1 Audio Layer3"; fc.AddFilter(ff); ff = new FileFilter(); ff.AddMimeType("audio/ogg"); ff.AddPattern("*.ogg"); ff.AddPattern("*.oga"); ff.Name = "Ogg Vorbis"; fc.AddFilter(ff); if(fc.Run() == (int)ResponseType.Accept) { if(music != IntPtr.Zero) { GIHPLib.SDL2_mixer.HaltMusic(); GIHPLib.SDL2_mixer.FreeMusic(music); } music = GIHPLib.SDL2_mixer.LoadMUS(fc.Filename); if(music == IntPtr.Zero) { MessageDialog md = new MessageDialog(this, DialogFlags.Modal, MessageType.Error, ButtonsType.Ok, "SDL_mixerでファイルを開くことに失敗しました。\nSDLからの報告内容:" + System.Runtime.InteropServices.Marshal.PtrToStringAnsi(GIHPLib.SDL2.GetError()) + "\nなお、Windows版SDL_mixerはASCIIのみのファイル名しか扱えないようです。\nまたLinux版SDL_mixerはディストリビューションによってはmp3は再生できないようです。"); md.Run(); md.Destroy(); } } fc.Destroy(); } else { MessageDialog md = new MessageDialog(this, DialogFlags.Modal, MessageType.Error, ButtonsType.Ok, "SDL初期化に失敗しています。報知音を鳴らせません。"); md.Run(); md.Destroy(); } } } class TimerMain { bool exit, pause, sleeping; int left_sec; System.Threading.Thread th; System.Threading.SemaphoreSlim running_lock, pausing_lock, controll_lock; public delegate void LeftTimeUpdater(); public event LeftTimeUpdater updateLeftTime; public delegate void CountingStatListener(bool stat); public event CountingStatListener setStat; public delegate void Alarmer(); public event Alarmer alarm; void start(Object obj) { running_lock.Wait(); sleeping = false; while(!exit) { if(pause) { pausing_lock.Wait(); if(setStat != null) { setStat(false); } running_lock.Release(); try { sleeping = true; System.Threading.Thread.Sleep(System.Threading.Timeout.Infinite); } catch(System.Threading.ThreadInterruptedException e) { sleeping = false; } running_lock.Wait(); if(setStat != null) { setStat(true); } pausing_lock.Release(); } if(updateLeftTime != null) { updateLeftTime(); } if(left_sec <= 0) { pause = true; if(alarm != null) { alarm(); } } else { try { System.Threading.Thread.Sleep(999); } catch(System.Exception e) { } left_sec--; } } } public TimerMain() { exit = false; pause = true; th = new System.Threading.Thread(start); running_lock = new System.Threading.SemaphoreSlim(1, 1); pausing_lock = new System.Threading.SemaphoreSlim(1, 1); controll_lock = new System.Threading.SemaphoreSlim(1, 1); th.Start(); } public bool setLeftTime(int sec) { using(var l1 = new GIHPLib.Locker(controll_lock)) { using(var l2 = new GIHPLib.Locker(running_lock, try_lock: true)) { if(l2.hasLock()) { left_sec = sec; return true; } return false; } } } public int getLeftTime() { return left_sec; } public bool resumeCountdown() { using(var l1 = new GIHPLib.Locker(controll_lock)) { using(var l2 = new GIHPLib.Locker(running_lock, try_lock: true)) { if(l2.hasLock()) { if(pause) { pause = false; th.Interrupt(); } return true; } return false; } } } public bool pauseCountdown() { using(var l1 = new GIHPLib.Locker(controll_lock)) { using(var l2 = new GIHPLib.Locker(pausing_lock, try_lock: true)) { if(l2.hasLock()) { pause = true; return true; } return false; } } } public void exitThread() { using(var l = new GIHPLib.Locker(controll_lock)) { exit = true; System.Threading.Thread.Yield(); while(!th.Join(0)) { if(sleeping) { th.Interrupt(); } System.Threading.Thread.Yield(); } } } } }
問題点
まず、Windowsで日本語を含むパスを開けないという致命傷を負っています。
また、LinuxではSDL2_mixerのディストリビューションの公式配布パッケージを使う場合、コンパイル時の設定などによってmp3デコードが不可能になっている場合があります。少なくともFedoraはそうでした。
Windowsの日本語の件に関しては、対処方法を探ってみたいと思います。
あと、アラームを設定として記憶できるようにするべきだとも思います。