天天看點

iOS音視訊開發六:音頻渲染

我們将通過拆解采集 → 編碼 → 封裝 → 解封裝 → 解碼 → 渲染流程并實作 Demo 來向大家介紹如何在 iOS/Android 平台上手音視訊開發。

這裡是第六篇:iOS 音頻渲染 Demo。這個 Demo 裡包含以下内容:

  • 1)實作一個音頻解封裝子產品;
  • 2)實作一個音頻解碼子產品;
  • 3)實作一個音頻渲染子產品;
  • 4)實作對 MP4 檔案中音頻部分的解封裝和解碼邏輯,并将解封裝、解碼後的資料送給渲染子產品播放;
  • 5)詳盡的代碼注釋,幫你了解代碼邏輯和原理。

前五篇:

iOS要開發,采集音頻并存儲為 PCM 檔案

​​​​​​iOS音視訊開發二:音頻編碼,采集 PCM 資料編碼為 AAC

iOS音視訊開發三:音頻封裝,采集編碼并封裝為 M4A

​​​​​​iOS音視訊開發四:音頻解封裝,從 MP4 中解封裝出 AAC

iOS音視訊開發五:音頻解碼

1、音頻解封裝子產品

在這個 Demo 中,解封裝子產品 

KFMP4Demuxer

 的實作與 《iOS 音頻解封裝 Demo》 中一樣,這裡就不再重複介紹了。

2、音頻解碼子產品

同樣的,解封裝子產品 

KFAudioDecoder

 的實作與 《iOS 音頻解碼 Demo》 中一樣,這裡就不再重複介紹了。

3、音頻渲染子產品

接下來,我們來實作一個音頻渲染子產品 

KFAudioRender

,在這裡輸入解碼後的資料進行渲染播放。

KFAudioRender.h

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

@class KFAudioRender;

NS_ASSUME_NONNULL_BEGIN

@interface KFAudioRender : NSObject
+ (instancetype)new NS_UNAVAILABLE;
- (instancetype)init NS_UNAVAILABLE;
- (instancetype)initWithChannels:(NSInteger)channels bitDepth:(NSInteger)bitDepth sampleRate:(NSInteger)sampleRate;

@property (nonatomic, copy) void (^audioBufferInputCallBack)(AudioBufferList *audioBufferList); // 音頻渲染資料輸入回調。
@property (nonatomic, copy) void (^errorCallBack)(NSError *error); // 音頻渲染錯誤回調。
@property (nonatomic, assign, readonly) NSInteger audioChannels; // 聲道數。
@property (nonatomic, assign, readonly) NSInteger bitDepth; // 采樣位深。
@property (nonatomic, assign, readonly) NSInteger audioSampleRate; // 采樣率。

- (void)startPlaying; // 開始渲染。
- (void)stopPlaying; // 結束渲染。
@end

NS_ASSUME_NONNULL_END
           

上面是 

KFAudioRender

 接口的設計,除了

初始化

接口,主要是有音頻渲染

資料輸入回調

錯誤回調

的接口,另外就是

擷取聲道數

擷取采樣率

的接口,以及

開始渲染

結束渲染

的接口。

這裡重點需要看一下音頻渲染

資料輸入回調

接口,系統的音頻渲染單元每次會主動通過回調的方式要資料,我們這裡封裝的 

KFAudioRender

 則是用

資料輸入回調

接口來從外部擷取一組待渲染的音頻資料送給系統的音頻渲染單元。

本文福利, 免費領取C++音視訊學習資料包、技術視訊,内容包括(音視訊開發,面試題,FFmpeg ,webRTC ,rtmp ,hls ,rtsp ,ffplay ,編解碼,推拉流,srs)↓↓↓↓↓↓見下面↓↓文章底部點選免費領取↓↓

KFAudioRender.m

#import "KFAudioRender.h"

#define OutputBus 0

@interface KFAudioRender ()
@property (nonatomic, assign) AudioComponentInstance audioRenderInstance; // 音頻渲染執行個體。
@property (nonatomic, assign, readwrite) NSInteger audioChannels; // 聲道數。
@property (nonatomic, assign, readwrite) NSInteger bitDepth; // 采樣位深。
@property (nonatomic, assign, readwrite) NSInteger audioSampleRate; // 采樣率。
@property (nonatomic, strong) dispatch_queue_t renderQueue;
@property (nonatomic, assign) BOOL isError;
@end

