音頻解碼 Audio Converter

需求

iOS中將壓縮音頻數(shù)據(jù)(如AAC)進行解碼以得到原始音頻數(shù)據(jù)類型:線性PCM.

本例最終實現(xiàn)的是通過Audio Queue采集到AAC壓縮數(shù)據(jù),將其解碼為PCM數(shù)據(jù),并將解碼后的PCM數(shù)據(jù)以錄制的形式保存在沙盒中.可調(diào)整解碼后采樣率,解碼器類型等參數(shù).

本例可拓展,不僅僅解碼AAC音頻數(shù)據(jù)流,還可以是音頻文件,視頻文件中的音頻等等.


實現(xiàn)原理

利用Audio Toolbox Framework中的Audio Converter可以實現(xiàn)音頻數(shù)據(jù)解碼,即將AAC數(shù)據(jù)轉(zhuǎn)為原始音頻數(shù)據(jù)PCM.


閱讀前提:


GitHub地址(附代碼) : 音頻解碼

簡書地址 : 音頻解碼

掘金地址 : 音頻解碼

博客地址 : 音頻解碼


1.初始化

1.1. 初始化解碼器

初始化解碼器實例, 通過指定原始數(shù)據(jù)格式,最終解碼后的格式,采樣率,以及使用硬編還是軟編,以下是具體步驟.

- (instancetype)initWithSourceFormat:(AudioStreamBasicDescription)sourceFormat destFormatID:(AudioFormatID)destFormatID sampleRate:(float)sampleRate isUseHardwareDecode:(BOOL)isUseHardwareDecode {
    if (self = [super init]) {
        mSourceFormat   = sourceFormat;
        mAudioConverter = [self configureDecoderBySourceFormat:sourceFormat
                                                    destFormat:&mDestinationFormat
                                                  destFormatID:destFormatID
                                                    sampleRate:sampleRate
                                           isUseHardwareDecode:isUseHardwareDecode];
    }
    return self;
}

1.2. 配置解碼后ASBD音頻流信息

    AudioStreamBasicDescription destinationFormat = {0};
    destinationFormat.mSampleRate = sampleRate;
    if (destFormatID != kAudioFormatLinearPCM) {
        NSLog(@"Not get compression format after decoding !");
        return NULL;
    } else {
        destinationFormat.mFormatID = destFormatID;
        destinationFormat.mChannelsPerFrame = sourceFormat.mChannelsPerFrame;
        destinationFormat.mFormatID          = kAudioFormatLinearPCM;
        destinationFormat.mFormatFlags       = (kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked);
        destinationFormat.mFramesPerPacket   = kXDXAudioPCMFramesPerPacket;
        destinationFormat.mBitsPerChannel    = KXDXAudioBitsPerChannel;
        destinationFormat.mBytesPerFrame     = destinationFormat.mBitsPerChannel / 8 *destinationFormat.mChannelsPerFrame;
        destinationFormat.mBytesPerPacket    = destinationFormat.mBytesPerFrame * destinationFormat.mFramesPerPacket;
        destinationFormat.mReserved          =  0;
    }
    memcpy(destFormat, &destinationFormat, sizeof(AudioStreamBasicDescription));

對音頻做解碼操作,實際就是將壓縮數(shù)據(jù)格式如AAC格式轉(zhuǎn)為線性PCM原始音頻數(shù)據(jù),通過kAudioFormatProperty_FormatInfo屬性可以自動獲取指定音頻格式的參數(shù)信息.

1.3. 選擇解碼器類型

AudioClassDescription結(jié)構(gòu)體描述了系統(tǒng)使用音頻解碼器信息,其中最重要的就是使用硬編或軟編。然后解碼器的數(shù)量导狡,即數(shù)組的個數(shù),由當前的聲道數(shù)決定弦叶。

