LoginSignup
6
3

More than 3 years have passed since last update.

【Unity】AudioClipを司る内部ユーティリティ "UnityEditor.AudioUtil" のラッパーを作る【Editor拡張】

Last updated at Posted at 2020-01-22

UnityEditor.AudioUtilとは

エディタ上でAudioClipを扱うための内部APIです。
リファレンスは存在しませんが、その全貌はここで見ることができます。
全貌と言ってもほぼexternなのでメソッド名や型しか分かりませんが。

使うと何ができるの?

AudioSourceを使わずに、Editor上でAudioClipを再生できます。
AudioClipを選択したときにInspector下部にプレビューとしてサウンドプレイヤーが表示されますが、あんな感じのGUIを自前定義のカスタムInspectorやEditorWindow上に構築できるわけです。

私の場合、AudioClip(のラッパークラス)にこんなカスタムPropertyDrawerを定義するのに使いました。
image.png

準備

内部APIなので、当然普通に呼ぶことはできません。Reflectionを使って無理やり呼び出します。

メソッド名列挙

ここを見てpublicメソッドの名前を全部列挙します。
列挙型管理なのは、コード補完が効いて扱いやすいため。タイポ怖い。

ついでに使うnamespaceも全て書いておきます。

using System;
using UnityEditor;
using UnityEngine;
using System.Reflection;
using System.Collections.Concurrent;
using System.Linq;
using System.Linq.Expressions;

public static class InternalAudioUtil
{
    enum Method
    {
        PlayClip,
        StopClip,
        PauseClip,
        ResumeClip,
        LoopClip,
        IsClipPlaying,
        StopAllClips,
        GetClipPosition,
        GetClipSamplePosition,
        SetClipSamplePosition,
        GetSampleCount,
        GetChannelCount,
        GetBitRate,
        GetBitsPerSample,
        GetFrequency,
        GetSoundSize,
        GetSoundCompressionFormat,
        GetTargetPlatformSoundCompressionFormat,
        GetAmbisonicDecoderPluginNames,
        HasPreview,
        GetImporterFromClip,
        GetMinMaxData,
        GetDuration,
        GetFMODMemoryAllocated,
        GetFMODCPUUsage,
        IsTrackerFile,
        GetMusicChannelCount,
        GetLowpassCurve,
        GetListenerPos,
        UpdateAudio,
        SetListenerTransform,
        HasAudioCallback,
        GetCustomFilterChannelCount,
        GetCustomFilterProcessTime,
        GetCustomFilterMaxIn,
        GetCustomFilterMaxOut,
    }
}

以降のコードは全てこのInternalAudioUtilクラス内に記述します。

メソッド取得・コンパイル・キャッシュ

ReflectionとExpressionをこねくり回します。
全部public staticだし、オーバーロードの曖昧性がないので楽でいいですね。
Expression構築については、neuecc先生の手法を参考にしています。

    //AudioUtil型
    static readonly Type tAudioUtil = typeof(Editor).Assembly.GetType("UnityEditor.AudioUtil");
    //コンパイル済みメソッドのキャッシュ
    static readonly ConcurrentDictionary<Method, Func<object[], object>> 
        compiled = new ConcurrentDictionary<Method, Func<object[], object>>();

    //キャッシュからメソッドを取得する。コンパイル済みでなければコンパイルしてキャッシュし、それを返す。
    static Func<object[], object> GetOrCompile(Method method)
    {
        return compiled.GetOrAdd(method, _m =>
        {
            //キャッシュが存在しなければここに来る

            //MethodInfo取得
            var m = tAudioUtil.GetMethod(_m.ToString(), BindingFlags.Static | BindingFlags.Public);

            //voidメソッドのためのreturn先ラベルを定義
            var voidTarget = Expression.Label(typeof(object));

            //引数はobject[]
            var args = Expression.Parameter(typeof(object[]), "args");
            //MethodInfoのパラメータの型に引数をキャストするExpressionの束
            var parameters = m.GetParameters()
                .Select((x, index) =>
                    Expression.Convert(
                        Expression.ArrayIndex(args, Expression.Constant(index)),
                    x.ParameterType))
                .ToArray();
            //式木構築
            var lambda = Expression.Lambda<Func<object[], object>>(
                m.ReturnType == typeof(void)
                    //voidメソッドの場合、ブロックにしてreturn default(object)する必要がある
                    ? (Expression)Expression.Block(
                        Expression.Call(null, m, parameters),
                        Expression.Return(voidTarget, Expression.Default(typeof(object))),
                        Expression.Label(voidTarget, Expression.Constant(null))
                    )
                    //返り値がある場合はCallして結果をobjectにキャストするだけ
                    : Expression.Convert(
                        Expression.Call(null, m, parameters),
                        typeof(object)),
                args);

            //コンパイルしてキャッシュしつつ返す
            return lambda.Compile();
        });
    }

