Loading services/core/java/com/android/server/audio/AudioService.java +2 −0 Original line number Diff line number Diff line Loading @@ -6052,6 +6052,8 @@ public class AudioService extends IAudioService.Stub pw.println("\nAudioDeviceBroker:"); mDeviceBroker.dump(pw, " "); pw.println("\nSoundEffects:"); mSfxHelper.dump(pw, " "); pw.println("\n"); pw.println("\nEvent logs:"); Loading services/core/java/com/android/server/audio/SoundEffectsHelper.java +302 −251 Original line number Diff line number Diff line Loading @@ -27,244 +27,232 @@ import android.media.MediaPlayer.OnCompletionListener; import android.media.MediaPlayer.OnErrorListener; import android.media.SoundPool; import android.os.Environment; import android.os.Handler; import android.os.Looper; import android.os.Message; import android.util.Log; import android.util.PrintWriterPrinter; import com.android.internal.annotations.GuardedBy; import com.android.internal.util.XmlUtils; import org.xmlpull.v1.XmlPullParserException; import java.io.File; import java.io.IOException; import java.io.PrintWriter; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.List; /** * 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 */ 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; /* Sound effect file names */ 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 * 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; // listener for SoundPool sample load completion indication @GuardedBy("mSoundEffectsLock") private SoundPoolCallback mSoundPoolCallBack; // thread for SoundPool listener private SoundPoolListenerThread mSoundPoolListenerThread; // message looper for SoundPool listener @GuardedBy("mSoundEffectsLock") private Looper mSoundPoolLooper = null; private static final int EFFECT_NOT_IN_SOUND_POOL = 0; // SoundPool sample IDs > 0 // volume applied to sound played with playSoundEffect() private static int sSoundEffectVolumeDb; private static final int MSG_LOAD_EFFECTS = 0; private static final int MSG_UNLOAD_EFFECTS = 1; private static final int MSG_PLAY_EFFECT = 2; private static final int MSG_LOAD_EFFECTS_TIMEOUT = 3; interface OnEffectsLoadCompleteHandler { 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) { mContext = context; sSoundEffectVolumeDb = mContext.getResources().getInteger( mSfxAttenuationDb = mContext.getResources().getInteger( com.android.internal.R.integer.config_soundEffectVolumeDb); startWorker(); } /*package*/ void loadSoundEffects(OnEffectsLoadCompleteHandler onComplete) { boolean success = doLoadSoundEffects(); if (onComplete != null) { onComplete.run(success); } sendMsg(MSG_LOAD_EFFECTS, 0, 0, onComplete, 0); } private boolean doLoadSoundEffects() { int status; /** * Unloads samples from the sound pool. * This method can be called to free some memory when * sound effects are disabled. */ /*package*/ void unloadSoundEffects() { sendMsg(MSG_UNLOAD_EFFECTS, 0, 0, null, 0); } synchronized (mSoundEffectsLock) { if (mSoundPool != null) { return true; /*package*/ void playSoundEffect(int effect, int volume) { sendMsg(MSG_PLAY_EFFECT, effect, volume, null, 0); } loadTouchSoundAssets(); /*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); } 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)) { private void startWorker() { mSfxWorker = new SfxWorker(); mSfxWorker.start(); synchronized (this) { while (mSfxHandler == null) { try { // Wait for mSoundPoolCallBack to be set by the other thread mSoundEffectsLock.wait(SOUND_EFFECTS_LOAD_TIMEOUT_MS); wait(); } catch (InterruptedException e) { Log.w(TAG, "Interrupted while waiting sound pool listener thread."); Log.w(TAG, "Interrupted while waiting " + mSfxWorker.getName() + " to start"); } } 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; private void sendMsg(int msg, int arg1, int arg2, Object obj, int delayMs) { mSfxHandler.sendMessageDelayed(mSfxHandler.obtainMessage(msg, arg1, arg2, obj), delayMs); } /* * 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; private void logEvent(String msg) { mSfxLogger.log(new AudioEventLogger.StringEvent(msg)); } 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++; // All the methods below run on the worker thread private void onLoadSoundEffects(OnEffectsLoadCompleteHandler onComplete) { if (mSoundPoolLoader != null) { // Loading is ongoing. mSoundPoolLoader.addHandler(onComplete); return; } } else { mSoundEffectFilesMap[effect][1] = poolId[mSoundEffectFilesMap[effect][0]]; if (mSoundPool != null) { if (onComplete != null) { onComplete.run(true /*success*/); } return; } // 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."); } logEvent("effects loading started"); mSoundPool = new SoundPool.Builder() .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(); } } else { status = -1; } }); mSoundPoolLoader.addHandler(onComplete); 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; 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); } } mSoundPool.release(); mSoundPool = null; } if (resourcesToLoad > 0) { sendMsg(MSG_LOAD_EFFECTS_TIMEOUT, 0, 0, null, SOUND_EFFECTS_LOAD_TIMEOUT_MS); } else { logEvent("effects loading completed, no effects to load"); mSoundPoolLoader.onComplete(true /*success*/); } return (status == 0); } /** * Unloads samples from the sound pool. * This method can be called to free some memory when * sound effects are disabled. */ /*package*/ void unloadSoundEffects() { synchronized (mSoundEffectsLock) { void onUnloadSoundEffects() { if (mSoundPool == null) { return; } int[] poolId = new int[SOUND_EFFECT_FILES.size()]; for (int fileIdx = 0; fileIdx < SOUND_EFFECT_FILES.size(); fileIdx++) { poolId[fileIdx] = 0; if (mSoundPoolLoader != null) { mSoundPoolLoader.addHandler(new OnEffectsLoadCompleteHandler() { @Override public void run(boolean success) { onUnloadSoundEffects(); } for (int effect = 0; effect < AudioManager.NUM_SOUND_EFFECTS; effect++) { if (mSoundEffectFilesMap[effect][1] <= 0) { continue; }); } if (poolId[mSoundEffectFilesMap[effect][0]] == 0) { mSoundPool.unload(mSoundEffectFilesMap[effect][1]); mSoundEffectFilesMap[effect][1] = -1; poolId[mSoundEffectFilesMap[effect][0]] = -1; logEvent("effects unloading started"); for (Resource res : mResources) { if (res.mSampleId != EFFECT_NOT_IN_SOUND_POOL) { mSoundPool.unload(res.mSampleId); } } mSoundPool.release(); mSoundPool = null; logEvent("effects unloading completed"); } } /*package*/ void playSoundEffect(int effectType, int volume) { synchronized (mSoundEffectsLock) { doLoadSoundEffects(); if (mSoundPool == null) { return; } 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) sSoundEffectVolumeDb / 20); volFloat = (float) Math.pow(10, (float) mSfxAttenuationDb / 20); } else { volFloat = volume / 1000.0f; } if (mSoundEffectFilesMap[effectType][1] > 0) { mSoundPool.play(mSoundEffectFilesMap[effectType][1], volFloat, volFloat, 0, 0, 1.0f); Resource res = mResources.get(mEffects[effect]); if (res.mSampleId != EFFECT_NOT_IN_SOUND_POOL && res.mLoaded) { mSoundPool.play(res.mSampleId, volFloat, volFloat, 0, 0, 1.0f); } else { MediaPlayer mediaPlayer = new MediaPlayer(); try { String filePath = getSoundEffectFilePath(effectType); String filePath = getResourceFilePath(res); mediaPlayer.setDataSource(filePath); mediaPlayer.setAudioStreamType(AudioSystem.STREAM_SYSTEM); mediaPlayer.prepare(); Loading @@ -290,7 +278,6 @@ class SoundEffectsHelper { } } } } private static void cleanupPlayer(MediaPlayer mp) { if (mp != null) { Loading @@ -314,23 +301,21 @@ class SoundEffectsHelper { private static final String ASSET_FILE_VERSION = "1.0"; 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) { String filePath = Environment.getProductDirectory() + SOUND_EFFECTS_PATH + SOUND_EFFECT_FILES.get(mSoundEffectFilesMap[effectType][0]); private String getResourceFilePath(Resource res) { String filePath = Environment.getProductDirectory() + SOUND_EFFECTS_PATH + res.mFileName; if (!new File(filePath).isFile()) { filePath = Environment.getRootDirectory() + SOUND_EFFECTS_PATH + SOUND_EFFECT_FILES.get(mSoundEffectFilesMap[effectType][0]); filePath = Environment.getRootDirectory() + SOUND_EFFECTS_PATH + res.mFileName; } return filePath; } private void loadTouchSoundAssetDefaults() { SOUND_EFFECT_FILES.add("Effect_Tick.ogg"); for (int i = 0; i < AudioManager.NUM_SOUND_EFFECTS; i++) { mSoundEffectFilesMap[i][0] = 0; mSoundEffectFilesMap[i][1] = -1; int defaultResourceIdx = mResources.size(); mResources.add(new Resource("Effect_Tick.ogg")); for (int i = 0; i < mEffects.length; i++) { mEffects[i] = defaultResourceIdx; } } Loading @@ -338,7 +323,7 @@ class SoundEffectsHelper { XmlResourceParser parser = null; // only load assets once. if (!SOUND_EFFECT_FILES.isEmpty()) { if (!mResources.isEmpty()) { return; } Loading Loading @@ -385,12 +370,7 @@ class SoundEffectsHelper { continue; } int i = SOUND_EFFECT_FILES.indexOf(file); if (i == -1) { i = SOUND_EFFECT_FILES.size(); SOUND_EFFECT_FILES.add(file); } mSoundEffectFilesMap[fx][0] = i; mEffects[fx] = findOrAddResourceByFileName(file); } else { break; } Loading @@ -409,62 +389,133 @@ class SoundEffectsHelper { } } private final class SoundPoolListenerThread extends Thread { SoundPoolListenerThread() { super("SoundPoolListenerThread"); private int findOrAddResourceByFileName(String fileName) { for (int i = 0; i < mResources.size(); i++) { 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 public void run() { Looper.prepare(); synchronized (mSoundEffectsLock) { mSoundPoolLooper = Looper.myLooper(); if (mSoundPool != null) { mSoundPoolCallBack = new SoundPoolCallback(); // This call makes SoundPool to start using the thread's looper // for load complete message handling. mSoundPool.setOnLoadCompleteListener(mSoundPoolCallBack); } mSoundEffectsLock.notify(); synchronized (SoundEffectsHelper.this) { mSfxHandler = new SfxHandler(); SoundEffectsHelper.this.notify(); } 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 { @GuardedBy("mSoundEffectsLock") private int mStatus = 1; // 1 means neither error nor last sample loaded yet @GuardedBy("mSoundEffectsLock") List<Integer> mSamples = new ArrayList<Integer>(); private List<OnEffectsLoadCompleteHandler> mLoadCompleteHandlers = new ArrayList<OnEffectsLoadCompleteHandler>(); @GuardedBy("mSoundEffectsLock") public int status() { return mStatus; SoundPoolLoader() { // SoundPool use the current Looper when creating its message handler. // 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") public void setSamples(int[] samples) { for (int i = 0; i < samples.length; i++) { // do not wait ack for samples rejected upfront by SoundPool if (samples[i] > 0) { mSamples.add(samples[i]); } void addHandler(OnEffectsLoadCompleteHandler handler) { if (handler != null) { mLoadCompleteHandlers.add(handler); } } @Override public void onLoadComplete(SoundPool soundPool, int sampleId, int status) { synchronized (mSoundEffectsLock) { int i = mSamples.indexOf(sampleId); if (i >= 0) { mSamples.remove(i); if (status == 0) { int remainingToLoad = 0; for (Resource res : mResources) { if (res.mSampleId == sampleId && !res.mLoaded) { logEvent("effect " + res.mFileName + " loaded"); res.mLoaded = true; } if (res.mSampleId != EFFECT_NOT_IN_SOUND_POOL && !res.mLoaded) { remainingToLoad++; } if ((status != 0) || mSamples.isEmpty()) { mStatus = status; mSoundEffectsLock.notify(); } 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")); } } } Loading
services/core/java/com/android/server/audio/AudioService.java +2 −0 Original line number Diff line number Diff line Loading @@ -6052,6 +6052,8 @@ public class AudioService extends IAudioService.Stub pw.println("\nAudioDeviceBroker:"); mDeviceBroker.dump(pw, " "); pw.println("\nSoundEffects:"); mSfxHelper.dump(pw, " "); pw.println("\n"); pw.println("\nEvent logs:"); Loading
services/core/java/com/android/server/audio/SoundEffectsHelper.java +302 −251 Original line number Diff line number Diff line Loading @@ -27,244 +27,232 @@ import android.media.MediaPlayer.OnCompletionListener; import android.media.MediaPlayer.OnErrorListener; import android.media.SoundPool; import android.os.Environment; import android.os.Handler; import android.os.Looper; import android.os.Message; import android.util.Log; import android.util.PrintWriterPrinter; import com.android.internal.annotations.GuardedBy; import com.android.internal.util.XmlUtils; import org.xmlpull.v1.XmlPullParserException; import java.io.File; import java.io.IOException; import java.io.PrintWriter; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.List; /** * 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 */ 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; /* Sound effect file names */ 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 * 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; // listener for SoundPool sample load completion indication @GuardedBy("mSoundEffectsLock") private SoundPoolCallback mSoundPoolCallBack; // thread for SoundPool listener private SoundPoolListenerThread mSoundPoolListenerThread; // message looper for SoundPool listener @GuardedBy("mSoundEffectsLock") private Looper mSoundPoolLooper = null; private static final int EFFECT_NOT_IN_SOUND_POOL = 0; // SoundPool sample IDs > 0 // volume applied to sound played with playSoundEffect() private static int sSoundEffectVolumeDb; private static final int MSG_LOAD_EFFECTS = 0; private static final int MSG_UNLOAD_EFFECTS = 1; private static final int MSG_PLAY_EFFECT = 2; private static final int MSG_LOAD_EFFECTS_TIMEOUT = 3; interface OnEffectsLoadCompleteHandler { 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) { mContext = context; sSoundEffectVolumeDb = mContext.getResources().getInteger( mSfxAttenuationDb = mContext.getResources().getInteger( com.android.internal.R.integer.config_soundEffectVolumeDb); startWorker(); } /*package*/ void loadSoundEffects(OnEffectsLoadCompleteHandler onComplete) { boolean success = doLoadSoundEffects(); if (onComplete != null) { onComplete.run(success); } sendMsg(MSG_LOAD_EFFECTS, 0, 0, onComplete, 0); } private boolean doLoadSoundEffects() { int status; /** * Unloads samples from the sound pool. * This method can be called to free some memory when * sound effects are disabled. */ /*package*/ void unloadSoundEffects() { sendMsg(MSG_UNLOAD_EFFECTS, 0, 0, null, 0); } synchronized (mSoundEffectsLock) { if (mSoundPool != null) { return true; /*package*/ void playSoundEffect(int effect, int volume) { sendMsg(MSG_PLAY_EFFECT, effect, volume, null, 0); } loadTouchSoundAssets(); /*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); } 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)) { private void startWorker() { mSfxWorker = new SfxWorker(); mSfxWorker.start(); synchronized (this) { while (mSfxHandler == null) { try { // Wait for mSoundPoolCallBack to be set by the other thread mSoundEffectsLock.wait(SOUND_EFFECTS_LOAD_TIMEOUT_MS); wait(); } catch (InterruptedException e) { Log.w(TAG, "Interrupted while waiting sound pool listener thread."); Log.w(TAG, "Interrupted while waiting " + mSfxWorker.getName() + " to start"); } } 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; private void sendMsg(int msg, int arg1, int arg2, Object obj, int delayMs) { mSfxHandler.sendMessageDelayed(mSfxHandler.obtainMessage(msg, arg1, arg2, obj), delayMs); } /* * 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; private void logEvent(String msg) { mSfxLogger.log(new AudioEventLogger.StringEvent(msg)); } 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++; // All the methods below run on the worker thread private void onLoadSoundEffects(OnEffectsLoadCompleteHandler onComplete) { if (mSoundPoolLoader != null) { // Loading is ongoing. mSoundPoolLoader.addHandler(onComplete); return; } } else { mSoundEffectFilesMap[effect][1] = poolId[mSoundEffectFilesMap[effect][0]]; if (mSoundPool != null) { if (onComplete != null) { onComplete.run(true /*success*/); } return; } // 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."); } logEvent("effects loading started"); mSoundPool = new SoundPool.Builder() .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(); } } else { status = -1; } }); mSoundPoolLoader.addHandler(onComplete); 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; 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); } } mSoundPool.release(); mSoundPool = null; } if (resourcesToLoad > 0) { sendMsg(MSG_LOAD_EFFECTS_TIMEOUT, 0, 0, null, SOUND_EFFECTS_LOAD_TIMEOUT_MS); } else { logEvent("effects loading completed, no effects to load"); mSoundPoolLoader.onComplete(true /*success*/); } return (status == 0); } /** * Unloads samples from the sound pool. * This method can be called to free some memory when * sound effects are disabled. */ /*package*/ void unloadSoundEffects() { synchronized (mSoundEffectsLock) { void onUnloadSoundEffects() { if (mSoundPool == null) { return; } int[] poolId = new int[SOUND_EFFECT_FILES.size()]; for (int fileIdx = 0; fileIdx < SOUND_EFFECT_FILES.size(); fileIdx++) { poolId[fileIdx] = 0; if (mSoundPoolLoader != null) { mSoundPoolLoader.addHandler(new OnEffectsLoadCompleteHandler() { @Override public void run(boolean success) { onUnloadSoundEffects(); } for (int effect = 0; effect < AudioManager.NUM_SOUND_EFFECTS; effect++) { if (mSoundEffectFilesMap[effect][1] <= 0) { continue; }); } if (poolId[mSoundEffectFilesMap[effect][0]] == 0) { mSoundPool.unload(mSoundEffectFilesMap[effect][1]); mSoundEffectFilesMap[effect][1] = -1; poolId[mSoundEffectFilesMap[effect][0]] = -1; logEvent("effects unloading started"); for (Resource res : mResources) { if (res.mSampleId != EFFECT_NOT_IN_SOUND_POOL) { mSoundPool.unload(res.mSampleId); } } mSoundPool.release(); mSoundPool = null; logEvent("effects unloading completed"); } } /*package*/ void playSoundEffect(int effectType, int volume) { synchronized (mSoundEffectsLock) { doLoadSoundEffects(); if (mSoundPool == null) { return; } 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) sSoundEffectVolumeDb / 20); volFloat = (float) Math.pow(10, (float) mSfxAttenuationDb / 20); } else { volFloat = volume / 1000.0f; } if (mSoundEffectFilesMap[effectType][1] > 0) { mSoundPool.play(mSoundEffectFilesMap[effectType][1], volFloat, volFloat, 0, 0, 1.0f); Resource res = mResources.get(mEffects[effect]); if (res.mSampleId != EFFECT_NOT_IN_SOUND_POOL && res.mLoaded) { mSoundPool.play(res.mSampleId, volFloat, volFloat, 0, 0, 1.0f); } else { MediaPlayer mediaPlayer = new MediaPlayer(); try { String filePath = getSoundEffectFilePath(effectType); String filePath = getResourceFilePath(res); mediaPlayer.setDataSource(filePath); mediaPlayer.setAudioStreamType(AudioSystem.STREAM_SYSTEM); mediaPlayer.prepare(); Loading @@ -290,7 +278,6 @@ class SoundEffectsHelper { } } } } private static void cleanupPlayer(MediaPlayer mp) { if (mp != null) { Loading @@ -314,23 +301,21 @@ class SoundEffectsHelper { private static final String ASSET_FILE_VERSION = "1.0"; 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) { String filePath = Environment.getProductDirectory() + SOUND_EFFECTS_PATH + SOUND_EFFECT_FILES.get(mSoundEffectFilesMap[effectType][0]); private String getResourceFilePath(Resource res) { String filePath = Environment.getProductDirectory() + SOUND_EFFECTS_PATH + res.mFileName; if (!new File(filePath).isFile()) { filePath = Environment.getRootDirectory() + SOUND_EFFECTS_PATH + SOUND_EFFECT_FILES.get(mSoundEffectFilesMap[effectType][0]); filePath = Environment.getRootDirectory() + SOUND_EFFECTS_PATH + res.mFileName; } return filePath; } private void loadTouchSoundAssetDefaults() { SOUND_EFFECT_FILES.add("Effect_Tick.ogg"); for (int i = 0; i < AudioManager.NUM_SOUND_EFFECTS; i++) { mSoundEffectFilesMap[i][0] = 0; mSoundEffectFilesMap[i][1] = -1; int defaultResourceIdx = mResources.size(); mResources.add(new Resource("Effect_Tick.ogg")); for (int i = 0; i < mEffects.length; i++) { mEffects[i] = defaultResourceIdx; } } Loading @@ -338,7 +323,7 @@ class SoundEffectsHelper { XmlResourceParser parser = null; // only load assets once. if (!SOUND_EFFECT_FILES.isEmpty()) { if (!mResources.isEmpty()) { return; } Loading Loading @@ -385,12 +370,7 @@ class SoundEffectsHelper { continue; } int i = SOUND_EFFECT_FILES.indexOf(file); if (i == -1) { i = SOUND_EFFECT_FILES.size(); SOUND_EFFECT_FILES.add(file); } mSoundEffectFilesMap[fx][0] = i; mEffects[fx] = findOrAddResourceByFileName(file); } else { break; } Loading @@ -409,62 +389,133 @@ class SoundEffectsHelper { } } private final class SoundPoolListenerThread extends Thread { SoundPoolListenerThread() { super("SoundPoolListenerThread"); private int findOrAddResourceByFileName(String fileName) { for (int i = 0; i < mResources.size(); i++) { 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 public void run() { Looper.prepare(); synchronized (mSoundEffectsLock) { mSoundPoolLooper = Looper.myLooper(); if (mSoundPool != null) { mSoundPoolCallBack = new SoundPoolCallback(); // This call makes SoundPool to start using the thread's looper // for load complete message handling. mSoundPool.setOnLoadCompleteListener(mSoundPoolCallBack); } mSoundEffectsLock.notify(); synchronized (SoundEffectsHelper.this) { mSfxHandler = new SfxHandler(); SoundEffectsHelper.this.notify(); } 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 { @GuardedBy("mSoundEffectsLock") private int mStatus = 1; // 1 means neither error nor last sample loaded yet @GuardedBy("mSoundEffectsLock") List<Integer> mSamples = new ArrayList<Integer>(); private List<OnEffectsLoadCompleteHandler> mLoadCompleteHandlers = new ArrayList<OnEffectsLoadCompleteHandler>(); @GuardedBy("mSoundEffectsLock") public int status() { return mStatus; SoundPoolLoader() { // SoundPool use the current Looper when creating its message handler. // 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") public void setSamples(int[] samples) { for (int i = 0; i < samples.length; i++) { // do not wait ack for samples rejected upfront by SoundPool if (samples[i] > 0) { mSamples.add(samples[i]); } void addHandler(OnEffectsLoadCompleteHandler handler) { if (handler != null) { mLoadCompleteHandlers.add(handler); } } @Override public void onLoadComplete(SoundPool soundPool, int sampleId, int status) { synchronized (mSoundEffectsLock) { int i = mSamples.indexOf(sampleId); if (i >= 0) { mSamples.remove(i); if (status == 0) { int remainingToLoad = 0; for (Resource res : mResources) { if (res.mSampleId == sampleId && !res.mLoaded) { logEvent("effect " + res.mFileName + " loaded"); res.mLoaded = true; } if (res.mSampleId != EFFECT_NOT_IN_SOUND_POOL && !res.mLoaded) { remainingToLoad++; } if ((status != 0) || mSamples.isEmpty()) { mStatus = status; mSoundEffectsLock.notify(); } 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")); } } }