本文章是由機器翻譯。

DirectX 要素

模擬類比合成器

Charles Petzold

下載代碼示例

大約在 50 年前,一名物理學家和工程師名叫羅伯特 · 穆格電子音樂合成器功能創建的頗不尋常:器官型鍵盤。一些電子音樂的作曲家輕視這種平淡和老式的控制設備,同時其他作曲家 — — 和特別是表演者 — — 對此發展表示歡迎。1960 年代末,由溫卡洛斯的 Switched-On 巴赫已成為最暢銷的古典專輯的所有時間,和 Moog 合成器進入了主流。

早期的 Moog 合成器是模組化的程式設計與跳線。1970 年,然而,Minimoog 被釋放 — — 小、 便於使用和發揮,和售價僅為 1,495 元。(這些早期合成器好歷史是一本書,"類比天:發明和影響 Moog 合成器"[哈佛大學出版社,2004 年],由 Trevor 捏和弗蘭克 Trocco.)

我們將分類 Moog 和類似合成器作為"類比"設備因為他們創建使用不同電壓從修造從電晶體、 電阻、 電容電路生成的聲音。相比之下,更現代的"數碼"合成器創建聲音通過演算法計算或數位化的樣品。較舊的設備進一步被列為"減法"合成器:而不是建築通過結合正弦波 (稱為添加劑的合成技術) 的複合聲,消減合成器開始與豐富的諧波的波形 — — 如鋸齒或方形波 — — 然後運行它通過篩選器,以消除某些諧波和改變聲音的音色。

由羅伯特 · 穆格率先的關鍵概念是"電壓控制"。考慮振盪器,它是生成的某種基本的音訊波形合成器的元件。在早些時候的合成器,可能由一個電阻的電路,在某個地方的值控制這個波形的頻率和可能通過撥號控制的可變電阻器。但在壓控振盪器 (VCO),振盪器的頻率受輸入電壓。例如,每增加一個伏到振盪器可能雙振盪器的頻率。這種方式,可以通過鍵盤生成增加每倍頻程一伏特的電壓控制壓控振盪器的頻率。

在類比合成器,從一個或多個 Vco 輸出進入壓控濾波器 (VCF) 改變波形的諧波含量。輸入的電壓到 VCF 控制濾波器的截止頻率或篩選器的反應 (篩選器的品質或 Q) 的清晰度。VCF 的輸出然後進入壓控的放大器 (VCA),其中的增益由另一個電壓控制。

信封發電機

但是,一旦你開始談論勾通和 VCAs,事情就變得複雜,和一個小背景是必要的。

早在 19 世紀,一些科學家 (最突出的是 Hermann von Helmholtz) 開始進軍重大,物理學的探索和聲音的感覺。聲音訊率和響度變得相對簡單等特點與疑難問題的音色相比 — — 這使我們能夠分辨鋼琴小提琴或伸縮喇叭的聲音的品質。它是假設 (和有些證明) 音色與聲音的諧波含量,是不同程度的正弦曲線,構成了聲音的強度有關。

但 20 世紀研究人員開始時作進一步的調查,他們發現不是這麼簡單。諧波含量變化對音樂的音調,當然,這有助於樂器的音色。特別是注意從樂器的一開始對聽覺感知至關重要。在鋼琴的錘子或小提琴弓首先觸及的字串,或振動空氣推進入金屬或木制的管時,會發生非常複雜的諧波活動。這種複雜性降低速度非常快,但如果沒有它,音樂聲調聲音沉悶和遠不如有趣和獨特。

模仿真實音樂聲調的複雜性,合成器只是不能打開注意打開和關閉開關一樣。(以這種簡單的合成器聽一樣,檢查出 2013 年 2 月安裝在此列中的 ChromaticButtonKeyboard 程式 msdn.microsoft.com/magazine/jj891059.)在每個注釋的開始,聲音穩定之前,必須有簡介"曇花一現"的高容量和不同的音色。注意結束時,聲音必須不只是停止,但減少的數量和複雜性與消亡。

響度,那裡是對這一進程的一般模式:演奏的字串、 黃銅或木管樂器的儀器上的說明,聲音迅速上升到最大音量,然後死一點並保持穩定。注意結束時,它迅速地減少卷中。這兩個階段被稱為的"攻擊"和"釋放"。

