写一个录音并保存到手机的工具 安卓工具类

录音工具类

java 复制代码
public class AudioRecorder {

    private final String TAG = getClass().getSimpleName();

    private final Context mContext;
    private AudioRecord mAudioRecorder; //录音器
    private final Executor mExecutor = Executors.newSingleThreadExecutor();
    private final RecorderCallback callback;

    public AudioRecorder(Context context, RecorderCallback callback){
        this.mContext = context;
        this.callback = callback;
    }

    @SuppressLint("MissingPermission")
    public void start(){
        int sampleRateInHz = 16000;
        int channelConfig = AudioFormat.CHANNEL_IN_MONO;
        int audioFormat = AudioFormat.ENCODING_PCM_16BIT;
        //20ms audio for 16k/16bit/mono
//        int WAVE_FRAM_SIZE = 20 * 2 * 1 * SAMPLE_RATE / 1000;
        int minBufferSize = AudioRecord.getMinBufferSize(
                sampleRateInHz,
                channelConfig,
                audioFormat
        );
        Log.d(TAG, "minBufferSize: " + minBufferSize);
        mAudioRecorder = new AudioRecord(MediaRecorder.AudioSource.DEFAULT,
                sampleRateInHz, channelConfig,
                audioFormat,
                minBufferSize);
        if (mAudioRecorder.getState() != AudioRecord.STATE_UNINITIALIZED){
            mAudioRecorder.startRecording();
            mExecutor.execute(() -> {
                long startTime = System.currentTimeMillis();
                File cacheDir = mContext.getExternalCacheDir();
                if (!cacheDir.exists()){
                    if (!cacheDir.mkdirs()) Log.e(TAG, "mkdirs fail path: " + cacheDir.getAbsolutePath());
                }
                String pcmName = StringUtils.createFileName("record_", ".pcm");
                File pcmFile = new File(cacheDir, pcmName);
                try (FileOutputStream outputStream = new FileOutputStream(pcmFile)) {
                    byte[] data = new byte[minBufferSize];
                    while (mAudioRecorder.getRecordingState() == AudioRecord.RECORDSTATE_RECORDING){
                        int length = mAudioRecorder.read(data, 0, minBufferSize);
                        if (length > 0){
                            outputStream.write(data, 0, length);
                            if (callback != null){
                                callback.onReadData(data, 0, length);
                            }
                        }
                    }
                    Log.d(TAG, "Record done.");
                    long diff = System.currentTimeMillis() - startTime;
                    if (callback != null){
                        if (diff > 200){
                            callback.onFinish(pcmFile.getAbsolutePath());
                        } else {
                            callback.onRecordError(-2, "too short!");
                        }
                    }
                } catch (Exception e) {
                    Log.e(TAG, "Record error: " + e);
                    if (callback != null){
                        callback.onRecordError(-1, "Record error: " + e);
                    }
                }
            });
        }
    }

    public void stop(){
        if (mAudioRecorder != null){
            if (mAudioRecorder.getRecordingState() == AudioRecord.RECORDSTATE_RECORDING){
                mAudioRecorder.stop();
            }
        }
    }

    public void release(){
        if (mAudioRecorder != null){
            mAudioRecorder.release();
            mAudioRecorder = null;
        }
    }

    public interface RecorderCallback{
        void onReadData(byte[] data, int offsetInBytes, int length);

        void onRecordError(int code, String message);

        void onFinish(String path);
    }
}

使用弹窗录音

java 复制代码
class AudioRecordDialog(
    private val mContext: Context,
    private val listener: Listener
) : Dialog(mContext, R.style.dialog_center) {

    private var binding: DialogAudioRecordBinding

    private var audioRecorder: AudioRecorder?=null

    init {
        requestWindowFeature(Window.FEATURE_NO_TITLE)
        binding = DialogAudioRecordBinding.inflate(layoutInflater)
        setContentView(binding.root)
    }


    @SuppressLint("ClickableViewAccessibility")
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        window?.let {
            it.setGravity(Gravity.BOTTOM)
            it.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
            it.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
        }

        binding.tvTouch.setOnTouchListener { _, event ->
            when (event?.action) {
                MotionEvent.ACTION_DOWN -> {
                    startRecord()
                }

                MotionEvent.ACTION_UP -> {
                    stopRecord()
                }
            }
            false
        }

        setCancelable(true)
        setCanceledOnTouchOutside(true)
    }

    private fun startRecord(){
        audioRecorder = AudioRecorder(mContext, object : AudioRecorder.RecorderCallback{
            override fun onReadData(data: ByteArray, offsetInBytes: Int, length: Int) {
            }

            override fun onRecordError(code: Int, message: String) {
                binding.layoutFrame.post {
                    Toast.makeText(mContext, message, Toast.LENGTH_SHORT).show()
                    audioRecorder?.release()
                }
            }

            override fun onFinish(path: String) {
                binding.layoutFrame.post {
                    audioRecorder?.release()
                    listener.onFinish(path)
                    dismiss()
                }
            }
        })
        audioRecorder?.start()
    }

    private fun stopRecord(){
        audioRecorder?.stop()
    }

    override fun dismiss() {
        super.dismiss()
        audioRecorder?.release()
    }

    interface Listener {
        fun onFinish(path: String)
    }

}

弹窗的布局

xml 复制代码
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:fitsSystemWindows="true"
    android:paddingHorizontal="16dp"
    android:paddingVertical="16dp">
    <androidx.constraintlayout.widget.ConstraintLayout
        android:id="@+id/layoutFrame"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="parent">

        <TextView
            android:id="@+id/tvTouch"
            android:layout_width="120dp"
            android:layout_height="120dp"
            android:paddingVertical="12dp"
            android:clickable="true"
            android:background="@drawable/selector_60_primary"
            android:gravity="center"
            android:text="@string/touch_me_record"
            android:textColor="@color/white"
            android:textSize="12sp"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

    </androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
相关推荐
sjsjsbbsbsn1 小时前
RAG 基础学习总结
java·数据库·学习
今天又在写代码2 小时前
Docker部署
java·阿里云·docker
_Evan_Yao2 小时前
技术成长周记07|复盘中看清方向,多Agent开启新挑战
java·后端
人道领域2 小时前
【黑马点评日记】Redis分布式锁终极方案:Redisson全面解析(含源码解析)
java·数据库·redis·分布式·缓存
Achou.Wang2 小时前
go语言并发编程
java·开发语言·golang
小王师傅662 小时前
【Java结构化梳理】泛型-初步了解-中
java·开发语言
CQU_JIAKE2 小时前
[q]4.25
java·开发语言·前端
黄林晴2 小时前
Koin 开发者炸了!7 条规则根治运行时错误,自动扫描太香了
android
YaBingSec2 小时前
玄机网络安全靶场:GeoServer XXE 任意文件读取(CVE-2025-58360)
java·运维·网络·安全·web安全·tomcat·ssh