From a1a85a9d292869fb56d74c5b1cdcc8b60021591f Mon Sep 17 00:00:00 2001 From: Mikhail Naganov Date: Tue, 2 Jul 2019 11:44:35 -0700 Subject: AudioService: factor out sound effects handling Move the code related to sound effects loading and playback into a separate class SoundEffectsHelper. Bug: 135763139 Test: make and run phone Change-Id: I30a7d897d740b02943c68924074f14fae3c3eff8 --- .../com/android/server/audio/AudioService.java | 466 ++------------------ .../android/server/audio/SoundEffectsHelper.java | 470 +++++++++++++++++++++ 2 files changed, 506 insertions(+), 430 deletions(-) create mode 100644 services/core/java/com/android/server/audio/SoundEffectsHelper.java diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index a53786572f6c..6ee6175a4661 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -54,8 +54,6 @@ import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.pm.UserInfo; import android.content.res.Configuration; -import android.content.res.Resources; -import android.content.res.XmlResourceParser; import android.database.ContentObserver; import android.hardware.hdmi.HdmiAudioSystemClient; import android.hardware.hdmi.HdmiControlManager; @@ -82,11 +80,7 @@ import android.media.IRingtonePlayer; import android.media.IVolumeController; import android.media.MediaExtractor; import android.media.MediaFormat; -import android.media.MediaPlayer; -import android.media.MediaPlayer.OnCompletionListener; -import android.media.MediaPlayer.OnErrorListener; import android.media.PlayerBase; -import android.media.SoundPool; import android.media.VolumePolicy; import android.media.audiofx.AudioEffect; import android.media.audiopolicy.AudioMix; @@ -102,7 +96,6 @@ import android.net.Uri; import android.os.Binder; import android.os.Build; import android.os.Bundle; -import android.os.Environment; import android.os.Handler; import android.os.IBinder; import android.os.Looper; @@ -137,7 +130,6 @@ import android.widget.Toast; import com.android.internal.annotations.GuardedBy; import com.android.internal.util.DumpUtils; import com.android.internal.util.Preconditions; -import com.android.internal.util.XmlUtils; import com.android.server.EventLogTags; import com.android.server.LocalServices; import com.android.server.SystemService; @@ -146,15 +138,11 @@ import com.android.server.audio.AudioServiceEvents.VolumeEvent; import com.android.server.pm.UserManagerService; import com.android.server.wm.ActivityTaskManagerInternal; -import org.xmlpull.v1.XmlPullParserException; - -import java.io.File; import java.io.FileDescriptor; import java.io.IOException; import java.io.PrintWriter; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; -import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; @@ -294,19 +282,6 @@ public class AudioService extends IAudioService.Stub // protects mRingerMode private final Object mSettingsLock = new Object(); - private SoundPool mSoundPool; - private final Object mSoundEffectsLock = new Object(); - 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 SOUND_EFFECT_FILES = new ArrayList(); - - /* 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[][] SOUND_EFFECT_FILES_MAP = new int[AudioManager.NUM_SOUND_EFFECTS][2]; - /** Maximum volume index values for audio streams */ protected static int[] MAX_STREAM_VOLUME = new int[] { 5, // STREAM_VOICE_CALL @@ -453,6 +428,9 @@ public class AudioService extends IAudioService.Stub * @see System#MUTE_STREAMS_AFFECTED */ private int mMuteAffectedStreams; + @NonNull + private SoundEffectsHelper mSfxHelper; + /** * NOTE: setVibrateSetting(), getVibrateSetting(), shouldVibrate() are deprecated. * mVibrateSetting is just maintained during deprecation period but vibration policy is @@ -493,14 +471,6 @@ public class AudioService extends IAudioService.Stub private boolean mSystemReady; // true if Intent.ACTION_USER_SWITCHED has ever been received private boolean mUserSwitchedReceived; - // listener for SoundPool sample load completion indication - private SoundPoolCallback mSoundPoolCallBack; - // thread for SoundPool listener - private SoundPoolListenerThread mSoundPoolListenerThread; - // message looper for SoundPool listener - private Looper mSoundPoolLooper = null; - // volume applied to sound played with playSoundEffect() - private static int sSoundEffectVolumeDb; // previous volume adjustment direction received by checkForRingerModeChange() private int mPrevVolDirection = AudioManager.ADJUST_SAME; // mVolumeControlStream is set by VolumePanel to temporarily force the stream type which volume @@ -642,6 +612,8 @@ public class AudioService extends IAudioService.Stub PowerManager pm = (PowerManager)context.getSystemService(Context.POWER_SERVICE); mAudioEventWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "handleAudioEvent"); + mSfxHelper = new SoundEffectsHelper(mContext); + mVibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE); mHasVibrator = mVibrator == null ? false : mVibrator.hasVibrator(); @@ -732,9 +704,6 @@ public class AudioService extends IAudioService.Stub MAX_STREAM_VOLUME[AudioSystem.STREAM_SYSTEM]; } - sSoundEffectVolumeDb = context.getResources().getInteger( - com.android.internal.R.integer.config_soundEffectVolumeDb); - createAudioSystemThread(); AudioSystem.setErrorCallback(mAudioSystemCallback); @@ -3369,104 +3338,30 @@ public class AudioService extends IAudioService.Stub //========================================================================================== // Sound Effects //========================================================================================== + private static final class LoadSoundEffectReply + implements SoundEffectsHelper.OnEffectsLoadCompleteHandler { + private static final int SOUND_EFFECTS_LOADING = 1; + private static final int SOUND_EFFECTS_LOADED = 0; + private static final int SOUND_EFFECTS_ERROR = -1; + private static final int SOUND_EFFECTS_LOAD_TIMEOUT_MS = 5000; - private static final String TAG_AUDIO_ASSETS = "audio_assets"; - private static final String ATTR_VERSION = "version"; - private static final String TAG_GROUP = "group"; - private static final String ATTR_GROUP_NAME = "name"; - private static final String TAG_ASSET = "asset"; - private static final String ATTR_ASSET_ID = "id"; - private static final String ATTR_ASSET_FILE = "file"; - - 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 int mStatus = SOUND_EFFECTS_LOADING; - class LoadSoundEffectReply { - public int mStatus = 1; - }; - - private void loadTouchSoundAssetDefaults() { - SOUND_EFFECT_FILES.add("Effect_Tick.ogg"); - for (int i = 0; i < AudioManager.NUM_SOUND_EFFECTS; i++) { - SOUND_EFFECT_FILES_MAP[i][0] = 0; - SOUND_EFFECT_FILES_MAP[i][1] = -1; - } - } - - private void loadTouchSoundAssets() { - XmlResourceParser parser = null; - - // only load assets once. - if (!SOUND_EFFECT_FILES.isEmpty()) { - return; + @Override + public synchronized void run(boolean success) { + mStatus = success ? SOUND_EFFECTS_LOADED : SOUND_EFFECTS_ERROR; + notify(); } - loadTouchSoundAssetDefaults(); - - try { - parser = mContext.getResources().getXml(com.android.internal.R.xml.audio_assets); - - XmlUtils.beginDocument(parser, TAG_AUDIO_ASSETS); - String version = parser.getAttributeValue(null, ATTR_VERSION); - boolean inTouchSoundsGroup = false; - - 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)) { - inTouchSoundsGroup = true; - break; - } - } - } - while (inTouchSoundsGroup) { - XmlUtils.nextElement(parser); - String element = parser.getName(); - if (element == null) { - break; - } - 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 touch sound ID: "+id); - continue; - } - - int i = SOUND_EFFECT_FILES.indexOf(file); - if (i == -1) { - i = SOUND_EFFECT_FILES.size(); - SOUND_EFFECT_FILES.add(file); - } - SOUND_EFFECT_FILES_MAP[fx][0] = i; - } else { - break; - } + public synchronized boolean waitForLoaded(int attempts) { + while ((mStatus == SOUND_EFFECTS_LOADING) && (attempts-- > 0)) { + try { + wait(SOUND_EFFECTS_LOAD_TIMEOUT_MS); + } catch (InterruptedException e) { + Log.w(TAG, "Interrupted while waiting sound pool loaded."); } } - } catch (Resources.NotFoundException e) { - Log.w(TAG, "audio assets file not found", e); - } catch (XmlPullParserException e) { - Log.w(TAG, "XML parser exception reading touch sound assets", e); - } catch (IOException e) { - Log.w(TAG, "I/O exception reading touch sound assets", e); - } finally { - if (parser != null) { - parser.close(); - } + return mStatus == SOUND_EFFECTS_LOADED; } } @@ -3496,20 +3391,9 @@ public class AudioService extends IAudioService.Stub * This method must be called at first when sound effects are enabled */ public boolean loadSoundEffects() { - int attempts = 3; LoadSoundEffectReply reply = new LoadSoundEffectReply(); - - synchronized (reply) { - sendMsg(mAudioHandler, MSG_LOAD_SOUND_EFFECTS, SENDMSG_QUEUE, 0, 0, reply, 0); - while ((reply.mStatus == 1) && (attempts-- > 0)) { - try { - reply.wait(SOUND_EFFECTS_LOAD_TIMEOUT_MS); - } catch (InterruptedException e) { - Log.w(TAG, "loadSoundEffects Interrupted while waiting sound pool loaded."); - } - } - } - return (reply.mStatus == 0); + sendMsg(mAudioHandler, MSG_LOAD_SOUND_EFFECTS, SENDMSG_QUEUE, 0, 0, reply, 0); + return reply.waitForLoaded(3 /*attempts*/); } /** @@ -3529,61 +3413,6 @@ public class AudioService extends IAudioService.Stub sendMsg(mAudioHandler, MSG_UNLOAD_SOUND_EFFECTS, SENDMSG_QUEUE, 0, 0, null, 0); } - class SoundPoolListenerThread extends Thread { - public SoundPoolListenerThread() { - super("SoundPoolListenerThread"); - } - - @Override - public void run() { - - Looper.prepare(); - mSoundPoolLooper = Looper.myLooper(); - - synchronized (mSoundEffectsLock) { - if (mSoundPool != null) { - mSoundPoolCallBack = new SoundPoolCallback(); - mSoundPool.setOnLoadCompleteListener(mSoundPoolCallBack); - } - mSoundEffectsLock.notify(); - } - Looper.loop(); - } - } - - private final class SoundPoolCallback implements - android.media.SoundPool.OnLoadCompleteListener { - - int mStatus = 1; // 1 means neither error nor last sample loaded yet - List mSamples = new ArrayList(); - - public int status() { - return mStatus; - } - - 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]); - } - } - } - - 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) || mSamples. isEmpty()) { - mStatus = status; - mSoundEffectsLock.notify(); - } - } - } - } - /** @see AudioManager#reloadAudioSettings() */ public void reloadAudioSettings() { readAudioSettings(false /*userSwitch*/); @@ -5104,230 +4933,6 @@ public class AudioService extends IAudioService.Stub Settings.Global.putInt(mContentResolver, Settings.Global.MODE_RINGER, ringerMode); } - private String getSoundEffectFilePath(int effectType) { - String filePath = Environment.getProductDirectory() + SOUND_EFFECTS_PATH - + SOUND_EFFECT_FILES.get(SOUND_EFFECT_FILES_MAP[effectType][0]); - if (!new File(filePath).isFile()) { - filePath = Environment.getRootDirectory() + SOUND_EFFECTS_PATH - + SOUND_EFFECT_FILES.get(SOUND_EFFECT_FILES_MAP[effectType][0]); - } - return filePath; - } - - private boolean onLoadSoundEffects() { - int status; - - synchronized (mSoundEffectsLock) { - if (!mSystemReady) { - Log.w(TAG, "onLoadSoundEffects() called before boot complete"); - return false; - } - - 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, "onLoadSoundEffects() 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 SOUND_EFFECT_FILES_MAP[effect][1] is -1 must be loaded. - * If load succeeds, value in SOUND_EFFECT_FILES_MAP[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 (SOUND_EFFECT_FILES_MAP[effect][1] == 0) { - continue; - } - if (poolId[SOUND_EFFECT_FILES_MAP[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 { - SOUND_EFFECT_FILES_MAP[effect][1] = sampleId; - poolId[SOUND_EFFECT_FILES_MAP[effect][0]] = sampleId; - numSamples++; - } - } else { - SOUND_EFFECT_FILES_MAP[effect][1] = - poolId[SOUND_EFFECT_FILES_MAP[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, - "onLoadSoundEffects(), Error "+status+ " while loading samples"); - for (int effect = 0; effect < AudioManager.NUM_SOUND_EFFECTS; effect++) { - if (SOUND_EFFECT_FILES_MAP[effect][1] > 0) { - SOUND_EFFECT_FILES_MAP[effect][1] = -1; - } - } - - mSoundPool.release(); - mSoundPool = null; - } - } - return (status == 0); - } - - /** - * Unloads samples from the sound pool. - * This method can be called to free some memory when - * sound effects are disabled. - */ - private void onUnloadSoundEffects() { - synchronized (mSoundEffectsLock) { - 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; - } - - for (int effect = 0; effect < AudioManager.NUM_SOUND_EFFECTS; effect++) { - if (SOUND_EFFECT_FILES_MAP[effect][1] <= 0) { - continue; - } - if (poolId[SOUND_EFFECT_FILES_MAP[effect][0]] == 0) { - mSoundPool.unload(SOUND_EFFECT_FILES_MAP[effect][1]); - SOUND_EFFECT_FILES_MAP[effect][1] = -1; - poolId[SOUND_EFFECT_FILES_MAP[effect][0]] = -1; - } - } - mSoundPool.release(); - mSoundPool = null; - } - } - - private void onPlaySoundEffect(int effectType, int volume) { - synchronized (mSoundEffectsLock) { - - onLoadSoundEffects(); - - if (mSoundPool == null) { - return; - } - float volFloat; - // use default if volume is not specified by caller - if (volume < 0) { - volFloat = (float)Math.pow(10, (float)sSoundEffectVolumeDb/20); - } else { - volFloat = volume / 1000.0f; - } - - if (SOUND_EFFECT_FILES_MAP[effectType][1] > 0) { - mSoundPool.play(SOUND_EFFECT_FILES_MAP[effectType][1], - volFloat, volFloat, 0, 0, 1.0f); - } else { - MediaPlayer mediaPlayer = new MediaPlayer(); - try { - String filePath = getSoundEffectFilePath(effectType); - 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); - } - } - } - } - - private void cleanupPlayer(MediaPlayer mp) { - if (mp != null) { - try { - mp.stop(); - mp.release(); - } catch (IllegalStateException ex) { - Log.w(TAG, "MediaPlayer IllegalStateException: "+ex); - } - } - } - private void onPersistSafeVolumeState(int state) { Settings.Global.putInt(mContentResolver, Settings.Global.AUDIO_SAFE_VOLUME_STATE, @@ -5374,24 +4979,25 @@ public class AudioService extends IAudioService.Stub break; case MSG_UNLOAD_SOUND_EFFECTS: - onUnloadSoundEffects(); + mSfxHelper.unloadSoundEffects(); break; case MSG_LOAD_SOUND_EFFECTS: - //FIXME: onLoadSoundEffects() should be executed in a separate thread as it - // can take several dozens of milliseconds to complete - boolean loaded = onLoadSoundEffects(); - if (msg.obj != null) { - LoadSoundEffectReply reply = (LoadSoundEffectReply)msg.obj; - synchronized (reply) { - reply.mStatus = loaded ? 0 : -1; - reply.notify(); + { + LoadSoundEffectReply reply = (LoadSoundEffectReply) msg.obj; + if (mSystemReady) { + mSfxHelper.loadSoundEffects(reply); + } else { + Log.w(TAG, "[schedule]loadSoundEffects() called before boot complete"); + if (reply != null) { + reply.run(false); } } + } break; case MSG_PLAY_SOUND_EFFECT: - onPlaySoundEffect(msg.arg1, msg.arg2); + mSfxHelper.playSoundEffect(msg.arg1, msg.arg2); break; case MSG_SET_FORCE_USE: diff --git a/services/core/java/com/android/server/audio/SoundEffectsHelper.java b/services/core/java/com/android/server/audio/SoundEffectsHelper.java new file mode 100644 index 000000000000..4413ab64601b --- /dev/null +++ b/services/core/java/com/android/server/audio/SoundEffectsHelper.java @@ -0,0 +1,470 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.audio; + +import android.content.Context; +import android.content.res.Resources; +import android.content.res.XmlResourceParser; +import android.media.AudioAttributes; +import android.media.AudioManager; +import android.media.AudioSystem; +import android.media.MediaPlayer; +import android.media.MediaPlayer.OnCompletionListener; +import android.media.MediaPlayer.OnErrorListener; +import android.media.SoundPool; +import android.os.Environment; +import android.os.Looper; +import android.util.Log; + +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.lang.reflect.Field; +import java.util.ArrayList; +import java.util.List; + +/** + * A helper class for managing sound effects loading / unloading + * used by AudioService. + * @hide + */ +class SoundEffectsHelper { + private static final String TAG = "AS.SoundEffectsHelper"; + + 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 SOUND_EFFECT_FILES = new ArrayList(); + + /* 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; + + // volume applied to sound played with playSoundEffect() + private static int sSoundEffectVolumeDb; + + interface OnEffectsLoadCompleteHandler { + void run(boolean success); + } + + SoundEffectsHelper(Context context) { + mContext = context; + sSoundEffectVolumeDb = mContext.getResources().getInteger( + com.android.internal.R.integer.config_soundEffectVolumeDb); + } + + /*package*/ void loadSoundEffects(OnEffectsLoadCompleteHandler onComplete) { + boolean success = doLoadSoundEffects(); + 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); + } + + /** + * 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) { + 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; + } + + 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; + } + } + mSoundPool.release(); + mSoundPool = null; + } + } + + /*package*/ void playSoundEffect(int effectType, int volume) { + synchronized (mSoundEffectsLock) { + + doLoadSoundEffects(); + + if (mSoundPool == null) { + return; + } + float volFloat; + // use default if volume is not specified by caller + if (volume < 0) { + volFloat = (float) Math.pow(10, (float) sSoundEffectVolumeDb / 20); + } else { + volFloat = volume / 1000.0f; + } + + if (mSoundEffectFilesMap[effectType][1] > 0) { + mSoundPool.play(mSoundEffectFilesMap[effectType][1], + volFloat, volFloat, 0, 0, 1.0f); + } else { + MediaPlayer mediaPlayer = new MediaPlayer(); + try { + String filePath = getSoundEffectFilePath(effectType); + 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); + } + } + } + } + + private static void cleanupPlayer(MediaPlayer mp) { + if (mp != null) { + try { + mp.stop(); + mp.release(); + } catch (IllegalStateException ex) { + Log.w(TAG, "MediaPlayer IllegalStateException: " + ex); + } + } + } + + private static final String TAG_AUDIO_ASSETS = "audio_assets"; + private static final String ATTR_VERSION = "version"; + private static final String TAG_GROUP = "group"; + private static final String ATTR_GROUP_NAME = "name"; + private static final String TAG_ASSET = "asset"; + private static final String ATTR_ASSET_ID = "id"; + private static final String ATTR_ASSET_FILE = "file"; + + 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 String getSoundEffectFilePath(int effectType) { + String filePath = Environment.getProductDirectory() + SOUND_EFFECTS_PATH + + SOUND_EFFECT_FILES.get(mSoundEffectFilesMap[effectType][0]); + if (!new File(filePath).isFile()) { + filePath = Environment.getRootDirectory() + SOUND_EFFECTS_PATH + + SOUND_EFFECT_FILES.get(mSoundEffectFilesMap[effectType][0]); + } + 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; + } + } + + private void loadTouchSoundAssets() { + XmlResourceParser parser = null; + + // only load assets once. + if (!SOUND_EFFECT_FILES.isEmpty()) { + return; + } + + loadTouchSoundAssetDefaults(); + + try { + parser = mContext.getResources().getXml(com.android.internal.R.xml.audio_assets); + + XmlUtils.beginDocument(parser, TAG_AUDIO_ASSETS); + String version = parser.getAttributeValue(null, ATTR_VERSION); + boolean inTouchSoundsGroup = false; + + 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)) { + inTouchSoundsGroup = true; + break; + } + } + } + while (inTouchSoundsGroup) { + XmlUtils.nextElement(parser); + String element = parser.getName(); + if (element == null) { + break; + } + 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 touch sound ID: " + id); + 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; + } else { + break; + } + } + } + } catch (Resources.NotFoundException e) { + Log.w(TAG, "audio assets file not found", e); + } catch (XmlPullParserException e) { + Log.w(TAG, "XML parser exception reading touch sound assets", e); + } catch (IOException e) { + Log.w(TAG, "I/O exception reading touch sound assets", e); + } finally { + if (parser != null) { + parser.close(); + } + } + } + + private final class SoundPoolListenerThread extends Thread { + SoundPoolListenerThread() { + super("SoundPoolListenerThread"); + } + + @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(); + } + Looper.loop(); + } + } + + private final class SoundPoolCallback implements + android.media.SoundPool.OnLoadCompleteListener { + + @GuardedBy("mSoundEffectsLock") + private int mStatus = 1; // 1 means neither error nor last sample loaded yet + @GuardedBy("mSoundEffectsLock") + List mSamples = new ArrayList(); + + @GuardedBy("mSoundEffectsLock") + public int status() { + return mStatus; + } + + @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]); + } + } + } + + 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) || mSamples.isEmpty()) { + mStatus = status; + mSoundEffectsLock.notify(); + } + } + } + } +} -- cgit v1.2.3-59-g8ed1b From 251c595896e9a27c3fb70a61541c479f435442e0 Mon Sep 17 00:00:00 2001 From: Mikhail Naganov Date: Tue, 2 Jul 2019 14:35:42 -0700 Subject: Refactor SoundEffectsHelper for asynchronous loading Use a dedicated thread for all SoundPool and MediaPlayer operations. This ensures that the client (AudioService) doesn't get blocked or delayed. This also removes the need to use locking as all access to data happens on the same thread. Simplify the implementation of the mapping between UI effect IDs and SoundPool sample IDs. Add event logging for sound effects loading. Bug: 135763139 Test: normal scenario media volume responsiveness with stuck SoundPool behavior when declared sound assets are missing Change-Id: Ie5a1684a2cabe05ff53270ad97cab161a87a59f9 --- .../com/android/server/audio/AudioService.java | 2 + .../android/server/audio/SoundEffectsHelper.java | 553 +++++++++++---------- 2 files changed, 304 insertions(+), 251 deletions(-) diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index 6ee6175a4661..e7690bfd7bf8 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -6028,6 +6028,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:"); diff --git a/services/core/java/com/android/server/audio/SoundEffectsHelper.java b/services/core/java/com/android/server/audio/SoundEffectsHelper.java index 4413ab64601b..cf5bc8d88c73 100644 --- a/services/core/java/com/android/server/audio/SoundEffectsHelper.java +++ b/services/core/java/com/android/server/audio/SoundEffectsHelper.java @@ -27,267 +27,254 @@ 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 SOUND_EFFECT_FILES = new ArrayList(); - - /* 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 mResources = new ArrayList(); + 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); + } + + 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*/); } - /* - * 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]]; + return; + } + + 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(); } } - // 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."); - } - } + }); + 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 { - status = -1; + logEvent("effect " + filePath + " rejected by SoundPool"); + Log.w(TAG, "SoundPool could not load file: " + filePath); } + } - 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; - } + 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) { - 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; - } - - 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; + void onUnloadSoundEffects() { + 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 = null; } + mSoundPool.release(); + mSoundPool = null; + logEvent("effects unloading completed"); } - /*package*/ void playSoundEffect(int effectType, int volume) { - synchronized (mSoundEffectsLock) { - - doLoadSoundEffects(); - - if (mSoundPool == null) { - return; - } - float volFloat; - // use default if volume is not specified by caller - if (volume < 0) { - volFloat = (float) Math.pow(10, (float) sSoundEffectVolumeDb / 20); - } else { - volFloat = volume / 1000.0f; - } + 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; + } - if (mSoundEffectFilesMap[effectType][1] > 0) { - mSoundPool.play(mSoundEffectFilesMap[effectType][1], - volFloat, volFloat, 0, 0, 1.0f); - } else { - MediaPlayer mediaPlayer = new MediaPlayer(); - try { - String filePath = getSoundEffectFilePath(effectType); - 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); - } + 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 = 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); } } } @@ -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; } } @@ -338,7 +323,7 @@ class SoundEffectsHelper { XmlResourceParser parser = null; // only load assets once. - if (!SOUND_EFFECT_FILES.isEmpty()) { + if (!mResources.isEmpty()) { return; } @@ -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; } @@ -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 mSamples = new ArrayList(); + private List mLoadCompleteHandlers = + new ArrayList(); - @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 (remainingToLoad == 0) { + onComplete(true); } - if ((status != 0) || mSamples.isEmpty()) { - mStatus = status; - mSoundEffectsLock.notify(); + } 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")); } } } -- cgit v1.2.3-59-g8ed1b