Android音视频之AudioRecord录音(一)
在音视频开发中,录音当然是必不可少的。首先我们要学会单独的录音功能,当然这里说的录音是指用audiorecord来录音,读取录音原始数据,读到的就是所谓的pcm数据。对于录音来说,最重要的几个参数要搞明白:
1、simplerate采样率,采样率就是采样频率,每秒钟记录多少个样本。
2、channelconfig通道配置,其实就是所谓的单通道,双通道之类的,audioformat.channel_in_mono单通道,audioformat.channel_in_stereo双通道,这里只列了这两种,还有其它的,可自行查阅。
3、audioformat音频格式,其实就是采样的精度,每个样本的位数,audioformat.encoding_pcm_8bit每个样本占8位,audioformat.encoding_pcm_16bit每个样本占16位,这里也只用了这两个,别的没研究。
在学习过程中会用到的一些参数,我这里封装了一个类,如下
public class audioparams { enum format { single_8_bit, double_8_bit, single_16_bit, double_16_bit } private format format; int simplerate; audioparams(int simplerate, format f) { this.simplerate = simplerate; this.format = f; } audioparams(int simplerate, int channelcount, int bits) { this.simplerate = simplerate; set(channelcount, bits); } int getbits() { return (format == format.single_8_bit || format == format.double_8_bit) ? 8 : 16; } int getencodingformat() { return (format == format.single_8_bit || format == format.double_8_bit) ? audioformat.encoding_pcm_8bit : audioformat.encoding_pcm_16bit; } int getchannelcount() {return (format == format.single_8_bit || format == format.single_16_bit) ? 1 : 2;} int getchannelconfig() { return (format == format.single_8_bit || format == format.single_16_bit) ? audioformat.channel_in_mono : audioformat.channel_in_stereo; } int getoutchannelconfig() { return (format == format.single_8_bit || format == format.single_16_bit) ? audioformat.channel_out_mono : audioformat.channel_out_stereo; } void set(int channelcount, int bits) { if ((channelcount != 1 && channelcount != 2) || (bits != 8 && bits != 16)) { throw new illegalargumentexception("不支持其它格式 channelcount=$channelcount bits=$bits"); } if (channelcount == 1) { if (bits == 8) { format = format.single_8_bit; } else { format = format.single_16_bit; } } else { if (bits == 8) { format = format.double_8_bit; } else { format = format.double_16_bit; } } } }
这里固定使用了单通道8位,双通道8位,单通道16位,双通道16位,所以用了枚举来限制。
为了方便把录音数据拿出来显示、存储,这里写了一个回调方法如下
public interface recordcallback { /** * 数据回调 * * @param bytes 数据 * @param len 数据有效长度,-1时表示数据结束 */ void onrecord(byte[] bytes, int len); }
有了这些参数,现在就可以录音了,先看一下样例
public void startrecord(audioparams params, recordcallback callback) { int simplerate = params.simplerate; int channelconfig = params.getchannelconfig(); int audioformat = params.getencodingformat(); // 根据audiorecord提供的api拿到最小缓存大小 int buffersize = audiorecord.getminbuffersize(simplerate, channelconfig, audioformat); //创建record对象 record = new audiorecord(mediarecorder.audiosource.mic, simplerate, channelconfig, audioformat, buffersize); recordthread = new thread(() -> { byte[] buffer = new byte[buffersize]; record.startrecording(); recording = true; while (recording) { int read = record.read(buffer, 0, buffersize); // 将数据回调到外部 if (read > 0 && callback != null) { callback.onrecord(buffer, read); } } if (callback != null) { // len 为-1时表示结束 callback.onrecord(buffer, -1); recording = false; } //释放资源 release(); }); recordthread.start(); }
这个方法就是简单的采集音频数据,这个数据就是最原始的pcm数据。
拿到pcm数据以后,如果直接保存到文件是无法直接播放的,因为这只是一堆数据,没有任何格式说明,如果想让普通播放器可以播放,需要在文件中加入文件头,来告诉播放器这个数据的格式,这里是直接保存成wav格式的数据。下面就是加入wav格式文件头的方法
private static byte[] getwavefileheader(int totaldatalen, int samplerate, int channelcount, int bits) { byte[] header = new byte[44]; // riff/wave header header[0] = 'r'; header[1] = 'i'; header[2] = 'f'; header[3] = 'f'; int filelength = totaldatalen + 36; header[4] = (byte) (filelength & 0xff); header[5] = (byte) (filelength >> 8 & 0xff); header[6] = (byte) (filelength >> 16 & 0xff); header[7] = (byte) (filelength >> 24 & 0xff); //wave header[8] = 'w'; header[9] = 'a'; header[10] = 'v'; header[11] = 'e'; // 'fmt ' chunk header[12] = 'f'; header[13] = 'm'; header[14] = 't'; header[15] = ' '; // 4 bytes: size of 'fmt ' chunk header[16] = 16; header[17] = 0; header[18] = 0; header[19] = 0; // pcm format = 1 header[20] = 1; header[21] = 0; header[22] = (byte) channelcount; header[23] = 0; header[24] = (byte) (samplerate & 0xff); header[25] = (byte) (samplerate >> 8 & 0xff); header[26] = (byte) (samplerate >> 16 & 0xff); header[27] = (byte) (samplerate >> 24 & 0xff); int byterate = samplerate * bits * channelcount / 8; header[28] = (byte) (byterate & 0xff); header[29] = (byte) (byterate >> 8 & 0xff); header[30] = (byte) (byterate >> 16 & 0xff); header[31] = (byte) (byterate >> 24 & 0xff); // block align header[32] = (byte) (channelcount * bits / 8); header[33] = 0; // bits per sample header[34] = (byte) bits; header[35] = 0; //data header[36] = 'd'; header[37] = 'a'; header[38] = 't'; header[39] = 'a'; header[40] = (byte) (totaldatalen & 0xff); header[41] = (byte) (totaldatalen >> 8 & 0xff); header[42] = (byte) (totaldatalen >> 16 & 0xff); header[43] = (byte) (totaldatalen >> 24 & 0xff); return header; }
根据几个参数设置一下文件头,然后直接写入录音采集到的pcm数据,就可被正常播放了。wav文件头格式定义,可查看或自行百度。
如果想要通过audiorecord录音直接保存到文件,可参考下面方法
public void startrecord(string filepath, audioparams params, recordcallback callback) { int channelcount = params.getchannelcount(); int bits = params.getbits(); final boolean storefile = filepath != null && !filepath.isempty(); startrecord(params, (bytes, len) -> { if (storefile) { if (file == null) { file f = new file(filepath); if (f.exists()) { f.delete(); } try { file = new randomaccessfile(f, "rw"); file.write(getwavefileheader(0, params.simplerate, channelcount, bits)); } catch (ioexception e) { e.printstacktrace(); } } if (len > 0) { try { file.write(bytes, 0, len); } catch (ioexception e) { e.printstacktrace(); } } else { try { // 因为在前面已经写入头信息,所以这里要减去头信息才是数据的长度 int length = (int) file.length() - 44; file.seek(0); file.write(getwavefileheader(length, params.simplerate, channelcount, bits)); file.close(); } catch (ioexception e) { e.printstacktrace(); } } } if (callback != null) { callback.onrecord(bytes, len); } }); }
先通过randomaccessfile创建文件,先写入文件头,由于暂时我们不知道会录多长,有多少pcm数据,长度先用0表示,等录音结束后,通过seek(int)方法重新写入文件头信息,也可以先把pcm数据保存到临时文件,然后再写入到一个新的文件中,这里就不举例说明了。
最后放入完整类的代码
package cn.sskbskdrin.record.audio; import android.media.audiorecord; import android.media.mediarecorder; import java.io.file; import java.io.ioexception; import java.io.randomaccessfile; /** * @author sskbskdrin * @date 2019/april/3 */ public class audiorecordmanager { private audioparams default_format = new audioparams(8000, 1, 16); private audiorecord record; private thread recordthread; private boolean recording = false; private randomaccessfile file; public void startrecord(string filepath, recordcallback callback) { startrecord(filepath, default_format, callback); } public void startrecord(string filepath, audioparams params, recordcallback callback) { int channelcount = params.getchannelcount(); int bits = params.getbits(); final boolean storefile = filepath != null && !filepath.isempty(); startrecord(params, (bytes, len) -> { if (storefile) { if (file == null) { file f = new file(filepath); if (f.exists()) { f.delete(); } try { file = new randomaccessfile(f, "rw"); file.write(getwavefileheader(0, params.simplerate, channelcount, bits)); } catch (ioexception e) { e.printstacktrace(); } } if (len > 0) { try { file.write(bytes, 0, len); } catch (ioexception e) { e.printstacktrace(); } } else { try { // 因为在前面已经写入头信息,所以这里要减去头信息才是数据的长度 int length = (int) file.length() - 44; file.seek(0); file.write(getwavefileheader(length, params.simplerate, channelcount, bits)); file.close(); } catch (ioexception e) { e.printstacktrace(); } } } if (callback != null) { callback.onrecord(bytes, len); } }); } public void startrecord(audioparams params, recordcallback callback) { int simplerate = params.simplerate; int channelconfig = params.getchannelconfig(); int audioformat = params.getencodingformat(); // 根据audiorecord提供的api拿到最小缓存大小 int buffersize = audiorecord.getminbuffersize(simplerate, channelconfig, audioformat); //创建record对象 record = new audiorecord(mediarecorder.audiosource.mic, simplerate, channelconfig, audioformat, buffersize); recordthread = new thread(() -> { byte[] buffer = new byte[buffersize]; record.startrecording(); recording = true; while (recording) { int read = record.read(buffer, 0, buffersize); // 将数据回调到外部 if (read > 0 && callback != null) { callback.onrecord(buffer, read); } } if (callback != null) { // len 为-1时表示结束 callback.onrecord(buffer, -1); recording = false; } //释放资源 release(); }); recordthread.start(); } public void stop() { recording = false; } public void release() { recording = false; if (record != null) { record.stop(); record.release(); } record = null; file = null; recordthread = null; } private static byte[] getwavefileheader(int totaldatalen, int samplerate, int channelcount, int bits) { byte[] header = new byte[44]; // riff/wave header header[0] = 'r'; header[1] = 'i'; header[2] = 'f'; header[3] = 'f'; int filelength = totaldatalen + 36; header[4] = (byte) (filelength & 0xff); header[5] = (byte) (filelength >> 8 & 0xff); header[6] = (byte) (filelength >> 16 & 0xff); header[7] = (byte) (filelength >> 24 & 0xff); //wave header[8] = 'w'; header[9] = 'a'; header[10] = 'v'; header[11] = 'e'; // 'fmt ' chunk header[12] = 'f'; header[13] = 'm'; header[14] = 't'; header[15] = ' '; // 4 bytes: size of 'fmt ' chunk header[16] = 16; header[17] = 0; header[18] = 0; header[19] = 0; // pcm format = 1 header[20] = 1; header[21] = 0; header[22] = (byte) channelcount; header[23] = 0; header[24] = (byte) (samplerate & 0xff); header[25] = (byte) (samplerate >> 8 & 0xff); header[26] = (byte) (samplerate >> 16 & 0xff); header[27] = (byte) (samplerate >> 24 & 0xff); int byterate = samplerate * bits * channelcount / 8; header[28] = (byte) (byterate & 0xff); header[29] = (byte) (byterate >> 8 & 0xff); header[30] = (byte) (byterate >> 16 & 0xff); header[31] = (byte) (byterate >> 24 & 0xff); // block align header[32] = (byte) (channelcount * bits / 8); header[33] = 0; // bits per sample header[34] = (byte) bits; header[35] = 0; //data header[36] = 'd'; header[37] = 'a'; header[38] = 't'; header[39] = 'a'; header[40] = (byte) (totaldatalen & 0xff); header[41] = (byte) (totaldatalen >> 8 & 0xff); header[42] = (byte) (totaldatalen >> 16 & 0xff); header[43] = (byte) (totaldatalen >> 24 & 0xff); return header; } public interface recordcallback { /** * 数据回调 * * @param bytes 数据 * @param len 数据有效长度,-1时表示数据结束 */ void onrecord(byte[] bytes, int len); } }
如有不对之处还请评论指正
下一篇: html开发中CSS按钮式链接解析
推荐阅读
-
Android技巧一之启动屏+新功能左右导航逻辑
-
Android开发之将两张图片合并为一张图片的方法
-
Android开发笔记之Android中数据的存储方式(一)
-
Android为TV端助力之解决setOnItemSelectedListener一进来就自动执行一次的问题
-
android之换头像及遇到的一些坑
-
一起学Android之Layout
-
一起学Android之Animation
-
Android技巧一之启动屏+新功能左右导航逻辑
-
Android属性动画Property Animation系列一之ObjectAnimator_html/css_WEB-ITnose
-
Android开发之将两张图片合并为一张图片的方法