G.711 编解码原理

G.711 是 ITU-T 定制出来的一套语音压缩标准,它代表了对数 PCM(logarithmic pulse-code modulation)抽样标准,是主流的波形声音编解码标准,主要用于电话。

G.711 编码采用 8kHz 采样率,有 A-law 和μ-law 两种编码方式,分别是将 13bit 和 14bit 编码为 8bit,因此 G711 固定码率是 8kHz×8bit=64kbps。两者都是对数变换,A-law 更加方便计算机处理。μ-law 提供了略微高一些的动态范围,但代价是对于弱信号的量化误差相对 A-law 高一些。

转换公式

下面分别介绍两种算法的转换公式。

A-law:

一般采用 A=87.6, 画出图来则是如下图,用 x 表示输入的采样值,F(x) 表示通过 A-law 变换后的采样值,y 是对 F(x) 进行量化后的采样值。

image-20201130144420043

由此可见在输入的 x 为高值的时候,F(x) 的变化是缓慢的,有较大范围的 x 对应的 F(x) 最终被量化为同一个 y,精度较低。相反在低声强区域,也就是 x 为低值的时候,F(x) 的变化很剧烈,有较少的不同 x 对应的 F(x) 被量化为同一个 y。意思就是说在声音比较小的区域,精度较高,便于区分,而声音比较大的区域,精度不是那么高。

μ-law 的公式如下,μ取值一般为 255

和 A-law 画在同一个坐标轴中就能发现 A-law 在低强度信号下,精度要稍微高一些。

image-20201130144536867

实际应用中,采用浮点数计算的方式计算,然后进行量化,计算量会比较大,实际上对于 A-law(A=87.6 时),是采用 13 折线近似的方式来计算的,而μ-law(μ=255 时)则是 15 段折线近似的方式。

A-law 如下表计算,第一列是采样点,共 13bit,最高位为符号位。对于前两行,折线斜率均为 1/2,跟负半段的相应区域位于同一段折线上,对于 3 到 8 行,斜率分别是 1/4 到 1/128,共 6 段折线,加上负半段对应的 6 段折线,总共 13 段折线,这就是所谓的 A-law 十三段折线法

解码公式:

相应的μ-law 的计算方法如下表。

本质上跟 A-law 的区别不大

16bitPCM 转 alaw 代码

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
// pcm2alaw.c
// https://blog.csdn.net/wzying25/article/details/79398055
// @sun 2020.06.23
#include "stdio.h"

#define SIGN_BIT (0x80) /* Sign bit for a A-law byte. */
#define QUANT_MASK (0xf) /* Quantization field mask. */
#define NSEGS (8) /* Number of A-law segments. */
#define SEG_SHIFT (4) /* Left shift for segment number. */
#define SEG_MASK (0x70) /* Segment field mask. */

static short seg_end[8] = {
0xFF, 0x1FF, 0x3FF, 0x7FF, 0xFFF,
0x1FFF, 0x3FFF, 0x7FFF}; //分成不均匀的8个分段,算上负数,总共是16个分段

static int search(int val, short *table, int size) {
int i;
for (i = 0; i < size; i++) {
if (val <= *table++) return (i);
}
return (size);
}

/*********************************************************************
* 输入参数范围 :-32768~32767
* 返回8位无符号整数
* linear2alaw() - Convert a 16-bit linear PCM value to 8-bit A-law
* For further information see John C. Bellamy's Digital Telephony, 1982,
* John Wiley & Sons, pps 98-111 and 472-476.
*********************************************************************/
unsigned char linear2alaw(short int pcm_val) /* 2's complement (16-bit range) */
{
int mask;
int seg;
unsigned char aval;

//这里右移3位,因为采样值是16bit,而A-law是13bit,存储在高13位上,低3位被舍弃
pcm_val = pcm_val >> 3;

if (pcm_val >= 0) {
mask = 0xD5; /* sign (7th) bit = 1 二进制的11010101*/
} else {
mask = 0x55; /* sign bit = 0 二进制的01010101*/
pcm_val = -pcm_val - 1; //负数转换为正数计算
}

/* Convert the scaled magnitude to segment number. */
seg = search(pcm_val, seg_end, 8); //返回pcm_val属于哪个分段

/* Combine the sign, segment, and quantization bits. */

if (seg >= 8) /* out of range, return maximum value. */
return (0x7F ^ mask);
else {
aval = seg << SEG_SHIFT;
// aval为每一段的偏移,分段量化后的数据需要加上该偏移(aval)
//分段量化
//量化方法: (pcm_val-分段值),然后取有效的高4位 (0分段例外)
//比如 pcm_val = 0x7000 ,那么seg=7 ,第7段的范围是0x4000~0x7FFF
//,段偏移aval=7<<4=0x7F 0x7000-0x4000=0x3000
// ,然后取有效的高4位,即右移10(seg+3),0x3000>>10=0xC
//上一步等效为:(0x7000>>10)&0xF=0xC 。也就是: (pcm_val >> (seg + 3)) &
// QUANT_MASK 然后加上段偏移 0x7F(aval) ,加法等效于或运算,即 |aval

if (seg < 2)
aval |= (pcm_val >> 4) & QUANT_MASK; // 0、1段折线的斜率一样
else
aval |= (pcm_val >> (seg + 3)) & QUANT_MASK;
return (
aval ^
mask); //异或0x55,目的是尽量避免出现连续的0,或连续的1,提高传输过程的可靠性
}
}
1
2
3
4
5
6
7
// pcm2alaw.h
// @sun 2020.06.23

#pragma once
#include "stdio.h"

