【Unityエディタ拡張】音楽プレイヤーを作る

初めまして!プログラマの近藤です。
最近Unityを触りはじめ、エディタ拡張について勉強しています。
今回はそのエディタ拡張機能を使って、エディタ上で音楽を聴く「Audio再生機」を作ってみました。その紹介をしていこうと思います。

待って!そもそもUnityのエディタ拡張って??

Unityは標準で搭載されている機能の他にユーザーが機能を追加していくことができます。
それを総称して「エディタ拡張」と呼びます。自分はそう呼んでいます。
追加できる機能が多く、全てを紹介出来ない(分かってない)ので、
今回使用した機能を説明します。

新規ウィンドウの追加
ヒエラルキーやインスペクターなど標準搭載のウィンドウの他に新しくウィンドウを作成できます。

各種入力領域の追加

ボタンや文字列、チェックボックス、スライダーからカラーパレットまで、
あったらいいなって入力項目を追加できます。
これは新規で作成したウィンドウ内にもできますし、既存のウィンドウにも追加可能です。

他にも文字の色を真っ赤にしたり、アイコンの見た目を好きな画像にしたり、
本当にたくさん追加可能な機能があります。

これがやっていて楽しい。とても楽しい。
ゲーム制作もそうなのですが、制作してすぐ反映されるものはやっていて楽しいですね!

本題に戻って

色々できるエディタ拡張の中で今回は「音楽を再生する」ということに焦点をあてて
音楽データ(AudioClip)を再生する「Audio再生機」を作ってみました。

完成品お披露目

こんな感じになりました!!ザ・シンプル!

Resources/Audioフォルダ内のAudioClipを一覧に表示して
選択したAudioClipを再生します。
いくつかボタンがありますが、一般的な音楽再生ソフトと大体同じ機能なはず、です多分。
最後にコード全部載せますので、
それをResources/Editorフォルダに入れるとそのまま使用することができます。

ウィンドウ起動時

audio_list[] audio_list;
void Awake()
{
    var audios = Resources.LoadAll<AudioClip>("Audio");
    audio_list = new AudioClip[audios.Length];
    for (int i = 0; i < audio_list.Length; ++i)
    {
        audio_list[i] = audios[i];
    }
    // 再生用のオブジェクト生成
    Create();
}

Resouces/Audio内のAudioClipをすべてリストに格納します。
自分の持ってた音源が.m4aだったのですが
UnityにImportできずmp3に変換してImportしました。

https://docs.unity3d.com/ja/current/Manual/AudioFiles.html
.m4aはサポートされていないんですね、残念。

再生用オブジェクト生成

void Create()
{
    AudioObj = GameObject.Find("Audio");
    if (AudioObj == null)
    {
        AudioObj = new GameObject("Audio");

        AudioObj.hideFlags = HideFlags.HideAndDontSave;
    }

    AudioSource = AudioObj.GetComponent<AudioSource>();
    if (AudioSource == null)
    {
        AudioSource = AudioObj.AddComponent<AudioSource>();
    }
}

ウィンドウ起動時に再生用のAudioというオブジェクトを生成して、
そのオブジェクトにAudioSourceコンポーネントを追加しています。このオブジェクトを使って音楽を再生させます。

何も設定を行わないと生成した再生用のオブジェクトがヒエラルキーに表示されてしまうので
HideFlags.HideAndDontSaveというフラグを設定しておくとSceneに保存されませんし表示もされなくなります。
今回のような裏で動いてほしい場合にはうってつけの設定でした。さすユニ。

生成したらちゃんと削除もする

当たり前だろって言われそうですが、自分は完全に忘れてました。
HideFlags.HideAndDontSaveを設定したオブジェクトは
ヒエラルキーに表示されないので、忘れがちになります。自分は完全に忘れてました。

// 生成したオブジェクトを削除
void Delete()
{
    if (AudioObj)
    {
        DestroyImmediate(AudioObj);
    }
}

オブジェクトの削除にはDestroyでは無く、DestroyImmediateを使用します。
OnGUI()内でDestroyを使用するとエラーが出て正常に削除できないです。

再生ボタンを押して再生する

static bool playBotton = false;
void OnGUI()
{
    // 再生、停止ボタン
    playBotton = GUILayout.Toggle(playBotton, playBotton ? "[]" : "|>", "button");
    if (playBotton)
    {
        // 再生処理
        AudioSource.clip = audio_list[current_play_idx];
        AudioSource.Play();
    }
    else
    {
        // 停止
        AudioSource.Stop();
    }
}

GUILayout.Toggleは、ボタンを押すともう一度押すまでtrueなので
再生ボタンのような押しっぱなしがいい場合に重宝します。
さらに指定スキンをボタンにすること他のボタンとの統一感が出て視認性UP!

GUI.skinには画像を設定できるのでテキストで頑張って|>とかせずに画像を用意したほうがいいと思います。
フォントサイズの調整も必要なくなるので。

