AudioRecord API로 녹음한 pcm파일을 wav파일로 변환하기
pcm (Pulse Code Modulation) 파일은 아날로그 신호를 디지털 신호로 변환하는 방식 중 하나를 의미한다.
pcm은 압축되지 않은 원시 오디오 데이터를 나타내기 위해 주로 사용되고 “.pcm” , “.raw” 등의 확장자로 사용될 수 있다.
WAV파일은 PCM 데이터에 헤더와 메타데이터가 추가된 오디오 데이터 저장 형식이다.
WAV파일 헤더에는 오디오 데이터의 형식, 채널수, 샘플링 속도 비트 당 샘플 수 등과 같은 오디오 샘플에 관한 정보가 들어가있다.
메타데이터에는 곡의 제목, 아티스트 정보등 사용자가 원하는 정보를 포함 할 수 있다.
wav헤더에 대해서 알아야 wav파일을 만들 수 있다.
WAV헤더는 파일 시작 부분에 위치해 파일에 대한 기본적인 정보들을 가지고 있다.
총 44바이트로 이루어져 있으며 아래와 같은 필드로 구성되어 있다.
Chunk ID (4바이트)
파일이 RIFF 형식임을 나타내기 위해 “RIFF” 4글자 ASCII 문자열
File Size (4바이트)
파일 전체에 대한 Size를 나타낸다. 단 Chunk ID 값과 File Size 값을 제외한 값이기 때문에 전체 파일크기 - 8 byte가 된다.
리틀 엔디안(little endian)값으로 저장한다
Format (4바이트)
“WAVE” 4글자 ASCII 문자열이 들어간다.
Subchunk1 ID (4 바이트)
"fmt "라는 4글자 ASCII 문자열이 들어간다 (fmt + 공백)
Subchunk1 Size (4 바이트):
헤더의 첫 번째 서브체이크 크기를 나타냅니다. 일반적으로 16바이트 고정
Audio Format (2 바이트):
오디오 데이터의 포맷을 나타낸다.
PCM의 경우 1로 설정되는데 대부분의 wav파일은 PCM이기 때문에 1 고정값 처럼 쓰인다.
Num Channels (2 바이트):
오디오 채널의 수를 나타낸다.
모노 1 , 스트레오 2 …
Sample Rate (4 바이트):
샘플링 속도를 나타냅니다.
리틀 엔디안(little endian
Byte Rate (4 바이트):
초당 바이트 수
리틀 엔디안(little endian)
Block Align (2 바이트):
블록의 크기를 나타냅니다. 채널 수와 비트 당 샘플 수에 따라 결정됩니다.
Bits Per Sample (2 바이트):
비드 뎁스를 의미한다.
Subchunk2 ID (4 바이트):
"data"라는 4글자로 이루어진 ASCII 문자열을 쓴다.
Subchunk2 Size (4 바이트):
오디오 데이터의 크기를 나타낸다. 즉 전체파일 - header크기
리틀 엔디안(little endian)
💡 리틀 엔디안이란?
리틀 엔디안은 가장 낮은 주소에 작은 단위의 바이트가 저장되는 방식
예시
16진수 0x12345678을 메모리에 저장
[0x78 , 0x56 , 0x34 , 0x12]
안드로이드에서 AudioRecord API를 활용해 추출한 PCM파일을 WAV로 변환하기
private var sampleRateInHz = 16000
private var audioChannel = AudioFormat.CHANNEL_IN_MONO
private var audioFormat = AudioFormat.ENCODING_PCM_16BIT
bufferSize = AudioRecord.getMinBufferSize(sampleRateInHz, channelConfig, audioFormat)
private fun addWavHeader(out: FileOutputStream, totalAudioLen: Long, totalDataLen: Long) {
val sampleRate = sampleRateInHz.toLong()
val channels = if (channelConfig == AudioFormat.CHANNEL_IN_MONO) 1 else 2
val bitsPerSample = if (audioFormat == AudioFormat.ENCODING_PCM_8BIT) 8 else 16
val byteRate = sampleRate * channels * bitsPerSample / 8
val blockAlign = channels * bitsPerSample / 8
val header = ByteArray(44)
header[0] = 'R'.code.toByte() // 1.RIFF
header[1] = 'I'.code.toByte()
header[2] = 'F'.code.toByte()
header[3] = 'F'.code.toByte()
header[4] = (totalDataLen and 0xffL).toByte() // 2.파일 사이즈 크기
header[5] = (totalDataLen shr 8 and 0xffL).toByte()
header[6] = (totalDataLen shr 16 and 0xffL).toByte()
header[7] = (totalDataLen shr 24 and 0xffL).toByte()
header[8] = 'W'.code.toByte() // 3.WAVE
header[9] = 'A'.code.toByte()
header[10] = 'V'.code.toByte()
header[11] = 'E'.code.toByte()
header[12] = 'f'.code.toByte() // 4.'fmt '
header[13] = 'm'.code.toByte()
header[14] = 't'.code.toByte()
header[15] = ' '.code.toByte()
header[16] = 16 // 5.16고정
header[17] = 0
header[18] = 0
header[19] = 0
header[20] = 1 // 6.format = PCM = 1
header[21] = 0
header[22] = channels.toByte() // 7.채널수
header[23] = 0
header[24] = (sampleRate and 0xffL).toByte() // 8.샘플레이트
header[25] = (sampleRate shr 8 and 0xffL).toByte()
header[26] = (sampleRate shr 16 and 0xffL).toByte()
header[27] = (sampleRate shr 24 and 0xffL).toByte()
header[28] = (byteRate and 0xffL).toByte() // 9.바이트 레이트
header[29] = (byteRate shr 8 and 0xffL).toByte()
header[30] = (byteRate shr 16 and 0xffL).toByte()
header[31] = (byteRate shr 24 and 0xffL).toByte()
header[32] = blockAlign.toByte() // 10.블록크기
header[33] = 0
header[34] = bitsPerSample.toByte() // 11.비트 뎁스
header[35] = 0
header[36] = 'd'.code.toByte() // 12.'data'
header[37] = 'a'.code.toByte()
header[38] = 't'.code.toByte()
header[39] = 'a'.code.toByte()
header[40] = (totalAudioLen and 0xffL).toByte() // 13.전체 파일 - headr = pcm크기 (메타데이터 없음)
header[41] = (totalAudioLen shr 8 and 0xffL).toByte()
header[42] = (totalAudioLen shr 16 and 0xffL).toByte()
header[43] = (totalAudioLen shr 24 and 0xffL).toByte()
out.write(header, 0, 44)
}
val out = FileOutputStream(wav파일경로)
val in = FileInputStream(pcm파일경로)
val totalAudioLen: Long = in.getChannel().size() // 오디오 파일 사이즈
val totalDataLen = totalAudioLen + 36 // 헤더크기 더하기 (1,2 필드 제외)
addWavHeader(out, totalAudioLen, totalDataLen)
val data = ByteArray(bufferSize)
var bytesRead: Int
//pcm파일 wav에 쓰기
while (in.read(data).also { bytesRead = it } != -1) {
out.write(data, 0, bytesRead)
}
in.close()
out.close()
참조
https://anythingcafe.tistory.com/2
https://crystalcube.co.kr/123
https://gdnn.tistory.com/277