如何自然地平移音频样本数据?

How to pan audio sample data naturally?

我正在开发目前仅针对 Android 的 Flutter 插件。这是一种综合的东西;用户可以将音频文件加载到内存中,他们可以使用名为 Oboe.

的音频库调整音高(不是音高偏移)并以最少的延迟播放多个声音

我设法从 MediaCodec class 支持的音频文件中获取 PCM 数据,并且还通过手动访问 PCM 数组来操纵播放成功地处理了音高。

这个PCM数组存储为float数组,范围从-1.0到1.0。我现在想支持平移功能,就像什么内部Androidclass比如SoundPool。我打算了解 SoundPool 如何处理声像。在执行平移效果时,我必须将 2 个值传递给 SoundPool:左和右。这 2 个值是浮点数,必须在 0.0 到 1.0 之间。

比如我通过(1.0F,0.0F),那么用户只能用左耳听到声音。 (1.0F, 1.0F) 将是正常的(中心)。平移不是问题......直到我遇到处理立体声。我知道如何使用立体声 PCM 数据执行平移,但我不知道如何执行 自然 平移。

如果我尝试将所有声音移到左侧,则必须在左侧播放右声道的声音。相反,如果我尝试将所有声音转移到右侧,则必须在右侧播放左声道的声音。我还注意到有一个叫做 Panning Rule 的东西,这意味着当它移到一边时声音一定会大一点(大约 +3dB)。我试图找到一种方法来执行自然的平移效果,但我真的找不到算法或参考。

下面是float立体声PCM数组的结构,其实我在解码音频文件的时候并没有修改数组,所以应该是通用的结构

[left_channel_sample_0, right_channel_sample_0, left_channel_sample_1, right_channel_sample_1,
...,
left_channel_sample_n, right_channel_sample_n]

我必须像下面的 C++ 代码一样将这个 PCM 数组传递给音频流

void PlayerQueue::renderStereo(float * audioData, int32_t numFrames) {
    for(int i = 0; i < numFrames; i++) {
        //When audio file is stereo...
        if(player->isStereo) {
            if((offset + i) * 2 + 1 < player->data.size()) {
                audioData[i * 2] += player->data.at((offset + i) * 2);
                audioData[i * 2 + 1] += player->data.at((offset + i) * 2 + 1);
            } else {
                //PCM data reached end
                break;
            }
        } else {
            //When audio file is mono...
            if(offset + i < player->data.size()) {
                audioData[i * 2] += player->data.at(offset + i);
                audioData[i * 2 + 1] += player->data.at(offset + i);
            } else {
                //PCM data reached end
                break;
            }
        }

        //Prevent overflow
        if(audioData[i * 2] > 1.0)
            audioData[i * 2] = 1.0;
        else if(audioData[i * 2] < -1.0)
            audioData[i * 2] = -1.0;

        if(audioData[i * 2 + 1] > 1.0)
            audioData[i * 2 + 1] = 1.0;
        else if(audioData[i * 2 + 1] < -1.0)
            audioData[i * 2 + 1] = -1.0;
    }

    //Add numFrames to offset, so it can continue playing PCM data in next session
    offset += numFrames;

    if(offset >= player->data.size()) {
        offset = 0;
        queueEnded = true;
    }
}

为了简化代码,我排除了播放操作的计算。如您所见,我必须手动将 PCM 数据传递给 audioData 浮点数组。我正在添加 PCM 数据以执行混合多种声音,包括相同的声音。

  1. 这个PCM阵列如何实现声像效果?如果能按照SoundPool的机制就好了,不过只要我能正常的做panning效果就好了。 (例如:平移值可以是 -1.0 到 1.0,0 表示居中)

  2. 应用Panning Rule时,PCM和分贝有什么关系?我知道如何使声音变大,但我不知道如何以精确的分贝使声音变大。这个有公式吗?

泛规则或泛法的实施因制造商而异。

一种经常使用的实现方式是,当声音完全向一侧移动时,该侧以最大音量播放,而另一侧则完全衰减。如果声音在中间播放,两边大约衰减3分贝。

为此,您可以将声源乘以计算出的振幅。例如(未经测试的伪代码)

player->data.at((offset + i) * 2) * 1.0; // left signal at full volume
player->data.at((offset + i) * 2 + 1) * 0.0; // right signal fully attenuated

