一、引言:"音量"这件简单事,背后有多复杂?
用户眼里的"音量控制"只是一根滑块,上下滑动而已。但 Android 系统内部的音量控制,藏着出乎意料的复杂度:
- 为什么插入耳机前调的音量,拔掉耳机后扬声器音量没变?(设备独立音量)
- 为什么调节音量时,有时候调的是铃声,有时候调的是媒体音量?(Stream 音量选择)
- 为什么蓝牙耳机有时候是在手机端调音量,有时候是在耳机端调?(绝对音量 vs 本地衰减)
- 为什么同样是"10格音量",实际响度差不多,而不是成倍递增?(对数音量曲线)
每一个问题背后,都是一套精心设计的子系统。本篇将揭开 Android 音量控制的全貌,从用户按下音量键到硬件寄存器写入,完整追踪这段旅程。
二、音量控制三层架构
Android 的音量系统由三个独立的层次组成,每层有各自的职责和存储:

2.1 Master Volume(全局音量)
Master Volume 是系统范围的全局音量系数,作用于所有音频输出。它的值域是 0.0f ~ 1.0f,默认为 1.0f(即不衰减)。
java
// AudioManager API
audioManager.setMasterVolume(0.8f, 0); // 设置全局音量为80%
audioManager.setMasterMute(true, 0); // 全局静音(不改变音量值)
float masterVolume = audioManager.getMasterVolume();
与 Stream Volume 的关系 :最终 PCM 数据的增益 = masterGain × streamGain × deviceGain。Master Volume 相当于给所有流加了一个总衰减器。在大多数设备上,Master Volume 被设置为固定的 1.0f,实际音量完全由 Stream Volume 和设备增益控制。
2.2 Stream Volume(流类型音量)
这是用户日常接触最多的音量层。每种音频类型(Stream)有各自独立的音量范围和当前值:
| Stream 类型 | 值 | 最大格数 | 典型使用场景 |
|---|---|---|---|
STREAM_VOICE_CALL |
0 | 5 | 电话通话 |
STREAM_SYSTEM |
1 | 7 | 系统声音(按键音) |
STREAM_RING |
2 | 7 | 来电铃声 |
STREAM_MUSIC |
3 | 15 | 媒体播放(音乐/视频) |
STREAM_ALARM |
4 | 7 | 闹钟 |
STREAM_NOTIFICATION |
5 | 7 | 通知声音 |
STREAM_DTMF |
8 | 15 | 拨号按键音 |
注意 STREAM_MUSIC 有 15 格(最精细),而铃声/闹钟类只有 7 格------因为音乐需要更细腻的调节,而铃声不需要那么精确。
Android 10+ VolumeGroup:新版 Android 引入了 VolumeGroup 概念,将相关 Stream 归为一组共享音量:
xml
<!-- audio_policy_engine_configuration.xml -->
<VolumeGroups>
<VolumeGroup name="music" indexMin="0" indexMax="15">
<!-- MUSIC、GAME 共享同一音量组 -->
<VolumeGroupAttributes streamType="AUDIO_STREAM_MUSIC"/>
<VolumeGroupAttributes streamType="AUDIO_STREAM_GAME"/>
</VolumeGroup>
<VolumeGroup name="ring" indexMin="0" indexMax="7">
<VolumeGroupAttributes streamType="AUDIO_STREAM_RING"/>
<VolumeGroupAttributes streamType="AUDIO_STREAM_NOTIFICATION"/>
</VolumeGroup>
</VolumeGroups>
2.3 Device Volume(设备音量)
这是最容易被忽略却极其重要的一层:每个设备对于每个 Stream,独立存储各自的音量。
ini
Settings.System 中的存储键名示例:
volume_music_speaker = 10 // 扬声器上的媒体音量
volume_music_headset = 13 // 有线耳机上的媒体音量
volume_music_bt_a2dp = 8 // 蓝牙耳机上的媒体音量
volume_ring_speaker = 5 // 扬声器上的铃声音量
volume_ring_headset = 5 // 有线耳机上的铃声音量
当用户插入耳机时,AudioService 从 Settings.System 读取 volume_music_headset 并应用;拔出耳机后,回读 volume_music_speaker------这就是"设备音量记忆"功能的实现原理。
java
// AudioService.java 中的设备音量恢复逻辑(简化版)
private void onAudioDeviceChange(int device, boolean connected) {
if (connected) {
// 从 Settings 恢复该设备上次的音量
int savedIndex = Settings.System.getInt(mContentResolver,
getVolumeSettingKeyForDevice(STREAM_MUSIC, device),
getDefaultVolumeIndex(STREAM_MUSIC));
setStreamVolumeInt(STREAM_MUSIC, savedIndex, device, false);
}
}
三、音量曲线映射:为什么不用线性?
3.1 人耳的对数感知
人类听觉遵循斯蒂文斯幂定律:感知响度与声压级的对数成正比。简单说,声压加倍(+6dB)才让人感觉声音"明显变响",而不是简单的成比例增加。
如果用线性映射(index 8 = 50% 物理能量),那么:
- 1格到2格:听起来有很大变化
- 14格到15格:几乎感觉不到变化
用对数(dB)映射 后,每格的感知响度变化趋于均匀,用户体验更好。
3.2 Android 的音量曲线
Android 使用分段曲线,在 audio_policy_engine_configuration.xml 中定义:
xml
<VolumeCurve>
<!-- 曲线点:(index_percent, volume_dBFS) -->
<!-- index_percent: 0=最小, 100=最大 -->
<!-- volume_dBFS: 0=满幅, 负值=衰减, -∞=静音 -->
<Point>0,-9600</Point> <!-- 0格 → -96dB(实际静音) -->
<Point>1,-5800</Point> <!-- 1格 → -58dB(刚好有声音)-->
<Point>33,-3300</Point> <!-- 5格 → -33dB(适中音量) -->
<Point>66,-1700</Point> <!-- 10格 → -17dB(偏高音量) -->
<Point>100,0</Point> <!-- 15格 → 0dB(最大,无衰减)-->
</VolumeCurve>
从曲线点可以看出:
- 低档(0~33%):dB 值变化陡峭,每格变化量大(低音量区精细控制)
- 高档(66~100%):dB 值变化平缓,接近最大(高音量区相对粗粒)
3.3 index → dB → 线性增益的转换
完整的转换链如下:
scss
音量 index(0~15)
↓ 查 VolumeCurve 插值
dBFS 值(如 -17.0)
↓ gain = pow(10, dBFS / 20)
线性增益(如 0.141)
↓ PCM 数据 × 线性增益
最终 PCM 输出(音量衰减后的信号)
AudioFlinger 中的实际代码:
cpp
// frameworks/av/services/audioflinger/AudioMixer.cpp
// 将 dB 值转换为线性增益系数
static inline float db_to_amplitude_ratio(float decibels)
{
return exp(decibels * (float)(M_LN10 / 20.));
// 等价于 pow(10, decibels / 20),但 exp 更快
}
// 混音时应用音量
void AudioMixer::process__TwoTracks16BitsStereoNoResampling(state_t* state)
{
// ...
int16_t *in = (int16_t *)t.buffers;
int32_t vl = t.volume[0]; // 从共享内存读取的 Q4.12 格式音量
int32_t vr = t.volume[1];
// 乘以音量系数(使用定点数乘法优化)
*out++ += (vl * in[0]) >> 12;
*out++ += (vr * in[1]) >> 12;
}
AudioFlinger 使用 Q4.12 定点数格式存储音量(1.0f = 4096),避免了浮点运算开销。
四、音量调节完整流程
当用户按下音量键,到硬件实际调音,中间经历了以下完整链路:

