倍速是播放器的一个非常重要的功能,倍速的原理听上去很简单: 音频和视频帧都有一个pts标识当前的时间戳。倍速的场景是要求音频流倍速和视频流倍速。
- 视频的倍速比较简单,就是解码的时候设置帧率,让MediaCodec解码视频帧的时候根据设置的帧率来就行了。
- 音频的倍速比较麻烦,因为声音在倍速的同时,还要考虑声音码率和声调的改变。当过快或者过慢的时候,实际上音频已经失真了,这时候也要考虑一下。
视频是一帧一帧的画面,音频是一串比特流,然后调整比特流的码率和振幅,用AudioTrack来渲染音频。
ExoPlayer中定义的PlaybackParameters.java中存储这倍速相关的参数:
/** The factor by which playback will be sped up. */
public final float speed;
/** The factor by which the audio pitch will be scaled. */
public final float pitch;
public final float volume;
我们设置倍速的时候通过将PlaybackParameters设置进SimpleExoPlayer实例中,然后通过ExoPlayerImplInternal传入setPlaybackParametersInternal函数
private void setPlaybackParametersInternal(PlaybackParameters playbackParameters) {
mediaClock.setPlaybackParameters(playbackParameters);
sendPlaybackParametersChangedInternal(
mediaClock.getPlaybackParameters(), /* acknowledgeCommand= */ true);
}
mediaClock就是DefaultMediaClock实例,DefaultMediaClock中的setPlaybackParameters方法如下:
@Override
public void setPlaybackParameters(PlaybackParameters playbackParameters) {
if (rendererClock != null) {
rendererClock.setPlaybackParameters(playbackParameters);
playbackParameters = rendererClock.getPlaybackParameters();
}
standaloneClock.setPlaybackParameters(playbackParameters);
}
这个rendererClock就是定义的render实例,之前也已经讲过,音频的渲染处理类是MediaCodecAudioRenderer.java,视频的渲染处理类是MediaCodecVideoRenderer.java,两者的父类是MediaCodecRenderer.java 这儿将PlaybackParameters实例设置到MediaCodecVideoRenderer中,因为只有MediaCodecAudioRenderer.java实现了MediaClock:
public class MediaCodecAudioRenderer extends MediaCodecRenderer implements MediaClock
这里为什么要这么实现,因为音频对时间的校准非常严格,之前分析音视频同步的时候也发现了音频那边的时间计算非常严格.基本上都是以音频的pts为基准的.
执行到MediaCodecAudioRenderer.setPlaybackParameters,然后设置到DefaultAudioSink.setPlaybackParameters
@Override
public void setPlaybackParameters(PlaybackParameters playbackParameters) {
audioSink.setPlaybackParameters(playbackParameters);
}
音频倍速
DefaultAudioSink是控制声音的模块.我们要实现音频的倍速,首先需要是的对声音进行处理,因为倍速之后的音频就不能按照正常的音频进行播放了,声音的码率和pitch都会发生变化. 这儿选中的AudioProcessor是SonicAudioProcessor,看一下SonicAudioProcessor.isActive函数,speed发生变化,SonicAudioProcessor会被激活.
@Override
public boolean isActive() {
return pendingOutputAudioFormat.sampleRate != Format.NO_VALUE
&& (Math.abs(speed - 1f) >= CLOSE_THRESHOLD
|| Math.abs(pitch - 1f) >= CLOSE_THRESHOLD
|| Math.abs(volume - 1f) >= CLOSE_THRESHOLD
|| pendingOutputAudioFormat.sampleRate != pendingInputAudioFormat.sampleRate);
}
在SonicAudioProcessor初始化的时候,会更新一下当前的Sonic设置,将speed/pitch/volume设置进来.
@Override
public void flush() {
if (isActive()) {
inputAudioFormat = pendingInputAudioFormat;
outputAudioFormat = pendingOutputAudioFormat;
if (pendingSonicRecreation) {
sonic =
new Sonic(
inputAudioFormat.sampleRate,
inputAudioFormat.channelCount,
speed,
pitch,
volume,
outputAudioFormat.sampleRate);
} else if (sonic != null) {
sonic.flush();
}
}
outputBuffer = EMPTY_BUFFER;
inputBytes = 0;
outputBytes = 0;
inputEnded = false;
}
在SonicAudioProcessor.queueInput中传入音频流:
@Override
public void queueInput(ByteBuffer inputBuffer) {
Sonic sonic = Assertions.checkNotNull(this.sonic);
if (inputBuffer.hasRemaining()) {
ShortBuffer shortBuffer = inputBuffer.asShortBuffer();
int inputSize = inputBuffer.remaining();
inputBytes += inputSize;
sonic.queueInput(shortBuffer);
inputBuffer.position(inputBuffer.position() + inputSize);
}
int outputSize = sonic.getOutputSize();
if (outputSize > 0) {
if (buffer.capacity() < outputSize) {
buffer = ByteBuffer.allocateDirect(outputSize).order(ByteOrder.nativeOrder());
shortBuffer = buffer.asShortBuffer();
} else {
buffer.clear();
shortBuffer.clear();
}
sonic.getOutput(shortBuffer);
outputBytes += outputSize;
buffer.limit(outputSize);
outputBuffer = buffer;
}
}
- inputBuffer是传入的原始音频流
- 原始音频流会通过Sonic来加工处理,sonic.queueInput加工处理原始比特流
- 加工完的比特流会通过sonic.getOutput(shortBuffer)返回,直接将加工后的比特流返回给AudioTrack处理即可.
Sonic.java参考:github.com/waywardgeek… Sonic是一种用于加快或减慢语音速度的简单算法。然而,与先前的变更算法不同,它针对超过2倍的加速进行了优化语速。 Sonic库是一个非常简单的ANSI C库,其设计旨在轻松集成到流语音应用程序(例如TTS后端)中。
Sonic背后的主要动机是使盲人和视障者通过像espeak这样的开源语音引擎来提高他们的生产力。视力人士也可以使用Sonic。例如,声波可以改善在Android手机上听有声读物的经验。
ExoPlayer引入了Sonic来处理音频的倍速和pitch和volume操作.
public void queueInput(ShortBuffer buffer) {
int framesToWrite = buffer.remaining() / channelCount;
int bytesToWrite = framesToWrite * channelCount * 2;
inputBuffer = ensureSpaceForAdditionalFrames(inputBuffer, inputFrameCount, framesToWrite);
buffer.get(inputBuffer, inputFrameCount * channelCount, bytesToWrite / 2);
inputFrameCount += framesToWrite;
processStreamInput();
}
private void processStreamInput() {
// Resample as many pitch periods as we have buffered on the input.
int originalOutputFrameCount = outputFrameCount;
float s = speed / pitch;
float r = rate * pitch;
if (s > 1.00001 || s < 0.99999) {
changeSpeed(s);
} else {
copyToOutput(inputBuffer, 0, inputFrameCount);
inputFrameCount = 0;
}
if (r != 1.0f) {
adjustRate(r, originalOutputFrameCount);
}
if(volume != 1.0f) {
// Adjust output volume.
scaleSamples(outputBuffer, originalOutputFrameCount, outputFrameCount - originalOutputFrameCount,
volume);
}
}
针对倍速的处理,在changeSpeed函数中写明了: float s = speed / pitch 速度和音调有很强的关系,因为速度发生变化,音调的升高降低的时间点和持续时长都会发生变化.
private void changeSpeed(float speed) {
if (inputFrameCount < maxRequiredFrameCount) {
return;
}
int frameCount = inputFrameCount;
int positionFrames = 0;
do {
if (remainingInputToCopyFrameCount > 0) {
positionFrames += copyInputToOutput(positionFrames);
} else {
int period = findPitchPeriod(inputBuffer, positionFrames);
if (speed > 1.0) {
positionFrames += period + skipPitchPeriod(inputBuffer, positionFrames, speed, period);
} else {
positionFrames += insertPitchPeriod(inputBuffer, positionFrames, speed, period);
}
}
} while (positionFrames + maxRequiredFrameCount <= frameCount);
removeProcessedInputFrames(positionFrames);
}
- 处理当前时间点位置到送入的input frame count之间的所有数据
- 先将之前处理过的ByteBuffer数据拷贝到output buffer中
- 如果当前speed > 1.0,就是快放,那肯定要条过部分音频数据,如何跳过,还是要讲究策略的. 一般而言,如果speed > 2.0,这时候已经无法听清楚完成音频了,所以之后不需要保证音频能听,只需要跳过音频帧就行了. 如果speed在1.0和2.0之间,这时候音频播放基本上没有影响,这时候拷贝音频数据的时候还要给remainingInputToCopyFrameCount赋值,将原有的音频数据拷贝过去虽然时跳帧,但是部分原始数据在加工之后还是输出到ouput buffer队列中
private int copyInputToOutput(int positionFrames) {
int frameCount = Math.min(maxRequiredFrameCount, remainingInputToCopyFrameCount);
copyToOutput(inputBuffer, positionFrames, frameCount);
remainingInputToCopyFrameCount -= frameCount;
return frameCount;
}
- 如果speed < 1.0,就是慢放,原来的数据需要填充一些数据才能铺满整个timestamp 这里也有判断条件,如果speed < 0.5f,这时候根本无法听清楚音频了,不会渲染任何数据 speed 在0.5和1.0之间,此时还有拷贝部分数据到output buffer中
- 填充好的ouput buffer送到AudioTrack中渲染播放
视频倍速
MediaCodecRenderer.updateCodecOperatingRate中传入的codecOperatingRate用作控制视频的倍速,视频的倍速用MediaCodec解码来控制,
private void updateCodecOperatingRate() throws ExoPlaybackException {
if (Util.SDK_INT < 23) {
return;
}
float newCodecOperatingRate =
getCodecOperatingRateV23(rendererOperatingRate, codecFormat, getStreamFormats());
if (codecOperatingRate == newCodecOperatingRate) {
// No change.
} else if (newCodecOperatingRate == CODEC_OPERATING_RATE_UNSET) {
// The only way to clear the operating rate is to instantiate a new codec instance. See
// [Internal ref: b/71987865].
drainAndReinitializeCodec();
} else if (codecOperatingRate != CODEC_OPERATING_RATE_UNSET
|| newCodecOperatingRate > assumedMinimumCodecOperatingRate) {
// We need to set the operating rate, either because we've set it previously or because it's
// above the assumed minimum rate.
Bundle codecParameters = new Bundle();
codecParameters.putFloat(MediaFormat.KEY_OPERATING_RATE, newCodecOperatingRate);
codec.setParameters(codecParameters);
codecOperatingRate = newCodecOperatingRate;
}
}
- 设置MediaCodec帧率,来控制视频的codec解码,来达到视频倍速的效果
- 视频倍速没有什么复杂的逻辑
关键抓住音频倍速的关键点 : 跳帧处理