Android音頻(7)——項目實戰——耳麥插拔


一、驅動程序上報耳麥拔插事件


1. 在有些Android版本中並不會在狀態欄上顯示耳麥圖標。切換聲道也不在系統中實現,而是在驅動中實現的。

2. headset headPhone lineOut
headset:既有聽筒又有Mic
headPhone:只有聽筒,沒有Mic
lineOut: 就是輸出模擬信號到音箱

驅動需要上報三種設備的拔插:headset、headPhone、lineOut。

3. 怎么上報:
(1) 輸入子系統:可以上報按鍵事件也可以上報開關事件(EV_SW),事件類型包括headset、headPhone、lineOut。

(2) switch class子系統:通過uevent向用戶空間發送數據,Android中有個線程專門監聽這類事件。

具體使用哪一個要看Android源代碼。

對於輸入設備都需要指定能產生同步類事件EV_SYN. 對於按鍵事件,輸入子系統還需要設置按鍵值的范圍,但是對於開關類事件不需要設置。

使用switch dev子系統時,名字必須要設置為"h2w",Android系統監聽/sys/class/switch/h2w這個虛擬設備。

4. 驅動Demo代碼
由於耳塞的檢查Codec給過來的硬件中斷線沒有接到Soc上所以使用軟件模擬。

#include <linux/module.h>
#include <linux/types.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/fs.h>
#include <linux/err.h>
#include <linux/switch.h>
#include <linux/input.h>

static struct input_dev *g_virtual_input;

static ssize_t input_test_store(struct device *dev,
                     struct device_attribute *attr,
                     const char *buf, size_t count)
{
    long code;
    long val;
    char *endp;

    /* 如果字符串前面含有非數字, simple_strtol不能處理 */
    while ((*buf == ' ') || (*buf == '\t'))
        buf++;

    code = simple_strtol(buf, &endp, 0);

    /* 如果字符串前面含有非數字, simple_strtol不能處理 */
    while ((*endp == ' ') || (*endp == '\t'))
        endp++;
    val  = simple_strtol(endp, NULL, 0);

    printk("emulate to report EV_SW: 0x%x 0x%x\n", code, val);
    input_event(g_virtual_input, EV_SW, code, val);
    input_sync(g_virtual_input);

    return count;
}

static DEVICE_ATTR(test_input, S_IRUGO | S_IWUSR, NULL, input_test_store);

static int register_input_device_for_jack(void)
{
    int err;

    /* 分配input_dev */
    g_virtual_input = input_allocate_device();

    /* 設置 */
    /* 2.1 能產生哪類事件 */
    set_bit(EV_SYN, g_virtual_input->evbit);
    set_bit(EV_SW, g_virtual_input->evbit);


    /* 2.2 能產生這類事件中的哪些 */
    /* headset = 聽筒 + MIC = SW_HEADPHONE_INSERT + SW_MICROPHONE_INSERT
     *    同時上報 SW_HEADPHONE_INSERT 和 SW_MICROPHONE_INSERT, 就表示headset
     *    為了簡化, 對於android系統只上報SW_MICROPHONE_INSERT也表示headset
     */
    set_bit(SW_HEADPHONE_INSERT, g_virtual_input->swbit);
    set_bit(SW_MICROPHONE_INSERT, g_virtual_input->swbit);
    set_bit(SW_LINEOUT_INSERT, g_virtual_input->swbit);

    /* 2.3 這些事件的范圍 */

    g_virtual_input->name = "alsa_switch"; /* 不重要 */

    /* 注冊 */
    err = input_register_device(g_virtual_input);
    if (err) {
        input_free_device(g_virtual_input);
        printk("input_register_device for virtual jack err!\n");
        return err;
    }

    /*
     * sysfs文件創建方法:
     * 創建/sys/class/input/inputX/test_input文件
     *   可以執行類似下面的命令來模擬耳麥的動作:
     *       觸發上報headset插入: echo 4 1 > /sys/class/input/inputX/test_input
     *       觸發上報headset取下: echo 4 0 > /sys/class/input/inputX/test_input
     *       觸發上報headphone插入: echo 2 1 > /sys/class/input/inputX/test_input
     *       觸發上報headphonet取下: echo 2 0 > /sys/class/input/inputX/test_input
     */
    err = device_create_file(&g_virtual_input->dev, &dev_attr_test_input);
    if (err) {
        printk("device_create_file for test_input err!\n");
        input_unregister_device(g_virtual_input);
        input_free_device(g_virtual_input);
        return err;
    }

    return 0;
}

