分享两个神经网络的超低音频码率 codec:
Lyra:
https://github.com/google/lyra
LpcNet:
https://github.com/xiph/LPCNet
这两个都是传统声码基础上使用神经网络进一步压缩音频,Lyra 是 google 开发的最低码率 3.2k,音频质量较好而且编译工具是谷歌官方的 bazel(直接支持android,源码使用了 tensorflow lite 推理加速)。LpcNet 码率是 1.6k 不过在嵌入式平台或者移动端有算力要求,音质也稍微差一些。经过我的各种尝试,对比目前市面常用的 amr-wb、amr-nb 或者 opus 等编码格式,想在超低码率下保证通话质量并且在 android 等嵌入式设备中使用 Lyra 比较不错,目前已经在对讲机、app 语音通话等产品中验证通过。
Lyra
下面是 gtihub 的介绍:
目前训练数据未开源,不过提供的 tflite 模型中文效果已经非常不错了。下面是编译流程如下,主要是 jdk、andorid sdk、bazel、python 等环境:
sudo apt install default-jdk
wget https://github.com/bazelbuild/bazel/releases/download/5.3.2/bazel-5.3.2-linux-x86_64
chmod +x bazel-5.3.2-linux-x86_64
sudo apt update
sudo apt upgrade -y
sudo apt install python3
sudo apt install python3-pip
pip3 install numpy -i https://pypi.tuna.tsinghua.edu.cn/simple/
wget https://dl.google.com/android/repository/commandlinetools-linux-7583922_latest.zip
unzip commandlinetools-linux-7583922_latest.zip
rm -rf commandlinetools-linux-7583922_latest.zip
cd cmdline-tools/
bin/sdkmanager --sdk_root=$HOME/android/sdk --install "platforms;android-30" "build-tools;30.0.3" "ndk;21.4.7075529"
然后编译示例工程,如果想要编译成 android 平台的只需要 --config 指定 android_arm64 即可(需要将测试文件、模型、可执行文件一起传到手机上):
# 编译
bazel build -c opt lyra/cli_example:encoder_main
bazel build -c opt lyra/cli_example:decoder_main
# 运行
bazel-bin/lyra/cli_example/encoder_main --input_path=lyra/testdata/sample1_16kHz.wav --output_dir=$HOME/temp --bitrate=3200
bazel-bin/lyra/cli_example/decoder_main --encoded_path=$HOME/temp/sample1_16kHz.lyra --output_dir=$HOME/temp/ --bitrate=3200
如果想把 lyra 封装成接口打包 so 给 andorid 使用就比较麻烦, 因为 bazel 编译工具 cc_library 不能把依赖的 tflite 编译进去(也许可以但是我没搞出来),后面发现这个问题在 issue 中很多人问然后有个兄弟提供了一个思路:直接在示例 android_example 中添加你需要的接口然后打包成 android_binary 从 apk 中获取需要的 so 动态库:
具体操作如下:
然后就可以在 jni_lyra_benchmark_lib.cc 使用 api了,新增三个函数即可,分别为编解码初始化、编码、解码函数:
Java_com_lyra_LyraCodec_codecInit
Java_com_lyra_LyraCodec_encode
Java_com_lyra_LyraCodec_decode
这样就导出了 lyra 的初始化、编码、解码三个函数,不过在 jni 使用的时候必须按照相应的全路径名称。然后编译 apk 并提取 so :
bazel build -c opt lyra/android_example:lyra_android_example --config=android_arm64 --copt=-DBENCHMARK
cd bazel-bin/lyra/android_example
然后就可以在自己的项目中愉快的使用 lyra 进行音频编解码了:
LpcNet
LPCNet 是一个 数字信号处理(DSP) 和 神经网络(NN)巧妙结合应用于语音合成中 vocoder 的工作,可以在普通的CPU上实时合成高质量语音。网络结构主要是 Frame rate 和 sample rate 加一个 lpc 计算(这部分要求样本强的自相关性所以模型输入的采样率必须是16k),算法内部不赘述,下面是编译过程:
编译脚本如下,我是在 wsl 中编译的:
./autogen.sh
./configure
make
其中 sutogen.sh 会下载,可以提前下载放到源码根目录就不会等待那么长:
https://media.xiph.org/lpcnet/data/lpcnet_data-2ddc476.tar.gz
MediaCodec
这个是 andorid 系统内置的音视频编解码(包含软件算法+硬件编解码+厂商提供的算法), android 设备碎片化严重不过一般的常用算法都支持, 2G/3G 时代的 arm-wb/nb 、opus、以及 raw 和无损压缩格式等等一般都有。使用比较简单,
codec 的配置:
package com.tyf.demo.codec;
import android.media.MediaCodec;
import android.media.MediaFormat;
import java.util.Arrays;
public class Config_Amr_nb implements Config{
@Override
public String getMime() {
return MediaFormat.MIMETYPE_AUDIO_AMR_NB;
}
@Override
public int getSampleRate() {
// AMR_NB 是固定的 8k采样率
return 8000;
}
@Override
public int getChannel() {
return 1;
}
@Override
public int getBitDepth() {
return 16;
}
@Override
public int getBitRate() {
// 8 种类
// 编码模式 编码名称 比特率 帧大小 帧头
// Mode 0 AMR 4.75 4.75 kbit/s 13 bytes 04 (00000100)
// Mode 1 AMR 5.51 5.15 kbit/s 14 bytes 0C (00001100)
// Mode 2 AMR 5.9 5.9 kbit/s 16 bytes 14 (00010100)
// Mode 3 AMR 6.7 6.7 kbit/s 18 bytes 1C (00011100)
// Mode 4 AMR 7.4 7.4 kbit/s 20 bytes 24 (00100100)
// Mode 5 AMR 7.95 7.95 kbit/s 21 bytes 2C (00101100)
// Mode 6 AMR 10.2 10.2 kbit/s 27 bytes 34 (00110100)
// Mode 7 AMR 12.2 12.2 kbit/s 32 bytes 3C (00111100)
int i = 0;
return Arrays.asList(4750,5150,5900,6700,7400,7950,10200,12200).get(i);
}
@Override
public double frameTs() {
return 20;
}
@Override
public double frameSize() {
// 根据比特率计算 20 ms 音频压缩之后的大小
// 20 ms 就是 50 分之 1 秒、比特率除 50 得到比特数、再除 8 得到字节数
double lenAfterEncodeWith20ms = Double.valueOf(Double.valueOf(getBitRate()) / 8d / (1000d / frameTs()));
return lenAfterEncodeWith20ms;
}
@Override
public MediaCodec getEncoder() {
MediaCodec encoder = null;
try {
encoder = MediaCodec.createEncoderByType(getMime());
MediaFormat encodeFormat = MediaFormat.createAudioFormat(getMime(), getSampleRate(), getChannel());
encodeFormat.setInteger(MediaFormat.KEY_BIT_RATE, getBitRate());
encodeFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE,10*1024);
encoder.configure(encodeFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
}
catch (Exception e){
e.printStackTrace();
}
return encoder;
}
@Override
public MediaCodec getDecoder() {
MediaCodec decoder = null;
try {
decoder = MediaCodec.createDecoderByType(getMime());
MediaFormat decodeFormat = MediaFormat.createAudioFormat(getMime(), getSampleRate(), getChannel());
decodeFormat.setInteger(MediaFormat.KEY_BIT_RATE, getBitRate());
decoder.configure(decodeFormat, null, null, 0);
}
catch (Exception e){
e.printStackTrace();
}
return decoder;
}
}
codec 的使用:
package com.tyf.demo.utils;
import android.media.MediaCodec;
import com.tyf.demo.MainActivity;
import com.tyf.demo.codec.Config;
import java.nio.ByteBuffer;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicBoolean;
// 编解码器
public class CodecRunner {
MainActivity activity;
// 编码器和解码器
MediaCodec encoder;
MediaCodec decoder;
public CodecRunner(MainActivity activity, Config config){
this.activity = activity;
this.encoder = config.getEncoder();
this.decoder = config.getDecoder();
activity.Log("--------------");
if(isInit()){
activity.Log("Mime:"+config.getMime());
activity.Log("SampleRate:"+config.getSampleRate());
activity.Log("BitRate:"+config.getBitRate());
activity.Log("BitDepth:"+config.getBitDepth());
activity.Log("frameTs:"+config.frameTs()+" ms");
activity.Log("frameSize:"+config.frameSize()+" byte");
activity.Log("Encoder:"+this.encoder.getName());
activity.Log("Decoder:"+this.decoder.getName());
}
activity.Log("Coder Inited:"+isInit());
activity.Log("--------------");
}
public boolean isInit(){
return this.encoder!=null&&this.decoder!=null;
}
// 开始编解码
public void start(BlockingQueue<byte[]> recordQueue, BlockingQueue<byte[]> playQueue,BlockingQueue<byte[]> encodeQueue,BlockingQueue<byte[]> decodeQueue,AtomicBoolean talk){
// 开始编码 [ recordQueue => encodeQueue ]
Executors.newSingleThreadExecutor().submit(() -> {
activity.Log("开始编码..");
try {
encoder.start();
while (talk.get()) {
byte[] data = recordQueue.take();
int inputIndex = encoder.dequeueInputBuffer(-1);
if (inputIndex >= 0) {
ByteBuffer inputBuffer = encoder.getInputBuffer(inputIndex);
inputBuffer.clear();
inputBuffer.put(data);
encoder.queueInputBuffer(inputIndex, 0, data.length, System.nanoTime() / 1000, 0);
}
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
int outputIndex = encoder.dequeueOutputBuffer(bufferInfo, 0);
while (outputIndex >= 0) {
ByteBuffer outputBuffer = encoder.getOutputBuffer(outputIndex);
byte[] encodedData = new byte[bufferInfo.size];
outputBuffer.get(encodedData);。
encodeQueue.put(encodedData);
encoder.releaseOutputBuffer(outputIndex, false);
outputIndex = encoder.dequeueOutputBuffer(bufferInfo, 0);
}
}
encoder.stop();
encoder.release();
activity.Log("停止编码..");
} catch (Exception e) {
e.printStackTrace();
activity.Log("编码错误:"+e.getMessage()==null?e.getCause().toString():e.getMessage()); // 记录编码错误
}
});
// 开始解码 [ decodeQueue => playQueue ]
Executors.newSingleThreadExecutor().submit(() -> {
try {
decoder.start();
while (talk.get()) {
byte[] encodedData = decodeQueue.take();
int inputIndex = decoder.dequeueInputBuffer(10000);
if (inputIndex >= 0) {
ByteBuffer inputBuffer = decoder.getInputBuffer(inputIndex);
inputBuffer.clear(); // 清除缓冲区
inputBuffer.put(encodedData);
decoder.queueInputBuffer(inputIndex, 0, encodedData.length, System.nanoTime() / 1000, 0);
}
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
int outputIndex = decoder.dequeueOutputBuffer(bufferInfo, 10000);
while (outputIndex >= 0) {
ByteBuffer outputBuffer = decoder.getOutputBuffer(outputIndex);
byte[] decodedData = new byte[bufferInfo.size];
outputBuffer.get(decodedData); // 将数据从输出缓冲区读入数组
playQueue.put(decodedData);
activity.Log("Decode Len:"+encodedData.length+"=> "+decodedData.length);
decoder.releaseOutputBuffer(outputIndex, false);
outputIndex = decoder.dequeueOutputBuffer(bufferInfo, 0);
}
}
decoder.stop();
decoder.release();
activity.Log("停止解码..");
} catch (Exception e) {
e.printStackTrace();
}
});
// 模拟网络传输 [ encodeQueue => decodeQueue ]
Executors.newSingleThreadExecutor().submit(()->{
activity.Log("模拟传输..");
try {
while (talk.get()) {
byte[] encodedData = encodeQueue.take();
decodeQueue.put(encodedData);
}
} catch (Exception e) {
e.printStackTrace();
}
});
}
}