均匀量化与非均匀量化:语音压扩原理与MATLAB实现
简介面向数字信号处理、通信原理等课程学习者的一份开源MATLAB实现包主题是均匀量化与非均匀量化的对比。非均匀量化部分覆盖常见的μ律/A律压缩扩张策略可帮助理解实际信号处理中如何通过压缩低幅度区间来减小量化误差。压缩包共4个文件包含核心的MATLAB量化脚本、PDF格式的量化分析报告、README说明文档和一张结果示意图整体仅788KB下载后便于快速研读。脚本按照题目给定图像中的量化任务实现均匀与非均匀两种模式输出误差分析结果方便对比不同量化方式下的信噪比与重构效果。报告部分梳理了量化原理、实验步骤和结论示意图则直观展示了差异读者可结合报告与代码深入掌握量化算法的工程实现。代码以开源方式发布已有815人学习适合正在学习量化概念、需要参考MATLAB代码完成课程设计或验证理论结果的学生与工程师。1. 为什么均匀量化在语音信号上费力不讨好1.1 从一次电话通话说起量化就是给连续信号上户口做语音信号处理课设时我第一次认真对比了均匀量化和非均匀量化在语音上的表现。同样用4bit量化同一段语音均匀量化的结果像隔着棉被听人说话换成μ律压扩之后哪怕只给3bit内容还是能听懂。这就是非均匀量化存在的意义——它改变了量化步长在信号幅度上的分配方式让有限的量化级数花在更需要的地方。先说清楚量化到底在做什么。连续信号在时间上采样之后每个采样点的幅度仍然是连续的要在数字系统里存储或传输必须把它映射到有限个离散电平上这个映射过程就是量化。你可以把量化理解成给信号上户口系统里只认户口本上登记的那几个等级其他值全部靠最近的那个等级来代管。上户口是有代价的登记值和真实值之间的差就是量化误差也叫量化噪声。量化器设计的核心问题就是在量化位数有限的前提下怎么分配这些户口名额最划算。均匀量化最简单粗暴——把整个动态范围等分成N份每个区间分配一个代表电平。但这里藏着一个在语音场景下非常致命的假设它默认信号在每个幅度区间出现的概率是一样的。1.2 均匀量化的致命短板小信号被消灭了实际语音信号的幅度分布有个明显特征小幅度出现的概率远大于大幅度。说话时的正常音量大部分时间的波形都徘徊在较小幅度附近只有少数音节、爆破音才会冲到高幅度区。均匀量化不管这一点它给1V附近的信号和0.01V附近的信号分配了完全相同的绝对精度。量化误差的绝对值大致固定在一个量化步长Δ以内但信噪比要看误差相对信号有多大。一个满幅3V的信号加上±Δ/2的误差几乎听不出来一个幅度只有0.02V的弱辅音加上同样的Δ/2可能整个被噪声淹没。这就像你用同样粗细的笔去画一栋大楼和一只蚂蚁蚂蚁那笔下去就看不见了。更麻烦的是动态范围问题。语音信号的峰值和最小值之间可以有40~50dB的差距如果为了让小信号不被吃掉而把量化步长做小就要指数级增加量化位数。每减少一半步长多需要1bit想把动态范围扩大6dB就要在满量程信号上多花1bit。均匀量化在这种场景下性价比极低非均匀量化就是奔着解决这个问题来的。2. 均匀量化的数学原理与MATLAB代码逐段拆解2.1 量化步长、量化级数与过载区的关系动手写代码前先把均匀量化器的几个关键参数理清楚。假设输入信号被限制在区间[x_min, x_max]内量化位数为B那么总共有L 2^B个量化级量化步长Δ由下面公式决定delta (x_max - x_min) / (2^B);注意这个公式默认所有量化级都用于覆盖x_min到x_max之间的范围。实际信号如果超出这个区间就会进入过载区量化误差会迅速变大。过载区的误差行为和正常工作区完全不同正常区内误差均匀分布在[-Δ/2, Δ/2]之间过载区的误差则线性增长。设计量化器时通常会把输入范围留出一定余量避免信号频繁撞上过载区。均匀量化器的信噪比有一个经典公式在信号均匀分布且不发生过载时SQNR ≈ 6.02B 4.77 - 20log10(A/σ) dB其中A是量化器满量程幅度σ是信号有效值。那个20log10(A/σ)项其实就是峰值因子PAPR语音信号的峰值因子通常在12~16dB这直接吃掉了一两个bit的信噪比优势。均匀量化面对这种信号等于白白把宝贵的量化级数浪费在了不常出现的大幅度上。2.2 uniform_quantize函数从模拟信号到量化索引我从最基础的均匀量化函数开始写这是一个可以直接复制到MATLAB里用的完整版本function [q_index, q_out] uniform_quantize(x, x_min, x_max, B) % uniform_quantize 均匀量化器 % 输入 % x - 输入信号向量 % x_min - 量化范围下限 % x_max - 量化范围上限 % B - 量化位数 % 输出 % q_index - 量化索引从0到2^B-1的整数序列 % q_out - 反量化后的重建信号 if nargin 4 || B 1 error(量化位数必须为正整数); end if x_max x_min error(x_max必须大于x_min); end L 2^B; delta (x_max - x_min) / L; % 先做限幅把超出范围的样本拉回到边界 x_clip max(min(x, x_max), x_min); % 计算每个样本落在哪个量化区间 % 注意这里用 round 而不是 floor配合后面的重建公式 % 可以让每个区间的代表电平位于区间中点 temp_index round((x_clip - x_min) / delta - 0.5); % 索引范围是 0:2^B-1超出边界的强制拉回 q_index max(0, min(L-1, temp_index)); % 反量化恢复代表电平区间中点 q_out x_min (q_index 0.5) * delta; end这段代码的关键在temp_index的计算。我把原始信号映射到量化区间编号时用了round减去0.5的处理这样索引恰好落在区间中点。举个例子B3、x_min-1、x_max1时Δ0.25区间划分是[-1,-0.75), [-0.75,-0.5)等每个区间的中点就是重建电平。用round而不是floor是为了让误差在正负两个方向上都均匀分布而不是系统性偏向一侧。实际使用中直接调用下面这段就能完成量化与重建% 生成一个测试信号叠加了两个不同幅度的正弦波 fs 8000; % 采样率 8kHz t (0:fs*0.1-1) / fs; x 0.9 * sin(2*pi*400*t) 0.01 * sin(2*pi*1800*t); % 3bit均匀量化 [q_idx, q_rec] uniform_quantize(x, -1, 1, 3); % 对比原始信号和重建信号的误差 err x - q_rec; mse_val mean(err.^2); snr_val 10 * log10(sum(x.^2) / sum(err.^2)); fprintf(均匀量化 3bit MSE: %.4f, SNR: %.2f dB\n, mse_val, snr_val);看这个测试信号的设计0.9幅度的大正弦代表强信号0.01幅度的小正弦代表弱信号。均匀量化下那个小正弦极有可能直接被量化噪声吞掉这正是我想在后面跟非均匀量化对比的基线。2.3 反量化与误差分析的无形陷阱写反量化代码时容易踩一个隐蔽的坑。如果你用floor计算索引这是很多教材里的写法反量化公式就应该是x_min (q_index 0.5) * delta如果你用round(q_index_exact)这种写法重建公式要相应调整搞错一个零点五个delta整个重建信号会整体偏移产生一个可观的直流偏置。上面代码里还有一个细节我先对x做了限幅再计算索引但输出q_out在接近边界时可能会出现一个让人困惑的现象——当信号恰好落在最后一个区间时比如x 0.99、B3索引应该等于7重建输出约等于0.90625你会发现重建信号永远达不到x_max这是中点量化器的正常特性不是bug。但如果后续处理里要求重建信号的范围真的能达到满幅就需要在最外侧区间使用边界电平而不是中点电平这是不同量化器设计规格的区别。均匀量化的误差分析还有一个容易忽略点MSE的计算要用重建信号和原始信号而不是索引和模拟值之间的差。很多初学者在这里把时间对齐搞错尤其当信号经过滤波器或插值之后误差序列的时间偏移会把SNR算得异常难看。做对比实验时我通常直接从量化器的输入输出两端对齐采样避免经过任何异步处理。3. 非均匀量化用压扩曲线把信号掰弯再掰直3.1 μ律与A律的工程直觉非均匀量化的核心思想一句话就能说清对信号先做一次非线性变换让变换后的信号在幅度上均匀分布再做均匀量化接收端再用反变换还原。这就是压扩companding技术名字来自compression压缩和expansion扩张。为什么压扩能改善小信号信噪比因为压缩函数在小幅度区域的斜率更大。你想象一条向上凸的曲线横轴是输入、纵轴是输出输入从0增长到0.1时输出可能已经从0涨到了0.5输入再从0.1涨到1输出才从0.5涨到1。压缩之后小信号的幅度被放大了大信号反而被压缩了。在小信号区间同样的量化步长对应的原始信号绝对误差更小等效于给小信号分配了更精细的量化级。通信领域最著名的两条压扩律是μ律和A律。μ律主要用在北美和日本μ的标准值是255A律用在中国和欧洲A值一般取87.56。两个公式看起来不同但思想完全一致用一个对数函数做压缩小信号区是近似的线性放大大信号区是对数压缩。之所以用对数而不是平方、开方之类的曲线是因为对数函数在小信号区有理论上的最优性——它能以较小的复杂度逼近对数量化器。3.2 mu_law_compress和expand的MATLAB实现先看μ律压缩器的代码。需要注意的是μ律公式要求输入信号归一化到[-1, 1]这是一个前置条件少了这一步后面全白搭。function y mu_law_compress(x, mu) % mu_law_compress μ律压缩 % x 必须归一化到 [-1, 1]mu 通常取 255 if nargin 2 mu 255; end x max(min(x, 1), -1); % 防止输入越界 y sign(x) .* (log(1 mu .* abs(x)) / log(1 mu)); end function x_r mu_law_expand(y, mu) % mu_law_expand μ律扩张压缩的逆变换 if nargin 2 mu 255; end y max(min(y, 1), -1); x_r sign(y) .* ((1/mu) .* (exp(abs(y) .* log(1 mu)) - 1)); end压缩处理里有几个细节。sign(x)保留符号否则μ律公式对正负信号无法区分。log(1 mu .* abs(x))里那个1是为了让x0时输出也是0同时避免log(0)的问题。mu255时如果输入是0.001压缩输出大概是0.0095输入是0.5时输出约0.89——可以清楚看到小信号被放大了近10倍大信号反而被压到0.9附近。这个放大比例正是不均匀量化改善小信号信噪比的直接原因。调用时注意函数内部做了输入限幅到[-1,1]。这个限幅放在这里是为了防止归一化不彻底导致log参数为负但工程上不推荐依赖它来兜底——如果数据真的超出范围了你应该返回去检查归一化逻辑而不是让压缩器悄悄地把信号削掉。扩张函数exp(abs(y) .* log(1mu))是压缩函数的精确逆变换。这里的数值范围会造成一些隐性问题exp的参数在y接近1时会达到5.54在双精度浮点下完全没问题但如果后续用单精度DSP这个动态范围就值得警惕。3.3 A律压扩分段函数里藏着两个线性段A律公式是分段函数MATLAB实现时要用逻辑索引分别处理两段function y a_law_compress(x, A) % a_law_compress A律压缩 % A 通常取 87.6 if nargin 2 A 87.6; end x max(min(x, 1), -1); x_abs abs(x); y zeros(size(x)); % 小信号段近似线性放大 linear_region x_abs 1/A; y(linear_region) A * x_abs(linear_region) / (1 log(A)); % 大信号段对数压缩 log_region ~linear_region; y(log_region) (1 log(A * x_abs(log_region))) / (1 log(A)); % 恢复符号 y sign(x) .* y; end function x_r a_law_expand(y, A) if nargin 2 A 87.6; end y max(min(y, 1), -1); y_abs abs(y); x_r zeros(size(y)); % 反线性段 lin_region y_abs 1 / (1 log(A)); x_r(lin_region) (y_abs(lin_region) * (1 log(A))) / A; % 反对数段 log_region ~lin_region; x_r(log_region) exp(y_abs(log_region) * (1 log(A)) - 1) / A; x_r sign(y) .* x_r; endA87.6这个值不是拍脑袋定的。它保证在两段函数交界处、也就是x1/A附近线性段和对数段连续且导数也连续整个压扩曲线没有突变。这个性质在数学上重要在工程上更重要——如果交界处有折点压缩后的信号会出现额外的谐波分量。你可能会在课本上看到A律13折线近似这个说法。这是历史上为了用数字电路实现A律而想出来的巧办法用8段直线去逼近A律曲线每段斜率按2的幂次递减这样在定点DSP上只需要移位和加法不需要查对数表。现在做MATLAB仿真直接用浮点公式就行不用纠结折线细节但如果你要在嵌入式TMS320上实现回到13折线或者μ律的15折线近似代码效率会高得多。3.4 压缩器配合量化器的完整链路非均匀量化的完整链路是先压缩再均匀量化然后反量化重建模拟电平最后扩张回来。我在工程里一般把它封装成一个函数省的每次写一串function y_rec nonuniform_quantize(x, B, mu, x_max) % nonuniform_quantize 基于μ律压扩的非均匀量化 % 输入 % x - 原始信号未经归一化 % B - 量化位数 % mu - μ律参数默认255 % x_max - 信号最大绝对值的估计值用于归一化 if nargin 3 || isempty(mu) mu 255; end if nargin 4 || isempty(x_max) x_max max(abs(x)); end % 归一化到 [-1, 1] x_norm x / x_max; % μ律压缩 x_comp mu_law_compress(x_norm, mu); % 均匀量化此时信号在 [-1, 1] 范围 [~, q_rec] uniform_quantize(x_comp, -1, 1, B); % 扩张 y_norm mu_law_expand(q_rec, mu); % 反归一化 y_rec y_norm * x_max; end注意这里x_max的选择会影响整个量化器的实际有效动态范围。如果x_max设置得比真实信号最大值大得多归一化之后信号整体变小相当于用满量程的一部分做量化等效位数下降。我在给测试信号做3bit量化时踩过这个坑设置x_max10结果把动态范围浪费了9倍。调用方式和均匀量化做同样的事情x_nq nonuniform_quantize(x, 3, 255, 1); err_nq x - x_nq; snr_nq 10 * log10(sum(x.^2) / sum(err_nq.^2)); fprintf(μ律非均匀量化 3bit SNR: %.2f dB\n, snr_nq);4. 均匀 vs 非均匀用信噪比和频谱图说话4.1 仿真对比实验设计代码都写完了接下来做一个标准对比实验。我的测试信号就是2.2节里那个混合正弦包含0.9幅度的大信号和0.01幅度的小信号两个频率分得很开方便观察频谱。分别用均匀量化和μ律非均匀量化在2bit、3bit、4bit三个档位下跑记录SNR和MSE。B_list 2:4; snr_uniform zeros(size(B_list)); snr_nonuniform zeros(size(B_list)); for k 1:length(B_list) B B_list(k); % 均匀量化 [~, q_u] uniform_quantize(x, -1, 1, B); err_u x - q_u; snr_uniform(k) 10 * log10(sum(x.^2) / sum(err_u.^2)); % 非均匀量化 x_nu nonuniform_quantize(x, B, 255, 1); err_nu x - x_nu; snr_nonuniform(k) 10 * log10(sum(x.^2) / sum(err_nu.^2)); end % 打印对比表 fprintf(量化位数 | 均匀量化 SNR | μ律非均匀 SNR | 提升幅度\n); for k 1:length(B_list) fprintf(%d bit | %.2f dB | %.2f dB | %.2f dB\n, ... B_list(k), snr_uniform(k), snr_nonuniform(k), ... snr_nonuniform(k) - snr_uniform(k)); end这个实验的要点是小心选择输入信号的归一化方式。两种量化器最后都要重建回原始幅度所以我在统一入口处都把信号归一化到±1再送进各自量化器。如果你用了一个峰值不是1的信号归一化因子差异可能会掩盖真实的性能对比。4.2 结果解读小信号区的逆袭3bit量化的典型结果大概是均匀量化SNR在13~15dBμ律非均匀量化能到20~24dB提升8~10dB等价于多用了1.5bit左右。关键不在这个总SNR数字而在于误差的分布。均匀量化的误差在整个时间轴上幅度基本一致这意味着它的噪声是大片均匀铺开的非均匀量化的误差在大信号区明显增大但在小信号区被压得非常低几乎消失。如果你把重建信号的频谱画出来均匀量化那个0.01幅度、1800Hz的小峰会被噪声底完全盖住但非均匀量化仍然能看到那个峰。这在实际听感上的差异是质变——弱辅音、气声、呼吸声这类低幅度成分能不能保留直接决定了语音是自然还是机械。另一个观察角度是每个频段的SNR。即使总SNR差不多的两组参数下误差频谱的形状也完全不同均匀量化产生的是白噪声状频谱非均匀量化在低幅度时段内噪声更低因此语音场景下感知信噪比比客观数字提升更大。这也解释了为什么电话系统一路都用A律/μ律而不是均匀量化。4.3 什么时候你根本不需要非均匀量化非均匀量化也不是万能的。如果你的信号本身就接近均匀分布或者动态范围很窄、峰值因子很小压扩几乎不会带来增益反而白白增加了非线性变换的计算开销和延迟。我在给一个超声回波信号做量化时就发现回波信号经过对数放大之后幅度分布本身已经接近压缩过了再做μ律压扩SNR提升不到1dB还引入了额外的硬件资源占用。还有一类场景是数字通信里的星座映射。QPSK、16QAM这类调制方式的星座点本身已经经过了精心设计解调器对信号的幅度分布有具体要求直接对它做压扩反而会干扰星座判决这时候应该用无失真重建的思路来设计量化器而不是追求小信号感知增益。判断要不要上非均匀量化我的经验是先画一张信号幅度直方图看它是不是长尾分布、小信号占比高不高。如果直方图中段和尾部的面积已经很小了比如超过80%的采样点集中在中间几个量化区间里那就值得用压扩。如果分布接近均匀均匀量化就够。5. 仿真踩坑清单与工程落地建议5.1 三个容易出错却极其隐蔽的坑第一个坑是μ律压缩函数输入范围不归一化。直接把原始信号送进mu_law_compress信号幅度0.5和2都会输出接近同一个上界扩张之后根本无法还原。我在最初的测试里把0.1幅度信号压出来之后重建居然变成1.2整个波形彻底失真排查了半天发现是忘了除以x_max。第二个坑是量化索引的边界处理。索引是0-based逻辑但MATLAB数组是1-based如果你用索引去数组里查值比如qd_index q_index 1再查表顺序就对上了。但如果你想直接用q_index参与乘法比如重建电平计算就必须保持0-based两个场景混用哪个都行但别在同一段代码里混着用这是我调试时最容易出现怎么越界了和怎么重建结果整体偏移的根源。第三个坑是A律线性段和日志段的边界处理。A律公式中有个1/A的分界点A87.6时大约是0.0114。如果输入信号大部分都在这个阈值附近用逻辑索引区分段的方式在浮点下是没有问题但一旦跳到定点实现1/A的量化误差会让交界处出现一个不连续的小台阶在频谱上表现为额外谐波。仿真阶段看不出问题做硬件实现时要注意这个细节。5.2 从仿真走向定点DSP时的额外注意如果只是做MATLAB仿真浮点代码足够用了。但如果要移植到DSP或FPGA上有几点值得提前考虑。第一浮点对数/指数函数在定点芯片上非常贵μ律、A律的乘法展开公式例如多项式逼近或查表法是常用替代。查表法的粒度选择会影响量化精度我做过一个对比256点查表的μ律压缩性能损失大约0.1~0.2dB工程上完全可接受。第二定点实现时要特别关注乘法中间结果的位宽。以μ律255为例log(1255*|x|)里的255*x在16bit定点下容易溢出正确的做法是先把x的左移8位再走查表而不是先乘255再归一化。这类中间位宽问题在仿真中完全看不到只有定点模型跑起来才会突然爆炸。第三多个量化器级联时的参数匹配。有些系统里既有ADC量化又有链路里的数据压缩量化两者的压扩参数如果设置不同信号会经历两次压扩、两次扩张中间再叠加噪声压缩失真的影响会成倍放大。我在一个数据采集链路里就干过这种事AD前做了一次μ律压扩DSP里又做了一次结果整个链路的噪声底抬高了将近3dB最终发现是两处参数不匹配统一之后立刻恢复正常。把这段代码框架搭完之后我自己最大的收获是量化器的性能优劣很少能脱离信号场景单独评价。均匀量化在峰值因子低的场景里很干净利落μ律、A律在语音和窄带通信里表现惊艳但关键还是要先分析信号的统计特性再决定用哪种量化策略。你可以把上面的函数直接拿去改把测试信号换成自己手头的真实数据跑一遍对比就会对量化这回事有更直观的感觉。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →