5 changed files with 558 additions and 5 deletions
@ -0,0 +1,225 @@ |
|||||
|
package qianmu.container.recorder; |
||||
|
|
||||
|
|
||||
|
import static qianmu.container.recorder.IRecord.TYPE_HOTWORD; |
||||
|
import static qianmu.container.recorder.IRecord.TYPE_JNI; |
||||
|
import static qianmu.container.recorder.IRecord.TYPE_NATIVE; |
||||
|
import android.content.Context; |
||||
|
import android.util.Log; |
||||
|
|
||||
|
import java.io.File; |
||||
|
import java.io.FileNotFoundException; |
||||
|
import java.io.FileOutputStream; |
||||
|
import java.io.IOException; |
||||
|
import java.util.ArrayList; |
||||
|
import java.util.Arrays; |
||||
|
import java.util.List; |
||||
|
import java.util.Objects; |
||||
|
|
||||
|
/** |
||||
|
* Created by Android Studio. |
||||
|
* User: gaozhilong |
||||
|
* Date: 2025/12/19 |
||||
|
* Time: 11:23 |
||||
|
*/ |
||||
|
public class AudioRecorder { |
||||
|
|
||||
|
private static final String TAG = "AudioRecorder"; |
||||
|
|
||||
|
private State mCurrentState = State.NO_READY; |
||||
|
private IRecord.RecordConfig mConfig; |
||||
|
private List<RecorderListener> mListeners = new ArrayList<>(); |
||||
|
private IRecord mRecorder; |
||||
|
private FileOutputStream mDumpFileStream; |
||||
|
private Context mContext; |
||||
|
|
||||
|
private AudioRecorder() { |
||||
|
} |
||||
|
|
||||
|
private static class AudioRecorderHolder { |
||||
|
private static final AudioRecorder INSTANCE = new AudioRecorder(); |
||||
|
} |
||||
|
|
||||
|
public static AudioRecorder getInstance() { |
||||
|
return AudioRecorderHolder.INSTANCE; |
||||
|
} |
||||
|
|
||||
|
public synchronized void init(Context context, IRecord.RecordConfig cfg) { |
||||
|
Log.i(TAG, "record cfg=" + cfg.toString()); |
||||
|
mConfig = cfg; |
||||
|
mContext = context; |
||||
|
if (mConfig.recorderType == TYPE_NATIVE) { |
||||
|
mRecorder = new NativeRecorder(); |
||||
|
} else if (mConfig.recorderType == TYPE_JNI) { |
||||
|
mRecorder = new JniRecorder(); |
||||
|
} else if (mConfig.recorderType == TYPE_HOTWORD) { |
||||
|
|
||||
|
} |
||||
|
mRecorder.init(context, mConfig); |
||||
|
mCurrentState = State.READY; |
||||
|
openDump(); |
||||
|
} |
||||
|
|
||||
|
public int getAudioRecordSessionId() { |
||||
|
if (mRecorder instanceof NativeRecorder) |
||||
|
return ((NativeRecorder) mRecorder).getAudioRecordSessionId(); |
||||
|
return 0; |
||||
|
} |
||||
|
|
||||
|
public synchronized void startRecord() { |
||||
|
Log.i(TAG, "startRecord"); |
||||
|
if (mCurrentState == State.NO_READY) { |
||||
|
Log.e(TAG, "record not initialized"); |
||||
|
return; |
||||
|
} |
||||
|
if (mCurrentState == State.START) { |
||||
|
Log.w(TAG, "already recording...ignore"); |
||||
|
return; |
||||
|
} |
||||
|
Log.d(TAG, "startRecord: ============startRecord begin============"); |
||||
|
mRecorder.start(); |
||||
|
new Thread(this::recordRead).start(); |
||||
|
Log.d(TAG, "startRecord: ============startRecord end============"); |
||||
|
} |
||||
|
|
||||
|
public synchronized void stopRecord() { |
||||
|
Log.d(TAG, "stopRecord: ============stopRecord begin============"); |
||||
|
if (mCurrentState == State.NO_READY || mCurrentState == State.READY) { |
||||
|
Log.e(TAG, "record not started yet"); |
||||
|
} else { |
||||
|
mRecorder.stop(); |
||||
|
mCurrentState = State.STOP; |
||||
|
} |
||||
|
Log.d(TAG, "stopRecord: ============stopRecord end============"); |
||||
|
} |
||||
|
|
||||
|
public void release() { |
||||
|
Log.d(TAG, "release: ============release begin============"); |
||||
|
if (mRecorder != null) { |
||||
|
mRecorder.release(); |
||||
|
} |
||||
|
mCurrentState = State.NO_READY; |
||||
|
Log.d(TAG, "release: ============release end============"); |
||||
|
} |
||||
|
|
||||
|
private void recordRead() { |
||||
|
byte[] audioData = new byte[mRecorder.getBufferSize()]; |
||||
|
mCurrentState = State.START; |
||||
|
while (mCurrentState == State.START) { |
||||
|
int size = mRecorder.read(audioData, 0, mRecorder.getBufferSize()); |
||||
|
|
||||
|
byte[] publishBuffer; |
||||
|
if (mConfig.filter != null) { |
||||
|
publishBuffer = new byte[audioData.length / mConfig.audioChannel * mConfig.filter.length]; |
||||
|
splitAudio(audioData, mConfig.audioChannel, publishBuffer, mConfig.filter); |
||||
|
} else { |
||||
|
if (size > 0) { |
||||
|
publishBuffer = Arrays.copyOfRange(audioData, 0, size); |
||||
|
} else { |
||||
|
Log.w(TAG, "read size <= 0"); |
||||
|
publishBuffer = new byte[0]; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (size > 0) { |
||||
|
if (mListeners != null) { |
||||
|
for (RecorderListener listener : mListeners) { |
||||
|
listener.onAudioBuffer(publishBuffer, publishBuffer.length); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
dump(publishBuffer); |
||||
|
} |
||||
|
} |
||||
|
closeDump(); |
||||
|
} |
||||
|
|
||||
|
public void registerListener(RecorderListener cb) { |
||||
|
if (cb != null && !mListeners.contains(cb)) { |
||||
|
Log.i(TAG, "registerListener " + cb.toString()); |
||||
|
mListeners.add(cb); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void splitAudio(byte[] src, int channelNum, byte[] raw, int[] mics) { |
||||
|
int frameSize = src.length / channelNum / 2; |
||||
|
int rawCh = mics.length; |
||||
|
|
||||
|
for (int i = 0; i < frameSize; i++) { |
||||
|
int rawNum = 0; |
||||
|
for (int j = 0; j < channelNum; j++) { |
||||
|
if (j != mics[rawNum]) { |
||||
|
continue; |
||||
|
} |
||||
|
|
||||
|
int rawIdx = (i * rawCh + rawNum) * 2; |
||||
|
int srcIdx = (i * channelNum + j) * 2; |
||||
|
raw[rawIdx] = src[srcIdx]; |
||||
|
raw[rawIdx + 1] = src[srcIdx + 1]; |
||||
|
|
||||
|
rawNum++; |
||||
|
if (rawNum == rawCh) { |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public void unregisterListener(RecorderListener cb) { |
||||
|
if (cb != null && mListeners.contains(cb)) { |
||||
|
Log.i(TAG, "unregisterListener " + cb.toString()); |
||||
|
mListeners.remove(cb); |
||||
|
} |
||||
|
if (mListeners.isEmpty()) { |
||||
|
stopRecord(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void openDump() { |
||||
|
if (mConfig.dump) return; |
||||
|
|
||||
|
try { |
||||
|
String dumpPath = mContext.getExternalFilesDir("record") + File.separator + System.currentTimeMillis() + ".pcm"; |
||||
|
File file = new File(dumpPath); |
||||
|
if (!Objects.requireNonNull(file.getParentFile()).exists()) { |
||||
|
if (file.getParentFile().mkdirs()) { |
||||
|
Log.i(TAG, "successfully created " + file.getParentFile().getPath()); |
||||
|
} |
||||
|
} |
||||
|
mDumpFileStream = new FileOutputStream(file); |
||||
|
} catch (IllegalStateException | FileNotFoundException e) { |
||||
|
Log.e(TAG, "create audio data file error: " + e.getMessage()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void dump(byte[] data) { |
||||
|
if (mDumpFileStream == null) return; |
||||
|
try { |
||||
|
mDumpFileStream.write(data); |
||||
|
} catch (IOException e) { |
||||
|
Log.e(TAG, "write audio data error: " + e.getMessage()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void closeDump() { |
||||
|
if (mDumpFileStream == null) return; |
||||
|
try { |
||||
|
mDumpFileStream.flush(); |
||||
|
mDumpFileStream.close(); |
||||
|
} catch (IOException e) { |
||||
|
Log.e(TAG, "close audio data file error: " + e.getMessage()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public interface RecorderListener { |
||||
|
void onAudioBuffer(byte[] data, int volume); |
||||
|
} |
||||
|
|
||||
|
public enum State { |
||||
|
NO_READY, |
||||
|
READY, |
||||
|
START, |
||||
|
STOP |
||||
|
} |
||||
|
} |
||||
|
|
||||
@ -0,0 +1,44 @@ |
|||||
|
package qianmu.container.recorder; |
||||
|
|
||||
|
import android.content.Context; |
||||
|
import android.media.AudioFormat; |
||||
|
import android.media.MediaRecorder; |
||||
|
/** |
||||
|
* Created by Android Studio. |
||||
|
* User: gaozhilong |
||||
|
* Date: 2025/12/19 |
||||
|
* Time: 11:24 |
||||
|
*/ |
||||
|
public interface IRecord { |
||||
|
|
||||
|
int TYPE_NATIVE = 0; |
||||
|
int TYPE_JNI = 1; |
||||
|
int TYPE_HOTWORD = 2; |
||||
|
|
||||
|
void init(Context context, RecordConfig config); |
||||
|
|
||||
|
void start(); |
||||
|
|
||||
|
int read(byte[] buffer, int offset, int length); |
||||
|
|
||||
|
void stop(); |
||||
|
|
||||
|
void release(); |
||||
|
|
||||
|
int getBufferSize(); |
||||
|
|
||||
|
class RecordConfig { |
||||
|
public int recorderType = 0; // 0: 普通录音, 1: JNI录音机
|
||||
|
public int audioSource = MediaRecorder.AudioSource.VOICE_RECOGNITION; |
||||
|
public int sampleRate = 16000; |
||||
|
public int audioChannel = AudioFormat.CHANNEL_IN_MONO; |
||||
|
public int audioEncoding = AudioFormat.ENCODING_PCM_16BIT; |
||||
|
public int captureAudioMs = 2500; |
||||
|
public int audioFormat = AudioFormat.ENCODING_PCM_16BIT; |
||||
|
public int captureSession = 0; |
||||
|
public boolean dump = false; |
||||
|
public int[] filter; |
||||
|
public int recordInterval = 0; |
||||
|
} |
||||
|
} |
||||
|
|
||||
@ -0,0 +1,49 @@ |
|||||
|
package qianmu.container.recorder; |
||||
|
|
||||
|
import android.content.Context; |
||||
|
|
||||
|
import com.aispeech.AIAudioRecord; |
||||
|
|
||||
|
/** |
||||
|
* Created by Android Studio. |
||||
|
* User: gaozhilong |
||||
|
* Date: 2025/12/19 |
||||
|
* Time: 11:24 |
||||
|
*/ |
||||
|
public class JniRecorder implements IRecord { |
||||
|
private int mBufferSize; |
||||
|
AIAudioRecord mJniRecorder; |
||||
|
|
||||
|
@Override |
||||
|
public void init(Context context, RecordConfig cfg) { |
||||
|
mBufferSize = cfg.sampleRate * 100 * 2 * cfg.audioChannel / 1000; |
||||
|
mJniRecorder = new AIAudioRecord(); |
||||
|
mJniRecorder._native_setup(cfg.audioSource, cfg.sampleRate, cfg.audioChannel); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void start() { |
||||
|
mJniRecorder._native_start(); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public int read(byte[] buffer, int offset, int length) { |
||||
|
return mJniRecorder._native_read_in_byte_array(buffer, 0, length); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void stop() { |
||||
|
mJniRecorder._native_stop(); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void release() { |
||||
|
|
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public int getBufferSize() { |
||||
|
return mBufferSize; |
||||
|
} |
||||
|
} |
||||
|
|
||||
@ -0,0 +1,99 @@ |
|||||
|
package qianmu.container.recorder; |
||||
|
|
||||
|
import static android.Manifest.permission.RECORD_AUDIO; |
||||
|
import android.content.Context; |
||||
|
import android.content.pm.PackageManager; |
||||
|
import android.media.AudioFormat; |
||||
|
import android.media.AudioRecord; |
||||
|
import android.util.Log; |
||||
|
|
||||
|
import androidx.core.content.ContextCompat; |
||||
|
|
||||
|
/** |
||||
|
* Created by Android Studio. |
||||
|
* User: gaozhilong |
||||
|
* Date: 2025/12/19 |
||||
|
* Time: 11:25 |
||||
|
*/ |
||||
|
public class NativeRecorder implements IRecord { |
||||
|
public static final String TAG = "NativeRecorder"; |
||||
|
private int mBufferSize; |
||||
|
private AudioRecord mAudioRecord; |
||||
|
|
||||
|
@Override |
||||
|
public void init(Context context, RecordConfig config) { |
||||
|
if (config.recordInterval > 0) { |
||||
|
int channelCount = (config.audioChannel == AudioFormat.CHANNEL_IN_MONO) ? 1 : 2; |
||||
|
int frameSize = config.sampleRate * 2 / 1000; |
||||
|
mBufferSize = frameSize * config.recordInterval * channelCount; |
||||
|
} else { |
||||
|
mBufferSize = AudioRecord.getMinBufferSize(config.sampleRate, config.audioChannel, config.audioEncoding); |
||||
|
} |
||||
|
Log.d(TAG, "mBufferSize:" + mBufferSize); |
||||
|
if (ContextCompat.checkSelfPermission(context, RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED) { |
||||
|
mAudioRecord = new AudioRecord(config.audioSource, config.sampleRate, config.audioChannel, config.audioFormat, mBufferSize); |
||||
|
} else { |
||||
|
throw new RuntimeException("no permission to record!"); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void start() { |
||||
|
if (mAudioRecord != null) { |
||||
|
mAudioRecord.startRecording(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public int read(byte[] buffer, int offset, int length) { |
||||
|
if (mAudioRecord != null) { |
||||
|
return mAudioRecord.read(buffer, 0, length); |
||||
|
} else { |
||||
|
return 0; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void stop() { |
||||
|
if (mAudioRecord != null) { |
||||
|
mAudioRecord.stop(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void release() { |
||||
|
if (mAudioRecord != null) { |
||||
|
mAudioRecord.release(); |
||||
|
mAudioRecord = null; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public int getBufferSize() { |
||||
|
return mBufferSize; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Check whether recorder is recording. |
||||
|
* |
||||
|
* @return true if audio record is in recording state, otherwise false. |
||||
|
*/ |
||||
|
public boolean isRecording() { |
||||
|
boolean bRecording = false; |
||||
|
|
||||
|
if ((mAudioRecord != null) && (mAudioRecord.getState() == AudioRecord.STATE_INITIALIZED)) { |
||||
|
bRecording = (mAudioRecord.getRecordingState() == AudioRecord.RECORDSTATE_RECORDING); |
||||
|
} |
||||
|
|
||||
|
return bRecording; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取正在录音的会话ID |
||||
|
* |
||||
|
* @return |
||||
|
*/ |
||||
|
public int getAudioRecordSessionId() { |
||||
|
return isRecording() ? mAudioRecord.getAudioSessionId() : 0; |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue