【Android】AudioRecord--录音并将 PCM文件转为WAV

新博客地址http://www.jianshu.com/u/7ae8671ca7f4

前言

Android提供可以用来录音的有AudioRecord和MediaRecord,通过MediaRecord录音,我们可以选择所要录音的格式,然后录完之后,自动生成这种格式的文件,而AudioRecord得到的是PCM编码格式的数据,因此如果我们要对录音的数据做处理,显然我们需要的是通过后者,同时后者还支持对于录音过程中,将模拟信号转化为数字信号的相关参数的设置,其中包括采样率和量化深度,同时也包括通道数目。由于项目的需要所以选择采用AudioRecord,今天写了下,将录音得到的PCM数据转化为WAV,github上应该有这样的工具类可以拿来用,只需要进行一次转化,也没必要搞这么多。

实现功能

通过AudioRecord进行录音,将生成数据写入PCM文件,然后转化为Wav文件

AudioRecord

AudioRecord是Android中用来管理从音频硬件设备获取的音频资源,我们可以对其进行模拟信号向数字信号转化的时候的一些参数的设置,模拟信号向数字信号进行转换的过程为采样,量化,编码。模拟信号是连续的信号,而数字信号是离散的,首先对模拟信号进行采样,采样率即为每一秒采样的次数,量化即为对我们采样得到的数据的一种表示,量化深度即为我们采用多少位的数据进行表示,此处编码过程和量化是同时进行的。每一个AudioRecord对象,通过read()从硬件设备中读取相应的数据,系统会根据采样频率,通道等参数为每个对象分配相应大小的缓存。

构造函数

AudioRecord(int audioSource, int sampleRateInHz, int channelConfig, int audioFormat, int bufferSizeInBytes)

audioSource:音频的来源,常用的为麦克风,其它的几个没有做过多的研究。
samplerateInHz:这个就是采样频率,奈奎斯特定理。我们一般采用44100HZ(在进行模拟/数字信号的转换过程中,当采样频率fs.max大于信号中最高频率fmax的2倍时(fs.max>2fmax),采样之后的数字信号完整地保留了原始信号中的信息,采用的频率正好为人耳所能接听最大频率的两倍)
channelConfig:音频的通道,单声道还是立体声,如果是立体声那么就会又两个声道,所谓声道就是在录制或者播放的过程中,从不同的位置采集或者回放的不同音频信号。即为声源数量和扬声器的数量。
audioFormat:该参数为量化深度,即为每次采样的位数
bufferSizeInBytes:通过 getMinBufferSize()方法可以获得,根据我们采样录制的过程中的参数来确定,每次从硬件读取数据所需要的缓冲区的大小。

开始录音,停止录音

public void startRecording ()
public void stop ()

获取数据

read (byte[] audioData, int offsetInBytes, int sizeInBytes)

通过read函数,我们可以将从硬件获得的音频数据写到我们的一个数组之中,然后建立一个文件输出流,将数据写到外部文件中。如果需要对数据做处理,则在此处进行,如进行一些变声操作等。

此时我们得到数据写到文件中,然后做处理,而不能够对其做一个实时的数据比对或者是其他的处理,然后看到了下面这个方法

录音数据实时处理

public int setNotificationMarkerPosition (int markerInFrames)
public int setPositionNotificationPeriod (int periodInFrames)
public void setRecordPositionUpdateListener (AudioRecord.OnRecordPositionUpdateListener listener, Handler handler)

释放资源

public void release ()

释放掉资源之后,要将原来的对象设为null,涉及到release和null的一带你问题,我们将一个对象设为null,那么这个对象是没有指向的,如果release的话是将对象指向的内存释放掉,但是那段内存还是存在的,对象还是指向哪里的,所以如果在release之后,不置为null,如果在后面误调用了,将会出现问题。

PCM

PCM是在由模拟信号向数字信号转化的一种常用的编码格式,称为脉冲编码调制,PCM将模拟信号按照一定的间距划分为多段,然后通过二进制去量化每一个间距的强度。PCM表示的是音频文件中随着时间的流逝的一段音频的振幅。Android在WAV文件中支持PCM的音频数据。

WAV文件

WAV,MP3等是我们比较常见的音频格式,不同的编码格式对原始音频采用的编码方式也是不同的,通常为了方便传输等问题,会对原始音频进行压缩,同时为了能够使得播放器能够识别该种格式,所以在每种格式的头文件都是特定的,有一定的规则,来让播放器识别出是该种格式,然后按着相应的解码算法去播放后面的音频文件。
WAV是一RIFF为标准的,RIFF是一种资源交换档案标准,是将文件存储在每一个标记块中的档案格式。基本构成单位是trunk,每个trunk是由标记位,数据大小,数据存储,三个部分构成的。
WAV的头文件表示格式分析
基本必要组成有三个trunk,RIFF,FMT,Data

