Android | PCM to WAV

이동욱·2023년 12월 13일
0

AudioRecord API로 녹음한 pcm파일을 wav파일로 변환하기

1. PCM과 WAV는 무엇인가?

1. PCM

pcm (Pulse Code Modulation) 파일은 아날로그 신호를 디지털 신호로 변환하는 방식 중 하나를 의미한다.
pcm은 압축되지 않은 원시 오디오 데이터를 나타내기 위해 주로 사용되고 “.pcm” , “.raw” 등의 확장자로 사용될 수 있다.

2. WAV

WAV파일은 PCM 데이터에 헤더와 메타데이터가 추가된 오디오 데이터 저장 형식이다.

WAV파일 헤더에는 오디오 데이터의 형식, 채널수, 샘플링 속도 비트 당 샘플 수 등과 같은 오디오 샘플에 관한 정보가 들어가있다.

메타데이터에는 곡의 제목, 아티스트 정보등 사용자가 원하는 정보를 포함 할 수 있다.

2. WAV 만들기

1. WAV헤더구조

wav헤더에 대해서 알아야 wav파일을 만들 수 있다.

WAV헤더는 파일 시작 부분에 위치해 파일에 대한 기본적인 정보들을 가지고 있다.

총 44바이트로 이루어져 있으며 아래와 같은 필드로 구성되어 있다.

  1. Chunk ID (4바이트)

    파일이 RIFF 형식임을 나타내기 위해 “RIFF” 4글자 ASCII 문자열

  2. File Size (4바이트)

    파일 전체에 대한 Size를 나타낸다. 단 Chunk ID 값과 File Size 값을 제외한 값이기 때문에 전체 파일크기 - 8 byte가 된다.

    리틀 엔디안(little endian)값으로 저장한다

  3. Format (4바이트)

    “WAVE” 4글자 ASCII 문자열이 들어간다.

  4. Subchunk1 ID (4 바이트)

    "fmt "라는 4글자 ASCII 문자열이 들어간다 (fmt + 공백)

  5. Subchunk1 Size (4 바이트):

    헤더의 첫 번째 서브체이크 크기를 나타냅니다. 일반적으로 16바이트 고정

  6. Audio Format (2 바이트):

    오디오 데이터의 포맷을 나타낸다.

    PCM의 경우 1로 설정되는데 대부분의 wav파일은 PCM이기 때문에 1 고정값 처럼 쓰인다.

  7. Num Channels (2 바이트):

    오디오 채널의 수를 나타낸다.

    모노 1 , 스트레오 2 …

  8. Sample Rate (4 바이트):

    샘플링 속도를 나타냅니다.

    리틀 엔디안(little endian

  9. Byte Rate (4 바이트):

    초당 바이트 수

    리틀 엔디안(little endian)

  10. Block Align (2 바이트):

    블록의 크기를 나타냅니다. 채널 수와 비트 당 샘플 수에 따라 결정됩니다.

  11. Bits Per Sample (2 바이트):

    비드 뎁스를 의미한다.

  12. Subchunk2 ID (4 바이트):

    "data"라는 4글자로 이루어진 ASCII 문자열을 쓴다.

  13. Subchunk2 Size (4 바이트):

    오디오 데이터의 크기를 나타낸다. 즉 전체파일 - header크기

    리틀 엔디안(little endian)

💡 리틀 엔디안이란?
리틀 엔디안은 가장 낮은 주소에 작은 단위의 바이트가 저장되는 방식

예시
16진수 0x12345678을 메모리에 저장
[0x78 , 0x56 , 0x34 , 0x12]


3. 코드 예시(Kotlin)

안드로이드에서 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

profile
프론트엔드

0개의 댓글