More-iOS開發中的音訊相關內容總結

PJHubs發表於2018-04-12

這段時間陸陸續續的在做一些關於iOS開發細節的東西,先是跟進了音訊部分(以下簡稱為Audio),主要分為以下幾大部分:

  1. Audio的架構和框架
  2. 編解碼/檔案封裝格式
  3. 播放系統聲音/震動/提示聲音
  4. 綜合demo
  5. 使用AVFoundation框架進行中英文語音識別

說起iOS中的Audio,耳熟能詳的就是AVFoundation,畢竟它是個全能型的框架,不過的AVFoundation現在的地位可以類比JavaScript現在的地位,JavaScript現在甚至都插手嵌入式開發了?。

但也就是這種什麼所謂的全能型選手,擁有大而全的技能,卻缺少了一些底蘊。也就是在這段時間中,我才發現,居然還有專門針對3D音效的openAL、擅長編解碼過程的AudioToolBox等等一些非常優秀的音訊處理框架,重點是這些框架都是iOS SDK中本身就提供了的。

根據網上資料,梳理了如下一張在iOS中的音訊處理各個框架所處的位置,

ww (3).png

高層服務

AVAudioPlayer

**基本操作:**播放、暫停、停止、迴圈等等一些基本的音訊播放功能。

**控制:**可對音訊進行任意時間位置播放;進度控制。

**其它:**可從檔案或緩衝區播放聲音;獲取音視訊關鍵引數,如音訊標題、作者、功率等等。

如果我們並不想實現比如3D立體音效,精確的音訊歌詞同步等功能,那麼這個框架所提供的API是完全足夠的,但是如果我們想要的進行一些比如對音訊流的捕獲,捕獲後還要進行一些RTSP、RTMP等流媒體協議的處理,再或者進行一些RAC、PCM或PCM轉MP3等一些音訊的轉碼方式處理,那這個框架就非常捉雞了。?但是它能夠非常輕鬆的進行簡單的音訊操作,如上所示基本操作、控制等。

AudioQueue

相對於AVAudioPlayer來說,其更加強大!它不僅能夠完成播放音訊和錄製音訊,還能夠通過AudioQueue拿到音訊的原始資訊,想想看!我們能夠拿到音訊的原始資訊,那就可以做比如任意的編碼解碼、一些特效轉化如變音等等騷操作!我們還可以進行任意的應用層封裝,比如說封裝成適用於RTMP、RTSP的流媒體協議處理。

使用Audio Queue,我們只需要進行三個步驟即可:

  1. 初始化Audio Queue。新增一些播放源、音訊格式等。
  2. 管理回撥方法。在回撥方法中我們可以拿到音訊的原始資料。
  3. 例項化Audio Queue。使用AudioQueueOutput完成音訊的最終播放。

openAL

emmm,看到openAL我會想到openGL,openGL主要是用於處理一些3D的影象或變化,openAL主要是在聲源物體、音效緩衝和收聽者這三者之間進行設定來實現3D效果,比如可以設定聲源的方向、速度、狀態等,所以我們可以聽到聲音由遠及近的這種3D效果。

總的來說,openAL主要有三個方面,

  1. 聲源的設定;
  2. 接收者的控制;
  3. 聲源模式的設定。例如聲源是由遠及近運動,還是由近及遠運動,我們還可以把聲源設定在一個3D空間中。

AudioFile

對音訊檔案的資訊進行讀取(注意不是對音訊檔案進行編解碼),通過AudioFile框架的相關API對一個音訊檔案資訊進行讀取,主要有以下幾大步驟:

  1. AudioFileOpenURL。首先我們要通過一個URL開啟音訊檔案。

  2. AudioFileGetPropertyInfo。獲取我們想要讀取的音訊檔案資訊型別。

  3. AudioFileGetProperty。得到相關音訊的屬性NSLog出來即可。

  4. AudioFileClose。關閉音訊檔案。(開啟檔案就要關閉檔案?)

