单声道 通话录音android_android双声道转单声道 - CSDN
精华内容
参与话题
  • 如题,需要在将通话双方的声音录制为两个文件。 想法 1:由于 audiorecord 只能有一个实例,如果想开两个 audiorecord 会报错。那么我们修改源码让系统支持多个 audiorecord 实例,问题是如何修改? 想法 2:...
  • 通话录音自动上传功能

    千次阅读 2017-09-29 19:33:50
    通话录音

          需求:项目中有一期需要做通话录音自动上传功能需求。订单详情页面,用户拨打电话以后,自动将通话录音上传到云端服务器。原来的方式是通话完成,用户从本地文件夹中选择对应的录音文件,然后上传云端。因为业务人员感觉这个操作比较麻烦,因此希望能够自动上传通话录音。

        

        调研:  自动上传通话录音功能包括两个方面:

        1)通话录音的采集:用户拨打电话时,采集通话录音。这需要监听用户拨打订单电话的状态,包括电话的接通和挂断;

        2)通话录音自动上传:通话结束后,将采集到的订单通话录音文件上传服务器。


        这通话录音的采集有两种方案:

        1)应用自身采集,这样能够无差错的建立起订单和通话录音文件的映射关系。

        2)系统采集,然后抓取对应的通话录音文件。因为录音文件的生成是有系统负责,应用本身并不能进行控制。 这有一个问题:不同的系统,通话录音的保存路径不一样,录音文件的命名规则也不相同,不能够针对所有类型的设备进行处理,可以对一些特殊的设备进行处理。当然还存在安全性问题,不能准确无误的建立起订单和录音文件的映射关系。


          实现:

     因为方案一是一个通用方案,因此采用方案一方式实现。

    1)监听用户的通话

        /**
         * 启动通话监听
         */
        private void startCallListener() {
            if (isRooted || isMIUI) {
                //获得电话管理器
                manager = (TelephonyManager) getSystemService(TELEPHONY_SERVICE);
                //为管理器设置监听器,监听电话的呼叫状态
                phoneListener = new MyPhoneListener();
                manager.listen(phoneListener, PhoneStateListener.LISTEN_CALL_STATE);
            }
        }


    通话监听器:

        /**
         * 电话监听器
         * 说明: 监听双向通话
         */
        private class MyPhoneListener extends PhoneStateListener {
            public void onCallStateChanged(int state, String incomingNumber) {
                if(TextUtils.isEmpty(incomingNumber)) {
                    return;
                }
                switch (state) {
                    case TelephonyManager.CALL_STATE_RINGING:
                        //来电振动
                        LogUtils.d(TAG, "CALL_STATE_RINGING:" + incomingNumber);
                        break;
                    case TelephonyManager.CALL_STATE_OFFHOOK:
                        LogUtils.d(TAG, "CALL_STATE_OFFHOOK:" + incomingNumber);
                        // 当接通电话开始通话时  可以进行录音
                        if (null != callRecordEvent
                                && Utils.GetStringNoNil(callRecordEvent.phone).equals(incomingNumber)) {
                            //检查是否需要进行通话录音(只有订单通话才记录通话录音)
                            recordStartTime = System.currentTimeMillis();
                            phoneNumber = incomingNumber.replace(" ", "");
                            LogUtils.d(TAG, "onCallStateChanged recordStartTime:" + TimeUtil.format(TimeUtil.FORMAT_YYYY_MM_DD_HH_MM_SS, recordStartTime) + " phoneNumber: " + phoneNumber);
                            handler.sendEmptyMessage(MSG_START_CALL_RECORD);
                        }
                        break;
                    case TelephonyManager.CALL_STATE_IDLE:
                        LogUtils.d(TAG, "CALL_STATE_IDLE:" + incomingNumber);
                        //挂断电话时停止录音
                        handler.sendEmptyMessage(MSG_STOP_CALL_RECORD);
                        if (null != callRecordEvent && !TextUtils.isEmpty(incomingNumber)) {
                            recordStopTime = System.currentTimeMillis();
                            //录音文件事件复位等待下一次操作
                            callRecordEvent.phone = "";
                            //记录通话结束的手机号
                            phoneNumber = incomingNumber.replace(" ", "");
                            LogUtils.d(TAG, "onCallStateChanged recordStopTime:" + TimeUtil.format(TimeUtil.FORMAT_YYYY_MM_DD_HH_MM_SS, recordStopTime) + " phoneNumber: " + phoneNumber);
                        }
                        break;
                }
            }
        }

    2)监听到用户启动通话接通时,开始启动通话录音:

        /**
         * 启动通话录音
         * 说明: 只有Root设备才能启动通话录音功能
         */
        private void startCallRecord() {
            if (isRooted && !startFlag) {
                // 创建录音器
                createMediaRecorder();
                // 开始记录录音
                startRecording();
            }
        }

    创建录音器:

        /**
         * 创建录音器对象
         */
        private void createMediaRecorder() {
            //1) Create MediaRecorder
            mRecorder = new MediaRecorder();
            // Set audio and video source and encoder
            try {
                //1代表单声道,2代表双声道(立体声)
                mRecorder.setAudioChannels(2);
                //2) 这两项需要放在setOutputFormat之前
                mRecorder.setAudioSource(MediaRecorder.AudioSource.VOICE_CALL);
            } catch (Exception e) {
                e.printStackTrace();
            }
            try {
                //3) Set output file format(mp4格式)
                mRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
            } catch (Exception e) {
                e.printStackTrace();
            }
            try {
                //4) 这两项需要放在setOutputFormat之后
                mRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
            } catch (Exception e) {
                e.printStackTrace();
            }
            // Set output file path
            //初始化缓存目录
            createOutputFile();
            try {
                //设置输出文件路径
                mRecorder.setOutputFile(audioPath);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    开始通话录音

        /**
         * 开始录音
         */
        private void startRecording() {
            LogUtils.d(TAG, "bf mRecorder.prepare()");
            try {
                mRecorder.prepare();
            } catch (Exception e) {
                e.printStackTrace();
            }
            LogUtils.d(TAG, "af mRecorder.prepare()");
            try {
                LogUtils.d(TAG, "bf mRecorder.start()");
                mRecorder.start();   // Recording is now started
            } catch (Exception e) {
                e.printStackTrace();
                //启动出错
                if(!isMIUI){
                    //MIUI支持录音文件抓取
                    ToastUtils.showToast(this, R.string.audio_start_failed, Toast.LENGTH_SHORT);
                }
                handler.sendEmptyMessage(MSG_START_CALL_RECORD_FAILED);
                return;
            }
            //设置标签: 已经启动通话录音
            startFlag = true;
            LogUtils.d(TAG, "af mRecorder.start()");
            LogUtils.d(TAG, "Start recording ...");
        }
    3)通话结束时停止通话录音开始上传
        /**
         * 结束通话录音
         * 说明: 如果自动录音成功优先使用自动录音;
         * 如果自动录音启动失败,且是小米系统,可以尝试抓取录音文件进行上传
         * (1、有些手机未Root权限判断为已经Root;2、系统自身已经开启通话录音导致自动录音失败)。
         */
        private void stopCallRecord() {
            if (isRooted && startFlag) {
                //如果是Root设备查找自我录音的文件
                // (通话录音开启成功时使用自动录音的文件;否则使用小米手机的通话录音)
                //停止录音设备
                stopMediaRecorder();
                //录音文件是非空的录音文件
                if (FileHelper.getFileSize(audioPath) > 0) {
                    //录音结束开始启动通话录音文件上传
                    handler.sendEmptyMessage(MSG_UPLOAD_CALL_RECORD);
                } else {
                    //空文件删除
                    FileHelper.deleteFile(audioPath);
                }
                // Set button status flag
                startFlag = false;
            } else if (isMIUI) {
                //MIUI系统设备有自动通话录音功能抓取对应的通话录音文件
                if(null != callRecordEvent) {
                    //只有在贷后自动录音处理才抓取录音文件
                    startFilterAudioFile(phoneNumber, recordStartTime,
                            callRecordEvent.son_order_id);
                }
            }
        }

          产生问题:程序安装后,发现有的手机运行没有问题,有的手机运行录音一直不成功,录音文件大小为0。是什么原因导致录音不成功呢?

           通话录音是一种比较危险权限,系统未Root时,除了系统自身授权,其他的应用是无法获取到这种权限的。
    因此即使看到应用已经授权录音权限,如果手机未Root,自己采集通话录音不能成功。红米Note4是稳定版的系统,

    因此应用不能采集到通话录音。


           出现这种情况,有两种解决方案:

           方式一:将手机Root。这是一种比较危险的操作。

           方式二:应用不进行录音,有系统录音,应用抓取系统的录音文件进行上传。


           对于未Root的手机,采用方式二进行处理。工作人员使用的手机是红米手机。小米手机的通话录音文件,

    保存在一个固定的目录:

    内存目录/MIUI/sound_recorder/call_rec,可以从目录中抓取通话录音文件。

    小米手机通话录音文件的保存的文件名也很有特征:通话录音@手机号xxx_时间串.mp3,

    我们可以根据手机号+时间串来抓取对应的通话录音文件

        /**
         * 从小米文件夹中抓取对应的录音文件
         *
         * @param phoneNumber     : 手机号
         * @param recordStartTime : 录音文件开始时间
         * @param son_order_id    : 订单ID
         */
        private void startFilterAudioFile(final String phoneNumber, final long recordStartTime,
                                          String son_order_id) {
            if (TextUtils.isEmpty(phoneNumber) || TextUtils.isEmpty(son_order_id)) {
                //手机号或者订单号不存在不需要上传
                return;
            }
            //首先检查录音文件保存目录是否存在
            if (!FileHelper.fileIsExists(Config.callRecordDir)) {
                //对应的录音文件夹不存在
                return;
            }
            //抓取对应的录音文件(手机号+通话开始时间)
            File dir = new File(Config.callRecordDir);
            if (!dir.isDirectory()) {
                return;
            }
            //获取其中的子文件列表
            File[] files = dir.listFiles(new FileFilter() {
    
                @Override
                public boolean accept(File pathname) {
                    if (pathname.isDirectory()) {
                        //子文件夹不处理
                        return false;
                    }
    
                    //检测录音文件有效性
                    if (pathname.length() <= 0) {
                        //删除空白文件
                        FileHelper.deleteFile(pathname.getPath());
                        return false;
                    }
    
                    //过滤文件名日志
                    String simpleName = pathname.getName();
                    //去掉所有的空格字符
                    simpleName = simpleName.replace(" ", "");
    
                    if (!simpleName.contains(phoneNumber)) {
                        return false;
                    }
    
                    //过滤文件名中的时间
                    String dateStr = parseDate(simpleName);
                    if(TextUtils.isEmpty(dateStr)) {
                        return false;
                    }
                    //最早的时间
                    Date fileDate = DateUtils.strToDate(dateStr, TimeUtil.FORMAT_NO_SPLIT_YYYY_MM_DD_CHS_HH_MM_SS);
                    //录音文件时间一定在录音开始时间和录音结束时间的监听范围内
                    long fileTime = fileDate.getTime();
                    return fileTime >= 0 && fileTime >= recordStartTime && fileTime <= recordStopTime;
                }
            });
            if (null == files || files.length <= 0) {
                //未找到符合条件的文件
                return;
            }
    
            //有多个文件符合条件时优先查找文件名刚好一致的文件
            File destFile = null;
            if(files.length > 1) {
                //如果有多个文件符合条件优先获取时间刚好相同的文件
                String str = TimeUtil.format(TimeUtil.FORMAT_NO_SPLIT_YYYY_MM_DD_CHS_HH_MM_SS, recordStartTime);
                for (File file : files) {
                    if(file.getName().contains(str)) {
                        //找到一个完全匹配的文件
                        destFile = file;
                        break;
                    }
                }
            }
            //如果没有找到完全匹配的文件默认采用过滤文件列表中第一个
            if(null == destFile) {
                destFile = files[0];
            }
    
            //开始上传录音文件
            startUploadFile(destFile.getPath(), son_order_id);
        }

    这样,对于未Root的小米手机,也能够实现自动抓取通话录音文件了。

          总结:对于功能开发,首先需要从安全型方面考虑,权限方面需要优先考虑。








    展开全文
  • 另外,新版本的Android 10系统会对录音有调整,引入了一个录音焦点的概念,也就是说以前的麦克风只能一个APP使用,必须要等它断开了别人才能用,现在换成可以抢的形式,也就是如果没有音焦,代码有可能不会报错,...

    废话

    权限、权限、权限,必须要先获取了录音权限,其他的事情晚点再说。

    另外,新版本的Android 10系统会对录音有调整,引入了一个录音焦点的概念,也就是说以前的麦克风只能一个APP使用,必须要等它断开了别人才能用,现在换成可以抢的形式,也就是如果没有音焦,代码有可能不会报错,但是是录不进声音的。

    Android系统API提供的录音方式就两种:MediaRecorder、AudioRecord

    MediaRecorder:简易模式,调用简单,只有开始、结束,录音之后的文件也是指定编码格式,系统播放器可以直接播放。

    AudioRecord:原始模式,可以暂停、继续,可以实时获取到录音录制的数据,然后进行一些骚操作,然后录出来的东西是最原始的pcm数据,系统播放器不能直接播放。

    MediaRecorder

    话不多说,直接上代码,具体用法,直接将需要保存文件的路径通过构造方法传进去,然后调用开始和结束方法即可:

    
    import android.media.MediaRecorder;
    import android.os.Handler;
    
    import java.io.File;
    import java.io.IOException;
    
    /**
     * 录音功能
     */
    
    public class MediaRecordingUtils {
    
        //文件路径
        private String filePath;
    
        private MediaRecorder mMediaRecorder;
        private final String TAG = "fan";
        public static final int MAX_LENGTH = 1000 * 60 * 200;// 最大录音时长,单位毫秒,1000*60*10;
    
        private OnAudioStatusUpdateListener audioStatusUpdateListener;
    
        /**
         * 文件存储默认sdcard/record
         */
        public MediaRecordingUtils() {
        }
    
        public MediaRecordingUtils(String filePath) {
            this.filePath=filePath;
    //        File path = new File(filePath);
    //        if (!path.exists())
    //            path.mkdirs();
    //        this.FolderPath = filePath;
        }
    
        private long startTime;
        private long endTime;
    
    
        /**
         * 开始录音 使用aac格式
         * 录音文件
         *
         * @return
         */
        public void startRecord() {
            // 开始录音
            /* ①Initial:实例化MediaRecorder对象 */
            if (mMediaRecorder == null)
                mMediaRecorder = new MediaRecorder();
            try {
                /* ②setAudioSource/setVedioSource */
                mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);// 设置麦克风
                /* ②设置音频文件的编码:AAC/AMR_NB/AMR_MB/Default 声音的(波形)的采样 */
                mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.DEFAULT);
                /*
                 * ②设置输出文件的格式:THREE_GPP/MPEG-4/RAW_AMR/Default THREE_GPP(3gp格式
                 * ,H263视频/ARM音频编码)、MPEG-4、RAW_AMR(只支持音频且音频编码要求为AMR_NB)
                 */
                mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
    
    //            filePath = FolderPath + DateUtil.getTimeForLong() + ".aac";
                /* ③准备 */
                mMediaRecorder.setOutputFile(filePath);
                mMediaRecorder.setMaxDuration(MAX_LENGTH);
                mMediaRecorder.prepare();
                /* ④开始 */
                mMediaRecorder.start();
                // AudioRecord audioRecord.
                /* 获取开始时间* */
                startTime = System.currentTimeMillis();
                updateMicStatus();
                ALog.e("fan", "startTime" + startTime);
            } catch (IllegalStateException e) {
                ALog.e(TAG, "call startAmr(File mRecAudioFile) failed!" + e.getMessage());
            } catch (IOException e) {
                ALog.e(TAG, "call startAmr(File mRecAudioFile) failed!" + e.getMessage());
            }
        }
    
        /**
         * 停止录音
         */
        public long stopRecord() {
            if (mMediaRecorder == null)
                return 0L;
            endTime = System.currentTimeMillis();
    
            //有一些网友反应在5.0以上在调用stop的时候会报错,翻阅了一下谷歌文档发现上面确实写的有可能会报错的情况,捕获异常清理一下就行了,感谢大家反馈!
            try {
                mMediaRecorder.stop();
                mMediaRecorder.reset();
                mMediaRecorder.release();
                mMediaRecorder = null;
    
                audioStatusUpdateListener.onStop(filePath);
                filePath = "";
    
            } catch (RuntimeException e) {
                try {
                    mMediaRecorder.reset();
                    mMediaRecorder.release();
                    mMediaRecorder = null;
    
                    File file = new File(filePath);
                    if (file.exists())
                        file.delete();
    
                    filePath = "";
                } catch (Exception e1) {
    
                }
    
            }
            return endTime - startTime;
        }
    
        /**
         * 取消录音
         */
        public void cancelRecord() {
    
            try {
    
                mMediaRecorder.stop();
                mMediaRecorder.reset();
                mMediaRecorder.release();
                mMediaRecorder = null;
    
            } catch (RuntimeException e) {
                mMediaRecorder.reset();
                mMediaRecorder.release();
                mMediaRecorder = null;
            }
            File file = new File(filePath);
            if (file.exists())
                file.delete();
    
            filePath = "";
    
        }
    
        private final Handler mHandler = new Handler();
        private Runnable mUpdateMicStatusTimer = new Runnable() {
            public void run() {
                updateMicStatus();
            }
        };
    
    
        private int BASE = 1;
        private int SPACE = 100;// 间隔取样时间
    
        public void setOnAudioStatusUpdateListener(OnAudioStatusUpdateListener audioStatusUpdateListener) {
            this.audioStatusUpdateListener = audioStatusUpdateListener;
        }
    
        /**
         * 更新麦克状态
         */
        private void updateMicStatus() {
    
            if (mMediaRecorder != null) {
                double ratio = (double) mMediaRecorder.getMaxAmplitude() / BASE;
                double db = 0;// 分贝
                if (ratio > 1) {
                    db = 20 * Math.log10(ratio);
                    if (null != audioStatusUpdateListener) {
                        audioStatusUpdateListener.onUpdate(db, System.currentTimeMillis() - startTime);
                    }
                }
                mHandler.postDelayed(mUpdateMicStatusTimer, SPACE);
            }
        }
    
        public String getFilePath() {
            return filePath;
        }
    
        public interface OnAudioStatusUpdateListener {
            /**
             * 录音中...
             *
             * @param db   当前声音分贝
             * @param time 录音时长
             */
            public void onUpdate(double db, long time);
    
            /**
             * 停止录音
             *
             * @param filePath 保存路径
             */
            public void onStop(String filePath);
        }
    
    }
    

    AudioRecord

    
    /**
     * 录音
     * 用法:1-init,filePath文件的后缀为.pcm 2-start  3-stop
     * stop之后,所有的音频数据会以pcm的格式写入到filePath这个文件内,并且是末尾添加的方式,而非覆盖(以达到暂停录音继续录音的效果),需要转换为其他格式才能让系统播放器直接播放
     */
    public class AudioRecordingUtils {
    
    
        //指定音频源 这个和MediaRecorder是相同的 MediaRecorder.AudioSource.MIC指的是麦克风
        private static final int mAudioSource = MediaRecorder.AudioSource.MIC;
        //指定采样率 (MediaRecoder 的采样率通常是8000Hz AAC的通常是44100Hz。 设置采样率为44100,目前为常用的采样率,官方文档表示这个值可以兼容所有的设置)
        private static final int mSampleRateInHz = 44100;
        //指定捕获音频的声道数目。在AudioFormat类中指定用于此的常量
        private static final int mChannelConfig = AudioFormat.CHANNEL_IN_STEREO; //立体声
        //指定音频量化位数 ,在AudioFormaat类中指定了以下各种可能的常量。通常我们选择ENCODING_PCM_16BIT和ENCODING_PCM_8BIT PCM代表的是脉冲编码调制,它实际上是原始音频样本。
        //因此可以设置每个样本的分辨率为16位或者8位,16位将占用更多的空间和处理能力,表示的音频也更加接近真实。
        private static final int mAudioFormat = AudioFormat.ENCODING_PCM_16BIT;
        //指定缓冲区大小。调用AudioRecord类的getMinBufferSize方法可以获得。
    
    
        private AudioRecord audioRecord = null;  // 声明 AudioRecord 对象
        private int recordBufSize = 0; // 声明recoordBufffer的大小字段
    
        private boolean isRecording = false;
    
        private String saveFilePath;
        //    private FileOutputStream os = null;
        private File mRecordingFile;
    
        private OnAudioRecordingListener onAudioRecordingListener;
    
        public void init(String filePath, OnAudioRecordingListener onAudioRecordingListener) {
            this.onAudioRecordingListener = onAudioRecordingListener;
            saveFilePath = filePath;
            recordBufSize = AudioRecord.getMinBufferSize(mSampleRateInHz, mChannelConfig, mAudioFormat);//计算最小缓冲区
            audioRecord = new AudioRecord(mAudioSource, mSampleRateInHz, mChannelConfig,
                    mAudioFormat, recordBufSize);//创建AudioRecorder对象
    
            //创建一个流,存放从AudioRecord读取的数据
            mRecordingFile = new File(saveFilePath);
            if (mRecordingFile.exists()) {//音频文件保存过了删除
                mRecordingFile.delete();
            }
            try {
                mRecordingFile.createNewFile();//创建新文件
            } catch (IOException e) {
                e.printStackTrace();
                ALog.e("lu", "创建储存音频文件出错");
            }
    
        }
        public static double bytes2Double(byte[] arr) {
            long value = 0;
            for (int i = 0; i < 8; i++) {
                value |= ((long) (arr[i] & 0xff)) << (8 * i);
            }
            return Double.longBitsToDouble(value);
        }
        public void startRecording() {
            //判断AudioRecord的状态是否初始化完毕
            //在AudioRecord对象构造完毕之后,就处于AudioRecord.STATE_INITIALIZED状态了。
            if (audioRecord == null || audioRecord.getState() == AudioRecord.STATE_UNINITIALIZED) {
                ALog.e("尚未初始化完成");
                return;
            }
    
            XyObservable.addTask(new XyCallBack() {//开一个子线程的意思
                private double volume = 0;
    
                @Override
                public void run() {
                    //标记为开始采集状态
                    isRecording = true;
                    try {
                        //获取到文件的数据流
                        DataOutputStream mDataOutputStream = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(mRecordingFile, true)));
                        byte[] buffer = new byte[recordBufSize];
                        audioRecord.startRecording();//开始录音
                        //getRecordingState获取当前AudioReroding是否正在采集数据的状态
                        while (isRecording && audioRecord.getRecordingState() == AudioRecord.RECORDSTATE_RECORDING) {
                            int bufferReadResult = audioRecord.read(buffer, 0, recordBufSize);
                            for (int i = 0; i < bufferReadResult; i++) {
                                mDataOutputStream.write(buffer[i]);
                            }
                            setFinish();//这里会调到下面的finish()方法,finish()方法处于UI线程中
                        }
                        mDataOutputStream.close();
                    } catch (Throwable t) {
                        ALog.e("lu", "Recording Failed");
                        stopRecording();
                    }
                }
    
                @Override
                public void finish() {
                    if (onAudioRecordingListener != null) {
                        onAudioRecordingListener.onChange(volume);
                    }
                }
            });
    
        }
    
        /**
         * 暂停录音
         */
        public void pauseRecording() {
            isRecording = false;
            if (audioRecord.getRecordingState() == AudioRecord.RECORDSTATE_RECORDING) {
                audioRecord.stop();
            }
        }
    
        //停止录音
        public void stopRecording() {
            isRecording = false;
            //停止录音,回收AudioRecord对象,释放内存
            if (audioRecord != null) {
                if (audioRecord.getRecordingState() == AudioRecord.RECORDSTATE_RECORDING) {
                    audioRecord.stop();
                }
                if (audioRecord.getState() == AudioRecord.STATE_INITIALIZED) {
                    audioRecord.release();
                }
            }
        }
    
        public interface OnAudioRecordingListener {
            public void onChange(double volume);
        }
    }
    

    然后再附带一个将原始pcm转换为wav格式的方法:

    
    public class Pcm2WavUtils {
    
        /**
         * PCM文件转WAV文件
         *
         * @param inPcmFilePath  输入PCM文件路径
         * @param outWavFilePath 输出WAV文件路径
         * @param sampleRate     采样率,例如44100
         * @param channels       声道数 单声道:1或双声道:2
         * @param bitNum         采样位数,8或16
         */
        public void convertPcm2Wav(String inPcmFilePath, String outWavFilePath, int sampleRate,
                                          int channels, int bitNum) {
    
            FileInputStream in = null;
            FileOutputStream out = null;
            byte[] data = new byte[1024];
    
            try {
                //采样字节byte率
                long byteRate = sampleRate * channels * bitNum / 8;
    
                in = new FileInputStream(inPcmFilePath);
                out = new FileOutputStream(outWavFilePath);
    
                //PCM文件大小
                long totalAudioLen = in.getChannel().size();
    
                //总大小,由于不包括RIFF和WAV,所以是44 - 8 = 36,在加上PCM文件大小
                long totalDataLen = totalAudioLen + 36;
    
                writeWaveFileHeader(out, totalAudioLen, totalDataLen, sampleRate, channels, byteRate);
    
                int length = 0;
                while ((length = in.read(data)) > 0) {
                    out.write(data, 0, length);
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                if (in != null) {
                    try {
                        in.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
                if (out != null) {
                    try {
                        out.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    
        /**
         * 输出WAV文件
         *
         * @param out           WAV输出文件流
         * @param totalAudioLen 整个音频PCM数据大小
         * @param totalDataLen  整个数据大小
         * @param sampleRate    采样率
         * @param channels      声道数
         * @param byteRate      采样字节byte率
         * @throws IOException
         */
        private void writeWaveFileHeader(FileOutputStream out, long totalAudioLen,
                                                long totalDataLen, int sampleRate, int channels, long byteRate) throws IOException {
            byte[] header = new byte[44];
            header[0] = 'R'; // RIFF
            header[1] = 'I';
            header[2] = 'F';
            header[3] = 'F';
            header[4] = (byte) (totalDataLen & 0xff);//数据大小
            header[5] = (byte) ((totalDataLen >> 8) & 0xff);
            header[6] = (byte) ((totalDataLen >> 16) & 0xff);
            header[7] = (byte) ((totalDataLen >> 24) & 0xff);
            header[8] = 'W';//WAVE
            header[9] = 'A';
            header[10] = 'V';
            header[11] = 'E';
            //FMT Chunk
            header[12] = 'f'; // 'fmt '
            header[13] = 'm';
            header[14] = 't';
            header[15] = ' ';//过渡字节
            //数据大小
            header[16] = 16; // 4 bytes: size of 'fmt ' chunk
            header[17] = 0;
            header[18] = 0;
            header[19] = 0;
            //编码方式 10H为PCM编码格式
            header[20] = 1; // format = 1
            header[21] = 0;
            //通道数
            header[22] = (byte) channels;
            header[23] = 0;
            //采样率,每个通道的播放速度
            header[24] = (byte) (sampleRate & 0xff);
            header[25] = (byte) ((sampleRate >> 8) & 0xff);
            header[26] = (byte) ((sampleRate >> 16) & 0xff);
            header[27] = (byte) ((sampleRate >> 24) & 0xff);
            //音频数据传送速率,采样率*通道数*采样深度/8
            header[28] = (byte) (byteRate & 0xff);
            header[29] = (byte) ((byteRate >> 8) & 0xff);
            header[30] = (byte) ((byteRate >> 16) & 0xff);
            header[31] = (byte) ((byteRate >> 24) & 0xff);
            // 确定系统一次要处理多少个这样字节的数据,确定缓冲区,通道数*采样位数
            header[32] = (byte) (channels * 16 / 8);
            header[33] = 0;
            //每个样本的数据位数
            header[34] = 16;
            header[35] = 0;
            //Data chunk
            header[36] = 'd';//data
            header[37] = 'a';
            header[38] = 't';
            header[39] = 'a';
            header[40] = (byte) (totalAudioLen & 0xff);
            header[41] = (byte) ((totalAudioLen >> 8) & 0xff);
            header[42] = (byte) ((totalAudioLen >> 16) & 0xff);
            header[43] = (byte) ((totalAudioLen >> 24) & 0xff);
            out.write(header, 0, 44);
        }
    }
    展开全文
  • android 权限录音权限检测

    万次阅读 2016-04-22 10:53:55
    最近在项目中要用到语音通话功能,后来测试发现通话过程中有听不到对方声音的情况,经过检测 其中有部分原因是因为有些手机在app安装后会被手机直接禁止录音权限,发现问题后去解决的过程发现,系统自己提供的检查...
    
    
    最近在项目中要用到语音通话功能,后来测试发现通话过程中有听不到对方声音的情况,经过检测 其中有部分原因是因为有些手机在app安装后会被手机直接禁止录音权限,发现问题后去解决的过程发现,系统自己提供的检查app所获的权限方法
    

    boolean flag = (PackageManager.PERMISSION_GRANTED == 
    pm.checkPermission("android.permission.RECORD_AUDIO", "包名")); 
    boolean flag = PermissionChecker.checkSelfPermission(this, Manifest.permission.)== PermissionChecker.PERMISSION_GRANTED; 
    if (flag){ 
    ToastUtil.showMessage("有权限"); 
    }else { 
    ToastUtil.showMessage("无权限"); 
    return; 
    }
     
    在6.0系统一下 无论是关闭或者打开app的录音权限都能获取到权限,6.0以上则正常,后来发现是6.0以后google加强了权限管理,6.0之前的权限检测只是检测到是否在清单文件中注册,这个方法显示不适用于目前自己的项目,后来经过一番查找终于找到了一个通用的方法

      // 音频获取源
        public static int audioSource = MediaRecorder.AudioSource.MIC;
        // 设置音频采样率,44100是目前的标准,但是某些设备仍然支持22050,16000,11025
        public static int sampleRateInHz = 44100;
        // 设置音频的录制的声道CHANNEL_IN_STEREO为双声道,CHANNEL_CONFIGURATION_MONO为单声道
        public static int channelConfig = AudioFormat.CHANNEL_IN_STEREO;
        // 音频数据格式:PCM 16位每个样本。保证设备支持。PCM 8位每个样本。不一定能得到设备支持。
        public static int audioFormat = AudioFormat.ENCODING_PCM_16BIT;
        // 缓冲区字节大小
        public static int bufferSizeInBytes = 0;
        /**
         * 判断是是否有录音权限
         */
        public static boolean isHasPermission(final Context context){
            bufferSizeInBytes = 0;
            bufferSizeInBytes = AudioRecord.getMinBufferSize(sampleRateInHz,
                    channelConfig, audioFormat);
            AudioRecord audioRecord =  new AudioRecord(audioSource, sampleRateInHz,
                    channelConfig, audioFormat, bufferSizeInBytes);
            //开始录制音频
            try{
                // 防止某些手机崩溃,例如联想
                audioRecord.startRecording();
            }catch (IllegalStateException e){
                e.printStackTrace();
            }
            /**
             * 根据开始录音判断是否有录音权限
             */
            if (audioRecord.getRecordingState() != AudioRecord.RECORDSTATE_RECORDING) {
                DialogUtils.createDialogBox3(context, "", context.getResources().getString(R.string.setting_tips), context.getResources().getString(R.string.cancel), context.getResources().getString(R.string.setting), new DialogUtils.DialogOnClick() {
                    @Override
                    public void leftOnClick() {
    
                    }
    
                    @Override
                    public void rightOnClick() {
                        context.startActivity(new Intent(Settings.ACTION_MANAGE_APPLICATIONS_SETTINGS));
                    }
                });
    
                return false;
            }
            audioRecord.stop();
            audioRecord.release();
            audioRecord = null;
    
            return true;
        }
    经过测试 这个方法目前能试用于所有机型..
    ps:做android开发 兼容性问题确实让人头疼啊  
    展开全文
  • 如何使Android录音实现内录功能

    千次阅读 2020-08-13 11:24:41
    原址 背景 之前在做直播的时候需要使用到内录功能,比如经常看到游戏主播在直播玩游戏,游戏的...相信大家都很熟悉Android如果录音的了: int frequency = 44100; int audioEncoding = AudioFormat.ENCODING_PCM

    背景

    之前在做直播的时候需要使用到内录功能,比如经常看到游戏主播在直播玩游戏,游戏的声音不是通过MIC录制的,而是内录完成的。故在此记录一下。

    相信大家都很熟悉Android如果录音的了:

    
             int frequency = 44100;
            int audioEncoding = AudioFormat.ENCODING_PCM_16BIT;
            int channelConfiguration = AudioFormat.CHANNEL_CONFIGURATION_MONO;
            int minBufferSize = AudioRecord.getMinBufferSize(frequency, channelConfiguration,    audioEncoding);
            int audioSource = MediaRecorder.AudioSource.MIC;
            AudioRecord audioRecord = new AudioRecord(audioSource, frequency,
                    channelConfiguration, audioEncoding, minBufferSize);
            audioRecord.startRecording();
            ...

    AudioSource输入源介绍

    项目 介绍 权限
    DEFAULT 默认。在源码 system/media/audio/include/system/audio.h配置默认项
    MIC 麦克风
    VOICE_UPLINK 电话录音上行线路 android.permission.CAPTURE_AUDIO_OUTPUT,系统权限不允许第三方app使用
    VOICE_DOWNLINK 电话录音下行线路 android.permission.CAPTURE_AUDIO_OUTPUT,系统权限不允许第三方app使用
    VOICE_CALL 电话录音上下线路 android.permission.CAPTURE_AUDIO_OUTPUT,系统权限不允许第三方app使用
    CAMCORDER 摄像头的麦克风
    VOICE_RECOGNITION 语音识别
    VOICE_COMMUNICATION 网络电话
    REMOTE_SUBMIX 传输到远程的音频混合流。默认情况下如何用该项录音,本地扬声器或者耳机的声音将会被截走 android.permission.CAPTURE_AUDIO_OUTPUT,系统权限不允许第三方app使用

    好了,现在我们知道了REMOTE_SUBMIX可以实现内录功能了。有两点比较麻烦:

    • 需要系统权限

    • 会截走扬声器和耳机的声音,也就是说再录音时本地无法播放声音

    系统权限问题

    这个对我来说比较好办,因为我是直接在android设备板子上开发,可以直接使用系统签名编译。首先在AndroidManifest.xml添加
    android:sharedUserId="android.uid.system"
    其次,

    第一种方法:

    adb shell 执行:
    signapk.jar platform.x509.pem platform.pk8 app-unsigned.apk signed.apk
    adb push signed.apk /system/app

    第二种方法:

    编写Android.mk : 设置签名为platform

    LOCAL_PATH := $(call my-dir)
    include $(CLEAR_VARS)
    LOCAL_MODULE_TAGS := optional
    LOCAL_SRC_FILES := $(call all-java-files-under, src) 
    LOCAL_PACKAGE_NAME := YourApp
    LOCAL_CERTIFICATE := platform
    LOCAL_PROGUARD_FLAG_FILES := proguard.flags
    include $(BUILD_PACKAGE)
    include $(call all-makefiles-under,$(LOCAL_PATH))

    截走扬声器和耳机的声音问题

    修改framework下av/services/audiopolicy/AudioPolicyManager.cpp

    
    audio_devices_t AudioPolicyManager::getDeviceForStrategy(routing_strategy strategy,
                                                                 bool fromCache)

    getDeviceForStrategy方法下找到

    if (mAvailableOutputDevices.getDevice(AUDIO_DEVICE_OUT_REMOTE_SUBMIX, String8("0")) != 0) {
           device2 = availableOutputDeviceTypes & AUDIO_DEVICE_OUT_REMOTE_SUBMIX;
    }

    修改为

    if (mAvailableOutputDevices.getDevice(AUDIO_DEVICE_OUT_REMOTE_SUBMIX, String8("0")) != 0) {
           device2 = availableOutputDeviceTypes & AUDIO_DEVICE_OUT_REMOTE_SUBMIX;
    
           device2 |= (availableOutputDeviceTypes & AUDIO_DEVICE_OUT_WIRED_HEADPHONE);
    
           device2 |= (availableOutputDeviceTypes & AUDIO_DEVICE_OUT_SPEAKER);
    }

    意思是声音输出的设备添加了耳机和扬声器,这里可根据实际情况设置。
    至此,将最开始的录音代码

    int audioSource = MediaRecorder.AudioSource.MIC;

    改成

    int audioSource = MediaRecorder.AudioSource.REMOTE_SUBMIX;

    就可以实现内录功能了。
    <br/>


    ps:

    在不修改源码的情况下,第三方app目前暂不知如何实现内录。

    延伸阅读

    5.0以后请求Android录屏默认会弹出确认框,但在系统app下请求就不会弹出了(具体可以去看源码)。这也是为了谷歌为了安全考虑。
    不过5.0的时候这个弹框却是一个大漏洞,被国内360发现了,给你们链接^_^
    Android 5.0屏幕录制漏洞

    展开全文
  • /** * */ package media; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.File;...import java.io.FileInputStream;...import java.io.FileNotFoundException;...
  • Android路上--AudioRecord 录音详解

    千次阅读 2018-12-25 12:00:15
    AudioRecord + AudioTrack + AudioFormat,通过pcm文件合并并转wav实现录音、暂停、播放、停止、重录、计时等功能。
  • try { if (isSpecialDevice()) { // 音频获取源 int audioSource = MediaRecorder.AudioSource.MIC; // 设置音频采样率,44100是目前的标准,但是某些设备仍然支持22050,16000,11025...
  • Android 音频录音与播放

    千次阅读 2019-07-24 17:59:48
    介绍音频的采集、编码、生成文件、转码等操作,通过 AudioRecord 采集音频,生成三种格式的文件格式(pcm、wav、aac),用 AudioStack 来播放这个音频。...所谓 PCM 录音就是将声音等模拟信号变成符号化的脉...
  • 转载MTK通话背景音

    千次阅读 2015-06-05 16:50:40
    简述一下,MTK手机有两种前无古人、非常有用的功能:魔音通话通话背景音。这两种功能在现实中就是一把双刃剑,有利有弊,看你怎么使用。所以一些比较大牌的手机公司,例如联想,通常在它一些非智能的用MTK芯片的...
  • 在单路录音中,有两种情况导致底层录音资源被占用的问题: 1 开启vmLog后,拨打一个电话,挂断电话。如果挂断电话后,没有关闭vmlog进程,则会导致其它AP 无法得到底层的录音资源,从而无法录音。 2 打开第三方...
  • Android 判断是否有录音权限

    千次阅读 2017-05-24 18:13:06
    最近在项目中要用到语音通话功能,后来测试发现通话过程中有听不到对方声音的情况,经过检测 其中有部分原因是因为有些手机在app安装后会被手机直接禁止录音权限,发现问题后去解决的过程发现,系统自己提供的检查...
  • android系统添加USB AUDIO设备的放音和录音功能
  • android 录音

    千次阅读 2012-06-07 10:35:34
    直接给上个详细的使用Android MediaRecorder进行手机录音解说代码: package cn.com.chenzheng_java.media; import java.io.IOException; import android.app.Activity; import android.media.MediaRecorder; ...
  • Android深入浅出之Audio 第一部分 AudioTrack分析 一目的 本文的目的是通过从Audio系统来分析Android的代码,包括Android自定义的那套机制和一些常见类的使用,比如Thread,MemoryBase等。 分析的流程...
  • 直接给上个详细的解说代码: package ... import java.io.IOException; ...import android.app.Activity; import android.media.MediaRecorder; import android.os.Bundle; /** * @description...
  • audio代码比较复杂,除了...建议先抽空看看如下代码:kernel, linux alsa 架构:kernel-3.10/sound/soc/mediatek/kernel-3.10/Documentation/sound/alsa/soc/android 上层alsa接口external/tinyalsa/hal:vendor/mediate
  • 这篇文章主要介绍了Android App调用MediaRecorder实现录音功能的实例,MediaRecorder非常强大,不仅能够用来录制音频还可以录制视频,需要的朋友可以参考下 MediaRecorder Android的MediaRecorder包含了Audio和...
1 2 3 4 5 ... 13
收藏数 260
精华内容 104
关键字:

单声道 通话录音android