Merge: Audio management for android + audio Abstraction API
authorJean Privat <jean@pryen.org>
Thu, 22 Jan 2015 06:27:35 +0000 (13:27 +0700)
committerJean Privat <jean@pryen.org>
Thu, 22 Jan 2015 06:27:35 +0000 (13:27 +0700)
Fixes some problems with the audio management for the android platform. The audio API for android still needs some work but is fine to use now !
The Abstraction API for sound management is very simple for now

Pull-Request: #1101
Reviewed-by: Alexis Laferrière <alexis.laf@xymus.net>
Reviewed-by: Alexandre Terrasa <alexandre@moz-code.org>
Reviewed-by: Jean Privat <jean@pryen.org>
Reviewed-by: Lucas Bajolet <r4pass@hotmail.com>

examples/mnit_simple/assets/testsound.ogg [moved from examples/mnit_simple/assets/sound.ogg with 100% similarity]
examples/mnit_simple/res/raw/testsound.ogg [moved from examples/mnit_simple/res/raw/sound.ogg with 100% similarity]
examples/mnit_simple/res/values/dimens.xml
examples/mnit_simple/src/test_audio.nit
lib/android/assets_and_resources.nit
lib/android/audio.nit
lib/app/audio.nit [new file with mode: 0644]

index da43c8f..fa1d020 100644 (file)
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="utf-8"?>
 <resources>
-    <dimen name="test_dimen_1">25dp</dimen>
-    <dimen name="test_dimen_2">150dp</dimen>
-</resources> 
+    <dimen name="test_dimen_1">25px</dimen>
+    <dimen name="test_dimen_2">150px</dimen>
+</resources>
index decf7c5..88a2812 100644 (file)
@@ -21,27 +21,29 @@ import simple_android
 import android::audio
 
 redef class App
-       var soundsp: Sound
-       var soundmp: Sound
+       var soundsp: nullable Sound
+       var soundmp: nullable Sound
+       var test_assets = false
+       var test_ressources = true
 
-       redef fun init_window
+       redef fun window_created
        do
                super
-
-               default_mediaplayer.reset
-               manage_audio_mode
-
-               # Retrieve sound
-               soundsp = load_sound("sound.ogg")
-               soundmp = load_music("xylofon.ogg")
+               if test_assets then
+                       soundsp = load_sound("testsound.ogg")
+                       soundmp = load_music("xylofon.ogg")
+               end
+               if test_ressources then
+                       soundsp = load_sound_from_res("testsound")
+                       soundmp = load_music_from_res("xylofon")
+               end
                default_mediaplayer.looping = true
-               default_mediaplayer.prepare
                soundmp.play
        end
 
        redef fun input( ie )
        do
-               if ie isa PointerEvent and ie.depressed then 
+               if ie isa PointerEvent and ie.depressed then
                        soundsp.play
                end
                return super
