录音功能
This commit is contained in:
xuxinchao
2022-07-14 13:36:54 +08:00
parent 3b4cb40d06
commit ac56acfed3
13 changed files with 1806 additions and 0 deletions

View File

@@ -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 声道数: 0error
*/
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;
}
}
}

View File

@@ -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
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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
}
}

View File

@@ -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);
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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 ChunkFact 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;
}
}
}