赞
踩
根据所编写的流程,一步一步来,理解所写的内容,一定会有不小的收获
先看一下最终的目录结构,以及最终的实现效果
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:+" }
<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); } } }
<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); } }
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();
}
}
}
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编写一个录音软件,你差不多就已经掌握了。一千个人有一千个哈姆雷特,每个人对编码的理解应该都不大一样,总体代码还有一些地方可以优化,如果有好的意见或建议非常欢迎提出。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。