凌霄的博客
音频开发之录制pcm格式文件
音频开发之录制pcm格式文件

前几篇的文章都是camera下采集视频数据进行显示,保存下来的文件也是h264格式的,并没有包含音频数据,所以多多少少有点单调的感觉。没有声音的视频是没有灵魂的,所以最近了解了一下音频相关的开发,给视频注入灵魂。

https://blog-1252348761.cos.ap-chengdu.myqcloud.com/audio/2aa952e856da4d46855855635a771be8.gif

1. 基础知识

开始音频学习之前,有必要先了解一下基础知识,因为在音频开发过程中,经常会涉及到这些。掌握了这些重要的概念,在学习中很多参数的配置会更容易理解。

  1. PCM编码格式

    首先看看百度百科给出的解释:PCM 脉冲编码调制是Pulse Code Modulation的缩写。脉冲编码调制是数字通信的编码方式之一。主要过程是将话音、图像等模拟信号每隔一定时间进行取样,使其离散化,同时将抽样值按分层单位四舍五入取整量化,同时将抽样值按一组二进制码来表示抽样脉冲的幅值。 这个解释得非常抽象,反正我是没看懂⊙﹏⊙。简单的来说就是将声音数字化,转换为二进制序列,这样就可以把声音保存下来了,而保存它的容器可以是mp3,wav等等容器。

  2. 音频采集输入源

    这个就相当于声明孩子他妈是谁,也就是说声音的源头在哪儿。可选的类型以常量的形式定义在 MediaRecorder.AudioSource 类中 ,比较常用的是下面几个

    1. MediaRecorder.AudioSource.CAMCORDER 设定录音来源于同方向的相机麦克风相同,若相机无内置相机或无法识别,则使用预设的麦克风
    2. MediaRecorder.AudioSource.DEFAULT  默认音频源
    3. MediaRecorder.AudioSource.MIC 设定录音来源为主麦克风
    4. MediaRecorder.AudioSource.VOICE_CALL设定录音来源为语音拨出的语音与对方说话的声音
    5. MediaRecorder.AudioSource.VOICE_COMMUNICATION 摄像头旁边的麦克风
    6. MediaRecorder.AudioSource.VOICE_RECOGNITION 语音识别
  3. 采样率

    我们把采样到的一个个静止画面再以采样率同样的速度回放时,看到的就是连续的画面。同样的道理,把以44.1kHZ采样率记录的CD以同样的速率播放时,就能听到连续的声音。显然,这个采样率越高,听到的声音和看到的图像就越连贯。当然,人的听觉和视觉器官能分辨的采样率是有限的,基本上高于44.1kHZ采样的声音,绝大部分人已经觉察不到其中的分别了。 而目前44100Hz是唯一可以保证兼容所有Android手机的采样率 。所以,如果不是特殊设备和用途,这个值建议设置为44100。

  4. 通道数

    声道数一般表示声音录制时的音源数量或回放时相应的扬声器数量。常用的有:单通道和双通道。 可选的值以常量的形式定义在 AudioFormat 类中,常用的是 CHANNEL_IN_MONO(单通道),CHANNEL_IN_STEREO(双通道)

  5. 量化精度

    1. 声音的位数就相当于画面的颜色数,表示每个取样的数据量,当然数据量越大,回放的声音越准确,不至于把开水壶的叫声和火车的鸣笛混淆。同样的道理,对于画面来说就是更清晰和准确,不至于把血和西红柿酱混淆。不过受人的器官的机能限制,16位的声音和24位的画面基本已经是普通人类的极限了,电话就是3kHZ取样的7位声音,而CD是44.1kHZ取样的16位声音,所以CD就比电话更清楚。
    2. 对于一个采样点,需要用二进制数字来表示,这个二进制的精度可以是:4bit、8bit、16bit、32bit。 位数越多,表示的声音就越精细,声音的质量就越好。不过数据量也会变大。
  6. 帧间隔

    音频不像视频那样,有一帧一帧的概念。它是约定一个时间为单位,然后这个时间内的数据为一帧,这个时间被称为采样时间。这个时间没有特别的标准,要看具体的编解码器。

2. 音频录制

了解音频开发相关基础知识之后,我们就可以开始使用android提供的相关API实现音频的录制了,android提供了两套音频录制的API:

  1. MediaRecorder:比较上层的 API,它可以直接把手机麦克风的音频数据进行编码然后储存成文件。使用简单,但是支持的格式有限,并且不支持对音频进行进一步的处理,例如变声、混音等。
  2. AudioRecord:比较底层的一个 API,能够得到原始的 PCM音频数据。由于我们得到的是原始的 PCM 数据,我们可以对音频进行进一步的处理,例如编码、混音和变声等。

