aop-part2-chapter07
목차
- 인트로, 프로젝트 셋업
- 기본 UI 구성하기
- 권한 요청하기
- 녹음 기능 구현하기
- 완성도 높이기 - 오디오 시각화
- 완성도 높이기 - 마무리
Screenshot1Screenshot2 결과화면
![]() |
![]() |
1) 권한 요청하기
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
녹음을 위해 manifast에 녹음 요청 권한 설정을 해야한다.
private val requiredPermissions = arrayOf(Manifest.permission.RECORD_AUDIO) // RECORD_AUDIO의 Permission 값
private fun requestAudioPermission(){
requestPermissions(requiredPermissions, REQUEST_RECORD_AUDIO_PERMISSION)
}
Main Activity에 requiredPermissions 선언 해준뒤 onCreate() 내부에 권한을 최종 요청을 해준다.
override fun onRequestPermissionsResult( // 가장 중요한 부분 !! Permission을 받아야 실행이 가능하다.
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
val audioRecordPermissionGranted =
requestCode == REQUEST_RECORD_AUDIO_PERMISSION && grantResults.firstOrNull() == PackageManager.PERMISSION_GRANTED
// requesetCode가 201과 같은지 확인 하고 / GrantResults: 인가한 결과 값의 [0]번째 인덱스가 PERMISSION_GRANTED 이면 True 반환 !
if(!audioRecordPermissionGranted){
finish()
}
}
권한 허용 확인 요청 기능 구현이 필요 하다.
2) SoundVisuallizerView 구성하기,
<com.example.aop_part2_chaptor07.SoundVisuallizerView
android:id="@+id/soundVisualizerView"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginBottom="10dp"
app:layout_constraintBottom_toTopOf="@id/recordTimeTextView"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
- class로 구현한 soundvisuallizerView를 가져와 layout에 위치 시킴
package com.example.aop_part2_chaptor07
import android.content.Context
import android.graphics.Canvas
import android.graphics.Paint
import android.util.AttributeSet
import android.view.View
class SoundVisuallizerView( // Sound가 계속 움직이는 그런 모습을 보이기 위한 View
context : Context, // context와 attrs를 받는다.
attrs: AttributeSet? = null
) : View(context, attrs){
var onRequestCurrentAmplitude: (() -> Int)? = null
private val amplitudePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = context.getColor(R.color.purple_500)
strokeWidth = LINE_WIDTH // 가로 폭
strokeCap = Paint.Cap.ROUND // 양쪽 끝을 동그랗게 라운드 처리 !
} // 조금더 곡선이 부드럽게 그려진다.
private var drawingWidth : Int = 0
private var drawingHeight : Int = 0
private var drawingAmplitudes: List<Int> = emptyList()
private var isReplaying : Boolean = false
private var replayingPosition : Int = 0
private val visualizeRepeatAction : Runnable = object : Runnable{
override fun run() {
if(!isReplaying) {
// Amplitude를 가져오고 Draw를 요청 !!
val currentAmpletude = onRequestCurrentAmplitude?.invoke() ?: 0
// Amplietude 값 가져오기
drawingAmplitudes = listOf(currentAmpletude) + drawingAmplitudes
// 오른쪽부터 순차적 처리
}
else{
replayingPosition++
}
// Ampletude, Draw
invalidate() // 드로잉 처리
handler?.postDelayed(this, ACTION_INTERVAL)
}
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { // 새로운 가로와 세로가 들어오면 ?!
super.onSizeChanged(w, h, oldw, oldh)
drawingWidth = w
drawingHeight = h
}
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
canvas ?: return
val centerY = drawingHeight / 2f // 뷰의 중앙 높이
var offsetX = drawingWidth.toFloat() // 뷰의 오른 쪽 끝 위치
// 어떤 것을 그릴지
// 뷰의 오른쪽 끝부터 보이기 시작해야함.
drawingAmplitudes.let {
amplitudes ->
if(isReplaying){
amplitudes.takeLast(replayingPosition) // 가장 뒤부터 리플레이 포지션까지
// 재생시에는 역으로 보여줘야 하기 때문에 takeLast를 사용 한것 !
}else{
amplitudes
}
}.forEach {
// 높이 대비 몇 % 로 그릴지 80%로 그림,
amplitue -> val lineLength = amplitue / MAX_AMPLITUDE * drawingHeight * 0.8F
// offsetX 다시 계산
// 뷰는 오른쪽 부터 그린다.
offsetX -= LINE_SPACE
if(offsetX < 0){
return@forEach
} // 뷰의 왼쪽영역보다 밖에 있다면
// 좌상단(0,0) 우하단(w,h)
canvas.drawLine(
offsetX,
centerY - lineLength / 2F,
offsetX,
centerY + lineLength / 2F,
amplitudePaint
)
}
}
fun startVisualizing(isReplaying : Boolean){
this.isReplaying = isReplaying
handler?.post(visualizeRepeatAction)
}
fun stopVisualizing(){
replayingPosition = 0
handler?.removeCallbacks(visualizeRepeatAction)
}
fun clearVisualization(){
drawingAmplitudes = emptyList()
invalidate()
}
companion object{ // 별도 상수로 빼주기위한 companion
private const val LINE_WIDTH = 10F
private const val LINE_SPACE = 15F
private const val MAX_AMPLITUDE = Short.MAX_VALUE.toFloat() // 32767
private const val ACTION_INTERVAL = 20L
}
}
soundvisuallizerView.class
paint는 색상을 집어 넣어준다
Kotlin 표준 라이브러리는 몇 가지 객체의 Context 내에서 코드 블록{}을 실행하는 것이 유일한 목적인 몇 가지 함수가 포함되어 있다.
객체에서 이 람다 함수를 호출하면 해당 함수는 일시적인 Scope를 생성하고, 해당 Scope 안에서는 객체의 이름 없이도 접근이 가능하다.
이러한 함수를 Scope Function(범위 지정 함수)이라고 하며, let, run, with, apply, also가 있다.
3) 녹음기능 구현하기
private fun initVariables(){
state = State.BEFORE_RECORDING
}
private fun startRecord(){ // 오디오 녹음 시작 상태 변경 (State)
recorder = MediaRecorder().apply {
setAudioSource(MediaRecorder.AudioSource.MIC) // MIC : Microphone audio source
setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP) // 비디오 포맷으로 가장 많이 사용되는 THREE_GPP
setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB) // Audio Recording 시 Audio Encoder 를 설정해 주는 함수 입니다.
setOutputFile(recordingFilePath) // 파일을 저장하는 chache 위치
prepare() // prepare 단계는 앞서 설정한 설정값들로 recording 을 준비하는 단계 입니다.
}
recorder?.start()
soundVisualizerView.startVisualizing(false)
recordTimeTextView.startCountUp()
state = State.ON_RECORDING // 녹음 중으로 변경 시킴,
}
음성 녹음을 위한 최초 설정과 start 버튼 클릭시 확인
Android Developer 의 MediaRecorder API Guide 를 참고하면, MediaRecorder 는 아래와 같은 state machine 을 갖습니다.
따라서 MediaRecorder API 를 사용할 때 State 를 잘 따라서 코딩해야 합니다.
예를들어 Initail 상태에서는 바로 Prepared 상태로 갈 수 없고, Initailized 와 DataConfigured 상태를 거쳐야지 Prepared 상태가 될 수 있습니다. 이를 어기게 되면 StateIllegalException 이 발생하고, 원하는 동작을 얻을 수 없으니 조심해야 합니다.
정리(전체코드)
package com.example.aop_part2_chaptor07
import android.Manifest
import android.content.pm.PackageManager
import android.media.MediaPlayer
import android.media.MediaRecorder
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.Button
class MainActivity : AppCompatActivity() {
private val soundVisualizerView : SoundVisuallizerView by lazy {
findViewById<SoundVisuallizerView>(R.id.soundVisualizerView)
}
private val recordTimeTextView : countUpTextView by lazy {
findViewById(R.id.recordTimeTextView)
}
private val resetButton : Button by lazy {
findViewById<Button>(R.id.resetButton)
}
private val recordButton : RecordButton by lazy {
findViewById<RecordButton>(R.id.recordButton)
}
private val recordingFilePath : String by lazy { // ("--외부 앱 전용 공간--")
"${externalCacheDir?.absolutePath}/recording.3gp"
} // /storage/emulated/0/Android/data/com.tistory.blackjin.storeapplicaion/cache/recording.3gp 이런 형
private var state = State.BEFORE_RECORDING
set(value) { // set(value)를 이용하여 resetButton을 누를 수 있는 시기 설정 해준다.
field = value
resetButton.isEnabled = (value == State.AFTER_RECORDING) || (value == State.ON_PLAYING) // 리셋 버튼을 누를수 있는 시기를 설
recordButton.updateIconWithState(value)
}
private val requiredPermissions = arrayOf(Manifest.permission.RECORD_AUDIO) // RECORD_AUDIO의 Permission 값
private var player : MediaPlayer? = null // player는 녹음을 한 것을 실행 시키기 위한 변수
private var recorder : MediaRecorder? = null // recorder는 녹음을 하기 위한 변수
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
requestAudioPermission()
initViews()
bindViews()
initVariables()
}
override fun onRequestPermissionsResult( // 가장 중요한 부분 !! Permission을 받아야 실행이 가능하다.
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
val audioRecordPermissionGranted =
requestCode == REQUEST_RECORD_AUDIO_PERMISSION && grantResults.firstOrNull() == PackageManager.PERMISSION_GRANTED
// requesetCode가 201과 같은지 확인 하고 / GrantResults: 인가한 결과 값의 [0]번째 인덱스가 PERMISSION_GRANTED 이면 True 반환 !
if(!audioRecordPermissionGranted){
finish()
}
}
private fun requestAudioPermission(){
requestPermissions(requiredPermissions, REQUEST_RECORD_AUDIO_PERMISSION)
}
private fun initViews(){
recordButton.updateIconWithState(state)
}
private fun bindViews(){
soundVisualizerView.onRequestCurrentAmplitude = { // sound 진폭 모습 설정 !
recorder?.maxAmplitude ?: 0
} // 콜백함수를 통해 진폭값을 계속 받아와야한다.
recordButton.setOnClickListener{
when(state){
State.BEFORE_RECORDING -> { // 녹음 시작 전일 때 클릭하면 -> 녹음 시작이 된다
startRecord()
}
State.ON_RECORDING -> { // 녹음 중일때 클릭하면 -> 녹음이 멈춤
stopRecording()
}
State.AFTER_RECORDING -> { // 녹음 완료 됐을때 클릭 -> 녹음한 것이 실행됨
startPlaying()
}
State.ON_PLAYING -> { // 녹음을 한 것을 싱행 중일 때 클릭 -> 녹음 실행이 멈춤
stopPlaying()
}
}
}
resetButton.setOnClickListener{
stopPlaying()
soundVisualizerView.clearVisualization()
recordTimeTextView.clearCountTimer()
state = State.BEFORE_RECORDING
}
}
private fun initVariables(){
state = State.BEFORE_RECORDING
}
private fun startRecord(){ // 오디오 녹음 시작 상태 변경 (State)
recorder = MediaRecorder().apply {
setAudioSource(MediaRecorder.AudioSource.MIC) // MIC : Microphone audio source
setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP) // 비디오 포맷으로 가장 많이 사용되는 THREE_GPP
setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB) // Audio Recording 시 Audio Encoder 를 설정해 주는 함수 입니다.
setOutputFile(recordingFilePath) // 파일을 저장하는 chache 위치
prepare() // prepare 단계는 앞서 설정한 설정값들로 recording 을 준비하는 단계 입니다.
}
// Android Developer 의 MediaRecorder API Guide 를 참고하면, MediaRecorder 는 아래와 같은 state machine 을 갖습니다.
// 따라서 MediaRecorder API 를 사용할 때 State 를 잘 따라서 코딩해야 합니다.
// 예를들어 Initial 상태에서는 바로 Prepared 상태로 갈 수 없고, Initailized 와 DataConfigured 상태를 거쳐야지 Prepared 상태가 될 수 있습니다.
// 이를 어기게 되면 StateIllegalException 이 발생하고, 원하는 동작을 얻을 수 없으니 조심해야 합니다.
recorder?.start()
soundVisualizerView.startVisualizing(false)
recordTimeTextView.startCountUp()
state = State.ON_RECORDING // 녹음 중으로 변경 시킴,
}
private fun stopRecording(){ // 오디오 멈추기
recorder?.run {
stop() // 간단하다 stop()을 주면 된다.
release()
}
recorder = null // recorder는 stop이 됐으니깐 null 값을 준다.
soundVisualizerView.stopVisualizing()
recordTimeTextView.stopCountUp()
state = State.AFTER_RECORDING
}
private fun startPlaying(){ // 오디오 녹음 된 파일 재생
player = MediaPlayer().apply {
setDataSource(recordingFilePath) // 캐시에 저장돼있는 DataSource를 가져옴
prepare()
}
player?.setOnCompletionListener {
stopPlaying()
state = State.AFTER_RECORDING // 재생이 다됐으니깐 !
} // 현재 전달된 파일이 전부다 재생 됐을 때 발생하는 리스너
player?.start() // setDataSource -> prepare() -> start()
soundVisualizerView.startVisualizing(true)
recordTimeTextView.startCountUp()
state = State.ON_PLAYING
}
private fun stopPlaying(){ // 오디오 녹음 재생을 중지 !!
player?.release() // MediaPlayer State Machine을 참조 하면 release()가 end를 위한 조건
player = null
soundVisualizerView.stopVisualizing()
recordTimeTextView.stopCountUp()
state = State.AFTER_RECORDING
}
companion object{
private const val REQUEST_RECORD_AUDIO_PERMISSION = 201
}
}
'안드로이드 > 앱개발(Android)' 카테고리의 다른 글
(Android) 파이어베이스 Notification 알림 앱 (0) | 2022.01.28 |
---|---|
(코틀린 kotlin) 웹뷰 앱 (0) | 2022.01.25 |
(코틀린 kotlin) 타이머 앱 (0) | 2022.01.22 |
(코틀린 kotlin) 전자액자 앱 (0) | 2022.01.21 |
(코틀린 kotlin) 계산기 앱 (0) | 2022.01.21 |