Spotify APIでaudio_featuresが使えなくなったので代わりを作る

# SpotifyのAPIが（いくつか）使えなくなった
音楽ストリーミングサービスのSpotifyでは曲やプレイリストの情報を取得するAPIが用意されています。曲の特徴を表すaudio_featuresというものがあり、Spotifyの独自指標ではありますが、acousticness(生楽器っぽさ)やdanceability(踊れるノリのいい曲か)といった特徴を返してくれる機能がありました。

https://developer.spotify.com/documentation/web-api/reference/get-audio-features


しかし、2024年11月に一部APIの利用が停止されてしまいました。

https://developer.spotify.com/blog/2024-11-27-changes-to-the-web-api

# いろんなライブラリを使って、簡単に曲の分析をしよう！
先に話したaudio_featuresで取得可能な特徴を取得するには、専用の機械学習モデルを作成する必要があります。学習データを集めて、「これはノリがいい曲」「これは暗い曲」などのフラグをつける作業はかなり大変です。
なので、今回はPythonで利用可能な音声処理のライブラリ等を使って、簡易的に曲の特徴量を取得するコードを作成しようと思います。
このコードはSpotifyで取り扱っていない曲についてもmp3ファイルさえあれば特徴量抽出することが可能なメリットもあります。

コードは本記事の最後に記載しています。

# 特徴量
Spotifyのaudio_featuresで取得できる情報のうち、以下の特徴量を算出しようと思います。
- acousticness
- danceability
- instrumentalness
- liveness
- speechiness
- valence
- energy


## 前処理
その前に各特徴量を抽出するための基本音響情報を計算します。
```python
audio_path = "/path/to/mp3"

# --- (1) pydubでMP3を読み込み ---
audio = AudioSegment.from_mp3(audio_path)
# numpy配列(float32)に変換
y = np.array(audio.get_array_of_samples(), dtype=np.float32) / 32768.0
if audio.channels == 2:
    y = y.reshape((-1, 2)).mean(axis=1)

# サンプリングレートを統一するためリサンプリング
orig_sr = audio.frame_rate
if orig_sr != sr:
    y = librosa.resample(y, orig_sr=orig_sr, target_sr=sr)

# --- (2) 基礎音響特徴量 ---
# テンポ
tempo, _ = librosa.beat.beat_track(y=y, sr=sr)
if isinstance(tempo, np.ndarray):
    tempo = float(tempo[0])
elif isinstance(tempo, (np.float32, np.float64)):
    tempo = float(tempo)

# HPSS => パーカッシブ / ハーモニック
# パーカッシブ: ドラムなどのリズミカルな曲
# ハーモニック: メロディ主体の曲
# 比率を計算し、値が大きいほどリズム要素の強い曲
y_harm, y_perc = librosa.effects.hpss(y)
rms_harm = np.mean(librosa.feature.rms(y=y_harm))
rms_perc = np.mean(librosa.feature.rms(y=y_perc))
hpss_ratio = rms_perc / (rms_harm + 1e-9)

# RMS (全体)
# librosa.feature.rms()でフレームごとの音波形の振幅
# 平均と標準偏差を計算
rms_all = librosa.feature.rms(y=y)[0]
rms_mean = np.mean(rms_all)
rms_std = np.std(rms_all)

# スペクトルセントロイド
# 音の重心周波数
# 値が高いほど高周波成分が多く「明るい・鋭い」印象の音になる
sc_arr = librosa.feature.spectral_centroid(y=y, sr=sr)[0]
spectral_centroid_mean = np.mean(sc_arr)

# スペクトルロールオフ
# 曲のエネルギーがどの周波数までに含まれているか（ここでは85%）を示す指標。
# 値が高いほど高音域までエネルギーが多く含まれているといえる。
rolloff_arr = librosa.feature.spectral_rolloff(y=y, sr=sr, roll_percent=0.85)[0]
spectral_rolloff_mean = np.mean(rolloff_arr)

# スペクトルバンド幅
# スペクトルの広がり(帯域幅)を示し、値が大きいほど音の周波数分布が広い(高低域ともに強い)ことを意味する。
bandwidth_arr = librosa.feature.spectral_bandwidth(y=y, sr=sr)[0]
spectral_bandwidth_mean = np.mean(bandwidth_arr)

# スペクトルコントラスト
# 各周波数帯ごとのエネルギー差を特徴づける指標。
# 値が大きいほど同一フレーム内での高周波帯と低周波帯の音量差(コントラスト)が大きい。
spec_contrast = librosa.feature.spectral_contrast(y=y, sr=sr)
spectral_contrast_mean_arr = np.mean(spec_contrast, axis=1)  # shape=(7,)
spectral_contrast_mean = np.mean(spectral_contrast_mean_arr)

# onset_strength
# リズムの明瞭度
# 音の立ち上がり(アタック)の強さを示す指標。
onset_env = librosa.onset.onset_strength(y=y, sr=sr)
onset_mean = np.mean(onset_env)
onset_std = np.std(onset_env)

# Zero Crossing Rate
# 波形が正から負、あるいは負から正に「0」を越える回数の比率。
# 値が高いほど高周波成分やノイズ的要素が多い、あるいは無声音が多いとされる。
zcr_arr = librosa.feature.zero_crossing_rate(y=y)[0]
zcr_mean = np.mean(zcr_arr)

```

