本文章是由機器翻譯。

DirectX 要素

建構 Windows 8 的聲音振盪器

Charles Petzold

下載代碼示例

Charles Petzold
我一直在製造電子樂器作為一種愛好現在大約 35 年。我開始在晚 20 世紀 70 年代佈線了 TTL 和 CMOS 晶片和晚得多去軟體路線 — — 先用多媒體擴展到 Windows 在 1991 年和最近Windows Presentation Foundation(WPF) 的 NAudio 庫與 MediaStreamSource 類在 Silverlight 和 Windows Phone 7。就在去年,我專門討論我觸摸的一對夫婦分期付款 & 列去為 Windows Phone 應用程式播放聲音和音樂。

我也許應該厭倦了這個時候,,或許不願意探索又一次的聲音代 API。但我沒有,因為我認為 Windows 8 可能是的最佳 Windows 平臺尚未製作樂器。Windows 8 將高性能的音訊 API 結合在一起 — — XAudio2 的 DirectX 元件 — — 與上手持平板電腦觸控式螢幕。這種結合提供了很大的潛力,而且我特別感興趣探索如何可以作為一個微妙和親密到樂器完全在軟體中實現介面利用觸摸。

振盪器、範例及頻率

任何音樂合成器的聲音發電設施的核心是多個振盪器,所以稱為,因為它們生成更多或更少週期的振盪波形,在特定的頻率和數量。在生成音樂的聲音,通常創建不會發生變化的週期波形的振盪器聲音相當無聊。更有趣的振盪器納入顫音,顫音或不斷變化的音色,而且他們只有大約定期。

一種程式,希望創建使用 XAudio2 振盪器開始通過調用 XAudio2Create 函數。這提供了一個實現 IXAudio2 介面的物件。從該物件中,您可以調用 CreateMasteringVoice 只需一次獲取實例的 IXAudio2MasteringVoice,其中主要的音訊混音器的作用。只有一個 IXAudio2MasteringVoice 存在的任何時候。相反,你會一般 CreateSourceVoice 多次調用創建的 IXAudio2SourceVoice 介面的多個實例。每個這些 IXAudio2SourceVoice 實例可以作為獨立的振盪器。合併多個振盪器伴唱的文書、 合奏或一個完整的樂團。

IXAudio2SourceVoice 物件的創建和提交緩衝區包含一個描述波形的數位序列生成的聲音。這些數位通常稱為樣本。他們常常以恒定速率 16 位寬 (CD 音訊的標準),和他們來 — — 通常 44,100 Hz (也為 CD 音訊標準) 左右。這種技術具有脈衝代碼調製或 PCM 別致的名稱。

雖然此序列的樣本可以描述十分複雜的波形,往往一個合成器生成一個相當簡單的樣本流 — — 最常用的方波、 三角波或鋸齒 — — 與對應的波形頻率 (視為螺距) 和被看作是卷的平均振幅週期。

