handle countdown timer sounds

This commit is contained in:
darthpaul
2022-11-27 14:31:51 +00:00
parent 034497ba52
commit c680a1b68a
7 changed files with 128 additions and 82 deletions

View File

@ -53,6 +53,7 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera
const val PHOTO_MODE_INDEX = 1 const val PHOTO_MODE_INDEX = 1
const val VIDEO_MODE_INDEX = 0 const val VIDEO_MODE_INDEX = 0
private const val MIN_SWIPE_DISTANCE_X = 100 private const val MIN_SWIPE_DISTANCE_X = 100
private const val TIMER_2_SECONDS = 2001
} }
private lateinit var defaultScene: Scene private lateinit var defaultScene: Scene
@ -60,6 +61,7 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera
private lateinit var timerScene: Scene private lateinit var timerScene: Scene
private lateinit var mOrientationEventListener: OrientationEventListener private lateinit var mOrientationEventListener: OrientationEventListener
private lateinit var mFocusCircleView: FocusCircleView private lateinit var mFocusCircleView: FocusCircleView
private lateinit var mediaSoundHelper: MediaSoundHelper
private var mPreview: MyPreview? = null private var mPreview: MyPreview? = null
private var mediaSizeToggleGroup: MaterialButtonToggleGroup? = null private var mediaSizeToggleGroup: MaterialButtonToggleGroup? = null
private var mPreviewUri: Uri? = null private var mPreviewUri: Uri? = null
@ -172,6 +174,7 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
mPreview = null mPreview = null
mediaSoundHelper.release()
} }
override fun onBackPressed() { override fun onBackPressed() {
@ -182,6 +185,8 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera
private fun initVariables() { private fun initVariables() {
mIsHardwareShutterHandled = false mIsHardwareShutterHandled = false
mediaSoundHelper = MediaSoundHelper(this)
mediaSoundHelper.loadSounds()
} }
override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean { override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
@ -321,6 +326,7 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera
mPreview = CameraXInitializer(this).createCameraXPreview( mPreview = CameraXInitializer(this).createCameraXPreview(
preview_view, preview_view,
listener = this, listener = this,
mediaSoundHelper = mediaSoundHelper,
outputUri = outputUri, outputUri = outputUri,
isThirdPartyIntent = isThirdPartyIntent, isThirdPartyIntent = isThirdPartyIntent,
initInPhotoMode = isInPhotoMode, initInPhotoMode = isInPhotoMode,
@ -487,6 +493,7 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera
} }
private fun cancelTimer() { private fun cancelTimer() {
mediaSoundHelper.stopTimerCountdown2SecondsSound()
countDownTimer?.cancel() countDownTimer?.cancel()
countDownTimer = null countDownTimer = null
resetViewsOnTimerFinish() resetViewsOnTimerFinish()
@ -855,10 +862,19 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera
hideViewsOnTimerStart() hideViewsOnTimerStart()
shutter.setImageState(intArrayOf(R.attr.state_timer_cancel), true) shutter.setImageState(intArrayOf(R.attr.state_timer_cancel), true)
timer_text.beVisible() timer_text.beVisible()
var playSound = true
countDownTimer = object : CountDownTimer(timerMode.millisInFuture, 1000) { countDownTimer = object : CountDownTimer(timerMode.millisInFuture, 1000) {
override fun onTick(millisUntilFinished: Long) { override fun onTick(millisUntilFinished: Long) {
val seconds = (TimeUnit.MILLISECONDS.toSeconds(millisUntilFinished) + 1).toString() val seconds = (TimeUnit.MILLISECONDS.toSeconds(millisUntilFinished) + 1).toString()
timer_text.setText(seconds) timer_text.setText(seconds)
if (playSound && config.isSoundEnabled) {
if (millisUntilFinished <= TIMER_2_SECONDS) {
mediaSoundHelper.playTimerCountdown2SecondsSound()
playSound = false
} else {
mediaSoundHelper.playTimerCountdownSound()
}
}
} }
override fun onFinish() { override fun onFinish() {

View File

@ -8,6 +8,8 @@ import android.net.Uri
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.util.Log import android.util.Log
import androidx.annotation.RawRes
import com.simplemobiletools.camera.R
import java.io.File import java.io.File
/** /**
@ -15,59 +17,59 @@ import java.io.File
*/ */
class MediaActionSound(private val context: Context) { class MediaActionSound(private val context: Context) {
companion object { companion object {
val SHUTTER_CLICK = MediaSound.ManufacturerSound("camera_click.ogg")
val FOCUS_COMPLETE = MediaSound.ManufacturerSound("camera_focus.ogg")
val START_VIDEO_RECORDING = MediaSound.ManufacturerSound("VideoRecord.ogg")
val STOP_VIDEO_RECORDING = MediaSound.ManufacturerSound("VideoStop.ogg")
val TIMER_COUNTDOWN = MediaSound.RawResSound(R.raw.beep)
val TIMER_COUNTDOWN_2_SECONDS = MediaSound.RawResSound(R.raw.beep_2_secs)
private const val NUM_MEDIA_SOUND_STREAMS = 1 private const val NUM_MEDIA_SOUND_STREAMS = 1
private val SOUND_DIRS = arrayOf( private val SOUND_DIRS = arrayOf(
"/product/media/audio/ui/", "/product/media/audio/ui/", "/system/media/audio/ui/"
"/system/media/audio/ui/"
)
private val SOUND_FILES = arrayOf(
"camera_click.ogg",
"camera_focus.ogg",
"VideoRecord.ogg",
"VideoStop.ogg"
) )
private const val TAG = "MediaActionSound" private const val TAG = "MediaActionSound"
const val SHUTTER_CLICK = 0
const val FOCUS_COMPLETE = 1
const val START_VIDEO_RECORDING = 2
const val STOP_VIDEO_RECORDING = 3
private const val STATE_NOT_LOADED = 0 private const val STATE_NOT_LOADED = 0
private const val STATE_LOADING = 1 private const val STATE_LOADING = 1
private const val STATE_LOADING_PLAY_REQUESTED = 2 private const val STATE_LOADING_PLAY_REQUESTED = 2
private const val STATE_LOADED = 3 private const val STATE_LOADED = 3
private val SOUNDS = arrayOf(SHUTTER_CLICK, FOCUS_COMPLETE, START_VIDEO_RECORDING, STOP_VIDEO_RECORDING, TIMER_COUNTDOWN, TIMER_COUNTDOWN_2_SECONDS)
} }
private class SoundState(val name: Int) { sealed class MediaSound {
var id = 0 // 0 is an invalid sample ID. class ManufacturerSound(val fileName: String, var path: String = "") : MediaSound()
class RawResSound(@RawRes val resId: Int) : MediaSound()
}
private class SoundState(
val mediaSound: MediaSound,
// 0 is an invalid sample ID.
var loadId: Int = 0,
var streamId: Int = 0,
var state: Int = STATE_NOT_LOADED var state: Int = STATE_NOT_LOADED
var path: String? = null )
}
private var soundPool: SoundPool? = SoundPool.Builder() private var soundPool: SoundPool? = SoundPool.Builder().setMaxStreams(NUM_MEDIA_SOUND_STREAMS).setAudioAttributes(
.setMaxStreams(NUM_MEDIA_SOUND_STREAMS) AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION).setFlags(AudioAttributes.FLAG_AUDIBILITY_ENFORCED)
.setAudioAttributes( .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION).build()
AudioAttributes.Builder() ).build()
.setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION)
.setFlags(AudioAttributes.FLAG_AUDIBILITY_ENFORCED)
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
.build()
).build()
private var mediaPlayer: MediaPlayer? = null private var mediaPlayer: MediaPlayer? = null
private var playCompletionRunnable: Runnable? = null private var playCompletionRunnable: Runnable? = null
private val mSounds: Array<SoundState?> = arrayOfNulls(SOUND_FILES.size) private val sounds = SOUNDS.map { SoundState(it) }
private val playTimeHandler = Handler(Looper.getMainLooper()) private val playTimeHandler = Handler(Looper.getMainLooper())
private val mLoadCompleteListener = SoundPool.OnLoadCompleteListener { _, sampleId, status -> private val mLoadCompleteListener = SoundPool.OnLoadCompleteListener { _, sampleId, status ->
for (sound in mSounds) { for (sound in sounds) {
if (sound!!.id != sampleId) { if (sound.loadId != sampleId) {
continue continue
} }
var soundToBePlayed: SoundState? = null var soundToBePlayed: SoundState? = null
synchronized(sound) { synchronized(sound) {
if (status != 0) { if (status != 0) {
sound.state = STATE_NOT_LOADED sound.state = STATE_NOT_LOADED
sound.id = 0 sound.loadId = 0
Log.e(TAG, "OnLoadCompleteListener() error: $status loading sound: ${sound.name}") Log.e(TAG, "OnLoadCompleteListener() error: $status loading sound: ${sound.mediaSound}")
return@OnLoadCompleteListener return@OnLoadCompleteListener
} }
when (sound.state) { when (sound.state) {
@ -76,11 +78,11 @@ class MediaActionSound(private val context: Context) {
soundToBePlayed = sound soundToBePlayed = sound
sound.state = STATE_LOADED sound.state = STATE_LOADED
} }
else -> Log.e(TAG, "OnLoadCompleteListener() called in wrong state: ${sound.state} for sound: ${sound.name}") else -> Log.e(TAG, "OnLoadCompleteListener() called in wrong state: ${sound.state} for sound: ${sound.mediaSound}")
} }
} }
if (soundToBePlayed != null) { if (soundToBePlayed != null) {
playSoundPool(soundToBePlayed!!) playWithSoundPool(soundToBePlayed!!)
} }
break break
} }
@ -88,89 +90,113 @@ class MediaActionSound(private val context: Context) {
init { init {
soundPool!!.setOnLoadCompleteListener(mLoadCompleteListener) soundPool!!.setOnLoadCompleteListener(mLoadCompleteListener)
for (i in mSounds.indices) {
mSounds[i] = SoundState(i)
}
} }
private fun loadSound(sound: SoundState?): Int { private fun loadSound(sound: SoundState): Int {
val soundFileName = SOUND_FILES[sound!!.name] var id = 0
for (soundDir in SOUND_DIRS) { when (val mediaSound = sound.mediaSound) {
val soundPath = soundDir + soundFileName is MediaSound.ManufacturerSound -> {
sound.path = soundPath for (soundDir in SOUND_DIRS) {
val id = soundPool!!.load(soundPath, 1) val soundPath = soundDir + mediaSound.fileName
if (id > 0) { mediaSound.path = soundPath
sound.state = STATE_LOADING id = soundPool!!.load(soundPath, 1)
sound.id = id break
return id }
}
is MediaSound.RawResSound -> {
id = soundPool!!.load(context, mediaSound.resId, 1)
} }
} }
if (id > 0) {
sound.state = STATE_LOADING
sound.loadId = id
return id
}
return 0 return 0
} }
fun load(soundName: Int) { fun load(mediaSound: MediaSound) {
if (soundName < 0 || soundName >= SOUND_FILES.size) { val sound = sounds.first { it.mediaSound == mediaSound }
throw RuntimeException("Unknown sound requested: $soundName") synchronized(sound) {
}
val sound = mSounds[soundName]
synchronized(sound!!) {
when (sound.state) { when (sound.state) {
STATE_NOT_LOADED -> { STATE_NOT_LOADED -> {
loadSound(sound).let { soundId -> loadSound(sound).let { soundId ->
if (soundId <= 0) { if (soundId <= 0) {
Log.e(TAG, "load() error loading sound: $soundName") Log.e(TAG, "load() error loading sound: $mediaSound")
} }
} }
} }
else -> Log.e(TAG, "load() called in wrong state: $sound for sound: $soundName") else -> Log.e(TAG, "load() called in wrong state: $sound for sound: $mediaSound")
} }
} }
} }
fun play(soundName: Int, onPlayComplete: (() -> Unit)? = null) { fun play(mediaSound: MediaSound, onPlayComplete: (() -> Unit)? = null) {
if (soundName < 0 || soundName >= SOUND_FILES.size) {
throw RuntimeException("Unknown sound requested: $soundName")
}
removeHandlerCallbacks() removeHandlerCallbacks()
if (onPlayComplete != null) { if (onPlayComplete != null) {
playCompletionRunnable = Runnable { playCompletionRunnable = Runnable {
onPlayComplete.invoke() onPlayComplete.invoke()
} }
} }
val sound = mSounds[soundName] val sound = sounds.first { it.mediaSound == mediaSound }
synchronized(sound!!) { synchronized(sound) {
when (sound.state) { when (sound.state) {
STATE_NOT_LOADED -> { STATE_NOT_LOADED -> {
val soundId = loadSound(sound) val soundId = loadSound(sound)
if (soundId <= 0) { if (soundId <= 0) {
Log.e(TAG, "play() error loading sound: $soundName") Log.e(TAG, "play() error loading sound: $mediaSound")
} else { } else {
sound.state = STATE_LOADING_PLAY_REQUESTED sound.state = STATE_LOADING_PLAY_REQUESTED
} }
} }
STATE_LOADING -> sound.state = STATE_LOADING_PLAY_REQUESTED STATE_LOADING -> sound.state = STATE_LOADING_PLAY_REQUESTED
STATE_LOADED -> { STATE_LOADED -> {
playSoundPool(sound) playWithSoundPool(sound)
} }
else -> Log.e(TAG, "play() called in wrong state: ${sound.state} for sound: $soundName") else -> Log.e(TAG, "play() called in wrong state: ${sound.state} for sound: $mediaSound")
} }
} }
} }
private fun playSoundPool(sound: SoundState) { private fun playWithSoundPool(sound: SoundState) {
if (playCompletionRunnable != null) { if (playCompletionRunnable != null) {
val duration = getSoundDuration(sound.path!!) val duration = getSoundDuration(sound.mediaSound)
playTimeHandler.postDelayed(playCompletionRunnable!!, duration) playTimeHandler.postDelayed(playCompletionRunnable!!, duration)
} }
soundPool!!.play(sound.id, 1.0f, 1.0f, 0, 0, 1.0f) val streamId = soundPool!!.play(sound.loadId, 1.0f, 1.0f, 0, 0, 1.0f)
sound.streamId = streamId
}
private fun getSoundDuration(mediaSound: MediaSound): Long {
releaseMediaPlayer()
mediaPlayer = when (mediaSound) {
is MediaSound.ManufacturerSound -> MediaPlayer.create(context, Uri.fromFile(File(mediaSound.path)))
is MediaSound.RawResSound -> MediaPlayer.create(context, mediaSound.resId)
}
return mediaPlayer!!.duration.toLong()
}
fun stop(mediaSound: MediaSound) {
val sound = sounds.first { it.mediaSound == mediaSound }
synchronized(sound) {
when (sound.state) {
STATE_LOADED -> {
soundPool!!.stop(sound.streamId)
}
else -> Log.w(TAG, "stop() should be called after sound is loaded for sound: $mediaSound")
}
}
} }
fun release() { fun release() {
if (soundPool != null) { if (soundPool != null) {
for (sound in mSounds) { for (sound in sounds) {
synchronized(sound!!) { synchronized(sound) {
sound.state = STATE_NOT_LOADED sound.state = STATE_NOT_LOADED
sound.id = 0 sound.loadId = 0
sound.streamId = 0
} }
} }
soundPool?.release() soundPool?.release()
@ -189,10 +215,4 @@ class MediaActionSound(private val context: Context) {
mediaPlayer?.release() mediaPlayer?.release()
mediaPlayer = null mediaPlayer = null
} }
private fun getSoundDuration(soundPath: String): Long {
releaseMediaPlayer()
mediaPlayer = MediaPlayer.create(context, Uri.fromFile(File(soundPath)))
return mediaPlayer!!.duration.toLong()
}
} }

View File

@ -9,6 +9,8 @@ class MediaSoundHelper(context: Context) {
mediaActionSound.load(MediaActionSound.START_VIDEO_RECORDING) mediaActionSound.load(MediaActionSound.START_VIDEO_RECORDING)
mediaActionSound.load(MediaActionSound.STOP_VIDEO_RECORDING) mediaActionSound.load(MediaActionSound.STOP_VIDEO_RECORDING)
mediaActionSound.load(MediaActionSound.SHUTTER_CLICK) mediaActionSound.load(MediaActionSound.SHUTTER_CLICK)
mediaActionSound.load(MediaActionSound.TIMER_COUNTDOWN)
mediaActionSound.load(MediaActionSound.TIMER_COUNTDOWN_2_SECONDS)
} }
fun playShutterSound() { fun playShutterSound() {
@ -23,6 +25,18 @@ class MediaSoundHelper(context: Context) {
mediaActionSound.play(MediaActionSound.STOP_VIDEO_RECORDING) mediaActionSound.play(MediaActionSound.STOP_VIDEO_RECORDING)
} }
fun playTimerCountdownSound() {
mediaActionSound.play(MediaActionSound.TIMER_COUNTDOWN)
}
fun playTimerCountdown2SecondsSound() {
mediaActionSound.play(MediaActionSound.TIMER_COUNTDOWN_2_SECONDS)
}
fun stopTimerCountdown2SecondsSound() {
mediaActionSound.stop(MediaActionSound.TIMER_COUNTDOWN_2_SECONDS)
}
fun release() { fun release() {
mediaActionSound.release() mediaActionSound.release()
} }

View File

@ -4,6 +4,7 @@ import android.net.Uri
import androidx.camera.view.PreviewView import androidx.camera.view.PreviewView
import com.simplemobiletools.camera.helpers.CameraErrorHandler import com.simplemobiletools.camera.helpers.CameraErrorHandler
import com.simplemobiletools.camera.helpers.MediaOutputHelper import com.simplemobiletools.camera.helpers.MediaOutputHelper
import com.simplemobiletools.camera.helpers.MediaSoundHelper
import com.simplemobiletools.commons.activities.BaseSimpleActivity import com.simplemobiletools.commons.activities.BaseSimpleActivity
class CameraXInitializer(private val activity: BaseSimpleActivity) { class CameraXInitializer(private val activity: BaseSimpleActivity) {
@ -11,6 +12,7 @@ class CameraXInitializer(private val activity: BaseSimpleActivity) {
fun createCameraXPreview( fun createCameraXPreview(
previewView: PreviewView, previewView: PreviewView,
listener: CameraXPreviewListener, listener: CameraXPreviewListener,
mediaSoundHelper: MediaSoundHelper,
outputUri: Uri?, outputUri: Uri?,
isThirdPartyIntent: Boolean, isThirdPartyIntent: Boolean,
initInPhotoMode: Boolean, initInPhotoMode: Boolean,
@ -20,6 +22,7 @@ class CameraXInitializer(private val activity: BaseSimpleActivity) {
return CameraXPreview( return CameraXPreview(
activity, activity,
previewView, previewView,
mediaSoundHelper,
mediaOutputHelper, mediaOutputHelper,
cameraErrorHandler, cameraErrorHandler,
listener, listener,

View File

@ -37,6 +37,7 @@ import com.simplemobiletools.commons.helpers.ensureBackgroundThread
class CameraXPreview( class CameraXPreview(
private val activity: AppCompatActivity, private val activity: AppCompatActivity,
private val previewView: PreviewView, private val previewView: PreviewView,
private val mediaSoundHelper: MediaSoundHelper,
private val mediaOutputHelper: MediaOutputHelper, private val mediaOutputHelper: MediaOutputHelper,
private val cameraErrorHandler: CameraErrorHandler, private val cameraErrorHandler: CameraErrorHandler,
private val listener: CameraXPreviewListener, private val listener: CameraXPreviewListener,
@ -49,14 +50,12 @@ class CameraXPreview(
private const val AF_SIZE = 1.0f / 6.0f private const val AF_SIZE = 1.0f / 6.0f
private const val AE_SIZE = AF_SIZE * 1.5f private const val AE_SIZE = AF_SIZE * 1.5f
private const val CAMERA_MODE_SWITCH_WAIT_TIME = 500L private const val CAMERA_MODE_SWITCH_WAIT_TIME = 500L
private const val TOGGLE_FLASH_DELAY = 700L
} }
private val config = activity.config private val config = activity.config
private val contentResolver = activity.contentResolver private val contentResolver = activity.contentResolver
private val mainExecutor = ContextCompat.getMainExecutor(activity) private val mainExecutor = ContextCompat.getMainExecutor(activity)
private val displayManager = activity.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager private val displayManager = activity.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
private val mediaSoundHelper = MediaSoundHelper(activity)
private val windowMetricsCalculator = WindowMetricsCalculator.getOrCreate() private val windowMetricsCalculator = WindowMetricsCalculator.getOrCreate()
private val videoQualityManager = VideoQualityManager(activity) private val videoQualityManager = VideoQualityManager(activity)
private val imageQualityManager = ImageQualityManager(activity) private val imageQualityManager = ImageQualityManager(activity)
@ -123,7 +122,6 @@ class CameraXPreview(
init { init {
bindToLifeCycle() bindToLifeCycle()
mediaSoundHelper.loadSounds()
} }
private fun bindToLifeCycle() { private fun bindToLifeCycle() {
@ -354,11 +352,6 @@ class CameraXPreview(
orientationEventListener.disable() orientationEventListener.disable()
} }
override fun onDestroy(owner: LifecycleOwner) {
super.onDestroy(owner)
mediaSoundHelper.release()
}
override fun isInPhotoMode(): Boolean { override fun isInPhotoMode(): Boolean {
return isPhotoCapture return isPhotoCapture
} }

Binary file not shown.

Binary file not shown.