AI2 Component  (Version nb184)
Sound.java
Go to the documentation of this file.
1 // -*- mode: java; c-basic-offset: 2; -*-
3 // Copyright 2011-2018 MIT, All rights reserved
4 // Released under the Apache License, Version 2.0
5 // http://www.apache.org/licenses/LICENSE-2.0
6 
7 package com.google.appinventor.components.runtime;
8 
24 
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;
31 
32 import java.io.IOException;
33 import java.util.HashMap;
34 import java.util.Map;
35 
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,
69  nonVisible = true,
70  iconName = "images/soundEffect.png")
71 @SimpleObject
72 @UsesPermissions(permissionNames = "android.permission.VIBRATE, android.permission.INTERNET")
73 public class Sound extends AndroidNonvisibleComponent
75 
76  private boolean loadComplete; // did the sound finish loading
77 
78  // The purpose of this class is to avoid getting rejected by the Android verifier when the
79  // Sound component code is loaded into a device with API level less than 8, where the verifier
80  // will reject OnLoadCompleteListener. We do this trick by putting
81  // the use of OnLoadCompleteListener in the class OnLoadHelper and arranging (see below) for
82  // the class to be compiled only if the API level is at least 8.
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) {
87  loadComplete = true;
88  }
89  });
90  }
91  }
92 
93  private static final int MAX_STREAMS = 10;
94 
95  // max number of consecutive delays to wait for a sound to load
96  private static final int MAX_PLAY_DELAY_RETRIES = 10;
97  // number of ms in each delay before retrying
98  private static final int PLAY_DELAY_LENGTH = 50;
99 
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;
104 
105  // soundMap maps sounds (assets, etc) that are loaded into soundPool to their respective
106  // soundIds.
107  private final Map<String, Integer> soundMap;
108 
109  // We will wait for Sound loading to complete before trying to play, but only
110  // if the API level is at least 8, because onLoadCompleteListener is not available
111  // in earlier APIs. For those early systems, attempting to play a sound before it is loaded
112  // will fail to play the sound and there will be no retry, although there might be a "cannot
113  // play" error.
114  private final boolean waitForLoadToComplete = (SdkLevel.getLevel() >= SdkLevel.LEVEL_FROYO);
115 
116  private String sourcePath; // name of source
117  private int soundId; // id of sound in the soundPool
118  private int streamId; // stream id returned from last call to SoundPool.play
119  private int minimumInterval; // minimum interval between Play() calls
120  private long timeLastPlayed; // the system time when Play() was last called
121  private final Vibrator vibe;
122  private final Handler playWaitHandler = new Handler();
123 
124  //save a pointer to this Sound component to use in the error in postDelayed below
125  private final Component thisComponent;
126 
127 
128  public Sound(ComponentContainer container) {
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);
134  sourcePath = "";
135  loadComplete = true; //nothing to wait for until we attempt to load
136  form.registerForOnResume(this);
137  form.registerForOnStop(this);
138  form.registerForOnDestroy(this);
139 
140  // Make volume buttons control media, not ringer.
141  form.setVolumeControlStream(AudioManager.STREAM_MUSIC);
142 
143  // Default property values
144  MinimumInterval(500);
145 
146  if (waitForLoadToComplete) {
147  new OnLoadHelper().setOnloadCompleteListener(soundPool);
148  }
149  }
150 
151 
152 
157  category = PropertyCategory.BEHAVIOR,
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() {
161  return sourcePath;
162  }
163 
175  defaultValue = "")
177  public void Source(String path) {
178  sourcePath = (path == null) ? "" : path;
179 
180  // Clear the previous sound.
181  if (streamId != 0) {
182  soundPool.stop(streamId);
183  streamId = 0;
184  }
185  soundId = 0;
186 
187  if (sourcePath.length() != 0) {
188  Integer existingSoundId = soundMap.get(sourcePath);
189  if (existingSoundId != null) {
190  soundId = existingSoundId;
191 
192  } else {
193  Log.i("Sound", "No existing sound with path " + sourcePath + ".");
194  try {
195  int newSoundId = MediaUtil.loadSoundPool(soundPool, form, 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;
200  // set flag to show that loading has begun
201  loadComplete = false;
202  } else {
203  form.dispatchErrorOccurredEvent(this, "Source",
205  }
206  } catch (PermissionException e) {
207  form.dispatchPermissionDeniedEvent(this, "Source", e);
208  } catch (IOException e) {
209  form.dispatchErrorOccurredEvent(this, "Source",
211  }
212  }
213  }
214  }
215 
224  category = PropertyCategory.BEHAVIOR,
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;
229  }
230 
239  defaultValue = "500")
241  public void MinimumInterval(int interval) {
242  minimumInterval = interval;
243  }
244 
245 
246  // number of retries remaining before signaling an error
247  private int delayRetries;
248 
252  @SimpleFunction(description = "Plays the sound specified by the Source property.")
253  public void Play() {
254  if (soundId != 0) {
255  long currentTime = System.currentTimeMillis();
256  if (timeLastPlayed == 0 || currentTime >= timeLastPlayed + minimumInterval) {
257  timeLastPlayed = currentTime;
258  delayRetries = MAX_PLAY_DELAY_RETRIES;
259  playWhenLoadComplete();
260  } else {
261  // fail silently
262  Log.i("Sound", "Unable to play because MinimumInterval has not elapsed since last play.");
263  }
264  } else {
265  // Alert the user that the sound is bad, but would need to look in the log to distinguish
266  // this error from the UNABLE_TO_PLAY_MEDIA error in playAndCheck.
267  Log.i("Sound", "Sound Id was 0. Did you remember to set the Source property?");
268  form.dispatchErrorOccurredEvent(this, "Play",
270  }
271  }
272 
273  // Attempt to play the sound, possibly after a delay to allow the sound to load.
274  private void playWhenLoadComplete() {
275  if (loadComplete || !waitForLoadToComplete) {
276  playAndCheckResult();
277  } else {
278  Log.i("Sound", "Sound not ready: retrying. Remaining retries = " + delayRetries);
279  // if the sound wasn't ready we retry after a delay. We implement the delay by posting
280  // to a separate handler: using a loop with a sleep might seem simpler, but it would block
281  // the UI thread.
282  playWaitHandler.postDelayed(new Runnable() {
283  @Override
284  public void run() {
285  if (loadComplete) {
286  playAndCheckResult();
287  } else if (delayRetries > 0) {
288  delayRetries--;
289  playWhenLoadComplete();
290  } else {
291  form.dispatchErrorOccurredEvent(thisComponent, "Play",
292  ErrorMessages.ERROR_SOUND_NOT_READY, sourcePath);
293  }
294  }
295  }, PLAY_DELAY_LENGTH);
296  }
297  }
298 
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);
303  if (streamId == 0) {
304  form.dispatchErrorOccurredEvent(this, "Play",
305  ErrorMessages.ERROR_UNABLE_TO_PLAY_MEDIA, sourcePath);
306 }
307  }
308 
309 
313  @SimpleFunction(description = "Pauses playing the sound if it is being played.")
314  public void Pause() {
315  if (streamId != 0) {
316  soundPool.pause(streamId);
317  } else {
318  Log.i("Sound", "Unable to pause. Did you remember to call the Play function?");
319  }
320  }
321 
325  @SimpleFunction(description = "Resumes playing the sound after a pause.")
326  public void Resume() {
327  if (streamId != 0) {
328  soundPool.resume(streamId);
329  } else {
330  Log.i("Sound", "Unable to resume. Did you remember to call the Play function?");
331  }
332  }
333 
337 @SimpleFunction(description = "Stops playing the sound if it is being played.")
338  public void Stop() {
339  if (streamId != 0) {
340  soundPool.stop(streamId);
341  streamId = 0;
342  } else {
343  Log.i("Sound", "Unable to stop. Did you remember to call the Play function?");
344  }
345  }
346 
350  @SimpleFunction(description = "Vibrates for the specified number of milliseconds.")
351  public void Vibrate(int millisecs) {
352  vibe.vibrate(millisecs);
353  }
354 
355  @SimpleEvent(description = "The SoundError event is no longer used. " +
356  "Please use the Screen.ErrorOccurred event instead.",
357  userVisible = false)
358  public void SoundError(String message) {
359  }
360 
361  // OnStopListener implementation
362 
363  @Override
364  public void onStop() {
365  Log.i("Sound", "Got onStop");
366  if (streamId != 0) {
367  soundPool.pause(streamId);
368  }
369  }
370 
371  // OnResumeListener implementation
372 
373  @Override
374  public void onResume() {
375  Log.i("Sound", "Got onResume");
376  if (streamId != 0) {
377  soundPool.resume(streamId);
378  }
379  }
380 
381  // OnDestroyListener implementation
382 
383  @Override
384  public void onDestroy() {
385  prepareToDie();
386  }
387 
388  // Deletable implementation
389 
390  @Override
391  public void onDelete() {
392  prepareToDie();
393  }
394 
395  private void prepareToDie() {
396  if (streamId != 0) {
397  soundPool.stop(streamId);
398  soundPool.unload(streamId);
399  }
400  soundPool.release();
401  vibe.cancel();
402  // The documentation for SoundPool suggests setting the reference to null;
403  soundPool = null;
404  }
405 }
com.google.appinventor.components.annotations.SimpleFunction
Definition: SimpleFunction.java:23
com.google.appinventor.components.runtime.util.ErrorMessages
Definition: ErrorMessages.java:17
com.google.appinventor.components.runtime.util
-*- mode: java; c-basic-offset: 2; -*-
Definition: AccountChooser.java:7
com.google.appinventor.components.runtime.util.ErrorMessages.ERROR_UNABLE_TO_LOAD_MEDIA
static final int ERROR_UNABLE_TO_LOAD_MEDIA
Definition: ErrorMessages.java:93
com.google.appinventor.components.runtime.util.ErrorMessages.ERROR_UNABLE_TO_PLAY_MEDIA
static final int ERROR_UNABLE_TO_PLAY_MEDIA
Definition: ErrorMessages.java:95
com.google.appinventor.components.common.YaVersion
Definition: YaVersion.java:14
com.google.appinventor.components.annotations.DesignerProperty
Definition: DesignerProperty.java:25
com.google.appinventor.components
com.google.appinventor.components.common.PropertyTypeConstants.PROPERTY_TYPE_NON_NEGATIVE_INTEGER
static final String PROPERTY_TYPE_NON_NEGATIVE_INTEGER
Definition: PropertyTypeConstants.java:206
com.google.appinventor.components.runtime.util.MediaUtil.loadSoundPool
static int loadSoundPool(SoundPool soundPool, Form form, String mediaPath)
Definition: MediaUtil.java:686
com.google.appinventor.components.runtime.util.MediaUtil
Definition: MediaUtil.java:53
com.google.appinventor.components.annotations.DesignerComponent
Definition: DesignerComponent.java:22
com.google.appinventor.components.annotations.SimpleEvent
Definition: SimpleEvent.java:20
com.google.appinventor.components.runtime.Sound.Sound
Sound(ComponentContainer container)
Definition: Sound.java:128
com.google.appinventor.components.annotations.PropertyCategory.BEHAVIOR
BEHAVIOR
Definition: PropertyCategory.java:15
com.google.appinventor.components.runtime.Sound.onResume
void onResume()
Definition: Sound.java:374
com.google.appinventor.components.runtime.Sound.onDestroy
void onDestroy()
Definition: Sound.java:384
com.google.appinventor.components.annotations.UsesPermissions
Definition: UsesPermissions.java:21
com.google.appinventor.components.runtime.OnResumeListener
Definition: OnResumeListener.java:14
com.google.appinventor.components.runtime.Sound
Definition: Sound.java:73
com.google.appinventor.components.runtime.AndroidNonvisibleComponent
Definition: AndroidNonvisibleComponent.java:17
com.google.appinventor.components.runtime.util.SdkLevel
Definition: SdkLevel.java:19
com.google.appinventor.components.runtime.Sound.onStop
void onStop()
Definition: Sound.java:364
com.google.appinventor.components.annotations.SimpleProperty
Definition: SimpleProperty.java:23
com.google.appinventor.components.annotations.PropertyCategory
Definition: PropertyCategory.java:13
com.google.appinventor.components.runtime.errors.PermissionException
Definition: PermissionException.java:16
com.google.appinventor.components.runtime.ComponentContainer
Definition: ComponentContainer.java:16
com.google.appinventor.components.runtime.util.SdkLevel.getLevel
static int getLevel()
Definition: SdkLevel.java:45
com.google.appinventor.components.runtime
Copyright 2009-2011 Google, All Rights reserved.
Definition: AccelerometerSensor.java:8
com.google.appinventor.components.runtime.Sound.MinimumInterval
void MinimumInterval(int interval)
Definition: Sound.java:241
com.google.appinventor.components.runtime.Component
Definition: Component.java:17
com.google.appinventor.components.runtime.Map< String, Integer >
com.google.appinventor.components.runtime.Deleteable
Definition: Deleteable.java:15
com.google.appinventor.components.runtime.Sound.onDelete
void onDelete()
Definition: Sound.java:391
com.google.appinventor.components.common
Definition: ComponentCategory.java:7
com.google.appinventor.components.runtime.Sound.Source
void Source(String path)
Definition: Sound.java:177
com.google.appinventor.components.common.ComponentCategory
Definition: ComponentCategory.java:48
com.google.appinventor.components.runtime.util.SdkLevel.LEVEL_FROYO
static final int LEVEL_FROYO
Definition: SdkLevel.java:25
com.google.appinventor.components.runtime.OnStopListener
Definition: OnStopListener.java:15
com.google.appinventor.components.annotations.SimpleObject
Definition: SimpleObject.java:23
com.google
com
com.google.appinventor.components.runtime.errors
Definition: ArrayIndexOutOfBoundsError.java:7
com.google.appinventor.components.runtime.ComponentContainer.$form
Form $form()
com.google.appinventor.components.common.PropertyTypeConstants.PROPERTY_TYPE_ASSET
static final String PROPERTY_TYPE_ASSET
Definition: PropertyTypeConstants.java:22
com.google.appinventor.components.common.PropertyTypeConstants
Definition: PropertyTypeConstants.java:14
com.google.appinventor.components.runtime.OnDestroyListener
Definition: OnDestroyListener.java:15
com.google.appinventor.components.annotations
com.google.appinventor