例如,如果採樣速率是 44,100 Hz,而且每個週期的 100 個樣本得到逐步較大、 然後較小、 然後負,和背為零的值,由此產生的聲音的頻率是除以 100 或 441 Hz 的 44,100 — — 頻率接近人類的聽覺範圍的知覺中心。(440 Hz 的頻率是中間 C 以上 A 和被用作一種優化的標準。

IXAudio2SourceVoice 介面繼承一個名為 IXAudio2Voice 的 SetVolume 方法並定義其自己命名的 SetFrequencyRatio 的方法。我特別好奇的這後一種方法,因為它似乎可以提供一種方法來創建生成在變頻和麻煩最少的特定週期波形的振盪器。

圖 1 顯示一個名為實現這一技術的 SawtoothOscillator1 類的大部分。雖然我使用熟悉的 16 位整數樣品用於定義波形,內部 XAudio2 使用 32 位浮點點樣品。對於性能關鍵的應用程式,您可能需要探索存在浮點和整數之間的性能差異。

圖 1 SawtoothOscillator1 類的多

SawtoothOscillator1::SawtoothOscillator1(IXAudio2* pXAudio2)
{
  // Create a source voice
  WAVEFORMATEX waveFormat;
  waveFormat.wFormatTag = WAVE_FORMAT_PCM;
  waveFormat.
nChannels = 1;
  waveFormat.
nSamplesPerSec = 44100;
  waveFormat.
nAvgBytesPerSec = 44100 * 2;
  waveFormat.
nBlockAlign = 2;
  waveFormat.wBitsPerSample = 16;
  waveFormat.cbSize = 0;
  HRESULT hr = pXAudio2->CreateSourceVoice(&pSourceVoice, &waveFormat,
                                           0, XAUDIO2_MAX_FREQ_RATIO);
  if (FAILED(hr))
    throw ref new COMException(hr, "CreateSourceVoice failure");
  // Initialize the waveform buffer
  for (int sample = 0; sample < BUFFER_LENGTH; sample++)
    waveformBuffer[sample] =
      (short)(65535 * sample / BUFFER_LENGTH - 32768);
  // Submit the waveform buffer
  XAUDIO2_BUFFER buffer = {0};
  buffer.AudioBytes = 2 * BUFFER_LENGTH;
  buffer.pAudioData = (byte *)waveformBuffer;
  buffer.Flags = XAUDIO2_END_OF_STREAM;
  buffer.PlayBegin = 0;
  buffer.PlayLength = BUFFER_LENGTH;
  buffer.LoopBegin = 0;
  buffer.LoopLength = BUFFER_LENGTH;
  buffer.LoopCount = XAUDIO2_LOOP_INFINITE;
  hr = pSourceVoice->SubmitSourceBuffer(&buffer);
  if (FAILED(hr))
    throw ref new COMException(hr, "SubmitSourceBuffer failure");
  // Start the voice playing
  pSourceVoice->Start();
}
void SawtoothOscillator1::SetFrequency(float freq)
{
  pSourceVoice->SetFrequencyRatio(freq / BASE_FREQ);
}
void SawtoothOscillator1::SetAmplitude(float amp)
{
  pSourceVoice->SetVolume(amp);
}

在標頭檔中,基頻設置乾淨地將分為 44,100 的取樣速率。 從,可這就是該頻率的波形的一個週期的長度計算緩衝區的大小:

static const int BASE_FREQ = 441;
static const int BUFFER_LENGTH = (44100 / BASE_FREQ);

此外在頁眉中檔作為欄位該緩衝區的定義是:

short waveformBuffer[BUFFER_LENGTH];

後創建 IXAudio2SourceVoice 物件,Sawtooth­Oscillator1 建構函式與一個週期的鋸齒波形緩衝區已滿 — — 簡單的波形振幅的-32768 從延伸到振幅的 32,767。 指示應永遠重複 IXAudio2SourceVoice 提交此緩衝區。

不需要任何進一步的代碼,這是永遠扮演 441 Hz 鋸齒波的振盪器。 這就是偉大的但它不是非常多的用途。 為了讓 SawtoothOscillator1 有點更多功能,我還包含了 SetFrequency 的一種方法。 此參數是類使用調用 SetFrequencyRatio 的頻率。 傳遞給 SetFrequencyRatio 的值的範圍可以從 XAUDIO2_MIN_FREQ_RATIO (或 1/1,024.0) 的浮點型值超過較早前為 CreateSourceVoice 參數指定的最大值。 我用 XAUDIO2_MAX_FREQ_RATIO (或 1,024.0) 這一論點。 人的聽覺範圍 — — 約 20 Hz 至 20,000 Hz — — 是由這些應用到 441 基頻的兩個常量定義的邊界內好。

緩衝區和回檔

我必須承認我是 SetFrequencyRatio 方法的最初有點懷疑。 數位增加和減少的波形頻率不是一個簡單的任務。 我覺得必須要與通過演算法生成的波形進行比較結果。 這是 OscillatorCompare 專案,這是此列的可下載代碼背後的動力。

OscillatorCompare 專案包括我已經描述以及一個 SawtoothOscillator2 類的 SawtoothOscillator1 類。 這第二類有一個 SetFrequency 方法,控制項類如何動態組建定義波形的樣本。 此波形是不斷建造在緩衝區中,並提交即時回應回檔中的 IXAudio2SourceVoice 物件。

通過實現 IXAudio2VoiceCallback 介面,類可以從 IXAudio2SourceVoice 接收回調。 實現此介面的類的實例然後作為參數傳遞給 CreateSourceVoice 方法。 SawtoothOscillator2 類實現此介面本身和它傳遞給 CreateSourceVoice,也表明它不會做出的 SetFrequencyRatio 使用它自己的實例:

pXAudio2->CreateSourceVoice(&pSourceVoice, &waveFormat,
        XAUDIO2_VOICE_NOPITCH, 1.0f,
        this);

實現 IXAudio2VoiceCallback 的類,可以使用 OnBufferStart 方法時它是提交一個新的波形資料緩衝區的時候收到通知。 一般時使用 OnBufferStart 來使波形資料保持最新,您會想要保持一雙緩衝區和替換它們。 如果您從另一個源例如音訊檔獲得音訊資料,這可能是最好的解決辦法。 目標是不要讓音訊處理器成為"餓"。保持前處理緩衝區有助於防止饑餓,但並不能保證它。

但我被吸引向由 IXAudio2VoiceCallback 定義的另一種方法 — — OnVoiceProcessingPassStart。 除非您正在用很小的緩衝區,一般 OnVoiceProcessingPassStart 比 OnBufferStart 更頻繁調用,並指示時將要處理的音訊資料區塊和需要多少位元組。 在 XAudio2 文檔中,此回檔方法被促進作為一個最低的延遲,這通常是非常可取的互動式的電子音樂儀器。 你不想按下一個鍵,聽說明之間的延遲 !

SawtoothOscillator2 標頭檔定義兩個常量:

static const int BUFFER_LENGTH = 1024;
static const int WAVEFORM_LENGTH = 8192;

第一個常數是緩衝區的用來提交波形資料的長度。 這裡它充當一個環形緩衝區。 對 OnVoiceProcessingPassStart 方法的調用請求特定數目的位元組為單位)。 該方法通過將這些位元組放在緩衝區 (從它掉最後一次離開的地方開始) 和 SubmitSourceBuffer 只呼籲該更新部分緩衝區的回應。 您想要此緩衝區,必須足夠大,因此您的程式碼不覆蓋仍正在發揮在背景緩衝區的一部分。