從上我們看到基本上都是歸類於Get方法,但是AudioFile也提供了一個豐富的set方法,可以實時的修改對應音訊相關資訊。

舉個??!!!

我們首先得引入#import <AudioToolbox/AudioToolbox.h>框架,從Xcode 7開始,我們就不需要手動引入framework了,因為當我們引入iOS SDK中對應的framework中的相關.h檔案時,Xcode會自動幫我們匯入對應的framework。

    // 首先從應用沙盒中提取音訊檔案路徑
    NSString *audioPath = [[NSBundle mainBundle] pathForResource:@"test" ofType:@"mp3"];
    // 轉置成URL
    NSURL *audioURL = [NSURL fileURLWithPath:audioPath];
    // 開啟音訊
    // 設定音訊檔案識別符號
    AudioFileID audioFile;
    // 通過轉置後的音訊檔案URL,開啟獲取到的音訊檔案
    // kAudioFileReadPermission:只讀方式開啟音訊檔案;(__bridge CFURLRef):只接受C語言風格型別變數,所以我們要用一個強轉橋接型別轉回去
    AudioFileOpenURL((__bridge CFURLRef)audioURL, kAudioFileReadPermission, 0, &audioFile);
    // 讀取
    UInt32 dictionarySize = 0;
    AudioFileGetPropertyInfo(audioFile, kAudioFilePropertyInfoDictionary, &dictionarySize, 0);
    CFDictionaryRef dictionary;
    AudioFileGetProperty(audioFile, kAudioFilePropertyInfoDictionary, &dictionarySize, &dictionary);
    // 經過以上兩步,我們就拿到了對應音訊的相關資訊。再強轉橋接型別回去即可。
    NSDictionary *audioDic = (__bridge NSDictionary *)dictionary;
    for (int i = 0; i < [audioDic allKeys].count; i++) {
        NSString *key = [[audioDic allKeys] objectAtIndex:i];
        NSString *value = [audioDic valueForKey:key];
        NSLog(@"%@-%@", key, value);
    }
    CFRelease(dictionary);
    AudioFileClose(audioFile);
複製程式碼

執行工程後,即可看到對應的log,

Audio 2.png

與iOS Audio有關的framework有:

framework Name uses
MediaPlayer.framework VC,提供一些控制類ViewController,使用起來較為簡單,致命缺點:功能單一,對底層API高度封裝、高度整合,不利於自定義
AudioIUnit.framework 底層,提供核心音訊處理外掛,例如音訊單元型別、音訊元件介面、音訊輸入輸出單元,用於控制音訊的底層互動
OpenAL.framework 3D,提供3D音訊效果
AVFoundation.framework 全能型,音訊的錄製、播放及後期處理等(基於C)
AudioToolbox.framework 編解碼,音訊編解碼格式轉化

綜上所述,在日常開發中我和大家也要重點關注iOS音訊架構中的高層服務框架,這部分框架是日常開發中經常會手擼程式碼的地方,而在framework層面,我們要重點關注AVFoundation,雖然它是一個基於C的framework。?,但是它卻能夠對音訊進行精細入微的控制,當我們使用AVFoundation進行錄音和播放時,能夠拿到音訊的原始PCM解碼之後的資料,拿到這些資料能夠對音訊進行特效的處理。如果我們要做一個音訊播放類的產品,那麼用到MediaPlayer.framework的次數會很多。

在中層服務中,如果大家有對音訊做了一些比如RTMP、RTSP等流媒體處理的時,可能會用到Audio Convert Services(感覺我是用不到了?)。比如這麼個場景,當我們使用RTMP進行語音直播的時候,通過麥克風採集到的資料可能是原始的PCM資料,但是我們想在播放時候使用AAC格式進行播放,那就得把PCM轉成AAC,那就得用Audio Convert Services這個中間層服務。

