1 # This file is part of NIT ( http://www.nitlanguage.org ).
3 # Copyright 2014 Romain Chanoir <romain.chanoir@viacesi.fr>
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
9 # http://www.apache.org/licenses/LICENSE-2.0
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
17 # Android audio services, wraps a part of android audio API
18 # This module modifies the default behaviour of the audio loading:
19 # It is first loaded from the `res/raw` folder.
20 # The file extension is not needed for the `res/raw` loading part.
21 # If it didn't work, it is loaded from the `assets` folder.
22 # The file extension is needed for the `assets` loading part.
24 # `assets` contains the portable version of sounds, since the `res` folder exsists only in android projects.
26 # For this example, the sounds "test_sound" and "test_music" are located in the "assets/sounds" folder,
27 # they both have ".ogg" extension. "test_sound" is a short sound and "test_music" a music track
30 # # Note that you need to specify the path from "assets" folder and the extension
31 # var s = app.load_sound("sounds/test_sound.ogg")
32 # var m = app.load_music("sounds/test_music.ogg")
37 # Now, the sounds are in "res/raw"
39 # s = app.load_sound_from_res("test_sound")
40 # m = app.load_music_from_res("test_sound")
45 # See http://developer.android.com/reference/android/media/package-summary.html for more infos
50 intrude import assets_and_resources
55 import android.media.MediaPlayer;
56 import android.media.SoundPool;
57 import java.io.IOException;
58 import android.media.AudioManager;
59 import android.media.AudioManager.OnAudioFocusChangeListener;
60 import android.content.Context;
61 import android.util.Log;
64 # FIXME: This listener is not working at the moment, but is needed to gain or give up the audio focus
67 static OnAudioFocusChangeListener afChangeListener = new OnAudioFocusChangeListener() {
68 public void onAudioFocusChange(int focusChange) {
69 if(focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT) {
70 }else if (focusChange == AudioManager.AUDIOFOCUS_GAIN) {
71 }else if (focusChange == AudioManager.AUDIOFOCUS_LOSS) {
77 # AudioManager of the application, used to manage the audio mode
78 private extern class NativeAudioManager in "Java" `{ android.media.AudioManager `}
82 # ( MODE_NORMAL = 0, MODE_RINGTONE = 1, MODE_IN_CALL = 2 or MODE_IN_COMMUNICATION = 3 )
83 fun mode: Int in "Java" `{ return self.getMode(); `}
85 # Sets the audio mode.
86 # ( MODE_NORMAL = 0, MODE_RINGTONE = 1, MODE_IN_CALL = 2 or MODE_IN_COMMUNICATION = 3 )
87 fun mode
=(i
: Int) in "Java" `{ self.setMode((int)i); `}
89 # Sends a request to obtain audio focus
90 fun request_audio_focus: Int in "Java" `{
91 return self.requestAudioFocus
(afChangeListener
, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN);
94 # Gives up audio focus
95 fun abandon_audio_focus: Int in "Java" `{ return self.abandonAudioFocus(afChangeListener); `}
98 # Media Player from Java, used to play long sounds or musics, not simultaneously
99 # This is a low-level class, use `MediaPlater` instead
100 private extern class NativeMediaPlayer in "Java" `{ android.media.MediaPlayer `}
104 MediaPlayer mp
= new MediaPlayer();
105 mp
.setAudioStreamType
(AudioManager.STREAM_MUSIC);
108 fun start in "Java" `{ self.start(); `}
109 fun prepare
in "Java" `{
112 }catch(Exception e) {
113 Log.e("Error preparing the Media Player", e.getMessage());
118 new create
(context
: NativeActivity, id
: Int): NativeMediaPlayer
121 return MediaPlayer.create(context, (int)id);
122 }catch(Exception e) {
127 fun pause
in "Java" `{ self.pause(); `}
128 fun stop in "Java" `{ self.stop(); `}
129 fun playing
: Bool in "Java" `{ return self.isPlaying(); `}
130 fun release in "Java" `{ self.release(); `}
131 fun duration
: Int in "Java" `{ return self.getDuration(); `}
132 fun looping: Bool in "Java" `{ return self.isLooping(); `}
133 fun looping
=(b
: Bool) in "Java" `{ self.setLooping(b); `}
134 fun volume=(vol: Float) in "Java" `{ self.setVolume((float)vol, (float)vol); `}
135 fun both_volume
(left_volume
, right_volume
: Float) in "Java" `{ self.setVolume((float)left_volume, (float)right_volume); `}
136 fun stream_type=(stream_type: Int) in "Java" `{ self.setAudioStreamType((int)stream_type); `}
137 fun data_source_fd
(fd
: NativeFileDescriptor, start_offset
, length
: Int): Int in "Java" `{
139 self.setDataSource(fd, start_offset, length);
141 }catch(Exception e) {
145 fun data_source_path
(path
: JavaString): Int in "Java" `{
147 self.setDataSource(path);
149 }catch(Exception e) {
150 Log.e("Error loading the Media Player", e.getMessage());
154 fun reset
in "Java" `{ self.reset(); `}
157 # Sound Pool from Java, used to play sounds simultaneously
158 # This is a low-level class, use `SoundPool`instead
159 private extern class NativeSoundPool in "Java" `{ android.media.SoundPool `}
162 new(max_streams
, stream_type
, src_quality
: Int) in "Java" `{
163 return new SoundPool((int)max_streams, (int)stream_type, (int)src_quality);
165 fun load_asset_fd
(afd
: NativeAssetFileDescriptor, priority
: Int): Int in "Java" `{
167 int id = self.load(afd, (int)priority);
174 fun load_id
(context
: NativeActivity, resid
, priority
: Int): Int in "Java" `{
176 int id = self.load(context, (int)resid, (int)priority);
183 fun load_path
(path
: JavaString, priority
: Int): Int in "Java" `{
185 int id = self.load(path, (int)priority);
192 fun play
(sound_id
: Int, left_volume
, right_volume
: Float, priority
, l
: Int, rate
: Float): Int in "Java" `{
193 return self.play((int)sound_id, (float)left_volume, (float)right_volume, (int)priority, (int)l, (float)rate);
195 fun pause
(stream_id
: Int) in "Java" `{ self.pause((int)stream_id); `}
196 fun auto_pause in "Java" `{ self.autoPause(); `}
197 fun auto_resume
in "Java" `{ self.autoResume(); `}
198 fun resume(stream_id: Int) in "Java" `{ self.resume((int)stream_id); `}
199 fun set_loop
(stream_id
, l
: Int) in "Java" `{ self.setLoop((int)stream_id, (int)l); `}
200 fun set_priority(stream_id, priority: Int) in "Java" `{ self.setPriority((int)stream_id, (int)priority); `}
201 fun set_rate
(stream_id
: Int, rate
: Float) in "Java" `{ self.setRate((int)stream_id, (float)rate); `}
202 fun set_volume(stream_id: Int, left_volume, right_volume: Float) in "Java" `{ self.setVolume((int)stream_id, (float)left_volume, (float)right_volume); `}
203 fun stop
(stream_id
: Int) in "Java" `{ self.stop((int)stream_id); `}
204 fun unload(sound_id: Int): Bool in "Java" `{ return self.unload((int)sound_id); `}
205 fun release
in "Java" `{ self.release(); `}
209 # Used to play sound, best suited for sounds effects in apps or games
212 # Latest error on this sound pool
213 var error: nullable Error = null
215 private var nsoundpool: NativeSoundPool is noinit
217 # The maximum number of simultaneous streams for this SoundPool
218 var max_streams = 10 is writable
220 # The audio stream type, 3 is STREAM_MUSIC, default for game application
221 var stream_type = 3 is writable
223 # The sample-rate converter quality, currently has no effect
224 var src_quality = 0 is writable
226 # Left volume value, range 0.0 to 1.0
227 var left_volume = 1.0 is writable
229 # Right volume value, range 0.0 to 1.0
230 var right_volume = 1.0 is writable
232 # Playback rate, 1.0 = normal playback, range 0.5 to 2.0
233 var rate = 1.0 is writable
235 # Loop mode, 0 = no loop, -1 = loop forever
236 var looping = 0 is writable
239 private var priority = 1
241 init do self.nsoundpool = new NativeSoundPool(max_streams, stream_type, src_quality)
243 # Load the sound from an asset file descriptor
244 # this function is for advanced use
245 private fun load_asset_fd(afd: NativeAssetFileDescriptor): Sound do
246 var resval = nsoundpool.load_asset_fd(afd, self.priority)
248 self.error = new Error("Unable to load sound from assets")
249 return new Sound.priv_init(null, -1, self, self.error)
251 return new Sound.priv_init(null, resval, self, null)
255 # Returns only the id corresponding to the soundpool where the sound is loaded.
256 # Needed by `load
` of `Sound`.
257 private fun load_asset_fd_rid(afd: NativeAssetFileDescriptor): Int do
258 return nsoundpool.load_asset_fd(afd, self.priority)
261 # Load the sound from its resource id
262 fun load_id(context: NativeActivity, id:Int): Sound do
263 var resval = nsoundpool.load_id(context, id, priority)
265 self.error = new Error("Unable to load sound from assets")
266 return new Sound.priv_init(null, -1, self, self.error)
268 return new Sound.priv_init(null, resval, self, null)
272 # Returns only the id corresponding to the soundpool where the sound is loaded.
273 private fun load_id_rid(context: NativeActivity, id: Int): Int do
274 return nsoundpool.load_id(context, id, priority)
277 # Load the sound from the specified path
278 fun load_path(path: String): Sound do
279 sys.jni_env.push_local_frame(1)
280 var resval = nsoundpool.load_path(path.to_java_string, priority)
281 sys.jni_env.pop_local_frame
283 self.error = new Error("Unable to load sound from path : " + path)
284 return new Sound.priv_init(null, -1, self, self.error)
286 return new Sound.priv_init(null, resval, self, null)
290 # Play a sound from a sound ID
291 # return non-zero streamID if successful, zero if failed
292 fun play(id: Int): Int do
293 return nsoundpool.play(id, left_volume, right_volume, priority, looping, rate)
296 # Load a sound by its name in the resources, the sound must be in the `res
/raw
` folder
297 fun load_name(resource_manager: ResourcesManager, context: NativeActivity, sound: String): Sound do
298 var id = resource_manager.raw_id(sound)
299 var resval = nsoundpool.load_id(context, id, priority)
301 self.error = new Error("Unable to load sound from resources : " + sound)
302 return new Sound.priv_init(null, -1, self, self.error)
304 return new Sound.priv_init(null, resval, self, null)
308 # Returns only the id corresponding to the soundpool where the sound is loaded.
309 private fun load_name_rid(resource_manager: ResourcesManager, context: NativeActivity, sound: String): Int do
310 var id = resource_manager.raw_id(sound)
311 return nsoundpool.load_id(context, id, priority)
314 # Pause a playback stream
315 fun pause_stream(stream_id: Int) do nsoundpool.pause(stream_id)
317 # Pause all active_streams
318 fun auto_pause do nsoundpool.auto_pause
320 # Resume all previously active streams
321 fun auto_resume do nsoundpool.auto_resume
323 # Resume a playback stream
324 fun resume(stream_id: Int) do nsoundpool.resume(stream_id)
326 # Set loop mode on a stream
327 fun stream_loop=(stream_id, looping: Int) do nsoundpool.set_loop(stream_id, looping)
329 # Change stream priority
330 fun stream_priority=(stream_id, priority: Int) do nsoundpool.set_priority(stream_id, priority)
332 # Change playback rate
333 fun stream_rate=(stream_id: Int, rate: Float) do nsoundpool.set_rate(stream_id, rate)
336 fun stream_volume(stream_id: Int, left_volume, right_volume: Float) do
337 nsoundpool.set_volume(stream_id, left_volume, right_volume)
340 # Stop a playback stream
341 fun stop_stream(stream_id: Int) do nsoundpool.stop(stream_id)
343 # Unload a sound from a sound ID
344 fun unload(sound: Sound): Bool do return nsoundpool.unload(sound.soundpool_id)
346 # Destroys the object
347 fun destroy do nsoundpool.release
350 # Used to play sounds, designed to use with medium sized sounds or streams
351 # The Android MediaPlayer has a complex state diagram that you'll need to
352 # respect if you want your MediaPlayer to work fine, see the android doc
354 private var nmedia_player: NativeMediaPlayer is noinit
356 # Used to control the state of the mediaplayer
357 private var is_prepared = false is writable
359 # The sound associated with this mediaplayer
360 var sound: nullable Music = null is writable
363 var error: nullable Error = null
365 # Create a new MediaPlayer, but no sound is attached, you'll need
366 # to use `load_sound
` before using it
367 init do self.nmedia_player = new NativeMediaPlayer
369 # Init the mediaplayer with a sound resource id
370 init from_id(context: NativeActivity, id: Int) do
371 self.nmedia_player = new NativeMediaPlayer.create(context, id)
372 if self.nmedia_player.is_java_null then
373 self.error = new Error("Failed to create the MediaPlayer")
374 self.sound = new Music.priv_init(id, self, self.error)
376 self.sound = new Music.priv_init(id, self, null)
379 # Load a sound for a given resource id
380 fun load_sound(id: Int, context: NativeActivity): Music do
381 # FIXME: maybe find a better way to handle this situation
382 # If two different music are loaded with the same `MediaPlayer`,
383 # a new `NativeMediaPlayer` will be created for the secondd music
384 # and the nit program will loose the handle to the previous one
385 # If the previous music is playing, we need to stop it
392 self.nmedia_player = new NativeMediaPlayer.create(context, id)
393 if self.nmedia_player.is_java_null then
394 self.error = new Error("Failed to load a sound")
395 self.sound = new Music.priv_init(id, self, new Error("Sound loading failed"))
396 return self.sound.as(not null)
398 if self.error != null then self.error = null
399 self.sound = new Music.priv_init(id, self, null)
400 self.is_prepared = true
401 return self.sound.as(not null)
405 # Starts or resumes playback
406 # REQUIRE `self.sound
!= null`
408 if self.error != null then return
409 if not is_prepared then prepare
413 # Stops playback after playback has been stopped or paused
414 # REQUIRE `self.sound
!= null`
416 if self.error != null then return
421 # Prepares the player for playback, synchronously
422 # REQUIRE `self.sound
!= null`
424 if self.error != null then return
426 nmedia_player.prepare
431 # REQUIRE `self.sound
!= null`
433 if self.error != null then return
438 # Checks whether the mediaplayer is playing
439 fun playing: Bool do return nmedia_player.playing
441 # Releases the resources associated with this MediaPlayer
443 nmedia_player.release
447 # Reset MediaPlayer to its initial state
448 fun reset do nmedia_player.reset
450 # Sets the datasource (file-path or http/rtsp URL) to use
451 fun data_source(path: String): Music do
452 sys.jni_env.push_local_frame(1)
453 var retval = nmedia_player.data_source_path(path.to_java_string)
454 sys.jni_env.pop_local_frame
456 self.error = new Error("could not load the sound " + path)
457 self.sound = new Music.priv_init(null, self, self.error)
460 self.sound = new Music.priv_init(null, self, null)
462 return self.sound.as(not null)
464 # Sets the data source (NativeFileDescriptor) to use
465 fun data_source_fd(fd: NativeAssetFileDescriptor): Music do
466 if not fd.is_java_null then
467 if nmedia_player.data_source_fd(fd.file_descriptor, fd.start_offset, fd.length) == 0 then
468 self.error = new Error("could not load the sound")
469 self.sound = new Music.priv_init(null, self, self.error)
471 self.sound = new Music.priv_init(null, self, null)
473 return self.sound.as(not null)
475 var error = new Error("could not load the sound")
476 return new Music.priv_init(null, self, error)
480 # Checks whether the MediaPlayer is looping or non-looping
481 fun looping: Bool do return nmedia_player.looping
483 # Sets the player to be looping or non-looping
484 fun looping=(b: Bool) do nmedia_player.looping = b
486 # Sets the volume on this player
487 fun volume=(volume: Float) do nmedia_player.volume = volume
489 # Sets the left volume and the right volume of this player
490 fun both_volume(left_volume, right_volume: Float) do nmedia_player.both_volume(left_volume, right_volume)
492 # Sets the audio stream type for this media player
493 fun stream_type=(stream_type: Int) do nmedia_player.stream_type = stream_type
496 redef class PlayableAudio
497 # Flag to know if the user paused the sound
498 # Used when the app pause all sounds or resume all sounds
499 var paused: Bool = false
501 redef init do add_to_sounds(self)
506 # Resource ID of this sound
507 var id: nullable Int is noinit
509 # The SoundPool who loaded this sound
510 var soundpool: SoundPool is noinit
512 # The SoundID of this sound in his SoundPool
513 var soundpool_id: Int is noinit
515 private init priv_init(id: nullable Int, soundpool_id: Int, soundpool: SoundPool, error: nullable Error) is nosuper do
517 self.soundpool_id = soundpool_id
518 self.soundpool = soundpool
519 if error != null then
522 self.is_loaded = true
527 if is_loaded then return
528 var retval_resources = app.default_soundpool.load_name_rid(app.resource_manager, app.native_activity, self.name.strip_extension)
529 if retval_resources == -1 then
530 self.error = new Error("failed to load" + self.name)
531 var nam = app.asset_manager.open_fd(self.name)
532 if nam.is_java_null then
533 self.error = new Error("Failed to get file descriptor for " + self.name)
535 var retval_assets = app.default_soundpool.load_asset_fd_rid(nam)
536 if retval_assets == -1 then
537 self.error = new Error("Failed to load" + self.name)
539 self.soundpool_id = retval_assets
540 self.soundpool = app.default_soundpool
542 self.soundpool.error = null
546 self.soundpool_id = retval_resources
547 self.soundpool = app.default_soundpool
549 self.soundpool.error = null
555 if not is_loaded then load
556 if self.error != null then return
557 soundpool.play(soundpool_id)
561 if self.error != null or not self.is_loaded then return
562 soundpool.pause_stream(soundpool_id)
567 if self.error != null or not self.is_loaded then return
568 soundpool.resume(soundpool_id)
576 # Resource ID of this sound
577 var id: nullable Int is noinit
579 # The MediaPlayer who loaded this sound
580 var media_player: MediaPlayer is noinit
582 private init priv_init(id: nullable Int, media_player: MediaPlayer, error: nullable Error) is nosuper do
584 self.media_player = media_player
585 if error != null then
588 self.is_loaded = true
593 if is_loaded then return
594 var mp_sound_resources = app.default_mediaplayer.load_sound(app.resource_manager.raw_id(self.name.strip_extension), app.native_activity)
595 if mp_sound_resources.error != null then
596 self.error = mp_sound_resources.error
597 var nam = app.asset_manager.open_fd(self.name)
598 if nam.is_java_null then
599 self.error = new Error("Failed to get file descriptor for " + self.name)
601 var mp_sound_assets = app.default_mediaplayer.data_source_fd(nam)
602 if mp_sound_assets.error != null then
603 self.error = mp_sound_assets.error
605 self.media_player = app.default_mediaplayer
607 self.media_player.error = null
611 self.media_player = app.default_mediaplayer
613 self.media_player.error = null
619 if not is_loaded then load
620 if self.error != null then return
625 if self.error != null or not self.is_loaded then return
631 if self.error != null or not self.is_loaded then return
640 # Returns the default MediaPlayer of the application.
641 # When you load a music, it goes in this MediaPlayer.
642 # Use it for advanced sound management
643 var default_mediaplayer: MediaPlayer is lazy do return new MediaPlayer
645 # Returns the default MediaPlayer of the application.
646 # When you load a short sound (not a music), it's added to this soundpool.
647 # Use it for advanced sound management.
648 var default_soundpool: SoundPool is lazy do return new SoundPool
650 # Get the native audio manager
651 private fun audio_manager: NativeAudioManager import native_activity in "Java" `{
652 return (AudioManager)App_native_activity(self).getSystemService
(Context.AUDIO_SERVICE);
655 # Sets the stream of the app to STREAM_MUSIC.
656 # STREAM_MUSIC is the default stream used by android apps.
657 private fun manage_audio_stream import native_activity in "Java" `{
658 App_native_activity(self).setVolumeControlStream
(AudioManager.STREAM_MUSIC);
661 # Retrieves a sound with a soundpool in the `assets
` folder using its name.
662 # Used to play short songs, can play multiple sounds simultaneously
663 redef fun load_sound(path) do
664 var fd = asset_manager.open_fd(path)
665 if not fd.is_java_null then
666 return add_to_sounds(default_soundpool.load_asset_fd(fd)).as(Sound)
668 var error = new Error("Failed to load Sound {path}")
669 return new Sound.priv_init(null, -1, default_soundpool, error)
673 # Retrieves a music with a media player in the `assets
` folder using its name.
674 # Used to play long sounds or musics, can't play multiple sounds simultaneously
675 redef fun load_music(path) do
676 var fd = asset_manager.open_fd(path)
677 if not fd.is_java_null then
678 return add_to_sounds(default_mediaplayer.data_source_fd(fd)).as(Music)
680 var error = new Error("Failed to load music {path}")
681 return new Music.priv_init(null, default_mediaplayer, error)
685 # Same as `load_sound
` but load the sound from the `res
/raw
` folder
686 fun load_sound_from_res(sound_name: String): Sound do
687 return add_to_sounds(default_soundpool.load_name(resource_manager,self.native_activity, sound_name)).as(Sound)
690 # Same as `load_music
` but load the sound from the `res
/raw
` folder
691 fun load_music_from_res(music: String): Music do
692 return add_to_sounds(default_mediaplayer.load_sound(resource_manager.raw_id(music), self.native_activity)).as(Music)
695 redef fun on_pause do
698 # Pausing sounds that are not already paused by user
699 # `s
.paused
` is set to false because `pause
` set it to true
700 # and we want to know which sound has been paused by the user
701 # and which one has been paused by the app
707 audio_manager.abandon_audio_focus
710 redef fun on_create do
712 audio_manager.request_audio_focus
716 redef fun on_resume do
718 audio_manager.request_audio_focus
720 # Resumes only the sounds paused by the App
721 if not s.paused then s.resume
728 # Sounds handled by the application, when you load a sound, it's added to this list.
729 # This array is used in `pause
` and `resume
`
730 private var sounds = new Array[PlayableAudio]
732 # Factorizes `sounds
.add
` to use it in `load_music
`, `load_sound
`, `load_music_from_res
` and `load_sound_from_res
`
733 private fun add_to_sounds(sound: PlayableAudio): PlayableAudio do