音楽プレイヤーアプリを作ってみた。詳細は以下のページに。
ヘッドレスとは・・・画面を持たないことを意味する。サーバで動作し、ブラウザから再生コントロールやライブラリの操作を行うことをコンセプトにしている。
私は Raspberry Pi のような小さいコンピュータが大好きで家に何台もある。そのうち1台は Raspotify を入れ、オーディオアンプと繋ぎ、大きなスピーカーで音を鳴らしている。
Spotify はいいぞ、というのは何度か書いたかもしれない。 また、以前には、 Volumio いいぞ、という日記も書いた。
基本的に、音楽再生を担うプレイヤーと、制御するUIを分離できるので、ほかのマシンに再生させたり、再生コントロールをすることができる。
この分野では、Volumio が目立っていたが、アップデートによってフリーソフトではなくなってしまったので、使いにくくなってしまった。また、Volumio は、OS環境全体に対する要求が強かった。Raspberry Pi で動かすには、専用のイメージで起動させる必要があり、カスタマイズがかなり厄介という声が多かった。 自分のメディアライブラリを構築する用途では、 Jellyfin もかなり好きだが、Jellyfin では「サーバで音を鳴らす」ができない。 そもそもなぜサーバで音を鳴らしたいのか?BluetoothやAirPlayじゃだめなのか?といえば、有線接続にこだわりたい点と、あふれるDIY魂である。
そういうわけで、自分でも作ってみたかったので、作ってみることにした。
このプロジェクトは、仕事で普段使っている技術を慣熟するためのものでもあるので、そこまで大きな挑戦をしないつもりだったが、やってみると、難しいことも多かった。
技術スタック
Node.js (TypeScript) をメインにする。
フロントエンドには Remix を使った。
Remix は、Reactレンダラーや router などがセットになったフレームワーク。
バックエンドでは、 Express と Socket.io でHTTPまわりを処理。Prisma で SQLite を使う。
基本設計
今回作るシステムは、じぶんちのサーバにインストールして使うものである。音楽ファイルが置いてあるサーバにReedをインストールしておき、スマホなどのデバイスはWebブラウザでアクセスすることで、ライブラリの探索や再生コントロールをすることができる。
こんな感じである。
Reed サーバのコア部分としては、プロジェクト開始時に思い描いた設計からほとんど変わらないシンプルなものである。 3つのプロセス(と仮に呼ぶが実態は1プロセス)が相互連携することで動作する。
- Seeker
- ファイルを探索してライブラリを構築する
- PlaybackManager
- Seeker が作ったデータベースをもとに動作する
- 音楽再生を管理する
- HTTPサーバ
- Web UI の描画
- API を露出させ、WebUIと通信する
- PlaybackManager や Seeker を制御する
ライブラリの構築について
楽曲コンテンツは、だいたいファイルシステムに格納されている。Reedでは、ファイルシステムの構造を参考にして、自分のSQLiteデータベース上に独自のライブラリを構築する。 ライブラリは、楽曲・アーティスト・アルバムなどが相互にリレーションしているデータ構造である。 ファイルシステムのフォルダ名、ファイル名などが手がかりになるし、MP3 の ID3 タグも手がかりになる。(※ とっかかりとして MP3 をスタート地点にしているが、今後ロスレス音源やハイレゾ音源に対応したいと思っているのだ)
なるべく正確な楽曲メタデータが欲しいし、アルバムアートワークも画面に表示したいので、楽曲データベースの利用を考える。 僕が好きな Jellyfin では、楽曲データベースを使うかどうかは完全に任意だし、複数のデータベースを組み合わせることもできるようになっている。すごい。それを見習い、いちおうの抽象化をしつつも、 musicbrainz を使うことにした。
レート制限をはじめ、いくつか利用方法に注意があるので、律儀にまもる。
アルバムの検索がなかなか難しかったが・・・これは Apatch Solr の構文でクエリを書けば良い。
アルバムアートは、 musicbrainz サーバからローカルにダウンロードしておき、SQLite にバイナリを格納している。Web UI が img
タグで所定のエンドポイントにアクセスすると、HTTPサーバは SQLite からデータを取り出してバイナリをレスポンスするってわけ。
今回のライブラリ構築プロセス Seeker
では、またまた Jellyfin を参考にして、複数のエントリーポイント(ファイル検索の始点)を登録できる仕組みにしている。
エントリーポイントは、設定画面から登録できる。
現在は、定期巡回や変更検知などは未実装だが、今後やってみたい。
音楽再生について
Node.js での音楽再生の方法を調べてみると、なかなか厳しいということがわかってきた。
もっとも原始的な方法は、OSにもともと備わっている再生コマンドを child_process.exec
でサブプロセスとして実行することだ。この方法では、幅広いフォーマットのファイルを再生できるし、Node.jsの負荷もない。
しかし、Reedでは、再生ポジションを制御したかったし、音量制御もしたかったので、別の方法を探すことにする。
基本的には、speaker パッケージを使って再生する方針でいく。speaker は、PCM音源しか対応していないので、これに、MP3 などのデコーダーライブラリを組み合わせていく。
多くのサンプルでは、デコーダーと speaker を直結させて音楽再生をしているが、 Reedでは、自前のオーディオミキサーを構築することにした。 このミキサーでは、speaker パッケージ に PCMバッファーを流す前に、PCMデータを加工し、音量制御を行う。 Web UI のボリュームコントロールをいじると、PCMデータの倍率も連動して変化する。サーバOS側の音量制御にはいっさいノータッチで、ボリューム制御機能ができた。
Node.js はそもそもシングルスレッドだから、ミキサーの実装は、かなり厳しいのだが、ボリューム制御機能は無くてはならない大事な機能だからしょーがない。たとえば、RemixやSeeker がスレッドを使いすぎると、再生がうまくいかない懸念がある。もしそうなったら、PlaybackManagerだけを別プロセスに分離してプロセス間通信する設計にしても良い。
Web UI について
今回は Remix の学習も兼ねているので、Remix で UI を構築していく。 といっても、基本的には React である。
Remix には、独自のデータフローモデルが存在する。 loader
と action
である。 loader
は、画面描画に必要なデータを集めてきて、view に渡す役割がある。 action
は、データのミューテーションを行う役割がある。いちおう Remix Way を体験したかったので、一部の画面で loader や action を採用してみたが、今回のアプリケーションでマッチする場面はかなり少なくて、使いにくかった。よって、大部分をブラウザからの fetch で行うことにした。
Reed では、複数のデバイスから同時に Web UI を使えることを最初から考えていた。これを実現するには、シンプルな原則を守れば良い。これも、Remix のデータフローやFluxモデルと同じ考え「データの流れを一方行に限定する」だ。 まず、現在の PlaybackManager の状態は、ちくいち、WebSocket で配信されてくる。これには、いま再生している楽曲の情報とか、位置とか、音量とか、再生モードなどがある。Web UI は、愚直に WebSocket から受け取るステータスを UI に反映するだけで良い。 再生や停止や音量変更などのアクションは、Reedサーバに対して一方的に送りつける。しかるべきのち、PlaybackManager の状態が変化し、WebSocket で最新ステータスが送られてくるので、UI は WebSocket の情報をたよって UI を更新するだけで良い。
おわりに、 ランディングページにも書いたが、まだ開発者モードしかないし、配布できる状態にもなっていないが、いちおう動くところまでできたので、一区切りつけることにした。 ほかにも作りたいプロジェクトがたくさんあるのでな。次のプロジェクトに移ることにする。
以上