Spotifyの音量正規化機能は大抵はうまく動いているのだが、曲によっては音量や音自体がおかしくなることがある。特に、Pink Floydの"Speak to me"(1973)の先頭のように音量がとても小さい時に破綻する。それでも、Google Play Musicのように、ないよりはずっといいのは確かだ。

が、近頃、SpotifyのWeb APIが使えるようになり、その中に曲の(音的な)特徴を取得する機能があるのに気付き、それを使って音量正規化ができないかと思っていた。具体的には、"Get Audio Analysis for a Track"(以下、Analysis)や"Get Audio Features for a Track"(以下、Features)が使えそうなので、調べてみた。

すると、どちらでも音量正規化に必要となる特徴量(曲の音量)が取れそうなことが分かった。具体的には、それらで取得できる"loudness"や"energy"である。なお、Analysisは取得できる情報は多いものの、上記の値程度しか使わないし、毎回分析するのか処理が遅いので、Featuresを使うことにした。

新しい音量正規化の処理として、以下のような手順を考えた。

  1. 新しい曲の再生開始時に、Features APIを実行し、音量の特徴量を取得する。(→ L)
  2. Lから、音量を正規化するための音量調整量を求める。(→ G)
  3. GをSpotifyの音量値に変換し(→ V)、設定する。

いくつかの曲で試したところ、音量の特徴量Lとしては、loudnessが良さそうだった。定義(概要)は以下のようなので、理論上はそのまま使えそうだ。

The overall loudness of a track in decibels (dB). Loudness values are averaged across the entire track and are useful for comparing relative loudness of tracks.

なお、energyの定義(概要)は以下のようなので、うまくLに変換できれば、(数値でなく)聴感的な音量の正規化ができそうなのだが、いい方法が思い付かなかったので、今回は見送った。

Energy is a measure from 0.0 to 1.0 and represents a perceptual measure of intensity and activity.

GMB(gmusicbrowser)での音量正規化の結果と比較して検討し、loudnessをそのままLとして使う場合、LからGへの変換式は以下とした。

G= X - L

Xは目標とする(一定にしたあとの)再生音量(dB)で、試行錯誤の結果、-13(dB)が最もGMBの音量に近くなったので採用した。

(10/12 6:26追記) 後から気付いたのだが、上のXは正確には2つの要素からなる。一つは、目標とする音量Yであり、これが仮想的な0dBとなる。もう一つは、正規化した音のシステムの最大音量(0dB)からの余裕Zであり、Yの音量は実際のシステムでは-Zとなる。音量をYに正規化したあと、音量(振幅)はZまで大きくなることが可能である。XはYとZの和であり、通常はY= Z= X/2と設定するので、上のように書いても大きな問題ではないが、動作の調整・確認をしている時に「何かおかしい」と思ったので書いた。

ただ、これは考え過ぎとか誤解だと思う。今、書いたあとに気付いたのだが、Yは実際には「平均」音量であり、もしYを「最大」音量(0dB)とすれば、Z= 0となる(それで充分)だから、Y= Xとなり、上の変換式はそのままで正しい。最初は最大音量と考えてその式を書いたのに、実際の動作を見ているうちに誤解したのだろう。 (だから、この節は書かなくても全く問題ないのだが、忘れるために書いておくw)

が、更に気付いたのは、Lはloudnessであって最大音量ではないので、やはりYは平均音量と考えるべきで、やっぱり上の節は要る。別の考えをすれば、Lに係数が要るのかも知れないということだ。その係数は、その曲の振幅の変化量(= ダイナミックレンジ)を示す値だろう(が、それはSpotifyでは分からない)。

Spotifyの音量を調整するには、アプリの音量そのものを変更する以外に、Spotifyアプリのあとにアンプやミキサーを入れてそれで調整する手がある。しかし、外部プログラムから手軽にゲインを変更可能なものが見つからなかった(MIDIを使えば制御できそうではあった)のと、できるとしても結構大げさになるので、今回は見送った。

Spotifyアプリの音量を変更するには、(現在のSpotifyアプリはDbusでは音量が調整できないため、)Spotifyアプリにウインドウシステム(X11)経由でイベント(キーやマウスホイール)を送ってボリュームを操作する方法と、PulseAudioのSpotifyアプリのボリュームを設定する方法がある。後者は音量値にdBや相対値が指定でき、pactlコマンドで簡単に実行できるので採用した。

