YouTube (Music)のプレイリストのギャップレス再生

提供:Turgenev's Wiki

YouTube Musicの無料版は、Googleアカウントすら不要のサービスながら、Apple music等の有料サービスと比較してもそれなりの範囲の楽曲をカバーしている。アルバムはYouTubeのプレイリストとして管理されており、検索は曲からアルバムへの移動ができるYouTube Music上で行う方が便利であるがURLの「music.」を消せば(「www.」に変えれば)YouTube上でも視聴可能である。しかし、YouTubeもYouTube Musicも(筆者の知る限り、少なくとも無料版では)ギャップレス再生には対応しておらず、ライブアルバムやアタッカ有りの交響曲などで曲間の音が不自然に途切れてしまう。これを解決しようというのが今回のテーマである。ついでに広告が非表示になり、再生中に「動画が一時停止されました。続きを再生しますか?」のポップアップが表示されることもなくなる。

なお動作確認したのはWindowsとLinuxのみである。ただ再生に使用するソフトであるmpvはMacにもあるのでMacでも動くだろう。Androidは頑張ればLinuxでできることは全てできるという印象なのでできそうだがMacよりは大変だろう。mpv-androidというAndroid用のmpvクライアントもあるらしいので使えそうではある。iPhoneは知らない。

yt-dlp

YouTube側が提供するソフトウェアで解決するのは難しそうなので、内部データを直接用いる必要がある。そのため、YouTube(やその他多くの動画サービス)のデータを解析・ダウンロードすることができるフリーソフトであるyt-dlphttps://github.com/yt-dlp/yt-dlp)を使用する。

