Sono riuscito a ottenere il risultato desiderato utilizzando due AVPlayer che suono contemporaneamente. È stato introdotto un AVPlayer che ha mediato i dati audio sul canale sinistro e il silenzio sulla destra; e viceversa nell'altro AVPlayer. Infine, l'effetto viene applicato solo a un'istanza di AVPlayer.
Poiché applicare l'effetto proprietario su un'istanza di AVPlayer si è rivelata banale, l'ostacolo più grande è stato come equalizzare l'input stereo.
ho trovato un paio di domande correlate (Panning a mono signal with MultiChannelMixer & MTAudioProcessingTap, AVPlayer playback of single channel audio stereo→mono) e un tutorial (Processing AVPlayer’s audio with MTAudioProcessingTap - che è stato riferito a in quasi tutti gli altri tutorial che ho cercato di google) ognuno dei quali ha indicato la soluzione probabilmente si trova all'interno MTAudioProcessingTap.
Purtroppo, la documentazione ufficiale per il tocco MTAudioProcessing (o qualsiasi altro aspetto di MediaToolbox) è più o meno nil. Voglio dire, solo some sample code è stato trovato online e le intestazioni (MTAudioProcessingTap.h) tramite Xcode. Ma con il aforementioned tutorial sono riuscito a iniziare.
Per rendere le cose non troppo facili, ho deciso di utilizzare Swift, anziché Objective-C, in cui erano disponibili tutorial esistenti. La conversione delle chiamate non era poi così male e ho persino trovato quasi pronto example of creating MTAudioProcessingTap in Swift 2. Sono riuscito ad agganciare i tap e a manipolare leggermente l'audio con esso (beh, potrei emettere il flusso così com'è e azzerarlo completamente, almeno). Per equalizzare i canali, tuttavia, era un compito per lo Accelerate framework, ovvero la parte di vDSP.
Tuttavia, utilizzando le API C che utilizzano ampiamente i puntatori (caso in punto: vDSP) con Swift gets cumbersome rather quickly - almeno il confronto con il modo in cui è fatto con Objective-C. Questo è stato anche un problema quando ho inizialmente scritto MTAudioProcessingTaps in Swift: non potevo passare AudioTapContext senza errori (in Obj-C ottenere il contesto è facile come AudioTapContext *context = (AudioTapContext *)MTAudioProcessingTapGetStorage(tap);
) e tutti gli UnsafeMutablePointers mi hanno fatto pensare che Swift non è lo strumento giusto per il lavoro.
Quindi, per la classe di elaborazione, ho abbandonato Swift e l'ho refactored in Objective-C.
E, come accennato in precedenza, sto usando due AVPlayer; così in AudioPlayerController.swift ho:
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()
io uso “TapType” per i canali distinti ed è definita (in Objective-C), il più semplice:
typedef NS_ENUM(NSUInteger, TapType) {
TapTypeL = 0,
TapTypeR = 1
};
callback MTAudioProcessingTap vengono creati abbastanza allo stesso modo di in the tutorial. Sulla creazione, però, risparmio il TapType al contesto in modo da poter controllare in ProcessCallback:
static void tap_InitLeftCallback(MTAudioProcessingTapRef tap, void *clientInfo, void **tapStorageOut) {
struct AudioTapContext *context = calloc(1, sizeof(AudioTapContext));
context->channel = TapTypeL;
*tapStorageOut = context;
}
E, infine, il sollevamento pesi reale accade nel callback di processo con funzioni 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);
}
}
Ci può spiegare che cosa intendi quando dici "fai apparire lo stereo come mono", per favore? – mark
Ho un file audio stereo. Quando si suona normalmente il canale sinistro viene emesso sul canale sinistro e sul canale destro verso destra; se faccio una panoramica a sinistra, sento solo ciò che è sul canale sinistro e viceversa. Voglio prendere entrambi i canali dell'audio ed emettere entrambi i canali (= media: '(L + R)/2') sul canale sinistro e sul canale destro. Dovrebbe essere come se avessi un audio mono che viene emesso indipendentemente su ciascun canale. Ora, se eseguo il pan dell'audio, non dovrebbe esserci alcuna differenza. Dopo questo, applicherei un effetto solo al canale destro, quindi se faccio una panoramica, sento la normale versione mono sulla sinistra e la versione mono modificata sulla destra. –