上記のように、音量値にdBが指定できるので、基本的に、GがそのままSpotifyの音量値として使える。ただし、Gが大き過ぎる(例: 0よりかなり大きい)と音質が劣化するので、上限(Vmax)を設けることにした。今回はLは6(dB)とした。結局、Spotifyの音量設定値V(dB)は以下のようになる。

V= G (G < Vmaxの時),
     Vmax (G >= Vmaxの時)

ただし、pactlコマンドの仕様により、符号付きの値は現在値からの相対値になってしまい、負のdBは相対値とみなされるために(、結局全然)使えないので(どういうつもりなのか、作者に聞きたい)、pactlに指定する時に0..1の数値(R)に変換している。これはdBから数(比率)への変換で、以下のとおりである。

R= 10(V/20)

ここで不思議だったのは、Rをpactlに指定すると期待どおりの結果にはなったのだが、設定後に取得した音量の%の値が想定と異なるのである。例えば、"0.5"を指定すると、結果は"50%"でなく"79%"となる。dBの値は"-6.02dB"と正しいが、%はどうもおかしい気がする。本来は50%になるべきと思うのだが、%の値も対数なのだろうか。謎ではあるが、実際の音量や聴感的には期待どおりなので、深くは考えないことにした。ただ、将来的に、バージョンアップなどでこの動作が変わってしまうリスクはある。

pactlコマンドでSpotifyアプリの音量を設定するには、以下の手順で行う。

  1. Spotifyアプリのsink-inputのIDを得る。→ siid
    • pactl list sink-inputsの出力を解析する。
  2. Spotifyアプリの音量をRに設定する。
    • pactl set-sink-input-volume siid Rを実行する。

上記の処理を実装して聴いてみたところ、概ね期待通り動いている(→ 曲が変わるとボリューム(画面中央下部)が動くデモ)。ただし、以下のような問題がある。

  • 曲の切り替わりの検出に時間ズレが生じることがあるので、音量設定タイミングがずれることがある。
    • 音量の小さい曲の後に大きい曲が掛かる場合、音量を下げるのが遅れる場合があって、その時には、次の曲の先頭が(一瞬)大音量で再生されてしまう。
    • 逆に、音量の大きい曲の後に小さい曲が掛かる場合、音量を上げるのが早過ぎる場合があって、その時にも、次の曲の先頭が(一瞬)大音量で再生されてしまう。
  • 曲によっては少し大きく感じることがある。
    • 例: Commodores "Easy", CHIC "Le Freak"

曲の切り替わりの検出精度は、Spotify API自体にズレがあるので、容易には向上できず、本質的な解決は難しいのだが、音量を一気に変えずに、1回の変更量に上限を持たせて少しずつ変えるようにして(ただし、音量を下げる時は2倍の速さにする)緩和を試みた。また、変更前の音量が0dBを超えている場合は、超過分を一度に下げるようにした。この修正の都合で、pactlへの音量設定値を、比率(R)でなく現在の音量とVの差分にした(結局、符号付きのdB指定(相対値)だけでも良くなった)。

(10/3 12:23追記) 曲の頭が一瞬大音量になることがある問題は、JACKのエフェクタを使えば緩和できるかも知れない。Spotifyの出力を、瞬間的な大音量を抑えるようなエフェクタ(リミッター?)につなぎ、音量正規化がonの時だけそのエフェクタをonにすれば良さそうだ。エフェクタの制御は本アプリ(Spotifyミニプレーヤー)からMIDIで行う。おもしろそうだが、パラメタの設定は難しそうだ。余りにも気になるようなら、やってみたい。

この場合は、音量正規化もJACKでできる。Spotifyの出力をミキサーのSpotify用入力につなぎ、その音量を本アプリから行えばいい。上記エフェクタの入力レベルが変えられるなら、それでも可能だ。ただ、これ自体の価値はそれほどない。あくまでも、大音量を抑えるついでにできるということだ。

大きく聴こえる曲は、今のところどうしようもない。数値で正規化する限界なのかと思い、採用を見送ったenergyがうまく使えないものかと思っている。ただ、GMBの正規化でも同様なことはあるし、自分や周囲の状態によっても音量は違って聴こえる(要は「気のせい」)から、あまり深追いしても仕方ないのかも知れない。

