Android 分场景集成不同音频倍速算法的实现
Posted 却把清梅嗅
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android 分场景集成不同音频倍速算法的实现相关的知识,希望对你有一定的参考价值。
概述
上文 《Android 音频倍速的原理与算法分析》 中, 我们针对音频倍速的基本原理进行了梳理,并逐步引申出了 android
平台上常用的2种算法实现:Sonic
和 SoundTouch
。
初步结论是,在用户启用音频倍速时,我们需要 根据具体场景切换不同实现 ,以此保证最佳的用户体验。
举例来说,对于常规音乐——尤其是背景乐、打击感比较强的音乐,我们优先选择 SoundTouch
, 而对于人声更纯粹的音频(相声评书、歌手清唱等)而言,Sonic
才是更好的选择。
本文以 Google
开源的 ExoPlayer
为例,从源码分析播放器自身的 Sonic
具体是如何实现的倍速;之后,再尝试将 SoundTouch
集成,为播放器提供不同应用场景下不同的倍速实现。
Sonic 源码分析
1. AudioProcessor 音频处理器简介
ExoPlayer
默认内部集成了 Sonic
实现音频的变速及变调,并且是 java
版本的,适合着手学习。
开发者只需要实现 ExoPlayer
提供的 AudioProcessor
接口就能对定制属于自己的音频效果,比如变速变调、萝莉音、背景音效等等。
这样的设计非常常见,比如
OkHttp
的interceptor
、View
的事件分发和拦截机制等等。
// 音频处理器接口,将音频数据作为输入并进行转换,从而修改其通道数,编码或采样率。
public interface AudioProcessor
// 将Processor配置为处理指定格式的输入音频
// 1.调用此方法后,调用isActive()以确定音频处理器是否处于活动状态。 如果此实例处于活动状态,则返回配置的输出音频格式;
// 2.调用此方法后,有必要flush()以应用新配置;
// 3.应用新配置之前,仍可通过旧的输入/输出格式安全地将数据入列和输出;
// 4.当配置发生变更,请调用queueEndOfStream()。
AudioFormat configure(AudioFormat inputAudioFormat) throws UnhandledAudioFormatException;
// 返回当前Processor是否是活跃的,并处理InputBuffer。
boolean isActive();
// 将音频数据通过 InputBuffer 入列以供处理。
void queueInput(ByteBuffer buffer);
// 将输入流标记为结束
// 调用getOutput()将返回所有剩余的输出数据,可能需要多次调用才能读取所有剩余的输出数据。一旦读取了所有剩余的输出数据,isEnded()将返回true。
void queueEndOfStream();
// 返回一个缓冲区,该缓冲区包含在其 position 和 limit 之间的已处理输出数据。
ByteBuffer getOutput();
// 返回当前Processor是否不再有数据的输出,直到其调用 flush() 且新的数据输入进来。
boolean isEnded();
// 清除所有缓冲的数据和挂起的输出。
// 若之后Processor仍处于活跃状态,还需准备一个最新格式配置的新输入流。
void flush();
// 重置并释放所有资源
void reset();
2. SonicAudioProcessor 流程分析
ExoPlayer
中,当用户针对音频进行倍速播放时,会在 DefaultAudiosink
中进行配置:
// DefaultAudioSink.java
public final class DefaultAudioSink implements AudioSink
public static class DefaultAudioProcessorChain implements AudioProcessorChain
// ...
private final SonicAudioProcessor sonicAudioProcessor;
// player.setSpeed() 最终执行该方法,通过 sonicAudioProcessor.setSpeed() 应用倍速配置
@Override
public PlaybackParameters applyPlaybackParameters(PlaybackParameters playbackParameters)
silenceSkippingAudioProcessor.setEnabled(playbackParameters.skipSilence);
return new PlaybackParameters(
sonicAudioProcessor.setSpeed(playbackParameters.speed),
sonicAudioProcessor.setPitch(playbackParameters.pitch),
sonicAudioProcessor.setVolume(playbackParameters.volume),
playbackParameters.skipSilence);
了解 SonicAudioProcessor
完整的工作流程有利于进一步理解 Sonic
,其内部包含专门处理变速变调的逻辑,这里我们只关注核心流程:
// SonicAudioProcessor.java
public final class SonicAudioProcessor extends AudioProcessor
private Sonic sonic;
private ByteBuffer buffer;
private ShortBuffer shortBuffer;
private ByteBuffer outputBuffer;
// 1. 设置倍速后进行标记,下次新的音频数据输入时,重建Sonic
public float setSpeed(float speed)
if (this.speed != speed)
this.speed = speed;
pendingSonicRecreation = true;
return speed;
// 2. 缓冲区数据清空,根据新的配置重建或释放Sonic
@Override
public void flush()
if (isActive())
if (pendingSonicRecreation)
// 2.1 重建对应音调、音速的Sonic
sonic = new Sonic(speed, ...);
else if (sonic != null)
// 2.2 重置sonic
sonic.flush();
// ...
// 3.只有当前音速、音调、声音至少有一项发生变更,SonicProcessor才会开始处理数据(活跃的)
public boolean isActive()
return pendingOutputAudioFormat.sampleRate != Format.NO_VALUE
&& (Math.abs(speed - 1f) >= 0.01f
|| Math.abs(pitch - 1f) >= 0.01f
|| Math.abs(volume - 1f) >= 0.01f
|| pendingOutputAudioFormat.sampleRate != pendingInputAudioFormat.sampleRate);
// 4.【重要】处理音频的InputBuffer,比如倍速处理
@Override
public void queueInput(ByteBuffer inputBuffer)
// ... 详细处理
// 5.返回音频的OutputBuffer,这个Buffer内已经是倍速完成后的音频数据了
@Override
public ByteBuffer getOutput()
ByteBuffer outputBuffer = this.outputBuffer;
this.outputBuffer = EMPTY_BUFFER;
return outputBuffer;
// 6.标记本次输入结束
@Override
public void queueEndOfStream()
if (sonic != null)
// 音频输入结束,这里强制将buffer中剩下的数据进行倍速处理并返回
// 该方法执行完成后,Sonic中buffer是干净的
sonic.queueEndOfStream();
inputEnded = true;
// 7.该方法返回true,表示AudioProcessor的本次处理结束
@Override
public boolean isEnded()
return inputEnded && (sonic == null || sonic.getOutputSize() == 0);
纵观整个音频处理的流程,读者可以确定,最重要的核心逻辑在 queueInput()
方法中,其内部包含了音频输入pcm
数据的变速和变调的逻辑:
// SonicAudioProcessor.java
@Override
public void queueInput(ByteBuffer inputBuffer)
if (inputBuffer.hasRemaining())
ShortBuffer shortBuffer = inputBuffer.asShortBuffer();
int inputSize = inputBuffer.remaining();
inputBytes += inputSize;
// 1. 将未处理的pcm数据输入sonic
sonic.queueInput(shortBuffer);
inputBuffer.position(inputBuffer.position() + inputSize);
// 2. 从sonic的buffer中获取变速后的outputSize,
// 创建一个空的shortBuffer用于接收变速后的pcm输出数据
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();
// 3.将变速后的数据输出到shortBuffer,outputBuffer最终由getOutput()方法返回
sonic.getOutput(shortBuffer);
outputBytes += outputSize;
buffer.limit(outputSize);
outputBuffer = buffer;
3.Sonic 分析
经过上述分析,可得出 Sonic
向外暴露的几个重要方法如下:
final class Sonic
// 1.将InputBuffer中的剩余数据加入自己的队列
public void queueInput(ShortBuffer buffer);
// 2.获取可用输出,输出到ShortBuffer中,buffer将从position位置输入数据
public void getOutput(ShortBuffer buffer);
// 3.获取可用输出的size
public int getOutputSize();
// 4.将已经入列的所有数据强制生成输出,不会在输出中导致额外的延迟,但有可能会导致失真
public void queueEndOfStream();
// 5.清除状态以准备接收新的InputBuffer
public void flush();
从关键的 API
可以看出,Sonic
内部处理也需要引入 数据缓冲区 保证同步机制以及避免音频失真,并提供 queueEndOfStream
、flush
方法响应 SonicAudioProcessor
对应方法的调用。
本文只针对核心的倍速算法流程进行分析,即 Sonic
的 queueInput
方法:
// Sonic.java
public void queueInput(ShortBuffer buffer)
// 1.计算buffer中数据对应的帧数和字节数
int framesToWrite = buffer.remaining() / channelCount;
int bytesToWrite = framesToWrite * channelCount * 2;、
// 2.保证自身的inputBuffer有足够的空间并输入数据
inputBuffer = ensureSpaceForAdditionalFrames(inputBuffer, inputFrameCount, framesToWrite);
buffer.get(inputBuffer, inputFrameCount * channelCount, bytesToWrite / 2);
inputFrameCount += framesToWrite;
// 3.处理数据数据
processStreamInput();
private void processStreamInput()
// ...
// 4.这里只关心倍速相关处理:
if (s > 1.00001 || s < 0.99999)
// 4.1 若变速,执行倍速算法
changeSpeed(s);
else
// 4.2 并未倍速,将数据原封不动输入到outputBuffer
copyToOutput(inputBuffer, 0, inputFrameCount);
inputFrameCount = 0;
// ...
从注释中可看出,changeSpeed()
方法就是核心的变速算法:
private void changeSpeed(float speed)
// ...
// 1.多次执行直至输入数据处理完毕
do
if (remainingInputToCopyFrameCount > 0)
positionFrames += copyInputToOutput(positionFrames);
else
// 2.核心方法,计算基音周期
int period = findPitchPeriod(inputBuffer, positionFrames);
// 3.核心方法,进行语音信号的合成,以达到倍速的效果
if (speed > 1.0)
positionFrames += period + skipPitchPeriod(inputBuffer, positionFrames, speed, period);
else
positionFrames += insertPitchPeriod(inputBuffer, positionFrames, speed, period);
while (positionFrames + maxRequiredFrameCount <= frameCount);
// ...
// 4.skipPitchPeriod()内部计算交给了overlapAdd()函数,不细讲
private int skipPitchPeriod(short[] samples, int position, float speed, int period)
// ...
overlapAdd(...);
return newFrameCount;
集成 SoundTouch
1. 编译so
和Sonic
提供了java
和C++
多平台实现不同,SoundTouch
只有 C++
的实现,因此需要通过CMake
编译生成so
文件,从而接入在Android
平台上。
对此笔者参考了 SoundTouchDemo 版本的代码,由于该仓库版本较旧,因此略微进行了更新,感兴趣的读者可以参考 soundtouch-android 这个仓库:
https://github.com/qingmei2/soundtouch-android
2. 定义 SoundTouch 类
从本质上讲,抛开语音信号处理的具体算法实现,Sonic
和 SoundTouch
在结构上以及思想上并无不同,都是内部维护 Buffer
并不断 接收输入 和 返回输出:
public class SoundTouch
static
System.loadLibrary("soundtouch");
// 1.初始化SoundTouch,需要传入音频流类型、声道数、采样率、采样大小、音频速度、音调等参数
private static synchronized native final void setup(int track, int channels, int samplingRate, int bytesPerSample, float tempo, float pitchSemi);
// 2.将pcm数据输入,参考Sonic#queueInput
private static synchronized native final void putBytes(int track, byte[] input, int length);
// 3.获取输出和输出大小,参考Sonic#getOutput 以及 Sonic#getOutputSize
private static synchronized native final int getBytes(int track, byte[] output, int toGet);
// 4.设置倍速
private static synchronized native final void setTempoChange(int track, float tempoChange);
// 5.参考Sonic#flush
private static synchronized native final void finish(int track, int bufSize);
读者只需关注核心逻辑,省略变调等其它实现,完整代码参考 这里 。
3.定义 SoundTouchAudioProcessor 类
整体逻辑梳理清楚后,即可依葫芦画瓢,定制对应的 SoundTouchAudioProcessor
实现了,限于篇幅,本文仅列出最重要的queueInput
方法的实现:
final class SoundTouchAudioProcessor
public void queueInput(ByteBuffer inputBuffer)
SoundTouch soundTouch = (SoundTouch)Assertions.checkNotNull(this.soundTouch);
new StringBuilder("");
byte[] input = new byte[0];
int outputSize;
if (inputBuffer.hasRemaining())
ShortBuffer shortBuffer = inputBuffer.asShortBuffer();
outputSize = inputBuffer.remaining();
this.inputBytes += (long)outputSize;
input = new byte[outputSize];
// 1.和Sonic不同,这里我们将InputBuffer中剩余的数据取出,存放到新的byte[]中
for(int i = 0; i < outputSize; ++i)
input[i] = inputBuffer.get(inputBuffer.position() + i);
// 2.将输入交给soundTouch
soundTouch.putBytes(input);
inputBuffer.position(inputBuffer.position() + outputSize);
byte[] output = new byte[4096];
// 3.获取输出,相比Sonic方便的是,SoundTouch#getBytes 将数据大小一并返回了
outputSize = soundTouch.getBytes(output);
if (outputSize > 0)
if (this.buffer.capacity() < outputSize)
this.buffer = ByteBuffer.allocateDirect(outputSize).order(ByteOrder.nativeOrder());
this.shortBuffer = this.buffer.asShortBuffer();
else
this.buffer.clear();
this.shortBuffer.clear();
// 4.最后和Sonic一样,将输出交给outputBuffer
this.buffer.put(Arrays.copyOf(output, outputSize));
this.outputBytes += (long)outputSize;
this.buffer.limit(outputSize);
this.buffer.position(0);
this.outputBuffer = this.buffer;
最终,我们成功实现了SoundTouchAudioProcessor
,并可根据业务需求,动态调整SonicAudioProcessor
和SoundTouchAudioProcessor
音频倍速处理的切换。
参考
本文部分文案节选自下述资料,有兴趣的读者可以进行针对性深入了解。
关于我
以上是关于Android 分场景集成不同音频倍速算法的实现的主要内容,如果未能解决你的问题,请参考以下文章