截屏

AudioRecord音频采集

    private val sampleRate = mediaFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE)    private val channelCount = mediaFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT)    private val minBufferSize = AudioRecord.getMinBufferSize(sampleRate, if (channelCount == 1) CHANNEL_IN_MONO else CHANNEL_IN_STEREO, AudioFormat.ENCODING_PCM_16BIT);        runInBackground {            audioRecord = AudioRecord(                MediaRecorder.AudioSource.MIC,                sampleRate,                if (channelCount == 1) CHANNEL_IN_MONO else CHANNEL_IN_STEREO,                AudioFormat.ENCODING_PCM_16BIT,                2 * minBufferSize            )            audioRecord.startRecording()        }

音频采集时须要设置采集参数,设置的这些参数须要与创立MediaCodec时的参数统一。

  • sampleRate是采样率:44100
  • channelCount是通道数:1
  • 单个采样数据大小格局:AudioFormat.ENCODING_PCM_16BIT
  • 最小数据buffer:AudioRecord.getMinBufferSize()计算获取
override fun onInputBufferAvailable(codec: MediaCodec, index: Int) {        try {            codec.getInputBuffer(index)?.let { bb ->                var startTime = System.currentTimeMillis();                var readSize = audioRecord.read(bb, bb.capacity())                log { "read time ${System.currentTimeMillis() - startTime} read size $readSize" }                if (readSize < 0) {                    readSize = 0                }                codec.queueInputBuffer(index, 0, readSize, System.nanoTime() / 1000, 0)            }        }catch (e:Exception){            e.printStackTrace()        }    }

这里采纳的阻塞的形式采集数据,所以AudioRecord根据设置的采样频率生成数据的,咱们能够间接把以后的工夫设置为录制的工夫戳。

MediaCodec编码音频数据

 val mediaFormat = MediaFormat.createAudioFormat(                MediaFormat.MIMETYPE_AUDIO_AAC,                audioSampleRate,                audioChannelCount        )        mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, audioBitRate)        mediaFormat.setInteger(                MediaFormat.KEY_AAC_PROFILE,                MediaCodecInfo.CodecProfileLevel.AACObjectLC        )        mediaFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, audioMaxBufferSize)

为MediaCodec创立MediaFormat并设置参数,这里设置的音频参数必须与AudioRecord统一。

  • MIME_TYPE:"audio/mp4a-latm"
  • 采样频率与AudioRecord统一:44100
  • 通道数与AudioRecord统一:1
  • KEY_AAC_PROFILE配置为低带宽要求类型:AACObjectLC
  • KEY_BIT_RATE设置的大小影响编码压缩率:128 * 1024
override fun onInputBufferAvailable(codec: MediaCodec, index: Int) {        try {            codec.getInputBuffer(index)?.let { bb ->                var startTime = System.currentTimeMillis();                var readSize = audioRecord.read(bb, bb.capacity())                log { "read time ${System.currentTimeMillis() - startTime} read size $readSize" }                if (readSize < 0) {                    readSize = 0                }                codec.queueInputBuffer(index, 0, readSize, System.nanoTime() / 1000, 0)            }        }catch (e:Exception){            e.printStackTrace()        }    }

给MediaCodec传数据的时候设置的工夫戳是以后的零碎工夫,因为咱们应用rtp发送实时数据,所以flag不须要设置完结标记。

    audioCodec = object : AudioEncodeCodec(mediaFormat) {            override fun onOutputBufferAvailable(                    codec: MediaCodec,                    index: Int,                    info: MediaCodec.BufferInfo            ) {                try {                    val buffer = codec.getOutputBuffer(index) ?: return                    if (lastSendAudioTime == 0L) {                        lastSendAudioTime = info.presentationTimeUs;                    }                    val increase =                            (info.presentationTimeUs - lastSendAudioTime) * audioSampleRate / 1000 / 1000                    if (hasAuHeader) {                        buffer.position(info.offset)                        buffer.get(bufferArray, 4, info.size)                        auHeaderLength.apply {                            bufferArray[0] = this[0]                            bufferArray[1] = this[1]                        }                        auHeader(info.size).apply {                            bufferArray[2] = this[0]                            bufferArray[3] = this[1]                        }                        audioRtpWrapper?.sendData(bufferArray, info.size + 4, 97, true, increase.toInt())                    } else {                        buffer.position(info.offset)                        buffer.get(bufferArray, 0, info.size)                        audioRtpWrapper?.sendData(bufferArray, info.size, 97, true, increase.toInt())                    }                    lastSendAudioTime = info.presentationTimeUs                    codec.releaseOutputBuffer(index, false)                } catch (e: Exception) {                    e.printStackTrace()                }            }    }