@implementation KFAudioRender
#pragma mark - Lifecycle
- (instancetype)initWithChannels:(NSInteger)channels bitDepth:(NSInteger)bitDepth sampleRate:(NSInteger)sampleRate {
    self = [super init];
    if (self) {
        _audioChannels = channels;
        _bitDepth = bitDepth;
        _audioSampleRate = sampleRate;
        _renderQueue = dispatch_queue_create("com.KeyFrameKit.audioRender", DISPATCH_QUEUE_SERIAL);
    }
    
    return self;
}

- (void)dealloc {
    // 清理音頻渲染執行個體。
    if (_audioRenderInstance) {
        AudioOutputUnitStop(_audioRenderInstance);
        AudioUnitUninitialize(_audioRenderInstance);
        AudioComponentInstanceDispose(_audioRenderInstance);
        _audioRenderInstance = nil;
    }
}

#pragma mark - Action
- (void)startPlaying {
    __weak typeof(self) weakSelf = self;
    dispatch_async(_renderQueue, ^{
        if (!weakSelf.audioRenderInstance) {
            NSError *error = nil;
            // 第一次 startPlaying 時建立音頻渲染執行個體。
            [weakSelf _setupAudioRenderInstance:&error];
            if (error) {
                // 捕捉并回調建立音頻渲染執行個體時的錯誤。
                [weakSelf _callBackError:error];
                return;
            }
        }
        
        // 開始渲染。
        OSStatus status = AudioOutputUnitStart(weakSelf.audioRenderInstance);
        if (status != noErr) {
            // 捕捉并回調開始渲染時的錯誤。
            [weakSelf _callBackError:[NSError errorWithDomain:NSStringFromClass([KFAudioRender class]) code:status userInfo:nil]];
        }
    });
}

- (void)stopPlaying {
    __weak typeof(self) weakSelf = self;
    dispatch_async(_renderQueue, ^{
        if (weakSelf.audioRenderInstance && !self.isError) {
            // 停止渲染。
            OSStatus status = AudioOutputUnitStop(weakSelf.audioRenderInstance);
            // 捕捉并回調停止渲染時的錯誤。
            if (status != noErr) {
                [weakSelf _callBackError:[NSError errorWithDomain:NSStringFromClass([KFAudioRender class]) code:status userInfo:nil]];
            }
        }
    });
}