yt-dlpはyoutube-dl(のフォークであるyoutube-dlc)のフォークである。現時点でのyoutube-dlの最新版(2021.12.17)では、YouTube動画のダウンロードが異常に遅くなる不具合がある(YouTube側の変更によるもので、発生はおそらく2021年頃から?)(https://github.com/ytdl-org/youtube-dl/issues/30715 によると修正は完了しているがリリースに反映されていない)が、yt-dlpではこの問題が修正されている。

インストールは実行ファイル単体でダウンロードしてもいいし、pipのパッケージとして入れてもよい。

例えば、yt-dlp -F 動画URLとすると、ダウンロード可能なフォーマット(画質・音質に差がある)が一覧表示される。なおURLに&が含まれる場合は””で囲うこと。これを見るとわかるが、YouTubeの動画は内部では動画と音声に分けて管理されており、再生時にブラウザで両者を合成している。最も高品質な音声データは”251”というフォーマット番号のもので、Opus形式で圧縮されたWebMオーディオとなっている。これをギャップレス再生することができれば目的は達成できそうである。

WebMオーディオのギャップレス再生に対応したソフトは少なく、筆者が知っているものはmpvのみである。mpvの使い方やオプションについてはMPVとギャップレス再生も参照。mpv以外だとWindows Media PlayerやFoobar2000などはwavやflacのギャップレス再生には対応しているが、webaの再生においてはわずかに隙間が聴こえる。従ってこの記事ではmpvを使用する。

mpvでの内部URLの再生

yt-dlpでは(名前の通り)音声データをローカルにダウンロードすることもできるが、著作権法・YouTubeの利用規約上の問題があるため、もっと良い方法を考える。実は、yt-dlpでは音声データをダウンロードするのではなくそのURLを取得することもできる(通常の動画のURLと区別するために以下では”内部URL”と呼ぶことにしよう)。またmpvはWeb上のリソースの再生にも対応している。従って、yt-dlpでURLを取得してからそれをmpvに渡すという流れにすれば求める機能が実現できそうである。なお、前述のMPVとギャップレス再生にもあるように、ギャップレス再生を有効にするためにmpvに特定のオプション(最新版ならおそらくprefetch-playlist=yesaudio-buffer=[2以上くらいの数字]のどちらか)を設定する必要がある。

ちなみに、mpv自体にもyoutube-dlとの連携機能があり、内部URLではなく通常のYouTubeのURLを渡しても再生することができるが、この場合ギャップレス再生にはならない(https://github.com/mpv-player/mpv/issues/7436)(--prefetch-playlistも効果がない)。

ところで、この内部URLを見てみると最初のほうにexpire=xxxxxxxxxxとUNIX時間らしきものが書いてあるパラメータがあり、どうやらこのURLの有効期限は6時間しかないようである。従って6時間以上かかるプレイリストを再生するあるいは再生を途中で止めて時間が経ったなどの場合は再生できなくなってしまう。参照する内部URLを動的で切り替えてくれるプロキシ的なものを用意するとかプレイリストを途中で書き換える(この場合mpvの--prefetch-playlistを使うべきではない)とかすれば解決できそうだがそれほど簡単ではない気がするため放置している(簡単なのであればYouTubeのプレイリストURLを直接指定してのギャップレス再生にmpvが対応していたのではないかと思う)。

実装の方針

では再生用のコマンドの設計を定める。現実にはプレイリストを丸ごと聴くとは限らないから、開始曲と終了曲を指定できるようにしたい。そこで今回は、

ytp "[開始曲のプレイリストID付きURL]" [再生曲数指定なしあるいは0なら最後まで再生]

という構文で動作するytpというプログラムを作成する。丸ごと聴きたければ最初の曲のリンクを渡せばよい。Windowsにおいてはスタートボタンを押して「検索」欄に入力すれば任意のコマンドを実行できるので(※Windows 11のバージョン22H2ではバグで引数が読まれないので代わりにWin+Rを使うとよい)、YouTube Musicのプレイリスト画面から始めてかなり少ないステップで音楽を聴くことができるようになる。ここで「プレイリストID付きURL」と言っているのは、https://[wwwまたはmusic].youtube.com/watch?v=動画のID&list=プレイリストのID&index=[数]という形式のもので、このindexの部分から開始曲を取得する。www.youtube.comのほうではプレイリスト内の動画を右クリックして表示されるメニューからこの形式のURLをコピーできる。一方でYouTube Musicでは、アルバム画面で曲目をShift+右クリックすれば同様にURLを得られるが、indexの部分が含まれていない。そこで今回は、Requestlyというブラウザ拡張機能(Chrome、Edge、Safari、FireFox等で使える)を使用し、YouTube MusicのWebページを書き換えてURLに&index=[数]を付加する。

RequestlyによるWebページの書き換え

このセクションに含まれる情報は比較的貴重なものである可能性があります。

Requestlyの「Insert Scripts」を使用する。最初、呼び出し条件(If request URL Contains)にmusic.youtube.com/playlistを指定してもどうしてもうまくいかなかったのだが、YouTube Musicはシングルページのアプリケーションとして実装されているようで、URLは形式的に書き換えられているだけらしい(https://github.com/requestly/requestly/issues/225)。そこで、呼び出し条件には”music.youtube.com”だけを指定し、MutationObserverというのを用いてページ内の変化を常時監視するような形でスクリプトを書く。呼び出しのタイミングは「Insert Before Page Load」に設定すること。

ただ、このコードだと条件が緩すぎて、一度の遷移につき十回ほど書き換え関数が実行されてしまうので改善を検討中。しかし厳しすぎるとこんどは高速で遷移した場合などにうまく実行されなかったりする。URLの書き換え自体は、CSSセレクタみたいなやつを適当に書けばいいので容易。

ちなみに、www.youtube.comのほうに強制的に遷移させる(履歴ごと上書きするためにlocation.replaceを使うのがよい)という方法もある。

new MutationObserver(() => {
    if (location.href.indexOf("youtube.com/playlist") >= 0) {
        urlAddIndex();
    }
}).observe(document, { subtree: true, childList: true });

function urlAddIndex() {
    const playlist_node = document.querySelectorAll('ytmusic-responsive-list-item-renderer.style-scope.ytmusic-shelf-renderer');
    if (!playlist_node) return;
    for (const item of playlist_node) {
        var index_node = item.querySelector("div.left-items.style-scope.ytmusic-responsive-list-item-renderer > yt-formatted-string");
        var title_node = item.querySelector("div.flex-columns.style-scope.ytmusic-responsive-list-item-renderer > div.title-column.style-scope.ytmusic-responsive-list-item-renderer > yt-formatted-string");
        if (!title_node || !index_node) continue;
        if (title_node.innerHTML.indexOf("&index=") >= 0) continue;
        if (index_node.innerHTML == " ") continue;//for unavailable movies
        var newHTML = title_node.innerHTML.replace("\">", "&index=" + index_node.innerHTML + "\">");
        title_node.innerHTML = newHTML;
    }
}
  • Requestlyの要求権限には「すべてのウェブサイト上にある自分の全データの読み取りと変更」が含まれる。自己責任でインストールすること。

music.youtube.comとwww.youtube.comで内容が異なるプレイリスト及び利用不可能な曲を含むプレイリストへの対策

このセクションに含まれる情報は比較的貴重なものである可能性があります。

最初に述べた通り基本的にはURLがmusic.youtube.comでもwww.youtube.comでも同じプレイリストを視聴することができるが、一部のプレイリストでは含まれる動画が異なる場合がある(v=の後にあるIDで確認できる)。クラシック音楽のアルバムにはあまりないが、ロックバンドによるアルバムなどではしばしば見られる。

例えばThe Beatles 1967 - 1970の最初の曲は、「www」経由だとアルバムのジャケットと同じ静止画が映像として表示され、冒頭の音声は左側に極端に偏ったものが再生される(ビートルズの曲にはこのようなものが多いらしい)。一方、「music」経由だと動きのある映像が表示され、音声も左右に偏りのないものが聴こえる。明らかに全く別のデータである。

次はThe Lamb Lies Down On Broadway (Remastered 2008)を見てみよう。「www」はやはりアルバムのジャケットと同じ静止画の映像だが、「music」のほうはディスクが回転する地味なアニメーション映像が表示される。音声に関しては一聴して違いは感じられず、実際おそらくいずれも全く同じデータに由来する(せいぜい非可逆圧縮による音質の違い程度しかない)ものかと思われる。しかし、ギャップレス再生という観点では重要な違いがあり、「music」のほうの音声は冒頭or末尾ごく短い無音が挿入されているようで、mpvでギャップレス再生をしても隙間が聞こえてしまう。

ということで結論としては、常に「www」のほうのプレイリストデータを使用すべきである。といっても、現状のyt-dlpはmusic.youtube.comのURLを指定したとしてもwww.youtube.comのデータしか取ってこない(https://github.com/yt-dlp/yt-dlp/issues/622)(--extractor-argsなども効果なし)ため、特に対策などは不要である。

筆者がこの問題に気づく以前は、yt-dlpでプレイリストIDから動画IDの一覧を取得し(このように内部データを参照しない操作では--flat-playlistを指定すると大幅に速度が向上することを付記しておく)、URLの動画IDと照合してindexを計算していた。しかし両URLで内容が異なるプレイリストだと「www」のデータの中から「music」でのIDを検索しても見つからないため、ページの書き換えでindex番号を付加する(URLの動画IDは無視する)方法にしたという経緯があった。

また、一部のプレイリストでは利用不可能(有料版のみ)として灰色で表示されている曲があり、これはwww.youtube.comで見るとそもそもプレイリスト内に表示されず、URLのindexもこれらを無視したものになっている。一方でyt-dlpはデフォルトでは利用不可能な曲も含めて曲数をカウントするため、wwwのURLのindexを使うと曲数がズレてしまう。対策としては、www.youtube.comのURLが渡された際にはyt-dlpに--compat-options no-youtube-unavailable-videosを指定すれば正しい曲が選ばれる。

ytpコマンドの実装

では、URLを受け取って再生する部分を実装する。本質的な部分は全てyt-dlpがやってくれるので使用言語は何でもいいが、個人的に既にGit Bashをインストールしていたので、本体をシェルスクリプトで作成し、それを呼び出すためだけのバッチを別で作成するという方針にした。このためだけに読者にGit Bashをインストールさせるのもどうかと思うが、罠だらけのcmdのバッチファイルで文字列処理を書く気にはなれないし、かといってPythonとかを使うのも大袈裟な感じである。

ところで、ただbatから実行するだけだとコマンドプロンプトのウインドウ(startを使うならbashのウインドウ)が再生中にずっと出ていて邪魔である。やり方が悪いのかわからないがnohupを使ってもうまくいかない(パイプを使ってmpv.exeに直接プレイリストを渡していてそこにシェルが介在するため難しそう?)。そこでvbsを使ってGit Bashをウインドウ非表示で呼び出すことにした。しかしvbsはbatと違って拡張子無しで実行できないのでさらにこのvbsを呼び出すだけのbatを作成した。結果としてやや回りくどい実装になってしまった。

bat内ではmpvのオプションを指定できるようにした。

スクリプトの内容

このセクションに含まれる情報は比較的貴重なものである可能性があります。

ではスクリプトの内容を添付する。ここでは、mpvに渡すオプションの例として、フルスクリーン指定--fs=yes、及び前述のビートルズのような極端なステレオを緩和して左右それぞれ3:1の割合で混ぜる--af=lavfi=[pan=stereo|c0=0.75*c0+0.25*c1|c1=0.25*c0+0.75*c1]を使った。

  • ytp.bat(最初に呼ばれるランチャー)

    cd %~dp0
    set "snd=%2"
    if "%2"=="" set "snd=0"
    start "" "ytp-invisible.vbs" %1 %snd% "--fs=yes" "\'--af=lavfi=[pan=stereo|c0=0.75*c0+0.25*c1|c1=0.25*c0+0.75*c1]\'"
    

    再生曲数が指定されていない場合は0を補う。--afオプションに関してはバックスラッシュとシングルクォートでエスケープしている(試行錯誤のすえ、とりあえずこれで問題なく動いている)。

  • ytp-invisible.vbs(bashを非表示で起動するためのvbs)

    Dim oWshShell
    Set oWshShell = CreateObject("WScript.Shell")
    Dim mpv_args
    For cnt = 2 To WScript.Arguments.Count - 1
     mpv_args =  mpv_args & " " & WScript.Arguments(cnt)
    Next
    oWshShell.Run """C:\Program Files\Git\bin\bash.exe"" -c 'ytpx.sh """ & Wscript.Arguments(0) & """ " & Wscript.Arguments(1) & mpv_args & "'", 0, False
    

    2つ目以降の引数をつなげて渡している。Runのオプションで0(非表示)、False(終了を待たない)を指定している。デバッグ時は0を1に変えておくとウィンドウが表示される。

  • ytpx.sh(処理を行う本体)

    #!/bin/sh
    cd /path/to/mpv_dir
    ARR=(`echo $1 | sed -r "s/^.*(www|music)\.youtube\.com\/watch\?v=[^&]+&list=([^&]+)&index=([^&]+)$/\1 \2 \3/"`)
    if [ ${ARR[0]} = "www" ]; then
      NA_OPTION="--compat-options no-youtube-unavailable-videos"
    fi
    INDEX=${ARR[2]}
    if [ "$2" != "0" ]; then
      END_INDEX=`expr $INDEX + $2 - 1`
    fi
    shift 2
    yt-dlp $NA_OPTION -f 251 --extractor-args "youtube:lang=ja" -O title -O url -I $INDEX:$END_INDEX ${ARR[1]} | sed '1~2 s/^/#EXTINF:-1,/' | sed '1i#EXTM3U' | ./mpv.exe --player-operation-mode=pseudo-gui --prefetch-playlist=yes --playlist=- $@
    

    まず3行目で、長さ3の配列ARRに①”www”または”music”②プレイリストID③index、をそれぞれ入れる(動画IDは捨てる)。4~6行目で--compat-options no-youtube-unavailable-videosの有無を設定する。

    7~10行目では開始indexと再生曲数を用いて植木算をして終了曲のindexを計算している。$2が0なら$END_INDEXは未設定のため事実上は空文字列(終了曲の指定なし)となる。

    最後がメインのコマンド実行である。yt-dlpを用いてタイトルと内部URLの一覧を取得する(なお--extractor-args "youtube:lang=ja"は日本語タイトルを取得しようとして入れてあるが、現状では(おそらくyt-dlpがwww.のほうしか見ないため)効果なし)。フォーマットは最高音質の251を指定する。さらに、ここまでに計算した開始・終了インデックスを-Iオプションで渡す。出力形式としてはタイトルと内部URLが交互に書かれた複数行テキストが返ってくる。

    次に、mpvにm3u形式のプレイリスト(単にurlやファイル名を列挙する書式と違って、再生時のタイトルを指定できる)として渡すため、タイトルがある行(奇数行)の先頭に#EXTINF:-1,を付加し、さらに全体の先頭行に#EXTM3Uを付ける。そして最後にmpvの--playlistに対して標準出力(ハイフンで表される)を渡すことで、めでたくタイトル付きで音声をギャップレス再生できる。動画データを渡していないので画面は真っ黒である。

    追加のmpvのオプションとしては、バッチファイルから渡されてきたものに加えて、CUIからの実行でもGUIを強制する--player-operation-mode=pseudo-guiとWeb上リソースのギャップレス再生に有効な--prefetch-playlist=yesを指定しているが、古いバージョンではさらにオプションが必要かもしれない(MPVとギャップレス再生 も参照)。もちろん、スクリプト内ではなくmpv.confで指定してもよい。

    なお(lib)mpvを内部で使用するmpv以外のクライアント(Linuxのcelluloidなど)の場合はハイフンを用いた標準入力からの受け取り指定ができないこともある。手元のLinuxではとりあえず一旦m3uファイルに書き込んでからそれをcelluloidに渡すように変えてある(自明なのでここでは省略)。celluloidなどを使う場合も、適宜オプションを指定すること。

ショートカットキーからの起動

上記の実装では引用符の入力など多少の回りくどいキーボード操作が必要である。URLはクリップボードに入っているので、ショートカットキーから起動することももちろん可能である。ただし再生曲数を入力するためのダイアログは必要である。以下のAutoHotkeyのスクリプトの例では、Ctrl+Alt+Shift+Lを押すと入力ダイアログを表示して再生曲数を取得し、ytp.batを起動する。

^!+l::
InputBox, play_numbers, Gapless Play YouTube, Enter the number of tracks to play ("" or "0" will play to the end)
Run ytp.bat "%clipboard%" %play_numbers%

英語は間違っているかもしれない。

ダイアログはvbsのInputboxでも出せるが、Windows標準のショートカットキーの設定(ショートカット(←これはファイルの形式のこと)に対して設定できる)は若干管理しづらいイメージがあるためAutoHotkeyをおすすめしておく。