Wizard Notes

Pythonを使った音楽信号分析の技術録、作曲活動に関する雑記

「米津玄師の似た曲データベース」作成のための類似曲検索システム設計

www.wizard-notes.com

2021年7月に、開発中であるテンポ分析に基づく類似曲分析システムの設計・実装を一から見直し、米津玄師の似た曲データベースを作りました。

この類似曲分析システムの全体像と、各処理で使われている技術の紹介、そしてシステム設計・実装上のノウハウ・注意点を紹介します。

構成・処理フロー

システム全体の構成・処理フローとしては以下のようになっております。

経験的に、①~⑤ の処理に分けることで楽曲分析システムのメンテナンスや改善がしやすくなります。

f:id:Kurene:20210731131236p:plain:w500

① 分析する楽曲リスト作成+メタデータ抽出・タグ付け

分析する楽曲の、音源のパスやメタデータを格納したリストを最初に作ります。

分析する楽曲の音源がまとめて格納されているフォルダを指定し…という実装もできますが、分析の度に楽曲データをコピペしたりする運用になってしまうので非推奨です。

メタデータとしては、

  • アーティスト名
  • 楽曲名
  • アルバム名
  • BPM
  • リリース年
  • ジャンル

あたりが分析に使いやすい情報です。

なお、この段階でBPM のような人が与える情報をこのリストに付与しておくと後々管理・分析しやすいと思います。

以下は、今回実装したシステムで作成された楽曲リストです。

f:id:Kurene:20210731140210p:plain:w500

なお、このような処理を自動化するには、Mutagen, PyTagLib の利用が考えられます。

www.wizard-notes.com

www.wizard-notes.com

このリストに格納されたパスを元に、②、④の処理を行います。

② テンポ特徴量抽出

類似楽曲分析の特徴量としては、音色、コード進行、リズム/テンポに基づく手法、あるいはそれらを統合した手法があります。

現在開催中のシステムでは、あまり馴染みのない楽曲をパッと聞いた時の印象はリズムやテンポが支配的であると仮定し、テンポに関する特徴量を採用しています。

詳しくは以下の記事をご覧ください。

www.wizard-notes.com

www.wizard-notes.com

大量の楽曲を分析する際の注意として、音源の読み込み→特徴量算出は1曲ずつ行うように実装すべきです。

mp3ファイルなどはファイルサイズとしては小さいですが、分析のためにPCM形式にデコードすると

1楽曲あたり 数十MB 以上 のデータサイズとなってしまいます。

数十曲、数百曲の分析となると、数十GB以上のデータをメモリ上で扱える環境が必要となってしまいます。

③ 類似度の算出・可視化

基本的な流れとしては、

  • PCA (主成分分析)で次元圧縮(寄与率が高いベクトルのみ利用)
  • 楽曲間の類似度行列(コサイン類似度)を算出
  • t-sne (可視化用)

を実施します。

t-sneは類似度行列の結果にそこそこ近くなるので可視化にのみ利用します。

初期値やperplexityの設定次第で分布が割と変わってしまうので、類似度行列の算出には利用しないほうがよいと思います。

今回のPCAの実行結果と類似度行列を以下に示します。

f:id:Kurene:20210731141105p:plain:w500

f:id:Kurene:20210731141121p:plain:w500

また、テンポに基づいているかどうかを確認するため、BPMの順に楽曲-特徴量行列をソートし、その 楽曲間類似度行列を算出・プロットすることにより、テンポに基づく楽曲分析システムとして正常に動作しているかどうかを確認しています。

f:id:Kurene:20210731141822p:plain:w500

詳しくは以下の記事をご覧ください。

www.wizard-notes.com

④ サブスクサービス での楽曲URL・情報取得

⑤向けに、サブスクリプションサービス での楽曲URLを取得します。

詳しくは各記事をご覧ください。

www.wizard-notes.com

www.wizard-notes.com

www.wizard-notes.com

実際にやってみると、

