共计 7473 个字符,预计需要花费 19 分钟才能阅读完成。
截屏
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 97
a=rtpmap:97 mpeg4-generic/44100/1
a=fmtp: 97 streamtype=5;config=1208;sizeLength=13; indexLength=3
- 没有 au header
m=audio 40020 RTP/AVP 97
a=rtpmap:97 mpeg4-generic/44100/1
a=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 测试播放
- vlc 关上工程目录下的 play_audio.sdp/play_audio_auheader.sdp。
- 启动 Android 利用指定运行 vlc 的电脑的 ip 地址。
- 开始录制,如何 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