Android 에서 제공하는 AudioRecord Class 를 사용하면 음성녹음 기능을 쉽게 구현할 수 있습니다.

Application 에서는 AudioRecord Class 에서 제공하는 세가지 형태의 read method를 통해서 음성 데이터를 가져올 수 있습니다.

- read(byte[], int, int), read(short[], int, int), read(ByteBuffer, int)

어떤 형태의 함수를 쓸 것인지는 Application 에서 다루는 Data 형태에 따라 사용자가 결정하면 됩니다.

 

이 포스팅에서는 간단한 음성녹음 예제를 다루어 볼 것입니다.

간단하게 버튼 두개를 두고, 하나의 버튼으로는 녹음 시작/정지 를 수행하고, 다른 하나의 버튼으로는 재생/정지를 구현해 보도록 하겠습니다.

 

1. main layout 에 button 추가 하기

Record Button 과 Play Button 을 추가하고, 각각 Button 이 클릭 되면 해당 동작을 수행할 함수 onRecord/onPlay 를 정의 합니다.

<Button
android:id="@+id/bt_record"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Record"
android:onClick="onRecord"/>

<Button
android:id="@+id/bt_play"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Play"
android:onClick="onPlay"/>

 

2. AudioRecord 생성 및 초기화

AudioRecord 생성자를 보면, AudioRecord 객체를 생성하기 위해 필요한 인자들을 확인 할 수 있습니다.

- public AudioRecord(int audioSource, int sampleRateInHz, int channelConfig, int audioFormat, int bufferSizeInBytes)

첫번째 인자인 AudioSource 의 경우 MediaRecorder.AudioSource 에 int 값이 정의되어 있고, Mic 로 들어오는 음성을 녹음하기 위해서는 1의 값을 넣어주면 됩니다.

두번째 세번째 네번째 인자는 실제 녹음될 오디오 데이터의 samplerate / channel / format 값을 의미합니다.

Sample Rate 는 아날로그 신호를 디지털로 변환 할 때, 1초당 몇개의 sample 을 추출할 것인가에 대한 내용으로 보통 8000Hz~48000Hz 가 많이 사용됩니다. 물론 값이 클수록 아날로그 신호와 비슷한 형태가 되니 고음질의 컨텐츠가 되겠지요.

Channel 값은 mono/streo 중에 선택하면 되고, audioformat 의 값은 PCM Data 를 받을 것이기 때문에 AudioFormat.ENCODING_PCM_16BIT 를 넣어주도록 하겠습니다.

마지막으로 buffer size 는 한번에 전달 받을 audio data 의 크기를 나타내고, 보통은 AudioRecord 의 getMinBufferSize 함수를 호출해서 return 되는 값으로 지정을 해줍니다.

 

public class MainActivity extends Activity {
    private static final String TAG = "MainActivity";

    private int mAudioSource = MediaRecorder.AudioSource.MIC;
    private int mSampleRate = 44100;
    private int mChannelCount = AudioFormat.CHANNEL_IN_STEREO;
    private int mAudioFormat = AudioFormat.ENCODING_PCM_16BIT;
    private int mBufferSize = AudioTrack.getMinBufferSize(mSampleRate, mChannelCount, mAudioFormat);

    public AudioRecord mAudioRecord = null;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mAudioRecord = new AudioRecord(mAudioSource, mSampleRate, mChannelCount, mAudioFormat, mBufferSize);
        mAudioRecord.startRecording();
    }

여기까지 하면 실제 레코딩 할 준비가 완료 되었고, 다음은 실제 레코딩을 시작해 보도록 하겠습니다.

 

3. 녹음 시작 - PCM Data 가져오기

녹음을 시작하고 PCM Data 를 가져오는 부분은 UI가 멈춰보이지 않도록 별도의 Thread 로 분리해야 합니다. 

이를 위해 mRecordThread 를 추가로 정의하고, 레코딩 중인지 아닌지를 알기 위해 isRecording 이라는 boolean 변수를 정의하였습니다.

mRecordThread 는 onCreate 함수 내부에 정의를 해놓고, 실제 start 되는 것은 사용자가 버튼을 누른 순간에 이루어지도록 합니다.

