ExoPlayer 漫谈之倍速

2,561 阅读4分钟

倍速是播放器的一个非常重要的功能,倍速的原理听上去很简单: 音频和视频帧都有一个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解码,来达到视频倍速的效果
  • 视频倍速没有什么复杂的逻辑

关键抓住音频倍速的关键点 : 跳帧处理