4.1 第一步:PhoneWindowManager 接收按键事件
java
// frameworks/base/services/core/java/com/android/server/policy/PhoneWindowManager.java
@Override
public long interceptKeyBeforeDispatching(IBinder focusedToken, KeyEvent event, int policyFlags) {
final int keyCode = event.getKeyCode();
if (keyCode == KeyEvent.KEYCODE_VOLUME_UP || keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) {
// 通知 AudioService 处理音量变化
mAudioManagerInternal.adjustSuggestedStreamVolumeForUid(
AudioManager.USE_DEFAULT_STREAM_TYPE,
keyCode == KEYCODE_VOLUME_UP ? AudioManager.ADJUST_RAISE
: AudioManager.ADJUST_LOWER,
AudioManager.FLAG_SHOW_UI, // 显示音量调节 UI
callingPackage, uid, pid, targetSdkVersion);
}
}
"建议 Stream 类型"的选择逻辑:
- 如果当前有媒体在播放 → 选
STREAM_MUSIC - 如果有来电 → 选
STREAM_RING - 通话中 → 选
STREAM_VOICE_CALL - 其他 → 选
STREAM_RING(默认)
这就是"为什么有时候按音量键调的是铃声,有时候调的是媒体音量"------系统根据当前活跃状态自动判断。
4.2 第二步:AudioService 计算新 Index
java
// frameworks/base/services/core/java/com/android/server/audio/AudioService.java
public void adjustStreamVolume(int streamType, int direction, int flags,
String callingPackage, String caller, int uid, ...) {
// 1. 获取当前音量 index
int index = mStreamStates[streamType].getIndex(device);
// 2. 按方向调整 ±1
int newIndex = index + direction; // direction: +1=RAISE, -1=LOWER
// 3. 检查边界(不超过 maxIndex,不低于 0)
newIndex = Math.min(Math.max(newIndex, 0),
mStreamStates[streamType].getMaxIndex());
// 4. 检查 DND 模式(来电铃声可能被限制)
if (isDndEnabled() && streamType == STREAM_RING) {
// DND 开启时铃声静音
return;
}
// 5. 检查安全音量警告
if (checkSafeMediaVolume(streamType, newIndex, device)) {
// 弹出警告对话框
return;
}
// 6. 应用新音量
setStreamVolumeInt(streamType, newIndex, device, true);
}
4.3 第三步:setStreamVolumeInt 持久化并通知
java
private void setStreamVolumeInt(int streamType, int index, int device, boolean force) {
// 1. 更新内存中的音量状态
mStreamStates[streamType].setIndex(index, device, TAG);
// 2. 持久化到 Settings.System
sendMsg(mAudioHandler, MSG_PERSIST_VOLUME, SENDMSG_QUEUE, streamType, 0,
mStreamStates[streamType], PERSIST_DELAY);
// 3. 通知 AudioFlinger 更新实际音量
setStreamVolumeIndex(streamType, device, index);
}
4.4 第四步:AudioFlinger 更新增益
java
// AudioFlinger 通过 IAudioFlinger Binder 接口接收音量设置
// AudioFlinger.cpp
status_t AudioFlinger::setStreamVolume(audio_stream_type_t stream, float value,
audio_io_handle_t output)
{
// 更新所有使用该 stream 的 Track 的音量
for (size_t i = 0; i < mPlaybackThreads.size(); i++) {
PlaybackThread *thread = mPlaybackThreads.valueAt(i);
thread->setStreamVolume(stream, value);
}
return NO_ERROR;
}
在混音线程的 threadLoop() 中,AudioMixer 会读取最新的音量值,对 PCM 数据做实时增益处理。
4.5 第五步:AudioHAL 写入硬件寄存器
对于需要硬件级别音量控制的情况(如 USB Audio、某些 BT 设备),AudioFlinger 还会调用 HAL 接口:
cpp
// hardware/interfaces/audio/aidl/android/hardware/audio/core/IStreamOut.aidl
interface IStreamOut {
// Android 15 AIDL 接口
void setVolume(float left, float right);
}
HAL 实现将这个调用转换为实际的 ALSA 控件写入或 Codec 寄存器操作。
五、特殊音量控制机制
5.1 绝对音量(Absolute Volume)
普通音量调节方式:本地衰减------手机端将 PCM 数据乘以增益系数,发送到蓝牙耳机的是已经衰减的信号。
绝对音量方式:远程控制------手机以满幅信号发送,同时通过 AVRCP(音频/视频遥控协议)命令通知耳机调整自身的硬件音量。
java
// AudioService.java 中的绝对音量处理
private void setAbsoluteVolumeForA2dp(int index) {
if (isAbsoluteVolumeDevice(device)) {
// 将 Android 音量 index 映射到 AVRCP 音量(0-127)
int avrcpVolume = (int) Math.round(index * 127.0 / maxIndex);
mA2dpService.setAvrcpAbsoluteVolume(avrcpVolume);
// 注意:此时本地增益不做任何衰减(保持满幅)
}
}
优点:耳机硬件 DAC 以满信号工作,SNR(信噪比)最优,音质更好。
为什么会有"绝对音量不好用"的抱怨?有些蓝牙耳机的音量响应不均匀(1格到2格变化很大,14格到15格几乎无变化),这是耳机厂商实现问题,与 Android 无关。
可以通过开发者选项关闭绝对音量(调试用):
bash
adb shell settings put system bluetooth_absolute_volume 0
5.2 DND 勿扰模式(Do Not Disturb)
DND 模式通过 NotificationManager 管理,对音量系统的影响主要在铃声和通知:
java
// DND 影响的主要是 STREAM_RING 和 STREAM_NOTIFICATION
// AudioService.java 中检查 DND
private boolean checkRingerModeForDnd(int stream, int flags) {
int ringerMode = mRingerMode;
// 静音模式:所有铃声/通知静音
if (ringerMode == AudioManager.RINGER_MODE_SILENT) {
return false; // 不允许调节这些 stream
}
// 振动模式:铃声静音但允许调节其他 stream
if (ringerMode == AudioManager.RINGER_MODE_VIBRATE
&& stream == AudioManager.STREAM_RING) {
return false;
}
return true;
}
三种 RingerMode 对音量的影响:
| RingerMode | STREAM_RING | STREAM_NOTIFICATION | STREAM_MUSIC |
|---|---|---|---|
NORMAL |
正常播放 | 正常播放 | 不受影响 |
VIBRATE |
静音+震动 | 静音 | 不受影响 |
SILENT |
静音 | 静音 | 不受影响 |
媒体音量(STREAM_MUSIC)不受 DND/RingerMode 影响------这是设计决定,避免 DND 打断用户正在播放的音乐。
5.3 安全音量(Safe Volume)
欧盟有法规要求:使用耳机超过一小时后,若音量超过 85dB(等效),设备需要发出警告。Android 遵循此规范,在 AudioService 中实现了安全音量保护:
java
// AudioService.java
private boolean checkSafeMediaVolume(int streamType, int index, int device) {
// 仅对 STREAM_MUSIC 且使用耳机时检查
if (streamType != STREAM_MUSIC) return false;
if (!isHeadsetDevice(device)) return false;
// 检查是否超过安全音量阈值(通常是最大音量的80%左右)
if (index > mSafeMediaVolumeIndex) {
// 已经在高音量区使用超过20分钟
if (mSafeMediaVolumeState == SAFE_MEDIA_VOLUME_ACTIVE) {
// 弹出警告对话框,让用户确认
sendMsg(mAudioHandler, MSG_PERSIST_SAFE_VOLUME_STATE, SENDMSG_QUEUE, 0, 0, null, 0);
return true; // 阻止音量继续升高
}
}
return false;
}
安全音量警告弹出后,用户点击"确认"后系统允许继续提升,但会重新开始计时。注意:此功能只有在设备满足欧盟CTA-2034标准时才会启用,许多国内定制ROM会禁用此功能("去掉恼人的音量限制"------虽然有点破坏耳朵健康保护)。
5.4 通话音量的独立性
STREAM_VOICE_CALL 的音量调节路径与其他 Stream 不同:
java
// 通话音量调节最终调用 TelecomService
// TelecomService 通过 InCallService 控制通话音量
// 最终影响的是 IN_CALL 模式下的 earpiece/headset 硬件音量
// 关键区别:通话音量不通过 AudioFlinger 的软件增益
// 而是直接通过 HAL setVoiceVolume() 控制硬件 Codec
status_t AudioFlinger::setVoiceVolume(float value)
{
// 直接调用 HAL 设置通话语音音量
// 这条路径不经过混音器,直接到硬件
AutoMutex lock(mHardwareLock);
mPrimaryHardwareDev->setVoiceVolume(value);
return NO_ERROR;
}
这也是为什么通话音量按键调的是听筒/耳机的硬件音量,而不是软件混音的增益------这样可以保证语音质量不受软件处理影响。
六、VolumeShaper:平滑音量过渡
Android 7.0 引入的 VolumeShaper 允许以动画曲线方式平滑改变音量,避免突变带来的"咔哒"声:
6.1 基本概念
kotlin
// VolumeShaper 定义了一条时间→音量的曲线
val duckConfig = VolumeShaper.Configuration.Builder()
.setInterpolatorType(
VolumeShaper.Configuration.INTERPOLATOR_TYPE_CUBIC_MONOTONIC // 平滑曲线
)
.setOptionFlags(VolumeShaper.Configuration.OPTION_FLAG_VOLUME_IN_DBFS)
.setDuration(500L) // 过渡时间 500ms
.setCurve(
floatArrayOf(0f, 1f), // 时间点:0%和100%
floatArrayOf(0f, -12f) // 音量:0dBFS → -12dBFS (降低到约25%)
)
.build()
6.2 实战:Duck 和 Unduck
kotlin
class SmoothVolumeController(private val mediaPlayer: MediaPlayer) {
private var duckShaper: VolumeShaper? = null
private var unduckShaper: VolumeShaper? = null
private val duckConfig = VolumeShaper.Configuration.Builder()
.setInterpolatorType(VolumeShaper.Configuration.INTERPOLATOR_TYPE_CUBIC_MONOTONIC)
.setDuration(300L)
.setCurve(floatArrayOf(0f, 1f), floatArrayOf(0f, -12f)) // → -12dBFS
.build()
private val unduckConfig = VolumeShaper.Configuration.Builder()
.setInterpolatorType(VolumeShaper.Configuration.INTERPOLATOR_TYPE_CUBIC_MONOTONIC)
.setDuration(300L)
.setCurve(floatArrayOf(0f, 1f), floatArrayOf(-12f, 0f)) // -12dBFS → 0
.build()
fun duck() {
duckShaper = mediaPlayer.createVolumeShaper(duckConfig)
duckShaper?.apply(VolumeShaper.Operation.PLAY) // 开始渐变
}
fun unduck() {
// 方法一:反向播放 duck 曲线
duckShaper?.apply(VolumeShaper.Operation.REVERSE)
// 方法二:用新的 unduck 曲线
unduckShaper = mediaPlayer.createVolumeShaper(unduckConfig)
unduckShaper?.apply(VolumeShaper.Operation.PLAY)
}
fun fadeIn(durationMs: Long = 500L) {
val fadeConfig = VolumeShaper.Configuration.Builder()
.setInterpolatorType(VolumeShaper.Configuration.INTERPOLATOR_TYPE_CUBIC_MONOTONIC)
.setDuration(durationMs)
.setCurve(floatArrayOf(0f, 1f), floatArrayOf(-80f, 0f)) // 淡入
.build()
mediaPlayer.createVolumeShaper(fadeConfig)
.apply(VolumeShaper.Operation.PLAY)
}
fun fadeOut(durationMs: Long = 500L, onComplete: () -> Unit = {}) {
val fadeConfig = VolumeShaper.Configuration.Builder()
.setInterpolatorType(VolumeShaper.Configuration.INTERPOLATOR_TYPE_CUBIC_MONOTONIC)
.setDuration(durationMs)
.setCurve(floatArrayOf(0f, 1f), floatArrayOf(0f, -80f)) // 淡出
.build()
mediaPlayer.createVolumeShaper(fadeConfig)
.apply(VolumeShaper.Operation.PLAY)
// 淡出完成后的回调(手动计算时机)
Handler(Looper.getMainLooper()).postDelayed({
onComplete()
}, durationMs)
}
}
适用场景:
- 切歌时的淡入淡出
- 导航语音时 duck 背景音乐
- 视频开始/结束的音量过渡
七、Android 15 的新变化
7.1 VolumeGroup 的完善
Android 15 完成了 VolumeGroup 从 AudioPolicy 到 AudioService 的完整打通。现在可以通过 VolumeGroup ID 直接操作音量,而不必操心具体的 Stream 类型:
java
// Android 15: VolumeGroupInfo API
AudioVolumeGroupChangeHandler groupHandler = new AudioVolumeGroupChangeHandler() {
@Override
public void onAudioVolumeGroupChanged(int group, int flags) {
// 某个音量组的音量发生变化
AudioVolumeGroup volumeGroup = getAudioVolumeGroup(group);
Log.d("Volume", "Group " + volumeGroup.name() + " changed");
}
};
audioManager.registerVolumeGroupCallback(executor, groupHandler);
7.2 空间音频的独立音量管理
Android 15 对空间化音频(Spatialized Audio)添加了独立的音量管理:
java
// 空间音频使用独立的 VolumeGroup "spatialized"
// 其音量曲线针对头戴式设备优化(更低的最大dB,保护听力)
if (isSpatializedAudio && isHeadsetDevice(device)) {
applyVolumeCurve(SPATIALIZED_VOLUME_CURVE); // 使用专用曲线
} else {
applyVolumeCurve(DEFAULT_MEDIA_VOLUME_CURVE);
}
7.3 音量变化广播的改进
Android 15 对 ACTION_VOLUME_CHANGED 广播增加了更多信息,方便 App 做精确处理:
java
// Android 15 中 EXTRA_VOLUME_STREAM_VALUE 现在包含设备信息
intent.putExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, streamType);
intent.putExtra(AudioManager.EXTRA_VOLUME_STREAM_VALUE, index);
intent.putExtra(AudioManager.EXTRA_PREV_VOLUME_STREAM_VALUE, prevIndex);
// Android 15 新增:
intent.putExtra(AudioManager.EXTRA_OUTPUT_DEVICE_INFO, deviceInfo);
八、实战案例
8.1 监听音量变化
kotlin
class VolumeObserver(private val context: Context) {
private val contentObserver = object : ContentObserver(Handler(Looper.getMainLooper())) {
override fun onChange(selfChange: Boolean) {
val audioManager = context.getSystemService(AudioManager::class.java)
val currentVolume = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC)
val maxVolume = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC)
val volumePercent = currentVolume * 100 / maxVolume
Log.d("Volume", "媒体音量: $currentVolume/$maxVolume ($volumePercent%)")
}
}
// 方法一:ContentObserver(监听 Settings 变化,有延迟)
fun startObserving() {
context.contentResolver.registerContentObserver(
Settings.System.CONTENT_URI,
true,
contentObserver
)
}
// 方法二:BroadcastReceiver(更直接,推荐)
private val volumeReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (intent.action == "android.media.VOLUME_CHANGED_ACTION") {
val streamType = intent.getIntExtra(
AudioManager.EXTRA_VOLUME_STREAM_TYPE, -1)
val newVolume = intent.getIntExtra(
AudioManager.EXTRA_VOLUME_STREAM_VALUE, -1)
Log.d("Volume", "Stream $streamType 音量变为 $newVolume")
}
}
}
fun registerReceiver() {
context.registerReceiver(
volumeReceiver,
IntentFilter("android.media.VOLUME_CHANGED_ACTION")
)
}
fun unregister() {
context.contentResolver.unregisterContentObserver(contentObserver)
try { context.unregisterReceiver(volumeReceiver) } catch (e: Exception) {}
}
}
8.2 正确设置音量
kotlin
class VolumeManager(private val context: Context) {
private val audioManager = context.getSystemService(AudioManager::class.java)
// 设置绝对音量值(0到最大)
fun setMusicVolume(index: Int) {
val maxIndex = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC)
val safeIndex = index.coerceIn(0, maxIndex)
audioManager.setStreamVolume(
AudioManager.STREAM_MUSIC,
safeIndex,
AudioManager.FLAG_SHOW_UI // 显示系统音量条UI
)
}
// 设置百分比音量(0.0 ~ 1.0)
fun setMusicVolumePercent(percent: Float) {
val maxIndex = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC)
val index = (percent * maxIndex).toInt().coerceIn(0, maxIndex)
audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, index, 0)
}
// 获取当前音量百分比
fun getMusicVolumePercent(): Float {
val current = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC)
val max = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC)
return current.toFloat() / max
}
// 增减音量(相对调节)
fun adjustVolume(direction: Int) {
// direction: AudioManager.ADJUST_RAISE / ADJUST_LOWER / ADJUST_SAME
audioManager.adjustStreamVolume(
AudioManager.STREAM_MUSIC,
direction,
AudioManager.FLAG_SHOW_UI
)
}
// 静音/取消静音
fun setMuted(muted: Boolean) {
// Android 6+: 使用 adjustStreamVolume 代替已废弃的 setStreamMute
audioManager.adjustStreamVolume(
AudioManager.STREAM_MUSIC,
if (muted) AudioManager.ADJUST_MUTE else AudioManager.ADJUST_UNMUTE,
0
)
}
fun isMuted(): Boolean =
audioManager.isStreamMute(AudioManager.STREAM_MUSIC)
}
8.3 检测当前设备音量能力
kotlin
fun logAudioVolumeInfo(audioManager: AudioManager) {
// 枚举所有 Stream 的音量信息
val streams = listOf(
AudioManager.STREAM_MUSIC to "MUSIC",
AudioManager.STREAM_RING to "RING",
AudioManager.STREAM_ALARM to "ALARM",
AudioManager.STREAM_VOICE_CALL to "VOICE_CALL",
AudioManager.STREAM_NOTIFICATION to "NOTIFICATION"
)
for ((stream, name) in streams) {
val current = audioManager.getStreamVolume(stream)
val min = audioManager.getStreamMinVolume(stream)
val max = audioManager.getStreamMaxVolume(stream)
Log.d("Audio", "$name: $current (min=$min, max=$max), " +
"muted=${audioManager.isStreamMute(stream)}")
}
// 检查绝对音量是否生效(BT设备)
Log.d("Audio", "AbsoluteVolume enabled: " +
(Settings.System.getInt(audioManager.contentResolver,
"bluetooth_absolute_volume", 1) == 1))
}
九、调试技巧
9.1 查看当前音量状态
bash
# 查看所有 Stream 当前音量
adb shell dumpsys audio | grep -A 3 "Stream volumes"
# 典型输出:
# Stream volumes:
# - VOICE_CALL: 4 / 5 (0.800000)
# - SYSTEM: 5 / 7 (0.714286)
# - RING: 6 / 7 (0.857143)
# - MUSIC: 10 / 15 (0.666667)
# - ALARM: 6 / 7 (0.857143)
# - NOTIFICATION: 6 / 7 (0.857143)
# 查看每设备的音量存储
adb shell settings list system | grep volume_music
# 输出:
# volume_music_speaker=10
# volume_music_headset=13
# volume_music_bt_a2dp=8
# 查看 RingerMode
adb shell dumpsys audio | grep "ringer mode"
# 输出:ringer mode= 2 (NORMAL) (0=SILENT, 1=VIBRATE, 2=NORMAL)
9.2 验证音量曲线
bash
# 查看音量曲线配置(设备相关,路径可能不同)
adb shell cat /system/etc/audio_policy_engine_configuration.xml | grep -A 5 "VolumeCurve"
# 强制设置音量(绕过 DND 等限制,调试用)
adb shell media volume --set 10 --stream 3 # stream 3 = STREAM_MUSIC
9.3 追踪音量调节事件
bash
# 开启 AudioService 详细日志
adb shell setprop log.tag.AudioService V
adb logcat -s AudioService:V | grep -E "volume|Volume|setStream"
# 关键日志示例:
# AudioService: adjustStreamVolume() stream=3 direction=1 device=2
# AudioService: setStreamVolume(stream=3, index=11, device=SPEAKER)
# AudioService: persist volume: index=11 stream=3 device=2
9.4 常见问题诊断
问题 1:蓝牙耳机音量失控(总是很响或很轻)
bash
# 检查绝对音量是否开启
adb shell settings get system bluetooth_absolute_volume
# 如果是 1 且耳机不支持好的 AVRCP,可以关闭试试
adb shell settings put system bluetooth_absolute_volume 0
问题 2:音量键没有反应
bash
# 检查按键是否被应用拦截
adb shell dumpsys input | grep "VOLUME"
# 查看 PhoneWindowManager 是否在处理
adb logcat -s PhoneWindowManager:V | grep "VOLUME"
问题 3:DND 模式下媒体音量仍然能调节
bash
# DND 不影响 STREAM_MUSIC,这是正常行为
# 查看 DND 当前状态
adb shell dumpsys notification | grep "ZenMode"
十、总结
| 知识点 | 要点 |
|---|---|
| 三层架构 | Master(全局)× Stream(按类型)× Device(按设备),三层独立乘法计算 |
| 设备记忆 | 每个 Stream 每个设备独立存储在 Settings.System,切换设备自动恢复 |
| 音量曲线 | 非线性对数映射,index→dB→gain,符合人耳感知 |
| 调节链路 | 音量键→PhoneWindowManager→AudioService→AudioFlinger→HAL,全链路约5层 |
| 绝对音量 | BT A2DP 用 AVRCP 直接控制耳机硬件,本地不衰减,音质更好 |
| DND 影响 | 仅影响 RING/NOTIFICATION,不影响 MUSIC/ALARM |
| VolumeShaper | 平滑音量过渡,避免突变"咔哒"声,推荐用于淡入淡出 |
音量控制是用户每天都会接触的功能,其背后却有着精妙的工程设计------从对数感知曲线到每设备独立记忆,每个细节都是为了更好的用户体验。下一篇(也是本系列的最终篇),我们将深入 Audio HAL 与硬件接口,看看软件与硬件的边界在哪里,以及如何为新硬件开发 Audio HAL 驱动。
踩坑提示 :曾经有个需求"把视频的音量和音乐 App 分离控制",最直觉的做法是用
setStreamVolume()直接设置STREAM_MUSIC,但这会影响所有STREAM_MUSIC类型的 App!正确做法是用AudioTrack.setVolume()在 Track 级别控制单个播放器的音量,或者使用MediaPlayer.setVolume(left, right)--- 这样只影响当前播放实例,不会动 Stream 层面的全局音量。Stream 级别的音量应该由用户自己通过系统 UI 调节,不要在代码里"帮用户调"。