这里主要介绍的是使用AudioRecord进行录制,MediaRecorder的录制比较简单,就不做过多介绍了。首先看看AudioRecord的构造函数:

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

需要传入5个参数,分别是输入源,采样率,通道数,量化精度,音频缓冲区的大小 。其中最后一个也是最重要的一个参数,它代表音频缓冲区的大小,该缓冲区的值不能低于一帧音频帧的大小,

一帧音频帧大小 = 采样率 x 位宽 x 采样时间 x 通道数 ,这个值不用我们自己计算,AudioRecord 类提供了一个帮助你确定这个值的函数 :

public static int getMinBufferSize(int sampleRateInHz, int channelConfig, int audioFormat)

当创建好AudioRecord对象之后,就可以开始进行音频数据的采集了,控制采集的开始和停止的方法是下面这两个函数:

public void startRecording()
public void stop()

开始采集之后,通过线程循环取走音频数据:

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

最终录制的代码如下,注意该方法需要在子线程中运行:

private File mAudioFile;
private FileOutputStream mAudioFileOutput;
private boolean isRecording = false;
private int sampleRate = 44100;//所有android系统都支持  采样率
//单声道输入
private int channelConfig = AudioFormat.CHANNEL_IN_MONO;
//PCM_16是所有android系统都支持的  16位的声音就是人类能听到的极限了,再高就听不见了 位数越高声音越清晰
private int autioFormat = AudioFormat.ENCODING_PCM_16BIT;
private int recordBufSize = 0; // 声明recoordBufffer的大小字段
private AudioRecord audioRecord = null; 
private boolean startAudioRecord(String fileName) {
        isRecording = true;
        mAudioFile = new File(mPath+fileName+System.currentTimeMillis()+".pcm");
        if (!mAudioFile.getParentFile().exists()){
            mAudioFile.getParentFile().mkdirs();
        }
        try {
            mAudioFile.createNewFile();
            //创建文件输出流
            mAudioFileOutput = new FileOutputStream(mAudioFile);
            //计算audioRecord能接受的最小的buffer大小
            recordBufSize = AudioRecord.getMinBufferSize(sampleRate,
                    channelConfig,
                    autioFormat);
            Log.e(TAG, "最小的buffer大小: " + recordBufSize);
            audioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC,
                    sampleRate,
                    channelConfig,
                    autioFormat, recordBufSize);
            //初始化一个buffer 用于从AudioRecord中读取声音数据到文件流
            byte data[] = new byte[recordBufSize];
            //开始录音
            audioRecord.startRecording();

            while (isRecording){
                //只要还在录音就一直读取
                int read = audioRecord.read(data,0,recordBufSize);
                if (read > 0){
                    mAudioFileOutput.write(data,0,read);
                }
            }
            //stopRecorder();
        } catch (IOException e) {
            e.printStackTrace();
            stopAudioRecord();
            return false;
        }
        return true;
    }

public boolean stopAudioRecord(){
        isRecording = false;
        if (audioRecord != null){
            audioRecord.stop();
            audioRecord.release();
            audioRecord = null;
        }
        try {
            mAudioFileOutput.flush();
            mAudioFileOutput.close();
        } catch (IOException e) {
            e.printStackTrace();
            return false;
        }
        return true;
    }

3. 音频播放

播放pcm格式的音频和录制不同的地方有三个:

一个是创建AudioTrack对象,和AudioRecord的构造方法不同,第一个参数是音乐类型,AudioManager.STREAM_MUSIC表示用扬声器播放,最后多出的一个参数是播放模式,一般使用AudioTrack.MODE_STREAM,适用于大多数的场景,将audio buffers从java层传递到native层即返回。 如果audio buffers占用内存多,应该使用MODE_STREAM。 比如播放时间很长的声音文件, 比如音频文件使用高采样率等:

public AudioTrack(int streamType, int sampleRateInHz, int channelConfig, int audioFormat, int bufferSizeInBytes, int mode)

另外两个是把录制的startRecording()和read()方法换成下面两个:

public void play()
public int write(byte[] audioData, int offsetInBytes, int sizeInBytes)

最终播放pcm的代码如下:

