之前公司有个录音的需求,需要用户在使用某个功能的时候实时录用。在App退到后台时候暂停录音,回到前台时候重新录音。
一开始的的做法是使用MediaRecorder去录音,app挂到后台时候停止录音,回到前台重新录音。在录音动作结束的时候,将多个amr文件合并成一个单独的amr文件。下面是amr文件合并的工具类

/**
 * author: wangzh
 * create: 2018/11/26 14:47
 * description: amr录音文件合并工具,因为RAW_AMR,AMR_NB的头文件长度固定为6个字节,AMR_WB的头文件长度为9个字节。
 * 所以使用RandomAccessFile就可以轻松的完成amr文件的合并了。
 * version: 1.0
 */
public class AMRMergeUtil {


    public static final int SEEK_LENGTH_AMR_NB = 6;
    public static final int SEEK_LENGTH_AMR_WB = 9;

    /**
     * 缓冲的数组大小
     */
    private static final int BUFF_BYTE_ARRAY_LENGTH = 4096;

    /**
     * @param rootDir    存放着需要合并的amr录音文件的父目录绝对路径
     * @param seekLength 跳过的头部的长度,RAW_AMR,AMR_NB的头文件长度固定为6个字节,AMR_WB的头文件长度为9个字节。
     * @return 返回{@link #Result}对象
     */
    public static Result merge(String rootDir, int seekLength) throws IOException {
        Result result = new Result();
        if (TextUtils.isEmpty(rootDir)) {
            result.setResult(false);
            result.setMessage("传入的文件夹路径为空");
            return result;
        }
        File sourceDir = new File(rootDir);
        if (!sourceDir.exists() || sourceDir.isFile()) {
            result.setResult(false);
            result.setMessage("传入的文件夹不存在或者不是文件夹");
            return result;
        }
        File[] files = sourceDir.listFiles();
        //文件夹为空或者里面没有更多文件,辣鸡
        if (files == null || files.length == 0) {
            result.setResult(false);
            result.setMessage("没有找到录音文件");
            return result;
        } else if (files.length == 1) {
            //只有一个文件,美滋滋
            result.setResult(true);
            result.setMessage("");
            result.setAbsolutePath(files[0].getAbsolutePath());
            return result;
        } else {
            //目标录音文件
            File desAmrFile = new File(rootDir, String.valueOf("desAmr.amr"));
            if (!desAmrFile.createNewFile()) {
                result.setResult(false);
                result.setMessage("创建保存目标文件失败");
                return result;
            }
            List<File> list = Arrays.asList(files);
            //自然排序。时间戳靠前的排在前面
            Collections.sort(list);
            RandomAccessFile outRaf = new RandomAccessFile(desAmrFile, "rw");
            RandomAccessFile inRaf = null;
            final int length = list.size();
            //缓冲数据
            byte[] buff = new byte[BUFF_BYTE_ARRAY_LENGTH];
            int readLength = -1;
            for (int i = 0; i < length; i++) {
                //读取文件夹内容,只读形式打开
                inRaf = new RandomAccessFile(list.get(i), "r");
                if (i != 0) {
                    //跳过指定的字节以后再读
                    inRaf.seek(seekLength);
                    //循环读写
                    while ((readLength = inRaf.read(buff)) != -1) {
                        outRaf.write(buff, 0, readLength);
                    }
                } else {
                    //循环读写,
                    while ((readLength = inRaf.read(buff)) != -1) {
                        outRaf.write(buff, 0, readLength);
                    }
                }
                inRaf.close();
            }
            outRaf.close();
            result.setResult(true);
            result.setAbsolutePath(desAmrFile.getAbsolutePath());
            return result;
        }
    }

    /**
     * 合并结果的返回类
     */
    public static class Result implements Serializable {

        private static final long serialVersionUID = 1L;

        /**
         * true是合并成功,false是失败
         */
        private boolean result;

        /**
         * 合并的录音文件的路径
         */
        private String absolutePath;

        /**
         * 合并结果的信息,{@link #result}为true该值为空,为false会附带着失败原因
         */
        private String message;

        /**
         * 录音文件的长度
         */
        private int duration;

        public boolean isResult() {
            return result;
        }

        public void setResult(boolean result) {
            this.result = result;
        }

        public String getAbsolutePath() {
            return absolutePath;
        }

        public void setAbsolutePath(String absolutePath) {
            this.absolutePath = absolutePath;
        }

        public String getMessage() {
            return message;
        }

        public void setMessage(String message) {
            this.message = message;
        }

        public int getDuration() {
            return duration;
        }

        public void setDuration(int duration) {
            this.duration = duration;
        }

        @Override
        public String toString() {
            return "Result{" +
                    "result=" + result +
                    ", absolutePath='" + absolutePath + '\'' +
                    ", message='" + message + '\'' +
                    ", duration=" + duration +
                    '}';
        }
    }

}

虽说这个功能实现了,产品验收也没说有啥不妥。但是自己在自测的时候总觉得可以把录音的音量的音量调节大一点就好了。

放大音量

MediaRecorder是Google对底层录音功能封装的比较完善的一个类,其录制的音频文件格式并没有太多进行处理的机会了。在使用MediaRecorder的api的时候,我们就能发现,相对于AudioRecord,MediaRecorder的初始化以及配置使用都相对于比较简单。
之前开发录音功能的时间比较赶选择了MediaRecorder,闲下来学习实现了放大录音文件的音量的功能。

sample

前面讲了这么多,先把项目的sample放出来。
在sample里面已经实现了AudioRecord录音的暂停、重新开始录制,pcm2wav,wav2amr的功能。

思路

  1. 如何实现录音的暂停录音、继续录音

AudioRecord在录音的时候,只需要传入一个写入录音字节流的文件,start或者stop的时候,只要不reset或者release便可以实现文件的暂停录制和重新录制。

  1. 如何放大录音文件的录入音量

AudioRecord在录制的时候,需要初始化录音的采样频率、声道、位数配置等。这三个参数直接决定了录制文件的质量。在该文章的sample中,采用了8kHz的采样频率,单声道以及16bit的位数配置(opencore-amr库在不引入vo-amrwbenc支持的情况下,只支持8kHz采样的wav文件解析,所以sample的采样率是8kHz)。
在录音的时候,想要放大声音质量,只要将读到的字节流数组的元素逐个加倍放大即可。但是这里需要注意byte值的溢出,还有可能出现一些噪音问题。所以建议倍数不要设置过大,我这边采用了2倍的增益。

  1. pcm2wav,wav2amr

录音结束的时候,AudioRecord保存的文件是pcm的格式。想要保存成wav的格式,还需要加入wav特有的头文件。在Sample的Pcm2wav.java类中有具体实现。而将wav文件转换成amr文件,则需要使用opencore-amr库来编解码。项目路径,使用ndk编译以后即可完成wav2amr的功能。