Android 에서 음성을 녹음할 때 사용할 수 있는 API 는 여러가지가 있지만, 지난 포스팅에서 가장 대표적으로 사용되는 AudioRecord API 를 이용한 예제를 다루어 보았습니다.

이번 포스팅에서는 MediaRecorder API 를 이용한 음성 녹음 예제를 다루어 보겠습니다.

AudioRecord 를 이용한 예제처럼 간단하게 완성할 수 있습니다.

 

1. main layout 구성하기

layout 은 간단히 버튼을 두개 놓고, 하나는 녹음 시작/정지, 다른 하나는 녹음된 컨텐츠를 재생/정지 하도록 구현해 보겠습니다.

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context="com.codetravel.mediarecorder.MainActivity">

<Button
android:id="@+id/bt_record"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Start Recording"/>

<Button
android:id="@+id/bt_play"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Start Playing"/>
</LinearLayout>

이를 위해 bt_record 와 bt_play 를 각각 layout 에 추가 하고, MainActivity 에서 각각의 버튼에 대한 OnClickListener() 를 구현 해 줍니다.

OnClickListener() 에서는 현재 레코딩 상태에 따라서 recorder 를 Start 하거나 Stop 하는 부분이 구현될 것입니다.

 

2. MediaRecorder 생성 후 레코딩 준비하기

recorder 를 start 하기에 앞서, MediaRecorder 객체를 하나 생성 하고 레코딩 할 파일의 포맷, 인코더 등등을 설정 해 줍니다.

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

        mRecorder = new MediaRecorder();                          
    }    

    // 레코더 기본 설정 
    void initAudioRecorder() {                                                                
        mRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);                              
        mRecorder.setOutputFormat(MediaRecorder.OutputFormat.AAC_ADTS);                       
        mRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);                            
                                                                                              
        mPath = Environment.getExternalStorageDirectory().getAbsolutePath() + "/record.aac";  
        Log.d(TAG, "file path is " + mPath);                                                  
        mRecorder.setOutputFile(mPath);                                                       
        try {                                                                                 
            mRecorder.prepare();                                                              
        } catch (Exception e) {                                                               
            e.printStackTrace();                                                              
        }                                                                                     
    }                                                                                                                                                 

initAudioRecorder() 함수 안에다가 MediaRecorder prepare 단계 까지 모든 설정들을 구현 합니다.

MediaRecorder 의 State Diagram 을 참고하면, 한번 MediaRecorder 를 Stop 할 시에, 각종 설정들을 다시 해 주어야 하기 때문에 함수로 분리하여 구현하는 것이 추후 더 편리합니다.

 

3. 녹음 시작/ 정지

isRecording 이라는 flag 를 두어 현재 녹음 중인지 아닌지를 구분 할 수 있습니다.

현재 녹음중이 아니면 initAudioRecorder(), start() 를 수행 하여 녹음을 시작하고, 녹음중 일 경우에는 stop() 을 호출하여 녹음을 정지합니다.

isRecording flag 와 button 의 text 는 현재 상태에 맞게 update 합니다.

    boolean isRecording = false;                                                                      
    Button mBtRecord = null;                                       
                                                                  
    @Override                                                     
    protected void onCreate(Bundle savedInstanceState) {          
        ..
        mBtRecord = (Button) findViewById(R.id.bt_record);        
        mBtRecord.setOnClickListener(new View.OnClickListener() { 
            @Override                                             
            public void onClick(View v) {                         
                if (isRecording == false) {                       
                    initAudioRecorder();                          
                    mRecorder.start();                            
                                                                  
                    isRecording = true;                           
                    mBtRecord.setText("Stop Recording");          
                } else {                                          
                    mRecorder.stop();
                             
                    isRecording = false;                          
                    mBtRecord.setText("Start Recording");         
                }                                                 
            }                                                     
        });                                                       

 

4. 재생 시작하기

재생 버튼에 대한 onClickListener 를 생성하고 재생을 위한 MediaPlayer 객체를 하나 생성합니다.

버튼이 한번 클릭 되었을 때, setDataSource/prepare/start 과정을 진행 해줍니다.

'MediaPlayer' 도 MediaRecorder 와 같이 State Diagram 을 참고하여 각 State 에 맞는 함수를 호출해 주어야 합니다.

한번 Stop 을 호출 하게 되면 stopped 상태가 되어 다시 started 상태로 가기 위해서는 prepare/start 함수를 순차적으로 호출해야 합니다.

=> 만약 현재 상태에서 부를 수 없는 함수를 호출하는 경우 ( 예를들어 stopped 상태에서 prepare를 호출하지 않고 start 부터 호출하는 경우)에는 IllegalStateException 이 발생하니 주의 해야 합니다.

 
    MediaPlayer mPlayer = null;
    boolean isPlaying = false;
    Button mBtPlay = null;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ..
        mBtPlay = (Button) findViewById(R.id.bt_play);
        mBtPlay.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (isPlaying == false) {
                    try {
                        mPlayer.setDataSource(mPath);
                        mPlayer.prepare();
                    }catch (Exception e) {
                        e.printStackTrace();
                    }
                    mPlayer.start();

                    isPlaying = true;
                    mBtPlay.setText("Stop Playing");
                }
                else {
                    mPlayer.stop();

                    isPlaying = false;
                    mBtPlay.setText("Start Playing");
                }
            }
        });

        mPlayer = new MediaPlayer();
        mPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
            @Override
            public void onCompletion(MediaPlayer mp) {
                isPlaying = false;
                mBtPlay.setText("Start Playing");
            }
        });
    }

MediaPlayer는 onCompletion callback method 를 제공해 주고 있어서, 파일 재생이 끝날 경우에 대한 처리를 편리하게 해 줄 수 있습니다.

파일 재생이 완료되면 isPlaying flag 를 false 로 설정 하고, 버튼의 Text 를 변경해 주는 일을 수행하도록 구현 하였습니다.

 

여기까지 진행하면 음성을 녹음하고 재생하는 코드가 완성 됩니다. 상세 코드는 'github 코드' 를 참고하세요.

현재는 음성 녹음 뿐이지만 다음 포스팅에서는 카메라로 들어오는 Video 컨텐츠도 함께 레코딩 하여 비디오 파일을 만들어 보도록 하겠습니다.

 

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. 라는 오류가 뜨는데 왜그럴까요... ㅠㅠ

+ Recent posts