のようなタイトルやアーティスト名に揺らぎがある楽曲で URL 取得が困難でした。

このような楽曲に対しては、現状、手動でURLを追加しています。

最終的に、以下のような JSONファイルにURLを書き出しています。手動でのURL追加後にJSONファイルを上書きしないように、lockというプロパティを用意しました。

{
    "lock": 1,
    "urls": [
        "https://music.apple.com/jp/album/街/1238071672?i=1238071673&uo=4",
        "https://music.apple.com/jp/album/ゴーゴー幽霊船/1238071672?i=1238071678&uo=4",
        "https://music.apple.com/jp/album/駄菓子屋商売/1238071672?i=1238071679&uo=4",
        "https://music.apple.com/jp/album/caribou/1238071672?i=1238071680&uo=4",
        "https://music.apple.com/jp/album/あめふり婦人/1238071672?i=1238071681&uo=4",
        "https://music.apple.com/jp/album/ディスコバルーン/1238071672?i=1238071682&uo=4",
        "https://music.apple.com/jp/album/vivi/1238071672?i=1238071683&uo=4",
        "https://music.apple.com/jp/album/トイパトリオット/1238071672?i=1238071684&uo=4",
        "https://music.apple.com/jp/album/恋と病熱/1238071672?i=1238071685&uo=4",
        "https://music.apple.com/jp/album/black-sheep/1238071672?i=1238071686&uo=4",
        "https://music.apple.com/jp/album/乾涸びたバスひとつ/1238071672?i=1238071687&uo=4",
        "https://music.apple.com/jp/album/首なし閑古鳥/1238071672?i=1238071688&uo=4",
        "https://music.apple.com/jp/album/心像放映/1238071672?i=1238071689&uo=4",
        #....
    ]
}

⑤ 似た曲データベース作成

①,③,④の結果を統合し、データベースを作成します。

米津玄師の例では、Markdown(HTML のTable) を使いました。

具体的には、以下のようにMarkdown形式の表を作成しています。

def make_md(title, album, artist, bpm, date, spotify_url, itunes_url, sim=None):
    
    if "https" == spotify_url[0:5]:
        spotify_href = f"<a href=\"{spotify_url}\">Spotify</a>"
    else:
        spotify_href = "Spotify"
    if "https" == itunes_url[0:5]:
        itunes_href  = f"<a href=\"{itunes_url}\">Apple Music</a>"
    else:
        itunes_href = "itunes"

    md = ""
    md += f"{title}<br>{spotify_href} {itunes_href}<br>"
    md += "<ul>"
    if print_artist_name:
        md += f"<li>{artist}</li>"
    md += f"<li>{album}</li><li>BPM: {bpm}</li>"
    if sim is not None:
        md += f"<li>類似度: {sim}</li>"
    md += "</ul>"
    
    return md

for k in range(n_songs):
    array2d[k,0] = make_md(titles[k], albums[k], artists[k], bpms[k], dates[k], 
                           url_spotify[k], url_itunes[k])
    for m in range(0, n_similarsongs):
        sim = float(similarsongs[k]["similarsongs"][m]["similarity"])
        n = int(similarsongs[k]["similarsongs"][m]["index"])
        array2d[k,m+1] = make_md(titles[n], albums[n], artists[n], bpms[n], dates[n],
                               url_spotify[n], url_itunes[n], sim=sim)
        array2d_color[k,m+1] = sim


with open(f'{basefilepath}_embed.md', 'w', encoding="utf-8") as f:
    f.write(make_markdown_table(array2d))

実装・ファイル管理の諸注意

文字コードUTF-8推奨

楽曲名は日本語・英語以外の記号が使われていることも多々あります。

そのため、shift-jisを使うと文字化けに悩まされます。

なるべく UTF-8 でファイル作成・書き込みするようにするのが安全です。

CSV ではなくて TSVやJSONを使う

楽曲ファイル名に "," が含まれている場合があるため、TSV / JSON で管理するのがお勧めです。