セルフホスト型音楽配信サーバ&プレイヤーKanadeを作った

はじめに#

「前編の続きがないな, おかしいな…」

それは… 本当に… そう

2022年にRaspberry PiとMediaPlayerを使ってOpenHomeレンダラを作る (前編)という記事を書いて, 最後にしれっと「後編に続く」と書き残したのですが, 続きませんでした.

4年も.

この記事は, 2022年の記事で予告していた「MediaPlayerで続きをやる後編」ではありません.

その代わり,自分で音楽サーバを作ったという話です.

前編では, OpenHomeレンダラとして使えそうなJava製のMediaPlayerを見つけて,

これで行けそうだし, 後編で環境構築を書こう

みたいな顔をして終わっていましたが, なんやかんやあってセルフホスト型の音楽配信サーバ兼プレイヤーであるKanadeを作ることになりました1.

だいぶ長めに書いていきます. マジで長いです.

今回作ったもの:

GitHub - petitstrawberry/kanade: A self-hosted music player server written in Rust.

A self-hosted music player server written in Rust. - petitstrawberry/kanade

GitHub - petitstrawberry/KanadeApp: Native iOS & macOS client for kanade — a self-hosted, multi-room music system.

Native iOS & macOS client for kanade — a self-hosted, multi-room music system. - petitstrawberry/KanadeApp

しばらく前編の続きと動機の話が続くので, Kanade自体の説明が読みたい方はこちらまで飛ばしてください.

前編のあと, どうなったのか#

まずは2022年の記事の後日談から.

前編を書いた時点では, MediaPlayerはかなり良さそうに見えていました.

実際, OpenHomeレンダラとしてちゃんと使えるというだけでも当時の自分にはかなり魅力的でした.

moOdeやVolumioのような「全部入りの音楽再生ディストリビューション」は便利なのですが, 前編でも書いたように, 自分が欲しかったのはあくまで, OpenHomeコントローラから扱えること, 必要なら自分でいじれることでした.

Java製で, OpenHomeを話せて, プラグインもある.

いい感じだな〜と思っていましたが, やっぱり自分で作りたくなってきました.

MediaPlayerがダメだったわけではない#

GitHub - PeteManchester/MediaPlayer

Contribute to PeteManchester/MediaPlayer development by creating an account on GitHub.

MediaPlayerがダメだったという話ではないということです. (私の要求が気持ち悪いだけ)

統合されたシステムが欲しい#

具体的に言うと, Kanadeを作る前の私の音楽再生環境は以下のような感じでした.

  • 自宅サーバにMinimServerをDLNAサーバとして配置
  • リビングのRaspberry PiはDLNAレンダラとして, Lumin Appなどのコントローラから操作
  • 外出時のスマホからはTailscale経由で foobar2000 mobileでDLNAサーバから無理やり引っ張って再生

ライブラリは同じMinimServerにあるのですが, アクセス方法もUIもプレイリストもキューも全部別.

しかもfoobar2000 mobileのDLNAクライアントとしての使い勝手は, 正直「なんとか聴ける」レベルでした. アーティスト一覧が出るまでが遅いし, キューの管理も微妙だし, そもそもDLNA経由だと外出先から自宅にアクセスするのにVPNを張るなり工夫が必要でした.

良さげなサービス Roon

Roonという商用サービスを知っている方はいるかもしれません. 特にその中のRoon ARCという機能は自宅ライブラリの管理とリモート再生を統合したシステムで, 外出先からも自宅ライブラリにアクセスして, スマホでそのまま聴けます. 統一されたライブラリ, 統一されたUI, 家でも外でも同じ体験ができます.

私「良さげやん」

しかし…Roonはサブスクリプションで年額$150くらいかかる

学生には無理です.

さらに, Roonの魅力というのはメタデータ周りのデータベースがかなり豊富でそれを自動で割り当ててくれるというところがあるのですが, 私的には自分のサーバで自分のライブラリを自分の都合で扱いたいという派閥なのでちょっと違うなという感じでした. おそらくこのデータベースが豊富であることが売りらしいので, それを使わずARCのためだけにこの金額を払うのは厳しいなあ, ということで断念しました.

仕方がないので, DLNAサーバを主軸にした構成を捨てて, メディアサーバを主軸にした構成に移行しようという考えになります.

