top of page

Unity WebGLのLipSyncに挑戦 vol.2

執筆者の写真: snackvirtualsnackvirtual

前回のまとめ


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;
        }
    }
}

閲覧数:7回0件のコメント

最新記事

すべて表示

xAIML SUNABA

Comments


bottom of page