Quantcast
Channel: C#タグが付けられた新着記事 - Qiita
Viewing all articles
Browse latest Browse all 9304

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

$
0
0

UnityEditor.AudioUtilとは

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

使うと何ができるの?

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

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

準備

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

メソッド名列挙

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

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

usingSystem;usingUnityEditor;usingUnityEngine;usingSystem.Reflection;usingSystem.Collections.Concurrent;usingSystem.Linq;usingSystem.Linq.Expressions;publicstaticclassInternalAudioUtil{enumMethod{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型staticreadonlyTypetAudioUtil=typeof(Editor).Assembly.GetType("UnityEditor.AudioUtil");//コンパイル済みメソッドのキャッシュstaticreadonlyConcurrentDictionary<Method,Func<object[],object>>compiled=newConcurrentDictionary<Method,Func<object[],object>>();//キャッシュからメソッドを取得する。コンパイル済みでなければコンパイルしてキャッシュし、それを返す。staticFunc<object[],object>GetOrCompile(Methodmethod){returncompiled.GetOrAdd(method,_m=>{//キャッシュが存在しなければここに来る//MethodInfo取得varm=tAudioUtil.GetMethod(_m.ToString(),BindingFlags.Static|BindingFlags.Public);//voidメソッドのためのreturn先ラベルを定義varvoidTarget=Expression.Label(typeof(object));//引数はobject[]varargs=Expression.Parameter(typeof(object[]),"args");//MethodInfoのパラメータの型に引数をキャストするExpressionの束varparameters=m.GetParameters().Select((x,index)=>Expression.Convert(Expression.ArrayIndex(args,Expression.Constant(index)),x.ParameterType)).ToArray();//式木構築varlambda=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);//コンパイルしてキャッシュしつつ返すreturnlambda.Compile();});}

呼ぶ

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

staticTRetCall<TRet>(Methodmethod)=>(TRet)GetOrCompile(method).Invoke(null);staticTRetCall<T0,TRet>(Methodmethod,T0arg0)=>(TRet)GetOrCompile(method).Invoke(newobject[]{arg0});staticTRetCall<T0,T1,TRet>(Methodmethod,T0arg0,T1arg1)=>(TRet)GetOrCompile(method).Invoke(newobject[]{arg0,arg1});staticTRetCall<T0,T1,T2,TRet>(Methodmethod,T0arg0,T1arg1,T2arg2)=>(TRet)GetOrCompile(method).Invoke(newobject[]{arg0,arg1,arg2});staticTRetCall<T0,T1,T2,T3,TRet>(Methodmethod,T0arg0,T1arg1,T2arg2,T3arg3)=>(TRet)GetOrCompile(method).Invoke(newobject[]{arg0,arg1,arg2,arg3});staticvoidCall(Methodmethod)=>GetOrCompile(method).Invoke(null);staticvoidCall<T0>(Methodmethod,T0arg0)=>GetOrCompile(method).Invoke(newobject[]{arg0});staticvoidCall<T0,T1>(Methodmethod,T0arg0,T1arg1)=>GetOrCompile(method).Invoke(newobject[]{arg0,arg1});staticvoidCall<T0,T1,T2>(Methodmethod,T0arg0,T1arg1,T2arg2)=>GetOrCompile(method).Invoke(newobject[]{arg0,arg1,arg2});staticvoidCall<T0,T1,T2,T3>(Methodmethod,T0arg0,T1arg1,T2arg2,T3arg3)=>GetOrCompile(method).Invoke(newobject[]{arg0,arg1,arg2,arg3});

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

publicstaticvoidPlayClip(AudioClipclip)=>Call(Method.PlayClip,clip,0,false);[Obsolete("The parameters <startSample> and <loop> are not working")]publicstaticvoidPlayClip(AudioClipclip,intstartSample,boolloop)=>Call(Method.PlayClip,clip,startSample,loop);publicstaticvoidStopClip(AudioClipclip)=>Call(Method.StopClip,clip);publicstaticvoidPauseClip(AudioClipclip)=>Call(Method.PauseClip,clip);publicstaticvoidResumeClip(AudioClipclip)=>Call(Method.ResumeClip,clip);publicstaticvoidLoopClip(AudioClipclip)=>Call(Method.LoopClip,clip);publicstaticboolIsClipPlaying(AudioClipclip)=>Call<AudioClip,bool>(Method.IsClipPlaying,clip);publicstaticvoidStopAllClips()=>Call(Method.StopAllClips);publicstaticfloatGetClipPosition(AudioClipclip)=>Call<AudioClip,float>(Method.GetClipPosition,clip);publicstaticintGetClipSamplePosition(AudioClipclip)=>Call<AudioClip,int>(Method.GetClipSamplePosition,clip);publicstaticvoidSetClipSamplePosition(AudioClipclip,intiSamplePosition)=>Call(Method.SetClipSamplePosition,clip,iSamplePosition);publicstaticintGetSampleCount(AudioClipclip)=>Call<AudioClip,int>(Method.GetSampleCount,clip);publicstaticintGetChannelCount(AudioClipclip)=>Call<AudioClip,int>(Method.GetChannelCount,clip);publicstaticintGetBitRate(AudioClipclip)=>Call<AudioClip,int>(Method.GetBitRate,clip);publicstaticintGetBitsPerSample(AudioClipclip)=>Call<AudioClip,int>(Method.GetBitsPerSample,clip);publicstaticintGetFrequency(AudioClipclip)=>Call<AudioClip,int>(Method.GetFrequency,clip);publicstaticintGetSoundSize(AudioClipclip)=>Call<AudioClip,int>(Method.GetSoundSize,clip);publicstaticAudioCompressionFormatGetSoundCompressionFormat(AudioClipclip)=>Call<AudioClip,AudioCompressionFormat>(Method.GetSoundCompressionFormat,clip);publicstaticAudioCompressionFormatGetTargetPlatformSoundCompressionFormat(AudioClipclip)=>Call<AudioClip,AudioCompressionFormat>(Method.GetTargetPlatformSoundCompressionFormat,clip);publicstaticstring[]GetAmbisonicDecoderPluginNames()=>Call<string[]>(Method.GetAmbisonicDecoderPluginNames);publicstaticboolHasPreview(AudioClipclip)=>Call<AudioClip,bool>(Method.HasPreview,clip);publicstaticAudioImporterGetImporterFromClip(AudioClipclip)=>Call<AudioClip,AudioImporter>(Method.GetImporterFromClip,clip);publicstaticfloat[]GetMinMaxData(AudioImporterimporter)=>Call<AudioImporter,float[]>(Method.GetMinMaxData,importer);publicstaticdoubleGetDuration(AudioClipclip)=>Call<AudioClip,double>(Method.GetDuration,clip);publicstaticintGetFMODMemoryAllocated()=>Call<int>(Method.GetFMODMemoryAllocated);publicstaticfloatGetFMODCPUUsage()=>Call<float>(Method.GetFMODCPUUsage);publicstaticboolIsTrackerFile(AudioClipclip)=>Call<AudioClip,bool>(Method.IsTrackerFile,clip);publicstaticintGetMusicChannelCount(AudioClipclip)=>Call<AudioClip,int>(Method.GetMusicChannelCount,clip);publicstaticAnimationCurveGetLowpassCurve(AudioLowPassFilterlowPassFilter)=>Call<AudioLowPassFilter,AnimationCurve>(Method.GetLowpassCurve,lowPassFilter);publicstaticVector3GetListenerPos()=>Call<Vector3>(Method.GetListenerPos);publicstaticvoidUpdateAudio()=>Call(Method.UpdateAudio);publicstaticvoidSetListenerTransform(Transformt)=>Call(Method.SetListenerTransform,t);publicstaticboolHasAudioCallback(MonoBehaviourbehaviour)=>Call<MonoBehaviour,bool>(Method.HasAudioCallback,behaviour);publicstaticintGetCustomFilterChannelCount(MonoBehaviourbehaviour)=>Call<MonoBehaviour,int>(Method.GetCustomFilterChannelCount,behaviour);publicstaticintGetCustomFilterProcessTime(MonoBehaviourbehaviour)=>Call<MonoBehaviour,int>(Method.GetCustomFilterProcessTime,behaviour);publicstaticfloatGetCustomFilterMaxIn(MonoBehaviourbehaviour,intchannel)=>Call<MonoBehaviour,int,float>(Method.GetCustomFilterMaxIn,behaviour,channel);publicstaticfloatGetCustomFilterMaxOut(MonoBehaviourbehaviour,intchannel)=>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/


Viewing all articles
Browse latest Browse all 9304

Trending Articles