From: Jean Privat Date: Thu, 22 Jan 2015 06:27:35 +0000 (+0700) Subject: Merge: Audio management for android + audio Abstraction API X-Git-Tag: v0.7.1~18 X-Git-Url: http://nitlanguage.org?hp=3633c5a00dd9d81e34737e38c78aefb1bc6da60f Merge: Audio management for android + audio Abstraction API 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 Reviewed-by: Alexandre Terrasa Reviewed-by: Jean Privat Reviewed-by: Lucas Bajolet --- diff --git a/examples/mnit_simple/assets/sound.ogg b/examples/mnit_simple/assets/testsound.ogg similarity index 100% rename from examples/mnit_simple/assets/sound.ogg rename to examples/mnit_simple/assets/testsound.ogg diff --git a/examples/mnit_simple/res/raw/sound.ogg b/examples/mnit_simple/res/raw/testsound.ogg similarity index 100% rename from examples/mnit_simple/res/raw/sound.ogg rename to examples/mnit_simple/res/raw/testsound.ogg diff --git a/examples/mnit_simple/res/values/dimens.xml b/examples/mnit_simple/res/values/dimens.xml index da43c8f..fa1d020 100644 --- a/examples/mnit_simple/res/values/dimens.xml +++ b/examples/mnit_simple/res/values/dimens.xml @@ -1,5 +1,5 @@ - 25dp - 150dp - + 25px + 150px + diff --git a/examples/mnit_simple/src/test_audio.nit b/examples/mnit_simple/src/test_audio.nit index decf7c5..88a2812 100644 --- a/examples/mnit_simple/src/test_audio.nit +++ b/examples/mnit_simple/src/test_audio.nit @@ -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 diff --git a/lib/android/assets_and_resources.nit b/lib/android/assets_and_resources.nit index 82ae3d1..8195e2c 100644 --- a/lib/android/assets_and_resources.nit +++ b/lib/android/assets_and_resources.nit @@ -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 diff --git a/lib/android/audio.nit b/lib/android/audio.nit index 366c7d4..2301bd0 100644 --- a/lib/android/audio.nit +++ b/lib/android/audio.nit @@ -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 # @@ -14,54 +14,88 @@ # 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 index 0000000..de68bb9 --- /dev/null +++ b/lib/app/audio.nit @@ -0,0 +1,43 @@ +# This file is part of NIT ( http://www.nitlanguage.org ). +# +# Copyright 2014 Romain Chanoir +# +# 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