Android音频开发之音频采集(AudioRecord)

版权声明:本文为卫伟学习总结文章,转载请注明出处!
在Android系统中,一般使用AudioRecord或者MediaRecord来采集音频。
AudioRecord:是一个比较偏底层的API,它可以获取到一帧帧PCM数据,之后可以对这些数据进行处理。
MediaRecorder:是基于AudioRecorder的API(最终还是会创建AudioRecord用来与AudioFlinger进行交互),它可以直接将采集到的音频数据转化为执行的编码格式,并保存。
直播技术采用的就是AudioRecorder采集音频数据。
一、基本API
获取最小的缓冲区大小,用于AudioRecord采集到的音频数据。

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

audioRecord构造方法: 根据具体的参数配置,请求硬件资源创建一个可以用于采集音频的AudioRecord对象。
audioSource:音频采集的来源
audioSampleRate:音频采样率
channelConfig: 声道
audioFormat: 音频采样精度,指定采样的数据的格式和每次采样的大小
bufferSizeBytes:AudioRecord采集的音频数据所存放的缓冲区大小。

public Params initAudioDevice() {
    int[] sampleRates = {48000, 44100, 22050, 16000, 11025,8000,4000};
    for (int sampleRate : sampleRates) {
        //编码制式
        int audioFormat = mConfig.audioFormat;
        // stereo 立体声,
        int channelConfig = mConfig.channelConfig;
        int buffsize = 2 * AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioFormat);
        mAudioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC, sampleRate, channelConfig,
                audioFormat, buffsize);
        if (mAudioRecord.getState() != AudioRecord.STATE_INITIALIZED) {
            continue;
        }
        this.buffer = new byte[Math.min(4096, buffsize)];

        return new Params(sampleRate,
                channelConfig == AudioFormat.CHANNEL_CONFIGURATION_STEREO ? 2 : 1);
    }

    return null;
}

开始采集
开始采集之后,状态为RECORDSTATE_RECORDING 。

public void startRecording ()

读取录制内存,将采集到的数据读取到缓冲区
方法调用的返回值的状态码:
情况异常:

  • 1.ERROR_INVALID_OPERATION if the object wasn't properly initialized
  • 2.ERROR_BAD_VALUE if the parameters don't resolve to valid data and indexes.

情况正常:the number of bytes that were read

public int read (ByteBuffer audioBuffer, int sizeInBytes)
public int read (byte[] audioData, int offsetInBytes, int sizeInBytes)
public int read (short[] audioData, int offsetInShorts, int sizeInShorts)

获取AudioRecord的状态
用于检测AudioRecord是否确保了获取适当的硬件资源。在AudioRecord对象实例化之后调用。
STATE_INITIALIZED 初始完毕
STATE_UNINITIALIZED 未初始化

public int getState ()

返回当前AudioRecord的采集状态
public static final int RECORDSTATE_STOPPED = 1; 停止状态
调用void stop()之后的状态
public static final int RECORDSTATE_RECORDING = 3;正在采集
调用startRecording()之后的状态

public int getRecordingState() 

二、AudioRecord采集音频的基本流程

  • 权限

    <uses-permission android:name="android.permission.RECORD_AUDIO" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    
  • 构造一个AudioRecord对象。

  • 开始采集

  • 读取采集的数据

  • 停止采集

三、Android音频开发之音频采集
构造一个AudioRecord对象

AudioRecord audioRecord = new AudioRecord(audioResource, audioSampleRate, channelConfig, audioFormat, bufferSizeInBytes);

获取bufferSizeLnBytes值
bufferSizeLnBytes是AudioRecord采集的音频数据所存放的缓冲区大小。
注意:这个大小不能随便设置,AudioRecord提供对应的API来获取这个值。

this.bufferSizeInBytes = AudioRecord.getMinBufferSize(audioSampleRate, channelConfig, audioFormat);

通过bufferSizeInBytes返回就可以知道传入给AudioRecord.getMinBufferSize的参数是否支持当前的硬件设备。

