3

我为 iPhone 编写了一个小型软件合成器。
为了进一步调整性能,我用 Shark 测量了我的应用程序,发现我在浮点/SInt16 转换中浪费了很多时间。
因此,我重写了一些部分以通过预先计算返回“即用型”SInt16 样本的查找表来绕过转换。到目前为止,这工作正常。
目前我正在尝试重写一些过滤器和我的 ADSR 包络实现以仅使用整数算术,但我可以使用一些技巧来执行没有浮点数的乘法/除法。
我的目标是iPhone 规范格式

  • LPCM
  • 16 位整数样本

在不使用浮点数的情况下将幅度应用于我的最终样本的好方法是什么?

编辑:
到目前为止,我唯一想到的是,我可以通过右移当前样本来除以 2 的幂。

inBuffer[frame] = wavetable[i % cycleLengthInSamples] >> 4;

但我想不出任何优雅的方式来创建一个平滑的 ADSR 信封。

Edit2: 感谢您的所有精彩回答!
我目前的做法:

  • 将我所有的 ADSR 包络值带入正 SInt16 范围
  • 与波表中的当前值相乘(将中间体存储为 SInt32)
  • 将结果右移 16

这似乎工作:)

4

4 回答 4

4

定点很好,因为在这种情况下您使用的是 16 位。最简单的方法是根据您需要的精度乘以 10 的幂。如果您可以使用 32 位整数作为中间值,您应该能够获得不错的精度。最后,您可以根据需要转换回 16 位 int、舍入或截断。

编辑:您想向左移动,以使值更大。将移位结果存储在更精确的类型中(32 位或 64 位,具体取决于您的需要)。如果您使用有符号类型,简单的移位将不起作用

小心你是否将两个定点数相乘或相除。乘以 (a*n) * (b n) 最终得到 a b n^2 而不是 a b n。除法是 (a n) / (b n) 是 (a/b) 而不是 ((a n)/b)。这就是为什么我建议使用 10 的幂,如果您不熟悉定点,可以很容易地发现您的错误。

完成计算后,您向右移动以返回 16 位整数。如果你想变得花哨,你也可以在移位之前进行四舍五入。

如果您真的对实现高效的定点感兴趣,我建议您阅读一下。http://www.digitalsignallabs.com/fp.pdf

于 2009-09-18T15:51:29.337 回答
3

这个 SO 问题的答案在实施方面非常全面。这里的解释比我在那里看到的要多一点:

一种方法是将所有数字强制在一个范围内,例如 [-1.0,1.0)。然后将这些数字映射到范围 [-2^15,(2^15)-1] 中。例如,

Half = round(0.5*32768); //16384
Third = round((1.0/3.0)*32768); //10923

当您将这两个数字相乘时,您会得到

Temp = Half*Third; //178962432
Result = Temp/32768; //5461 = round(1.0/6.0)*32768

最后一行中除以 32768 是Patros提出的关于需要额外缩放步骤的乘法的观点。如果您明确编写 2^N 缩放比例,这将更有意义:

x1 = x1Float*(2^15);
x2 = x2Float*(2^15);
Temp = x1Float*x2Float*(2^15)*(2^15);
Result = Temp/(2^15); //get back to 2^N scaling

所以这就是算术。对于实现,请注意两个 16 位整数的乘法需要 32 位结果,因此 Temp 应为 32 位。此外,32768 不能用 16 位变量表示,因此请注意编译器将生成 32 位立即数。正如您已经指出的那样,您可以转换为乘/除以 2 的幂,这样您就可以编写

N = 15;
SInt16 x1 = round(x1Float * (1 << N));
SInt16 x2 = round(x2Float * (1 << N));
SInt32 Temp = x1*x2;
Result = (SInt16)(Temp >> N);
FloatResult = ((double)Result)/(1 << N);

但是假设 [-1,1) 不是正确的范围?如果您希望将数字限制为 [-4.0,4.0),则可以使用 N = 13。然后您有 1 个符号位,二进制点之前有 2 位,之后有 13 位。这些分别称为 1.15 和 3.13 定点小数类型。你用分数的精度换取净空。

只要您注意饱和度,添加和减去小数类型就可以正常工作。正如帕特罗斯所说,对于除法,缩放实际上抵消了。所以你必须做

Quotient = (x1/x2) << N;

或者,为了保持精度

Quotient = (SInt16)(((SInt32)x1 << N)/x2); //x1 << N needs wide storage

乘以和除以整数正常工作。例如,要除以 6,您可以简单地写

Quotient = x1/6; //equivalent to x1Float*(2^15)/6, stays scaled

在除以 2 的幂的情况下,

Quotient = x1 >> 3; //divides by 8, can't do x1 << -3 as Patros pointed out

但是,添加和减去整数并不是天真的工作。您必须首先查看整数是否适合您的 xy 类型,制作等效的小数类型,然后继续。

我希望这对这个想法有所帮助,请查看其他问题中的代码以获得干净的实现。

于 2009-09-19T06:59:26.040 回答
1

看看这个描述快速乘法算法的页面。

http://www.newton.dep.anl.gov/askasci/math99/math99199.htm

于 2009-09-18T16:02:19.137 回答
1

一般来说,假设您将使用带符号的 16.16 定点表示。这样一个 32 位整数将有一个有符号的 16 位整数部分和一个 16 位小数部分。然后我不知道iPhone开发中使用的是什么语言(也许是Objective-C?),但是这个例子是C语言的:

#include <stdint.h>

typedef fixed16q16_t int32_t ;
#define FIXED16Q16_SCALE 1 << 16 ;

fixed16q16_t mult16q16( fixed16q16_t a, fixed16q16_t b )
{
    return (a * b) / FIXED16Q16_SCALE ;
}

fixed16q16_t div16q16( fixed16q16_t a, fixed16q16_t b )
{
    return (a * FIXED16Q16_SCALE) / b ;
}

请注意,以上是一个简单的实现,并没有提供算术溢出保护。例如,在 div16q16() 中,我在除法之前进行多次以保持精度,但根据操作数,操作可能会溢出。您可以使用 64 位中间体来克服这个问题。此外,除法总是向下舍入,因为它使用整数除法。这提供了最佳性能,但可能会影响迭代计算的精度。修复很简单,但会增加开销。

请注意,当乘以或除以恒定的 2 次幂时,大多数编译器会发现微不足道的优化并使用移位。然而,C 并没有定义负符号整数右移的行为,所以为了安全和可移植性,我把它留给编译器来解决。YMV 在您使用的任何语言上。

在 OO 语言中,fixed16q16_t 自然会成为具有运算符重载的类的候选对象,因此您可以像使用普通算术类型一样使用它。

您可能会发现在类型之间进行转换很有用:

double fixed16q16_to_double( fixed16q16_t fix )
{
    return (double)fix / FIXED16Q16_SCALE ;
}

int fixed16q16_to_int( fixed16q16_t fix )
{
    // Note this rounds to nearest rather than truncates
    return ((fix + FIXED16Q16_SCALE/2)) / FIXED16Q16_SCALE ;
}

fixed16q16_t int_to_fixed16q16( int i )
{
    return i * FIXED16Q16_SCALE ;
}

fixed16q16_t double_to_fixed16q16( double d )
{
    return (int)(d * FIXED16Q16_SCALE) ;
}

这是基础,可以变得更复杂并添加三角和其他数学函数。

固定加法和减法适用于内置的 + 和 - 运算符及其变体。

于 2009-09-21T20:34:33.747 回答