【Python】稀によくある、サウンド素材トラブルを未然に防ごう！Pythonで作る自動チェッカーのすゝめ

# 最初に

本記事の内容では、外注からサウンド素材を納品して頂いた際に発生しがちな問題と、それらをPythonで検出する手法を紹介します。ゲームプロジェクトでは、サウンド素材（SE、BGM、ボイス、ジングルなど）を外注して揃えることも多いかと思います。

そういった時に、早めに問題を検出することができれば、開発速度を極力落とさず、プロジェクトへの組み込みに集中することができます。この記事が、サウンド素材の品質チェックやトラブル防止の手助けとなれば幸いです。

本記事の対象者は以下の通りです。

- サウンド素材を外注しているゲーム開発プロジェクトのエンジニア
- サウンド素材の納品時にトラブルを経験したことがある方
- Pythonを使ってサウンド素材の自動チェックに挑戦したい方

## 実行環境

- OS：MacOS
- Python：3.13.0 (3.9 以上が推奨です)
- IDE：PyCharm Community Edition 2024.3

# サウンド素材におけるトラブルの種類

ゲームプロジェクトでは、SE、BGM、ボイス、ジングルといったサウンド素材を外注で制作することがよくあります。こうした素材は、ゲームの世界観を彩る重要な要素ですが、納品された素材に問題があると、後工程で以下のようなトラブルが発生する可能性があります。

- **音量の不整合**：
素材によって音量レベルがバラバラな場合、調整に時間がかかる。
- **命名規則違反**：
プロジェクトのルールに従っていないファイル名が納品されると、後からプランナーさんやエンジニアさんが修正対応に追われることがある。
- **フォーマットミス**：
WAV形式を指定していたはずが、MP3形式の素材が混ざっていたことによって、以下の問題が発生する場合がある。
    - 正しく読み込めない
    - 再生されない
    - 音質が劣化する
- **不要な無音部分の混入**：
意図していない無音の混入によって、サウンドの再生タイミングがずれてしまう。
- **ファイル欠損**：
転送エラーなどにより、ファイルが欠損してサイズが0KBになる場合、サウンドが再生されなくなる。

:::note warn

ファイルの変換ミスなどによって、名前は.wavになっているのに、実際の中身が.mp3になっている、なんてこともあります。この問題は、使用ツールやケアレスミスが原因で起こりますが、問題の特定や修正にエンジニアの工数が取られてしまうため、プロジェクトのスケジュールにも影響を及ぼしかねません。

:::

# 人力でのチェックについて

先述したサウンド素材トラブルを未然に防ぐためには、納品時に素材をチェックする必要があります。例えば先述した、フォーマットの確認、音量の調整、命名規則の確認などです。

小規模なプロジェクトであれば、手作業での確認も可能です。
しかし大規模なプロジェクトの場合は、膨大な素材を確認する必要が生じます。手作業では途方もない時間がかかり、尚且つミスが起こりやすくなるため、機械による自動チェックが必要になってくるでしょう。

そこで、今回はPythonを使って自動チェックスクリプトを用意していきます。

# スクリプト設計のポイント

サウンド素材のチェックを効率化するために、Pythonを用いた自動チェックスクリプトを設計していきましょう。今回のスクリプトでは、先述したサウンド素材のトラブルを検出するために、以下の項目をカバーしていきます。

1. **ファイル形式の一致**
    - 納品されたファイルの拡張子と実際の中身が一致しているか
    （例：`.wav`ファイルの中身が別形式（例：`.mp3`）になっていないか）
2. **フォーマットの整合性**
    - サンプルレート、ビット深度、チャンネル数がプロジェクトで指定した状態か
    （例: 44.1kHz、16bit、ステレオになっているか）
3. **命名規則の遵守**
    - ファイル名がプロジェクトの命名規則（例: `SE_01_Jump.wav`）に従っているか
4. **音量レベルの確認**
    - サウンド素材の音量が指定された範囲（例: -3dB ～ -15dB）内に収まっているか
5. **無音部分の検出**
    - 素材の冒頭や末尾に不要な無音が含まれていないか
6. **ファイルサイズの妥当性**
    - ファイルが0KBでないか、また極端に小さいサイズではないか

### **使用するツール・ライブラリ**

今回のスクリプトでは以下のツールとライブラリを使用します。

- **Python標準ライブラリ**
    - **`wave`**：WAV形式のファイルを解析するために使用。
    - **`os`**：ファイル操作やサイズ確認のために使用。
    - **`re`**：命名規則のチェックに正規表現を使用。
