Android 录音MediaRecorder到AudioRecord

研究录音是源于即时通讯的项目。写出一个即时通讯很简单,但是写好一个即时通讯就不是一件容易的事,比如聊天中语音的加入。接下来就来描述一下自己对语音的见解和处理方式。

首先写到语音,当然首当其冲的是运用到网上百分之八九十的处理方案MediaRecorder,这个也是我首先用到的方式,主要是由于以前写过接入环信的项目,它里面所提供的就是MediaRecorder。大概的来介绍一下这个类的几个常用方法吧。

Initial:初始状态,当使用new()方法创建一个MediaRecorder对象或者调用了reset()方法时,该MediaRecorder对象处于Initial状态。在设定视频源或者音频源之后将转换为Initialized状态。另外,在除Released状态外的其它状态通过调用reset()方法都可以使MediaRecorder进入该状态。

Initialized:已初始化状态,可以通过在Initial状态调用setAudioSource()或setVideoSource()方法进入该状态。在这个状态可以通过setOutputFormat()方法设置输出格式,此时MediaRecorder转换为DataSourceConfigured状态。另外,通过reset()方法进入Initial状态。

DataSourceConfigured:数据源配置状态,这期间可以设定编码方式、输出文件、屏幕旋转、预览显示等等。可以在Initialized状态通过setOutputFormat()方法进入该状态。另外,可以通过reset()方法回到Initial状态,或者通过prepare()方法到达Prepared状态。
Prepared:就绪状态,在DataSourceConfigured状态通过prepare()方法进入该状态。在这个状态可以通过start()进入录制状态。另外,可以通过reset()方法回到Initialized状态。
Recording:录制状态,可以在Prepared状态通过调用start()方法进入该状态。另外,它可以通过stop()方法或reset()方法回到Initial状态。
Released:释放状态(官方文档给出的词叫做Idle state 空闲状态),可以通过在Initial状态调用release()方法来进入这个状态,这时将会释放所有和MediaRecorder对象绑定的资源。
Error:错误状态,当错误发生的时候进入这个状态,它可以通过reset()方法进入Initial状态

这个MediaRecorder的主要是有停供的方法来进行编码语音,好处自然就是方便,另外一个就是它所编译的语音文件体积非常小。大概有AAC和ARM用得比较多,环信使用的就是ARM的编码方式。其实我觉得差别不是很大,都不是特别好的录音,对于项目要求不高的可以考虑下,下面我也会粘贴出工具类,方便使用(EaseVoiceRecorder),对了,忘了说,它还提供通道设置setAudioChannels  1是单声道 2是多声道;setAudioSamplingRate采样率,网上说的是越大越好,但是我选了很多中个人觉得16000比较适中。

import java.io.File;
import java.io.IOException;
import java.util.Date;


import android.content.Context;
import android.content.pm.PackageManager;
import android.media.MediaRecorder;
import android.os.Handler;
import android.os.SystemClock;
import android.text.format.Time;
import android.util.Log;

import com.lvgou.distribution.driect.entity.EMError;
import com.lvgou.distribution.utils.PathUtil;

public class EaseVoiceRecorder {
    MediaRecorder recorder;

    static final String PREFIX = "voice";
    static final String EXTENSION = ".mp3";
    String uid;

    private boolean isRecording = false;
    private long startTime = -4;
    private String voiceFilePath = null;
    private String voiceFileName = null;
    private File file;
    private Handler handler;
    private Context mContext;

//    public EaseVoiceRecorder(Handler handler) {
//        this.handler = handler;
//    }

    public EaseVoiceRecorder() {

    }

