低音频码率 codec

分享两个神经网络的超低音频码率 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();
            }
        });

    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

0x13

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值