#pragma mark - Private Method
- (void)_setupAudioRenderInstance:(NSError**)error {
    // 1、設定音頻元件描述。
    AudioComponentDescription audioComponentDescription = {
        .componentType = kAudioUnitType_Output,
        //.componentSubType = kAudioUnitSubType_VoiceProcessingIO, // 回聲消除模式
        .componentSubType = kAudioUnitSubType_RemoteIO,
        .componentManufacturer = kAudioUnitManufacturer_Apple,
        .componentFlags = 0,
        .componentFlagsMask = 0
    };
    
    // 2、查找符合指定描述的音頻元件。
    AudioComponent inputComponent = AudioComponentFindNext(NULL, &audioComponentDescription);

    // 3、建立音頻元件執行個體。
    OSStatus status = AudioComponentInstanceNew(inputComponent, &_audioRenderInstance);
    if (status != noErr) {
        *error = [NSError errorWithDomain:NSStringFromClass(self.class) code:status userInfo:nil];
        return;
    }
    
    // 4、設定執行個體的屬性:可讀寫。0 不可讀寫,1 可讀寫。
    UInt32 flag = 1;
    status = AudioUnitSetProperty(_audioRenderInstance, kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Output, OutputBus, &flag, sizeof(flag));
    if (status != noErr) {
        *error = [NSError errorWithDomain:NSStringFromClass(self.class) code:status userInfo:nil];
        return;
    }
    
    // 5、設定執行個體的屬性:音頻參數,如:資料格式、聲道數、采樣位深、采樣率等。
    AudioStreamBasicDescription inputFormat = {0};
    inputFormat.mFormatID = kAudioFormatLinearPCM; // 原始資料為 PCM,采用聲道交錯格式。
    inputFormat.mFormatFlags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagsNativeEndian | kAudioFormatFlagIsPacked;
    inputFormat.mChannelsPerFrame = (UInt32) self.audioChannels; // 每幀的聲道數。
    inputFormat.mFramesPerPacket = 1; // 每個資料包幀數。
    inputFormat.mBitsPerChannel = (UInt32) self.bitDepth; // 采樣位深。
    inputFormat.mBytesPerFrame = inputFormat.mChannelsPerFrame * inputFormat.mBitsPerChannel / 8; // 每幀位元組數 (byte = bit / 8)。
    inputFormat.mBytesPerPacket = inputFormat.mFramesPerPacket * inputFormat.mBytesPerFrame; // 每個包位元組數。
    inputFormat.mSampleRate = self.audioSampleRate; // 采樣率
    status = AudioUnitSetProperty(_audioRenderInstance, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, OutputBus, &inputFormat, sizeof(inputFormat));
    if (status != noErr) {
        *error = [NSError errorWithDomain:NSStringFromClass(self.class) code:status userInfo:nil];
        return;
    }

    // 6、設定執行個體的屬性:資料回調函數。
    AURenderCallbackStruct renderCallbackRef = {
        .inputProc = audioRenderCallback,
        .inputProcRefCon = (__bridge void *) (self) // 對應回調函數中的 *inRefCon。
    };
    status = AudioUnitSetProperty(_audioRenderInstance, kAudioUnitProperty_SetRenderCallback, kAudioUnitScope_Global, OutputBus, &renderCallbackRef, sizeof(renderCallbackRef));
    if (status != noErr) {
        *error = [NSError errorWithDomain:NSStringFromClass(self.class) code:status userInfo:nil];
        return;
    }
    
    // 7、初始化執行個體。
    status = AudioUnitInitialize(_audioRenderInstance);
    if (status != noErr) {
        *error = [NSError errorWithDomain:NSStringFromClass(self.class) code:status userInfo:nil];
        return;
    }
}

- (void)_callBackError:(NSError*)error {
    self.isError = YES;
    if (self.errorCallBack) {
        dispatch_async(dispatch_get_main_queue(), ^{
            self.errorCallBack(error);
        });
    }
}

#pragma mark - Render Callback
static OSStatus audioRenderCallback(void *inRefCon, AudioUnitRenderActionFlags *ioActionFlags, const AudioTimeStamp *inTimeStamp, UInt32 inOutputBusNumber, UInt32 inNumberFrames, AudioBufferList *ioData) {
    // 通過音頻渲染資料輸入回調從外部擷取待渲染的資料。
    KFAudioRender *audioRender = (__bridge KFAudioRender *) inRefCon;
    if (audioRender.audioBufferInputCallBack) {
        audioRender.audioBufferInputCallBack(ioData);
    }
    
    return noErr;
}

@end
           

上面是 

KFAudioRender

 的實作,從代碼上可以看到主要有這幾個部分:

  • 1)建立音頻渲染執行個體。第一次調用 

    -startPlaying

     才會建立音頻渲染執行個體。
    • 在 

      -_setupAudioRenderInstance:

       方法中實作。
  • 2)處理音頻渲染執行個體的資料回調,并在回調中通過 KFAudioRender 的對外資料輸入回調接口向更外層要待渲染的資料。
    • 在 

      audioRenderCallback(...)

       方法中實作回調處理邏輯。通過 

      audioBufferInputCallBack

       回調接口向更外層要資料。
  • 3)實作開始渲染和停止渲染邏輯。
    • 分别在 

      -startPlaying

       和 

      -stopPlaying

       方法中實作。注意,這裡是開始和停止操作都是放在串行隊列中通過 

      dispatch_async

       異步處理的,這裡主要是為了防止主線程卡頓。
  • 4)捕捉音頻渲染開始和停止操作中的錯誤,抛給 KFAudioRender 的對外錯誤回調接口。
    • 在 

      -startPlaying

       和 

      -stopPlaying

       方法中捕捉錯誤,在 

      -_callBackError:

       方法向外回調。
  • 5)清理音頻渲染執行個體。
    • 在 

      -dealloc

       方法中實作。

