音视频学习系列第(二)篇---音频采集和播放

音视频系列

音频采集AudioRecord

AudioRecord与MediaRecorder区别
前者采集的是原始的音频数据,后者会对音频数据进行编码压缩并存储成文件

AudioRecord的使用

1.AudioRecord参数配置

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

audioSource
音频采集的输入源,可选值在MediaRecorder.AudioSource中以常量值定义,如

public static final int MIC = 1;   //表示手机麦克风输入

sampleRateInHz
采样率,录音设备1S内对声音信号的采集次数,单位Hz,目前44100Hz是唯一可以保证兼容所有Android手机的采样率。
背景知识
Hz,物质在1S内周期性变化的次数
我们知道人耳能听到的声音频率范围在20Hz到20KHz之间,为了不失真,采样频率应该在40KHz以上

channelConfig
通道数的配置,可选值在AudioFormat中以常量值定义,常用的如下

public static final int CHANNEL_IN_LEFT = 0x4;
public static final int CHANNEL_IN_RIGHT = 0x8;
public static final int CHANNEL_IN_FRONT = 0x10;
//单通道
public static final int CHANNEL_IN_MONO = CHANNEL_IN_FRONT;   
//双通道
public static final int CHANNEL_IN_STEREO = (CHANNEL_IN_LEFT | CHANNEL_IN_RIGHT);

audioFormat
用来配置数据位宽,可选值在可选值在AudioFormat中以常量值定义,常用的如下

public static final int ENCODING_PCM_16BIT = 2;
public static final int ENCODING_PCM_8BIT = 3;

背景知识
PCM通过抽样、量化、编码三个步骤将连续变化的模拟信号转换为数字编码。

bufferSizeInBytes
配置的是AudioRecord内部音频缓冲区的大小,该值不能低于一帧音频帧的大小,一帧音频帧的大小计算如下
int size=采样率 * 采样时间 * 位宽 * 通道数
其中采样时间一般取2.5ms~120ms,具体取多少由厂商或者应用决定
每一帧采样的时间越短,产生的延时越小,但碎片化的数据也会越多
在Android开发中,应该使用AudioRecord类中的方法

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

来计算音频缓冲区的大小

2.音频采集方法

audioRecord.startRecording();   //开始录制
audioRecord.stop();    //停止录制
audioRecord.read(bytes,0,bytes.length);  //读取录音数据

3.示例代码

<uses-permission android:name="android.permission.RECORD_AUDIO"/>

并且该权限属于危险权限,需要动态获取权限

public class AudioCapture {
private static final String TAG = "AudioCapture";

private final int DEFAULT_SOURCE = MediaRecorder.AudioSource.MIC;  //麦克风
private final int DEFAULT_RATE = 44100;    //采样率
private final int DEFAULT_CHANNEL = AudioFormat.CHANNEL_IN_STEREO;   //双通道(左右声道)
private final int DEFAULT_FORMAT = AudioFormat.ENCODING_PCM_16BIT;   //数据位宽16位

private AudioRecord mAudioRecord;
private int mMinBufferSize;
private onAudioFrameCaptureListener mOnAudioFrameCaptureListener;

private boolean isRecording = false;

public void startRecord() {
    startRecord(DEFAULT_SOURCE, DEFAULT_RATE, DEFAULT_CHANNEL, DEFAULT_FORMAT);
}


public void startRecord(int audioSource, int sampleRateInHz, int channelConfig, int audioFormat) {

    mMinBufferSize = AudioRecord.getMinBufferSize(sampleRateInHz, channelConfig, audioFormat);
    if (mMinBufferSize == AudioRecord.ERROR_BAD_VALUE) {
        Log.d(TAG, "Invalid parameter");
        return;
    }

    mAudioRecord = new AudioRecord(audioSource, sampleRateInHz, channelConfig,
            audioFormat, mMinBufferSize);
    if (mAudioRecord.getState() == AudioRecord.STATE_UNINITIALIZED) {
        Log.d(TAG, "AudioRecord initialize fail");
        return;
    }

    mAudioRecord.startRecording();
    isRecording = true;
    CaptureThread t = new CaptureThread();
    t.start();
    Log.d(TAG, "AudioRecord Start");
}


public void stopRecord() {
    isRecording = false;
    if (mAudioRecord.getRecordingState() == AudioRecord.RECORDSTATE_RECORDING) {
        mAudioRecord.stop();
    }
    mAudioRecord.release();
    mOnAudioFrameCaptureListener = null;
    Log.d(TAG, "AudioRecord Stop");
}


private class CaptureThread extends Thread {