(10/6 21:22追記) 音量で気になっていることの一つは、静かな曲の音量が大きくなり過ぎることなので、それを解消するのに上記のenergy(以下、E)を使って音量設定値Vを調整してみた。

静かな曲はloudness(L)が小さいためにVが大きくなるので、音量が大きくなりやすい。一方、そういう曲はEも小さいので、Eの大きさ(小ささ)でVを調整することを考えた。Eが小さい時は、その分Vを減らすのである。

Eがどういう単位・仕様の値なのか不明(0..1ということだけ明らか)なのだが、試行錯誤して、Eの値をdBに変換してVに加える(Eは0より小さいので、実際には引かれる)ようにした。Eで調整した音量設定値V'は、次の式で求められる。

V'= V + k * 20 * log10(E) (E >= Eminの時)
      V + k * 20 * log10(Emin) (E < Eminの時)

kはEの強さを調整するための定数で、0.05から0.3程度が良さそうであるが、いろいろな曲で試したところ、0.15辺りが最も適当だった。ただ、曲によって変わるので、更に調整が要りそうだ。Eminは想定する最小のEで、0.0001とした(log(0)はエラーになるので、それを防ぐために定義している)。

この式は全くの思い付き(と誤り)から出て来たものである。Eの詳細が不明なので、そのままdBに変換していいのか怪しいが、VはdBなので、VをEで調整するのなら、少なくとも、EをdBに変換した値を使うのは適切だと思う。

残念ながら、この方式は、元々音量が小さくなくて更に大きく聞こえる曲(例: 上記の"Easy"や"Le Freak")には効果がない。それらはEが大きいため(だから、うるさく聞こえるのだろう)、ほとんどVが減らないからである。それらには別の特徴量が使えるのかも知れない。

(10/9 11:09追記) 音量でもう一つ気になっていることは、クラシックの曲の正規化がうまく行かないことだ。クラシックの曲はポップ音楽と違ってダイナミックレンジ(音量の幅)が広く、平均音量が小さいことが多いため、ポップ音楽と同じように正規化すると音量設定値Vが10dB前後ととても大きくなってしまう。一方、DACは0dB以上の音は再生できないので、音質が劣化する。

これに対処するには、まず、目標音量Xを下げて(例: -20dB)、(形式的な)増幅の余地を増やす必要がある。また、10/6の追記とは逆に、energy(E)に応じて音量設定値Vを増やす方が良さそうだ。そこで、E(のdB値)に応じてloudness(L)を増やすことにした。ただし、LがXより大きい場合に更にLを増やすと、Vが減って逆に音量が下がってしまうので、Lを増すのはLが小さい場合だけにした。クラシック音楽用の音量設定値V''は、次の式とした。

V''= X' - L''
  X': クラシック音楽用の目標音量 (dB)
  
  L''= L' (L' < X'の時)
        L (L' >= X'の時)
  
    L'= L + m * 20*log10(1 - E) (E >= Eminの時)
         L + m * 20*log10(1 - Emin) (E < Eminの時)

mはEの強さを調整するための定数で、いろいろな曲で試したところ、0.05辺りが最も適当だった。また、X'は-24dBとした。これらも継続して調整が要りそうである。

なお、目標音量をかなり下げるために音質の劣化が心配だが、まず、ほとんどの場合に実際の設定音量は0dB付近になるため、大きな問題はない。なお、仮に設定音量が目標音量と同じくらいになった場合には音の有効ビット数が減るため、音質が劣化する。その有効ビット数は、元の音を16ビット相当(ダイナミックレンジ: 約100dB)とした場合に設定音量を-24dBに落とした場合には、概ね (100-24)/6= 12.7ビット程度になる。この場合でも、有効なダイナミックレンジは76dB程度あるはずだ。

実際に、いろいろな音量の曲に対してこの方式で音量正規化を行った場合と音量正規化を行わない場合の音量を比較したところ(どちらも、アンプの音量は最初に調整した後は変えないものとする)、音量が適正だと感じることがほとんどだった。数値的にも、それらの曲に対して約9dB(= 2.8倍)の幅で調整を行っていたので、効果はありそうだ(→ クラシック音楽でも、異なる曲を混ぜて聴く時には音量正規化はあった方がいい)。なお、この方式ではポップ音楽も正規化できるが、方式やパラメタが最適でないうえに、ポップ音楽はダイナミックレンジが狭い場合が多いため設定音量が目標音量(-24dB)付近になることが多いので、音量と音質の点で得策でない。そのため、この方式をクラシック音楽用のモードにし、従来のをポップ音楽用にした。

