【Demucs＆Librosa】AI音声解析で音楽中の演奏区間を抽出する



## :metal:結論

AI音声解析で楽曲中の演奏のみの区間を抽出するプログラムを作成しました！

```
Start: 0.00s, End: 13.72s　
Start: 20.68s, End: 33.65s　
Start: 105.53s, End: 118.54s
Start: 256.28s, End: 268.43s　
```


## :guitar:目的

ギター好きな人がギターソロの区間を効率的に検索できるようなデータを作ってみたい。。！

その一環として、楽曲にギターソロが存在するのか、存在した場合どの秒数なのか、楽曲を分析済みのデータベースが必要になることがわかりました。

そのための準備として、まずはギターソロが存在するであろう
イントロ・間奏・アウトロの**演奏区間**について
まずは開始時間と終了時間を楽曲から抽出する必要がありました。

私自身今回始めてpythonを使いますので、初心者にもわかりやすいように書いていこうと思います:relaxed:

## :v:ゴール

楽曲を各パートに分離し、ボーカルパートのみのmp3ファイルを作成。
その後、ボーカルが存在しない区間の開始時間と終了時間を分析し出力する。

あんまり具体的な曲を例に使うとめんどくさそうなので、今回はband_song.mp3を分析するとします。

人間の耳で聞くとイントロリフ、サビ終わりリフ、間奏、アウトロという楽器だけのパートが存在する曲を分析していきます。

### 前提

・曲にギターが存在する
・5秒以上ボーカル無音区間があるとき間奏とする

#### 曲にギターが存在する

ここの判定は人間がやります。
楽器の有無を判定できるモデルもあるようですが、今回は間奏抽出に焦点を置きます。

#### 5秒以上ボーカル無音区間があるとき間奏とする

今回は肌感で５秒以上歌わなかったらもう間奏だろ！という基準で判定しています。
今回楽器演奏のみの区間を抽出するので、すなわちボーカル無音区間の抽出となります。

そうなるとボーカルはずっと歌っているわけではないので、歌の意図的な間や息継ぎや休符、も間奏となってしまいます。

本来複数の楽曲を解析し、実際のギターソロの持続時間を計測・集計すれば平均値はでるのですが、そこまでするのは時間がかかりすぎるので、ちょっと無理やりかもしれませんがいったん５秒と基準を定めました。

### 使用環境

MacBookPro Sonoma M3

python 3.9.7
pyenv 2.4.23

demucs 4.0.1
librosa 0.10.2.post1

## :writing_hand_tone2:手順

- python環境を構築する
- Demucsで楽曲を各パートに分離する
- Librosaでボーカルパートを分析する

まず大量の音楽を学習させるのは大変なので、学習済モデルを使用して、パート毎に分離します。
楽曲は複数の楽器が混ざっていると分析しにくくなるので、まずは分離させます。


### python環境構築しよう

できるだけPCが汚れないように、仮想環境でpythonを起動していこうと思います。

pyenvでpythonのバージョンを切り替えるようにして、venv上にライブラリをインストールし、pythonを実行させます。

```
＃プロジェクトディレクトリ作成
mkdir guitar_soro_app
cd guitar_soro_app

 # 仮想環境ファイルを作成
python -m venv venv

 # 仮想環境を起動（勝手に環境内のコマンドラインへ）
source venv/bin/activate

 # 仮想環境内でインストール
python -m pip install --upgrade pip
python -m pip install librosa
python -m pip install -U demucs
```
プロジェクトの最終的なディレクトリはこのような構成になります。
musicに音楽ファイル、script配下に区間算出のPythonファイルをいれていきます。
separatedとoutputはライブラリによって勝手に作られるので気にしなくても大丈夫です！

```
guitar_soro_app/
├── music/
│   └── band_song.mp3
├── output/
│   └── htdemucs_6s/
│       └── ultra_soul/
│           └── vocals.wav
├── scripts/
│   └── librosa/
│       └── analyze.py
├── separated/
│   └── htdemucs_6s/
│       └── band_song/
│           └── vocals.mp3
│           └── guitar.mp3
│           └── ...(各パートのmp3ファイル)
└── venv/
    └── bin/
```



