使用 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 会很容易,我就完成了。

谢谢。

【问题讨论】:

我认为转换低/高i16s 的最佳选择是位移。 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)&amp;0xFFFF)-0x8000(尽管需要 3 微秒而不是 2 微秒)。或者实际上,向左移动,转换为浮点数,并在后续操作中考虑0x10000 的额外因素。如果以后可以补偿额外的因素,那么高位字甚至可以被一个位与掩码。

以上是关于使用 SIMD 指令去交错音频通道的主要内容,如果未能解决你的问题,请参考以下文章

使用 libsndfile 为 MATLAB 编写多声道音频

从线性 PCM 中提取音频通道

使用 SIMD 对半字节的去交错向量

向量体系结构----SIMD指令集扩展和GPU

我需要有关 gstreamer-0.10 上的音频“交错”的帮助

gstreamer:交错 2 个音频 - 链接错误