OVRLipSyncMicInputを使うときにマイクの選択を簡単にしたい

7 min read読了の目安(約6500字

OVRLipSyncMicInputを使うときにマイクの選択がめんどくさかった

VRMモデルをOVRLipSyncを使って口を動かそうと思った時にスムーズにマイクの切り替えができませんでした。 OVRLipSyncMicInput.cs を使って行おうとしたら

Play前 Play後

使っているマイクを示している一番下にある項目 Selected Device にはマイクを配列で取得して最初の要素の文字列が入るようになっています。それが使うものとして動作します。切り替えるには OVRLipSyncMicInput.cs をいじるか外部から差し込むしかなかったので差し込むことにしました。

作ったもの

今回作ったのはVRMモデルにLipSyncをするものになりますのでその部分を含んだcsになります。UnityEditorのみの動作を想定しています。

VRMLipSyncContextMorphTarget.cs
VRMLipSyncContextMorphTarget.cs
using System.Collections.Generic;
using System.Linq;
#if UNITY_EDITOR
using UnityEditor;
#endif
using UnityEngine;
using VRM;

namespace Component
{
    [RequireComponent(typeof(OVRLipSyncMicInput))]
    [DefaultExecutionOrder(10000)]
    public class VRMLipSyncContextMorphTarget : OVRLipSyncContext
    {

        [SerializeField] private VRMBlendShapeProxy shapeProxy;
        [SerializeField, Range(0.0f, 2.0f)] private float visemesVolume = 2.0f;
        
	[HideInInspector] public string selected;

        private BlendShapePreset[] visemePresets = {
            BlendShapePreset.Neutral,
            BlendShapePreset.Unknown,
            BlendShapePreset.Unknown,
            BlendShapePreset.Unknown,
            BlendShapePreset.Unknown,
            BlendShapePreset.Unknown,
            BlendShapePreset.Unknown,
            BlendShapePreset.Unknown,
            BlendShapePreset.Unknown,
            BlendShapePreset.Unknown,
            BlendShapePreset.A,
            BlendShapePreset.E,
            BlendShapePreset.I,
            BlendShapePreset.O,
            BlendShapePreset.U
        };

        private void Start()
        {
            var micInput = GetComponent<OVRLipSyncMicInput>();
            micInput.StopMicrophone();
            micInput.selectedDevice = selected;
            micInput.StartMicrophone();
        }

        void LateUpdate()
        {
            if (shapeProxy == null) return;
            var frame = GetCurrentPhonemeFrame();
            var values = visemePresets.Select((preset, i) => BlendShapePairBuilder(preset, frame.Visemes[i]));
            shapeProxy.SetValues(values);
        }

        private KeyValuePair<BlendShapeKey, float> BlendShapePairBuilder(BlendShapePreset preset, float viseme)
        {
            return new KeyValuePair<BlendShapeKey, float>(BlendShapeKey.CreateFromPreset(preset), viseme * visemesVolume);
        }
    }
    
#if UNITY_EDITOR

    [CustomEditor(typeof(VRMLipSyncContextMorphTarget))]
    public class VRMLipSyncContextMorphTargetEditor : Editor
    {

        private VRMLipSyncContextMorphTarget vrmLipSyncContextMorphTarget;
        private int selectedIndex;
        private string selectedIndexKey = $"{nameof(VRMLipSyncContextMorphTarget)}.{nameof(selectedIndex)}";

        void OnEnable()
        {
            vrmLipSyncContextMorphTarget = (VRMLipSyncContextMorphTarget)target;
        }

        public override void OnInspectorGUI()
        {
            DrawDefaultInspector();
            
            EditorGUI.BeginChangeCheck();

            selectedIndex = EditorPrefs.GetInt(selectedIndexKey);
            
            var micDevices = Microphone.devices;
            var label = "Select Microphone";
            var index = micDevices.Length > 0 && selectedIndex < micDevices.Length ? EditorGUILayout.Popup(label, selectedIndex, micDevices) : -1;
            
            if (!EditorGUI.EndChangeCheck()) return;
            Undo.RecordObject(vrmLipSyncContextMorphTarget, nameof(VRMLipSyncContextMorphTarget));
            selectedIndex = index;
            EditorPrefs.SetInt(selectedIndexKey, selectedIndex);
            vrmLipSyncContextMorphTarget.selected = micDevices[index];
        }
    }
#endif
}

Inspectorで表示すると以下のようになります。

そしてマイクを選択するときはこう

これでだいぶ楽になりました。

軽い解説

VRMに反映している部分は省きます。

VRMLipSyncContextMorphTarget.cs から

    [RequireComponent(typeof(OVRLipSyncMicInput))]
    // OVRLipSyncMicInputのStart()の後に切り替えたいため
    [DefaultExecutionOrder(10000)]
    public class VRMLipSyncContextMorphTarget : OVRLipSyncContext
    {

        [SerializeField] private VRMBlendShapeProxy shapeProxy;
        [SerializeField, Range(0.0f, 2.0f)] private float visemesVolume = 2.0f;
        
	// Inspectorで選択したマイクの名前を保持する
	[HideInInspector] public string selected;

	~~~ 省略 ~~~

        private void Start()
        {
            // OVRLipSyncMicInputの初期化が終わってるはずなので切り替え処理を入れる
            var micInput = GetComponent<OVRLipSyncMicInput>();
            micInput.StopMicrophone();
            micInput.selectedDevice = selected;
            micInput.StartMicrophone();
        }
	
	~~~ 省略 ~~~

次は VRMLipSyncContextMorphTargetEditor

    [CustomEditor(typeof(VRMLipSyncContextMorphTarget))]
    public class VRMLipSyncContextMorphTargetEditor : Editor
    {

        private VRMLipSyncContextMorphTarget vrmLipSyncContextMorphTarget;
        private int selectedIndex;
	// EditorPrefsに保存するときのキー
        private string selectedIndexKey = $"{nameof(VRMLipSyncContextMorphTarget)}.{nameof(selectedIndex)}";

        void OnEnable()
        {
            vrmLipSyncContextMorphTarget = (VRMLipSyncContextMorphTarget)target;
        }

        public override void OnInspectorGUI()
        {
            DrawDefaultInspector();
            
            EditorGUI.BeginChangeCheck();

            selectedIndex = EditorPrefs.GetInt(selectedIndexKey);
            
	    // UnityEngineで用意されているマイクの取得処理
            var micDevices = Microphone.devices;
            var label = "Select Microphone";
	    // Inspectorで選択したマイクを取得する
            var index = micDevices.Length > 0 && selectedIndex < micDevices.Length ? EditorGUILayout.Popup(label, selectedIndex, micDevices) : -1;
            
	    // Popupで変更がなければ処理を行わない
            if (!EditorGUI.EndChangeCheck()) return;
            Undo.RecordObject(vrmLipSyncContextMorphTarget, nameof(VRMLipSyncContextMorphTarget));
            selectedIndex = index;
            EditorPrefs.SetInt(selectedIndexKey, selectedIndex);
	    // VRMLipSyncContextMorphTargetに保持させる
            vrmLipSyncContextMorphTarget.selected = micDevices[index];
        }
    }

まとめ

Inspector拡張久しぶりにやりましたが簡単に選択UI作れたのでよかったです。作った後に、切り替えくらい実は別Componentとかでデフォルトにあるのでは…?と思いましたが考えないことにしました。