    /**
     * @param appContext
     * @param userId     传入userId 用于标示 名称
     * @return
     */
    public String startRecording(Context appContext, String userId) {
        mContext = appContext;
        file = null;
        startTime = -4;
        try {
            // need to create recorder every time, otherwise, will got exception
            // from setOutputFile when try to reuse
            if (recorder != null) {
                recorder.release();
                recorder = null;
            }
            recorder = new MediaRecorder();
            recorder.setAudioSource(MediaRecorder.AudioSource.MIC);
//            recorder.setOutputFormat(MediaRecorder.OutputFormat.AAC_ADTS);
//            recorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
//            recorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
//            recorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
/*//           方案一
           recorder.setOutputFormat(MediaRecorder.OutputFormat.AMR_NB);
            recorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);
            recorder.setAudioChannels(2); // MONO
            recorder.setAudioSamplingRate(16000); // 8000Hz*/
//           方案二
           recorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
            recorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
            recorder.setAudioChannels(2);
            recorder.setAudioSamplingRate(16000);
/*//            方案三
            recorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP);
            recorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);*/
//            recorder.setAudioEncodingBitRate(64); // seems if change this to
            // 128, still got same file
            // size.
            // one easy way is to use temp file
            // file = File.createTempFile(PREFIX + userId, EXTENSION,
            // User.getVoicePath());
            voiceFileName = getVoiceFileName(userId);
            voiceFilePath = PathUtil.getInstance().getVoicePath() + "/" + voiceFileName;
            file = new File(voiceFilePath);
            recorder.setOutputFile(file.getAbsolutePath());
            recorder.prepare();
            isRecording = true;
            recorder.start();
        } catch (IOException e) {

        }
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    while (isRecording) {
//                        android.os.Message msg = new android.os.Message();
//                        msg.what = recorder.getMaxAmplitude() * 13 / 0x7FFF;
//                        handler.sendMessage(msg);
                        SystemClock.sleep(100);
                    }
                } catch (Exception e) {
                    // from the crash report website, found one NPE crash from
                    // one android 4.0.4 htc phone
                    // maybe handler is null for some reason
                }
            }
        }).start();
        startTime = new Date().getTime();
        return file == null ? null : file.getAbsolutePath();
    }

    /**
     * stop the recoding
     *
     * @return seconds of the voice recorded
     */

    public void discardRecording() {
        if (recorder != null) {
            try {
                recorder.stop();
                recorder.release();
                recorder = null;
                if (file != null && file.exists() && !file.isDirectory()) {
                    file.delete();
                }
            } catch (IllegalStateException e) {
            } catch (RuntimeException e) {
            }
            isRecording = false;
        }
    }

    public int getRatio() {
        if (recorder != null) {
            int ratio = recorder.getMaxAmplitude() / 600;
            return ratio;
        }
        return -1;
    }

    public int stopRecoding() {
        if (recorder != null) {
            isRecording = false;
            if (startTime > -1) {
                try {
                    recorder.stop();
                    recorder.release();
                } catch (Exception e) {
                    e.printStackTrace();
                }
                recorder = null;
            }
            int seconds = (int) (new Date().getTime() - startTime) / 1000;
            if (seconds > 0) {
                if (file == null || !file.exists() || !file.isFile()) {
                    return EMError.FILE_INVALID;
                }
                if (file.length() == 0) {
                    file.delete();
                    return EMError.FILE_INVALID;
                }
            }
            return seconds;
        }
        return 0;
    }

    protected void finalize() throws Throwable {
        super.finalize();
        if (recorder != null) {
            recorder.release();
        }
    }

    private String getVoiceFileName(String uid) {
        Time now = new Time();
        now.setToNow();
        this.uid = uid;
        return uid + now.toString().substring(0, 15) + EXTENSION;
    }

    public boolean isRecording() {
        return isRecording;
    }

    public String getVoiceFilePath() {
        return voiceFilePath;
    }

    public String getVoiceTargetFilePath() {
        Time now = new Time();
        now.setToNow();
        return PathUtil.getInstance().getVoicePath() + "/" + uid + now.toString().substring(0, 15) + ".mp3";
    }

    public String getVoiceFileName() {
        return voiceFileName;
    }
}

体积这么小而且这么方便,很大的一个缺点,也是我放弃的理由,就是录下的音质不太好,总感觉被什么笼罩着在。因此就开始寻求其它的解决方式,接下来就引用到了AudioRecord。主要是因为ios也是在使用这个所以就尝试着加入它来试试。大概描述一下简单工作流程:
1.创建一个数据流。
2.构造一个AudioRecord对象,其中需要的最小录音缓存buffer大小可以通过getMinBufferSize方法得到。如果buffer容量过小,将导致对象构造的失败。
3.初始化一个buffer,该buffer大于等于AudioRecord对象用于写声音数据的buffer大小。
4.开始录音。
5.从AudioRecord中读取声音数据到初始化buffer,将buffer中数据导入数据流。
6.停止录音。
7.关闭数据流。
首先遇到的问题是录音下来的东西是不能播放的,就是PCM格式的音频文件,也就是通常遇到的raw文件。简单的来说就是裸音。如果需要播放的话,必须要给裸数据加上头文件。这样得到的就是WAV格式的音频的文件,也是是电脑上经常使用的无损音质了,但是对于这么好的音质,引来的第一大问题就是文件体积比较大简单来说录音10秒大概就有四百多KB的大小。这可能对于即时通讯的项目是不行的,先粘贴出工具类先,方便大家其它地方有用到。
import android.media.AudioFormat;
import android.media.AudioRecord;
import android.os.Environment;
import android.text.format.Time;
import android.util.Log;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Date;


