7 package com.google.appinventor.components.runtime;
25 import android.content.Context;
26 import android.media.AudioManager;
27 import android.media.SoundPool;
28 import android.os.Handler;
29 import android.os.Vibrator;
30 import android.util.Log;
32 import java.io.IOException;
33 import java.util.HashMap;
55 @DesignerComponent(version = YaVersion.SOUND_COMPONENT_VERSION,
56 description =
"<p>A multimedia component that plays sound " +
57 "files and optionally vibrates for the number of milliseconds " +
58 "(thousandths of a second) specified in the Blocks Editor. The name of " +
59 "the sound file to play can be specified either in the Designer or in " +
60 "the Blocks Editor.</p> <p>For supported sound file formats, see " +
61 "<a href=\"http://developer.android.com/guide/appendix/media-formats.html\"" +
62 " target=\"_blank\">Android Supported Media Formats</a>.</p>" +
63 "<p>This <code>Sound</code> component is best for short sound files, such as sound " +
64 "effects, while the <code>Player</code> component is more efficient for " +
65 "longer sounds, such as songs.</p>" +
66 "<p>You might get an error if you attempt to play a sound " +
67 "immeditely after setting the source.</p>",
68 category = ComponentCategory.MEDIA,
70 iconName =
"images/soundEffect.png")
72 @UsesPermissions(permissionNames =
"android.permission.VIBRATE, android.permission.INTERNET")
76 private boolean loadComplete;
83 private class OnLoadHelper {
84 public void setOnloadCompleteListener (SoundPool soundPool) {
85 soundPool.setOnLoadCompleteListener(
new android.media.SoundPool.OnLoadCompleteListener() {
86 public void onLoadComplete(SoundPool soundPool,
int sampleId,
int status) {
93 private static final int MAX_STREAMS = 10;
96 private static final int MAX_PLAY_DELAY_RETRIES = 10;
98 private static final int PLAY_DELAY_LENGTH = 50;
100 private static final float VOLUME_FULL = 1.0f;
101 private static final int LOOP_MODE_NO_LOOP = 0;
102 private static final float PLAYBACK_RATE_NORMAL = 1.0f;
103 private SoundPool soundPool;
116 private String sourcePath;
118 private int streamId;
119 private int minimumInterval;
120 private long timeLastPlayed;
121 private final Vibrator vibe;
122 private final Handler playWaitHandler =
new Handler();
129 super(container.
$form());
130 thisComponent =
this;
131 soundPool =
new SoundPool(MAX_STREAMS, AudioManager.STREAM_MUSIC, 0);
132 soundMap =
new HashMap<String, Integer>();
133 vibe = (Vibrator) form.getSystemService(Context.VIBRATOR_SERVICE);
136 form.registerForOnResume(
this);
137 form.registerForOnStop(
this);
138 form.registerForOnDestroy(
this);
141 form.setVolumeControlStream(AudioManager.STREAM_MUSIC);
144 MinimumInterval(500);
146 if (waitForLoadToComplete) {
147 new OnLoadHelper().setOnloadCompleteListener(soundPool);
158 description =
"The name of the sound file. Only certain " +
159 "formats are supported. See http://developer.android.com/guide/appendix/media-formats.html.")
160 public String Source() {
178 sourcePath = (path ==
null) ?
"" : path;
182 soundPool.stop(streamId);
187 if (sourcePath.length() != 0) {
188 Integer existingSoundId = soundMap.get(sourcePath);
189 if (existingSoundId !=
null) {
190 soundId = existingSoundId;
193 Log.i(
"Sound",
"No existing sound with path " + sourcePath +
".");
196 if (newSoundId != 0) {
197 soundMap.put(sourcePath, newSoundId);
198 Log.i(
"Sound",
"Successfully began loading sound: setting soundId to " + newSoundId +
".");
199 soundId = newSoundId;
201 loadComplete =
false;
203 form.dispatchErrorOccurredEvent(
this,
"Source",
207 form.dispatchPermissionDeniedEvent(
this,
"Source", e);
208 }
catch (IOException e) {
209 form.dispatchErrorOccurredEvent(
this,
"Source",
225 description =
"The minimum interval, in milliseconds, between sounds. If you play a sound, " +
226 "all further Play() calls will be ignored until the interval has elapsed.")
227 public
int MinimumInterval() {
228 return minimumInterval;
239 defaultValue =
"500")
242 minimumInterval = interval;
247 private int delayRetries;
252 @
SimpleFunction(description =
"Plays the sound specified by the Source property.")
255 long currentTime = System.currentTimeMillis();
256 if (timeLastPlayed == 0 || currentTime >= timeLastPlayed + minimumInterval) {
257 timeLastPlayed = currentTime;
258 delayRetries = MAX_PLAY_DELAY_RETRIES;
259 playWhenLoadComplete();
262 Log.i(
"Sound",
"Unable to play because MinimumInterval has not elapsed since last play.");
267 Log.i(
"Sound",
"Sound Id was 0. Did you remember to set the Source property?");
268 form.dispatchErrorOccurredEvent(
this,
"Play",
274 private void playWhenLoadComplete() {
275 if (loadComplete || !waitForLoadToComplete) {
276 playAndCheckResult();
278 Log.i(
"Sound",
"Sound not ready: retrying. Remaining retries = " + delayRetries);
282 playWaitHandler.postDelayed(
new Runnable() {
286 playAndCheckResult();
287 }
else if (delayRetries > 0) {
289 playWhenLoadComplete();
291 form.dispatchErrorOccurredEvent(thisComponent,
"Play",
292 ErrorMessages.ERROR_SOUND_NOT_READY, sourcePath);
295 }, PLAY_DELAY_LENGTH);
299 private void playAndCheckResult() {
300 streamId = soundPool.play(soundId, VOLUME_FULL, VOLUME_FULL, 0, LOOP_MODE_NO_LOOP,
301 PLAYBACK_RATE_NORMAL);
302 Log.i(
"Sound",
"SoundPool.play returned stream id " + streamId);
304 form.dispatchErrorOccurredEvent(
this,
"Play",
305 ErrorMessages.ERROR_UNABLE_TO_PLAY_MEDIA, sourcePath);
313 @SimpleFunction(description =
"Pauses playing the sound if it is being played.")
314 public
void Pause() {
316 soundPool.pause(streamId);
318 Log.i(
"Sound",
"Unable to pause. Did you remember to call the Play function?");
325 @
SimpleFunction(description =
"Resumes playing the sound after a pause.")
326 public
void Resume() {
328 soundPool.resume(streamId);
330 Log.i(
"Sound",
"Unable to resume. Did you remember to call the Play function?");
337 @
SimpleFunction(description =
"Stops playing the sound if it is being played.")
340 soundPool.stop(streamId);
343 Log.i(
"Sound",
"Unable to stop. Did you remember to call the Play function?");
350 @
SimpleFunction(description =
"Vibrates for the specified number of milliseconds.")
351 public
void Vibrate(
int millisecs) {
352 vibe.vibrate(millisecs);
355 @
SimpleEvent(description =
"The SoundError event is no longer used. " +
356 "Please use the Screen.ErrorOccurred event instead.",
358 public
void SoundError(String message) {
365 Log.i(
"Sound",
"Got onStop");
367 soundPool.pause(streamId);
375 Log.i(
"Sound",
"Got onResume");
377 soundPool.resume(streamId);
395 private void prepareToDie() {
397 soundPool.stop(streamId);
398 soundPool.unload(streamId);