/* * 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; /** *

A class for producing sounds that match those produced by various actions * taken by the media and camera APIs.

* *

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.

* *

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.

* *

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.

* */ 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; } } } /** *

Play one of the predefined platform sounds for media actions.

* *

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}.

* *

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.

*

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.

* *

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.

* * @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; } } }