自分で作ったオーディオプレーヤー
(nexo.sh)- 2025年になっても、iPhoneでユーザーが所有する音楽ファイルを自由に聴いたり同期したりするのが難しいという問題意識から出発し、ローカル優先の音楽プレーヤーをSwiftUIで自作した
- Apple Musicのサブスクリプションを解約すると、iCloud Music Library同期が停止し、iTunes Matchは年額24.99ドルで256kbps AACのコピーをオンライン保存する別料金の選択肢となる
- React Native/Expoは深くネストされたiCloudフォルダの走査やファイル権限の処理で失敗やクラッシュが多く、AppleネイティブAPIを使うSwiftUI実装のほうがよりシンプルな選択になった
- アプリはSQLite FTS5でファイル名やartist、album、titleといったメタデータを検索し、iCloud取り込み・ライブラリ管理・再生画面を分離して構成している
- iOSではsecurity-scoped bookmarkと個人開発ビルドの制限により、個人が作ったアプリを長く安定して使う過程でも、ファイルアクセスと配布の壁が残っている
なぜ自分で音楽プレーヤーを作ったのか
- iPhoneでユーザーが所有する音楽ファイルを再生するには、Appleに費用を払うか、いくつもの制約を回避しなければならない状況が今も残っている
- ローカル音楽ライブラリをすでに整理しているユーザーにとって、ストリーミングサービスは必須というより利便機能に近い
- Apple Musicのサブスクリプションを解約すると、iCloud Music Libraryベースのデバイス間同期が止まる
- この機能はサブスクリプションの背後にある
- iTunes Matchを使えば年額24.99ドルで再び利用できる
- Matchは256kbps AACのコピーをオンラインに保存し、元のファイルはユーザーが変更しない限りそのまま残る
- サブスクリプションがなければクラウド同期は消え、ケーブル/Wi‑Fi同期に戻る必要がある
- 目標は、オーディオファイルを取り込み、整理し、再生する基本的な音楽プレーヤーを自分で作ることだった
- 完成したアプリは全文検索、iCloud対応、ローカル優先の体験を提供し、GitHubで公開されている
既存の選択肢が合わなかった理由
-
Apple純正アプリ
- FilesアプリでiCloud上の音楽ファイルを直接再生することはできるが、音楽鑑賞用のアプリとして設計されていない
- プレイリスト管理、メタデータの並べ替え、再生キューのような中核機能が不足している
- 音楽再生自体は可能でも、全体のユーザー体験は制限が多く、関連するAppleコミュニティでの議論もある
-
サードパーティアプリ
- App Storeにはオフライン音楽再生アプリが多いが、ユーザーがすでに所有しているファイルを再生するアプリにもサブスクリプション型の価格を適用していることが多い
- Dopplerには買い切りモデルという利点があった
- ただしUXはアルバム管理中心で、検索品質も満足できず、ネストの深いフォルダが多いiCloud取り込みは遅く使いづらかった
React NativeからSwiftUIへ切り替えた過程
-
最初に選んだReact Native/Expo
- 過去のSwift経験からSwiftを避け、Web開発の経験を再利用できるReact Native/Expoを先に選んだ
- 再生UIの実装は簡単で、必要な機能を備えているように見えたGionatha Sturbaのmusic-playerテンプレートを使った
- 問題はファイルシステムアクセスとクラウドファイル同期で大きくなった
expo-filesystemのようなライブラリは基本的なファイル選択は支援していたが、深くネストされたiCloudディレクトリの再帰走査は頻繁に失敗したり、アプリをクラッシュさせたりした- iOSのサンドボックスでは、明示的なユーザー権限なしにアプリがファイルを読めない
- 結果としてJavaScriptベースのアプローチはAppleネイティブAPIを直接使うより複雑になり、Swiftへ移行すればiCloudファイルアクセスとサンドボックス権限をより適切に制御できた
-
SwiftUIを選んだ理由
- UIKitやStoryboardではなくSwiftUIを選び、宣言的なUIレイヤーを構築して、ドメインロジックとデータ同期に集中した
- Swiftの
async/awaitとSwift Actorsの統合により、データフローと並行処理の管理が容易になった - SwiftUIはアプリを独立したViewModelコンポーネントとして構造化するのに役立った
- OpenAI o1やDeepSeekのようなLLMは、純粋なUIコードやデータバインディングコードを生成する際に、複雑な相互依存をあまり作らなかった
アプリ構造とデータモデル
-
3つの主要画面
- アプリはLibrary import、Library management、Player and playbackの3つのフローに分かれる
- Library importではiCloudライブラリフォルダを追加し、すべてのフォルダからオーディオファイルをスキャンしてSQLiteデータベースにパスを挿入する
- Library managementでは追加した曲を管理し、プレイリストを構成する
- Player and playbackではリピート、シャッフル、キュー管理、再生・停止・次の曲の機能を担う
- Apple純正のファイルピッカーは、キーワードで検索した複数ディレクトリや複数ファイルを一度に選ぶフローには合わない
- 空のライブラリでアプリを起動すると、Syncタブに「Add iCloud Source」ボタンが表示される
- フォルダを選ぶと、ツリー走査中に進捗バーを表示し、インデックス作成が終わるとLibraryタブへ移動する
- 最初の画面にはPlaylists / Artists / Albums / Songsが表示される
- トラックをタップすると下部にMini-Playerが現れ、それをタップすると全画面のPlayerへ移動する
- 全画面Playerではシャッフル、リピート、キューの並べ替え、音量調整を提供する
- Syncで「+」を押して別のフォルダを選ぶと、新しい曲をバックグラウンドでマージし、再起動は不要である
-
サーバー風のロジック層
- モバイルアプリだが、Web・クラウドバックエンドの経験をもとにバックエンド風アーキテクチャを適用した
- ドメイン・ロジック層をViewとViewModel層から分離し、クラウド同期、メタデータ解析、SQLiteデータアクセスを安定して処理する
- レイヤー構造は次の通り
- SQLiteが元の曲レコードとFTSインデックスを保存する
- Repositoryがデータベースをラップし、非同期APIを公開する
- ドメインActorが取り込み、検索、キューロジックのようなビジネスルールを持つ
- ViewModelがActorを購読し、UI向けの構造体に変換する
- SwiftUI Viewは受け取ったデータをレンダリングする
- 状態変更はスレッドセーフに保たれ、iCloud同期・再生・UIが直接レイヤーを飛び越えないよう分離されている
SQLite FTS5で作ったローカル検索
- 永続ストレージにはSQLiteを使い、CoreDataは使っていない
- CoreDataを避けた理由は、スキーマ、生SQLクエリ、とくに全文検索を直接制御する必要があったからだ
- iOS 11前後から、FTS機能を含むSQLiteを追加設定なしで使える
- 一般的なクエリにはSQLite.swiftを使ったが、FTSクエリは通常のSQL文で処理した
- SQLite FTS5は、ファイル名やartist、album、titleのようなメタデータを、別の検索インフラなしで問い合わせ可能にする
-
FTSテーブル構成
- FTS5テーブルは2つ使用する
songs_fts:SQLiteSongRepositoryが使用し、artist、title、album、albumArtistをインデックスするsource_paths_fts:SQLiteSourcePathSearchRepositoryが使用し、fullPath、fileNameをインデックスする- 2つのFTSテーブルは、基本のB-treeテーブルである
songs、source_pathsの横に置かれる - FTSはUIからは読み取り専用で、すべての書き込みはRepository内部で処理される
songs_fts作成例では、songIdをUNINDEXEDにし、unicode61トークナイザーを使用する- さまざまな文字を扱うために
unicode61を選んだ - 検索しないキーは
UNINDEXEDに指定し、用語辞書が不要に肥大化しないようにする
-
部分検索とランキング
- ユーザーが「lumine」と入力すると、内部的には「lumine*」のようにワイルドカードを付けて部分検索の結果を即座に見つける
- 検索結果の並べ替えにはSQLiteの
bm25を使う - この方式は、予測可能なスキーマ、ローカル優先のアプローチ、強力な全文検索を提供し、ネットワーク依存や外部サービスの追加を必要としない
iOSのファイルアクセスとブックマークの制約
- iOSアプリはファイル位置を記憶するために永続ブックマークを保存できる
- しかし、アプリのサンドボックス外のファイルへの拡張アクセスを与えるsecurity-scoped bookmarksはmacOSでしか提供されない
- iOSアプリは通常のブックマークでファイルパスを記憶し、ドキュメントピッカーを通じて再アクセスを要求できるが、そのアクセスが静かに継続される保証はない
- 関連内容はAppleのbookmark documentationにある
- これを緩和するため、ファイルをアプリ自身のサンドボックスコンテナへコピーする代替メカニズムを実装した
- ブックマークが有効な間に、バックグラウンドでファイルを先回りしてコピーする
- iOSが権限をリセットしても、壊れたオーディオファイル参照にアクセスするリスクを減らす
- このアプローチはインデックス速度も改善する
- アクセスが有効な間にフォルダ構造を一度スキャンする
- 関連するオーディオファイルだけを取り込む
- 深くネストされたディレクトリを安全に走査する
- 外部場所にある個別オーディオファイルを、デバイス再起動後も安定して再生する問題は解決していない
- ネイティブアプリでもこのユースケースはiOSで十分にサポートされておらず、ファイルアクセスを安定して処理するのは依然として複雑である
再生とUI実装
-
メタデータ解析
- オーディオファイルのメタデータ解析には、Apple AVFoundationフレームワークの
AVURLAssetを使った AVURLAssetはtitleやalbum artistのようなメディアファイルのメタデータを調べられる- トラック番号のような一部フィールドはID3タグから直接探す必要があった
- 公式ドキュメントがエッジケースを十分に扱っておらず、GitHub検索で実装例を参考にした
- オーディオファイルのメタデータ解析には、Apple AVFoundationフレームワークの
-
オーディオ再生
- ライブラリのインデックス作成後は、
AVAudioPlayerインスタンスを初期化して再生する方式でオーディオプレーヤーを実装した - Control Centerで音楽を制御するには、
AVAudioPlayerDelegateプロトコルを実装し、AppleのMPRemoteCommandCenterに接続する必要がある MPRemoteCommandCenterは、システムレベルの再生制御にアプリが応答できるようにする
- ライブラリのインデックス作成後は、
SwiftとApple開発の経験
-
難しかった点
- XcodeのリアルタイムSwiftUIプレビューは進歩しているが、開発体験全体としては、Flutterが5年前に提供していたVSCode統合、リアルタイムシミュレーターリロード、慣れたデバッグツールの水準には及ばなかった
- SwiftのNeovimまたはVSCode向けLSPサポートには
xcode-build-serverのような追加ツールが必要で、Web中心のエコシステムの開発体験とは異なる - Apple SDKの一部は依然としてObjective-C風のままである
- Spotlightファイル検索は
NSMetadataQueryでしか公開されておらず、NSMetadataQueryはKVOと文字列キーを使う - Swiftフレンドリーなラッパーはまだない
- SwiftUIの宣言的UIは有用だが、iCloud権限が絡むアプリ全体の動作はプレビューでエミュレートできず、クラウドとの相互作用を直接モックする必要がある
-
良かった点
async/awaitのおかげで、I/O中心の並行処理コードをコールバックなしで命令型コードのように書けた- Actorの中に同期コードも簡単に入れられ、JavaScriptエコシステムのように呼び出せた
- React NativeやFlutterのオープンソースバインディングに縛られず、Appleネイティブライブラリを直接使えた
- 多くのApple APIにはサンプルがあり、始めやすかった
- SwiftUIのReact的なUI構成方式は、生産性と試行の余地を提供した
個人用アプリを長く使いにくいiOS
- 約1.5週間の実装を経て、iCloudストレージからオーディオファイルを取り込めるローカル/オフライン音楽プレーヤーを作った
- しかし開発者証明書がなければ、アプリは7日間しか実行できず、その後は再ビルドが必要になる
- Apple Developer Programに登録するには99ドルを払わなければならない
- EUのDMA Act以後も、サイドローディングは完全には開放されていない
- EUユーザーは開発者サイトからサードパーティマーケットプレイスのアプリをインストールできる
- 開発者は依然としてAppleの年99ドルプログラムに登録する必要がある
- AppleのAlternative Termsにも同意しなければならない
- 個人・趣味用途では7日間の開発ビルド制限は消えない
- iOSのPWAにも制限が残る
- iOS 16〜18.xのアップデート後でも、PWAはSafariサンドボックス内で動作する
- WebGL2とweb-pushは利用できる
- Web Bluetooth、USB、NFC、Background Sync、約50MBを超える保証ストレージは提供されない
- WebGLはMetal shimを経由するため、実際のフレームレートはネイティブのMetalアプリより劣ることが多い
- AIは不慣れな技術を扱うのに必要な知識を得やすくし、ソフトウェア開発の複雑さを下げたが、iOSアプリは依然として人工的なルールに従わなければならない
- 個人が自作したアプリであっても、1週間を超えて実行するにはAppleの最後のハードルが残っており、これは個人アプリの開発と配布を制約している
1件のコメント
Hacker News のコメント
Winamp が標準の音楽プレーヤーだった時代を過ごしてきて、ストリーミング時代になっても今なお ローカル音楽ライブラリをフォルダで整理している
ここのコメントの他の人たちと同じように、オフラインで音楽を聴くための趣味プロジェクトとして昔ながらの音楽プレーヤーを作った。1ページの HTML/JS アプリで、キーボード操作一式と簡単なキュー機能を入れてある
ここで試せる: https://nobsutils.com/mp
ディレクトリ内のファイル群、コレクション全体のシャッフル、特定ディレクトリだけの再生といったシンプルさがよかった
ちなみに、現在の標準的なブックマークショートカットである
cmd + dが、Webサイトのブックマークではなくテーマを変更するFLAC 形式で25年間音楽コレクションを作ってきて、昨年 Android フォンと 1TB の MicroSD カードを買い、すべての音楽を入れられるようになった
技術が追いつくまで長いプロジェクトだったが、今では全部ポケットに入れられるのでかなり満足している
業界が押しつけてくるものをストリーミングし、広告を受け入れながらコントロールを手放す借り手になりたくないのは自分だけではないはずで、自分のアプリまで作っている人たちを見るのも素晴らしい
きちんと再エンコードすれば、違いを聞き分けられない透過的な音質で、はるかに小さいカードにすべての音楽を入れられるし、バックアップとしてはデスクトップに FLAC を保管しておけばいい
自分のコレクションは FLAC/APE/ALAC/WavePack が25%程度にすぎないのに 3TB を超えていて、そのため移動中に音楽を聴くのが難しい
モバイル端末に何を入れるかを事前に選べない
サーバーは https://www.navidrome.org、クライアントは https://symfonium.app の組み合わせがとてもよかった
いまは 2TB の SD カードもあるので、価格が下がったらおそらく1枚買うと思う
変更しても反映されなかったり、反映された後で突然まったく別のカバーに変わったりして、見たところ Android のバグのようだった
こういう問題を経験したことがあるのか気になる
アルバム全体を聴きつつ、途中で休んだりデバイスを替えたりできるように、自分で Web アプリを作った
アルバムを最初から最後まで聴くのが好きなのだが、少なくとも YouTube Music は再生位置を覚えてくれず、別のデバイスでもう一度アルバムを開いて中断した位置を探さなければならないので不便だった
自分の Web アプリは URL を貼り付けるとサーバー側で
yt-dlpでダウンロードし、そこからストリーミングできる。再生位置を常に覚えてくれるので、車ではスマホで聴き、職場ではノートPCで続きを聴けるNTS Radio のように別のソースのミックスを追加するときにもよく合う
公開されているなら、その Web アプリを一度見てみたい
まだ全部は読んでいないが良い記事で、開発者がより細かな判断をどう下し、なぜそうしたのかを読むのが好きだ
試してみたオーディオプレーヤーが気に入らないという考えには共感する
音楽アプリの世界では画面構成と UI がほぼ一般化しているようだが、自分はそれが気に入らないし、うまく理解もできない
どの音楽アプリとも格闘しているような感覚なので、何か新しいものを作ろうとする人は歓迎したい
今も Apple Music アプリに自分の ローカルファイルを入れて使っている
Apple Music のストリーミングサービスはオフにして、macOS の Apple Music アプリにすべて取り込んだうえで、2007年のようにスマホをノートPCにつないで iPod のように同期している
すべてが期待どおりに動作し、音楽が頻繁に変わるわけでもないので同期も問題にならない
有線で同期するときには妙な懐かしさも感じる
async/awaitがなかった頃の Swift で、並行処理コードが Go や JS/TS よりも不格好でボイラープレートが多いように感じた、という点には同意しにくい非同期処理は並行処理コードを書くのを簡単にできる一方で、コードベースが大きくなるほど推論は単純でなくなることがある
複雑な非同期コードベースでは、コードの流れと並行性を理解するのがより難しい
スレッド上のコード実行コストを下げることが目的なら 軽量グリーンスレッドという解決策があり、スレッドコードの保守コストを下げることが目的なら、非同期は長期的にはより多くの労力を要求する可能性が高い
プロセスやサービスベースのカプセル化だけでも大きな利点がある
これは並行処理そのものの失敗ではなく、async/await 抽象化の限界に近いと思う
この記事が、曲を管理・再生するソフトウェアだけでなく、物理的なデバイスについての話であることを期待していた
数年前、音楽を聴くのが本当に好きだけれど、まだスマホを使う準備はできていない10歳の息子にMP3プレーヤーを買ってあげようとしたところ、選択肢があまりにひどくて驚いた
AppleがiPodを終了したことで、誰も埋められない大きな空白ができたのだと思う
USBメモリ型のiPod shuffleは、これまで使った中で最高のMP3プレーヤーで、小さく、別ケーブルなしで挿せて、バッテリーも長持ちした
音楽を探すための画面はなかったが、それは意図されたもので、シャッフルに任せればよかった
こうした比較的シンプルな概念でさえ、ハードウェア市場でまともに再現されなかったのは残念だ
人々はハードウェアの問題ではなくソフトウェア/DRMの問題だと言うだろうが、良い低価格の携帯音楽再生デバイスがないのは本当に残念だ
Spotifyとスマートフォンの存在がMP3プレーヤーを殺し、この2つが市場の空気をあまりにも多く占めて、残りを窒息させてしまった
たとえば https://www.fiio.com/cp13 と https://www.fiio.com/jm21 がある
電子工作や「古い」デバイスの再活用を学ぶ小さな教育プロジェクトとしても悪くない
外装カスタマイズの選択肢も多く、BluetoothやUSB‑Cの追加のような、より踏み込んだ改造も可能だ
他の人たちが言っているように、ソフトウェア面もWindowsのiTunes、macOSのFinder/Music統合で今でもうまく動作する
中国メーカーは、Mini iPhone 16やMini S24のように見栄えがよく、音楽再生もでき、スマートフォン機能もあるデバイスを50〜100ドルで売っている
親たちはMP3プレーヤーの代わりに、似たようなデバイスを子どもに買い与える可能性が高い
14歳の息子にスマホを持たせないのは一般的ではない子育て方針で、尊重するが、そうした需要が現在可能な供給を上回るほど多いようには見えない
10歳の子どもには高いかもしれないが、eBayで中古を見つけられるかもしれない
Music.appでローカル音楽を管理し、FinderのiPhone同期を使う組み合わせは、今でも自分にはうまく合っている
ただしMusic.appは、ひどかったBig Surでの書き直し以降、何年もメンテナンスされていないようで、厄介なバグもある
欠点はあるものの、この組み合わせは私が見た中で最高の音楽ライブラリ管理 + モバイル同期ソリューションだが、ソフトウェアがメンテナンスされておらず、クロスプラットフォームでもないため、結局は自分でシステムを書くことになりそうだ
OS 9時代の名残であるモーダルな設定/環境設定ウィンドウのように、iTunes特有の奇妙さが多く残っている
OS Xの設定ウィンドウはモーダルであるべきではない
完全に新しく作られたWinUIベースのWindows用Musicのように、ゼロから作り直したバージョンを社内で作業中なのではないかという気がする
一番いら立つのはポッドキャスト同期で、iTunesが担当していた頃はうまく動いていたが、Big SurのFinder同期にはバグがあり、別のトラックやポッドキャストに移ってから戻ると再生位置を覚えていない
そのため、数分より長いポッドキャストは実質的にデバイスで使いにくくなる
人々はAndroidに乗り換える代わりに、本当に自分の音楽アプリを作ってしまう
青い吹き出しのせいなのか、それともすべてが「ただ動く」からなのか気になる。オフライン音楽再生を除いては
自分で焼くことの何が問題なのか分からない
VLCアプリが私のファイルサーバー上のFLACのメタデータを安定して解析できず、ストレージ制限のため音楽コレクション全体をデバイスに保存することもできなかったからだ
私のアプリは、もっと多くの機能が欲しいという意味ではプロトタイプだが、オーディオ再生、リモートファイルアクセス、FLACメタデータ解析は備えているので、追加機能を実装する動機が足りなくなる程度には動作する
筆者と同じく、最初は経験があり、保守中のアプリもいくつかあるのでReact Nativeを使いたかったが、他のプラットフォームを対象にしたりデバッグしたりしたくはなかった
そこでSwiftUIを試すことにし、特殊なツール[1]でホットリロードのようなものを作った
Xcodeにカスタムリンカフラグを入れなければならないかなり汚いハックだが、TypeScriptとReact Nativeの開発者体験を懐かしく思わずに済む程度には動作する
[1] https://github.com/johnno1962/InjectionIII
実際、筆者が気に入る無料の音楽アプリを見つけられなかったというのは少し不思議だと思う
それでも、何かを作るのに必ずしも言い訳が必要なわけではないので、話を始めるための物語だったのかもしれない
選んだ音楽をデバイスに保存することもできる
オフラインの音楽コレクションがあるなら、https://play.google.com/store/apps/details?id=in.krosbits.mu...を本当におすすめします
問題なく快適に動作します
Plex、Jellyfin、WebDAV、SMBなどにも対応しています