byte[] header = new byte[44];
        //RIFF WAVE Chunk
        // RIFF标记占据四个字节
        header[0] = 'R'; 
        header[1] = 'I';
        header[2] = 'F';
        header[3] = 'F';
        //数据大小表示,由于原始数据为long型,通过四次计算得到长度
        header[4] = (byte) (totalDataLen & 0xff);
        header[5] = (byte) ((totalDataLen >> 8) & 0xff);
        header[6] = (byte) ((totalDataLen >> 16) & 0xff);
        header[7] = (byte) ((totalDataLen >> 24) & 0xff);
        //WAVE标记占据四个字节
        header[8] = 'W';
        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) (longSampleRate & 0xff);
        header[25] = (byte) ((longSampleRate >> 8) & 0xff);
        header[26] = (byte) ((longSampleRate >> 16) & 0xff);
        header[27] = (byte) ((longSampleRate >> 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) (1 * 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);

RIFF块只是单纯对写标记符,FMT块是对录音相应的一些参数的设置,通道数,采样率和采样深度等,Data块是对数据的大小的表示。
在当时看的时候遇到的一个问题就是在

header[40] = (byte) (totalAudioLen & 0xff);
        header[41] = (byte) ((totalAudioLen >> 8) & 0xff);
        header[42] = (byte) ((totalAudioLen >> 16) & 0xff);
        header[43] = (byte) ((totalAudioLen >> 24) & 0xff);

看不明白其所要表示的意思,然后请教了一个学长之后,有所得,同时也想起来了,之前做的一些关于通过位和逻辑运算解决的一些问题,意识到其重要性。

遇到问题

0xff表示的是16进制的数据即为1111 ,1111。&运算符即为同时为1的时候该位为1,否则为0,由于totalAudioLen是long型,而数组是byte型的,所以无法存放,byte是8位,long是32位,所以想到拆分为4个八位来存放,通过&运算,第一步将源数据末尾8位获得到了,因为0xff的前24为全是0,所以起到一个筛选的作用,这样通过移位最终达到了long型数据,然后存在数组之中。

实现流程

主线程中录音,然后通过一个线程用来将录音数据写入到文件中。将录音和文件的转化封装起来。

代码实现

public class AudioUtil {

    private static AudioUtil mInstance;
    private AudioRecord recorder;
    //录音源
    private static int audioSource = MediaRecorder.AudioSource.MIC;
    //录音的采样频率
    private static int audioRate = 44100;
    //录音的声道,单声道
    private static int audioChannel = AudioFormat.CHANNEL_IN_MONO;
    //量化的深度
    private static int audioFormat = AudioFormat.ENCODING_PCM_16BIT;
    //缓存的大小
    private static int bufferSize = AudioRecord.getMinBufferSize(audioRate,audioChannel,audioFormat);
    //记录播放状态
    private boolean isRecording = false;
    //数字信号数组
    private byte [] noteArray;
    //PCM文件
    private File pcmFile;
    //WAV文件
    private File wavFile;
    //文件输出流
    private OutputStream os;
    //文件根目录
    private String basePath = Environment.getExternalStorageDirectory().getAbsolutePath()+"/yinfu/";
    //wav文件目录
    private String outFileName = basePath+"/yinfu.wav";
    //pcm文件目录
    private String inFileName = basePath+"/yinfu.pcm";

    private AudioUtil(){
        createFile();//创建文件
        recorder = new AudioRecord(audioSource,audioRate,audioChannel,audioFormat,bufferSize);
    }

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

    //读取录音数字数据线程
    class WriteThread implements Runnable{
        public void run(){
            writeData();
        }
    }

    //开始录音
    public void startRecord(){
        isRecording = true;
        recorder.startRecording();
    }

    //停止录音
    public void stopRecord(){
        isRecording = false;
        recorder.stop();
    }

    //将数据写入文件夹,文件的写入没有做优化
    public void writeData(){
        noteArray = new byte[bufferSize];
        //建立文件输出流
        try {
            os = new BufferedOutputStream(new FileOutputStream(pcmFile));
        }catch (IOException e){

        }
        while(isRecording == true){
            int recordSize = recorder.read(noteArray,0,bufferSize);
            if(recordSize>0){
                try{
                    os.write(noteArray);
                }catch(IOException e){

                }
            }
        }
        if (os != null) {
            try {
                    os.close();
                }catch (IOException e){

                }
        }
    }

    // 这里得到可播放的音频文件
    public void convertWaveFile() {
        FileInputStream in = null;
        FileOutputStream out = null;
        long totalAudioLen = 0;
        long totalDataLen = totalAudioLen + 36;
        long longSampleRate = AudioUtil.audioRate;
        int channels = 1;
        long byteRate = 16 *AudioUtil.audioRate * channels / 8;
        byte[] data = new byte[bufferSize];
        try {
            in = new FileInputStream(inFileName);
            out = new FileOutputStream(outFileName);
            totalAudioLen = in.getChannel().size();
            //由于不包括RIFF和WAV
            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();
        }
    }

    /*
    任何一种文件在头部添加相应的头文件才能够确定的表示这种文件的格式,wave是RIFF文件结构,每一部分为一个chunk,其中有RIFF WAVE chunk,
    FMT Chunk,Fact chunk,Data chunk,其中Fact chunk是可以选择的,
     */
    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
        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) (longSampleRate & 0xff);
        header[25] = (byte) ((longSampleRate >> 8) & 0xff);
        header[26] = (byte) ((longSampleRate >> 16) & 0xff);
        header[27] = (byte) ((longSampleRate >> 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) (1 * 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);
    }

    //创建文件夹,首先创建目录,然后创建对应的文件
    public void createFile(){
        File baseFile = new File(basePath);
        if(!baseFile.exists())
            baseFile.mkdirs();
        pcmFile = new File(basePath+"/yinfu.pcm");
        wavFile = new File(basePath+"/yinfu.wav");
        if(pcmFile.exists()){
            pcmFile.delete();
        }
        if(wavFile.exists()){
           wavFile.delete();
        }
        try{
            pcmFile.createNewFile();
            wavFile.createNewFile();
        }catch(IOException e){

        }
    }

    //记录数据
    public void recordData(){
        new Thread(new WriteThread()).start();
    }
}

本篇主要是对AudioRecord录音,将PCM转化为WAV上,虽然涉及到文件的读写操作比较多,但是此处也不做过多的介绍了,准备单独开一篇博客写写关于IO的问题。还有对录音实时数据的处理也要研究下了。

你可能感兴趣的:(Android之路)