static void unregister_input_device_for_jack(void)
{
    device_remove_file(&g_virtual_input->dev, &dev_attr_test_input);

    input_unregister_device(g_virtual_input);
    input_free_device(g_virtual_input);
}


/**************************************************************************************************************/

static struct switch_dev g_virtual_switch;

static ssize_t state_test_store(struct device *dev,
                     struct device_attribute *attr,
                     const char *buf, size_t count)
{
    long val;

    val = simple_strtol(buf, NULL, 0);

    printk("emulate to report swtich state: 0x%x\n", val);
    switch_set_state(&g_virtual_switch, val);

    return count;
}

static DEVICE_ATTR(test_state, S_IRUGO | S_IWUSR, NULL, state_test_store);

static int register_switch_device_for_jack(void)
{
    int err;

    g_virtual_switch.name = "h2w"; /*名字必須是這個,Android系統中使用它判斷*/
    err = switch_dev_register(&g_virtual_switch);
    if (err) {
        printk("switch_dev_register h2w err!\n");
        return err;
    }

    /* 創建/sys/class/switch/h2w/test_state文件
     *   可以執行類似下面的命令來模擬耳麥的動作:
     *       觸發上報headset插入: echo 1 > /sys/class/switch/h2w/test_state
     *       觸發上報headset取下: echo 0 > /sys/class/switch/h2w/test_state
     */
    err = device_create_file(g_virtual_switch.dev, &dev_attr_test_state);
    if (err) {
        printk("device_create_file test err!\n");
        switch_dev_unregister(&g_virtual_switch);
        return err;
    }

    return 0;
}

static void unregister_switch_device_for_jack(void)
{
    device_remove_file(g_virtual_switch.dev, &dev_attr_test_state);
    switch_dev_unregister(&g_virtual_switch);
}

/**************************************************************************************************************/

static int __init virtual_jack_init(void)
{
    int err;

    err = register_input_device_for_jack();
    err = register_switch_device_for_jack();

    return 0;
}

static void __exit virtual_jack_exit(void)
{
    unregister_input_device_for_jack();
    unregister_switch_device_for_jack();
}

module_init(virtual_jack_init);
module_exit(virtual_jack_exit);

MODULE_AUTHOR("weidongshan@qq.com");
MODULE_DESCRIPTION("Virutal jack driver for sound card");
MODULE_LICENSE("GPL");

/*
對應的Makefile:
KERN_DIR = /media/ubuntu/works/tiny4412/linux-3.0.86

obj-m    += virtual_jack.o

all:
    make -C $(KERN_DIR) M=`pwd` modules 

clean:
    make -C $(KERN_DIR) M=`pwd` modules clean
    rm -rf modules.order

*/
View Code

 

二、在狀態欄顯示耳麥圖標

1. 優秀相關博文:

Android4.4監聽耳機插入處理方法:https://blog.csdn.net/anndy_peng/article/details/30240135
直接給出的是補丁和圖片①,可直接參考它來實現再狀態欄上添加耳塞圖標的App:
https://github.com/fire855/android_frameworks_base-mtk/commit/7661c081b037a32e273afaf70349a6a1518dab48

[整理]Android屏幕適配(不同的屏幕分辨率和尺寸),選擇mic的圖標大小的時候會使用到:https://blog.csdn.net/ttkatrina/article/details/50623043