原來與 44,100 Hz 取樣速率的聲音,對 OnVoiceProcessingPassStart 的調用總是請求 882 位元組或 441 16 位樣品。 換句話說,OnVoiceProcessingPassStart 被稱為恒定速度的 100 倍每秒,或每 10 毫秒。 雖然沒有記載,此 10 ms 持續時間可以視為 XAudio2 音訊處理"量程,",它是一個好的數位,要牢記。 因此,對於這種方法編寫的代碼不能磨蹭。 避免 API 呼叫和運行時庫調用。

第二個常數是波形的所需的一個週期的長度。 它可能是一個陣列,包含的波形,樣本的大小,但在 SawtoothOscillator2 中它僅用於計算。

SawtoothOscillator2 中的 SetFrequency 方法使用該常量來計算所需波形的頻率成正比的角度增量:

angleIncrement = (int)(65536.0
                * WAVEFORM_LENGTH
                * freq / 44100.0);

雖然 angleIncrement 是一個整數,它被處理,就好像它包括整數和小數部分組成的字。 這是波形的用來確定每個連續示例的值。

例如,假設 SetFrequency 的參數是 440 Hz。 AngleIncrement 將計算為 5,356,535。 以十六進位格式,這是 0x51BBF7,這被視為整數 0x51 (或十進位的 81),與 0xBBF7,相當於 0.734 小數部分。 如果波形的完整週期是 8,192 位元組和使用唯一的整數部分和跳過 81 位元組每個樣本的由此產生的頻率是約 436.05 hz 的頻率。 (那是除以 8,192 44,100 倍 81)。如果您跳過 82 位元組,由此產生的頻率是 441.43 Hz。 你想要這兩個頻率之間的事。

這就是為什麼還需要輸入計算分數的部分。 整件事情可能會更容易在浮點數,和浮動點甚至可能更快一些現代的處理器,但圖 2 顯示更多的"傳統"僅使用整數的方法。 每次調用 SubmitSourceBuffer 指定的環形緩衝區條,更新的通知。

圖 2 在 SawtoothOscillator2 OnVoiceProcessingPassStart

void _stdcall SawtoothOscillator2::OnVoiceProcessingPassStart(UINT32 bytesRequired)
{
  if (bytesRequired == 0)
      return;
  int startIndex = index;
  int endIndex = startIndex + bytesRequired / 2;
  if (endIndex <= BUFFER_LENGTH)
  {
    FillAndSubmit(startIndex, endIndex - startIndex);
  }
  else
  {
    FillAndSubmit(startIndex, BUFFER_LENGTH - startIndex);
    FillAndSubmit(0, endIndex % BUFFER_LENGTH);
  }
  index = (index + bytesRequired / 2) % BUFFER_LENGTH;
}
void SawtoothOscillator2::FillAndSubmit(int startIndex, int count)
{
  for (int i = startIndex; i < startIndex + count; i++)
  {
    pWaveformBuffer[i] = (short)(angle / WAVEFORM_LENGTH - 32768);
    angle = (angle + angleIncrement) % (WAVEFORM_LENGTH * 65536);
  }
  XAUDIO2_BUFFER buffer = {0};
  buffer.AudioBytes = 2 * BUFFER_LENGTH;
  buffer.pAudioData = (byte *)pWaveformBuffer;
  buffer.Flags = 0;
  buffer.PlayBegin = startIndex;
  buffer.PlayLength = count;
  HRESULT hr = pSourceVoice->SubmitSourceBuffer(&buffer);
  if (FAILED(hr))
    throw ref new COMException(hr, "SubmitSourceBuffer");
}

