我在前面的文章 (Android 智能手机上的音频浅析) 中说过 Android 手机上有一块专门用于音频处理的 DSP, 它的特点是频率低(一般几百 MHZ), 内部 memory 小(通常不超过 100k word). 要想让 Audio DSP 上放下更多的内容以及能流畅的运行, 要有一些应对措施. 今天就聊聊这些措施.
1, 频率低的应对措施
由于 DSP 的频率低, 要想软件能流畅的运行, 就得把运行时的 load 降下来. 主要的措施有两种, 定点化和 load 优化. 先看定点化.
DSP 有定点 DSP 和浮点 DSP 之分. 一般来说, 定点 DSP 具有速度快, 功耗低, 价格便宜的特点; 而浮点 DSP 则计算精确, 动态范围大, 速度快, 易于编程, 功耗大, 价格高. 音频处理用的 DSP 通常都是定点 DSP. 定点 DSP, 处理定点数据会相当快, 但是处理浮点数据就会非常慢. 那在定点 DSP 上涉及到浮点运算怎么办呢? 解决方法是浮点运算定点化来节约处理时间, 即用定点数表示浮点数. 定点数中最高位表示符号位, 符号位右边 n 位表示整数, 剩下的表示小数. 这种表示方法叫 Q 格式.
Q 格式表示为: Qm.n, 表示数据用 m 比特表示整数部分, n 比特表示小数部分, 共需要 m+n+1 位来表示这个数据, 多余的一位用作符合位(不表示出来). 例如 Q15 表示小数部分有 15 位, 一个 short 型数据, 占 2 个字节, 最高位是符号位, 后面 15 位是小数位, 表示的范围是:-1<X<0.9999695 . 浮点数据转化为 Q15, 将数据乘以 2^15;Q15 数据转化为浮点数据, 将数据除以 2^15. 例如假设数据存储空间为 2 个字节, 0.333×2^15=10911=0x2A9F,0.333 的所有运算就可以用 0x2A9F 表示, 同理 10911×2^(-15)=0.332977294921875, 可以看出浮点数据通过 Q 格式转化后是有误差的.
再看看 Q 格式的加减乘除基本运算. 加减法时须转换成相同的 Q 格式才能加减. 不同 Q 格式的数据相乘, 相当于 Q 值相加, 要右移 n 位得到正确的值. 不同 Q 格式的数据相除, 相当于 Q 值相减, 要左移 n 位得到正确的值. 这里举一个乘的例子: 两个小数相乘, 0.333*0.414=0.137862
- 0.333*2^15=10911=0x2A9F,0.414*2^15=13565=0x34FD
- short a = 0x2A9F;
- short b = 0x34FD;
- short c = a * b>> 15; // 两个 Q15 格式的数据相乘后为 Q30 格式数据, 因此为了得到 Q15 的数据结果需要右移 15 位
这样 c 的结果是 0x11A4=0001000110100100, 这个数据同样是 Q15 格式的, 它的小数点假设在第 15 位左边, 即为 0.001000110100100=0.1378173828125... 和实际结果 0.137862 差距不大. 或者 0x11A4 / 2^15 = 0.1378173828125
其他的基本运算就不举例了, 网上讲 Q 格式的有一些文章, 感兴趣自己去看.
实际应用中, 浮点运算大都时候都是既有整数部分, 也有小数部分的. 所以要选择一个适当的定标格式才能更好的处理运算. 一般用如下两种方法: 一是使用适中的定标, 既可以表示一定的整数复位也可以表示小数复位, 如对于 2812 的 32 位系统, 使用 Q15 格式, 可表示 - 65536.0~65535.999969482 区间内的数据. 二是全部采用小数, 这样因为小数之间相乘永远是小数, 永远不会溢出. 取一个极限最大值(最好使用 2 的 n 次幂), 转换成 x/Max 的小数(如果 Max 是取的 2 的 n 次幂, 就可以使用移位代替除法). 刚开始用 Q 格式时会很别扭, 不习惯, 用多了就慢慢习惯了.
为了降 load, 我们在软件开发过程中要做到以下两点: 一是选择算法实现时一定要用定点实现. Audio DSP 上会运行好多音频处理算法, 比如各种 codec, 这些 codec 的制定者一般会提供两套 reference code, 一套定点实现的, 一套浮点实现的, 我们在选择时一定要选用定点实现的. 对于应用在 ARM 上的音频处理算法, 同样是推荐用定点实现的算法, 虽然 ARM 频率高. 在 ARM 上还是要优化算法把 load 降到尽量低. 二是在我们自己的代码中如遇到要做浮点运算(比如算百分比), 要用 Q 格式定点化去算, 而不是直接用浮点数去运算.
再来看 load 优化. 我在前面的文章 (音频的编解码及其优化方法和经验) 中 load 优化的一些通用方法, 在 DSP 上也适用. 不过由于 DSP 频率低, 且 DSP 上运行的音频处理算法多, 有些算法又比较耗 load(比如 AEC), 要想让音频软件流畅的运行, 复杂的算法都是要做汇编优化的. 做这些不仅专业而且耗时, load 优化没有最低只有更低.
2,memory 低的应对措施
DSP 的内部 memory 分两种, DTCM(Data Tightly Coupled Memory, 数据紧密耦合存储器)和 PTCM(Program Tightly Coupled Memory, 程序紧密耦合存储器).DTCM 用于存 data,PTCM 用于存 code. 同时还有外部 memory(DDR), 它也可存 data 和 code.data 和 code 尽量放内部, 因为这样速度快效率高, 不到万不得已才将它们放在外部 memory 上. 即使放在外部的也是一些低频访问的 data 和 code, 如初始化函数等. 今天我们主要讲的是应对 DTCM(data memory)小的措施. DTCM 主要分以下几个区域: const 区, data 区, bss 区, overlay 区. const 区顾名思义就是放一些常量的, data 区是放已初始化的全局变量和静态变量, bss 区是放未初始化的全局变量和静态变量, overlay 区主要是根据场景的互斥做一些 memory 的复用, 这是应对 memory 小的主要措施. 我们先看 overlay 机制.
Overlay 机制是指不会同时发生的场景下使用的 memory 可以复用, 在音频上主要有两种情况. 一是 music 使用的 memory 可以和 voice 使用的 memory 复用, 因为音乐播放和打电话不可能同时发生. 通常是音乐播放时来电话, 音乐播放就暂停, 把音乐播放的 context 以及还在 buffer 中未播放的数据拷贝到外部 memory 上, 把 music 使用的 memory 让给 voice 用. 打电话结束后再把放在外部 memory 上的音乐相关的 context 以及未播放的数据拷进内部 memory 原先的位置上, 从而继续音乐的播放. 二是各种 codec 使用的 memory 的复用, 因为同时只有一种 codec 在使用. 音乐的 decoder 有 MP3,AAC, 播放音乐时只可能有一种 decoder 在用. Voice 的 codec 有 AMR-NB,AMR-WB,EVS, 打电话时只可能有一种 codec 在用. 它与第一种的区别是不需要保存上下文. 这样可以画出 DTCM 的分布图, 如下图:
上图中 memory 地址是由低向高增长, 分别是 const 区, data 区, bss 区, overlay 区. 在 overlay 区 music 和 voice 有相同的起始地址, 在 music 中 decoder MP3 和 AAC 又有相同的起始地址, 在 voice 中 codec AMR-NB,AMR-WB 和 EVS 又有相同的起始地址.
对于 PTCM, 同样可以用 overlay 机制. 不过除非一些代码重写, 一般很难省 code size 了.
除了 overlay 机制, 其他的就要一点一点的抠来省 memory 了, 主要有以下几点:
1) 定义数据类型时能用 short 的就不要用 int
2)在 overlay 区域, buffer 的大小都是指定的. 指定时要正确算出大小值, 不要指定大了, 指定大了就浪费了.
3)在 data 区或者 bss 区的 buffer 要看是不是分大了, 比如有的 buffer 分三块就够了, 也就没必要分四块了, 分四块一是浪费了 buffer, 二是有些场景下增加了时延.
4) DSP 上每个 thread/task 的栈的大小都是指定的. 为了省 memory, 栈的大小不可能很大, 一般不超过 1k word. 这就要求写代码时不能有大的局部变量数组等, 遇到时就要通过一些技巧解决. 如一个要把双声道的数据从 interleave 变成 non-interleave 的函数, 写成了如下实现, 避免了大的局部变量数组. 通常的做法是用一个大的局部变量数组先存右声道数据, 最后再一起拷到指定位置上.
- void interleave_to_noninterleave(int16_t *buf, int32_t frame_cnt)
- {
- int i,j;
- short temp;
- for(i = 1; i < frame_cnt; i++) {
- temp = buf[2*i];
- for(j = 0; j < i; j++)
- buf[2*i - j] = buf[2*i - j -1];
- buf[i] = temp;
- }
- }
5)代码编好后会生成一个各 buffer 起始地址和大小的文件. 关注那些 size 较大的 buffer, 分析有没有减小的可能.
总体而言, DSP
由于频率低和 memory 小的限制, 在上面写代码比在 ARM 上要求高些, 花的时间长些. 长时间在 ARM 上写代码, 转到 DSP 上会有些不习惯, 有个适应过程.
来源: https://www.cnblogs.com/talkaudiodev/p/9096398.html