//獲取解碼器的描述信息
    AudioClassDescription *audioClassDesc = [self getAudioCalssDescriptionWithType:destFormatID fromManufacture:kAppleHardwareAudioCodecManufacturer];
...

- (AudioClassDescription *)getAudioCalssDescriptionWithType:(AudioFormatID)type fromManufacture:(uint32_t)manufacture {
    static AudioClassDescription desc;
    UInt32 decoderSpecific = type;
    UInt32 size;
    OSStatus status = AudioFormatGetPropertyInfo(kAudioFormatProperty_Decoders,
                                                 sizeof(decoderSpecific),
                                                 &decoderSpecific,
                                                 &size);
    
    if (status != noErr) {
        NSLog(@"Error3室啊:硬解碼AAC get info 失敗, status= %d", (int)status);
        return nil;
    }
    
    //計算aac解碼器的個數(shù)
    unsigned int count = size / sizeof(AudioClassDescription);
    //創(chuàng)建一個包含count個解碼器的數(shù)組
    AudioClassDescription description[count];
    //將滿足aac解碼的解碼器的信息寫入數(shù)組
    status = AudioFormatGetProperty(kAudioFormatProperty_Encoders,
                                    sizeof(decoderSpecific),
                                    &decoderSpecific,
                                    &size,
                                    &description);
    
    if (status != noErr) {
        NSLog(@"Error!:硬解碼AAC get propery 失敗, status= %d", (int)status);
        return nil;
    }
    
    for (unsigned int i = 0; i < count; i++) {
        if (type == description[i].mSubType && manufacture == description[i].mManufacturer) {
            desc = description[i];
            return &desc;
        }
    }
    return nil;
}

注意:硬解即利用設備GPU硬件完成高效解碼,降低CPU消耗. 軟解就是傳統(tǒng)的通過CPU計算京腥。

1.4. 創(chuàng)建解碼器

AudioConverterNewSpecific: 通過指定解碼器來創(chuàng)建audio converter實例對象.第3,4個
分別是解碼器的數(shù)量與解碼器描述,同上,與聲道數(shù)保持一致.

    // Create the AudioConverterRef.
    AudioConverterRef converter = NULL;
    if (![self checkError:AudioConverterNewSpecific(&sourceFormat, &destinationFormat, destinationFormat.mChannelsPerFrame, audioClassDesc, &converter) withErrorString:@"Audio Converter New failed"]) {
        return NULL;
    }else {
        printf("Audio converter create successful \n");
    }

2.解碼

2.1. 計算解碼數(shù)據(jù)大小

注意,當使用Audio Convert無論做編解碼,每次都需要1024個采樣點才能完成一次轉(zhuǎn)換,此值是固定的.

根據(jù)解碼器的采樣點,計算解碼出音頻數(shù)據(jù)的大小.因為線性PCM的數(shù)據(jù)可以通過公式算出,即數(shù)據(jù)包數(shù)量*聲道數(shù)*每個數(shù)據(jù)包中字節(jié)數(shù).

// Note: audio convert must set 1024.
    UInt32 ioOutputDataPackets = kIOOutputDataPackets;
    UInt32 outputBufferSize = (UInt32)(ioOutputDataPackets * destFormat.mChannelsPerFrame * destFormat.mBytesPerFrame);

2.2. 為解碼后音頻數(shù)據(jù)預分配內(nèi)存

我們可以將2.1中算出的size為這個Buffer list分配內(nèi)存.

// Set up output buffer list.
    // Set up output buffer list.
    AudioBufferList fillBufferList = {0};
    fillBufferList.mNumberBuffers = 1;
    fillBufferList.mBuffers[0].mNumberChannels  = destFormat.mChannelsPerFrame;
    fillBufferList.mBuffers[0].mDataByteSize    = outputBufferSize;
    fillBufferList.mBuffers[0].mData            = malloc(outputBufferSize * sizeof(char));

2.3. 解碼音頻數(shù)據(jù)