呼ぶ

new object[]{...}やらキャストやらを毎回書くのは嫌なので、適当に中間メソッドを作っておいて、

    static TRet Call<TRet>(Method method) 
        => (TRet)GetOrCompile(method).Invoke(null);
    static TRet Call<T0, TRet>(Method method, T0 arg0) 
        => (TRet)GetOrCompile(method).Invoke(new object[] { arg0 });
    static TRet Call<T0, T1, TRet>(Method method, T0 arg0, T1 arg1)
        => (TRet)GetOrCompile(method).Invoke(new object[] { arg0, arg1 });
    static TRet Call<T0, T1, T2, TRet>(Method method, T0 arg0, T1 arg1, T2 arg2)
        => (TRet)GetOrCompile(method).Invoke(new object[] { arg0, arg1, arg2 });
    static TRet Call<T0, T1, T2, T3, TRet>(Method method, T0 arg0, T1 arg1, T2 arg2, T3 arg3)
        => (TRet)GetOrCompile(method).Invoke(new object[] { arg0, arg1, arg2, arg3 });

    static void Call(Method method)
        => GetOrCompile(method).Invoke(null);
    static void Call<T0>(Method method, T0 arg0) 
        => GetOrCompile(method).Invoke(new object[] { arg0 });  
    static void Call<T0, T1>(Method method, T0 arg0, T1 arg1)
        => GetOrCompile(method).Invoke(new object[] { arg0, arg1 });
    static void Call<T0, T1, T2>(Method method, T0 arg0, T1 arg1, T2 arg2)
        => GetOrCompile(method).Invoke(new object[] { arg0, arg1, arg2 });
    static void Call<T0, T1, T2, T3>(Method method, T0 arg0, T1 arg1, T2 arg2, T3 arg3)
        => GetOrCompile(method).Invoke(new object[] { arg0, arg1, arg2, arg3 });

