共计 10873 个字符,预计需要花费 28 分钟才能阅读完成。
音频 PCM 数据的采集和播放,读写音频 wav 文件
应用 AudioRecord 和 AudioTrack 实现音频 PCM 数据的采集和播放,并读写音频 wav 文件
筹备工作
Android 提供了 AudioRecord 和 MediaRecord。MediaRecord 可抉择录音的格局。AudioRecord 失去 PCM 编码格局的数据。AudioRecord 可能设置模拟信号转化为数字信号的相干参数,包含采样率和量化深度,同时也包含通道数目等。
PCM
PCM 是在由模拟信号向数字信号转化的一种罕用的编码格局,称为脉冲编码调制,PCM 将模拟信号依照肯定的间距划分为多段,而后通过二进制去量化每一个间距的强度。PCM 示意的是音频文件中随着工夫的流逝的一段音频的振幅。Android 在 WAV 文件中反对 PCM 的音频数据。
WAV
WAV,MP3 等比拟常见的音频格式,不同的编码格局对应不通过的原始音频。为了不便传输,通常会压缩原始音频。为了分别出音频格式,每种格局有特定的头文件(header)。WAV 以 RIFF 为规范。RIFF 是一种资源替换档案规范。RIFF 将文件存储在每一个标记块中。根本形成单位是 trunk,每个 trunk 由标记位,数据大小,数据存储,三个局部形成。
PCM 打包成 WAV
PCM 是原始音频数据,WAV 是 windows 中常见的音频格式,只是在 pcm 数据中增加了一个文件头。
起始地址 | 占用空间 | 本地址数字的含意 |
---|---|---|
00H | 4byte | RIFF,资源交换文件标记。 |
04H | 4byte | 从下一个地址开始到文件尾的总字节数。高位字节在前面,这里就是 001437ECH,换成十进制是 1325036byte,算上这之前的 8byte 就正好 1325044byte 了。 |
08H | 4byte | WAVE,代表 wav 文件格式。 |
0CH | 4byte | FMT,波形格局标记 |
10H | 4byte | 00000010H,16PCM,我的了解是用 16bit 的数据表示一个量化后果。 |
14H | 2byte | 为 1 时示意线性 PCM 编码,大于 1 时示意有压缩的编码。这里是 0001H。 |
16H | 2byte | 1 为单声道,2 为双声道,这里是 0001H。 |
18H | 4byte | 采样频率,这里是 00002B11H,也就是 11025Hz。 |
1CH | 4byte | Byte 率 =采样频率 * 音频通道数 * 每次采样失去的样本位数 /8 ,00005622H,也就是22050Byte/s=11025 * 1 * 16/2 |
20H | 2byte | 块对齐 = 通道数 * 每次采样失去的样本位数 /8,0002H,也就是 2 == 1 * 16/8 |
22H | 2byte | 样本数据位数,0010H 即 16,一个量化样本占 2byte。 |
24H | 4byte | data,一个标记而已。 |
28H | 4byte | Wav 文件理论音频数据所占的大小,这里是 001437C8H 即 1325000,再加上 2CH 就正好是 1325044,整个文件的大小。 |
2CH | 不定 | 量化数据 |
AudioRecord
AudioRecord 可实现从音频输出设施记录声音的性能。失去 PCM 格局的音频。读取音频的办法有 read(byte[], int, int)
,read(short[], int, int)
或 read(ByteBuffer, int)
。可依据存储形式和需要抉择应用这项办法。
须要权限<uses-permission android:name="android.permission.RECORD_AUDIO" />
AudioRecord 构造函数
public AudioRecord(int audioSource, int sampleRateInHz, int channelConfig, int audioFormat, int bufferSizeInBytes)
- audioSource 音源设施,罕用麦克风
MediaRecorder.AudioSource.MIC
- samplerateInHz 采样频率,44100Hz 是目前所有设施都反对的频率
- channelConfig 音频通道,单声道还是立体声
- audioFormat 该参数为量化深度,即为每次采样的位数
- bufferSizeInBytes 可通过
getMinBufferSize()
办法确定,每次从硬件读取数据所须要的缓冲区的大小。
获取 wav 文件
若要取得 wav 文件,须要在 PCM 根底上减少一个 header。能够将 PCM 文件转换成 wav,这里提供一种 PCM 与 wav 简直同时生成的思路。
PCM 与 wav 同时创立,给 wav 文件一个默认的 header。录制线程启动后,同时写 PCM 与 wav。录制实现时,从新生成 header,利用 RandomAccessFile
批改 wav 文件的 header。
AudioTrack
应用 AudioTrack
播放音频。初始化 AudioTrack 时,要依据录制时的参数进行设定。
代码示例
工具类 WindEar
实现音频 PCM 数据的采集和播放,与读写音频 wav 文件的性能。
AudioRecordThread
应用AudioRecord
录制 PCM 文件,可抉择同时生成 wav 文件AudioTrackPlayThread
应用 AudioTrack 播放 PCM 或 wav 音频文件的线程WindState
示意以后状态,例如是否在播放,录制等等
PCM 文件的读写采纳 FileOutputStream
和 FileInputStream
generateWavFileHeader
办法能够生成 wav 文件的 header
/**
* 音频录制器
* 应用 AudioRecord 和 AudioTrack API 实现音频 PCM 数据的采集和播放,并实现读写音频 wav 文件
* 查看权限,查看麦克风的工作放在 Activity 中进行
*/
public class WindEar {
private static final String TAG = "rustApp";
private static final String TMP_FOLDER_NAME = "AnWindEar";
private static final int RECORD_AUDIO_BUFFER_TIMES = 1;
private static final int PLAY_AUDIO_BUFFER_TIMES = 1;
private static final int AUDIO_FREQUENCY = 44100;
private static final int RECORD_CHANNEL_CONFIG = AudioFormat.CHANNEL_IN_STEREO;
private static final int PLAY_CHANNEL_CONFIG = AudioFormat.CHANNEL_OUT_STEREO;
private static final int AUDIO_ENCODING = AudioFormat.ENCODING_PCM_16BIT;
private AudioRecordThread aRecordThread; // 录制线程
private volatile WindState state = WindState.IDLE; // 以后状态
private File tmpPCMFile = null;
private File tmpWavFile = null;
private OnState onStateListener;
private Handler mainHandler = new Handler(Looper.getMainLooper());
/**
* PCM 缓存目录
*/
private static String cachePCMFolder;
/**
* wav 缓存目录
*/
private static String wavFolderPath;
private static WindEar instance = new WindEar();
private WindEar() {}
public static WindEar getInstance() {if (null == instance) {instance = new WindEar();
}
return instance;
}
public void setOnStateListener(OnState onStateListener) {this.onStateListener = onStateListener;}
/**
* 初始化目录
*/
public static void init(Context context) {
// 存储在 App 内或 SD 卡上
// cachePCMFolder = context.getFilesDir().getAbsolutePath() + File.separator + TMP_FOLDER_NAME;
cachePCMFolder = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator
+ TMP_FOLDER_NAME;
File folder = new File(cachePCMFolder);
if (!folder.exists()) {boolean f = folder.mkdirs();
Log.d(TAG, String.format(Locale.CHINA, "PCM 目录:%s -> %b", cachePCMFolder, f));
} else {for (File f : folder.listFiles()) {boolean d = f.delete();
Log.d(TAG, String.format(Locale.CHINA, "删除 PCM 文件:%s %b", f.getName(), d));
}
Log.d(TAG, String.format(Locale.CHINA, "PCM 目录:%s", cachePCMFolder));
}
wavFolderPath = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator
+ TMP_FOLDER_NAME;
// wavFolderPath = context.getFilesDir().getAbsolutePath() + File.separator + TMP_FOLDER_NAME;
File wavDir = new File(wavFolderPath);
if (!wavDir.exists()) {boolean w = wavDir.mkdirs();
Log.d(TAG, String.format(Locale.CHINA, "wav 目录:%s -> %b", wavFolderPath, w));
} else {Log.d(TAG, String.format(Locale.CHINA, "wav 目录:%s", wavFolderPath));
}
}
/**
* 开始录制音频
*/
public synchronized void startRecord(boolean createWav) {if (!state.equals(WindState.IDLE)) {Log.w(TAG, "无奈开始录制,以后状态为" + state);
return;
}
try {tmpPCMFile = File.createTempFile("recording", ".pcm", new File(cachePCMFolder));
if (createWav) {SimpleDateFormat sdf = new SimpleDateFormat("yyMMdd_HHmmss", Locale.CHINA);
tmpWavFile = new File(wavFolderPath + File.separator + "r" + sdf.format(new Date()) + ".wav");
}
Log.d(TAG, "tmp file" + tmpPCMFile.getName());
} catch (IOException e) {e.printStackTrace();
}
if (null != aRecordThread) {aRecordThread.interrupt();
aRecordThread = null;
}
aRecordThread = new AudioRecordThread(createWav);
aRecordThread.start();}
public synchronized void stopRecord() {if (!state.equals(WindState.RECORDING)) {return;}
state = WindState.STOP_RECORD;
notifyState(state);
}
/**
* 播放录制好的 PCM 文件
*/
public synchronized void startPlayPCM() {if (!isIdle()) {return;}
new AudioTrackPlayThread(tmpPCMFile).start();}
/**
* 播放录制好的 wav 文件
*/
public synchronized void startPlayWav() {if (!isIdle()) {return;}
new AudioTrackPlayThread(tmpWavFile).start();}
public synchronized void stopPlay() {if (!state.equals(WindState.PLAYING)) {return;}
state = WindState.STOP_PLAY;
}
public synchronized boolean isIdle() {return WindState.IDLE.equals(state);
}
/**
* 音频录制线程
* 应用 FileOutputStream 来写文件
*/
private class AudioRecordThread extends Thread {
AudioRecord aRecord;
int bufferSize = 10240;
boolean createWav = false;
AudioRecordThread(boolean createWav) {
this.createWav = createWav;
bufferSize = AudioRecord.getMinBufferSize(AUDIO_FREQUENCY,
RECORD_CHANNEL_CONFIG, AUDIO_ENCODING) * RECORD_AUDIO_BUFFER_TIMES;
Log.d(TAG, "record buffer size =" + bufferSize);
aRecord = new AudioRecord(MediaRecorder.AudioSource.MIC, AUDIO_FREQUENCY,
RECORD_CHANNEL_CONFIG, AUDIO_ENCODING, bufferSize);
}
@Override
public void run() {
state = WindState.RECORDING;
notifyState(state);
Log.d(TAG, "录制开始");
try {
// 这里抉择 FileOutputStream 而不是 DataOutputStream
FileOutputStream pcmFos = new FileOutputStream(tmpPCMFile);
FileOutputStream wavFos = new FileOutputStream(tmpWavFile);
if (createWav) {writeWavFileHeader(wavFos, bufferSize, AUDIO_FREQUENCY, aRecord.getChannelCount());
}
aRecord.startRecording();
byte[] byteBuffer = new byte[bufferSize];
while (state.equals(WindState.RECORDING) && !isInterrupted()) {int end = aRecord.read(byteBuffer, 0, byteBuffer.length);
pcmFos.write(byteBuffer, 0, end);
pcmFos.flush();
if (createWav) {wavFos.write(byteBuffer, 0, end);
wavFos.flush();}
}
aRecord.stop(); // 录制完结
pcmFos.close();
wavFos.close();
if (createWav) {
// 批改 header
RandomAccessFile wavRaf = new RandomAccessFile(tmpWavFile, "rw");
byte[] header = generateWavFileHeader(tmpPCMFile.length(), AUDIO_FREQUENCY, aRecord.getChannelCount());
Log.d(TAG, "header:" + getHexString(header));
wavRaf.seek(0);
wavRaf.write(header);
wavRaf.close();
Log.d(TAG, "tmpWavFile.length:" + tmpWavFile.length());
}
Log.i(TAG, "audio tmp PCM file len:" + tmpPCMFile.length());
} catch (Exception e) {Log.e(TAG, "AudioRecordThread:", e);
notifyState(WindState.ERROR);
}
notifyState(state);
state = WindState.IDLE;
notifyState(state);
Log.d(TAG, "录制完结");
}
}
private static String getHexString(byte[] bytes) {StringBuilder sb = new StringBuilder();
for (byte b : bytes) {sb.append(Integer.toHexString(b)).append(",");
}
return sb.toString();}
/**
* AudioTrack 播放音频线程
* 应用 FileInputStream 读取文件
*/
private class AudioTrackPlayThread extends Thread {
AudioTrack track;
int bufferSize = 10240;
File audioFile = null;
AudioTrackPlayThread(File aFile) {setPriority(Thread.MAX_PRIORITY);
audioFile = aFile;
int bufferSize = AudioTrack.getMinBufferSize(AUDIO_FREQUENCY,
PLAY_CHANNEL_CONFIG, AUDIO_ENCODING) * PLAY_AUDIO_BUFFER_TIMES;
track = new AudioTrack(AudioManager.STREAM_MUSIC,
AUDIO_FREQUENCY,
PLAY_CHANNEL_CONFIG, AUDIO_ENCODING, bufferSize,
AudioTrack.MODE_STREAM);
}
@Override
public void run() {super.run();
state = WindState.PLAYING;
notifyState(state);
try {FileInputStream fis = new FileInputStream(audioFile);
track.play();
byte[] aByteBuffer = new byte[bufferSize];
while (state.equals(WindState.PLAYING) &&
fis.read(aByteBuffer) >= 0) {track.write(aByteBuffer, 0, aByteBuffer.length);
}
track.stop();
track.release();} catch (Exception e) {Log.e(TAG, "AudioTrackPlayThread:", e);
notifyState(WindState.ERROR);
}
state = WindState.STOP_PLAY;
notifyState(state);
state = WindState.IDLE;
notifyState(state);
}
}
private synchronized void notifyState(final WindState currentState) {if (null != onStateListener) {mainHandler.post(new Runnable() {
@Override
public void run() {onStateListener.onStateChanged(currentState);
}
});
}
}
public interface OnState {void onStateChanged(WindState currentState);
}
/**
* 示意以后状态
*/
public enum WindState {
ERROR,
IDLE,
RECORDING,
STOP_RECORD,
PLAYING,
STOP_PLAY
}
/**
* @param out wav 音频文件流
* @param totalAudioLen 不包含 header 的音频数据总长度
* @param longSampleRate 采样率, 也就是录制时应用的频率
* @param channels audioRecord 的频道数量
* @throws IOException 写文件谬误
*/
private void writeWavFileHeader(FileOutputStream out, long totalAudioLen, long longSampleRate,
int channels) throws IOException {byte[] header = generateWavFileHeader(totalAudioLen, longSampleRate, channels);
out.write(header, 0, header.length);
}
/**
* 任何一种文件在头部增加相应的头文件才可能确定的示意这种文件的格局,* wave 是 RIFF 文件构造,每一部分为一个 chunk,其中有 RIFF WAVE chunk,* FMT Chunk,Fact chunk,Data chunk, 其中 Fact chunk 是能够抉择的
*
* @param pcmAudioByteCount 不包含 header 的音频数据总长度
* @param longSampleRate 采样率, 也就是录制时应用的频率
* @param channels audioRecord 的频道数量
*/
private byte[] generateWavFileHeader(long pcmAudioByteCount, long longSampleRate, int channels) {
long totalDataLen = pcmAudioByteCount + 36; // 不蕴含前 8 个字节的 WAV 文件总长度
long byteRate = longSampleRate * 2 * channels;
byte[] header = new byte[44];
header[0] = 'R'; // RIFF
header[1] = 'I';
header[2] = 'F';
header[3] = 'F';
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;
// 通道数
header[22] = (byte) channels;
header[23] = 0;
// 采样率,每个通道的播放速度
header[24] = (byte) (longSampleRate & 0xff);
header[25] = (byte) ((longSampleRate >> 8) & 0xff);
header[26] = (byte) ((longSampleRate >> 16) & 0xff);
header[27] = (byte) ((longSampleRate >> 24) & 0xff);
// 音频数据传送速率, 采样率 * 通道数 * 采样深度 /8
header[28] = (byte) (byteRate & 0xff);
header[29] = (byte) ((byteRate >> 8) & 0xff);
header[30] = (byte) ((byteRate >> 16) & 0xff);
header[31] = (byte) ((byteRate >> 24) & 0xff);
// 确定零碎一次要解决多少个这样字节的数据,确定缓冲区,通道数 * 采样位数
header[32] = (byte) (2 * channels);
header[33] = 0;
// 每个样本的数据位数
header[34] = 16;
header[35] = 0;
//Data chunk
header[36] = 'd';//data
header[37] = 'a';
header[38] = 't';
header[39] = 'a';
header[40] = (byte) (pcmAudioByteCount & 0xff);
header[41] = (byte) ((pcmAudioByteCount >> 8) & 0xff);
header[42] = (byte) ((pcmAudioByteCount >> 16) & 0xff);
header[43] = (byte) ((pcmAudioByteCount >> 24) & 0xff);
return header;
}
}
【Android 音视频开发系列教程】