if (AudioRecord.ERROR_BAD_VALUE == bufferSizeInBytes || AudioRecord.ERROR == bufferSizeInBytes) {
throw new RuntimeException("Unable to getMinBufferSize");
}

//bufferSizeInBytes is available...

开始采集

  • 在开始录音之前,首先要判断一下AudioRecord的状态是否已经初始化完毕了。

    //判断AudioRecord的状态是否初始化完毕
    //在AudioRecord对象构造完毕之后,就处于AudioRecord.STATE_INITIALIZED状态了。
    int state = audioRecord.getState();
    if (state == AudioRecord.STATE_UNINITIALIZED) {
      throw new RuntimeException("AudioRecord STATE_UNINITIALIZED");
    }
    
  • 开始采集

    audioRecord.startRecording();
    //开启线程读取数据
    new Thread(recordTask).start();
    
  • 读取采集的数据
    上面提到,AudioRecord在采集数据时会将数据存放到缓冲区中,因此我们只需要创建一个数据流去从缓冲区中将采集的数据读取出来即可。
    创建一个数据流,一边从AudioRecord中读取音频数据到缓冲区,一边将缓冲区中数据写入到数据流。

因为需要使用IO操作,因此读取数据的过程应该在子线程中执行

//创建一个流,存放从AudioRecord读取的数据
File saveFile = new File(Environment.getExternalStorageDirectory(), "audio-record.pcm");
DataOutputStream dataOutputStream = new DataOutputStream(
            new BufferedOutputStream(new FileOutputStream(saveFile)));