### 楽曲からボーカルとギターを分離しよう

Demucsは音源分離のための学習済みモデルの音声系のAIです。

音楽のミックスから特定のトラック（ボーカル、ドラム、ベース、その他）を分離することができます。

（耳コピする人はまじで便利です！）

Demucsの最新版は作成者が退社されたこちらのリポジトリになります（V4）
今回はこちらを使っていきます。

https://github.com/adefossez/demucs

Facebookで作成した一つ前の公式？モデル（V3）もありましたがこちらは使用しませんでした。
既存の記事はこちらの解説が多いです。

https://github.com/facebookresearch/demucs/tree/v3


http://cedro3.com/ai/demucs/#google_vignette

今までの機能ではボーカル、ベース、ドラム、その他という感じで４つにしか分類できませんでした。
なぜならキーボードとギターは音域が被っているので分離が難しいからです。

しかし最近のV4では **htdemucs_6s** というモデルではその他を更に解析することができ、音域が被っているギターとキーボード（ピアノ）を分離することができます。

#### demucsさっそく使っていく

仮想環境を起動します（勝手にvenv環境内のコマンドラインへ入ります）
```
source venv/bin/activate
```
そこでdemucsコマンドを実行します。

```
 demucs --name htdemucs_6s --mp3 music/ultra_soul.mp3
```

ターミナルから実行すると勝手にseparatedというフォルダが作成され、その中に楽曲ごとに分離されていました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3621281/5b79d669-a486-2908-0853-17e511084174.png)


コマンドラインには下記のオプションを追加しています。
（--mp3）
- 使用する音楽ファイルと同様のファイル形式で出力させること
    - デフォルト値がwavで出力しようとしたのでエラ-がでた。変換対象の拡張子と同じだと変換できた。

（--name htdemucs_6s）
- 使用するモデルを選択すること　
    - ゆくゆくのギター音の解析のため、ギターとピアノを分離したかった。

他にもオプションや学習済モデルがたくさんあるのですが、公式に詳しく書いていないので、ヘルプオプションで把握していくしかないです。

```
 demucs --help


 オプション引数:
-h, --help: このヘルプメッセージを表示して終了します。
-s SIG, --sig SIG: ローカルでトレーニングされたXP署名。
-n NAME, --name NAME: プリトレーニング済みモデルの名前または署名。デフォルトはhtdemucs。
--repo REPO: -nで使用するプリトレーニング済みモデルが含まれるフォルダ。
-v, --verbose: 詳細モードを有効にします。
-o OUT, --out OUT: 抽出したトラックを保存するフォルダ。モデル名のサブフォルダが作成されます。
--filename FILENAME: 出力ファイルの名前を設定します。{track}（拡張子なしのトラック名）、{trackext}（トラックの拡張子）、{stem}（分離された音源名）、{ext}（デフォルトの出力ファイル拡張子）を使用できます。デフォルトは{track}/{stem}.{ext}。
-d DEVICE, --device DEVICE: 使用するデバイス。デフォルトは利用可能な場合はcuda、そうでない場合はcpu。
--shifts SHIFTS: エクイバリアント安定化のためのランダムシフト数。分離時間が増加しますが、Demucsの品質が向上します（論文では10が使用されました）。
--overlap OVERLAP: 分割部分のオーバーラップ量。
--no-split: オーディオをチャンクに分割しません（大量のメモリを使用する可能性があります）。
--segment SEGMENT: 各チャンクの分割サイズを設定します。これにより、GPUのメモリ使用量を節約できます。
--two-stems STEM: オーディオを{STEM}とno_{STEM}にのみ分離します。
--int24: 出力のwavファイルを24ビットのwavとして保存します。
--float32: 出力のwavファイルをfloat32として保存します（サイズが2倍になります）。
--clip-mode {rescale,clamp}: クリッピングを回避するための戦略を設定します。信号全体を必要に応じてスケールダウンする（rescale）またはハードクリッピングする（clamp）。
--flac: 出力wavファイルをflac形式に変換します。
--mp3: 出力wavファイルをmp3形式に変換します。
--mp3-bitrate MP3_BITRATE: 変換後のmp3のビットレート。
--mp3-preset {2,3,4,5,6,7}: MP3エンコーダープリセット。2は最高品質、7は最速の処理速度。デフォルトは2。
-j JOBS, --jobs JOBS: ジョブの数を設定します。これによりメモリ使用量が増加する可能性がありますが、複数コアが利用可能な場合は処理速度が大幅に向上します。
```