なぜ既存のセルフホスト型音楽サーバではダメだったのか#

「いや, 世の中にセルフホスト型音楽サーバなんていくらでもあるんだから,それ使えばいいだろ」と思う方がいるかもしれません.

Jellyfin, Navidrome, Subsonic, Airsonic, Gonic, etc…

セルフホスト界隈は, 音楽系サーバの種類が豊富です. 豊富なんですが, 実際に使ってみると, 私の用途とはだいぶズレていました.

どれも「音楽をブラウザやアプリから聴く」という目的には十分なのですが,

  • タグの扱いが雑
    • 日本語のタグが怪しい, DSDをサポートしないなど
  • そもそも重い
  • オーディオ志向の機能(lossless配信, DSD対応)が薄い
  • 「音楽ライブラリを自分の好みに合わせて扱う」という思想が弱い

というような感じでなんとなく微妙に機能が足りないよなあ、みたいな印象でした.

「日本語のタグをちゃんと読んで, losslessで配信して, 軽くて, 自分の好きにできるやつ」 が欲しかったのですが, そんな都合の良いものがある訳がなく, じゃあ作るか, という流れです.

では, 何が欲しかったのか#

MediaPlayerやMPDまわりでしばらく遊び, さらに既存の音楽サーバも一通り試した結果, 欲しいものはだいぶ明確になりました.

大雑把に言うと, 欲しかったのはこんなシステムです.

  1. 自分の音楽ライブラリを自分で持てる
  2. サーバが権威を持ち, クライアントは薄くしたい
  3. スマホで, 商用サービスと同じ体験で聴けること(一番大事)
  4. 再生先を複数マシンに分散したい
  5. でも操作感は一つのシステムとして揃えたい
  6. iPhoneやMacでも再生したい (外出時も)
  7. Webからも雑に使いたい
  8. 付加機能はあとから足したい

こうして書くと, まあまあ面倒くさい.

Kanadeとは#

Kanadeは, 自分の音楽ライブラリをスキャンして管理し, 複数のクライアントから操作でき, 複数の再生ノードへ配信できる, セルフホスト型音楽配信サーバ&プレイヤーです.

GitHub - petitstrawberry/kanade: A self-hosted music player server written in Rust.

A self-hosted music player server written in Rust. - petitstrawberry/kanade

名前は日本語で「奏」です. (あの, 某吹奏楽アニメのキャラから取りました…)

全体のアーキテクチャ#

まずは雑に全体像を出します.

  Web / TUI / iOS / macOS
             │
             │ WebSocket / HTTP
             ▼
      Kanade Server (:8080)
   ┌───────────────────────┐
   │ kanade-core           │
   │ kanade-db (SQLite)    │
   │ kanade-scanner        │
   │ axum WS + media       │
   └───────────────────────┘
        │             │
        │             └── HTTP media / HLS
        │
        ├── WebSocket → Node A (MPDなど)
        ├── WebSocket → Node B (別マシン)
        └── WebSocket → Node C (別マシン)

ポイントは, サーバが単一の権威を持つことと, クライアントやノードはそこにぶら下がることです.

技術スタック#

サーバ側はRustです.

主に使っているものは以下の通り.

  • Rust
  • tokio
  • axum
  • SQLite
  • lofty

クライアント側は用途ごとに分かれています.

  • Web: Svelte 5 + hls.js
  • TUI: ratatui
  • iOS / macOS: Swift + AVFoundation

Kanadeの主な機能・特徴#

Kanade Web UI

機能を列挙するとだいたい以下の通りです.

  • ライブラリ管理
  • ストリーミング再生
  • 独立した再生ノード
  • ローカル再生

ライブラリ管理#

  • ローカルの音楽フォルダをスキャン
  • メタデータを保存して全文検索対応
  • アルバム, アーティスト, ジャンル, 曲単位でブラウズ
  • プレイリスト管理
    • 手動プレイリスト
    • スマートプレイリスト

対応フォーマットはかなり広めで, 以下のようなものを扱えます.

  • FLAC
  • MP3
  • M4A
  • Ogg Vorbis / Opus
  • WAV
  • AIFF
  • WMA
  • APE
  • DSD (DSF)