解析AudioConverterFillComplexBuffer:用來解碼音頻數(shù)據(jù).同時需要指定回調(diào)函數(shù)(C語言函數(shù)),

第二個參數(shù)即指定回調(diào)函數(shù),此回調(diào)函數(shù)中主要做的是為即將解碼的數(shù)據(jù)進行賦值,即我們要把原始音頻數(shù)據(jù)賦值給回調(diào)函數(shù)中的ioData參數(shù),這是我們在解碼前最后一次控制原始音頻數(shù)據(jù),此回調(diào)函數(shù)執(zhí)行后即完成了解碼的過程,新的數(shù)據(jù)會填充到第五個參數(shù)中,也就是我們上面預定義的fillBufferList.

  • userInfo: 自定義一個結(jié)構(gòu)體,用來與解碼回調(diào)函數(shù)間交互以傳遞數(shù)據(jù).在這里是將原始音頻數(shù)據(jù)信息傳給解碼回調(diào)函數(shù)中.
  • ioOutputDataPackets: 填入函數(shù)中時表示原始音頻數(shù)據(jù)包的數(shù)量,而函數(shù)調(diào)用完成時表示轉(zhuǎn)換后輸出的音頻數(shù)據(jù)包總數(shù),注意,當我們做解碼時,輸出肯定為PCM類型數(shù)據(jù),所以需要提供1024個AAC采樣點.而做編碼時會將PCM數(shù)據(jù)壓縮成很多音頻數(shù)據(jù)包,僅僅需要1個完整的PCM數(shù)據(jù)包即可.
  • outputPacketDescriptions: 轉(zhuǎn)換完成后,如果此參數(shù)非空,表示轉(zhuǎn)換器輸出使用的音頻數(shù)據(jù)包描述,它必須提前分配好內(nèi)存,以讓轉(zhuǎn)換器賦值到其中.

最終,我們將轉(zhuǎn)換后得到的AAC數(shù)據(jù)以回調(diào)函數(shù)的形式傳給調(diào)用者.


OSStatus DecodeConverterComplexInputDataProc(AudioConverterRef              inAudioConverter,
                                             UInt32                         *ioNumberDataPackets,
                                             AudioBufferList                *ioData,
                                             AudioStreamPacketDescription   **outDataPacketDescription,
                                             void                           *inUserData) {
    XDXConverterInfoType *info = (XDXConverterInfoType *)inUserData;
    
    if (info->sourceDataSize <= 0) {
        ioNumberDataPackets = 0;
        return -1;
    }
    
    *outDataPacketDescription = &info->packetDesc;
    (*outDataPacketDescription)[0].mStartOffset             = 0;
    (*outDataPacketDescription)[0].mDataByteSize            = info->sourceDataSize;
    (*outDataPacketDescription)[0].mVariableFramesInPacket  = 0;
    
    ioData->mNumberBuffers              = 1;
    ioData->mBuffers[0].mData           = info->sourceBuffer;
    ioData->mBuffers[0].mNumberChannels = info->sourceChannelsPerFrame;
    ioData->mBuffers[0].mDataByteSize   = info->sourceDataSize;
    
    return noErr;
}