當我們想做一些音訊加密演算法或音訊的加密聲波,那可能就會使用到中間層的Audio Unit Services,它可以對硬體層進行一些精細的控制。而Audio File Services是對音訊檔案的封裝和變化。因此啊,除了底層服務的相關框架外,中間層和高層服務是需要我們(尤其是我自己?)去重點掌握的。

Audio SystemSound

SystemSound框架用於播放系統聲音,比如某些特殊的提示音、震動等,若我們要使用該框架來播放自定義聲音,要求對應的音訊編碼方式為PCM的原始音訊,長度一般不超過30秒(你要想超過也沒法,只不過不推薦?)。

當我們使用該框架呼叫震動功能時,只能用於iPhone系列裝置,iPod和iPad系列均無效,因為只有iPhone系列裝置的厚度能夠允許塞下震動模組(而且還是改進後的Tapic Engine)。當我們使用該框架播放系統音樂效果時,靜音情況下無效;播放提示音樂效果時,無論靜音與否均有效。

因此使用SystemSound適用於播放提示音及遊戲中的特殊短音效用處會更大。

舉個??!

    NSString *deviceType = [[UIDevice currentDevice] model];
    if ([deviceType isEqualToString:@"iPhone"]) {
        // 呼叫正常的震動模組,靜音後無效
        AudioServicesPlaySystemSound(kSystemSoundID_Vibrate);
    } else {
        UIAlertController *alertVC = [UIAlertController alertControllerWithTitle:@"注意" message:@"您的裝置不支援震動" preferredStyle:UIAlertControllerStyleAlert];
        [self presentViewController:alertVC animated:true completion:^{
            
        }];
    }
複製程式碼

以上是我們進行呼叫震動模組的測試程式碼,上文已經說明只有iPhone系列裝置中才能體現效果,因此我們最好是加上裝置型別判斷(當然你可以不加?),改框架也是基於C的(比較直接操作底層硬體),程式碼風格也是趨向於C,實際上就這一句話AudioServicesPlaySystemSound(kSystemSoundID_Vibrate);,大家可以從這篇文章中找到其它SystemSoundID,如果系統提供的音效並不適合我們,那麼我們可以載入自定義音效,

    NSURL *systemSoundURL = [NSURL fileURLWithPath:[[NSBundle mainBundle] pathForResource:@"test" ofType:@"mp3"]];
    // 建立ID
    SystemSoundID systemSoundID;
    AudioServicesCreateSystemSoundID((CFURLRef)CFBridgingRetain(systemSoundURL), &systemSoundID);
    // 註冊callBack
    AudioServicesAddSystemSoundCompletion(systemSoundID, nil, nil, soundFinishPlaying, nil);
    // 播放聲音
    AudioServicesPlaySystemSound(systemSoundID);
複製程式碼

分析以上測試程式碼發現一個有趣的現象,就算是自定義音效也是要通過AudioServicesPlaySystemSound去載入音訊檔案識別符號,所以可以大膽的推測!之所以iOS系統佔用這麼大的儲存空間是有相當大的一部分為系統音效音訊資源。不用的音效還沒法刪除,估計也是怕其他App會用到吧。?

音訊引數(瞭解的不多,先記錄一波)

取樣率:

常用的如44100,CD就是。還有一些其它的32千赫茲。取樣頻率越高,所能描繪的聲波頻率也就越高。

量化精度

精度嘛,衡量一個東西的精確程度。是將模擬訊號分成多個等級的量化單位。量化的精度越高,聲音的振幅就越接近原音。因為我們平時聽到的音樂或者聲音都是模擬訊號,而經過計算機處理的都是數字訊號,將模擬訊號轉換為數字訊號的這個過程我們稱之為量化。而量化,我們得需要一定的訊號來逼近它,這種逼近的過程,也就是量化的過程,這種逼近的精度,也就成為量化精度。所以不管我們如何逼近,那也只是逼近而已,與原來的模擬資訊還是有些不同。精度越高,聽起來就越細膩

位元率

數字訊號每秒鐘傳輸的訊號量。

看一個綜合例項,??

audio3.png