為更多的撞擊式打樁設備 — — 包括鋼琴 — — 說明在攻擊期間迅速達到最大音量,但然後死慢慢如果儀器仍未受潮,例如,在鋼琴鍵按住。一旦釋放該鍵,注很快死。

要實現這些效果,合成器實現叫做"信封產生器"。圖 1 顯示調用攻擊-朽爛-持續釋放 (ADSR) 信封相當標準的例子。橫軸是時間,與垂直軸是響度。


圖 1 攻擊衰變持續釋放信封

當按下鍵盤上的鍵注釋開始對聲音、 你聽到給水管爆裂的聲音首先,攻擊和朽爛的部分,然後注意在承受水準穩定下來。當釋放鍵和注意兩端,釋放部分發生。為鋼琴類型的聲音,衰變時間可能是幾秒鐘,並承受水準訂為零,所以繼續衰變,只要按住鍵的聲音。

即使最簡單的類比合成器有兩個 ADSR 信封:其中一個控制音量和其他控制項的篩選器。這通常是一個低通濾波器。注意被觸擊,截止頻率較快的增長,以允許通過,更多的高頻諧波,然後截止頻率稍有下降。這強調了很多,創建獨特的類比合成器鳴聲。

AnalogSynth 專案

約九個月前,當我正在考慮使用 XAudio2 小 1970年年代類比合成器的數字仿真的程式,我意識到信封發電機會的工作更具挑戰性的方面之一。它不是即使我清楚是否這些信封發電機外部音訊處理流 (並因此 SetVolume 和 SetFilterParameters 的存取方法 XAudio2 語音),或以某種方式被內置到音訊流。

我最終定居在執行作為 XAudio2 音訊效果的信封上 — — 更正式稱為音訊處理物件 (APOs)。這意味著信封的邏輯上的音訊流直接工作。編碼重複數位的二階濾波器的濾波邏輯內置於 XAudio2 之後,我變得更有信心,這種方法。通過使用我自己的篩選器代碼,我以為也許能夠更改程式的結構的濾波演算法在將來受到重大幹擾。

圖 2 顯示的螢幕,由此產生的類比的­Synth 程式,其原始程式碼的代碼你可以在下載 archive.msdn.microsoft.com/mag201307DXF。雖然我受 Minimoog 上控制項的佈局,我保持實際 UI 相當簡單,例如,使用滑塊而不撥。我大部分是重點的在內部結構上。


圖 2 AnalogSynth 螢幕

鍵盤是一系列自訂鍵控制項處理指標事件並分組到八度的控制項。鍵盤是實際上六個八度音階的寬度,可以使用下面的鍵厚厚的灰色條紋水準滾動。紅點標識中間 C.

程式可以發揮 10 同時注意到,但那是用一個簡單的 #define MainPage.xaml.cs 在多變。(Minimoog 像早期的類比合成器是單聲道)。每一個這些 10 聲音是實例類的我叫 SynthVoice。SynthVoice 已設置聲音 (包括頻率、 數量和信封) 的所有各項參數的方法,以及命名觸發器和釋放來指示當鍵已按下或釋放的方法。

Minimoog 實現其特徵的"強力"聲音部分由兩個並行運行的振盪器,往往稍有 mistuned,故意或頻率漂移在類比電路中常見。

因此,每個 SynthVoice 創建兩個類的實例振盪器,它從左上角的控制台中所示控制圖 2。控制台允許您設置的波形和相對體積這些兩個振盪器,並向上或向下,可以由一個或兩個八度音階換位頻率。此外,您可以抵消的第二個振盪器的頻率達八度。

振盪器的每個實例創建一個 IXAudio2SourceVoice 物件,並公開命名為 SetFrequency、 SetAmplitude 和 SetWaveform 的方法。SynthVoice 將兩個 IXAudio2SourceVoice 輸出路由到 IXAudio2SubmixVoice,然後具現化兩個自訂的音訊效果,稱為 FilterEnvelopeEffect 和振幅­EnvelopeEffect,它適用于此 submix 聲音。這兩個效果分享叫 EnvelopeGenerator,我不久就會描述一類。

