the cabsからはじめる音響信号処理入門(Python)

##  はじめに
the cabs のライブ、申し込みすら叶わなかったその悔しさを胸に、Pythonで音を分析してみました。この記事では、信号処理による解析の過程と結果をお届けします。


##  参考リンクまとめ

https://x.com/thecabs_japan/status/1910618842738352513


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4046871/f1e3d0db-13c9-4d6b-b8f3-02cea1ce99e0.png)

使う曲
the cabsの『2月の兵隊』は、私が浪人生活を共に過ごした曲です。
(ハヌマーンの猿の学生と共にずっと聴いてました笑笑)



https://youtu.be/soLFVWKc5B4?si=CI2mvOK05IpuIb5O


https://aiacademy.jp/media/?p=1037




##  Pythonコード

YouTubeの音楽動画から音声をMP3形式で抽出し、波形やスペクトログラムによって可視化を行った後、テンポ（BPM）の推定、MFCCに基づく構造クラスタリング、ピッチ推定、そしてスペクトル重心やRMSに基づいた感情スコア（焦燥感・浮遊感）の算出までを一貫して行います。



```python
# 完全統合版：YouTube → MP3 → 可視化 → テンポ解析 → 構造解析 → ピッチ/感情マッピング

# --- Step 1: ライブラリのインストール（Colab再起動後用） ---
!pip install yt-dlp pydub librosa scikit-learn --quiet
!apt install ffmpeg -y

# --- Step 2: YouTubeからMP3をダウンロード ---
import yt_dlp
import os
from pydub import AudioSegment

# ダウンロードするYouTube動画のURL
url = "https://www.youtube.com/watch?v=soLFVWKc5B4"  # 任意のYouTube動画URL

# 出力パス設定
output_path = "downloaded_audio.%(ext)s"

# yt-dlpの設定オプション
ydl_opts = {
    'format': 'bestaudio/best',  # 最も良い音質を選択
    'outtmpl': output_path,      # 出力ファイル名設定
    'postprocessors': [{
        'key': 'FFmpegExtractAudio',    # MP3形式に変換
        'preferredcodec': 'mp3',        # MP3形式で保存
        'preferredquality': '192',      # 音質設定
    }],
    'quiet': True  # 出力を抑制
}

# YouTubeから音声をダウンロードしてMP3に変換
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
    ydl.download([url])

# ダウンロードしたファイルの名前を変更
os.rename("downloaded_audio.mp3", "audio.mp3")
print("MP3変換完了")  # 成功メッセージ

# --- Step 3: 音声読み込みと可視化 ---
import librosa
import librosa.display
import matplotlib.pyplot as plt
import numpy as np

# 音声ファイルを読み込み（サンプルレートは元のまま）
y, sr = librosa.load("audio.mp3", sr=None)

# 波形表示
plt.figure(figsize=(12, 3))
librosa.display.waveshow(y, sr=sr)  # 波形の可視化
plt.title("Waveform")               # タイトル
plt.xlabel("Time (s)")              # X軸ラベル
plt.ylabel("Amplitude")             # Y軸ラベル
plt.grid(True)                      # グリッド表示
plt.show()                          # 表示

# スペクトログラムの表示
plt.figure(figsize=(12, 4))
D = librosa.amplitude_to_db(np.abs(librosa.stft(y)), ref=np.max)  # スペクトルのdB表示
librosa.display.specshow(D, sr=sr, x_axis='time', y_axis='log')    # スペクトログラムの可視化
plt.colorbar(format='%+2.0f dB')                                      # カラーバー表示
plt.title("Spectrogram")                                              
plt.tight_layout()                                                    # レイアウト調整
plt.show()

# --- Step 4: テンポ解析 ---
tempo, beat_frames = librosa.beat.beat_track(y=y, sr=sr)  # テンポと拍の位置を抽出
print(f" 推定テンポ: {float(tempo):.2f} BPM")  # 推定されたBPM（ビート/分）

# --- Step 5: 拍ごとの構造解析 + ピッチ + 感情指標 ---
from scipy.cluster.hierarchy import linkage, fcluster
from sklearn.preprocessing import StandardScaler
from scipy.spatial.distance import cdist

# MFCC（メル周波数ケプストラム係数）の抽出と標準化
mfcc = librosa.feature.mfcc(y=y, sr=sr, n_mfcc=13)  # MFCCを13次元で抽出
mfcc_scaled = StandardScaler().fit_transform(mfcc.T)  # 標準化

# 階層クラスタリングによる楽曲の構造推定
Z = linkage(mfcc_scaled, method='ward')  # クラスタリング
clusters = fcluster(Z, t=4, criterion='maxclust')  # 4つのセクションに分ける

# ピッチの推定
pitches, magnitudes = librosa.piptrack(y=y, sr=sr)  # ピッチとその強度を抽出
pitch_values = pitches[magnitudes > np.median(magnitudes)]  # 強度が中央値以上のピッチを取得
pitch_mean = np.mean(pitch_values) if len(pitch_values) > 0 else 0  # 平均ピッチを計算

# 感情スコア指標計算
centroid = librosa.feature.spectral_centroid(y=y, sr=sr)[0]  # スペクトル重心
rms = librosa.feature.rms(y=y)[0]  # RMS（Root Mean Square）

# 感情スコアの計算
emotion_score = {
    "焦燥感 (agitation)": float(tempo) * np.mean(centroid) * np.mean(rms),  # 焦燥感の計算
    "浮遊感 (floating)": (1.0 / (1 + np.mean(centroid))) * (1.0 / (1 + np.std(rms)))  # 浮遊感の計算
}

# 楽曲構造の可視化（クラスタごとに色分け）
plt.figure(figsize=(12, 2))
plt.plot(clusters, color='purple')  # クラスタの表示
plt.title("Estimated Song Structure Segments")  # タイトル
plt.xlabel("Frame Index")  # X軸ラベル
plt.ylabel("Section Cluster")  # Y軸ラベル
plt.grid(True)  # グリッド表示
plt.tight_layout()  # レイアウト調整
plt.show()

# 感情スコアをDataFrame化して表示
import pandas as pd
emotion_df = pd.DataFrame.from_dict(emotion_score, orient='index', columns=["Score"])  # DataFrameに変換
emotion_df.index.name = "Emotion"  # インデックス名設定

# 感情スコアを表形式で表示
from IPython.display import display
display(emotion_df)  # 表形式で表示


```
結果


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4046871/87a5106b-1e32-419d-be33-9db069e6f61e.png)


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4046871/740d3dec-a8a5-4b64-b422-223c4c13fe3e.png)



