VRMモデルをSALSAでリップシンクさせる

# はじめに
VRMモデルのリップシンクを実装してみました。ARKitを使ったアプリにも対応できるように、iOS端末で動作するリップシンクが必要だったので、[SALSA](https://assetstore.unity.com/packages/tools/animation/salsa-with-randomeyes-16944)というアセットを使いました。
![LipSync.gif](https://qiita-image-store.s3.amazonaws.com/0/162000/87cb235c-a33f-ba19-b866-0cafbd4167d9.gif)
サンプルプロジェクトこちら ⇒ [VRMLipSyncSample](https://github.com/sotanmochi/VRMLipSyncSample/releases/tag/v1.0)
VRMモデルをランタイムロードする場合のサンプルとかOVRLipSyncを使ったサンプルも一緒に入ってます。

# オーディオファイルの再生に合わせたリップシンク
## SALSAのインポート
アセットストアから[SALSA](https://assetstore.unity.com/packages/tools/animation/salsa-with-randomeyes-16944)をインポートする
![ImportSalsa.png](https://qiita-image-store.s3.amazonaws.com/0/162000/69e82700-8a8f-3448-c8d6-c08f0a6bb6b0.png)

## シーン作成
新しいシーンを作成してVRMモデルを配置する
![SceneSetup.png](https://qiita-image-store.s3.amazonaws.com/0/162000/77fb5854-7fa6-8743-ec72-53bd71a57174.png)

## リップシンクの設定
空のゲームオブジェクトを作成して「Salsa3D」をアタッチする。
ゲームオブジェクトの名前は「SampleVoice」などに変更しておく。
![AttachSalsa.png](https://qiita-image-store.s3.amazonaws.com/0/162000/4dab0a87-1797-0f59-d91d-09e45febf800.png)

Salsa3DのAudioSourceの値を確認しておく
![AttachSalsa2.png](https://qiita-image-store.s3.amazonaws.com/0/162000/5ac8d179-8ab9-080a-dcf2-973bdb8f27a6.png)

Salsa3DのSpeech Propertiesの値を調整する（※この画像に示されている値が適切とは限らない）
![AttachSalsa3.png](https://qiita-image-store.s3.amazonaws.com/0/162000/54ddf2f7-c445-f058-ca0a-0e25d5b78ec0.png)

AudioSourceのAudioClipにサンプルボイスのオーディオファイルをセットする
![AttachSalsa4.png](https://qiita-image-store.s3.amazonaws.com/0/162000/bfcf16f5-a565-2147-ebfe-0d09286c21cb.png)

VRMモデルのゲームオブジェクトに以下のようなスクリプトをアタッチする

```cs:VRMLipSyncSalsaTarget.cs

using UnityEngine;
using VRM;
using CrazyMinnow.SALSA;

public class VRMLipSyncSalsaTarget : MonoBehaviour
{
	public BlendShapePreset SaySmallBlendShapeKey = BlendShapePreset.I;
	public BlendShapePreset SayMediumBlendShapeKey = BlendShapePreset.E;
	public BlendShapePreset SayLargeBlendShapeKey = BlendShapePreset.O;

	public VRMBlendShapeProxy blendShapeProxy;
	public Salsa3D salsa3d;

	void Start()
	{
		if(salsa3d == null)
		{
			salsa3d = GetComponent<Salsa3D>();
		}
		if(blendShapeProxy == null)
		{
			blendShapeProxy = GetComponent<VRMBlendShapeProxy>();
		}
	}

	void Update()
	{
		if((salsa3d != null) && (blendShapeProxy != null))
		{
			SalsaBlendAmounts blendAmounts = salsa3d.sayAmount;

			// Change the value range before set the BlendShape values.
			float saySmall = blendAmounts.saySmall / 100.0f;
			float sayMedium = blendAmounts.sayMedium / 100.0f;
			float sayLarge = blendAmounts.sayLarge / 100.0f;

			blendShapeProxy.SetValue(SaySmallBlendShapeKey, saySmall);
			blendShapeProxy.SetValue(SayMediumBlendShapeKey, sayMedium);
			blendShapeProxy.SetValue(SayLargeBlendShapeKey, sayLarge);
		}
	}
}
```
![AttachSalsaTarget2.png](https://qiita-image-store.s3.amazonaws.com/0/162000/f84764d8-7954-c317-8b5e-aab58c9f45d0.png)

スクリプトをアタッチした後、BlendShapeProxyとSalsa3Dをセットする
![AttachSalsaTarget3.png](https://qiita-image-store.s3.amazonaws.com/0/162000/07ddcee4-9c71-c410-377c-4a79b3ca7471.png)

シーンを再生すると、サンプルボイスに合わせたリップシンクが実現できているはずです。
![LipSync.gif](https://qiita-image-store.s3.amazonaws.com/0/162000/5fcb8044-76a3-3e61-a7cc-735fb642ed4a.gif)

# マイク入力に合わせたリップシンク
上記で作成したシーンの「SampleVoice」というオブジェクトに以下のようなスクリプトをアタッチします。
シーンを再生するとマイク入力に合わせたリップシンクが実現できているはずです。ただし、マイク入力の音がミュートされずに再生されて聞こえてしまいます。

```cs:MicrophoneInputSource.cs

using UnityEngine;
using UnityEngine.Audio;

[RequireComponent(typeof(AudioSource))]
public class MicrophoneInputSource : MonoBehaviour
{
	private AudioSource audioSource;

	void Start()
	{
		if(Microphone.devices.Length > 0)
		{
			audioSource = GetComponent<AudioSource>();
			audioSource.clip = Microphone.Start(null, true, 3, 44100);
			audioSource.loop = true;

			// Use the Attenuation of AudioMixer to mute microphone input.
			audioSource.mute = false;

			while((Microphone.GetPosition("") <= 0)){}
			audioSource.Play();
		}
	}

	void Update()
	{
		if(audioSource != null)
		{
			float volume = GetAveragedVolume();
			Debug.Log("Mic input volume: " + volume);
		}
	}

	float GetAveragedVolume()
	{ 
		float[] data = new float[256];
		float a = 0;
		audioSource.GetOutputData(data,0);
		foreach(float s in data)
		{
			a += Mathf.Abs(s);
		}
		return a / 256.0f;
	}
}
```
![AttachMicInput.png](https://qiita-image-store.s3.amazonaws.com/0/162000/31458aeb-63b1-d20c-bd8a-112eabff3383.png)

# マイク入力に合わせたリップシンクの改良
AudioMixerを新規作成してMasterのAttenuationを-80.00dBに設定する
![AudioMixer.png](https://qiita-image-store.s3.amazonaws.com/0/162000/eda1e468-9cb0-f0a0-1313-25855f6f570f.png)

上記で作成・設定したAudioMixerのMasterをAudioClipのOutputにセットする
![AudioMixer2.png](https://qiita-image-store.s3.amazonaws.com/0/162000/8a11ed62-fd96-9c6b-bf2e-f8130fbb1c00.png)

これで、マイク入力の音が聞こえない状態で、マイク入力に合わせたリップシンクが実現できているはずです。

最初は「audioSource.mute = true」で実現できるかと思っていましたが、その方法だとマイク入力の音が取得されず、リップシンクできませんでした。
「[UnityでAudioSource.Mute=trueにした時にMicrophone(マイク)から音量が取得できない時の対処法](https://qiita.com/CST_negi/items/c8862f8d05dd9e871917)」という記事を参考にしたら上手くいきました。

# おわりに
[SALSA](https://assetstore.unity.com/packages/tools/animation/salsa-with-randomeyes-16944)というアセットを使ってVRMモデルをリップシンクさせてみました。
[サンプルプロジェクト](https://github.com/sotanmochi/VRMLipSyncSample/releases/tag/v1.0)に含まれているSALSAを使ったシーンは、iPadで実行してリップシンクが動くことを確認しました。
この記事では説明していませんが、VRMモデルをランタイムロードする場合は手順の一部（「VRMLipSyncSalsaTargetをアタッチする」など）を実行時に処理します。

# 参考情報
- [[iOSでも動く] SALSAでバーチャルYouTuber向けのリップシンクを導入する方法](https://qiita.com/nkjzm/items/18c6ab7f5428076ab413)
- [【AssetStore】リップシンクのアセット紹介します！【SALSA With RandomEyes】](https://www.youtube.com/watch?v=qjpsh500FD4)
- [Unity のオーディオの再生・エフェクト・解析周りについてまとめてみた](http://tips.hecomi.com/entry/2014/11/11/021147)
- [Unityでマイクのボリュームをリアルタイムに取得する](https://www.jonki.net/entry/20130607/1370622815)
- [UnityでAudioSource.Mute=trueにした時にMicrophone(マイク)から音量が取得できない時の対処法](https://qiita.com/CST_negi/items/c8862f8d05dd9e871917)