1. 程式人生 > >Android java層音訊相關的分析與理解(二)音量控制相關

Android java層音訊相關的分析與理解(二)音量控制相關

上一篇我們簡單地說了一下Android java層的基本框架。接下來我們就來聊一下在android中音量控制的相關內容。

1.音量定義

在Android中,音量的控制與流型別是密不可分的,每種流型別都獨立地擁有自己的音量設定,各種流型別的音量是互不干擾的,例如音樂音量、通話音量就是相互獨立的。Andorid當前在AudioSystem.java預設有10種流型別(見下表列二)。既然Android當中有10種流型別,每種流型別的音量都是相互獨立的,所以在預設音量方面,Android也給每種流型別初始化了一個預設的音量(下表列二、三)和最大音量(下表列四)。雖然Android中擁有10種流型別,但是為了便於使用,android通過判斷裝置的型別,去對映具體流型別。當前Android在AudioSystem.java中提供了3個裝置(DEFAULT,VOICE,TELEVISION)作為可選擇項,分別去對映我們具體的音訊流型別。其中,DEFAULT和VOICE型別的音訊對映是一致的。


所以,從上表中可以看出,在手機裝置當中,我們當前可調控的流型別音量其實只有5個,當你想調節STREAM_SYSTEM,STREAM_NOTIFICATION等流型別的音量時,實際上是調節了STREAM_RING的音量。當前可控的流型別可以通過下表更直觀地顯示:


每次手機開機,在Android6.0之前,SettingsProvide的內容提供者會將流型別預設值寫入到資料庫settings.db裡面,當然,這是個SQLite資料庫,如果資料庫已存在則不再進行寫入。如下圖:


但是,從Andorid6.0開始,google將設定部分相關的內容從settings.db轉移出來,轉為以xml形式非同步儲存在/data/system/user/0(使用者名稱)/目錄下。當前負責儲存音量和鈴聲相關的檔案為settings_system.xml。形式如下:

<setting id="4" name="volume_alarm" value="6" package="android" />
<setting id="0" name="volume_music" value="11" package="android" />
<setting id="3" name="volume_voice" value="4" package="android" />
<setting id="32" name="ringtone" value="content://media/internal/audio/media/180" package="com.android.providers.media" />
<setting id="13" name="hearing_aid" value="0" package="android" />
<setting id="1" name="volume_ring" value="5" package="android" />
當前Android6.0將這些設定轉移出來,是為了便於執行並提高效能。更改一個設定可以從原本的400ms左右變為10ms左右。這大大地提高了讀寫的效率。另一方面,它為每個使用者都會新建一個這樣的表從而避免了多使用者的設定的衝突。使用者體驗更好,設定更人性化。

2.音量調整

在Android手機上有兩種改變系統音量的方式。最直接的做法就是通過手機的音量鍵進行音量調整,還有就是從設定介面中調整某一種型別音訊的音量。他們都是都是通過AudioService進行的。


2.1 音量鍵的處理流程

音量鍵被按下後,Android輸入系統將該事件一路派發給Activity,如果無人截獲這個事件,承載當前Activity的顯示的PhoneWindow類的onKeyDown()或onKeyUp()函式將會將其處理,從而開始了通過音量鍵調整音量的處理流程。需要注意的是,按照Android的輸入事件派發策略,Window物件在事件的派發佇列中排在Activity的後面(應該說排在隊尾比較合適),所以應用程式可以重寫自己的onKeyDown()函式,將音量鍵用作其他的功能。

