[.NET] Speex 语音编解码介绍, 使用, 代码示例

Speex 是一个开源的, 适合语音编解码的算法, 常应用于网络电话中.在下面的的介绍中, 我们将使用对 Speex 编码在 .NET 中的使用做介绍SpeexSharp 可以在 nuget 中直接安装, 并且已经封装了编解码器的类供使用. 如果你不希望了解 Speex 的具体编解码过程, 可以忽略下面的 ‘编码’ 和 ‘解码’ 部分, 只看 Speex 的介绍, 然后直接使用这些类进行编解码.

Speex 是一个开源的, 适合语音编解码的算法, 常应用于网络电话中.

在下面的的介绍中, 我们将使用 SpeexSharp 对 Speex 编码在 .NET 中的使用做介绍

SpeexSharp 可以在 nuget 中直接安装, 并且已经封装了编解码器的类供使用. 如果你不希望了解 Speex 的具体编解码过程, 可以忽略下面的 ‘编码’ 和 ‘解码’ 部分, 只看 Speex 的介绍, 然后直接使用这些类进行编解码.


采样

Speex 的编解码是基于采样的, 传入数据的时候, 我们需要给定采样, 传出的时候, Speex 也是解码为采样.

Speex 支持的采样格式有两种, 浮点数和有符号 16 位整数.


模式和质量

Speex 目前有三种模式, 窄带, 宽带, 超宽带. 这三种模式中, 对音频数据编码后的数据大小是不一样的. 如其名, 在窄带模式下, 音频编码后最小, 质量也最低, 反之, 超宽带是编码后最大, 质量最好的模式.

选择好模式之后, 你还可以对编码质量进行微调. 质量的等级是一个从 0 到 10 的值(包含 0 和 10), 设置编码器的质量之后, 编码的结果大小和质量也会变更.


存储流

要进行 Speex 编码, 我们需要一个存储需要编码的数据的缓冲区, Speex 已经为我们准备好了这个类型, 并且 Speex 会自动管理这个缓冲区. 它叫做 SpeexBits.

初始化一个 SpeexBits, 我们需要声明一个 SpeexBits 类型的变量, 然后调用 Speex 的初始化函数.

1
2
SpeexBits bits;
Speex.BitsInit(&bits);

无论是编码还是解码, 都是需要 SpeexBits 作为存储的.

编码时, 用户将采样数据的指针传给编码函数, 函数内部对帧进行编码, 最后将编码后的结果放入 SpeexBits 中.

解码时则是用户将需要解码的数据放入 SpeexBits, 将输出数据的缓冲区传给解码函数, 解码函数从中读取, 解码数据, 然后将解码后的数据写入用户指定的缓冲区中.

另外, SpeexBits 之所以叫 Bits, 是因为它其中存储的数据, 基本单位是比特. 在编码时, 你可能会得到 69.5 个字节. 也就是 556 个比特. 在这种情况下, 我们要存储它的数据时, 肯定是要向上舍入, 也就是存储它内部的 70 个字节.


帧大小与采样率

Speex 的编解码是对于 “帧” 而言的. 每一次编码, 都必须是一个完整的帧, 即一定数量的采样数. 而帧的大小取决于上面提到的 Speex 编码模式.

而且, 在编码的时候, 传入采样的采样率也应该与编码器设定的采样率一致. 这样才能获得最好的编码效果.

数据 \ 模式窄带宽带超宽带
帧大小160320640
默认采样率8000Hz16000Hz32000Hz

注意, 帧大小是针对 ‘采样数量’ 的, 例如, 如果你要以宽带模式编码浮点数采样, 那么你需要 320 个浮点采样, 每个 4 字节, 总共需要 1280 个字节. 如果是带符号 16 位整数则是需要 640 个字节.

如果你希望将使用 Speex 编码的语音存储到文件, 你可能需要做一些处理. 因为 Speex 的编解码是针对于帧的, 所以你的文件中至少需要有标识帧的地方. 在解码时, 读取一帧, 然后调用解码方法, 得到原始采样.

最简单的方式就是在每一帧的前面加一字节的头, 这个字节用来标识后面多少字节是一帧.


编码过程

要进行完整的编码, 需要进行以下大概步骤

  1. 准备用于存储的 SpeexBits
  2. 初始化一个编码器
  3. 调用编码方法
  4. 从 SpeexBits 中读取编码结果

下面是宽带模式编码的示例代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// 要进行编码的采样数据
float[] samples;

// 取 320 个采样, 也就是宽带模式下的一帧
float[] frame = samples.Take(320).ToArray();

