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>
相关推荐
赏金术士20 分钟前
第六章:UI组件与Material3主题
android·ui·kotlin·compose
TechMerger2 小时前
Android 17 重磅重构!服役 20 年的 MessageQueue 迎来无锁改造,卡顿大幅优化!
android·性能优化
yuhuofei20214 小时前
【Python入门】Python中字符串相关拓展
android·java·python
dalancon4 小时前
Android Input Spy Window
android
dalancon6 小时前
InputDispatcher派发事件,查找目标窗口
android
我命由我123456 小时前
Android Framework P3 - MediaServer 进程、认识 ServiceManager 进程
android·c语言·开发语言·c++·visualstudio·visual studio·android runtime
天才少年曾牛7 小时前
Android14 新增系统服务后,应用调用出现 “hidden api” 警告的原因与解决方案
android·frameworks
赏金术士7 小时前
Jetpack Compose 底部导航实战教程(完整版)
android·kotlin·compose
随遇丿而安7 小时前
第5周:XML 资源、样式和主题,真正解决的是“页面以后还改不改得动”
android
zh_xuan8 小时前
Android 获取系统内存页大小:sysconf(_SC_PAGESIZE) 与 JNI 实现
android·jni·ndk·内存页大小