    @Override
    public void run() {
        while (isRecording) {
            byte[] buffer = new byte[mMinBufferSize];
            int result = mAudioRecord.read(buffer, 0, buffer.length);
            Log.d(TAG, "Captured  " + result + "  byte");
            if (mOnAudioFrameCaptureListener != null) {
                mOnAudioFrameCaptureListener.onAudioFrameCapture(buffer);
            }
        }
    }
}


public interface onAudioFrameCaptureListener {
    void onAudioFrameCapture(byte[] audioData);
}

public void setOnAudioFrameCaptureListener(onAudioFrameCaptureListener listener) {
    mOnAudioFrameCaptureListener = listener;
  }
}

调用方式

audioCapture=new AudioCapture();
audioCapture.startRecord();

音频播放AudioTrack

AudioTrack,MediaPlayer,SoundPool的区别

mediaplayer适合长时间播放音乐
soundpool适合短时间的音频片段,如游戏声音,按键声音
audiotrack更接近底层,更灵活,播放的是pcm音频数据

AudioTrack的使用

1.AudioTrack参数配置

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

streamType
音频管理策略,如我们在小米手机调节音量时,会出现3种声音调节的类型,音乐,铃声,闹钟
该参数的可选值在AudioManager类中,如:

STREAM_MUSCI:音乐声
STREAM_RING:铃声
STREAM_NOTIFICATION:通知声

sampleRateInHz
采样率,看源码知道,范围在4000~192000

public static final int SAMPLE_RATE_HZ_MIN = 4000;
public static final int SAMPLE_RATE_HZ_MAX = 192000;

channelConfig
通道数的配置,可选值在AudioFormat中以常量值定义,常用的如下

public static final int CHANNEL_IN_LEFT = 0x4;
public static final int CHANNEL_IN_RIGHT = 0x8;
public static final int CHANNEL_IN_FRONT = 0x10;
//单通道
public static final int CHANNEL_IN_MONO = CHANNEL_IN_FRONT;   
//双通道
public static final int CHANNEL_IN_STEREO = (CHANNEL_IN_LEFT | CHANNEL_IN_RIGHT);

audioFormat
用来配置数据位宽,可选值在可选值在AudioFormat中以常量值定义,常用的如下

public static final int ENCODING_PCM_16BIT = 2;
public static final int ENCODING_PCM_8BIT = 3;

bufferSizeInBytes
配置的是AudioTrack内部音频缓冲区的大小,同样AudioTrack提供了获取缓冲区大小的方法

AudioTrack.getMinBufferSize(sampleRateInHz, channelConfig, audioFormat);

mode
AudioTrack有两种播放方式 MODE_STATIC和MODE_STREAM
前者是一次性将所有数据写入播放缓冲区,然后播放
后者是一边写入一边播放

2.音频播放方法

mAudioTrack.play();  //开始播放
mAudioTrack.stop(); //停止播放
mAudioTrack.write(audioData,offsetInBytes,sizeInBytes);//将pcm数据写入缓冲区

3.示例代码

