ExoPlayer AudioProcessor处理模型
ExoPlayer内部维护了一个AudioProcessChain,它的作用就是串联各个AudioProcessor,将上一个AudioProcessor的输出传递到下一个AudioProcessor.伪代码类似
while (有数据) {
// 将输入喂给第一个Processor
processor[0].queueInput(inputBuffer);
// 依次传递下去
for (i = 0; i < processors.length; i++) {
ByteBuffer output = processor[i].getOutput();
if (i + 1 < processors.length) {
processor[i+1].queueInput(output);
} else {
// 最后一个processor的输出就是最终结果
writeToSink(output);
}
}
}
音频帧
这里的音频帧是指音频采样点。而不是mp3或者aac一个音频帧的概念。后者可能是多个样本,而前者是单声道1个采样点,双声道2个采样点。
SilenceSkippingAudioProcessor
SilenceSkippingAudioProcessor内部处理的模型,一言以蔽之,就是计算从当前时间点应该跳过的音频帧。后续每次音频播放的时候播放器会询问应该跳过的音频帧数。然后播放器内部会计算正确的应该在的时间戳。同时对于静音数据,SilenceSkippingAudioProcessor不会继续往下传递。因此最终也不会输出声音。
构造函数参数讲解
public SilenceSkippingAudioProcessor(
long minimumSilenceDurationUs, long paddingSilenceUs, short silenceThresholdLevel)
minimumSilenceDurationUs代表最短会被认为静音的时间短。短于这个时间短的静音片段不认为是静音。
paddingSilenceUs: 静音-非静音或者非静音-静音过度的时间长度,它的长度不能超过minimumSilenceDurationUs/2,silenceThresholdLevel,静音的阈值。小于这个数值的录音会被判定为静音。
ExoPlayer跳过静音的处理是由SilenceSkippingAudioProcessor,这个AudioProcessor进行处理的。它内部维持了一个状态机。
STATE_NOISY ---> (可能静音) ---> STATE_MAYBE_SILENT
^ |
| v
| STATE_SILENT
| |
+------ (检测到噪声) ---------------
SilenceSkippingAudioProcessor的处理单位的最小单位是音频帧。一个音频帧等同于一个样本,双声道则是两个样本。
STATE_NOISY是否能够切换到STATE_MAYBE_SILENT是由findNoiseLimit决定的,findNoiseLimit的逻辑是如果找到了单个采样位置它的阈值超过了静音阈值,那么把这个采样所属的整个音频帧都标记为’非静音’。因此如果findNoiseLimit返回的值等于position,说明整个Buffer中都是读取到静音的数据,
否则的话,就把当前明确标记为非静音的数据输出。
当状态切换为STATE_MAYBE_SILENT,处理逻辑如下。
收集后续的静音片段,直到要么达成阈值,要么遇到了非静音的。如果遇到了非静音,那么将状态切换为STATE_NOISY,STATE_NOISY负责把这部分不够长的音频输出。在收集到了足够的静音sample数据之后会进入STATE_SILENT。同时计算skippedFrames,当遇到下一个非静音的Sample的时候,切换到STATE_NOISY状态。
padding逻辑
进入静音时:会把 静音前的最后一小段样本(paddingSize)保留下来,一并输出。
跳过静音后恢复时:会把 静音后最开始的一小段样本也输出,作为恢复区间。
它为了解决什么问题?
原始音频: [声音][静音][声音]
直接跳过: [声音][声音]
这样第二段声音会“啪”地贴到前一段,缺乏过渡。
因此为了平滑声音的输出,这里会把一小段静音数据切出来,仍然输出。这样两段非静音数据数据之间就不会听着那么突兀。而这个切分的逻辑会导致部分静音数据仍然输出。