PhoneWindow的onKeyDown()函式實現如下(省略部分程式碼):
……
switch (keyCode) {
       case KeyEvent.KEYCODE_VOLUME_UP:
       case KeyEvent.KEYCODE_VOLUME_DOWN:
       case KeyEvent.KEYCODE_VOLUME_MUTE: {
         ……
         /*
在這裡,先判斷mMediaController是否為空(顯示的音量調整UI是否還存在),假如存在,就直接呼叫mMediaController的adjustVolume進行調整音量。不存在就通過MediaSession去建立UI並調整。
*/
         if (mMediaController != null) {
             mMediaController.adjustVolume(direction, AudioManager.FLAG_SHOW_UI);
         } else {
             MediaSessionLegacyHelper.getHelper(getContext()).sendAdjustVolumeBy(
                    mVolumeControlStreamType, direction,
                    AudioManager.FLAG_SHOW_UI | AudioManager.FLAG_VIBRATE
                  | AudioManager.FLAG_FROM_KEY);
         }
         return true;
   }
……

上面的程式碼顯示,PhoneWindow接收onKeyDown()事件處理時,先判斷顯示的音量調整UI是否存在,假如存在,就直接在當前的流型別上進行調整音量。假如不存在就通過MediaSession去建立UI並調整音量。

在這裡Android從5.0開始使用MediaSession對音量進行控制。通過MeidaSession相關的類,最終在MeidaSessionService中呼叫AudioService的adjustSuggestedStreamVolume()進行真正的音量設定的初步處理。

AudioService的adjustSuggestedStreamVolume()實現如下(省略部分程式碼):

……
int streamType;
        boolean isMute = isMuteAdjust(direction);
        //在這裡也可以更改需要修改的流型別
if (mVolumeControlStream != -1) {
            streamType = mVolumeControlStream;
        } else {
            //通過getActiveStreamType()函式獲取要控制的流型別
            streamType = getActiveStreamType(suggestedStreamType);
        }
        ensureValidStreamType(streamType);
        final int resolvedStream = mStreamVolumeAlias[streamType];
        ……
        // For notifications/ring, show the ui before making any adjustments
        // Don't suppress mute/unmute requests
        if (mVolumeController.suppressAdjustment(resolvedStream, flags, isMute)) {
            direction = 0;
            flags &= ~AudioManager.FLAG_PLAY_SOUND;
            flags &= ~AudioManager.FLAG_VIBRATE;
            if (DEBUG_VOL) Log.d(TAG, "Volume controller suppressed adjustment");
        }

        adjustStreamVolume(streamType, direction, flags, callingPackage, caller, uid);
        ……

adjustSuggestedStreamVolume()負責接收MeidaSessionService傳入的資訊,然後針對要修改流型別獲取相應的對映,更改是否顯示ui的標誌,然後將具體的調整音量操作交給adjustStreamVolume()去完成。

另外,關於這個adjustSuggestedStreamVolume()有點是需要特別說明一下。它剛開始的時候有一個判斷,條件是一個名為mVolumeControlStream的整型變數是否等於-1,從這塊程式碼來看,mVolumeControlStream比引數傳入的suggestedStreamType厲害多了,只要它不是-1,那麼要調整音量的流型別就是它。那這麼厲害的控制手段,是做什麼用的呢?其實,mVolumeControlStream是VolumePanel通過forceVolumeControlStream()函式設定的。什麼是VolumePanel呢?就是我們按下音量鍵後的那個音量條提示框了。VolumePanel在顯示時會呼叫forceVolumeControlStream強制後續的音量鍵操作固定為促使它顯示的那個流型別。並在它關閉時取消這個強制設定,即置mVolumeControlStream為-1。

AudioService的adjustStreamVolume()實現如下(省略部分程式碼):
……
        //確認一下調整的音量方向和流型別
        ensureValidDirection(direction);
        ensureValidStreamType(streamType);
        
        // 首先還是獲取streamType對映到的流型別。
        int streamTypeAlias = mStreamVolumeAlias[streamType];

        VolumeStreamState streamState = mStreamStates[streamTypeAlias];

        final int device = getDeviceForStream(streamTypeAlias);

        //然後獲取這個streamType的當前音量 
        int aliasIndex = streamState.getIndex(device);
        boolean adjustVolume = true;
        int step;

        ……
        //確定當前流型別的音量等級
        if ((streamTypeAlias == AudioSystem.STREAM_MUSIC) &&
               ((device & mFixedVolumeDevices) != 0)) {
            flags |= AudioManager.FLAG_FIXED_VOLUME;
            if (mSafeMediaVolumeState == SAFE_MEDIA_VOLUME_ACTIVE &&
                    (device & mSafeMediaVolumeDevices) != 0) {
                step = mSafeMediaVolumeIndex;
            } else {
                step = streamState.getMaxIndex();
            }
            if (aliasIndex != 0) {
                aliasIndex = step;
            }
        } else {
                step = rescaleIndex(10, streamType, streamTypeAlias);
        }
         
         ……
        //判斷是否該改變情景模式。例如當從震動轉換成響鈴時,不需要更改音量。adjustVolume作為一個控制量,控制是否需要更改音量。
            final int result = checkForRingerModeChange(aliasIndex, direction, step,
                    streamState.mIsMuted);
            adjustVolume = (result & FLAG_ADJUST_VOLUME) != 0;
            
            ……
            //呼叫adjustIndex()更改VolumeStreamState物件中儲存的音量值
           } else if (streamState.adjustIndex(direction * step, device, caller)
                    || streamState.mIsMuted) {
               
//傳送訊息給AudioHandle,更改音量。
                sendMsg(mAudioHandler,
                        MSG_SET_DEVICE_VOLUME,
                        SENDMSG_QUEUE,
                        device,
                        0,
                        streamState,
                        0);
            }

            ……
        //最後通過sendVolumeUpdate去通知音量已經發生變化了。
        int index = mStreamStates[streamType].getIndex(device);
        sendVolumeUpdate(streamType, oldIndex, index, flags);
    }

