AudioManager.playSoundEffect 调用流程及扩展

853 阅读3分钟

AudioManager.playSoundEffect

用于播放预定义的音效.通常用于移动应用程序和游戏中,以在执行某些操作时向用户提供音频反馈,例如按钮按下或菜单选择。AudioManager类提供了许多预定义的音效,可以播放,开发人员也可以创建自己的自定义音效以在其应用程序中使用。

一. app 调用后流程:

AudioManager.playSoundEffect -> AudioService.playSoundEffect -> SoundEffectsHelper.playSoundEffect -> SoundEffectsHelper.onLoadSoundEffects(重点)

在onLoadSoundEffects方法中主要完成以下几件事:

  1. 调用 loadSoundAssets 方法解析 audio_assets.xml 获取声音资产信息 (音源id,音源文件名,会有一个 mResource 列表来保存),音源 Id 就对应 AudioManager 中的定义的 FX_XXX
  2. 获取音源文件路径(system/media/audio/ui || product/media/audio/ui),调用SoundPool.load方法从获取的路径加载声音并返回声音 ID
  3. 将声音ID保存, 作为 SoundPool.play 的参数。
// frameworks/base/services/core/java/com/android/server/audio/SoundEffectsHelper.java 
private void loadSoundAssets() {
    XmlResourceParser parser = null;
​
    // only load assets once.
    if (!mResources.isEmpty()) {
        return;
    }
    
    //1.在音源列表中添加默认音源 mResources.add(new Resource("Effect_Tick.ogg"));
    //2.初始化数组 mEffects,存储默认音源index Arrays.fill(mEffects, defaultResourceIdx);
    // mEffects 会在接下来的 assets.xml 解析中更新完成 =》 ※1
    loadSoundAssetDefaults();
​
    try {
        // 解析 audio_assets.xml 获取声音资产信息
        parser = mContext.getResources().getXml(com.android.internal.R.xml.audio_assets);
​
        XmlUtils.beginDocument(parser, TAG_AUDIO_ASSETS);
        String version = parser.getAttributeValue(null, ATTR_VERSION);
        Map<Integer, Integer> parserCounter = new HashMap<>();
        if (ASSET_FILE_VERSION.equals(version)) {
            while (true) {
                XmlUtils.nextElement(parser);
                String element = parser.getName();
                if (element == null) {
                    break;
                }
                if (element.equals(TAG_GROUP)) {
                    String name = parser.getAttributeValue(null, ATTR_GROUP_NAME);
                    if (!GROUP_TOUCH_SOUNDS.equals(name)) {
                        Log.w(TAG, "Unsupported group name: " + name);
                    }
                } else if (element.equals(TAG_ASSET)) {
                    String id = parser.getAttributeValue(null, ATTR_ASSET_ID);
                    String file = parser.getAttributeValue(null, ATTR_ASSET_FILE);
                    int fx;
​
                    try {
                        Field field = AudioManager.class.getField(id);
                        fx = field.getInt(null);
                    } catch (Exception e) {
                        Log.w(TAG, "Invalid sound ID: " + id);
                        continue;
                    }
                    int currentParserCount = parserCounter.getOrDefault(fx, 0) + 1;
                    parserCounter.put(fx, currentParserCount);
                    if (currentParserCount > 1) {
                        Log.w(TAG, "Duplicate definition for sound ID: " + id);
                    }
                    // ※1 将解析到的音源信息 保存到 mResource 中,并将 mResource中的索引填充到 mEffect 数组中。
                    /* mResource 列表为 {fileName,mSampleId,,Loaded}
                        [
                            {"Effect_Tick.ogg", 0, false},
                            {"KeypressStandard.ogg", 0, false},
                            ......
                        ]
                    */
                    // mSampleId 在后续 load 音源文件时确定 ※2
                    // meffect 为用于后续查找 Resource, mResource.get(mEffect[effect])
                    mEffects[fx] = findOrAddResourceByFileName(file);
                } else {
                    break;
                }
            }
​
            boolean navigationRepeatFxParsed = allNavigationRepeatSoundsParsed(parserCounter);
            boolean homeSoundParsed = parserCounter.getOrDefault(AudioManager.FX_HOME, 0) > 0;
            if (navigationRepeatFxParsed || homeSoundParsed) {
                AudioManager audioManager = mContext.getSystemService(AudioManager.class);
                if (audioManager != null && navigationRepeatFxParsed) {
                    audioManager.setNavigationRepeatSoundEffectsEnabled(true);
                }
                if (audioManager != null && homeSoundParsed) {
                    audioManager.setHomeSoundEffectEnabled(true);
                }
            }
        }
    } catch (Resources.NotFoundException e) {
        Log.w(TAG, "audio assets file not found", e);
    } catch (XmlPullParserException e) {
        Log.w(TAG, "XML parser exception reading sound assets", e);
    } catch (IOException e) {
        Log.w(TAG, "I/O exception reading sound assets", e);
    } finally {
        if (parser != null) {
            parser.close();
        }
    }
}

经过解析 assets.xml 后 音源信息被保存到 mResource 中,之后 load 声音并赋值mSampleId,作为 SoundPool.play 的参数。

private void onLoadSoundEffects(OnEffectsLoadCompleteHandler onComplete) {
    // ...
    int resourcesToLoad = 0;
    for (Resource res : mResources) {
        String filePath = getResourceFilePath(res);
        int sampleId = mSoundPool.load(filePath, 0);
        if (sampleId > 0) {
            res.mSampleId = sampleId;
            res.mLoaded = false;
            resourcesToLoad++;
        } else {
            logEvent("effect " + filePath + " rejected by SoundPool");
            Log.w(TAG, "SoundPool could not load file: " + filePath);
        }
    }
    // ...
}

