前回のまとめ
UnityでのAudioは、ダイレクトのPCで動作させるときは、入力ファイル通りのサンプリング周波数だが、WebGLになるとAACで圧縮され、サンプリング周波数が変わってしまうことが判明した。
そしてこのサンプリング周波数は「audiosource.clip.frequency」のような方法では取得できない。
対応方法
それならば、周波数が既知の正弦波をサンプルして、この間隔からサンプリング周波数をカントしてしまえばいい。
無音+既知の周波数の正弦波+無音で音声を挟んで、この正弦波でサンプリング周波数をカウントしてしまえばいい。
そしてこの前後の挿入音を削除して再生し、このときの計測したサンプリング周波数を元にLipSyncを計算すれば、完全にリアルタイムではないが、WebGLでの口パクは実現する。
その結果がこれである。
PCダイレクトの場合:右上のサンプリング周波数が44100Hzになっている。
WebGLの場合:右上のサンプリング周波数が48000Hzになっている。
両者ともほぼ同様の口パクが実現できている。
なお、参考までにコードを掲載しておく。
using System.Collections;
using System;
using UnityEngine;
using UnityEngine.Networking;
using TMPro;
using Live2D.Cubism.Framework.MouthMovement;
//using Unity.VisualScripting;
//using System.Collections.Generic;
//using UnityEditor.Experimental.GraphView;
public class AudioPlayer : MonoBehaviour
{
public AudioSource audioSource;
public AudioClip audioClip_error;
string url = InputFieldManager.url_domain + "Fee_voice/Fee_voice.wav";
public TMP_Text debug_text;
CubismMouthController Target { get; set; }
public static float[] Samples;
public static float[] slicedSamples;
public static int SampleCount;
public int Gain = 5;
public float Smoothing = 0;
int sampleslength;
int samplingFrequency;
float last_averageVolume;
float VelocityBuffer;
// サンプリング周波数測定用の、前側のReference信号の最後の位置と、後ろ側のReference信号の最初の位置
int frontRefsoundEnd,backRefsoundEnd;
int audioPlaybackStopCount;
void Start()
{
Target = GetComponent<CubismMouthController>();
if (Target == null)
{
Debug.LogError("CubismMouthController not found!");
}
}
void LateUpdate()
{
// Playback準備とwavファイルPlayをキック
if (InputFieldManager.is_Playback_ready){
InputFieldManager.is_Playback_ready = false;
StartCoroutine(Playback());
}
// Playフラグで口パクスタート
if (InputFieldManager.is_Lipsync_start){
LipSync();
}else{
Target.MouthOpening = Fee_ParameterSet.paramMouthOpenY;
}
}
IEnumerator Playback()
{
using (var uwr = UnityWebRequestMultimedia.GetAudioClip(url, AudioType.WAV))
{
yield return uwr.SendWebRequest();
if (uwr.result == UnityWebRequest.Result.ConnectionError || uwr.result == UnityWebRequest.Result.ProtocolError)
{
Debug.LogError(uwr.error);
// 再生準備
audioSource = GetComponent<AudioSource>();
audioSource.clip = audioClip_error;
// Audioデータからサンプリング周波数と発声時間を求めて、その後音声部分のみ取り出す
get_audio_data_for_LipSync();
yield return new WaitForSeconds(0.5f);
// 0.5秒待って再生開始 同時にフラグをTrueにしてLipSyncなど動作開始
InputFieldManager.is_Playback_play = true;
InputFieldManager.is_Lipsync_start = true;
Debug.Log("audioplayer.Play:Connection Error message");
audioSource.Play();
// さらに0.5秒待ってText字幕再生
yield return new WaitForSeconds(0.5f);
InputFieldManager.is_Subtitle_start = true;
}
else
{
// 再生準備
var audioClip = DownloadHandlerAudioClip.GetContent(uwr);
var audioSource = GetComponent<AudioSource>();
audioSource.clip = audioClip;
// Audioデータからサンプリング周波数と発声時間を求めて、その後音声部分のみ取り出す
get_audio_data_for_LipSync();
yield return new WaitForSeconds(0.2f);
// 0.5秒待って再生開始 同時にフラグをTrueにしてLipSyncなど動作開始
InputFieldManager.is_Playback_play = true;
InputFieldManager.is_Lipsync_start = true;
Debug.Log("audioplayer.Play");
audioSource.Play();
// さらに0.5秒待ってText字幕再生
yield return new WaitForSeconds(0.5f);
InputFieldManager.is_Subtitle_start = true;
}
}
}
void get_audio_data_for_LipSync(){
// 配列に読み込み
sampleslength = audioSource.clip.samples * 12 / 10; // WebGLでサンプリング周波数が狂うため、1.2倍のバッファにしておく
Samples = new float[sampleslength];
Array.Clear(Samples, 0, sampleslength);
audioSource.clip.GetData(Samples,0);
int count;
// サンプリング周波数計算
// WebGL時になぜかサンプリング周波数が狂い、これが取得できないため、計測する
// 音源が 1秒無音+1秒100Hz正弦波+1秒無音+音声+1秒無音+1秒100Hz正弦波+1秒無音 で構成されている
// 正弦波部分でサンプリング周波数を計算する
float threshold = 0.5f;
int[] positionListFront = new int[100];
int[] positionListBack = new int[100];
// 100Hzの正弦波が閾値を超える位置を捕捉
// 前後とも100個取り出したら終了
count = 0;
for (int i = 0; i < sampleslength - 1; i++)
{
if (Samples[i] <= threshold && Samples[i + 1] > threshold)
{
positionListFront[count] = i;
count ++;
if (count > 99){
break;
}
}
}
count = 0;
for (int i = sampleslength-2; i > 0; i--)
{
if (Samples[i] <= threshold && Samples[i + 1] > threshold)
{
positionListBack[count] = i;
count ++;
if (count > 99){
break;
}
}
}
// 平均値計算
int sum = 0;
count = 0;
for (int i = 0; i < 99; i++)
{
sum += positionListFront[i+1] - positionListFront[i];
count ++;
sum += positionListBack[i] - positionListBack[i+1];
count ++;
}
float average = sum / count;
// 平均値から10%外れた点は特異点として削除して、再度計算
sum = 0;
count = 0;
for (int i = 0; i < 99; i++)
{
int distance = positionListFront[i+1] - positionListFront[i];
if ((distance > (int)(average*0.9f)) && (distance < (int)(average * 1.1f))){
sum += distance;
frontRefsoundEnd = positionListFront[i];
count ++;
}
distance = positionListBack[i] - positionListBack[i+1];
if ((distance > (int)(average*0.9f)) && (distance < (int)(average * 1.1f))){
sum += distance;
backRefsoundEnd = positionListBack[i];
count ++;
}
}
// サンプリング周波数確定
samplingFrequency = sum / count * 100;
Debug.Log("count = " + count.ToString());
// AudioClipを 0.5秒無音+音声+0.5秒無音 に編集
int start_position = frontRefsoundEnd + samplingFrequency/2;
int end_position = backRefsoundEnd - samplingFrequency/2;
int length = end_position - start_position;
slicedSamples = new float[sampleslength];
Array.Copy(Samples, start_position, slicedSamples, 0, length);
audioSource.clip.SetData(slicedSamples, 0);
// Text字幕の時間を設定(無音時間を削除。最初の0.5秒はフラグを立てるときに設定する)
InputFieldManager.Fee_subtitle_endtime = (end_position - start_position - samplingFrequency)/samplingFrequency;
// audiosource.stop()させるためのカウント数=前後に0.5秒を挿入した音声のカウント数を得る
audioPlaybackStopCount = end_position - start_position;
SampleCount = 0;
}
void LipSync(){
// フレーム周波数取得
float frameRate = 1.0f / Time.deltaTime;
// 音量の計算
float voiceDataSum = 0f;
int delta = (int)(samplingFrequency / frameRate) ;
int i;
for (i = SampleCount; i < SampleCount + delta; i++)
{
voiceDataSum += Mathf.Abs(slicedSamples[i]);
}
float averageVolume = voiceDataSum / delta * Gain;
SampleCount = i;
averageVolume = Mathf.Clamp(averageVolume, 0.0f, 1.0f);
averageVolume = Mathf.SmoothDamp(last_averageVolume, averageVolume, ref VelocityBuffer, Smoothing * 0.1f);
debug_text.text = "averageVolume = " + averageVolume.ToString("F4")
+ "\nSampleCount = " + SampleCount.ToString()
+ "\nsamplingFrequency =" + samplingFrequency.ToString();
Target.MouthOpening = averageVolume;
last_averageVolume = averageVolume;
Debug.Log("SampleCount = " + SampleCount.ToString() + " samplelength = "+ sampleslength);
// 前後に0.5秒無音が入っているカウントで再生をStopさせる
if (SampleCount >= audioPlaybackStopCount){
audioSource.Stop();
InputFieldManager.is_Lipsync_start = false;
SampleCount = 0;
}
}
}
Comments