[UNITY]2DRPG開発日誌 #48 ラブデリック語を自動生成しようとして失敗した記録

ラブデリック語ってなに?

我々KITTYPOOLがリスペクトする作品「moon」の開発チーム「ラブデリック」、及び「ラブデリック」解散後の元チームメンバーが携わったゲームのなかの、キャラクターの独特な音声のことを「ラブデリック」と…私は呼んでいます(ファンの間では「ラブデリック語」とは呼ばれていないようです)。

これなんですが、音声ファイルを分割するのってなかなか面倒なんですよね。

キャラ数が増えるともう大変です。

そこで、一つの長い音声ファイルを用意すれば、「自動でランダムに分割し、ランダムに並び替えて再生する」コードを組んでみました。

public void RandomVoice()
{
   //初期化
   sfxSource.clip = null;
   sfxSource.clip = voice;
   Dictionary<float,float> dic_StartTime_EndTime = new Dictionary<float, float>(); //区切り再生位置の始まりと終わり
   List<float> ar_StartTime = new List<float>(); //区切り再生位置ディクショナリーのキー
   List<int> li_StartTime_RandomIndex = new List<int>(); //再生するIndex
   float randomTime_Start = 0; //区切りの終わり
   float randomTime_End = 0; //区切りの始まり

   //声を区切るタイミングをランダム生成
   while (randomTime_Start < voice.length)
   {
      //区切り再生位置の終わりを設定
      randomTime_End += UnityEngine.Random.Range(separateMin, separateMax);

      //元音声の長さを超えたら、元音声の長さにする
      if (randomTime_End > voice.length) randomTime_End = voice.length;

      //区切り再生位置を保存
      dic_StartTime_EndTime.Add(randomTime_Start, randomTime_End);

      //区切り再生位置のキーを保存
      ar_StartTime.Add(randomTime_Start);

      //区切り再生位置の始まりを再設定
      randomTime_Start = randomTime_End;
   }

   //再生するIndexをランダム設定
   while (true) //無限ループ
   {
      //全て登録できたら
      if (li_StartTime_RandomIndex.Count >= ar_StartTime.Count)
         break; //無限ループを抜ける

      //設定するIndexをランダムに設定
      int copyIndex = UnityEngine.Random.Range(0, ar_StartTime.Count);

      //ランダムに生成したIndexが既に登録済みでないかチェック
      bool exist = false;
      foreach(int item in li_StartTime_RandomIndex)
      {
         //登録済みだったら
         if (item == copyIndex)
         {
            exist = true; //「登録済み」にして、
            break; //foreachを抜ける
         }
      }
      if (exist) continue; //「登録済み」なら登録せず、無限ループ再開
      li_StartTime_RandomIndex.Add(copyIndex); //未登録なので登録する
   }

   //ランダムIndexのIndexを設定
   int currentRandomIndexIndex = 0;

   //再生位置と待ち時間を設定
   float startTime = ar_StartTime[li_StartTime_RandomIndex[currentRandomIndexIndex]];
   float endTime = dic_StartTime_EndTime[ar_StartTime[li_StartTime_RandomIndex[currentRandomIndexIndex]]];
   float waitTime = endTime - startTime;

   //再生
   sfxSource.time = startTime;
   sfxSource.Play();

   //待ち時間を経過したら無限ループ再生開始
   tween_VoiceLoop = DOVirtual.DelayedCall(waitTime, () => VoiceLoop(), false);

   //ループ再生
   void VoiceLoop()
   {
      //ループ前に再生したのが最後のランダムIndexIndexだったら
      if ((currentRandomIndexIndex + 1) >= li_StartTime_RandomIndex.Count)
      {
         //IndexIndexを最初に戻す
         currentRandomIndexIndex = 0;
      }
      else ++currentRandomIndexIndex; //最後じゃなかったら、次のIndexIndexに進む

      //再生を止める
      sfxSource.Stop();

      //再生位置と待ち時間を再設定
      startTime = ar_StartTime[li_StartTime_RandomIndex[currentRandomIndexIndex]];
      endTime = dic_StartTime_EndTime[ar_StartTime[li_StartTime_RandomIndex[currentRandomIndexIndex]]];
      waitTime = endTime - startTime;
      sfxSource.time = startTime;

      //再生
      sfxSource.Play();

      //待ち時間を経過したら無限ループ再開
      tween_VoiceLoop = DOVirtual.DelayedCall(waitTime, () => VoiceLoop(), false);
   }
}

※コルーチン処理にDOTweenを使用しています。

よし、出来たぞ! と思って試した結果がこちら。

…失敗ですね。

本来は元の音声の「ブレス位置」や「文節」で音声を区切るべきなのに、完全にランダムな位置で区切ってしまうと「ブツ切り」になってしまい、人が喋ってる声には聴こえませんでした。

違和感なく自動化するとしたら、

  1. AudioClipを波形データとして取得
  2. 波形データを数値化する
  3. 音声レベルの小さい位置で区切り位置を保存

という手順が必要だと思いますが、ちょっと開発コストが大きすぎるので手動で区切り音声を作るほうが良さそうです…。