クラシック音楽用の音量正規化モードを追加したため、ミニプレーヤのUIを変更し、音量正規化のon/offでなく、off("-")またはモード(ポップ("P")/クラシック("C"))で表示するようにした。

それから、音量正規化のモードを切り替える際に音量が大きく増加することがある(例: クラシック → off)ので、安全のため、再生中にそのような切り替えを行った場合は一時停止するようにした。

最後に、気になっていた、静かな曲での正規化結果を比較してみる。以下は、"Speak to me"の先頭約20秒(鼓動)の部分の右チャネルの波形(上から順に、GMB (正規化off), Spotify(正規化off), GMB (正規化on), Spotify(正規化on), Spotify(今回の正規化))である。なお、それぞれの開始時刻は合わせていないので、波形の位置は異なる。

Pink Floyd "Speak to me"の先頭約20秒の右チャネルの波形比較

正規化した波形(最後の3つ)を見ると、明らかに、Spotifyの正規化は「やりすぎ」であり、そのためにおかしく聴こえていたことが分かる。一方、今回の正規化(一番下)は小さ目ではあるが、GMB(上から3番目)と同様の振幅になっている。聴いた感じでも問題なく、今回の方式が有効であることが確認できた。

なお、今回の正規化の振幅が小さ目なのは、この曲の音量がとても小さく、本来の音量設定値が上限(6dB)を超えているために、充分に音量を上げられないためである。

→ 音量制限の上限を解除して試したところ、振幅がSpotify以上に大きくなってしまった(フルスケールを超えた)。パラメタの調整(あるいは、上記のenergyの利用など)が要るようだ・・・ そして、Spotifyの正規化の処理は正しかったようだ。この点はがっかりした。

→ 音量設定値の上限を7.5dBにしたら振幅がGMBと同等になったので、当面はこれで試してみたい。

→ (19:40追記) どうやら、(当たり前ではあるが、)0dB(100%)より大きく増幅するのは良くないようで、少し長くクラシック(0dB超えの曲ばかり)を聴いたあとにポップに切り替えたら違和感が生じた(耳がずっと圧迫されていたような感じ)。また、当たり前だが、たまに音量がオーバーレンジ(0dBを超える)するのも余りいい気分でない(ただし、瞬間的なので聴いていても分からない)。それで、上限を0dBにすることにした。それで音量が小さくなってしまう曲はもともと小さく作っていると考えて、諦めることにする。

馴染みのない方のために説明すると、0dBを超えて増幅するというのは、デジカメのデジタルズームとかISO感度を高くするようなもので、確かにそれらしい画像は写って便利なのだが、無理してデータを作ってるから画質は期待できないということだ。

あと、"Speak to me"の冒頭の音がおかしく聴こえるので、Spotifyアプリの正規化は動的に処理(例: 現在までの音量などで現在の音量を調整する)しているのかと思っていたが、波形はそうでもないので、音量を上げ過ぎているだけなのかも知れない。ただ、目標音量を小にすると音量が下がり過ぎてしまうので、Spotifyアプリは今一つなことは確かだ。

副次的な効果として、今までは、音量正規化をon/offする際はSpotifyアプリを再起動する必要があるので、再生が停まり画面が変わってしまうなど、ちょっとしたストレスだったが、今回の方式では気軽にいつでも(まさに"on the fly")切り替えられるようになり、大変便利になった。実際には、このイライラを解消したいということが、今回の作業に着手する大きな動機になった。

(10/6 21:22, 21:39 変数名の重複を修正、設定値を更新)

 

PS. その後、調整・修正・改良しながら使っていて(というか、そればっかりしているがw)、上記の曲間のタイミングずれによる不意の大音量を解消したいと思っている。が、それは難しいことも分かった。今のところ、以下のようなことを考えている。

  • コンプレッサーやリミッターで大音量を制限する。
    • 大音量がほんの一瞬に抑えられるが、通常の曲(特に、クラシックは音量レベル(上記L)が小さいと認識されるため、大抵、音量(上記V)が最大に設定される)の大音量も制限してしまうので、(聴感上は分からないが、)常時は使いたくない。必要な時だけonにする必要がある。
  • コンプレッサーなどを使わないで回避する。
    • Spotify web APIのタイミングズレを補償する。
      • (無音で?)曲間を検知して、曲間と認識する。
      • 「スマート」な処理を作る?
      • 次の曲の先読み?
    • 我慢する。

