Android 音视频基础1——AudioTrack、AudioRecorder、MediaRecorder音频数据采集播放处理
简介
AudioRecorder,可以采集PCM格式的原始音频文件,对PCM格式数据,根据一定的协议约定,可以转换成wav格式等其他格式;
AudioTrack,可以直接播放PCM格式的文件,也就是说上面利用AudioRecorder生成的原始文件可以通过AudioTrack直接播放;
MediaRecorder,可以录制音频,也可以配合相机(Camera)录制视频,相比较而言,MediaRecorder功能更丰富,更易用,使用场景也更多,而AudioRecorder和AudioTrack则更底层一点,且功能存在局限性。
[x] 音视频基础0——目录
[x] 音视频基础1——AudioTrack、AudioRecorder、MediaRecorder音频数据采集播放处理
[ ] 音视频基础2——Android Camera API及Camera2 API的使用
[ ] 音视频基础3——Android Camera API及Camera2 采集视频数据
[ ] 音视频基础4——Android MediaCodec API学习
[ ] 音视频基础5——Android 实现rtmp推流
[x] 音视频基础6——交叉编译动态库
[ ] 音视频基础7——MacOs下编译FFMpeg4.2.2
下面分别介绍 这三个音频采集过程中常用的类。
AudioRecord
AudioRecord的说明比较简单,构造好后,通过其read系列方法,从指定的硬件源中读取数据到缓存中(这个缓存的大小的最小值由 采样率 通道数 音频数据类型 的值获取而来),然后将其写入到文件,这个原始的文件就是PCM文件,再根据媒体格式之间的转换协议,做下转换,就可以得到其他格式的文件了。
由于比较简单,这里直接贴上我针对AudioRecord封装的代码。
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220public class AudioRecordWrapper { private static final String TAG = "AudioRecordWrapper"; private static final int STATE_INIT = 0; // 初始化状态 private static final int STATE_RECORDING = 1; // 录制状态 private static final int STATE_STOP = 2; // 停止状态 private static final int STATE_RELEASE = 3; // 释放状态 private int mRecordState; // 当前的状态 /** * most time is {@link android.media.MediaRecorder.AudioSource#MIC} */ private int audioSource = MediaRecorder.AudioSource.MIC; // 录音硬件来源 private int sampleRateInHz = 44100; // 采样率 private int channelConfig = AudioFormat.CHANNEL_IN_MONO; // 声道设置 /** * {@link android.media.AudioFormat#ENCODING_PCM_8BIT} * {@link android.media.AudioFormat#ENCODING_PCM_16BIT} * {@link android.media.AudioFormat#ENCODING_PCM_FLOAT} */ private int audioFormat = AudioFormat.ENCODING_PCM_16BIT; // PCM 源文件格式 private String dirPath = PathUtils.getExternalAppAudioRecordPath(); // PCM 和 WAV 文件目录 private String srcFileName = "sr_test.pcm"; // PCM 文件名称 private String disFileName = "sr_test.wav"; // WAV 文件名称 private AudioRecord mAudioRecord; // 用来音频录制的AudioRecord private int mBufferedSizeInBytes; // 音频录制的最小缓冲值 public AudioRecordWrapper() { int bufferSizeInBytes = AudioRecord.getMinBufferSize(sampleRateInHz, channelConfig, audioFormat); mAudioRecord = new AudioRecord(audioSource, sampleRateInHz, channelConfig, audioFormat, bufferSizeInBytes); } public AudioRecordWrapper(Builder builder) { this.audioSource = builder.audioSource; this.sampleRateInHz = builder.sampleRateInHz; this.channelConfig = builder.channelConfig; this.audioFormat = builder.audioFormat; this.dirPath = builder.dirPath; this.srcFileName = builder.srcFileName; this.disFileName = builder.disFileName; mBufferedSizeInBytes = AudioRecord.getMinBufferSize(sampleRateInHz, channelConfig, audioFormat); if (mBufferedSizeInBytes <= 0) { throw new IllegalArgumentException("AudioRecord can not be created due to mBufferedSizeInBytes(need great than 0), mBufferedSizeInBytes: " + mBufferedSizeInBytes); } mAudioRecord = new AudioRecord(audioSource, sampleRateInHz, channelConfig, audioFormat, mBufferedSizeInBytes); mRecordState = STATE_INIT; } /** * @param recordFileName pcm 文件名称 * @param audioSource 音频文件来源(通常是麦克风) * @param sampleRateInHz 采样率 * @param channelConfig 声道设置 * @param audioFormat PCM数据格式 */ public void createAudioRecord(String recordFileName, int audioSource, int sampleRateInHz, int channelConfig, int audioFormat) { mBufferedSizeInBytes = AudioRecord.getMinBufferSize(sampleRateInHz, channelConfig, audioFormat); if (mBufferedSizeInBytes <= 0) { throw new IllegalArgumentException("AudioRecord is not available due to mBufferedSizeInBytes: " + mBufferedSizeInBytes); } mAudioRecord = new AudioRecord(audioSource, sampleRateInHz, channelConfig, audioFormat, mBufferedSizeInBytes); int state = mAudioRecord.getState(); LogUtils.i(TAG, "createAudio state: " + state + ", initialize: " + (state == AudioRecord.STATE_INITIALIZED)); this.srcFileName = recordFileName; this.mRecordState = STATE_INIT; } /** * 不能依靠{@link AudioRecord#getState()} 来进行判断 */ public void startRecord() { LogUtils.d(TAG, "startRecord()"); if (mAudioRecord == null || (mRecordState != STATE_INIT && mRecordState != STATE_STOP)) { LogUtils.w(TAG, "can not startRecord due to mAudioRecord is null or mRecordState is wrong"); return; } try { mAudioRecord.startRecording(); mRecordState = STATE_RECORDING; ThreadUtils.getIoPool().execute(this::writeAudioDataToFile); } catch (Exception e) { LogUtils.printStackTrace(TAG, e); } } public void startRecord(String recordFileName) { this.srcFileName = recordFileName; LogUtils.d(TAG, "startRecord(), mRecordFileName: " + srcFileName); startRecord(); } /** * 将音频数据写入文件 * * @throws IOException */ private void writeAudioDataToFile() { LogUtils.d(TAG, "writeAudioDataToFile()"); File recordDir = FileUtils.makeDirs(dirPath); File file = new File(recordDir, srcFileName); if (file.exists()) { file.delete(); } try (BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(file))) { byte[] audioData = new byte[mBufferedSizeInBytes]; while (mRecordState == STATE_RECORDING) { // 从音频采集硬件中读取数据 int readSize = mAudioRecord.read(audioData, 0, mBufferedSizeInBytes); if (readSize > 0) { bos.write(audioData, 0, readSize); } else { LogUtils.w(TAG, "readSize: " + readSize); } } bos.flush(); } catch (Throwable e) { LogUtils.printStackTrace(TAG, e); } } public void stopRecord() { LogUtils.d(TAG, "stopRecord()"); if (mAudioRecord == null) { LogUtils.w(TAG, "mAudioRecord is null"); return; } if (mRecordState == STATE_INIT || mRecordState == STATE_RELEASE) { LogUtils.w(TAG, "stopRecord(), error state"); return; } this.mRecordState = STATE_STOP; pcm2wav("test_" + System.currentTimeMillis() + ".wav"); } public void release() { LogUtils.d(TAG, "release()"); if (mAudioRecord != null) { mAudioRecord.release(); mAudioRecord = null; } this.mRecordState = STATE_RELEASE; } public void pcm2wav(String wavFileName) { File pcmFile = new File(dirPath, srcFileName); File wavFile; if (TextUtils.isEmpty(wavFileName)) { wavFile = new File(dirPath, disFileName); } else { wavFile = new File(dirPath, wavFileName); } ThreadUtils.getIoPool().execute(() -> { try { MediaUtils.pcm2Wav(pcmFile, wavFile, false, 1, 44100, 16); } catch (IOException e) { LogUtils.printStackTrace(e); } }); } /** * AudioRecorderWrapper构造器 */ static class Builder { private int audioSource; // 录音硬件来源 private int sampleRateInHz; // 采样率 private int channelConfig; // 声道设置 private int audioFormat; // PCM 源文件格式 private String dirPath; // PCM 和 WAV 文件目录 private String srcFileName; // PCM 文件名称 private String disFileName; // WAV 文件名称 public Builder audioSource(int audioSource) { this.audioSource = audioSource; return this; } public Builder sampleRate(int sampleRateInHz) { this.sampleRateInHz = sampleRateInHz; return this; } public Builder channel(int channelConfig) { this.channelConfig = channelConfig; return this; } public Builder audioFormat(int audioFormat) { this.audioFormat = audioFormat; return this; } public Builder dirPath(String dirPath) { this.dirPath = dirPath; return this; } public Builder srcFileName(String srcFileName) { this.srcFileName = srcFileName; return this; } public Builder disFileName(String disFileName) { this.disFileName = disFileName; return this; } public AudioRecordWrapper build() { return new AudioRecordWrapper(this); } }}
使用时需要注意一下几点:
请确保你获取到了麦克风权限;
读取文件的IO操作请放到子线程中进行;
注意资源的释放;
不要过于依赖其提供的状态来进行业务上的判断,最好自己维护。
AudioTrack
AudioTrack类管理和播放Java应用程序的单个音频资源。它允许将PCM音频缓冲区流传输到音频接收器以进行播放。
AudioTrack有两种工作模式:MODE_STATIC & MODE_STREAM。
MODE_STATIC,静态播放,一次性全部加载到内存播放,这就要求播放的文件小,适用场景自然是短音频;
MODE_STREAM,流式播放,将PCM音频数据读入到输入流,然后不断从输入流中读取数据进行播放,可以播放大文件;
AudioTrack使用时有一点需要注意,AudioTrack每次能播放多少数据取决于buffer设置:
MODE_STATIC模式下,buffer大小是播放声音的最大大小;
MODE_STREAM模式下,buffer取决于读取数据是采用的totalBufferSize(计算出来的)大小;
AudioTrack虽然提供了getState这个方法来获取当前的状态,但我们实际上并不能依据该状态来进行判断,因为需要调用系统硬件都是采用异步形式,我们不能准确的获取到当前的状态,一般需要自己定义一个变量来表示状态。
下面是对AudioTrack的封装,构造好AudioTrackWrapper后,调用start方法即可播放传入的PCM文件,调用stop方法即可停止播放,注意调用完毕后记得调用release释放资源。(ThreadUtils为封装的一个线程操作工具类)
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285/** * AudioTrack 封装 * AudioTrack 可以读取 音频采集生成的 PCM数据 并播放 * * @author randy * @date 2021/01/06 */public class AudioTrackWrapper { private static final String TAG = "AudioTrackWrapper"; private final AudioTrack mAudioTrack; private int streamType = AudioManager.STREAM_MUSIC; private int sampleRateInHz = 44100; private int channelConfig = AudioFormat.CHANNEL_IN_MONO; private int audioFormat = AudioFormat.ENCODING_PCM_16BIT; /** * See {@link AudioTrack#MODE_STATIC} and {@link AudioTrack#MODE_STREAM}. */ private int mode = AudioTrack.MODE_STREAM; private File pcmFile; /** * 默认构造行数 */ public AudioTrackWrapper() { int minBufferSize = AudioTrack.getMinBufferSize(sampleRateInHz, channelConfig, audioFormat); if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) { AudioAttributes audioAttributes; AudioFormat format; int sessionId = AudioManager.AUDIO_SESSION_ID_GENERATE; audioAttributes = new AudioAttributes.Builder() .setLegacyStreamType(streamType) .build(); format = new AudioFormat.Builder() .setChannelMask(channelConfig) .setEncoding(audioFormat) .setSampleRate(sampleRateInHz) .build(); mAudioTrack = new AudioTrack(audioAttributes, format, minBufferSize, mode, sessionId); } else { mAudioTrack = new AudioTrack(streamType, sampleRateInHz, channelConfig, audioFormat, minBufferSize, mode); } } /** * 传参构造器 * * @param streamType 流类型 * @param sampleRateInHz 采样率 * @param channelConfig 声道配置 * @param audioFormat 音频数据的呈现格式 8位、16位还是浮点 * @param mode 模式(static or stream) */ public AudioTrackWrapper(int streamType, int sampleRateInHz, int channelConfig, int audioFormat, int mode) { int minBufferSize = AudioTrack.getMinBufferSize(sampleRateInHz, channelConfig, audioFormat); if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) { AudioAttributes audioAttributes; AudioFormat format; int sessionId = AudioManager.AUDIO_SESSION_ID_GENERATE; audioAttributes = new AudioAttributes.Builder() .setLegacyStreamType(streamType) .build(); format = new AudioFormat.Builder() .setChannelMask(channelConfig) .setEncoding(audioFormat) .setSampleRate(sampleRateInHz) .build(); mAudioTrack = new AudioTrack(audioAttributes, format, minBufferSize, mode, sessionId); } else { mAudioTrack = new AudioTrack(streamType, sampleRateInHz, channelConfig, audioFormat, minBufferSize, mode); } } /** * 构造器构造 * * @param builder 构造器 */ public AudioTrackWrapper(Builder builder) { this.streamType = builder.streamType; this.sampleRateInHz = builder.sampleRateInHz; this.channelConfig = builder.channelConfig; this.audioFormat = builder.audioFormat; this.mode = builder.mode; mAudioTrack = new AudioTrack(streamType, sampleRateInHz, channelConfig, audioFormat, AudioTrack.getMinBufferSize(sampleRateInHz, channelConfig, audioFormat), mode); } /** * 开始播放 * * @param pcmPath pcm 所在目录路径 * @param pcmName pcm 文件名 */ public void start(String pcmPath, String pcmName) { start(new File(pcmPath), pcmName); } /** * 开始播放 * * @param pcmDir pcm 所在目录 * @param pcmName pcm 文件名 */ public void start(File pcmDir, String pcmName) { File pcmFile = new File(pcmDir, pcmName); start(pcmFile); } /** * 开始播放 * * @param pcmFile 要播放的pcm文件 */ public void start(File pcmFile) { if (pcmFile == null || !pcmFile.exists()) { LogUtils.e(TAG, "pcmFile is null or not exists!!!"); return; } this.pcmFile = pcmFile; LogUtils.d(TAG, "start(), pcmFile: " + pcmFile.getPath()); if (mode == AudioTrack.MODE_STREAM) { playByStream(pcmFile); } else { playByStatic(pcmFile); } } /** * 流模式,数据会从 Java 层传输到底层,并排队阻塞等待播放 * 1、所以这里需要开启线程独立播放 * 2、为保证能重复使用,需要对状态进行控制 * * @param pcmFile pcm 文件 */ private void playByStream(File pcmFile) { this.pcmFile = pcmFile; LogUtils.d(TAG, "playByStream()"); // 先取消 ThreadUtils.cancel(readPcmFileTask); ThreadUtils.executeByIo(readPcmFileTask); } private final ThreadUtils.Task
MediaRecorderMediaRecorder用来录制音频和视频的,使用比较广,音视频的录制控制基于下面这个状态机:
所以在使用的时候要严格控制函数的调用顺序,否则很容易产生IllegalStateException。录制音频的一般调用循序如下所示:
1234567891011MediaRecorder mediaRecorder = new MediaRecorder();mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GP);mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.ARM_NB);mediaRecorder.setOutputFile(OUTPUT_FILE_PATH);mediaRecorder.prepare();mediaRecorder.start();...mediaRecorder.stop();mediaRecorder.reset(); // reset之后MediaRecorder会重新回到Initial状态,再次调用setAudioSource可重复使用mediaRecorder.release(); // 释放了之后就不能再使用了。
录制视频大体和录制音频相同,知识需要加上视频源设置、视频输出格式、视频编码、以及预览的容器,具体如下:
1234567891011121314MediaRecorder mediaRecorder = new MediaRecorder();mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);mediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA) // 指定视频源mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4); // 这里要换成视频格式mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.ARM_NB); mediaRecorder.setVidioEncoder(MediaRecorder.VideoEncoder.MPEG_4_SP); // 设置视频编码mediaRecorder.setOutputFile(OUTPUT_FILE_PATH);mediaRecorder.setPreviewDisplay(targetSurface); // 指定视频预览的容器mediaRecorder.prepare();mediaRecorder.start();...mediaRecorder.stop();mediaRecorder.reset(); // reset之后MediaRecorder会重新回到Initial状态,再次调用setAudioSource可重复使用mediaRecorder.release(); // 释放了之后就不能再使用了。
下面贴出利用MediaRecorder录制音频的代码片段:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152private File outPutFile; private void onMediaRecorderStartClick() { PermissionUtils.permission(PermissionConstants.MICROPHONE).callback(new PermissionUtils.SimpleCallback() { @Override public void onGranted() { if (mMediaRecorder == null) { mMediaRecorder = new MediaRecorder(); } try { mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);// 设置麦克风 mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4); mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC); String fileName = TimeUtils.getNowString() + ".m4a"; File outPutDir = FileUtils.makeDirs(PathUtils.getExternalAppAudioRecordPath()); outPutFile = new File(outPutDir, fileName); mMediaRecorder.setOutputFile(outPutFile.getPath()); mMediaRecorder.prepare(); mMediaRecorder.start(); } catch (Exception e) { LogUtils.printStackTrace(TAG, e); } } @Override public void onDenied() { ToastUtils.showLong("您拒绝了录音权限,无法录音!"); } }).request(); } private void onMediaRecorderStopClick() { LogUtils.d(TAG, "onMediaRecorderStopClick"); try { if (mMediaRecorder != null) { mMediaRecorder.stop(); mMediaRecorder.release(); mMediaRecorder = null; } } catch (Exception e) { LogUtils.printStackTrace(TAG, e); if (mMediaRecorder != null) { mMediaRecorder.reset(); mMediaRecorder.release(); mMediaRecorder = null; } // 删除掉异常停止前产生的文件 FileUtils.deleteFile(outPutFile); } }
总结以上就是Android音频采集相关的内容,AudioRecord主要负责音频采集,AudioTrack负责pcm数据的播放,MediaRecorder既可以进行音频采集,也可以配合CameraApi进行视频采集,视频采集会加接下来的文章中讲到。