Android-PCM

  1. 一、简介
  2. 采样频率
  3. 采样位数
  4. 比特率 bitrate
  5. 有损和无损
  6. 录制音频 AudioRecord
  7. 播放音频 AudioTrack
  8. wav文件
  9. 文件头
  10. wav体
  11. 分离PCM16的左右声道

一、简介

主要完成对音频的实时录制,并输出PCM音频数据。PCM(脉冲编码调制)是数字通信的一种编码方式。通过对模拟信号的抽样、量化、编码完成数据转换。

采样频率

人对频率的识别范围是 20HZ - 20000HZ。

  • 8000hz 为电话采样。
  • 22050 的采样频率是常用的。
  • 44100已是CD音质, 超过48000的采样对人耳已经没有意义

对采样率为44.1kHz的AAC(Advanced Audio Coding)音频进行解码时,一帧的解码时间须控制在23.22毫秒内。通常是按1024个采样点一帧。

不同于视频帧,音频帧略有不同。视频帧即一帧代表一副图像。单音频帧和编码格式相关,不同编码不同的标准。只需要根据音频的采样频率和采样精度,就可以确定他的帧数,如44100HZ 16位的PCM音频。每秒音频数据是44100 16kbs即44100 2 byte。

采样位数

采样精度取决于采样位数的大小:

  • 1 字节(也就是8bit) 只能记录 256 个数, 也就是只能将振幅划分成 256 个等级
  • 2 字节(也就是16bit) 可以细到 65536 个数, 这已是 CD 标准了;
  • 4 字节(也就是32bit) 能把振幅细分到 4294967296 个等级, 实在是没必要了

上面是单声道(mono)双声道(stereo)则是双倍。

比特率 bitrate

码率是指经过编码后的音频数据每秒钟需要用多少个比特来表示.

有损和无损

我们所使用的CD的采样标准就是44.1k,即无损。

录制音频 AudioRecord

Android提供了AudioRecord类完成音频的录制工作,并且返回PCM数据格式。

public AudioRecord(int audioSource, int sampleRateInHz, int channelConfig, int audioFormat, int bufferSizeInBytes)

  • audioSource 音源设备,常用麦克风MediaRecorder.AudioSource.MIC。
  • samplerateInHz 采样频率,44100Hz是目前所有设备都支持的频率。范围在4000~192000。
  • channelConfig 音频通道,单声道AudioFormat.CHANNEL_IN_MONO还是立体声AudioFormat.CHANNEL_IN_STEREO,还有其他声道如左、右声道。
  • audioFormat 该参数为量化深度,即为每次采样的位数,最好使用16bit,因为8bit的采样位数有效设备不兼容。
  • bufferSizeInBytes 可通过getMinBufferSize()方法确定,每次从硬件读取数据所需要的缓冲区的大小。

实例:
权限:

<uses-permission android:name="android.permission.RECORD_AUDIO" />
<!-- 文件管理权限,可以忽略 -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

工具类:

1.打开AudioRecord权限
2.根据采样频率、声道类型、采样精度调用AudioRecord.getMinBufferSize() 获取最小Buffer大小
3.创建AudioRecord对象
4.startRecording开启录音、stop关闭录音
5.线程中通过read获取音频数据

public class AudioRecordChannel {

private AudioRecord audioRecord;
private ExecutorService executor;

private int minBufferSize;
private boolean isStart = false;

private FileOutputStream fos = null;

public AudioRecordChannel() {
minBufferSize = AudioRecord.getMinBufferSize(44100, AudioFormat.CHANNEL_IN_STEREO, AudioFormat.ENCODING_PCM_16BIT);
audioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC, 44100, AudioFormat.CHANNEL_IN_STEREO, AudioFormat.ENCODING_PCM_16BIT, minBufferSize);

executor = Executors.newSingleThreadExecutor();
}

public void openFile() throws IOException {
File file = new File(Environment.getExternalStorageDirectory().getAbsolutePath(), "test.pcm");
if (!file.exists()) {
file.createNewFile();
}
fos = new FileOutputStream(file);
}