切り替えて使うにしても、コンプレッサーなどはどうも気に入らない(onにしている時は妥協していることになる)ので、使わない方法を考えている。でも、できるかどうかは不明だ。まあ、あまり頻繁に起こらない(実際、昨日確実に起こっていた曲で今日は起こらない)ので、我慢するのが一番効率的なのかも知れないw

その他、以下のような改良をした。

  • 音量設定処理の遅延を防ぐ。
    • 曲間にSpotify web APIの認証トークン(有効期限がある)を更新すると音量設定処理が遅くなる可能性があるので、可能な限り、「忙しくない」時にトークンの更新をし、曲間での更新を抑えるするようにした。
      • 具体的には、再生中に一定間隔(例: 2分)で(無駄に)web APIをアクセスする。この時、トークンの有効期限を短め(例: -5分)に見て早目に更新しておく。一方、曲の切り替わり時のアクセスでは、有効期限ギリギリまで(例: -10秒)トークンの更新をしないように指示する。
  • 自動的に音量をリセットする。
    • 再生を停止してある程度(例: 20秒)時間が経ったら音量をデフォルト値(例: -10dB)に戻すようにして、(小音量の曲の再生後、)次に再生する曲の頭が不意に大音量にならないようにした。
      • ただ、次に再生する曲の頭が小さくなってしまうことがあるという弊害はある。

(10/5 10:50)

PS2. PSに書いた、Spotify web APIの曲の切り替わり通知タイミングずれによる意図しない大音量を防ぐ方法について考えた。曲の切り替わりを正確に検出して、その切り替わった時点で音量を設定すればいい。そのためには、まだ曲間になっていない場合にはそれまで待てばいいが、既に曲間が過ぎている場合には時間を戻して、出た音の音量を変える必要がある。果たして、そんなことはできるのか?

まず、曲の切り替わりを正確に検出するのが難しい。 一番簡単なのは無音検出だろうか。現在の再生位置が曲の終わり付近になった場合やSpotifyアプリから来る「曲が変わった」イベントの付近で(これらはある程度正確と考えられる)無音になったら「切り替わり」と判定するか。曲の中には終わったと見せかけて再度始まるものもあるが、さすがに終わりの数秒間ではなさそうだ。だが、今と次の曲がどちらもメドレーとかライブとか雑音が多くて、無音部分がなかったら駄目だ。。。

無音を検出すること自体は可能だが、ちょっと重い。そもそもそういうプログラムが要る(探せばあるだろう)。無音がない場合にも対応するには、音の特徴を分析するのだろうか。それは結構重い・・・

時間を戻すのは、不可能に思えて実は可能だw 戻す期間を数秒間に制限すれば、再生する波形をその時間分格納し、古いものから順に再生するようにすれば(ところてんのような感じ)、その数秒間は戻すことができる(正確には、Spotifyが再生したと思っている音はまだ再生されていないから、出てしまったはずの音(= 過去の音)を操作することができる)。問題は、再生に常にその数秒間の遅延が生じることだ。再生ボタンを押してから数秒待たないと音が出ないし、停めても数秒間は音が出る(こっちは何とかできる気がする)。それが許容できるだろうか。

一方、今日は問題が全然起こらなくなってしまって、「もしかしたら、いじっているうちに直ったのか?」と、淡い期待をするのだが、実際にはそんなことはないのは、今までの経験から明らかだw でも、滅多に起こらないことは確かなので、わざわざ上に書いたような大掛かりなものを作って苦労しなくてもいいような気がしている。ただ、技術的な興味から、やってみたい気分はある。

そして思ったのは、(昔の)日本にはこういう物好きな技術者が多く、それだけならまだいいが、お客や経営者が無理難題を吹っかけたにも関わらず、(24時間戦うw)彼らによって実現されてしまった製品を素直に販売してしまったから、ガラパゴス化してしまったのかも知れないということだ。

(10/5 21:28)

  •   0
  •   2

コメントを書く

名前    

メール 

URL