- (void)decodeFormatByConverter:(AudioConverterRef)audioConverter sourceBuffer:(void *)sourceBuffer sourceBufferSize:(UInt32)sourceBufferSize sourceFormat:(AudioStreamBasicDescription)sourceFormat dest:(AudioStreamBasicDescription)destFormat completeHandler:(void(^)(AudioBufferList *destBufferList, UInt32 outputPackets, AudioStreamPacketDescription *outputPacketDescriptions))completeHandler {
    ...
    
    XDXConverterInfoType userInfo        = {0};
    userInfo.sourceBuffer                = sourceBuffer;
    userInfo.sourceDataSize              = sourceBufferSize;
    userInfo.sourceChannelsPerFrame      = sourceFormat.mChannelsPerFrame;
    userInfo.packetDesc.mDataByteSize    = (UInt32)sourceBufferSize;
    userInfo.packetDesc.mStartOffset     = 0;
    userInfo.packetDesc.mVariableFramesInPacket = 0;
    
    AudioStreamPacketDescription outputPacketDesc;
    OSStatus status = AudioConverterFillComplexBuffer(audioConverter,
                                                      DecodeConverterComplexInputDataProc,
                                                      &userInfo,
                                                      &ioOutputDataPackets,
                                                      &fillBufferList,
                                                      &outputPacketDesc);
    
    // if interrupted in the process of the conversion call, we must handle the error appropriately
    if (status != noErr) {
        if (status == kAudioConverterErr_HardwareInUse) {
            printf("Audio Converter returned kAudioConverterErr_HardwareInUse!\n");
        } else {
            if (![self checkError:status withErrorString:@"AudioConverterFillComplexBuffer error!"]) {
                return;
            }
        }
    } else {
        if (ioOutputDataPackets == 0) {
            // This is the EOF condition.
            status = noErr;
        }
        
        if (completeHandler) {
            completeHandler(&fillBufferList, ioOutputDataPackets, &outputPacketDesc);
        }
    }
}

3. 模塊對接

因為音頻解碼要依賴音頻采集,所以我們這里以audio unit采集為例作示范,即使用audio unit采集pcm數(shù)據(jù)然后使用此模塊解碼得到aac數(shù)據(jù).如需了解請參考如下鏈接

3.1. 初始化解碼器

如下,在音頻采集的類中聲明一個解碼器實例變量,然后初始化它. 僅僅需要設置原始數(shù)據(jù)格式,解碼后的格式,采樣率,使用硬編,軟編即可.

@property (nonatomic, strong) XDXAduioDecoder *audioDecoder;

...

        // audio decode: aac->pcm
        self.audioDecoder = [[XDXAduioDecoder alloc] initWithSourceFormat:m_audioInfo->mDataFormat
                                                             destFormatID:kAudioFormatLinearPCM
                                                               sampleRate:48000
                                                      isUseHardwareDecode:YES];

3.2. 解碼音頻數(shù)據(jù)

在Audio Queue采集AAC音頻數(shù)據(jù)的回調(diào)中將AAC數(shù)據(jù)送入解碼器,然后在回調(diào)函數(shù)中將得到的PCM數(shù)據(jù)其寫入文件.

注意: 直接用Audio Queue采集AAC類型音頻數(shù)據(jù),實際系統(tǒng)在其內(nèi)部做了一次轉(zhuǎn)換,即直接采集其實只能采原始PCM數(shù)據(jù),直接用Audio Queue設置采集AAC相當于系統(tǒng)在內(nèi)部為我們做了一次轉(zhuǎn)換.

static void CaptureAudioDataCallback(void *                                 inUserData,
                                     AudioQueueRef                          inAQ,
                                     AudioQueueBufferRef                    inBuffer,
                                     const AudioTimeStamp *                 inStartTime,
                                     UInt32                                 inNumPackets,
                                     const AudioStreamPacketDescription*    inPacketDesc) {
    
    XDXAudioQueueCaptureManager *instance = (__bridge XDXAudioQueueCaptureManager *)inUserData;
    
    [instance.audioDecoder decodeAudioWithSourceBuffer:inBuffer->mAudioData
                                      sourceBufferSize:inBuffer->mAudioDataByteSize
                                       completeHandler:^(AudioBufferList * _Nonnull destBufferList, UInt32 outputPackets, AudioStreamPacketDescription * _Nonnull outputPacketDescriptions) {
                                           if (instance.isRecordVoice) {
                                               [[XDXAudioFileHandler getInstance] writeFileWithInNumBytes:destBufferList->mBuffers->mDataByteSize
                                                                                             ioNumPackets:outputPackets
                                                                                                 inBuffer:destBufferList->mBuffers->mData
                                                                                             inPacketDesc:outputPacketDescriptions];
                                           }
                                           
                                           free(destBufferList->mBuffers->mData);
                                       }];
    
    if (instance.isRunning) {
        AudioQueueEnqueueBuffer(inAQ, inBuffer, 0, NULL);
    }
}

