AudioManager.playSoundEffect
用于播放预定义的音效.通常用于移动应用程序和游戏中,以在执行某些操作时向用户提供音频反馈,例如按钮按下或菜单选择。AudioManager类提供了许多预定义的音效,可以播放,开发人员也可以创建自己的自定义音效以在其应用程序中使用。
一. app 调用后流程:
AudioManager.playSoundEffect -> AudioService.playSoundEffect -> SoundEffectsHelper.playSoundEffect -> SoundEffectsHelper.onLoadSoundEffects(重点)
在onLoadSoundEffects方法中主要完成以下几件事:
- 调用 loadSoundAssets 方法解析 audio_assets.xml 获取声音资产信息 (音源id,音源文件名,会有一个 mResource 列表来保存),音源 Id 就对应 AudioManager 中的定义的 FX_XXX
- 获取音源文件路径(system/media/audio/ui || product/media/audio/ui),调用SoundPool.load方法从获取的路径加载声音并返回声音 ID
- 将声音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, 使用扩展的值即可。