Share via


本文章是由機器翻譯。

UI 最前線

WPF 應用程式中的 MIDI 音樂

Charles Petzold

下載範例程式碼

每台電腦都包含內建的 16 重奏樂團,準備好播放音樂。 它們代表可能是由 Windows 支援的聲音和視訊功能的陣列最 underutilized 的元件的這個群組列的成員可能覺得很多棄置。

這個 16 片段橫區段是硬體或軟體是否符合 MIDI 標準稱為中實作的電子音樂合成器 — 樂器的樂器數位介面。 在 Win32 的 API 透過 MIDI 合成器播放音樂支援透過函式開頭的單字 midiOut。

MIDI 支援不屬於.NET] Framework 不過,所以如果想存取 Windows 表單] 或 [Windows Presentation Foundation (WPF) 應用程式中的這個 MIDI 合成器需要使用 P/Invoke 或外部程式庫。

我是很高興在 NAudio 音效庫上我在我最後一欄中討論的 CodePlex 可用尋找 MIDI 支援。 您可以從 codeplex.com/naudio 下載該原始程式碼的程式庫。 在本文中我使用 NAudio 版本 1.3.8。

簡短的範例

您可以 MIDI 視為高階的介面,以在其中您處理樂器及備忘稿的一種聲音音訊。

MIDI 標準是在早期 1980年而開發。 電子音樂合成器製造商想標準的方式來與合成器,連線 (如鍵盤) 的電子音樂控制站,其來源,與系統傳輸透過以 pokey 3,125 每秒的位元組速率 5 針連接器纜線的小型訊息 (大部份是一個、 兩個或三個位元組的長度而定)。

兩個最重要的這些訊息會稱為附註] 和筆記關。 當一個音樂家按一個鍵 MIDI 鍵盤上時,鍵盤會產生附註] 訊息,指出已按下 [附註和索引鍵 ’s 速度。 合成器會播放該便箋的較高的索引鍵 velocities 通常 louder 來回應。 當音樂家釋放索引鍵時,鍵盤產生筆記關閉訊息,並合成器會藉由關閉附註回應。 沒有實際的音訊資料經歷 MIDI 纜線。

雖然 MIDI 仍然用來連接電子音樂硬體,它也可以用完全內透過軟體的電腦。 音效佈告欄可以包括 MIDI 合成器及 Windows 本身會模擬完全在軟體中的 MIDI 合成器。

存取該合成器在使用 NAudio 庫 WinForms 或 WPF 應用程式、 將 NAudio.dll 加入為參考及包含 
this 在您的來源程式碼中使用指示詞:

using NAudio.Midi;

假設您想要您的應用程式來播放單一的一秒鐘附註,聽起來中間 C 的一個鋼琴。 您可以執行,以下列程式碼:

MidiOut midiOut = new MidiOut(0);
midiOut.Send(MidiMessage.StartNote(60, 127, 0).RawData);
Thread.Sleep(1000);
midiOut.Send(MidiMessage.StopNote(60, 0, 0).RawData);
Thread.Sleep(1000);
midiOut.Close();
midiOut.Dispose();

一部電腦可能有多個的 MIDI 合成器的存取 ; MidiOut 建構函式引數是以選取要開啟一個數字 ID。 建構函式會引發例外狀況,如果 MIDI 輸出裝置正在使用中。

程式可以取得第一個使用靜態 MidiOut.NumberOfDevices 屬性探索多少合成器皆存在 MIDI 合成器的資訊。 數字 ID 範圍從 0 到裝置數目減一。 靜態 MidiOut.DeviceInfo 方法接受數字 ID,並傳回型別描述合成器的 MidiOutCapabilities 的物件。 (我 won’t 會使用這些功能。 針對本文的其餘部分我只是使用預設 MIDI 合成器可用具有零的識別碼)。

MidiOut 類別的 Send 方法會將訊息傳送至 MIDI 合成器。 MIDI 訊息組成一個、 兩個或三個位元組,但 Win32 API (及 NAudio) 想要它們封裝到單一的 32 位元整數。 MidiMessage.StartNote 和 MidiMessage.StopNote 方法為您做此封裝。 您可以取代兩個引數以傳送 0x007F3C90 和 0x00003C80,分別。

StartNote 和 StopNote 的第一個引數為程式碼範圍從 0 到 127 指出實際附註值 60 所在中間 C。 較高的 octave 是 72。 較低的 octave 為 48。 第二個引數是索引鍵是按下或釋放的速度。 (發行 velocities 通常會忽略由合成器)。這些可以範圍從 0 至 127。 降低 MidiMessage.StartNote 使柔和的附註到第二個引數。 (我討論第三個引數短時間內)。

兩個呼叫到 Thread.Sleep 暫停 1,000 毫秒的執行緒。 這是一種之訊息的預存時間的非常簡單方法,但應避免在使用者介面執行緒中。 第二個 Sleep 呼叫是為了讓附註死之前關閉呼叫會突然被截斷。

Polyphony 呢?

’s 方式,您可以播放一個附註。 在同一時間的多個筆記呢? ’s 可能也。 比方說如果想播放 C 主要套索,而不是只是單一 C 便箋您可以取代第一個傳送訊息以下列:

midiOut.Send(MidiMessage.StartNote(60, 127, 0).RawData);
midiOut.Send(MidiMessage.StartNote(64, 127, 0).RawData);
midiOut.Send(MidiMessage.StartNote(67, 127, 0).RawData);
midiOut.Send(MidiMessage.StartNote(72, 127, 0).RawData);

然後取代第二個傳送訊息與:

midiOut.Send(MidiMessage.StopNote(60, 0, 0).RawData);
midiOut.Send(MidiMessage.StopNote(64, 0, 0).RawData);
midiOut.Send(MidiMessage.StopNote(67, 0, 0).RawData);
midiOut.Send(MidiMessage.StopNote(72, 0, 0).RawData);

視各種備忘稿來啟動和停止在各種時間可能想要放棄 Thread.Sleep 使用並取得實際的計時器涉及,特別是如果您使用者介面執行緒上播放音樂。 更多關於這很快就。

沒有結合 MIDI 訊息與時間資訊的 MIDI 檔案格式,但這些檔案需要特殊的軟體,才能建立,我 won’t 會討論它們在這裡。

樂器和通道

到目前為止我被播放只鋼琴音效。 您可以切換播放使用 MIDI 程式變更訊息以 ChangePatch 方法實作中 NAudio 其他樂器音效合成器:

midiOut.Send(MidiMessage.ChangePatch(47, 0).RawData);

ChangePatch 的第一個引數是數字的程式碼,範圍從 0 到 127 來指示特定的樂器音效。

入 MIDI 早期實際的聲音來自超出合成器已由透過撥號及補充程式纜線演奏完全控制的。 (,’s 為何特定合成器安裝程式或樂器音效通常稱為 「 補充程式 」)。稍後在 MIDI 檔案的建立者想要一組標準的樂器讓該檔案會音效不管它們在播放合成器幾乎相同。 這導致呼叫一般的 MIDI 標準。

良好的參考以進行一般的 MIDI 是 Wikipedia 項目 en.wikipedia.org/wiki/General_midi 。 在標題 「 Melodic 聲音 」 下是 128 的樂器聲音的範圍從 1 到 128 的代碼。 因此前一個範例中的程式碼 47 是樂器 48 是 Timpani 聲音的此清單中,您可以使用 ChangePatch] 方法中的以零起始的代碼。

