1.使用AudioRecord录制原始音频
除了通过意图启动录音机和使用MediaRecorder之外,Android还提供了第三种方法来捕获音频:使用成为AudioRecord的类。AudioRecord是三种方法里最灵活的(因为允许访问原始音频流),但是它拥有的内置功能也是最少的,如不会自动压缩音频。
使用AudioRecord的基础知识非常简单。我们只需要构造一个AudioRecord类型的对象,并传入各种不同的配置参数。
需要指定的第一个值是音频源。下面使用的值与用于MediaRecorder的值相同,其在MediaRecorder.AudioSource中定义。实际上,这意味着可以使用MediaRecorder.AudioSource.MIC。
int audioSource=MediaRecorder.AudioSource.MIC;
需要指定的第二个值是 录制的采样率。以赫兹(Hz)为单位指定它。MediaRecorder的采样音频是8kHz,而CD质量的音频通常是44.1Hz。不同的安卓手机硬件能够以不同的采样率进行采样。而本示例则以11025Hz的采样率进行采样,这是另外一个常用的采样率。
int frequency = 11025;
接下来,需要指定的第三个值是 捕获的音频通道的数量。在AudioFormat类中指定了用于此参数的常量,而且可根据名称理解它们。
AudioFormat.CHANNEL_CONFIGURATION_MONO;//channelConfiguration AudioFormat.CHANNEL_CONFIGURATION_STEREO; AudioFormat.CHANNEL_CONFIGURATION_INVALID; AudioFormat.CHANNEL_CONFIGURATION_DEFAULT;
PCM代表脉冲编码调制,它实际上是原始的音频样本。16位将占用更多的空间和处理能力,但是表示的音频将更接近真实。
最后,需要指定的是第五个值是缓冲区大小。实际上可以查询AudioRecord类以获得最小缓冲区大小,查询方式是调用getMinBufferSize静态方法,同时传入采样率、通道配置以及音频格式。
int bufferSize = AudioTrack.getMinBufferSize(frequency,channelConfiguration, audioEncoding);
完成了以上步骤,我们就可以构造实际的AudioRecord对象。
AudioRecord audioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC, frequency, channelConfiguration, audioEncoding, bufferSize);
但值得注意的是,AudioRecord类实际上 并不保存捕获的音频,因此需要手动保存捕获的音频。可以首先利用以下方法来创建一个文件。
File path = new File( Environment.getExternalStorageDirectory().getAbsolutePath() + "/AudioRecorder/files/"); path.mkdirs(); try { recordingFile = File.createTempFile("recording", ".pcm", path); } catch (IOException e) { throw new RuntimeException("Couldn't create file on SD card", e); }
接下来再创建该文件对应的OutputStream输出流,尤其是出于 性能和便利的原因,可以将它包装在BufferedOutputStream和DataOutputStream中。
DataOutputStream dos = new DataOutputStream( new BufferedOutputStream(new FileOutputStream( recordingFile)));
现在就可以启动捕获,同时将音频样本写入到文件中。可以使用short数组来保存从AudioRecord对象读取而来的音频。同时, 将采用比AudioRecord对象的缓冲区更小的数组,从而在确保将音频读出来之前缓冲区没有被填满。
为了确保此数组小于缓冲区大小,需要将缓冲区大小除以4.因为缓冲区的大小以字节为单位,而每个short类型的数据占用2个字节,所以除以2并不足够。所以除以4就刚好使得该数组是AudioRecord对象内部缓冲区大小的一半了。
short[] audiodata = new short[bufferSize / 4];
至此,只需要调用AudioRecord对象上的startRecording方法即可。
录制开始之后,可以构造一个循环,不断从AudioRecorder对象读取音频并放入short数组中,同时写入对应文件的DataOutputStream
while (isRecording) { int bufferReadResult = audioRecord.read(buffer, 0,bufferSize); for (int i = 0; i < bufferReadResult; i++) { dos.writeShort(buffer[i]); } }
当想结束录音的时候,调用AudioRecord对象上的stop方法和DataOutputStream上的close方法。
2.使用AudioTrack来播放原始音频
AudioTrack允许播放AudioRecord捕获的原始音频类,而它们并不能使用MediaPlayer对象播放。
为了构造一个AudioTrack对象,需要传入以下一系列配置变量来描述待播放的音频
第一个参数是流类型。可能的值定义为AudioManager类的常量。例如
AudioManager.STREAM_MUSIC//正常播放音乐的音频流
第二个参数是播放音频数据的采样率,这里的参数需要和AudioRecord指定的参数一致。
第三个参数是通道配置。可能的值与构造AudioRecord的值相同。
第四个参数是音频格式。可能的值与构造AudioRecord的值相同。
第五个参数是将在对象中用于存储音频的缓冲区大小。为了确定使用最小的缓冲区大小,可以调用getMinBufferSize方法,同时传入采样率、通道配置和音频格式。
最后一个参数是模式。可能的值定义为AudioTrack类中的常量。
AudioTrack.MODE_STATIC://在播放发生之前将所有的音频数据转移到AudioTrack对象 AudioTrack.MODE_STREAM://在播放的同时将音频数据持续地转移到AudioTrack对象。
AudioTrack audioTrack = new AudioTrack( AudioManager.STREAM_MUSIC, frequency, channelConfiguration, audioEncoding, bufferSize, AudioTrack.MODE_STREAM);
构造了AudioTrack对象之后,就需要打开音频源,将音频数据读取到缓冲区中,并将它传递给AudioTrack对象。我们可以根据一个包含了正确格式的原始PCM数据的文件,构造DataInputStream。
DataInputStream dis = new DataInputStream( new BufferedInputStream(new FileInputStream( recordingFile)));
然后调用AudioTrack对象上的play方法,并开始从DataInputStream读取音频。
audioTrack.play(); while (isPlaying && dis.available() > 0) { int i = 0; while (dis.available() > 0 && i < audiodata.length) { audiodata[i] = dis.readShort(); i++; } audioTrack.write(audiodata, 0, audiodata.length); } dis.close();
下面是一个完整的示例,通过使用异步任务,每个操作都在它们各自的线程中完成,所以并不会阻碍UI线程。
public class AltAudioRecorder extends Activity implements OnClickListener { RecordAudio recordTask; PlayAudio playTask; Button startRecordingButton, stopRecordingButton, startPlaybackButton, stopPlaybackButton; TextView statusText; File recordingFile; boolean isRecording = false; boolean isPlaying = false; int frequency = 11025; int channelConfiguration = AudioFormat.CHANNEL_CONFIGURATION_MONO; int audioEncoding = AudioFormat.ENCODING_PCM_16BIT; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); statusText = (TextView) this.findViewById(R.id.StatusTextView); startRecordingButton = (Button) this .findViewById(R.id.StartRecordingButton); stopRecordingButton = (Button) this .findViewById(R.id.StopRecordingButton); startPlaybackButton = (Button) this .findViewById(R.id.StartPlaybackButton); stopPlaybackButton = (Button) this .findViewById(R.id.StopPlaybackButton); startRecordingButton.setOnClickListener(this); stopRecordingButton.setOnClickListener(this); startPlaybackButton.setOnClickListener(this); stopPlaybackButton.setOnClickListener(this); stopRecordingButton.setEnabled(false); startPlaybackButton.setEnabled(false); stopPlaybackButton.setEnabled(false); File path = new File( Environment.getExternalStorageDirectory().getAbsolutePath() + "/Android/data/AudioRecorder/files/"); path.mkdirs(); try { recordingFile = File.createTempFile("recording", ".pcm", path); } catch (IOException e) { throw new RuntimeException("Couldn't create file on SD card", e); } } public void onClick(View v) { if (v == startRecordingButton) { record(); } else if (v == stopRecordingButton) { stopRecording(); } else if (v == startPlaybackButton) { play(); } else if (v == stopPlaybackButton) { stopPlaying(); } } public void play() { startPlaybackButton.setEnabled(true); playTask = new PlayAudio(); playTask.execute(); stopPlaybackButton.setEnabled(true); } public void stopPlaying() { isPlaying = false; stopPlaybackButton.setEnabled(false); startPlaybackButton.setEnabled(true); } public void record() { startRecordingButton.setEnabled(false); stopRecordingButton.setEnabled(true); // For Fun startPlaybackButton.setEnabled(true); recordTask = new RecordAudio(); recordTask.execute(); } public void stopRecording() { isRecording = false; } private class PlayAudio extends AsyncTask<Void, Integer, Void> { @Override protected Void doInBackground(Void... params) { isPlaying = true; int bufferSize = AudioTrack.getMinBufferSize(frequency, channelConfiguration, audioEncoding); short[] audiodata = new short[bufferSize / 4]; try { DataInputStream dis = new DataInputStream( new BufferedInputStream(new FileInputStream( recordingFile))); AudioTrack audioTrack = new AudioTrack( AudioManager.STREAM_MUSIC, frequency, channelConfiguration, audioEncoding, bufferSize, AudioTrack.MODE_STREAM); audioTrack.play(); while (isPlaying && dis.available() > 0) { int i = 0; while (dis.available() > 0 && i < audiodata.length) { audiodata[i] = dis.readShort(); i++; } audioTrack.write(audiodata, 0, audiodata.length); } dis.close(); startPlaybackButton.setEnabled(false); stopPlaybackButton.setEnabled(true); } catch (Throwable t) { Log.e("AudioTrack", "Playback Failed"); } return null; } } private class RecordAudio extends AsyncTask<Void, Integer, Void> { @Override protected Void doInBackground(Void... params) { isRecording = true; try { DataOutputStream dos = new DataOutputStream( new BufferedOutputStream(new FileOutputStream( recordingFile))); int bufferSize = AudioRecord.getMinBufferSize(frequency, channelConfiguration, audioEncoding); AudioRecord audioRecord = new AudioRecord( MediaRecorder.AudioSource.MIC, frequency, channelConfiguration, audioEncoding, bufferSize); short[] buffer = new short[bufferSize]; audioRecord.startRecording(); int r = 0; while (isRecording) { int bufferReadResult = audioRecord.read(buffer, 0, bufferSize); for (int i = 0; i < bufferReadResult; i++) { dos.writeShort(buffer[i]); } publishProgress(new Integer(r)); r++; } audioRecord.stop(); dos.close(); } catch (Throwable t) { Log.e("AudioRecord", "Recording Failed"); } return null; } protected void onProgressUpdate(Integer... progress) { statusText.setText(progress[0].toString()); } protected void onPostExecute(Void result) { startRecordingButton.setEnabled(true); stopRecordingButton.setEnabled(false); startPlaybackButton.setEnabled(true); } } }