private FileInputStream mAudioPlayInputStream;
public void doPlay(File audioFile) {
    if (audioFile.isDirectory() || !audioFile.exists()) return;
            mIsPlaying = true;
            //配置播放器
            //音乐类型,扬声器播放
            int streamType= AudioManager.STREAM_MUSIC;
            //录音时采用的采样频率,所以播放时同样的采样频率
            int sampleRate=44100;
            //单声道,和录音时设置的一样
            int channelConfig=AudioFormat.CHANNEL_OUT_MONO;
            //录音时使用16bit,所以播放时同样采用该方式
            int audioFormat=AudioFormat.ENCODING_PCM_16BIT;
            //流模式
            int mode= AudioTrack.MODE_STREAM;

            //计算最小buffer大小
            int minBufferSize = AudioTrack.getMinBufferSize(sampleRate,channelConfig,audioFormat);

            byte data[] = new byte[minBufferSize];
            //构造AudioTrack  不能小于AudioTrack的最低要求,也不能小于我们每次读的大小
            mAudioTrack = new AudioTrack(streamType,sampleRate,channelConfig,audioFormat,
                    Math.max(minBufferSize,data.length),mode);

            //从文件流读数据
            try{
                //循环读数据,写到播放器去播放
                mAudioPlayInputStream = new FileInputStream(audioFile);

                //循环读数据,写到播放器去播放
                int read;
                //只要没读完,循环播放
                mAudioTrack.play();
                while (mIsPlaying){
                    int ret = 0;
                    read = mAudioPlayInputStream.read(data);
                    if (read > 0){
                        ret = mAudioTrack.write(data,0,read);
                    }
                    //mAudioFileOutput.write(data,0,read);
                    //检查write的返回值,处理错误
                    switch (ret){
                        case AudioTrack.ERROR_INVALID_OPERATION:
                        case AudioTrack.ERROR_BAD_VALUE:
                        case AudioManager.ERROR_DEAD_OBJECT:
                            Log.d(TAG, "doPlay: 失败,错误码:"+ret);
                            return;
                        default:
                            break;
                    }
                }
            }catch (Exception e){
                e.printStackTrace();
                //读取失败
                Log.d(TAG, "doPlay: 失败");
            }finally {
                stopPlay();
                Log.d(TAG, "结束播放");
            }
    }
public void stopPlay() {
        mIsPlaying = false;
        //播放器释放
        if (mAudioTrack != null) {
            mAudioTrack.stop();
            mAudioTrack.release();
            mAudioTrack = null;
        }
        //关闭文件输入流
        if (mAudioPlayInputStream != null) {
            try {
                mAudioPlayInputStream.close();
            } catch (IOException ex) {
                ex.printStackTrace();
            }
        }
    }

最后贴上所有的代码:

public class AudioUtil {
    private static final String TAG = "AudioUtil";
    private AudioRecord audioRecord = null;  // 声明 AudioRecord 对象
    private int recordBufSize = 0; // 声明recoordBufffer的大小字段

    //所有android系统都支持  采样率:采样率越高,听到的声音和看到的图像就越连贯
    // 基本上高于44.1kHZ采样的声音,绝大部分人已经觉察不到其中的分别了
    private int sampleRate = 44100;
    //单声道输入
    private int channelConfig = AudioFormat.CHANNEL_IN_MONO;
    //PCM_16是所有android系统都支持的  16位的声音就是人类能听到的极限了,再高就听不见了 位数越高声音越清晰
    private int autioFormat = AudioFormat.ENCODING_PCM_16BIT;
    private long mStartTimeStamp;
    private File mAudioFile;
    private String mPath = ContentValue.MAIN_PATH + "/AudioSimple/";
    private FileOutputStream mAudioFileOutput; //存储录音文件
    private FileInputStream mAudioPlayInputStream; //播放录音文件
    private boolean isRecording = false;

    private static AudioUtil audioUtil;
    private boolean mIsPlaying;
    private AudioTrack mAudioTrack;
    private String mRecordFileName; //录音保存的文件路径
    private String mPlayFileName; //播放录音的文件路径

    private Runnable mAudioRunnableTask = new Runnable() {
        @Override
        public void run() {
            boolean result = startAudioRecord(mRecordFileName);
            if (result){
                Log.e(TAG, "录音结束");
            }else {
                Log.e(TAG, "录音失败");
            }
        }
    };

    private Runnable mAudioPlayRunnableTask = new Runnable() {
        @Override
        public void run() {
            File file = new File(mPlayFileName);
            doPlay(file);
        }
    };

    private AudioUtil(){}
    public static AudioUtil getInstance(){
        if (audioUtil == null){
            synchronized (AudioUtil.class){
                if (audioUtil == null){
                    audioUtil = new AudioUtil();
                }
            }
        }
        return audioUtil;
    }