UIを右寄せに表示したい

勝手なこだわりだったんですが、音量調整のスライダーは右寄せにしたかったんです。
GUILayout.FlexibleSpace()を使用してそれっぽく見せています。

float volume = 0.0f;
void OnGUI()
{
    // widthを描画領域まで使うようにする
    EditorGUILayout.BeginHorizontal(GUILayout.ExpandWidth(true));
    {
        GUILayout.FlexibleSpace();
        // ボリューム
        volume = EditorGUILayout.Slider(volume, 0, 1);
     }
     EditorGUILayout.EndHorizontal();
}

(画像見づらいです、申し訳ありません。)
GUILayout.ExpandWidth(true)
を使用して、描画領域の最大まで確保した状態で
GUILayout.FlexibleSpace()を使うと右寄せに表示することができました。

困ったこと

Unityエディタを選択していないと再生されない
音楽プレイヤーを名乗りたいのならば非表示時にも再生していて欲しいところ。
デフォルトの設定だとエディタ選択中以外は動かないようになっているらしく、
Build Settingsから動くように変更する必要があるようです。

  • File/Build Settingsからビルド設定画面を開き、Player Settingsのボタンを押す。
  • インスペクターの設定項目「Run In BackGround」にチェックをつける。

この設定をしておくと、Unityエディタのウィンドウを選択していなくても動作します。

完成!

あとはボタン押したら次の曲にしたり、ボリューム調整できるようにしたりと
好きな機能、というか最低限必要な機能を入れていって
「Audio再生機」の完成です!

以下、コード全文 Resources/Editorフォルダに入れると使用できます。

using UnityEditor;
using UnityEngine;

public class AudioPlayer: EditorWindow
{
    static Vector2  MIN_WINDOW_SIZE             = new Vector2(240, 100);
    const float     BUTTON_SIZE_Y_SCALE         = 6.0f;
    const float     CORRECT_BUTTON_SIZE_SCALE   = 0.8f;
    const float     MAX_BUTTON_SIZE             = 36.0f;
    const int       MAX_FONT_SIZE               = 30;
    const int       LABEL_FONT_SIZE             = 20;

    // ボタン、フォントサイズ
    float button_size_y;
    int font_size;

    // 再生,停止
    bool playBotton = false;
    bool is_play = false;
    // 次の曲、最初から
    bool nextBotton = false;
    bool prevBotton = false;
    // 音量
    float volume = 0.5f;

    // 再生する曲リストの項目
    int play_idx = 0;
    int prev_play_idx = -1;

    // 曲一覧のスクロール位置
    Vector2 leftScrollPos;

    // 音の再生に使うオブジェクト
    static GameObject AudioObj;
    static AudioSource AudioSource;