import static com.lvgou.distribution.driect.AudioFileFunc.isSdcardExit;

/**
 * Created by Administrator on 2017/6/12.
 */

public class ChatAudioRecord {
    // 缓冲区字节大小
    private int bufferSizeInBytes = 0;

    //AudioName裸音频数据文件 ,麦克风
    private String AudioName = "";

    //NewAudioName可播放的音频文件
    private String NewAudioName = "";

    private AudioRecord audioRecord;
    private boolean isRecord = false;// 设置正在录制的状态

    public boolean isRecording() {
        return isRecord;
    }

    private static ChatAudioRecord mInstance;

    private ChatAudioRecord() {

    }

    public synchronized static ChatAudioRecord getInstance() {
        if (mInstance == null)
            mInstance = new ChatAudioRecord();
        return mInstance;
    }

    public String getVoiceFilePath() {
       /* try {
            execute(new File(NewAudioName),myuid);
            Log.e("lkhfkhsdfg", "--------"+NewAudioName );
            return NewAudioName;
        } catch (Exception e) {
            e.printStackTrace();
            Log.e("lkhfkhsdfg", "---------"+e );

        }
        return "";*/
//        writeMP3();
        return NewAudioName;
    }
    private String myuid="";
    public int startRecordAndFile(String uid) {
        //判断是否有外部存储设备sdcard
        if (isSdcardExit()) {
            if (isRecord) {
                return ErrorCode.E_STATE_RECODING;
            } else {
                if (audioRecord == null)
                    myuid=uid;
                    creatAudioRecord(uid);

                audioRecord.startRecording();
                // 让录制状态为true
                isRecord = true;
                // 开启音频文件写入线程
                new Thread(new AudioRecordThread()).start();
                startTime = new Date().getTime();
                return ErrorCode.SUCCESS;
            }

        } else {
            return ErrorCode.E_NOSDCARD;
        }

    }

    private long startTime = -4;

    public int stopRecordAndFile() {
        close();
        int seconds = (int) (new Date().getTime() - startTime) / 1000;
        return seconds;
    }


    public long getRecordFileSize() {
        return AudioFileFunc.getFileSize(NewAudioName);
    }


    private void close() {
        if (audioRecord != null) {
            System.out.println("stopRecord");
            isRecord = false;//停止文件写入
            audioRecord.stop();
            audioRecord.release();//释放资源
            audioRecord = null;
        }
    }

    private String uid = "";
    static final String EXTENSION = ".mp3";

    private String getVoiceFileName(String uid,String namess) {
        Time now = new Time();
        now.setToNow();
        this.uid = uid;
        return uid + now.toString().substring(0, 15) + namess;
    }

    private final static String AUDIO_RAW_FILENAME = "RawAudio.raw";
    private final static String AUDIO_WAV_FILENAME = "FinalAudio.wav";
    public final static String AUDIO_AMR_FILENAME = "FinalAudio.amr";

    public String getRawFilePath(String userId) {
        String mAudioRawPath = "";
        if (isSdcardExit()) {
            String fileBasePath = Environment.getExternalStorageDirectory().getAbsolutePath();
            mAudioRawPath = fileBasePath + "/" + getVoiceFileName(userId,AUDIO_RAW_FILENAME);
        }

        return mAudioRawPath;
    }
    /**
     * 获取编码后的WAV格式音频文件路径
     * @return
     */
    public String getWavFilePath(String userId){
        String mAudioWavPath = "";
        if(isSdcardExit()){
            String fileBasePath = Environment.getExternalStorageDirectory().getAbsolutePath();
            mAudioWavPath = fileBasePath+"/"+getVoiceFileName(userId,AUDIO_WAV_FILENAME);
        }
        return mAudioWavPath;
    }

    private void creatAudioRecord(String userId) {
        // 获取音频文件路径
        AudioName = getRawFilePath(userId);
//        voiceFilePath= voiceFilePath = PathUtil.getInstance().getVoicePath() + "/" + voiceFileName;
//        NewAudioName = AudioFileFunc.getWavFilePath();
        NewAudioName = getWavFilePath(userId);

        // 获得缓冲区字节大小
        bufferSizeInBytes = AudioRecord.getMinBufferSize(AudioFileFunc.AUDIO_SAMPLE_RATE,
                AudioFormat.CHANNEL_IN_STEREO, AudioFormat.ENCODING_PCM_16BIT);

        // 创建AudioRecord对象
        audioRecord = new AudioRecord(AudioFileFunc.AUDIO_INPUT, AudioFileFunc.AUDIO_SAMPLE_RATE,
                AudioFormat.CHANNEL_IN_STEREO, AudioFormat.ENCODING_PCM_16BIT, bufferSizeInBytes);
    }


