Как выровнять стереовход и применить звуковой эффект только к одному каналу на iOS?

мне нужно обработать стереофонический аудиофайл на iOS следующим образом:

  • оба канала должны иметь одинаковую интенсивность, то есть. сделайте стерео выглядеть как моно
  • маршрута моно аудио на левый и правый каналы
  • применить эффекты к аудио, которое выводится на правый канал

в настоящее время у меня есть:

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

эффект является подклассом AVAudioUnitEffect.

у меня проблемы создание стереовхода в виде моно и вывод AVAudioPlayerNode на отдельные каналы.

Я попытался установить громкость PlayerNodes в 0.5 и pan в -1.0 и 1.0, но, поскольку вход стерео, это не дает желаемых эффектов.

С AVFoundation, я полагаю, что у меня есть по крайней мере два варианта: либо я...

(1) выровняйте каналы для PlayerNodes, чтобы оба PlayerNodes выглядели как моно-после чего я мог бы использовать ту же логику, что и раньше: имея равный объем на обоих PlayerNodes, другое панорамирование влево и вправо и применение эффекта на одном PlayerNode, после MixerNode, результат эффект появляется только в правом выходном канале.

(2) Держите PlayerNodes как стерео (pan = 0.0), применяйте эффект только к одному PlayerNode, а затем скажите MixerNode использовать оба канала одного PlayerNode в качестве источника для левого канала и каналов другого для правого канала. Я полагаю, что тогда MixerNode эффективно уравнял бы вход каналы, так что это будет выглядеть как вход моно и эффект можно услышать только от одного выходного канала.

вопрос в том, возможна ли любая из вышеупомянутых стратегий и как? Есть ли другой вариант, который я упустил?

Я использую Swift для проекта, но могу справиться с Objective-C.


судя по отсутствию ответов и моим собственным исследованиям, мне кажется, что AVFoundation может быть не так. Простота использования AVFoundation заманчиво, но я открыт для альтернатив. В настоящее время я исследую MTAudioProcessingTap-классах и они могут быть полезны. Помощь по-прежнему ценится.

1 ответов


мне удалось получить желаемый результат, используя два AVPlayers, которые я играю одновременно. Один AVPlayer имеет вход, который имеет усредненные звуковые данные на левом канале и тишину справа; и наоборот в другом AVPlayer. Наконец, эффект применяется только к одному экземпляру AVPlayer.

поскольку применение проприетарного эффекта на экземпляре AVPlayer оказалось тривиальным, самым большим препятствием было выравнивание стереовхода.

я нашел пару смежных вопросы (панорамирование моносигнала с помощью MultiChannelMixer & MTAudioProcessingTap, AVPlayer воспроизведение одноканального аудио стерео→моно) и учебник (обработка аудио AVPlayer с MTAudioProcessingTap - на который ссылались почти во всех других учебниках, которые я пытался google), все из которых указали, что решение, вероятно, находится в MTAudioProcessingTap.

к сожалению, официальная документация для MTAudioProcessing tap (или любой другой аспект MediaToolbox) более или менее шь. Я имею в виду, только пример кода был найден онлайн и заголовки (MTAudioProcessingTap.h) через Xcode. Но с вышеупомянутый учебник мне удалось запустить.

чтобы сделать вещи не слишком легкими, я решил использовать Swift, а не Objective-C, в котором были доступны существующие учебники. Преобразование вызовов было не так уж плохо, и я даже нашел почти готов пример создание MTAudioProcessingTap в Swift 2. Мне удалось зацепиться за обработку кранов и слегка манипулировать аудио с ним (ну, я мог бы вывести поток как есть и полностью обнулить его, по крайней мере). Однако выравнивание каналов было задачей для ускорить рамки, а именно vDSP часть.

однако, используя API C, которые широко используют указатели (case in point: vDSP) с Swift становится громоздким, а быстро!--11--> - по крайней мере, по сравнению с тем, как это делается с Objective-C. Это также было проблемой, когда я изначально писал MTAudioProcessingTaps в Swift: я не мог передать AudioTapContext без сбоев (в Obj-C получение контекста так же просто, как AudioTapContext *context = (AudioTapContext *)MTAudioProcessingTapGetStorage(tap);) и все UnsafeMutablePointers заставили меня думать, что Swift не является правильным инструментом для работы.

так, для класса обработки, я угробил Свифт и переработать это в Objective-С.
И, как упоминалось ранее, я использую два AVPlayers; так в AudioPlayerController.Свифт У меня:

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);
    }
}