voice-changerのユニットテスト戦略:品質保証のためのテストケース設計
1. はじめに:リアルタイム音声処理の品質課題
リアルタイムボイスチェンジャー(Realtime Voice Changer)は、低遅延と高品質を両立する音声処理システムとして注目されている。しかし、多様なモデル(RVC, DiffusionSVC, SoVitsSvc40等)、複数のピッチ抽出アルゴリズム(Crepe, Dio, Harvest)、そしてさまざまな環境設定が組み合わさることで、品質保証は大きな課題となっている。
本記事では、voice-changerのユニットテスト戦略を詳細に解説し、以下のポイントに焦点を当てる:
- コアコンポーネントのテスト対象特定
- 境界値分析に基づくテストケース設計
- 性能と品質の両面をカバーするテスト手法
- 自動化されたテストスイートの構築方法
2. テスト対象の特定:コアコンポーネント分析
voice-changerのコア機能はserver/voice_changerディレクトリに実装されている。主要なクラスとメソッドを整理すると以下の通りである:
2.1 モデル管理コンポーネント
ModelSlotManager
├── __init__(self, model_dir: str)
├── getAllSlotInfo(self, reload: bool = False)
├── get_slot_info(self, slotIndex: int | StaticSlot)
└── save_model_slot(self, slotIndex: int, slotInfo: ModelSlots)
VoiceChangerManager
├── loadModel(self, params: LoadModelParams)
├── generateVoiceChanger(self, val: int | StaticSlot)
└── changeVoice(self, receivedData: AudioInOut)
2.2 音声変換コンポーネント
VoiceChanger
├── __init__(self, params: VoiceChangerParams)
├── setModel(self, model: Any)
├── on_request(self, receivedData: AudioInOut) -> tuple[AudioInOut, list[Union[int, float]]]
└── update_settings(self, key: str, val: Any)
RVC (Retrieval-based Voice Conversion)
├── __init__(self, params: VoiceChangerParams, slotInfo: RVCModelSlot)
├── initialize(self)
├── inference(self, receivedData: AudioInOut, crossfade_frame: int, sola_search_frame: int)
└── export2onnx(self)
DiffusionSVC
├── __init__(self, params: VoiceChangerParams, slotInfo: DiffusionSVCModelSlot)
├── initialize(self)
├── inference(self, receivedData: AudioInOut, crossfade_frame: int, sola_search_frame: int)
└── generate_input(self, newData: AudioInOut, crossfadeSize: int, solaSearchFrame: int = 0)
2.3 補助コンポーネント
PitchExtractorManager
├── initialize(cls, params: VoiceChangerParams)
├── getPitchExtractor(cls, pitchExtractorType: PitchExtractorType, gpu: int) -> PitchExtractor
└── loadPitchExtractor(cls, pitchExtractorType: PitchExtractorType, gpu: int) -> PitchExtractor
VolumeExtractor
├── __init__(self, hop_size: float)
├── extract(self, audio: torch.Tensor)
└── get_mask_from_volume(self, volume, block_size: int, threshold=-60.0, device='cpu') -> torch.Tensor
3. テスト戦略の設計:マルチレベルアプローチ
voice-changerのユニットテスト戦略は、以下の4つのレベルで構成される:
3.1 単体機能テスト
個々の関数やメソッドの正しさを検証する。主なテスト項目は:
- 入力バリデーション
- 正常系の処理結果
- 異常系のエラーハンドリング
3.2 モジュール統合テスト
複数のコンポーネントが協調して動作することを確認する。例えば:
- ピッチ抽出 → 特徴量抽出 → 推論の一連の流れ
- モデル切り替え → 設定更新 → 音声変換の一貫性
3.3 性能・負荷テスト
リアルタイム処理における性能を評価する:
- 処理遅延(latency)の測定
- CPU/GPU使用率の監視
- 長時間動作時のメモリリーク検出
3.4 品質指標評価
音声変換の品質を客観的指標で評価する:
- 信号対雑音比(SNR)
- 短時間フーリエ変換(STFT)によるスペクトル比較
- 主観的評価尺度(MOS: Mean Opinion Score)
4. テストケース設計:境界値分析と組み合わせテスト
4.1 入力パラメータの境界値分析
音声処理における代表的なパラメータとその境界値を整理する:
| パラメータ | 正常範囲 | 境界値 | 異常値 |
|---|---|---|---|
| サンプリングレート | 16000-48000 Hz | 16000, 48000 | 8000, 96000 |
| ピッチシフト量 | -12-+12 半音 | -12, +12 | -24, +24 |
| 話者ID | 0-最大話者数 | 0, max_id | -1, max_id+1 |
| ノイズ低減閾値 | -60-0 dB | -60, 0 | -80, +10 |
| クロスフェードサイズ | 0-2048 サンプル | 0, 2048 | -1, 4096 |
4.2 モデルとピッチ抽出器の組み合わせテスト
voice-changerでは複数のモデルとピッチ抽出器の組み合わせが可能である。代表的な組み合わせをテストする必要がある:
4.3 テストデータの設計
効果的なテストを行うためには、多様な特性を持つテストデータが必要である:
-
基準音声データ
- 男性声 / 女性声 / 子供声
- 無音区間を含む音声
- さまざまな話速(遅い/普通/速い)
-
特殊ケースデータ
- 極端に高い/低い周波数の音声
- ノイズの多い環境での録音
- 非常に短い音声(50ms以下)
-
パラメータ組み合わせ
- 各パラメータの最小値/最大値/中間値の組み合わせ
- 互換性のないパラメータの組み合わせ
5. 単体テストの実装例
5.1 ModelSlotManagerのテスト
import unittest
from server.voice_changer.ModelSlotManager import ModelSlotManager
import tempfile
import os
class TestModelSlotManager(unittest.TestCase):
def setUp(self):
# テスト用の一時ディレクトリを作成
self.temp_dir = tempfile.TemporaryDirectory()
self.model_dir = self.temp_dir.name
self.manager = ModelSlotManager(self.model_dir)
# テスト用モデルスロットデータを作成
self.test_slot_info = {
"name": "Test Model",
"type": "RVC",
"path": os.path.join(self.model_dir, "test_model.pth"),
"samplingRate": 44100,
"params": {}
}
def tearDown(self):
# 一時ディレクトリを削除
self.temp_dir.cleanup()
def test_save_and_get_slot_info(self):
# スロットを保存
self.manager.save_model_slot(0, self.test_slot_info)
# スロット情報を取得
retrieved_info = self.manager.get_slot_info(0)
# 保存した情報と取得した情報が一致することを確認
self.assertEqual(retrieved_info["name"], self.test_slot_info["name"])
self.assertEqual(retrieved_info["type"], self.test_slot_info["type"])
self.assertEqual(retrieved_info["samplingRate"], self.test_slot_info["samplingRate"])
def test_get_all_slot_info(self):
# 複数のスロットを保存
for i in range(3):
slot_info = self.test_slot_info.copy()
slot_info["name"] = f"Test Model {i}"
self.manager.save_model_slot(i, slot_info)
# すべてのスロット情報を取得
all_slots = self.manager.getAllSlotInfo()
# スロット数が正しいことを確認
self.assertEqual(len(all_slots), 3)
# 各スロットの情報が正しいことを確認
for i, slot in enumerate(all_slots):
self.assertEqual(slot["name"], f"Test Model {i}")
def test_invalid_slot_index(self):
# 存在しないスロットインデックスを指定した場合の挙動を確認
with self.assertRaises(ValueError):
self.manager.get_slot_info(999)
if __name__ == '__main__':
unittest.main()
5.2 RVCモデルの推論テスト
import unittest
import numpy as np
import torch
from server.voice_changer.RVC.RVC import RVC
from server.voice_changer.VoiceChangerParams import VoiceChangerParams
class TestRVCModel(unittest.TestCase):
def setUp(self):
# テスト用のパラメータを設定
self.params = VoiceChangerParams()
self.params.gpu = 0 # CPUでテストする場合は-1
self.params.modelType = "RVC"
# テスト用のスロット情報
self.slot_info = {
"path": "path/to/test/model", # テスト用モデルパス
"samplingRate": 44100,
"f0Detector": "crepe",
"params": {
"transpose": 0,
"indexRatio": 0.75,
"filterRadius": 3,
"rmsMixRate": 0.25
}
}
# RVCモデルを初期化
self.rvc = RVC(self.params, self.slot_info)
self.rvc.initialize()
def test_inference_basic_functionality(self):
# テスト用の入力音声データを生成 (1秒間の44.1kHz正弦波)
sample_rate = 44100
duration = 1.0 # 1秒
frequency = 440.0 # A4 (ラ)
t = np.linspace(0, duration, int(sample_rate * duration), endpoint=False)
input_audio = np.sin(2 * np.pi * frequency * t).astype(np.float32)
# クロスフェードとソラ検索フレームを設定
crossfade_frame = 1024
sola_search_frame = 20
# 推論を実行
output_audio, performance = self.rvc.inference(
input_audio,
crossfade_frame,
sola_search_frame
)
# 出力が正しい形状であることを確認
self.assertEqual(output_audio.dtype, np.float32)
self.assertTrue(len(output_audio) > 0)
# 性能指標が返されることを確認
self.assertTrue(isinstance(performance, list))
self.assertTrue(all(isinstance(p, (int, float)) for p in performance))
def test_inference_with_different_pitch_shifts(self):
# さまざまなピッチシフトで推論をテスト
input_audio = np.random.randn(44100).astype(np.float32) # ノイズを入力として使用
for shift in [-2, 0, 2, 5, -5]: # 半音単位のシフト
# ピッチシフト設定を更新
self.rvc.update_settings("transpose", shift)
# 推論を実行
output_audio, _ = self.rvc.inference(input_audio, 1024, 20)
# 出力が生成されることを確認
self.assertTrue(len(output_audio) > 0)
def test_invalid_input_handling(self):
# 無効な入力(None)を渡した場合のエラーハンドリング
with self.assertRaises(TypeError):
self.rvc.inference(None, 1024, 20)
# 空の配列を入力とした場合
empty_audio = np.array([], dtype=np.float32)
with self.assertRaises(ValueError):
self.rvc.inference(empty_audio, 1024, 20)
if __name__ == '__main__':
unittest.main()
5.3 ピッチ抽出器のテスト
import unittest
import numpy as np
import torch
from server.voice_changer.PitchExtractorManager import PitchExtractorManager
from server.voice_changer.VoiceChangerParams import VoiceChangerParams
class TestPitchExtractors(unittest.TestCase):
def setUp(self):
# テスト用パラメータ
self.params = VoiceChangerParams()
self.manager = PitchExtractorManager.initialize(self.params)
# テスト用音声データ (440Hzの正弦波、1秒間)
self.sample_rate = 44100
self.duration = 1.0
t = np.linspace(0, self.duration, int(self.sample_rate * self.duration), endpoint=False)
self.audio = np.sin(2 * np.pi * 440.0 * t).astype(np.float32)
# ブロックサイズとモデルサンプリングレート
self.block_size = 512
self.model_sr = 44100
def test_pitch_extractor_crepe(self):
# Crepeピッチ抽出器を取得
crepe = self.manager.getPitchExtractor("crepe", 0)
# ピッチ抽出を実行
pitch = crepe.extract(
self.audio,
self.sample_rate,
self.block_size,
self.model_sr,
pitch=None,
f0_up_key=0,
silence_front=0
)
# 結果の検証
self.assertTrue(isinstance(pitch, np.ndarray))
self.assertTrue(len(pitch) > 0)
# 基本周波数が440Hz付近であることを確認(許容誤差±5Hz)
mean_pitch = np.mean(pitch[pitch > 0]) # 無音部分を除外
self.assertAlmostEqual(mean_pitch, 440.0, delta=5.0)
def test_pitch_extractor_dio(self):
# Dioピッチ抽出器を取得
dio = self.manager.getPitchExtractor("dio", 0)
# ピッチ抽出を実行
pitch = dio.extract(
self.audio,
self.sample_rate,
self.block_size,
self.model_sr,
pitch=None,
f0_up_key=0,
silence_front=0
)
# 結果の検証
self.assertTrue(isinstance(pitch, np.ndarray))
self.assertTrue(len(pitch) > 0)
# 基本周波数が440Hz付近であることを確認(許容誤差±10Hz)
mean_pitch = np.mean(pitch[pitch > 0])
self.assertAlmostEqual(mean_pitch, 440.0, delta=10.0)
def test_pitch_shift(self):
# Harvestピッチ抽出器を取得
harvest = self.manager.getPitchExtractor("harvest", 0)
# 異なるピッチシフトで抽出を実行
for shift in [-2, 0, 2, 5]: # 半音単位
pitch = harvest.extract(
self.audio,
self.sample_rate,
self.block_size,
self.model_sr,
pitch=None,
f0_up_key=shift,
silence_front=0
)
# ピッチシフトに応じた周波数変化を確認
expected_pitch = 440.0 * (2 ** (shift / 12))
mean_pitch = np.mean(pitch[pitch > 0])
self.assertAlmostEqual(mean_pitch, expected_pitch, delta=10.0)
if __name__ == '__main__':
unittest.main()
6. 統合テストの実装例
6.1 音声変換パイプラインのテスト
import unittest
import numpy as np
import torch
from server.voice_changer.VoiceChangerManager import VoiceChangerManager
from server.voice_changer.VoiceChangerParams import VoiceChangerParams
from server.voice_changer.ModelSlotManager import ModelSlotManager
class TestVoiceConversionPipeline(unittest.TestCase):
def setUp(self):
# テスト用パラメータ
self.params = VoiceChangerParams()
self.params.gpu = 0 # CPUでテストする場合は-1
self.params.sampleRate = 44100
# モデルスロットマネージャーとボイスチェンジャーマネージャーの初期化
self.model_slot_manager = ModelSlotManager("path/to/model/directory")
self.voice_changer_manager = VoiceChangerManager.get_instance(self.params)
# テスト用音声データ (1秒間の440Hz正弦波)
self.sample_rate = 44100
self.duration = 1.0
t = np.linspace(0, self.duration, int(self.sample_rate * self.duration), endpoint=False)
self.input_audio = np.sin(2 * np.pi * 440.0 * t).astype(np.float32)
def test_full_conversion_pipeline(self):
# モデルをロード (テスト用RVCモデル)
load_params = {
"slotIndex": 0,
"modelType": "RVC",
"f0Detector": "crepe",
"inputSampleRate": 44100,
"outputSampleRate": 44100
}
self.voice_changer_manager.loadModel(load_params)
# 音声変換を実行
output_audio, performance = self.voice_changer_manager.changeVoice(self.input_audio)
# 出力の検証
self.assertTrue(isinstance(output_audio, np.ndarray))
self.assertEqual(output_audio.dtype, np.float32)
self.assertTrue(len(output_audio) > 0)
# 入力と出力の長さがほぼ同じであることを確認 (許容誤差±5%)
input_length = len(self.input_audio)
output_length = len(output_audio)
length_diff = abs(input_length - output_length) / input_length
self.assertLess(length_diff, 0.05)
# 性能指標の検証
self.assertTrue(isinstance(performance, list))
self.assertEqual(len(performance), 3) # [処理時間, GPU使用率, CPU使用率]
def test_model_switching(self):
# 最初のモデルをロード
self.voice_changer_manager.loadModel({
"slotIndex": 0,
"modelType": "RVC",
"f0Detector": "crepe",
"inputSampleRate": 44100,
"outputSampleRate": 44100
})
# 最初のモデルで変換
output1, _ = self.voice_changer_manager.changeVoice(self.input_audio)
# 2番目のモデルに切り替え
self.voice_changer_manager.loadModel({
"slotIndex": 1,
"modelType": "RVC",
"f0Detector": "dio",
"inputSampleRate": 44100,
"outputSampleRate": 44100
})
# 2番目のモデルで変換
output2, _ = self.voice_changer_manager.changeVoice(self.input_audio)
# 異なるモデルの出力は異なるはず
self.assertFalse(np.array_equal(output1, output2))
# 両方の出力が有効であることを確認
self.assertTrue(np.max(np.abs(output1)) > 0.01)
self.assertTrue(np.max(np.abs(output2)) > 0.01)
if __name__ == '__main__':
unittest.main()
7. 性能テストの実装例
7.1 リアルタイム処理性能の測定
import unittest
import numpy as np
import time
import statistics
from server.voice_changer.VoiceChangerManager import VoiceChangerManager
from server.voice_changer.VoiceChangerParams import VoiceChangerParams
class TestRealtimePerformance(unittest.TestCase):
def setUp(self):
# テスト用パラメータ
self.params = VoiceChangerParams()
self.params.gpu = 0 # 使用可能なGPUを指定
self.params.sampleRate = 44100
# ボイスチェンジャーマネージャーの初期化
self.voice_changer_manager = VoiceChangerManager.get_instance(self.params)
# テスト用モデルをロード
self.voice_changer_manager.loadModel({
"slotIndex": 0,
"modelType": "RVC",
"f0Detector": "crepe",
"inputSampleRate": 44100,
"outputSampleRate": 44100
})
# テスト用音声データ (0.1秒間隔のブロック)
self.block_duration = 0.1 # 100msのブロック
self.sample_rate = 44100
self.block_size = int(self.sample_rate * self.block_duration)
t = np.linspace(0, self.block_duration, self.block_size, endpoint=False)
self.audio_block = np.sin(2 * np.pi * 440.0 * t).astype(np.float32)
def test_realtime_latency(self):
# 複数回の処理を実行して平均遅延を測定
num_iterations = 50
latencies = []
for _ in range(num_iterations):
# 処理前の時間を記録
start_time = time.perf_counter()
# 音声変換を実行
output, _ = self.voice_changer_manager.changeVoice(self.audio_block)
# 処理後の時間を記録
end_time = time.perf_counter()
# 遅延を計算して保存
latency = (end_time - start_time) * 1000 # ミリ秒に変換
latencies.append(latency)
# 統計量を計算
avg_latency = statistics.mean(latencies)
p95_latency = np.percentile(latencies, 95)
max_latency = max(latencies)
# 結果を表示
print(f"平均遅延: {avg_latency:.2f}ms")
print(f"95パーセンタイル遅延: {p95_latency:.2f}ms")
print(f"最大遅延: {max_latency:.2f}ms")
# リアルタイム処理要件を満たしていることを確認 (100ms以下)
self.assertLess(avg_latency, 100.0)
self.assertLess(p95_latency, 150.0)
def test_processing_stability(self):
# 長時間処理を行い、性能劣化を監視
num_blocks = 300 # 約30秒間
processing_times = []
for _ in range(num_blocks):
start_time = time.perf_counter()
output, _ = self.voice_changer_manager.changeVoice(self.audio_block)
end_time = time.perf_counter()
processing_time = (end_time - start_time) * 1000
processing_times.append(processing_time)
# 最初の10%と最後の10%の平均処理時間を比較
first_segment = processing_times[:int(num_blocks*0.1)]
last_segment = processing_times[-int(num_blocks*0.1):]
avg_first = statistics.mean(first_segment)
avg_last = statistics.mean(last_segment)
# 性能劣化が20%以内であることを確認
degradation = (avg_last - avg_first) / avg_first * 100
self.assertLess(degradation, 20.0)
print(f"性能劣化率: {degradation:.2f}%")
if __name__ == '__main__':
unittest.main()
8. テストカバレッジの測定と向上
8.1 カバレッジ測定の設定
pytest-covを使用してテストカバレッジを測定する:
# テストカバレッジを測定してレポートを生成
pytest --cov=server.voice_changer tests/ --cov-report=html --cov-report=term
8.2 カバレッジ向上のための戦略
- 条件網羅:if文やループのすべての分岐をテストする
- 例外処理のテスト:エラーハンドリングコードをテストする
- 境界値分析:極端な入力値を使用したテストを追加する
- 組み合わせテスト:複数のパラメータ組み合わせをテストする
8.3 目標カバレッジ率
| コンポーネント | 目標カバレッジ率 | 重要度 |
|---|---|---|
| 音声変換コア | 90%以上 | 高 |
| モデル管理 | 85%以上 | 高 |
| パラメータ管理 | 80%以上 | 中 |
| UI関連コード | 60%以上 | 低 |
9. テスト自動化とCI/CD統合
9.1 GitHub Actionsを使用した自動テスト
name: Voice Changer Tests
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main, develop ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.9'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install pytest pytest-cov
- name: Run tests with coverage
run: |
pytest --cov=server.voice_changer tests/ --cov-report=xml
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
file: ./coverage.xml
fail_ci_if_error: true
threshold: 1% # 許容されるカバレッジ低下率
9.2 テスト結果の可視化
- カバレッジレポート:HTML形式で生成されたカバレッジレポートを確認する
- テストダッシュボード:CI/CDツールのダッシュボードでテスト結果を追跡する
- 性能監視:処理時間やリソース使用率のトレンドを監視する
10. まとめと今後の課題
本記事では、voice-changerの品質保証のためのユニットテスト戦略を詳細に解説した。コアコンポーネントの特定、境界値分析に基づくテストケース設計、単体テストと統合テストの実装例を通じて、堅牢なテスト体系の構築方法を示した。
10.1 達成された成果
- 主要コンポーネントに対する網羅的なテストケースの設計
- リアルタイム音声処理に特化した性能テスト手法の確立
- CI/CDパイプラインへのテスト自動化の統合
10.2 今後の課題
-
モデル品質の客観的評価指標の開発
- SNRやSTFT比較に加え、主観的品質評価の自動化
-
多言語・多 speaker のテストデータセットの構築
- 多様な音声特性に対するロバスト性の確保
-
A/Bテストフレームワークの導入
- 異なるモデルやパラメータ設定の客観的比較
-
モデル劣化検知システムの構築
- 長期運用における性能劣化の早期発見
voice-changerの開発は継続的に進化しており、テスト戦略もまた常に見直しと改善を行う必要がある。本記事で提示した手法を基に、さらに堅牢で信頼性の高いリアルタイムボイスチェンジャーを目指していく。
11. 参考文献
- P. Welinder, et al., "CREPE: A Convolutional Representation for Pitch Estimation", ICML 2018
- J. Yamamoto, et al., "Realtime Voice Conversion with Instantaneous Frequency Correction", IEICE Transactions on Information and Systems, 2021
- "PyTorch Testing Guide", PyTorch Documentation
- "Audio Signal Processing for Machine Learning", Towards Data Science, 2020
- "Software Testing Techniques", G. J. Myers, John Wiley & Sons, 2011
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



