前几篇的文章都是camera下采集视频数据进行显示,保存下来的文件也是h264格式的,并没有包含音频数据,所以多多少少有点单调的感觉。没有声音的视频是没有灵魂的,所以最近了解了一下音频相关的开发,给视频注入灵魂。
1. 基础知识
开始音频学习之前,有必要先了解一下基础知识,因为在音频开发过程中,经常会涉及到这些。掌握了这些重要的概念,在学习中很多参数的配置会更容易理解。
- PCM编码格式
首先看看百度百科给出的解释:PCM 脉冲编码调制是Pulse Code Modulation的缩写。脉冲编码调制是数字通信的编码方式之一。主要过程是将话音、图像等模拟信号每隔一定时间进行取样,使其离散化,同时将抽样值按分层单位四舍五入取整量化,同时将抽样值按一组二进制码来表示抽样脉冲的幅值。 这个解释得非常抽象,反正我是没看懂⊙﹏⊙。简单的来说就是将声音数字化,转换为二进制序列,这样就可以把声音保存下来了,而保存它的容器可以是mp3,wav等等容器。 - 音频采集输入源
这个就相当于声明孩子他妈是谁,也就是说声音的源头在哪儿。可选的类型以常量的形式定义在 MediaRecorder.AudioSource 类中 ,比较常用的是下面几个
- MediaRecorder.AudioSource.CAMCORDER 设定录音来源于同方向的相机麦克风相同,若相机无内置相机或无法识别,则使用预设的麦克风
- MediaRecorder.AudioSource.DEFAULT 默认音频源
- MediaRecorder.AudioSource.MIC 设定录音来源为主麦克风
- MediaRecorder.AudioSource.VOICE_CALL设定录音来源为语音拨出的语音与对方说话的声音
- MediaRecorder.AudioSource.VOICE_COMMUNICATION 摄像头旁边的麦克风
- MediaRecorder.AudioSource.VOICE_RECOGNITION 语音识别
- 采样率
我们把采样到的一个个静止画面再以采样率同样的速度回放时,看到的就是连续的画面。同样的道理,把以44.1kHZ采样率记录的CD以同样的速率播放时,就能听到连续的声音。显然,这个采样率越高,听到的声音和看到的图像就越连贯。当然,人的听觉和视觉器官能分辨的采样率是有限的,基本上高于44.1kHZ采样的声音,绝大部分人已经觉察不到其中的分别了。 而目前44100Hz是唯一可以保证兼容所有Android手机的采样率 。所以,如果不是特殊设备和用途,这个值建议设置为44100。 - 通道数
声道数一般表示声音录制时的音源数量或回放时相应的扬声器数量。常用的有:单通道和双通道。 可选的值以常量的形式定义在 AudioFormat 类中,常用的是 CHANNEL_IN_MONO(单通道),CHANNEL_IN_STEREO(双通道) - 量化精度
- 声音的位数就相当于画面的颜色数,表示每个取样的数据量,当然数据量越大,回放的声音越准确,不至于把开水壶的叫声和火车的鸣笛混淆。同样的道理,对于画面来说就是更清晰和准确,不至于把血和西红柿酱混淆。不过受人的器官的机能限制,16位的声音和24位的画面基本已经是普通人类的极限了,电话就是3kHZ取样的7位声音,而CD是44.1kHZ取样的16位声音,所以CD就比电话更清楚。
- 对于一个采样点,需要用二进制数字来表示,这个二进制的精度可以是:4bit、8bit、16bit、32bit。 位数越多,表示的声音就越精细,声音的质量就越好。不过数据量也会变大。
- 帧间隔
音频不像视频那样,有一帧一帧的概念。它是约定一个时间为单位,然后这个时间内的数据为一帧,这个时间被称为采样时间。这个时间没有特别的标准,要看具体的编解码器。
2. 音频录制
了解音频开发相关基础知识之后,我们就可以开始使用android提供的相关API实现音频的录制了,android提供了两套音频录制的API:
-
MediaRecorder
:比较上层的API
,它可以直接把手机麦克风的音频数据进行编码然后储存成文件。使用简单,但是支持的格式有限,并且不支持对音频进行进一步的处理,例如变声、混音等。 -
AudioRecord
:比较底层的一个API
,能够得到原始的PCM
音频数据。由于我们得到的是原始的PCM
数据,我们可以对音频进行进一步的处理,例如编码、混音和变声等。
这里主要介绍的是使用AudioRecord
进行录制,MediaRecorder
的录制比较简单,就不做过多介绍了。首先看看AudioRecord的构造函数:
public AudioRecord(int audioSource, int sampleRateInHz, int channelConfig, int audioFormat, int bufferSizeInBytes)
复制代码
需要传入5个参数,分别是输入源,采样率,通道数,量化精度,音频缓冲区的大小 。其中最后一个也是最重要的一个参数,它代表音频缓冲区的大小,该缓冲区的值不能低于一帧音频帧的大小,
一帧音频帧大小 = 采样率 x 位宽 x 采样时间 x 通道数 ,这个值不用我们自己计算,AudioRecord 类提供了一个帮助你确定这个值的函数 :
public static int getMinBufferSize(int sampleRateInHz, int channelConfig, int audioFormat)
复制代码
当创建好AudioRecord对象之后,就可以开始进行音频数据的采集了,控制采集的开始和停止的方法是下面这两个函数:
public void startRecording()
public void stop()
复制代码
开始采集之后,通过线程循环取走音频数据:
public int read(byte[] audioData, int offsetInBytes, int sizeInBytes)
复制代码
最终录制的代码如下,注意该方法需要在子线程中运行:
private File mAudioFile;
private FileOutputStream mAudioFileOutput;
private boolean isRecording = false;
private int sampleRate = 44100;//所有android系统都支持 采样率
//单声道输入
private int channelConfig = AudioFormat.CHANNEL_IN_MONO;
//PCM_16是所有android系统都支持的 16位的声音就是人类能听到的极限了,再高就听不见了 位数越高声音越清晰
private int autioFormat = AudioFormat.ENCODING_PCM_16BIT;
private int recordBufSize = 0; // 声明recoordBufffer的大小字段
private AudioRecord audioRecord = null;
private boolean startAudioRecord(String fileName) {
isRecording = true;
mAudioFile = new File(mPath+fileName+System.currentTimeMillis()+".pcm");
if (!mAudioFile.getParentFile().exists()){
mAudioFile.getParentFile().mkdirs();
}
try {
mAudioFile.createNewFile();
//创建文件输出流
mAudioFileOutput = new FileOutputStream(mAudioFile);
//计算audioRecord能接受的最小的buffer大小
recordBufSize = AudioRecord.getMinBufferSize(sampleRate,
channelConfig,
autioFormat);
Log.e(TAG, "最小的buffer大小: " + recordBufSize);
audioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC,
sampleRate,
channelConfig,
autioFormat, recordBufSize);
//初始化一个buffer 用于从AudioRecord中读取声音数据到文件流
byte data[] = new byte[recordBufSize];
//开始录音
audioRecord.startRecording();
while (isRecording){
//只要还在录音就一直读取
int read = audioRecord.read(data,0,recordBufSize);
if (read > 0){
mAudioFileOutput.write(data,0,read);
}
}
//stopRecorder();
} catch (IOException e) {
e.printStackTrace();
stopAudioRecord();
return false;
}
return true;
}
public boolean stopAudioRecord(){
isRecording = false;
if (audioRecord != null){
audioRecord.stop();
audioRecord.release();
audioRecord = null;
}
try {
mAudioFileOutput.flush();
mAudioFileOutput.close();
} catch (IOException e) {
e.printStackTrace();
return false;
}
return true;
}
复制代码
3. 音频播放
播放pcm格式的音频和录制不同的地方有三个:
一个是创建AudioTrack对象,和AudioRecord的构造方法不同,第一个参数是音乐类型,AudioManager.STREAM_MUSIC表示用扬声器播放,最后多出的一个参数是播放模式,一般使用AudioTrack.MODE_STREAM,适用于大多数的场景,将audio buffers从java层传递到native层即返回。 如果audio buffers占用内存多,应该使用MODE_STREAM。 比如播放时间很长的声音文件, 比如音频文件使用高采样率等:
public AudioTrack(int streamType, int sampleRateInHz, int channelConfig, int audioFormat, int bufferSizeInBytes, int mode)
复制代码
另外两个是把录制的startRecording()和read()方法换成下面两个:
public void play()
public int write(byte[] audioData, int offsetInBytes, int sizeInBytes)
复制代码
最终播放pcm的代码如下:
private FileInputStream mAudioPlayInputStream;
public void doPlay(File audioFile) {
if (audioFile.isDirectory() || !audioFile.exists()) return;
mIsPlaying = true;
//配置播放器
//音乐类型,扬声器播放
int streamType= AudioManager.STREAM_MUSIC;
//录音时采用的采样频率,所以播放时同样的采样频率
int sampleRate=44100;
//单声道,和录音时设置的一样
int channelConfig=AudioFormat.CHANNEL_OUT_MONO;
//录音时使用16bit,所以播放时同样采用该方式
int audioFormat=AudioFormat.ENCODING_PCM_16BIT;
//流模式
int mode= AudioTrack.MODE_STREAM;
//计算最小buffer大小
int minBufferSize = AudioTrack.getMinBufferSize(sampleRate,channelConfig,audioFormat);
byte data[] = new byte[minBufferSize];
//构造AudioTrack 不能小于AudioTrack的最低要求,也不能小于我们每次读的大小
mAudioTrack = new AudioTrack(streamType,sampleRate,channelConfig,audioFormat,
Math.max(minBufferSize,data.length),mode);
//从文件流读数据
try{
//循环读数据,写到播放器去播放
mAudioPlayInputStream = new FileInputStream(audioFile);
//循环读数据,写到播放器去播放
int read;
//只要没读完,循环播放
mAudioTrack.play();
while (mIsPlaying){
int ret = 0;
read = mAudioPlayInputStream.read(data);
if (read > 0){
ret = mAudioTrack.write(data,0,read);
}
//mAudioFileOutput.write(data,0,read);
//检查write的返回值,处理错误
switch (ret){
case AudioTrack.ERROR_INVALID_OPERATION:
case AudioTrack.ERROR_BAD_VALUE:
case AudioManager.ERROR_DEAD_OBJECT:
Log.d(TAG, "doPlay: 失败,错误码:"+ret);
return;
default:
break;
}
}
}catch (Exception e){
e.printStackTrace();
//读取失败
Log.d(TAG, "doPlay: 失败");
}finally {
stopPlay();
Log.d(TAG, "结束播放");
}
}
public void stopPlay() {
mIsPlaying = false;
//播放器释放
if (mAudioTrack != null) {
mAudioTrack.stop();
mAudioTrack.release();
mAudioTrack = null;
}
//关闭文件输入流
if (mAudioPlayInputStream != null) {
try {
mAudioPlayInputStream.close();
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
复制代码
最后贴上所有的代码:
public class AudioUtil {
private static final String TAG = "AudioUtil";
private AudioRecord audioRecord = null; // 声明 AudioRecord 对象
private int recordBufSize = 0; // 声明recoordBufffer的大小字段
//所有android系统都支持 采样率:采样率越高,听到的声音和看到的图像就越连贯
// 基本上高于44.1kHZ采样的声音,绝大部分人已经觉察不到其中的分别了
private int sampleRate = 44100;
//单声道输入
private int channelConfig = AudioFormat.CHANNEL_IN_MONO;
//PCM_16是所有android系统都支持的 16位的声音就是人类能听到的极限了,再高就听不见了 位数越高声音越清晰
private int autioFormat = AudioFormat.ENCODING_PCM_16BIT;
private long mStartTimeStamp;
private File mAudioFile;
private String mPath = ContentValue.MAIN_PATH + "/AudioSimple/";
private FileOutputStream mAudioFileOutput; //存储录音文件
private FileInputStream mAudioPlayInputStream; //播放录音文件
private boolean isRecording = false;
private static AudioUtil audioUtil;
private boolean mIsPlaying;
private AudioTrack mAudioTrack;
private String mRecordFileName; //录音保存的文件路径
private String mPlayFileName; //播放录音的文件路径
private Runnable mAudioRunnableTask = new Runnable() {
@Override
public void run() {
boolean result = startAudioRecord(mRecordFileName);
if (result){
Log.e(TAG, "录音结束");
}else {
Log.e(TAG, "录音失败");
}
}
};
private Runnable mAudioPlayRunnableTask = new Runnable() {
@Override
public void run() {
File file = new File(mPlayFileName);
doPlay(file);
}
};
private AudioUtil(){}
public static AudioUtil getInstance(){
if (audioUtil == null){
synchronized (AudioUtil.class){
if (audioUtil == null){
audioUtil = new AudioUtil();
}
}
}
return audioUtil;
}
private AudioEncoder mAudioEncoder;
private boolean startAudioRecord(String fileName) {
isRecording = true;
mStartTimeStamp = System.currentTimeMillis();
mAudioFile = new File(mPath+fileName+mStartTimeStamp+".pcm");
if (!mAudioFile.getParentFile().exists()){
mAudioFile.getParentFile().mkdirs();
}
try {
mAudioFile.createNewFile();
//创建文件输出流
mAudioFileOutput = new FileOutputStream(mAudioFile);
//计算audioRecord能接受的最小的buffer大小
recordBufSize = AudioRecord.getMinBufferSize(sampleRate,
channelConfig,
autioFormat);
Log.e(TAG, "最小的buffer大小: " + recordBufSize);
audioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC,
sampleRate,
channelConfig,
autioFormat, recordBufSize);
//初始化一个buffer 用于从AudioRecord中读取声音数据到文件流
byte data[] = new byte[recordBufSize];
//开始录音
audioRecord.startRecording();
while (isRecording){
//只要还在录音就一直读取
int read = audioRecord.read(data,0,recordBufSize);
if (read > 0){
mAudioFileOutput.write(data,0,read);
}
}
//stopRecorder();
} catch (IOException e) {
e.printStackTrace();
stopAudioRecord();
return false;
}
return true;
}
public void doPlay(File audioFile) {
if(audioFile !=null){
mIsPlaying = true;
//配置播放器
//音乐类型,扬声器播放
int streamType= AudioManager.STREAM_MUSIC;
//录音时采用的采样频率,所以播放时同样的采样频率
int sampleRate=44100;
//单声道,和录音时设置的一样
int channelConfig=AudioFormat.CHANNEL_OUT_MONO;
//录音时使用16bit,所以播放时同样采用该方式
int audioFormat=AudioFormat.ENCODING_PCM_16BIT;
//流模式
int mode= AudioTrack.MODE_STREAM;
//计算最小buffer大小
int minBufferSize = AudioTrack.getMinBufferSize(sampleRate,channelConfig,audioFormat);
byte data[] = new byte[minBufferSize];
//构造AudioTrack 不能小于AudioTrack的最低要求,也不能小于我们每次读的大小
mAudioTrack = new AudioTrack(streamType,sampleRate,channelConfig,audioFormat,
Math.max(minBufferSize,data.length),mode);
//从文件流读数据
try{
//循环读数据,写到播放器去播放
mAudioPlayInputStream = new FileInputStream(audioFile);
//循环读数据,写到播放器去播放
int read;
//只要没读完,循环播放
mAudioTrack.play();
while (mIsPlaying){
int ret = 0;
read = mAudioPlayInputStream.read(data);
if (read > 0){
ret = mAudioTrack.write(data,0,read);
}
//mAudioFileOutput.write(data,0,read);
//检查write的返回值,处理错误
switch (ret){
case AudioTrack.ERROR_INVALID_OPERATION:
case AudioTrack.ERROR_BAD_VALUE:
case AudioManager.ERROR_DEAD_OBJECT:
Log.d(TAG, "doPlay: 失败,错误码:"+ret);
return;
default:
break;
}
}
}catch (Exception e){
e.printStackTrace();
//读取失败
Log.d(TAG, "doPlay: 失败");
}finally {
stopPlay();
Log.d(TAG, "结束播放");
}
}
}
public void startRecord(String fileName){
this.mRecordFileName = fileName;
new Thread(mAudioRunnableTask).start();
}
public void startPlay(String fileName){
this.mPlayFileName = fileName;
new Thread(mAudioPlayRunnableTask).start();
}
public boolean stopAudioRecord(){
isRecording = false;
if (audioRecord != null){
audioRecord.stop();
audioRecord.release();
audioRecord = null;
}
if (mAudioEncoder != null){
mAudioEncoder.stopEncodeAac();
}
try {
mAudioFileOutput.flush();
mAudioFileOutput.close();
} catch (IOException e) {
e.printStackTrace();
return false;
}
return true;
}
public boolean isRecording(){
return isRecording;
}
public boolean isPlaying(){
return mIsPlaying;
}
public void stopPlay(){
mIsPlaying = false;
//播放器释放
if(mAudioTrack != null){
mAudioTrack.stop();
mAudioTrack.release();
mAudioTrack = null;
}
//关闭文件输入流
if(mAudioPlayInputStream !=null){
try {
mAudioPlayInputStream.close();
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
private AudioListener mAudioListener;
public void setAudioListener(AudioListener listener){
this.mAudioListener = listener;
}
public interface AudioListener{
void onRecordFinish();
void onPlayFinish();
}
}
复制代码