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

录音工具类

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>
相关推荐
咖啡八杯2 小时前
GoF设计模式——解释器模式
java·后端·spring·设计模式
优橙教育2 小时前
5G网优培训 vs Java开发:转行选哪个?
java·开发语言·5g
GitLqr2 小时前
Flutter 无障碍开发实战:玩转 Semantics 解决视障用户使用痛点
android·flutter·dart
糖果店的幽灵2 小时前
【DeepAgents 从入门到精通】Context Management 上下文管理
java·人工智能·后端·spring·中间件·langgraph·deepagents
腻害兔3 小时前
【若依项目-产品经理视角】RuoYi-Vue-Pro 源码拆解:字典、短信、邮件、通知——后台系统的“基础设施四件套“!
java·前端·vue.js·产品经理·ai编程
碎光拾影4 小时前
ARM交叉工具链各工具作用及IMX6ULL平台LED+蜂鸣器裸机程序实现
java·开发语言·数据库
Miao121315 小时前
微服务 API 测试实践:海外某民宿平台如何构建模式驱动测试基础设施
java·开发语言
腻害兔5 小时前
【若依项目-产品经理视角】RuoYi-Vue-Pro 源码拆解:支付模块 yudao-module-pay,一个让产品经理都看懂的支付中台设计
java·前端·vue.js·产品经理·ai编程
雨白6 小时前
掌握 NestedScrolling 嵌套滑动:手写仿知乎折叠主页
android
CRMEB系统商城6 小时前
开源自建还是SaaS订阅?算一笔3年经济账
java·大数据·开发语言·开源