    private AudioEncoder mAudioEncoder;
    private boolean startAudioRecord(String fileName) {
        isRecording = true;
        mStartTimeStamp = System.currentTimeMillis();
        mAudioFile = new File(mPath+fileName+mStartTimeStamp+".pcm");
        if (!mAudioFile.getParentFile().exists()){
            mAudioFile.getParentFile().mkdirs();
        }
        try {
            mAudioFile.createNewFile();
            //创建文件输出流
            mAudioFileOutput = new FileOutputStream(mAudioFile);

            //计算audioRecord能接受的最小的buffer大小
            recordBufSize = AudioRecord.getMinBufferSize(sampleRate,
                    channelConfig,
                    autioFormat);
            Log.e(TAG, "最小的buffer大小: " + recordBufSize);
            audioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC,
                    sampleRate,
                    channelConfig,
                    autioFormat, recordBufSize);
            //初始化一个buffer 用于从AudioRecord中读取声音数据到文件流
            byte data[] = new byte[recordBufSize];
            //开始录音
            audioRecord.startRecording();

            while (isRecording){
                //只要还在录音就一直读取
                int read = audioRecord.read(data,0,recordBufSize);
                if (read > 0){
                    mAudioFileOutput.write(data,0,read);

                }
            }
            //stopRecorder();
        } catch (IOException e) {
            e.printStackTrace();
            stopAudioRecord();
            return false;
        }
        return true;
    }

    public void doPlay(File audioFile) {
        if(audioFile !=null){
            mIsPlaying = true;
            //配置播放器
            //音乐类型,扬声器播放
            int streamType= AudioManager.STREAM_MUSIC;
            //录音时采用的采样频率,所以播放时同样的采样频率
            int sampleRate=44100;
            //单声道,和录音时设置的一样
            int channelConfig=AudioFormat.CHANNEL_OUT_MONO;
            //录音时使用16bit,所以播放时同样采用该方式
            int audioFormat=AudioFormat.ENCODING_PCM_16BIT;
            //流模式
            int mode= AudioTrack.MODE_STREAM;

            //计算最小buffer大小
            int minBufferSize = AudioTrack.getMinBufferSize(sampleRate,channelConfig,audioFormat);

            byte data[] = new byte[minBufferSize];
            //构造AudioTrack  不能小于AudioTrack的最低要求,也不能小于我们每次读的大小
            mAudioTrack = new AudioTrack(streamType,sampleRate,channelConfig,audioFormat,
                    Math.max(minBufferSize,data.length),mode);

            //从文件流读数据
            try{
                //循环读数据,写到播放器去播放
                mAudioPlayInputStream = new FileInputStream(audioFile);

                //循环读数据,写到播放器去播放
                int read;
                //只要没读完,循环播放
                mAudioTrack.play();
                while (mIsPlaying){
                    int ret = 0;
                    read = mAudioPlayInputStream.read(data);
                    if (read > 0){
                        ret = mAudioTrack.write(data,0,read);
                    }
                    //mAudioFileOutput.write(data,0,read);
                    //检查write的返回值,处理错误
                    switch (ret){
                        case AudioTrack.ERROR_INVALID_OPERATION:
                        case AudioTrack.ERROR_BAD_VALUE:
                        case AudioManager.ERROR_DEAD_OBJECT:
                            Log.d(TAG, "doPlay: 失败,错误码:"+ret);
                            return;
                        default:
                            break;
                    }
                }
            }catch (Exception e){
                e.printStackTrace();
                //读取失败
                Log.d(TAG, "doPlay: 失败");
            }finally {
                stopPlay();
                Log.d(TAG, "结束播放");
            }
        }
    }

    public void startRecord(String fileName){
        this.mRecordFileName = fileName;
        new Thread(mAudioRunnableTask).start();
    }

    public void startPlay(String fileName){
        this.mPlayFileName = fileName;
        new Thread(mAudioPlayRunnableTask).start();
    }
    public boolean stopAudioRecord(){
        isRecording = false;
        if (audioRecord != null){
            audioRecord.stop();
            audioRecord.release();
            audioRecord = null;
        }
        if (mAudioEncoder != null){
            mAudioEncoder.stopEncodeAac();
        }
        try {
            mAudioFileOutput.flush();
            mAudioFileOutput.close();
        } catch (IOException e) {
            e.printStackTrace();
            return false;
        }
        return true;
    }
    public boolean isRecording(){
        return isRecording;
    }
    public boolean isPlaying(){
        return mIsPlaying;
    }
    public void stopPlay(){
        mIsPlaying = false;
        //播放器释放
        if(mAudioTrack != null){
            mAudioTrack.stop();
            mAudioTrack.release();
            mAudioTrack = null;
        }
        //关闭文件输入流
        if(mAudioPlayInputStream !=null){
            try {
                mAudioPlayInputStream.close();
            } catch (IOException ex) {
                ex.printStackTrace();
            }
        }
    }

    private AudioListener mAudioListener;
    public void setAudioListener(AudioListener listener){
        this.mAudioListener = listener;
    }
    public interface AudioListener{
        void onRecordFinish();
        void onPlayFinish();
    }
}

发表评论

textsms
account_circle
email

音频开发之录制pcm格式文件
前几篇的文章都是camera下采集视频数据进行显示,保存下来的文件也是h264格式的,并没有包含音频数据,所以多多少少有点单调的感觉。没有声音的视频是没有灵魂的,所以最近了解了一下音频…
扫描二维码继续阅读
2019-02-23