程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> 關於.NET >> WPF應用程序中的發聲功能

WPF應用程序中的發聲功能

編輯:關於.NET

幾個星期以前,我坐在一輛嶄新的豐田普銳斯汽車中,聽著租車公司的銷售代理講解著儀表盤上遍布 的陌生控制開關和指示器。“哇,”我想,“雖然技術和車一樣都那麼陳舊了,制造商仍繼續美化著用 戶界面”。

從最廣義的層面上說,用戶界面是人機交互的地方。雖然這一概念與技術本身一樣歷史悠久,但用戶 界面作為一種藝術形式大放異彩倚仗的卻是個人計算機革命。

現在,恐怕只有很小一部分個人計算機用戶能夠記得 Apple Macintosh 和 Microsoft Windows 圖形 用戶界面問世之前的情形了。當時,也就是 80 年代中晚期,一些專家曾擔憂用戶界面的標准化會導致 應用程序千篇一律,單調乏味。但事實並非如此。相反,隨著標准控件的推出卸下了設計者和編程者需 要自創滾動條的負擔,用戶界面實際上開始發生變革並變得愈發有趣起來。

就此而言,Windows Presentation Foundation (WPF) 所引入的新模式讓用戶界面更加光彩。WPF 為 保留模式圖形、動畫和 3-D 奠定了堅實的基礎。在此基礎上,它增加了父子元素樹狀層次結構以及一種 稱為 XAML 的強大標記語言。結果在通過模板化功能自定義現有控件以及通過組裝現有組件構建新控件 方面,都實現了無與倫比的靈活性。

而且,這些新概念不僅僅用於客戶端編程。XAML 和 WPF 類是 Microsoft .NET Framework 的健壯子 集,目前通過 Silverlight 即可用在基於 Web 的編程中。您已迎來在客戶端應用程序與 Web 應用程序 之間真正共享自定義控件的新時代。我確信在利用多點觸控等新技術的同時,這種勢頭會發展到移動應 用程序,並最終涵蓋各種各樣的信息和娛樂系統。

基於這些原因,我相信用戶界面已成為應用程序編程中更加關鍵的部分。本專欄將探討 WPF 和 Silverlight 中用戶界面設計的潛能,包括在可能的情況下使用跨平台代碼。

前奏

用戶界面選擇的好與壞並非總是一目了然。Microsoft Office 97 中首次引入的人性化活頁夾 Clippy 在當時看可能是一個非常不錯的創意。因此,我將更專注於技術性潛能而不是設計潛能。我將避 免使用“最佳做法”一詞,這個詞更適合用在歷史和市場環境。

有一個比較好的現成慣例:計算機若非為響應特定用戶命令而播放視頻或聲音文件,就不該發出任何 聲音。我打算打破這一限制,在此向您介紹如何通過在運行時生成波形數據,在 WPF 應用程序中播放自 定義聲音。

雖然此發聲功能尚未正式納入 .NET Framework 之中,但可通過 Codeplex (naudio.codeplex.com) 上提供的 NAudio 庫來實現。通過該站點中的鏈接,您可以到 Mark Heath 的博客中查看一些示例代碼 ,還可以查看 Sebastian Gray 的站點教程。

可以在 Windows 窗體或 WPF 應用程序中使用 NAudio 庫。由於該庫通過 PInvoke 訪問 Win32 API 函數,因此不能用於 Silverlight。

在本文中,我使用的是 NAudio 1.3.8 版。當您創建使用 NAudio 的項目時,需要將其編譯為可進行 32 位處理。請轉至“屬性”頁的“生成”選項卡,並從“平台目標”下拉列表中選擇 x86。

雖然該庫為需要使用聲音的專業化應用程序提供了許多功能,但我向您演示的使用方法可能適合更通 用的應用程序。

例如,假設您的應用程序允許用戶在窗口上四處拖動對象,而您希望伴隨拖動播放一個簡單的聲音( 比如一個正弦波),而該聲音的頻率隨著對象到窗口中心距離的增加而提高。

這就是波形音頻要做的工作。

如今,幾乎所有 PC 都帶有發聲硬件,該硬件通常通過主板右側的一兩個芯片實現其功能。一般來說 ,此硬件無非就是一對數字模擬轉換器 (DAC)。當向這兩個 DAC 傳送描述波形的恆定整數流時,就會發 出立體聲。

