[Android] 카메라 촬영시 셔터 소리(찰칵) 추가하기

이지훈·2024년 8월 16일
0

사진: UnsplashAlexander Andrews

TL;DR

cameraX 라이브러리를 사용하여 카메라를 구현할 때, 셔터 소리의 커스텀(음량 조절등)을 지원하기 위해선 셔터 소리 raw 파일과 media player 를 사용하여 구현 할 수 있다.

서두

프로젝트에 카메라를 사용하여 유저의 운동 능력을 평가하는 기능을 구현하게 되어, Android 의 CameraX 를 사용하여 이를 구현하는 부분 중, 셔터 소리를 추가하는 방법에 대해 알아보도록 하겠다. (사용자에게 사진이 촬영되었다는 것을 명시적으로 안내하기 위함)

cameraX를 사용하기 위한 라이브러리 추가와 같은 환경 설정 파트는 생략하기로 하겠다. (글의 핵심이 아니며, 하단의 샘플 예제를 제공)

본론

나는 당연히, 셔터 소리를 지정하는 옵션을 CameraX 라이브러리 측에서 지원하는 줄 알았다. (.setShutterSound(enabled: Boolean) 같은 옵션으로 껐다 킬수 있을 줄...)

하지만 그러지 않았고, 직접 camera 를 촬영하는 타이밍에 맞춰 사운드를 재생하는 구현을 직접 해줘야만 했다.

구글에서 제공하는 Camera 예제에서도 다양한 구현 사례들을 제시하지만, 모두 빌드해서 카메라 촬영 버튼을 눌러보면 무음이었다...
https://github.com/android/camera-samples

따라서 사운드를 재생하는 코드를 카메라 촬영 타이밍에 직접 추가해주기로 하였다.

 private val shutterSoundManager by lazy {
     applicationContext.getSystemService(Context.AUDIO_SERVICE) as AudioManager
 }
 
 private fun playShutterSound() {
     shutterSoundManager.setStreamVolume(
         AudioManager.STREAM_SYSTEM,
         1,
         AudioManager.FLAG_REMOVE_SOUND_AND_VIBRATE,
     )
     MediaActionSound().play(MediaActionSound.SHUTTER_CLICK)
}

위와 같은 코드를 추가하면 내가 원하는 타이밍(촬영할 떄)에, 별도의 raw 파일을 추가하지 않아도, shutterSound 를 앱에서 재생할 수 있었다.

    private fun initObserver() {
        repeatOnStarted {
            launch {
                viewModel.remainingSeconds.collect { remainingSeconds ->
                    if (remainingSeconds > 0) {
                        binding.tvTimer.visibility = View.VISIBLE
                        binding.tvTimer.text = remainingSeconds.toString()
                    } else {
                        playShutterSound()
                        captureAndAnalyzeImage()
                    }
                }
            }

            launch {
                viewModel.isCountdownActive.collect { isActive ->
                    if (isActive) {
                        binding.tvTimer.visibility = View.VISIBLE
                    } else {
                        binding.tvTimer.visibility = View.GONE
                    }
                }
            }
        }
    }

다음과 같이 타이머를 추가한 경우, 타이머의 초가 0초가 되었을 때, playShutterSound() 함수를 호출하고, 이미지를 캡쳐를 수행하게 된다.

문제 발생 1

음... 소리가 너~~무 크다...

shutterSoundManager.setStreamVolume(
    AudioManager.STREAM_SYSTEM,
    1,
    AudioManager.FLAG_REMOVE_SOUND_AND_VIBRATE,
)

해당 setStreamVolum 함수를 확인해보면, 인자가 3개가 존재하는데,

두번째로 들어가는 index 파라미터가 가장 볼륨을 조절할 수 있는 값이라 생각하여 조절해보았으나, 볼륨 소리가 조절되지 않았다... 그리고 소리 또한 살짝 이질적이라 느껴졌다.

문제 해결 1

소리 볼륨 조절과, 원하는 셔터 소리를 추가하기 위해, media player 를 사용하는 방식으로 구현 방식을 전면 수정하였다.

    private val mediaPlayer: MediaPlayer by lazy {
        MediaPlayer.create(this, R.raw.shutter_sound)
    }
    
    private fun playShutterSound() {
        // mediaPlayer 에서는 0~1 범위 내에서 현재 사용자가 지정한 볼륨 기준으로 소리의 볼륨을 조절할 수 있다.
        mediaPlayer.setVolume(0.1f, 0.1f)
        mediaPlayer.start()
    }
    
    override fun onDestroy() {
    	// 반드시! 
        mediaPlayer.release()

        super.onDestroy()
    }

mediaPlayer 는 exoPlayer 처럼 재생이 종료되고, 사용을 더이상 하지 않는 경우, onDestory 콜백내에서 반드시 release() 를 통해 방출을 해줘야 한다.

raw 파일 같은 경우엔, 사용자에게 친숙한 셔터 소리를 찾던 도중,
공짜로 다운 받을 수 있는 목록 중 아이폰 카메라 셔터 사운드 를 채택하였다.(2번 찰칵 소리가 나는 파일인 관계로 편집을 통해 한번만 소리가 재생되도록 수정)

결과

실기기를 통해 카메라 촬영할때와 유사한 사운드와, 볼륨을 구현할 수 있었다.
다만, 이는 라이브러리 혹은 OS 차원에서 좀 더 편하게 구현할 수 있도록 기능을 지원해야한다고 생각한다...

단순한 기능 구현이지만, 생각보다 예제 및 레퍼런스가 생각보다 존재하지 않아, 여러 곳의 자문을 구해, 구현할 수 있었다.

더 좋은 방법이 있다면 덧글을 통해 알려주시면 감사하겠습니다. (_ _)

추가

시스템 볼륨 설정 값과 상관없이 셔터 소리가 나와야하는 경우엔 추가적인 설정이 필요하다.(위의 방식은 시스템 볼륨을 0으로 설정하면 셔터 소리가 나오지 않는다)

코드가 살짝 길어지는 관계로 클래스를 따로 정의해서 나타내면

import android.content.Context
import android.media.AudioAttributes
import android.media.AudioManager
import android.media.MediaPlayer
import com.mediplussolution.android.csmsrenewal.R

class ShutterSoundPlayer(private val context: Context) {
    private var mediaPlayer: MediaPlayer? = null
    private val audioManager: AudioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager

    init {
        initializeMediaPlayer()
    }

    private fun initializeMediaPlayer() {
        mediaPlayer = MediaPlayer().apply {
            setAudioAttributes(
                AudioAttributes.Builder()
                    .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
                    .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION)
                    .build()
            )
            setDataSource(context.resources.openRawResourceFd(R.raw.shutter_sound))
            setVolume(0.3f, 0.3f)
            prepare()
        }
    }

    fun playShutterSound() {
        val originalVolume = audioManager.getStreamVolume(AudioManager.STREAM_SYSTEM)

        try {
            // 시스템 볼륨을 30%로 설정
            val maxVolume = audioManager.getStreamMaxVolume(AudioManager.STREAM_SYSTEM)
            val targetVolume = (maxVolume * 0.3f).toInt()
            audioManager.setStreamVolume(AudioManager.STREAM_SYSTEM, targetVolume, 0)

            // 소리 재생
            mediaPlayer?.start()

            // 재생이 끝날 때까지 대기
            mediaPlayer?.setOnCompletionListener {
                // 원래 볼륨으로 복구
                audioManager.setStreamVolume(AudioManager.STREAM_SYSTEM, originalVolume, 0)
            }
        } catch (e: Exception) {
            e.printStackTrace()
            // 예외 발생 시 원래 볼륨으로 복구
            audioManager.setStreamVolume(AudioManager.STREAM_SYSTEM, originalVolume, 0)
        }
    }

    fun release() {
        mediaPlayer?.release()
        mediaPlayer = null
    }
}

다음과 같이 셔터 소리를 재생 시킬 때, 시스템 볼륨을 강제로 원하는 볼륨으로 설정 후에, 재생하게 되면, 시스템 볼륨 설정 값과 무관하게 일정한 볼륨으로 셔터 소리를 재생 시킬 수 있다.

재생 이후 시스템 볼륨을 이전 볼륨 설정값으로 다시 복원 하는 것을 잊지 말자.

CameraX 라이브러리를 사용한 카메라 구현 및 셔터 소리 추가 예제는 아래 레포에서 확인할 수 있습니다.
https://github.com/easyhooon/PoseEstimationExample

참고)
https://everyshare.co.kr/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-audiomanager%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%98%EC%97%AC-%EC%9D%8C%EB%9F%89-%EC%A0%9C%EC%96%B4%ED%95%98%EA%B8%B0/

https://developer.android.com/reference/android/media/AudioManager#setStreamVolume(int,%20int,%20int)

https://developer.android.com/reference/android/media/MediaPlayer#setVolume(float,%20float)

profile
실력은 고통의 총합이다. Android Developer

0개의 댓글