private Runnable recordTask = new Runnable() {
@Override
public void run() {
    //设置线程的优先级
    android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIOR
    Log.i(TAG, "设置采集音频线程优先级");
    final byte[] data = new byte[bufferSizeInBytes];
    //标记为开始采集状态
    isRecording = true;
    Log.i(TAG, "设置当前当前状态为采集状态");
    //getRecordingState获取当前AudioReroding是否正在采集数据的状态
    while (isRecording && audioRecord.getRecordingState() == AudioRecord
        //读取采集数据到缓冲区中,read就是读取到的数据量
        final int read = audioRecord.read(data, 0, bufferSizeInBytes);
        if (AudioRecord.ERROR_INVALID_OPERATION != read && AudioRecord.E
            //将数据写入到文件中
            dataOutputStream.write(buffer,0,read);
        }
    }
}
};
  • 停止采集

    /**
     * 停止录音
     */
    public void stopRecord() throws IOException {
       Log.i(TAG, "停止录音,回收AudioRecord对象,释放内存");
       isRecording = false;
       if (audioRecord != null) {
          if (audioRecord.getRecordingState() == AudioRecord.RECORDSTATE_RECORDING) {
             audioRecord.stop();
             Log.i(TAG, "audioRecord.stop()");
            }
      if (audioRecord.getState() == AudioRecord.STATE_INITIALIZED) {
          audioRecord.release();
          Log.i(TAG, "audioRecord.release()");
      }
    }
    

AudioRecord采集PCM音频原始数据完整功能实现代码:

package com.weiwei.mediarecordertest;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import android.app.Activity;
import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioRecord;
import android.media.AudioTrack;
import android.media.MediaRecorder;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;

 /**
  * AudioRecord 录音采集
  * AudioTrack  实现录音播放 
 * @author 60116
 *
 */
public class StramActivity extends Activity{
 private Button bt_stream_recorder;
 private TextView tv_stream_msg;
 private ExecutorService mExecutorService;
 private long startRecorderTime, stopRecorderTime;
 private volatile boolean mIsRecording = false;
 private AudioRecord mAudioRecord;
 private FileOutputStream mFileOutputStream;
 private File mAudioRecordFile;
 private byte[] mBuffer;
 //buffer 值不能太大 避免OOM
 private static int BUFFER_SIZE = 2048;
 private boolean mIsPlaying = false;
 private Handler mHandler = new Handler(Looper.getMainLooper());
 
 @Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    setTitle("字节流录音");
    initView();
    mExecutorService = Executors.newSingleThreadExecutor();
    mBuffer = new byte[BUFFER_SIZE];
}
 
private void initView() {
    bt_stream_recorder = (Button) findViewById(R.id.bt_stream_recorder);
    tv_stream_msg = (TextView) findViewById(R.id.tv_stream_msg);

}

public void recorderaudio(View view) {
    if(mIsRecording) {
         bt_stream_recorder.setText("开始录音");
         //在开始录音中如果这个值没有变false,则一直进行,当再次点击变false时,录音才停止
         mIsRecording = false;
    } else {
        bt_stream_recorder.setText("停止录音");
        //提交后台任务 执行录音逻辑
        mIsRecording = true;
        //提交后台任务,执行录音逻辑
        mExecutorService.submit(new Runnable() {

            @Override
            public void run() {
                startRecorder();
            }
        });
    }
}

/**
 * 开始录音
 */
private void startRecorder() {
    //realeseRecorder();
    if(!dostart()) recorderFail();
}

 /**
 * 停止录音
 */
private void stopRecorder() {
    mIsRecording=false;
    if (!doStop()) recorderFail();

}

private boolean dostart() {
    try {
        // 记录开始录音时间
        startRecorderTime = System.currentTimeMillis();
        //创建录音文件
        mAudioRecordFile = new File(Environment.getExternalStorageDirectory().getAbsolutePath() +
                 "/recorderdemo/" + System.currentTimeMillis() + ".pcm");
        if (!mAudioRecordFile.getParentFile().exists())
            mAudioRecordFile.getParentFile().mkdirs();
        // 创建文件输出流
        mFileOutputStream = new FileOutputStream(mAudioRecordFile);
        // 配置AudioRecord
        int audioSource = MediaRecorder.AudioSource.MIC;
        //所有支持android系统都支持
        int sampleRate = 44100;
        // 单声道输入
        int channelConfig = AudioFormat.CHANNEL_IN_MONO;
        // pcm_16时所有android系统都支持的
        int autioFormat = AudioFormat.ENCODING_PCM_16BIT;
        //计算AudioRecord内部buffer最小
        int minBufferSize = AudioRecord.getMinBufferSize(sampleRate, channelConfig, autioFormat);
        //buffer不能小于最低要求,也不能小于我们每次我们读取的大小。
        mAudioRecord = new AudioRecord(audioSource, sampleRate, channelConfig, autioFormat, Math.max(minBufferSize, BUFFER_SIZE));

        //开始录音
        mAudioRecord.startRecording();
        
        //循环读取数据,写入输出流中
        while (mIsRecording) {
             //只要还在录音就一直读取
            int read = mAudioRecord.read(mBuffer, 0, BUFFER_SIZE);
            if(read <= 0) {
                return false;
            } else {
                Log.i("Tag8","mBuffer =" +mBuffer.toString());
                mFileOutputStream.write(mBuffer, 0, read);
            }
        }
        
        //退出循环,停止录音,释放资源
        stopRecorder();
    } catch (Exception e) {
        e.printStackTrace();
        return false;
    } finally {
        if (mAudioRecord != null) {
            mAudioRecord.release();
        }
    }
    return true;
}

private boolean recorderFail() {
    mHandler.post(new Runnable() {

        @Override
        public void run() {
            bt_stream_recorder.setText("开始录音");
            tv_stream_msg.setText("录取失败,请重新录入");

            mIsRecording = false;
            Log.i("Tag8", "go here111111111");
        }
    });
    return false;
}

private void realeseRecorder() {
    mAudioRecord.release();
}

private boolean doStop() {
    //停止录音,关闭文件输出流
    mAudioRecord.stop();
    mAudioRecord.release();
    mAudioRecord = null;
    Log.i("Tag8", "go here");
    //记录结束时间,统计录音时长
    stopRecorderTime = System.currentTimeMillis();
    //大于3秒算成功,在主线程更新UI
    final int send = (int) (stopRecorderTime - startRecorderTime) / 1000;
    if (send > 3) {
        mHandler.post(new Runnable() {
            @Override
            public void run() {
                tv_stream_msg.setText("录音成功:" + send + "秒");
                bt_stream_recorder.setText("开始录音");
                Log.i("Tag8", "go there");
            }
        });
    } else {
        recorderFail();
        return false;
    }
    return true;
}

/**
 * 播放声音
 * @param view
 */
public void player(View view) {
    mExecutorService.submit(new Runnable() {

        @Override
        public void run() {
            if (!mIsPlaying) {
                Log.i("Tag8", "go here");
                mIsPlaying = true;
                Log.i("Tag8","mAudioRecordFile =" +mAudioRecordFile.toString());
                doPlay(mAudioRecordFile);
            }
        }
    });
}

private void doPlay(File audioFile) {
    if(audioFile != null) {
        Log.i("Tag8","go there");
        //配置播放器
        //音乐类型,扬声器播放
        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);
        
        //构造AudioTrack  不能小于AudioTrack的最低要求,也不能小于我们每次读的大小
        AudioTrack audioTrack=new AudioTrack(streamType,sampleRate,channelConfig,audioFormat,
                Math.max(minBufferSize,BUFFER_SIZE),mode);
        audioTrack.setVolume(16f);
        //从文件流读数据
        FileInputStream inputStream = null;
        
        try {
            //循环读数据,写到播放器去播放
            inputStream = new FileInputStream(audioFile);
            
            //循环读数据,写到播放器去播放
            int read;
            //只要没读完,循环播放
            while((read=inputStream.read(mBuffer)) > 0) {
                Log.i("Tag8","read:"+read);
                int ret = audioTrack.write(mBuffer, 0, read);
                Log.i("Tag8","ret ==="+ret);
                //检查write的返回值,处理错误
                switch(ret) {
                case AudioTrack.ERROR_INVALID_OPERATION:
                case AudioTrack.ERROR_BAD_VALUE:
                case AudioManager.ERROR_DEAD_OBJECT:
                    playFail();
                    return;
                default:
                    break;
                }
            }
            audioTrack.play();
            Log.i("Tag8","播放成功。。。。");
        } catch (Exception e) {
            e.printStackTrace();
            Log.i("Tag8","e =" +e.toString());
            //读取失败
            playFail();
        } finally {
            mIsPlaying = false;
            Log.i("Tag8","播放完毕");
             //关闭文件输入流
            if(inputStream !=null){
                closeStream(inputStream);
            }
            //播放器释放
            resetQuietly(audioTrack);
        }
    }
}