我開始提到 MIDI 合成器就等於 16 棋子帶。 MIDI 合成器支援 16 頻道 。 任何時候每個通道有關聯與特定樂器,根據最新的程式變更訊息。 頻道號碼範圍從 0 到 15,並指定 StartNote、 StopNote 和 ChangePatch 方法的最後一個引數中。

通道 9 是特殊的。 這是 percussion 通道。 (它通常稱為頻道 10,但所 ’s 如果頻道編號 1 開始)。傳遞至 StartNote 和 StopNote 方法代碼參閱特定的非色調 percussion 聲音,而不是 pitches 通道 9。 在一般的 MIDI 上 Wikipedia 項目,請參閱 「 Percussion 」 標題下清單。比方說下列呼叫播放由 56 碼表示的 cowbell 聲音:

midiOut.Send(MidiMessage.StartNote(56, 127, 9).RawData);

還有更多 MIDI,但那些基本資訊。

以 XAML 為基礎的 MIDI

追隨與 WPF 和 XAML 的精神,我以為它會是開發一字串為基礎的直接在 XAML 檔案中內嵌短片段的音樂及播放它們格式的樂趣。 我稱之為這種格式 MIDI 字串 — 備忘稿及時間資訊的文字字串。 所有的語彙基元 (Token) 是以泛空白字元分隔。