    // 再生する音データリスト
    AudioClip[] audio_list;
    // ウィンドウ
    static AudioPlayer Window;
    [MenuItem("Window/Audio再生機")]
    static void Open()
    {
        // ウィンドウ作成
        if (Window == null)
        {
            Window = GetWindow<AudioPlayer>("Audio再生機");
        }
        Window.Show();
        Window.minSize = MIN_WINDOW_SIZE;
    }
    void Awake()
    {
        var audios = Resources.LoadAll<AudioClip>("Audio");
        audio_list = new AudioClip[audios.Length];
        for (int i = 0; i < audio_list.Length; ++i)
        {
            audio_list[i] = audios[i];
        }
        // 再生用のオブジェクトを生成
        Create();
    }
    void OnDestroy()
    {
        // 生成したオブジェクトを削除
        Delete();
    }
    void OnGUI()
    {
        Set();

        BeginWindows();
        // 曲の一覧
        {
            // ボリューム調整用スライダーの分のマージン
            float magin = 30;
            EditorGUILayout.BeginVertical(GUI.skin.box, GUILayout.Height(Window.position.size.y - button_size_y - magin));
            {
                Color color = GUI.skin.box.normal.textColor;

                if (Window.position.size.y == MIN_WINDOW_SIZE.y)
                {
                    color.a = 0.9f;
                    GUI.skin.box.normal.textColor = color;
                    GUILayout.Button(audio_list[play_idx].name, GUI.skin.box, GUILayout.ExpandWidth(true), GUILayout.ExpandHeight(true));
                }
                else
                {
                    leftScrollPos = EditorGUILayout.BeginScrollView(leftScrollPos);
                    {
                        for (int i = 0; i < audio_list.Length; ++i)
                        {
                            // 選択されている項目だけ目立つように他項目の透明度を下げる
                            color.a = 0.5f;
                            if (i == play_idx)
                            {
                                color.a = 0.9f;
                            }
                            GUI.skin.box.normal.textColor = color;
                            if (GUILayout.Button(audio_list[i].name, GUI.skin.box, GUILayout.ExpandWidth(true)))
                            {

                                // 選択していた場合は再生ボタンをONにする、再生していた場合は一時停止
                                if (play_idx == i)
                                {
                                    if (playBotton)
                                    {
                                        playBotton = false;
                                    }
                                    else
                                    {
                                        playBotton = true;
                                    }
                                }
                                play_idx = i;
                            }

                        }
                    }
                    EditorGUILayout.EndScrollView();
                }
            }
            EditorGUILayout.EndVertical();
        }
        // 操作パネル
        EditorGUILayout.BeginVertical();
        {
            EditorGUILayout.BeginHorizontal();
            {
                // 文字が大きくなりすぎないように制限を掛ける
                if (button_size_y >= MAX_BUTTON_SIZE)
                {
                    font_size = MAX_FONT_SIZE;
                }
                GUI.skin.button.fontSize = font_size;

                // 曲の最初からボタン
                prevBotton = GUILayout.Button("|<<", GUILayout.Height(button_size_y));

                // 再生、停止ボタン
                playBotton = GUILayout.Toggle(playBotton, playBotton ? "[]" : "|>", "button", GUILayout.Height(button_size_y));

                // 次の曲ボタン
                nextBotton = GUILayout.Button(">>|", GUILayout.Height(button_size_y));
            }
            EditorGUILayout.EndHorizontal();

            EditorGUILayout.BeginHorizontal();
            {
                GUILayout.FlexibleSpace();
                // ボリューム
                GUI.skin.label.fontSize = 10;
                GUILayout.Label("Vol");
                volume = EditorGUILayout.Slider(volume, 0, 1);
            }
            EditorGUILayout.EndHorizontal();
        }
        EditorGUILayout.EndVertical();

        EndWindows();

        if (AudioSource == null)
            return;
        // 曲数を超えたら、停止
        if (play_idx >= audio_list.Length)
        {
            play_idx = 0;
            prev_play_idx = 0;
            is_play = false;
            playBotton = false;
            AudioSource.Stop();
        }
        // 再生処理
        if (playBotton)
        {
            // 曲が終了したら次を選択する
            if (is_play && !AudioSource.isPlaying)
            {
                ++play_idx;
            }
            // 曲が選択された or 一時停止中 なら再生する
            else if (play_idx != prev_play_idx || !is_play)
            {
                prev_play_idx = play_idx;
                AudioSource.clip = audio_list[play_idx];
                is_play = true;
                AudioSource.Play();
            }
        }
        // 一時停止処理
        else if (AudioSource.isPlaying)
        {
            is_play = false;
            AudioSource.Pause();
        }
        // 曲の再生時間をリセットする
        if (prevBotton)
        {
            AudioSource.time = 0.0f;
        }
        // 次の曲を選択
        if (nextBotton)
        {
            ++play_idx;
        }

        // ボリューム設定
        AudioSource.volume = volume;
    }
    void Set()
    {
     // ボタンのサイズ、フォントのサイズをウィンドウサイズから設定する
        button_size_y = Window.position.size.y / BUTTON_SIZE_Y_SCALE;
        font_size = (int)(button_size_y * CORRECT_BUTTON_SIZE_SCALE);

        // 最大よりは大きくならないように
        if (button_size_y > MAX_BUTTON_SIZE)
        {
            button_size_y = MAX_BUTTON_SIZE;
        }

        // 曲一覧の項目に使用するGUIスキン(box)の設定
        GUI.skin.box.fontSize = LABEL_FONT_SIZE;
        GUI.skin.box.alignment = TextAnchor.MiddleLeft;
        GUI.skin.box.wordWrap = false;
    }
    // 音楽再生オブジェクトを生成
    void Create()
    {
        AudioObj = GameObject.Find("Audio");
        if (AudioObj == null)
        {
            AudioObj = new GameObject("Audio");

            AudioObj.hideFlags = HideFlags.HideAndDontSave;
        }

        AudioSource = AudioObj.GetComponent<AudioSource>();
        if (AudioSource == null)
        {
            AudioSource = AudioObj.AddComponent<AudioSource>();
        }
    }
    // 生成したオブジェクトを削除
    void Delete()
    {
        if (AudioObj)
        {
            DestroyImmediate(AudioObj);
        }
    }
}

終わり

ちゃんと音楽を再生できるものが出来ました。
ただ音楽プレイヤーとして機能不足が目立ちます。いまどきシャッフルとリピートのない音楽プレイヤーとかないですからね。これから追加していけたらなと思います。

Unityの音楽データは基本オブジェクトに追加して実行しないと音が再生されないので、
実行せずに手軽に確認したいときに役に立つかなぁと思ったりしています。

長々とお付き合い頂きありがとうございました!

おまけ

音楽聞きながらだと作業捗る系プログラマな自分ですが、実は
可愛い画像を見ると作業捗る系プログラマでもあるんですね。
やはり作業効率あげるなら可愛い画像の表示は必須。ということで用意しました。
こんな感じ

© Unity Technologies Japan/UCL

いつか次の記事を書く機会があればこの機能について書こうかなと思います。