ExoPlayer 漫谈之Sonic调整音量

2,489 阅读4分钟

提一个问题:如何在播放视频的时候调整声音的大小?

我们使用Android手机播放视频的时候,发现声音大了,我们手动调低音量;发现声音小了,我们手动调高音量。

这个过程中,都要依赖手动,如果你在不断地刷短视频的时候,如果需要用户不断地手动调整音量键,那这个体验是不能忍受的。

这对我们提了一个要求:我们能在解码音频流的时候通过矩阵运算调整音频原始数据的大小,达到调整音量的目的?

这个思路是可行的,接下来我们分析一下声音的特征,进而给出如何做的方式。

声音的三个特征:

  • 音调:声音频率的高低叫做音调(Pitch),是声音的三个主要的主观属性,即音量(响度)、音调、音色(也称音品) 之一。表示人的听觉分辨一个声音的调子高低的程度。音调主要由声音的频率决定,同时也与声音强度有关
  • 响度:人主观上感觉声音的大小(俗称音量),由“振幅”(amplitude)和人离声源的距离决定,振幅越大响度越大,人和声源的距离越小,响度越大。(单位:分贝dB)
  • 音色:又称音品,波形决定了声音的音色。声音因不同物体材料的特性而具有不同特性,音色本身是一种抽象的东西,但波形是把这个抽象直观的表现。音色不同,波形则不同。典型的音色波形有方波,锯齿波,正弦波,脉冲波等。不同的音色,通过波形,完全可以分辨的。

声波的振幅表示声音的音量大小:

波长长短是衡量声音音调的因素:

音色主要和声波的波纹有关:

这儿我们要调整的是响度,就是调整声音的振幅,学过矩阵运算的同学都知道,声音振幅的调整通过简单的矩阵运算就能实现。 这儿先推荐一下ijkplayer调整声音的做法: ijkplayer 开源项目分析(十二)filter改变声音音量 这是ffmpeg提供的做法,通过在filter中传入af : volume=3dB 来动态调整音量的目的。

借助ffmpeg的强大后盾,实现这个功能自然不难,但是一些使用MediaCodec的播放器,能实现这个功能吗? 我们要在ExoPlayer中实现动态调整声音振幅的功能。

DefaultAudioSink.java是ExoPlayer中播放音频的控制类。 其中提供了多种AudioProcessor来处理音频数据。 AudioProcessor中主要函数:

  AudioFormat configure(AudioFormat inputAudioFormat) throws UnhandledAudioFormatException;

  boolean isActive();

  void queueInput(ByteBuffer buffer);

  void queueEndOfStream();

  ByteBuffer getOutput();

  boolean isEnded();

  void flush();

  void reset();
  • configure : 配置当前的AudioFormat
  • isActive : 当前的AudioProcessor是否可用
  • queueInput : 输入input buffer数据,这个ByteBuffer是原始数据
  • queueEndOfStream:当前队列中已经没有数据了
  • getOutput:处理完的ByteBuffer数据,送给DefaultAudioSink,开始AudioTrack播放
  • ChannelMappingAudioProcessor
  • FloatResamplingAudioProcessor:将24-bit和32-bit 的整型audio转化为32-bit的浮点型audio,整型和浮点型占用的位宽不一样,浮点型表现出现的声音更加细腻
  • ResamplingAudioProcessor:将其他位数的audio数据重采样位16-bit的audio数据
  • SilenceSkippingAudioProcessor:跳过静音部分
  • TeeAudioProcessor
  • TrimmingAudioProcessor

SonicAudioProcessor很重要了,倍速和调整声音振幅都依赖他。SonicAudioProcessor是根据Sonic算法来调整pitch、speed、volume变化时的声音处理。

我们这里只谈如何调整声音振幅: github.com/JeffMony/Pl…

  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);
    }
  }

  private void scaleSamples(short samples[],
                            int position,
                            int numSamples,
                            float volume) {
    int fixedPointVolume = (int)(volume * 4096.0f);
    int start = position * channelCount;
    int stop = start + numSamples * channelCount;

    for(int xSample = start; xSample < stop; xSample++) {
      int value = (samples[xSample] * fixedPointVolume) >> 12;
      if(value > 32767) {
        value = 32767;
      } else if(value < -32767) {
        value = -32767;
      }
      samples[xSample] = (short)value;
    }
  }

这里的volume是更新振幅的倍数。例如volume=1.2f,将振幅从原来的值调整为1.2倍。

声音的分贝如何计算? result = 20 * log(Cur/Max) Cur表示当前振幅 Max表示最大振幅 所以声音的分贝总是负的(Android平台下是的)。 volume(dB) = 20 * log(Cur / Max)

  • volume 表示计算出来的分贝值
  • Max表示最大振幅
  • Cur表示当前振幅

输入的参数有两个: MeanVolume , BaseVolume

  • MeanVolume: 平均分贝
  • BaseVolume: 基准分贝
BaseVolume - MeanVolume = result
20*log(CurBase/Max) - 20*log(Cur/Max) = result
20*log(CurBase/Cur)=result
CurBase/Cur = 10^(result/20)
CurBase = Cur * 10^(result/20)

我们最终关注的是 (10^(result/20))

如果要设置到ExoPlayer中的数也是 (10^(result/20))

本文所讲源码均来自项目:github.com/JeffMony/Pl…