SawtoothOscillator1 和 SawtoothOscillator2 可以在 OscillatorCompare 程式並行相比。 網頁有兩對滑塊控制項更改的頻率和每個振盪器的卷。 該頻率的滑塊控制項生成僅 24 至 132 的整數值。 我借來的音樂設備數位介面 (MIDI) 標準用於表示球場的代碼從這些值。 24 的值對應于 C 低於中間 C,變槳科學記數法叫做 C 1 (八度 1 C),並有約 32.7 赫茲頻率,其中的三個八度。 132 的值對應于 C 10、 中東-C,以上六個八度和約 16,744 Hz 的頻率。 關於這些滑塊工具提示轉換器科學變音符號和等效頻率顯示的當前值。

因為我在嘗試用這些兩個振子,我聽不清的區別。 我還在直觀地檢查所產生的波形,另一台電腦上安裝軟體示波器和我也看不到任何差異。 這表明我的 SetFrequency­比率方法實現智慧化,當然我們應該期望在一個系統中複雜的 DirectX。 我懷疑插上重新取樣後的波形資料以移頻正在執行。 如果你感到緊張,您可以設置 BASE_FREQ 非常低 — — 例如,至 20 赫茲 — — 和類將生成詳細的波形,由組成的 2,205 樣品。 您還可以嘗試以較高的值:例如,8,820 Hz 將導致波形的只是五個樣品要生成 ! 當然,這有一個有些不同的聲音,因為插值的波形介於之間鋸齒波和三角波,但由此產生的波形是仍平穩無"鋸齒"。

這並不意味著一切正常福。 與任一鋸齒振盪器、 頂幾個八度獲得相當混亂。 波形的採樣往往會發出高過之前,聽到的一種低頻率色彩和,打算在將來更充分調查。

壓低音量 !

SetVolume 方法定義的 IXAudio2Voice 和 IXAudio2SourceVoice 的繼承記錄作為浮點乘數,可以設置為值範圍從-2 ^24 至 2 ^24,這等於 16777216。

現實生活中,但是,您可能要將音量上一個 IXAudio2SourceVoice 物件保持為 0 和 1 之間的值。 值對應于沉默的 0 和 1 對應于沒有增益或衰減。 請記住無論波形的源關聯的 IXAudio2SourceVoice — — 正在通過演算法生成還是來自的音訊檔 — — 它可能已很有可能接近的-32768 和 32767 的最小和最大值的 16 位樣本。 如果您嘗試放大這些波形音量級別大於 1 時,樣品將超過一個 16 位整數的寬度和將剪切的最小和最大值。 將導致失真和雜訊。

當你開始組合 IXAudio2SourceVoice 的多個實例時,這一點非常重要。 這些多個實例的波形都被加在一起的混合。 如果您允許每個這些實例以擁有量的 1,聲音的總和很可能導致超出 16 位整數的大小的樣本。 偶爾可能發生此錯誤 — — 只間歇性的失真導致 — — 或慢性病,結果一件很麻煩的。

當使用多個生成完整 16 位寬波形的 IXAudio2SourceVoice 實例,一項安全措施將每個振盪器的卷設置為的聲音數除以 1。 保證總和不能超過 16 位的值。 此外可以通過掌握的聲音作出整體的卷調整。 你還可能想要看看 XAudio2CreateVolumeMeter 函數,它使您可以創建一個音訊處理物件,可以説明監視卷用於調試目的。

我們第一次的樂器

它是常見的樂器上片有鋼琴式鍵盤,但我過了很久最近由類型的按鈕鍵盤手風琴等俄羅斯巴彥 (其中我所熟悉的俄羅斯作曲家 Sofia Gubaidulina 工作) 上找到。 因為每個鍵是一個按鈕,而不是長的杠杆,太多鑰匙能裝在 tablet 螢幕,在有限的空間內所示圖 3

The ChromaticButtonKeyboard Program
圖 3 ChromaticButtonKeyboard 程式

