Android 实现录音功能

思路:

通过媒体录制器MediaRecorder实现:MediaRecorder是Android自带的音频和视频录制工具,它通过操纵摄像头和麦克风完成媒体录制,既可录制视频,又可单独录制音频。

MediaRecorder常用方法(录音与录像通用):

  • reset:重置录制资源。
  • prepare:准备录制。
  • start:开始录制。
  • stop:结束录制。
  • release:释放录制资源。
  • setOnErrorListener:设置错误监听器。可监听服务器异常和未知错误的事件。需要实现接口MediaRecorder.OnErrorListener的onError方法。
  • setOnInfoListener:设置信息监听器。可监听录制结束事件,包括达到录制时长或达到录制大小。需要实现接口MediaRecorder.OnInfoListener的onInfo方法。
  • setMaxDuration:设置可录制的最大时长,单位毫秒。
  • setMaxFileSize:设置可录制的最大文件大小,单位字节。
  • setOutputFile:设置输出文件的路径。

|--------------------|------|------|------------|
| setOutputFormat:设置媒体输出格式 ||||
| OutputFormat类的输出格式 | 格式分类 | 扩展名 | 格式说明 |
| AMR_NB | 音频 | .amr | 窄带格式 |
| AMR_WB | 音频 | .amr | 宽带格式 |
| AAC_ADTS | 音频 | .aac | 高级的音频传输流格式 |
| MPEG_4 | 视频 | .mp4 | MPEG4格式 |
| THREE_GPP | 视频 | .3gp | 3GP格式 |

音频录制示例,上代码

一.权限添加

复制代码
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
    <uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
    <uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
    <uses-permission android:name="android.permission.RECORD_AUDIO"/>
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32"/>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="32"/>

权限区分版本33及以上和以下

动态权限请求:

使用权限请求库,在app的build.gradle添加

复制代码
// 权限请求
    implementation 'com.guolindev.permissionx:permissionx:1.7.1'

使用音频抖动动效

复制代码
// https://github.com/xfans/VoiceWaveView
implementation 'com.github.xfans:VoiceWaveView:1.0.2'

/**
     * 请求视频、音频、图片权限
     */
    private void requestPermission() {
        ArrayList<String> requestList = new ArrayList<>();
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
            requestList.add(Manifest.permission.READ_MEDIA_IMAGES);
            requestList.add(Manifest.permission.RECORD_AUDIO);
            requestList.add(Manifest.permission.READ_MEDIA_VIDEO);
        } else {
            requestList.add(Manifest.permission.WRITE_EXTERNAL_STORAGE);
            requestList.add(Manifest.permission.READ_EXTERNAL_STORAGE);
            requestList.add(Manifest.permission.RECORD_AUDIO);
        }
        PermissionX.init(this)
                .permissions(requestList)
                .onExplainRequestReason((scope, deniedList) -> {
                    scope.showRequestReasonDialog(deniedList, UiUtil.getString(R.string.toast_permission_request), UiUtil.getString(R.string.toast_permission_allow), UiUtil.getString(R.string.toast_permission_deny));
                })
                .request((allGranted, grantedList, deniedList) -> {
                    if (allGranted) {
                        showVoiceAddDialog();
                        LogUtil.i(TAG, "所有申请的权限都已通过");
                    } else {
                        LogUtil.i(TAG, "您拒绝了如下权限:" + deniedList);
                    }
                });
    }

    /**
     * 判断是否有存储、音频权限
     * @return 
     */
    public boolean hasStoragePermissions() {
        boolean isStorage, isAudio, isVideo, isImages;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
            isImages = ContextCompat.checkSelfPermission(MyApplication.getInstance(), Manifest.permission.READ_MEDIA_IMAGES)
                    == PackageManager.PERMISSION_GRANTED;
            isAudio = ContextCompat.checkSelfPermission(MyApplication.getInstance(), Manifest.permission.RECORD_AUDIO)
                    == PackageManager.PERMISSION_GRANTED;
            isVideo = ContextCompat.checkSelfPermission(MyApplication.getInstance(), Manifest.permission.READ_MEDIA_VIDEO)
                    == PackageManager.PERMISSION_GRANTED;
            return isImages && isAudio && isVideo;
        } else {
            isStorage = ContextCompat.checkSelfPermission(MyApplication.getInstance(), Manifest.permission.WRITE_EXTERNAL_STORAGE)
                    == PackageManager.PERMISSION_GRANTED;
            isAudio = ContextCompat.checkSelfPermission(MyApplication.getInstance(), Manifest.permission.RECORD_AUDIO)
                    == PackageManager.PERMISSION_GRANTED;
            return isStorage || isAudio;
        }
    }