2. 參考①,tiny4412上操作步驟:

a. 確定在狀態欄上圖標的位置
   修改 frameworks/base/core/res/res/values/config.xml
   添加一行:<item><xliff:g id="id">headset</xliff:g></item>

b. 創建圖標文件
   從①中下載對應圖片保存在如下位置
   frameworks/base/packages/SystemUI/res/drawable-hdpi/stat_sys_headset_with_mic.png
   frameworks/base/packages/SystemUI/res/drawable-hdpi/stat_sys_headset_without_mic.png

c. 修改源碼, 當接收到消息時顯示/清除圖標 : 
   frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java
   參考①中的修改。
   
d. 編譯源文件
d.1 mmm frameworks/base/core/res    // 編譯config.xml
    得到  out/target/product/tiny4412/system/framework/framework-res.apk
d.2 mmm frameworks/base/packages/SystemUI   // 編譯圖標文件, 編譯源碼
    得到 out/target/product/tiny4412/system/priv-app/SystemUI/SystemUI.apk

e. 替換單板上的文件
   adb push到 /system/framework/framework-res.apk
   adb push到 /system/priv-app/SystemUI/SystemUI.apk 

f. 測試
   使用5.1所編譯得到的zImage啟動開發板,
   執行以下命令:
     觸發上報headset插入:   echo 4 1 > /sys/class/input/input0/test_input  #4=SW_MICROPHONE_INSERT,1=PlugIn
     觸發上報headset取下:   echo 4 0 > /sys/class/input/input0/test_input  
     觸發上報headphone插入: echo 2 1 > /sys/class/input/input0/test_input  #2=SW_HEADPHONE_INSERT,1=PlugIn
     觸發上報headphone取下: echo 2 0 > /sys/class/input/input0/test_input  
     觸發上報lineout插入:   echo 6 1 > /sys/class/input/input0/test_input  #6=SW_LINEOUT_INSERT,1=PlugIn
     觸發上報lineout取下:   echo 6 0 > /sys/class/input/input0/test_input  

測試結果:input的測試成功。但是uevent的失敗,原因見下文

 

三、耳麥拔插事件調用流程分析

1. Android系統使用input子系統還是使用Switch class(uevent)上報拔插操作,取決於 config_useDevInputEventForAudioJack 配置值,
該值為true時使用input子系統, 為false時使用uevent機制, 該值在下述文件中定義, 后一個文件會覆蓋前一個文件,目前在4412的配置文件
中選擇的是使用輸入子系統的。
frameworks/base/core/res/res/values/config.xml
device/friendly-arm/tiny4412/overlay/frameworks/base/core/res/res/values/config.xml

mUseDevInputEventForAudioJack的賦值:
InputManagerService(Context context) //InputManagerService.java
    mUseDevInputEventForAudioJack = context.getResources().getBoolean(R.bool.config_useDevInputEventForAudioJack);

 

2. 輸入子系統對耳麥插拔事件上報流程

InputReader::loopOnce() //InputReader.cpp
    mEventHub->getEvents(timeoutMillis, mEventBuffer, EVENT_BUFFER_SIZE);
    processEventsLocked(mEventBuffer, count);
        InputReader::processEventsForDeviceLocked
            device->process(rawEvents, count); //輸入子系統中為每一個/dev/input/eventX都創建一個InputDevice
                mapper->process(rawEvent); //對於開關事件,這里的InputMapper mapper是SwitchInputMapper
                    SwitchInputMapper::process(const RawEvent* rawEvent)
                        case EV_SW: processSwitch(rawEvent->code, rawEvent->value); //只是簡單記錄下來按鍵值
                        case EV_SYN: sync(rawEvent->when); //收到sync(type=EV_SYN,code=SYN_REPORT)后處理
                            //獲取監聽器,調用監聽器的notifySwitch,這里的監聽器是InputDispatch
                            getListener()->notifySwitch(&args);

