Android PCM를 WAV로 변환하는 방법

앱 내에서 녹음 파일은 초기에 .pcm 형식으로 저장되었습니다. 사용에 아무런 문제도 없었지만 최근 웹뷰에서 오디오 파일을 로드할 때 호환성 문제가 발생했습니다. 이에 .pcm 형식 파일을 .wav 형식으로 전환할 필요가 생겼습니다.

PCM

PCM(Pulse Code Modulation - 펄스 코드 모듈레이션)은 음성과 같은 아날로그 신호를 상징화된 펄스 세트로 전환하는 방법입니다. PCM 신호는 [1], [0]와 같은 상징으로 구성된 디지털 신호로, 압축이나 인코딩을 거치지 않습니다. 이는 전송 시스템의 잡음 및 왜곡으로부터 보호받는다는 장점이 있습니다. 또한 폭넓은 dynamic range를 제공하며 품질이 우수한 음질을 얻을 수 있습니다. 간단히 말해 PCM은 압축되지 않은 오디오 형식입니다.

WAV

WAV는 WAVE라는 약자로, .wav가 그 확장명입니다. 이는 손실없는 오디오 파일 형식으로 RIFF(Resource Interchange File Format) 규격을 따릅니다. 모든 WAV 파일은 하나의 파일 헤더를 가지고 있습니다. 이 헤더는 오디오 스트림의 인코딩 매개변수를 포함합니다. WAV는 PCM 외에도几乎所有 ACM 규격에 따른 인코딩 방법을 사용할 수 있습니다.

PCM과 WAV의 관계 PCM은 WAV 파일 내의 오디오 데이터의 한 가지 인코딩 방법입니다. PCM에 파일 헤더를 추가하면 WAV 형식으로 전환할 수 있습니다. 그러나 WAV는 PCM 외에도 다른 인코딩 방법을 사용할 수 있습니다.

PcmToWavUtil

package com.hgb.mytest;
import android.media.AudioFormat;
import android.media.AudioRecord;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
/**
* PCM 형식의 오디오를 WAV 형식으로 전환하는 유틸리티 클래스
*/
public class PcmToWavUtil {
    private int bufferSize; // 버퍼 크기
    private int sampleRate = 8000; // 8000|16000
    private int channel = AudioFormat.CHANNEL_IN_STEREO; // 스테레오
    private int encoding = AudioFormat.ENCODING_PCM_16BIT;
    
    public PcmToWavUtil() {
        this.bufferSize = AudioRecord.getMinBufferSize(sampleRate, channel, encoding);
    }
    
    /**
     * @param sampleRate 샘플레이트
     * @param channel 채널 수
     * @param encoding 오디오 데이터 형식
     */
    public PcmToWavUtil(int sampleRate, int channel, int encoding) {
        this.sampleRate = sampleRate;
        this.channel = channel;
        this.encoding = encoding;
        this.bufferSize = AudioRecord.getMinBufferSize(sampleRate, channel, encoding);
    }
    
    /**
     * PCM 파일을 WAV 파일로 전환
     *
     * @param inFilename 원본 파일 경로
     * @param outFilename 대상 파일 경로
     */
    public void pcmToWav(String inFilename, String outFilename) {
        FileInputStream in;
        FileOutputStream out;
        long totalAudioLen;
        long totalDataLen;
        long byteRate;
        int channels = 2;
        byte[] data = new byte[bufferSize];
        
        try {
            in = new FileInputStream(inFilename);
            out = new FileOutputStream(outFilename);
            totalAudioLen = in.getChannel().size();
            totalDataLen = totalAudioLen + 36;
            byteRate = 16 * sampleRate * channels / 8;
            
            writeWaveFileHeader(out, totalAudioLen, totalDataLen,
                    sampleRate, channels, byteRate);
            
            while (in.read(data) != -1) {
                out.write(data);
            }
            
            in.close();
            out.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    
    /**
     * WAV 파일 헤더 쓰기
     */
    private void writeWaveFileHeader(FileOutputStream out, long totalAudioLen,
            long totalDataLen, long sampleRate, int channels, long byteRate)
            throws IOException {
        byte[] header = new byte[44];
        header[0] = 'R'; // RIFF/WAVE 헤더
        header[1] = 'I';
        header[2] = 'F';
        header[3] = 'F';
        header[4] = (byte) (totalDataLen & 0xff);
        header[5] = (byte) ((totalDataLen >> 8) & 0xff);
        header[6] = (byte) ((totalDataLen >> 16) & 0xff);
        header[7] = (byte) ((totalDataLen >> 24) & 0xff);
        header[8] = 'W'; // WAVE
        header[9] = 'A';
        header[10] = 'V';
        header[11] = 'E';
        header[12] = 'f'; // 'fmt ' chunk
        header[13] = 'm';
        header[14] = 't';
        header[15] = ' ';
        header[16] = 16; // 'fmt ' chunk 크기
        header[17] = 0;
        header[18] = 0;
        header[19] = 0;
        header[20] = 1; // 형식 = 1
        header[21] = 0;
        header[22] = (byte) channels;
        header[23] = 0;
        header[24] = (byte) (sampleRate & 0xff);
        header[25] = (byte) ((sampleRate >> 8) & 0xff);
        header[26] = (byte) ((sampleRate >> 16) & 0xff);
        header[27] = (byte) ((sampleRate >> 24) & 0xff);
        header[28] = (byte) (byteRate & 0xff);
        header[29] = (byte) ((byteRate >> 8) & 0xff);
        header[30] = (byte) ((byteRate >> 16) & 0xff);
        header[31] = (byte) ((byteRate >> 24) & 0xff);
        header[32] = (byte) (2 * 16 / 8); // block align
        header[33] = 0;
        header[34] = 16; // 샘플당 비트 수
        header[35] = 0;
        header[36] = 'd'; // data chunk
        header[37] = 'a';
        header[38] = 't';
        header[39] = 'a';
        header[40] = (byte) (totalAudioLen & 0xff);
        header[41] = (byte) ((totalAudioLen >> 8) & 0xff);
        header[42] = (byte) ((totalAudioLen >> 16) & 0xff);
        header[43] = (byte) ((totalAudioLen >> 24) & 0xff);
        
        out.write(header, 0, 44);
    }
}

** демо 예제 **

휴대폰의 내장 카드 /sdcard/yxck/treamentRecord/ 디렉토리에 있는 123.pcm 파일을 123.wav로 전환합니다.

public class MainActivity extends AppCompatActivity {
    private PcmToWavUtil pcmToWavUtil = new PcmToWavUtil();
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        final String path = "/sdcard/yxck/treamentRecord/123.pcm";
        final String outpath = path.replace(".pcm", ".wav");
        
        Button button = (Button) findViewById(R.id.button);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                pcmToWavUtil.pcmToWav(path, outpath);
            }
        });
    }
}

** 결과 **

PCM를 WAV로 성공적으로 변환되었습니다.

태그: Android PCM WAV

6월 9일 01:55에 게시됨