简介:这份资源聚焦数字音频通信中的G.711编码与RTP实时传输协议结合应用,面向VoIP开发初学者、嵌入式音频工程师及流媒体协议学习者,帮助解决将G.711裸音频数据封装为RTP包并通过网络传输、最终由VLC成功接收播放的完整链路问题。压缩包共4个文件,包含C语言源码、SDP会话描述文件、G.711音频样本及说明文档,整体约2.16MB,覆盖从编码数据读取、RTP头构造到会话描述配置的关键环节。已有1486人学习下载,说明其在实时音频传输入门场景中具备一定参考价值。读者可借助源码理解RTP序列号、时间戳与负载封装逻辑,通过SDP文件掌握媒体会话协商方式,并利用样本音频与说明文档快速搭建可验证的传输实验,为VoIP或实时音频流服务开发打下实践基础。1. 从一段“能打通但没声音”的语音链路说起前阵子帮一个做对讲功能的朋友排查问题,现象很典型:信令通了,SDP 也协商成功了,RTP 包在 Wireshark 里一个接一个地飞,但两端就是听不到声音。抓包一看,payload 里全是0xD5 0xAA 0xD5 0xAA这种规律性极强的字节——典型的 G.711 编码后没做 RTP 封装,或者时间戳、序列号乱填导致的。这个场景几乎每个做实时语音的开发者都会撞上一次,而 g711 封装 rtp 传输恰好是绕不开的基本功。这份资源就是围绕这件事展开的:把 PCM 采样数据用 G.711(A-law 或 μ-law)压成 8bit,再按 RFC 3550 的格式塞进 RTP 包,通过 UDP 发出去,接收端反向解包还原成 PCM 播放。它适合正在做 VoIP、对讲机、语音网关、SIP 软电话、AI 语音对话链路的开发者,尤其是那些“协议栈用现成的、但底层封装想自己掌控”的人。下面我按“这东西怎么跑起来 → 核心封装怎么写的 → 怎么改”的顺序拆一遍。2. 项目结构与运行链路:从 PCM 到 RTP 包的完整路径2.1 目录结构与模块职责这类 g711 over rtp 的项目,结构通常不会太复杂,核心就四块:编码器、RTP 打包器、UDP 收发、解码播放。我拿到的这份代码目录大致是这样组织的:g711_rtp/ ├── include/ │ ├── g711.h # A-law / μ-law 编解码接口 │ ├── rtp_packet.h # RTP 头结构体与打包/解包函数 │ └── audio_device.h # 音频采集与播放抽象 ├── src/ │ ├── g711.c # 编解码查表实现 │ ├── rtp_packet.c # RTP 头填充、序列号与时间戳管理 │ ├── udp_socket.c # socket 创建、绑定、收发 │ └── main.c # 主循环:采集→编码→打包→发送 ├── test/ │ └── loopback_test.c # 本地回环测试 └── Makefileg711.c负责把 16bit 线性 PCM 压成 8bit,rtp_packet.c负责加 12 字节的 RTP 固定头,udp_socket.c只管搬运,main.c把这几步串成流水线。这种分层的好处是:你想换 Opus 或者 G.722,只需要替换编码模块,RTP 打包和 UDP 传输那层几乎不用动。2.2 编译与运行:三条命令跑通回环先确认依赖。这类项目一般只依赖标准 C 库和 POSIX socket,Linux 下直接 make 就行。如果涉及音频设备,可能会用到 ALSA 的libasound2-dev。# 安装音频开发库(Debian/Ubuntu 系) sudo apt-get install libasound2-dev # 编译 make # 运行回环测试:本机 5004 端口自发自收 ./loopback_test 127.0.0.1 5004loopback_test的逻辑是:从麦克风采集 20ms 的 PCM(8kHz 采样、16bit、单声道,正好 160 个采样点),编码成 G.711 后打成 RTP 包,发到指定地址,同时另一个线程在同一端口收包、解包、解码、播放。如果你听到自己的声音有延迟但清晰,说明整条链路是通的。提示:如果运行时报cannot open audio device,先确认当前用户是否在audio组里,或者直接用arecord -l看声卡有没有被识别。2.3 关键参数:采样率、打包间隔与时间戳增量G.711 本身不定义采样率,但 RTP 的时间戳增量必须和采样率对齐。这是最容易翻车的地方。常见配置如下:参数典型值说明采样率8000 Hz电话语音标准,G.711 的默认场景打包间隔20 ms一个 RTP 包承载 20ms 音频每包采样数1608000 × 0.02 = 160时间戳增量160每个 RTP 包时间戳加 160编码格式PCMA / PCMUA-law 对应 payload type 8,μ-law 对应 0时间戳增量写错,接收端的 jitter buffer 就会算错播放时刻,表现就是声音断断续续或者变调。我见过有人把增量写成 1,结果接收端以为每个包只隔 1/8000 秒,播放速度快了 160 倍,直接变成刺耳噪音。3. 核心封装拆解:G.711 编码与 RTP 头填充的代码实现3.1 G.711 编码:查表法比实时计算快一个数量级G.711 的 A-law 和 μ-law 都是对数压缩,直接按公式算会涉及浮点运算和对数,在嵌入式或高并发场景下不划算。常见做法是预生成一张 65536 项的查找表,把 16bit PCM 直接映射到 8bit 码字。// g711.c —— A-law 编码查表实现 #include "g711.h" static unsigned char alaw_encode_table[65536]; static int table_ready = 0; // 生成 A-law 编码表:输入 16bit 线性 PCM,输出 8bit A-law 码 static void build_alaw_table(void) { for (int i = 0; i < 65536; i++) { short pcm = (short)i; int sign = (pcm >= 0) ? 0x80 : 0x00; // 符号位 int magnitude = (pcm >= 0) ? pcm : -pcm; if (magnitude > 32767) magnitude = 32767; int exponent = 7; int mask = 0x4000; while (exponent > 0 && !(magnitude & mask)) { exponent--; mask >>= 1; } int mantissa = (magnitude >> (exponent + 3)) & 0x0F; alaw_encode_table[i] = (unsigned char)(sign | (exponent << 4) | mantissa); } table_ready = 1; } unsigned char pcm_to_alaw(short pcm) { if (!table_ready) build_alaw_table(); return alaw_encode_table[(unsigned short)pcm]; }这段代码的关键点有三个。第一,符号位单独处理,A-law 规定正数符号位为 1、负数为 0,和直觉相反,写反了声音会“发闷”。第二,指数位从 7 往下找最高有效位,决定压缩的段位。第三,尾数取指数位之后的 4 位,构成最终的 8bit。查表法把整个编码过程压成一次数组访问,实测比实时计算快 8 到 10 倍,在 8kHz 单声道下 CPU 占用几乎可以忽略。μ-law 的实现结构一样,只是符号位和段位映射不同,pcm_to_ulaw里把符号位改成(pcm >= 0) ? 0xFF : 0x7F,再按 μ-law 的段表处理即可。3.2 RTP 头填充:12 字节里每个字段都不能拍脑袋RTP 固定头是 12 字节,字段不多,但每个都有讲究。下面是一个典型的打包函数:// rtp_packet.c —— 填充 RTP 头并附加 G.711 payload #include <string.h> #include <arpa/inet.h> #include "rtp_packet.h" // 打包:payload 是已编码的 G.711 数据,len 是字节数 int rtp_pack(uint8_t *buf, const uint8_t *payload, int len, uint16_t seq, uint32_t timestamp, uint32_t ssrc, uint8_t payload_type) { rtp_header_t *hdr = (rtp_header_t *)buf; hdr->version = 2; // RTP 版本固定为 2 hdr->padding = 0; hdr->extension = 0; hdr->csrc_count = 0; hdr->marker = 0; // 语音包通常不置位,除非是静音后的第一个包 hdr->payload_type = payload_type; // PCMA=8, PCMU=0 hdr->seq = htons(seq); hdr->timestamp = htonl(timestamp); hdr->ssrc = htonl(ssrc); memcpy(buf + 12, payload, len); return 12 + len; }逐字段说明:version必须是 2,写 1 的话对端直接丢弃;payload_type决定对端用哪种解码器,PCMA 是 8、PCMU 是 0,协商阶段 SDP 里写的就是这个值;seq每发一个包加 1,接收端靠它检测丢包和乱序;timestamp每个包加 160(20ms × 8000Hz),接收端靠它做 jitter buffer 和播放调度;ssrc是同步源标识,同一路音频流保持不变,多路混流时靠它区分。所有多字节字段都要用htons/htonl转成网络字节序,这是血泪经验——在小端机器上忘了转,对端解析出来的序列号会大得离谱。3.3 发送循环:20ms 一个节拍,序列号和时间戳同步推进主循环的节奏感很重要。G.711 over RTP 通常按固定间隔发包,20ms 是最常见的打包周期。// main.c —— 采集、编码、打包、发送的主循环 uint16_t seq = 0; uint32_t timestamp = 0; uint32_t ssrc = 0x12345678; const int SAMPLES_PER_PACKET = 160; // 20ms @ 8kHz while (running) { short pcm_buf[SAMPLES_PER_PACKET]; int samples = audio_capture(pcm_buf, SAMPLES_PER_PACKET); if (samples <= 0) continue; uint8_t g711_buf[SAMPLES_PER_PACKET]; for (int i = 0; i < samples; i++) { g711_buf[i] = pcm_to_alaw(pcm_buf[i]); } uint8_t rtp_buf[12 + SAMPLES_PER_PACKET]; int pkt_len = rtp_pack(rtp_buf, g711_buf, samples, seq, timestamp, ssrc, 8); udp_send(rtp_buf, pkt_len); seq++; timestamp += SAMPLES_PER_PACKET; // 时间戳按采样数递增 usleep(20000); // 20ms 节拍 }这里有两个容易忽略的点。一是usleep(20000)只是粗略节拍,实际发包间隔会受系统调度影响,更稳的做法是用clock_nanosleep配合绝对时间。二是timestamp的增量必须等于本次打包的采样数,如果采集到的样本数不足 160(比如设备只给了 120 个),时间戳也只能加 120,否则接收端的时间轴会漂移。4. 避坑与排查:G.711 over RTP 最常见的五类翻车4.1 有 RTP 包但没声音:payload type 对不上现象是 Wireshark 能看到 RTP 流,但接收端解码出来全是静音。原因通常是 SDP 里协商的是 PCMU(payload type 0),发送端却按 PCMA(8)编码,或者反过来。解决方法是抓包看 RTP 头的 PT 字段,和 SDP 里的m=audio行对照,两边必须一致。A-law 和 μ-law 的码字不兼容,用错了解出来就是噪声。4.2 声音断断续续:时间戳增量与打包间隔不匹配现象是播放时每隔几百毫秒卡一下。原因是时间戳增量写成了固定值,但实际打包间隔在变。比如代码里写死timestamp += 160,但采集线程有时一次给 320 个样本,接收端就以为两个包的时间差是 20ms,实际是 40ms,jitter buffer 越积越多最后溢出。解决方法是让时间戳增量严格等于本次实际打包的采样数,不要写死。4.3 接收端听到的全是噪音:字节序没转现象是解码出来的波形完全不对,像白噪声。原因是在小端机器上直接给 RTP 头的seq、timestamp、ssrc赋值,没有调htons/htonl。接收端按网络字节序解析,拿到的值全错。解决方法是在打包函数里对这三个字段强制做字节序转换,解包时反向转回来。4.4 丢包后声音卡死:没处理序列号跳变现象是网络抖动丢了一两个包后,接收端播放直接停住。原因是接收端用seq做连续性判断,一旦发现跳变就丢弃后续所有包。正确做法是允许一定范围的乱序和丢包,用seq差值更新 jitter buffer 的期望位置,而不是直接重置。常见做法是维护一个滑动窗口,窗口内的包按seq排序后依次解码。4.5 长时间运行后声音变调:时间戳溢出没处理现象是通话跑了几小时后声音突然变快或变慢。原因是timestamp是 32 位无符号数,按 8kHz 递增大约 6 天溢出一次,但有些实现用有符号数比较,溢出后判断逻辑就乱了。解决方法是用无符号差值比较:(int32_t)(a - b) > 0这种写法能正确处理回绕,不要直接比大小。5. 二次开发:从单路对讲到多路混音与动态负载5.1 扩展方向一:多路 RTP 流的 SSRC 管理与混音单路跑通之后,下一步往往是多路。每路音频流要有独立的ssrc和seq/timestamp计数器,接收端按ssrc分流。混音时先把每路 G.711 解码成 PCM,做饱和加法(防止溢出削波),再重新编码发送。这里有个技巧:如果多路采样率一致,可以只在混音后做一次编码,省掉反复编解码的量化损失。// 多路混音:先解码再饱和相加 short mixed[160]; memset(mixed, 0, sizeof(mixed)); for (int ch = 0; ch < channel_count; ch++) { short pcm[160]; alaw_to_pcm(streams[ch].g711_buf, pcm, 160); for (int i = 0; i < 160; i++) { int sum = mixed[i] + pcm[i]; mixed[i] = (sum > 32767) ? 32767 : (sum < -32768) ? -32768 : sum; } }饱和加法那三行是必须的,直接mixed[i] += pcm[i]在多人同时说话时会溢出回绕,听感上就是突然的爆音。5.2 扩展方向二:动态负载类型与静音检测G.711 是固定码率 64kbps,但实际通话里大量时间是静音。可以加一个简单的能量检测:如果一帧 PCM 的均方根低于阈值,就发一个“静音包”或者干脆不发,接收端用舒适噪声生成(CNG)填充。RTP 头里的marker位可以用来标记静音后的第一个语音包,提醒接收端重置播放状态。这个改动能把平均带宽压到 20kbps 以下,对带宽敏感的场景很实用。5.3 验证方法:用 Wireshark 和自建回环双保险改完之后怎么确认没改坏?我一般走两步。第一步,Wireshark 抓包,过滤rtp,看序列号是否连续、时间戳增量是否稳定、PT 是否正确。第二步,跑loopback_test,用arecord录一段标准正弦波,经过完整链路后aplay播放,再用sox对比原始和还原的频谱,看有没有异常谐波。如果频谱干净、序列号连续,基本就没问题。5.4 一个我踩过的坑:别在发送线程里做重采样早期我图省事,在发送线程里直接把 16kHz 的 PCM 降采样到 8kHz 再编码。结果发送线程偶尔被重采样拖慢,20ms 的节拍变成 25ms,接收端 jitter buffer 频繁调整,声音一顿一顿的。后来改成采集线程做重采样,发送线程只做编码和打包,节拍立刻稳了。从那以后我每次改音频链路,都强制把“采集/重采样”和“编码/发送”拆到不同线程,用环形缓冲区解耦,再也不敢在一个循环里塞太多活。希望这个思路能帮到你,少走一段弯路。本文还有配套的精品资源,点击获取
简介:这份资源聚焦数字音频通信中的G.711编码与RTP实时传输协议结合应用,面向VoIP开发初学者、嵌入式音频工程师及流媒体协议学习者,帮助解决将G.711裸音频数据封装为RTP包并通过网络传输、最终由VLC成功接收播放的完整链路问题。压缩包共4个文件,包含C语言源码、SDP会话描述文件、G.711音频样本及说明文档,整体约2.16MB,覆盖从编码数据读取、RTP头构造到会话描述配置的关键环节。已有1486人学习下载,说明其在实时音频传输入门场景中具备一定参考价值。读者可借助源码理解RTP序列号、时间戳与负载封装逻辑,通过SDP文件掌握媒体会话协商方式,并利用样本音频与说明文档快速搭建可验证的传输实验,为VoIP或实时音频流服务开发打下实践基础。1. 从一段“能打通但没声音”的语音链路说起前阵子帮一个做对讲功能的朋友排查问题,现象很典型:信令通了,SDP 也协商成功了,RTP 包在 Wireshark 里一个接一个地飞,但两端就是听不到声音。抓包一看,payload 里全是0xD5 0xAA 0xD5 0xAA这种规律性极强的字节——典型的 G.711 编码后没做 RTP 封装,或者时间戳、序列号乱填导致的。这个场景几乎每个做实时语音的开发者都会撞上一次,而 g711 封装 rtp 传输恰好是绕不开的基本功。这份资源就是围绕这件事展开的:把 PCM 采样数据用 G.711(A-law 或 μ-law)压成 8bit,再按 RFC 3550 的格式塞进 RTP 包,通过 UDP 发出去,接收端反向解包还原成 PCM 播放。它适合正在做 VoIP、对讲机、语音网关、SIP 软电话、AI 语音对话链路的开发者,尤其是那些“协议栈用现成的、但底层封装想自己掌控”的人。下面我按“这东西怎么跑起来 → 核心封装怎么写的 → 怎么改”的顺序拆一遍。2. 项目结构与运行链路:从 PCM 到 RTP 包的完整路径2.1 目录结构与模块职责这类 g711 over rtp 的项目,结构通常不会太复杂,核心就四块:编码器、RTP 打包器、UDP 收发、解码播放。我拿到的这份代码目录大致是这样组织的:g711_rtp/ ├── include/ │ ├── g711.h # A-law / μ-law 编解码接口 │ ├── rtp_packet.h # RTP 头结构体与打包/解包函数 │ └── audio_device.h # 音频采集与播放抽象 ├── src/ │ ├── g711.c # 编解码查表实现 │ ├── rtp_packet.c # RTP 头填充、序列号与时间戳管理 │ ├── udp_socket.c # socket 创建、绑定、收发 │ └── main.c # 主循环:采集→编码→打包→发送 ├── test/ │ └── loopback_test.c # 本地回环测试 └── Makefileg711.c负责把 16bit 线性 PCM 压成 8bit,rtp_packet.c负责加 12 字节的 RTP 固定头,udp_socket.c只管搬运,main.c把这几步串成流水线。这种分层的好处是:你想换 Opus 或者 G.722,只需要替换编码模块,RTP 打包和 UDP 传输那层几乎不用动。2.2 编译与运行:三条命令跑通回环先确认依赖。这类项目一般只依赖标准 C 库和 POSIX socket,Linux 下直接 make 就行。如果涉及音频设备,可能会用到 ALSA 的libasound2-dev。# 安装音频开发库(Debian/Ubuntu 系) sudo apt-get install libasound2-dev # 编译 make # 运行回环测试:本机 5004 端口自发自收 ./loopback_test 127.0.0.1 5004loopback_test的逻辑是:从麦克风采集 20ms 的 PCM(8kHz 采样、16bit、单声道,正好 160 个采样点),编码成 G.711 后打成 RTP 包,发到指定地址,同时另一个线程在同一端口收包、解包、解码、播放。如果你听到自己的声音有延迟但清晰,说明整条链路是通的。提示:如果运行时报cannot open audio device,先确认当前用户是否在audio组里,或者直接用arecord -l看声卡有没有被识别。2.3 关键参数:采样率、打包间隔与时间戳增量G.711 本身不定义采样率,但 RTP 的时间戳增量必须和采样率对齐。这是最容易翻车的地方。常见配置如下:参数典型值说明采样率8000 Hz电话语音标准,G.711 的默认场景打包间隔20 ms一个 RTP 包承载 20ms 音频每包采样数1608000 × 0.02 = 160时间戳增量160每个 RTP 包时间戳加 160编码格式PCMA / PCMUA-law 对应 payload type 8,μ-law 对应 0时间戳增量写错,接收端的 jitter buffer 就会算错播放时刻,表现就是声音断断续续或者变调。我见过有人把增量写成 1,结果接收端以为每个包只隔 1/8000 秒,播放速度快了 160 倍,直接变成刺耳噪音。3. 核心封装拆解:G.711 编码与 RTP 头填充的代码实现3.1 G.711 编码:查表法比实时计算快一个数量级G.711 的 A-law 和 μ-law 都是对数压缩,直接按公式算会涉及浮点运算和对数,在嵌入式或高并发场景下不划算。常见做法是预生成一张 65536 项的查找表,把 16bit PCM 直接映射到 8bit 码字。// g711.c —— A-law 编码查表实现 #include "g711.h" static unsigned char alaw_encode_table[65536]; static int table_ready = 0; // 生成 A-law 编码表:输入 16bit 线性 PCM,输出 8bit A-law 码 static void build_alaw_table(void) { for (int i = 0; i < 65536; i++) { short pcm = (short)i; int sign = (pcm >= 0) ? 0x80 : 0x00; // 符号位 int magnitude = (pcm >= 0) ? pcm : -pcm; if (magnitude > 32767) magnitude = 32767; int exponent = 7; int mask = 0x4000; while (exponent > 0 && !(magnitude & mask)) { exponent--; mask >>= 1; } int mantissa = (magnitude >> (exponent + 3)) & 0x0F; alaw_encode_table[i] = (unsigned char)(sign | (exponent << 4) | mantissa); } table_ready = 1; } unsigned char pcm_to_alaw(short pcm) { if (!table_ready) build_alaw_table(); return alaw_encode_table[(unsigned short)pcm]; }这段代码的关键点有三个。第一,符号位单独处理,A-law 规定正数符号位为 1、负数为 0,和直觉相反,写反了声音会“发闷”。第二,指数位从 7 往下找最高有效位,决定压缩的段位。第三,尾数取指数位之后的 4 位,构成最终的 8bit。查表法把整个编码过程压成一次数组访问,实测比实时计算快 8 到 10 倍,在 8kHz 单声道下 CPU 占用几乎可以忽略。μ-law 的实现结构一样,只是符号位和段位映射不同,pcm_to_ulaw里把符号位改成(pcm >= 0) ? 0xFF : 0x7F,再按 μ-law 的段表处理即可。3.2 RTP 头填充:12 字节里每个字段都不能拍脑袋RTP 固定头是 12 字节,字段不多,但每个都有讲究。下面是一个典型的打包函数:// rtp_packet.c —— 填充 RTP 头并附加 G.711 payload #include <string.h> #include <arpa/inet.h> #include "rtp_packet.h" // 打包:payload 是已编码的 G.711 数据,len 是字节数 int rtp_pack(uint8_t *buf, const uint8_t *payload, int len, uint16_t seq, uint32_t timestamp, uint32_t ssrc, uint8_t payload_type) { rtp_header_t *hdr = (rtp_header_t *)buf; hdr->version = 2; // RTP 版本固定为 2 hdr->padding = 0; hdr->extension = 0; hdr->csrc_count = 0; hdr->marker = 0; // 语音包通常不置位,除非是静音后的第一个包 hdr->payload_type = payload_type; // PCMA=8, PCMU=0 hdr->seq = htons(seq); hdr->timestamp = htonl(timestamp); hdr->ssrc = htonl(ssrc); memcpy(buf + 12, payload, len); return 12 + len; }逐字段说明:version必须是 2,写 1 的话对端直接丢弃;payload_type决定对端用哪种解码器,PCMA 是 8、PCMU 是 0,协商阶段 SDP 里写的就是这个值;seq每发一个包加 1,接收端靠它检测丢包和乱序;timestamp每个包加 160(20ms × 8000Hz),接收端靠它做 jitter buffer 和播放调度;ssrc是同步源标识,同一路音频流保持不变,多路混流时靠它区分。所有多字节字段都要用htons/htonl转成网络字节序,这是血泪经验——在小端机器上忘了转,对端解析出来的序列号会大得离谱。3.3 发送循环:20ms 一个节拍,序列号和时间戳同步推进主循环的节奏感很重要。G.711 over RTP 通常按固定间隔发包,20ms 是最常见的打包周期。// main.c —— 采集、编码、打包、发送的主循环 uint16_t seq = 0; uint32_t timestamp = 0; uint32_t ssrc = 0x12345678; const int SAMPLES_PER_PACKET = 160; // 20ms @ 8kHz while (running) { short pcm_buf[SAMPLES_PER_PACKET]; int samples = audio_capture(pcm_buf, SAMPLES_PER_PACKET); if (samples <= 0) continue; uint8_t g711_buf[SAMPLES_PER_PACKET]; for (int i = 0; i < samples; i++) { g711_buf[i] = pcm_to_alaw(pcm_buf[i]); } uint8_t rtp_buf[12 + SAMPLES_PER_PACKET]; int pkt_len = rtp_pack(rtp_buf, g711_buf, samples, seq, timestamp, ssrc, 8); udp_send(rtp_buf, pkt_len); seq++; timestamp += SAMPLES_PER_PACKET; // 时间戳按采样数递增 usleep(20000); // 20ms 节拍 }这里有两个容易忽略的点。一是usleep(20000)只是粗略节拍,实际发包间隔会受系统调度影响,更稳的做法是用clock_nanosleep配合绝对时间。二是timestamp的增量必须等于本次打包的采样数,如果采集到的样本数不足 160(比如设备只给了 120 个),时间戳也只能加 120,否则接收端的时间轴会漂移。4. 避坑与排查:G.711 over RTP 最常见的五类翻车4.1 有 RTP 包但没声音:payload type 对不上现象是 Wireshark 能看到 RTP 流,但接收端解码出来全是静音。原因通常是 SDP 里协商的是 PCMU(payload type 0),发送端却按 PCMA(8)编码,或者反过来。解决方法是抓包看 RTP 头的 PT 字段,和 SDP 里的m=audio行对照,两边必须一致。A-law 和 μ-law 的码字不兼容,用错了解出来就是噪声。4.2 声音断断续续:时间戳增量与打包间隔不匹配现象是播放时每隔几百毫秒卡一下。原因是时间戳增量写成了固定值,但实际打包间隔在变。比如代码里写死timestamp += 160,但采集线程有时一次给 320 个样本,接收端就以为两个包的时间差是 20ms,实际是 40ms,jitter buffer 越积越多最后溢出。解决方法是让时间戳增量严格等于本次实际打包的采样数,不要写死。4.3 接收端听到的全是噪音:字节序没转现象是解码出来的波形完全不对,像白噪声。原因是在小端机器上直接给 RTP 头的seq、timestamp、ssrc赋值,没有调htons/htonl。接收端按网络字节序解析,拿到的值全错。解决方法是在打包函数里对这三个字段强制做字节序转换,解包时反向转回来。4.4 丢包后声音卡死:没处理序列号跳变现象是网络抖动丢了一两个包后,接收端播放直接停住。原因是接收端用seq做连续性判断,一旦发现跳变就丢弃后续所有包。正确做法是允许一定范围的乱序和丢包,用seq差值更新 jitter buffer 的期望位置,而不是直接重置。常见做法是维护一个滑动窗口,窗口内的包按seq排序后依次解码。4.5 长时间运行后声音变调:时间戳溢出没处理现象是通话跑了几小时后声音突然变快或变慢。原因是timestamp是 32 位无符号数,按 8kHz 递增大约 6 天溢出一次,但有些实现用有符号数比较,溢出后判断逻辑就乱了。解决方法是用无符号差值比较:(int32_t)(a - b) > 0这种写法能正确处理回绕,不要直接比大小。5. 二次开发:从单路对讲到多路混音与动态负载5.1 扩展方向一:多路 RTP 流的 SSRC 管理与混音单路跑通之后,下一步往往是多路。每路音频流要有独立的ssrc和seq/timestamp计数器,接收端按ssrc分流。混音时先把每路 G.711 解码成 PCM,做饱和加法(防止溢出削波),再重新编码发送。这里有个技巧:如果多路采样率一致,可以只在混音后做一次编码,省掉反复编解码的量化损失。// 多路混音:先解码再饱和相加 short mixed[160]; memset(mixed, 0, sizeof(mixed)); for (int ch = 0; ch < channel_count; ch++) { short pcm[160]; alaw_to_pcm(streams[ch].g711_buf, pcm, 160); for (int i = 0; i < 160; i++) { int sum = mixed[i] + pcm[i]; mixed[i] = (sum > 32767) ? 32767 : (sum < -32768) ? -32768 : sum; } }饱和加法那三行是必须的,直接mixed[i] += pcm[i]在多人同时说话时会溢出回绕,听感上就是突然的爆音。5.2 扩展方向二:动态负载类型与静音检测G.711 是固定码率 64kbps,但实际通话里大量时间是静音。可以加一个简单的能量检测:如果一帧 PCM 的均方根低于阈值,就发一个“静音包”或者干脆不发,接收端用舒适噪声生成(CNG)填充。RTP 头里的marker位可以用来标记静音后的第一个语音包,提醒接收端重置播放状态。这个改动能把平均带宽压到 20kbps 以下,对带宽敏感的场景很实用。5.3 验证方法:用 Wireshark 和自建回环双保险改完之后怎么确认没改坏?我一般走两步。第一步,Wireshark 抓包,过滤rtp,看序列号是否连续、时间戳增量是否稳定、PT 是否正确。第二步,跑loopback_test,用arecord录一段标准正弦波,经过完整链路后aplay播放,再用sox对比原始和还原的频谱,看有没有异常谐波。如果频谱干净、序列号连续,基本就没问题。5.4 一个我踩过的坑:别在发送线程里做重采样早期我图省事,在发送线程里直接把 16kHz 的 PCM 降采样到 8kHz 再编码。结果发送线程偶尔被重采样拖慢,20ms 的节拍变成 25ms,接收端 jitter buffer 频繁调整,声音一顿一顿的。后来改成采集线程做重采样,发送线程只做编码和打包,节拍立刻稳了。从那以后我每次改音频链路,都强制把“采集/重采样”和“编码/发送”拆到不同线程,用环形缓冲区解耦,再也不敢在一个循环里塞太多活。希望这个思路能帮到你,少走一段弯路。本文还有配套的精品资源,点击获取