Repository: VOICEVOX/voicevox_engine (Python)

Issue:
add: コア Song モック
## 内容
要望概要: コアに Song 関連モックを追加  

最近追加された Song 機能はリファクタリングの余地を残しており、また、安定性担保のためにテストが求められている (#1032, [#1041_comment](https://github.com/VOICEVOX/voicevox_engine/pull/1041#pullrequestreview-1861179261))。  
しかし前提としてコアの Song 系メソッドにモックが必要であり、現状リファクタリングとテストが実装不可能である。  
コア実装はプロプライエタリであるため、妥当なモック設計はメンテナさんのみが可能である。  

このような背景から、（メンテナさんによる）コア Song 関連モック追加を提案します。  

### Pros 良くなる点
- テスト追加の前提
- リファクタリングの前提

### Cons 悪くなる点
- 無し

### 実現方法
（対象がプロプライエタリであるため、メンテナさんのみが対応可能です）  

次のメソッドにモックメソッドを追加：  

- `predict_sing_consonant_length_forward()`
- `predict_sing_f0_forward()`
- `predict_sing_volume_forward()`
- `sf_decode_forward()`

追加箇所: https://github.com/VOICEVOX/voicevox_engine/blob/af8cde19b26e8dc27d470da504ae64b1fcef0bcc/voicevox_engine/dev/core/mock.py#L127-L129

## VOICEVOXのバージョン
0.16.0

## OSの種類/ディストリ/バージョン
- [x] Windows
- [x] macOS
- [x] Linux



Fix (patch):
diff --git a/voicevox_engine/core/core_wrapper.py b/voicevox_engine/core/core_wrapper.py
index d8fb10f7f..5426c9750 100644
--- a/voicevox_engine/core/core_wrapper.py
+++ b/voicevox_engine/core/core_wrapper.py
@@ -763,17 +763,17 @@ def predict_sing_f0_forward(
         Parameters
         ----------
         length : int
-            音素列の長さ
+            フレームの長さ
         phoneme : NDArray[np.int64]
-            音素列
+            フレームごとの音素
         note : NDArray[np.int64]
-            ノート列
+            フレームごとのノート
         style_id : NDArray[np.int64]
             スタイル番号
         Returns
         -------
         output : NDArray[np.float32]
-            フレームごとのF0
+            フレームごとの音高
         """
         output = np.zeros((length,), dtype=np.float32)
         self.assert_core_success(
@@ -800,17 +800,19 @@ def predict_sing_volume_forward(
         Parameters
         ----------
         length : int
-            音素列の長さ
+            フレームの長さ
         phoneme : NDArray[np.int64]
-            音素列
+            フレームごとの音素
         note : NDArray[np.int64]
-            ノート列
+            フレームごとのノート
+        f0 : NDArray[np.float32]
+            フレームごとの音高
         style_id : NDArray[np.int64]
             スタイル番号
         Returns
         -------
         output : NDArray[np.float32]
-            フレームごとのF0
+            フレームごとの音量
         """
         output = np.zeros((length,), dtype=np.float32)
         self.assert_core_success(
diff --git a/voicevox_engine/dev/core/mock.py b/voicevox_engine/dev/core/mock.py
index 666c55440..25190ea55 100644
--- a/voicevox_engine/dev/core/mock.py
+++ b/voicevox_engine/dev/core/mock.py
@@ -136,6 +136,98 @@ def decode_forward(
             ] * 256
         return np.array(result, dtype=np.float32)
 
+    def predict_sing_consonant_length_forward(
+        self,
+        length: int,
+        consonant: NDArray[np.int64],
+        vowel: NDArray[np.int64],
+        note_duration: NDArray[np.int64],
+        style_id: NDArray[np.int64],
+    ) -> NDArray[np.int64]:
+        """母音系列・子音系列・ノート列・スタイルIDから子音長系列を生成する"""
+        result = []
+        # mockとしての適当な処理、特に意味はない
+        for i in range(length):
+            # 子音が無い場合は長さ0
+            if consonant[0, i] == -1:
+                result.append(0)
+                continue
+
+            result.append(
+                (
+                    consonant[0, i] % 3
+                    + vowel[0, i] % 5
+                    + note_duration[0, i] % 7
+                    + style_id % 11
+                ).item()
+            )
+        return np.array(result, dtype=np.int64)
+
+    def predict_sing_f0_forward(
+        self,
+        length: int,
+        phoneme: NDArray[np.int64],
+        note: NDArray[np.int64],
+        style_id: NDArray[np.int64],
+    ) -> NDArray[np.float32]:
+        """音素系列・ノート系列・スタイルIDから音高系列を生成する"""
+        result = []
+        # mockとしての適当な処理。大体MIDIノートに従う周波数になるように調整
+        for i in range(length):
+            if note[0, i] == -1:
+                result.append(0)
+                continue
+            result.append(
+                (
+                    2 ** ((note[0, i] - 69) / 12)
+                    * (440 + phoneme[0, i] / 10 + style_id)
+                ).item()
+            )
+        return np.array(result, dtype=np.float32)
+
+    def predict_sing_volume_forward(
+        self,
+        length: int,
+        phoneme: NDArray[np.int64],
+        note: NDArray[np.int64],
+        f0: NDArray[np.float32],
+        style_id: NDArray[np.int64],
+    ) -> NDArray[np.float32]:
+        """音素系列・ノート系列・音高系列・スタイルIDから音量系列を生成する"""
+        result = []
+        # mockとしての適当な処理。大体0~10の範囲になるように調整
+        for i in range(length):
+            if note[0, i] == -1:
+                result.append(0)
+                continue
+            result.append(
+                (
+                    (phoneme[0, i] / 40)
+                    * (note[0, i] / 88)
+                    * (f0[0, i] / 440)
+                    * ((1 / 2) ** style_id)
+                    * 10
+                ).item()
+            )
+        return np.array(result, dtype=np.float32)
+
+    def sf_decode_forward(
+        self,
+        length: int,
+        phoneme: NDArray[np.int64],
+        f0: NDArray[np.float32],
+        volume: NDArray[np.float32],
+        style_id: NDArray[np.int64],
+    ) -> NDArray[np.float32]:
+        """入力からダミー音声波形を生成する"""
+        # 入力値を反映し、長さが 256 倍であるダミー配列を出力する
+        result: list[NDArray[np.float32]] = []
+        for i in range(length):
+            result += [
+                (f0[0, i] / 440) * volume[0, i] * (phoneme[0, i] / 40) + style_id
+            ] * 256
+        return np.array(result, dtype=np.float32)
+
     def supported_devices(self):
         return json.dumps(
             {
diff --git a/voicevox_engine/tts_pipeline/tts_engine.py b/voicevox_engine/tts_pipeline/tts_engine.py
index c0c553516..17d06c3e4 100644
--- a/voicevox_engine/tts_pipeline/tts_engine.py
+++ b/voicevox_engine/tts_pipeline/tts_engine.py
@@ -261,6 +261,7 @@ def calc_phoneme_lengths(
             note_duration = note_durations[i]
 
             # もし、次のノートの子音長が負になる場合、現在のノートの半分にする
+            # NOTE: 将来的にコアは非負になるのでこの処理は不要になる
             if next_consonant_length < 0:
                 next_consonant_length = consonant_lengths[i + 1] = note_duration // 2
             vowel_length = note_duration - next_consonant_length