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 */
32package com.jme3.audio.android;
33
34import android.app.Activity;
35import android.content.Context;
36import android.content.res.AssetFileDescriptor;
37import android.content.res.AssetManager;
38import android.media.AudioManager;
39import android.media.MediaPlayer;
40import android.media.SoundPool;
41import android.util.Log;
42
43import com.jme3.asset.AssetKey;
44import com.jme3.audio.AudioNode.Status;
45import com.jme3.audio.*;
46import com.jme3.math.FastMath;
47import com.jme3.math.Vector3f;
48import java.io.IOException;
49import java.util.HashMap;
50import java.util.concurrent.atomic.AtomicBoolean;
51import java.util.logging.Level;
52import 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 */
60public 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