InputDispatcher::notifySwitch(const NotifySwitchArgs* args) //InputDispatcher.cpp
    mPolicy->notifySwitch(args->eventTime, args->switchValues, args->switchMask, policyFlags);
    //上面調用的就是com_android_server_input_InputManagerService.cpp中的下面這個函數
    NativeInputManager::notifySwitch(nsecs_t when, uint32_t switchValues, uint32_t switchMask, uint32_t policyFlags) //com_android_server_input_InputManagerService.cpp
        //調用java的同名函數notifySwitch,位於InputManagerService.java中
        env->CallVoidMethod(mServiceObj, gServiceClassInfo.notifySwitch, when, switchValues, switchMask);
            notifySwitch(long whenNanos, int switchValues, int switchMask) //InputManagerService.java
                //調用到mWiredAccessoryCallbacks中的notifyWiredAccessoryChanged
                if (mUseDevInputEventForAudioJack) //這個就是來自上面的config.xml文件配置,為true才會選擇輸入子系統
                    mWiredAccessoryCallbacks.notifyWiredAccessoryChanged(whenNanos, switchValues, switchMask);

mWiredAccessoryCallbacks是何時注冊的:
startOtherServices() //SystemServer.java
    //WiredAccessoryManager作為一個callback傳給了inputManager
    inputManager.setWiredAccessoryCallbacks(new WiredAccessoryManager(context, inputManager));
        mWiredAccessoryCallbacks = callbacks;

因此上面調用的就是
WiredAccessoryManager.notifyWiredAccessoryChanged
mSwitchValues = (mSwitchValues & ~switchMask) | switchValues;
    //根據上報的值確定變量headset的取值,顯示那個圖標由變量headset決定的
    switch (mSwitchValues & (SW_HEADPHONE_INSERT_BIT | SW_MICROPHONE_INSERT_BIT | SW_LINEOUT_INSERT_BIT)) {
        case 0: headset = 0; break;
        case SW_HEADPHONE_INSERT_BIT: headset = BIT_HEADSET_NO_MIC; break;
        case SW_LINEOUT_INSERT_BIT: headset = BIT_LINEOUT; break;
        case SW_HEADPHONE_INSERT_BIT | SW_MICROPHONE_INSERT_BIT: headset = BIT_HEADSET; break;
        case SW_MICROPHONE_INSERT_BIT: headset = BIT_HEADSET; break;
        default: headset = 0; break;
    }
    //WiredAccessoryManager的函數,這個函數將會進入Audio系統,uevent分支最終也是調用這個函數。
    updateLocked(NAME_H2W, (mHeadsetState & ~(BIT_HEADSET | BIT_HEADSET_NO_MIC | BIT_LINEOUT)) | headset);

 

3. Switch class使用uevent對耳麥插拔事件上報流程

有一個UeventThread線程,循環讀取socket的uevent事件並且發送uevent.

