[Tauri #07] Tauri v2:CDDB / FreeDB終了後にCDの曲名が出ない問題を、MusicBrainzと自作プレーヤー「DISC.TOC」で解決

[Tauri #07] Tauri v2:CDDB / FreeDB終了後にCDの曲名が出ない問題を、MusicBrainzと自作プレーヤー「DISC.TOC」で解決

はじめに

音楽CDをパソコンに入れても、曲名が出ない。手元では、WinampでもWindows Media Playerでも、そうなりました。CDの冊子を見ながら「3曲目は何だっけ」とやるのは、苦痛でしかありません。

この「曲名が出ない問題」には、背景があります。次の章で整理しますが、要点はCDそのものには曲名が入っていないことと、曲名を引くデータベース(CDDB系)の事情が変わったことです。

そこで、CDを入れるだけで曲名を出して再生する軽いアプリを、Tauri v2で作りました。名前は DISC.TOC です。

  • CDを入れると、アルバム名・アーティスト・曲名を自動で表示(MusicBrainz)
  • 管理者権限なしでドライブを直接読む
  • 曲を選ぶとすぐ再生。シーク、音量、イコライザー、リピート、シャッフル付き
  • 曲名・アーティストは手動編集もできて、TXT / CSV / CUE / JSON に書き出せる
  • 選択曲、または全曲を WAV に書き出せる

スクリーンショット

[Tauri #07] DISC.TOC 再生画面

リポジトリ(MIT):

前回の記事:

1. なぜ曲名が出ないのか:CDDBとFreeDBの話

音楽CD(CD-DA)には、曲名もアーティスト名も入っていません。入っているのは、TOC(目次)、つまり各トラックの開始位置だけです。(CD-TEXT付きのCDは例外ですが、ごく一部です)

プレーヤーは、このTOCからディスクIDを計算して、ネット上のデータベースに「このIDのCDの曲名は?」と問い合わせています。昔その役を担っていたのが CDDB です。

  • CDDBは、利用者が曲名を登録して育てる形で始まったが、のちに商用化された(現在のGracenote)
  • その反動で、誰でも使えるデータベースとして FreeDB が生まれ、多くのプレーヤーがこれを参照した
  • FreeDB は 2020年3月に終了した。継承する gnudb などは残っている
  • MusicBrainz は、別系統の、誰でも編集できるオープンなデータベース。中心のデータはCC0で、APIも公開されている

どのプレーヤーがどのデータベースを引くかは、ソフトと時期でまちまちです。「昔は出たのに出なくなった」「このCDだけ出ない」が起きるのは、そのせいです。

DISC.TOC は、MusicBrainz を引く作りにしました。理由は、JSONのAPIがあること、データが再利用できること、載っていないCDは自分で登録できること(その導線として登録ページを開くボタンも付けました)の3つです。

CDDBのディスクIDも、TOCから計算できる

CDDB系のIDは、8桁の16進数で、TOCから計算します。

n = 各トラックの開始秒(オフセット/75 + 2)の「各桁の和」を、全トラックぶん足す
t = リードアウトの秒 − 最初のトラックの開始秒
ID = ((n % 255) << 24) | (t << 8) | トラック数

DISC.TOC も、このCDDB IDを計算して表示・書き出しに載せています。core に実装して、実機のTOCでテストしました。MusicBrainzのIDとは別物なので、両方持っておくと、ほかのデータベースと突き合わせるときに便利です。

2. 構成:Rustはドライブ担当、画面は素のJS

今回も、フロントは素のJavaScriptで、バンドラーなしです。Rust側は、ドライブとネットワークと保存だけを受け持ちます。

  • core/:純粋なロジック(TOCの解析、ディスクIDの計算、書き出し形式)。cargo test で確かめられる
  • core/src/cd_win.rs:Windowsのドライブアクセス
  • src-tauri/:Tauriの本体(MusicBrainzへの問い合わせ、保存、WAV書き出し)
  • ui/:画面(再生、イコライザー、編集)

ドライブに触る部分と、純粋な計算を分けたのが効きました。TOCとディスクIDは、ドライブがなくてもテストで回せます。

3. 管理者権限なしで、CDのTOCと音声を読む

一番の山場がここでした。CDの中身を生で読むソフトは、管理者権限を求めるものが多いからです。

結論から言うと、読み取りだけなら、通常の権限で足ります。ドライブを \\.\F: として読み取りで開き、DeviceIoControl を2種類使います。

// 読み取りだけで開く(書き込み権限は要らない)
CreateFileW(r"\\.\F:", GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, ..., OPEN_EXISTING, ...)

IOCTL_CDROM_READ_TOC = 0x24000   // 目次(各トラックの開始位置)
IOCTL_CDROM_RAW_READ = 0x2403E   // 生のオーディオ(CD-DA)

RAW_READ には、RAW_READ_INFO(16バイト)を渡します。オフセット(LBA×2048)、セクタ数、トラックモード(CD-DAは2)の3つです。1セクタは2352バイトで、16bit・ステレオ・44.1kHzのPCMがそのまま返ってきます。1秒は75セクタです。

読み取りが失敗することもあるので、数セクタずつ読んで、失敗したら3回までやり直しています。

「Win32_Security」を足し忘れる

windows-sys で CreateFileW を使うには、Win32_Foundation、Win32_Storage_FileSystem、Win32_System_IO に加えて、Win32_Security のフィーチャーも要ります。最初に足りなくて、unresolved import の1エラーだけ出ました。

4. MusicBrainzのディスクIDを自前で計算する

曲名を引くには、CDを一意に決めるIDが要ります。MusicBrainzのディスクIDは、TOCから計算できます。

  1. 最初のトラック番号、最後のトラック番号、リードアウトの位置、99トラック分の開始位置を、16進の文字列に並べる(位置は150セクタ分を足す)
  2. その文字列のSHA-1を取る
  3. Base64にして、+ / = を . _ - に置き換える

これを core に実装して、実機のTOCでテストしました。

問い合わせの順番

  1. ローカルに保存した曲情報(JSON)
  2. MusicBrainzのディスクID検索(/ws/2/discid/<id>)
  3. 見つからなければ、TOCでの近似検索(/ws/2/discid/-?toc=...)

MusicBrainzのAPIは1秒に1回までなので、2回目の問い合わせの前に1.1秒待ちます。User-Agentには、連絡先つきの名前を入れました。

解けていない謎

手元のCDは、ディスクIDでは404になり、TOC検索なら見つかりました。プレス違いなど、原因の想像はつきますが、確かめていません。ディスクIDが外れてもTOC検索で拾えるので、使い勝手は変わりません。

5. 最初は「全部読んでから再生」で、遅すぎた

最初の再生は、トラックを丸ごとWAVに読み出してから <audio> に渡す作りでした。動きはしたのですが、曲の切り替えにとても時間がかかり、3曲目を選ぶと止まることもありました。

Winampなら1〜2秒で切り替わります。「ストリーミングに切り替えないと、プレーヤーとして使えない」と判断して、作り直しました。

読みながら再生する

Rust: read_pcm(drive, lba, count)  → 生のPCMバイト列をそのまま返す
JS:   1秒ぶん(75セクタ)ずつ読む → AudioBufferSourceNode に並べて鳴らす
  • read_pcm は、JSONではなく tauri::ipc::Response でバイナリのまま返す
  • 最初の1回だけは38セクタ(約0.5秒)にして、鳴り始めを早める
  • 常に6秒先まで読んでおく。それ以上は200msごとに様子を見る
  • 読み込みが間に合わなくなったら、再生時計を後ろにずらして続ける
  • シークは、その位置から読み直す。一時停止は AudioContext.suspend()
  • 世代番号(gen)で、切り替え前の古い読み込みを捨てる

これで、動作がWinampと同じになりました。ドライブ側は、回転が止まった状態から読み始めるので、最初の1曲だけは数秒かかります。これは円盤を回し直す時間なので、ソフトでは縮められません。

ドライブのハンドルは、Rust側で使い回しています(失敗したら捨てて、次回に開き直す)。同時に呼ばれても壊れないよう、ミューテックスで1つずつ読ませています。

6. 画面は、WINAMP X2を丸ごと流用した

画面は、以前自作した「WINAMP X2」(lain-lab.comのトップページのプレーヤー)のコードを渡して、名前を変えて流用しました。CSSのクラスを dt-* に、色は --dt-* の変数にして、スキンはその値を差し替えるだけです(LAINとAQUA)。イコライザー、スペクトラム、プレイリストはそのまま動きました。

ただ、WINAMP X2では別々に動かせた MAIN / EQ / PLAYLIST の3つが、DISC.TOCでは1つのウィンドウに縦積みです。タイトルバーなしで窓を個別に動かすには、Tauriのウィンドウを分けて、状態を同期する作りが要ります。今回は見送りました。

名前については、WinampやWMPとは無関係の個人アプリだとREADMEに明記しています。見た目が似ていても、アプリ名もロゴも別物にしました。

7. WebView2では「ダウンロード」が動かない

曲リストの書き出しは、最初Blobを作って <a download> をクリックする、定番の書き方にしました。ところが、WebView2では何も起きません。

Rust側でネイティブの保存ダイアログを出して、そのパスにファイルを書く形に変えて解決しました。

app.dialog().file()
    .add_filter("TXT", &["txt"])
    .set_file_name("album.txt")
    .blocking_save_file()

CSVには、Excelで文字化けしないようにBOMを付けています。JSONは読み込み直せるので、曲情報を他のPCへ持っていけます。CUEは、曲の開始位置まで書き出します。

編集した曲情報は、ディスクIDごとに app_data_dir/discs/<id>.json に自動保存されます。次回からは、ネットなしですぐ出ます。

MusicBrainzに載っていないCDのために、登録ページ(/cdtoc/attach?toc=...)を開くボタンも付けました。

8. v0.2.0:WAV書き出し

ストリーミング用の read_pcm があるので、WAVへの書き出しは、頭から読んで44バイトのヘッダーを付けて書くだけです。ただ、現実のCDは傷があるので、次の点は気を付けました。

  • 書き込みは .wav.part に行い、最後まで成功したら .wav に名前を変える。中止した場合は .part を消す
  • 読めないセクタは、1セクタずつやり直す。それでも読めなければ無音を入れて、終わったときに件数を報告する
  • 書き出し中は再生を止める。同じドライブを同時に読むと、両方が遅くなるため
  • 進捗は、Tauriのイベント(rip-progress)で画面に流す

ファイル名は 01 - タイトル.wav の形で、コンピレーション盤は 01 - アーティスト - タイトル.wav にします。MP3 / OGG は、WAVが出せれば他のソフトで変換できるので後回しです。

9. 配布:GitHub Actionsで、17分かかった

配布は、[Tauri #06] と同じく、タグのpushでGitHub Actionsが動く構成です。ただ、今回はWindows専用にしたので、ジョブは1つだけです。

初回は17分かかりました。大半は cargo install tauri-cli で、約5分をここで使います。ほかに、Rustの依存のコンパイルがあります。

  • 次の改善案:tauri-cli をビルド済みのバイナリで入れる(taiki-e/install-action など)。依存のキャッシュも効かせる
  • 急ぎのときは、手元で cargo tauri build して、成果物を下書きに上げる手もある

インストーラーは .msi と .exe(NSIS)の2つが作れました。署名はしていないので、初回はSmartScreenの警告が出ることがあります。「詳細情報」→「実行」で起動できます。

インストール版も、一覧表示、再生、書き出しまで、cargo tauri dev と同じように動くことを確かめました。

まとめ:ドライブは生で読めて、再生はストリーミングにする

  • 非管理者でも、CreateFileW の読み取りと2種類のIOCTLで、TOCも生音声も読める
  • 曲名が出ないのは、CDにはTOCしかなく、問い合わせ先(CDDB / FreeDB系)の事情が変わったから
  • ディスクIDはTOCから自前で計算できる。外れたらTOC検索で拾う
  • 全部読んでから再生するのは遅い。読みながら鳴らして、6秒先まで先読みする
  • WebView2は、<a download> が動かない。保存はネイティブのダイアログで
  • 傷のあるCDには、リトライと無音埋めと、件数の報告を

動作確認は、1台のドライブと数枚のCDという限られた環境です。動かないドライブやCDがあれば、Issuesで教えてください。今後は、ウィンドウを個別に動かせるUIと、Linux対応を考えています。