圖 3 每個 SynthVoice 中顯示元件的組織。10 SynthVoice 物件,總共有 20 IXAudio2Source­語音進入 10 個 IXAudio2SubmixVoice 實例,然後路由到單一的 IXAudio2MasteringVoice 的實例。我使用 48,000 Hz 和整個 32 位浮點樣品的取樣速率。


圖 3 SynthVoice Class 的結構

使用者控制中心部分的控制台中的篩選器。切換按鈕允許篩選器來繞過 ; 否則,截止頻率是與現正播放的注意。(換句話說,濾波器的截止頻率跟蹤鍵盤)Emph­asis 滑塊控制篩選器的 Q 設置。信封滑塊控制信封對濾波器截止頻率的影響的程度。

篩檢程式封套和響度信封與關聯的四個滑塊同樣的工作。攻擊、 衰變和釋放的滑塊是所有持續時間從 10 毫秒到 10 秒的對數刻度。滑塊有工具提示值轉換器,以顯示與設置相關聯的持續時間。

AnalogSynth 使潛在的同時 IXAudio2SourceVoice 實例 20 或抵制的趨勢數位二階濾波器的截止頻率附近的音訊放大無音量調整。因此,AnalogSynth 容易超載,音訊。為了説明避免這種情況的使用者,該程式使用 XAudio2­CreateVolumeMeter 函數來創建監視傳出聲音的音訊效果。如果在右上角的綠點更改為紅色,音訊輸出被剪切,你應該使用最右邊來減小音量滑塊。

早期合成器插接線用於連接元件。由於這一遺產,特別是合成器安裝程式仍稱為"補丁"。如果你找到了一個修補程式,您想要保留的聲音,請按保存按鈕,並指定一個名稱。新聞載入按鈕來獲取清單以前保存的修補程式,然後選擇一個。這些修補程式 (以及當前的設置) 都存儲在本地設置區域中。

信封產生器演算法

實現包絡發生器的代碼基本上是一個狀態機,五個連續國家稱休眠、 攻擊、 衰減、 持續釋放。從 UI 的角度,看起來最自然的指定攻擊,朽爛,和維持的時間 dura­討論,但當實際執行您需要的轉換率的計算 — — 的增加或減少每單位時間的響度 (或濾波器截止頻率)。在 AnalogSynth 中的兩個音訊效果使用這些不斷變化的水準來實現效果。

這個狀態機並不總是作為順序中的關係圖作為圖 1 似乎暗示。例如,當一個鍵是按下和釋放如此迅速信封尚未達到承受部分釋放鍵時?起初我以為信封應獲准完成其攻擊和朽爛的部分,然後走進釋放部分中,但這並沒有工作好鋼琴式信封。在鋼琴的信封,承受水準是零和衰變時間相對較長。快速按下並釋放一個鍵仍有很長的朽爛 — — 如果它不在所有發佈 !

我決定為的快速按下並釋放,我會讓攻擊部分完成,但然後立即跳轉到釋放部分。這意味著最後率的下降將需要計算基於目前的水準上。這就解釋了為什麼有釋放的信封參數結構中如何處理有差異,此處所示:

struct EnvelopeGeneratorParameters
{
  float baseLevel;
  float attackRate;   // in level/msec
  float peakLevel;
  float decayRate;    // in level/msec
  float sustainLevel;
  float releaseTime;  // in msec
};

振幅包絡,增員被設置為 0、 peakLevel 設置為 1 和 sustainLevel 是那些值之間的某個地方。 三級篩檢程式封套,提到了適用于濾波器截止頻率的乘數:增員是 1,和 peakLevel 由標有"信封"滑塊和範圍可以從 1 到 16。 那倍頻器的 16 對應于四個八度。

AmplitudeEnvelopeEffect 和 FilterEnvelopeEffect 共用的 EnvelopeGenerator 類。 圖 4 顯示 EnvelopeGenerator 的標頭檔。 請注意要設置信封的參數的公共方法和兩個公共方法命名為攻擊和釋放觸發信封開始和結束。 應按順序調用這三個方法。 不編寫代碼來處理其參數通過其進展的中途轉車的信封。

圖 4 EnvelopeGenerator 標頭檔