更具體細節見上述代碼及其注釋。

4、解封裝和解碼 MP4 檔案中的音頻部分并渲染播放

我們在一個 ViewController 中來實作從 MP4 檔案中解封裝和解碼音頻資料進行渲染播放。

KFAudioRenderViewController.m

#import "KFAudioRenderViewController.h"
#import <AVFoundation/AVFoundation.h>
#import "KFAudioRender.h"
#import "KFMP4Demuxer.h"
#import "KFAudioDecoder.h"
#import "KFWeakProxy.h"

#define KFDecoderMaxCache 4096 * 5 // 解碼資料緩沖區最大長度。

@interface KFAudioRenderViewController ()
@property (nonatomic, strong) KFDemuxerConfig *demuxerConfig;
@property (nonatomic, strong) KFMP4Demuxer *demuxer;
@property (nonatomic, strong) KFAudioDecoder *decoder;
@property (nonatomic, strong) KFAudioRender *audioRender;
@property (nonatomic, strong) dispatch_semaphore_t semaphore;
@property (nonatomic, strong) NSMutableData *pcmDataCache; // 解碼資料緩沖區。
@property (nonatomic, assign) NSInteger pcmDataCacheLength;
@property (nonatomic, strong) CADisplayLink *timer;
@end

@implementation KFAudioRenderViewController
#pragma mark - Property
- (KFDemuxerConfig *)demuxerConfig {
    if (!_demuxerConfig) {
        _demuxerConfig = [[KFDemuxerConfig alloc] init];
        _demuxerConfig.demuxerType = KFMediaAudio;
        NSString *videoPath = [[NSBundle mainBundle] pathForResource:@"input" ofType:@"mp4"];
        _demuxerConfig.asset = [AVAsset assetWithURL:[NSURL fileURLWithPath:videoPath]];
    }
    
    return _demuxerConfig;
}

- (KFMP4Demuxer *)demuxer {
    if (!_demuxer) {
        _demuxer = [[KFMP4Demuxer alloc] initWithConfig:self.demuxerConfig];
        _demuxer.errorCallBack = ^(NSError *error) {
            NSLog(@"KFMP4Demuxer error:%zi %@", error.code, error.localizedDescription);
        };
    }
    
    return _demuxer;
}

- (KFAudioDecoder *)decoder {
    if (!_decoder) {
        __weak typeof(self) weakSelf = self;
        _decoder = [[KFAudioDecoder alloc] init];
        _decoder.errorCallBack = ^(NSError *error) {
            NSLog(@"KFAudioDecoder error:%zi %@", error.code, error.localizedDescription);
        };
        // 解碼資料回調。在這裡把解碼後的音頻 PCM 資料緩沖起來等待渲染。
        _decoder.sampleBufferOutputCallBack = ^(CMSampleBufferRef sampleBuffer) {
            if (sampleBuffer) {
                CMBlockBufferRef blockBuffer = CMSampleBufferGetDataBuffer(sampleBuffer);
                size_t totolLength;
                char *dataPointer = NULL;
                CMBlockBufferGetDataPointer(blockBuffer, 0, NULL, &totolLength, &dataPointer);
                if (totolLength == 0 || !dataPointer) {
                    return;
                }
                dispatch_semaphore_wait(weakSelf.semaphore, DISPATCH_TIME_FOREVER);
                [weakSelf.pcmDataCache appendData:[NSData dataWithBytes:dataPointer length:totolLength]];
                weakSelf.pcmDataCacheLength += totolLength;
                dispatch_semaphore_signal(weakSelf.semaphore);
            }
        };
    }
    
    return _decoder;
}