スマートプレイリスト#

スマートプレイリストは個人的にかなり好きな機能です.

単なるプレイリストではなく, ルールベースで曲を集められます.

例えば,

  • ジャンルがClassical
  • かつ composer に特定文字列を含む
  • あるいは album artist に特定文字列を含む

みたいな条件を, AND / ORで組み合わせられます.

この機能は, Audirvanaに同様のものがあって, 使っていた時に非常に便利だったのでパクリました. ライブラリが増えてくると, 「この条件で絞り込んだプレイリスト」が日常の選曲でかなり効く. ジャンルで絞ったり, 最近追加した曲だけ拾ったり, 特定の作曲家だけ集めたり. このスマートプレイリストを, モバイルでも, 据え置きの再生機でも, 同じように使いたい. これもKanadeを作る動機の一つでした.

ストリーミング再生#

KanadeはHLS経由で音声を配信します.

Webクライアントではhls.jsを使ってそのまま再生できますし, ネイティブクライアントでもHLSプレイリストをフェッチして再生する形です.

ストリーミングとローカル再生が同じ配信経路に乗っているので, クライアント側の実装が統一されています.

独立した再生ノード#

Kanadeの最大の特徴が, ノードごとに再生状態が独立していることです.

各ノードはそれぞれ

  • キュー
  • 音量
  • シャッフル
  • リピート
  • 現在位置

を個別に持ちます.

つまり,

  • リビングではアルバムを通しで流す
  • 書斎では別のキューを作る
  • 手元のiPhoneではローカル再生する

的なことが可能.

マルチルーム#

ノードは別マシンに置けます. まあ前述の通り独立しているので.

つまり, Raspberry Piでも, Linuxマシンでも, 何なら別のMacでも, サーバに繋いで再生先として扱えます.

  • サーバはNASの近くに置く
  • 再生ノードは各部屋のマシンに置く
  • コントロールはWeb/TUI/iPhone/Macからやる

こんな感じのことが可能.

ローカル再生#

サーバから別ノードへ投げるだけではなく, 手元の端末自体で再生することもできます.

Apple系のネイティブアプリは別リポジトリです2.

GitHub - petitstrawberry/KanadeApp: Native iOS & macOS client for kanade — a self-hosted, multi-room music system.

Native iOS & macOS client for kanade — a self-hosted, multi-room music system. - petitstrawberry/KanadeApp

多くのOpenHomeのコントロールアプリはコントロールしかできくて不便だったので, 付けました. 一番近いのはBubbleUPnPなんですが, Android専用なので….

詳細設計と実装の話#

ここからは, 細かい話です. 長いのでスキップしてもらっても全然大丈夫です.

詳細設計と実装の話を開く

サーバが権威を持つ#

Kanadeでかなり強く意識したのが, クライアントをなるべくステートレスにすることです.

クライアントが賢くなりすぎると,

  • WebとiPhoneで違う挙動になる
  • TUIだけちょっと状態同期がズレる
  • 接続し直した時に何が正なのか分からなくなる

みたいなことが起きがちなのでKanadeでは,

  • 状態はサーバが持つ
  • クライアントは表示と操作に集中する
  • 状態変化はサーバからpushする

という形にしました.

論理再生先と物理出力を分けた#

音楽サーバを作っていると, 「どこで音を出すか」という一つの概念の中に, ごちゃ混ぜになりがちなものがあります.

  • キューとかシャッフルとか, ユーザが見るもの
  • MPDを叩くとか, 実際に音を出すI/Oの話

これをそのまま一つの扱いにすると, 一見簡単そうでいて後から広げにくくなります.

なのでKanadeでは, ここを二つの概念に分けました.

Node は論理的な再生先です. 「リビング」「書斎」「このiPhone」みたいな単位で,

  • 名前を持つ
  • 独自のキューを持つ
  • 再生状態を持つ
  • 音量やシャッフル, リピートを持つ

というものです. ユーザが見て操作する対象はこっちです.

Output は物理的な音の出口です. 実際に音を出すためのI/Oアダプタで, MPDを叩くものでもいいし, 将来的には別のバックエンドでも構いません.

独立キューとハンドオフ#

NodeとOutputを分けた結果, 自然に生えた機能が独立キューとハンドオフです.