底部兩行重複前兩行上的鍵和提供,以紓緩共同和絃和旋律序列的指法。 否則,每個組的前三行中的 12 鍵提供所有備註的倍頻程,一般按昇冪從左至右。 在這裡的總範圍是大小的四個八度,這是大小的兩次什麼你與鋼琴鍵盤相同。

真正的巴彥有額外的倍頻程,但我不能使按鈕太小,不適合。 原始程式碼中允許您設置常數來嘗試這額外的倍頻程,或消除另一個倍頻程,並使按鈕甚至更大。

因為我不能說這個計畫聽起來像是在現實世界中存在的任何文書,我只是叫它 ChromaticButton­鍵盤。 金鑰是為 Key,從 ContentControl 派生,但執行一些觸摸處理,以維持一個 IsPressed 屬性,生成一個 IsPressedChanged 事件的自訂控制項的實例。 當你掃你的手指在鍵盤處理此控制項中的觸摸和觸摸處理的普通按鈕 (其中也有一個 IsPressed 屬性) 之間的差異是明顯:標準按鈕將設置 IsPressed 屬性設置為 true 只手指按時發生在表面的按鈕,此自訂鍵控制認為如果手指掃在從一側按鍵。

該程式創建六個是從較早的專案幾乎完全相同的 SawtoothOscillator1 類的 SawtoothOscillator 類的實例。 如果您的觸控式螢幕支援,您可以播放六同時注意到。 有沒有回檔,並由調用 SetFrequencyRatio 方法控制振盪器的頻率。

要跟蹤哪些振盪器可用以及哪個振盪器正在玩的 MainPage.xaml.h 檔定義了兩個標準的集合物件作為欄位:

std::vector<SawtoothOscillator *> availableOscillators;
std::map<int, SawtoothOscillator *> playingOscillators;

每個鍵物件原本的標記屬性設置為我前面討論的 MIDI 注釋代碼。 這就是 IsPressedChanged 處理常式如何確定哪個鍵被按下,和什麼頻率來計算。 MIDI 代碼也被用作 playingOscillators 集合的映射鍵。 直到我玩了重複說明已在播放,導致重複按鍵和異常的底部兩個行的一份說明,它運轉正常。 通過將一個值,指示關鍵所在的行的 Tag 屬性納入輕鬆地解決了這個問題:標記現在等於 MIDI 注代碼加行號的 1000 倍。

圖 4 顯示關鍵實例的 IsPressedChanged 處理常式。 當按下鍵時,振盪器是從 availableOscillators 集合中移除,給定頻率和非零的卷,並投入 playingOscillators 集合。 當釋放某個鍵時,振盪器提供零卷,並搬回了 availableOscillators。

圖 4 為關鍵實例的 IsPressedChanged 處理常式

void MainPage::OnKeyIsPressedChanged(Object^ sender, bool isPressed)
{
  Key^ key = dynamic_cast<Key^>(sender);
  int keyNum = (int)key->Tag;
  if (isPressed)
  {
    if (availableOscillators.size() > 0)
    {
      SawtoothOscillator* pOscillator = availableOscillators.back();
      availableOscillators.pop_back();
      double freq = 440 * pow(2, (keyNum % 1000 - 69) / 12.0);
      pOscillator->SetFrequency((float)freq);
      pOscillator->SetAmplitude(1.0f / NUM_OSCILLATORS);
      playingOscillators[keyNum] = pOscillator;
    }
  }
  else
  {
    SawtoothOscillator * pOscillator = playingOscillators[keyNum];
    if (pOscillator != nullptr)
    {
      pOscillator->SetAmplitude(0);
      availableOscillators.push_back(pOscillator);
      playingOscillators.erase(keyNum);
    }
  }
}

這就是簡單以及 multi-voice 的文書可以,當然它有缺陷:聲音應該不會關閉和打開交換器相似。 卷應滑行起來迅速順利時注意啟動,而且後退時,它將停止。 許多真實文書也說明隨著卷和音色的變化。 還有很多改進的餘地。

但考慮到代碼的簡潔性,它竟然行之有效和回應迅速。 如果您編譯為 ARM 處理器的程式,可以將它部署上的基於 ARM 的微軟表面和周圍玩上它與另一隻手,而我必須說是有點興奮時摟著一隻手臂在無約束平板電腦走。

CharlesPetzold 是 MSDN 雜誌和作者的"Windows 程式設計,第 6 版"長期貢獻 (O'Reilly 媒體,2012年),一本關於編寫應用程式的 Windows 8 書。 他的網站是 charlespetzold.com

感謝以下技術專家對本文的審閱:Tom馬修斯和ThomasPetchel