█ 【安卓学习之第三方库】 ZlwAudioRecorder学习:内部流程(含aar文件生成)
█ 系列文章目录
提示:这里是收集了安卓学习之常见问题的相关文章
- 【安卓学习之MP3】 MP3读取格式
- 【安卓学习之第三方库】 ZlwAudioRecorder学习:内部流程
- 【安卓学习之开源项目】 ParrotTongue:文字转语音(含LeLeTextToVoice、TextToMp3项目)
█ 文章目录
█ 读前说明
- 本文通过学习别人写demo,学习一些课件,参考一些博客,’学习相关知识,如果涉及侵权请告知
- 本文只简单罗列相关的代码实现过程
- 涉及到的逻辑以及说明也只是简单介绍,主要当做笔记,了解过程而已
█ 录音配置文件:RecordConfig
目的是
录制格式 | → | 保存格式 |
---|---|---|
8000Hz、16位、单声道 MP3格式 | → | 8000Hz、8位、单声道 MP3格式 |
RecordConfig.java
public class RecordConfig implements Serializable {
// 录音配置:MP3,录音为16位,wav也是16位
public RecordConfig setFormat(RecordFormat format) {
this.format = format;// 设置为 RecordFormat.MP3(录音和wav格式)
return this;
}
// 转成最终的mp3的位宽:8位
public RecordConfig setEncodingConfig(int encodingConfig) {
this.encodingConfig = encodingConfig;// 设置为 AudioFormat.ENCODING_PCM_8BIT(最终mp3格式)
return this;
}
/**
* 获取当前录音的采样位宽 单位bit ##wav使用:16位
*
* @return 采样位宽 0: error
*/
public int getEncoding() {
if(format == RecordFormat.MP3){//mp3后期转换
return 16;
}
if (encodingConfig == AudioFormat.ENCODING_PCM_8BIT) {
return 8;
} else if (encodingConfig == AudioFormat.ENCODING_PCM_16BIT) {
return 16;
} else {
return 0;
}
}
/**
* 获取当前录音的采样位宽 单位bit ##转最终的mp3:8位
*
* @return 采样位宽 0: error
*/
public int getRealEncoding() {
if (encodingConfig == AudioFormat.ENCODING_PCM_8BIT) {
return 8;
} else if (encodingConfig == AudioFormat.ENCODING_PCM_16BIT) {
return 16;
} else {
return 0;
}
}
// 录音配置,录音时为16位
public int getEncodingConfig() {
if(format == RecordFormat.MP3){//mp3后期转换
return AudioFormat.ENCODING_PCM_16BIT;
}
return encodingConfig;
}
}
█ 对外开放/过渡:RecordManager和RecordService
⚡️ RecordManager
内部定义
public class RecordManager {
private volatile static RecordManager instance;
private Application context;
// 单例模式
private RecordManager() {
}
public static RecordManager getInstance() {
if (instance == null) {
synchronized (RecordManager.class) {
if (instance == null) {
instance = new RecordManager();
}
}
}
return instance;
}
// 录音设置
public void start() {
if (context == null) {
Logger.e(TAG, "未进行初始化");
return;
}
Logger.i(TAG, "start...");
RecordService.startRecording(context);
}
public void stop() {
if (context == null) {
return;
}
RecordService.stopRecording(context);
}
// 修改配置
public boolean changeFormat(RecordConfig.RecordFormat recordFormat) {
return RecordService.changeFormat(recordFormat);
}
public boolean changeRecordConfig(RecordConfig recordConfig) {
return RecordService.changeRecordConfig(recordConfig);
}
}
外部初始化:录音文件的格式
录音是16位(RecordFormat.MP3),录音后转成MP3是8位(AudioFormat.ENCODING_PCM_8BIT)
RecordManager recordManager = RecordManager.getInstance();
recordManager.init(this.getApplicationContext(), BuildConfig.DEBUG);
recordManager.changeFormat(RecordConfig.RecordFormat.MP3);// MP3
recordManager.changeRecordConfig(recordManager.recordConfig.setEncodingConfig(AudioFormat.ENCODING_PCM_8BIT));
recordManager.changeRecordConfig(recordManager.recordConfig.setSampleRate(8000));// 8000Kmz
// 录音结果监听:RecordManager->RecordService->RecordHelper
recordManager.setRecordResultListener(new RecordResultListener() {
@Override
public void onResult(File result) {
}
});
⚡️ RecordService 录音服务
内部定义
public class RecordService extends Service {
private static RecordConfig currentConfig = new RecordConfig();// 录音配置
public static void startRecording(Context context) {
Intent intent = new Intent(context, RecordService.class);
intent.putExtra(ACTION_NAME, ACTION_START_RECORD);
intent.putExtra(PARAM_PATH, getFilePath());// 文件路径
context.startService(intent);
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (intent == null) {
return super.onStartCommand(intent, flags, startId);
}
Bundle bundle = intent.getExtras();
if (bundle != null && bundle.containsKey(ACTION_NAME)) {
switch (bundle.getInt(ACTION_NAME, ACTION_INVALID)) {
case ACTION_START_RECORD:
doStartRecording(bundle.getString(PARAM_PATH));// 开始录音
break;
case ACTION_STOP_RECORD:
doStopRecording();
break;
case ACTION_RESUME_RECORD:
doResumeRecording();
break;
case ACTION_PAUSE_RECORD:
doPauseRecording();
break;
default:
break;
}
return START_STICKY;
}
return super.onStartCommand(intent, flags, startId);
}
private void doStartRecording(String path) {
Logger.v(TAG, "doStartRecording path: %s", path);
RecordHelper.getInstance().start(path, currentConfig);
}
/**
* 改变录音格式
*/
public static boolean changeFormat(RecordConfig.RecordFormat recordFormat) {
if (getState() == RecordHelper.RecordState.IDLE) {
currentConfig.setFormat(recordFormat);
return true;
}
return false;
}
/**
* 改变录音配置
*/
public static boolean changeRecordConfig(RecordConfig recordConfig) {
if (getState() == RecordHelper.RecordState.IDLE) {
currentConfig = recordConfig;
return true;
}
return false;
}
}
█ 封装:RecordHelper
⚡️ RecordHelper
内部定义
public class RecordHelper {
private static final String TAG = RecordHelper.class.getSimpleName();
private volatile static RecordHelper instance;
private volatile RecordState state = RecordState.IDLE;
private static final int RECORD_AUDIO_BUFFER_TIMES = 1;
private RecordStateListener recordStateListener;// null
private RecordDataListener recordDataListener;// null
private RecordSoundSizeListener recordSoundSizeListener;// null
private RecordResultListener recordResultListener;// 录音结果监听:RecordManager->RecordService->RecordHelper
private RecordFftDataListener recordFftDataListener;// null
private RecordConfig currentConfig;// 要保存的mp3的配置,录制格式: MP3,采样率:8000Hz,位宽:16 bit,声道数:1
private AudioRecordThread audioRecordThread;
private Handler mainHandler = new Handler(Looper.getMainLooper());
private File resultFile = null;// 录音文件 转码后的 mp3文件
private File tmpFile = null;// pcm缓存文件(本例中没用到)
private List<File> files = new ArrayList<>();
private Mp3EncodeThread mp3EncodeThread;
private RecordHelper() {
}
static RecordHelper getInstance() {
if (instance == null) {
synchronized (RecordHelper.class) {
if (instance == null) {
instance = new RecordHelper();
}
}
}
return instance;
}
// 录制格式: MP3,采样率:8000Hz,位宽:16 bit,声道数:1
public void start(String filePath, RecordConfig config) {
this.currentConfig = config;// 配置文件
if (state != RecordState.IDLE && state != RecordState.STOP) {
Logger.e(TAG, "状态异常当前状态: %s", state.name());
return;
}
resultFile = new File(filePath);// 录音文件 转码后的 mp3文件
String tempFilePath = getTempFilePath();// pcm缓存文件(本例中没用到)
Logger.d(TAG, "----------------开始录制 %s------------------------", currentConfig.getFormat().name());
Logger.d(TAG, "参数: %s", currentConfig.toString());
Logger.i(TAG, "pcm缓存 tmpFile: %s", tempFilePath);
Logger.i(TAG, "录音文件 resultFile: %s", filePath);
audioRecordThread = new AudioRecordThread();// 启动 Mp3EncodeThread 线程
audioRecordThread.start();// 启动 AudioRecordThread 线程
}
}
█ 开始:AudioRecordThread 和 Mp3EncodeThread
⚡️ AudioRecordThread
private class AudioRecordThread extends Thread {
private AudioRecord audioRecord;
private int bufferSize;
AudioRecordThread() {
// 指定缓冲区大小。不能随便设置,调用getMinBufferSize方法可以获得,这边为640
// sampleRateInHz:8000(音频采样率) channelConfig:16(声道:单通道) audioFormat:2(音频采样精度/位宽:MP3 16位)
bufferSize = AudioRecord.getMinBufferSize(currentConfig.getSampleRate(),
currentConfig.getChannelConfig(), currentConfig.getEncodingConfig()) * RECORD_AUDIO_BUFFER_TIMES;
// D/^_^RecordHelper=============: record buffer size = 640
Logger.d(TAG, "record buffer size = %s", bufferSize);// 640
// MIC:1(音频采集的来源:麦克风) sampleRateInHz:8000(音频采样率) channelConfig:16(声道:单通道)
// audioFormat:2(音频采样精度/位宽:MP3 16位) bufferSizeInBytes:640(采集到的音频数据所存放的缓冲区大小)
// 创建AudioRecord。AudioRecord类实际上不会保存捕获的音频,因此需要手动创建文件并保存下载。
audioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC, currentConfig.getSampleRate(),
currentConfig.getChannelConfig(), currentConfig.getEncodingConfig(), bufferSize);
// 启动 Mp3EncodeThread 线程
if (currentConfig.getFormat() == RecordConfig.RecordFormat.MP3) {
if (mp3EncodeThread == null) {
initMp3EncoderThread(bufferSize);
} else {
Logger.e(TAG, "mp3EncodeThread != null, 请检查代码");
}
}
}
@Override
public void run() {
super.run();
switch (currentConfig.getFormat()) {
case MP3:
startMp3Recorder();
break;
default:
startPcmRecorder();
break;
}
}
private void startMp3Recorder() {
state = RecordState.RECORDING;
notifyState();
try {
audioRecord.startRecording();
short[] byteBuffer = new short[bufferSize];
while (state == RecordState.RECORDING) {
// 由于AudioRecord 在采集数据时会将数据存放到缓冲区中,
// 因此我们只需要创建一个数据流去从缓冲区中将采集的数据读取出来即可。
// 一边从 AudioRecord 中读取音频数据到缓冲区,一边将缓冲区 中数据写入到数据流。
int end = audioRecord.read(byteBuffer, 0, byteBuffer.length);// 最大为bufferSize,即640
if (mp3EncodeThread != null) {
// 通过另一个线程。编码成我们需要的格式
mp3EncodeThread.addChangeBuffer(new Mp3EncodeThread.ChangeBuffer(byteBuffer, end));
}
notifyData(ByteUtils.toBytes(byteBuffer));// short[] 转 byte[]
}
audioRecord.stop();
} catch (Exception e) {
Logger.e(e, TAG, e.getMessage());
notifyError("录音失败");
}
if (state != RecordState.PAUSE) {
state = RecordState.IDLE;
notifyState();
stopMp3Encoded();
} else {
Logger.d(TAG, "暂停");
}
}
}
private void initMp3EncoderThread(int bufferSize) {
try {
// 读取到的数据传递过去,转码,并保存到resultFile中
mp3EncodeThread = new Mp3EncodeThread(resultFile, bufferSize);
mp3EncodeThread.start();
} catch (Exception e) {
Logger.e(e, TAG, e.getMessage());
}
}
⚡️ Mp3EncodeThread
public class Mp3EncodeThread extends Thread {
private static final String TAG = Mp3EncodeThread.class.getSimpleName();
private List<ChangeBuffer> cacheBufferList = Collections.synchronizedList(new LinkedList<ChangeBuffer>());
private File file;
private FileOutputStream os;
private byte[] mp3Buffer;
private EncordFinishListener encordFinishListener;
/**
* 是否已停止录音
*/
private volatile boolean isOver = false;
/**
* 是否继续轮询数据队列
*/
private volatile boolean start = true;
public Mp3EncodeThread(File file, int bufferSize) {
this.file = file;
mp3Buffer = new byte[(int) (7200 + (bufferSize * 2 * 1.25))];// 长度 8800
RecordConfig currentConfig = RecordService.getCurrentConfig();
int sampleRate = currentConfig.getSampleRate();
// 录制格式: MP3,采样率:8000Hz,位宽:16 bit,声道数:1
// W/^_^Mp3EncodeThread==========: in_sampleRate:8000,getChannelCount:1 ,out_sampleRate:8000 位宽: 8,
Logger.w(TAG, "in_sampleRate:%s,getChannelCount:%s ,out_sampleRate:%s 位宽: %s,",
sampleRate, currentConfig.getChannelCount(), sampleRate, currentConfig.getRealEncoding());
// inSampleRate:8000(输入采样率) outChannel:1(输出通道)
// outSampleRate:8000(输出采样率)0 outBitrate:8(输出位宽)
Mp3Encoder.init(sampleRate, currentConfig.getChannelCount(), sampleRate, currentConfig.getRealEncoding());
}
// 保存 录音的数据
public void addChangeBuffer(ChangeBuffer changeBuffer) {
if (changeBuffer != null) {
cacheBufferList.add(changeBuffer);
synchronized (this) {
notify();
}
}
}
@Override
public void run() {
try {
this.os = new FileOutputStream(file);
} catch (FileNotFoundException e) {
Logger.e(e, TAG, e.getMessage());
return;
}
while (start) {
ChangeBuffer next = cacheBufferList.remove(0);// 取出 录音的数据
Logger.v(TAG, "处理数据:%s", next == null ? "null" : next.getReadSize());
lameData(next);// 转换
}
}
private void lameData(ChangeBuffer changeBuffer) {
if (changeBuffer == null) {
return;
}
short[] buffer = changeBuffer.getData();// 长度 640
int readSize = changeBuffer.getReadSize();
if (readSize > 0) {
// buffer_l:左声道输入数据 buffer_r: samples:输入数据的size mp3buf:输出数据
int encodedSize = Mp3Encoder.encode(buffer, buffer, readSize, mp3Buffer);
if (encodedSize < 0) {// 输出到mp3buf的byte数量
Logger.e(TAG, "Lame encoded size: " + encodedSize);
}
try {
os.write(mp3Buffer, 0, encodedSize);// 写入到文件中
} catch (IOException e) {
Logger.e(e, TAG, "Unable to write to file");
}
}
}
// 将MP3结尾信息写入buffer中
private void finish() {
start = false;
final int flushResult = Mp3Encoder.flush(mp3Buffer);// 返回刷写的数量
if (flushResult > 0) {
try {
os.write(mp3Buffer, 0, flushResult);
os.close();
} catch (final IOException e) {
Logger.e(TAG, e.getMessage());
}
}
Logger.d(TAG, "转换结束 :%s", file.length());
if (encordFinishListener != null) {
encordFinishListener.onFinish();
}
}
}
█ 录音过程打印的数据信息
⚡️ 通过 Lame库 编码生成 MP3 音频文件。
Mp3Encoder的方法
public class Mp3Encoder{
static {
System.loadLibrary("mp3lame");
}
/**
* 初始化 lame编码器
*
* @param inSampleRate 输入采样率
* @param outChannel 声道数
* @param outSampleRate 输出采样率
* @param outBitrate 比特率(kbps)
* @param quality 0~9,0最好,这里采用默认值 7
*/
public static native void init(int inSampleRate, int outChannel, int outSampleRate, int outBitrate, int quality);
/**
* 编码,把 AudioRecord 录制的 PCM 数据转换成 mp3 格式
*
* @param buffer_l 左声道输入数据
* @param buffer_r 右声道输入数据
* @param samples 输入数据的size
* @param mp3buf 输出数据
* @return 输出到mp3buf的byte数量
*/
public static native int encode(short[] buffer_l, short[] buffer_r, int samples, byte[] mp3buf);
/**
* 刷写
*
* @param mp3buf mp3数据缓存区
* @return 返回刷写的数量
*/
public static native int flush(byte[] mp3buf);
/**
* 关闭 lame 编码器,释放资源
*/
public static native void close();
}
⚡️ 打印的数据信息
V/^_^RecordService============: doStartRecording path:
/storage/emulated/0/Record/com.xxx.xxx.debug/record_20210811_10_54_18.mp3
D/^_^RecordHelper=============: ----------------开始录制 MP3------------------------
D/^_^RecordHelper=============: 参数: 录制格式: MP3,采样率:8000Hz,位宽:16 bit,声道数:1
I/^_^RecordHelper=============: pcm缓存 tmpFile: /storage/emulated/0/Record/record_tmp_20210811_10_54_18.pcm
I/^_^RecordHelper=============: 录音文件 resultFile:
/storage/emulated/0/Record/com.xxx.xxx.debug/record_20210811_10_54_18.mp3
D/^_^RecordHelper=============: record buffer size = 640
D/audio_hw_primary: adev_open_input_stream: enter: sample_rate(8000) channel_mask(0x10) devices(0x80000004)
stream_handle(0xe896b800) io_handle(110) source(1) format 1
W/^_^Mp3EncodeThread==========: in_sampleRate:8000,getChannelCount:1 ,out_sampleRate:8000 位宽: 8,
I/msm8916_platform: platform_check_capture_codec_backend_cfg:txbecf: afe: Codec selected backend: 7
current bit width: 16 and sample rate: 8000, channels 1 format 1
I/msm8916_platform: platform_check_capture_codec_backend_cfg:txbecf: afe: Codec selected backend: 7
updated bit width: 16 and sample rate: 48000
D/ACDB-LOADER: ACDB -> send_audio_cal, acdb_id = 4, path = 1, app id = 0x11132, sample rate = 48000
V/^_^Mp3EncodeThread==========: 处理数据:640
I/chatty: uid=10072(com.xxx.xxx.debug) Thread-39 identical 2 lines
V/^_^Mp3EncodeThread==========: 处理数据:640
V/^_^Mp3EncodeThread==========: 处理数据:640
I/chatty: uid=10072(com.xxx.xxx.debug) Thread-39 identical 9 lines
V/^_^Mp3EncodeThread==========: 处理数据:640
D/audio_hw_primary: in_set_parameters: enter: kvpairs=routing=0
I/SoundTriggerHwService::Module: onCallbackEvent no clients
D/^_^Mp3EncodeThread==========: 转换结束 :1512
D/^_^RecordHelper=============: 录音结束 file: /storage/emulated/0/Record/com.xxx.xxx.debug/record_20210811_10_54_18.mp3
█ Assemble 生成aar文件
1.android studio 设置
2.生成aar文件(当你引用SDK接口运行的时候找不到aar中引用的三方库,需要在你的项目gradle中重新引入依赖)
█ 相关资料
提示:这里是参考的相关文章
- 2020-12-10 zhaolewei/ZlwAudioRecorder: AudioRecorder: Android 录音及录音可视化相关lib,支持pcm、wav、mp3音频的录制 :如640个字节读出,640个字节转换
- 2018-12-19 AudioRecord的基本参数_u010029439的博客-CSDN博客
- 2019.08.01 录制MP3格式的音频( lame 库的编译及使用) - 简书:MP3Recorder System.loadLibrary(“mp3lame”);
- 2018.09.19 Android录制语音文件wav转mp3 - 简书:Lame库/AndroidAudioConverter/Recorder-Android
- Android 录音(录音时为pcm,然后转为MP3)_plx_csdn的博客-CSDN博客_android pcm转mp3:全部读出,全部转换
- 2019-01-17 Android中Pcm文件转换为Mp3_飞翔的匹诺曹的博客-CSDN博客_android pcm转mp3:直接转mp3会出现噪音。。因为安卓字节是小端排序,lame是大端排序,所以需要转换
- 2022-02-25 Android studio 生成ARR包_Joey789的博客-CSDN博客_android 生成arr
- 2022-02-25 Gradle中,没有Assemble任务(Task)_Joey789的博客-CSDN博客
- 解决aar无法引用第三方远程依赖库问题 _ 开发小记 _ 建站知识 _ 顺晟科技
10.2018.12.23 Android SDK开发初体验 --aar打包无法引用三方依赖问题 - 简书
█ 免责声明
博主分享的所有文章内容,部分参考网上教程,引用大神高论,部分亲身实践,记下笔录,内容可能存在诸多不实之处,还望海涵,本内容仅供学习研究使用,切勿用于商业用途,若您是部分内容的作者,不喜欢此内容被分享出来,可联系博主说明相关情况通知删除,感谢您的理解与支持! |
---|
提示:转载请注明出处:
https://blog.csdn.net/ljb568838953/article/details/116142977