// 定义并初始化用于存储的 SpeexBits
SpeexBits bits;
Speex.BitsInit(&bits);

// 获取表示宽带模式的指针, 0, 1, 2 分别是窄带, 宽带, 超宽带
SpeexMode* mode = Speex.LibGetMode(1);

// 初始化编码器, 得到表示编码器状态的指针
void* encoderState = Speex.EncoderInit(mode);

// 将数组转为指针
fixed (float* framePtr = frame)
{
    // 重置 SpeexBits 内容
    Speex.BitsReset(&bits);

    // 调用编码方法
    Speex.Encode(encoderState, framePtr, &bits);
}

// 获取编码后的数量 (也就是 bits 中存储的字节数)
int bitCount = bits.BitCount;
int byteCount = (bits.BitCount + 7) >> 3   // 向上舍入

// 声明一个缓冲区用于存储编码后结果
byte[] buffer = new byte[byteCount];

// 固定缓冲区, 转为指针
fixed (byte* bufferPtr = buffer)
{
    // 将 Bits 内存储的编码结果写入到我们自己的缓冲区中
    Speex.BitsWrite(&bits, bufferPtr, buffer.Length);
}

// 做其他处理.

需要注意的是, 每一次编码之后, 你都应该重置一下 SpeexBits

因为编码方法的结果再往 SpeexBits 存入时, 如果没有抹除旧的数据, SpeexBits 中就会同时存储着旧的数据和新的数据, 如果你没有手动往 SpeexBits 里面写入一些东西做标识, 那么你就无法区分不同的帧了.

最简单的方式就是, 每一次编码后, 读取编码结果, 然后清空 SpeexBits.

如果你需要将所有采样都编码了, 很简单, 只需要用 for 进行循环就好了. 但在这之前, 你还需要对原始的采样做填充处理, 确保它的大小是帧大小的整数倍, 这样你在做编码的时候, 就不会出现访问冲突的问题了.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
float[] samples;

int padLength = samples.Length % 360;
if (padLength != 0)
    padLength = 360 - padLength;

float[] paddedSamples = new [samples.Length + padLength];
Array.Copy(samples, 0, paddedSamples, 0, samples.Length);

fixed (float* paddedSamplesPtr = paddedSamples)
{
    for (int i = 0; i < paddedSamples.Length; i += 360)
    {
        Speex.BitsReset(&bits);
        Speex.Encode(encoderState, paddedSamplesPtr + i, &bits);
    }
}

解码过程

解码同样很简单, 只需要我们将已经编码的一帧以及输出缓冲区传入到 Decode 函数中, Speex 就会将解码后的一帧存入到缓冲区中.

  1. 准备用于存储的 SpeexBits
  2. 初始化一个解码器
  3. 将需要解码的帧存入到 SpeexBits 中
  4. 调用解码方法

同样的, 解码的时候也是逐帧解码的, 你传入的缓冲区至少能容纳一帧的音频才可以.

下面是宽带模式解码的示例代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 要进行解码的 Speex 数据
byte[] speex;

// 声明用于存储解码结果的缓冲区
float[] buffer = new float[320];

// 定义并初始化用于存储的 SpeexBits
SpeexBits bits;
Speex.BitsInit(&bits);

// 获取表示宽带模式的指针
SpeexMode* mode = Speex.LibGetMode(1);

// 初始化解码器
void* decoderState = Speex.DecoderInit(mode);

// 固定 Speex 数据
fixed (byte* speexPtr = speex)
{
    // 将其读入到 SpeexBits 中
    Speex.BitsReadFrom(&bits, speexPtr, speex.Length);
}

// 固定缓冲区
fixed (float* bufferPtr = buffer)
{
    // 进行解码
    Speex.Decode(decoderState, &bits, bufferPtr);
}

// 做其他处理

需要注意的是, 在解码时, 读入 SpeexBits 的数据只能是一帧, 如果你存入两帧或者更多的话, 那么解码会出问题.

如果你存入了半个帧或者数据损坏的一个帧, 解码仍然能成功, 只不过输出结果的质量会下降.


托管调用

以上, 我们已经了解了 Speex 编解码的具体过程, 但 SpeexSharp 还提供了编解码器的类封装, 内部会自动初始化 SpeexBits, 存取编解码数据等.

如果要使用这些封装好的类来进行编解码, 只需要新建 SpeexEncoder 或 SpeexDecoder 的示例即可.

Built with Hugo
主题 StackJimmy 设计