从MediaCodec读出的是aac原始的数据,咱们能够依据具体的需要来决定是否增加au header发送。这里实现了有au header和没有 au header两种计划。没有au header的状况咱们间接把MediaCode读出的数据通过rtp发送进来。有au header的状况咱们须要在原始的aac数据后面追加4个字节的au header。是否有au header与vlc播放的sdp内容无关。前面会详解介绍sdp内容的设置。

    private val auHeaderLength = ByteArray(2).apply {        this[0] = 0        this[1] = 0x10    }    private fun auHeader(len: Int): ByteArray {        return ByteArray(2).apply {            this[0] = (len and 0x1fe0 shr 5).toByte()            this[1] = (len and 0x1f shl 3).toByte()        }    }
  • au header length占用两个字节,它会形容au header的大小,这里设置为2.
  • au header 占用两个字节,它形容了aac原始数据的大小,这里须要依据MediaCodec返回的aac原始数据大小进行设置。

Rtp发送数据

咱们应用jrtplib库来发送数据,这里对库进行简略的封装并提供了java封装类RtpWrapper。

public class RtpWrapper {    private long nativeObject = 0;    private IDataCallback callback;    public RtpWrapper() {        init();    }    @Override    protected void finalize() throws Throwable {        release();        super.finalize();    }    public void setCallback(IDataCallback callback) {        this.callback = callback;    }    void receivedData(byte[] buffer, int len) {        if(this.callback != null)        this.callback.onReceivedData(buffer, len);    }    public interface IDataCallback {        void onReceivedData(byte[] buffer, int len);    }    static {        try {            System.loadLibrary("rtp-lib");            initLib();        } catch (Throwable e) {            e.printStackTrace();        }    }    private native static void initLib();    private native boolean init();    private native boolean release();    public native boolean open(int port, int payloadType, int sampleRate);    public native boolean close();    /**     * @param ip "192.168.1.1"     * @return     */    public native boolean addDestinationIp(String ip);    public native int sendData(byte[] buffer, int len, int payloadType, boolean mark, int increase);}

open办法要指定发送数据应用的端口,payloadType设置载体类型,sampleRate是采样率。
addDestinationIp用于增加接收端ip地址,地址格局: "192.168.1.1"。
sendData办法用于发送数据,increase是工夫距离,工夫单位是 sampleRate/秒

            override fun onOutputFormatChanged(codec: MediaCodec, format: MediaFormat) {                audioRtpWrapper = RtpWrapper()                audioRtpWrapper?.open(audioRtpPort, audioPayloadType, audioSampleRate)                audioRtpWrapper?.addDestinationIp(ip)            }

MediaCodec返回format的时候创立rtp连贯并指定目标地址。

