1 /* 2 * Copyright (c) 2009-2010 jMonkeyEngine 3 * All rights reserved. 4 * 5 * Redistribution and use in source and binary forms, with or without 6 * modification, are permitted provided that the following conditions are 7 * met: 8 * 9 * * Redistributions of source code must retain the above copyright 10 * notice, this list of conditions and the following disclaimer. 11 * 12 * * Redistributions in binary form must reproduce the above copyright 13 * notice, this list of conditions and the following disclaimer in the 14 * documentation and/or other materials provided with the distribution. 15 * 16 * * Neither the name of 'jMonkeyEngine' nor the names of its contributors 17 * may be used to endorse or promote products derived from this software 18 * without specific prior written permission. 19 * 20 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 22 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 23 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR 24 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 25 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 26 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 27 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 28 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 29 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 30 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 */ 32 package com.jme3.audio.android; 33 34 import android.app.Activity; 35 import android.content.Context; 36 import android.content.res.AssetFileDescriptor; 37 import android.content.res.AssetManager; 38 import android.media.AudioManager; 39 import android.media.MediaPlayer; 40 import android.media.SoundPool; 41 import android.util.Log; 42 43 import com.jme3.asset.AssetKey; 44 import com.jme3.audio.AudioNode.Status; 45 import com.jme3.audio.*; 46 import com.jme3.math.FastMath; 47 import com.jme3.math.Vector3f; 48 import java.io.IOException; 49 import java.util.HashMap; 50 import java.util.concurrent.atomic.AtomicBoolean; 51 import java.util.logging.Level; 52 import java.util.logging.Logger; 53 54 /** 55 * This class is the android implementation for {@link AudioRenderer} 56 * 57 * @author larynx 58 * @author plan_rich 59 */ 60 public class AndroidAudioRenderer implements AudioRenderer, 61 SoundPool.OnLoadCompleteListener, MediaPlayer.OnCompletionListener { 62 63 private static final Logger logger = Logger.getLogger(AndroidAudioRenderer.class.getName()); 64 private final static int MAX_NUM_CHANNELS = 16; 65 private final HashMap<AudioNode, MediaPlayer> musicPlaying = new HashMap<AudioNode, MediaPlayer>(); 66 private SoundPool soundPool = null; 67 private final Vector3f listenerPosition = new Vector3f(); 68 // For temp use 69 private final Vector3f distanceVector = new Vector3f(); 70 private final Context context; 71 private final AssetManager assetManager; 72 private HashMap<Integer, AudioNode> soundpoolStillLoading = new HashMap<Integer, AudioNode>(); 73 private Listener listener; 74 private boolean audioDisabled = false; 75 private final AudioManager manager; 76 77 public AndroidAudioRenderer(Activity context) { 78 this.context = context; 79 manager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); 80 context.setVolumeControlStream(AudioManager.STREAM_MUSIC); 81 assetManager = context.getAssets(); 82 } 83 84 @Override 85 public void initialize() { 86 soundPool = new SoundPool(MAX_NUM_CHANNELS, AudioManager.STREAM_MUSIC, 87 0); 88 soundPool.setOnLoadCompleteListener(this); 89 } 90 91 @Override 92 public void updateSourceParam(AudioNode src, AudioParam param) { 93 // logger.log(Level.INFO, "updateSourceParam " + param); 94 95 if (audioDisabled) { 96 return; 97 } 98 99 if (src.getChannel() < 0) { 100 return; 101 } 102 103 switch (param) { 104 case Position: 105 if (!src.isPositional()) { 106 return; 107 } 108 109 Vector3f pos = src.getWorldTranslation(); 110 break; 111 case Velocity: 112 if (!src.isPositional()) { 113 return; 114 } 115 116 Vector3f vel = src.getVelocity(); 117 break; 118 case MaxDistance: 119 if (!src.isPositional()) { 120 return; 121 } 122 break; 123 case RefDistance: 124 if (!src.isPositional()) { 125 return; 126 } 127 break; 128 case ReverbFilter: 129 if (!src.isPositional() || !src.isReverbEnabled()) { 130 return; 131 } 132 break; 133 case ReverbEnabled: 134 if (!src.isPositional()) { 135 return; 136 } 137 138 if (src.isReverbEnabled()) { 139 updateSourceParam(src, AudioParam.ReverbFilter); 140 } 141 break; 142 case IsPositional: 143 break; 144 case Direction: 145 if (!src.isDirectional()) { 146 return; 147 } 148 149 Vector3f dir = src.getDirection(); 150 break; 151 case InnerAngle: 152 if (!src.isDirectional()) { 153 return; 154 } 155 break; 156 case OuterAngle: 157 if (!src.isDirectional()) { 158 return; 159 } 160 break; 161 case IsDirectional: 162 if (src.isDirectional()) { 163 updateSourceParam(src, AudioParam.Direction); 164 updateSourceParam(src, AudioParam.InnerAngle); 165 updateSourceParam(src, AudioParam.OuterAngle); 166 } else { 167 } 168 break; 169 case DryFilter: 170 if (src.getDryFilter() != null) { 171 Filter f = src.getDryFilter(); 172 if (f.isUpdateNeeded()) { 173 // updateFilter(f); 174 } 175 } 176 break; 177 case Looping: 178 if (src.isLooping()) { 179 } 180 break; 181 case Volume: 182 183 soundPool.setVolume(src.getChannel(), src.getVolume(), 184 src.getVolume()); 185 186 break; 187 case Pitch: 188 189 break; 190 } 191 192 } 193 194 @Override 195 public void updateListenerParam(Listener listener, ListenerParam param) { 196 // logger.log(Level.INFO, "updateListenerParam " + param); 197 if (audioDisabled) { 198 return; 199 } 200 201 switch (param) { 202 case Position: 203 listenerPosition.set(listener.getLocation()); 204 205 break; 206 case Rotation: 207 Vector3f dir = listener.getDirection(); 208 Vector3f up = listener.getUp(); 209 210 break; 211 case Velocity: 212 Vector3f vel = listener.getVelocity(); 213 214 break; 215 case Volume: 216 // alListenerf(AL_GAIN, listener.getVolume()); 217 break; 218 } 219 220 } 221 222 @Override 223 public void update(float tpf) { 224 float distance; 225 float volume; 226 227 // Loop over all mediaplayers 228 for (AudioNode src : musicPlaying.keySet()) { 229 230 MediaPlayer mp = musicPlaying.get(src); 231 { 232 // Calc the distance to the listener 233 distanceVector.set(listenerPosition); 234 distanceVector.subtractLocal(src.getLocalTranslation()); 235 distance = FastMath.abs(distanceVector.length()); 236 237 if (distance < src.getRefDistance()) { 238 distance = src.getRefDistance(); 239 } 240 if (distance > src.getMaxDistance()) { 241 distance = src.getMaxDistance(); 242 } 243 volume = src.getRefDistance() / distance; 244 245 AndroidAudioData audioData = (AndroidAudioData) src.getAudioData(); 246 247 if (FastMath.abs(audioData.getCurrentVolume() - volume) > FastMath.FLT_EPSILON) { 248 // Left / Right channel get the same volume by now, only 249 // positional 250 mp.setVolume(volume, volume); 251 252 audioData.setCurrentVolume(volume); 253 } 254 } 255 } 256 } 257 258 public void setListener(Listener listener) { 259 if (audioDisabled) { 260 return; 261 } 262 263 if (this.listener != null) { 264 // previous listener no longer associated with current 265 // renderer 266 this.listener.setRenderer(null); 267 } 268 269 this.listener = listener; 270 this.listener.setRenderer(this); 271 272 } 273 274 @Override 275 public void cleanup() { 276 // Cleanup sound pool 277 if (soundPool != null) { 278 soundPool.release(); 279 soundPool = null; 280 } 281 282 // Cleanup media player 283 for (AudioNode src : musicPlaying.keySet()) { 284 MediaPlayer mp = musicPlaying.get(src); 285 { 286 mp.stop(); 287 mp.release(); 288 src.setStatus(Status.Stopped); 289 } 290 } 291 musicPlaying.clear(); 292 } 293 294 @Override 295 public void onCompletion(MediaPlayer mp) { 296 mp.seekTo(0); 297 mp.stop(); 298 // XXX: This has bad performance -> maybe change overall structure of 299 // mediaplayer in this audiorenderer? 300 for (AudioNode src : musicPlaying.keySet()) { 301 if (musicPlaying.get(src) == mp) { 302 src.setStatus(Status.Stopped); 303 break; 304 } 305 } 306 } 307 308 /** 309 * Plays using the {@link SoundPool} of Android. Due to hard limitation of 310 * the SoundPool: After playing more instances of the sound you only have 311 * the channel of the last played instance. 312 * 313 * It is not possible to get information about the state of the soundpool of 314 * a specific streamid, so removing is not possilbe -> noone knows when 315 * sound finished. 316 */ 317 public void playSourceInstance(AudioNode src) { 318 if (audioDisabled) { 319 return; 320 } 321 322 AndroidAudioData audioData = (AndroidAudioData) src.getAudioData(); 323 324 if (!(audioData.getAssetKey() instanceof AudioKey)) { 325 throw new IllegalArgumentException("Asset is not a AudioKey"); 326 } 327 328 AudioKey assetKey = (AudioKey) audioData.getAssetKey(); 329 330 try { 331 if (audioData.getId() < 0) { // found something to load 332 int soundId = soundPool.load( 333 assetManager.openFd(assetKey.getName()), 1); 334 audioData.setId(soundId); 335 } 336 337 int channel = soundPool.play(audioData.getId(), 1f, 1f, 1, 0, 1f); 338 339 if (channel == 0) { 340 soundpoolStillLoading.put(audioData.getId(), src); 341 } else { 342 src.setChannel(channel); // receive a channel at the last 343 // playing at least 344 } 345 } catch (IOException e) { 346 logger.log(Level.SEVERE, 347 "Failed to load sound " + assetKey.getName(), e); 348 audioData.setId(-1); 349 } 350 } 351 352 @Override 353 public void onLoadComplete(SoundPool soundPool, int sampleId, int status) { 354 AudioNode src = soundpoolStillLoading.remove(sampleId); 355 356 if (src == null) { 357 logger.warning("Something went terribly wrong! onLoadComplete" 358 + " had sampleId which was not in the HashMap of loading items"); 359 return; 360 } 361 362 AudioData audioData = src.getAudioData(); 363 364 if (status == 0) // load was successfull 365 { 366 int channelIndex; 367 channelIndex = soundPool.play(audioData.getId(), 1f, 1f, 1, 0, 1f); 368 src.setChannel(channelIndex); 369 } 370 } 371 372 public void playSource(AudioNode src) { 373 if (audioDisabled) { 374 return; 375 } 376 377 AndroidAudioData audioData = (AndroidAudioData) src.getAudioData(); 378 379 MediaPlayer mp = musicPlaying.get(src); 380 if (mp == null) { 381 mp = new MediaPlayer(); 382 mp.setOnCompletionListener(this); 383 mp.setAudioStreamType(AudioManager.STREAM_MUSIC); 384 } 385 386 try { 387 AssetKey<?> key = audioData.getAssetKey(); 388 389 AssetFileDescriptor afd = assetManager.openFd(key.getName()); // assetKey.getName() 390 mp.setDataSource(afd.getFileDescriptor(), afd.getStartOffset(), 391 afd.getLength()); 392 mp.prepare(); 393 mp.setLooping(src.isLooping()); 394 mp.start(); 395 src.setChannel(0); 396 src.setStatus(Status.Playing); 397 musicPlaying.put(src, mp); 398 399 } catch (IllegalStateException e) { 400 e.printStackTrace(); 401 } catch (Exception e) { 402 e.printStackTrace(); 403 } 404 } 405 406 /** 407 * Pause the current playing sounds. Both from the {@link SoundPool} and the 408 * active {@link MediaPlayer}s 409 */ 410 public void pauseAll() { 411 if (soundPool != null) { 412 soundPool.autoPause(); 413 for (MediaPlayer mp : musicPlaying.values()) { 414 mp.pause(); 415 } 416 } 417 } 418 419 /** 420 * Resume all paused sounds. 421 */ 422 public void resumeAll() { 423 if (soundPool != null) { 424 soundPool.autoResume(); 425 for (MediaPlayer mp : musicPlaying.values()) { 426 mp.start(); //no resume -> api says call start to resume 427 } 428 } 429 } 430 431 public void pauseSource(AudioNode src) { 432 if (audioDisabled) { 433 return; 434 } 435 436 MediaPlayer mp = musicPlaying.get(src); 437 if (mp != null) { 438 mp.pause(); 439 src.setStatus(Status.Paused); 440 } else { 441 int channel = src.getChannel(); 442 if (channel != -1) { 443 soundPool.pause(channel); // is not very likley to make 444 } // something useful :) 445 } 446 } 447 448 public void stopSource(AudioNode src) { 449 if (audioDisabled) { 450 return; 451 } 452 453 // can be stream or buffer -> so try to get mediaplayer 454 // if there is non try to stop soundpool 455 MediaPlayer mp = musicPlaying.get(src); 456 if (mp != null) { 457 mp.stop(); 458 src.setStatus(Status.Paused); 459 } else { 460 int channel = src.getChannel(); 461 if (channel != -1) { 462 soundPool.pause(channel); // is not very likley to make 463 // something useful :) 464 } 465 } 466 467 } 468 469 @Override 470 public void deleteAudioData(AudioData ad) { 471 472 for (AudioNode src : musicPlaying.keySet()) { 473 if (src.getAudioData() == ad) { 474 MediaPlayer mp = musicPlaying.remove(src); 475 mp.stop(); 476 mp.release(); 477 src.setStatus(Status.Stopped); 478 src.setChannel(-1); 479 ad.setId(-1); 480 break; 481 } 482 } 483 484 if (ad.getId() > 0) { 485 soundPool.unload(ad.getId()); 486 ad.setId(-1); 487 } 488 } 489 490 @Override 491 public void setEnvironment(Environment env) { 492 // not yet supported 493 } 494 495 @Override 496 public void deleteFilter(Filter filter) { 497 } 498 } 499