Androidでアプリごとの音量を変えられない理由を、実測で確かめた
「ライブ配信アプリの音だけ少し下げたい」。そう思って調べ始めたら、Androidの音の仕組みを一段深く覗くことになりました。
出力全体に増幅をかけると効くアプリと効かないアプリがあること、そして
AndroidはBluetooth機器ごとに音量を覚えていないことが分かりました。
推測ではなく、実機の dumpsys 出力で1つずつ確かめた記録です。
出力先: Bluetooth A2DP 接続の骨伝導イヤホン(OpenRun by Shokz)
adb shell dumpsys と settings list という
Android標準の診断コマンドだけです。自分の端末が今どういう状態かを表示するもので、
アプリの解析(逆コンパイル・バイナリ解析・通信の傍受)は一切行っていません。
登場するアプリはOSの状態を説明するための例で、特定のアプリや企業を批判する意図はありません。
出力に含まれるパッケージ名など、説明に不要な情報は伏せています。
1. そもそもAndroidの音量は「アプリ」に紐づいていない
Androidの音量はアプリ単位ではなく、音の役割(ストリーム)単位で管理されています。 メディア、着信音、アラーム、通知、通話。この5つ前後の枠しかありません。
これは設計思想の話です。「着信音とアラームは確実に鳴ってほしい、動画の音は自由に下げたい」という 役割ベースの要求から出発しているため、アプリという単位が最初から存在しません。 同じアプリがメディアも通知も鳴らす以上、アプリを単位にすると利用者の意図と噛み合わない、という判断です。
Windowsの音量ミキサーがアプリごとに調整できるのは、デスクトップでプロセスごとの オーディオセッションをユーザーが管理する文化があるからで、出発点が違います。
2. それなら出力全体に効果をかけてしまえばいい、と考えた
Androidには LoudnessEnhancer という音量を持ち上げるエフェクトがあります。
通常は「自分のアプリが鳴らす音」に対して使うものですが、
オーディオセッションに 0 を指定すると出力ミックス全体が対象になるとドキュメントに書かれています。
// 0 = グローバル出力ミックス
val fx = LoudnessEnhancer(0)
fx.setTargetGain(-800) // mB単位。-800 = -8dB
fx.enabled = true
ここで一つ工夫をしました。判定は「下げる方向」で行うことにしたのです。 音を大きくすると「大きくなった気がする」で済んでしまいますが、小さくなるのは聞けばすぐ分かります。 プラシーボを排除するには減衰のほうが確実です。
結果ははっきり下がりました。ただし——
3. 音楽アプリは下がったのに、配信アプリは下がらなかった
音楽アプリの音は明確に小さくなったのに、ライブ配信アプリの音はまったく変わりませんでした。 同じ端末、同じイヤホン、同じ瞬間です。なぜ片方だけ効かないのか。
adb shell dumpsys audio で再生中のプレイヤーを見ると、決定的な違いがありました。
piid:3335 type:AAudio u/pid:10330/1052 state:started
usage=USAGE_MEDIA sessionId:-1
piid:3359 type:android.media.AudioTrack u/pid:10591/17113
usage=USAGE_MEDIA sessionId:3545
| アプリ | 再生方式 | セッションID | 結果 |
|---|---|---|---|
| 音楽アプリ | AudioTrack | 3545 | 効く |
| 配信アプリ | AAudio | -1(なし) | 効かない |
セッションIDが -1。これは「エフェクトを一切通さない」という宣言です。 AAudio は低遅延再生のためのAPIで、音声処理の段をできるだけ飛ばして最短経路でスピーカーへ送ります。 ライブ配信は遅延が命なので、この選択自体はまったく合理的です。
裏付けは出力スレッドの側にもありました。dumpsys media.audio_flinger の抜粋です。
Output thread ... name AudioOut_D → 2 Effect Chains
Output thread ... name AudioOut_15 → 0 Effect Chains
Output thread ... name AudioOut_1D → 0 Effect Chains
Output thread ... name AudioOut_25 → 0 Effect Chains
Output thread ... name AudioOut_45 → 0 Effect Chains
Output thread ... name AudioOut_55 → 0 Effect Chains
出力スレッドは6本あり、エフェクトチェーンを持っているのは1本だけです。 グローバルに載せたつもりの効果は、実際にはこの1本にしか乗っていません。 低遅延パスを通るアプリの音は、その横をすり抜けていきます。
※ 上のコードは「自分の端末で試したらどうなるか」を確かめたもので、 他人が使っているアプリの音を勝手に操作する目的で使うものではありません。 結論から言えば、そうした用途にはそもそも使えません(この後で分かります)。
4. 「アプリ個別の音量」に抜け道はないのか
もう一つの候補が MediaSession です。アプリが再生セッションを公開していれば、
外部から setVolumeTo() でそのセッションの音量を操作できます。
これを確かめました。
$ adb shell dumpsys media_session
package=(音楽アプリ)
active=true volumeType=LOCAL, controlType=ABSOLUTE
package=com.android.server.telecom
active=false volumeType=LOCAL
(配信アプリは一覧に無い)
※ 出力に含まれるパッケージ名は伏せています(内容の説明に必要な部分だけを残しています)。
ここも2重に塞がっていました。
- 配信アプリはそもそも MediaSession を公開していません。掴む対象が存在しない
- 音楽アプリは公開していますが
volumeType=LOCALです。これは「音量は端末のストリーム音量で管理する」という意味で、setVolumeTo()を呼んでもメディア音量全体が動くだけ。 セッション固有の音量を持つのはREMOTE(Chromecastなどへキャスト中)のときだけです
残る手段は「他アプリの音をキャプチャして加工し、鳴らし直す」ですが、 これは相手が録音を許可している場合のみ成立し、しかも遅延と二重再生が付いてきます。実用にはなりません。
なぜメーカー製アプリはできるのか
SamsungのSound Assistantはアプリごとの音量を実現しています。あれが可能なのは、 メーカー純正で署名レベルの特権を持っているからです。 サードパーティのアプリには開放されていません。この差は埋められません。
つまり「アプリごとの音量」は、技術的にできないのではなく、サードパーティには許されていない、 が正確な答えでした。
5. では、Bluetooth機器ごとの音量は?
アプリ単位が無理なら、せめて「イヤホンを繋いだときだけ音量を変える」はどうか。 Androidは機器を判別して音量を覚えているように見えます。実際に覚えていました——ただし種類までです。
$ adb shell settings list system | grep volume_music
volume_music_speaker=0
volume_music_earpiece=110
volume_music_usb_headset=90
volume_music_bt_a2dp=90 ← Bluetoothの枠はこの1つだけ
volume_music_ble_headset=50
volume_music_bt_sco=40
スピーカー、有線、USB、Bluetoothでそれぞれ別の値を持っています。しかし Bluetoothの枠は「bt_a2dp」1つだけです。 検証した端末には6台のBluetooth機器がペアリングされていますが、全部でこの1枠を共有しています。
つまり、骨伝導イヤホン用に上げた音量が、次に別のイヤホンを繋いだときにもそのまま適用されます。 機器ごとには覚えてくれません。
6. 穴は見つけたが、先客がいた
「Androidが種類ごとまでしかやらないなら、機器ごとにやるアプリを作ればいい」—— そう考えて調べたところ、すでにありました。
Bluetooth Volume Manager(BVM)は、Bluetooth機器ごとにメディア・通話・着信・通知の音量を記憶し、 接続時に自動で復元します。無料、広告なし、オープンソース。評価3.9、レビュー約4,000件。 接続時の再生コマンド送信やアプリ自動起動まで持っています。
自分が思いついた穴は、たいてい誰かがすでに埋めています。 ここで「それでも作る」と進むのは、たいてい良い判断ではありません。 調べる前に作らなくてよかった、というのがこの日いちばんの収穫でした。
7. 結局どこまでできるのか(まとめ)
| やりたいこと | 可否 | 理由 |
|---|---|---|
| アプリごとに音量を変える | ✕ | サードパーティには権限が開放されていない |
| 端末全体の音量を増減する | △ | 低遅延再生のアプリには効かない |
| 機器ごとにシステム音量を変える | ○ | 全アプリに効く。ただし端末MAXが上限 |
| 自分のアプリの音を増減する | ◎ | 制約なし。上限も超えられる |
最後の行が、この調査で唯一そのまま使えた結論でした。
自分が鳴らす音に対してなら、機器ごとのゲイン調整は完全に自由にできます。
出力先の名前は AudioDeviceInfo.getProductName() から取得でき、
Bluetoothの権限も要りません(実際に "OpenRun by Shokz" という実機器名が取れました)。
読み上げ機能を持つアプリなら、「スピーカーでは100%、骨伝導イヤホンでは200%」といった設定が素直に作れます。 他人のアプリの音は動かせませんが、自分のアプリの音なら思いどおりです。