unsigned char linear2alaw(int pcm_val);
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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
/***********************************************************************
* @Copyright (C) 2020 all right reserved
* @file main.c
* @ingroup wav
* @author SunZhenliang
* @date 2020-06
* @brief pcm2alaw
***********************************************************************/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "pcm2alaw.h"

typedef struct WAV_HEADER {
char ChunkID[4]; // "RIFF"
unsigned int ChunkSize; // 文件长度(WAVE文件的大小, 不含前8个字节)
char Format[4]; // "WAVE"

char SubChunk1ID[4]; // "fmt "
unsigned int SubChunk1Size; // 过渡字节(不定)
unsigned short int AudioFormat; // 格式类别(10H为PCM格式的声音数据)
unsigned short int NumChannels; // 通道数(单声道为1, 双声道为2)
unsigned int SampleRate; // 采样率
unsigned int ByteRate; // 波形音频数据传输速率
unsigned short int BlockAlign; // data数据块长度、字节
unsigned short int BitsPerSample; // PCM 位宽

char SubChunk2ID[4]; // "data"
unsigned int SubChunk2Size; // data数据的长度
} WAV_HEADER;

typedef struct ALAW_HEADER {
char ChunkID[4]; // "RIFF"
unsigned int ChunkSize; // 文件长度(WAVE文件的大小, 不含前8个字节)
char Format[4]; // "WAVE"

char SubChunk1ID[4]; // "fmt "
unsigned int SubChunk1Size; // 过渡字节(不定)
unsigned short int AudioFormat; // 格式类别(10H为PCM格式的声音数据)
unsigned short int NumChannels; // 通道数(单声道为1, 双声道为2)
unsigned int SampleRate; // 采样率
unsigned int ByteRate; // 波形音频数据传输速率
unsigned short int BlockAlign; // data数据块长度、字节
unsigned short int BitsPerSample; // PCM 位宽

char SubChunk3ID[4]; // "fact"
unsigned int SubChunk3Size; // fact数据的长度
unsigned int SampleLength; // data数据的长度

char SubChunk2ID[4]; // "data"
unsigned int SubChunk2Size; // data数据的长度
} ALAW_HEADER;

int main(int argc, char *argv[]) {
FILE *fpin = NULL;
FILE *fout = NULL;
WAV_HEADER wav;
ALAW_HEADER alaw;
char tmp;
short int pcm_val;

fpin = fopen("./M1F1-int16-AFsp.wav", "rb");
fread(&wav, sizeof(struct WAV_HEADER), 1, fpin);
fout = fopen("./8bitalaw.wav", "w+b");

// WAV_HEADER
printf("ChunkID=%c%c%c%c\n", wav.ChunkID[0], wav.ChunkID[1], wav.ChunkID[2],
wav.ChunkID[3]);
printf("ChunkSize=%d\n", wav.ChunkSize);
printf("Format=%c%c%c%c\n", wav.Format[0], wav.Format[1], wav.Format[2],
wav.Format[3]);
printf("SubChunk1ID=%c%c%c%c\n", wav.SubChunk1ID[0], wav.SubChunk1ID[1],
wav.SubChunk1ID[2], wav.SubChunk1ID[3]);
printf("SubChunk1Size=%d\n", wav.SubChunk1Size);
printf("AudioFormat=%d\n", wav.AudioFormat);
printf("NumChannels=%d\n", wav.NumChannels);
printf("SampleRate=%d\n", wav.SampleRate);
printf("ByteRate=%d\n", wav.ByteRate);
printf("BlockAlign=%d\n", wav.BlockAlign);
printf("BitsPerSample=%d\n", wav.BitsPerSample);
printf("SubChunk2ID=%c%c%c%c\n", wav.SubChunk2ID[0], wav.SubChunk2ID[1],
wav.SubChunk2ID[2], wav.SubChunk2ID[3]);
printf("SubChunk2Size=%d\n", wav.SubChunk2Size);

// Get alaw header
strcpy(alaw.ChunkID, wav.ChunkID);
alaw.ChunkSize = (wav.ChunkSize - 36) / 2 + 48;
strcpy(alaw.Format, wav.Format);
strcpy(alaw.SubChunk1ID, wav.SubChunk1ID);
alaw.SubChunk1Size = wav.SubChunk1Size;
alaw.AudioFormat = wav.AudioFormat;
alaw.NumChannels = wav.NumChannels;
alaw.SampleRate = wav.SampleRate;
alaw.ByteRate = wav.ByteRate;
alaw.BlockAlign = wav.BlockAlign;
alaw.BitsPerSample = wav.BitsPerSample;

strcpy(alaw.SubChunk3ID, "fact");
alaw.SubChunk3Size = 4;
alaw.SampleLength = (wav.ChunkSize - 36) / 2;

strcpy(alaw.SubChunk2ID, wav.SubChunk2ID);
alaw.SubChunk2Size = (wav.ChunkSize - 36) / 2;

// write into 8bitalaw.wav
fwrite(&alaw, sizeof(ALAW_HEADER), 1, fout);
while (1 == fread(&pcm_val, sizeof(short int), 1, fpin)) {
tmp = linear2alaw(pcm_val); // 16bit in, 8bit out
fputc(tmp, fout);
}

fclose(fpin);
fclose(fout);
return 0;
}

总结

G.71 的原理和计算也比较简单,但是其中用到的一些基本原理同样在其他编码算法中得到了应用,对其进行深入的了解有助于更好的理解其他的算法。

源代码中关于移位运算,掩码运算,还不是完全的理解,只能根据自己的经验进行一些猜测,之后会继续学习,希望对这方面能有更深入的认识。

参考资料