我需要在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保持为立体声(pan = 0.0),仅对一个PlayerNode应用效果,然后告诉MixerNode使用一个PlayerNode的两个通道作为左声道的源,另一个通道用于右声道。我想,然后MixerNode将有效地均衡输入通道,因此它会显示为输入为单声道,只能从一个输出通道听到效果。
问题是:上述任何一种策略是否可能以及如何?还有其他选择我忽略了吗?
我正在使用Swift进行项目,但可以应对Objective-C。
由于缺乏回应和我自己的研究,在我看来,AVFoundation可能不是可行的方法。使用AVFoundation的简单性很诱人,但我对其他选择持开放态度。目前我正在研究MTAudioProcessingTap
- 类,它们可能有用。帮助仍然受到赞赏。
答案 0 :(得分:4)
我通过使用两个同时播放的AVPlayers设法获得了预期的结果。一个AVPlayer的输入在左声道上有平均音频数据,在右边有静音;反之亦然在另一个AVPlayer中。最后,效果仅应用于一个AVPlayer实例。
由于在AVPlayer实例上应用专有效果被证明是微不足道的,最大的障碍是如何均衡立体声输入。
我发现了几个相关的问题(Panning a mono signal with MultiChannelMixer & MTAudioProcessingTap,AVPlayer playback of single channel audio stereo→mono)和一个教程(Processing AVPlayer’s audio with MTAudioProcessingTap - 几乎所有其他我尝试谷歌的教程都参考了)这表明解决方案可能位于MTAudioProcessingTap。
可悲的是,MTAudioProcessing tap(或MediaToolbox的任何其他方面)的官方文档或多或少 nil。我的意思是,只有some sample code在线找到并且标题(MTAudioProcessingTap.h) )通过Xcode。但随着aforementioned tutorial我成功了。
为了让事情变得不那么容易,我决定使用Swift而不是Objective-C,其中现有的教程可用。转换电话并没有那么糟糕,我甚至找到了几乎准备好的example of creating MTAudioProcessingTap in Swift 2。我确实设法挂钩处理水龙头并用它轻轻地操纵音频(好吧 - 我可以按原样输出流并至少完全归零)。然而,为了均衡频道,Accelerate framework是vDSP的一项任务,即gets cumbersome rather quickly部分。
但是,使用广泛使用Swift in the tutorial的指针(例如:vDSP)的C API - 至少与使用Objective-C的方式相比。当我最初在Swift中编写MTAudioProcessingTaps时,这也是一个问题:我无法在没有失败的情况下传递AudioTapContext(在Obj-C中获取上下文就像AudioTapContext *context = (AudioTapContext *)MTAudioProcessingTapGetStorage(tap);
一样简单)并且所有UnsafeMutablePointers让我认为Swift不是'这是工作的正确工具。
因此,对于处理类,我抛弃了Swift并在Objective-C中重构了它 并且,如前所述,我正在使用两个AVPlayers;所以在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回调的创建方式与ref几乎相同。但是在创建时,我将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);
}
}