注意都是大寫字母 A 到後面加上任何數目的 + 符號或 # signs (每個會引發俯仰角一個 semitone) 或 – 井號或字母 b (以降低俯仰角一個 semitone),且後面緊接著一個選擇性 octave 數字位置在中間 C octave 開頭是 octave 四的 G。 (這是一種編號 octaves 的標準方法)。因此,[C# 下方中間 C 是:

C# 3

字母 r 之後本身是一個其餘部分。 附註或某個其他部分 (選擇性) 後面可以工期表示直到下一個附註的時間期間。 比方說,這是一季附註如果也是預設值表示無期間:

1/4

工期是自黏 — 也就是如果工期不遵循附註,最後的工期將會使用。 如果以斜線開頭期間,分子被假設為 1。

該工期指出時間,直到下一個附註。 此工期也會用於附註的長度 — 也就是關閉時間,直到開啟附註。 更 staccato 音效您可以將附註 ’s 長度成小於其工期。 或者您可能會想連續備忘稿稍微重疊。 指出附註 ’s 長度相同的方式為工期,但負號:

–3/16

工期及長度一定會出現附註的它們套用,但順序 doesn’t 影響之後。 長度不會自黏。 如果沒有出現附註長度,工期用於長度。

附註也可以可以加語彙基元 (Token)。 如果要設定樂器語音,請依照下列字母我以零起始的修補程式數。 例如,這表示連續的備忘稿 violin:

i40

鋼琴是預設補充程式。

若要設定新的磁碟區 (也就是速度) 的連續備忘稿使用 V,如下:

v64

我和 V,後面的數必須介於零到 127。

在預設的情況下,tempo 是每分鐘 60 季備忘稿。 若要設定下列的筆記的新 tempo,使用 T,例如後面跟著季備忘稿每分鐘,數:

t120

如果您喜歡的備忘稿以相同的參數進行群組,可以將它們放在括號中。 這裡 ’s C 主要套索:

(c4 e4 g4) c5

只備忘稿,可能會出現在括號中。 垂直列 | 分隔的頻道。 通道要同時,播放,以及它們都是完全獨立,包括 tempos。

如果特定通道含有一個首都 P 任何地方內,該通道就會變成 percussion 通道。 該頻道可以包含備忘稿或將滑鼠停留時會正常標記法,但也允許 percussion 語音,若要以數字表示。 比方說,這是 cowbell:

p56

如果您移至 en.wikipedia.org/wiki/Charge_(fanfare) ,看到 「 收費! 」 調整通常在運動事件播放。 可以表示的 MIDI 字串格式為:

"t100 i56 g4/12 c5 e5 g5 3/16-3/32 e5 /16 g5 2"

MidiStringPlayer

MidiStringPlayer 是隨附在可下載的原始程式碼 Petzold.Midi 程式庫專案中只公開的類別。 它是衍生自 FrameworkElement 讓您可以在視覺樹狀結構,在 XAML 檔案中內嵌,但它有沒有視覺化的外觀。 設定 「 MidiString 屬性前一個範例所示的格式為字串和呼叫播放 (以及,選擇性地,停止在完成前停止序列)。

