デシベルのすすめ

画面写真をクリックするとWebGLビルドに飛びます。

こんにちは。技術部平山です。

今日は、デシベル(db)のお話です。どうでしょう?使い慣れてますか?

使い慣れている方は読む必要がないお話です。

Unity屋にとっての音量

Unity上で音量を調整しようとすれば、 AudioSource.volumeに0から1の値を入れます。

0だと無音で、1だと元のデータそのまま、あとは小さいほど音が小さい、というだけの話です。 何もわかりにくさはありません。

また、もう少し詳しいプログラマであれば、 サウンドデータが波を表す値の配列であることを知っているかもしれません。 例えば16bit値で波を表していれば、 -32768から32767までの値がズラッと並んだものがサウンドデータです。 それにAudioSource.volumeの0から1の値が掛け算されて、 スピーカーから出てきます。0.5なら半分にされますし、0.1なら1/10です。

わかりやすいですね。何か問題なんですか?

サウンド屋さんにとっての音量

実は、サウンドのプロの方々は、音量を表すのに 「デシベル(db)」という単位を使っています。 10倍大きいと20増える、というもので、 例えばデシベルが-6だと、音量が半分になります。

元の値 デシベル
1 0
0.5 -6
0.1 -20
0.01 -40
0.001 -60

「何それわかりにくい」

と思いますよね?0.5で何が悪いの?

でも、音のプロにとっては、デシベルの方がずっと使いやすいのです。何故か?

理由は、人間の聴覚特性にあります。

人間の聴覚特性

人間の感覚は多くの場合対数的です。

1000円のランチが500円引きなのと、 100万円の車が500円引きなのと、 違いは同じ500円です。

でも、100万円が99万9500円になっても、 全然うれしくないですよね? 1000円なら半額で50%ですが、 100万円なら99.95%ですから、 比率で見れば全然ありがたみがありません。

このように、人は多くの場合差ではなく比率で見ます。 これは聴覚に関しても同じです。

音のデータの大きさが1から0.1になるのと、 0.1から0.01になるのは、同じような変化と感じます。 両方とも1/10ですからね。 前者は0.9も減っていて、後者は0.09しか減ってないのにです。

スライダー問題

音量の調節をスライダーである場合には、特にこれが問題になります。 0から1のスライダーがあると考えてください。

f:id:hirasho0:20190626104516p:plain

左端が0で右端が1だとします。 1から0.1に落とした所で、「今下げた分くらいもう一回下げたい」 と思ったとします。もう一回1/10にすればいいので、 0.01にすれば良さそうです。 しかし、調整に使える幅はさっきの1/10しかありません。 さらにもう一回1/10にしようと思うと、 もう1/100しか幅がないので、微調整はほとんど無理です。

このように、 「右の方はかなりいじってもあまり変わらないのに、 左の方はわずかに動かしただけで派手に変わってしまう」 ということが起きます。

しかし、スライダーで調整する時には、 「どこであれ1cm動かした時の変化は同じであってほしい」 のです。デシベルにすれば、かなりそれに近づきます。

f:id:hirasho0:20190626104508p:plainf:id:hirasho0:20190626104511p:plain

第二のスライダーはデシベルで設定するためのものです。 まず0から-12に下げ、次に-24まで下げたとします。 どちらも差は12ですが、 それに対応する元の音量は、「1から0.25」と「0.25から0.625」 です。差は0.75と0.1875で、全然違いますが、 デシベルにすれば同じ幅になるのです。 是非サンプル でいじってみてください。

これが、デシベルが広く使われている理由です。 ほぼほぼ「整数2桁」で調整ができます。

一般的に、調整幅が広い場合には、 デシベルのように対数化した値で調整できるようにします。 そうでないと、0.0001から1、というように調整幅の桁数が 多くなって調整が難しくなります。

例えばグラフィックスでも同じことが起こり、 光の明るさなんかは対数で設定できた方が良いでしょう。 星の明るさと昼の太陽の明るさでは100億倍違ったりします (wikipediaの「等級」のページ参考) 。

「整数2桁」の使いやすさ

音量に限らず、大抵の調整は「整数2桁」でできるようにしておくと便利です。 人間が違いを認識できる段階はそれくらいでしょう。

例えば、敵の攻撃力なんかは、 序盤は5とか10で、終盤は1万、みたいなことになりますよね? 序盤は1の価値が凄まじく大きいですが、終盤は1どころか100 変えても大差ありません。

もしこれをスライダーにして調整させれば、 音量と同じく使いにくい状態になるでしょう。 こういう場合も対数にすればスライダーで調整しやすくなるのかもしれませんね。

実装

相互変換

まず0から1の値とデシベルの相互変換が必要です。 0から1の値にも名前が欲しいので、仮に「リニア値」 と呼んでおきましょうか。

public static float ToDecibel(float linear, float dbMin)
{
    var decibel = dbMin;
    if (linear > 0f)
    {
        decibel = 20f * Mathf.Log10(linear);
        decibel = Mathf.Max(decibel, dbMin);
    }
    return decibel;
}

public static float FromDecibel(float decibel)
{
    return Mathf.Pow(10f, decibel / 20f);
}

リニア値は、10を底とする対数(Log10)を取り、 20を掛ければデシベルになります。 例えば0.1であれば、対数を取ると-1です。 これに20を掛けて-20となります。

この際、Log10(0)はマイナス無限になるため、 一定以上小さい値は最低値に固定してしまいます。 「どれくらいで無音とみなすか」がコード中のdbMinで、 -80あたりが良いのではないでしょうか。

サウンドデータが16bitであれば、-32768から32767で、 -80dbは1万分の1ですから、-3から3までの幅に落ちます。 ほぼ無音と言えるでしょう。 もし完全に0に落としたいのであれば、-90とか-100とかにしても 良いのかもしれません。

幅が狭い方がスライダーの調整はしやすいのですが、 -60くらいだと、スピーカーの音量が大きい時に微妙に聴こえてしまいます。 私が試した限りではありますが、-80で良い気がします。

見える所を全部デシベルにする

あとは、データの運用の話です。 データのパイプラインのどこをデシベル、どこをリニア値にするか、 という話なのですが、私の現状の結論は可能な限りデシベルです。

すなわち、AudioSource.volumeに値を渡す寸前に FromDecibel()を使ってリニア値に変換し、 逆にAudioSource.volumeを外に出す時には、 すぐにToDecibel()を使ってデシベルにしてしまいます。

例えばBGMの音量を設定する関数、 みたいなのを作った場合はデシベルを受け取るようにし、 お客さんが音量調節できるスライダーを作った場合は、それもデシベルにします。 それを端末に保存する時もデシベルで、 効果音の音量をエクセルやスプレッドシートで設定する時もデシベルです。 AudioSourceを直接いじるプログラマ以外にはデシベルしか見えない という状態にします。

複数の設定値の合成

設定値が複数重なった場合は、デシベルを加算します。

例えばある音素材のデフォルトの音量が-6dbで、 あるシーンでそれを-12dbして鳴らす設定で、 さらにお客さんが全体の音量を-6dbと設定しているのであれば、 全部足して-24dbとなります。

これは、リニア値であれば掛け算です。元データが0.5(=-6db)、 シーンの設定が0.25(=-12db)、お客さんが0.5(=-6db)を設定していれば、 全部掛けて0.0625とします。これはデシベルで言えば-24dbでして、 計算結果は同じになります。デシベルの方が足し算で済んで簡単だと思いませんか?

スライダーを作る時に一工夫

デシベルのスライダーを作るのは簡単で、 左端で-80、右端で0、となるようなスライダーを用意すれば済みます。

ただ、-40、つまり1/100以下の領域を微妙に調整したいことは あんまりありません。すごく小さい音から無音の間って、 そんなに調整細かくやらなくてもいいですよね?

「細かく調整したい場所にスライダーの長さを割く」 ことが使いやすさにつながるわけで、 -80から0まで均等に使ってしまうと、-80から-40までという あんまり必要ない場所にかなりの長さが割かれてしまいます。

そこで、もう一工夫してはいかがでしょうか。

とりあえず二乗

-80から-40までの幅よりも、 -40から0までの幅の方が大きくなるように、 さらに変換関数を挟みます。 いろんな方法がありますが、「二乗する」は一番簡単でしょう。

今、-80dbで0、0dbで1である、としましょう。 例えば、-40dbであれば真ん中ですから0.5ですね。 これをこのまま使わず、二乗して0.25にしてからスライダーの位置とします。

どうでしょう?

-40dbで0.25ということは、-80dbから-40dbまでがスライダーの1/4の長さしか 使っていないということです。残りの3/4は -40dbから0dbに使われます。 細かく調整したい上の方により長い距離を使えるので、調整がしやすくなるのです。

f:id:hirasho0:20190626104519p:plain

コードはこんな感じでしょう。

static float DbToSliderPos(float db)
{
    float pos = (db + 80f) / 80f; //[-80,0]→[0,1]
    return pos * pos; // 二乗
}

逆に、スライダーからデシベルにする時には、 2乗の逆関数、つまり平方根を取ります。 コードにすれば、こんな感じです。

static float SliderPosToDb(float pos)
{
    pos = Mathf.Sqrt(pos);
    float db = (pos * 80f) - 80f; //[0,1]→[-80,0]
    return db;
}

お客さんに調整してもらうUIであれば、 これくらいが丁度いいんじゃないかなと思います。

実際、サウンドのプロの方が使うCUBASE 等のサウンド編集ソフトでも、 音量調節のスライダーは下の方が狭くなっています。 こういう工夫はあってもいいのではないでしょうか。

終わりに

このへんの整備は、 データの量産が始まる前にやっておいた方がいいと思います。

開発序盤は音量のことなんか気にせずにダミーデータをテキトーに 鳴らしていて、開発中盤になってからサウンドが合流、 ということは結構ありますよね。

そうなると、サウンド素人なプログラマが サウンド絡みの処理を作り終わってデータ入力パイプラインの 設計まで終わった後に、サウンドの専門家が参加することになります。 効果音のパラメータのスプレッドシートやjson等々も 全部リニア値で指定、みたいなことになっていると、 サウンドの人がまともな調整をするのはかなり大変になります。 「セリフしゃべってる間はBGMを-8dbで」 とかサウンドさんが言っても、それをいちいちリニア値に変換しないと いけなくなってしまうのです。

実は東京プリズンの開発がそうでした。 かなり後になって慌ててサウンドさん向けに デシベルでデバグ用の音量グラフをつけたり、 お客さん用のオプション画面のスライダーを デシベルベースに直したりしたのです(しかも2乗の工夫はしてなかった)。 そして、入力データはもはやデシベルにはできませんでした。

もし、同じように入力データ側をいじれない状況になってしまっていても、 できることはあります。 お客さん用の音量調節UIや、サウンドさん向けの音量デシベル表示くらいは、 後からでもできるはずです。 やっておいて損はないと思いますよ。