BadCase
录音功能
This commit is contained in:
@@ -0,0 +1,179 @@
|
||||
package com.zhjt.mogo_core_function_devatools.badcase.record;
|
||||
|
||||
import android.media.AudioFormat;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Locale;
|
||||
|
||||
|
||||
public class RecordConfig implements Serializable {
|
||||
/**
|
||||
* 录音格式 默认WAV格式
|
||||
*/
|
||||
private RecordFormat format = RecordFormat.WAV;
|
||||
/**
|
||||
* 通道数:默认双通道
|
||||
*/
|
||||
private int channelConfig = AudioFormat.CHANNEL_IN_STEREO;
|
||||
|
||||
/**
|
||||
* 位宽
|
||||
*/
|
||||
private int encodingConfig = AudioFormat.ENCODING_PCM_16BIT;
|
||||
|
||||
/**
|
||||
* 采样率
|
||||
*/
|
||||
private int sampleRate = 16000;
|
||||
|
||||
|
||||
|
||||
public RecordConfig() {
|
||||
}
|
||||
|
||||
public RecordConfig(RecordFormat format) {
|
||||
this.format = format;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param format 录音文件的格式
|
||||
* @param channelConfig 声道配置
|
||||
* 单声道:See {@link AudioFormat#CHANNEL_IN_MONO}
|
||||
* 双声道:See {@link AudioFormat#CHANNEL_IN_STEREO}
|
||||
* @param encodingConfig 位宽配置
|
||||
* 8Bit: See {@link AudioFormat#ENCODING_PCM_8BIT}
|
||||
* 16Bit: See {@link AudioFormat#ENCODING_PCM_16BIT},
|
||||
* @param sampleRate 采样率 hz: 8000/16000/44100
|
||||
*/
|
||||
public RecordConfig(RecordFormat format, int channelConfig, int encodingConfig, int sampleRate) {
|
||||
this.format = format;
|
||||
this.channelConfig = channelConfig;
|
||||
this.encodingConfig = encodingConfig;
|
||||
this.sampleRate = sampleRate;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 获取当前录音的采样位宽 单位bit
|
||||
*
|
||||
* @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
|
||||
*
|
||||
* @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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前的声道数
|
||||
*
|
||||
* @return 声道数: 0:error
|
||||
*/
|
||||
public int getChannelCount() {
|
||||
if (channelConfig == AudioFormat.CHANNEL_IN_MONO) {
|
||||
return 1;
|
||||
} else if (channelConfig == AudioFormat.CHANNEL_IN_STEREO) {
|
||||
return 2;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
//get&set
|
||||
|
||||
public RecordFormat getFormat() {
|
||||
return format;
|
||||
}
|
||||
|
||||
public RecordConfig setFormat(RecordFormat format) {
|
||||
this.format = format;
|
||||
return this;
|
||||
}
|
||||
|
||||
public int getChannelConfig() {
|
||||
return channelConfig;
|
||||
}
|
||||
|
||||
public RecordConfig setChannelConfig(int channelConfig) {
|
||||
this.channelConfig = channelConfig;
|
||||
return this;
|
||||
}
|
||||
|
||||
public int getEncodingConfig() {
|
||||
if (format == RecordFormat.MP3) {//mp3后期转换
|
||||
return AudioFormat.ENCODING_PCM_16BIT;
|
||||
}
|
||||
return encodingConfig;
|
||||
}
|
||||
|
||||
public RecordConfig setEncodingConfig(int encodingConfig) {
|
||||
this.encodingConfig = encodingConfig;
|
||||
return this;
|
||||
}
|
||||
|
||||
public int getSampleRate() {
|
||||
return sampleRate;
|
||||
}
|
||||
|
||||
public RecordConfig setSampleRate(int sampleRate) {
|
||||
this.sampleRate = sampleRate;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format(Locale.getDefault(), "录制格式: %s,采样率:%sHz,位宽:%s bit,声道数:%s", format, sampleRate, getEncoding(), getChannelCount());
|
||||
}
|
||||
|
||||
public enum RecordFormat {
|
||||
/**
|
||||
* mp3格式
|
||||
*/
|
||||
MP3(".mp3"),
|
||||
/**
|
||||
* wav格式
|
||||
*/
|
||||
WAV(".wav"),
|
||||
/**
|
||||
* pcm格式
|
||||
*/
|
||||
PCM(".pcm");
|
||||
|
||||
private String extension;
|
||||
|
||||
public String getExtension() {
|
||||
return extension;
|
||||
}
|
||||
|
||||
RecordFormat(String extension) {
|
||||
this.extension = extension;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,423 @@
|
||||
package com.zhjt.mogo_core_function_devatools.badcase.record;
|
||||
|
||||
import android.media.AudioRecord;
|
||||
import android.media.MediaRecorder;
|
||||
import android.os.Environment;
|
||||
import android.util.Log;
|
||||
|
||||
import com.mogo.eagle.core.utilcode.mogo.logger.CallerLogger;
|
||||
import com.mogo.eagle.core.utilcode.mogo.logger.Logger;
|
||||
import com.mogo.eagle.core.utilcode.util.FileUtils;
|
||||
import com.zhjt.mogo_core_function_devatools.badcase.record.fft.FftFactory;
|
||||
import com.zhjt.mogo_core_function_devatools.badcase.record.listener.RecordListener;
|
||||
import com.zhjt.mogo_core_function_devatools.badcase.record.mp3.Mp3EncodeThread;
|
||||
import com.zhjt.mogo_core_function_devatools.badcase.record.utils.ByteUtils;
|
||||
import com.zhjt.mogo_core_function_devatools.badcase.record.utils.WavUtils;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.BufferedOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
|
||||
public class RecordHelper {
|
||||
private static final String TAG = RecordHelper.class.getSimpleName();
|
||||
private volatile RecordState state = RecordState.IDLE;
|
||||
private static final int RECORD_AUDIO_BUFFER_TIMES = 1;
|
||||
/*
|
||||
* 录音文件存放路径
|
||||
*/
|
||||
public static final String ROOT_PATH = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + "Mogo" + File.separator + "DataCollection" + File.separator;//程序外部存储跟目录
|
||||
private static final String TEMP_PATH = ROOT_PATH + "temp" + File.separator;
|
||||
private RecordListener listener;
|
||||
private final RecordConfig currentConfig;
|
||||
private AudioRecordThread audioRecordThread;
|
||||
|
||||
private File resultFile = null;
|
||||
private File tmpFile = null;
|
||||
private List<File> files = new ArrayList<>();
|
||||
private Mp3EncodeThread mp3EncodeThread;
|
||||
|
||||
public RecordHelper(RecordConfig config) {
|
||||
this.currentConfig = config;
|
||||
}
|
||||
|
||||
|
||||
public RecordState getState() {
|
||||
return state;
|
||||
}
|
||||
|
||||
|
||||
public void registerRecordListener(RecordListener listener) {
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
public void unregisterRecordListener() {
|
||||
this.listener = null;
|
||||
}
|
||||
|
||||
public void start(String fileName) {
|
||||
|
||||
if (state != RecordState.IDLE && state != RecordState.STOP) {
|
||||
return;
|
||||
}
|
||||
String path = getFilePath(fileName);
|
||||
resultFile = new File(path);
|
||||
String tempFilePath = getTempFilePath();
|
||||
tmpFile = new File(tempFilePath);
|
||||
audioRecordThread = new AudioRecordThread();
|
||||
audioRecordThread.start();
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
if (state == RecordState.IDLE) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (state == RecordState.PAUSE) {
|
||||
makeFile();
|
||||
state = RecordState.IDLE;
|
||||
notifyState();
|
||||
stopMp3Encoded();
|
||||
} else {
|
||||
state = RecordState.STOP;
|
||||
notifyState();
|
||||
}
|
||||
}
|
||||
|
||||
public void pause() {
|
||||
if (state != RecordState.RECORDING) {
|
||||
return;
|
||||
}
|
||||
state = RecordState.PAUSE;
|
||||
notifyState();
|
||||
}
|
||||
|
||||
public void resume() {
|
||||
if (state != RecordState.PAUSE) {
|
||||
return;
|
||||
}
|
||||
String tempFilePath = getTempFilePath();
|
||||
tmpFile = new File(tempFilePath);
|
||||
audioRecordThread = new AudioRecordThread();
|
||||
audioRecordThread.start();
|
||||
}
|
||||
|
||||
private void notifyState() {
|
||||
if (listener == null) {
|
||||
return;
|
||||
}
|
||||
listener.onStateChange(state);
|
||||
if (state == RecordState.STOP || state == RecordState.PAUSE) {
|
||||
listener.onSoundSize(0);
|
||||
}
|
||||
}
|
||||
|
||||
private void notifyFinish() {
|
||||
if (listener != null) {
|
||||
listener.onStateChange(RecordState.FINISH);
|
||||
listener.onResult(resultFile);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void notifyError(final String error) {
|
||||
if (listener != null) {
|
||||
listener.onError(error);
|
||||
}
|
||||
}
|
||||
|
||||
private FftFactory fftFactory = new FftFactory(FftFactory.Level.Original);
|
||||
|
||||
private void notifyData(final byte[] data) {
|
||||
if (listener != null) {
|
||||
listener.onData(data);
|
||||
byte[] fftData = fftFactory.makeFftData(data);
|
||||
if (fftData != null) {
|
||||
listener.onSoundSize(getDb(fftData));
|
||||
listener.onFftData(fftData);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private int getDb(byte[] data) {
|
||||
double sum = 0;
|
||||
double ave;
|
||||
int length = Math.min(data.length, 128);
|
||||
int offsetStart = 8;
|
||||
for (int i = offsetStart; i < length; i++) {
|
||||
sum += data[i];
|
||||
}
|
||||
ave = (sum / (length - offsetStart)) * 65536 / 128f;
|
||||
int i = (int) (Math.log10(ave) * 20);
|
||||
return i < 0 ? 27 : i;
|
||||
}
|
||||
|
||||
private void initMp3EncoderThread(int bufferSize) {
|
||||
try {
|
||||
mp3EncodeThread = new Mp3EncodeThread(resultFile, bufferSize, currentConfig);
|
||||
mp3EncodeThread.start();
|
||||
} catch (Exception e) {
|
||||
// Log.e(e, TAG, e.getMessage());
|
||||
CallerLogger.INSTANCE.d("$M_DEVA$TAG", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private class AudioRecordThread extends Thread {
|
||||
private final AudioRecord audioRecord;
|
||||
private int bufferSize;
|
||||
|
||||
AudioRecordThread() {
|
||||
bufferSize = AudioRecord.getMinBufferSize(currentConfig.getSampleRate(),
|
||||
currentConfig.getChannelConfig(), currentConfig.getEncodingConfig()) * RECORD_AUDIO_BUFFER_TIMES;
|
||||
Logger.d(TAG, "record buffer size = %s", bufferSize);
|
||||
audioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC, currentConfig.getSampleRate(),
|
||||
currentConfig.getChannelConfig(), currentConfig.getEncodingConfig(), bufferSize);
|
||||
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 startPcmRecorder() {
|
||||
state = RecordState.RECORDING;
|
||||
notifyState();
|
||||
Logger.d(TAG, "开始录制 Pcm");
|
||||
FileOutputStream fos = null;
|
||||
try {
|
||||
fos = new FileOutputStream(tmpFile);
|
||||
audioRecord.startRecording();
|
||||
byte[] byteBuffer = new byte[bufferSize];
|
||||
|
||||
while (state == RecordState.RECORDING) {
|
||||
int end = audioRecord.read(byteBuffer, 0, byteBuffer.length);
|
||||
notifyData(byteBuffer);
|
||||
fos.write(byteBuffer, 0, end);
|
||||
fos.flush();
|
||||
}
|
||||
audioRecord.stop();
|
||||
files.add(tmpFile);
|
||||
if (state == RecordState.STOP) {
|
||||
makeFile();
|
||||
} else {
|
||||
Logger.i(TAG, "暂停!");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
notifyError("录音失败");
|
||||
} finally {
|
||||
try {
|
||||
if (fos != null) {
|
||||
fos.close();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
if (state != RecordState.PAUSE) {
|
||||
state = RecordState.IDLE;
|
||||
notifyState();
|
||||
Logger.d(TAG, "录音结束");
|
||||
}
|
||||
}
|
||||
|
||||
private void startMp3Recorder() {
|
||||
state = RecordState.RECORDING;
|
||||
notifyState();
|
||||
|
||||
try {
|
||||
audioRecord.startRecording();
|
||||
short[] byteBuffer = new short[bufferSize];
|
||||
|
||||
while (state == RecordState.RECORDING) {
|
||||
int end = audioRecord.read(byteBuffer, 0, byteBuffer.length);
|
||||
if (mp3EncodeThread != null) {
|
||||
mp3EncodeThread.addChangeBuffer(new Mp3EncodeThread.ChangeBuffer(byteBuffer, end));
|
||||
}
|
||||
notifyData(ByteUtils.toBytes(byteBuffer));
|
||||
}
|
||||
audioRecord.stop();
|
||||
} catch (Exception e) {
|
||||
notifyError("录音失败");
|
||||
}
|
||||
if (state != RecordState.PAUSE) {
|
||||
state = RecordState.IDLE;
|
||||
notifyState();
|
||||
stopMp3Encoded();
|
||||
} else {
|
||||
Logger.d(TAG, "暂停");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void stopMp3Encoded() {
|
||||
if (mp3EncodeThread != null) {
|
||||
mp3EncodeThread.stopSafe(new Mp3EncodeThread.EncordFinishListener() {
|
||||
@Override
|
||||
public void onFinish() {
|
||||
notifyFinish();
|
||||
mp3EncodeThread = null;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
Logger.e(TAG, "mp3EncodeThread is null, 代码业务流程有误,请检查!! ");
|
||||
}
|
||||
}
|
||||
|
||||
private void makeFile() {
|
||||
switch (currentConfig.getFormat()) {
|
||||
case MP3:
|
||||
return;
|
||||
case WAV:
|
||||
mergePcmFile();
|
||||
makeWav();
|
||||
break;
|
||||
case PCM:
|
||||
mergePcmFile();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
notifyFinish();
|
||||
Logger.i(TAG, "录音完成! path: %s ; 大小:%s", resultFile.getAbsoluteFile(), resultFile.length());
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加Wav头文件
|
||||
*/
|
||||
private void makeWav() {
|
||||
if (!FileUtils.isFile(resultFile) || resultFile.length() == 0) {
|
||||
return;
|
||||
}
|
||||
byte[] header = WavUtils.generateWavFileHeader((int) resultFile.length(), currentConfig.getSampleRate(), currentConfig.getChannelCount(), currentConfig.getEncoding());
|
||||
WavUtils.writeHeader(resultFile, header);
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并文件
|
||||
*/
|
||||
private void mergePcmFile() {
|
||||
boolean mergeSuccess = mergePcmFiles(resultFile, files);
|
||||
if (!mergeSuccess) {
|
||||
notifyError("合并失败");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并Pcm文件
|
||||
*
|
||||
* @param recordFile 输出文件
|
||||
* @param files 多个文件源
|
||||
* @return 是否成功
|
||||
*/
|
||||
private boolean mergePcmFiles(File recordFile, List<File> files) {
|
||||
if (recordFile == null || files == null || files.size() <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
FileOutputStream fos = null;
|
||||
BufferedOutputStream outputStream = null;
|
||||
byte[] buffer = new byte[1024];
|
||||
try {
|
||||
fos = new FileOutputStream(recordFile);
|
||||
outputStream = new BufferedOutputStream(fos);
|
||||
|
||||
for (int i = 0; i < files.size(); i++) {
|
||||
BufferedInputStream inputStream = new BufferedInputStream(new FileInputStream(files.get(i)));
|
||||
int readCount;
|
||||
while ((readCount = inputStream.read(buffer)) > 0) {
|
||||
outputStream.write(buffer, 0, readCount);
|
||||
}
|
||||
inputStream.close();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, e.getMessage());
|
||||
return false;
|
||||
} finally {
|
||||
try {
|
||||
if (outputStream != null) {
|
||||
outputStream.close();
|
||||
}
|
||||
if (fos != null) {
|
||||
fos.close();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < files.size(); i++) {
|
||||
files.get(i).delete();
|
||||
}
|
||||
files.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
private String getFilePath(String fileName) {
|
||||
if (!FileUtils.createOrExistsDir(ROOT_PATH)) {
|
||||
Logger.w(TAG, "文件夹创建失败:%s", ROOT_PATH);
|
||||
return null;
|
||||
}
|
||||
|
||||
String format = currentConfig.getFormat().getExtension();
|
||||
String filePath = String.format(Locale.getDefault(), "%s%s%s", ROOT_PATH, fileName, format);
|
||||
return filePath;
|
||||
}
|
||||
|
||||
private String getTempFilePath() {
|
||||
if (!FileUtils.createOrExistsDir(TEMP_PATH)) {
|
||||
Logger.e(TAG, "文件夹创建失败:%s", TEMP_PATH);
|
||||
}
|
||||
String fileName = String.format(Locale.getDefault(), "tmp_%s", FileUtils.getNowString(new SimpleDateFormat("yyyyMMddHHmmssSSS", Locale.SIMPLIFIED_CHINESE)));
|
||||
return String.format(Locale.getDefault(), "%s%s.pcm", TEMP_PATH, fileName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 表示当前状态
|
||||
*/
|
||||
public enum RecordState {
|
||||
/**
|
||||
* 空闲状态
|
||||
*/
|
||||
IDLE,
|
||||
/**
|
||||
* 录音中
|
||||
*/
|
||||
RECORDING,
|
||||
/**
|
||||
* 暂停中
|
||||
*/
|
||||
PAUSE,
|
||||
/**
|
||||
* 正在停止
|
||||
*/
|
||||
STOP,
|
||||
/**
|
||||
* 录音流程结束(转换结束)
|
||||
*/
|
||||
FINISH
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package com.zhjt.mogo_core_function_devatools.badcase.record;
|
||||
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
|
||||
import com.zhjt.mogo_core_function_devatools.badcase.record.listener.RecordListener;
|
||||
|
||||
|
||||
public class RecordManager {
|
||||
private static final String TAG = RecordManager.class.getSimpleName();
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
private volatile static RecordManager instance;
|
||||
private final RecordHelper recordHelper;
|
||||
/**
|
||||
* 录音配置
|
||||
*/
|
||||
private final RecordConfig currentConfig = new RecordConfig();
|
||||
|
||||
private RecordManager() {
|
||||
recordHelper = new RecordHelper(currentConfig);
|
||||
}
|
||||
|
||||
public static RecordManager getInstance() {
|
||||
if (instance == null) {
|
||||
synchronized (RecordManager.class) {
|
||||
if (instance == null) {
|
||||
instance = new RecordManager();
|
||||
}
|
||||
}
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
// /**
|
||||
// * @param showLog 是否开启日志
|
||||
// */
|
||||
// public void setISDebug(boolean showLog) {
|
||||
// Logger.IsDebug = showLog;
|
||||
// }
|
||||
|
||||
public void start(String fileName) {
|
||||
recordHelper.start(fileName);
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
recordHelper.stop();
|
||||
}
|
||||
|
||||
public void resume() {
|
||||
recordHelper.resume();
|
||||
}
|
||||
|
||||
public void pause() {
|
||||
recordHelper.pause();
|
||||
}
|
||||
|
||||
|
||||
public RecordConfig getRecordConfig() {
|
||||
return currentConfig;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 录音数据监听回调
|
||||
*/
|
||||
public void registerRecordListener(RecordListener listener) {
|
||||
recordHelper.registerRecordListener(listener);
|
||||
}
|
||||
public void unregisterRecordListener() {
|
||||
recordHelper.unregisterRecordListener();
|
||||
}
|
||||
|
||||
public boolean changeFormat(RecordConfig.RecordFormat recordFormat) {
|
||||
if (getState() == RecordHelper.RecordState.IDLE) {
|
||||
currentConfig.setFormat(recordFormat);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean changeSampleRate(int sampleRate) {
|
||||
if (getState() == RecordHelper.RecordState.IDLE) {
|
||||
currentConfig.setSampleRate(sampleRate);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean changeEncodingConfig(int encodingConfig) {
|
||||
if (getState() == RecordHelper.RecordState.IDLE) {
|
||||
currentConfig.setEncodingConfig(encodingConfig);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前的录音状态
|
||||
*
|
||||
* @return 状态
|
||||
*/
|
||||
public RecordHelper.RecordState getState() {
|
||||
return recordHelper.getState();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
package com.zhjt.mogo_core_function_devatools.badcase.record.fft;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 复数
|
||||
*
|
||||
* @author test
|
||||
*/
|
||||
public class Complex {
|
||||
|
||||
/**
|
||||
* 实数部分
|
||||
*/
|
||||
private final double real;
|
||||
|
||||
/**
|
||||
* 虚数部分 imaginary
|
||||
*/
|
||||
private final double im;
|
||||
|
||||
public Complex(double real, double imag) {
|
||||
this.real = real;
|
||||
im = imag;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("hypot: %s, complex: %s+%si", hypot(), real, im);
|
||||
}
|
||||
|
||||
public double hypot() {
|
||||
return Math.hypot(real, im);
|
||||
}
|
||||
|
||||
public double phase() {
|
||||
return Math.atan2(im, real);
|
||||
}
|
||||
|
||||
/**
|
||||
* 复数求和
|
||||
*/
|
||||
public Complex plus(Complex b) {
|
||||
double real = this.real + b.real;
|
||||
double imag = this.im + b.im;
|
||||
return new Complex(real, imag);
|
||||
}
|
||||
|
||||
// return a new Complex object whose value is (this - b)
|
||||
public Complex minus(Complex b) {
|
||||
double real = this.real - b.real;
|
||||
double imag = this.im - b.im;
|
||||
return new Complex(real, imag);
|
||||
}
|
||||
|
||||
// return a new Complex object whose value is (this * b)
|
||||
public Complex times(Complex b) {
|
||||
Complex a = this;
|
||||
double real = a.real * b.real - a.im * b.im;
|
||||
double imag = a.real * b.im + a.im * b.real;
|
||||
return new Complex(real, imag);
|
||||
}
|
||||
|
||||
// return a new object whose value is (this * alpha)
|
||||
public Complex scale(double alpha) {
|
||||
return new Complex(alpha * real, alpha * im);
|
||||
}
|
||||
|
||||
// return a new Complex object whose value is the conjugate of this
|
||||
public Complex conjugate() {
|
||||
return new Complex(real, -im);
|
||||
}
|
||||
|
||||
// return a new Complex object whose value is the reciprocal of this
|
||||
public Complex reciprocal() {
|
||||
double scale = real * real + im * im;
|
||||
return new Complex(real / scale, -im / scale);
|
||||
}
|
||||
|
||||
// return the real or imaginary part
|
||||
public double re() {
|
||||
return real;
|
||||
}
|
||||
|
||||
public double im() {
|
||||
return im;
|
||||
}
|
||||
|
||||
// return a / b
|
||||
public Complex divides(Complex b) {
|
||||
Complex a = this;
|
||||
return a.times(b.reciprocal());
|
||||
}
|
||||
|
||||
// return a new Complex object whose value is the complex exponential of this
|
||||
public Complex exp() {
|
||||
return new Complex(Math.exp(real) * Math.cos(im), Math.exp(real) * Math.sin(im));
|
||||
}
|
||||
|
||||
// return a new Complex object whose value is the complex sine of this
|
||||
public Complex sin() {
|
||||
return new Complex(Math.sin(real) * Math.cosh(im), Math.cos(real) * Math.sinh(im));
|
||||
}
|
||||
|
||||
// return a new Complex object whose value is the complex cosine of this
|
||||
public Complex cos() {
|
||||
return new Complex(Math.cos(real) * Math.cosh(im), -Math.sin(real) * Math.sinh(im));
|
||||
}
|
||||
|
||||
// return a new Complex object whose value is the complex tangent of this
|
||||
public Complex tan() {
|
||||
return sin().divides(cos());
|
||||
}
|
||||
|
||||
|
||||
// a static version of plus
|
||||
public static Complex plus(Complex a, Complex b) {
|
||||
double real = a.real + b.real;
|
||||
double imag = a.im + b.im;
|
||||
Complex sum = new Complex(real, imag);
|
||||
return sum;
|
||||
}
|
||||
|
||||
// See Section 3.3.
|
||||
@Override
|
||||
public boolean equals(Object x) {
|
||||
if (x == null) return false;
|
||||
if (this.getClass() != x.getClass()) return false;
|
||||
Complex that = (Complex) x;
|
||||
return (this.real == that.real) && (this.im == that.im);
|
||||
}
|
||||
|
||||
// See Section 3.3.
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(real, im);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
package com.zhjt.mogo_core_function_devatools.badcase.record.fft;
|
||||
|
||||
|
||||
public class FFT {
|
||||
|
||||
// compute the FFT of x[], assuming its length is a power of 2
|
||||
public static Complex[] fft(Complex[] x) {
|
||||
int n = x.length;
|
||||
|
||||
// base case
|
||||
if (n == 1) return new Complex[]{x[0]};
|
||||
|
||||
// radix 2 Cooley-Tukey FFT
|
||||
if (n % 2 != 0) {
|
||||
throw new IllegalArgumentException("n is not a power of 2");
|
||||
}
|
||||
|
||||
// fft of even terms
|
||||
Complex[] even = new Complex[n / 2];
|
||||
for (int k = 0; k < n / 2; k++) {
|
||||
even[k] = x[2 * k];
|
||||
}
|
||||
Complex[] q = fft(even);
|
||||
|
||||
// fft of odd terms
|
||||
for (int k = 0; k < n / 2; k++) {
|
||||
even[k] = x[2 * k + 1];
|
||||
}
|
||||
Complex[] r = fft(even);
|
||||
|
||||
// combine
|
||||
Complex[] y = new Complex[n];
|
||||
for (int k = 0; k < n / 2; k++) {
|
||||
double kth = -2 * k * Math.PI / n;
|
||||
Complex wk = new Complex(Math.cos(kth), Math.sin(kth));
|
||||
y[k] = q[k].plus(wk.times(r[k]));
|
||||
y[k + n / 2] = q[k].minus(wk.times(r[k]));
|
||||
}
|
||||
return y;
|
||||
}
|
||||
|
||||
public static double[] fft(double[] x, int sc) {
|
||||
int len = x.length;
|
||||
if (len == 1) {
|
||||
return x;
|
||||
}
|
||||
Complex[] cs = new Complex[len];
|
||||
double[] ds = new double[len / 2];
|
||||
for (int i = 0; i < len; i++) {
|
||||
cs[i] = new Complex(x[i], 0);
|
||||
}
|
||||
Complex[] ffts = fft(cs);
|
||||
|
||||
for (int i = 0; i < ds.length; i++) {
|
||||
ds[i] = Math.sqrt(Math.pow(ffts[i].re(), 2) + Math.pow(ffts[i].im(), 2)) / x.length;
|
||||
}
|
||||
return ds;
|
||||
}
|
||||
|
||||
// compute the inverse FFT of x[], assuming its length is a power of 2
|
||||
public static Complex[] ifft(Complex[] x) {
|
||||
int n = x.length;
|
||||
Complex[] y = new Complex[n];
|
||||
|
||||
// take conjugate
|
||||
for (int i = 0; i < n; i++) {
|
||||
y[i] = x[i].conjugate();
|
||||
}
|
||||
|
||||
// compute forward FFT
|
||||
y = fft(y);
|
||||
|
||||
// take conjugate again
|
||||
for (int i = 0; i < n; i++) {
|
||||
y[i] = y[i].conjugate();
|
||||
}
|
||||
|
||||
// divide by n
|
||||
for (int i = 0; i < n; i++) {
|
||||
y[i] = y[i].scale(1.0 / n);
|
||||
}
|
||||
|
||||
return y;
|
||||
|
||||
}
|
||||
|
||||
// compute the circular convolution of x and y
|
||||
public static Complex[] cconvolve(Complex[] x, Complex[] y) {
|
||||
|
||||
// should probably pad x and y with 0s so that they have same length
|
||||
// and are powers of 2
|
||||
if (x.length != y.length) {
|
||||
throw new IllegalArgumentException("Dimensions don't agree");
|
||||
}
|
||||
|
||||
int n = x.length;
|
||||
|
||||
// compute FFT of each sequence
|
||||
Complex[] a = fft(x);
|
||||
Complex[] b = fft(y);
|
||||
|
||||
// point-wise multiply
|
||||
Complex[] c = new Complex[n];
|
||||
for (int i = 0; i < n; i++) {
|
||||
c[i] = a[i].times(b[i]);
|
||||
}
|
||||
|
||||
// compute inverse FFT
|
||||
return ifft(c);
|
||||
}
|
||||
|
||||
|
||||
// compute the linear convolution of x and y
|
||||
public static Complex[] convolve(Complex[] x, Complex[] y) {
|
||||
Complex ZERO = new Complex(0, 0);
|
||||
|
||||
Complex[] a = new Complex[2 * x.length];
|
||||
for (int i = 0; i < x.length; i++) a[i] = x[i];
|
||||
for (int i = x.length; i < 2 * x.length; i++) a[i] = ZERO;
|
||||
|
||||
Complex[] b = new Complex[2 * y.length];
|
||||
for (int i = 0; i < y.length; i++) b[i] = y[i];
|
||||
for (int i = y.length; i < 2 * y.length; i++) b[i] = ZERO;
|
||||
|
||||
return cconvolve(a, b);
|
||||
}
|
||||
|
||||
// display an array of Complex numbers to standard output
|
||||
public static void show(Complex[] x, String title) {
|
||||
System.out.println(title);
|
||||
System.out.println("-------------------");
|
||||
for (int i = 0; i < SIZE; i++) {
|
||||
System.out.println(x[i]);
|
||||
}
|
||||
System.out.println();
|
||||
}
|
||||
|
||||
private static final int SIZE = 16384 / 4;
|
||||
|
||||
public static double fun(int x) {
|
||||
return Math.sin(15f * x);//f= 3.142
|
||||
}
|
||||
|
||||
public static double getY(double[] d) {
|
||||
double y = 0;
|
||||
int x = 0;
|
||||
for (int i = 0; i < d.length; i++) {
|
||||
if (d[i] > y) {
|
||||
y = d[i];
|
||||
x = i;
|
||||
}
|
||||
}
|
||||
x++;
|
||||
log(String.format("x: %s , y: %s", x, y));
|
||||
log(String.format("频率: %sHz", (float) x / SIZE));
|
||||
log(String.format("频率2: %sHz", (float) (SIZE - x) / SIZE));
|
||||
log(String.format("振幅: %s", y));
|
||||
return y;
|
||||
}
|
||||
|
||||
public static void log(String s) {
|
||||
System.out.println(s);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package com.zhjt.mogo_core_function_devatools.badcase.record.fft;
|
||||
|
||||
import com.zhjt.mogo_core_function_devatools.badcase.record.utils.ByteUtils;
|
||||
|
||||
/**
|
||||
* FFT 数据处理工厂
|
||||
*/
|
||||
public class FftFactory {
|
||||
private static final String TAG = FftFactory.class.getSimpleName();
|
||||
private Level level = Level.Original;
|
||||
|
||||
public FftFactory(Level level) {
|
||||
// this.level = level;
|
||||
}
|
||||
|
||||
public byte[] makeFftData(byte[] pcmData) {
|
||||
// Logger.d(TAG, "pcmData length: %s", pcmData.length);
|
||||
if (pcmData.length < 1024) {
|
||||
return null;
|
||||
}
|
||||
|
||||
double[] doubles = ByteUtils.toHardDouble(ByteUtils.toShorts(pcmData));
|
||||
double[] fft = FFT.fft(doubles, 0);
|
||||
|
||||
switch (level) {
|
||||
case Original:
|
||||
return ByteUtils.toSoftBytes(fft);
|
||||
case Maximal:
|
||||
// return doFftMaximal(fft);
|
||||
default:
|
||||
return ByteUtils.toHardBytes(fft);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private byte[] doFftMaximal(double[] fft) {
|
||||
byte[] bytes = ByteUtils.toSoftBytes(fft);
|
||||
byte[] result = new byte[bytes.length];
|
||||
|
||||
for (int i = 0; i < bytes.length; i++) {
|
||||
|
||||
if (isSimpleData(bytes, i)) {
|
||||
result[i] = bytes[i];
|
||||
} else {
|
||||
result[Math.max(i - 1, 0)] = (byte) (bytes[i] / 2);
|
||||
result[Math.min(i + 1, result.length - 1)] = (byte) (bytes[i] / 2);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private boolean isSimpleData(byte[] data, int i) {
|
||||
|
||||
int start = Math.max(0, i - 5);
|
||||
int end = Math.min(data.length, i + 5);
|
||||
|
||||
byte max = 0, min = 127;
|
||||
for (int j = start; j < end; j++) {
|
||||
if (data[j] > max) {
|
||||
max = data[j];
|
||||
}
|
||||
if (data[j] < min) {
|
||||
min = data[j];
|
||||
}
|
||||
}
|
||||
|
||||
return data[i] == min || data[i] == max;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* FFT 处理等级
|
||||
*/
|
||||
public enum Level {
|
||||
|
||||
/**
|
||||
* 原始数据,不做任何优化
|
||||
*/
|
||||
Original,
|
||||
|
||||
/**
|
||||
* 对音乐进行优化
|
||||
*/
|
||||
Music,
|
||||
|
||||
/**
|
||||
* 对人声进行优化
|
||||
*/
|
||||
People,
|
||||
|
||||
/**
|
||||
* 极限优化
|
||||
*/
|
||||
Maximal
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.zhjt.mogo_core_function_devatools.badcase.record.listener;
|
||||
|
||||
import com.zhjt.mogo_core_function_devatools.badcase.record.RecordHelper;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
public interface RecordListener {
|
||||
/**
|
||||
* 实时返回音量大小
|
||||
*
|
||||
* @param soundSize 当前音量大小
|
||||
*/
|
||||
void onSoundSize(int soundSize);
|
||||
|
||||
/**
|
||||
* @param data 录音可视化数据,即傅里叶转换后的数据:fftData
|
||||
*/
|
||||
void onFftData(byte[] data);
|
||||
|
||||
/**
|
||||
* 当前的录音状态发生变化
|
||||
*
|
||||
* @param data 当前音频数据
|
||||
*/
|
||||
void onData(byte[] data);
|
||||
|
||||
/**
|
||||
* 录音完成回调
|
||||
* 录音文件
|
||||
*
|
||||
* @param result 录音文件
|
||||
*/
|
||||
void onResult(File result);
|
||||
|
||||
|
||||
/**
|
||||
* 当前的录音状态发生变化
|
||||
*
|
||||
* @param state 当前状态
|
||||
*/
|
||||
void onStateChange(RecordHelper.RecordState state);
|
||||
|
||||
/**
|
||||
* 录音错误
|
||||
*
|
||||
* @param error 错误
|
||||
*/
|
||||
void onError(String error);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
package com.zhjt.mogo_core_function_devatools.badcase.record.mp3;
|
||||
|
||||
import com.zhjt.mogo_core_function_devatools.badcase.record.RecordConfig;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
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, RecordConfig currentConfig) {
|
||||
this.file = file;
|
||||
mp3Buffer = new byte[(int) (7200 + (bufferSize * 2 * 1.25))];
|
||||
int sampleRate = currentConfig.getSampleRate();
|
||||
Mp3Encoder.init(sampleRate, currentConfig.getChannelCount(), sampleRate, currentConfig.getRealEncoding());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
this.os = new FileOutputStream(file);
|
||||
} catch (FileNotFoundException e) {
|
||||
return;
|
||||
}
|
||||
|
||||
while (start) {
|
||||
ChangeBuffer next = next();
|
||||
lameData(next);
|
||||
}
|
||||
}
|
||||
|
||||
public void addChangeBuffer(ChangeBuffer changeBuffer) {
|
||||
if (changeBuffer != null) {
|
||||
cacheBufferList.add(changeBuffer);
|
||||
synchronized (this) {
|
||||
notify();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void stopSafe(EncordFinishListener encordFinishListener) {
|
||||
this.encordFinishListener = encordFinishListener;
|
||||
isOver = true;
|
||||
synchronized (this) {
|
||||
notify();
|
||||
}
|
||||
}
|
||||
|
||||
private ChangeBuffer next() {
|
||||
for (; ; ) {
|
||||
if (cacheBufferList == null || cacheBufferList.size() == 0) {
|
||||
try {
|
||||
if (isOver) {
|
||||
finish();
|
||||
}
|
||||
synchronized (this) {
|
||||
wait();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
} else {
|
||||
return cacheBufferList.remove(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void lameData(ChangeBuffer changeBuffer) {
|
||||
if (changeBuffer == null) {
|
||||
return;
|
||||
}
|
||||
short[] buffer = changeBuffer.getData();
|
||||
int readSize = changeBuffer.getReadSize();
|
||||
if (readSize > 0) {
|
||||
int encodedSize = Mp3Encoder.encode(buffer, buffer, readSize, mp3Buffer);
|
||||
// if (encodedSize < 0) {
|
||||
// Logger.e(TAG, "Lame encoded size: " + encodedSize);
|
||||
// }
|
||||
try {
|
||||
os.write(mp3Buffer, 0, encodedSize);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
if (encordFinishListener != null) {
|
||||
encordFinishListener.onFinish();
|
||||
}
|
||||
}
|
||||
|
||||
public static class ChangeBuffer {
|
||||
private short[] rawData;
|
||||
private int readSize;
|
||||
|
||||
public ChangeBuffer(short[] rawData, int readSize) {
|
||||
this.rawData = rawData.clone();
|
||||
this.readSize = readSize;
|
||||
}
|
||||
|
||||
short[] getData() {
|
||||
return rawData;
|
||||
}
|
||||
|
||||
int getReadSize() {
|
||||
return readSize;
|
||||
}
|
||||
}
|
||||
|
||||
public interface EncordFinishListener {
|
||||
/**
|
||||
* 格式转换完毕
|
||||
*/
|
||||
void onFinish();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.zhjt.mogo_core_function_devatools.badcase.record.mp3;
|
||||
|
||||
|
||||
public class Mp3Encoder {
|
||||
|
||||
static {
|
||||
System.loadLibrary("lamemp3");
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取lame版本号
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public static native String getVersion();
|
||||
|
||||
public native static void close();
|
||||
|
||||
public native static int encode(short[] buffer_l, short[] buffer_r, int samples, byte[] mp3buf);
|
||||
|
||||
public native static int flush(byte[] mp3buf);
|
||||
|
||||
public native static void init(int inSampleRate, int outChannel, int outSampleRate, int outBitrate, int quality);
|
||||
|
||||
public static void init(int inSampleRate, int outChannel, int outSampleRate, int outBitrate) {
|
||||
init(inSampleRate, outChannel, outSampleRate, outBitrate, 7);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.zhjt.mogo_core_function_devatools.badcase.record.mp3;
|
||||
|
||||
import android.media.MediaExtractor;
|
||||
import android.media.MediaFormat;
|
||||
import android.util.Log;
|
||||
|
||||
import com.mogo.eagle.core.utilcode.util.FileUtils;
|
||||
import com.zhjt.mogo_core_function_devatools.badcase.record.RecordConfig;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
|
||||
public class Mp3Utils {
|
||||
private static final String TAG = Mp3Utils.class.getSimpleName();
|
||||
|
||||
/**
|
||||
* 获取mp3音频的总时长 单位:ms
|
||||
*
|
||||
* @param mp3FilePath MP3文件路径
|
||||
* @return 时长
|
||||
*/
|
||||
public static long getDuration(String mp3FilePath) {
|
||||
if (!FileUtils.isFileExists(mp3FilePath)) {
|
||||
return 0;
|
||||
}
|
||||
if (!mp3FilePath.endsWith(RecordConfig.RecordFormat.MP3.getExtension())) {
|
||||
return 0;
|
||||
}
|
||||
MediaExtractor mex = null;
|
||||
try {
|
||||
mex = new MediaExtractor();
|
||||
mex.setDataSource(mp3FilePath);
|
||||
MediaFormat mf = mex.getTrackFormat(0);
|
||||
long duration = mf.getLong(MediaFormat.KEY_DURATION) / 1000L;
|
||||
return duration;
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, e.getMessage());
|
||||
} finally {
|
||||
if (mex != null) {
|
||||
mex.release();
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
package com.zhjt.mogo_core_function_devatools.badcase.record.utils;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Arrays;
|
||||
|
||||
|
||||
public class ByteUtils {
|
||||
public static double[] toHardDouble(short[] shorts) {
|
||||
int length = 512;
|
||||
double[] ds = new double[length];
|
||||
for (int i = 0; i < length; i++) {
|
||||
ds[i] = shorts[i];
|
||||
}
|
||||
return ds;
|
||||
}
|
||||
|
||||
public static byte[] toHardBytes(double[] doubles) {
|
||||
byte[] bytes = new byte[doubles.length];
|
||||
for (int i = 0; i < doubles.length; i++) {
|
||||
double item = doubles[i];
|
||||
bytes[i] = (byte) (item > 127 ? 127 : item);
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
|
||||
public static byte[] toSoftBytes(double[] doubles) {
|
||||
double max = getMax(doubles);
|
||||
|
||||
double sc = 1f;
|
||||
if (max > 127) {
|
||||
sc = (max / 128f);
|
||||
}
|
||||
|
||||
byte[] bytes = new byte[doubles.length];
|
||||
for (int i = 0; i < doubles.length; i++) {
|
||||
double item = doubles[i] / sc;
|
||||
bytes[i] = (byte) (item > 127 ? 127 : item);
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
|
||||
public static double getMax(double[] data) {
|
||||
double max = 0;
|
||||
for (double datum : data) {
|
||||
if (datum > max) {
|
||||
max = datum;
|
||||
}
|
||||
}
|
||||
return max;
|
||||
}
|
||||
|
||||
/**
|
||||
* short[] 转 byte[]
|
||||
*/
|
||||
public static byte[] toBytes(short[] src) {
|
||||
int count = src.length;
|
||||
byte[] dest = new byte[count << 1];
|
||||
for (int i = 0; i < count; i++) {
|
||||
dest[i * 2] = (byte) (src[i]);
|
||||
dest[i * 2 + 1] = (byte) (src[i] >> 8);
|
||||
}
|
||||
|
||||
return dest;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* short[] 转 byte[]
|
||||
*/
|
||||
public static byte[] toBytes(short src) {
|
||||
byte[] dest = new byte[2];
|
||||
dest[0] = (byte) (src);
|
||||
dest[1] = (byte) (src >> 8);
|
||||
|
||||
return dest;
|
||||
}
|
||||
|
||||
/**
|
||||
* int 转 byte[]
|
||||
*/
|
||||
public static byte[] toBytes(int i) {
|
||||
byte[] b = new byte[4];
|
||||
b[0] = (byte) (i & 0xff);
|
||||
b[1] = (byte) ((i >> 8) & 0xff);
|
||||
b[2] = (byte) ((i >> 16) & 0xff);
|
||||
b[3] = (byte) ((i >> 24) & 0xff);
|
||||
return b;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* String 转 byte[]
|
||||
*/
|
||||
public static byte[] toBytes(String str) {
|
||||
return str.getBytes();
|
||||
}
|
||||
|
||||
/**
|
||||
* long类型转成byte数组
|
||||
*/
|
||||
public static byte[] toBytes(long number) {
|
||||
ByteBuffer buffer = ByteBuffer.allocate(8);
|
||||
buffer.putLong(0, number);
|
||||
return buffer.array();
|
||||
}
|
||||
|
||||
public static int toInt(byte[] src, int offset) {
|
||||
return ((src[offset] & 0xFF)
|
||||
| ((src[offset + 1] & 0xFF) << 8)
|
||||
| ((src[offset + 2] & 0xFF) << 16)
|
||||
| ((src[offset + 3] & 0xFF) << 24));
|
||||
}
|
||||
|
||||
public static int toInt(byte[] src) {
|
||||
return toInt(src, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 字节数组到long的转换.
|
||||
*/
|
||||
public static long toLong(byte[] b) {
|
||||
ByteBuffer buffer = ByteBuffer.allocate(8);
|
||||
buffer.put(b, 0, b.length);
|
||||
return buffer.getLong();
|
||||
}
|
||||
|
||||
/**
|
||||
* byte[] 转 short[]
|
||||
* short: 2字节
|
||||
*/
|
||||
public static short[] toShorts(byte[] src) {
|
||||
int count = src.length >> 1;
|
||||
short[] dest = new short[count];
|
||||
for (int i = 0; i < count; i++) {
|
||||
dest[i] = (short) ((src[i * 2] & 0xff) | ((src[2 * i + 1] & 0xff) << 8));
|
||||
}
|
||||
return dest;
|
||||
}
|
||||
|
||||
public static byte[] merger(byte[] bt1, byte[] bt2) {
|
||||
byte[] bt3 = new byte[bt1.length + bt2.length];
|
||||
System.arraycopy(bt1, 0, bt3, 0, bt1.length);
|
||||
System.arraycopy(bt2, 0, bt3, bt1.length, bt2.length);
|
||||
return bt3;
|
||||
}
|
||||
|
||||
public static String toString(byte[] b) {
|
||||
return Arrays.toString(b);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
package com.zhjt.mogo_core_function_devatools.badcase.record.utils;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import com.mogo.eagle.core.utilcode.util.FileUtils;
|
||||
import com.zhjt.mogo_core_function_devatools.badcase.record.RecordConfig;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.RandomAccessFile;
|
||||
|
||||
/**
|
||||
* @author zhaolewei on 2018/7/3.
|
||||
* pcm 转 wav 工具类
|
||||
* http://soundfile.sapp.org/doc/WaveFormat/
|
||||
*/
|
||||
public class WavUtils {
|
||||
private static final String TAG = WavUtils.class.getSimpleName();
|
||||
|
||||
/**
|
||||
* 生成wav格式的Header
|
||||
* wave是RIFF文件结构,每一部分为一个chunk,其中有RIFF WAVE chunk,
|
||||
* FMT Chunk,Fact chunk(可选),Data chunk
|
||||
*
|
||||
* @param totalAudioLen 不包括header的音频数据总长度
|
||||
* @param sampleRate 采样率,也就是录制时使用的频率
|
||||
* @param channels audioRecord的频道数量
|
||||
* @param sampleBits 位宽
|
||||
*/
|
||||
public static byte[] generateWavFileHeader(int totalAudioLen, int sampleRate, int channels, int sampleBits) {
|
||||
WavHeader wavHeader = new WavHeader(totalAudioLen, sampleRate, (short) channels, (short) sampleBits);
|
||||
return wavHeader.getHeader();
|
||||
}
|
||||
|
||||
/**
|
||||
* 将header写入到pcm文件中 不修改文件名
|
||||
*
|
||||
* @param file 写入的pcm文件
|
||||
* @param header wav头数据
|
||||
*/
|
||||
public static void writeHeader(File file, byte[] header) {
|
||||
if (!FileUtils.isFile(file)) {
|
||||
return;
|
||||
}
|
||||
|
||||
RandomAccessFile wavRaf = null;
|
||||
try {
|
||||
wavRaf = new RandomAccessFile(file, "rw");
|
||||
wavRaf.seek(0);
|
||||
wavRaf.write(header);
|
||||
wavRaf.close();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
try {
|
||||
if (wavRaf != null) {
|
||||
wavRaf.close();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Pcm 转 WAV 文件
|
||||
*
|
||||
* @param pcmFile File
|
||||
* @param header wavHeader
|
||||
* @throws IOException Exception
|
||||
*/
|
||||
public static void pcmToWav(File pcmFile, byte[] header) throws IOException {
|
||||
if (!FileUtils.isFile(pcmFile)) {
|
||||
return;
|
||||
}
|
||||
String pcmPath = pcmFile.getAbsolutePath();
|
||||
String wavPath = pcmPath.substring(0, pcmPath.length() - 4) + ".wav";
|
||||
writeHeader(new File(wavPath), header);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取WAV文件的头信息
|
||||
*
|
||||
* @param wavFilePath 文件地址
|
||||
* @return header
|
||||
*/
|
||||
private static byte[] getHeader(String wavFilePath) {
|
||||
if (!new File(wavFilePath).isFile()) {
|
||||
return null;
|
||||
}
|
||||
byte[] buffer = null;
|
||||
File file = new File(wavFilePath);
|
||||
final int size = 44;
|
||||
FileInputStream fis = null;
|
||||
ByteArrayOutputStream bos = null;
|
||||
try {
|
||||
fis = new FileInputStream(file);
|
||||
bos = new ByteArrayOutputStream(size);
|
||||
byte[] b = new byte[size];
|
||||
int len;
|
||||
if ((len = fis.read(b)) != size) {
|
||||
return null;
|
||||
}
|
||||
bos.write(b, 0, len);
|
||||
buffer = bos.toByteArray();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
try {
|
||||
if (fis != null) {
|
||||
fis.close();
|
||||
fis = null;
|
||||
}
|
||||
if (bos != null) {
|
||||
bos.close();
|
||||
bos = null;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
return buffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取wav音频时长 ms
|
||||
*
|
||||
* @param filePath wav文件路径
|
||||
* @return 时长 -1: 获取失败
|
||||
*/
|
||||
public static long getWavDuration(String filePath) {
|
||||
if (!filePath.endsWith(RecordConfig.RecordFormat.WAV.getExtension())) {
|
||||
return -1;
|
||||
}
|
||||
byte[] header = getHeader(filePath);
|
||||
return getWavDuration(header);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取wav音频时长 ms
|
||||
*
|
||||
* @param header wav音频文件字节数组
|
||||
* @return 时长 -1: 获取失败
|
||||
*/
|
||||
public static long getWavDuration(byte[] header) {
|
||||
if (header == null || header.length < 44) {
|
||||
Log.e(TAG, "header size有误");
|
||||
return -1;
|
||||
}
|
||||
int byteRate = ByteUtils.toInt(header, 28);//28-31
|
||||
int waveSize = ByteUtils.toInt(header, 40);//40-43
|
||||
return waveSize * 1000L / byteRate;
|
||||
}
|
||||
|
||||
public static String headerToString(byte[] header) {
|
||||
if (header == null || header.length < 44) {
|
||||
return null;
|
||||
}
|
||||
StringBuilder stringBuilder = new StringBuilder();
|
||||
|
||||
for (int i = 0; i < 4; i++) {
|
||||
stringBuilder.append((char) header[i]);
|
||||
}
|
||||
stringBuilder.append(",");
|
||||
|
||||
stringBuilder.append(ByteUtils.toInt(header, 4));
|
||||
stringBuilder.append(",");
|
||||
|
||||
for (int i = 8; i < 16; i++) {
|
||||
stringBuilder.append((char) header[i]);
|
||||
}
|
||||
stringBuilder.append(",");
|
||||
|
||||
for (int i = 16; i < 24; i++) {
|
||||
stringBuilder.append(header[i]);
|
||||
}
|
||||
stringBuilder.append(",");
|
||||
|
||||
stringBuilder.append(ByteUtils.toInt(header, 24));
|
||||
stringBuilder.append(",");
|
||||
|
||||
stringBuilder.append(ByteUtils.toInt(header, 28));
|
||||
stringBuilder.append(",");
|
||||
|
||||
for (int i = 32; i < 36; i++) {
|
||||
stringBuilder.append(header[i]);
|
||||
}
|
||||
stringBuilder.append(",");
|
||||
|
||||
for (int i = 36; i < 40; i++) {
|
||||
stringBuilder.append((char) header[i]);
|
||||
}
|
||||
stringBuilder.append(",");
|
||||
|
||||
stringBuilder.append(ByteUtils.toInt(header, 40));
|
||||
|
||||
return stringBuilder.toString();
|
||||
}
|
||||
|
||||
public static class WavHeader {
|
||||
/**
|
||||
* RIFF数据块
|
||||
*/
|
||||
final String riffChunkId = "RIFF";
|
||||
int riffChunkSize;
|
||||
final String riffType = "WAVE";
|
||||
|
||||
/**
|
||||
* FORMAT 数据块
|
||||
*/
|
||||
final String formatChunkId = "fmt ";
|
||||
final int formatChunkSize = 16;
|
||||
final short audioFormat = 1;
|
||||
short channels;
|
||||
int sampleRate;
|
||||
int byteRate;
|
||||
short blockAlign;
|
||||
short sampleBits;
|
||||
|
||||
/**
|
||||
* FORMAT 数据块
|
||||
*/
|
||||
final String dataChunkId = "data";
|
||||
int dataChunkSize;
|
||||
|
||||
WavHeader(int totalAudioLen, int sampleRate, short channels, short sampleBits) {
|
||||
this.riffChunkSize = totalAudioLen;
|
||||
this.channels = channels;
|
||||
this.sampleRate = sampleRate;
|
||||
this.byteRate = sampleRate * sampleBits / 8 * channels;
|
||||
this.blockAlign = (short) (channels * sampleBits / 8);
|
||||
this.sampleBits = sampleBits;
|
||||
this.dataChunkSize = totalAudioLen - 44;
|
||||
}
|
||||
|
||||
public byte[] getHeader() {
|
||||
byte[] result;
|
||||
result = ByteUtils.merger(ByteUtils.toBytes(riffChunkId), ByteUtils.toBytes(riffChunkSize));
|
||||
result = ByteUtils.merger(result, ByteUtils.toBytes(riffType));
|
||||
result = ByteUtils.merger(result, ByteUtils.toBytes(formatChunkId));
|
||||
result = ByteUtils.merger(result, ByteUtils.toBytes(formatChunkSize));
|
||||
result = ByteUtils.merger(result, ByteUtils.toBytes(audioFormat));
|
||||
result = ByteUtils.merger(result, ByteUtils.toBytes(channels));
|
||||
result = ByteUtils.merger(result, ByteUtils.toBytes(sampleRate));
|
||||
result = ByteUtils.merger(result, ByteUtils.toBytes(byteRate));
|
||||
result = ByteUtils.merger(result, ByteUtils.toBytes(blockAlign));
|
||||
result = ByteUtils.merger(result, ByteUtils.toBytes(sampleBits));
|
||||
result = ByteUtils.merger(result, ByteUtils.toBytes(dataChunkId));
|
||||
result = ByteUtils.merger(result, ByteUtils.toBytes(dataChunkSize));
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user