独立キュー#

よくある「一つのプレイヤーに一つのキュー」だと,

  • ある部屋で再生していた流れを壊したくない
  • でも別の部屋では別のものを流したい
  • さらに手元の端末では試聴したい

みたいな時に困ります.

Kanadeではノード単位でキューが独立して持っています. また, 設定も独立しているので,

  • Node Aはshuffle off
  • Node Bはshuffle on
  • Node Cはrepeat all

みたいなことも普通にできます.

ハンドオフ#

ハンドオフは, あるノードのキューと再生位置を別ノードへ引き継ぐ機能です.

要するに, リビングで流していたものの続きをiPhoneで聴く…みたいなことができます.

しかも単に曲を投げ直すだけではなく,

  • キュー
  • 現在位置
  • シャッフル
  • リピート

のようなコンテキストも一緒に持っていけるのが嬉しいところ.

配信の仕組み#

Kanadeでは配信にHLSを使っています.

具体的には, 音声ファイルをfMP4へremuxしてセグメント化し, HLSプレイリスト経由で配信します. 音声データそのものの再エンコードはしないので, losslessのまま持っていける形式はそのまま配信できます.

HLSセグメントは事前に全部作るのではなく, 必要になった時にremuxしてキャッシュします. そうじゃないと, よく聴く曲ですら毎回remuxすることになり, 遅いので…

署名付きURL#

