AudioRecord
关于音频不仅有AudioRecord,还有一个MediaRecorder。MediaRecorder基于文件录音,系统已实现大量的封装,集成了录音、编码、压缩等,支持少量的音频格式文件,操作起来更加简单,而AudioRecord基于字节流录音,AudioTrack更接近底层,使用起来更加灵活,能实现更多的功能。对比如下:
AudioRecord
优点:可以实现语音的实时处理,进行边录边播,对音频的实时处理,AudioTrack 更接近底层。
缺点:输出的是PCM的语音数据,如果保存成音频文件是不能被播放器播放的,需要用 AudioTrack 来播放。API还有待完善,常见的暂停功能都不支持。
MediaRecorder
优点:系统封装的完整,直接调用即可,操作简单,录制的音频文件可以用系统自带的播放器播放。
缺点:无法实现实时处理音频,输出的音频格式少。录制的音频文件是经过压缩后的,需要设置编码器。
注:本篇中用AudioTrack来播放pcm文件。
名词解释
- 采样率:采样率即采样频率,指每秒钟取得声音样本的次数,采样频率越高,能表现的频率范围就越大,音质就会越好,声音的还原度也更真实,但此同时带来的弊端是占有的内存资源也会越大。因为人耳的分辨率有限,并不是频率越高越好,44KHz已相当于CD音质了,目前的常用采样频率都不超过48KHz。
-声音采集: 麦克风的原理是将将采集的声音转换为模拟电信号,之后将模拟电信号数字化,也就是用高低电平表示的信号(计算机能识别的信号),在Android中有一个AudioRecord类就能录制语音,并将语音转换为PCM数据,声音在经过麦克风转换为模拟电信号并最终又转换为PCM数据,在转换为PCM数据时就要依赖于三个参数,分别是:声道数、采样位数和采样频率。 - 采样位深:位深度也叫采样位深,音频的位深度决定动态范围,它是用来衡量声音波动变化的一个参数,也可以说是声卡的分辨率。它的数值越大,分辨率也就越高,所发出声音的能力越强。在计算机中采样位数一般有8位和16位之分,即分成2的8次方和2的16次方之分,PCM 16位每个样本,保证设备支持。PCM 8位每个样本,不一定能得到设备支持。
-声道数: 很好理解,有单声道和立体声之分,单声道的声音只能使用一个喇叭发声(有的也处理成两个喇叭输出同一个声道的声音),立体声的PCM可以使两个喇叭都发声(一般左右声道有分工) ,更能感受到空间效果 - 构造函数:
public AudioRecord(int audioSource, int sampleRateInHz, int channelConfig, int audioFormat, int bufferSizeInBytes)
audioSource:录音源,指定声音是从哪里录制的
sampleRateInHz:采样率
channelConfig:声道数
audioFormat:采样位深
bufferSizeInBytes:最小缓冲大小,可以通过 getMinBufferSize 获取。
补充:
存储量= 采样率 * 采样时间 * 采样位深 / 8 * 声道数(Bytes)。以采样率为44.1kHZ、采样位深为16位、双声道计算,一分钟消耗的内存为10.335M。
AudioRecord实现流程
一、权限
<!--录音-->
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<!--读取SD卡-->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
二、开始录音
//开始录音
public void StartRecord() {
Log.i(TAG,"开始录音");
//16K采集率
int frequency = 16000;
//格式
int channelConfiguration = AudioFormat.CHANNEL_CONFIGURATION_MONO;
//16Bit
int audioEncoding = AudioFormat.ENCODING_PCM_16BIT;
//生成PCM文件
file = new File(Environment.getExternalStorageDirectory().getAbsolutePath() + "/reverseme.pcm");
Log.i(TAG,"生成文件");
//如果存在,就先删除再创建
if (file.exists())
file.delete();
Log.i(TAG,"删除文件");
try {
file.createNewFile();
Log.i(TAG,"创建文件");
} catch (IOException e) {
Log.i(TAG,"未能创建");
throw new IllegalStateException("未能创建" + file.toString());
}
try {
//输出流
OutputStream os = new FileOutputStream(file);
BufferedOutputStream bos = new BufferedOutputStream(os);
DataOutputStream dos = new DataOutputStream(bos);
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();
Log.i(TAG, "开始录音");
isRecording = true;
while (isRecording) {
int bufferReadResult = audioRecord.read(buffer, 0, bufferSize);
for (int i = 0; i < bufferReadResult; i++) {
dos.writeShort(buffer[i]);
}
}
audioRecord.stop();
dos.close();
} catch (Throwable t) {
Log.e(TAG, "录音失败");
}
}
这里用一个isRecording变量去记录开始录音,而且,录音不能放在主线程中。
三、点击事件
case R.id.startAudio:
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
StartRecord();
Log.e(TAG,"start");
}
});
thread.start();
printLog("开始录音");
ButtonEnabled(false, true, false);
break;
为了防止ANR,控制一下按钮的焦点
//获取/失去焦点
private void ButtonEnabled(boolean start, boolean stop, boolean play) {
startAudio.setEnabled(start);
stopAudio.setEnabled(stop);
playAudio.setEnabled(play);
}
三、停止录音
case R.id.stopAudio:
isRecording = false;
ButtonEnabled(true, false, true);
printLog("停止录音");
break;
把isRecording置为false即可
四、播放音频
//播放文件
public void PlayRecord() {
if(file == null){
return;
}
//读取文件
int musicLength = (int) (file.length() / 2);
short[] music = new short[musicLength];
try {
InputStream is = new FileInputStream(file);
BufferedInputStream bis = new BufferedInputStream(is);
DataInputStream dis = new DataInputStream(bis);
int i = 0;
while (dis.available() > 0) {
music[i] = dis.readShort();
i++;
}
dis.close();
AudioTrack audioTrack = new AudioTrack(AudioManager.STREAM_MUSIC,
16000, AudioFormat.CHANNEL_CONFIGURATION_MONO,
AudioFormat.ENCODING_PCM_16BIT,
musicLength * 2,
AudioTrack.MODE_STREAM);
audioTrack.play();
audioTrack.write(music, 0, musicLength);
audioTrack.stop();
} catch (Throwable t) {
Log.e(TAG, "播放失败");
}
}
这里就是利用了AudioTrack来播放pcm文件的。
五、删除音频
//删除文件
private void deleFile() {
if(file == null){
return;
}
file.delete();
printLog("文件删除成功");
}
点击停止录音后,会生成pcm文件,我们播放音频,删除音频都是对pcm文件进行操作。
完整代码
package com.example.audiorecorder;
import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioRecord;
import android.media.AudioTrack;
import android.media.MediaRecorder;
import android.os.Bundle;
import android.os.Environment;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.ScrollView;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
public static final String TAG = "PCMSample";
//是否在录制
private boolean isRecording = false;
//开始录音
private Button startAudio;
//结束录音
private Button stopAudio;
//播放录音
private Button playAudio;
//删除文件
private Button deleteAudio;
private ScrollView mScrollView;
private TextView tv_audio_succeess;
//pcm文件
private File file;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initView();
}
//初始化View
private void initView() {
mScrollView = (ScrollView) findViewById(R.id.mScrollView);
tv_audio_succeess = (TextView) findViewById(R.id.tv_audio_succeess);
printLog("初始化成功");
startAudio = (Button) findViewById(R.id.startAudio);
startAudio.setOnClickListener(this);
stopAudio = (Button) findViewById(R.id.stopAudio);
stopAudio.setOnClickListener(this);
playAudio = (Button) findViewById(R.id.playAudio);
playAudio.setOnClickListener(this);
deleteAudio = (Button) findViewById(R.id.deleteAudio);
deleteAudio.setOnClickListener(this);
}
//点击事件
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.startAudio:
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
StartRecord();
Log.e(TAG,"start");
}
});
thread.start();
printLog("开始录音");
ButtonEnabled(false, true, false);
break;
case R.id.stopAudio:
isRecording = false;
ButtonEnabled(true, false, true);
printLog("停止录音");
break;
case R.id.playAudio:
PlayRecord();
ButtonEnabled(true, false, false);
printLog("播放录音");
break;
case R.id.deleteAudio:
deleFile();
break;
}
}
//打印log
private void printLog(final String resultString) {
tv_audio_succeess.post(new Runnable() {
@Override
public void run() {
tv_audio_succeess.append(resultString + "\n");
mScrollView.fullScroll(ScrollView.FOCUS_DOWN);
}
});
}
//获取/失去焦点
private void ButtonEnabled(boolean start, boolean stop, boolean play) {
startAudio.setEnabled(start);
stopAudio.setEnabled(stop);
playAudio.setEnabled(play);
}
//开始录音
public void StartRecord() {
Log.i(TAG,"开始录音");
//16K采集率
int frequency = 16000;
//格式
int channelConfiguration = AudioFormat.CHANNEL_CONFIGURATION_MONO;
//16Bit
int audioEncoding = AudioFormat.ENCODING_PCM_16BIT;
//生成PCM文件
file = new File(Environment.getExternalStorageDirectory().getAbsolutePath() + "/reverseme.pcm");
Log.i(TAG,"生成文件");
//如果存在,就先删除再创建
if (file.exists())
file.delete();
Log.i(TAG,"删除文件");
try {
file.createNewFile();
Log.i(TAG,"创建文件");
} catch (IOException e) {
Log.i(TAG,"未能创建");
throw new IllegalStateException("未能创建" + file.toString());
}
try {
//输出流
OutputStream os = new FileOutputStream(file);
BufferedOutputStream bos = new BufferedOutputStream(os);
DataOutputStream dos = new DataOutputStream(bos);
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();
Log.i(TAG, "开始录音");
isRecording = true;
while (isRecording) {
int bufferReadResult = audioRecord.read(buffer, 0, bufferSize);
for (int i = 0; i < bufferReadResult; i++) {
dos.writeShort(buffer[i]);
}
}
audioRecord.stop();
dos.close();
} catch (Throwable t) {
Log.e(TAG, "录音失败");
}
}
//播放文件
public void PlayRecord() {
if(file == null){
return;
}
//读取文件
int musicLength = (int) (file.length() / 2);
short[] music = new short[musicLength];
try {
InputStream is = new FileInputStream(file);
BufferedInputStream bis = new BufferedInputStream(is);
DataInputStream dis = new DataInputStream(bis);
int i = 0;
while (dis.available() > 0) {
music[i] = dis.readShort();
i++;
}
dis.close();
AudioTrack audioTrack = new AudioTrack(AudioManager.STREAM_MUSIC,
16000, AudioFormat.CHANNEL_CONFIGURATION_MONO,
AudioFormat.ENCODING_PCM_16BIT,
musicLength * 2,
AudioTrack.MODE_STREAM);
audioTrack.play();
audioTrack.write(music, 0, musicLength);
audioTrack.stop();
} catch (Throwable t) {
Log.e(TAG, "播放失败");
}
}
//删除文件
private void deleFile() {
if(file == null){
return;
}
file.delete();
printLog("文件删除成功");
}
}