如何在iOS上使立体声输入均衡并仅对单声道应用音频效果?

9

我需要在iOS上处理立体声音频文件,具体要求如下:

  • 两个声道的强度应该相等,即将立体声转换为单声道
  • 将单声道音频路由到左右两个声道
  • 对输出到右声道的音频应用效果

目前我所拥有的是:

            +-------------------+
            | AVAudioPlayerNode +------------------------+
            +--------^----------+                        |
                     |                                   |
            +--------+---------+                +--------v---------+
    File ---> AVAudioPCMBuffer |                | AVAudioMixerNode +---> Output
            +--------+---------+                +--------^---------+
                     |                                   |
            +--------v----------+  +-------------------+ |
            | AVAudioPlayerNode +--> AVAudioUnitEffect +-+
            +-------------------+  +-------------------+

效果是AVAudioUnitEffect的子类。
我遇到了问题,无法将立体声输入转换为单声道,并将AVAudioPlayerNode输出到不同的通道。
我尝试将PlayerNodes的音量设置为0.5,平移为-1.0和1.0,但由于输入是立体声,这不会产生所需的效果。
使用AVFoundation,我想至少有两个选择:要么...
(1)使PlayerNodes的通道均衡,以便两个PlayerNodes都显示为单声道 - 之后我可以使用与之前相同的逻辑:在两个PlayerNodes上具有相等的音量,其他左右平移并在一个PlayerNode上应用效果后,通过MixerNode,结果仅在右输出通道中出现效果。
(2)保持PlayerNodes为立体声(平移= 0.0),仅在一个PlayerNode上应用效果,然后告诉MixerNode使用一个PlayerNode的两个通道作为左通道的源,另一个通道作为右通道。然后我认为MixerNode将有效地均衡输入通道,因此它将显示为输入为单声道,效果只能从一个输出通道中听到。
问题是:上述策略是否可行?如何操作?是否有其他选项我没有考虑?
我正在项目中使用Swift,但也可以使用Objective-C。
根据缺乏响应和我的研究,我认为AVFoundation可能不是正确的选择。使用AVFoundation的简单性很诱人,但我也愿意尝试其他方法。目前我正在研究MTAudioProcessingTap类,它们可能有用。仍然需要帮助。

1
请问您说“使立体声变为单声道”是什么意思?能解释一下吗? - Mark
我有一个立体声音频文件。正常播放时,左声道输出到左声道,右声道输出到右声道;如果我向左平移,则只听到左声道的内容,反之亦然。我想将音频的两个声道都输出到左声道和右声道(即平均值:(L + R) / 2),就像我有一个独立输出到每个声道的单声道音频一样。现在,如果我平移音频,不应该有任何区别。之后,我会仅对右声道应用一个效果,因此,如果我平移,我会在左侧听到正常的单声道版本,在右侧听到修改后的单声道版本。 - Jari Keinänen
1个回答

6
我成功地使用了两个AVPlayer同时播放来达到期望的效果。一个AVPlayer在左声道上有平均音频数据和右声道上有静音,而另一个AVPlayer则相反。最后,该效果仅应用于一个AVPlayer实例。
由于将专有效果应用于AVPlayer实例非常简单,因此最大的障碍是如何使立体声输入均衡。
我发现了一些相关问题(Panning a mono signal with MultiChannelMixer & MTAudioProcessingTapAVPlayer playback of single channel audio stereo→mono)和教程(Processing AVPlayer’s audio with MTAudioProcessingTap——几乎所有我尝试谷歌搜索的其他教程都引用了它),它们都表明解决方案可能在MTAudioProcessingTap中。

可悲的是,MTAudioProcessing tap(或MediaToolbox的任何其他方面)的官方文档几乎为零。我的意思是,只有一些示例代码可以在网上找到,并且通过Xcode找到了标题(MTAudioProcessingTap.h)。但是通过上述教程,我设法开始了。

为了不让事情变得太容易,我决定使用Swift而不是Objective-C,因为已经有现成的教程可用。转换调用并不那么困难,我甚至找到了一个几乎准备好的在Swift 2中创建MTAudioProcessingTap示例。我成功地挂接了处理tap,并轻松操纵音频(嗯——我至少可以输出流并将其完全归零)。然而,均衡通道是Accelerate框架的任务,即其中的vDSP部分。
然而,使用大量指针的C APIs(例如:vDSP)与Swift一起使用变得很快繁琐——至少与Objective-C相比是如此。这也是当我最初用Swift编写MTAudioProcessingTaps时遇到的问题:我无法在不失败的情况下传递AudioTapContext(在Obj-C中,获取上下文就像AudioTapContext *context = (AudioTapContext *)MTAudioProcessingTapGetStorage(tap);这样简单),而且所有的UnsafeMutablePointers让我觉得Swift并不适合这项工作。