公式にはギターとピアノの分離は、精度が悪いと書いていましたが、わりとギターの音のみにしっかり切り離されていて精度よかったです。

#### いざ聞いてみる

完璧ではないものの、十分きれいに別れています！！

#### Demucsを使用するまでの失敗
:::note warn
splitterというパッケージでも同様の分離処理ができるようなのですが、私の環境ではうまく作動しませんでした。

インストール・実行しようとしたが、依存関係の不整合やビルドエラー（distutils.msvccompiler の問題など）でうまく動作しませんでした。
古いライブラリがプロジェクト内で使用されているみたいで、sonomaのような新しいMacの環境だと微妙かもしれません。


Demucsのほうがsplitterよりコミット履歴が新しく精度も悪くないので、今回はDemucsを使用しています。
:::



### librosaを使って間奏の区間を洗い出してみよう

LibROSAは、Pythonで音声・音楽データを扱うためのライブラリです。

音響信号の読み込みや再生速度・音程の変更、テンポやビートの検出、MFCCなどの特徴量抽出、スペクトログラムの描画などが簡単にできます。

音声解析や音楽情報検索（MIR）の基礎的な処理に適しており、柔軟で幅広い用途に対応可能です。

https://github.com/librosa/librosa

さきほど分離したボーカルのみの音源を用いて、無音区間を検出して、間奏区間の開始から終了の時間を出力させます。
これにより、曲全体のどの部分が楽器演奏中心の区間なのかが割り出せます。
（イントロや間奏）

具体的な方法を言うと、ボーカルのみのパートを細かくフレームサイズという単位に音楽を切り取って、そのフレームが無音かどうかを判定しています。

もっと噛み砕くと、0.01秒毎に曲を分断して、音の強さを表すデシベルという単位に変換します。
そして、その区間が人間の耳で聞こえる閾値（デシベル）かどうかを判定します。


vocal_fileの部分には先程Demucsで分離したvocal.mp3を分析させます。


```python:scripts/librosa/analyze.py
import librosa
import numpy as np

def detect_silent_sections(file_path, silence_threshold=-30.0, min_silence_duration=5):
    """
    ボーカルトラックから無音区間（ボーカルがない区間）を検出
    """
    try:
        print(f"Loading file: {file_path}")
        y, sr = librosa.load(file_path, sr=None)

        print("Audio loaded. Calculating energy...")
        frame_length = 2048  # フレームサイズ
        hop_length = 512     # フレームの間隔
        energy = np.array([
            sum(abs(y[i:i+frame_length]**2)) for i in range(0, len(y), hop_length)
        ])

        # デシベルスケールに変換
        energy_db = 10 * np.log10(energy + 1e-6)  # 1e-6はゼロ割防止用
        print("Energy calculated. Converting to dB scale...")

        # 無音フレームの判定
        print("Detecting silence...")
        silence_frames = energy_db < silence_threshold

        # デバッグログを追加（フレームごとの音量を表示）
        for i, energy_value in enumerate(energy_db):
            time = i * hop_length / sr
            print(f"Time: {time:.2f}s, Energy (dB): {energy_value:.2f}, Silent: {energy_value < silence_threshold}")

        # 無音フレームを秒単位でグループ化
        print("Grouping silent sections...")
        silence_times = []
        silence_start = None
        current_silence_duration = 0

        for i, is_silent in enumerate(silence_frames):
            time = i * hop_length / sr  # フレームを秒に変換
            if is_silent:
                if silence_start is None:
                    silence_start = time
                current_silence_duration += hop_length / sr
            else:
                if silence_start is not None:
                    # 無音区間が指定された最小継続時間以上の場合のみ記録
                    if current_silence_duration >= min_silence_duration:
                        silence_end = time
                        silence_times.append([silence_start, silence_end])
                        print(f"Detected silence from {silence_start:.2f}s to {silence_end:.2f}s")
                    # リセット
                    silence_start = None
                    current_silence_duration = 0

        # 最後の無音区間を確認
        if silence_start is not None and current_silence_duration >= min_silence_duration:
            silence_end = len(y) / sr
            silence_times.append([silence_start, silence_end])
            print(f"Detected silence from {silence_start:.2f}s to {silence_end:.2f}s")

        print("Silence detection complete.")
        return silence_times

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

if __name__ == "__main__":
    #分析したいボーカルのみのファイルパスを記載
    vocal_file = "/Users/sousuke/Desktop/practice/demucs_test/separated/htdemucs_6s/band_song/vocal.mp3"
    try:
        silent_sections = detect_silent_sections(vocal_file, silence_threshold=0, min_silence_duration=5)
        print("Detected silent sections (vocal-free):")
        for start, end in silent_sections:
            print(f"Start: {start:.2f}s, End: {end:.2f}s")
    except Exception as e:
        print(f"Failed to process: {e}")
```
ここの抽出に肝なのが、**何をもって無音とするかの閾値**です。

