Files
frameworks_base/media/java/android/media/MediaActionSound.java
Eric Laurent d7a267de78 MediaActionSound: fix SoundPool load race condition
If play() was called shortly after load() and before the load
completion callback, the sample was considered
loaded although the SoundPool was not ready to play it.

Fixed by implementing a more robust state machine for sound
load and play control.

Bug: 22499793
Change-Id: I727e07f842a0adc5225dc248f16bc4a7225a1c8e
2016-04-04 12:53:01 -07:00

290 lines
11 KiB
Java

/*
* Copyright (C) 2012 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 android.media;
import android.media.AudioManager;
import android.media.SoundPool;
import android.util.Log;
/**
* <p>A class for producing sounds that match those produced by various actions
* taken by the media and camera APIs. </p>
*
* <p>This class is recommended for use with the {@link android.hardware.camera2} API, since the
* camera2 API does not play any sounds on its own for any capture or video recording actions.</p>
*
* <p>With the older {@link android.hardware.Camera} API, use this class to play an appropriate
* camera operation sound when implementing a custom still or video recording mechanism (through the
* Camera preview callbacks with
* {@link android.hardware.Camera#setPreviewCallback Camera.setPreviewCallback}, or through GPU
* processing with {@link android.hardware.Camera#setPreviewTexture Camera.setPreviewTexture}, for
* example), or when implementing some other camera-like function in your application.</p>
*
* <p>There is no need to play sounds when using
* {@link android.hardware.Camera#takePicture Camera.takePicture} or
* {@link android.media.MediaRecorder} for still images or video, respectively,
* as the Android framework will play the appropriate sounds when needed for
* these calls.</p>
*
*/
public class MediaActionSound {
private static final int NUM_MEDIA_SOUND_STREAMS = 1;
private SoundPool mSoundPool;
private SoundState[] mSounds;
private static final String[] SOUND_FILES = {
"/system/media/audio/ui/camera_click.ogg",
"/system/media/audio/ui/camera_focus.ogg",
"/system/media/audio/ui/VideoRecord.ogg",
"/system/media/audio/ui/VideoStop.ogg"
};
private static final String TAG = "MediaActionSound";
/**
* The sound used by
* {@link android.hardware.Camera#takePicture Camera.takePicture} to
* indicate still image capture.
* @see #play
*/
public static final int SHUTTER_CLICK = 0;
/**
* A sound to indicate that focusing has completed. Because deciding
* when this occurs is application-dependent, this sound is not used by
* any methods in the media or camera APIs.
* @see #play
*/
public static final int FOCUS_COMPLETE = 1;
/**
* The sound used by
* {@link android.media.MediaRecorder#start MediaRecorder.start()} to
* indicate the start of video recording.
* @see #play
*/
public static final int START_VIDEO_RECORDING = 2;
/**
* The sound used by
* {@link android.media.MediaRecorder#stop MediaRecorder.stop()} to
* indicate the end of video recording.
* @see #play
*/
public static final int STOP_VIDEO_RECORDING = 3;
/**
* States for SoundState.
* STATE_NOT_LOADED : sample not loaded
* STATE_LOADING : sample being loaded: waiting for load completion callback
* STATE_LOADING_PLAY_REQUESTED : sample being loaded and playback request received
* STATE_LOADED : sample loaded, ready for playback
*/
private static final int STATE_NOT_LOADED = 0;
private static final int STATE_LOADING = 1;
private static final int STATE_LOADING_PLAY_REQUESTED = 2;
private static final int STATE_LOADED = 3;
private class SoundState {
public final int name;
public int id;
public int state;
public SoundState(int name) {
this.name = name;
id = 0; // 0 is an invalid sample ID.
state = STATE_NOT_LOADED;
}
}
/**
* Construct a new MediaActionSound instance. Only a single instance is
* needed for playing any platform media action sound; you do not need a
* separate instance for each sound type.
*/
public MediaActionSound() {
mSoundPool = new SoundPool.Builder()
.setMaxStreams(NUM_MEDIA_SOUND_STREAMS)
.setAudioAttributes(new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION)
.setFlags(AudioAttributes.FLAG_AUDIBILITY_ENFORCED)
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
.build())
.build();
mSoundPool.setOnLoadCompleteListener(mLoadCompleteListener);
mSounds = new SoundState[SOUND_FILES.length];
for (int i = 0; i < mSounds.length; i++) {
mSounds[i] = new SoundState(i);
}
}
private int loadSound(SoundState sound) {
int id = mSoundPool.load(SOUND_FILES[sound.name], 1);
if (id > 0) {
sound.state = STATE_LOADING;
sound.id = id;
}
return id;
}
/**
* Preload a predefined platform sound to minimize latency when the sound is
* played later by {@link #play}.
* @param soundName The type of sound to preload, selected from
* SHUTTER_CLICK, FOCUS_COMPLETE, START_VIDEO_RECORDING, or
* STOP_VIDEO_RECORDING.
* @see #play
* @see #SHUTTER_CLICK
* @see #FOCUS_COMPLETE
* @see #START_VIDEO_RECORDING
* @see #STOP_VIDEO_RECORDING
*/
public void load(int soundName) {
if (soundName < 0 || soundName >= SOUND_FILES.length) {
throw new RuntimeException("Unknown sound requested: " + soundName);
}
SoundState sound = mSounds[soundName];
synchronized (sound) {
switch (sound.state) {
case STATE_NOT_LOADED:
if (loadSound(sound) <= 0) {
Log.e(TAG, "load() error loading sound: " + soundName);
}
break;
default:
Log.e(TAG, "load() called in wrong state: " + sound + " for sound: "+ soundName);
break;
}
}
}
/**
* <p>Play one of the predefined platform sounds for media actions.</p>
*
* <p>Use this method to play a platform-specific sound for various media
* actions. The sound playback is done asynchronously, with the same
* behavior and content as the sounds played by
* {@link android.hardware.Camera#takePicture Camera.takePicture},
* {@link android.media.MediaRecorder#start MediaRecorder.start}, and
* {@link android.media.MediaRecorder#stop MediaRecorder.stop}.</p>
*
* <p>With the {@link android.hardware.camera2 camera2} API, this method can be used to play
* standard camera operation sounds with the appropriate system behavior for such sounds.</p>
* <p>With the older {@link android.hardware.Camera} API, using this method makes it easy to
* match the default device sounds when recording or capturing data through the preview
* callbacks, or when implementing custom camera-like features in your application.</p>
*
* <p>If the sound has not been loaded by {@link #load} before calling play,
* play will load the sound at the cost of some additional latency before
* sound playback begins. </p>
*
* @param soundName The type of sound to play, selected from
* SHUTTER_CLICK, FOCUS_COMPLETE, START_VIDEO_RECORDING, or
* STOP_VIDEO_RECORDING.
* @see android.hardware.Camera#takePicture
* @see android.media.MediaRecorder
* @see #SHUTTER_CLICK
* @see #FOCUS_COMPLETE
* @see #START_VIDEO_RECORDING
* @see #STOP_VIDEO_RECORDING
*/
public void play(int soundName) {
if (soundName < 0 || soundName >= SOUND_FILES.length) {
throw new RuntimeException("Unknown sound requested: " + soundName);
}
SoundState sound = mSounds[soundName];
synchronized (sound) {
switch (sound.state) {
case STATE_NOT_LOADED:
loadSound(sound);
if (loadSound(sound) <= 0) {
Log.e(TAG, "play() error loading sound: " + soundName);
break;
}
// FALL THROUGH
case STATE_LOADING:
sound.state = STATE_LOADING_PLAY_REQUESTED;
break;
case STATE_LOADED:
mSoundPool.play(sound.id, 1.0f, 1.0f, 0, 0, 1.0f);
break;
default:
Log.e(TAG, "play() called in wrong state: " + sound.state + " for sound: "+ soundName);
break;
}
}
}
private SoundPool.OnLoadCompleteListener mLoadCompleteListener =
new SoundPool.OnLoadCompleteListener() {
public void onLoadComplete(SoundPool soundPool,
int sampleId, int status) {
for (SoundState sound : mSounds) {
if (sound.id != sampleId) {
continue;
}
int playSoundId = 0;
synchronized (sound) {
if (status != 0) {
sound.state = STATE_NOT_LOADED;
sound.id = 0;
Log.e(TAG, "OnLoadCompleteListener() error: " + status +
" loading sound: "+ sound.name);
return;
}
switch (sound.state) {
case STATE_LOADING:
sound.state = STATE_LOADED;
break;
case STATE_LOADING_PLAY_REQUESTED:
playSoundId = sound.id;
sound.state = STATE_LOADED;
break;
default:
Log.e(TAG, "OnLoadCompleteListener() called in wrong state: "
+ sound.state + " for sound: "+ sound.name);
break;
}
}
if (playSoundId != 0) {
soundPool.play(playSoundId, 1.0f, 1.0f, 0, 0, 1.0f);
}
break;
}
}
};
/**
* Free up all audio resources used by this MediaActionSound instance. Do
* not call any other methods on a MediaActionSound instance after calling
* release().
*/
public void release() {
if (mSoundPool != null) {
for (SoundState sound : mSounds) {
synchronized (sound) {
sound.state = STATE_NOT_LOADED;
sound.id = 0;
}
}
mSoundPool.release();
mSoundPool = null;
}
}
}