那麼會涉及多少數據呢?現在的應用程序一般生成“CD 音質”的聲音。采樣率是恆定的每秒 44,100 個樣本。(Nyquist Theorem 指出采樣率須至少為最高頻率的兩倍時才能重現聲音。慣常的說法是人耳 能夠聽到頻率介於 20Hz 與 20,000Hz 之間的聲音,因此 44,100 可謂充裕。)每個樣本都是一個有符 號 16 位整數,即一個表示 96 分貝信噪比的大小。

創建波形

Win32 API 通過一個以 waveOut 一詞開頭的函數集合來訪問發聲硬件。NAudio 庫將這些函數封裝在 一個 WaveOut 類中,該類用於處理 Win32 互操作,並消除大部分雜音。

WaveOut 要求您提供一個實現 IWaveProvider 接口的類,這意味著該類應定義一個 WaveFormat 類 型的可獲取屬性,以(至少)指示采樣率和聲道數。該類還定義一個名為 Read 的方法。Read 方法的參 數包含一個該類需要在其中填充波形數據的字節數組緩沖區。使用默認設置時,每秒將調用 10 次此 Read 方法。在填滿此緩沖區後,您便會聽到毫無美感的斷續聲和刺耳的穩態噪聲。

NAudio 提供了幾個實現 IWaveProvider 的抽象類,這些抽象類可使常見的音頻作業更容易實現。 WaveProvider16 類實現一個抽象 Read 方法,使您可以用短型數據而不是字節來填充緩沖區,這樣不必 將樣本一分為二。

圖 1 是一個派生自 WaveProvider16 的簡單 SineWaveOscillator 類。通過該構造函數可以指定采 樣率,但使用另一個參數調用基類構造函數,該參數指示表示非立體聲的單聲道。

圖 1 為 NAudio 生成正弦波樣本的類

class SineWaveOscillator : WaveProvider16 {
  double phaseAngle;

  public SineWaveOscillator(int sampleRate):
   base(sampleRate, 1) {
  }

  public double Frequency { set; get; }
  public short Amplitude { set; get; }

  public override int Read(short[] buffer, int offset,
   int sampleCount) {

   for (int index = 0; index < sampleCount; index++) {
    buffer[offset + index] =
     (short)(Amplitude * Math.Sin(phaseAngle));
    phaseAngle +=
     2 * Math.PI * Frequency / WaveFormat.SampleRate;

    if (phaseAngle > 2 * Math.PI)
     phaseAngle -= 2 * Math.PI;
   }
   return sampleCount;
  }
}

SineWaveOscillator 定義兩個屬性,分別名為 Frequency(雙精度類型)和 Amplitude(短型)。 該程序維護一個名為 phaseAngle 的字段,該字段始終位於 0 到 2π 范圍之間。在每次采樣時,都會 將 phaseAngle 傳遞給 Math.Sin 函數,然後按稱為相角增量的值遞增,這是一項涉及頻率和采樣率的 簡單計算。

(如果您要同時生成許多波形,將需要盡量使用整數算術運算優化處理速度,甚至需要以短型數組的 形式實現一個正弦波表。但為了簡化波形音頻的使用,使用浮點計算也可以。)

若要在程序中使用 SineWaveOscillator,需要引用 NAudio.dll 庫和一個 using 指令:

using NAudio.Wave;

下面是啟動聲音播放的一些代碼。

WaveOut waveOut = new WaveOut();
SineWaveOscillator osc = new SineWaveOscillator(44100);
osc.Frequency = 440;
osc.Amplitude = 8192;
waveOut.Init(osc);
waveOut.Play();

此處 Frequency 屬性初始化為 440Hz。在音樂領域中,該頻率為高於中央 C 調的 A 調的頻率,通 常用作標准音調並用於調音目的。當然,隨著聲音的播放,Frequency 屬性可發生更改。若要關閉聲音 ,可將 Amplitude 設置為 0,但 SineWaveOscillator 將繼續接收對 Play 方法的調用。若要停止這些 調用,請對 WaveOut 對象調用 Stop。當您不再需要 WaveOut 對象時,應對該對象調用 Dispose 以便 正確釋放資源。

走調

當我在示例程序中使用 SineWaveOscillator 時,它當時未按預期的方式工作。我想的是伴隨窗口中 的對象拖動播放一個聲音,並希望該聲音的頻率隨該對象到窗口中心距離的不同而變化。但在我移動對 象時,頻率轉換不太流暢。我聽到的是起伏不平的滑奏(就像手指滑過一串鋼琴鍵或豎琴弦一樣),而 我想要的效果是流暢的滑音(就像長號或格什溫創作的“藍色狂想曲”開頭的單簧管演奏一樣)。

