圖3:該模塊圖說明了SimpleAudio數(shù)字音頻應(yīng)用的體系結(jié)構(gòu)
3 數(shù)據(jù)音頻應(yīng)用的實(shí)現(xiàn)
圖3給出了數(shù)字音頻應(yīng)用的體系結(jié)構(gòu)。它共有6個(gè)線程,包括主線程和用Orchestrator實(shí)例表示的異步事件處理器。BufferPair將每個(gè)插座接口連接至相應(yīng)的DSP接口。主線程監(jiān)控用戶指令,并在用戶請求關(guān)閉會(huì)話時(shí)調(diào)用SimpleAudio實(shí)例的terminateActivity()方法。所有其它線程通過調(diào)用continueActivity()業(yè)務(wù),定期輪詢SimpleAudio實(shí)例。到了關(guān)機(jī)時(shí),該方法返回false值。
在缺省配置中,該應(yīng)用以8kHz采樣頻率對麥克風(fēng)輸入進(jìn)行采樣,每次采樣采集8比特?cái)?shù)據(jù)。這種配置每秒鐘產(chǎn)生8k字節(jié)的數(shù)字音頻數(shù)據(jù),這對簡單的語音應(yīng)用來說已經(jīng)足夠。但是,它不適合高保真立體聲信號(hào)。一般的CD錄制以44.1kHz的采樣頻率對兩個(gè)立體聲信道每次采樣16比特。這種高保真度信號(hào)的帶寬要求為176.4千字節(jié)/秒。
在缺省配置中,插槽讀模塊和寫模塊采用足夠的帶寬進(jìn)行可靠傳輸,以可靠提供所有從DSPReader模塊采集的數(shù)據(jù)。我們采用了一種直接壓縮技術(shù),一連串同樣的字節(jié)值(像出現(xiàn)在靜音期間的那樣)由一個(gè)專用的轉(zhuǎn)義(escape)值、重復(fù)次數(shù)和重復(fù)值表示。當(dāng)然,更先進(jìn)的壓縮技術(shù)將更為合適。
在實(shí)時(shí)系統(tǒng)中,由抖動(dòng)描述特定實(shí)時(shí)組件的理想執(zhí)行時(shí)間的預(yù)期偏離,由一個(gè)確切的線程描述數(shù)字音頻應(yīng)用的每個(gè)組件。SocketWriter線程接收來自DSPReader模塊的原始數(shù)據(jù)流,對數(shù)據(jù)進(jìn)行壓縮,并將數(shù)據(jù)傳送至網(wǎng)絡(luò)插座通道。如果網(wǎng)絡(luò)插座通道的帶寬有限,只能達(dá)到預(yù)算的8千字節(jié)/秒,那么任何導(dǎo)致SocketWriter延遲數(shù)據(jù)傳輸?shù)亩秳?dòng)影響將隨著時(shí)間而累積。
在缺省配置中,預(yù)計(jì)SocketWriter每125s傳輸1字節(jié)數(shù)據(jù)。如果每秒的音頻數(shù)據(jù)有1個(gè)字節(jié)延遲半毫秒,則1小時(shí)后,累積延遲將約為2秒。為防止抖動(dòng)延遲的累積,該架構(gòu)包含一個(gè)運(yùn)行在16Hz的監(jiān)視線程。
在每個(gè)周期內(nèi),該線程強(qiáng)制讓SocketWriter和DSPWriter組件丟棄62.5ms之前的數(shù)據(jù)。由于我們處理的是音頻數(shù)據(jù),所以通常來講,丟棄的臨時(shí)數(shù)據(jù)值比允許數(shù)據(jù)到達(dá)時(shí)間偏移更可取一些。人們通常不會(huì)注意到丟棄臨時(shí)數(shù)據(jù)字節(jié)的影響。
請注意在第1行出現(xiàn)的@StaticAnalyzable注釋,源碼列表中的@ StaticAnalyzable(enforce_time_analysis = {false}, enforce_non_blocking = {false})。這代表了部分方法簽名(method signature)。注意該注釋給出了enforce_time_analysis 和enforce_non_blocking屬性的參數(shù)值,兩者都是false。這表示該方法的實(shí)現(xiàn)無需將其本身限制在子集內(nèi),對于該子集,靜態(tài)分析器可從中推斷執(zhí)行該方法所需要的嚴(yán)格CPU時(shí)間上限,也不要求靜態(tài)分析器驗(yàn)證該方法執(zhí)行時(shí)永遠(yuǎn)不會(huì)阻斷。
如果這些屬性定義沒有給出,硬實(shí)時(shí)驗(yàn)證器將認(rèn)為程序不合法,因?yàn)樵谠创a列表的(!orchestrator.destroy()) { through 57, }執(zhí)行時(shí),靜態(tài)分析器無法確定該循環(huán)包含了多少次第55行。此外,main方法的執(zhí)行可能會(huì)在第59行的socket_ reader_thread.join()至63行的orchestrator_thread.join()之間阻斷,以及在第51行sa.awaitTermination()調(diào)用的await-Termination()方法中阻斷。
在@StaticAnalyzable注釋中未注明的是enforce_memory_analysis屬性的值。該屬性的缺省值為true,這意味著該方法的實(shí)現(xiàn)必須符合限定的指導(dǎo)方針以使執(zhí)行該方法時(shí)靜態(tài)分析器能夠確定將要分配的最大內(nèi)存。假設(shè)該環(huán)境的實(shí)時(shí)Java規(guī)則將內(nèi)存作為運(yùn)行棧的一部分,則臨時(shí)內(nèi)存分配的上限就表示必需的主線程的運(yùn)行時(shí)棧大小。
注釋有助于軟件開發(fā),并大大簡化軟件維護(hù)工作。通常,系統(tǒng)架構(gòu)師將復(fù)雜的系統(tǒng)功能分為較小的組件,以便由不同的開發(fā)小組實(shí)現(xiàn)。因此,描述不同組件之間連接的接口定義,不僅詳細(xì)說明了可以在組件間傳遞的參數(shù)類型,還包括在每個(gè)組件中必須實(shí)現(xiàn)的實(shí)時(shí)處理的限制,能減少軟件維護(hù)方面的開銷。
對于現(xiàn)有軟件的修改必須遵從組件接口注釋中描述的所有其它特殊實(shí)時(shí)限制。如果軟件維護(hù)人員違反了這些接口要求,他們可以從字節(jié)碼驗(yàn)證器得到直接、明確的反饋。從而確保現(xiàn)有大型實(shí)時(shí)軟件系統(tǒng)的不斷變化不會(huì)動(dòng)搖現(xiàn)有系統(tǒng)的穩(wěn)定性。
在對可靠運(yùn)行該主程序所需的堆棧內(nèi)存進(jìn)行分析時(shí),靜態(tài)分析器必須確定在該方法以及該方法所調(diào)用的方法中,每個(gè)對象要求分配多大內(nèi)存。為了支持靜態(tài)分析結(jié)果的模塊化合成,字節(jié)碼驗(yàn)證器要求每個(gè)由主程序調(diào)用的方法被聲明為@Static-Analyzable,而enforce_time_ analysis屬性設(shè)置為true。快速復(fù)查main方法的實(shí)現(xiàn)可確保無限循環(huán)內(nèi)不產(chǎn)生分配。這是字節(jié)碼驗(yàn)證器所要執(zhí)行的任務(wù)之一。
在第37行的socket_reader_thread = new Thread-Stack(SocketReader.class);到41行的orchestrator_thread = new ThreadStack(Orchestrator.class)之間分配了幾個(gè)新的ThreadStack對象;每次分配描述了主程序派生的線程所使用的堆棧內(nèi)存。一般來說,靜態(tài)分析工具可能難以確定可靠執(zhí)行這些子線程所必需的堆棧內(nèi)存數(shù)量。
每個(gè)ThreadStack構(gòu)造函數(shù)的參數(shù)為提供代碼由相應(yīng)線程執(zhí)行的類(Class)。靜態(tài)分析器要求每個(gè)在該環(huán)境中傳遞的NoHeapRealtimeThread子類具有帶@ StaticAnalyzable注釋,且enforce_ memory_analysis屬性設(shè)置為true的run()方法。如果ThreadStack構(gòu)造函數(shù)的參數(shù)并非來自BoundAsyncEventHandler(例如在Orchestrator類的情況下),則靜態(tài)分析器要求該類的asyncEventHandler()方法采用@StaticAnalyzable注釋來聲明,且enforce_memory_analysis屬性設(shè)置為true。
當(dāng)前線程的運(yùn)行時(shí)棧能滿足所有臨時(shí)內(nèi)存需要。請注意,我們在第23行分配了兩個(gè)臨時(shí)BufferPair實(shí)例,microphone_stream = new BufferPair();而在第24行,speaker_stream = new Buffer-Pair();然后這些對象的參數(shù)被傳遞至構(gòu)造函數(shù),用于包含該軟件應(yīng)用的不同功能組件的各個(gè)線程。硬實(shí)時(shí)驗(yàn)證器實(shí)施的限制之一在于,stack-allocated對象的參數(shù)不能比引用參數(shù)的對象本身生存時(shí)間更長,同樣是通過注釋機(jī)制來執(zhí)行。我們來看一下SocketReader類的構(gòu)造函數(shù):
@ScopedPure
@StaticAnalyzable(enforce_time_analysis = {false}, enforce_non_blocking = {false})
SocketReader(SimpleAudio sa, Buffer-Pair buffers, String socket_name) throws
FileNotFoundException
@ScopedPure注釋說明該構(gòu)造函數(shù)的每個(gè)輸入引用參數(shù)(reference parameter)可以指代那些位于外部嵌套作用域的運(yùn)行時(shí)棧的對象。字節(jié)碼驗(yàn)證器確保這些參數(shù)的內(nèi)容絕不會(huì)復(fù)制到那些由于具有@Scoped指派而未被同樣區(qū)分的變量上。
此外,它禁止將內(nèi)部嵌套作用域變量的值復(fù)制到外部嵌套作用域變量。一個(gè)例外情況是,在特殊環(huán)境下,它可證明帶參數(shù)對象位于與要賦值變量相同或更外層嵌套的作用域。如果這一構(gòu)造函數(shù)的參數(shù)未由@Scoped注釋指定,字節(jié)碼驗(yàn)證器將不允許主程序?qū)?shù)傳至堆棧分配的BufferPair和SimpleAudio對象。
本應(yīng)用展示的RTSJ支持的實(shí)時(shí)編程抽象之一為PeriodicTimer類。注意,本應(yīng)用在第49行舉例說明了PeriodicTimer對象,drumbeat = new PeriodicTimer(start_time, period, orchestrator);并將結(jié)果賦值給本地drumbeat變量。參數(shù)之一為orchestrator對象的引用參數(shù),其本身是BoundAsyncEventHandler的一個(gè)實(shí)例。該drumbeat周期計(jì)時(shí)器被設(shè)置為每秒觸發(fā)orchestrator對象的handleAsyncEvent()方法執(zhí)行16次,即每62.5微秒一次。
采用C或C++語言的實(shí)時(shí)開發(fā)人員可以實(shí)現(xiàn)這些實(shí)時(shí)Java技術(shù)所支持的許多相同構(gòu)造。但是,C或C++程序員必須產(chǎn)生懸掛指針(dangling pointer)以及內(nèi)存泄漏,他們還缺乏標(biāo)準(zhǔn)工具的支持來自動(dòng)分析執(zhí)行時(shí)間和堆棧大小。
另外,C和C++程序員還缺乏完整性檢查以確保方法的實(shí)現(xiàn)能夠滿足文檔化實(shí)時(shí)接口的要求,并確保方法調(diào)用能夠傳遞同樣滿足文檔接口要求的參數(shù)。最后,在對現(xiàn)在軟件系統(tǒng)進(jìn)行維護(hù)的過程中,C和C++程序員沒有工具支持來保證對現(xiàn)有軟件的修改與在原軟件開發(fā)過程中假設(shè)的各種組成要求是相符的。
傳統(tǒng)Java在生產(chǎn)效率和成本上具有許多優(yōu)勢。規(guī)范地使用實(shí)時(shí)Java技術(shù)可提供許多這樣的優(yōu)勢。與使用C和C++相比,一般Java程序員在新代碼開發(fā)期間具有2倍的生產(chǎn)率,而在現(xiàn)在軟件維護(hù)期間具有5到10倍的生產(chǎn)率。隨著嵌入實(shí)時(shí)軟件的大小和復(fù)雜度增加,這些激發(fā)人們向更現(xiàn)代的軟件工程技術(shù)(如由實(shí)時(shí)Java實(shí)現(xiàn)的工程技術(shù))轉(zhuǎn)化的因素已越來越重要了。
評(píng)論
查看更多