private void closeStream(FileInputStream inputStream) {
    try {
        inputStream.close();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

private void resetQuietly(AudioTrack audioTrack) {
    try {
        audioTrack.stop();
        audioTrack.release();
    } catch (Exception e) {
        e.printStackTrace();
    }
}

/**
 * 播放失败
 */
private void playFail() {
    mHandler.post(new Runnable() {
        @Override
        public void run() {
            tv_stream_msg.setText("播放失败");
        }
    });
}

@Override
protected void onDestroy() {
    super.onDestroy();
    if (mExecutorService != null) {
        mExecutorService.shutdownNow();
    }
    if (mAudioRecord != null) {
        mAudioRecord.stop();
        mAudioRecord.release();
        mAudioRecord = null;
    }
}

获取到的PCM格式的音频文件

用Audacity播放器播放PCM原始数据也能听见声音。
注意:

  • 采集数据之后,保存的文件为audio-record.pcm,这个文件并不能使用普通的播放器播放。它是一个原始的文件,没有任何播放格式,因此就无法被播放器识别并播放。
  • 上面的问题可以有两种解决方法
  • 使用AudioTrack播放pcm格式的音频数据。
  • 将pcm数据转化为wav格式数据,这样就可以被播放器识别啦。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,793评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,567评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,342评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,825评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,814评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,680评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,033评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,687评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,175评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,668评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,775评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,419评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,020评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,206评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,092评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,510评论 2 343

推荐阅读更多精彩内容