使用AudioRecorder + MediaCodeC,从0到1构建一个录音软件
根据所编写的流程,一步一步来,理解所写的内容,一定会有不小的收获
一、架构给的大致流程(针对于自主研发的车机)
- 点击开始录制
- 点击结束录制(prepare shutdown车辆准备下电时也要结束录制)
- recordApp通过audioRecorder进行录制,建议参数16khz,8channel,16bit
- 录到原始的pcm数据后,需要根据车型选择通道,具体根据产品定义(如左前、右后)
- 使用MediaCodeC进行编码,建议(aac格式)
- 使用MediaMuxer进行容器封装,保存音频文件,文件大小限制30M,满后结束,并重新开始新文件录制,文件命名:AudioFile_vin号(车架号,车辆的唯一标识)_年月日_开始时分秒_结束时分秒
- 将最终文件存放到android对应目录下,由logService进行文件上传
二、我的编码思路
- 创建项目
- 修改配置文件,依赖公司的maven的仓库
- 申请audio录制、文件读写相关的权限
- 初始化一个服务,录制相关的操作放到servcie中去做
- 检查文件最终存放的目录是否存在,如果不存在,就新创建一个
- 检查配置麦克风相关内容
- 配置AudioRecorder参数,初始化AudioRecorder
- 开始录音,新建一个线程去执行耗时操作,将音频数据通过io流放入到原始pcm文件中
- 结束录音,释放相关的资源,根据车机环境以及录制的时间点等等构建最终的mp4文件名称
- 原始的pcm文件太大,需要对其进行裁剪拼接处理(因为选择的是8通道,录到的是车机所有的声音,而vc1我们需要的是主驾的,即左前的数据)
- 使用MediaCodeC对pcm文件进行编码,添加ADTS头,对文件进行容器封装生成最终的mp4文件
三、具体的编码实践
先看一下最终的目录结构,以及最终的实现效果
- 配置公司对应车型的仓库依赖。配置项目包名、签名、依赖
在setting.gradle添加对应maven依赖
pluginManagement {
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
//添加公司的maven仓库依赖
maven {
allowInsecureProtocol = true
url 'http://nexus3.human-horizons.com:8081/repository/mce-release'
}
}
}
rootProject.name = "SoundRecorder"
include ':app'
在app目录下的build.gradle中配置:包名、签名、dataBinding、项目所需的依赖等等
plugins {
id 'com.android.application'
}
android {
namespace 'com.hryt.soundrecorder'
compileSdk 33
defaultConfig {
applicationId "com.hryt.soundrecorder"
minSdk 30
targetSdk 33
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
signingConfigs {
debug {
storeFile file('hryt.jks')
storePassword 'human-horizons'
keyAlias = 'hryt'
keyPassword 'human-horizons'
}
release {
storeFile file('hryt.jks')
storePassword 'human-horizons'
keyAlias = 'hryt'
keyPassword 'human-horizons'
}
}
dataBinding {
enabled = true
}
buildTypes {
debug {
signingConfig signingConfigs.debug
debuggable true
}
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.release
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
dependencies {
implementation 'androidx.appcompat:appcompat:1.4.1'
implementation 'com.google.android.material:material:1.5.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
implementation 'com.hryt.UIFramework:design3:+'
implementation "com.hryt.app.platform:AppCommon-Logger:+"
implementation "com.hryt.opensdk:com.hht.audio:+"
implementation 'io.reactivex.rxjava3:rxjava:3.0.13'
implementation 'io.reactivex.rxjava3:rxandroid:3.0.0'
implementation "com.hryt.app.platform:AppMW-ConfigRepo:+"
implementation "com.hryt.app.platform:AppMW-CarPowerRepo:+"
}
- 申请权限
app目录下的AndroidManifest.xml中申明使用权限
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
在MainActivity中检查权限,申请权限
private static final String[] PERMISSIONS = {
Manifest.permission.RECORD_AUDIO,
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.MANAGE_EXTERNAL_STORAGE};
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
Logger.tag(TAG).i(" onCreate ");
super.onCreate(savedInstanceState);
checkPermissionAndRequest();
}
private void checkPermissionAndRequest() {
for (int i = 0; i < PERMISSIONS.length; i++) {
if (ContextCompat.checkSelfPermission(this, PERMISSIONS[i]) != PackageManager.PERMISSION_GRANTED) {
Logger.tag(TAG).i(" checkPermissionAndRequest not have permission = " + PERMISSIONS[i]);
ActivityCompat.requestPermissions(this, new String[]{PERMISSIONS[i]}, i);
}
}
}
- 创建服务,用于执行业务操作,使用接口来通信
在AndroidManifest.xml中,service属于四大组件之一,都需要在这个文件中声明
<application
android:name=".SoundRecorderApplication"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/HrytTheme"
tools:targetApi="31">
<service
android:name=".service.SoundRecorderService"
android:enabled="true"
android:exported="false" />
<activity
android:name=".view.MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
定义开始录制以及结束录制的接口
public interface ISoundRecord {
void startRecord();
void stopRecord();
}
在service中定义私有内部类实现这个接口
public class SoundRecorderService extends BaseService {
private static final String TAG = SoundRecorderService.class.getSimpleName();
@Nullable
@Override
public IBinder onBind(Intent intent) {
Logger.tag(TAG).i(" onBind ");
return new SoundRecordBinder();
}
@Override
public boolean onUnbind(Intent intent) {
Logger.tag(TAG).i(" onUnbind ");
return super.onUnbind(intent);
}
//具体的业务代码先不粘贴,免得代码太多影响思路
private class SoundRecordBinder extends Binder implements ISoundRecord {
@Override
public void startRecord() {
//业务代码
}
@Override
public void stopRecord() {
//业务代码
}
}
在viewModel中绑定这个服务,绑定成功后,就可以使用这个接口去调用service中业务代码实现了
public class MainViewModel extends BaseViewModel {
private static final String TAG = MainViewModel.class.getSimpleName();
private static Context context = AppGlobals.getInitialApplication().getApplicationContext();
public MutableLiveData<Boolean> mIsRecord = new MutableLiveData<>(false);
private ISoundRecord mISoundRecord;
private final ServiceConnection connection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
Logger.tag(TAG).i(" onServiceConnected: " + service);
mISoundRecord = (ISoundRecord) service;
}
@Override
public void onServiceDisconnected(ComponentName name) {
Logger.tag(TAG).i(" onServiceDisconnected ");
mISoundRecord = null;
}
};
public void initService() {
Logger.tag(TAG).i(" initService ");
Intent intent = new Intent(context, SoundRecorderService.class);
context.bindService(intent, connection, Context.BIND_AUTO_CREATE);
}
public void unbindService() {
Logger.tag(TAG).i(" unbindService ");
context.unbindService(connection);
}
}
- 检查目录是否存在,初始化carPower,初始化listener,初始化Observer
MainActivity大体全部代码
public class MainActivity extends BaseBVMActivity<ActivityMainBinding, MainViewModel> {
private static final String TAG = MainActivity.class.getSimpleName();
private static final String[] PERMISSIONS = {
Manifest.permission.RECORD_AUDIO,
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.MANAGE_EXTERNAL_STORAGE};
@Override
protected int getLayoutId() {
return R.layout.activity_main;
}
@Override
public void onOverlayChanged(@NonNull Configuration newConfig) {
super.onOverlayChanged(newConfig);
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
Logger.tag(TAG).i(" onCreate ");
super.onCreate(savedInstanceState);
checkPermissionAndRequest();
}
private void checkPermissionAndRequest() {
for (int i = 0; i < PERMISSIONS.length; i++) {
if (ContextCompat.checkSelfPermission(this, PERMISSIONS[i]) != PackageManager.PERMISSION_GRANTED) {
Logger.tag(TAG).i(" checkPermissionAndRequest not have permission = " + PERMISSIONS[i]);
ActivityCompat.requestPermissions(this, new String[]{PERMISSIONS[i]}, i);
}
}
}
@Override
public void onResume() {
Logger.tag(TAG).i(" onResume ");
super.onResume();
}
@Override
protected void initData() {
Logger.tag(TAG).i(" init data ");
binding.setVm(viewModel);
initService();
checkDirExistence();
initListener();
initCarPower();
initObserver();
}
private void initCarPower() {
Logger.tag(TAG).i(" init car power ");
viewModel.initCarPower();
}
private void initService() {
Logger.tag(TAG).i(" initService ");
viewModel.initService();
}
private void checkDirExistence() {
viewModel.checkDirExistence();
}
private void initListener() {
Logger.tag(TAG).i(" initListener ");
binding.clStartAndStopRecord.setOnClickListener(v -> {
if (viewModel.mIsRecord.getValue()) {
viewModel.stopRecord();
} else {
viewModel.startRecord();
MainActivity.this.moveTaskToBack(false);
}
});
}
private void initObserver() {
viewModel.mIsRecord.observe(this, aBoolean -> {
Logger.tag(TAG).i(" initObserver mIsRecord = " + aBoolean);
});
}
@Override
public void onDestroy() {
Logger.tag(TAG).i(" onDestroy ");
viewModel.unbindService();
super.onDestroy();
}
@Override
public void onPause() {
Logger.tag(TAG).i(" onPause ");
super.onPause();
}
@Override
public void onStop() {
Logger.tag(TAG).i(" onStop ");
super.onStop();
}
}
MainViewModel大体全部代码
public class MainViewModel extends BaseViewModel {
private static final String TAG = MainViewModel.class.getSimpleName();
public MutableLiveData<Boolean> mIsRecord = new MutableLiveData<>(false);
private static Context context = AppGlobals.getInitialApplication().getApplicationContext();
private ISoundRecord mISoundRecord;
private final ServiceConnection connection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
Logger.tag(TAG).i(" onServiceConnected: " + service);
mISoundRecord = (ISoundRecord) service;
}
@Override
public void onServiceDisconnected(ComponentName name) {
Logger.tag(TAG).i(" onServiceDisconnected ");
mISoundRecord = null;
}
};
public void initService() {
Logger.tag(TAG).i(" initService ");
Intent intent = new Intent(context, SoundRecorderService.class);
context.bindService(intent, connection, Context.BIND_AUTO_CREATE);
}
public void unbindService() {
Logger.tag(TAG).i(" unbindService ");
context.unbindService(connection);
}
public void startRecord() {
if (mISoundRecord != null) {
mISoundRecord.startRecord();
mIsRecord.setValue(true);
} else {
Logger.tag(TAG).i("startRecord not bind, retry one time");
initService();
}
}
public void stopRecord() {
if (mISoundRecord != null) {
mISoundRecord.stopRecord();
mIsRecord.setValue(false);
} else {
Logger.tag(TAG).i("stopRecord not bind, retry one time");
initService();
}
}
public void checkDirExistence() {
Logger.tag(TAG).i(" checkDirExistence ");
SoundRecordFileUtil.createNewDir(Constants.BASE_PATH + Constants.SOUND_RECORDER);
}
public void initCarPower() {
CarPowerRepository.getInstance(AppGlobals.getInitialApplication())
.registerCarPowerCallBack(new ICarPowerCallBack() {
@Override
public void onCarPowerSuspend() {
}
@Override
public void onCarPowerPrepareShutDown() {
Logger.tag(TAG).i(" onCarPowerPrepareShutDown ");
ICarPowerCallBack.super.onCarPowerPrepareShutDown();
if (Boolean.TRUE.equals(mIsRecord.getValue())) {
stopRecord();
}
}
});
}
}
checkDirExistence对应的工具类(避免影响思路,先展示一部分)
public class SoundRecordFileUtil {
public static void createNewDir(String dirPath) {
File dirFile = new File(dirPath);
if (!dirFile.exists()) {
dirFile.mkdir();
}
}
}
- 开始录制
SoundRecorderService.SoundRecordBinder中的业务代码
private class SoundRecordBinder extends Binder implements ISoundRecord {
private final String audioNamePrefix = "AudioFile";
private volatile boolean isRecording;
private volatile File currentPcmFile;
private volatile long startTime;
@Override
public void startRecord() {
if (isRecording) {
Logger.tag(TAG).i(" startRecord is recording return");
return;
}
AudioFocusManager.getInstance().setMicMute(true);
AudioRecord audioRecord = AudioRecordManager.getInstance().getAudioRecord();
int recordBuffSize = AudioRecordManager.getInstance().getRecordBuffSize();
isRecording = true;
audioRecord.startRecording();
Logger.tag(TAG).i(" startRecord start recording");
RxUtils.handleRecord(Completable.create(emitter -> {
startRecording(audioRecord, recordBuffSize);
}));
}
private void startRecording(AudioRecord audioRecord, int recordBuffSize) {
Logger.tag(TAG).i(" startRecording begin recordBuffSize = " + recordBuffSize);
byte[] data = new byte[recordBuffSize];
startTime = System.currentTimeMillis();
currentPcmFile = new File(Constants.BASE_PATH + startTime + ".pcm");
FileOutputStream fileOutputStream = null;
try {
if (!currentPcmFile.exists()) {
currentPcmFile.createNewFile();
Logger.tag(TAG).i(" create a file name = " + currentPcmFile.getPath());
}
fileOutputStream = new FileOutputStream(currentPcmFile);
} catch (IOException e) {
Logger.tag(TAG).i(" create new file " + e.getMessage());
}
int read;
if (fileOutputStream != null) {
while (isRecording) {
read = audioRecord.read(data, 0, recordBuffSize);
if (AudioRecord.ERROR_INVALID_OPERATION != read) {
try {
fileOutputStream.write(data);
} catch (IOException e) {
Logger.tag(TAG).i("write date " + e.getMessage());
}
}
}
}
try {
fileOutputStream.close();
} catch (IOException e) {
Logger.tag(TAG).i(e.getMessage());
}
}
@Override
public void stopRecord() {
Logger.tag(TAG).i(" stopRecord ");
isRecording = false;
AudioFocusManager.getInstance().setMicMute(false);
AudioRecordManager.getInstance().releaseAudioRecord();
Logger.tag(TAG).i(" init file time ");
Long endTime = System.currentTimeMillis();
//将pcm文件通过aac编码转换为mp4文件
StringBuilder mp4FileNameBuilder = new StringBuilder();
//获取车机vin号
String vin = AppMWConfigRepository.getInstance(AppGlobals.getInitialApplication()).getVin();
String day = RecordDateUtil.millisToString(startTime, RecordDateUtil.YEAR_MONTH_DAY);
String startT = RecordDateUtil.millisToString(startTime, RecordDateUtil.HOUR_MINUTE_SECOND);
String endT = RecordDateUtil.millisToString(endTime, RecordDateUtil.HOUR_MINUTE_SECOND);
mp4FileNameBuilder.append(Constants.BASE_PATH).append(Constants.SOUND_RECORDER).append(File.separator)
.append(audioNamePrefix).append("_").append(vin).append("_").append(day).append("_")
.append(startT).append("_").append(endT).append(".mp4");
Logger.tag(TAG).i("create mp4 file name = " + mp4FileNameBuilder);
//创建转换的实例
PcmToMp4Util pcmToMp4 = new PcmToMp4Util(currentPcmFile, mp4FileNameBuilder.toString());
RxUtils.handleRecord(Completable.create(emitter -> {
Logger.tag(TAG).i(" start original pcm data convert to byte array");
//文件太大,直接将原始的pcm数据转换成字节数组,会导致内存溢出,所以需要将大文件先切割,再转换
List<File> fileList = SoundRecordFileUtil.splitFile(currentPcmFile,
Constants.BASE_PATH, Constants.SMALL_FILE_SIZE);
Logger.tag(TAG).i(" clear original data ");
//将原始的pcm文件的数据清掉
SoundRecordFileUtil.clearFileContent(currentPcmFile);
Logger.tag(TAG).i(" add file list date to pcm file ");
//遍历小文件列表,将数据加入到pcm文件中
SoundRecordFileUtil.addDataToPcm(currentPcmFile, fileList);
Logger.tag(TAG).i(" delete files ");
//将原来的小文件们都删除
SoundRecordFileUtil.deleteFiles(fileList);
Logger.tag(TAG).i(" pcm conversion to mp4 ");
//将pcm文件转换为mp4文件
startPcmToMp4(pcmToMp4);
Logger.tag(TAG).i(" delete original pcm file ");
//最后将pcm文件删除
SoundRecordFileUtil.deleteFile(currentPcmFile);
}));
}
private void startPcmToMp4(PcmToMp4Util pcmToMp4) {
pcmToMp4.startPcmToMp4();
}
}
判断当前是否在录制中,如果在录制中直接return,检查麦克风
if (isRecording) {
Logger.tag(TAG).i(" startRecord is recording return");
return;
}
AudioFocusManager.getInstance().setMicMute(true);
AudioFocusManager单例类,音频以及麦克风
public class AudioFocusManager {
private static final String TAG = AudioFocusManager.class.getSimpleName();
private Context mContext;
private AudioManager mAudioManager;
private static volatile AudioFocusManager mInstance;
private AudioFocusManager() {
}
public static AudioFocusManager getInstance() {
if (mInstance == null) {
synchronized (AudioFocusManager.class) {
if (mInstance == null) {
mInstance = new AudioFocusManager();
}
}
}
return mInstance;
}
public void init(Context context) {
this.mContext = context;
initAudio();
}
private void initAudio() {
mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
Logger.tag(TAG).i(" initAudio microphoneMute = " + isMicrophoneMute());
if (isMicrophoneMute()) {
mAudioManager.setMicrophoneMute(false);
}
}
public void release() {
Logger.tag(TAG).i(" release isMicrophoneMute = " + isMicrophoneMute());
if (isMicrophoneMute()) {
mAudioManager.setMicrophoneMute(false);
}
}
public void setMicMute(boolean micMute) {
Logger.tag(TAG).i(" setMicMute value = " + micMute);
mAudioManager.setMicrophoneMute(micMute);
}
public boolean isMicrophoneMute() {
Logger.tag(TAG).i(" isMicrophoneMute value = " + mAudioManager.isMicrophoneMute());
return mAudioManager.isMicrophoneMute();
}
}
构建AudioRecorder实例,进行录制
AudioRecord audioRecord = AudioRecordManager.getInstance().getAudioRecord();
int recordBuffSize = AudioRecordManager.getInstance().getRecordBuffSize();
isRecording = true;
audioRecord.startRecording();
Logger.tag(TAG).i(" startRecord start recording");
AudioRecordManager单例类,构建音频录制类,以及音频缓冲大小
public class AudioRecordManager {
private static final String TAG = AudioRecordManager.class.getSimpleName();
private AudioRecord mAudioRecord;
private int recordBuffSize;
private static volatile AudioRecordManager mInstance;
private AudioRecordManager() {
}
public static AudioRecordManager getInstance() {
if (mInstance == null) {
synchronized (AudioRecordManager.class) {
if (mInstance == null) {
mInstance = new AudioRecordManager();
}
}
}
return mInstance;
}
private AudioRecord initAudioRecord() {
recordBuffSize = AudioRecord.getMinBufferSize(Constants.CONTENT_SAMPLING_RATE,
Constants.CHANNEL_IN_EIGHT, AudioFormat.ENCODING_PCM_16BIT);
Logger.tag(TAG).i(" initAudioRecord recordBuffSize = " + recordBuffSize);
if (ActivityCompat.checkSelfPermission(AppGlobals.getInitialApplication(),
Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
Logger.tag(TAG).i(" not have permission = " + Manifest.permission.RECORD_AUDIO);
return null;
}
mAudioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC,
Constants.CONTENT_SAMPLING_RATE, Constants.CHANNEL_IN_EIGHT,
AudioFormat.ENCODING_PCM_16BIT, recordBuffSize);
return mAudioRecord;
}
public AudioRecord getAudioRecord() {
Logger.tag(TAG).i(" getAudioRecord mAudioRecord = " + mAudioRecord);
if (mAudioRecord == null) {
mAudioRecord = initAudioRecord();
}
return mAudioRecord;
}
public int getRecordBuffSize() {
Logger.tag(TAG).i(" getRecordBuffSize recordBuffSize = " + recordBuffSize);
return recordBuffSize;
}
public void releaseAudioRecord() {
Logger.tag(TAG).i(" releaseAudioRecord mAudioRecord = " + mAudioRecord);
if (mAudioRecord != null) {
mAudioRecord.stop();
mAudioRecord.release();
mAudioRecord = null;
}
}
}
定义相关常量
public class Constants {
public static final String BASE_PATH = Environment
.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) + File.separator;
public static final String SOUND_RECORDER = "soundrecorder";
//内容的采样率
public static final int CONTENT_SAMPLING_RATE = 44100;
//比特率
public static final int CONTENT_BIT_RATE = 64000;
//缓冲区大小
public static final int BUFFER_SIZE = 65536;
//要求的最低sdk版本
public static final int MIN_SDK_INT = 21;
//八通道
public static final int CHANNEL_IN_EIGHT = AudioFormat.CHANNEL_IN_LEFT
| AudioFormat.CHANNEL_IN_RIGHT
| AudioFormat.CHANNEL_IN_FRONT
| AudioFormat.CHANNEL_IN_BACK
| AudioFormat.CHANNEL_IN_LEFT_PROCESSED
| AudioFormat.CHANNEL_IN_RIGHT_PROCESSED
| AudioFormat.CHANNEL_IN_FRONT_PROCESSED
| AudioFormat.CHANNEL_IN_BACK_PROCESSED;
public static final int BYTE_ARRAY_SIZE = 1024;
public static final int PCM_SPLIT_STEP = 16;
public static final int SMALL_FILE_SIZE = 16777216;
//numbers
public static final int INT_ZERO = 0;
public static final int INT_ONE = 1;
public static final int INT_TWO = 2;
public static final int INT_THREE = 3;
public static final int INT_FOUR = 4;
public static final int INT_FIVE = 5;
public static final int INT_SIX = 6;
public static final int INT_SEVEN = 7;
public static final int INT_EIGHT = 8;
public static final int INT_NINE = 9;
public static final int INT_TEN = 10;
}
新开线程进行字节流写入
RxUtils.handleRecord(Completable.create(emitter -> {
startRecording(audioRecord, recordBuffSize);
}));
private void startRecording(AudioRecord audioRecord, int recordBuffSize) {
Logger.tag(TAG).i(" startRecording begin recordBuffSize = " + recordBuffSize);
byte[] data = new byte[recordBuffSize];
startTime = System.currentTimeMillis();
currentPcmFile = new File(Constants.BASE_PATH + startTime + ".pcm");
FileOutputStream fileOutputStream = null;
try {
if (!currentPcmFile.exists()) {
currentPcmFile.createNewFile();
Logger.tag(TAG).i(" create a file name = " + currentPcmFile.getPath());
}
fileOutputStream = new FileOutputStream(currentPcmFile);
} catch (IOException e) {
Logger.tag(TAG).i(" create new file " + e.getMessage());
}
int read;
if (fileOutputStream != null) {
while (isRecording) {
read = audioRecord.read(data, 0, recordBuffSize);
if (AudioRecord.ERROR_INVALID_OPERATION != read) {
try {
fileOutputStream.write(data);
} catch (IOException e) {
Logger.tag(TAG).i("write date " + e.getMessage());
}
}
}
}
try {
fileOutputStream.close();
} catch (IOException e) {
Logger.tag(TAG).i(e.getMessage());
}
}
rxjava线程工具类
public class RxUtils {
private static final String TAG = RxUtils.class.getSimpleName();
public static void handleRecord(Completable completable) {
Logger.tag(TAG).i(" handleRecord ");
completable.subscribeOn(Schedulers.io()).subscribe();
}
}
- 结束录制
结束录制,释放资源
Logger.tag(TAG).i(" stopRecord ");
isRecording = false;
AudioFocusManager.getInstance().setMicMute(false)
AudioRecordManager.getInstance().releaseAudioRecord();
创建新文件名称
Logger.tag(TAG).i(" init file time ");
Long endTime = System.currentTimeMillis();
//将pcm文件通过aac编码转换为mp4文件
StringBuilder mp4FileNameBuilder = new StringBuilder();
//获取车机vin号
String vin = AppMWConfigRepository.getInstance(AppGlobals.getInitialApplication()).getVin();
String day = RecordDateUtil.millisToString(startTime, RecordDateUtil.YEAR_MONTH_DAY);
String startT = RecordDateUtil.millisToString(startTime, RecordDateUtil.HOUR_MINUTE_SECOND);
String endT = RecordDateUtil.millisToString(endTime, RecordDateUtil.HOUR_MINUTE_SECOND);
mp4FileNameBuilder.append(Constants.BASE_PATH).append(Constants.SOUND_RECORDER).append(File.separator)
.append(audioNamePrefix).append("_").append(vin).append("_").append(day).append("_")
.append(startT).append("_").append(endT).append(".mp4");
Logger.tag(TAG).i("create mp4 file name = " + mp4FileNameBuilder);
RecordDateUtil时间工具类
public class RecordDateUtil {
public static final String YEAR_MONTH_DAY = "yyyyMMdd";
public static final String HOUR_MINUTE_SECOND = "HHmmss";
public static long stringToMillis(String dateStr, String format) {
return DateUtils.stringToMillis(dateStr, format);
}
public static String millisToString(Long timeMillis, String format) {
return DateUtils.millisToString(timeMillis, format);
}
}
创建转换的实例,新开一个线程执行对应的操作,录制的时间比较长,pcm文件录了八个通道的数据,文件特别大,需要对原始文件进行处理,再进行编码转换
16bit的,所以每个通道2个字节。每幀数据就是16个字节。按照上图通道顺序排列的。
//创建转换的实例
PcmToMp4Util pcmToMp4 = new PcmToMp4Util(currentPcmFile, mp4FileNameBuilder.toString());
RxUtils.handleRecord(Completable.create(emitter -> {
Logger.tag(TAG).i(" start original pcm data convert to byte array");
//文件太大,直接将原始的pcm数据转换成字节数组,会导致内存溢出,所以需要将大文件先切割,再转换
List<File> fileList = SoundRecordFileUtil.splitFile(currentPcmFile,
Constants.BASE_PATH, Constants.SMALL_FILE_SIZE);
Logger.tag(TAG).i(" clear original data ");
//将原始的pcm文件的数据清掉
SoundRecordFileUtil.clearFileContent(currentPcmFile);
Logger.tag(TAG).i(" add file list date to pcm file ");
//遍历小文件列表,将数据加入到pcm文件中
SoundRecordFileUtil.addDataToPcm(currentPcmFile, fileList);
Logger.tag(TAG).i(" delete files ");
//将原来的小文件们都删除
SoundRecordFileUtil.deleteFiles(fileList);
Logger.tag(TAG).i(" pcm conversion to mp4 ");
//将pcm文件转换为mp4文件
startPcmToMp4(pcmToMp4);
Logger.tag(TAG).i(" delete original pcm file ");
//最后将pcm文件删除
SoundRecordFileUtil.deleteFile(currentPcmFile);
}));
private void startPcmToMp4(PcmToMp4Util pcmToMp4) {
pcmToMp4.startPcmToMp4();
}
处理pcm原始大文件的工具类
public class SoundRecordFileUtil {
private static final String TAG = SoundRecordFileUtil.class.getSimpleName();
public static void createNewDir(String dirPath) {
File dirFile = new File(dirPath);
if (!dirFile.exists()) {
dirFile.mkdir();
}
}
/**
*
* @param file : 原始的大文件
* @param desPath : 生成的小文件的目录
* @param byteNum : 定义的字节数组的大小
* @return
*/
public static List<File> splitFile(File file, String desPath, int byteNum) {
List<File> fileList = new ArrayList<>();
FileInputStream fis = null;
try {
fis = new FileInputStream(file);
//创建规定大小的byte数组
byte[] b = new byte[byteNum];
int len;
//name为以后的小文件命名做准备
//遍历将大文件读入byte数组中,当byte数组读满后写入对应的小文件中
while ((len = fis.read(b)) != -1) {
File newFile = new File(desPath + System.currentTimeMillis() + ".pcm");
FileOutputStream fos = new FileOutputStream(newFile);
//将byte数组写入对应的小文件中
fos.write(b, 0, len);
fos.flush();
//结束资源
fos.close();
fileList.add(newFile);
}
return fileList;
} catch (IOException e) {
Logger.tag(TAG).i(" IOException " + e.getMessage());
return null;
} finally {
try {
if (fis != null) {
//结束资源
fis.close();
}
} catch (IOException e) {
Logger.tag(TAG).i(" close IOException = " + e.getMessage());
}
}
}
public static void addDataToPcm(File file, List<File> fileList) {
for (int i = 0; i < fileList.size(); i++) {
byte[] bytes = fileConvertToByteArray(fileList.get(i));
byte[] newByteArray = byteArrayConvertToNewByteArray(bytes,
Constants.PCM_SPLIT_STEP, Constants.INT_FOUR, Constants.INT_FIVE);
writeNewDataToFile(file, newByteArray);
}
}
public static byte[] fileConvertToByteArray(File file) {
byte[] data = null;
try {
FileInputStream fis = new FileInputStream(file);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int len;
byte[] buffer = new byte[Constants.BYTE_ARRAY_SIZE];
while ((len = fis.read(buffer)) != -1) {
baos.write(buffer, 0, len);
}
data = baos.toByteArray();
fis.close();
baos.close();
} catch (Exception e) {
Logger.tag(TAG).i(" fileConvertToByteArray exception " + e.getMessage());
}
return data;
}
/**
* @param bytes : 原来的字节数组
* @param step : 截取数据的步长
* @param passageWayOne : 通道的索引1
* @param passageWayTwo : 通道的索引2
* @return
*/
public static byte[] byteArrayConvertToNewByteArray(byte[] bytes, int step,
int passageWayOne, int passageWayTwo) {
byte[] newBytes = new byte[bytes.length / step * Constants.INT_TWO];
for (int i = 0, k = 0; i < bytes.length; i += step, k += Constants.INT_TWO) {
newBytes[k] = bytes[i + passageWayOne];
newBytes[k + 1] = bytes[i + passageWayTwo];
}
return newBytes;
}
public static void clearFileContent(File file) {
try {
if (!file.exists()) {
file.createNewFile();
}
FileWriter fileWriter = new FileWriter(file);
fileWriter.write("");
fileWriter.flush();
fileWriter.close();
} catch (IOException e) {
Logger.tag(TAG).i(" clearFileContent exception " + e.getMessage());
}
}
public static void writeNewDataToFile(File file, byte[] data) {
OutputStream fos = null;
try {
fos = new FileOutputStream(file, true);
fos.write(data);
fos.flush();
} catch (IOException e) {
Logger.tag(TAG).i(" writeNewDataToFile IOException " + e.getMessage());
} finally {
if (fos != null) {
try {
fos.close();
} catch (IOException e) {
Log.i(TAG, " writeNewDataToFile close IOException " + e.getMessage());
}
}
}
}
public static void deleteFiles(List<File> fileList) {
for (int i = 0; i < fileList.size(); i++) {
File file = fileList.get(i);
if (file.exists() && file.isFile()) {
file.delete();
}
}
}
public static boolean deleteFile(File file) {
if (file.exists() && file.isFile()) {
return file.delete();
}
return false;
}
}
将pcm音频文件转换为mp4文件的工具类
public class PcmToMp4Util {
private static final String TAG = PcmToMp4Util.class.getSimpleName();
private File pcmFile, mp4File;
private FileInputStream fis = null;
private FileOutputStream fos = null;
private MediaCodec encodeCodec;
public PcmToMp4Util(File pcmFile, String mp4Path) {
this.pcmFile = pcmFile;
mp4File = new File(mp4Path);
if (!mp4File.exists()) {
try {
mp4File.createNewFile();
Logger.tag(TAG).i(" PCMToAAC acc file create new file");
} catch (IOException e) {
Logger.tag(TAG).i(" createNewFile e = " + e.getMessage());
}
}
}
public void startPcmToMp4() {
if (pcmFile == null || !pcmFile.exists()) {
Logger.tag(TAG).i(" startPcmToMp4 pcm file not exist ");
return;
}
Logger.tag(TAG).i(" start pcm to mp4 ");
try {
//pcm文件获取
fis = new FileInputStream(pcmFile);
fos = new FileOutputStream(mp4File);
/*
*手动构建编码Format,参数含义:mine类型、采样率、通道数量
*设置音频采样率,44100是目前的标准,但是某些设备仍然支持22050,16000,11025
*/
MediaFormat encodeFormat = MediaFormat.createAudioFormat(MediaFormat.MIMETYPE_AUDIO_AAC,
Constants.CONTENT_SAMPLING_RATE, Constants.INT_ONE);
encodeFormat.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC);
//比特率 声音中的比特率是指将模拟声音信号转换成数字声音信号后,单位时间内的二进制数据量,是间接衡量音频质量的一个指标
encodeFormat.setInteger(MediaFormat.KEY_BIT_RATE, Constants.CONTENT_BIT_RATE);
//最大的缓冲区大小,如果inputBuffer大小小于我们定义的缓冲区大小,可能报出缓冲区溢出异常
encodeFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, Constants.BUFFER_SIZE);
//构建编码器
encodeCodec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_AUDIO_AAC);
//数据格式,surface用来渲染解析出来的数据;加密用的对象;标志 encode :1 decode:0
encodeCodec.configure(encodeFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
//用于描述解码得到的byte[]数据的相关信息
MediaCodec.BufferInfo encodeBufferInfo = new MediaCodec.BufferInfo();
//启动编码
encodeCodec.start();
/*
* 同步方式,流程是在while中
* dequeueInputBuffer -> queueInputBuffer填充数据 -> dequeueOutputBuffer -> releaseOutputBuffer
*/
boolean hasAudio = true;
byte[] pcmData = new byte[Constants.BUFFER_SIZE];
while (true) {
//所有的数据都运进数据工厂后不再添加
if (hasAudio) {
//从pcm文件中,获取一组输入缓冲区
ByteBuffer[] inputBuffers = encodeCodec.getInputBuffers();
//返回当前可用的输入缓冲区的索引,参数0表示立即返回,小于0无限等待输入缓冲区的可用性,大于0表示等待的时间
int inputIndex = encodeCodec.dequeueInputBuffer(0);
//如果返回的是-1,表示当前没有可用的缓冲区
if (inputIndex != -1) {
Logger.tag(TAG).i("found the input cart index = " + inputIndex);
//将MediaCodec数据取出来放到这个缓冲区里
ByteBuffer inputBuffer = inputBuffers[inputIndex];
//清除里面旧的东西
inputBuffer.clear();
//将pcm数据读取到字节数组中,返回值为读取到的缓冲区的总字节数
int size = fis.read(pcmData);
//size小于0表示没有更多数据
if (size < 0) {
//当前pcm已经读取完了
Logger.tag(TAG).i("the current pcm has been read completely");
encodeCodec.queueInputBuffer(inputIndex, 0, 0, 0,
MediaCodec.BUFFER_FLAG_END_OF_STREAM);
hasAudio = false;
} else {
inputBuffer.limit(size);
inputBuffer.put(pcmData, 0, size);
Logger.tag(TAG).i("Audio data has been read, "
+ "and the current length of the audio data is:" + size);
//告诉工厂数据的序号、偏移量、大小、演示时间、标志等
encodeCodec.queueInputBuffer(inputIndex, 0, size, 0, 0);
}
} else {
Logger.tag(TAG).i("no available input carts");
}
}
//数据工厂已经把数据运进去了,但是是否加工转换成我们想要的数据(mp4)还是未知的,像输入缓冲流那样对输出缓冲流做类似的操作
int outputIndex = encodeCodec.dequeueOutputBuffer(encodeBufferInfo, 0);
switch (outputIndex) {
case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
Logger.tag(TAG).i("the format of the output has been changed" + encodeCodec.getOutputFormat());
break;
case MediaCodec.INFO_TRY_AGAIN_LATER:
Logger.tag(TAG).i("timed out not obtained");
break;
case MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED:
Logger.tag(TAG).i("output buffer changed");
break;
default:
Logger.tag(TAG).i("The encoded data has been obtained, "
+ "and the current parsed data length is:" + encodeBufferInfo.size);
//获取一组字节输出缓冲区数组
ByteBuffer[] outputBuffers = encodeCodec.getOutputBuffers();
//拿到当前装满数据的字节缓冲区
ByteBuffer outputBuffer;
if (Build.VERSION.SDK_INT >= Constants.MIN_SDK_INT) {
outputBuffer = encodeCodec.getOutputBuffer(outputIndex);
} else {
outputBuffer = outputBuffers[outputIndex];
}
//将数据放到新的容器里,便于后期传输,aac编码中需要ADTS头部,大小为7
int outPacketSize = encodeBufferInfo.size + Constants.INT_SEVEN;
byte[] newAACData = new byte[outPacketSize];
//添加ADTS
addADTStoPacket(newAACData, outPacketSize);
//从ADTS后面开始插入编码后的数据,写入到字节数组中
outputBuffer.get(newAACData, Constants.INT_SEVEN, encodeBufferInfo.size);
outputBuffer.position(encodeBufferInfo.offset);
//清空当前字节输出缓冲区
outputBuffer.clear();
//数据通过io流,写入mp4文件中
fos.write(newAACData);
fos.flush();
//把装载数据的装载物放回数据工厂里面
encodeCodec.releaseOutputBuffer(outputIndex, false);
break;
}
//当前的编码和解码已经完成
if ((encodeBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
Logger.tag(TAG).i("Indicates that the current encoding and decoding have been completed");
break;
}
}
} catch (IOException e) {
Logger.tag(TAG).i(" error msg = " + e.getMessage());
} finally {
if (encodeCodec != null) {
encodeCodec.stop();
encodeCodec.release();
}
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (fos != null) {
try {
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
/**
* 添加ADTS头
*/
private void addADTStoPacket(byte[] packet, int packetLen) {
// CHECKSTYLE.OFF: MagicNumber
// AAC LC
int profile = 2;
// 44.1KHz
int freqIdx = 4;
// CPE
int chanCfg = 2;
// fill in ADTS data
packet[0] = (byte) 0xFF;
packet[1] = (byte) 0xF9;
packet[2] = (byte) (((profile - 1) << 6) + (freqIdx << 2) + (chanCfg >> 2));
packet[3] = (byte) (((chanCfg & 3) << 6) + (packetLen >> 11));
packet[4] = (byte) ((packetLen & 0x7FF) >> 3);
packet[5] = (byte) (((packetLen & 7) << 5) + 0x1F);
packet[6] = (byte) 0xFC;
// CHECKSTYLE.ON: MagicNumber
}
}
四、总结
如果你耐心看到了这里,那么从0到1编写一个录音软件,你差不多就已经掌握了。一千个人有一千个哈姆雷特,每个人对编码的理解应该都不大一样,总体代码还有一些地方可以优化,如果有好的意见或建议非常欢迎提出。