    class AudioRecordThread implements Runnable {
        @Override
        public void run() {
            writeDateTOFile();//往文件中写入裸数据
            copyWaveFile(AudioName, NewAudioName);//给裸数据加上头文件
//            writeMP3();
        }
    }

    /**
     * 这里将数据写入文件,但是并不能播放,因为AudioRecord获得的音频是原始的裸音频,
     * 如果需要播放就必须加入一些格式或者编码的头信息。但是这样的好处就是你可以对音频的 裸数据进行处理,比如你要做一个爱说话的TOM
     * 猫在这里就进行音频的处理,然后重新封装 所以说这样得到的音频比较容易做一些音频的处理。
     */
    private void writeDateTOFile() {
        // new一个byte数组用来存一些字节数据,大小为缓冲区大小
        byte[] audiodata = new byte[bufferSizeInBytes];
        FileOutputStream fos = null;
        int readsize = 0;
        try {
            File file = new File(AudioName);
            if (file.exists()) {
                file.delete();
            }
            fos = new FileOutputStream(file);// 建立一个可存取字节的文件
        } catch (Exception e) {
            e.printStackTrace();
        }
        while (isRecord == true) {
            readsize = audioRecord.read(audiodata, 0, bufferSizeInBytes);
            if (AudioRecord.ERROR_INVALID_OPERATION != readsize && fos != null) {
                try {
                    fos.write(audiodata);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        try {
            if (fos != null)
                fos.close();// 关闭写入流
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    // 这里得到可播放的音频文件
    private void copyWaveFile(String inFilename, String outFilename) {
        FileInputStream in = null;
        FileOutputStream out = null;
        long totalAudioLen = 0;
        long totalDataLen = totalAudioLen + 36;
        long longSampleRate = AudioFileFunc.AUDIO_SAMPLE_RATE;
        int channels = 2;
        long byteRate = 16 * AudioFileFunc.AUDIO_SAMPLE_RATE * channels / 8;
        byte[] data = new byte[bufferSizeInBytes];
        try {
            in = new FileInputStream(inFilename);
            out = new FileOutputStream(outFilename);
            totalAudioLen = in.getChannel().size();
            totalDataLen = totalAudioLen + 36;
            WriteWaveFileHeader(out, totalAudioLen, totalDataLen,
                    longSampleRate, channels, byteRate);
            while (in.read(data) != -1) {
                out.write(data);
            }
            in.close();
            out.close();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 这里提供一个头信息。插入这些信息就可以得到可以播放的文件。
     * 为我为啥插入这44个字节,这个还真没深入研究,不过你随便打开一个wav
     * 音频的文件,可以发现前面的头文件可以说基本一样哦。每种格式的文件都有
     * 自己特有的头文件。
     */
    private void WriteWaveFileHeader(FileOutputStream out, long totalAudioLen,
                                     long totalDataLen, long longSampleRate, int channels, long byteRate)
            throws IOException {
        byte[] header = new byte[44];
        header[0] = 'R'; // RIFF/WAVE header
        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';
        header[9] = 'A';
        header[10] = 'V';
        header[11] = 'E';
        header[12] = 'f'; // 'fmt ' chunk
        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;
        header[20] = 1; // format = 1
        header[21] = 0;
        header[22] = (byte) channels;
        header[23] = 0;
        header[24] = (byte) (longSampleRate & 0xff);
        header[25] = (byte) ((longSampleRate >> 8) & 0xff);
        header[26] = (byte) ((longSampleRate >> 16) & 0xff);
        header[27] = (byte) ((longSampleRate >> 24) & 0xff);
        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) (2 * 16 / 8); // block align
        header[33] = 0;
        header[34] = 16; // bits per sample
        header[35] = 0;
        header[36] = 'd';
        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);
    }


}

接下来就来处理体积大这个问题,首先我用到的是lame,就是去网上找到mp3lame.so库,然后修改它的mk文件,将其引入到项目中,其实也就是在录音的过程中将音频文件的编码改成mp3格式。于是就在github上找了相关的文章
https://github.com/search?q=android+mp3+&type=Repositories&ref=searchresults
https://github.com/yhirano/Mp3VoiceRecorderSampleForAndroid
关于使用,只需要项目中的libmp3lame.so文件,和com.uraroji.garage.android.lame包下的SimpleLame.java文件和RecMicToMp3.java文件,注意SimpleLame.java必须放在com.uraroji.garage.android.lame包下。
但是对于按照它的操作出现的问题就是
这一下就尴尬了,然后在网上找了一些解决方式,后来报错就是找不到so库的错,接着我又将so库复制到其它的几个文件夹里面还是相同的问题,我然后就去修改那些.h和.c文件。希望在这里找到一些解药,对于一个多年没接触C语言的人来说,那个里面的调用着实让我看不懂。因此就放弃了这条路。
接着就是VoAACEncorder,这个方式,我也记不得是在github上哪个项目弄下来的。首先是它的demo可以运行,并且音质也很不错,基本就是AAC的音频格式,并且这个格式的体积并不大,我录了120秒的音频文件大概就只有470kb左右的大小吧,当我看到这里的时候认定自己选择的就是这个,接下来就想方设法的研究出来然后插入到自己项目中。主要的就是jni中so库的试用,github里的项目是ec编写的,但是对于现在这个as称道的时代,还是一个一个复制吧。就两个so文件libAacEncoder.so和libVoAACEncoder.so。方法也就那几个,我也粘贴出util.
首先出现的问题就和lame后面那个一样,找不到so库,

这对于一个使用jni不熟练的人来说确实是个难题。其实这个问题的原因就是那个java文件中的native方法,native标识的方法就是调用C语言的地方,由于没有找到指定的位子。但是,对于标识指定位子的文件就是so库里面的mk文件里面标识的,这个对于我们来说是不方便编辑和更改的,但是我们可以更改外部编写有native方法文件的包名啊。按照mk文件里的包名进行重新创造。首先在我的一台测试机上运行成功,也能正常录音,但是当我换一台测试机的时候,又报了一个错,就是缺少64位的so库。这个问题就很尴尬了啊,提供的是armeabi里面的so啊,这个应该是32位和64位通用的啊,然后我发现我还有一些其它的文件夹,安卓在寻找so文件的时候是会优先寻找和自己匹配的文件夹下的so,没有找打就会报错。接着我就引入了armeabi-v7a包,在这个下面把armeabi里面的so文件拷贝过来,这个也就是处理兼容性问题的地方。然后就能顺利运行和录音了。但是还是存在一个缺点,对于即时通讯的项目是录音完成后马上发送的,这个录音的结构来说是边录边转码的,很有可能在你没转码成功的情况下就进行发送,这就导致服务器那边出现找不到文件的错误。我的处理方式是在录音完成后会停顿一秒,然后再进行发送,这样就大大降低了发送失败的次数。但偶尔还是会存在失败,接着我就在失败返回监听的方法里面再次将失败了的文件重新发送,如果还是失败,那么我就告诉用户失败了,在消息后面加一个红色边框里面是感叹号,让用户自己点击后重新发送,目前这样的处理方式是没有出现过失败的了。
import android.text.format.Time;
import android.util.Log;

import com.lvgou.distribution.utils.PathUtil;

import java.util.Date;

import lwx.linin.aac.VoAAC;

/**
 * Created by Administrator on 2017/6/14.
 */

public class ACCVoiceRecorder {
    private int sampleRateInHz = 16000;
    private VoAAC aac;
    private String fileName;
    static final String EXTENSION = ".mp3";
    private String uid = "";
    private long startTime = -4;
    private boolean isRecording = false;//是否正在录音
    private String voiceFilePath;

    public ACCVoiceRecorder() {
    }

    public String getVoiceFilePath() {
        return voiceFilePath;
    }

    public void startRecording(String userId) {
        fileName = getVoiceFileName(userId);

        voiceFilePath = PathUtil.getInstance().getVoicePath() + "/" + fileName;
        Log.e("aslkdfhakshfd", "------------"+voiceFilePath );
        aac = new VoAAC(voiceFilePath);
        aac.sampleRateInHz(sampleRateInHz);
        aac.start();
        startTime = new Date().getTime();
        isRecording = true;
    }

    private String getVoiceFileName(String uid) {
        Time now = new Time();
        now.setToNow();
        this.uid = uid;
        return uid + now.toString().substring(0, 15) + EXTENSION;
    }

    public int stopRecoding() {
        int seconds = (int) (new Date().getTime() - startTime) / 1000;
        aac.stop();
        isRecording = false;
        return seconds;
    }

    public void discardRecording() {
        stopRecoding();
    }

    public boolean isRecording() {
        return isRecording;
    }

    public String getVoiceFileName() {
        return fileName;
    }
}


猜你喜欢

转载自blog.csdn.net/greatdaocaoren/article/details/73433527