## acousticness
アコースティックさを示す指標。
[ここ](https://www.digimart.net/spcl/agwords/acoustic.html#:~:text=%E3%80%8C%E3%82%A2%E3%82%B3%E3%83%BC%E3%82%B9%E3%83%86%E3%82%A3%E3%83%83%E3%82%AF%E3%80%8D%E3%81%A8%E3%81%AF%E3%80%8C%E9%9F%B3%E9%9F%BF,%E3%81%AA%E3%81%BE%EF%BC%89%E6%A5%BD%E5%99%A8%E3%82%92%E6%8C%87%E3%81%99%E3%80%82)曰く、「電気を使わない楽器」のことを指すらしい。
この指標が低いほど打ち込みっぽい曲で、高いほどギターやドラムといった楽器を使用している曲という認識。

以下の要素をアコースティック要素が高いとして計算。
- スペクトルセントロイドが小さい≒高音域が強くない。
- HPSSが低い≒リズミカル要素が小さい。
- テンポもゆっくりな曲
```python
alpha = 0.5
sc_max = 8000.0
sc_norm = min(spectral_centroid_mean / sc_max, 1.0)  # スペクトルセントロイドを最大8kHzで正規化
hpss_perc = min(hpss_ratio, 5.0) / 5.0               # HPSS比を0~5の範囲に制限し、さらに0~1にスケーリング
tempo_norm = min(tempo / 200.0, 1.0)                 # テンポを200bpmで正規化し、上限を1.0
# (1 - 各種正規化値)を乗算し、値が大きいほどアコースティック要素が強いとみなす。
_acoustic = (1 - hpss_perc * alpha) * (1 - sc_norm) * (1 - tempo_norm * 0.3)
acousticness = max(0.0, min(_acoustic, 1.0))         # 0~1にクリップ

```

## danceability
ダンスに向いているノリの良い曲が高くなる指標。

以下の要素をダンスに向いているとして計算。
- ダンスミュージック多いBPM110~130
- 音の立ち上がりが強い≒わかりやすいリズム

```python
# テンポが110-130の範囲で最高、それより極端に遅い・速いと低下する
ideal_tempo_min, ideal_tempo_max = 110, 130
if tempo < ideal_tempo_min:
    tempo_score = tempo / ideal_tempo_min
elif tempo > ideal_tempo_max:
    tempo_score = (200 - tempo) / (200 - ideal_tempo_max)
    if tempo_score < 0:
        tempo_score = 0
else:
    tempo_score = 1.0
# onset_mean（音の立ち上がり強度）をダンスしやすさに反映させるために正規化
onset_norm = min(onset_mean / 2.0, 1.0)
# テンポスコアとアタック(onset)を平均
_dance = (tempo_score + onset_norm) / 2.0
# 0~1にクリップ
danceability = max(0.0, min(_dance, 1.0))

```

## instrumentalness
ボーカル要素が少ないほど高くなる指標。
ボーカル音声を分離することが理想的ですが、実現するには機械学習モデルなど別途ツールが必要です。ここでは「ボーカルっぽい要素」を計算し、それの逆数を取ることにします。

以下の要素をボーカル要素として計算。これの逆数とする。
- ハーモニック要素が多いとボーカルの可能性が高い
- 曲の中で強い周波数帯が人声帯域(1k~3kHz)

```python
# ハーモニック要素を正規化
harmonic_ratio = min(rms_harm / (rms_mean + 1e-9), 1.0)

# ボーカル帯域(約1k~3kHz)に近いほど "vocalness" が上がるとする
# ここでは 2kHz を中心, ±1kHz で正規化 → centroidが2kHzの場合 vocalっぽい
vocal_center = 2000.0
vocal_range = 1000.0  # ±1kHz
dist_from_vocal_center = abs(spectral_centroid_mean - vocal_center)
if dist_from_vocal_center > vocal_range:
    freq_mid = 0.0
else:
    freq_mid = 1.0 - (dist_from_vocal_center / vocal_range)

# ボーカルらしさ
vocalness_approx = (harmonic_ratio + freq_mid) / 2.0
# instrumentalness = 1.0 - vocalness
raw_inst = 1.0 - vocalness_approx
instrumentalness = max(0.0, min(raw_inst, 1.0))

```

## liveness
ライブ感が強いと高くなる指標。
以下をライブ要素として計算。
- ノイズ音が多い
- 音の振れ幅が大きい

```python
rms_sorted = np.sort(rms_all)
n_frames = len(rms_sorted)
low10_idx = int(n_frames*0.1)
if low10_idx < 1:
    low10_idx = 1
lowest_10pct_mean = np.mean(rms_sorted[:low10_idx])  # 下位10%の平均
bg_ratio = min((lowest_10pct_mean/(rms_mean+1e-9))*10.0, 1.0)  # 背景ノイズ比

applause_norm = min(zcr_mean / 0.1, 1.0)  # ZCR=0.1 => 1.0
dyn_norm = min(rms_std * 10.0, 1.0)

_liveness = (bg_ratio + applause_norm + dyn_norm)/3.0
liveness = max(0.0, min(_liveness, 1.0))
```

## speechiness
話すような音声が多いほど高くなる指標。
以下をspeechiness要素として計算
- 発話時間が多い≒音楽的でない部分(ノイズなど)が多い
- 発話が多い≒音の立ち上がりが多い

```python
# ゼロクロッシングレート(zcr)やオンセット量が多ければ、ラップや語りのような成分が多いと仮定
s_zcr = min(zcr_mean / 0.03, 1.0)    # ZCRから見たspeech成分の推定
s_onset = min(onset_mean / 5.0, 1.0) # Onsetから見たspeech成分の推定
_speech = 0.5 * s_zcr + 0.5 * s_onset
speechiness = max(0.0, min(_speech, 1.0))
```

## valence
曲がポジティブ、明るいと高くなる指標。
以下をポジティブな要素として計算
- テンポが早い
- 高周波数成分が高い≒明るい曲調

```python
# 明るさやポジティブ感を Tempo + Spectral Centroid + Rolloff で近似
tempo_norm = min(tempo / 200.0, 1.0)  # 200 BPMを仮上限
sc_norm_val = min(spectral_centroid_mean / 5000.0, 1.0)
roll_norm = min(spectral_rolloff_mean / 6000.0, 1.0)

# ベース値0.2 + (0.3*tempo_norm) + (0.3*sc_norm_val) + (0.2*roll_norm)
_val = 0.2 + 0.3*tempo_norm + 0.3*sc_norm_val + 0.2*roll_norm
valence = max(0.0, min(_val, 1.0))
```

## energy
曲の強さや活発さを表す指標。
以下をenergy要素として計算
- 音が大きい
- 音の振れ幅が大きい
- 音の立ち上がりが大きい
- 明るい曲

```python
loudness_norm = min(rms_mean * 20.0, 1.0)       # 大きい音量
dynamic_range_norm = min(rms_std * 10.0, 1.0)   # ダイナミクスが広い
onset_intensity_norm = min(onset_mean / 5.0, 1.0)  # アタックが多い
centroid_norm = min(spectral_centroid_mean / 3000.0, 1.0)  # 明るくシャープな音像
_energy = (
    loudness_norm + dynamic_range_norm + onset_intensity_norm + centroid_norm
) / 4.0
energy = max(0.0, min(_energy, 1.0))
```

# 実験
各特徴量がどうなるかを実際の曲に対する数値を確認します。

実験には魔王魂で提供されているボーカル曲(全44曲)を使用します。
https://maou.audio/category/song/

## acousticness
実験に使用した曲で最もacousiticnessが高かったのは「月と隼」(acousticness: 0.646)でした。
ピアノや民族的な音があり、確かに生楽器感は強いと思います。
<iframe width="560" height="315" src="https://www.youtube.com/embed/J5biP-8DYo8?si=l_hQwUw0l2vQawN9" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>

一方で、もっともacousticnessが低かったのは「泡沫のレクエルド」(acousticness: 0.434)でした。
イントロ、アウトロで大きく強調している音とか電子音っぽいので、そこを拾っているのではないかと思います。
<iframe width="560" height="315" src="https://www.youtube.com/embed/7vMVYAivYLU?si=3KdGZ6qi3dT_wKPd" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>

## danceability
danceabilityが最も高かったのは「VENUS ~女神と魔女の仮面~」(Danceability: 0.857)でした。
ダンスができるかというと、そういう曲ではないような気がします。がドラムの音がはっきりしてるところなどが評価されているんでしょうか。
<iframe width="560" height="315" src="https://www.youtube.com/embed/RN6TDNKuIvA?si=tDqDoLqZM42AZJVH" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>

一方で最もdanceabilityが低かったのは「UNNiVERSE」(Danceability: 0.542)でした。
ゆったりとした曲なので、ダンスには向いてないでしょうね。
<iframe width="560" height="315" src="https://www.youtube.com/embed/UgccwRi3e-A?si=c_QAZAeCZN1VjidV" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>

## instrumentalness
instrumentalnessが最も高かったのは「ヒカリトリガー」(instrumentalness: 0.653)でした。
かなり楽器が強調されている曲ですね。サビ終わりのリズムとかかなり特徴的ですね。

<iframe width="560" height="315" src="https://www.youtube.com/embed/QIlHnLxzkek?si=DJPkc9Uh2Sjkz4Y9" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>

一方で、最もinstrumentalnessが低かったのは「アフレラ」(instrumentalness: 0.142)でした。
ヒカリトリガーと比較して、ボーカルの強調が強いように思えます。

<iframe width="560" height="315" src="https://www.youtube.com/embed/XV2BmgtXwVM?si=LfRmNRoIS2XAX2Wu" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>

## liveness
livenessが最も高かったのは「Feels happiness」(liveness: 0.987)でした。
ライブ感？って感じですね。使われている音の何かがノイズとして判定されていたりするんでしょうか？
<iframe width="560" height="315" src="https://www.youtube.com/embed/7tRTbYxLDPg?si=zs9t6ah-izmWwAG-" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>

一方で、最もlivenessが低かったのは「Luna」(liveness: 0.390)でした。
ピアノとボーカルだけの部分が多く、ノイズなどもなく安定しているため、livenessが低いようです。
<iframe width="560" height="315" src="https://www.youtube.com/embed/N2P03bq1xN0?si=u9oe6Kb9liGSvj2_" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>

## speechiness
speechinessが最も高かったのは「VENUS ~女神と魔女の仮面~」(Speechiness: 0.857)でした。
発話数も比較的に多いのかな？って感じですね。
danceablityと同様にspeechinessの計算にonset(音の立ち上がり)を参照しているため、はっきりした音が多いため、こちらも高くなっている可能性があります。
<iframe width="560" height="315" src="https://www.youtube.com/embed/RN6TDNKuIvA?si=tDqDoLqZM42AZJVH" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>

一方、最もspeechinessが低かったのは「Luna」(Speechiness: 0.580)でした。
確かにゆったりした曲調ですので、発話数は少ないかもしれません。
こちらもlivenessの計算と同様にノイズの少なさを参照しているため、speechinessも低くなっているようです。
<iframe width="560" height="315" src="https://www.youtube.com/embed/N2P03bq1xN0?si=u9oe6Kb9liGSvj2_" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>

## valence
valenceが最も高かったのは「シャイニングスター」(valence: 0.821)でした。
有名な曲ですよね。明るい曲調です。
<iframe width="560" height="315" src="https://www.youtube.com/embed/Qd01-6xVSHk?si=rxa65k_caVgM5V2c" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>

一方でvalenceが最も低かったのは「月と隼」(valence: 0.580)でした。
哀愁のある曲調なため、納得感があります。
<iframe width="560" height="315" src="https://www.youtube.com/embed/J5biP-8DYo8?si=l_hQwUw0l2vQawN9" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>

## energy
energyが最も高かったのは「ベガロスト」(energy: 0.804)でした。
サビの力強さなどが評価されているようです。
<iframe width="560" height="315" src="https://www.youtube.com/embed/LHS10NJY28c?si=gcfDeqkP2JaO6jtu" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>

一方で最もenergyが最も低かったのは「Luna」(energy: 0.493)でした。
<iframe width="560" height="315" src="https://www.youtube.com/embed/N2P03bq1xN0?si=u9oe6Kb9liGSvj2_" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>

## 評価
各特徴量が最も高かった曲、低かった曲を確認しました。
主観ですが納得感のあるものあれば、？ってなるものもありました。
「Luna」や「VENUS」など異なる特徴量の最高値/最低値に入ってきました。これは各特徴量の計算ロジックが共通していることが原因と思われます。


# まとめ
Spotify APIのaudio_featuresで取得できる特徴量を簡易に計算するコードを作成しました。
Spotify APIと違い、Spotifyで取り扱っていない曲についてもmp3ファイルさえあれば特徴量抽出することが可能なメリットもあります。
実験結果から、おおまかには狙っている特徴を掴めているんじゃないかと思います。
一方で基本的に曲全体に対する平均化を行なっているところなど、かなり雑に特徴づけている印象もあります。
曲を印象付ける要素として、ドラムのリズムであったり、使われているコード進行、ひいては歌詞の内容など考慮できる点は多くあります。
これらの分析を自動化できれば、より精緻な計算ができると思います。

コード内で使われている定数などを調整しながら、納得感のある特徴量値にしていただければと思います。


# コード
コード実行において、pydubやlibrosaなどのライブラリインストールおよび、それらを使用するためのツールインストールを伴いますのでご注意ください。
```python
from pydub import AudioSegment
import numpy as np
import librosa

def audio_features(audio_path, sr=22050):
    
    # --- (1) pydubでMP3を読み込み ---
    audio = AudioSegment.from_mp3(audio_path)
    # numpy配列(float32)に変換
    y = np.array(audio.get_array_of_samples(), dtype=np.float32) / 32768.0
    if audio.channels == 2:
        y = y.reshape((-1, 2)).mean(axis=1)

    # リサンプリング
    orig_sr = audio.frame_rate
    if orig_sr != sr:
        y = librosa.resample(y, orig_sr=orig_sr, target_sr=sr)

    # --- (2) 基礎音響特徴量 ---
    # テンポ
    tempo, _ = librosa.beat.beat_track(y=y, sr=sr)
    if isinstance(tempo, np.ndarray):
        tempo = float(tempo[0])
    elif isinstance(tempo, (np.float32, np.float64)):
        tempo = float(tempo)

    # HPSS => パーカッシブ / ハーモニック
    y_harm, y_perc = librosa.effects.hpss(y)
    rms_harm = np.mean(librosa.feature.rms(y=y_harm))
    rms_perc = np.mean(librosa.feature.rms(y=y_perc))
    hpss_ratio = rms_perc / (rms_harm + 1e-9)

    # RMS (全体)
    rms_all = librosa.feature.rms(y=y)[0]
    rms_mean = np.mean(rms_all)
    rms_std = np.std(rms_all)

    # スペクトルセントロイド
    sc_arr = librosa.feature.spectral_centroid(y=y, sr=sr)[0]
    spectral_centroid_mean = np.mean(sc_arr)

    # スペクトルロールオフ
    rolloff_arr = librosa.feature.spectral_rolloff(y=y, sr=sr, roll_percent=0.85)[0]
    spectral_rolloff_mean = np.mean(rolloff_arr)

    # スペクトルバンド幅
    bandwidth_arr = librosa.feature.spectral_bandwidth(y=y, sr=sr)[0]
    spectral_bandwidth_mean = np.mean(bandwidth_arr)

    # スペクトルコントラスト
    spec_contrast = librosa.feature.spectral_contrast(y=y, sr=sr)
    spectral_contrast_mean_arr = np.mean(spec_contrast, axis=1)  # shape=(7,)
    spectral_contrast_mean = np.mean(spectral_contrast_mean_arr)

    # onset_strength => リズムの明瞭度
    onset_env = librosa.onset.onset_strength(y=y, sr=sr)
    onset_mean = np.mean(onset_env)
    onset_std = np.std(onset_env)

    # Zero Crossing Rate
    zcr_arr = librosa.feature.zero_crossing_rate(y=y)[0]
    zcr_mean = np.mean(zcr_arr)

    # (A) acousticness
    alpha = 0.5
    sc_max = 8000.0
    sc_norm = min(spectral_centroid_mean / sc_max, 1.0)
    hpss_perc = min(hpss_ratio, 5.0) / 5.0
    tempo_norm = min(tempo / 200.0, 1.0)
    _acoustic = (1 - hpss_perc * alpha) * (1 - sc_norm) * (1 - tempo_norm * 0.3)
    acousticness = max(0.0, min(_acoustic, 1.0))

    # (B) danceability
    ideal_tempo_min, ideal_tempo_max = 110, 130
    if tempo < ideal_tempo_min:
        tempo_score = tempo / ideal_tempo_min
    elif tempo > ideal_tempo_max:
        tempo_score = (200 - tempo) / (200 - ideal_tempo_max)
        if tempo_score < 0:
            tempo_score = 0
    else:
        tempo_score = 1.0
    onset_norm = min(onset_mean / 2.0, 1.0)
    _dance = (tempo_score + onset_norm) / 2.0
    danceability = max(0.0, min(_dance, 1.0))

    # (C) instrumentalness
    harmonic_ratio = min(rms_harm / (rms_mean + 1e-9), 1.0)

    vocal_center = 2000.0
    vocal_range = 1000.0  # ±1kHz
    dist_from_vocal_center = abs(spectral_centroid_mean - vocal_center)
    if dist_from_vocal_center > vocal_range:
        freq_mid = 0.0
    else:
        freq_mid = 1.0 - (dist_from_vocal_center / vocal_range)

    # ボーカルらしさ
    vocalness_approx = (harmonic_ratio + freq_mid) / 2.0
    # instrumentalness = 1.0 - vocalness
    raw_inst = 1.0 - vocalness_approx
    instrumentalness = max(0.0, min(raw_inst, 1.0))

    # (D) liveness
    rms_sorted = np.sort(rms_all)
    n_frames = len(rms_sorted)
    low10_idx = int(n_frames * 0.1)
    if low10_idx < 1:
        low10_idx = 1
    lowest_10pct_mean = np.mean(rms_sorted[:low10_idx])  # 下位10%の平均
    bg_ratio = min((lowest_10pct_mean / (rms_mean + 1e-9)) * 10.0, 1.0)  # 背景ノイズ比

    applause_norm = min(zcr_mean / 0.1, 1.0)  # ZCR=0.1 => 1.0
    dyn_norm = min(rms_std * 10.0, 1.0)

    _liveness = (bg_ratio + applause_norm + dyn_norm) / 3.0
    liveness = max(0.0, min(_liveness, 1.0))

    # (E) speechiness
    s_zcr = min(zcr_mean / 0.03, 1.0)
    s_onset = min(onset_mean / 5.0, 1.0)
    _speech = 0.5 * s_zcr + 0.5 * s_onset
    speechiness = max(0.0, min(_speech, 1.0))

    # (F) valence
    tempo_norm = min(tempo / 200.0, 1.0)  # 200 BPMを仮上限
    sc_norm_val = min(spectral_centroid_mean / 5000.0, 1.0)
    roll_norm = min(spectral_rolloff_mean / 6000.0, 1.0)

    _val = 0.2 + 0.3 * tempo_norm + 0.3 * sc_norm_val + 0.2 * roll_norm
    valence = max(0.0, min(_val, 1.0))

    # (G) energy (0.0~1.0)
    loudness_norm = min(rms_mean * 20.0, 1.0)  # 大きい音量なら高め
    dynamic_range_norm = min(rms_std * 10.0, 1.0)  # ダイナミクス広いなら高め
    onset_intensity_norm = min(onset_mean / 5.0, 1.0)  # アタック多いなら高め
    # timbreの一要素として、スペクトルセントロイドを加味
    centroid_norm = min(spectral_centroid_mean / 3000.0, 1.0)

    # 4つを平均 (用途に応じて加重平均も可)
    _energy = (
        loudness_norm + dynamic_range_norm + onset_intensity_norm + centroid_norm
    ) / 4.0
    energy = max(0.0, min(_energy, 1.0))

    features = {
        # 基礎
        "tempo": tempo,
        "hpss_ratio": hpss_ratio,
        "rms_mean": rms_mean,
        "rms_std": rms_std,
        "spectral_centroid_mean": spectral_centroid_mean,
        "spectral_rolloff_mean": spectral_rolloff_mean,
        "spectral_bandwidth_mean": spectral_bandwidth_mean,
        "spectral_contrast_mean": spectral_contrast_mean,
        "onset_mean": onset_mean,
        "onset_std": onset_std,
        "zcr_mean": zcr_mean,
        # 近似指標 + energy
        "acousticness": acousticness,
        "danceability": danceability,
        "instrumentalness": instrumentalness,
        "liveness": liveness,
        "speechiness": speechiness,
        "valence": valence,
        "energy": energy,
    }
    return features

```