AudioService的adjustStreamVolume ()針對音量設定做了很多的操作,所以在這裡簡單地總結一下這個函式都作了什麼:

1)     準備工作。計算按下音量鍵的音量步進值。主要通過rescaleIndex()函式的實現。

2)     檢查是否需要改變情景模式。checkForRingerModeChange()和情景模式有關。呼叫adjustIndex()更改VolumeStreamState物件中儲存的音量值。

3)     通過sendMsg()傳送訊息MSG_SET_DEVICE_VOLUME到mAudioHandler。

4)     呼叫sendVolumeUpdate()函式,通知外界音量發生了變化。

VolumeStreamState是AudioService的一個內部類,當進行音量或者鈴聲模式管理時,需要鎖定這個物件,避免順序出錯。

下面是VolumeSteramState獲取音量和儲存音量的操作:

......
        public void readSettings() {
            //先鎖定,避免出錯
            synchronized (VolumeStreamState.class) {
                     ……
                    
                    String name = getSettingNameForDevice(device);
                    int defaultIndex = (device == AudioSystem.DEVICE_OUT_DEFAULT) ?
                            AudioSystem.DEFAULT_STREAM_VOLUME[mStreamType] : -1;
                    //通過讀取setting資料庫去獲取值
int index = Settings.System.getIntForUser(
                            mContentResolver, name, defaultIndex, UserHandle.USER_CURRENT);
……
        }
 
......
        public boolean setIndex(int index, int device, String caller) {
            ……
//先鎖定,避免出錯
            synchronized (VolumeStreamState.class) {
                oldIndex = getIndex(device);
                index = getValidIndex(index);
                ……
    // 首先是在mIndexMap中儲存設定的音量值
                mIndexMap.put(device, index);

                changed = oldIndex != index;
                if (changed) {
                    // 同時設定所有對映到當前流型別的其他流的音量                    boolean currentDevice = (device == getDeviceForStream(mStreamType));
                    int numStreamTypes = AudioSystem.getNumStreamTypes();
                    for (int streamType = numStreamTypes - 1; streamType >= 0; streamType--) {
                        if (streamType != mStreamType &&
                                mStreamVolumeAlias[streamType] == mStreamType) {
                           ……
            }
          
                // 傳送通知
                mVolumeChanged.putExtra(AudioManager.EXTRA_VOLUME_STREAM_VALUE, index);
                mVolumeChanged.putExtra(AudioManager.EXTRA_PREV_VOLUME_STREAM_VALUE, oldIndex);
                mVolumeChanged.putExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE_ALIAS,
                        mStreamVolumeAlias[mStreamType]);
                sendBroadcastToAll(mVolumeChanged);
            }
            return changed;
        }

VolumeSteramState可以直接通過呼叫Settings.System.getIntForUser()去獲取資料庫中的音量,但是,在更新音量時,它只是更新了內部儲存的音量而沒有做更多的處理。所以,真正的更新音量的操作應該由mAudioHandler去處理。

從AudioService的adjustStreamVolume ()可以知道,adjustStreamVolume()給AudioHandler傳送了帶有“MSG_SET_DEVICE_VOLUME”的訊息。AudioHandler根據此訊息會進行setDeviceVolume()處理。

以下是setDeviceVolume()的主要內容:

……
private void setDeviceVolume(VolumeStreamState streamState, int device) {
//先鎖定,避免出錯
synchronized (VolumeStreamState.class) {
                //通過VolumeStreamState呼叫AudioSystem的setStreamVolumeIndex()設定音量到底層的AudioFlinger裡面去
                streamState.applyDeviceVolume_syncVSS(device);                
……
            //繼續給AudioHandler傳送資訊,呼叫persistVolume(),通過System.putIntForUser()將目標音量儲存在Setting資料庫中
            sendMsg(mAudioHandler,
                    MSG_PERSIST_VOLUME,
                    SENDMSG_QUEUE,
                    device,
                    0,
                    streamState,
                    PERSIST_DELAY);
        }

從上面程式碼可以看出,AudioService通過setDeviceVolume()真正地更改了音量。setDeviceVolume()先用過VolumeStremState呼叫AudioSystem的setStreamVolumeIndex()設定音量到底層的AudioFlinger裡面去,然後在通過AudioHandler.persistVolume()將音量真正儲存起來。這樣就完成了大部分的音量調整了。

之後,AudioService通過sendVolumeUpdate()去更新介面。sendVolumeUpdate()會通過AIDL去呼叫VolumeDialogController.java中的onVolumeChangedW()方法,從而顯示介面調整。在這裡就不細說了。

總的來說,通過音量鍵去調整音量的序列圖如下:


2.2 通過設定調整音量

在Android中,除了通過音量鍵直接調節音量之外,還可以在系統設定進行音量的調整。在當前系統設定應用當中,Android主要通過呼叫SeekBarVolumizer去顯示介面並調整音量的。SeekBarVolumizer是一個控制元件,當我們觸動這個控制元件的時候,控制元件會根據當初的音量和模式去呼叫AudioManager的adjustStreamVolume(靜音或震動模式)或setStreamVolume(普通模式)去調整相對應的音量。具體的調整音量方式其實是大致一樣的,這裡就不細說了。

普通模式下通過設定調整音量的序列圖如下:


2.3 靜音與震動

靜音與震動是另外的2種響鈴模式。響鈴模式的調整其實與上面音量調整的設定的思想和流程大部分是一致的。當前外部程式設定為靜音或震動的流程為:先通過呼叫AudioManager去進行響鈴模式的調整。實際的響鈴模式調整發生在AudioService。AudioService設定並儲存流型別的模式。稍微不同的是當前AudioService並沒有呼叫AudioSystem去儲存RingerMode,而是直接通過persistRingerMode()將其儲存在資料庫當中。然後Android6.0調整當前設定模式的序列圖為:


在音訊中,設定音量等等流程上是挺簡單的。只需要我們細心點去一步一步往下走,就能找到我們想要的東西。