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

#UnityEditor.AudioUtilとは
エディタ上でAudioClipを扱うための内部APIです。
リファレンスは存在しませんが、[その全貌はここで見ることができます。](https://github.com/Unity-Technologies/UnityCsReference/blob/master/Editor/Mono/Audio/Bindings/AudioUtil.bindings.cs)
全貌と言ってもほぼexternなのでメソッド名や型しか分かりませんが。

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

私の場合、AudioClip（のラッパークラス）にこんなカスタムPropertyDrawerを定義するのに使いました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/84499/e11c8953-88cb-2da3-aac4-0e0530555a3b.png)

#準備
内部APIなので、当然普通に呼ぶことはできません。Reflectionを使って無理やり呼び出します。
## メソッド名列挙
[ここ](https://github.com/Unity-Technologies/UnityCsReference/blob/master/Editor/Mono/Audio/Bindings/AudioUtil.bindings.cs)を見てpublicメソッドの名前を全部列挙します。
列挙型管理なのは、コード補完が効いて扱いやすいため。タイポ怖い。

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

```cs
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先生の手法](http://neue.cc/2014/01/27_446.html)を参考にしています。

```cs
    //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[]{...}`やらキャストやらを毎回書くのは嫌なので、適当に中間メソッドを作っておいて、

```cs
	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については後述します。

```cs
	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()`の第２引数以降は指定しても何も起きない
`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/