問題在於:每次從 WaveOut 調用 Play 方法時,都會導致整個緩沖區按同一頻率值進行填充。在 Play 方法填充緩沖區期間,頻率不能在響應用戶拖動鼠標操作時更改,因為 Play 正在用戶界面線程中 執行。

那麼該問題有多糟糕,而這些緩沖區有大呢?

NAudio 中的 WaveOut 類包含一個 DesiredLatency 屬性,默認情況下該屬性設置為 300 毫秒。該 類還包含一個設置為 3 的 NumberOfBuffers 屬性。(多個緩沖區有助於提高吞吐量,因為在 API 讀取 一個緩沖區的同時應用程序可以填充另一個緩沖區。)因此,每個緩沖區等同於 0.1 秒的采樣。通過試 驗,我發現無論如何減少 DesiredLatency 都會導致聽得到的頓音。可以增加緩沖區數(請務必選擇適 當的值以使緩沖區字節數大小為 4 的倍數),但這樣做似乎幫助不大。還可以通過將靜態方法調用 WaveCallbackInfo.FunctionCallback 傳遞給 WaveOut 構造函數,讓 Play 方法在輔助線程上運行,但 這樣做也沒有明顯效果。

我很快發現,我需要的是在填充緩沖區期間親自執行滑音的振蕩器。我需要的是 PortamentoSineWaveOscillator,而不是 SineWaveOscillator。

PortamentoSineWaveOscillator

我還需要進行其他一些更改。人對頻率的感知是對數級的。八度音定義為頻率的翻倍,並且樂譜中的 各八度音在聽覺上是類似的。對於人的神經系統而言,100Hz 和 200Hz 之間的差異與 1000Hz 和 2000Hz 之間的差異是相同的。在音樂領域,每個八度音包含 12 個在聽覺上相等的音階(稱為半音)。 因此,這些半音的頻率會按等於 2 的 12 次方根的乘法因子連續遞增。

我希望我的滑音也是對數級的,所以在 PortamentoSineWaveOscillator 中定義了一個名為 Pitch 的新屬性,該屬性如下計算頻率:

Frequency = 440 * Math.Pow(2, (Pitch - 69) / 12)

這是比較標准的公式,符合樂器數字接口 (MIDI) 中所使用的約定(我將在以後的專欄中對此進行討 論)。如果自下而上地為鋼琴的所有音符進行編號,其中中央 C 調分配有 Pitch 值 60,則高於中央 C 調的 A 調為 69,並且該公式確定其頻率為 440Hz。在 MIDI 中,這些 Pitch 值是整數,而在 PortamentoSineWaveOscillator 類中,Pitch 是雙精度類型,因此音符之間的分級可行的。

在 PortamentoSineWaveOscillator 中,Play 方法檢測 Pitch 何時發生更改,然後根據緩沖區的剩 余大小逐漸地更改用於計算頻率的值(因此,相角會遞增)。該邏輯使 Pitch 可以在方法執行期間發生 更改,但前提是 Play 在輔助線程上執行。

如代碼下載中的 AudibleDragging 程序所示,這樣做大獲成功!該程序在接近窗口中心的位置創建 七個顏色不同的小方塊。當您用鼠標捕獲到它們時,該程序就會使用 PortamentoSineWaveOscillator 創建一個 WaveOut 對象。隨著對象的拖動,該程序只需確定到窗口中心的距離,並根據以下公式設置振 蕩器的音調即可:

60 + 12 * distance / 200;

換句話說,每移動 200 個單位的距離,中央 C 調就會提高一個八度。當然,AudibleDragging 是一 個比較笨的小程序,它可能令您比以往更加確信應用程序應永遠是無聲的。但是,在運行時生成自定義 聲音的潛能是如此強大,以致於不能妄加否定。

播放

當然,您並不僅限於使用單正弦波振蕩器。您還可以從 WaveProvider16 派生一個混音器,並使用該 混音器組合多個振蕩器。可以將簡單波形組合為更復雜的波形。Pitch 屬性的使用為指定音符建議了一 種簡單方法。

但如果您希望應用程序從揚聲器中放出音樂和樂器的聲音,您一定樂意知道 NAudio 還包含可用於從 Windows 窗體或 WPF 應用程序生成 MIDI 消息的類。我很快會為您說明如何做到這一點。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved