#UnityEditor.AudioUtilとは
エディタ上でAudioClipを扱うための内部APIです。
リファレンスは存在しませんが、その全貌はここで見ることができます。
全貌と言ってもほぼexternなのでメソッド名や型しか分かりませんが。
#使うと何ができるの?
AudioSourceを使わずに、Editor上でAudioClipを再生できます。
AudioClipを選択したときにInspector下部にプレビューとしてサウンドプレイヤーが表示されますが、あんな感じのGUIを自前定義のカスタムInspectorやEditorWindow上に構築できるわけです。
私の場合、AudioClip(のラッパークラス)にこんなカスタムPropertyDrawerを定義するのに使いました。
#準備
内部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/