try {                    val buffer = codec.getOutputBuffer(index) ?: return                    if (lastSendAudioTime == 0L) {                        lastSendAudioTime = info.presentationTimeUs;                    }                    val increase =                            (info.presentationTimeUs - lastSendAudioTime) * audioSampleRate / 1000 / 1000                    if (hasAuHeader) {                        buffer.position(info.offset)                        buffer.get(bufferArray, 4, info.size)                        auHeaderLength.apply {                            bufferArray[0] = this[0]                            bufferArray[1] = this[1]                        }                        auHeader(info.size).apply {                            bufferArray[2] = this[0]                            bufferArray[3] = this[1]                        }                        audioRtpWrapper?.sendData(bufferArray, info.size + 4, 97, true, increase.toInt())                    } else {                        buffer.position(info.offset)                        buffer.get(bufferArray, 0, info.size)                        audioRtpWrapper?.sendData(bufferArray, info.size, 97, true, increase.toInt())                    }                    lastSendAudioTime = info.presentationTimeUs                    codec.releaseOutputBuffer(index, false)                } catch (e: Exception) {                    e.printStackTrace()                }

发送数据的时候须要指定payloadType,间隔上次发送数据的工夫距离等信息。
(info.presentationTimeUs - lastSendAudioTime)计算的是以奥妙为单位的工夫距离。
(info.presentationTimeUs - lastSendAudioTime) * audioSampleRate / 1000 / 1000转换成sampleRate/秒为单位的工夫距离。
rtp发送aac数据应用的payloadType为97。

SDP文件配置

vlc播放器播放rtp音频数据时须要指定sdp文件,它通过读取sdp文件中的信息能够理解rpt接管端口、payloadType类型、音频的格局等信息用于接管数据流并解码播放。这里有两种配置形式用于反对有au header和没有au header的状况。

  • 有au header
m=audio 40020  RTP/AVP 97a=rtpmap:97 mpeg4-generic/44100/1a=fmtp: 97 streamtype=5;config=1208;sizeLength=13; indexLength=3
  • 没有au header
m=audio 40020  RTP/AVP 97a=rtpmap:97 mpeg4-generic/44100/1a=fmtp: 97 streamtype=5;config=1208

sdp文件配置了端口号为40020, Rtp payload type为97,音频的采样率为44100、通道数为1。
config是用于形容音频信息的,它的生成形式如下:

    private val audioChannelCount = 1;    private val audioProfile = 2    /**     *  97000, 88200, 64000, 48000,44100, 32000, 24000, 22050,16000, 12000, 11025, 8000,7350, 0, 0, 0     */    private val audioIndex = 4    private val audioSpecificConfig = ByteArray(2).apply {        this[0] = ((audioProfile).shl(3).and(0xff)).or(audioIndex.ushr(1).and(0xff)).toByte()        this[1] = ((audioIndex.shl(7).and(0xff)).or(audioChannelCount.shl(3).and(0xff))).toByte()    }

audioProfile 与设置给MediaCodec的Profile统一,这里设置的是AACObjectLC(2),所以audioProfile是2。

audioChannelCount通道数是1 。

audioIndex通过正文的采样率表格找到对应44100采样率的index为4 。

比拟有au header和没有au header的两个版本,发现它们的区别在于是否配置了sizeLength和indexLength。

我这里的au header是两个字节的,sizeLength为13代表占用了13bit,indexLength为3代表占用3bit。配合发送数据时增加au header的代码就容易了解了。

   private fun auHeader(len: Int): ByteArray {        return ByteArray(2).apply {            this[0] = (len and 0x1fe0 shr 5).toByte()            this[1] = (len and 0x1f shl 3).toByte()        }    }

vlc测试播放

  1. vlc关上工程目录下的play_audio.sdp/play_audio_auheader.sdp 。
  2. 启动Android利用指定运行vlc的电脑的ip地址。
  3. 开始录制,如何vlc关上的是play_audio_auheader.sdp,那么在开始录制前须要选中auHeader check box

总结

  • AudioRecord的设置信息与MediaCodec的配置信息必须统一。
  • AudioRecord采纳block的形式读取数据,这样咱们能够间接应用零碎工夫来配置encode工夫戳。
  • 是否须要增加au header与sdp配置无关,vlc播放器会依照sdp配置解析au header。
  • sdp中的config须要依照理论的音频配置信息计算得出,否则不能失常播放。

工程git地址

https://github.com/mjlong123123/AudioRecorder