public void start() {
if (!isStart) {
isStart = true;
executor.submit(new AudioTask());
}
}

public void stop() throws IOException {
audioRecord.stop();
isStart = false;
executor.shutdown();
if (fos != null) {
fos.flush();
fos.close();
fos = null;
}
audioRecord.release();
}

class AudioTask implements Runnable {

@Override
public void run() {
audioRecord.startRecording();
byte[] data = new byte[minBufferSize];
while (isStart) {
int dataLen = audioRecord.read(data, 0, data.length);
// 写文件
if (fos != null) {
try {
fos.write(data);
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}

}

播放音频 AudioTrack

播放方式分为MODE_STATICMODE_STREAM。前者一次性获取完整数据后播放,后者边获取流数据边进行播放。

public AudioTrack(AudioAttributes attributes, AudioFormat format, int bufferSizeInBytes, int mode, int sessionId)

  • attributes:播放流类型。声音的作用setUsage()、播放的是什么setContentType()
  • format:播放流格式。流类型setEncoding()、声道setChannelMask()
  • bufferSizeInBytes:数据大小。
  • mode:播放模式即MODE_STATICMODE_STREAM
  • sissionId:会话ID

实例

1.确认数据的获取方式?本地的完整数据还是远程流数据。根据不同方式选择MODE_
2.初始化,创建对象
3.根据不同MODE,对其write数据(MODE_STATIC需要先write填充数据,后paly、而MODE_STREAM需要先play,后write填充数据)

下面展示的是流数据。

public class AudioTrackChannel {

private AudioTrack audioTrack;
private ExecutorService executor;

private int minBufferSize;
private boolean isStart = false;

public AudioTrackChannel() {
minBufferSize = AudioRecord.getMinBufferSize(44100, AudioFormat.CHANNEL_IN_STEREO, AudioFormat.ENCODING_PCM_16BIT);
audioTrack = new AudioTrack(
new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA)
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.build(),
new AudioFormat.Builder()
.setEncoding(AudioFormat.ENCODING_PCM_16BIT)
.setChannelMask(AudioFormat.CHANNEL_IN_STEREO)
.build(),
minBufferSize,
AudioTrack.MODE_STREAM,
AudioManager.AUDIO_SESSION_ID_GENERATE
);

executor = Executors.newSingleThreadExecutor();
}

public void play() {
if (!isStart) {
isStart = true;
audioTrack.play();
executor.submit(new AudioTask());
}
}

public void stop() {
audioTrack.stop();
isStart = false;
executor.shutdown();
audioTrack.release();
}

class AudioTask implements Runnable {

FileInputStream fis;

public AudioTask() {
try {
fis = new FileInputStream(new File(Environment.getExternalStorageDirectory().getAbsolutePath(), "test.pcm"));
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}

@Override
public void run() {
byte[] data = new byte[minBufferSize];
while (true) {
try {
if (!(isStart && fis.available() > 0)) break;
} catch (IOException e) {
e.printStackTrace();
}

try {
int readCount = fis.read(data);
if (readCount == AudioTrack.ERROR_INVALID_OPERATION || readCount == AudioTrack.ERROR_BAD_VALUE) {
continue;
}
audioTrack.write(data, 0, readCount);

} catch (IOException e) {
e.printStackTrace();
}

}

try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

wav文件

wav格式是微软公司开发的一种音频格式。

文件头

wav格式的音频会存在一个头信息内容。大小由44个字节组成。

4字节数据,内容为“RIFF”,表示资源交换文件标识
4字节数据,内容为一个整数,表示从下个地址开始到文件尾的总字节数
4字节数据,内容为“WAVE”,表示WAV文件标识
4字节数据,内容为“fmt ”,表示波形格式标识(fmt ),最后一位空格。
4字节数据,内容为一个整数,表示PCMWAVEFORMAT的长度
2字节数据,内容为一个短整数,表示格式种类(值为1时,表示数据为线性PCM编码)
2字节数据,内容为一个短整数,表示通道数,单声道为1,双声道为2
4字节数据,内容为一个整数,表示采样率,比如44100
4字节数据,内容为一个整数,表示波形数据传输速率(每秒平均字节数),大小为 采样率 通道数 采样位数
2字节数据,内容为一个短整数,表示DATA数据块长度,大小为 通道数 * 采样位数
2字节数据,内容为一个短整数,表示采样位数,即PCM位宽,通常为8位或16位
4字节数据,内容为“data”,表示数据标记符
4字节数据,内容为一个整数,表示接下来声音数据的总大小

实例

byte[] header = new byte[44];
header[0] = 'R'; // RIFF
header[1] = 'I';
header[2] = 'F';
header[3] = 'F';
// wavData + 44 - 8
header[4] = (byte) (totalDataLen & 0xff);//数据大小
header[5] = (byte) ((totalDataLen >> 8) & 0xff);
header[6] = (byte) ((totalDataLen >> 16) & 0xff);
header[7] = (byte) ((totalDataLen >> 24) & 0xff);
header[8] = 'W';//WAVE
header[9] = 'A';
header[10] = 'V';
header[11] = 'E';
//FMT Chunk
header[12] = 'f'; // 'fmt '
header[13] = 'm';
header[14] = 't';
header[15] = ' ';//过渡字节
//数据大小
header[16] = 16; // 4 bytes: size of 'fmt ' chunk
header[17] = 0;
header[18] = 0;
header[19] = 0;
//编码方式 10H为PCM编码格式
header[20] = 1; // format = 1
header[21] = 0;
//通道数 2
header[22] = (byte) channels;
header[23] = 0;
//采样率,每个通道的播放速度 44100
header[24] = (byte) (sampleRate & 0xff);
header[25] = (byte) ((sampleRate >> 8) & 0xff);
header[26] = (byte) ((sampleRate >> 16) & 0xff);
header[27] = (byte) ((sampleRate >> 24) & 0xff);
//音频数据传送速率,采样率*通道数*采样精度/8 (44100 * 2 * 16 /8)
header[28] = (byte) (byteRate & 0xff);
header[29] = (byte) ((byteRate >> 8) & 0xff);
header[30] = (byte) ((byteRate >> 16) & 0xff);
header[31] = (byte) ((byteRate >> 24) & 0xff);
// 确定系统一次要处理多少个这样字节的数据,确定缓冲区,通道数*采样精度 / 8
header[32] = (byte) (channels * 16 / 8);
header[33] = 0;
//每个样本的数据精度 16 or 8
header[34] = 16;
header[35] = 0;
//Data chunk
header[36] = 'd';//data
header[37] = 'a';
header[38] = 't';
header[39] = 'a';
// PCM数据大小
header[40] = (byte) (wavDataLen & 0xff);
header[41] = (byte) ((wavDataLen >> 8) & 0xff);
header[42] = (byte) ((wavDataLen >> 16) & 0xff);
header[43] = (byte) ((wavDataLen >> 24) & 0xff);
out.write(header, 0, 44);

wav体

直接使用PCM数据即可。

分离PCM16的左右声道

在使用PCM16双声道数据中,左右声道的采样值是间隔存储的。每个采样值占用2byte字节,因此只需要间隔保存即可分离左右声道信息,但是分割后只是声道数量-1,采样精度依旧是16bit而不是8bit。

实例

public static void pcm16_split(String filePath) throws IOException {
File srcPath = new File(filePath);
File leftPath = new File(Environment.getExternalStorageDirectory().getAbsolutePath(), "test_left.pcm");
File rightPath = new File(Environment.getExternalStorageDirectory().getAbsolutePath(), "test_right.pcm");

if (!leftPath.exists()) leftPath.createNewFile();
if (!rightPath.exists()) rightPath.createNewFile();

FileInputStream fis = new FileInputStream(srcPath);
FileOutputStream lfos = new FileOutputStream(leftPath);
FileOutputStream rfos = new FileOutputStream(rightPath);

byte[] data = new byte[4];
while (fis.read(data) > 0) {
lfos.write(data, 0, 2);
rfos.write(data, 2, 2);
}

fis.close();
lfos.flush();
rfos.flush();
lfos.close();
rfos.close();
}

github
上图为左右声道分离后的音频波形图。