如何均衡立体声输入并将音频效果仅应用于 iOS 上的单个通道?

How to equalize stereo input and apply audio effect only to single channel on iOS?

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


我目前拥有的是:

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

该效果是 AVAudioUnitEffect 的子类。

我无法将立体声输入显示为单声道并将 AVAudioPlayerNode 输出到单独的通道。

我尝试将 PlayerNodes 的音量设置为 0.5 并将声像设置为 -1.0 和 1.0,但是,由于输入是立体声,这不会产生预期的效果。

有了 AVFoundation,我想我至少有两个选择:要么我…

(1) 均衡 PlayerNode 的通道,使两个 PlayerNode 都显示为单声道 — 之后我可以使用与以前相同的逻辑:在两个 PlayerNode 上具有相同的音量,其他向左和向右平移,并将效果应用于一个PlayerNode 将在 MixerNode 之后导致效果仅出现在右输出通道中。

(2) 保持 PlayerNodes 为立体声 (pan = 0.0),仅在一个 PlayerNode 上应用效果,然后告诉 MixerNode 使用一个 PlayerNode 的两个通道作为左通道的源,另一个的通道作为左通道的源右声道。我想 MixerNode 会有效地均衡输入通道,因此它会显示为单声道输入并且只能从一个输出通道听到效果。

问题是:上述任何一种策略是否可行,如何实现?还有其他我忽略的选择吗?

我正在为项目使用 Swift,但可以应付 Objective-C。


从缺乏回应和我自己的研究来看,在我看来 AVFoundation 可能不是要走的路。使用 AVFoundation 的简单性很诱人,但我对替代方案持开放态度。目前我正在研究 MTAudioProcessingTap-类,它们可能会有用。仍然感谢您的帮助。

我通过使用同时播放的两个 AVPlayer 设法获得了预期的结果。一个 AVPlayer 的输入在左声道具有平均音频数据,在右声道具有静音;在另一个 AVPlayer 中反之亦然。最后,效果仅应用于一个 AVPlayer 实例。

事实证明,在 AVPlayer 实例上应用专有效果是微不足道的,最大的障碍是如何均衡立体声输入。

我发现了几个相关问题(Panning a mono signal with MultiChannelMixer & MTAudioProcessingTap, AVPlayer playback of single channel audio stereo→mono) and a tutorial (Processing AVPlayer’s audio with MTAudioProcessingTap — 在我尝试的几乎所有其他教程中都提到了 google)所有这些都表明解决方案可能在 MTAudioProcessingTap 中。

遗憾的是,MTAudioProcessing tap(或 MediaToolbox 的任何其他方面)的官方文档或多或少 nil。 我的意思是,只有 some sample code was found online and the headers (MTAudioProcessingTap.h) through Xcode. But with the aforementioned tutorial 我设法开始了。

为了让事情变得不太容易,我决定使用 Swift,而不是 Objective-C,其中现有教程可用。转换呼叫并没有那么糟糕,我什至发现它的 example of creating MTAudioProcessingTap in Swift 2. I did manage to hook on processing taps and lightly manipulate audio with it (well—I could output the stream as-is and zero it out completely, at least). To equalize the channels, however, was a task for the Accelerate framework, namely the vDSP 部分几乎准备就绪。

然而,使用广泛使用指针的 C API(典型例子:vDSP)和 Swift gets cumbersome rather quickly——至少与 Objective-C 的处理方式相比。当我最初在 Swift 中编写 MTAudioProcessingTaps 时,这也是一个问题:我无法在没有失败的情况下传递 AudioTapContext(在 Obj-C 中,获取上下文就像 AudioTapContext *context = (AudioTapContext *)MTAudioProcessingTapGetStorage(tap); 一样简单)并且所有 UnsafeMutablePointers 让我认为 Swift 不是这项工作的正确工具。

因此,对于处理 class,我放弃了 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 回调的创建方式与 in the tutorial 几乎相同。不过,在创建时,我将 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);
    }
}