읽어온 PCM 데이터를 파일에 쓰기 위해 filepath 를 지정해 주고, FileOutputStream 을 통해 file open/write/close 를 수행하였습니다.

 

    public Thread mRecordThread = null;
    public boolean isRecording = false;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ..
        mRecordThread = new Thread(new Runnable() {
            @Override
            public void run() {
                byte[] readData = new byte[mBufferSize];
                mFilepath = Environment.getExternalStorageDirectory().getAbsolutePath() +"/record.pcm";
                FileOutputStream fos = null;
                try {
                    fos = new FileOutputStream(mFilepath);
                } catch(FileNotFoundException e) {
                    e.printStackTrace();
                }

                while(isRecording) {
                    int ret = mAudioRecord.read(readData, 0, mBufferSize);  //  AudioRecord의 read 함수를 통해 pcm data 를 읽어옴
                    Log.d(TAG, "read bytes is " + ret);

                    try {
                        fos.write(readData, 0, mBufferSize);    //  읽어온 readData 를 파일에 write 함
                    }catch (IOException e){
                        e.printStackTrace();
                    }
                }

                mAudioRecord.stop();
                mAudioRecord.release();
                mAudioRecord = null;

                try {
                    fos.close(); 
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        });
    }

    public void onRecord(View view) {
        if(isRecording == true) {
            isRecording = false;
            mBtRecord.setText("Record");
        }
        else {
            isRecording = true;
            mBtRecord.setText("Stop");

            if(mAudioRecord == null) {
                mAudioRecord =  new AudioRecord(mAudioSource, mSampleRate, mChannelCount, mAudioFormat, mBufferSize);
                mAudioRecord.startRecording();
            }
            mRecordThread.start();
        }

    }

여기까지 수행 후 실행 시켜 보면 앱이 바로 죽는데, 로그를 확인해 보면 음성녹음을 위한 permission 이 없어서 AudioRecord 가 생성되지 못한 것을 알 수 있습니다.

11-28 20:06:31.467   770 14497 W ServiceManager: Permission failure: android.permission.RECORD_AUDIO from uid=10419 pid=-1
11-28 20:06:31.467   770 14497 E         : Request requires android.permission.RECORD_AUDIO
11-28 20:06:31.467   770 14497 E AudioFlinger: openRecord() permission denied: recording not allowed
11-28 20:06:31.472 31635 31635 E AndroidRuntime: java.lang.RuntimeException: Unable to start activity ComponentInfo{com.codetravel.audiorecord/com.codetravel.audiorecord.MainActivity}: java.lang.IllegalStateException: startRecording() called on an uninitialized AudioRecord.

음성녹음과 추후 파일 write 를 위해 아래 두개의 permission 을 AndroidManifest.xml 파일에 추가해 줍니다.

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

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

Record 버튼을 누르게 되면 녹음이 쭉 진행 되고, Stop 버튼을 누르면 /storage/emulated/0/ 위치 밑에 record.pcm 이름으로 녹음된 파일이 저장 된 것을 확인 할 수 있습니다.

 

4. 녹음 완료 -  생성된 파일 재생하기

생성된 파일을 재생하기 위해 AudioTrack 을 사용해 보도록 하겠습니다.

AudioTrack 도 AudioRecord 와 마찬가지로 PCM Audio Data 를 다룰 수 있는 API 중에 하나입니다.

AudioTrack 의 생성자는 AudioRecord class 의 생성자와 비슷한 형태를 띄기 때문에, 각 매개변수는 어렵지 않게 채울 수 있습니다.

- AudioTrack(int streamType, int sampleRateInHz, int channelConfig, int audioFormat, int bufferSizeInBytes, int mode) 

첫번째 매개변수인 streamtype 은 하기 링크에 정의되어 있는 stream type 중, 용도에 맞는 Stream type 값을 넣어주면 됩니다. (Music/Alarm/Voice call 등)

http://androidxref.com/8.0.0_r4/xref/frameworks/base/media/java/android/media/AudioManager.java#319

 

AudioRecord Class 에서 PCM Data 를 읽어오기 위해 read 함수를 이용했다면, 이번에는 write 함수를 이용해 Track 에 PCM Data 를 써주어서 스피커로 송출 될 수 있도록 하겠습니다.

파일에서 PCM Data 를 읽어와 Track 에 쓰기 위한 별도의 Thread 인 mPlayThread 를 onCreate 함수 내부에 정의 하였습니다.

mPlayThread 는 FileInputStream 을 이용해서 File 의 내용을 읽어다가 AudioTrack 에 write 해주는 역할을 합니다.

 

   
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ..
        mAudioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, mSampleRate, mChannelCount, mAudioFormat, mBufferSize, AudioTrack.MODE_STREAM); // AudioTrack 생성
        ..
        mPlayThread = new Thread(new Runnable() {
            @Override
            public void run() {
                byte[] writeData = new byte[mBufferSize];
                FileInputStream fis = null;
                try {
                    fis = new FileInputStream(mFilePath);
                }catch (FileNotFoundException e) {
                    e.printStackTrace();
                }

                DataInputStream dis = new DataInputStream(fis);
                mAudioTrack.play();  // write 하기 전에 play 를 먼저 수행해 주어야 함

                while(isPlaying) {
                    try {
                        int ret = dis.read(writeData, 0, mBufferSize);
                        if (ret <= 0) {
                            (MainActivity.this).runOnUiThread(new Runnable() { // UI 컨트롤을 위해 
                                @Override
                                public void run() {
                                    isPlaying = false;
                                    mBtPlay.setText("Play");
                                }
                            });
                            break;
                        }
                        mAudioTrack.write(writeData, 0, ret); // AudioTrack 에 write 를 하면 스피커로 송출됨
                    }catch (IOException e) {
                        e.printStackTrace();
                    }

                }
                mAudioTrack.stop();
                mAudioTrack.release();
                mAudioTrack = null;

                try {
                    dis.close();
                    fis.close();
                }catch (IOException e) {
                    e.printStackTrace();
                }
            }
        });
    }

    public void onPlay(View view) {
        if(isPlaying == true) {
            isPlaying = false;
            mBtPlay.setText("Play");
        }
        else {
            isPlaying = true;
            mBtPlay.setText("Stop");

            if(mAudioTrack == null) {
                mAudioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, mSampleRate, mChannelCount, mAudioFormat, mBufferSize, AudioTrack.MODE_STREAM);
            }
            mPlayThread.start();
        }

    }

아직 많은 예외처리들이 필요하지만 , 여기까지 하면 아주 아~주 간단한 Recorder 와 Player 가 완성 되었습니다. ^^

전체 코드 첨부하니 필요하신 분들은 참고하세요.

 

AudioRecord.zip

 

  1. 익명 2020.01.07 11:16

    비밀댓글입니다

  2. 플랑크 2020.12.15 11:53

    감사합니다 ^^

  3. 상수리 2021.02.16 21:40

    Caused by: java.lang.IllegalStateException: startRecording() called on an uninitialized AudioRecord. 라는 오류가 뜨는데 왜그럴까요... ㅠㅠ

  4. ㅇㅇ 2022.09.28 21:55

    이상하게 한번 녹음하고 한번 재생하면 바로 작동하는데 두번째 녹음과 재생부터는 앱이 팅깁니다. 무슨 문제일까요?

+ Recent posts