- **外部ライブラリ**
    - **`numpy`**：音声データを効率的に処理するための数値計算ライブラリ。
    - **`pydub`**：音量レベルの測定や無音部分の検出を行うための音声処理ライブラリ。
    - **`ffmpeg`**：`pydub`が音声ファイルを処理するために依存する外部ツール。音声のエンコード・デコード、フォーマット変換などを行う。

:::note
スクリプトを作り始める前に、自身のPCにPythonがインストールされていることと、バージョンが3.9.0以上であることをご確認ください。

インストールされていない場合は、[公式サイト](https://www.python.org/downloads/)を参考にインストールしてください。

また、必要な外部ライブラリに関しても、別途インストールをお願いします。

```python
pip install numpy pydub
```

```python
brew install ffmpeg
```

:::

# スクリプト用意

それでは、実際にスクリプトを用意していきましょう。

尚、今回のスクリプトと、テストするためのサウンドファイルは以下のリポジトリに用意しましたので、ご自由にお使いください。

https://github.com/rossam/SoundChecker

## 1.  **ファイル形式のチェック**

先述したように、ゲームプロジェクトでは、外注から納品された音声ファイルが拡張子に対して正しい形式になっていない場合があります。特に、`.wav`拡張子が付いているのに実際には`.mp3`形式であるなど、見つけにくいトラブルの元が潜んでいる可能性があります。

これをチェックするためのテスト用コードを用意しましょう。

```python
def check_file_format(folder_path):
    """
    ファイル形式の一致をチェックする
    :param folder_path: チェック対象のフォルダパス
    """
    print("\nファイル形式のチェックを開始します...")
    
    # os.walkを使って指定フォルダ内の全ファイルを再帰的に探索
    for root, _, files in os.walk(folder_path):
        for file in files:
		        # 拡張子が.wavのファイルのみチェック
            if file.lower().endswith(".wav"):
                file_path = os.path.join(root, file)
                try:
                    # WAVファイルとして開けるか確認
                    with wave.open(file_path, 'rb') as wav_file:
		                    # チャネル情報を取得（有効性の確認）
                        wav_file.getnchannels()
                        
                # WAV形式でない場合
                except wave.Error:
                    print(f"エラー: {file} はWAV形式ではありません。")
                    
                # 予期しないエラーが発生した場合
                except Exception as e:
                    print(f"エラー: {file} のチェック中に予期しないエラーが発生しました: {e}")
```

### 出力例

```bash
ファイル形式のチェックを開始します...
エラー: file6_empty.wav のチェック中に予期しないエラーが発生しました: 
エラー: file1_wrong_format.wav はWAV形式ではありません。
```

## 2. **フォーマットのチェック**

納品された音声ファイルが、プロジェクト要件に沿ったフォーマット（サンプルレート、ビット深度、チャンネル数など）で作成されていない場合、ゲーム内で音が正しく再生されない、音質が劣化するなどの問題が発生する可能性があります。このようなトラブルを防ぐために、フォーマットの整合性をチェックするスクリプトを用意してみましょう。

```python
def check_format_consistency(folder_path, sample_rate=44100, sample_width=2, channels=2):
    """
    フォーマットの整合性をチェックする
    :param folder_path: チェック対象のフォルダパス
    :param sample_rate: 指定のサンプルレート（例: 44100Hz）
    :param sample_width: 指定のビット深度（例: 16bit = 2bytes）
    :param channels: 指定のチャンネル数（例: ステレオ=2）
    """
    print("\nフォーマットの整合性をチェックします...")

    # 指定フォルダ内の全ファイルを再帰的に探索
    for root, _, files in os.walk(folder_path):
        for file in files:
            # 拡張子が.wavのファイルのみチェック
            if not file.lower().endswith(".wav"):
                continue

            file_path = os.path.join(root, file)

            try:
                # WAVファイルとして開けるか確認
                with wave.open(file_path, 'rb') as wav_file:
                    # 実際のサンプルレート、ビット深度、チャンネル数を取得
                    actual_sample_rate = wav_file.getframerate()
                    actual_sample_width = wav_file.getsampwidth()
                    actual_channels = wav_file.getnchannels()

                    # サンプルレートのチェック
                    if actual_sample_rate != sample_rate:
                        print(f"エラー: {file} のサンプルレートが不一致です（{actual_sample_rate} != {sample_rate}）")

                    # ビット深度のチェック
                    elif actual_sample_width != sample_width:
                        print(f"エラー: {file} のビット深度が不一致です（{actual_sample_width} != {sample_width}）")

                    # チャンネル数のチェック
                    elif actual_channels != channels:
                        print(f"エラー: {file} のチャンネル数が不一致です（{actual_channels} != {channels}）")

						# 予期しないエラーが発生した場合
            except Exception as e:
                print(f"エラー: {file} のチェック中に予期しないエラーが発生しました: {e}")
```

### 出力例

```bash
フォーマットの整合性をチェック中...
エラー: file6_empty.wav のチェック中に予期しないエラーが発生しました: 
エラー: file2_wrong_format.wav のサンプルレートが不一致です（8000 != 44100）
エラー: file1_wrong_wavfile.wav のチェック中に予期しないエラーが発生しました: file does not start with RIFF id
```

## 3. ファイルの命名規則チェック

ゲームプロジェクトでのサウンド素材は、命名規則を守ることで整理がしやすくなり、管理ミスを防ぐことができます。今回は、`file+数字`が含まれている形式を命名規則として設定し、チェックする仕組みを導入します。

```python
def check_naming_rules(folder_path, naming_pattern=r"^file\d+_[A-Za-z0-9_]+\.wav$"):
    """
    命名規則の遵守をチェックする
    :param folder_path: チェック対象のフォルダパス
    :param naming_pattern: 命名規則の正規表現（デフォルトは "file+数字_任意の文字列.wav"）
    """
    print("\n命名規則のチェックを開始します...")

    # 正規表現パターンをコンパイル
    pattern = re.compile(naming_pattern)

    for root, _, files in os.walk(folder_path):
        for file in files:
            # .wavファイルのみチェック
            if not file.lower().endswith(".wav"):
                continue

            file_path = os.path.join(root, file)

            # ファイル名が命名規則に従っているかチェック
            if not pattern.match(file):
                print(f"エラー: {file} は命名規則に違反しています。")
```

:::note

正規表現部分について簡単に解説します。

1. **`^file\d+`**:
    - `file`という文字列で始まり、その後に1つ以上の数字が続きます。
2. **`_[A-Za-z0-9_]+`**:
    - アンダースコア（`_`）の後に、アルファベット、数字、またはアンダースコアが1文字以上続きます。
3. **`\.wav$`**:
    - `.wav`拡張子で終わります。

:::

### 出力例

```bash
命名規則のチェックを開始します...
エラー: wrong_named_file.wav は命名規則に違反しています。
```

## 4. 音量レベルのチェック

使用するサウンド素材の音量がある程度統一されていないと、再生時に不自然な印象を与えることがあります。特に効果音やBGMは、音量が極端に小さかったり、または大きすぎる場合、調整が必要となります。このような問題を早めに検出するために、音量レベルをチェックする仕組みを追加します。

```python
def check_volume_level(folder_path, min_db=-20, max_db=-3):
    """
	  音量レベルが指定範囲内かをチェックする
	  :param folder_path: チェック対象のフォルダパス
	  :param min_db: 許容する音量の下限（デフォルトは-20dB）
	  :param max_db: 許容する音量の上限（デフォルトは-3dB）
    """
    print("\n音量レベルのチェックを開始します...")

    for root, _, files in os.walk(folder_path):
        for file in files:
            # .wavファイルのみチェック
            if not file.lower().endswith(".wav"):
                continue

            file_path = os.path.join(root, file)

            # ファイルサイズを確認し、0KBのファイルをスキップ
            if os.path.getsize(file_path) == 0:
                print(f"スキップ: {file} は空のファイルです。")
                continue

            try:
                # ファイルをAudioSegmentで読み込む
                audio = AudioSegment.from_file(file_path)

                # 音量レベルを取得
                rms_db = audio.dBFS

                # 音量が範囲内かを確認
                if rms_db < min_db:
                    print(f"エラー: {file} の音量が小さすぎます（{rms_db:.2f}dB < {min_db}dB）")
                elif rms_db > max_db:
                    print(f"エラー: {file} の音量が大きすぎます（{rms_db:.2f}dB > {max_db}dB）")

            except Exception as e:
                print(f"エラー: {file} の音量チェック中にエラーが発生しました: {e}")
```

:::note

`AudioSegment`は音声データの分析や、操作、保存を行うためのクラスです。

今回、詳しくは説明しませんが、サウンドデータの再生時間の取得、速度の変更、音量調整、サウンドデータ同士での結合など、いろんな編集をスクリプトで行うことができます。

:::

### 出力例

```bash
音量レベルのチェックを開始します...
スキップ: file6_empty.wav は空のファイルです。
エラー: file4_low_volume.wav の音量が小さすぎます（-43.04dB < -20dB）
```

## 5. 冒頭と末尾の無音検出

使用するサウンド素材で、冒頭や末尾に不要な無音部分が含まれていると、再生タイミングがずれるなどの問題が発生することがあります。このような問題を防ぐため、サウンド素材の冒頭と末尾に無音時間が含まれているかを検出する仕組みを追加します。

```python
def check_silence(folder_path, silence_threshold=-50, chunk_size=10):
    """
    冒頭と末尾の無音部分を検出する
    :param folder_path: チェック対象のフォルダパス
    :param silence_threshold: 無音と判定する音量（デフォルトは-50dBFS）
    :param chunk_size: 無音検出の精度（デフォルトは10ms単位）
    """
    print("\n無音部分のチェックを開始します...")

    for root, _, files in os.walk(folder_path):
        for file in files:
            # .wavファイルのみチェック
            if not file.lower().endswith(".wav"):
                continue

            file_path = os.path.join(root, file)

            # ファイルサイズを確認し、0KBのファイルをスキップ
            if os.path.getsize(file_path) == 0:
                print(f"スキップ: {file} は空のファイルです。")
                continue

            try:
                # ファイルをAudioSegmentで読み込む
                audio = AudioSegment.from_file(file_path)

                # 冒頭の無音部分を検出
                start_trim = silence.detect_leading_silence(audio, silence_threshold, chunk_size)
                # 末尾の無音部分を検出（逆再生して同じ方法で確認）
                end_trim = silence.detect_leading_silence(audio.reverse(), silence_threshold, chunk_size)

                # ファイル全体の長さ（ミリ秒）
                total_length = len(audio)

                # 無音部分があるかを確認
                if start_trim > 0 or end_trim > 0:
                    print(f"エラー: {file} に不要な無音部分があります。")
                    print(f"  - 冒頭の無音: {start_trim}ms")
                    print(f"  - 末尾の無音: {end_trim}ms")

            except Exception as e:
                print(f"エラー: {file} の無音チェック中にエラーが発生しました: {e}")
```

:::note

`silence` モジュールは、音声データ内の無音部分を検出するためのツールです。`pydub` ライブラリに含まれており、音量が一定の閾値を下回る箇所を「無音」として扱います。

こちらも今回は詳しく説明しませんが、無音部分の検出や削除、指定の音量範囲に基づく音声データのトリミングなど、音声解析に役立つさまざまな機能をスクリプトで実現できます。

:::

### 出力例

```bash
無音部分のチェックを開始します...
スキップ: file6_empty.wav は空のファイルです。
エラー: file5_unnecessary_silence.wav に不要な無音部分があります。
  - 冒頭の無音: 500ms
  - 末尾の無音: 500ms
```

## 6. ファイルサイズの妥当性チェック

音声素材のファイルサイズが極端に小さい（例えば 0KB）場合、ファイルが壊れている可能性があります。また、非常に大きいサイズのファイルは、不適切なフォーマットや異常な長さの素材が含まれている可能性があります。

このような問題を防ぐために、ファイルサイズの範囲をチェックする仕組みを追加しましょう。

```python
def check_file_size(folder_path, min_size=1, max_size=10_000_000):
    """
    ファイルサイズが妥当かをチェックする
    :param folder_path: チェック対象のフォルダパス
    :param min_size: ファイルサイズの下限（デフォルトは1バイト）
    :param max_size: ファイルサイズの上限（デフォルトは10MB）
    """
    print("\nファイルサイズのチェックを開始します...")

    for root, _, files in os.walk(folder_path):
        for file in files:
            # ファイルパスを取得
            file_path = os.path.join(root, file)

            # ファイルサイズを取得
            file_size = os.path.getsize(file_path)

            # ファイルサイズをチェック
            if file_size < min_size:
                print(f"エラー: {file} のサイズが小さすぎます（{file_size} バイト < {min_size} バイト）")
            elif file_size > max_size:
                print(f"エラー: {file} のサイズが大きすぎます（{file_size} バイト > {max_size} バイト）")
                
                
```

### 出力例

```bash
ファイルサイズのチェックを開始します...
エラー: file6_empty.wav のサイズが小さすぎます（0 バイト < 1 バイト）
```

# まとめ

プロジェクトの品質を保つためにも。サウンド素材のチェックは欠かせません。今回の記事ではPythonを使って自動化スクリプトを作成を行いました。これによってトラブルを未然に防ぎ、開発チーム全体の負担を軽減することができます。

本記事で紹介したスクリプトは、ゲーム開発以外の音声処理プロジェクトにも応用できます。また、もしも更に発展したチェックを実現したい場合は以下のような拡張を検討してみてください。

- **音声の波形やスペクトル解析**
    - 特定の周波数帯の確認や、異常音の検出。
- **自動修正機能の追加**
    - 命名規則や音量の調整、無音部分のトリミングをスクリプトで自動化。
- **CI/CD パイプラインへの統合**
    - 音声素材を納品後すぐに自動チェックする仕組みを構築。

自動化できるところは機械に任せて、ミスや漏れが発生しない環境を作り上げていきましょう！