音频 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 数据中增加了一个文件头。

起始地址占用空间本地址数字的含意
00H4byteRIFF,资源交换文件标记。
04H4byte从下一个地址开始到文件尾的总字节数。高位字节在前面,这里就是001437ECH,换成十进制是1325036byte,算上这之前的8byte就正好1325044byte了。
08H4byteWAVE,代表wav文件格式。
0CH4byteFMT ,波形格局标记
10H4byte00000010H,16PCM,我的了解是用16bit的数据表示一个量化后果。
14H2byte为1时示意线性PCM编码,大于1时示意有压缩的编码。这里是0001H。
16H2byte1为单声道,2为双声道,这里是0001H。
18H4byte采样频率,这里是00002B11H,也就是11025Hz。
1CH4byteByte率=采样频率*音频通道数*每次采样失去的样本位数/8,00005622H,也就是22050Byte/s=11025 * 1 * 16/2
20H2byte块对齐=通道数*每次采样失去的样本位数/8,0002H,也就是 2 == 1 * 16/8
22H2byte样本数据位数,0010H即16,一个量化样本占2byte。
24H4bytedata,一个标记而已。
28H4byteWav文件理论音频数据所占的大小,这里是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 文件的读写采纳 FileOutputStreamFileInputStream

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音视频开发系列教程】