class EnvelopeGenerator
{
private:
  enum class State
  {
    Dormant, Attack, Decay, Sustain, Release
  };
  EnvelopeGeneratorParameters params;
  float level;
  State state;
  bool isReleased;
  float releaseRate;
public:
  EnvelopeGenerator();
  void SetParameters(const EnvelopeGeneratorParameters params);
  void Attack();
  void Release();
  bool GetNextValue(float interval, float& value);
};

從信封發電機電流計算的值是通過重複調用 GetNextValue 獲得的。 間隔 argu­發言是以毫秒為單位,以及該方法計算新的值,基於該間隔可能切換過程中的國家。 信封已完成與釋放部分,GetNextValue 返回為真時顯示信封已完成,但我不實際使用該傳回值別處的程式中。

圖 5 顯示的 EnvelopeGenerator 類的實現。 靠近頂部的 GetNextValue 方法是代碼時釋放率的計算基於當前級別和釋放時間和釋放鍵,跳過直接到釋放狀態。

圖 5 執行 EnvelopeGenerator

EnvelopeGenerator::EnvelopeGenerator() : state(State::Dormant)
{
  params.baseLevel = 0;
}
void EnvelopeGenerator::SetParameters(const EnvelopeGeneratorParameters params)
{
  this->params = params;
}
void EnvelopeGenerator::Attack()
{
  state = State::Attack;
  level = params.baseLevel;
  isReleased = false;
}
void EnvelopeGenerator::Release()
{
  isReleased = true;
}
bool EnvelopeGenerator::GetNextValue(float interval, float& value)
{
  bool completed = false;
  // If note is released, go directly to Release state,
  // except if still attacking
  if (isReleased &&
    (state == State::Decay || state == State::Sustain))
  {
    state = State::Release;
    releaseRate = (params.baseLevel - level) / params.releaseTime;
  }
  switch (state)
  {
  case State::Dormant:
    level = params.baseLevel;
    completed = true;
    break;
  case State::Attack:
    level += interval * params.attackRate;
    if ((params.attackRate > 0 && level >= params.peakLevel) ||
      (params.attackRate < 0 && level <= params.peakLevel))
    {
      level = params.peakLevel;
      state = State::Decay;
    }
    break;
  case State::Decay:
    level += interval * params.decayRate;
    if ((params.decayRate > 0 && level >= params.sustainLevel) ||
      (params.decayRate < 0 && level <= params.sustainLevel))
    {
      level = params.sustainLevel;
      state = State::Sustain;
    }
    break;
  case State::Sustain:
    break;
  case State::Release:
    level += interval * releaseRate;
    if ((releaseRate > 0 && level >= params.baseLevel) ||
      (releaseRate < 0 && level <= params.baseLevel))
    {
      level = params.baseLevel;
      state = State::Dormant;
      completed = true;
    }
  }
  value = level;
  return completed;
}

一雙音訊效果

AmplitudeEnvelopeEffect 和 FilterEnvelopeEffect 的類從 CXAPOParametersBase 派生,所以他們可以接受參數,和兩個類都還保持執行信封計算 EnvelopeGenerator 類的一個實例。 這些兩個音訊效果的參數結構命名 AmplitudeEnvelopeParameters 和 FilterEnvelopeParameters。

AmplitudeEnvelopeParameters 結構是僅僅是一個 EnvelopeGeneratorParameters 結構和布林 keyPressed 欄位,當這聲音與相關聯的鍵是按下和虛假當它被釋放時是如此。 (篩選器­EnvelopeParameters 結構是只是稍微更複雜,因為它需要納入基地級濾波器截止頻率和 Q 設置.)這兩個效果類維護他們自己可以相比的參數值,以確定當信封攻擊或釋放的 keyPressed 資料成員狀態應觸發。

你可以看看這是如何工作圖 6,其中 AmplitudeEnvelopeEffect 在演示的過程重寫的代碼。 如果已啟用的效果和當地的 keyPressed 值為 false,但 keyPressed 效果的參數值為 true,此效果可使對 SetParameters 和攻擊 EnvelopeGenerator 實例的方法的調用。 如果相反的情況 — — 本地 keyPressed 的值為 true,但在參數中的那個是假的 — — 然後影響調用 Release 方法。

圖 6 在 AmplitudeEnvelopeEffect 的過程重寫

