Merge "Refactor SoundEffectsHelper for asynchronous loading" am: 70532e5231

am: d10d68dec1

Change-Id: I38e80b45290e9928a229353952a65cf694232b3c
This commit is contained in:
Mikhail Naganov
2019-09-10 11:12:38 -07:00
committed by android-build-merger
2 changed files with 320 additions and 267 deletions

View File

@@ -6052,6 +6052,8 @@ public class AudioService extends IAudioService.Stub
pw.println("\nAudioDeviceBroker:"); pw.println("\nAudioDeviceBroker:");
mDeviceBroker.dump(pw, " "); mDeviceBroker.dump(pw, " ");
pw.println("\nSoundEffects:");
mSfxHelper.dump(pw, " ");
pw.println("\n"); pw.println("\n");
pw.println("\nEvent logs:"); pw.println("\nEvent logs:");

View File

@@ -27,188 +27,85 @@ import android.media.MediaPlayer.OnCompletionListener;
import android.media.MediaPlayer.OnErrorListener; import android.media.MediaPlayer.OnErrorListener;
import android.media.SoundPool; import android.media.SoundPool;
import android.os.Environment; import android.os.Environment;
import android.os.Handler;
import android.os.Looper; import android.os.Looper;
import android.os.Message;
import android.util.Log; import android.util.Log;
import android.util.PrintWriterPrinter;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.util.XmlUtils; import com.android.internal.util.XmlUtils;
import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlPullParserException;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.PrintWriter;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
/** /**
* A helper class for managing sound effects loading / unloading * A helper class for managing sound effects loading / unloading
* used by AudioService. * used by AudioService. As its methods are called on the message handler thread
* of AudioService, the actual work is offloaded to a dedicated thread.
* This helps keeping AudioService responsive.
* @hide * @hide
*/ */
class SoundEffectsHelper { class SoundEffectsHelper {
private static final String TAG = "AS.SoundEffectsHelper"; private static final String TAG = "AS.SfxHelper";
private final Object mSoundEffectsLock = new Object();
@GuardedBy("mSoundEffectsLock")
private SoundPool mSoundPool;
private static final int NUM_SOUNDPOOL_CHANNELS = 4; private static final int NUM_SOUNDPOOL_CHANNELS = 4;
/* Sound effect file names */ /* Sound effect file names */
private static final String SOUND_EFFECTS_PATH = "/media/audio/ui/"; private static final String SOUND_EFFECTS_PATH = "/media/audio/ui/";
private static final List<String> SOUND_EFFECT_FILES = new ArrayList<String>();
/* Sound effect file name mapping sound effect id (AudioManager.FX_xxx) to private static final int EFFECT_NOT_IN_SOUND_POOL = 0; // SoundPool sample IDs > 0
* file index in SOUND_EFFECT_FILES[] (first column) and indicating if effect
* uses soundpool (second column) */
private final int[][] mSoundEffectFilesMap = new int[AudioManager.NUM_SOUND_EFFECTS][2];
private final Context mContext; private static final int MSG_LOAD_EFFECTS = 0;
private static final int MSG_UNLOAD_EFFECTS = 1;
// listener for SoundPool sample load completion indication private static final int MSG_PLAY_EFFECT = 2;
@GuardedBy("mSoundEffectsLock") private static final int MSG_LOAD_EFFECTS_TIMEOUT = 3;
private SoundPoolCallback mSoundPoolCallBack;
// thread for SoundPool listener
private SoundPoolListenerThread mSoundPoolListenerThread;
// message looper for SoundPool listener
@GuardedBy("mSoundEffectsLock")
private Looper mSoundPoolLooper = null;
// volume applied to sound played with playSoundEffect()
private static int sSoundEffectVolumeDb;
interface OnEffectsLoadCompleteHandler { interface OnEffectsLoadCompleteHandler {
void run(boolean success); void run(boolean success);
} }
private final AudioEventLogger mSfxLogger = new AudioEventLogger(
AudioManager.NUM_SOUND_EFFECTS + 10, "Sound Effects Loading");
private final Context mContext;
// default attenuation applied to sound played with playSoundEffect()
private final int mSfxAttenuationDb;
// thread for doing all work
private SfxWorker mSfxWorker;
// thread's message handler
private SfxHandler mSfxHandler;
private static final class Resource {
final String mFileName;
int mSampleId;
boolean mLoaded; // for effects in SoundPool
Resource(String fileName) {
mFileName = fileName;
mSampleId = EFFECT_NOT_IN_SOUND_POOL;
}
}
// All the fields below are accessed by the worker thread exclusively
private final List<Resource> mResources = new ArrayList<Resource>();
private final int[] mEffects = new int[AudioManager.NUM_SOUND_EFFECTS]; // indexes in mResources
private SoundPool mSoundPool;
private SoundPoolLoader mSoundPoolLoader;
SoundEffectsHelper(Context context) { SoundEffectsHelper(Context context) {
mContext = context; mContext = context;
sSoundEffectVolumeDb = mContext.getResources().getInteger( mSfxAttenuationDb = mContext.getResources().getInteger(
com.android.internal.R.integer.config_soundEffectVolumeDb); com.android.internal.R.integer.config_soundEffectVolumeDb);
startWorker();
} }
/*package*/ void loadSoundEffects(OnEffectsLoadCompleteHandler onComplete) { /*package*/ void loadSoundEffects(OnEffectsLoadCompleteHandler onComplete) {
boolean success = doLoadSoundEffects(); sendMsg(MSG_LOAD_EFFECTS, 0, 0, onComplete, 0);
if (onComplete != null) {
onComplete.run(success);
}
}
private boolean doLoadSoundEffects() {
int status;
synchronized (mSoundEffectsLock) {
if (mSoundPool != null) {
return true;
}
loadTouchSoundAssets();
mSoundPool = new SoundPool.Builder()
.setMaxStreams(NUM_SOUNDPOOL_CHANNELS)
.setAudioAttributes(new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION)
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
.build())
.build();
mSoundPoolCallBack = null;
mSoundPoolListenerThread = new SoundPoolListenerThread();
mSoundPoolListenerThread.start();
int attempts = 3;
while ((mSoundPoolCallBack == null) && (attempts-- > 0)) {
try {
// Wait for mSoundPoolCallBack to be set by the other thread
mSoundEffectsLock.wait(SOUND_EFFECTS_LOAD_TIMEOUT_MS);
} catch (InterruptedException e) {
Log.w(TAG, "Interrupted while waiting sound pool listener thread.");
}
}
if (mSoundPoolCallBack == null) {
Log.w(TAG, "loadSoundEffects() SoundPool listener or thread creation error");
if (mSoundPoolLooper != null) {
mSoundPoolLooper.quit();
mSoundPoolLooper = null;
}
mSoundPoolListenerThread = null;
mSoundPool.release();
mSoundPool = null;
return false;
}
/*
* poolId table: The value -1 in this table indicates that corresponding
* file (same index in SOUND_EFFECT_FILES[] has not been loaded.
* Once loaded, the value in poolId is the sample ID and the same
* sample can be reused for another effect using the same file.
*/
int[] poolId = new int[SOUND_EFFECT_FILES.size()];
for (int fileIdx = 0; fileIdx < SOUND_EFFECT_FILES.size(); fileIdx++) {
poolId[fileIdx] = -1;
}
/*
* Effects whose value in mSoundEffectFilesMap[effect][1] is -1 must be loaded.
* If load succeeds, value in mSoundEffectFilesMap[effect][1] is > 0:
* this indicates we have a valid sample loaded for this effect.
*/
int numSamples = 0;
for (int effect = 0; effect < AudioManager.NUM_SOUND_EFFECTS; effect++) {
// Do not load sample if this effect uses the MediaPlayer
if (mSoundEffectFilesMap[effect][1] == 0) {
continue;
}
if (poolId[mSoundEffectFilesMap[effect][0]] == -1) {
String filePath = getSoundEffectFilePath(effect);
int sampleId = mSoundPool.load(filePath, 0);
if (sampleId <= 0) {
Log.w(TAG, "Soundpool could not load file: " + filePath);
} else {
mSoundEffectFilesMap[effect][1] = sampleId;
poolId[mSoundEffectFilesMap[effect][0]] = sampleId;
numSamples++;
}
} else {
mSoundEffectFilesMap[effect][1] =
poolId[mSoundEffectFilesMap[effect][0]];
}
}
// wait for all samples to be loaded
if (numSamples > 0) {
mSoundPoolCallBack.setSamples(poolId);
attempts = 3;
status = 1;
while ((status == 1) && (attempts-- > 0)) {
try {
mSoundEffectsLock.wait(SOUND_EFFECTS_LOAD_TIMEOUT_MS);
status = mSoundPoolCallBack.status();
} catch (InterruptedException e) {
Log.w(TAG, "Interrupted while waiting sound pool callback.");
}
}
} else {
status = -1;
}
if (mSoundPoolLooper != null) {
mSoundPoolLooper.quit();
mSoundPoolLooper = null;
}
mSoundPoolListenerThread = null;
if (status != 0) {
Log.w(TAG,
"loadSoundEffects(), Error " + status + " while loading samples");
for (int effect = 0; effect < AudioManager.NUM_SOUND_EFFECTS; effect++) {
if (mSoundEffectFilesMap[effect][1] > 0) {
mSoundEffectFilesMap[effect][1] = -1;
}
}
mSoundPool.release();
mSoundPool = null;
}
}
return (status == 0);
} }
/** /**
@@ -217,54 +114,145 @@ class SoundEffectsHelper {
* sound effects are disabled. * sound effects are disabled.
*/ */
/*package*/ void unloadSoundEffects() { /*package*/ void unloadSoundEffects() {
synchronized (mSoundEffectsLock) { sendMsg(MSG_UNLOAD_EFFECTS, 0, 0, null, 0);
if (mSoundPool == null) { }
/*package*/ void playSoundEffect(int effect, int volume) {
sendMsg(MSG_PLAY_EFFECT, effect, volume, null, 0);
}
/*package*/ void dump(PrintWriter pw, String prefix) {
if (mSfxHandler != null) {
pw.println(prefix + "Message handler (watch for unhandled messages):");
mSfxHandler.dump(new PrintWriterPrinter(pw), " ");
} else {
pw.println(prefix + "Message handler is null");
}
pw.println(prefix + "Default attenuation (dB): " + mSfxAttenuationDb);
mSfxLogger.dump(pw);
}
private void startWorker() {
mSfxWorker = new SfxWorker();
mSfxWorker.start();
synchronized (this) {
while (mSfxHandler == null) {
try {
wait();
} catch (InterruptedException e) {
Log.w(TAG, "Interrupted while waiting " + mSfxWorker.getName() + " to start");
}
}
}
}
private void sendMsg(int msg, int arg1, int arg2, Object obj, int delayMs) {
mSfxHandler.sendMessageDelayed(mSfxHandler.obtainMessage(msg, arg1, arg2, obj), delayMs);
}
private void logEvent(String msg) {
mSfxLogger.log(new AudioEventLogger.StringEvent(msg));
}
// All the methods below run on the worker thread
private void onLoadSoundEffects(OnEffectsLoadCompleteHandler onComplete) {
if (mSoundPoolLoader != null) {
// Loading is ongoing.
mSoundPoolLoader.addHandler(onComplete);
return;
}
if (mSoundPool != null) {
if (onComplete != null) {
onComplete.run(true /*success*/);
}
return; return;
} }
int[] poolId = new int[SOUND_EFFECT_FILES.size()]; logEvent("effects loading started");
for (int fileIdx = 0; fileIdx < SOUND_EFFECT_FILES.size(); fileIdx++) { mSoundPool = new SoundPool.Builder()
poolId[fileIdx] = 0; .setMaxStreams(NUM_SOUNDPOOL_CHANNELS)
.setAudioAttributes(new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION)
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
.build())
.build();
loadTouchSoundAssets();
mSoundPoolLoader = new SoundPoolLoader();
mSoundPoolLoader.addHandler(new OnEffectsLoadCompleteHandler() {
@Override
public void run(boolean success) {
mSoundPoolLoader = null;
if (!success) {
Log.w(TAG, "onLoadSoundEffects(), Error while loading samples");
onUnloadSoundEffects();
}
}
});
mSoundPoolLoader.addHandler(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);
}
} }
for (int effect = 0; effect < AudioManager.NUM_SOUND_EFFECTS; effect++) { if (resourcesToLoad > 0) {
if (mSoundEffectFilesMap[effect][1] <= 0) { sendMsg(MSG_LOAD_EFFECTS_TIMEOUT, 0, 0, null, SOUND_EFFECTS_LOAD_TIMEOUT_MS);
continue; } else {
logEvent("effects loading completed, no effects to load");
mSoundPoolLoader.onComplete(true /*success*/);
} }
if (poolId[mSoundEffectFilesMap[effect][0]] == 0) { }
mSoundPool.unload(mSoundEffectFilesMap[effect][1]);
mSoundEffectFilesMap[effect][1] = -1; void onUnloadSoundEffects() {
poolId[mSoundEffectFilesMap[effect][0]] = -1; if (mSoundPool == null) {
return;
}
if (mSoundPoolLoader != null) {
mSoundPoolLoader.addHandler(new OnEffectsLoadCompleteHandler() {
@Override
public void run(boolean success) {
onUnloadSoundEffects();
}
});
}
logEvent("effects unloading started");
for (Resource res : mResources) {
if (res.mSampleId != EFFECT_NOT_IN_SOUND_POOL) {
mSoundPool.unload(res.mSampleId);
} }
} }
mSoundPool.release(); mSoundPool.release();
mSoundPool = null; mSoundPool = null;
} logEvent("effects unloading completed");
} }
/*package*/ void playSoundEffect(int effectType, int volume) { void onPlaySoundEffect(int effect, int volume) {
synchronized (mSoundEffectsLock) {
doLoadSoundEffects();
if (mSoundPool == null) {
return;
}
float volFloat; float volFloat;
// use default if volume is not specified by caller // use default if volume is not specified by caller
if (volume < 0) { if (volume < 0) {
volFloat = (float) Math.pow(10, (float) sSoundEffectVolumeDb / 20); volFloat = (float) Math.pow(10, (float) mSfxAttenuationDb / 20);
} else { } else {
volFloat = volume / 1000.0f; volFloat = volume / 1000.0f;
} }
if (mSoundEffectFilesMap[effectType][1] > 0) { Resource res = mResources.get(mEffects[effect]);
mSoundPool.play(mSoundEffectFilesMap[effectType][1], if (res.mSampleId != EFFECT_NOT_IN_SOUND_POOL && res.mLoaded) {
volFloat, volFloat, 0, 0, 1.0f); mSoundPool.play(res.mSampleId, volFloat, volFloat, 0, 0, 1.0f);
} else { } else {
MediaPlayer mediaPlayer = new MediaPlayer(); MediaPlayer mediaPlayer = new MediaPlayer();
try { try {
String filePath = getSoundEffectFilePath(effectType); String filePath = getResourceFilePath(res);
mediaPlayer.setDataSource(filePath); mediaPlayer.setDataSource(filePath);
mediaPlayer.setAudioStreamType(AudioSystem.STREAM_SYSTEM); mediaPlayer.setAudioStreamType(AudioSystem.STREAM_SYSTEM);
mediaPlayer.prepare(); mediaPlayer.prepare();
@@ -290,7 +278,6 @@ class SoundEffectsHelper {
} }
} }
} }
}
private static void cleanupPlayer(MediaPlayer mp) { private static void cleanupPlayer(MediaPlayer mp) {
if (mp != null) { if (mp != null) {
@@ -314,23 +301,21 @@ class SoundEffectsHelper {
private static final String ASSET_FILE_VERSION = "1.0"; private static final String ASSET_FILE_VERSION = "1.0";
private static final String GROUP_TOUCH_SOUNDS = "touch_sounds"; private static final String GROUP_TOUCH_SOUNDS = "touch_sounds";
private static final int SOUND_EFFECTS_LOAD_TIMEOUT_MS = 5000; private static final int SOUND_EFFECTS_LOAD_TIMEOUT_MS = 15000;
private String getSoundEffectFilePath(int effectType) { private String getResourceFilePath(Resource res) {
String filePath = Environment.getProductDirectory() + SOUND_EFFECTS_PATH String filePath = Environment.getProductDirectory() + SOUND_EFFECTS_PATH + res.mFileName;
+ SOUND_EFFECT_FILES.get(mSoundEffectFilesMap[effectType][0]);
if (!new File(filePath).isFile()) { if (!new File(filePath).isFile()) {
filePath = Environment.getRootDirectory() + SOUND_EFFECTS_PATH filePath = Environment.getRootDirectory() + SOUND_EFFECTS_PATH + res.mFileName;
+ SOUND_EFFECT_FILES.get(mSoundEffectFilesMap[effectType][0]);
} }
return filePath; return filePath;
} }
private void loadTouchSoundAssetDefaults() { private void loadTouchSoundAssetDefaults() {
SOUND_EFFECT_FILES.add("Effect_Tick.ogg"); int defaultResourceIdx = mResources.size();
for (int i = 0; i < AudioManager.NUM_SOUND_EFFECTS; i++) { mResources.add(new Resource("Effect_Tick.ogg"));
mSoundEffectFilesMap[i][0] = 0; for (int i = 0; i < mEffects.length; i++) {
mSoundEffectFilesMap[i][1] = -1; mEffects[i] = defaultResourceIdx;
} }
} }
@@ -338,7 +323,7 @@ class SoundEffectsHelper {
XmlResourceParser parser = null; XmlResourceParser parser = null;
// only load assets once. // only load assets once.
if (!SOUND_EFFECT_FILES.isEmpty()) { if (!mResources.isEmpty()) {
return; return;
} }
@@ -385,12 +370,7 @@ class SoundEffectsHelper {
continue; continue;
} }
int i = SOUND_EFFECT_FILES.indexOf(file); mEffects[fx] = findOrAddResourceByFileName(file);
if (i == -1) {
i = SOUND_EFFECT_FILES.size();
SOUND_EFFECT_FILES.add(file);
}
mSoundEffectFilesMap[fx][0] = i;
} else { } else {
break; break;
} }
@@ -409,62 +389,133 @@ class SoundEffectsHelper {
} }
} }
private final class SoundPoolListenerThread extends Thread { private int findOrAddResourceByFileName(String fileName) {
SoundPoolListenerThread() { for (int i = 0; i < mResources.size(); i++) {
super("SoundPoolListenerThread"); if (mResources.get(i).mFileName.equals(fileName)) {
return i;
}
}
int result = mResources.size();
mResources.add(new Resource(fileName));
return result;
}
private Resource findResourceBySampleId(int sampleId) {
for (Resource res : mResources) {
if (res.mSampleId == sampleId) {
return res;
}
}
return null;
}
private class SfxWorker extends Thread {
SfxWorker() {
super("AS.SfxWorker");
} }
@Override @Override
public void run() { public void run() {
Looper.prepare(); Looper.prepare();
synchronized (mSoundEffectsLock) { synchronized (SoundEffectsHelper.this) {
mSoundPoolLooper = Looper.myLooper(); mSfxHandler = new SfxHandler();
if (mSoundPool != null) { SoundEffectsHelper.this.notify();
mSoundPoolCallBack = new SoundPoolCallback();
// This call makes SoundPool to start using the thread's looper
// for load complete message handling.
mSoundPool.setOnLoadCompleteListener(mSoundPoolCallBack);
}
mSoundEffectsLock.notify();
} }
Looper.loop(); Looper.loop();
} }
} }
private final class SoundPoolCallback implements private class SfxHandler extends Handler {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_LOAD_EFFECTS:
onLoadSoundEffects((OnEffectsLoadCompleteHandler) msg.obj);
break;
case MSG_UNLOAD_EFFECTS:
onUnloadSoundEffects();
break;
case MSG_PLAY_EFFECT:
onLoadSoundEffects(new OnEffectsLoadCompleteHandler() {
@Override
public void run(boolean success) {
if (success) {
onPlaySoundEffect(msg.arg1 /*effect*/, msg.arg2 /*volume*/);
}
}
});
break;
case MSG_LOAD_EFFECTS_TIMEOUT:
if (mSoundPoolLoader != null) {
mSoundPoolLoader.onTimeout();
}
break;
}
}
}
private class SoundPoolLoader implements
android.media.SoundPool.OnLoadCompleteListener { android.media.SoundPool.OnLoadCompleteListener {
@GuardedBy("mSoundEffectsLock") private List<OnEffectsLoadCompleteHandler> mLoadCompleteHandlers =
private int mStatus = 1; // 1 means neither error nor last sample loaded yet new ArrayList<OnEffectsLoadCompleteHandler>();
@GuardedBy("mSoundEffectsLock")
List<Integer> mSamples = new ArrayList<Integer>();
@GuardedBy("mSoundEffectsLock") SoundPoolLoader() {
public int status() { // SoundPool use the current Looper when creating its message handler.
return mStatus; // Since SoundPoolLoader is created on the SfxWorker thread, SoundPool's
// message handler ends up running on it (it's OK to have multiple
// handlers on the same Looper). Thus, onLoadComplete gets executed
// on the worker thread.
mSoundPool.setOnLoadCompleteListener(this);
} }
@GuardedBy("mSoundEffectsLock") void addHandler(OnEffectsLoadCompleteHandler handler) {
public void setSamples(int[] samples) { if (handler != null) {
for (int i = 0; i < samples.length; i++) { mLoadCompleteHandlers.add(handler);
// do not wait ack for samples rejected upfront by SoundPool
if (samples[i] > 0) {
mSamples.add(samples[i]);
}
} }
} }
@Override
public void onLoadComplete(SoundPool soundPool, int sampleId, int status) { public void onLoadComplete(SoundPool soundPool, int sampleId, int status) {
synchronized (mSoundEffectsLock) { if (status == 0) {
int i = mSamples.indexOf(sampleId); int remainingToLoad = 0;
if (i >= 0) { for (Resource res : mResources) {
mSamples.remove(i); if (res.mSampleId == sampleId && !res.mLoaded) {
} logEvent("effect " + res.mFileName + " loaded");
if ((status != 0) || mSamples.isEmpty()) { res.mLoaded = true;
mStatus = status;
mSoundEffectsLock.notify();
}
} }
if (res.mSampleId != EFFECT_NOT_IN_SOUND_POOL && !res.mLoaded) {
remainingToLoad++;
}
}
if (remainingToLoad == 0) {
onComplete(true);
}
} else {
Resource res = findResourceBySampleId(sampleId);
String filePath;
if (res != null) {
filePath = getResourceFilePath(res);
} else {
filePath = "with unknown sample ID " + sampleId;
}
logEvent("effect " + filePath + " loading failed, status " + status);
Log.w(TAG, "onLoadSoundEffects(), Error " + status + " while loading sample "
+ filePath);
onComplete(false);
}
}
void onTimeout() {
onComplete(false);
}
void onComplete(boolean success) {
mSoundPool.setOnLoadCompleteListener(null);
for (OnEffectsLoadCompleteHandler handler : mLoadCompleteHandlers) {
handler.run(success);
}
logEvent("effects loading " + (success ? "completed" : "failed"));
} }
} }
} }