ギターソロ区間の自動判定に挑戦した話 🎸🎶🔍



## はじめに 🎯
前回記事では学習済モデルAIを用いて、楽曲中の演奏のみの区間、
つまりギターソロが存在するであろう区間を特定するプログラムを作成しました。

https://qiita.com/sousuke_neo/items/3df9f08ba425e3bde8f6


本記事ではDemucsで分離したギター音源を基に、特定の間奏区間が**リードギター（単音引き）主体**か**リズムギター（コード引き）主体**かを判定するアルゴリズムを作る挑戦を行いました。

音楽に詳しくない方に向けて説明すると、リードギターとはメロディを奏でるパートであり、リズムギターは伴奏の役割を果たすパートです。

この2つを自動で区別することは、音楽分析において難しい課題の1つです。本記事では、その挑戦の過程と失敗を振り返り、得られた知見を共有します。

---

## 目標 🎯🎧📊
Demucsで分離したギター音源から、以下のような情報を自動で判定することを目指しました：

1. 間奏の区間がリードギター（単音引き）主体か、リズムギター（コード引き）主体かを判定する。
2. ギターソロ区間の特徴を解析し、他のスタイルとの違いを明確にする。

---

## 取り組みの流れ

### 1. 初期アプローチ 🎸✨

#### 使用したツール
- **Demucs**: 音源分離ツール。ギター音源をリズムギターとリードギターが混在した状態で分離。
- **Librosa**: Pythonの音響信号処理ライブラリ。

#### 実装内容
- 音源から無音区間を検出し、間奏の区間を手動で指定。
- 指定された間奏区間を解析し、ピッチ情報とエネルギー情報を基にリードギターかリズムギターかを判定。

#### 主なロジック
- **ピッチ解析**: 音声中のピッチ（周波数）の同時発生数を解析。
  - 単音引き：ピッチの同時発生数が少ない。
  - コード引き：ピッチの同時発生数が多い。
- **Harmonic-to-Noise Ratio（HNR）**: ハーモニクスとノイズの比率を計算し、ハーモニクスが高い場合をリードギターと推定。

```python
#ハーモニクス率で判定
import librosa
import numpy as np
# import matplotlib.pyplot as plt


def analyze_guitar_section(file_path, intervals, threshold_harmonicity=0.5):
    """
    指定された区間で単音引き主体かコード引き主体かを判定

    :param file_path: ギターパート音源ファイルのパス
    :param intervals: [(start_time, end_time), ...] 区間のリスト（秒単位）
    :param threshold_harmonicity: 単音判定の閾値（高いほど単音寄りに判定）
    :return: 各区間ごとの判定結果 [{"interval": (start, end), "label": "単音引き" or "コード引き"}, ...]
    """
    try:
        print(f"Loading file: {file_path}")
        y, sr = librosa.load(file_path, sr=None)
        results = []

        for start, end in intervals:
            print(f"Analyzing section: {start:.2f}s to {end:.2f}s")
            start_sample = int(start * sr)
            end_sample = int(end * sr)
            y_section = y[start_sample:end_sample]

            # ハーモニクスとノイズ成分を分離
            print("Extracting harmonics and percussive components...")
            y_harmonic, y_percussive = librosa.effects.hpss(y_section)

            # ハーモニクスエネルギーを計算
            harmonic_energy = np.sum(y_harmonic ** 2)
            percussive_energy = np.sum(y_percussive ** 2)
            
            # ハーモニクス率を計算
            harmonicity = harmonic_energy / (harmonic_energy + percussive_energy + 1e-6)
            print(f"Harmonicity: {harmonicity:.2f}")

            # 判定
            label = "単音引き" if harmonicity > threshold_harmonicity else "コード引き"
            results.append({"interval": (start, end), "label": label})

        return results

    except Exception as e:
        print(f"An error occurred: {e}")
        raise


if __name__ == "__main__":
    # ギターパート音源ファイルのパス
    guitar_file = "/Users/manabe_soichiro/Desktop/practice/demucs_test/output/htdemucs_6s/shimi/guitar.wav"

    # 間奏区間のリスト（開始時間, 終了時間）
    intervals = [
        (0.0, 19.72),  # 例: 30秒から45秒の間奏区間
        (20.68, 33.65), # 例: 60秒から75秒の間奏区間
    ]

    # 分析実行
    results = analyze_guitar_section(guitar_file, intervals, threshold_harmonicity=0.6)

    # 結果出力
    print("Analysis Results:")
    for result in results:
        start, end = result["interval"]
        label = result["label"]
        print(f"Interval {start:.2f}s - {end:.2f}s: {label}")


```