void AmplitudeEnvelopeEffect::Process(UINT32 inpParamCount,
    const XAPO_PROCESS_BUFFER_PARAMETERS *pInpParam,
    UINT32 outParamCount,
    XAPO_PROCESS_BUFFER_PARAMETERS *pOutParam,
    BOOL isEnabled)
{
  // Get effect parameters
  AmplitudeEnvelopeParameters * pParams =
    reinterpret_cast<AmplitudeEnvelopeParameters *>
      (CXAPOParametersBase::BeginProcess());
  // Get buffer pointers and other information
  const float * pSrc = static_cast<float const*>(pInpParam[0].pBuffer);
  float * pDst = static_cast<float *>(pOutParam[0].pBuffer);
  int frameCount = pInpParam[0].ValidFrameCount;
  int numChannels = waveFormat.
nChannels;
  switch(pInpParam[0].BufferFlags)
  {
  case XAPO_BUFFER_VALID:
    if (!isEnabled)
    {
      for (int frame = 0; frame < frameCount; frame++)
      {
        for (int channel = 0; channel < numChannels; channel++)
        {
          int index = numChannels * frame + channel;
          pDst[index] = pSrc[index];
        }
      }
    }
    else
    {
      // Key being pressed
      if (!this->keyPressed && pParams->keyPressed)
      {
        this->keyPressed = true;
        this->envelopeGenerator.SetParameters(pParams->envelopeParams);
        this->envelopeGenerator.Attack();
      }
      // Key being released
      else if (this->keyPressed && !pParams->keyPressed)
      {
        this->keyPressed = false;
        this->envelopeGenerator.Release();
      }
      // Calculate interval in msec
      float interval = 1000.0f / waveFormat.
nSamplesPerSec;
      for (int frame = 0; frame < frameCount; frame++)
      {
        float volume;
        envelopeGenerator.GetNextValue(interval, volume);
        for (int channel = 0; channel < numChannels; channel++)
        {
          int index = numChannels * frame + channel;
          pDst[index] = volume * pSrc[index];
        }
      }
    }
    break;
  case XAPO_BUFFER_SILENT:
    break;
  }
  // Set output parameters
  pOutParam[0].ValidFrameCount = pInpParam[0].ValidFrameCount;
  pOutParam[0].BufferFlags = pInpParam[0].BufferFlags;
  CXAPOParametersBase::EndProcess();
}

效果可以打電話 EnvelopeGenerator 的 GetNextValue 方法為每個進程調用 (在這種情況下的時間間隔參數將指示 10 毫秒為單位) 或 (在這種情況下間隔是更像 21 微秒) 每個樣本 雖然第一種方法應該是足夠的我決定在理論上更平滑過渡為二。

將浮點卷傳回值從 GetNextValue 呼叫範圍從 0 (當注是第一次開始或結束) 為 1 的進攻高潮。 效果簡單相乘的浮點樣本通過此號碼。

現在開始的樂趣

我花了那麼多時間編碼類比­Synth 程式,我沒有很多時間來玩玩。 它很可能是一些控制項和參數需要一些微調,或者也許相當粗糙的優化 ! 特別是,長時間衰變和釋放卷上的時間聽起來不是很對,他們建議振幅包絡變化應該是對數的而不是線性。

此外令我大惑不解觸摸輸入與使用螢幕鍵盤。 真正的鋼琴上的鍵是敏感的他們被觸擊,和合成器鍵盤有試圖效仿這種相同的感覺的速度。 然而,大多數的觸控式螢幕,不能檢測觸摸速度或壓力。 但他們可作出輕微的手指運動對敏感的螢幕上,這是超出能力的一個真正的鍵盤。 螢幕鍵盤可更好地滿足以這種方式嗎? 那裡是只有一種方法來找出 !

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

感謝以下技術專家對本文的審閱:詹姆斯 McNellis (Microsoft)
詹姆斯 McNellis 是一個 c + + 愛好者和微軟的 Visual c + + 團隊的軟體發展人員在他那裡他生成 c + + 庫和維護的 C 運行時庫 (CRT)。他在微博 @JamesMcNellis,並通過線上其他地方可以發現 HTTP://jamesmcnellis.com/