4. 文件錄制

此部分可參考另一篇文章: 音頻文件錄制

5. 釋放解碼器資源

如需釋放內(nèi)存,請保證解碼器工作徹底結(jié)束后再釋放內(nèi)存.

- (void)freeEncoder {
    if (mAudioConverter) {
        AudioConverterDispose(mAudioConverter);
        mAudioConverter = NULL;
    }
}
最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市溅蛉,隨后出現(xiàn)的幾起案子公浪,更是在濱河造成了極大的恐慌,老刑警劉巖船侧,帶你破解...
    沈念sama閱讀 212,816評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件因悲,死亡現(xiàn)場離奇詭異,居然都是意外死亡勺爱,警方通過查閱死者的電腦和手機晃琳,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,729評論 3 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來琐鲁,“玉大人卫旱,你說我怎么就攤上這事∥Ф危” “怎么了顾翼?”我有些...
    開封第一講書人閱讀 158,300評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長奈泪。 經(jīng)常有香客問我适贸,道長,這世上最難降的妖魔是什么涝桅? 我笑而不...
    開封第一講書人閱讀 56,780評論 1 285
  • 正文 為了忘掉前任拜姿,我火速辦了婚禮,結(jié)果婚禮上冯遂,老公的妹妹穿的比我還像新娘蕊肥。我一直安慰自己,他們只是感情好蛤肌,可當我...
    茶點故事閱讀 65,890評論 6 385
  • 文/花漫 我一把揭開白布壁却。 她就那樣靜靜地躺著批狱,像睡著了一般。 火紅的嫁衣襯著肌膚如雪展东。 梳的紋絲不亂的頭發(fā)上赔硫,一...
    開封第一講書人閱讀 50,084評論 1 291
  • 那天,我揣著相機與錄音盐肃,去河邊找鬼爪膊。 笑死,一個胖子當著我的面吹牛恼蓬,可吹牛的內(nèi)容都是我干的惊完。 我是一名探鬼主播,決...
    沈念sama閱讀 39,151評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼处硬,長吁一口氣:“原來是場噩夢啊……” “哼小槐!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起荷辕,我...
    開封第一講書人閱讀 37,912評論 0 268
  • 序言:老撾萬榮一對情侶失蹤凿跳,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后疮方,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體控嗜,經(jīng)...
    沈念sama閱讀 44,355評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,666評論 2 327
  • 正文 我和宋清朗相戀三年骡显,在試婚紗的時候發(fā)現(xiàn)自己被綠了疆栏。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,809評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡惫谤,死狀恐怖壁顶,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情溜歪,我是刑警寧澤若专,帶...
    沈念sama閱讀 34,504評論 4 334
  • 正文 年R本政府宣布,位于F島的核電站蝴猪,受9級特大地震影響调衰,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜自阱,卻給世界環(huán)境...
    茶點故事閱讀 40,150評論 3 317
  • 文/蒙蒙 一嚎莉、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧动壤,春花似錦萝喘、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,882評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至哼丈,卻和暖如春启妹,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背醉旦。 一陣腳步聲響...
    開封第一講書人閱讀 32,121評論 1 267
  • 我被黑心中介騙來泰國打工饶米, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人车胡。 一個月前我還...
    沈念sama閱讀 46,628評論 2 362
  • 正文 我出身青樓檬输,卻偏偏與公主長得像,于是被迫代替她去往敵國和親匈棘。 傳聞我的和親對象是個殘疾皇子丧慈,可洞房花燭夜當晚...
    茶點故事閱讀 43,724評論 2 351

推薦閱讀更多精彩內(nèi)容