因此,对于处理类,我放弃了Swift并在Objective-C中进行了重构。
正如之前提到的,我正在使用两个AVPlayer;所以在AudioPlayerController.swift中我有:

var left = AudioTap.create(TapType.L)
var right = AudioTap.create(TapType.R)

asset = AVAsset(URL: audioList[index].assetURL!) // audioList is [MPMediaItem]. asset is class property

let leftItem = AVPlayerItem(asset: asset)
let rightItem = AVPlayerItem(asset: asset)

var leftTap: Unmanaged<MTAudioProcessingTapRef>?
var rightTap: Unmanaged<MTAudioProcessingTapRef>?

MTAudioProcessingTapCreate(kCFAllocatorDefault, &left, kMTAudioProcessingTapCreationFlag_PreEffects, &leftTap)
MTAudioProcessingTapCreate(kCFAllocatorDefault, &right, kMTAudioProcessingTapCreationFlag_PreEffects, &rightTap)

let leftParams = AVMutableAudioMixInputParameters(track: asset.tracks[0])
let rightParams = AVMutableAudioMixInputParameters(track: asset.tracks[0])
leftParams.audioTapProcessor = leftTap?.takeUnretainedValue()
rightParams.audioTapProcessor = rightTap?.takeUnretainedValue()

let leftAudioMix = AVMutableAudioMix()
let rightAudioMix = AVMutableAudioMix()
leftAudioMix.inputParameters = [leftParams]
rightAudioMix.inputParameters = [rightParams]
leftItem.audioMix = leftAudioMix
rightItem.audioMix = rightAudioMix

// leftPlayer & rightPlayer are class properties
leftPlayer = AVPlayer(playerItem: leftItem)
rightPlayer = AVPlayer(playerItem: rightItem)
leftPlayer.play()
rightPlayer.play()

我使用“TapType”来区分通道,它的定义(在Objective-C中)非常简单:

typedef NS_ENUM(NSUInteger, TapType) {
    TapTypeL = 0,
    TapTypeR = 1
};

MTAudioProcessingTap回调函数的创建方式与教程中几乎相同。不过,在创建时,我会将TapType保存到上下文中,以便在ProcessCallback中进行检查:

static void tap_InitLeftCallback(MTAudioProcessingTapRef tap, void *clientInfo, void **tapStorageOut) {
    struct AudioTapContext *context = calloc(1, sizeof(AudioTapContext));
    context->channel = TapTypeL;
    *tapStorageOut = context;
}

最后,实际的举重是在使用vDSP函数的过程回调中进行的:

static void tap_ProcessCallback(MTAudioProcessingTapRef tap, CMItemCount numberFrames, MTAudioProcessingTapFlags flags, AudioBufferList *bufferListInOut, CMItemCount *numberFramesOut, MTAudioProcessingTapFlags *flagsOut) {
    // output channel is saved in context->channel
    AudioTapContext *context = (AudioTapContext *)MTAudioProcessingTapGetStorage(tap);

    // this fetches the audio for processing (and for output)
    OSStatus status;    
    status = MTAudioProcessingTapGetSourceAudio(tap, numberFrames, bufferListInOut, flagsOut, NULL, numberFramesOut);

    // NB: we assume the audio is interleaved stereo, which means the length of mBuffers is 1 and data alternates between L and R in `size` intervals.
    // If audio wasn’t interleaved, then L would be in mBuffers[0] and R in mBuffers[1]
    uint size = bufferListInOut->mBuffers[0].mDataByteSize / sizeof(float);
    float *left = bufferListInOut->mBuffers[0].mData;
    float *right = left + size;

    // this is where we equalize the stereo
    // basically: L = (L + R) / 2, and R = (L + R) / 2
    // which is the same as: (L + R) * 0.5
    // ”vasm” = add two vectors (L & R), multiply by scalar (0.5)
    float div = 0.5;
    vDSP_vasm(left, 1, right, 1, &div, left, 1, size);
    vDSP_vasm(right, 1, left, 1, &div, right, 1, size);

    // if we would end the processing here the audio would be virtually mono
    // however, we want to use distinct players for each channel, so here we zero out (multiply the data by 0) the other
    float zero = 0;
    if (context->channel == TapTypeL) {
        vDSP_vsmul(right, 1, &zero, right, 1, size);
    } else {
        vDSP_vsmul(left, 1, &zero, left, 1, size);
    }
}

1
感谢您的研究!这是受您答案启发的一些工作实现 https://github.com/akovalov/stereo-audioplayer-ios - smartwolf

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接