- (KFAudioRender *)audioRender {
    if (!_audioRender) {
        __weak typeof(self) weakSelf = self;
        // 這裡設定的音頻聲道數、采樣位深、采樣率需要跟輸入源的音頻參數一緻。
        _audioRender = [[KFAudioRender alloc] initWithChannels:1 bitDepth:16 sampleRate:44100];
        _audioRender.errorCallBack = ^(NSError* error) {
            NSLog(@"KFAudioRender error:%zi %@", error.code, error.localizedDescription);
        };
        // 渲染輸入資料回調。在這裡把緩沖區的資料交給系統音頻渲染單元渲染。
        _audioRender.audioBufferInputCallBack = ^(AudioBufferList * _Nonnull audioBufferList) {
            if (weakSelf.pcmDataCacheLength < audioBufferList->mBuffers[0].mDataByteSize) {
                memset(audioBufferList->mBuffers[0].mData, 0, audioBufferList->mBuffers[0].mDataByteSize);
            } else {
                dispatch_semaphore_wait(weakSelf.semaphore, DISPATCH_TIME_FOREVER);
                memcpy(audioBufferList->mBuffers[0].mData, weakSelf.pcmDataCache.bytes, audioBufferList->mBuffers[0].mDataByteSize);
                [weakSelf.pcmDataCache replaceBytesInRange:NSMakeRange(0, audioBufferList->mBuffers[0].mDataByteSize) withBytes:NULL length:0];
                weakSelf.pcmDataCacheLength -= audioBufferList->mBuffers[0].mDataByteSize;
                dispatch_semaphore_signal(weakSelf.semaphore);
            }
        };
    }
    
    return _audioRender;
}