![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4046871/443e6edf-1995-4d43-a695-c72e526d88d8.png)


---

##  曲の解析レポート

### ① **波形（Waveform）**
- **時間軸（x軸）**：0秒〜約2分40秒
- **振幅（y軸）**：-1.0〜+1.0 の範囲
- **特徴**：
  - 冒頭と終盤に数回、**無音（または静音）部分**が確認できます（振幅が急激に0近くになる）。
  - 全体として**ダイナミックレンジ（音量の強弱差）が狭い**＝マスタリングされている可能性が高い。
  - **波形がほぼフラットに近い** → 圧縮された音像（音圧の高いEDMやポップ系に多い）。

---

### ② **スペクトログラム（Spectrogram）**
- **x軸**：時間（秒）
- **y軸**：周波数（0Hz〜16kHz超）
- **色**：明るい＝音が強い（+0 dB）、暗い＝音が弱い（-80 dB）
- **特徴**：
  - **64〜512 Hz付近が特に強く、安定的** → 低音（ベースやキック）が中心。
  - その上（1kHz〜8kHz）も中〜高域が活発 → ボーカルやシンセ、ハイハットの存在。
  - 無音区間（黒く抜けた帯）が数箇所ある → 曲の**ブレイク**または**展開の切り替え**部分。
  - 周波数成分が常に満遍なく存在しており、**音の密度が高い**。

---

### ③ **構造セグメント（Estimated Song Structure Segments）**
- **x軸**：フレーム番号（≒時間）
- **y軸**：クラスタ（1〜4のセクション番号）
- **特徴**：
  - 曲全体を4つのセクションに分類（クラスタリング）。
  - **セクションの切り替えが多く、繰り返しも見られる** → リフレイン構造あり（例：サビの再登場）。
  - 12500〜13000付近の連続したクラスタ3〜4は、**印象的な展開**（ブリッジや間奏など）を示唆。
  - 最終的にはクラスタ2で終わっている → **導入と同じセクションに戻る**パターンの可能性。

---

##  総合評価と推定ジャンル

| 項目 | 推定内容 |
|------|----------|
| **テンポ感** | 速め（おそらく160〜180 BPM） |
| **音圧** | 非常に高い（圧縮強め） |
| **構成** | セクションが多く、展開に富む |
| **ジャンル傾向** | EDM・アニメOP・J-pop・ロック系にも該当し得る |
| **感情表現** | 焦燥感・推進力・高揚感が強い |

---