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