Ffmpeg에 이어서, 업무를 진행하다가 이번엔 음성을 녹음하고 저장하는 기능을 추가하게 되었다.
정말 간단하게 녹음 및 재생이 가능한데,
이번 글에서는 그 간단한 음성을 녹음하고, 재생하는 방법에 대해서 알아보고자 한다.
우선,
다른 작업을 하기 앞서 음성 녹음을 진행하는데 반드시 필요한 권한을 추가해 주도록 한다.
<uses-permission android:name="android.permission.RECORD_AUDIO" />
manifext 파일에 추가를 해줘야할 뿐 아니라,
음성 녹음 기능을 실행하기 전에 반드시 위의 권한을 사용자로부터 받고 실행해야 한다.
본 게시글에서는 사용자로부터 권한을 받는 로직은 제외하고 작성하도록 하겠다.
다음으로
음성을 녹음하고, 재생하기 위해서 필요한 클래스에 대해서 알아보자.
음성 녹음과 재생을 위해서는 별다른 라이브러리를 추가하거나 할 필요 없이, 안드로이드에서 자체적으로 제공하는 클래스를 사용해서 쉽게 구현이 가능하다.
MediaRecorder
MediaPlayer
이름부터 너무 직관적으로, 미디어 레코더와 미디어 플레이어를 사용하면 된다.
그러면 음성 녹음부터 살펴보도록 하자.
음성 녹음은 다음과 같은 순서로 진행이 된다.
- 음성 녹음 시작
- (음성 녹음 일시 정지)
- (음성 녹음 재시작)
- 음성 녹음 정지
위의 작업에서 2, 3번은 선택에 의한 것이므로 넘어가도 되지만 간단한 로직이므로 설명하도록 하겠다.
음성 녹음 시작하는 부분부터 살펴보자.
val mediaRecorder = remember { mutableStateOf<MediaRecorder?>(null) }
val outputFile = remember { mutableStateOf<File?>(null) }
mediaRecorder.value = MediaRecorder()
outputFile.value = createOutputFile(context)
mediaRecorder.value?.apply {
setAudioSource(MediaRecorder.AudioSource.MIC)
setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)
setAudioEncoder(MediaRecorder.AudioEncoder.AAC)
setOutputFile(outputFile.value?.absolutePath)
prepare()
start()
}
아주 간단하게 음성 녹음을 시작할 수 있다.
함수 이름들만 봐도 너무나도 직관적이기 때문에, 처음 코드를 보더라도 천천히 읽으면 전부 어떤 의미로 작성된 코드인지 이해할 수 있다.
- MediaRecorder 객체를 생성한다.
- 파일 경로와 이름을 지정해서 파일을 저장할 수 있게 준비한다.
- 생성한 MediaRecorder 객체에 음성 녹음을 위한 세팅을 해준다.
- 음성 녹음을 시작한다.
의 4가지 순서대로 작업이 진행되게 된다.
여기서,
2번 로직의 경우는 다음과 같이 구현해두었다.
fun createOutputFile(context: Context): File {
val timeStamp: String = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date())
val storageDir: File? = context.getExternalFilesDir(null)
return File.createTempFile(
"AUDIO_${timeStamp}_",
".m4a",
storageDir
)
}
SimpleDateFormat을 사용하여 현재 시간을 파일 명의 prefix 넣어줌으로써 중복된 File이 생성되지 않도록 하였고,
suffix에는 음성 파일의 확장자를 지정해 주었다.
파일의 경로는 getExternalFilesDir(null)을 통해 기본적으로 저장되는 경로에 저장되도록 하였다.
다음으로, Recorder 세팅하는 부분을 확인하면
setAudioSource(MediaRecorder.AudioSource.MIC)
setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)
setAudioEncoder(MediaRecorder.AudioEncoder.AAC)
setOutputFile(outputFile.value?.absolutePath)
이 4가지를 확인할 수 있는데, 이 설정들은 제대로 해주지 않으면 음성 녹음에서 IllegalStateExceptions가 발생할 수 있다.
setAudioSource는 오디오를 녹음할 소스를 작성하는 부분이다.
소스의 종류는 이와 같이 많이 존재하는데, 우리는 마이크로 녹음을 할 것이기 때문에 MIC를 제외한 다른 부분은 확인하지 않아도 된다.
setOutputFormat는 녹음 결과의 포맷을 지정해 주는 부분이다.
포맷 또한 많이 제공해주고 있는데, 우리는 THREE_GPP나 MPEG_4를 사용하면 된다.
Google Developer 문서를 확인해 보면 THREE_GPP를 사용하고 있는데, 사용할 수 있는 다른 포맷을 찾아보다 MPEG_4 (. mp4) 파일도 사용이 가능해서 적용해 보았다.
구글링을 좀 더 해보면 여러 가지 포맷을 사용해서 저장이 가능한 것으로 보이는데, 딥하게 음성 녹음에 대해서 파고드는 경우가 아니라면 구태여 찾아볼 필요는 없어 보인다.
setAudioEncoder는 녹음할 음성 파일의 인코더를 지정해 주는 부분이다.
인코더는 비교적 개수가 적긴 한데, 필자는 여기에서 AAC를, 공식 문서에서는 AMR_NB를 사용하고 있다.
이 두 가지의 차이로는,
AAC : 높은 음질을 제공하는 오디오 코딩 형식. 높은 음질, 낮은 비트레이트에서도 효율적인 압축이 가능하지만, 오디오 압축을 위해 높은 CPU 사용량을 보인다.
AMR_NB : 좁은 대역폭에서 동작하는 오디오 코딩 형식으로, 주로 휴대전화의 음성 통화에 사용된다. 낮은 비트레이트에서 효과적인 음성 전송을 제공하며 음성 통화에 최적화되어있으며, 높은 음질이 필요한 음악이나 다양한 오디오가 들어가는 경우 비적합 함.
이다.
필자는 AAC와 AMR_NB를 둘 다 사용해서 확인을 해보았는데, 단순한 음성 녹음이라 그런지 큰 차이를 느끼지 못하였고 용량 또한 크게 차지가 나지 않아서 AAC를 사용하였다.
하지만, 통화에 최적화되어있는 인코더는 AMR_NB이기 때문에 실 업무에 사용할 때는 AMR_NB를 사용하였다.
다음으로는 setOutputFile을 통해 File의 상대 경로를 넣어주고,
prepare()과 start()를 통해 음성 녹음을 시작하면 된다.
이때, 음성 녹음이라고는 하지만 파일에 데이터를 "작성"하는 작업이기 때문에, IOException에 대한 처리를 해주어야 한다.
따라서,
try {
...
mediaRecorder.value?.apply {
...
}
} catch (e: IOException) {
e.printStackTrace()
}
이처럼 try-catch로 감싸주어야 정상적으로 사용이 가능하다.
음성 녹음을 시작했으니,
다음 순서인 음성 녹음이 끝나기 전 일시 정지와, 재개하는 로직, 마지막으로 종료하는 로직까지 살펴보도록 하자.
왜 갑자기 한 번에 설명하는 건가? 싶겠지만, 로직을 보면 이해가 될 것이다.
mediaRecorder.value?.apply {
pause()
}
일시 정지하고,
mediaRecorder.value?.apply {
resume()
}
다시 재개하고,
mediaRecorder.value?.apply {
stop()
reset()
release()
mediaRecorder.value = null
}
종료한다.
mediaRecorder에서 제공하는 함수가 정말 직관적이면서도 간단하기 때문에, 쉽게 사용이 가능하다.
reset()은 딱히 하지 않아도 상관없지만, release 하기 전에 reset을 통해 데이터를 한번 지워주고 release 하도록 추가한 것이다.
녹음하고 저장까지 끝났으니, 다음은 재생하는 로직을 확인해 보자.
MediaRecorder를 사용한 음성 녹음하는 로직과 거의 90% 동일한 방식으로
MediaPlayer를 사용할 수 있다.
MediaPlayer를 통한 음성 파일을 재생은 다음과 같은 순서로 진행된다.
- 녹음 파일 재생
- (녹음 파일 일시 정지)
- (녹음 파일 재개)
- 녹음 파일 재생 종료
2,3번의 경우 선택 사항이고, Recorder와 같이 간단하게 사용이 가능하므로 설명 넘어가도록 하고 1, 4번 항목에 대해서만 설명하도록 하겠다.
2, 3번에 대한 코드는 Github에 올려두었으니 필요시 참고하길 바란다.
mediaPlayer.value = MediaPlayer
.create(context, Uri.parse(outputFile.value?.absolutePath))
.apply {
setAudioAttributes(
AudioAttributes
.Builder()
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.setUsage(AudioAttributes.USAGE_MEDIA)
.build()
)
start()
}
MediaPlayer에 대한 객체를 생성하고,
setAudioAttributes를 통해 재생할 음성 파일에 대한 세팅을 진행해 준다.
ContentType에서는 재생할 파일의 종류를 선택해야 하는데, "음성 녹음" 파일이므로 Music으로 설정해 준다.
setUsage에서는 정말로 많은 플래그 값들을 볼 수 있는데,
우리는 Media 파일을 사용할 것이므로 Usage_Media를 설정해 주도록 한다.
이렇게 설정을 마친 후, start를 하게 되면 음성 파일이 재생되게 된다.
4번 항목인 녹음 파일 재생 종료는 2가지 케이스가 존재한다.
- 정지 버튼을 클릭하는 경우
- 재생이 끝난 경우
여기서 2번 항목에 대해서는 별다르게 처리를 할 필요가 없이 알아서 종료가 된다.
따라서, 사용할 때는 우리가 재생 중이다라는 것을 보여주고 있던 UI를 재생하고 있지 않다는 UI로 변경해주기만 하면 된다.
mediaPlayer.value?.setOnCompletionListener {
isPlaying.value = false
}
mediaPlayer의 해당 Listener를 사용해서 정상적으로 재생이 끝났을 때 callBack 받을 수 있으므로 여기서 UI를 재생하고 있지 않다는 것으로 변경해 주면 된다.
1번 정지 버튼을 클릭하는 경우에는,
if (mediaPlayer.value != null) {
mediaPlayer.value!!.release()
mediaPlayer.value = null
}
mediaRecorder와 비슷하게 해당 Player 객채를 release 해주고, 우리가 사용하고 있던 Player 객체를 null로 갱신시켜 준다.
마지막으로 간단하게, 일시 정지와 재개에 대해서 작성하자면,
mediaPlayer.value?.pause()
mediaPlayer.value?.start()
이 두 가지 함수로 간단하게 가능하다.
아주 간단하게 MediaRecorder, MediaPlayer를 사용해서 음성 녹음을 하고, 파일을 재생하는 로직을 구현해 보았다.
정말로 간단하고, Developer 문서에 잘 설명이 나와있어서 디테일하게 알아가면서 적용할 수 있던 기능이었다.
음성 녹음을 재생하는 도중 포지션을 변경하는 등 추가적으로 넣어볼 법한 기능은 찾아보면 많을 것 같지만,
이미 그런 기능 재공을 위한 함수도 제공해주고 있는 것 같아서 조금만 더 찾아보면 쉽게 구현이 가능할 것 같다.
최근 들어 Media File을 컨트롤하는 업무를 많이 진행하는 것 같은데,
아직까지는 그냥 기능을 구현했다. 라는 정도밖에 되지 않는 것 같은 기분이 든다.
시간이 될 때마다 더 깊게 공부하고 리팩터링 하여 조금 더 사용성이 높은 기능을 구현해 봐야겠다.
해당 게시글에 사용한 예제는 Github에 올려두었다.
https://github.com/HeeGyeong/ComposeSample
'Android > Utility' 카테고리의 다른 글
[Android] Network 연결 여부를 화면과 API에서 체크하는 방법. (0) | 2024.06.03 |
---|---|
[Android] 정책 변경 후 구글 플레이스토어 개발자 계정 생성부터 신규 앱 배포까지 과정 정리 - 2주간 테스터 20명 유지하기 (24) | 2024.03.16 |
[Android] ffmpeg를 사용하여 동영상의 용량을 줄여보자. (0) | 2024.01.24 |
[Android] Notification UI에 대한 몇 가지 변경 방법 (0) | 2024.01.10 |
[Git] Git에서 Head에 잘못 커밋했을 때 커밋 가져오는 방법 (2) | 2023.12.27 |