音频开发中常见的四个错误
生活随笔
收集整理的這篇文章主要介紹了
音频开发中常见的四个错误
小編覺得挺不錯的,現在分享給大家,幫大家做個參考.
以下內容主要為音頻開發人員所編寫,但同樣也能為其他領域并與此相關的開發者帶來幫助。在下文當中我將介紹針對開發人員的診斷工具,并分享常見的四個錯誤以及如何檢測問題是否存在并做得更好。文 /?Michael Tyson譯 /?John原文?http://atastypixel.com/blog/four-common-mistakes-in-audio-development/制作音頻產品是一項極富創造性的事業,如果你是音頻開發者,那么你開發的產品將為更多相關行業人員帶來創作上的幫助,這不能不說是一件頗具成就感的事業。但與此同時這也意味著每一位音頻開發者肩上的擔子非常沉重,他們需要承擔確保產品服務穩定可靠的責任。一個DJ設備出現不必要的噪聲,這對使用者與開發者來說都是不愿意看到的。而現在我們處于一個跨設備協同大行其道的時代,由于流程的復雜,出現問題時尋找問題的根源往往會成為一件十分麻煩的事情。
錯誤難以避免
電視節目《The Tonight Show》的音頻工程師告訴我,他們選擇Loopy的主要原因是他們多年以來一直是Loopy的用戶,Loopy是值得信賴的。即使一個應用程序在某一環節出現故障,其概率也為千分之一。但一旦長時間不間斷地使用設備,出現故障的可能性就會大大提升,而且故障出現的頻率也會大大增加。尤其是在現場直播活動當中,一點小小的故障都能讓臺上正在表演的藝術家信心崩潰,而直播也意味著技術人員無法在演播過程中打斷直播修理調整,這無疑需要整個團隊冒著巨大的的風險。因此在下文中,我們將重點關注音頻開發人員需要盡到的義務,因為我們需要確保所開發的應用程序在所有時間都穩定可靠。每個人都不是完美的,漏洞能夠減少但無法絕對避免,因此下文我不會站在某個制高點去單純地指示大家該怎么做。我在 Audiobus和 The Amazing Audio Engine上的工作經歷使得我更加傾向于從代碼開發的角度闡述這些命題。然而現實往往是殘酷的,問題遠比我想象的要復雜,甚至許多高知名度的庫也違反了多條規則,以至于最近我不得不趕忙修復Loopy中的一些故障。事實證明,這些故障大多是由第三方庫(不是音頻引擎,而是其他東西)在執行不當操作時引起的。以下是我想要強調的四項容易出現的錯誤:
1. 不要在音頻線程上堅守“鎖(locks)”。
例如:pthread_mutex_lock或@synchronized。2. 不要在音頻線程上使用Objective-C / Swift語言。
例如:[myInstancedoAThing]或myInstance.something。3. 不要在音頻線程上分配內存。
例如:malloc()、new Abcd或[MyClass alloc]。4. 不要在音頻線程上執行文件或網絡I/O。
例如:read、write或sendto。盡管以上內容看上去并無關聯,但違反上述準則中的任何一個都可能會讓你的產品出現很嚴重的問題,尤其是當使用第三方庫的時候。這里需要強調的是,音頻系統容易出現的故障還有很多,例如邏輯錯誤或者只是要求太多的設備功能,但是以上四個問題屬于比較容易發現且被解決的。違反這些規則可能導致一些無關痛癢的錯誤,也可能將整個音頻系統推向崩潰的邊緣,那么究竟是什么原因導致這一切的發生呢?執行任何音頻應用程序都至少需要運行兩個線程:主線程和音頻線程。通常還需要執行其他線程例如網絡線程或用于處理UI的線程。這些線程與當前正在運行的其他所有應用程序線程會共享CPU這一有限的運算資源:而渲染實時音頻的性能要求非常高:每n秒系統就需要將n秒的音頻數據傳輸到音頻硬件。否則,緩沖區將耗盡,用戶會聽到討厭的毛刺或爆音聲——音頻播放到完全靜音之間的粗暴過渡。音頻線程需要定期進行維護,并且需要在非常短的時間限制內完成該任務,一般僅需要幾毫秒或更短的時間。這是一個實時線程,其具備一定的權限。如果UI中發生了一些異常(上方的藍色線程)或者有網絡操作(橙色線程)正在運行,同時CPU也在渲染一些音頻,那么CPU 會丟棄所有內容使得有足夠的算力服務于音頻線程——這是CPU當前需要處理的頭等大事。接下來才是真正麻煩的地方要知道,我們今天所使用的流行操作系統都不是真正的“實時”操作系統。他們采取“盡力而為”的策略,盡力滿足用戶對于算力的需求但卻無法達到最佳效果。這就意味著一些超出計算機能力范圍的任務可能會導致音頻線程中斷并白白浪費時間。因此,我們的目標是最大程度地減少該問題發生的可能性并降低風險。如果您在音頻線程上運行的代碼中違反了上述規則之一,則會發生一些尷尬的事情。假設我們有一些代碼使用與主線程共享的數據結構,例如是一個簡單的播放音符的列表,我們期待的是響應用戶按下按鈕以在該列表中添加和刪除音符的操作:// Define some typesstruct Note {int noteId;float frequency;float velocity;uint64_t startTime;};struct NoteList {int noteCount;struct Note notes[1]; // noteCount-1 Notesfollow};struct NoteList * __noteList;...// Functions to add and remove notes-(int)addNoteWithFrequency:(float)frequency?velocity:(float)velocityatTime:(uint64_t)startTime;- (void)removeNoteWithId:(int)noteId;我們可以編寫一些可接受該列表并根據其中的內容生成音頻的音頻渲染代碼。但是這一過程會使用于主線程和音頻線程之間共享的計算資源。這些線程可以中斷甚至同時運行,所以我們可能會遇到這樣的情況:音頻線程在與主線程編輯數據的同時讀取數據,從而導致進程崩潰或數據損壞。解決這些并發問題的常用方法是使用“鎖(locks)”(也稱為互斥鎖或互斥對象),也就是一次只允許一個線程通過:當我們要與共享的數據結構進行交互時,我們將查看它是否已被鎖定;如果是這樣那么我們需要待其解鎖之后將其鎖定,直至完成該進程后再將其解鎖。從而避免了并發訪問的情況。繼續剛才的示例,我們將在此處以及操作列表的函數中使用互斥鎖保護進程:pthread_mutex_t __noteListMutex;void MyAudioRenderFunction() {// Lock it uppthread_mutex_lock(__noteListMutex);// Make noisefor ( int i=0;i<__noteList->noteCount; i++ ) {ProduceAudioForNote(&__noteList->notes[i]);}// Okay, we're done, unlockpthread_mutex_unlock(__noteListMutex);}如果主線程當前正在更新列表,而此時在音頻線程上使用pthread_mutex_lock時會發生什么?CPU將阻塞音頻線程,并放棄該線程,轉而使用另一個不受阻塞的線程。如果我們花太長時間無法完成主線程上的列表更新,那么…隨著時間的流逝,音頻系統出現了故障。看起來和聽起來就像這樣:如果我們將列表更新代碼替換為運行速度很快的部分,那么我們只需短時保持住鎖的效果,就可以了嗎?答案遠非如此。一些開發人員認為,只要不長時間持有鎖就可以了,實際上他們錯了。還記得上圖中的其他黃色線程嗎?這些黃色進程的優先級比主線程高一點,也許是我們的應用程序正在做一些與MIDI相關的工作;也許它正在執行一些對時間要求嚴格的脫機處理或某些網絡通信……無論如何,這些操作都可能需要更高的優先級。多線程所面對的一項問題是,它超出了我們的控制范圍。調度程序(一種引導CPU注意力的“神秘野獸”)可以隨時中斷線程,并將CPU時間分配給更多需要它的線程;除此之外,調度程序還需要將CPU分配給其他正在運行的應用程序中的其他線程。此時此刻,進程的情況是:因為我們的輔助線程(黃色)的優先級高于我們的主線程(藍色),所以調度程序會從音頻線程正在等待的主線程上竊取CPU時間。這一過程被稱為“優先級倒置”。通過將把主線程上保持鎖定的時間最小化,我們可以降低發生這種情況的可能性,但實際上這一問題并未完全消除。我們的應用每天需要處理上千個用戶的會話,將其與Audiobus或IAA多應用程序環境結合使用會大大提升整個系統崩潰的風險。哪怕在一個典型的會話中有千分之一的機會出現bug,但如果我們的應用每天處理一萬個會話,就意味著bug無時無刻不發生。摒棄了鎖,那么Objective-C或Swift又有什么問題?
如果僅使用Obj-C / Swift渲染音頻那么這會非常方便——無論是傳遞對象還是繼承等都可以實現,除此之外許多第三方音頻庫也可以做到這一點,那么問題出在哪里?問題的關鍵在于:Objective-C和Swift持有鎖是其正常操作的一部分。在Objective-C的消息發送系統(即調用Obj-C方法)的背后,是一系列包括持有鎖在內的完成工作所需的必要代碼。(相關源代碼可訪問:opensource.apple.com:[objc_msgSend](https://opensource.apple.com/source/objc4/objc4-680/runtime/Messengers.subproj/objc-msg-arm64.s) [__class_lookupMethodAndLoadCache3](https://opensource.apple.com/source/objc4/objc4-680/runtime/objc-runtime-new.mm) 以及lookUpImpOrForward lock(runtimeLock)和lock(cacheUpdateLock)。)順便說一句,通過點語法(myInstance.property)訪問屬性也算作一個Objective-C方法調用,因此這也是不可行的。實際上,我們甚至不能允許ARC保留Objective-C或Swift對象,因為該保留機制也持有一個鎖(可參閱:[sidetable_retain](https://opensource.apple.com/source/objc4/objc4-680/runtime/NSObject.mm)中的table.trylock()以及sidetable_retain_slow和table.lock()。分配內存又存在什么問題?
Malloc和其相似的一系列用來分配內存以供進程使用的函數,其分配內存的執行時間不受限制,這意味著整個過程可能比可能需要花費更長的時間,其造成的后果與優先級倒置相似。遺憾的是,這里我無法提供明確的代碼示例以幫助你了解此項問題。而伴隨著無限的執行時間,malloc還使用了一個鎖。文件和網絡IO也是如此
所有的I/O功能——read,fread,fgets,write,send,sendtorecv、recvfrom等也有無限的執行時間,其需要在輔助線程上進行。那么libdispatch和正在使用的塊呢?
不幸的是,這些也是禁區。盡管您可以安全地在音頻線程上調用一個塊,只要不在其中保留或釋放它。在音頻線程上創建一個塊會導致一些內存分配以及一些對象的保留,同時這兩個對象都將持有鎖。那么,該怎么辦?
就像我之前說的,我們所使用的絕大多數操作系統都不是真正的實時操作系統,這意味著操作系統本身無法保證實時性可以得到有效落實。因此,我們所追求的是最大程度地減少遇到麻煩的機會。好消息是:這里有很多工具可以幫助您解決此問題,同時也有一些非常容易遵循的模式。首先,Objective-C圍繞C構建,并且實際上可以像C結構一樣從implementation塊中的C函數訪問Objective-C,示例如下:FFCrewMember * jayne;...jayne->location = FFLocationBunk;這是一個普通的取消引用舊指針,沒有Objective-C來持有鎖或其他任何東西,故而在實時線程上使用它是絕對安全的。因此,您仍然可以繞過并使用對象。但是就像我之前說的,您需要避免任何保留。除此之外,在聲明一個Objective-C實例變量時我們只需要使用該__unsafe_unretained屬性來繞過任何ARC內容:void MyCFunction(__unsafe_unretainedFFFertileLand * thisLand) {__unsafe_unretained FFFertileLand *yourGrave = thisLand;}小菜一碟,是嗎?需要注意的是:在尋求其他專家的驗證時,Tempo Rubato的RolfW?hrmann(NLog,Nave,iSEM)建議禁止從音頻代碼中引用對Objective-C或Swift對象的任何引用,即使其具有該__unsafe_unretained屬性,僅僅是傳入C或C ++變量也不能進行。他主張將兩者完全分開。當然,這是最安全的選擇。這是一個非常防御的策略。跨線程同步呢?我們如何更換鎖?
這里有很多可能性。蘋果提供了許多非常有用的內部組件如libkern/OSAtomic.hheader,還有OSAtomicEnqueue和OSAtomicDequeue,OSAtomicAdd32Barrier以及OSMemoryBarrier。如果不知道該怎么辦,使用trylock(例如pthread_mutex_trylock)也是可行的選擇。在所有的現代處理器上,你可以安全賦值給一個int,double,float,bool,BOOL或在一個線程中的指針變量并讀取其不同的線程而不用擔心線程被打斷。其中只有部分值已在讀取時被分配,這是因為字節,半字和字長的分配是atomic的(http://ds.michael.tyson.id.au/qv8jv8mtfC/ armv7-a-r-manual-A3.5.4.pdf) (《ARM?體系結構參考手冊》ARMv7-A和ARMv7-R版),只要該變量是自然對齊的(如果它是Objective-C實例變量)就可存在于未打包的結構中。注意,這不一定適用于其他類型的變量。如果您使用的是32位處理器,并且分配了一個uint64_t 變量,您可能會遇到麻煩,因為處理器需要兩條單獨的指令來存儲值,而另一個線程可以在讀取過程中途讀取該值。如果你不想被亂成一團的頭緒所影響,其實有解決方案可供你使用。以下是我自己研制的一些解決方案:[TPCircularBuffer(https://github.com/michaeltyson/TPCircularBuffer) 是一個被廣泛使用的循環緩沖區庫,我 早在幾年前就寫過這一庫并且至今仍每天使用它。你可以將數據從一個線程的一端粘到另一端,然后從另一線程中拉出它而無需持有任何鎖,并且通過虛擬內存策略,您可以完全忽略“使用帶wrap point的循環緩沖區”這一事實。它還使您可以讀寫AudioBufferLists(交錯和非交錯),并且還可以攜帶AudioTimestamp值,所有這些都使其可以與CoreAudio一起使用。其 被內置于 在AmazingAudio Engine 2中作為AECircularBuffer。來自?? AmazingAudio Engine 2的AEManagedValue提供了一個指針變量,該指針變量經過精心設計以使其分配過程可以達到atomic,并且僅在音頻線程完成該值后才釋放。也就是說,您可以使用它指向您喜歡的任何數據結構或Objective-C類,并且當您更改值時,僅在不會與音頻線程混淆的情況下舊值才會被釋放。接下來我將在原有示例的基礎上,借助AEManagedValue維護對NoteList指針的引用,并在更改列表時簡單地重新分配列表:@interface MyClass ()@property (nonatomic, strong)AEManagedValue * noteList;@end@implementation MyClass- (instancetype)init {...self.noteList = [AEManagedValue new];...}-(int)addNoteWithFrequency:(float)frequency?velocity:(float)velocityatTime:(uint64_t)startTime {// Get old list, and copy it to new onestruct NoteList * oldNoteList =self.noteList.pointerValuestruct NoteList * newNoteList =malloc([self sizeOfNoteListWithCount:oldNoteList->count + 1]);memcpy(newNoteList,?oldNoteList, [selfsizeOfNoteListWithCount:oldNoteList->count]);// UpdatenewNoteList->count++;newNoteList->notes[newNoteList->count-1]= ...;// Assign new list - old value will beautomatically freed at a safe timeself.noteList.pointerValue = newNoteList;}voidMyAudioRenderFunction(__unsafe_unretained MyClass * self) {// Get latest valuestruct NoteList * noteList =AEManagedValueGetValue(self->_noteList);// Make noisefor ( int i=0; i<noteList->noteCount;i++ ) {ProduceAudioForNote(¬eList->notes[i]);}}或者,讓事情變得更簡單:AEArray,同樣源自Amazing Audio Engine 2,其建立在AEManagedValue用以實現NSArray和C數組。你可以在音頻線程之間安全地訪問其間的映射,也可以直接在音頻線程上訪問Objective-C實例或者提供一個在這些Objective-C對象和C結構之間進行映射的塊。因此,我們可以再次回顧示例。假設MyNote在NSArray中有一個Objective-C類:@interface MyClass ()@property (nonatomic, strong)NSMutableArray * playingNotes;@property (nonatomic, strong) AEArray *noteArray;@end@implementation MyClass- (instancetype)init {...self.playingNotes = [NSMutableArray array];self.noteArray = [[AEArray alloc]initWithCustomMapping:^void *(id item) {// We'll provide a map between theObjective-C MyNote instance, the properties of// which we cannot safely access on theaudio thread; and our C struct, which we// *can* safely access.// This happens on the main thread during acall to "updateWithContentsOfArray",// and the pointer we return will be freedautomatically when the original// Objective-C object is removed from thearray.struct Note * note = malloc(sizeof(structNote));note->frequency =((MyNote*)item).frequency;note->velocity =((MyNote*)item).velocity;note->startTime =((MyNote*)item).startTime;return note;}];...}- (int)addNote:(MyNote *)note {// Update our array[self.playingNotes addObject:note];[self.noteArrayupdateWithContentsOfArray:self.playingNotes];}voidMyAudioRenderFunction(__unsafe_unretained MyClass * self) {// Enumerate the pointers in the arrayAEArrayEnumeratePointers(self->_noteArray,struct Note *, note, {ProduceAudioForNote(note);}}來自?? TheAmazing Audio Engine 2的[AEMessageQueue](http://theamazingaudioengine.com/doc2/interface_a_e_message_queue.html) 可被開發者用于安排要音頻線程上執行的塊:[self.messageQueueperformBlockOnAudioThread:^{_state = newState;}];…從另一個角度看,開發者可以在主線程上安全地計劃目標或者選擇器:AEMessageQueuePerformSelectorOnMainThread(self->_messageQueue,self,@selector(doSomethingWithTrack:),AEArgumentScalar(track),AEArgumentNone);這個工作有點像libdispatch,但是音頻線程是完全安全的。那么,如何知道自己的項目是否有問題?
我創建了一個可以使該診斷瑣事變得更容易一些的工具,其思想來自泰勒·霍利迪(TaylorHolliday)(Audulus名望)。這是一個名為RealtimeWatchdog的小型庫(現在也已內置在The AmazingAudio Engine 2和版本1中)。在您將其添加到項目中后,它將密切監控音頻線程上的任何不安全活動,并在發現任何異常時發出警告。它不會捕獲所有內容,也不會捕獲Apple自己的系統代碼中的任何內容,但是它將捕獲一些在您的代碼以及您正在使用的任何靜態庫的代碼中的鎖、內存分配、所有正在被使用的Objective-C活動(但不包括Swift)、所有對象保留以及一些通用I/O任務。要使用它,只需將“RealtimeWatchdog”添加到您的Cocoapods Podfile中,(“pod ‘RealtimeWatchdog’”)然后運行podinstall即可。它會自動通知您有關調試版本的所有違規信息,并且絕對不會在您的發行的版本中執行任何操作。對于調試版本,它會減慢Objective-C消息的發送速度,因此您可以隨時通過注釋由AERealtimeWatchdog.h定義的REALTIME_WATCHDOG_ENABLED來禁用它。如果您不使用Cocoapods,請查看GitHub存儲庫上的說明。如果您使用的是AmazingAudio Engine 1或2,則只需取消注釋其中的由AERealtimeWatchdog.h定義的REALTIME_WATCHDOG_ENABLED即可將其打開。最后的想法
由于iOS平臺的便攜性、便利性、可負擔性和強大功能,越來越多的音樂家正在出售其所使用的硬件并轉向iOS。我經常聽到用戶關于Loopy和Audiobus如何幫助他們開創無限可能,不能不說這令人興奮無比。但是我也經常聽到失望和沮喪:應用程序故障、無法正常工作、需要解決方案……許多故障都是因為開發者違反了一些本可以避免的注意事項,這些都是可以被優化的。需要注意的是,該建議僅基于對系統級情況的假設。但是iOS和Mac OS X是封閉式系統,我們只能通過opensource.apple.com來了解一下,選擇的范圍非常之小。即使我們遵循這些規則,問題依舊可能會發生。因此,我們所能做的是進行有根據的猜測,并在理想情況下進行測試和實驗,盡管這可能很困難但在技術上可行。您可以加入Core Audio API郵件列表并提出問題,打開Xcode并查看您在音頻代碼中正在做什么;嘗試使用RealtimeWatchdog來檢查您的代碼以及所使用的任何第三方庫的代碼,也可以考慮選擇C ++——對于音頻而言,C ++往往比Objective-C更安全,并且比單獨使用C提供更多的功能。LiveVideoStack?秋季招聘
LiveVideoStack正在招募編輯/記者/運營,與全球頂尖多媒體技術專家和LiveVideoStack年輕的伙伴一起,推動多媒體技術生態發展。同時,也歡迎你利用業余時間、遠程參與內容生產。了解崗位信息請在BOSS直聘上搜索“LiveVideoStack”,或通過微信“Tony_Bao_”與主編包研交流。
總結
以上是生活随笔為你收集整理的音频开发中常见的四个错误的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 实现视频和音频的零延迟是标准的零和博弈
- 下一篇: 5G万物智联下互联网通信技术升级之路