二.录制代码

复制代码
package com.calendar.master.gp.dialog;

import android.app.AlertDialog;
import android.app.Dialog;
import android.media.MediaRecorder;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.text.format.DateFormat;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.view.WindowManager;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.Fragment;
import androidx.viewpager2.adapter.FragmentStateAdapter;
import androidx.viewpager2.widget.ViewPager2;

import com.calendar.master.gp.R;
import com.calendar.master.gp.bean.NoteVoice;
import com.calendar.master.gp.databinding.DialogAddVoiceBinding;
import com.calendar.master.gp.databinding.DialogEmojiBinding;
import com.calendar.master.gp.fragment.EmojiFragment;
import com.calendar.master.gp.listener.IVoiceSaveListener;
import com.google.android.material.tabs.TabLayout;
import com.google.android.material.tabs.TabLayoutMediator;
import com.oooppqqzzz.base.utils.LogUtil;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import me.xfans.lib.voicewaveview.VoiceWaveView;

/**
 * 描述:
 * 作者: shawn
 * 时间: 2023/4/2716:44
 */
public class VoiceAddDialogFragment extends DialogFragment {
    private static final String TAG = "VoiceAddDialogFragment";

    private MediaRecorder mMediaRecorder;
    private String voicePath;
    private String outputFileName;
    private File fileVoice;
    private IVoiceSaveListener iVoiceSaveListener;

    private DialogAddVoiceBinding mBinding;
    private int recordingTime = 0; // 记录录音时长(秒)
    private Handler handler = new Handler(Looper.getMainLooper()) {
        @Override
        public void handleMessage(@NonNull Message msg) {
            // 更新UI显示录音时长
            mBinding.tvVoiceTime.setText(getRecordTime(recordingTime));
        }
    };
    ;