通過使用<AVFoundation/AVFoundation.h><AudioToolbox/AudioToolbox.h>框架來完成這個例項,在這個例項中,講讀取一個音訊檔案,對其進行播放、暫停、停止等操作,並可設定是否靜音、迴圈播放次數、調節音量、時間,並可看到當前音訊播放進度。

介面的搭建非常簡單,大家自定義即可,只需要拖拽出對應的相關控制元件屬性及方法即可。

// 播放按鈕點選事件
- (IBAction)playerBtnClick:(id)sender {
    // 設定音訊資源路徑
    NSString *playMusicPath = [[NSBundle mainBundle] pathForResource:@"test" ofType:@"mp3"];
    if (playMusicPath) {
        // 開啟Audio會話例項
        [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:nil];
        NSURL *musicURL = [NSURL fileURLWithPath:playMusicPath];
        audioPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:musicURL error:nil];
        audioPlayer.delegate = self;
        audioPlayer.meteringEnabled = true;
        // 設定定時器,每隔0.1秒重新整理音訊對應檔案資訊(偽裝成實時?)
        timer = [NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:@selector(monitor) userInfo:nil repeats:true];
        [audioPlayer play];
    }
}
複製程式碼
// 定時器任務
- (void)monitor {
    // numberOfChannels聲道數,一般都是2吧,代表左右雙聲道
    NSUInteger channels = audioPlayer.numberOfChannels;
    NSTimeInterval duration = audioPlayer.duration;
    [audioPlayer updateMeters];
    NSString *peakValue = [NSString stringWithFormat:@"%f, %f\n channels=%lu duration=%lu\n currentTime=%f", [audioPlayer peakPowerForChannel:0], [audioPlayer peakPowerForChannel:1], (unsigned long)channels, (unsigned long)duration, audioPlayer.currentTime];
    self.audioInfo.text = peakValue;
    self.musicProgress.progress = audioPlayer.currentTime / audioPlayer.duration;
}
複製程式碼
// 暫停按鈕點選事件
- (IBAction)pauseBtnClick:(id)sender {
    // 再次點選暫停才會播放
    if ([audioPlayer isPlaying]) {
        [audioPlayer pause];
    } else {
        [audioPlayer play];
    }
}
複製程式碼
// 停止按鈕點選事件
- (IBAction)stopBtnClick:(id)sender {
    self.volSlider.value = 0;
    self.timeSlider.value = 0;
    [audioPlayer stop];
}
複製程式碼
// 靜音按鈕點選方法
- (IBAction)muteSwitchClick:(id)sender {
    // 實際上音量為0即靜音
    // 剛好這還是個Switch開關
    audioPlayer.volume = [sender isOn];
}
複製程式碼
// 調節音訊時間方法(UIProgress)
- (IBAction)timeSliderClick:(id)sender {
    [audioPlayer pause];
    // 防止歸一化(Xcode預設都是0~1,轉化為實際值)
    [audioPlayer setCurrentTime:(NSTimeInterval)self.timeSlider.value * audioPlayer.duration];
    [audioPlayer play];
}
複製程式碼
// UIStepper點選事件(音訊迴圈播放)
- (IBAction)cycBtnClick:(id)sender {
    audioPlayer.numberOfLoops = self.cyc.value;
}
複製程式碼

Jan-29-2018 23-11-32.gif

語音識別

在iOS 7之後,AVFoundation提供了語音識別功能,使用它非常的簡單,

// 語音識別控制器
AVSpeechSynthesizer* speechManager = [[AVSpeechSynthesizer alloc] init];
speechManager.delegate = self;
// 語音識別單元
AVSpeechUtterance* uts = [[AVSpeechUtterance alloc] initWithString:@"23333"];
uts.rate = 0.5;
[speechManager speakUtterance:uts];
複製程式碼

需要注意,如果本機系統語言設定成了英文是不能夠識別中文的喔!

相關Demo見這。

原文連結:pjhubs.com

相關文章