4

I have an AVPlayer that plays audio from a mono AVAsset, doing some processing via an audio processing tap along the way. How can I convert this asset to stereo before reaching the tap? The second channel can be empty or a copy of the first channel (it’ll get filled up manually in the tap).

I've tried converting the mono to stereo within the tap, but apparently we have no control over the ASBD or AudioBufferList structure once we're inside the tap. I've also done offline conversion, but this presents big obstacles (can be quite slow, not suitable for web streaming).

Here is the barebones (but complete) code which you can use with any mono audio file. You'll see that by the time it hits the processing tap, there’s just the one channel available instead of the desired two channels. To use the code, you just need to add the MediaPlayer and TapProcessor classes below to a blank Single View Application, use the following ViewController code in place of the default code, and add in your own mono audio file to your project. Thanks for reading.

MediaPlayer.h

#import <Foundation/Foundation.h>

@interface MediaPlayer : NSObject

@end

MediaPlayer.m

#import "MediaPlayer.h"
#import "TapProcessor.h"
#import <AVFoundation/AVFoundation.h>

@interface MediaPlayer()

@property (nonatomic, strong) AVAsset *asset;
@property (nonatomic, strong) AVPlayer *player;
@property (nonatomic, strong) TapProcessor *audioTapProcessor;

@end


@implementation MediaPlayer

- (id)init {
    if (self = [super init]){
        NSString *path = [[NSBundle mainBundle] pathForResource:@"MonoSource"
                                                         ofType:@"mp3"];
        [self loadFileWithPath:path];
    }
    return self;
}

-(void)loadFileWithPath:(NSString*)path{
    NSURL *fileURL = [NSURL fileURLWithPath:path];
    NSDictionary *options = [NSDictionary dictionaryWithObject:[NSNumber numberWithBool:YES]
                                                        forKey:AVURLAssetPreferPreciseDurationAndTimingKey];
    self.asset = [AVURLAsset URLAssetWithURL:fileURL options:options];
    [self.asset loadValuesAsynchronouslyForKeys:@[@"tracks"] completionHandler:^{
        dispatch_async(dispatch_get_main_queue(), ^{
            AVKeyValueStatus status = [self.asset statusOfValueForKey:@"tracks" error:nil];
            switch (status) {
                case AVKeyValueStatusLoaded:
                    [self setupPlayer];
                    break;
                default:
                    break;
            }
        });
    }];
}

- (void) setupPlayer{

    AVPlayerItem *item = [AVPlayerItem playerItemWithAsset:self.asset];

    AVAssetTrack *audioTrack = [[self.asset tracksWithMediaType:AVMediaTypeAudio] objectAtIndex:0];
    [self printInfoForTrack:audioTrack];
    TapProcessor *newProcessor = [[TapProcessor alloc] initWithTrack:audioTrack];

    AVAudioMix *audioMix = [newProcessor audioMix];
    item.audioMix = audioMix;

    self.player = [AVPlayer playerWithPlayerItem:item];

    [self.player play];
}

-(void) printInfoForTrack:(AVAssetTrack*)track{
    CMAudioFormatDescriptionRef item = (__bridge CMAudioFormatDescriptionRef)[track.formatDescriptions objectAtIndex:0];
    const AudioStreamBasicDescription* desc = CMAudioFormatDescriptionGetStreamBasicDescription(item);
    NSLog(@"Number of track channels: %d", desc->mChannelsPerFrame);
}


@end

TapProcessor.h

#import <Foundation/Foundation.h>
#import <AVFoundation/AVFoundation.h>

@interface TapProcessor : NSObject

- (id)initWithTrack:(AVAssetTrack *)track;

@property (readonly, nonatomic) AVAssetTrack *track;
@property (readonly, nonatomic) AVAudioMix *audioMix;

@end

TapProcessor.m

#import "TapProcessor.h"

// TAP CALLBACKS
static void tap_InitCallback(MTAudioProcessingTapRef tap,
                             void *clientInfo,
                             void **tapStorageOut){
}

static void tap_FinalizeCallback(MTAudioProcessingTapRef tap){
}

static void tap_PrepareCallback(MTAudioProcessingTapRef tap,
                                CMItemCount maxFrames,
                                const AudioStreamBasicDescription *processingFormat){
    NSLog(@"Number of tap channels: %d", processingFormat->mChannelsPerFrame);
}

static void tap_UnprepareCallback(MTAudioProcessingTapRef tap){
}

static void tap_ProcessCallback(MTAudioProcessingTapRef tap,
                                CMItemCount numberFrames,
                                MTAudioProcessingTapFlags flags,
                                AudioBufferList *bufferListInOut,
                                CMItemCount *numberFramesOut,
                                MTAudioProcessingTapFlags *flagsOut){
    MTAudioProcessingTapGetSourceAudio(tap, numberFrames, bufferListInOut, NULL, NULL, NULL);
    *numberFramesOut = numberFrames;
}


@implementation TapProcessor

- (id)initWithTrack:(AVAssetTrack *)track{
    self = [super init];
    if (self){
        _track = track;
    }
    return self;
}

@synthesize audioMix = _audioMix;
- (AVAudioMix *)audioMix {
    if (!_audioMix){
        AVMutableAudioMix *audioMix = [AVMutableAudioMix audioMix];
        if (audioMix){
            AVMutableAudioMixInputParameters *audioMixInputParameters = [AVMutableAudioMixInputParameters audioMixInputParametersWithTrack:self.track];
            if (audioMixInputParameters) {
                MTAudioProcessingTapCallbacks callbacks;
                callbacks.version = kMTAudioProcessingTapCallbacksVersion_0;
                callbacks.clientInfo = (__bridge void *)self,
                callbacks.init = tap_InitCallback;
                callbacks.finalize = tap_FinalizeCallback;
                callbacks.prepare = tap_PrepareCallback;
                callbacks.unprepare = tap_UnprepareCallback;
                callbacks.process = tap_ProcessCallback;

                MTAudioProcessingTapRef audioProcessingTap;
                if (noErr == MTAudioProcessingTapCreate(kCFAllocatorDefault,
                                                        &callbacks,
                                                        kMTAudioProcessingTapCreationFlag_PreEffects,
                                                        &audioProcessingTap)){
                    audioMixInputParameters.audioTapProcessor = audioProcessingTap;
                    CFRelease(audioProcessingTap);
                    audioMix.inputParameters = @[audioMixInputParameters];
                    _audioMix = audioMix;
                }
            }
        }
    }
    return _audioMix;
}

@end

ViewController.h

#import <UIKit/UIKit.h>

@interface ViewController : UIViewController


@end

ViewController.m

#import "ViewController.h"
#import "MediaPlayer.h"

@interface ViewController ()

@property (nonatomic,strong) MediaPlayer *mediaPlayer;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.mediaPlayer = [[MediaPlayer alloc] init];
}

@end
Rogare
  • 3,234
  • 3
  • 27
  • 50
  • Check this http://stackoverflow.com/questions/15174485/avplayer-playback-of-single-channel-audio-stereo-mono/15742434#15742434 – jose920405 Apr 21 '16 at 17:14
  • @jose920405. Thanks for the comment. In that question the OP is going from stereo to stereo—just copying one channel to the other. In my case, I'm going from mono to stereo as my source audio is only one channel and the output is two channels. – Rogare Apr 21 '16 at 17:55
  • 1
    @Rogare Did you ever find a workaround for this? I'm struggling with a similar situation at them moment. – Danny Bravo Sep 02 '17 at 08:07

0 Answers0