val audioSource = MediaRecorder.AudioSource.MIC
val sampleRateInHz = 8000
val channelConfig = AudioFormat.CHANNEL_IN_MONO
val audioFormat = AudioFormat.ENCODING_PCM_16BIT
val minBufferSize = AudioRecord.getMinBufferSize(sampleRateInHz, channelConfig, audioFormat)
val audioRecord = AudioRecord(audioSource, sampleRateInHz, channelConfig, audioFormat, minBufferSize)
在Android中可以使用AudioRecord来录制麦克风的声音,如上代码是创建了一个AudioRecord对象,通过这个对象就可以采集到麦克风的声音了,采集到的声音格式为pcm格式,也就是无压缩的原始音频数据格式。上面代码中有三个重要参数,如下:
音视频开发是比较难,上面也是我自己的理解,可能是错误的,这里提供一篇别人的文章,仅供参考:https://www.cdsy.xyz/computer/fileABC/230205/cd40214.html
如果使用采样频率为8000Hz,采样位数为16位,通道为单声道,则一秒种采集到的声音的数据量为:
采样频率 x 采样位数 x 通道数 = 8000 x 16 x 1 = 128000bit
注:这个单位是bit(比特位)可以简写为b,8个bit为1个字节,1024字节为1KB,所以128000位换成KB等于:
128000 ÷ 8 ÷ 1024 = 15.625KB
如果采集1分钟,则采集到的PCM音频大小为:15.625KB x 60秒 = 937.5KB,差不多1MB。如果录3分钟(差不多一首歌曲的时长),则需要不到3MB的存储空间,注意,这是原始PCM音频数据需要的存储空间,算是非常小的了,平时我们下载的mp3音频是经过压缩的,也是3M左右,所以1分钟的PCM音频才3MB是非常小的。
在压缩的时候,有一个压缩参数叫码率(也叫做比特率),码率的单位是bit,表示1秒钟的PCM压缩后的大小是多少,比如我们设置码率为32000bit,假如1秒钟的PCM数据大小为128000bit(也就是15.625KB),压缩成AAC后,音频大小就变成了32000bit(大约4KB),看到了吧,原本15KB的音频,压缩后变成4KB,这个压缩比例为:128000 ÷ 32000 = 4倍,也就是说,压缩后的音频大小为原来的4分之一,小了很多,方便传输或存储。
如果按照前面例子的参数,录制1分钟,并按32kb的码率压缩为AAC,则1分钟的AAC文件大小约234KB,天哪,怎么这么小!因为我们使用的采样率比较低,所以AAC就小,相应的声音质量就低,8KHz的采样频率适合人的通话声音,如果要录制音乐,则需要使用更高的采样频率,相对的压缩时使得的码率也得跟着提高才行,比如,你使用44100Hz的采样频率,则1秒钟的PCM大小为:
44100 x 16 x 1 = 705600bit
如果你还使用32kb的码率,则比原来小了22倍(705600 ÷ 32000 = 22.05),那太恐怖了,声音质量肯定会大大下降的,705600bit大约为86KB,32000bit大约为4KB,86KB的音频变成4KB,你品,你细品!实际测试时,我发现44.1KHz + 32kb码率录制的音乐比8KHz + 32kb的音质好很多,而我使用的是动态码率,44.1KHz录1分钟为254K,8KHz录1分钟为239K,也没大多少,但是音质却好很多,神奇哈,一个压缩了22倍的音质竟然比压缩了4倍的好,只因一个采样率高,一个采样率低。
那AAC的码率选多少合适呢?我也不知道,百度上也找不到文章介绍说什么采样频率应该使用什么码率的。所以也只能靠自猜了,压缩比例为4倍肯定是没问题的,倍数太大了声音失真,倍数太小了音频文件太大,所以选 4 ~ 7倍的压缩率是比较适中的(这是我自己乱猜的),而我们平时常见的码率有320kb,256kb,192kb,128kb,64kb,32kb,那我们就使用这些常见码率的其中一个即可,挑选时自己计算一下压缩率,如果压缩率在4 ~ 7倍则是合适的,希望音质好一点则压缩率就调小一点,希望文件小一点,则把压缩率调大一点,比如,44100Hz采样率的音频,我希望用6倍的压缩率,则44100 x 16 x 1 ÷ 6 = 117600bit,就是说用6倍的6压缩率,压缩后大小为117600bit(约为117kb),然后我们看它与128kb这个常见码率接近,则可以使用128kb作为压缩码率。
需要注意的是:
比如32kb的码率,把位单位(千位:kb)换成我们熟悉的字节单位(千字节:KB),步骤如下:
一般表示比特率时,会用bps来表示 ,如32kbps。bps的意思为:bit per second,即每秒钟传输的比特数量,32kbps即表示每秒传输的比特位数量为32kb。
注意:网络供应商,如电信,在介绍宽带时,一般使用形如4Mbps的方式来表示网速(注意,这里的M是大写而b是小写),则它最初是这样转变过来的:b -> Kb -> Mb,前面有介绍到,大写的转换是要乘1024的,所以1024b = 1Kb,1024Kb = 1Mb。把位单位(兆位:mb)换成我们熟悉的字节单位(兆字节:MB),如下:
由此可见,当你拉了一条4Mbps的宽带时,你下载文件的速度最大就是0.5MB每秒,不要以为是4M每秒哦!注意:看上面的换算,步骤1是乘两个1024,而步骤2是除两个1024,还多除了一个8,所以两个1024可以化掉,直接除8好可,如4Mbps = 4 / 8 = 0.5MBps。
总结:
对应的视频也有码率,码率设置多少合适也可参照音频这里的方法,比如yuv视频压缩为h264视频,一般压缩比例是多少,然后根据你的实际yuv大小除以压缩比例,就得到一个码率,然后再去找找一些觉见的视频压缩码率,找一个接近的码率即可。
object H264Util {
/** 打印支持的H264编码器 */
@Suppress("DEPRECATION")
fun printSupportedH264Encoder() {
val codecCount = MediaCodecList.getCodecCount()
for (i in 0 until codecCount) {
val codecInfo = MediaCodecList.getCodecInfoAt(i)
if (!codecInfo.isEncoder) continue // 如果不是编码器,则找下一个
val mimeType = MediaFormat.MIMETYPE_VIDEO_AVC // H264的mime类型
val supportedH264 = codecInfo.supportedTypes.any { type -> type.equals(mimeType, true) }
if (supportedH264) {
val colorFormats = codecInfo.getCapabilitiesForType(mimeType).colorFormats
// 通过int值,找到它是在MediaCodecInfo.CodecCapabilities中的哪个变量
val colorFormatsConvert = Array(colorFormats.size) { index ->
ReflectUtil.getPublicStaticIntFieldNameByValue(MediaCodecInfo.CodecCapabilities::class.java, colorFormats[index])
}
Timber.i("H264编码器:${codecInfo.name}, 支持的颜色格式:${colorFormatsConvert.contentToString()}")
}
}
}
}
object ReflectUtil {
/** 获取指定类中的所有public static int类型的变量 */
fun getAllPublicStaticIntField(clazz: Class<*>): Map<Int, String> {
val fieldsMap: MutableMap<Int, String> = HashMap()
clazz.fields.filter {
Modifier.isPublic(it.modifiers)
&& Modifier.isStatic(it.modifiers)
&& it.genericType === Int::class.javaPrimitiveType
}.forEach {
fieldsMap[it.get(null) as Int] = it.name
}
return fieldsMap
}
/** 返回指定的值对应的是指定类的哪个变量,如果没有对应的变量,则返回value自身 */
fun getPublicStaticIntFieldNameByValue(clazz: Class<*>, value: Int): String {
return getAllPublicStaticIntField(clazz)[value] ?: value.toString()
}
}
如,在我的一个手机上运行H264Util.printSupportedH264Encoder(),结果如下:
H264编码器:OMX.qcom.video.encoder.avc, 支持的颜色格式:[2141391876, COLOR_FormatSurface, COLOR_FormatYUV420Flexible, COLOR_FormatYUV420SemiPlanar]
H264编码器:OMX.google.h264.encoder, 支持的颜色格式:[COLOR_FormatYUV420Flexible, COLOR_FormatYUV420Planar, COLOR_FormatYUV420SemiPlanar, COLOR_FormatSurface]
其中:
OMX.qcom.video.encoder.avc 是硬件编码器,支持NV12
OMX.google.h264.encoder 是软件编码器,支持NV12、I420
通过如下方法创建的是硬件编码器:
MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC)
1、获取缓冲区,往里面填入原始视频数据,再把缓冲区还给编码器
2、编码器把缓冲区中的数据进行编码,再往入缓冲区中,我们从缓冲区中取出编码好的压缩数据
需要注意的是:并不是放入一帧就立马编码一帧的数据,比如放入第一帧的时候,我们从输出缓冲区中并不能拿到数据,放入第二帧后,从输出缓冲区可以拿到3帧的数据(包含有一个配置帧),所以这是为什么在输出缓冲中使用while循环来获取数据的原因。
每次从编码器缓冲区取出来的数据都是正好一帧的数据,第一次出来的数据是配置帧,包含sps和pps的数据,第二帧肯定是一个关键帧,也称为IDR帧(视频的第一个着键帧称为IDR帧,其它关键帧不能称为IDR帧),后面就是P帧,多个P帧后又是I帧(关键帧),没看到过B帧,不知道要怎么样配置编码器可以出来B帧。除了前面的帧特殊一点之外,后面的都是放入一帧视频就会立马编码出一帧视频,所以那时候的while循环就显得有点多余,不知道去掉while循环能有多少优化。不过官方都是这样写的,说明while循环的影响不大,还是得按官方的写。当使用异步模式的时候,发现放入5帧才开始出数据,而且也不是一次出5帧的,有时候会连续往入两帧,有时候会连续出两帧,在编码结束的时候会一次出四五个普通帧。
I帧间隔:比如设置为2秒,则每2秒就会出来一个I帧,如果我们给编码器设置帧速为25帧/秒,即使我们并没有每秒给编码器25帧也没关系,反正编码器会统计给的帧数,每50帧的时候就会编码一个I帧出来,因为设置I帧间隔是2秒,每秒25帧,所以是每50帧出一个I帧,即第1、51、101、151帧是I帧。
在保存本地视频时,在视频的开头保存一次配置帧就可以了,配置帧中包含有视频的分辨率、帧速等信息,这样播放器才知道怎样播放。
而在网络发送中,一般每个关键帧前面都要加上配置帧再发送,或者每隔多少秒加一个配置帧到关键帧前面,预防最开始的配置帧网络发送失败,那后面的帧如果没有了配置帧则网络播放器就不知道如何解析这个视频流了。
var outputBufferIndex = mMediaCodec.dequeueOutputBuffer(mBufferInfo, 10_000)
当outputBufferIndex为MediaCodec.INFO_TRY_AGAIN_LATER,千万别一直死循环取数据了,或者睡眠一会再取数据也是不可取的,因为编码器里面没数据了也会返回这个,也如果没有往编码器放入新的数据,就是取等多久再取也是取不出数据的。