    @Override
    public void onStart() {
        super.onStart();
        setCustomStyle();
    }

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        if (getDialog() == null) {
            return super.onCreateView(inflater, container, savedInstanceState);
        }
        // 设置宽度为屏宽、靠近屏幕底部。
        final Window window = getDialog().getWindow();
        window.setBackgroundDrawableResource(R.color.transparent);
        window.getDecorView().setPadding(0, 0, 0, 0);
        WindowManager.LayoutParams wlp = window.getAttributes();
        wlp.gravity = Gravity.BOTTOM;
        wlp.width = WindowManager.LayoutParams.MATCH_PARENT;
        wlp.height = WindowManager.LayoutParams.WRAP_CONTENT;
        window.setAttributes(wlp);
        return super.onCreateView(inflater, container, savedInstanceState);
    }

    @NonNull
    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {
        // 加载布局文件
        mBinding = DialogAddVoiceBinding.inflate(getLayoutInflater());

        // 音频抖动控件
        VoiceWaveView voiceWaveView = mBinding.vwvVoice;
        voiceWaveView.setDuration(100);
        voiceWaveView.addHeader(12).addHeader(14).addHeader(26);

        voiceWaveView.addBody(27).addBody(17).addBody(38)
                .addBody(88)
                .addBody(38)
                .addBody(24)
                .addBody(8)
                .addBody(18)
                .addBody(48)
                .addBody(30)
                .addBody(60)
                .addBody(38);

        voiceWaveView.addFooter(24).addFooter(14).addFooter(8);

        mBinding.ivClose.setOnClickListener(v -> {
            this.dismiss();
        });

        mBinding.ivVoiceEdit.setOnClickListener(new View.OnClickListener() {
            boolean isInput = true;

            @Override
            public void onClick(View v) {
                if (isInput) {
                    isInput = false;
                    voiceWaveView.start();
                    mBinding.ivVoiceEdit.setBackgroundResource(R.mipmap.icon_voice_edit_stop);
                    getPath();
                    startRecord();
                } else {
                    isInput = true;
                    voiceWaveView.stop();
                    cancelRecord();
                    // 移除定时任务
                    handler.removeCallbacks(timerRunnable);
                    voiceStop(true);
                }
            }
        });

        mBinding.ivVoiceCancel.setOnClickListener(v -> {
            voiceStop(false);
            fileVoice.delete();
            recordingTime = 0;
            mBinding.tvVoiceTime.setText("00:00");
            mBinding.ivVoiceEdit.setBackgroundResource(R.mipmap.icon_voice_edit_start);
        });

        mBinding.ivVoiceSave.setOnClickListener(v -> {
            NoteVoice noteVoice = new NoteVoice();
            noteVoice.setUrl(voicePath);
            noteVoice.setTime(getRecordTime(recordingTime));
            noteVoice.setFileName(outputFileName);
            if (iVoiceSaveListener != null) {
                iVoiceSaveListener.save(noteVoice);
            }
            dismiss();
        });


        return new AlertDialog.Builder(getActivity()).setView(mBinding.getRoot())
                .create();
    }

    private void showAd(int selectIndex) {

    }

    private void voiceStop(boolean isStop) {
        if (isStop) {
            mBinding.ivVoiceCancel.setVisibility(View.VISIBLE);
            mBinding.ivVoiceSave.setVisibility(View.VISIBLE);
            mBinding.ivVoiceEdit.setVisibility(View.GONE);
        } else {
            mBinding.ivVoiceCancel.setVisibility(View.GONE);
            mBinding.ivVoiceSave.setVisibility(View.GONE);
            mBinding.ivVoiceEdit.setVisibility(View.VISIBLE);
        }
    }

    private void setCustomStyle() {
        // 设置主题,这里只能通过xml方式设置主题,不能通过Java代码处理,因为这是getWindow还是null,
        // 而且window的几乎所有属性,都可以通过xml设置
        setStyle(STYLE_NORMAL, R.style.AddDialogTheme);
    }

    /**
     * 录制前创建一个空文件并获取路径
     */
    private void getPath() {
        File appDir = new File(Environment.getExternalStorageDirectory() + File.separator + Environment.DIRECTORY_MUSIC, "NoteToVoice");
        if (!appDir.exists()) {
            appDir.mkdirs();
        }
        outputFileName = System.currentTimeMillis() + ".amr";
        fileVoice = new File(appDir, outputFileName);
        if (!fileVoice.exists()) {
            try {
                fileVoice.createNewFile();
                voicePath = fileVoice.getPath();
                LogUtil.i("shawn", "音频保存路径:" + voicePath);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 开始录音
     */
    private void startRecord() {
        // 充值录音时长
        recordingTime = 0;
        //开始录音操作
        mMediaRecorder = new MediaRecorder(); // 创建一个媒体录制器
        mMediaRecorder.setOnErrorListener(new MediaRecorder.OnErrorListener() {
            @Override
            public void onError(MediaRecorder mr, int what, int extra) {
                if (mr != null) {
                    mr.reset(); // 重置媒体录制器
                }
            }
        }); // 设置媒体录制器的错误监听器
        mMediaRecorder.setOnInfoListener(new MediaRecorder.OnInfoListener() {
            @Override
            public void onInfo(MediaRecorder mr, int what, int extra) {
                // 录制达到最大时长,或者达到文件大小限制,都停止录制
                if (what == MediaRecorder.MEDIA_RECORDER_INFO_MAX_DURATION_REACHED
                        || what == MediaRecorder.MEDIA_RECORDER_INFO_MAX_FILESIZE_REACHED) {
                    cancelRecord();
                }
            }
        }); // 设置媒体录制器的信息监听器
        mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC); // 设置音频源为麦克风
        mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.AMR_NB); // 设置媒体的输出格式
        mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB); // 设置媒体的音频编码器
        // mMediaRecorder.setAudioSamplingRate(8); // 设置媒体的音频采样率。可选
        // mMediaRecorder.setAudioChannels(2); // 设置媒体的音频声道数。可选
        // mMediaRecorder.setAudioEncodingBitRate(1024); // 设置音频每秒录制的字节数。可选
        mMediaRecorder.setMaxDuration(10 * 1000); // 设置媒体的最大录制时长
        // mMediaRecorder.setMaxFileSize(1024*1024*10); // 设置媒体的最大文件大小
        // setMaxFileSize与setMaxDuration设置其一即可
        mMediaRecorder.setOutputFile(voicePath); // 设置媒体文件的保存路径
        try {
            mMediaRecorder.prepare(); // 媒体录制器准备就绪
            mMediaRecorder.start(); // 媒体录制器开始录制
            startRecordingTimer();
        } catch (Exception e) {
            LogUtil.i("shawn", "录音出错 = " + e.getMessage());
            e.printStackTrace();
        }
    }

    // 取消录制操作
    private void cancelRecord() {
        if (mMediaRecorder != null) {
            mMediaRecorder.setOnErrorListener(null); // 错误监听器置空
            mMediaRecorder.setPreviewDisplay(null); // 预览界面置空
            try {
                LogUtil.i("shawn", "结束录音");
                mMediaRecorder.stop(); // 媒体录制器停止录制
            } catch (Exception e) {
                LogUtil.i("shawn", "结束录音出错 = " + e.getMessage());
                e.printStackTrace();
            }
            mMediaRecorder.release(); // 媒体录制器释放资源
            mMediaRecorder = null;
        }
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        // 移除定时任务
        handler.removeCallbacks(timerRunnable);
    }

    public void setVoiceSaveListener(IVoiceSaveListener iVoiceSaveListener) {
        this.iVoiceSaveListener = iVoiceSaveListener;
    }

    private void startRecordingTimer() {
        handler.postDelayed(timerRunnable, 1000); // 每隔一秒执行一次
    }

    private Runnable timerRunnable = new Runnable() {
        @Override
        public void run() {
            recordingTime++;
            handler.sendEmptyMessage(0); // 通知UI更新
            startRecordingTimer(); // 继续下一次定时
        }
    };

    private String getRecordTime(int recordingTime) {
        StringBuilder stringBuilder = new StringBuilder();
        if (recordingTime < 10) {
            stringBuilder.append("00:0").append(recordingTime);
        } else if (recordingTime < 60) {
            stringBuilder.append("00:").append(recordingTime);
        } else {
            int minutes = recordingTime / 60;
            int seconds = recordingTime % 60;
            if (minutes < 10) {
                stringBuilder.append("0").append(minutes).append(":");
            } else {
                stringBuilder.append(minutes).append(":");
            }
            if (seconds < 10) {
                stringBuilder.append("0").append(seconds);
            } else {
                stringBuilder.append(seconds);
            }
        }
        return stringBuilder.toString();
    }
}