人間の耳ではほぼ0～120デシベル（dB）の範囲の音を聞き取ることができます。

でもAIは音圧が少しでもあると、無音でないと判定してしまいます。
人間の聞こえないような僅かな音がしただけでも、厳密いうと無音ではないですよね。

detect_silent_sectionsという作成した関数の引数を下記のように、切り取り精度を調整していきました。

```
#無音の基準を０デシベルとする
silence_threshold=0

＃無音期間を５秒とする
min_silence_duration=5
```


#### 検証してみる

プロジェクト配下でターミナルで実行します。
```
python scripts/librosa/analyze.py
```

下記のように出力されました！

```
Grouping silent sections...
Detected silence from 0.00s to 13.72s
Detected silence from 20.68s to 33.65s
Detected silence from 105.53s to 118.54s
Detected silence from 256.28s to 268.43s
Silence detection complete.
Detected silent sections (vocal-free):
Start: 0.00s, End: 13.72s　
Start: 20.68s, End: 33.65s
Start: 105.53s, End: 118.54s
Start: 256.28s, End: 268.43s

```
この分析した曲の間奏は、下記のような構成です。

イントロ｜歌｜ギターリフ｜１番｜ギターリフ｜２番｜アウトロ

人間の耳で聴き比べたところ、かなり精度高く間奏を抽出できていること確認できました！
目標達成:v_tone1:

```
Start: 0.00s, End: 13.72s　（イントロ）
Start: 20.68s, End: 33.65s　（ギターリフ）
Start: 105.53s, End: 118.54s　（ギターリフ）
Start: 256.28s, End: 268.43s　（アウトロ）

```
## まとめ

この手順で間奏のみの区間を抽出できました！

- python環境を構築する
- Demucsで楽曲を各パートに分離する
- Librosaでボーカルパートを分析する


librosaとDemucsのような機械学習モデルを使えば初心者でも簡単に音楽分析できることがわかりました。
はじめはモデルを学習させるところから作ろうと思いましたが、膨大なデータとマシンパワが必要でお金が掛かりそう。。と思っていたのですが、助かりました:sunny:

次の目標としては間奏区間が抽出できたので、それをどうやってギターソロと判定するのかAIを使って調査していきたいと思います！

## このプロジェクトの課題

判定をもっと自動化させたいです！
- 一つのバッチにまとめる
→今はターミナルで手動実行している
- 曲にギターが存在しているか人間が判断している
→最近バンドでもギターない曲増えてるのでそこもAIに判断させたい
- パスがフルパスになってるのでフォルダ内をループさせる
→取得した音声データを一気に分析したい
- 出力結果をDBに保存する
→現在ターミナルに出力されるだけなので楽曲データベースとして保存する
- 何を持ってギターソロと判定するのかモデルが必要
→間奏がアルペジオやバッキングと同じであれば、想定しているメロディー主体のギターソロではない。