public class AudioPlayer {

private static final String TAG = "AudioPlayer";

private final int DEFAULT_STREAM_TYPE = AudioManager.STREAM_MUSIC;  //流音乐
private final int DEFAULT_RATE = 44100;    //采样率
private final int DEFAULT_CHANNEL = AudioFormat.CHANNEL_IN_STEREO;   //双通道(左右声道)
private final int DEFAULT_FORMAT = AudioFormat.ENCODING_PCM_16BIT;   //数据位宽16位
private static final int DEFAULT_PLAY_MODE = AudioTrack.MODE_STREAM;


private AudioTrack mAudioTrack;
private int mMinBufferSize;


private boolean isPlaying=false;



public void startPlay(){
    startPlay(DEFAULT_STREAM_TYPE,DEFAULT_RATE,DEFAULT_CHANNEL,DEFAULT_FORMAT);

}

public void startPlay(int streamType, int sampleRateInHz, int channelConfig, int audioFormat){
    if(isPlaying){
        Log.d(TAG,"AudioPlayer has played");
        return;
    }

    mMinBufferSize = AudioTrack.getMinBufferSize(sampleRateInHz, channelConfig, audioFormat);
    if (mMinBufferSize == AudioRecord.ERROR_BAD_VALUE) {
        Log.d(TAG, "Invalid parameter");
        return;
    }


    mAudioTrack=new AudioTrack(streamType,sampleRateInHz,channelConfig,audioFormat,
            mMinBufferSize,DEFAULT_PLAY_MODE);
    if(mAudioTrack.getState()==AudioTrack.STATE_UNINITIALIZED){
        Log.d(TAG, "AudioTrack initialize fail");
        return;
    }

    isPlaying=true;
}

public void stopPlay(){
    if(!isPlaying){
        Log.d(TAG, "AudioTrack is not playing");
        return;
    }

    if(mAudioTrack.getPlayState()==AudioTrack.PLAYSTATE_PLAYING){
        mAudioTrack.stop();
    }

    mAudioTrack.release();
    isPlaying=false;
}


private void play(byte[] audioData,int offsetInBytes, int sizeInBytes){
    if(!isPlaying){
        Log.d(TAG, "AudioTrack not start");
        return;
    }

    if(sizeInBytes<mMinBufferSize){
        Log.d(TAG, "audio data not enough");
        //return;
    }

    if(mAudioTrack.write(audioData,offsetInBytes,sizeInBytes)!=mMinBufferSize){
        Log.d(TAG, "AudioTrack can not write all the data");
    }

    mAudioTrack.play();
    Log.d(TAG, "played  "+sizeInBytes+"  bytes");
  }
}
测试
//原始音频的录入和播放
public class AudioPCMActivity extends DemoActivity {

private Button btn_audio_record;
private Button btn_audio_record_play;


private AudioCapture audioCapture;
private AudioPlayer audioPlayer;

private PcmFileWriter pcmFileWriter;
private PcmFileReader pcmFileReader;
private boolean isReading;

private String path="";


@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    setContentView(R.layout.activity_media_audio);
    super.onCreate(savedInstanceState);
}

@Override
public void initHead() {

}

@Override
public void initView() {
    btn_audio_record=findViewById(R.id.btn_audio_record);
    btn_audio_record_play=findViewById(R.id.btn_audio_record_play);
}

@Override
public void initData() {
    path=FileUtil.getAudioDir(this)+"/audioTest.pcm";
    audioCapture=new AudioCapture();
    audioPlayer=new AudioPlayer();
    pcmFileReader=new PcmFileReader();
    pcmFileWriter=new PcmFileWriter();

    String des = "录音权限被禁止,我们需要打开录音权限";
    String[] permissions = new String[]{Manifest.permission.RECORD_AUDIO};
    baseAt.requestPermissions(des, permissions, 100, new PermissionsResultListener() {
        @Override
        public void onPermissionGranted() {

        }
        @Override
        public void onPermissionDenied() {
            finish();

        }
    });

}

@Override
public void initEvent() {
    btn_audio_record.setOnTouchListener(new View.OnTouchListener() {
        @Override
        public boolean onTouch(View v, MotionEvent event) {
            if(event.getAction()==MotionEvent.ACTION_DOWN){
                Log.d("TAG","按住");
                start();
            }else if(event.getAction()==MotionEvent.ACTION_UP){
                Log.d("TAG","松开");
                stop();
            }
            return false;
        }
    });

    btn_audio_record_play.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            play();

        }
    });

}

//播放录音
private void play(){
    isReading=true;
    pcmFileReader.openFile(path);
    audioPlayer.startPlay();
    new AudioTrackThread().start();
}

private class AudioTrackThread extends Thread{
    @Override
    public void run() {
        byte[] buffer = new byte[1024];
        while (isReading && pcmFileReader.read(buffer,0,buffer.length)>0){
            audioPlayer.play(buffer,0,buffer.length);
        }
        audioPlayer.stopPlay();
        pcmFileReader.closeFile();
    }
}


//开始录音
private void start(){
    pcmFileWriter.openFile(path);
    btn_audio_record.setText("松开 结束");
    audioCapture.startRecord();
    audioCapture.setOnAudioFrameCaptureListener(new AudioCapture.onAudioFrameCaptureListener() {
        @Override
        public void onAudioFrameCapture(byte[] audioData) {
            pcmFileWriter.write(audioData,0,audioData.length);

        }
    });
}

//结束录音
private void stop(){
    btn_audio_record.setText("按住 录音");
    audioCapture.stopRecord();
    pcmFileWriter.closeFile();
  }
}

github
测试代码在com.sf.sofarmusic.demo.media下
其他代码在libplayer模块中

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

推荐阅读更多精彩内容