녹음 및 재생을 위한 객체 설명

안드로이드에서 녹음을 위한 객체는 MediaRecorder, 녹음된 파일을 재생하기 위한 객체는 MediaPlayer 라는 점을 알고가자.

코드 설명

MVVM을 이용한 실제 코드를 살펴보자 + 서버와 API 통신할 때 사용하기위해 mp3파일(오디오파일)을 multipart 파일로 타입캐스팅 해보자

Manifest.xml에 추가

녹음 기능을 사용하기위한 코드를 Mainfest.xml 파일에 등록시키자

 <uses-permission android:name="android.permission.RECORD_AUDIO" />

MainActivity

@AndroidEntryPoint
class MainActivity : ComponentActivity() {

    private val viewModel by viewModels<RecordViewModel>()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            Grade3_2Theme {
                MainScreen(
                    viewModel = viewModel,
                    activity = this
                )
            }
        }
    }
}

@Composable
fun MainScreen(
    viewModel: RecordViewModel,
    activity: MainActivity
) {

    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Button(onClick = { viewModel.checkAudioPermission(activity = activity) }) {
            Text(text = "마이크 권한")
        }
        Button(onClick = {
            if (!viewModel.recoding.value) {
                viewModel.startRecoding()
                viewModel.changeRecoding()
            } else {
                viewModel.stopRecording()
                viewModel.changeRecoding()
            }
        }) { Text(text = if (!viewModel.recoding.value) { "녹음하기" } else { "중지하기" }) }
        Button(onClick = {
            if (!viewModel.playing.value) {
                viewModel.playAudio()
                viewModel.changePlaying()
            } else {
                viewModel.stopAudio()
                viewModel.changePlaying()
            }
        }) { Text(text = if (!viewModel.playing.value) { "재생하기" } else { "중지하기" }) }
    }

}

RecordViewModel

권한 설정 로직을 ViewModel에 함수로 넣어 사용했는데 activity를 매개변수로 넘겨주는 게 맞는 방법인지는 모르겠다.

@HiltViewModel
class RecordViewModel @Inject constructor(
    @ApplicationContext private val context: Context
) : ViewModel() {

    private val _recoding = mutableStateOf(false)
    val recoding: State<Boolean> = _recoding

    private val _playing = mutableStateOf(false)
    val playing: State<Boolean> = _playing

    private val recordPermission = android.Manifest.permission.RECORD_AUDIO
    private var recorder: MediaRecorder? = null
    private var player: MediaPlayer? = null
    private lateinit var audioFileName: String

    fun changeRecoding() {
        _recoding.value = !_recoding.value
    }

    fun changePlaying() {
        _playing.value = !_playing.value
    }

    // 퍼미션 검사 및 요청 코드
    fun checkAudioPermission(activity: Activity): Boolean {
        return if (ActivityCompat.checkSelfPermission(
                context,
                recordPermission
            ) == PackageManager.PERMISSION_GRANTED
        ) {
            true
        } else {
            ActivityCompat.requestPermissions(activity, arrayOf(recordPermission), 0)
            false
        }
    }

    fun startRecoding() {
        val recordPath = context.getExternalFilesDir("/")!!.absolutePath
        val timeStamp: String = SimpleDateFormat("yyyyMMdd_HHmmss").format(Date())
        audioFileName = recordPath + "/" + "RecordExample_" + timeStamp + "_" + "audio.mp4";
        recorder = MediaRecorder().apply {
            setAudioSource((MediaRecorder.AudioSource.MIC))     // 오디오 소스 설정, 대부분 MIC 사용
            setOutputFormat((MediaRecorder.OutputFormat.MPEG_4))// 출력 파일 포맷 설정
            setAudioEncoder(MediaRecorder.AudioEncoder.AAC)     // 인코더 설정
            setOutputFile(audioFileName)                        // 출력 파일 이름 설정

            try {
                prepare()                                       // MediaRecorder 초기하 완료
            } catch (e: IOException) {
                Log.d("daeYoung", "prepare() failed")
            }

            start()                                             // 녹음 시작
            Log.d("daeYoung", "recordFileName: $audioFileName")
        }
    }

    fun stopRecording() {
        recorder?.apply {
            stop()                                              // 녹음 중지
            release()                                           // 작업 완료 시 리소스 확보
        }
        recorder = null
    }

    fun playAudio() {
        player = MediaPlayer().apply {
            setDataSource(audioFileName)
            runCatching { prepare() }.onSuccess {
                start()
            }.onFailure {
                Log.d("daeYoung", "${it.printStackTrace()}")
            }
        }
    }

    fun stopAudio() {
        player?.release()
        player = null
    }

}

아래의 사진은 실제 저장되는 경로와 파일 이름이다.

  • 파일 경로
    로그에 찍힌 저장 경로


    storage -> emulated -> 0 -> Android -> data 까지 들어가면 이후에는 핸드폰에 깔려있는 여러 어플들의 패키지명이 존재한다.

  • 파일 이름
    device file explorer에 저장된 파일 이름


Multipart 타입 캐스팅

Multipart는 이미지와 같은 Url을 폼 데이터 형식으로 서버와 통신하기 위해 사용되는 타입이다.

Multipart는 HTTP를 통해 File을 Server로 전송하기 위해 사용되는 Content-type 입니다. HTTP 프로토콜은 크게 Header와 Body로 구분이 되고 데이터는 Body에 들어가서 전송이 되는데, Body에 들어가는 데이터 타입을 명시해주는게 Content-type입니다. 이때 타입으로 지정해주는 형태를 MIME 타입으로 지정해줄 수 있는데, Multipart(=multipart/form-data)는 MIME 타입 중 하나입니다.

아래의 stopRecoding() 메서드안에 Multipart로 변환하는 코드를 추가하였다.

fun stopRecording() {
    recorder?.apply {
        stop()                                              // 녹음 중지
        release()                                           // 작업 완료 시 리소스 확보
    }
    recorder = null
    val file = File(audioFileName)
    val requestBody = file.asRequestBody("audio/mp3".toMediaTypeOrNull())
    val multipart = MultipartBody.Part.createFormData(
        "record",
        file.name,
        requestBody
    )
    Log.d("daeYoung", "fileName: ${file.name} filePath: ${file.absolutePath}")
    Log.d("daeYoung", "requestBody: ${requestBody} content-type: ${requestBody.contentType()}")
    Log.d("daeYoung", "multipart: ${multipart} body: ${multipart.body} body-content-type: ${multipart.body.contentType()} header: ${multipart.headers}")

}
  • 설명
    File 객체를 만들고 생성자 매개변수로 오디오 파일이 저장된 경로를 넣는다. File 객체의 asRequestBody() 메서드를 통해 RequestBody로 타입 캐스팅을 하고 MultipartBody.Part.createFormData()를 통해서 Multipart 타입으로 바꿔준다.

  • Log 출력 결과

    로그에 찍힌 RequestBody, Multipart


Reference

마이크 녹음(record) 기능 구현하기
안드로이드 공식 홈페이지
음성 파일 서버 업로드

데모 영상