我正在实现一个混音器,它无需 SIMD 指令即可正常工作,但很难弄清楚如何将声音数据提取到单独的 channel 中。
我的数据采用交错格式:L0R0 L1R1 L2R2 L3R3... 我以相同的格式将它们加载到 __m128i 中,因此寄存器中有 4 个样本。
我希望它们位于不同的 channel 中:L0L1L2L3 R0R1R2R3。这是我缺少的部分。
所以输入是:8 x i16(4xi32 交错) 我希望输出为 left = 4 x f32 和 right = 4 x f32,然后进行混合。
混合后,我可以交错 channel ,得到 L0R0 L1R1 L2R2...:
__m128 *src0 = mixed_channel0;
__m128 *src1 = mixed_channel1;
__m128 *dest = (__m128i *)buffer;
for (u32 sample_index = 0; sample_index < sample_chunk_count; ++sample_index)
{
__m128 s0 = _mm_load_ps((f32 *)src0++);
__m128 s1 = _mm_load_ps((f32 *)src1++);
__m128i l = _mm_cvtps_epi32(s0);
__m128i r = _mm_cvtps_epi32(s1);
__m128i lr0 = _mm_unpacklo_epi32(l, r);
__m128i lr1 = _mm_unpackhi_epi32(l, r);
*dest++ = _mm_packs_epi32(lr0, lr1);
}
基本上我需要做相反的事情:
__m128i input = [L0R0, L1R1, L2R2, L3R3] packed pairs of 16bit ints
// magic happens, then
__m128 left = [L0, L1, L2, L3] packed 32bit floats
__m128 right = [R0, R1, R2, R3] packed 32bit floats
即使我屏蔽了低/高阶 i16-s,那么如何将它们转换为 f32-s?屏蔽掉后我会得到:
__m128i right = [xx, R0, xx, R1, xx, R2, xx, R3]
__m128i left = [L0, xx, L1, xx, L2, xx, L3, xx]
如果我可以将它们转换为 4 x i32-s,那么使用 _mm_cvtepi32_ps 将它们转换为 f32-s 就很容易了,我就完成了。
谢谢。
最佳答案
从 16 位样本对到 32 位样本进行掩码和移位。
// clunky calling convention, but should inline ok.
__m128 unpack_leftright_16bit_channels(__m128i input, __m128 &right_retval) {
// input = [L0R0, L1R1, L2R2, L3R3] packed pairs of 16bit ints
// the one we're calling "right" is the low half of 0xLLLLRRRR
__m128i sign_extended_left = _mm_srai_epi32(input, 16);
//__m128i high_right = _mm_slli_epi32(input, 16);
//__m128i sign_extended_right = _mm_srai_epi32(high_right, 16);
__m128i sign_extended_right = _mm_madd_epi16(input, _mm_set1_epi32(0x00000001));
right_retval = _mm_cvtepi32_ps(sign_extended_right);
//__m128 right = [R0, R1, R2, R3] packed 32bit floats
__m128 left = _mm_cvtepi32_ps(sign_extended_left);
//__m128 left = [L0, L1, L2, L3] packed 32bit floats
return left;
}
这个compiles to what you'd expect with gcc5.3 ,或 clang3.7。 (命名约定:“右”输出是地址较低的输出,因此如果您正在编写矢量数据,则它位于右侧,以便右移首先向右移动,高位元素。这可能不是正确的音频 channel .)
对每个 32 位 block 的低 16 位一半进行符号扩展的另一个选项是 _mm_madd_epi16(input, _mm_set1_epi32(0x00000001))
进行加宽有符号将 ( pmaddwd
) 乘以 1
表示低半部分,将 0
表示高半部分。延迟较高,但只有单个微指令。对于上半部分,一次算术右移比使用 0x00010000
的 pmaddwd
更好。
或者按照@chtz的建议,向左移动,转换为 float ,并在后续操作中考虑额外的因子0x10000
。如果稍后可以补偿额外的因子,甚至可以通过按位 AND 来屏蔽高位字:
left = _mm_and_si128(input, _mm_set1_epi32(0xffff0000));
right = _mm_slli_epi32(input, 16);
// then convert to floats that are pre-scaled by 2^16.
转换后进行 FP 乘以 1/65536.0f
可能不值得;在大多数 CPU 上,shift + _mm_madd_epi16
会更便宜。 (不幸的是,您不能只从指数字段中减去 16,因为输入 0
会中断,并且将其设置为条件会花费更多指令。)
我提出的原始代码(使用 3 个类次)将成为大多数微架构上类次吞吐量的瓶颈(请参阅 Agner Fog's insn 表和微架构 pdf、 https://uops.info/ 以及 x86 标签 wiki 中的其他链接)。可能值得使用 SSSE3 pshufb 进行逻辑左移,仅使用实际移位指令进行算术右移,需要在每个 32- 的上半部分留下符号位的副本位元。如果没有 AVX,pshufb
会就地洗牌,就像 pslld
就地移动一样,因此它不会避免额外的 MOVDQA 指令来制作输入的第二个副本。
在 Skylake 上,立即向量移位在 p0/p1 上运行,cvtdq2ps
也是如此。使用 pshufb
进行左移会将吞吐量增加到每个时钟一个浮点输出向量,因为 shuffle 在端口 5 上运行。
在 Skylake 之前,立即向量移位仅在单个端口上运行,例如Haswell 中的 p0。至少这与 int->float 不是同一个端口:Haswell 在 p1 上运行 cvtdq2ps
。同样,pshufb 会将吞吐量增加到每个时钟一个 ps 向量。
使用 _mm_madd_epi16
的更新版本执行一次移位、一次整数乘法和两次 FP 转换。整数乘法与 Skylake 上的移位和 FP 转换(所有 p0/p1)竞争相同的端口,但至少它只有一个 uop。最近的 Zen(至少 3 和 4)在不同的端口上运行其中一些。
仅使用整数乘法进行符号扩展似乎非常浪费,但它只是 16 位,因此与运行两类制相比,希望它不会使 CPU 升温太多。在 Alder Lake E 核 (Gracemont) 上,pmaddwd
仍然是 1/时钟吞吐量,而对于 psrad
等矢量移位,吞吐量为 3/时钟。
关于audio - 使用 SIMD 指令解交错音频 channel ,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/39196188/