Coder Social home page Coder Social logo

zhoumeng1990 / audioanalyze Goto Github PK

View Code? Open in Web Editor NEW
83.0 4.0 16.0 410 KB

Android 主要实现录音功能,集合了录音、暂停、停止、播放、重录、计时、pcm转wav等功能,分别传了java和kotlin版本

Kotlin 48.76% Java 51.24%
audiorecord audiotrack pause

audioanalyze's Introduction

AudioRecord + AudioTrack + AudioFormat,通过pcm文件合并并转wav实现录音、暂停、播放、停止、重录、计时等功能

微信公众号:IT小顽童 最近公司的一个需求,集合了录音、暂停、停止、完成、播放、重录、计时等功能,没找到合适的资源,大部分都不支持暂停,就自己整理并整合了一个,参考官方文档和一些资料后,撸了个库,现在做一些说明和解析。

官方文档地址https://developer.android.google.cn/reference/android/media/AudioRecord

之前除了搞FFmpeg那段时间,音频这块接触的也不多,最近用Android原生去实现,并做下笔记,配合着已经上传 GitHub 上的代码,观赏性会更好一点,目前上传了 java 和 kotlin 版本,功能都已实现,并在实际项目中运行。本篇主要采用java去讲解,kotlin版本可自行参照,注释和实现方式都一样。


1、简单对比

AudioRecord和MediaRecorder两种都可以录制音频,MediaRecorder基于文件录音,系统已实现大量的封装,集成了录音、编码、压缩等,支持少量的音频格式文件,操作起来更加简单,而AudioRecord基于字节流录音,AudioTrack更接近底层,使用起来更加灵活,能实现更多的功能。因为考虑到需要对音频进行一系列处理,所以嘛,最后选择了AudioRecord。如下是简单的对比:

优点:可以实现语音的实时处理,进行边录边播,对音频的实时处理,AudioTrack更接近底层。

缺点:输出的是PCM的语音数据,如果保存成音频文件是不能被播放器播放的,需要用AudioTrack来播放。API还有待完善,常见的暂停功能都不支持。

优点:系统封装的完整,直接调用即可,操作简单,录制的音频文件可以用系统自带的播放器播放。

缺点:无法实现实时处理音频,输出的音频格式少。录制的音频文件是经过压缩后的,需要设置编码器。

今天只做AudioRecord的一些说明,MediaRecorder不做过多说明。


2、专业名词说明

采样率

  • 采样率:采样率即采样频率,指每秒钟取得声音样本的次数,采样频率越高,能表现的频率范围就越大,音质就会越好,声音的还原度也更真实,但此同时带来的弊端是占有的内存资源也会越大。因为人耳的分辨率有限,并不是频率越高越好,44KHz已相当于CD音质了,目前的常用采样频率都不超过48KHz。

声道

  • 声道:这个好理解,生活中也经常听到单声道、双声道等,在Android系统中,可以通过设置音频的录制的声道CHANNEL_IN_STEREO为双声道,CHANNEL_CONFIGURATION_MONO为单声道,双声道音质更加,但同样伴随着内存资源消耗更大的弊端。

采样位深

  • 采样位深:位深度也叫采样位深,音频的位深度决定动态范围,它是用来衡量声音波动变化的一个参数,也可以说是声卡的分辨率。它的数值越大,分辨率也就越高,所发出声音的能力越强。在计算机中采样位数一般有8位和16位之分,即分成2的8次方和2的16次方之分,PCM 16位每个样本,保证设备支持。PCM 8位每个样本,不一定能得到设备支持。

构造函数

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

audioSource:录音源,指定声音是从哪里录制的,官网文档参考戳此

sampleRateInHz:采样率

channelConfig:声道数

audioFormat:采样位深

bufferSizeInBytes:最小缓冲大小,可以通过getMinBufferSize获取。

补充

补充:存储量= 采样率 * 采样时间 * 采样位深 / 8 * 声道数(Bytes)。以采样率为44.1kHZ、采样位深为16位、双声道计算,一分钟消耗的内存为10.335M。


3、实现流程

流程图

初始化AudioRecord对象

    /**
     * 创建默认的录音对象
     *
     * @param fileName 文件名
     */
    public void createDefaultAudio(String fileName) {
        // 获得缓冲区字节大小
        bufferSizeInBytes = AudioRecord.getMinBufferSize(AUDIO_SAMPLE_RATE, AUDIO_CHANNEL, AUDIO_ENCODING);
        audioRecord = new AudioRecord(AUDIO_INPUT, AUDIO_SAMPLE_RATE, AUDIO_CHANNEL, AUDIO_ENCODING, bufferSizeInBytes);
        this.fileName = fileName;
        status = AudioStatus.STATUS_READY;

        AudioAttributes audioAttributes = new AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_MEDIA)
                .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC).build();

        AudioFormat audioFormat = new AudioFormat.Builder().setSampleRate(AUDIO_SAMPLE_RATE)
                .setEncoding(AUDIO_ENCODING).setChannelMask(AudioFormat.CHANNEL_OUT_MONO).build();

        audioTrack = new AudioTrack(audioAttributes, audioFormat, bufferSizeInBytes,
                AudioTrack.MODE_STREAM, AudioManager.AUDIO_SESSION_ID_GENERATE);
    }

以上参数已经介绍完,此处不做多余解释。

开始录音

    /**
     * 开始录音
     */
    public void startRecord() {
        if (status == AudioStatus.STATUS_NO_READY || TextUtils.isEmpty(fileName)) {
            throw new IllegalStateException("请检查录音权限");
        }
        if (status == AudioStatus.STATUS_START) {
            throw new IllegalStateException("正在录音");
        }
        audioRecord.startRecording();
        cachedThreadPool.execute(new Runnable() {
            @Override
            public void run() {
                writeDataTOFile();
            }
        });
    }
    /**
     * 将音频信息写入文件
     */
    private void writeDataTOFile() {
        // new一个byte数组用来存一些字节数据,大小为缓冲区大小
        byte[] audioData = new byte[bufferSizeInBytes];
        FileOutputStream fos = null;
        int readSize = 0;
        try {
            String currentFileName = fileName;
            if (status == AudioStatus.STATUS_PAUSE) {
                //假如是暂停录音 将文件名后面加个数字,防止重名文件内容被覆盖
                currentFileName += filesName.size();
            }
            filesName.add(currentFileName);
            File file = new File(FileUtils.getPcmFileAbsolutePath(currentFileName));
            if (file.exists()) {
                file.delete();
            }
            // 建立一个可存取字节的文件
            fos = new FileOutputStream(file);
        } catch (IllegalStateException e) {
            e.printStackTrace();
            throw new IllegalStateException(e.getMessage());
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
        //将录音状态设置成正在录音状态
        status = AudioStatus.STATUS_START;
        while (status == AudioStatus.STATUS_START) {
            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();
        }
    }

暂停/继续录制

    /**
     * 暂停录音
     */
    public void pauseRecord() {
        if (status != AudioStatus.STATUS_START) {
            throw new IllegalStateException("没有在录音");
        } else {
            audioRecord.stop();
            status = AudioStatus.STATUS_PAUSE;
        }
    }

说明:为啥说好的是暂停,此处确实停止?上文也曾提到过,AudioRecord没有提供暂停的API,目前所实现的的暂停,就是不断的使用停止和再次录制,形成多个文件,然后在合并成一个文件并转码成wav,最终形成 AudioFormat 可读取的文件,以此来达到暂停和在录制的功能。

停止录音

    /**
     * 停止录音
     */
    public void stopRecord() {
        if (status == AudioStatus.STATUS_NO_READY || status == AudioStatus.STATUS_READY) {
            throw new IllegalStateException("录音尚未开始");
        } else {
            audioRecord.stop();
            status = AudioStatus.STATUS_STOP;
            release();
        }
    }

执行此方法后,便赋值给status,以此来改变状态。

释放资源

    /**
     * 释放资源
     */
    public void release() {
        //假如有暂停录音
        try {
            if (filesName.size() > 0) {
                List<String> filePaths = new ArrayList<>();
                for (String fileName : filesName) {
                    filePaths.add(FileUtils.getPcmFileAbsolutePath(fileName));
                }
                //清除
                filesName.clear();
                if (isReset) {
                    isReset = false;
                    FileUtils.clearFiles(filePaths);
                } else {
                    //将多个pcm文件转化为wav文件
                    pcmFilesToWavFile(filePaths);
                }
            }
        } catch (IllegalStateException e) {
            throw new IllegalStateException(e.getMessage());
        }

        if (audioRecord != null) {
            audioRecord.release();
            audioRecord = null;
        }
        status = AudioStatus.STATUS_NO_READY;
    }

完成、合成、转码

/**
 * Created by ZhouMeng on 2018/8/31.
 * 将pcm文件转化为wav文件
 * pcm是无损wav文件中音频数据的一种编码方式,pcm加上wav文件头就可以转为wav格式,但wav还可以用其它方式编码。
 * 此类就是通过给pcm加上wav的文件头,来转为wav格式
 */
public class PcmToWav {
    /**
     * 合并多个pcm文件为一个wav文件
     * @param filePathList    pcm文件路径集合
     * @param destinationPath 目标wav文件路径
     * @return true|false
     */
    public static boolean mergePCMFilesToWAVFile(List<String> filePathList, String destinationPath) {
        File[] file = new File[filePathList.size()];
        byte buffer[] = null;

        int TOTAL_SIZE = 0;
        int fileNum = filePathList.size();

        for (int i = 0; i < fileNum; i++) {
            file[i] = new File(filePathList.get(i));
            TOTAL_SIZE += file[i].length();
        }

        // 填入参数,比特率等等。这里用的是16位单声道 8000 hz
        WaveHeader header = new WaveHeader();
        // 长度字段 = 内容的大小(TOTAL_SIZE) + 头部字段的大小(不包括前面4字节的标识符RIFF以及fileLength本身的4字节)
        header.fileLength = TOTAL_SIZE + (44 - 8);
        header.FmtHdrLeth = 16;
        header.BitsPerSample = 16;
        header.Channels = 2;
        header.FormatTag = 0x0001;
        header.SamplesPerSec = 8000;
        header.BlockAlign = (short) (header.Channels * header.BitsPerSample / 8);
        header.AvgBytesPerSec = header.BlockAlign * header.SamplesPerSec;
        header.DataHdrLeth = TOTAL_SIZE;

        byte[] h = null;
        try {
            h = header.getHeader();
        } catch (IOException e1) {
            Log.e("PcmToWav", e1.getMessage());
            return false;
        }

        // WAV标准,头部应该是44字节,如果不是44个字节则不进行转换文件
        if (h.length != 44) {
            return false;
        }

        //先删除目标文件
        File destFile = new File(destinationPath);
        if (destFile.exists()) {
            destFile.delete();
        }

        //合成所有的pcm文件的数据,写到目标文件
        try {
            buffer = new byte[1024 * 4]; // Length of All Files, Total Size
            InputStream inStream = null;
            OutputStream ouStream = null;

            ouStream = new BufferedOutputStream(new FileOutputStream(
                    destinationPath));
            ouStream.write(h, 0, h.length);
            for (int j = 0; j < fileNum; j++) {
                inStream = new BufferedInputStream(new FileInputStream(file[j]));
                int size = inStream.read(buffer);
                while (size != -1) {
                    ouStream.write(buffer);
                    size = inStream.read(buffer);
                }
                inStream.close();
            }
            ouStream.close();
        } catch (IOException ioe) {
            ioe.getMessage();
            return false;
        }
        FileUtils.clearFiles(filePathList);
//        File wavFile = new File(new File(destinationPath).getParent());
//        if (wavFile.exists()) {
//            FileUtils.deleteFile(wavFile);
//        }

        return true;
    }
}
/**
 * Created by ZhouMeng on 2018/8/31.
 * wav文件头
 */
public class WaveHeader {
    public final char fileID[] = {'R', 'I', 'F', 'F'};
    public int fileLength;
    public char wavTag[] = {'W', 'A', 'V', 'E'};
    public char FmtHdrID[] = {'f', 'm', 't', ' '};
    public int FmtHdrLeth;
    public short FormatTag;
    public short Channels;
    public int SamplesPerSec;
    public int AvgBytesPerSec;
    public short BlockAlign;
    public short BitsPerSample;
    public char DataHdrID[] = {'d','a','t','a'};
    public int DataHdrLeth;

    public byte[] getHeader() throws IOException {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        WriteChar(bos, fileID);
        WriteInt(bos, fileLength);
        WriteChar(bos, wavTag);
        WriteChar(bos, FmtHdrID);
        WriteInt(bos,FmtHdrLeth);
        WriteShort(bos,FormatTag);
        WriteShort(bos,Channels);
        WriteInt(bos,SamplesPerSec);
        WriteInt(bos,AvgBytesPerSec);
        WriteShort(bos,BlockAlign);
        WriteShort(bos,BitsPerSample);
        WriteChar(bos,DataHdrID);
        WriteInt(bos,DataHdrLeth);
        bos.flush();
        byte[] r = bos.toByteArray();
        bos.close();
        return r;
    }

    private void WriteShort(ByteArrayOutputStream bos, int s) throws IOException {
        byte[] myByte = new byte[2];
        myByte[1] =(byte)( (s << 16) >> 24 );
        myByte[0] =(byte)( (s << 24) >> 24 );
        bos.write(myByte);
    }

    private void WriteInt(ByteArrayOutputStream bos, int n) throws IOException {
        byte[] buf = new byte[4];
        buf[3] =(byte)( n >> 24 );
        buf[2] =(byte)( (n << 8) >> 24 );
        buf[1] =(byte)( (n << 16) >> 24 );
        buf[0] =(byte)( (n << 24) >> 24 );
        bos.write(buf);
    }

    private void WriteChar(ByteArrayOutputStream bos, char[] id) {
        for (char c : id) {
            bos.write(c);
        }
    }
}

4字节数据,内容为“RIFF”,表示资源交换文件标识

4字节数据,内容为一个整数,表示从下个地址开始到文件尾的总字节数

4字节数据,内容为“WAVE”,表示WAV文件标识

4字节数据,内容为“fmt ”,表示波形格式标识(fmt ),最后一位空格。

4字节数据,内容为一个整数,表示PCMWAVEFORMAT的长度

2字节数据,内容为一个短整数,表示格式种类(值为1时,表示数据为线性PCM编码)

2字节数据,内容为一个短整数,表示通道数,单声道为1,双声道为2

4字节数据,内容为一个整数,表示采样率,比如44100

4字节数据,内容为一个整数,表示波形数据传输速率(每秒平均字节数),大小为 采样率 * 通道数 * 采样位深

2字节数据,内容为一个短整数,表示DATA数据块长度,大小为 通道数 * 采样位深

2字节数据,内容为一个短整数,表示采样位数,即PCM位宽,通常为8位或16位

4字节数据,内容为“data”,表示数据标记符

4字节数据,内容为一个整数,表示接下来声音数据的总大小

播放

    /**
     * 播放合成后的wav文件
     *
     * @param filePath 文件的绝对路径
     */
    public void play(final String filePath) {
        audioTrack.play();

        cachedThreadPool.execute(new Runnable() {

            @Override
            public void run() {
                File file = new File(filePath);
                FileInputStream fis = null;
                try {
                    fis = new FileInputStream(file);
                } catch (FileNotFoundException e) {
                    e.printStackTrace();
                }
                byte[] buffer = new byte[bufferSizeInBytes];
                while (fis != null) {
                    try {
                        int readCount = fis.read(buffer);
                        if (readCount == AudioTrack.ERROR_INVALID_OPERATION || readCount == AudioTrack.ERROR_BAD_VALUE) {
                            continue;
                        }
                        if (readCount != 0 && readCount != -1) {
                            audioTrack.write(buffer, 0, readCount);
                        }
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        });
    }

释放资源

    /**
     * 释放audioTrack
     */
    public void releaseAudioTrack(){
        if (audioTrack == null) {
            return;
        }
        if (audioTrack.getPlayState() != AudioTrack.PLAYSTATE_STOPPED) {
            audioTrack.stop();
        }
        audioTrack.release();
        audioTrack = null;
    }

此处释放的资源,主要是audioTrack对象,在这之前已经释放过一次资源,上一次释放的为audioRecord对象相关。

重置

    /**
     * 重置,删除所有的pcm文件
     */
    private boolean isReset = false;

    public void setReset() {
        isReset = true;
    }

主要通过设置 isReset 参数来实现重置,此操作就是执行停止的操作,在停止的过程中做判断,把录制好的文件都删除,不在走合并和转码的过程,以此来达到重置的效果。

本篇就先到这里,后续还会有一篇讲解这过程中遇到的问题和解决方式。


微信扫我

audioanalyze's People

Contributors

zhoumeng1990 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

audioanalyze's Issues

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.