#### 結果
**失敗**: リズムギターが含まれているため、リードギターを正確に判定できませんでした。
- ピッチ解析では、コード引きと単音引きの違いを明確に区別できない。
- HNRの値も十分な指標とはならなかった。

---

### 2. ピッチ分布の可視化 📊🖼️🔍

#### 改善点
初期アプローチではピッチ解析の信頼性が低かったため、音源に原因がありそうだと考え、ピッチの分布を可視化することにしました。

```python
def plot_pitch_distribution(pitches, magnitudes, sr, interval_start, interval_end, threshold):
    """
    検出されたピッチの分布をプロット
    """
    pitch_values = []

    # 各フレームごとに、信頼度が高いピッチを収集
    for t in range(pitches.shape[1]):
        strong_pitches = pitches[:, t][magnitudes[:, t] > threshold]
        pitch_values.extend(strong_pitches)

    # ヒストグラムの作成
    plt.figure(figsize=(10, 6))
    plt.hist(pitch_values, bins=100, range=(50, 2000), color="skyblue", edgecolor="black")
    plt.title(f"Pitch Distribution ({interval_start:.2f}s to {interval_end:.2f}s)")
    plt.xlabel("Frequency (Hz)")
    plt.ylabel("Count")
    plt.grid(True)
    plt.show()


```


#### 実装内容
- ピッチ分布をヒストグラムで可視化。
  - ピッチの周波数帯域を80Hz–2000Hzに制限。
  - ピッチ信頼度（confidence）が一定以上のもののみを解析対象とする。

- **期待されるコード引きの区間**: ピッチが広く分布し、同時に多くの音が出ている特徴が見られる。
- **期待される単音引きの区間**: より狭い周波数帯域でピッチが集中する。


#### 結果
以下のグラフが得られました：

コード弾き主体の区間
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3621281/fc5d0f12-405d-6fc2-e255-d3a1c7ddb4ce.png)


単音引き主体の区間
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3621281/032e614c-abc1-c631-40aa-f44cdfb1beb9.png)

#### 分析
- グラフでは、単音引きとコード引きの違いが明確に現れない。
- ピッチカウントが多すぎる原因として、リズムギターの干渉やノイズが考えられる。

---

### 3. 時間的分離の導入 ⏱️📊🎶

#### 改善点
区間全体を解析するのではなく、**0.1秒単位でフレームを分割**し、そのフレーム内で支配的な特徴を解析するアプローチを試みました。

#### 実装内容
- 短時間フレームでの解析：
  - 各フレームごとに、最も強いピッチを抽出。
  - 各フレームでの支配的なピッチ数を集計し、区間全体で平均値を算出。
- ピッチ信頼度（confidence）の分布をプロットし、閾値の適切性を検証。

#### 結果
- 短時間フレーム解析でも、リズムギターが干渉し、正確なリードギター判定は困難。
- ピッチ信頼度の閾値を調整しても結果に大きな変化は見られず。

ピッチ信頼度の分布の0.1間のプロット
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3621281/64515d87-04f3-79af-54e4-f1d971b00745.png)


---

### 4. 課題と結論 🎶📉🤔

#### 課題
1. **リズムギターの干渉**: リードギターとリズムギターが同時に鳴っている場合、解析が混乱する。
2. **ノイズの影響**: ノイズや高周波数成分がピッチ解析に影響を与える。
3. **人間の直感とのギャップ**: 人間の耳では簡単に聞き分けられるが、アルゴリズムで同じ精度を再現するのは難しい。

### ソースコード

