车载设置--按键提示音

使用场景
一般在车载应用设置中,有一个 按键提示音 的功能开关,此开关的作用类似手机上的按键音,当用户点击某个按钮时,会发出 BB 声。但由于车载系统的特殊性,当用户从多媒体切换到收音机时,arm 端(多媒体,按键音)的声音是发不出来的。具体原因根据硬件的原理图来解释:
这里写图片描述
DSP 音频芯片主要用来处理声音的合成与混音,通常为四选一或者六选一,就是四路通道经过 DSP 处理只有一路的声音能够输出。通常多媒体,蓝牙电话,导航报点,按键音都是属于 arm 通道,而收音机则属于 Radio 通道,两者经过 DSP 的处理只能有一路的声音到达功放发出声音。进一步解释就是,当用户点击收音机图标进入收音机应用时,arm 告诉 MCU 此时用户切到了 Radio 通道,此时 MCU 会控制 DSP 只输出 Radio 的声音。如果此时用户再点击多媒体,而 arm 却没有通知 MCU 切换到 arm 通道,此时多媒体的声音仍然是听不到的,听到的仍然是收音机的声音,因为此时 DSP 只输出 Radio 通道的声音。同理,当用户在收音机界面时,DSP 只输出 Radio 的声音。此时用户点击按钮,按钮发出的声音由于属于 arm 端,是不会被 DSP 处理的,也就是用户听不到 BB 声,此时给用户的体验是不好的。所以如果遵从 Android 原生设计,声音只由 arm 发出,而在某些应用按键音无效,这就是一个很大的漏洞。但车载硬件在设计之初就考虑到了这个问题,beep 声由 MCU 发出,通过原理图可以看出,MCU 能通过发声电路发出一个 PWM 的脉冲信号到功放继而发出声音,所以,无论我们在那路通道,只要将按键事件告诉 MCU 即可。所以,我们想要实现 Beep 声,只需要将点击事件告诉 MCU 即可。而 Android 原生就有一套发声机制,为何不用?

Android 按键音原理
在 Android 系统中,所有的控件,只要监听了点击事件,当点击事件发生时,此时会有 beep 声。基于此,声音的发出应该是在控件的基类 View 中,先从 View 的 onTouchEvent 看起:

{
      // Use a Runnable and post this rather than calling
     // performClick directly. This lets other visual state
   // of the view update before click actions start.
          if (mPerformClick == null) {
                mPerformClick = new PerformClick();
                  }
         if (!post(mPerformClick)) {
            performClick();
                 }
 }  

performClick 大家都比较熟悉,主要用来处理点击事件,代码如下:

 public boolean performClick() {
        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);

        ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnClickListener != null) {
            playSoundEffect(SoundEffectConstants.CLICK);
            li.mOnClickListener.onClick(this);
            return true;
        }

        return false;
    }

playSoundEffect 从命名上猜测为按键音的发音函数,代码如下:

/**
     * Play a sound effect for this view.
     *
     * <p>The framework will play sound effects for some built in actions, such as
     * clicking, but you may wish to play these effects in your widget,
     * for instance, for internal navigation.
     *
     * <p>The sound effect will only be played if sound effects are enabled by the user, and
     * {@link #isSoundEffectsEnabled()} is true.
     *
     * @param soundConstant One of the constants defined in {@link SoundEffectConstants}
     */
    public void playSoundEffect(int soundConstant) {
        if (mAttachInfo == null || mAttachInfo.mRootCallbacks == null || !isSoundEffectsEnabled()) {
            return;
        }
        mAttachInfo.mRootCallbacks.playSoundEffect(soundConstant);
    }

mAttachInfo 又是什么鬼?从类的定义可知,里面有个 Callbacks 的接口,里面有个 playSoundEffect 的方法,所以实现了此接口的类应该就能发出 Beep 声,从注释可知其真正的方法写在 parent window 中, parent window 就是ViewRoot ,实现该回调接口:

 /**
     * A set of information given to a view when it is attached to its parent
     * window.
     */
    static class AttachInfo {
        interface Callbacks {
            void playSoundEffect(int effectId);
            boolean performHapticFeedback(int effectId, boolean always);
        }

接着看 ViewRoot 的 playSoundEffect 函数。

    public void playSoundEffect(int effectId) {
        checkThread();

        if (mMediaDisabled) {
            return;
        }

        try {
            final AudioManager audioManager = getAudioManager();

            switch (effectId) {
                case SoundEffectConstants.CLICK:
                    audioManager.playSoundEffect(AudioManager.FX_KEY_CLICK);
                    return;
                case SoundEffectConstants.NAVIGATION_DOWN:
                    audioManager.playSoundEffect(AudioManager.FX_FOCUS_NAVIGATION_DOWN);
                    return;
                case SoundEffectConstants.NAVIGATION_LEFT:
                    audioManager.playSoundEffect(AudioManager.FX_FOCUS_NAVIGATION_LEFT);
                    return;
                case SoundEffectConstants.NAVIGATION_RIGHT:
                    audioManager.playSoundEffect(AudioManager.FX_FOCUS_NAVIGATION_RIGHT);
                    return;
                case SoundEffectConstants.NAVIGATION_UP:
                    audioManager.playSoundEffect(AudioManager.FX_FOCUS_NAVIGATION_UP);
                    return;
                default:
                    throw new IllegalArgumentException("unknown effect id " + effectId +
                            " not defined in " + SoundEffectConstants.class.getCanonicalName());
            }
        } catch (IllegalStateException e) {
            // Exception thrown by getAudioManager() when mView is null
            Log.e(TAG, "FATAL EXCEPTION when attempting to play sound effect: " + e);
            e.printStackTrace();
        }
    }

在函数中主要通过 audioManager 调用 playSoundEffect ,实际是通过 AIDL 调用 AudioService 的 onPlaySoundEffect 方法。 `至此,Androd 原生的 beep 声原理弄清楚了。而我们需要做的是将 playSoundEffect 里的函数屏蔽掉,加上我们自己的逻辑,即通知 MCU 发声。

移植流程
在知道了 Android 原生的 View 按键音的最终实现是在 AudioService 的onPlaySoundEffect 方法中,那么我们可以在函数体里面屏蔽原功能,增加我们自己的处理逻辑。
第一步:参考 \frameworks\base\media\java\android\media 里面的 AIDL 的代码,创建文件

IPlaySoundEffectListener.aidl
package android.media;

oneway interface IPlaySoundEffectListener{ 
     void onPlaySoundEffect();
}

第二步:在接口 IAudioService 增加 setOnPlaySoundEffectListener(in IPlaySoundEffectListener l), 在 AudioService 中实现该方法 。

 private  IPlaySoundEffectListener mPlaySoundEffectListener;

 @Override
    public void setOnPlaySoundEffectListener(IPlaySoundEffectListener l){
         mPlaySoundEffectListener = l;
    }

  private void onPlaySoundEffect(int effectType, int volume) {
            synchronized (mSoundEffectsLock) {
             try {
                if(mPlaySoundEffectListener!=null){
                     mPlaySoundEffectListener.onPlaySoundEffect();
                  }
                }catch (Exception ex) {
                   Log.w(TAG, "onPlaySoundEffect: "+ex);
                } 
    }

应用
在应用层,通过 getSystemService(Context.AUDIO_SERVICE) 获取 AudioService 来注册回调监听,当发生点击事件时,会回调到我们接口函数,继而处理我们自己的逻辑通知 MCU 发声。

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值