画面写真をクリックすると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のスライダーがあると考えてください。
左端が0で右端が1だとします。 1から0.1に落とした所で、「今下げた分くらいもう一回下げたい」 と思ったとします。もう一回1/10にすればいいので、 0.01にすれば良さそうです。 しかし、調整に使える幅はさっきの1/10しかありません。 さらにもう一回1/10にしようと思うと、 もう1/100しか幅がないので、微調整はほとんど無理です。
このように、 「右の方はかなりいじってもあまり変わらないのに、 左の方はわずかに動かしただけで派手に変わってしまう」 ということが起きます。
しかし、スライダーで調整する時には、 「どこであれ1cm動かした時の変化は同じであってほしい」 のです。デシベルにすれば、かなりそれに近づきます。


第二のスライダーはデシベルで設定するためのものです。 まず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に使われます。 細かく調整したい上の方により長い距離を使えるので、調整がしやすくなるのです。
コードはこんな感じでしょう。
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や、サウンドさん向けの音量デシベル表示くらいは、 後からでもできるはずです。 やっておいて損はないと思いますよ。