MidiStringPlayer 也有 PlayOnLoad 屬性,以在項目載入時, 播放序列和僅取得 IsPlaying 屬性。 當它完成播放一系列的結束事件] 和 [如果有在 MIDI 字串語法 ’s 錯誤引發的失敗事件,會產生項目。 事件文字字串,表示有問題的語彙基元和錯誤的文字說明中包含的位移 (Offset)。

兩個 WPF 程式也會包含在可下載的程式碼中。 MusicComposer 程式可讓您以互動方式放在一起 MIDI 字串。 的 圖 1 所示,WpfMusicDemo 程式編碼一些簡單的順序在 MIDI 檔案。

圖 1 WpfMusicDemo.xaml 編碼幾位的簡易 MIDI 字串

<Window x:Class="WpfMusicDemo.Window1"
  xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:midi="clr-namespace:Petzold.Midi;assembly=Petzold.Midi"
  Title="WPF Music Demo" 
  Height="300" Width="300">
  <Grid>
    <midi:MidiStringPlayer Name="player"
      PlayOnLoad="True"
      MidiString="{Binding ElementName=chargeButton, Path=Tag}" />
        
    <UniformGrid Rows="2"
      ButtonBase.Click="OnButtonClick">
      <UniformGrid.Resources>
        <Style TargetType="Button">
          <Setter Property="HorizontalAlignment" Value="Center" />
          <Setter Property="VerticalAlignment" Value="Center" /> 
          <Style.Triggers>
            <DataTrigger 
              Binding="{Binding ElementName=player, Path=IsPlaying}"
              Value="True">
              <Setter Property="IsEnabled" Value="False" />
            </DataTrigger>
          </Style.Triggers>
        </Style>
      </UniformGrid.Resources>

      <Button Name="chargeButton"
        Content="Charge!"
        Tag="T100 I56 G4 /12 C5 E5 G5 3/16 -3/32 E5 /16 G5 /2" />
            
      <Button Content="Bach D-Minor Toccata"
        Tag="T24 I19 A5 /64 G5 A5 5/32 R /32 G5 /64 F5 E5 D5 C#5 /32 D5 /16 R 4/16 
A4 /64 G4 A4 5/32 
R /32 E4 F4 C#4 D4 /16 R 4/16 | T24
I19 A4 /64 G4 A4 5/32 R /32 G4 /64 F4 E4 D4 C#4 /32 D4 /16 R 4/16 
A3 /64 G3 A3 5/32 R /32 E3 F3 C#3 D3 /16 R 4/16"/>

      <Button Content="Shave &amp; a Haircut"
        Tag="T130 I58 C5 G4 /8 G4 Ab4 /4 G4 R I75 B4 C5" />

      <Button Content="Beethoven Fifth"
        Tag="T200 I71 R /8 G4 G4 G4 Eb4 7/8 R /8 F4 F4 F4 D4 5/4 | T200 I40 R /8 G4 G4 G4 
Eb4 7/8 R /8 F4 F4 F4 D4 5/4 | T200 I40 R /8 G4 
G4 G4 Eb4 7/8 R /8 F4 F4 F4 D4 5/4 | 
T200 I41 R /8 G3 G3 G3 Eb3 7/8 R /8 F3 F3 F3 D3 5/4 | T200 I43 R /8 G2 G2 G2 Eb2 7/8 R /8 
F2 F2 F2 D2 
5/4 | T200 I43 R /8 G2 G2 G2 Eb2 7/8 R /8 F2 F2 F2 D2 5/4"/>
            
    </UniformGrid>
  </Grid>
</Window>

音樂播放軟體的任何一項重要的一部分是計時器,不過對於 MidiStringPlayer 我使用 UI 執行緒上執行的豬最喜歡在非常簡單 DispatcherTimer。 這當然不是最佳。 如果另一個程式 hogging CPU,音樂播放會變成不規則。 DispatcherTimer 也無法產生 Tick 事件比大約每秒是令人滿意的簡單片段,但 doesn’t rhythmically 更複雜的音樂為提供必要的精確度 60 快。

Win32 API 包括特別為播放 MIDI 序列的高解析度計時器,但這不還對它 NAudio 文件庫。 也許上某些以後我取代 [DispatcherTimer 東西更精確和一般,但為現在我 ’m 快樂運作以及它並利用這個簡單的解決方案。

Charles Petzold   是 long-time 撰文編輯器,來 MSDN 雜誌*。*他最新的活頁簿是 「 的 Annotated Turing:A 引導式透過 Alan Turing ’s 歷史紙張 Computability 和 Turing 機器上的教學課程 」 (Wiley,2008)。在他的網站 charlespetzold.com Petzold 部落格。

多虧給來檢閱這份文件的技術專家下列:   Mark Heath