设置dialog样式,在style.xml

复制代码
<!-- 这里的parent必须是Theme.AppCompat.Dialog -->
    <style name="AddDialogTheme" parent="Theme.AppCompat.Dialog">
        <!-- 这两个属性对于一个常规的Dialog,一般必须设置的-->
        <!-- 这两个属性按照下面的值设置之后,确保了弹窗的实际显示效果,跟你在layout文件中的定义效果是一样的 -->
        <item name="android:windowIsFloating">false</item>
<!--        <item name="android:windowBackground">@drawable/radius_add_dialog_bg</item>-->
    </style>
相关推荐
阿巴斯甜6 小时前
Android 报错:Zip file '/Users/lyy/develop/repoAndroidLapp/l-app-android-ble/app/bu
android
Kapaseker7 小时前
实战 Compose 中的 IntrinsicSize
android·kotlin
xq95278 小时前
Andorid Google 登录接入文档
android
黄林晴9 小时前
告别 Modifier 地狱,Compose 样式系统要变天了
android·android jetpack
冬奇Lab21 小时前
Android触摸事件分发、手势识别与输入优化实战
android·源码阅读
城东米粉儿1 天前
Android MediaPlayer 笔记
android
Jony_1 天前
Android 启动优化方案
android
阿巴斯甜1 天前
Android studio 报错:Cause: error=86, Bad CPU type in executable
android
张小潇1 天前
AOSP15 Input专题InputReader源码分析
android
_小马快跑_1 天前
Kotlin | 协程调度器选择:何时用CoroutineScope配置,何时用launch指定?
android