Kanadeの /media/* は署名付きURLで一応保護しています.

  • WebSocket接続ごとにセッションを作る
  • サーバがセッション単位のHMAC-SHA256鍵を持つ
  • クライアントは生鍵を受け取らない
  • 必要なパスに対してサーバへ署名済みURLを要求する
  • URLは15分で失効する

HTTPでそのまま取りに行きたかったので, こういう形にしました.

タグとアートワーク#

ここは地味ですが, 割と大事なポイントです.

既存の音楽サーバ——Jellyfin, Navidrome, Subsonic系——は, FLACやMP3あたりのメジャーなフォーマットなら問題なく扱えます. タグもアートワークもちゃんと読む. そこは良い.

しかし, 日本語のタグやDSD (DSF) などのマイナーなフォーマットになると, 急に怪しくなります.

日本語タグの問題#

日本語の音楽ファイルは,

  • アーティスト名が文字化けする
  • album artistとartistの区別が怪しい
  • 読み取ったと思ったら別のフィールドに入っている

みたいなことが割と起きます. 前の方でもNavidromeの話で触れましたが, これはNavidromeに限った話ではなく, 多くの音楽サーバが抱えている問題です.

「文字化けしてても曲は再生できるからいいじゃん」と思うかもしれませんが, アーティスト名が壊れていると検索にもヒットしないし, ブラウズもままならないので, 実用上かなり困ります.

ので, まあ一通り気になったところは順次直していく感じで行きます. 海外のOSSだと日本語タグのバグ修正とかされづらいので, そこは自分で直していこうっていう感じで, ちょっとずつよくなる予定.

DSDのタグとアートワーク#

DSDファイルのタグ構造は, FLACやMP3とは異なる部分があり, 多くのサーバライブラリはDSDを「一応読めるけどタグは適当」くらいの扱いになります. アーティスト名が出ないとか, アルバムアートが表示されないとか… 結構あります.

Kanadeでは, DSDのタグ読み取りにdsf-metaという専用のライブラリを使っています. 一般的なloftyだけだとDSD周りのカバーが弱いので, そこを別途補強する形です.

結果として,

  • DSDファイルのアーティスト, アルバム, タイトルが正しく表示される
  • アルバムアートもちゃんと拾ってくる
  • FLACやMP3と混ざっていても違和感なくブラウズできる

という状態になっています.

まあ, DSDの再生自体はまだクライアント側が追いついていないので, 現状はタグとアートワークがちゃんと見える, という段階です. 再生はこれから.

運用と使い心地#

ここからは運用の話です.

セルフホスト系の話は, どうしても設計の話だけで終わるとふわっとするので, 実際のデプロイ感も書いておきます.

デプロイ#

KanadeはDocker Composeでも, Nix flakeでも扱えます.

Docker Composeでの基本構成#

サーバ側はだいたいこんな感じです.

services:
  kanade:
    image: ghcr.io/petitstrawberry/kanade:main
    ports:
      - "8080:8080"
    environment:
      MUSIC_DIR: /music
      DB_PATH: /data/kanade.db
      HLS_CACHE_DIR: /data/hls-cache
      BIND_ADDR: "0.0.0.0:8080"
      PUBLIC_HOST: "kanade.example.com"
      SCAN_INTERVAL_SECS: "300"
    volumes:
      - ./music:/music:ro
      - kanade-data:/data

NASとの連携#

上の例では ./music:/music:ro とローカルディレクトリをマウントしていますが, 実際には音楽ファイルはNAS上にあることが多いと思います.

Kanadeでは, SMB/CIFS用のCompose overlayを用意しています.

# docker-compose.smb.yml
# Usage: docker compose -f docker-compose.yml -f docker-compose.smb.yml up -d
services:
  kanade:
    volumes:
      - music:/music:ro
  mpd:
    volumes:
      - music:/music:ro

volumes:
  music:
    driver_opts:
      type: cifs
      o: "username=${SMB_USER},password=${SMB_PASS},ro,vers=3.0,iocharset=utf8"
      device: "//${SMB_HOST}/${SMB_SHARE}"

.env にNASの接続情報を書いておけば, あとはoverlayを重ねるだけでSMBボリュームが /music にマウントされます.

docker compose -f docker-compose.yml -f docker-compose.smb.yml up -d

NAS側でファイルを追加・整理したものが, Kanadeの定期スキャンで自動的に反映されます.

別マシンにノードを置く#

Kanadeの面白いところは, ノードを別マシンに出せることです.

例えばRaspberry Piや小型PC側に kanade-node だけ置いて, そこからローカルMPDを叩く構成にできます.

Standalone nodeのComposeはだいたいこんな感じです.

services:
  node:
    image: ghcr.io/petitstrawberry/kanade:main
    command: kanade-node
    environment:
      NODE_NAME: "living-room"
      SERVER_ADDR: "kanade.example.com:8080"
      MPD_HOST: "127.0.0.1"
      MPD_PORT: "6600"

リバースプロキシ#

外からHTTPSで気持ちよく使いたいなら, リバースプロキシを前に置くのがよき.

nginxやCaddyで,

  • TLS終端
  • WebSocketのupgrade
  • 静的ファイルとAPIの振り分け

をやればOK.

クライアントの使い分け#

Kanadeはクライアントが複数あります.

Web#

まず一番気軽なのはWebです. ブラウザがあればすぐ使えますし, 家の中のどの端末からでもアクセスしやすい.

TUI#

TUIは完全に趣味枠に見えて, 実際かなり趣味枠です. SSH越しに雑に触れるのも良いですし, 作業中に別ウィンドウで状態を見るのにも向いています.

iOS / macOSネイティブ#

KanadeApp on iPhone KanadeApp on Mac

日常使用という意味では, やはりネイティブクライアントがかなり便利です. ほとんどをこのクライアントの開発に時間を割いています.

単純にコントローラとして使うことができるだけでなく, その端末でローカル再生もできるのが大きいです.

GitHub - petitstrawberry/KanadeApp: Native iOS & macOS client for kanade — a self-hosted, multi-room music system.

Native iOS & macOS client for kanade — a self-hosted, multi-room music system. - petitstrawberry/KanadeApp

特にMacでは, 据え置き再生機に投げるか, このMacで鳴らすかをその場で選べるのがお気に入りポイントです.

実際に使ってみてどうか#

しばらく使ってみて, だいたい満足しています.

特に以下の点は満足度が高いです.

  • ライブラリと再生が一つのシステムとして繋がっている
  • サーバが権威なので, クライアントを跨いでも状態が分かりやすい
  • ノード単位で再生文脈を持てる
  • ローカル再生と据え置き再生機への出力を同列に扱える
  • 電車の中でもスマホでSpotifyと同じ感覚で聴ける

たぶん.

おわりに#

という訳で, 4年間放置された前編の続き?として,

今回はMediaPlayerの後編ではなく, 自作のセルフホスト型音楽配信サーバ&プレイヤーKanadeを作ったという話を書きました. ある程度使い物になるレベルにはなりました.

興味があればリポジトリも見てみてください.