以下のようにそれぞれのメソッドに対応する公開APIを定義します(これが一番疲れました)。
[Obsolete]を付けているPlayClipについては後述します。

    public static void PlayClip(AudioClip clip) => Call(Method.PlayClip, clip, 0, false);
    [Obsolete("The parameters <startSample> and <loop> are not working")]
    public static void PlayClip(AudioClip clip, int startSample, bool loop) => Call(Method.PlayClip, clip, startSample, loop);
    public static void StopClip(AudioClip clip) => Call(Method.StopClip, clip);
    public static void PauseClip(AudioClip clip) => Call(Method.PauseClip, clip);
    public static void ResumeClip(AudioClip clip) => Call(Method.ResumeClip, clip);
    public static void LoopClip(AudioClip clip) => Call(Method.LoopClip, clip);
    public static bool IsClipPlaying(AudioClip clip) => Call<AudioClip, bool>(Method.IsClipPlaying, clip);
    public static void StopAllClips() => Call(Method.StopAllClips);
    public static float GetClipPosition(AudioClip clip) => Call<AudioClip,float>(Method.GetClipPosition, clip);
    public static int GetClipSamplePosition(AudioClip clip) => Call<AudioClip, int>(Method.GetClipSamplePosition, clip);
    public static void SetClipSamplePosition(AudioClip clip, int iSamplePosition) => Call(Method.SetClipSamplePosition, clip, iSamplePosition);
    public static int GetSampleCount(AudioClip clip) => Call<AudioClip, int>(Method.GetSampleCount, clip);
    public static int GetChannelCount(AudioClip clip) => Call<AudioClip, int>(Method.GetChannelCount, clip);
    public static int GetBitRate(AudioClip clip) => Call<AudioClip, int>(Method.GetBitRate, clip);
    public static int GetBitsPerSample(AudioClip clip) => Call<AudioClip, int>(Method.GetBitsPerSample, clip);
    public static int GetFrequency(AudioClip clip) => Call<AudioClip, int>(Method.GetFrequency, clip);
    public static int GetSoundSize(AudioClip clip) => Call<AudioClip, int>(Method.GetSoundSize, clip);
    public static AudioCompressionFormat GetSoundCompressionFormat(AudioClip clip) => Call<AudioClip, AudioCompressionFormat>(Method.GetSoundCompressionFormat, clip);
    public static AudioCompressionFormat GetTargetPlatformSoundCompressionFormat(AudioClip clip) => Call<AudioClip, AudioCompressionFormat>(Method.GetTargetPlatformSoundCompressionFormat, clip);
    public static string[] GetAmbisonicDecoderPluginNames() => Call<string[]>(Method.GetAmbisonicDecoderPluginNames);
    public static bool HasPreview(AudioClip clip) => Call<AudioClip, bool>(Method.HasPreview, clip);
    public static AudioImporter GetImporterFromClip(AudioClip clip) => Call<AudioClip, AudioImporter>(Method.GetImporterFromClip, clip);
    public static float[] GetMinMaxData(AudioImporter importer) => Call<AudioImporter, float[]>(Method.GetMinMaxData, importer);
    public static double GetDuration(AudioClip clip) => Call<AudioClip, double>(Method.GetDuration, clip);
    public static int GetFMODMemoryAllocated() => Call<int>(Method.GetFMODMemoryAllocated);
    public static float GetFMODCPUUsage() => Call<float>(Method.GetFMODCPUUsage);
    public static bool IsTrackerFile(AudioClip clip) => Call<AudioClip, bool>(Method.IsTrackerFile, clip);
    public static int GetMusicChannelCount(AudioClip clip) => Call<AudioClip, int>(Method.GetMusicChannelCount, clip);
    public static AnimationCurve GetLowpassCurve(AudioLowPassFilter lowPassFilter) => Call<AudioLowPassFilter, AnimationCurve>(Method.GetLowpassCurve, lowPassFilter);
    public static Vector3 GetListenerPos() => Call<Vector3>(Method.GetListenerPos);
    public static void UpdateAudio() => Call(Method.UpdateAudio);
    public static void SetListenerTransform(Transform t) => Call(Method.SetListenerTransform, t);
    public static bool HasAudioCallback(MonoBehaviour behaviour) => Call<MonoBehaviour, bool>(Method.HasAudioCallback, behaviour);
    public static int GetCustomFilterChannelCount(MonoBehaviour behaviour) => Call<MonoBehaviour, int>(Method.GetCustomFilterChannelCount, behaviour);
    public static int GetCustomFilterProcessTime(MonoBehaviour behaviour) => Call<MonoBehaviour, int>(Method.GetCustomFilterProcessTime, behaviour);
    public static float GetCustomFilterMaxIn(MonoBehaviour behaviour, int channel) => Call<MonoBehaviour, int, float>(Method.GetCustomFilterMaxIn, behaviour, channel);
    public static float GetCustomFilterMaxOut(MonoBehaviour behaviour, int channel) => Call<MonoBehaviour, int, float>(Method.GetCustomFilterMaxOut, behaviour, channel);

これで準備OKです。

使用上の注意

大体メソッド名と引数名から予測できる通りの挙動をしますが、いくつかかなりヤバめの注意点があります。

PlayClip()の第2引数以降は指定しても何も起きない

PlayClip(AudioClip clip, int startSample, bool loop)
見るからに再生開始位置とループ有無を指定できそうですが、できません。
一応全て引数に取れるメソッドも定義していますが、この理由により[Obsolete]を付けています。

なお、代わりにSetClipSamplePosition()LoopClip()を同時に使うことで所望の挙動が得られます。

複数のAudioClipを再生すると、最後に再生したもの以外停止不能になる

そんなバカな、StopAllClips()があるじゃないか。私もそう思いました。

StopAllClips()は、最後に再生したclipを停止します。

clipを明示的に指定しなくてもいいのでとても便利ですね!!!!!

この状態になると、再生が終了するか、Unityを再起動するまで他のAudioClipは停止できません。
そのため、クロスフェードのプレビューとかは無理です。残念。

全てのメソッドの動作確認はしていない

きっとまだ罠があるので、ぜひ踏み抜いて教えてください。

参考リンク

AudioUtilクラス
https://github.com/Unity-Technologies/UnityCsReference/blob/master/Editor/Mono/Audio/Bindings/AudioUtil.bindings.cs

neuecc先生によるReflectionの高速化手法紹介
http://neue.cc/2014/01/27_446.html

Rtyper氏が作った旧AudioUtilのラッパー(とその問題点(上述したものと同じ))
https://forum.unity.com/threads/reflected-audioutil-class-for-making-audio-based-editor-extensions.308133/

6
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
3