(1)初始化:
WiredAccessoryManager //WiredAccessoryManager.java
    //創建一個觀察者
    mObserver = new WiredAccessoryObserver();
        //WiredAccessoryObserver構造函數調用
        WiredAccessoryObserver() 
            //創建一個觀察者事件的列表
            mUEventInfo = makeObservedUEventList();
                //當上面的config.xml中配置這個變量為false時就使用uevent機制
                if (!mUseDevInputEventForAudioJack) {
                    //用於監聽名為"h2w"這個Switch class驅動中創建的虛擬文件
                    //shell@tiny4412:/sys # find ./ -name h2w                                        
                    //./devices/virtual/switch/h2w
                    //./class/switch/h2w
                    //這里只監聽這三個事件,所以驅動程序通過uevent上報事件也只能上報這三個事件之一
                    uei = new UEventInfo("h2w", BIT_HEADSET, BIT_HEADSET_NO_MIC, BIT_LINEOUT);
        
        
init() //WiredAccessoryManager.java
    startObserving("DEVPATH="+uei.getDevPath());    
        //往UEventThread里面添加觀察者
        UEventThread t = getThread();
        //這里的this就是子類WiredAccessoryObserver
        t.addObserver(match, this);    //


(2)從native代碼中獲取uevent事件
run()
    //本地初始化
    nativeSetup();
        uevent_init() //android_os_UEventObserver.cpp
            s = socket(PF_NETLINK, SOCK_DGRAM, NETLINK_KOBJECT_UEVENT); //hardware/uevent.c
            setsockopt(s, SOL_SOCKET, SO_RCVBUFFORCE, &sz, sizeof(sz));
            bind(s, (struct sockaddr *) &addr, sizeof(addr))
    //等待下一個事件
    nativeWaitForNextEvent(); //UEventObserver.java
        uevent_next_event(char* buffer, int buffer_length) //hardware/uevent.c
            poll(&fds, 1, -1);
            recv(fd, buffer, buffer_length, 0);
    //讀取到數據后發送
    sendEvent(message);
        observer.onUEvent(event);
            //取出上面①中設置的觀察者
            UEventObserver observer = mTempObserversToSignal.get(i);
            observer.onUEvent(UEventObserver.UEvent event) //WiredAccessoryManager.java
                String devPath = event.get("DEVPATH");
                String name = event.get("SWITCH_NAME");
                int state = Integer.parseInt(event.get("SWITCH_STATE"));
                updateStateLocked(devPath, name, state);
                    //WiredAccessoryManager的函數,兩條分支(input和Switch class)在這里匯合了
                    updateLocked(String newName, int newState)

 

4. uevent實現和input實現在這里匯合位置

updateLocked(String newName, int newState) //WiredAccessoryManager.java
    //獲得message,然后發送message
    Message msg = mHandler.obtainMessage(MSG_NEW_DEVICE_STATE, headsetState, mHeadsetState, newName);
    mHandler.sendMessage(msg);

上面發送消息會觸發消息的handler被調用:
handleMessage(Message msg) {
case MSG_NEW_DEVICE_STATE:
    setDevicesState(msg.arg1, msg.arg2, (String)msg.obj);
        setDeviceStateLocked(curHeadset, headsetState, prevHeadsetState, headsetName);
            //終於進入了Audio系統了,設置有線設備的連接狀態
            mAudioManager.setWiredDeviceConnectionState(inDevice, state, headsetName);
                IAudioService service = getService(); //AudioManager.java
                service.setWiredDeviceConnectionState(device, state, name);
                    //指定一個延時時間后又把消息放到一個消息隊列中
                    delay = checkSendBecomingNoisyIntent(device, state);
                    queueMsgUnderWakeLock(mAudioHandler, MSG_SET_WIRED_DEVICE_CONNECTION_STATE, delay);
                        //會觸發消息的handler被調用
                        handleMessage(Message msg) 
                             onSetWiredDeviceConnectionState(msg.arg1, msg.arg2, (String)msg.obj);
                                sendDeviceConnectionIntent(device, state, name);
                                    //構建一個Intent結構,然后向應用程序廣播它,注冊對這個Intent感興趣的App就會收到它
                                    Intent intent = new Intent();
                                    intent.putExtra("state", state);
                                    intent.putExtra("name", name);    
                                    intent.setAction(Intent.ACTION_HEADSET_PLUG);                        
                                    ActivityManagerNative.broadcastStickyIntent(intent, null, UserHandle.USER_ALL);

 

5. App注冊感興趣的Intent:

//構造函數中添加
PhoneStatusBarPolicy(Context context, CastController cast) //PhoneStatusBarPolicy.java
    filter.addAction(Intent.ACTION_HEADSET_PLUG);

收到消息后onReceive被調用,updateHeadset是處理函數
onReceive(Context context, Intent intent)
if (action.equals(Intent.ACTION_HEADSET_PLUG))
    updateHeadset(intent);

在狀態欄中設置head set圖標的狀態
private final void updateHeadset(Intent intent) {
    final String action = intent.getAction();
    final int state = intent.getIntExtra("state", 4);
    final int mic = intent.getIntExtra("microphone", 4);

    switch (state) {
        case 0: //拔出
            mService.setIconVisibility("headset", false);
    break;
        case 1: //插入
            if (mic == 1) { //耳機上有mic顯示這張圖標
                mService.setIcon("headset", R.drawable.stat_sys_headset_with_mic, 0, null);
            } else {         //耳機上沒有mic顯示這張圖標
                mService.setIcon("headset", R.drawable.stat_sys_headset_without_mic, 0, null);
            }
            mService.setIconVisibility("headset", true);
            break;
    }
}

6. native函數有些是在framework中,有些是在system中,有些是在hardware中,有些是在external中。

 

四、切換聲音通道流程

1. 切換聲音通道流程
a.在驅動程序中切換:
比如: 插上耳麥發生中斷, 在中斷處理程序中設置聲卡讓聲音從耳機中輸出
b.把輸出通道的選擇權交給android系統
目前這是主流的做法,驅動應該提供切換的能力,但是不應該替App進行決策。此處講解此種方法。


3. 有個查看Android源碼非常方便的網站:androidxref.com/5.0.0_r2/
輸入audio_policy.conf,選擇右邊的"select all", 點search

一個完整復雜的audio_policy.conf的例子:http://androidxref.com/5.0.0_r2/xref/device/asus/grouper/audio_policy.conf

4. App只指定stream type, 從哪個聲卡播放出去那是AudioFlinger的內容。

5. output支持哪些device在策略配置文件/system/etc/audio_policy.conf(/etc下也有?)中就指定了。

6. 對應關系:
一個output對應一個播放線程,也對應一個聲卡,可能對應多個device; 一個device對應一個播放設備,比如喇叭或者耳機.

7. 怎樣切換通道:

主要涉及的文件是AudioPolicyManager.cpp,參考0006_handleDeviceConnection UML

一旦插上USB聲卡,就會創建一個對應的播放線程

驅動程序上報音頻拔插事件, 該事件為某個device插入或拔出, Android系統需要切換聲音通道。

過程為(核心文件為frameworks/av/services/audiopolicy/AudioPolicyManager.cpp): (核心函數為 setDeviceConnectionState)
a. checkOutputsForDevice
針對該device, 打開新的output, 創建新的playbackthread.
方法:
從audio_policy.conf中確定"本該有多少個output"可以支持它, mOutputs表示"已經打開的output", 兩者對比即可確定"尚未打開的output"

b. checkOutputForAllStrategies / checkOutputForStrategy
對所有的strategy分組聲音, 判斷是否需要遷移到新的output, 如果需要則遷移對應Track到新的output,方法: 
b.1 判斷是否需要遷移
對於該strategy, 得到它的oldDevice, 進而得到它的outputs (srcOutputs);
對於該strategy, 得到它的newDevice, 進而得到它的outputs (dstOutputs);
如果這2個srcOutputs、dstOutputs不相同, 表示需要遷移

b.2 如果遷移:
把對應的Track設置為invalidate狀態即可, App寫AudioTrack時發現它是invalidate狀態, 就會重新創建新的Track
    audio_devices_t oldDevice = getDeviceForStrategy(strategy, true /*fromCache*/);
    audio_devices_t newDevice = getDeviceForStrategy(strategy, false /*fromCache*/);
    SortedVector<audio_io_handle_t> srcOutputs = getOutputsForDevice(oldDevice, mPreviousOutputs);
    SortedVector<audio_io_handle_t> dstOutputs = getOutputsForDevice(newDevice, mOutputs);

c. getNewOutputDevice/setOutputDevice
這要操作HAL

 

8. output的參數信息會構成一個profile結構描述

audio_hw_modules {
  primary {
    outputs {
      primary {    //這些參數信息會構造成一個profile

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM