使用 SIMD 指令去交错音频通道
Posted
技术标签:
【中文标题】使用 SIMD 指令去交错音频通道【英文标题】:De-interleave audio channels using SIMD instructions 【发布时间】:2016-08-28 22:00:07 【问题描述】:我正在实现一个混音器,它在没有 SIMD 指令的情况下运行良好,但很难弄清楚如何将我的声音数据提取到单独的通道中。
我的数据采用交错格式:L0R0 L1R1 L2R2 L3R3... 我以相同的格式将它们加载到 __m128i 中,因此我在寄存器中有 4 个样本。
我希望它们位于不同的通道中:L0L1L2L3 R0R1R2R3。这是我缺少的部分。
所以输入是:8 x i16(4xi32 交错) 我希望输出为 left = 4 x f32 和 right = 4 x f32,然后进行混合。
混合后,我可以交错通道,我得到 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 会很容易,我就完成了。
谢谢。
【问题讨论】:
我认为转换低/高i16
s 的最佳选择是位移。 high_halves = _mm_srai_epi32(packed, 16);
。由于您的值是有符号的,您可能需要通过左移然后使用算术右移来对低半部分进行符号扩展。我想不出更好的 ATM 方式,但这确实有点笨重。
一些 SIMD 指令集(如 ARM NEON / ARMv8)有更多的双输入或双输出指令,我认为可以用一条指令解包。 (也许解压缩,IIRC)。因此,重要的是要专门针对英特尔 SSE,而不仅仅是任何 SIMD。
你能要求SSE3吗?虽然SSE/SSE2 对于现代 PC 的方式不是“必需的”,但它很常见(对于 Steam 游戏玩家来说,它是 91%)。 _mm_moveldup_ps
和 _mm_movehdup_ps
非常有用。
@ChuckWalbourn:你想用 SSE3 复制偶数或奇数元素是什么? OP 所需的输出不需要将任何数据移到它们开始的 32 位元素之外。起初我在设想使用 shufps
组合来自两个向量的数据,但后来我更仔细地阅读,并且中间代码块是显示输入和所需输出的代码块。
@PeterCordes 谢谢,_mm_srai_epi32
给了我左声道值; _mm_slli_epi32
16 然后_mm_srai_epi32
16 给了我正确的通道值,将它们转换为f32
-s 是直截了当的。如果可以的话,我会接受您的评论作为答案,或者我会在我在家进行测试时发布代码。谢谢!
【参考方案1】:
从 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
__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);
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。
这将成为大多数微架构上 shuffle 吞吐量的瓶颈(请参阅 Agner Fog's insn tables and microarch pdf 和 x86 标签 wiki 中的其他链接)。可能值得使用 SSSE3 pshufb
进行逻辑左移,仅使用实际移位指令进行算术右移,需要在每个 32 位元素的上半部分保留符号位的副本。如果没有 AVX,pshufb
就地洗牌,就像 pslld
就地移动一样(感谢 Intel :(),因此它不会避免额外的 MOV 指令来制作输入的第二个副本。
在 Skylake 上,立即向量移位在 p0/p1 上运行,cvtdq2ps
也是如此。使用pshufb
进行左移可以将吞吐量提高到每个时钟一个浮点输出向量,因为随机播放在端口 5 上运行。
在 Skylake 之前,立即向量移位仅在单个端口上运行,例如哈斯韦尔的 p0。至少这与 int->float 的端口不同:Haswell 在 p1 上运行 cvtdq2ps
。同样,pshufb 会将吞吐量提高到每个时钟一个 ps 向量。
似乎应该有更好的方法来做到这一点,比如使用 AND 掩码或其他东西。但似乎 2 次移位或 shuffle+shift 是将每个 32 位元素的低 16 位符号扩展为完整 32 位元素的最佳方式。
【讨论】:
对低位字进行符号扩展的可能替代方法:((x^0x8000)&0xFFFF)-0x8000
(尽管需要 3 微秒而不是 2 微秒)。或者实际上,向左移动,转换为浮点数,并在后续操作中考虑0x10000
的额外因素。如果以后可以补偿额外的因素,那么高位字甚至可以被一个位与掩码。以上是关于使用 SIMD 指令去交错音频通道的主要内容,如果未能解决你的问题,请参考以下文章
使用 libsndfile 为 MATLAB 编写多声道音频