index 82ae3d1..8195e2c 100644 (file)
@@ -102,6 +102,13 @@ extern class NativeAssetManager in "Java" `{ android.content.res.AssetManager `}
                }
                return afd;
        `}
+
+       # HACK for bug #845
+       redef fun new_global_ref import sys, Sys.jni_env `{
+               Sys sys = NativeResources_sys(recv);
+               JNIEnv *env = Sys_jni_env(sys);
+               return (*env)->NewGlobalRef(env, recv);
+       `}
 end
 
 # Assets manager using a `NativeAssetManager` to manage android assets
@@ -138,14 +145,18 @@ class AssetManager
 
        # Open an asset using ACCESS_STREAMING mode, returning a NativeInputStream
        fun open(file_name: String): NativeInputStream do
+               sys.jni_env.push_local_frame(1)
                var return_value =  native_assets_manager.open(file_name.to_java_string)
+               sys.jni_env.pop_local_frame
                return return_value
        end
 
        # Open an asset using it's name and returning a NativeAssetFileDescriptor
        # `file_name` is
        fun open_fd(file_name: String): NativeAssetFileDescriptor do
-               var return_value = native_assets_manager.open_fd(file_name.to_java_string)
+               sys.jni_env.push_local_frame(1)
+               var return_value = native_assets_manager.open_fd(file_name.to_java_string).new_global_ref
+               sys.jni_env.pop_local_frame
                return return_value
        end
 
@@ -158,7 +169,7 @@ class AssetManager
        # Return a bitmap from the assets
        fun bitmap(name: String): NativeBitmap do
                sys.jni_env.push_local_frame(1)
-               var return_value = new NativeBitmap.from_stream(native_assets_manager.open(name.to_java_string))
+               var return_value = new NativeBitmap.from_stream(native_assets_manager.open(name.to_java_string)).new_global_ref
                sys.jni_env.pop_local_frame
                return return_value
        end
@@ -291,6 +302,14 @@ extern class NativeBitmap in "Java" `{ android.graphics.Bitmap `}
        new from_resources(res: NativeResources, id: Int) in "Java" `{ return BitmapFactory.decodeResource(res, (int)id); `}
        fun width: Int in "Java" `{ return recv.getWidth(); `}
        fun height: Int in "Java" `{ return recv.getHeight(); `}
+
+       # HACK for bug #845
+       redef fun new_global_ref import sys, Sys.jni_env `{
+               Sys sys = NativeResources_sys(recv);
+               JNIEnv *env = Sys_jni_env(sys);
+               return (*env)->NewGlobalRef(env, recv);
+       `}
+
 end
 
 # Android AssetFileDescriptor, can be retrieve by AssetManager and used to load a sound in a SoundPool
@@ -337,6 +356,13 @@ extern class NativeAssetFileDescriptor in "Java" `{ android.content.res.AssetFil
        fun length: Int in "Java" `{ return (int)recv.getLength(); `}
        fun start_offset: Int in "Java" `{ return (int)recv.getStartOffset(); `}
        redef fun to_s: String import JavaString.to_s in "Java" `{ return JavaString_to_s(recv.toString()); `}
+
+       # HACK for bug #845
+       redef fun new_global_ref import sys, Sys.jni_env `{
+               Sys sys = NativeResources_sys(recv);
+               JNIEnv *env = Sys_jni_env(sys);
+               return (*env)->NewGlobalRef(env, recv);
+       `}
 end
 
 # Native class representing something drawable, can be retrieved from the resources
index 366c7d4..2301bd0 100644 (file)
@@ -1,4 +1,4 @@
-# this file is part of NIT ( http://www.nitlanguage.org ).
+# This file is part of NIT ( http://www.nitlanguage.org ).
 #
 # Copyright 2014 Romain Chanoir <romain.chanoir@viacesi.fr>
 #
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Android audio services
+# Android audio services, wraps a part of android audio API
 #
-# You can get a sound by loading it with a `SoundPool` or a `MediaPlayer`
-# the recommended way to load a sound is by it's resource ID or it's name
-# other ways are for advanced use
+# 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")
+# var m = app.load_music("sounds/test_music.ogg")
+# s.play
+# m.play
+# ~~~
+#
+# Now, the sounds are in "res/raw"
+# ~~~nitish
+# s = app.load_sound_from_res("test_sound")
+# m = app.load_music_from_res("test_sound")
+# s.play
+# m.play
+# ~~~
+#
+# See http://developer.android.com/reference/android/media/package-summary.html for more infos
 module audio
 
 import java
 import java::io
 import assets_and_resources
-import app
+import app::audio
 
 in "Java" `{
        import android.media.MediaPlayer;
        import android.media.SoundPool;
        import java.io.IOException;
        import android.media.AudioManager;
+       import android.media.AudioManager.OnAudioFocusChangeListener;
        import android.content.Context;
        import android.util.Log;
 `}
 
+# FIXME: This listener is not working at the moment, but is needed to gain or give up the audio focus
+# of the application
+in "Java inner" `{
+       static OnAudioFocusChangeListener afChangeListener = new OnAudioFocusChangeListener() {
+               public void onAudioFocusChange(int focusChange) {
+                       if(focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT) {
+                       }else if (focusChange == AudioManager.AUDIOFOCUS_GAIN) {
+                       }else if (focusChange == AudioManager.AUDIOFOCUS_LOSS) {
+                       }
+               }
+       };
+`}
+
 # AudioManager of the application, used to manage the audio mode
 extern class NativeAudioManager in "Java" `{ android.media.AudioManager `}
        super JavaObject
 
+       # Current audio mode.
+       # ( MODE_NORMAL = 0, MODE_RINGTONE = 1, MODE_IN_CALL = 2 or MODE_IN_COMMUNICATION = 3 )
        fun mode: Int in "Java" `{ return recv.getMode(); `}
+
+       # Sets the audio mode.
+       # ( MODE_NORMAL = 0, MODE_RINGTONE = 1, MODE_IN_CALL = 2 or MODE_IN_COMMUNICATION = 3 )
        fun mode=(i: Int) in "Java" `{ recv.setMode((int)i); `}
-       fun wired_headset_on: Bool in "Java" `{ return recv.isWiredHeadsetOn(); `}
-       fun wired_headset_on=(b: Bool) in "Java" `{ recv.setWiredHeadsetOn(b); `}
-       fun speakerphone_on: Bool in "Java" `{ return recv.isSpeakerphoneOn(); `}
-       fun speakerphone_on=(b: Bool) in "Java" `{ recv.setSpeakerphoneOn(b); `}
-       fun manage_audio_mode in "Java" `{
-               recv.setMode(0);
-               if (recv.isWiredHeadsetOn()) {
-                       recv.setSpeakerphoneOn(false);
-               } else {
-                       recv.setSpeakerphoneOn(true);
-               }
+
+       # Sends a request to obtain audio focus
+       fun request_audio_focus: Int in "Java" `{
+               return recv.requestAudioFocus(afChangeListener, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN);
        `}
 
+       # Gives up audio focus
+       fun abandon_audio_focus: Int in "Java" `{ return recv.abandonAudioFocus(afChangeListener); `}
 end
 
 # Media Player from Java, used to play long sounds or musics, not simultaneously
 # This is a low-level class, use `MediaPlater` instead
-extern class NativeMediaPlayer in "Java" `{ android.media.MediaPlayer `}
+private extern class NativeMediaPlayer in "Java" `{ android.media.MediaPlayer `}
        super JavaObject
 
-       new in "Java" `{ return new MediaPlayer(); `}
+       new in "Java" `{
+               MediaPlayer mp = new MediaPlayer();
+               mp.setAudioStreamType(AudioManager.STREAM_MUSIC);
+               return mp;
+       `}
        fun start in "Java" `{ recv.start(); `}
        fun prepare in "Java" `{
                try {
@@ -104,7 +138,7 @@ end
 
 # Sound Pool from Java, used to play sounds simultaneously
 # This is a low-level class, use `SoundPool`instead
-extern class NativeSoundPool in "Java" `{ android.media.SoundPool `}
+private extern class NativeSoundPool in "Java" `{ android.media.SoundPool `}
        super JavaObject
 
        new(max_streams, stream_type, src_quality: Int) in "Java" `{
@@ -165,7 +199,7 @@ class SoundPool
                return new SoundSP(null, nsoundpool.load_asset_fd(afd, priority), self)
        end
 
-       # Load the sound from it's resource id
+       # 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)
        end
@@ -184,7 +218,7 @@ class SoundPool
                return nsoundpool.play(id, left_volume, right_volume, priority, looping, rate)
        end
 
-       # Load a sound by it's name in the resources, the sound must be in the `res/raw` folder
+       # 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
                var id = resource_manager.raw_id(sound)
                return new SoundSP(id, nsoundpool.load_id(context, id, priority), self)
@@ -231,8 +265,11 @@ end
 class MediaPlayer
        private var nmedia_player: NativeMediaPlayer is noinit
 
+       # Used to control the state of the mediaplayer
+       private var is_prepared = false is writable
+
        # The sound associated with this mediaplayer
-       var sound: nullable Sound = null
+       var sound: nullable Sound = null is writable
 
        # Create a new MediaPlayer, but no sound is attached, you'll need
        # to use `load_sound` before using it
@@ -249,20 +286,21 @@ class MediaPlayer
        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)
        end
 
-       # Starts or resume playback
+       # Starts or resumes playback
        # REQUIRE `self.sound != null`
        fun start do
-               assert sound != null
+               if not is_prepared then prepare
                nmedia_player.start
        end
 
        # Stops playback after playback has been stopped or paused
        # REQUIRE `self.sound != null`
        fun stop do
-               assert sound != null
+               is_prepared = false
                nmedia_player.stop
        end
 
@@ -271,6 +309,7 @@ class MediaPlayer
        fun prepare do
                assert sound != null
                nmedia_player.prepare
+               is_prepared = true
        end
 
        # Pauses playback
@@ -292,7 +331,7 @@ class MediaPlayer
        # Reset MediaPlayer to its initial state
        fun reset do nmedia_player.reset
 
-       # Sets the datasource (file-pathor http/rtsp URL) to use
+       # Sets the datasource (file-path or http/rtsp URL) to use
        fun data_source(path: String): Sound do
                sys.jni_env.push_local_frame(1)
                nmedia_player.data_source_path(path.to_java_string)
@@ -323,14 +362,10 @@ class MediaPlayer
        fun stream_type=(stream_type: Int) do nmedia_player.stream_type = stream_type
 end
 
-# Represents an android sound that can be played by a SoundPool or a MediaPlayer
-# The only way to get a sound is by a MediaPlayer, a SoundPool, or the App
-abstract class Sound
+redef class Sound
 
-       # The resource ID of this sound
+       # Resource ID of this sound
        var id: nullable Int
-
-       fun play is abstract
 end
 
 # Sound implemented with a SoundPool
@@ -350,6 +385,8 @@ class SoundSP
        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
@@ -364,53 +401,81 @@ class SoundMP
                self.media_player = media_player
        end
 
-       redef fun play do self.media_player.start
+       redef fun play do media_player.start
+       redef fun pause do media_player.pause
+       redef fun resume do play
 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
        fun default_mediaplayer: MediaPlayer is cached do return new MediaPlayer
+
+       # Returns the default MediaPlayer of the application.
+       # When you load a short sound (not a music), it's added to this soundpool.
+       # Use it for advanced sound management.
        fun default_soundpool: SoundPool is cached do return new SoundPool
 
        # Get the native audio manager
-       private fun audio_manager: NativeAudioManager import native_activity in "Java" `{
+       fun audio_manager: NativeAudioManager import native_activity in "Java" `{
                return (AudioManager)App_native_activity(recv).getSystemService(Context.AUDIO_SERVICE);
        `}
 
-       # Manages whether the app sound need to be in headphones mode or not
-       # TODO: this method is not ideal, need to find a better way
-       fun manage_audio_mode import native_activity in "Java" `{
-               AudioManager manager = (AudioManager)App_native_activity(recv).getSystemService(Context.AUDIO_SERVICE);
-               manager.setMode(0);
-                       if (manager.isWiredHeadsetOn()) {
-                               manager.setSpeakerphoneOn(false);
-                       } else {
-                               manager.setSpeakerphoneOn(true);
-                       }
+       # 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" `{
+               App_native_activity(recv).setVolumeControlStream(AudioManager.STREAM_MUSIC);
        `}
 
-       # Retrieve a sound with a soundpool in the `assets` folder using it's name
+       # Retrieves a sound with a soundpool in the `assets` folder using its name.
        # Used to play short songs, can play multiple sounds simultaneously
-       fun load_sound(path: String): Sound do
-               return default_soundpool.load_asset_fd(asset_manager.open_fd(path))
+       redef fun load_sound(path: String): Sound do
+               return add_to_sounds(default_soundpool.load_asset_fd(asset_manager.open_fd(path)))
        end
 
-       # Retrieve a music with a media player in the `assets`folder using it's name
+       # 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)
-               var sound =  default_mediaplayer.data_source_fd(fd.file_descriptor, fd.start_offset, fd.length)
-               return sound
+               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
+       # Same as `load_sound` but load the sound from the `res/raw` folder
        fun load_sound_from_res(sound_name: String): Sound do
-               return default_soundpool.load_name(resource_manager,self.native_activity, sound_name)
+               return add_to_sounds(default_soundpool.load_name(resource_manager,self.native_activity, sound_name))
        end
 
-       # same as `load_music` but load the sound from the `res\raw` folder
+       # Same as `load_music` but load the sound from the `res/raw` folder
        fun load_music_from_res(music: String): Sound do
-               return default_mediaplayer.load_sound(resource_manager.raw_id(music), self.native_activity)
+               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
+       end
+
+       redef fun pause do
+               for s in sounds do s.pause
+               audio_manager.abandon_audio_focus
+       end
+
+       redef fun init_window do
+               super
+               audio_manager.request_audio_focus
+               manage_audio_stream
+       end
+
+       redef fun resume do
+               super
+               audio_manager.request_audio_focus
+               for s in sounds do s.resume
+       end
 end
diff --git a/lib/app/audio.nit b/lib/app/audio.nit
new file mode 100644 (file)
index 0000000..de68bb9
--- /dev/null
@@ -0,0 +1,43 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2014 Romain Chanoir <romain.chanoir@viacesi.fr>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# App audio abstraction
+#
+# Once the application has started (after `App.setup`)
+# use `App.load_sound` to get a sound
+# then `Sound.play` to play it
+module audio
+
+import app_base
+
+# Abstraction of a sound
+abstract class Sound
+
+       # Plays the sound
+       fun play is abstract
+
+       # Pauses the sound
+       fun pause is abstract
+
+       # Resumes the sound
+       fun resume is abstract
+end
+
+redef class App
+
+       # Loads a sound from the assets of the app, returns `null` if the loading failed
+       fun load_sound(name: String): nullable Sound do return null
+end