Skip to content

Instantly share code, notes, and snippets.

@Tsumio
Last active May 19, 2018 05:40
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Tsumio/e198e7dfe1c9cbdfc32e4d929a356007 to your computer and use it in GitHub Desktop.
Save Tsumio/e198e7dfe1c9cbdfc32e4d929a356007 to your computer and use it in GitHub Desktop.
EmofuriStaticLipSynch
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// リップシンクのソースを表すインターフェイス
/// </summary>
public interface ILipSynchSources {
////=============================================================================
//// Properties
////
////=============================================================================
/// <summary>
/// リップシンクするCSVデータのファイル名を返す
/// </summary>
string FileName { get; }
////=============================================================================
//// Public Method
////
////=============================================================================
/// <summary>
/// リップシンク開始時に呼ぶべきメソッド
/// </summary>
/// <param name="audioSource">親のAudioSource</param>
/// <param name="endValue">再生終了時のface_talkの値</param>
void StartLipSynch(AudioSource audioSource, float endValue);
/// <summary>
/// Updateメソッドで呼ぶべきメソッド
/// </summary>
void UpdateLipSynch();
/// <summary>
/// リップシンクを中断する
/// </summary>
void AbortLipSynch();
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
/// <summary>
/// リップシンクのソースをまとめているクラス。
/// </summary>
[Serializable]
public class LipSynchSources : ILipSynchSources {
////=============================================================================
//// Local Field
////
////=============================================================================
/// <summary>
/// リップシンクさせたいえもふりのオブジェクト本体
/// </summary>
[SerializeField]
private EmotePlayer _emotePlayer;
/// <summary>
/// 再生したい音声
/// </summary>
[SerializeField]
private AudioClip _audioClip;
/// <summary>
/// リップシンクのデータが入っているcsvファイル
/// </summary>
[SerializeField]
private TextAsset _csvSource;
/// <summary>
/// リップシンクのデータを出力したときのFPS
/// </summary>
[SerializeField]
private float _fps = 60.0f;
/// <summary>
/// 1フレームにかかる秒数(リップシンクのデータの1区切りはこの秒数で経過する)
/// </summary>
private float _eachFrameSec = 0;
/// <summary>
/// 経過した秒数
/// ここにTime.deltaTimeを加算して、EachFrameSecより小さくなるまでDequeueさせる
/// </summary>
private float _elapsedSec = 0;
/// <summary>
/// リップシンク用のCSVデータを保持するオリジナルのキュー
/// </summary>
private Queue<float> _originCsvData;
/// <summary>
/// リップシンク中に利用するCSVデータを保持するキュー
/// ここにオリジナルのキューをコピーする
/// </summary>
private Queue<float> _currentCsvData;
/// <summary>
/// リップシンクを開始するかどうか
/// </summary>
private bool _shouldPlayLipSynch;
/// <summary>
/// リップシンク終了時のface_talkの値
/// </summary>
private float _endValue = 0;
/// <summary>
/// 対象の変数ラベル
/// </summary>
private readonly string _variableLabel = "face_talk";
////=============================================================================
//// Properties
////
////=============================================================================
/// <summary>
/// リップシンクするCSVデータのファイル名を返す
/// </summary>
public string FileName {
get {
return _csvSource.name;
}
}
/// <summary>
/// リップシンク用のCSVデータを保持するキュー
/// </summary>
private Queue<float> OriginCsvData {
get {
if(_originCsvData == null) {
InitializeQueue();
}
return _originCsvData;
}
set {
_originCsvData = value;
}
}
/// <summary>
/// 1フレームにかかる秒数(リップシンクのデータの1区切りはこの秒数で経過する)
/// </summary>
private float EachFrameSec {
get {
if(_eachFrameSec == 0) {
_eachFrameSec = 1.0f / _fps;
}
return _eachFrameSec;
}
}
/// <summary>
/// リップシンクのデータを保存しているキューをDequeueすべきかどうか
/// </summary>
private bool ShouldDequeue {
get {
return _elapsedSec > EachFrameSec;
}
}
/// <summary>
/// 現在のCsvデータがキューを保持しているかどうか
/// </summary>
private bool HasQueue => _currentCsvData.Count > 0;
////=============================================================================
//// Public Method
////
////=============================================================================
/// <summary>
/// リップシンク開始時に呼ぶべきメソッド
/// </summary>
public void StartLipSynch(AudioSource audioSource, float endValue = 0) {
//オリジナルキューをコピーする(参照は別にする)
_currentCsvData = new Queue<float>(OriginCsvData);
//親のAudioSourceのクリップに、適切なAudioClipを設定する
audioSource.clip = _audioClip;
//リップシンクを開始するフラグを立て、音声も再生する
_shouldPlayLipSynch = true;
_endValue = endValue;
_elapsedSec = 0;
audioSource.Play();
}
/// <summary>
/// リップシンクを更新する
/// </summary>
public void UpdateLipSynch() {
//リップシンクするべきでなければ即リターン
if(!_shouldPlayLipSynch) {
return;
}
//CSVデータがなくなるまでDequeueしてリップの動きを更新する
if(HasQueue) {
RefreshLip();
} else {
EndLipSynch();
}
}
/// <summary>
/// リップシンクを中断する
/// </summary>
public void AbortLipSynch() {
_shouldPlayLipSynch = false;
}
////=============================================================================
//// Private Method
////
////=============================================================================
/// <summary>
/// セットされたCSVデータをもとにキューを作成する
/// 例外処理は特にしていない(無効なデータなら早めに落としたいため)
/// </summary>
/// <returns></returns>
private void InitializeQueue() {
//余計な空白を削ってから「,」で分割
var csvArray = _csvSource.text.Trim().Split(',');
//キューにCSVデータを登録していく
var tempQueue = new Queue<float>();
foreach(var csv in csvArray) {
tempQueue.Enqueue(float.Parse(csv));
}
//完成したキューを代入する
OriginCsvData = tempQueue;
}
/// <summary>
/// リップの動きを新しいものに変える
/// </summary>
private void RefreshLip() {
//経過時間を加算
_elapsedSec += Time.deltaTime;
//経過時間分だけDequeueする
while(ShouldDequeue) {
_emotePlayer.SetVariable(_variableLabel, _currentCsvData.Dequeue());
_elapsedSec -= EachFrameSec;
}
}
/// <summary>
/// リップシンクを終了する
/// </summary>
private void EndLipSynch() {
//CSVデータがなくなったら、リップシンクするべきでないというフラグを立てる
//Note:CSVデータがなくなったときに口を完全に閉じるようにしているが、開きたい場合もあるか?
_emotePlayer.SetVariable(_variableLabel, _endValue);
_shouldPlayLipSynch = false;
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
using System.Linq;
/// <summary>
/// 静的なリップシンクをおこなうためのクラス。
/// 1キャラクターが1つのAudioSourceを持つことを前提として作られている。
/// つまり、1キャラクターは同時に1つまでしかリップシンクしない(他キャラクターとは排他的な関係にある)。
/// </summary>
public class TsumioLipSynchController : MonoBehaviour {
////=============================================================================
//// Local Field
////
////=============================================================================
/// <summary>
/// リップシンク用のCSVデータを保持するキュー
/// </summary>
private Queue<float> _csvData;
/// <summary>
/// リップシンクのソースたち
/// </summary>
/// Note:ILipSynchSourcesを実装しているクラスなら何でもOKなのだが、具象クラスでないとUnityのインスペクタに表示されないのでこうしている。
/// なので、ILipSynchSourcesを実装している他クラスに付け替えることは問題ない(例えば外部のストレージからデータをもってくるクラスが考えられる)
[SerializeField]
private List<LipSynchSources> _sources;
/// <summary>
/// 音声を再生するのに使うAudioSource。
/// </summary>
[SerializeField]
private AudioSource _audioSource;
/// <summary>
/// ソースとファイル名を結びつけているディクショナリ
/// </summary>
private IDictionary<string, LipSynchSources> _sourceDict;
/// <summary>
/// 前回のリップシンク名。中断処理に使用する
/// </summary>
private string _prevLipSynchName;
////=============================================================================
//// Properties
////
////=============================================================================
/// <summary>
/// ソースとファイル名を結びつけているディクショナリ
/// </summary>
private IDictionary<string, LipSynchSources> SourceDict {
get {
if(_sourceDict == null) {
InitializeSourceDictionary();
}
return _sourceDict;
}
}
////=============================================================================
//// MonoBehaviour
////
////=============================================================================
public void Update() {
//リップシンクの更新
_sources.ForEach(s => s.UpdateLipSynch());
}
////=============================================================================
//// Public Method
////
////=============================================================================
/// <summary>
/// fileNameのCSVファイルをリップシンクさせ、音声を再生する。
/// リップシンク終了時のface_talkの値は0。
/// </summary>
/// <param name="fileName">リップシンクさせたいCSVファイル名</param>
public void StartVoiceAction(string fileName) {
StartVoiceAction(fileName, 0);
}
/// <summary>
/// fileNameのCSVファイルをリップシンクさせ、音声を再生する
/// </summary>
/// <param name="fileName">リップシンクさせたいCSVファイル名</param>
/// <param name="endValue">リップシンク終了時のface_talkの値</param>
public void StartVoiceAction(string fileName, float endValue) {
//ソースファイルが存在しないなら、即リターン
if(!TryGetSource(fileName)) {
return;
}
//前回のリップシンクを中断する
AbortPrevLipSynch(_prevLipSynchName);
//今回のリップシンク名を保存
_prevLipSynchName = fileName;
//リップシンクのアクションを開始
StartLipSynch(SourceDict[fileName], endValue);
}
/// <summary>
/// ランダムにCSVファイルを選択し、音声を再生する
/// </summary>
public void StartVoiceActionAtRandom() {
var fileName = SourceDict.ElementAt(UnityEngine.Random.Range(0, SourceDict.Count)).Value.FileName;
StartVoiceAction(fileName);
}
////=============================================================================
//// Private Method
////
////=============================================================================
/// <summary>
/// リップシンク音声をセットし、音声の再生を開始する
/// </summary>
/// <param name="target"></param>
private void StartLipSynch(ILipSynchSources target, float endValue) {
target.StartLipSynch(_audioSource, endValue);
}
/// <summary>
/// 前回のリップシンクを中断する
/// </summary>
private void AbortPrevLipSynch(string fileName) {
//無効な内容ならば即リターン
if(string.IsNullOrEmpty(fileName)) {
return;
}
//中断処理をする
SourceDict[_prevLipSynchName].AbortLipSynch();
}
/// <summary>
/// ソース用のディクショナリを初期化する
/// </summary>
private void InitializeSourceDictionary() {
_sourceDict = new Dictionary<string, LipSynchSources>();
_sources.ForEach( s => SourceDict.Add(s.FileName, s));
}
/// <summary>
/// ソースファイルが存在するかどうかを返す
/// </summary>
private bool TryGetSource(string fileName) {
//ファイルが存在するならtrue
if(SourceDict.ContainsKey(fileName)) {
return true;
}
//存在しないなら警告を表示し、falseを返す
Debug.LogWarning($"{fileName}という名前のCSVファイルは設定されていません。設定されている全てのファイル名は次の通りです。");
foreach(var s in SourceDict) {
Debug.LogWarning(s.Value.FileName);
}
return false;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment