audio - 使用 SIMD 指令解交错音频 channel

标签 audio x86 sse simd intrinsics

我正在实现一个混音器,它无需 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 表示高半部分。延迟较高,但只有单个微指令。对于上半部分,一次算术右移比使用 0x00010000pmaddwd 更好。

或者按照@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/ 以及 标签 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/

相关文章:

c++ - 如何使用 MSVC++ for x86-32 获得有效的 asm 来归零一个微小的结构?

assembly - 进行水平 SSE 向量求和(或其他缩减)的最快方法

c++ - 尝试初始化 __m128 类成员变量时出现 EXC_BAD_ACCESS 信号

java - 同步发出声音

audio - flac 编解码器,2 个文件,持续时间相同,但内存大小不同

c# - 使用Uri处理.wav文件

assembly - RSP中的R代表什么?

.net - 可以将 WinForms 应用程序配置为作为 "x86"运行而无需重新编译吗?

go - SSE2 从 golang 中的打包数据中提取 float

安卓 Java : how to disable microphone input while streaming system audio?