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 가 완성 되었습니다. ^^
전체 코드 첨부하니 필요하신 분들은 참고하세요.
'Android > 기본 개념' 카테고리의 다른 글
View.post() 사용하기 (0) | 2017.12.30 |
---|---|
MediaRecorder API 를 사용한 레코딩 예제 (0) | 2017.12.27 |
MediaRecorder API 를 사용한 레코딩 방법 (0) | 2017.12.20 |
runOnUiThread 사용하기 (0) | 2017.12.19 |
안드로이드 메인 스레드(Android Main Thread 또는 UI Thread) (1) | 2017.12.08 |