```python
import librosa
import numpy as np    

def analyze_dominant_pitch(pitches, magnitudes, sr, pitch_confidence_threshold, freq_range=(80, 1500)):
    """
    フレームごとに支配的なピッチを解析し、特定の周波数範囲内で評価
    """
    dominant_pitches = []
    for t in range(pitches.shape[1]):  # フレームごとに処理
        frame_pitches = pitches[:, t]
        frame_magnitudes = magnitudes[:, t]

        # 閾値以上の振幅を持つピッチを取得
        strong_pitches = frame_pitches[(frame_magnitudes > pitch_confidence_threshold) &
                                        (frame_pitches >= freq_range[0]) &
                                        (frame_pitches <= freq_range[1])]
        strong_magnitudes = frame_magnitudes[(frame_magnitudes > pitch_confidence_threshold) &
                                              (frame_pitches >= freq_range[0]) &
                                              (frame_pitches <= freq_range[1])]

        if len(strong_pitches) > 0:
            # 最大振幅を持つピッチを取得
            max_index = np.argmax(strong_magnitudes)
            dominant_pitches.append(strong_pitches[max_index])


    return dominant_pitches

def detect_guitar_playing_style_with_dominance(file_path, intervals, frame_duration=0.1, pitch_confidence_threshold=0.6, hnr_threshold=0.6):
    """
    支配的なピッチの解析を加えたギターパートの判定
    """
    try:
        print(f"Loading file: {file_path}")
        y, sr = librosa.load(file_path, sr=None)

        results = []
        frame_length = int(frame_duration * sr)  # フレームサイズ（サンプル数）

        for start_time, end_time in intervals:
            print(f"Analyzing section: {start_time:.2f}s to {end_time:.2f}s")
            start_sample = int(start_time * sr)
            end_sample = int(end_time * sr)
            section_audio = y[start_sample:end_sample]

            # 区間をフレームごとに分割
            num_frames = (len(section_audio) // frame_length) + 1
            dominant_pitch_count = 0
            total_frames = 0

            for i in range(num_frames):
                frame_start = i * frame_length
                frame_end = min((i + 1) * frame_length, len(section_audio))
                frame_audio = section_audio[frame_start:frame_end]

                # ハーモニクスとパーカッシブ成分の分離
                harmonic, _ = librosa.effects.hpss(frame_audio)

                # ピッチ解析
                pitches, magnitudes = librosa.piptrack(y=harmonic, sr=sr)

                # 支配的なピッチを取得
                dominant_pitches = analyze_dominant_pitch(pitches, magnitudes, sr, pitch_confidence_threshold)
                dominant_pitch_count += len(dominant_pitches)
                total_frames += 1

            # 平均支配ピッチ数
            avg_dominant_pitch_count = dominant_pitch_count / total_frames
            print(f"Average dominant pitch count: {avg_dominant_pitch_count:.2f}")

            # 判定ロジック
            if avg_dominant_pitch_count <= 2:  # 支配的なピッチ数が少なければ単音引きと判定
                results.append((start_time, end_time, "単音引き"))
            else:
                results.append((start_time, end_time, "コード引き"))

        print("Analysis Results:")
        for start, end, style in results:
            print(f"Interval {start:.2f}s - {end:.2f}s: {style}")
        return results

    except Exception as e:
        print(f"An error occurred: {e}")
        raise


if __name__ == "__main__":
    guitar_file = "/Users/manabe_soichiro/Desktop/practice/demucs_test/separated/htdemucs_6s/KMDT25/guitar.mp3"
    # 事前に指定された間奏の区間
    intervals = [(1.0, 13.0), (20.68, 30.65)]

    try:
        detect_guitar_playing_style_with_dominance(guitar_file, intervals)
    except Exception as e:
        print(f"Failed to process: {e}")


```



#### 結論
リードギターかリズムギターかを判定するアルゴリズムの開発は、現時点では**音源のさらなる分離技術の向上**が必要と判断しました。

---

## 学び 📚💡

今回の挑戦で得られた知見は以下の通りです：

1. **解析にはデータの質が重要**:
   - リズムギターとリードギターが混在する音源では解析の精度が大きく低下する。
2. **可視化の有用性**:
   - ピッチや信頼度の分布を可視化することで、問題点やパターンが浮き彫りになった。
3. **人間の耳の優位性**:
   - 人間の耳が持つ文脈理解や音色感知能力をアルゴリズムで再現するのは非常に難しい。

---

## 最後に 🔚📜🎶

今回の挑戦は失敗に終わりましたが、音響信号処理の難しさや可能性を改めて感じる良い機会となりました。この記事が同様の課題に取り組む方々の参考になれば幸いです。

次回は、機械学習やデータセットの活用を視野に入れた新たなアプローチを検討していきたいと思います。

悔しいです！