audio: move some services between the abstraction and the Android layers
[nit.git] / lib / android / audio.nit
index 9374ac7..a8d0afe 100644 (file)
 # limitations under the License.
 
 # Android audio services, wraps a part of android audio API
+# This module modifies the default behaviour of the audio loading:
+# It is first loaded from the `res/raw` folder.
+# The file extension is not needed for the `res/raw` loading part.
+# If it didn't work, it is loaded from the `assets` folder.
+# The file extension is needed for the `assets` loading part.
+#
+# `assets` contains the portable version of sounds, since the `res` folder exsists only in android projects.
 #
 # For this example, the sounds "test_sound" and "test_music" are located in the "assets/sounds" folder,
 # they both have ".ogg" extension. "test_sound" is a short sound and "test_music" a music track
+#
 # ~~~nitish
 # # Note that you need to specify the path from "assets" folder and the extension
 # var s = app.load_sound("sounds/test_sound.ogg")
@@ -39,8 +47,8 @@ module audio
 
 import java
 import java::io
-import assets_and_resources
-import native_app_glue # FIXME update this module to use nit_activity
+intrude import assets_and_resources
+import activities
 import app::audio
 
 in "Java" `{
@@ -67,7 +75,7 @@ in "Java inner" `{
 `}
 
 # AudioManager of the application, used to manage the audio mode
-extern class NativeAudioManager in "Java" `{ android.media.AudioManager `}
+private extern class NativeAudioManager in "Java" `{ android.media.AudioManager `}
        super JavaObject
 
        # Current audio mode.
@@ -101,13 +109,21 @@ private extern class NativeMediaPlayer in "Java" `{ android.media.MediaPlayer `}
        fun prepare in "Java" `{
                try {
                        self.prepare();
-               }catch(IOException e) {
+               }catch(Exception e) {
                        Log.e("Error preparing the Media Player", e.getMessage());
                        e.printStackTrace();
                }
        `}
 
-       fun create(context: NativeActivity, id: Int): NativeMediaPlayer in "Java" `{ return self.create(context, (int)id); `}
+       new create(context: NativeActivity, id: Int): NativeMediaPlayer
+       in "Java" `{
+               try {
+                       return MediaPlayer.create(context, (int)id);
+               }catch(Exception e) {
+                       return null;
+               }
+       `}
+
        fun pause in "Java" `{ self.pause(); `}
        fun stop in "Java" `{ self.stop(); `}
        fun playing: Bool in "Java" `{ return self.isPlaying(); `}
@@ -118,20 +134,21 @@ private extern class NativeMediaPlayer in "Java" `{ android.media.MediaPlayer `}
        fun volume=(vol: Float) in "Java" `{ self.setVolume((float)vol, (float)vol); `}
        fun both_volume(left_volume, right_volume: Float) in "Java" `{ self.setVolume((float)left_volume, (float)right_volume); `}
        fun stream_type=(stream_type: Int) in "Java" `{ self.setAudioStreamType((int)stream_type); `}
-       fun data_source_fd(fd: NativeFileDescriptor, start_offset, length: Int) in "Java"  `{
+       fun data_source_fd(fd: NativeFileDescriptor, start_offset, length: Int): Int in "Java"  `{
                try {
                        self.setDataSource(fd, start_offset, length);
-               }catch(IOException e) {
-                       Log.e("Error loading the Media Player with a file descriptor", e.getMessage());
-                       e.printStackTrace();
+                       return 1;
+               }catch(Exception e) {
+                       return 0;
                }
        `}
-       fun data_source_path(path: JavaString) in "Java" `{
+       fun data_source_path(path: JavaString): Int in "Java" `{
                try {
                        self.setDataSource(path);
-               }catch(IOException e) {
+                       return 1;
+               }catch(Exception e) {
                        Log.e("Error loading the Media Player", e.getMessage());
-                       e.printStackTrace();
+                       return 0;
                }
        `}
        fun reset in "Java" `{ self.reset(); `}
@@ -145,9 +162,33 @@ private extern class NativeSoundPool in "Java" `{ android.media.SoundPool `}
        new(max_streams, stream_type, src_quality: Int) in "Java" `{
                return new SoundPool((int)max_streams, (int)stream_type, (int)src_quality);
        `}
-       fun load_asset_fd(afd: NativeAssetFileDescriptor, priority: Int): Int in "Java" `{ return self.load(afd, (int)priority); `}
-       fun load_id(context: NativeActivity, resid, priority: Int): Int in "Java" `{ return self.load(context, (int)resid, (int)priority); `}
-       fun load_path(path: JavaString, priority: Int): Int in "Java" `{ return self.load(path, (int)priority); `}
+       fun load_asset_fd(afd: NativeAssetFileDescriptor, priority: Int): Int in "Java" `{
+               try {
+                       int id =  self.load(afd, (int)priority);
+                       return id;
+               }catch(Exception e){
+                       return -1;
+               }
+       `}
+
+       fun load_id(context: NativeActivity, resid, priority: Int): Int in "Java" `{
+               try {
+                       int id =  self.load(context, (int)resid, (int)priority);
+                       return id;
+               }catch(Exception e){
+                       return -1;
+               }
+       `}
+
+       fun load_path(path: JavaString, priority: Int): Int in "Java" `{
+               try {
+                       int id =  self.load(path, (int)priority);
+                       return id;
+               }catch(Exception e){
+                       return -1;
+               }
+       `}
+
        fun play(sound_id: Int, left_volume, right_volume: Float, priority, l: Int, rate: Float): Int in "Java" `{
                return self.play((int)sound_id, (float)left_volume, (float)right_volume, (int)priority, (int)l, (float)rate);
        `}
@@ -167,7 +208,12 @@ end
 
 # Used to play sound, best suited for sounds effects in apps or games
 class SoundPool
+
+       # Latest error on this sound pool
+       var error: nullable Error = null
+
        private var nsoundpool: NativeSoundPool is noinit
+
        # The maximum number of simultaneous streams for this SoundPool
        var max_streams = 10 is writable
 
@@ -196,21 +242,49 @@ class SoundPool
 
        # Load the sound from an asset file descriptor
        # this function is for advanced use
-       fun load_asset_fd(afd: NativeAssetFileDescriptor): Sound do
-               return new SoundSP(null, nsoundpool.load_asset_fd(afd, priority), self)
+       private fun load_asset_fd(afd: NativeAssetFileDescriptor): Sound do
+               var resval = nsoundpool.load_asset_fd(afd, self.priority)
+               if  resval == -1 then
+                       self.error = new Error("Unable to load sound from assets")
+                       return new Sound.priv_init(null, -1, self, self.error)
+               else
+                       return new Sound.priv_init(null, resval, self, null)
+               end
+       end
+
+       # Returns only the id corresponding to the soundpool where the sound is loaded.
+       # Needed by `load` of `Sound`.
+       private fun load_asset_fd_rid(afd: NativeAssetFileDescriptor): Int do
+               return nsoundpool.load_asset_fd(afd, self.priority)
        end
 
        # Load the sound from its resource id
        fun load_id(context: NativeActivity, id:Int): Sound do
-               return new SoundSP(null, nsoundpool.load_id(context, id, priority), self)
+               var resval = nsoundpool.load_id(context, id, priority)
+                       if  resval == -1 then
+                       self.error = new Error("Unable to load sound from assets")
+                       return new Sound.priv_init(null, -1, self, self.error)
+               else
+                       return new Sound.priv_init(null, resval, self, null)
+               end
+       end
+
+       # Returns only the id corresponding to the soundpool where the sound is loaded.
+       private fun load_id_rid(context: NativeActivity, id: Int): Int do
+               return nsoundpool.load_id(context, id, priority)
        end
 
        # Load the sound from the specified path
        fun load_path(path: String): Sound do
                sys.jni_env.push_local_frame(1)
-               var return_value = new SoundSP(0, nsoundpool.load_path(path.to_java_string, priority), self)
+               var resval = nsoundpool.load_path(path.to_java_string, priority)
                sys.jni_env.pop_local_frame
-               return return_value
+               if  resval == -1 then
+                       self.error = new Error("Unable to load sound from path: " + path)
+                       return new Sound.priv_init(null, -1, self, self.error)
+               else
+                       return new Sound.priv_init(null, resval, self, null)
+               end
        end
 
        # Play a sound from a sound ID
@@ -219,10 +293,22 @@ class SoundPool
                return nsoundpool.play(id, left_volume, right_volume, priority, looping, rate)
        end
 
-       # Load a sound by its name in the resources, the sound must be in the `res/raw` folder
-       fun load_name(resource_manager: ResourcesManager, context: NativeActivity, sound: String): Sound do
+       # Load a sound by its `name` in the resources, the sound must be in the `res/raw` folder
+       fun load_name(resource_manager: ResourcesManager, context: NativeActivity, name: String): Sound do
+               var id = resource_manager.raw_id(name)
+               var resval = nsoundpool.load_id(context, id, priority)
+               if  resval == -1 then
+                       self.error = new Error("Unable to load sound from resources: " + name)
+                       return new Sound.priv_init(null, -1, self, self.error)
+               else
+                       return new Sound.priv_init(null, resval, self, null)
+               end
+       end
+
+       # Returns only the id corresponding to the soundpool where the sound is loaded.
+       private fun load_name_rid(resource_manager: ResourcesManager, context: NativeActivity, sound: String): Int do
                var id = resource_manager.raw_id(sound)
-               return new SoundSP(id, nsoundpool.load_id(context, id, priority), self)
+               return nsoundpool.load_id(context, id, priority)
        end
 
        # Pause a playback stream
@@ -255,8 +341,9 @@ class SoundPool
        fun stop_stream(stream_id: Int) do nsoundpool.stop(stream_id)
 
        # Unload a sound from a sound ID
-       fun unload(sound: SoundSP): Bool do return nsoundpool.unload(sound.soundpool_id)
+       fun unload(sound: Sound): Bool do return nsoundpool.unload(sound.soundpool_id)
 
+       # Destroys the object
        fun destroy do nsoundpool.release
 end
 
@@ -270,7 +357,10 @@ class MediaPlayer
        private var is_prepared = false is writable
 
        # The sound associated with this mediaplayer
-       var sound: nullable Sound = null is writable
+       var sound: nullable Music = null is writable
+
+       # Error gestion
+       var error: nullable Error = null
 
        # Create a new MediaPlayer, but no sound is attached, you'll need
        # to use `load_sound` before using it
@@ -278,22 +368,44 @@ class MediaPlayer
 
        # Init the mediaplayer with a sound resource id
        init from_id(context: NativeActivity, id: Int) do
-               self.nmedia_player = new NativeMediaPlayer
-               self.nmedia_player = nmedia_player.create(context, id)
-               self.sound = new SoundMP(id, self)
+               self.nmedia_player = new NativeMediaPlayer.create(context, id)
+               if self.nmedia_player.is_java_null then
+                       self.error = new Error("Failed to create the MediaPlayer")
+                       self.sound = new Music.priv_init(id, self, self.error)
+               end
+               self.sound = new Music.priv_init(id, self, null)
        end
 
        # Load a sound for a given resource id
-       fun load_sound(id: Int, context: NativeActivity): Sound do
-               self.nmedia_player = self.nmedia_player.create(context, id)
-               self.sound = new SoundMP(id, self)
-               self.is_prepared = true
-               return self.sound.as(not null)
+       fun load_sound(id: Int, context: NativeActivity): Music do
+               # FIXME: maybe find a better way to handle this situation
+               # If two different music are loaded with the same `MediaPlayer`,
+               # a new `NativeMediaPlayer` will be created for the secondd music
+               # and the nit program will loose the handle to the previous one
+               # If the previous music is playing, we need to stop it
+               if playing then
+                       stop
+                       reset
+                       destroy
+               end
+
+               self.nmedia_player = new NativeMediaPlayer.create(context, id)
+               if self.nmedia_player.is_java_null then
+                       self.error = new Error("Failed to load a sound")
+                       self.sound = new Music.priv_init(id, self, new Error("Sound loading failed"))
+                       return self.sound.as(not null)
+               else
+                       if self.error != null then self.error = null
+                       self.sound = new Music.priv_init(id, self, null)
+                       self.is_prepared = true
+                       return self.sound.as(not null)
+               end
        end
 
        # Starts or resumes playback
        # REQUIRE `self.sound != null`
        fun start do
+               if self.error != null then return
                if not is_prepared then prepare
                nmedia_player.start
        end
@@ -301,6 +413,7 @@ class MediaPlayer
        # Stops playback after playback has been stopped or paused
        # REQUIRE `self.sound != null`
        fun stop do
+               if self.error != null then return
                is_prepared = false
                nmedia_player.stop
        end
@@ -308,6 +421,7 @@ class MediaPlayer
        # Prepares the player for playback, synchronously
        # REQUIRE `self.sound != null`
        fun prepare do
+               if self.error != null then return
                assert sound != null
                nmedia_player.prepare
                is_prepared = true
@@ -316,6 +430,7 @@ class MediaPlayer
        # Pauses playback
        # REQUIRE `self.sound != null`
        fun pause do
+               if self.error != null then return
                assert sound != null
                nmedia_player.pause
        end
@@ -333,18 +448,33 @@ class MediaPlayer
        fun reset do nmedia_player.reset
 
        # Sets the datasource (file-path or http/rtsp URL) to use
-       fun data_source(path: String): Sound do
+       fun data_source(path: String): Music do
                sys.jni_env.push_local_frame(1)
-               nmedia_player.data_source_path(path.to_java_string)
+               var retval = nmedia_player.data_source_path(path.to_java_string)
                sys.jni_env.pop_local_frame
-               self.sound = new SoundMP(null, self)
+               if retval == 0 then
+                       self.error = new Error("could not load the sound " + path)
+                       self.sound = new Music.priv_init(null, self, self.error)
+
+               else
+                       self.sound = new Music.priv_init(null, self, null)
+               end
                return self.sound.as(not null)
        end
        # Sets the data source (NativeFileDescriptor) to use
-       fun data_source_fd(fd: NativeFileDescriptor, start_offset, length: Int): Sound do
-               nmedia_player.data_source_fd(fd, start_offset, length)
-               self.sound = new SoundMP(null, self)
-               return self.sound.as(not null)
+       fun data_source_fd(fd: NativeAssetFileDescriptor): Music do
+               if not fd.is_java_null then
+                       if nmedia_player.data_source_fd(fd.file_descriptor, fd.start_offset, fd.length) == 0 then
+                               self.error = new Error("could not load the sound")
+                               self.sound = new Music.priv_init(null, self, self.error)
+                       else
+                               self.sound = new Music.priv_init(null, self, null)
+                       end
+                       return self.sound.as(not null)
+               else
+                       var error = new Error("could not load the sound")
+                       return new Music.priv_init(null, self, error)
+               end
        end
 
        # Checks whether the MediaPlayer is looping or non-looping
@@ -363,56 +493,158 @@ class MediaPlayer
        fun stream_type=(stream_type: Int) do nmedia_player.stream_type = stream_type
 end
 
-redef class Sound
+redef class PlayableAudio
+       # Flag to know if the user paused the sound
+       # Used when the app pause all sounds or resume all sounds
+       var paused: Bool = false
 
-       # Resource ID of this sound
-       var id: nullable Int
+       # Is `self` already loaded?
+       protected var is_loaded = false is writable
+
+       redef var error = null
 end
 
-# Sound implemented with a SoundPool
-class SoundSP
-       super Sound
+redef class Sound
+
+       # Resource ID of this sound
+       var id: nullable Int is noinit
 
        # The SoundPool who loaded this sound
-       var soundpool: SoundPool
+       var soundpool: SoundPool is noinit
 
        # The SoundID of this sound in his SoundPool
-       var soundpool_id: Int
+       var soundpool_id: Int is noinit
 
-       private init (id: nullable Int, soundpool_id: Int, soundpool: SoundPool) do
+       private init priv_init(id: nullable Int, soundpool_id: Int, soundpool: SoundPool, error: nullable Error) is nosuper do
                self.id = id
                self.soundpool_id = soundpool_id
                self.soundpool = soundpool
+               if error != null then
+                       self.error = error
+               else
+                       self.is_loaded = true
+               end
+       end
+
+       redef fun load do
+               if is_loaded then return
+               var retval_resources = app.default_soundpool.load_name_rid(app.resource_manager, app.native_activity, path.strip_extension)
+               if retval_resources == -1 then
+                       self.error = new Error("Failed to load " + path)
+                       var nam = app.asset_manager.open_fd(path)
+                       if nam.is_java_null then
+                               self.error = new Error("Failed to get file descriptor for " + path)
+                       else
+                               var retval_assets = app.default_soundpool.load_asset_fd_rid(nam)
+                               if retval_assets == -1 then
+                                       self.error = new Error("Failed to load " + path)
+                               else
+                                       self.soundpool_id = retval_assets
+                                       self.soundpool = app.default_soundpool
+                                       self.error = null
+                                       self.soundpool.error = null
+                               end
+                       end
+               else
+                       self.soundpool_id = retval_resources
+                       self.soundpool = app.default_soundpool
+                       self.error = null
+                       self.soundpool.error = null
+               end
+               is_loaded = true
+
+               var error = error
+               if error != null then print_error error
+       end
+
+       redef fun play do
+               if not is_loaded then load
+               if self.error != null then return
+               soundpool.play(soundpool_id)
+       end
+
+       redef fun pause do
+               if self.error != null or not self.is_loaded then return
+               soundpool.pause_stream(soundpool_id)
+               paused = true
+       end
+
+       redef fun resume do
+               if self.error != null or not self.is_loaded then return
+               soundpool.resume(soundpool_id)
+               paused = false
        end
 
-       redef fun play do soundpool.play(soundpool_id)
-       redef fun pause do soundpool.pause_stream(soundpool_id)
-       redef fun resume do soundpool.resume(soundpool_id)
 end
 
-# Sound Implemented with a MediaPlayer
-class SoundMP
-       super Sound
+redef class Music
+
+       # Resource ID of this sound
+       var id: nullable Int is noinit
 
        # The MediaPlayer who loaded this sound
-       var media_player: MediaPlayer
+       var media_player: MediaPlayer is noinit
 
-       private init (id: nullable Int, media_player: MediaPlayer) do
+       private init priv_init(id: nullable Int, media_player: MediaPlayer, error: nullable Error) is nosuper do
                self.id = id
                self.media_player = media_player
+               if error != null then
+                       self.error = error
+               else
+                       self.is_loaded = true
+               end
+       end
+
+       redef fun load do
+               if is_loaded then return
+               var mp_sound_resources = app.default_mediaplayer.load_sound(app.resource_manager.raw_id(path.strip_extension), app.native_activity)
+               if mp_sound_resources.error != null then
+                       self.error = mp_sound_resources.error
+                       var nam = app.asset_manager.open_fd(path)
+                       if nam.is_java_null then
+                               self.error = new Error("Failed to get file descriptor for " + path)
+                       else
+                               var mp_sound_assets = app.default_mediaplayer.data_source_fd(nam)
+                               if mp_sound_assets.error != null then
+                                       self.error = mp_sound_assets.error
+                               else
+                                       self.media_player = app.default_mediaplayer
+                                       self.error = null
+                                       self.media_player.error = null
+                               end
+                       end
+               else
+                       self.media_player = app.default_mediaplayer
+                       self.error = null
+                       self.media_player.error = null
+               end
+               is_loaded = true
+
+               var error = error
+               if error != null then print_error error
        end
 
-       redef fun play do media_player.start
-       redef fun pause do media_player.pause
-       redef fun resume do play
+       redef fun play do
+               if not is_loaded then load
+               if self.error != null then return
+               media_player.start
+       end
+
+       redef fun pause do
+               if self.error != null or not self.is_loaded then return
+               media_player.pause
+               paused = true
+       end
+
+       redef fun resume do
+               if self.error != null or not self.is_loaded then return
+               play
+               paused = false
+       end
 end
 
 redef class App
 
-       # Sounds handled by the application, when you load a sound, it's added to this list.
-       # This array is used in `pause` and `resume`
-       private var sounds = new Array[Sound]
-
        # Returns the default MediaPlayer of the application.
        # When you load a music, it goes in this MediaPlayer.
        # Use it for advanced sound management
@@ -424,59 +656,53 @@ redef class App
        var default_soundpool: SoundPool is lazy do return new SoundPool
 
        # Get the native audio manager
-       fun audio_manager: NativeAudioManager import native_activity in "Java" `{
+       private fun audio_manager: NativeAudioManager import native_activity in "Java" `{
                return (AudioManager)App_native_activity(self).getSystemService(Context.AUDIO_SERVICE);
        `}
 
        # Sets the stream of the app to STREAM_MUSIC.
        # STREAM_MUSIC is the default stream used by android apps.
-       private fun manage_audio_stream import native_activity, native_app_glue in "Java" `{
+       private fun manage_audio_stream import native_activity in "Java" `{
                App_native_activity(self).setVolumeControlStream(AudioManager.STREAM_MUSIC);
        `}
 
-       # Retrieves a sound with a soundpool in the `assets` folder using its name.
-       # Used to play short songs, can play multiple sounds simultaneously
-       redef fun load_sound(path: String): Sound do
-               return add_to_sounds(default_soundpool.load_asset_fd(asset_manager.open_fd(path)))
-       end
-
-       # Retrieves a music with a media player in the `assets` folder using its name.
-       # Used to play long sounds or musics, can't play multiple sounds simultaneously
-       fun load_music(path: String): Sound do
-               var fd = asset_manager.open_fd(path)
-               return add_to_sounds(default_mediaplayer.data_source_fd(fd.file_descriptor, fd.start_offset, fd.length))
-       end
-
        # Same as `load_sound` but load the sound from the `res/raw` folder
        fun load_sound_from_res(sound_name: String): Sound do
-               return add_to_sounds(default_soundpool.load_name(resource_manager,self.native_activity, sound_name))
+               return default_soundpool.load_name(resource_manager, native_activity, sound_name)
        end
 
        # Same as `load_music` but load the sound from the `res/raw` folder
-       fun load_music_from_res(music: String): Sound do
-               return add_to_sounds(default_mediaplayer.load_sound(resource_manager.raw_id(music), self.native_activity))
-       end
-
-       # Factorizes `sounds.add` to use it in `load_music`, `load_sound`, `load_music_from_res` and `load_sound_from_res`
-       private fun add_to_sounds(sound: Sound): Sound do
-               sounds.add(sound)
-               return sound
+       fun load_music_from_res(music: String): Music do
+               return default_mediaplayer.load_sound(resource_manager.raw_id(music), native_activity)
        end
 
-       redef fun pause do
-               for s in sounds do s.pause
+       redef fun on_pause do
+               super
+               for s in sounds do
+                       # Pausing sounds that are not already paused by user
+                       # `s.paused` is set to false because `pause` set it to true
+                       # and we want to know which sound has been paused by the user
+                       # and which one has been paused by the app
+                       if not s.paused then
+                               s.pause
+                               s.paused = false
+                       end
+               end
                audio_manager.abandon_audio_focus
        end
 
-       redef fun init_window do
+       redef fun on_create do
                super
                audio_manager.request_audio_focus
                manage_audio_stream
        end
 
-       redef fun resume do
+       redef fun on_resume do
                super
                audio_manager.request_audio_focus
-               for s in sounds do s.resume
+               for s in sounds do
+                       # Resumes only the sounds paused by the App
+                       if not s.paused then s.resume
+               end
        end
 end