最后就是播放声音, onPlaySoundEffect

void onPlaySoundEffect(int effect, int volume) {
    float volFloat;
    // use default if volume is not specified by caller
    if (volume < 0) {
        volFloat = (float) Math.pow(10, (float) mSfxAttenuationDb / 20);
    } else {
        volFloat = volume / 1000.0f;
    }
​
    Resource res = mResources.get(mEffects[effect]);
    if (mSoundPool != null && res.mSampleId != EFFECT_NOT_IN_SOUND_POOL && res.mLoaded) {
        // 有匹配的音源调用 SoundPool 的 play 播放。
        mSoundPool.play(res.mSampleId, volFloat, volFloat, 0, 0, 1.0f);
    } else {
        MediaPlayer mediaPlayer = new MediaPlayer();
        try {
            String filePath = getResourceFilePath(res);
            mediaPlayer.setDataSource(filePath);
            mediaPlayer.setAudioStreamType(AudioSystem.STREAM_SYSTEM);
            mediaPlayer.prepare();
            mediaPlayer.setVolume(volFloat);
            mediaPlayer.setOnCompletionListener(new OnCompletionListener() {
                public void onCompletion(MediaPlayer mp) {
                    cleanupPlayer(mp);
                }
            });
            mediaPlayer.setOnErrorListener(new OnErrorListener() {
                public boolean onError(MediaPlayer mp, int what, int extra) {
                    cleanupPlayer(mp);
                    return true;
                }
            });
            mediaPlayer.start();
        } catch (IOException ex) {
            Log.w(TAG, "MediaPlayer IOException: " + ex);
        } catch (IllegalArgumentException ex) {
            Log.w(TAG, "MediaPlayer IllegalArgumentException: " + ex);
        } catch (IllegalStateException ex) {
            Log.w(TAG, "MediaPlayer IllegalStateException: " + ex);
        }
    }
}

二.如何扩展 /修改 SoundEffect

根据上述调用流程来确定需要修改或增加的文件

1、首先我们调用了 AudioManager.playSoundEffect(int effectType)【这个函数有多个重载,主题思想一样,殊途同归】,其中的参数 effectType 是声音效果标识符,我们如果扩展首先要添加需要的 声音效果标识,如添加 FX_KEYPRESS_GAME 按键游戏音效, 同时需要把音效数量更新

// frameworks/base/media/java/android/media/AudioManager.java/* Game button sound
     *  @see #playSoundEffect(int)
     */
    public static final int FX_KEYPRESS_GAME = 16;
    /**
     * @hide Number of sound effects
     */
    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
//  public static final int NUM_SOUND_EFFECTS = 16;
    public static final int NUM_SOUND_EFFECTS = 17;

如果添加的音效不是被公开使用可以设置为 @hide,这样可以省去用 make update-api 更新这一步(毕竟Android 编译都不快)。

我这里是 app 直接调用所以需要把这个变量公开。

2、中间过程都不需要修改, 到了 onLoadSoundEffects, 需要解析 assets.xml 文件,上述说到音源id 与 AudioManager中定义的 FX_XX 对应,所以我们需要再 xml 中添加 FX_KEYPRESS_GAME 按键游戏音的音源信息.

<!--frameworks/base/core/res/res/xml/audio_assets.xml-->
<audio_assets version="1.0">
    <asset id="FX_KEY_CLICK" file="Effect_Tick.ogg"/>
    <asset id="FX_FOCUS_NAVIGATION_UP" file="Effect_Tick.ogg"/>
    <asset id="FX_FOCUS_NAVIGATION_DOWN" file="Effect_Tick.ogg"/>
    <asset id="FX_FOCUS_NAVIGATION_LEFT" file="Effect_Tick.ogg"/>
    <asset id="FX_FOCUS_NAVIGATION_RIGHT" file="Effect_Tick.ogg"/>
    <asset id="FX_KEYPRESS_STANDARD" file="KeypressStandard.ogg"/>
    <asset id="FX_KEYPRESS_SPACEBAR" file="KeypressSpacebar.ogg"/>
    <asset id="FX_KEYPRESS_DELETE" file="KeypressDelete.ogg"/>
    <asset id="FX_KEYPRESS_RETURN" file="KeypressReturn.ogg"/>
    <asset id="FX_KEYPRESS_INVALID" file="KeypressInvalid.ogg"/>
    <asset id="FX_BACK" file="Effect_Tick.ogg"/>
    <asset id="FX_KEYPRESS_GAME" file="Game.ogg"/>
</audio_assets>
​

3、 xml 中的 音源文件【file】我们需要 push 到 设备中。

frameworks/base/data/sounds/

3.1、可以直接手动 push 到对应目录,及上文说到的 system/media/audio/ui || product/media/audio/ui 这两个文件都可以,

3.2、也可以直接在目录 frameworks/base/data/sounds/ 下的 AllAudio.mk 中添加配置,需要同步把 音源文件添加到 frameworks/base/data/sounds/effects/ogg/ 中,也可以自己开个文件夹添加进去, 但在 mk 文件中要写对路径。

# 在 AllAudio.mk 中找个自己觉得合适的位置添加即可
$(LOCAL_PATH)/effects/ogg/Game.ogg:$(TARGET_COPY_OUT_PRODUCT)/media/audio/ui/Game.ogg \

3.3、也可以通过使用 overlay 的方式来添加配置(推荐),这样就不需要在源码上修改, 对于后续的扩展更友好。【待更新】

三.结束

这就完成了一个按键游戏音效的扩展, 后面直接调用 AudioMananger.playSoundEffect, 使用扩展的值即可。