要获得所需的振幅,您可以对左声道使用 sin 函数,对右声道使用 cos 函数。

注意当sin和cos的输入为pi/4时,两边的振幅都是0.707。这将使您在两侧衰减约 3 分贝。

所以剩下要做的就是将范围 [-1, 1] 映射到范围 [0, pi/2] 例如假设 pan 的值在 [-1, 1] 范围内。 (未经测试的伪代码)

pan_mapped = ((pan + 1) / 2.0) * (Math.pi / 2.0);

left_amplitude = sin(pan_mapped);
right_amplitude = cos(pan_mapped); 

更新:

另一个经常使用的选项(例如 ProTools DAW)是在每一侧都有一个声相设置。有效地将立体声源视为 2 个单声道源。这允许您在立体声场中自由放置左声源而不影响右声源。

为此,您需要:(未经测试的伪代码)

left_output  += left_source(i)  * sin(left_pan)
right_output += left_source(i)  * cos(left_pan)
left_output  += right_source(i) * sin(right_pan)
right_output += right_source(i) * cos(right_pan)

这 2 个声相的设置由操作员决定,并取决于录音和所需的效果。 如何将其映射到单个平移控件取决于您。我只是建议,当平移为 0(居中)时,左声道仅在左侧播放,右声道仅在右侧播放。否则你会干扰原来的立体声录音。

一种可能是段 [-1, 0) 控制右侧平移,而左侧保持不变。 [0, 1].

反之亦然
hPi = math.pi / 2.0
  
def stereoPan(x):
    if (x < 0.0):
        print("left source:")
        print(1.0) # amplitude to left channel
        print(0.0) # amplitude to right channel
        print("right source:")
        print(math.sin(abs(x) * hPi)) # amplitude to left channel
        print(math.cos(abs(x) * hPi)) # amplitude to right channel

    else:
        print("left source:")
        print(math.cos(x * hPi)) # amplitude to left channel
        print(math.sin(x * hPi)) # amplitude to right channel  
        print("right source:")
        print(0.0) # amplitude to left channel
        print(1.0) # amplitude to right channel

以下内容并不意味着与@ruff09 给出的优秀答案中的任何内容相矛盾。我只是想添加一些我认为在尝试模拟平移时相关的想法和理论。

我想指出,简单地使用体积差异有几个缺点。首先,它与现实世界的现象不符。想象一下,你正走在人行道上,马上就在街上,在你的右边,是一个拿着手提钻的工人。我们可以使右侧的声音音量为 100%,左侧的音量为 0%。但实际上,我们从该来源听到的大部分声音也来自左耳,淹没了其他声音。

如果您忽略左耳音量以使手提钻获得最大的右声像,那么即使是左侧的微小声音也能听到(这很荒谬),因为它们不会与左侧的手提钻内容竞争追踪。如果您确实为手提钻设置了左耳音量,那么基于音量的声相效果会将位置更偏向中心。进退两难!

在这种情况下,我们的耳朵如何区分位置?我知道有两个过程可以合并到平移算法中,使平移更加“自然”。一个是过滤组件。与小于我们头部宽度的波长匹配的高频会衰减。因此,您可以为声音添加一些差分低通滤波。另一个方面是,在我们的场景中,电钻声音在到达左耳之前几毫秒到达右耳。因此,您还可以根据平移角度添加一点延迟。基于时间的平移效果最明显地适用于波长大于我们头部的频率内容(例如,一些高通滤波也将是一个组件)。

关于我们耳朵的形状如何对声音产生不同的过滤效果,也有大量的研究。我认为我们在成长过程中通过下意识地将不同音色与不同位置相关联(尤其是与高度和前后立体声问题有关)来学会使用它。

不过计算成本很高。因此,诸如坚持纯粹基于振幅的声相这样的简化是常态。因此,对于 3D 世界中的声音,最好为需要动态位置更改的项目选择单声道源内容,并且仅将立体声内容用于背景音乐或不需要基于播放器位置的动态平移的环境内容。

我想做一些更多的实验,结合动态的基于时间的平移和一点幅度,看看这是否可以有效地用于立体声提示。实现动态延迟有点棘手,但没有过滤那么昂贵。我想知道是否有办法记录声源(对其进行预处理),使其更易于结合实时滤波器和基于时间的操作,从而产生有效的声相。