Android 闲来无事, 在ICS 中对Android 音频架构又是一通乱改,本来就调来调去的混乱逻辑变得更加飘忽不定。 本文旨在理清Android audio 架构,以volume setting 为例。
以情景分析方式入题:
首先从JNI 入手, 基本的声音调节函数
./mydroid/frameworks/base/core/jni/android_media_AudioSystem.cpp
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
static int
android_media_AudioSystem_setStreamVolumeIndex(JNIEnv *env, jobject thiz, jint stream, jint index)
{
return check_AudioSystem_Command(AudioSystem::setStreamVolumeIndex(static_cast <audio_stream_type_t>(stream), index));
}
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
调节特定码流的音量, 调到公用函数AudioSystem::setStreamVolumeIndex
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
status_t AudioSystem::setStreamVolumeIndex(audio_stream_type_t stream, int index)
{
const sp<IAudioPolicyService>& aps = AudioSystem::get_audio_policy_service();
if (aps == 0) return PERMISSION_DENIED;
return aps->setStreamVolumeIndex(stream, index);
}
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
又调到了AudioPolicyService.
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
status_t AudioPolicyService::setStreamVolumeIndex(audio_stream_type_t stream, int index)
{
if (mpAudioPolicy == NULL) {
return NO_INIT;
}
if (!checkPermission()) {
return PERMISSION_DENIED;
}
if (stream < 0 || stream >= AUDIO_STREAM_CNT) {
return BAD_VALUE;
}
return mpAudioPolicy->set_stream_volume_index(mpAudioPolicy, stream, index);
}
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
看到这,就要聊一下mpAudioPolicy 怎么来的了,这个关心ICS新架构。
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
AudioPolicyService::AudioPolicyService()
: BnAudioPolicyService() , mpAudioPolicyDev(NULL) , mpAudioPolicy(NULL)
{
char value[PROPERTY_VALUE_MAX];
const struct hw_module_t *module;
int forced_val;
int rc;
Mutex::Autolock _l(mLock);
//这两个线程一会会提到
// start tone playback thread
mTonePlaybackThread = new AudioCommandThread(String8(""));
// start audio commands thread
mAudioCommandThread = new AudioCommandThread(String8("ApmCommandThread"));
// 试图寻找AUDIO_POLICY_HARDWARE_MODULE_ID 这个模块,这个概念和当年的私有policy类是一样的概念。
// 现在audio hardware层分为传统的和非传统的, 现在还没看到谁家用新架构写Policy, 基本还都是用传统的。
/* instantiate the audio policy manager */
rc = hw_get_module(AUDIO_POLICY_HARDWARE_MODULE_ID, &module);
if (rc)
return;
rc = audio_policy_dev_open(module, &mpAudioPolicyDev);
LOGE_IF(rc, "couldn't open audio policy device (%s)", strerror(-rc));
if (rc)
return;
// 这里会创建一个policy device,
rc = mpAudioPolicyDev->create_audio_policy(mpAudioPolicyDev, &aps_ops, this,
&mpAudioPolicy);
LOGE_IF(rc, "couldn't create audio policy (%s)", strerror(-rc));
if (rc)
return;
rc = mpAudioPolicy->init_check(mpAudioPolicy);
LOGE_IF(rc, "couldn't init_check the audio policy (%s)", strerror(-rc));
if (rc)
return;
property_get("ro.camera.sound.forced", value, "0");
forced_val = strtol(value, NULL, 0);
mpAudioPolicy->set_can_mute_enforced_audible(mpAudioPolicy, !forced_val);
LOGI("Loaded audio policy from %s (%s)", module->name, module->id);
// load audio pre processing modules
if (access(AUDIO_EFFECT_VENDOR_CONFIG_FILE, R_OK) == 0) {
loadPreProcessorConfig(AUDIO_EFFECT_VENDOR_CONFIG_FILE);
} else if (access(AUDIO_EFFECT_DEFAULT_CONFIG_FILE, R_OK) == 0) {
loadPreProcessorConfig(AUDIO_EFFECT_DEFAULT_CONFIG_FILE);
}
}
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
挖进去看create_audio_policy, 现在传统的policy 在hardware/libhardware_legacy/audio/audio_policy_hal.cpp
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
dev->device.create_audio_policy = create_legacy_ap;
then, 看实现函数
static int create_legacy_ap(const struct audio_policy_device *device,
struct audio_policy_service_ops *aps_ops,
void *service,
struct audio_policy **ap)
{
。。。。
lap->policy.set_can_mute_enforced_audible =
ap_set_can_mute_enforced_audible;
lap->policy.init_check = ap_init_check;
lap->policy.get_output = ap_get_output;
lap->policy.start_output = ap_start_output;
lap->policy.stop_output = ap_stop_output;
lap->policy.release_output = ap_release_output;
lap->policy.get_input = ap_get_input;
lap->policy.start_input = ap_start_input;
lap->policy.stop_input = ap_stop_input;
lap->policy.release_input = ap_release_input;
lap->policy.init_stream_volume = ap_init_stream_volume;
lap->policy.set_stream_volume_index = ap_set_stream_volume_index;
lap->policy.get_stream_volume_index = ap_get_stream_volume_index;
lap->policy.get_strategy_for_stream = ap_get_strategy_for_stream;
lap->policy.get_devices_for_stream = ap_get_devices_for_stream;
lap->policy.get_output_for_effect = ap_get_output_for_effect;
lap->policy.register_effect = ap_register_effect;
lap->policy.unregister_effect = ap_unregister_effect;
lap->policy.set_effect_enabled = ap_set_effect_enabled;
lap->policy.is_stream_active = ap_is_stream_active;
lap->policy.dump = ap_dump;
lap->aps_ops = aps_ops;
lap->service_client =
new AudioPolicyCompatClient(aps_ops, service);
if (!lap->service_client) {
ret = -ENOMEM;
goto err_new_compat_client;
}
lap->apm = createAudioPolicyManager(lap->service_client);
if (!lap->apm) {
ret = -ENOMEM;
goto err_create_apm;
}
*ap = &lap->policy;
return 0;
}
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
看到了吧,还是要继续挖,又是个函数指针
ap_set_stream_volume_index
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
static int ap_set_stream_volume_index(struct audio_policy *pol,
audio_stream_type_t stream,
int index)
{
struct legacy_audio_policy *lap = to_lap(pol);
return lap->apm->setStreamVolumeIndex((AudioSystem::stream_type)stream,
index);
}
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
嘿,这个lap是audio_policy转来的, 这是刚刚初始化 的那个东西啊
lap->apm = createAudioPolicyManager(lap->service_client)
还记得吗,向上看20行。 那这apm是哪来的啊,继续挖。
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
extern "C" AudioPolicyInterface* createAudioPolicyManager(AudioPolicyClientInterface *clientInterface)
{
return new AudioPolicyManager(clientInterface);
}
class AudioPolicyManager: public AudioPolicyManagerBase
{
public:
AudioPolicyManager(AudioPolicyClientInterface *clientInterface)
: AudioPolicyManagerBase(clientInterface) {}
virtual ~AudioPolicyManager() {}
protected:
// true is current platform implements a back microphone
virtual bool hasBackMicrophone() const { return false; }
#ifdef WITH_A2DP
// true is current platform supports suplication of notifications and ringtones over A2DP output
virtual bool a2dpUsedForSonification() const { return true; }
#endif
};
竟然是这么单纯的类,继续看父累
原来实现都在父类里,mydroid/hardware/libhardware_legacy/audio/AudioPolicyManagerBase.cpp
这不就是GB当年的版本嘛。。
果然,看到我要找的函数
status_t AudioPolicyManagerBase::setStreamVolumeIndex(AudioSystem::stream_type stream, int index)
{
{
if ((index < mStreams[stream].mIndexMin) || (index > mStreams[stream].mIndexMax)) {
return BAD_VALUE;
}
// Force max volume if stream cannot be muted
if (!mStreams[stream].mCanBeMuted) index = mStreams[stream].mIndexMax;
LOGV("setStreamVolumeIndex() stream %d, index %d", stream, index);
mStreams[stream].mIndexCur = index;
// compute and apply stream volume on all outputs according to connected device
status_t status = NO_ERROR;
for (size_t i = 0; i < mOutputs.size(); i++) {
status_t volStatus = checkAndSetVolume(stream, index, mOutputs.keyAt(i), mOutputs.valueAt(i)->device());
if (volStatus != NO_ERROR) {
status = volStatus;
}
}
return status;
}
status_t AudioPolicyManagerBase::checkAndSetVolume(int stream, int index, audio_io_handle_t output, uint32_t device, int delayMs, bool force)
{
if (volume != mOutputs.valueFor(output)->mCurVolume[stream] ||
force) {
mOutputs.valueFor(output)->mCurVolume[stream] = volume;
LOGV("setStreamVolume() for output %d stream %d, volume %f, delay %d", output, stream, volume, delayMs);
if (stream == AudioSystem::VOICE_CALL ||
stream == AudioSystem::DTMF ||
stream == AudioSystem::BLUETOOTH_SCO) {
// offset value to reflect actual hardware volume that never reaches 0
// 1% corresponds roughly to first step in VOICE_CALL stream volume setting (see AudioService.java)
volume = 0.01 + 0.99 * volume;
// Force VOICE_CALL to track BLUETOOTH_SCO stream volume when bluetooth audio is
// enabled
if (stream == AudioSystem::BLUETOOTH_SCO) {
mpClientInterface->setStreamVolume(AudioSystem::VOICE_CALL, volume, output, delayMs);
}
}
mpClientInterface->setStreamVolume((AudioSystem::stream_type)stream, volume, output, delayMs);
}
}
继续mpClientInterface。。 这个是传进来的,与AudioPolicyManager 一起构造的
lap->service_client =
new AudioPolicyCompatClient(aps_ops, service);
class AudioPolicyCompatClient : public AudioPolicyClientInterface {
public:
AudioPolicyCompatClient(struct audio_policy_service_ops *serviceOps,
void *service) :
mServiceOps(serviceOps) , mService(service) {}
}
status_t AudioPolicyCompatClient::setStreamVolume(
AudioSystem::stream_type stream,
float volume,
audio_io_handle_t output,
int delayMs)
{
return mServiceOps->set_stream_volume(mService, (audio_stream_type_t)stream,
volume, output, delayMs);
}
阿,这个client就是个傀儡。。
绕了这麽久,其实这函数是传进来的。请别怪我绕这么个大弯,ICS就是这么干的
回过去,看AudioPolicyService, opt 是从这传进来的
rc = mpAudioPolicyDev->create_audio_policy(mpAudioPolicyDev, &aps_ops, this,
&mpAudioPolicy);
namespace {
struct audio_policy_service_ops aps_ops = {
open_output : aps_open_output,
open_duplicate_output : aps_open_dup_output,
close_output : aps_close_output,
suspend_output : aps_suspend_output,
restore_output : aps_restore_output,
open_input : aps_open_input,
close_input : aps_close_input,
set_stream_volume : aps_set_stream_volume,
set_stream_output : aps_set_stream_output,
set_parameters : aps_set_parameters,
get_parameters : aps_get_parameters,
start_tone : aps_start_tone,
stop_tone : aps_stop_tone,
set_voice_volume : aps_set_voice_volume,
move_effects : aps_move_effects,
#ifdef OMAP_ENHANCEMENT
set_FMRxActive : aps_set_FMRxActive,
#endif
};
}; // namespace <unnamed>
static int aps_set_stream_volume(void *service, audio_stream_type_t stream,
float volume, audio_io_handle_t output,
int delay_ms)
{
AudioPolicyService *audioPolicyService = (AudioPolicyService *)service;
return audioPolicyService->setStreamVolume(stream, volume, output,
delay_ms);
}
还是在audioPolicyService
int AudioPolicyService::setStreamVolume(audio_stream_type_t stream,
float volume,
audio_io_handle_t output,
int delayMs)
{
return (int)mAudioCommandThread->volumeCommand((int)stream, volume,
(int)output, delayMs);
}
终于看到点别的了, 这就是刚讲的两个thread, 这个是负责命令的。
status_t AudioPolicyService::AudioCommandThread::volumeCommand(int stream,
float volume,
int output,
int delayMs)
{
status_t status = NO_ERROR;
AudioCommand *command = new AudioCommand();
command->mCommand = SET_VOLUME;
VolumeData *data = new VolumeData();
data->mStream = stream;
data->mVolume = volume;
data->mIO = output;
command->mParam = data;
if (delayMs == 0) {
command->mWaitStatus = true;
} else {
command->mWaitStatus = false;
}
Mutex::Autolock _l(mLock);
insertCommand_l(command, delayMs);
LOGV("AudioCommandThread() adding set volume stream %d, volume %f, output %d",
stream, volume, output);
mWaitWorkCV.signal();
if (command->mWaitStatus) {
command->mCond.wait(mLock);
status = command->mStatus;
mWaitWorkCV.signal();
}
return status;
}
加到了命令队列,
bool AudioPolicyService::AudioCommandThread::threadLoop()
{
case SET_VOLUME: {
VolumeData *data = (VolumeData *)command->mParam;
LOGV("AudioCommandThread() processing set volume stream %d, \
volume %f, output %d", data->mStream, data->mVolume, data->mIO);
command->mStatus = AudioSystem::setStreamVolume(data->mStream,
data->mVolume,
data->mIO);
if (command->mWaitStatus) {
command->mCond.signal();
mWaitWorkCV.wait(mLock);
}
delete data;
}break;
}
终于,我们要离开policyservices 了。
status_t AudioSystem::setStreamVolume(int stream, float value, int output)
{
if (uint32_t(stream) >= AUDIO_STREAM_CNT) return BAD_VALUE;
const sp<IAudioFlinger>& af = AudioSystem::get_audio_flinger();
if (af == 0) return PERMISSION_DENIED;
af->setStreamVolume(stream, value, output);
return NO_ERROR;
}
Audio Flinger, 终于看见真正干活的了。
status_t AudioFlinger::setStreamVolume(int stream, float value, int output)
{
// check calling permissions
if (!settingsAllowed()) {
return PERMISSION_DENIED;
}
if (stream < 0 || uint32_t(stream) >= AUDIO_STREAM_CNT) {
return BAD_VALUE;
}
AutoMutex lock(mLock);
PlaybackThread *thread = NULL;
if (output) {
thread = checkPlaybackThread_l(output);
if (thread == NULL) {
return BAD_VALUE;
}
}
mStreamTypes[stream].volume = value;
if (thread == NULL) {
for (uint32_t i = 0; i < mPlaybackThreads.size(); i++) {
mPlaybackThreads.valueAt(i)->setStreamVolume(stream, value);
}
} else {
thread->setStreamVolume(stream, value);
}
return NO_ERROR;
}
还要写道子线程里。其实也是写到 mStreamTypes[stream].volume
好了这路先写到这,音量值已经写道audioflinger 内部的track
这个是统一音量,对于每种声音。 下面是另一个接口,是针对流的。
mydroid/frameworks/base/core/jni/android_media_AudioTrack.cpp
------------------------------------------------------------------------------------------------------------------------------
static void
android_media_AudioTrack_set_volume(JNIEnv *env, jobject thiz, jfloat leftVol, jfloat rightVol )
{
AudioTrack *lpTrack = (AudioTrack *)env->GetIntField(
thiz, javaAudioTrackFields.nativeTrackInJavaObj);
if (lpTrack == NULL ) {
jniThrowException(env, "java/lang/IllegalStateException",
"Unable to retrieve AudioTrack pointer for setVolume()");
return;
}
lpTrack->setVolume(leftVol, rightVol);
}
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
JNI 函数直接调到AudioTrack.
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
status_t AudioTrack::setVolume(float left, float right)
{
if (left > 1.0f || right > 1.0f) {
return BAD_VALUE;
}
AutoMutex lock(mLock);
mVolume[LEFT] = left;
mVolume[RIGHT] = right;
// write must be atomic
mCblk->volumeLR = (uint32_t(uint16_t(right * 0x1000)) << 16) | uint16_t(left * 0x1000);
return NO_ERROR;
}
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
看见了吗,他直接把值付到了mCblk里了。 这里提一句,mCblk是用于共享内存与AudioTrack 和AudioFlinger之间的,AudioTrack 负责录入,AudioFlinger 作为服务
端起动了相对应的inner audiotrack 负责处理具体数据。 具体细节网上有很多文章,此处不在细说。
AudioTrack 该干的都干完了,然后就看AudioFlinger表现了~~~ 希望读者明白audio的基本架构,AudioFlinger 在建立inner audiotrack时,为每一个track建了个线程,
放音一般为MixerThread.
到这时,两种接口设置的音量都设完了,下面就看子线程的了 。
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
bool AudioFlinger::MixerThread::threadLoop()
{
....
mixerStatus = prepareTracks_l(activeTracks, &tracksToRemove);
....
if (LIKELY(mixerStatus == MIXER_TRACKS_READY)) {
// mix buffers...
if (outputsReady(outputTracks)) {
mAudioMixer->process();
} else {
memset(mMixBuffer, 0, mixBufferSize);
}
sleepTime = 0;
writeFrames = mFrameCount;
}
}
}
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
这两个函数为每个track子线程的核心函数:prepareTracks_l and process()
先来看prepareTracks_l
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
// prepareTracks_l() must be called with ThreadBase::mLock held
uint32_t AudioFlinger::MixerThread::prepareTracks_l(const SortedVector< wp<Track> >& activeTracks, Vector< sp<Track> > *tracksToRemove)
{
// Check each track....
int param = AudioMixer::VOLUME;
if (track->mFillingUpStatus == Track::FS_FILLED) {
// no ramp for the first volume setting
// mFillingUpStatus 标志位用来避免ramp出现在 第一次声音配置
track->mFillingUpStatus = Track::FS_ACTIVE;
if (track->mState == TrackBase::RESUMING) {
track->mState = TrackBase::ACTIVE;
param = AudioMixer::RAMP_VOLUME;
}
// 需要理解为什么设这两个,现在看起来像重置。
mAudioMixer->setParameter(AudioMixer::RESAMPLE, AudioMixer::RESET, NULL);
} else if (cblk->server != 0) {
// If the track is stopped before the first frame was mixed,
// do not apply ramp
param = AudioMixer::RAMP_VOLUME;
}
这段代码主要负责计算音量
// compute volume for this track
uint32_t vl, vr, va;
// 无声的情况
if (track->isMuted() || track->isPausing() ||
mStreamTypes[track->type()].mute) {
vl = vr = va = 0;
if (track->isPausing()) {
track->setPaused();
}
} else {
// 有声音
// read original volumes with volume control
// 这是我们第一个接口设置的类别型音量
float typeVolume = mStreamTypes[track->type()].volume;
// masterVolume 是实际值,typeVolume 和track volume是百分比。
float v = masterVolume * typeVolume;
vl = (uint32_t)(v * cblk->volume[0]) << 12;
vr = (uint32_t)(v * cblk->volume[1]) << 12;
va = (uint32_t)(v * cblk->sendLevel);
}
// Delegate volume control to effect in track effect chain if needed
if (chain != 0 && chain->setVolume_l(&vl, &vr)) {
// Do not ramp volume if volume is controlled by effect
param = AudioMixer::VOLUME;
track->mHasVolumeController = true;
} else {
// force no volume ramp when volume controller was just disabled or removed
// from effect chain to avoid volume spike
if (track->mHasVolumeController) {
param = AudioMixer::VOLUME;
}
track->mHasVolumeController = false;
}
// 这两种格式没听说过,希望有人知道可以指教。
// Convert volumes from 8.24 to 4.12 format
int16_t left, right, aux;
uint32_t v_clamped = (vl + (1 << 11)) >> 12;
if (v_clamped > MAX_GAIN_INT) v_clamped = MAX_GAIN_INT;
left = int16_t(v_clamped);
v_clamped = (vr + (1 << 11)) >> 12;
if (v_clamped > MAX_GAIN_INT) v_clamped = MAX_GAIN_INT;
right = int16_t(v_clamped);
if (va > MAX_GAIN_INT) va = MAX_GAIN_INT;
aux = int16_t(va);
// XXX: these things DON'T need to be done each time
mAudioMixer->setBufferProvider(track);
mAudioMixer->enable(AudioMixer::MIXING);
最终设置左右音量到mixer 里
mAudioMixer->setParameter(param, AudioMixer::VOLUME0, (void *)left);
mAudioMixer->setParameter(param, AudioMixer::VOLUME1, (void *)right);
mAudioMixer->setParameter(param, AudioMixer::AUXLEVEL, (void *)aux);
}
Mixer 处理
case RAMP_VOLUME:
case VOLUME:
if ((uint32_t(name-VOLUME0) < MAX_NUM_CHANNELS)) {
track_t& track = mState.tracks[ mActiveTrack ];
if (track.volume[name-VOLUME0] != valueInt) {
LOGV("setParameter(VOLUME, VOLUME0/1: %04x)", valueInt);
track.prevVolume[name-VOLUME0] = track.volume[name-VOLUME0] << 16;
track.volume[name-VOLUME0] = valueInt;
if (target == VOLUME) {
track.prevVolume[name-VOLUME0] = valueInt << 16;
track.volumeInc[name-VOLUME0] = 0;
} else {
int32_t d = (valueInt<<16) - track.prevVolume[name-VOLUME0];
int32_t volInc = d / int32_t(mState.frameCount);
track.volumeInc[name-VOLUME0] = volInc;
if (volInc == 0) {
track.prevVolume[name-VOLUME0] = valueInt << 16;
}
}
invalidateState(1<<mActiveTrack);
}
return NO_ERROR;
} else if (name == AUXLEVEL) {
track_t& track = mState.tracks[ mActiveTrack ];
if (track.auxLevel != valueInt) {
LOGV("setParameter(VOLUME, AUXLEVEL: %04x)", valueInt);
track.prevAuxLevel = track.auxLevel << 16;
track.auxLevel = valueInt;
if (target == VOLUME) {
track.prevAuxLevel = valueInt << 16;
track.auxInc = 0;
} else {
int32_t d = (valueInt<<16) - track.prevAuxLevel;
int32_t volInc = d / int32_t(mState.frameCount);
track.auxInc = volInc;
if (volInc == 0) {
track.prevAuxLevel = valueInt << 16;
}
}
invalidateState(1<<mActiveTrack);
}
return NO_ERROR;
}
break;
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
然后是process, 这是个指针, 在计算输出声音时,音量的因素已经加里了。一系列专业的音频换算,音量的因素就加里了。
这是Android 在ICS里的音量控制方式,基本和GB 区别不大,就是更饶了。 如果是做ALSA的同学,肯定奇怪了,底层明明也提供了一系列音量
配置接口,和Android这个没关系吗? 确实没关系。
我是做TI平台的,举个TI的例子 :
static void select_output_device(struct omap_audio_device *adev)
{
set_output_volumes(adev);
}
static void set_output_volumes(struct omap_audio_device *adev)
{
unsigned int channel;
int speaker_volume;
int headset_volume;
speaker_volume = adev->mode == AUDIO_MODE_IN_CALL ? VOICE_CALL_SPEAKER_VOLUME :
NORMAL_SPEAKER_VOLUME;
headset_volume = adev->devices & AUDIO_DEVICE_OUT_WIRED_HEADSET ?
HEADSET_VOLUME :
HEADPHONE_VOLUME;
for (channel = 0; channel < 2; channel++) {
mixer_ctl_set_value(adev->mixer_ctls.speaker_volume, channel,
DB_TO_SPEAKER_VOLUME(speaker_volume));
mixer_ctl_set_value(adev->mixer_ctls.headset_volume, channel,
DB_TO_HEADSET_VOLUME(headset_volume));
}
}
看到了吧,speaker volume和headset volume 都是写死的。
希望对大家有所帮助。如果有问题,或是有错误,敬请指教。