PCM(Pulse Code Modulation),脉冲编码调制。人耳听到的是模拟信号,PCM是把声音从模拟信号转化为数字信号的技术。原理是用一个固定的频率对模拟信号进行采样,采样后的信号在波形上看就像一串连续的幅值不一的脉冲(脉搏似的短暂起伏的电冲击),把这些脉冲的幅值按一定精度进行量化,这些量化后的数值被连续的输出、传输、处理或记录到存储介质中,所有这些组成了数字音频的产生过程(抽样、量化、编码三个过程)。
播放音乐时,应用程序从存储介质中读取音频数据(MP3、WMA、AAC…),经过解码后,最终送到音频驱动程序中的就是PCM数据,反过来,在录音时,音频驱动不停地把采样所得的PCM数据送回给应用程序,由应用程序完成压缩、存储等任务。所以,音频驱动的两大核心任务就是:
playback:如何把用户空间的应用程序发过来的PCM数据,转化为人耳可以辨别的模拟音频
capture:把mic拾取到得模拟信号,经过采样、量化,转换为PCM信号送回给用户空间的应用程序。
WAV,MP3等是我们比较常见的音频格式,不同的编码格式对原始音频采用的编码方式也是不同的,通常为了方便传输等问题,会对原始音频进行压缩,同时为了能够使得播放器能够识别该种格式,所以在每种格式的头文件都是特定的,有一定的规则,来让播放器识别出是该种格式,然后按着相应的解码算法去播放后面的音频文件。
wav文件分为两个部分,第一个部分是wav头文件,第二个部分是PCM编码的音频数据部分。
1. AndioRecord介绍
AndioRecord类的主要功能是让各种JAVA应用能够管理音频资源,以便它们通过此类能够录制声音相关的硬件所收集的声音。此功能的实现就是通过”pulling”(读取)AudioRecord对象的声音数据来完成的。在录音过程中,应用所需要做的就是通过后面三个类方法中的一个去及时地获取AudioRecord对象的录音数据. AudioRecord类提供的三个获取声音数据的方法分别是read(byte[], int, int), read(short[], int, int), read(ByteBuffer, int). 无论选择使用那一个方法都必须事先设定方便用户的声音数据的存储格式。
代码如下
//录音对象
private AudioRecord audioRecord;
public void createAudio(String fileName, int audioSource, int sampleRateInHz, int channelConfig, int audioFormat) {
// 获得缓冲区字节大小
bufferSizeInBytes = AudioRecord.getMinBufferSize(sampleRateInHz,
channelConfig, channelConfig);
audioRecord = new AudioRecord(audioSource, sampleRateInHz, channelConfig, audioFormat, bufferSizeInBytes);
//this.fileName = fileName;
}
参数解释一下
audioSource:数据采集源,当然是麦克风了
sampleRateInHz:采样频率,设备一秒钟内对模拟信号的采样次数,在主流的采集卡上分为
(8Khz的电话采样率就可以达到人的对话程度)
22.05KHz:无线电广播;
44.1KHz:音频 CD,MP3等;
48KHz:miniDV、数字电视、DVD、电影和专业音频。
人耳能够感觉到的最高频率为20kHz,要满足人耳的听觉要求,则需要每秒进行40k次采样,即40kHz。我们常见的CD采样率为44.1kHz。(也比如我对接过硬件的扬声器最高是16KHz,则手机MIC采样率就不用太高,节省传输带宽)
channelConfig:声音在录制或播放时在不同空间位置采集或回放的相互独立的音频信号。
单声道 采样数据为8位的短整数(short);
双声道 采样数据为16位的整数,(int),高八位(左声道)和低八位(右声道)分别代表两个声道。
audioFormat:编码制式和采样大小:采集来的数据当然使用PCM编码(脉冲代码调制编码,即PCM编码。PCM通过抽样、量化、编码三个步骤将连续变化的模拟信号转换为数字编码。) android支持的采样大小16bit 或者8bit。当然采样大小越大,那么信息量越多,音质也越高,现在主流的采样大小都是16bit,在低质量的语音传输的时候8bit足够了。
2. 主要源码
public class AudioRecorder {
private static AudioRecorder audioRecorder;
//音频输入-麦克风
private final static int AUDIO_INPUT = MediaRecorder.AudioSource.MIC;
//采用频率
//44100是目前的标准,但是某些设备仍然支持22050,16000,11025
//采样频率一般共分为22.05KHz、44.1KHz、48KHz三个等级
private final static int AUDIO_SAMPLE_RATE = 16000;
//声道 单声道
private final static int AUDIO_CHANNEL = AudioFormat.CHANNEL_IN_MONO;
//编码
private final static int AUDIO_ENCODING = AudioFormat.ENCODING_PCM_16BIT;
// 缓冲区字节大小
private int bufferSizeInBytes = 0;
//录音对象
private AudioRecord audioRecord;
//录音状态
private Status status = Status.STATUS_NO_READY;
//文件名
private String fileName;
//录音文件
private List filesName = new ArrayList<>();
private AudioRecorder() {
}
//单例模式
public static AudioRecorder getInstance() {
if (audioRecorder == null) {
audioRecorder = new AudioRecorder();
}
return audioRecorder;
}
//录音对象
private AudioRecord audioRecord;
public void createAudio(String fileName, int audioSource, int sampleRateInHz, int channelConfig, int audioFormat) {
// 获得缓冲区字节大小
bufferSizeInBytes = AudioRecord.getMinBufferSize(sampleRateInHz,
channelConfig, channelConfig);
audioRecord = new AudioRecord(audioSource, sampleRateInHz, channelConfig, audioFormat, bufferSizeInBytes);
//this.fileName = fileName;
}
/**
* 创建默认的录音对象
*
* @param fileName 文件名
*/
public void createDefaultAudio(String fileName) {
// 获得缓冲区字节大小
bufferSizeInBytes = AudioRecord.getMinBufferSize(AUDIO_SAMPLE_RATE,
AUDIO_CHANNEL, AUDIO_ENCODING);
audioRecord = new AudioRecord(AUDIO_INPUT, AUDIO_SAMPLE_RATE, AUDIO_CHANNEL, AUDIO_ENCODING, bufferSizeInBytes);
this.fileName = fileName;
status = Status.STATUS_READY;
}
/**
* 开始录音
*
* @param listener 音频流的监听
*/
public void startRecord(final RecordStreamListener listener) {
if (status == Status.STATUS_NO_READY || TextUtils.isEmpty(fileName)) {
throw new IllegalStateException("录音尚未初始化,请检查是否禁止了录音权限~");
}
if (status == Status.STATUS_START) {
throw new IllegalStateException("正在录音");
}
Log.d("AudioRecorder","===startRecord==="+audioRecord.getState());
audioRecord.startRecording();
new Thread(new Runnable() {
@Override
public void run() {
writeDataTOFile(listener);
}
}).start();
}
/**
* 暂停录音
*/
public void pauseRecord() {
Log.d("AudioRecorder","===pauseRecord===");
if (status != Status.STATUS_START) {
throw new IllegalStateException("没有在录音");
} else {
audioRecord.stop();
status = Status.STATUS_PAUSE;
}
}
/**
* 停止录音
*/
public void stopRecord() {
Log.d("AudioRecorder","===stopRecord===");
if (status == Status.STATUS_NO_READY || status == Status.STATUS_READY) {
throw new IllegalStateException("录音尚未开始");
} else {
audioRecord.stop();
status = Status.STATUS_STOP;
release();
}
}
/**
* 释放资源
*/
public void release() {
Log.d("AudioRecorder","===release===");
//假如有暂停录音
try {
if (filesName.size() > 0) {
List filePaths = new ArrayList<>();
for (String fileName : filesName) {
filePaths.add(FileUtils.getPcmFileAbsolutePath(fileName));
}
//清除
filesName.clear();
//将多个pcm文件转化为wav文件
mergePCMFilesToWAVFile(filePaths);
} else {
//这里由于只要录音过filesName.size都会大于0,没录音时fileName为null
//会报空指针 NullPointerException
// 将单个pcm文件转化为wav文件
//Log.d("AudioRecorder", "=====makePCMFileToWAVFile======");
//makePCMFileToWAVFile();
}
} catch (IllegalStateException e) {
throw new IllegalStateException(e.getMessage());
}
if (audioRecord != null) {
audioRecord.release();
audioRecord = null;
}
status = Status.STATUS_NO_READY;
}
/**
* 取消录音
*/
public void canel() {
filesName.clear();
fileName = null;
if (audioRecord != null) {
audioRecord.release();
audioRecord = null;
}
status = Status.STATUS_NO_READY;
}
/**
* 将音频信息写入文件
*
* @param listener 音频流的监听
*/
private void writeDataTOFile(RecordStreamListener listener) {
// new一个byte数组用来存一些字节数据,大小为缓冲区大小
byte[] audiodata = new byte[bufferSizeInBytes];
FileOutputStream fos = null;
int readsize = 0;
try {
String currentFileName = fileName;
if (status == Status.STATUS_PAUSE) {
//假如是暂停录音 将文件名后面加个数字,防止重名文件内容被覆盖
currentFileName += filesName.size();
}
filesName.add(currentFileName);
File file = new File(FileUtils.getPcmFileAbsolutePath(currentFileName));
if (file.exists()) {
file.delete();
}
fos = new FileOutputStream(file);// 建立一个可存取字节的文件
} catch (IllegalStateException e) {
Log.e("AudioRecorder", e.getMessage());
throw new IllegalStateException(e.getMessage());
} catch (FileNotFoundException e) {
Log.e("AudioRecorder", e.getMessage());
}
//将录音状态设置成正在录音状态
status = Status.STATUS_START;
while (status == Status.STATUS_START) {
readsize = audioRecord.read(audiodata, 0, bufferSizeInBytes);
if (AudioRecord.ERROR_INVALID_OPERATION != readsize && fos != null) {
try {
fos.write(audiodata);
if (listener != null) {
//用于拓展业务
listener.recordOfByte(audiodata, 0, audiodata.length);
}
} catch (IOException e) {
Log.e("AudioRecorder", e.getMessage());
}
}
}
try {
if (fos != null) {
fos.close();// 关闭写入流
}
} catch (IOException e) {
Log.e("AudioRecorder", e.getMessage());
}
}
/**
* 将pcm合并成wav
*
* @param filePaths
*/
private void mergePCMFilesToWAVFile(final List filePaths) {
new Thread(new Runnable() {
@Override
public void run() {
if (PcmToWav.mergePCMFilesToWAVFile(filePaths, FileUtils.getWavFileAbsolutePath(fileName))) {
//操作成功
} else {
//操作失败
Log.e("AudioRecorder", "mergePCMFilesToWAVFile fail");
throw new IllegalStateException("mergePCMFilesToWAVFile fail");
}
fileName = null;
}
}).start();
}
/**
* 将单个pcm文件转化为wav文件
*/
private void makePCMFileToWAVFile() {
new Thread(new Runnable() {
@Override
public void run() {
if (PcmToWav.makePCMFileToWAVFile(FileUtils.getPcmFileAbsolutePath(fileName), FileUtils.getWavFileAbsolutePath(fileName), true)) {
//操作成功
} else {
//操作失败
Log.e("AudioRecorder", "makePCMFileToWAVFile fail");
throw new IllegalStateException("makePCMFileToWAVFile fail");
}
fileName = null;
}
}).start();
}
/**
* 获取录音对象的状态
*
* @return
*/
public Status getStatus() {
return status;
}
/**
* 获取本次录音文件的个数
*
* @return
*/
public int getPcmFilesCount() {
return filesName.size();
}
/**
* 录音对象的状态
*/
public enum Status {
//未开始
STATUS_NO_READY,
//预备
STATUS_READY,
//录音
STATUS_START,
//暂停
STATUS_PAUSE,
//停止
STATUS_STOP
}
}