#pragma mark - Lifecycle
- (void)viewDidLoad {
    [super viewDidLoad];
        
    _semaphore = dispatch_semaphore_create(1);
    _pcmDataCache = [[NSMutableData alloc] init];
    
    [self setupAudioSession];
    [self setupUI];
    
    // 通過一個 timer 來保證持續從檔案中解封裝和解碼一定量的資料。
    _timer = [CADisplayLink displayLinkWithTarget:[KFWeakProxy proxyWithTarget:self] selector:@selector(timerCallBack:)];
    [_timer addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
    [_timer setPaused:NO];
    
    [self.demuxer startReading:^(BOOL success, NSError * _Nonnull error) {
        NSLog(@"KFMP4Demuxer start:%d", success);
    }];
}

- (void)dealloc {
    
}

#pragma mark - Setup
- (void)setupUI {
    self.edgesForExtendedLayout = UIRectEdgeAll;
    self.extendedLayoutIncludesOpaqueBars = YES;
    self.title = @"Audio Render";
    self.view.backgroundColor = [UIColor whiteColor];
    
    
    // Navigation item.
    UIBarButtonItem *startRenderBarButton = [[UIBarButtonItem alloc] initWithTitle:@"Start" style:UIBarButtonItemStylePlain target:self action:@selector(startRender)];
    UIBarButtonItem *stopRenderBarButton = [[UIBarButtonItem alloc] initWithTitle:@"Stop" style:UIBarButtonItemStylePlain target:self action:@selector(stopRender)];
    self.navigationItem.rightBarButtonItems = @[startRenderBarButton, stopRenderBarButton];
}

#pragma mark - Action
- (void)startRender {
    [self.audioRender startPlaying];
}

- (void)stopRender {
    [self.audioRender stopPlaying];
}

#pragma mark - Utility
- (void)setupAudioSession {
    // 1、擷取音頻會話執行個體。
    AVAudioSession *session = [AVAudioSession sharedInstance];
    
    // 2、設定分類。
    NSError *error = nil;
    [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:&error];
    if (error) {
        NSLog(@"AVAudioSession setCategory error");
    }
    
    // 3、激活會話。
    [session setActive:YES error:&error];
    if (error) {
        NSLog(@"AVAudioSession setActive error");
    }
}

- (void)timerCallBack:(CADisplayLink *)link {
    // 定時從檔案中解封裝和解碼一定量(不超過 KFDecoderMaxCache)的資料。
    if (self.pcmDataCacheLength <  KFDecoderMaxCache && self.demuxer.demuxerStatus == KFMP4DemuxerStatusRunning && self.demuxer.hasAudioSampleBuffer) {
        CMSampleBufferRef audioBuffer = [self.demuxer copyNextAudioSampleBuffer];
        if (audioBuffer) {
            [self decodeSampleBuffer:audioBuffer];
            CFRelease(audioBuffer);
        }
    }
}

- (void)decodeSampleBuffer:(CMSampleBufferRef)sampleBuffer {
    // 擷取解封裝後的 AAC 編碼裸資料。
    CMBlockBufferRef blockBuffer = CMSampleBufferGetDataBuffer(sampleBuffer);
    size_t totolLength;
    char *dataPointer = NULL;
    CMBlockBufferGetDataPointer(blockBuffer, 0, NULL, &totolLength, &dataPointer);
    if (totolLength == 0 || !dataPointer) {
        return;
    }
    
    // 目前 AudioDecoder 的解碼接口實作的是單包(packet,1 packet 有 1024 幀)解碼。而從 Demuxer 擷取的一個 CMSampleBuffer 可能包含多個包,是以這裡要拆一下包,再送給解碼器。
    NSLog(@"SampleNum: %ld", CMSampleBufferGetNumSamples(sampleBuffer));
    for (NSInteger index = 0; index < CMSampleBufferGetNumSamples(sampleBuffer); index++) {
        // 1、擷取一個包的資料。
        size_t sampleSize = CMSampleBufferGetSampleSize(sampleBuffer, index);
        CMSampleTimingInfo timingInfo;
        CMSampleBufferGetSampleTimingInfo(sampleBuffer, index, &timingInfo);
        char *sampleDataPointer = malloc(sampleSize);
        memcpy(sampleDataPointer, dataPointer, sampleSize);
        
        // 2、将資料封裝到 CMBlockBuffer 中。
        CMBlockBufferRef packetBlockBuffer;
        OSStatus status = CMBlockBufferCreateWithMemoryBlock(kCFAllocatorDefault,
                                                              sampleDataPointer,
                                                              sampleSize,
                                                              NULL,
                                                              NULL,
                                                              0,
                                                              sampleSize,
                                                              0,
                                                              &packetBlockBuffer);
        
        if (status == noErr) {
            // 3、将 CMBlockBuffer 封裝到 CMSampleBuffer 中。
            CMSampleBufferRef packetSampleBuffer = NULL;
            const size_t sampleSizeArray[] = {sampleSize};
            status = CMSampleBufferCreateReady(kCFAllocatorDefault,
                                               packetBlockBuffer,
                                               CMSampleBufferGetFormatDescription(sampleBuffer),
                                               1,
                                               1,
                                               &timingInfo,
                                               1,
                                               sampleSizeArray,
                                               &packetSampleBuffer);
            CFRelease(packetBlockBuffer);
            
            // 4、解碼這個包的資料。
            if (packetSampleBuffer) {
                [self.decoder decodeSampleBuffer:packetSampleBuffer];
                CFRelease(packetSampleBuffer);
            }
        }
        dataPointer += sampleSize;
    }
}

@end
           

上面是 

KFAudioRenderViewController

 的實作,其中主要包含這幾個部分:

  • 1)在頁面加載完成後就啟動解封裝和解碼子產品,并通過一個 timer 來驅動解封裝器和解碼器。
    • 在 

      -viewDidLoad

       中實作。
  • 2)定時從檔案中解封裝一定量(不超過 KFDecoderMaxCache)的資料送給解碼器。
    • 在 

      -timerCallBack:

       方法中實作。
  • 3)解封裝後,需要将資料拆包,以包為機關封裝為 

    CMSampleBuffer

     送給解碼器解碼。
    • 在 

      -decodeSampleBuffer:

       方法中實作。
  • 4)在解碼子產品 

    KFAudioDecoder

     的資料回調中擷取解碼後的 PCM 資料緩沖起來等待渲染。
    • 在 

      KFAudioDecoder

       的 

      sampleBufferOutputCallBack

       回調中實作。
  • 5)在渲染子產品 

    KFAudioRender

     的輸入資料回調中把緩沖區的資料交給系統音頻渲染單元渲染。
    • 在 

      KFAudioRender

       的 

      audioBufferInputCallBack

       回調中實作。

更具體細節見上述代碼及其注釋。

本文福利, 免費領取C++音視訊學習資料包、技術視訊,内容包括(音視訊開發,面試題,FFmpeg ,webRTC ,rtmp ,hls ,rtsp ,ffplay ,編解碼,推拉流,srs)↓↓↓↓↓↓見下面↓↓文章底部點選免費領取↓↓