UI 最前線
WPF アプリケーションでの MIDI 音楽
Charles Petzold
あらゆる PC には、音楽を演奏する準備が万端の 16 人編成のバンドが付属しています。このバンドのメンバーは、おそらく非常に軽視されていると感じているはずです。これらのメンバーは、Windows でサポートされているさまざまな音声機能やとビデオ機能の中でも、最も利用されていないコンポーネントの代表と言えます。
この 16 人編成のバンドは、MIDI (Musical Instrument Digital Interface) という標準に準拠するハードウェアまたはソフトウェアで実装される電子音楽シンセサイザーです。Win32 API では、midiOut という単語から始まる関数を通じて、MIDI シンセサイザーによる音楽の再生をサポートします。
しかし、MIDI サポートは .NET Framework には含まれていないため、Windows フォーム アプリケーションや Windows Presentation Foundation (WPF) アプリケーションからこの MIDI シンセサイザーにアクセスする場合は、P/Invoke または外部ライブラリのいずれかを使用する必要があります。
前回のコラムで紹介した CodePlex から入手できる NAudio 音声ライブラリで MIDI がサポートされていると知ったとき、私は非常に喜びました。NAudio ライブラリは、ソース コードと共に codeplex.com/naudio (英語) からダウンロードできます。今回のコラムでは、NAudio Version 1.3.8 を使用しました。
簡単な例
MIDI は、楽器や楽譜を操作する際に使用する波形オーディオとの高度なインターフェイスと考えることができます。
MIDI 標準は 1980 年代初頭に開発されました。電子音楽シンセサイザーのメーカーは、電子音楽の制御装置 (キーボードなど) をシンセサイザーに接続する標準の方法を求めていました。これらのメーカーは、サイズの小さいメッセージ (ほとんどは長さが 1 ~ 3 バイト) を 5 ピン コネクタのケーブルを経由して毎秒わずか 3,125 バイトの速度で転送するシステムを考案しました。
これらのメッセージの中でも特に有名な 2 つのメッセージが、ノートオンとノートオフです。演奏者が MIDI キーボードの鍵盤を押すと、キーボードでノートオン メッセージが生成されて、押した音符と鍵盤のベロシティが通知されます。シンセサイザーでは、その応答としてその音を再生します (一般に、鍵盤のベロシティが高いほど大きい音量になります)。演奏者が鍵盤を放すと、キーボードでノートオフ メッセージが生成され、シンセサイザーは応答として音を停止します。実際のオーディオ データが、MIDI ケーブルを通じて転送されることはありません。
MIDI は現在も電子音楽ハードウェアの接続に使用されていますが、ソフトウェアを介して完全に PC 内部で使用することもできます。サウンド ボードには MIDI シンセサイザーを含めることができ、Windows 自体によって、完全にソフトウェアで MIDI シンセサイザーのエミュレーションが行われます。
NAudio ライブラリを使用して WinForms アプリケーションまたは WPF アプリケーションからこのシンセサイザーにアクセスするには、NAudio.dll を参照として追加し、ソース コードに次の using ディレクティブを含めます。
using NAudio.Midi;
アプリケーションで、ピアノの中央ハ音 (ピアノの中央にあるド音) のように聞こえる 1 秒間の単音を再生するとします。次のコードを使用すると、この処理を実行できます。
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();
PC から複数の MIDI シンセサイザーにアクセスできる場合があるため、MidiOut コンストラクターの引数は、アクセスするシンセサイザーを選択するための数値 ID です。MIDI の出力デバイスが既に使用中であればコンストラクターで例外が発生します。
プログラムでは存在するシンセサイザーの数を検出するために、まず、静的 MidiOut.NumberOfDevices プロパティを使用して MIDI シンセサイザーに関する情報を取得します。数値 ID の範囲は、0 からデバイス数より 1 少ない数までです。静的 MidiOut.DeviceInfo メソッドは、数値 ID を受け取って、シンセサイザーを説明する MidiOutCapabilities 型のオブジェクトを返します (今回のコラムでは、これらの機能は使用しません。以降の説明では、ID 0 でアクセスできる既定の MIDI シンセサイザーを使用します)。
MidiOut クラスの Send メソッドでは、メッセージを MIDI シンセサイザーに送信します。MIDI メッセージは 1 ~ 3 バイトで構成されていますが、Win32 API (および NAudio) では、これらのメッセージを単一の 32 ビット整数値にパックします。このパック処理を実行するのが、MidiMessage.StartNote メソッドと MidiMessage.StopNote メソッドです。Send メソッドの 2 つの引数は、それぞれ 0x007F3C90 と 0x00003C80 の値に置き換えることができます。
StartNote メソッドと StopNote メソッドの最初の引数は、実際の音階を示す 0 ~ 127 の範囲のコードで、値 60 は中央ハ音 (ピアノの中央にあるド音) を表します。1 オクターブ上の音階は 72 で、1 オクターブ下の音階は 48 です。2 つ目の引数は、鍵盤押したり放したりするときのベロシティです (放したときのベロシティは、シンセサイザーでは通常無視されます)。ベロシティは 0 ~ 127 の範囲で指定できます。MidiMessage.StartNote メソッドで 2 つ目の引数の値を小さくすると、音が柔らかくなります (3 つ目の引数についてはこの後説明します)。
Thread.Sleep メソッドの 2 回の呼び出しでは、それぞれスレッドを 1,000 ミリ秒間中断します。これはメッセージのタイミングを指定する非常に単純な方法ですが、ユーザー インターフェイス スレッドでは使用しないでください。2 回目の Sleep メソッドの呼び出しは、Close メソッドの呼び出しによって音が突然中断しないように音を消すために必要です。
多重奏に挑戦する
ここまでの説明は単音の再生方法です。では、同時に複数の音を再生する場合はどうかと言えば、この処理も実行できます。たとえば、ただの単音のハ音ではなく、ハ長調の和音を再生する場合は、最初の Send メソッドの代わりに次のコードを使用します。
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);
次に、2 つ目の Send メソッドを次のコードに置き換えます。
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 ファイル形式もありますが、このようなファイルを作成するには専用のソフトウェアが必要なので、ここでは説明しません。
楽器とチャンネル
ここまでは、ピアノの音だけを再生してきました。MIDI のプログラム チェンジ メッセージを使用すると、他の楽器音を再生するようにシンセサイザーを切り替えることができます。このメッセージは、NAudio では次のように ChangePatch メソッドで実装します。
midiOut.Send(MidiMessage.ChangePatch(47, 0).RawData);
ChangePatch メソッドの最初の引数は、特定の楽器の音を表す 0 ~ 127 の範囲の数値コードです。
MIDI の開発当初、シンセサイザーから出力される実際の音は、ユーザーがダイヤルとパッチ ケーブルを使用してすべて制御していました (特定のシンセサイザーの設定や楽器の音がよく "パッチ" と呼ばれるのはこのためです)。その後、MIDI ファイルの作成者は、楽器の標準的なセットを用意して、再生するシンセサイザーに関係なくほとんど同じ音を再生するようにしようと考えました。その結果、General MIDI という標準が作成されました。
General MIDI の詳しいリファレンスについては、Wikipedia のエントリ (ja.wikipedia.org/wiki/General_MIDI) を参照してください。「Melodic sounds」には、128 種類の楽器の音が 1 ~ 128 の範囲のコードと共に一覧されています。ChangePatch メソッドでは 0 から始まるコードを使用するので、上記のコード例のコード 47 は、この一覧の楽器 48 (ティンパニの音) に相当します。
今回のコラムの冒頭で、MIDI シンセサイザーを 16 人編成のバンドと表現しました。MIDI シンセサイザーでは、16 の "チャンネル" をサポートします。すべてのチャンネルは、最新のプログラム チェンジ メッセージに基づいて特定の楽器に必ず関連付けられます。チャンネル番号の範囲は 0 ~ 15 で、StartNote メソッド、StopNote メソッド、および ChangePatch メソッドの最後の引数で指定します。
チャンネル 9 は特別なチャンネルです。これはパーカッション チャンネルです (このチャンネルはよくチャンネル 10 と呼ばれますが、これはチャンネルの番号が 1 で始まる場合の名前です)。チャンネル 9 では、StartNote メソッドと StopNote メソッドに渡すコードは、ピッチではなく、音階のない特定のパーカッションの音を表します。使用できるパーカッションの詳細については、Wikipedia の「General MIDI」にある「Percussion notes」の一覧を参照してください。たとえば、次のように呼び出すと、コード 56 で指定されるカウベル音が再生されます。
midiOut.Send(MidiMessage.StartNote(56, 127, 9).RawData);
MIDI について説明することはまだありますが、ここでは基本事項にとどめることにします。
XAML ベースの MIDI
WPF と XAML の考え方に基づき、短い楽曲を XAML ファイルに直接埋め込んで再生する文字列ベースの形式を開発したらおもしろいだろうと考えました。この形式を、MIDI 文字列と呼ぶことにします。これは、音符とタイミングの情報に関するテキスト文字列です。すべてのトークンはホワイトスペースで区切ります。
音符は、まず A ~ G の大文字、次に任意の数のプラス記号 (+) か番号記号 (#) (1 文字ごとにピッチを半音上げる)、またはマイナス記号 (–) か b の文字 (ピッチを半音下げる)、最後に省略可能なオクターブ番号を組み合わせて表します。オクターブ番号では、中央ハ音 (ピアノの中央にあるド音) はオクターブ 4 です (これは、オクターブに番号を付ける標準の方法です)。したがって、中央ハ音の下の嬰ハ (ピアノの中央にあるド音の下のド シャープ) は、次のようになります。
C#3
単独の R は休止です。音符または休止の後には、オプションで持続期間を指定できます。持続期間は、次の音が始まるまでの期間を示します。たとえば、次の文字列は 4 分音符を表します。4 分音符は、持続期間が指定されていない場合の既定値でもあります。
1/4
持続期間は固定です。つまり、音符の後に持続期間を指定しなければ、直近に指定した持続期間が使用されます。持続期間がスラッシュで始まる場合、分子は 1 と想定されます。
この持続期間は、次の音までの時間を示します。また、音の長さ、つまり音が消えるまでの時間にも、この持続期間が使用されます。スタッカートを強調音にするために、音の長さをその持続期間よりも短くすることがあるでしょう。また、連続する音がある程度重なるようにすることもあるでしょう。次のように、音の長さの指定方法は、マイナス記号を付けることを除き持続期間と同じです。
–3/16
持続期間と音の長さは、必ず適用対象の音の後に指定しますが、指定する順序は関係ありません。また、長さは固定ではありません。音の長さを指定しないと、持続期間が長さとして使用されます。
音の前にトークンを指定することもできます。楽器の音を設定するには、I の文字の後に 0 から始まるパッチ番号を指定します。たとえば、次の文字列は、連続音にバイオリンを使用することを示します。
I40
ピアノは既定のパッチです。
連続音に新しい音量 (ベロシティ) を指定するには、次のように V を使用します。
V64
I でも V でも、文字の後の数値は、0 ~ 127 の範囲で指定する必要があります。
既定のテンポは、毎分 60 個の 4 分音符を再生する速度です。後続の音に新しいテンポを設定するには、T の後に毎分再生する 4 分音符の数を指定します。たとえば、次のようになります。
T120
一連の音をまったく同じパラメーターで再生するには、これらの音をかっこで囲みます。ハ長調の和音は以下のとおりです。
(C4 E4 G4 C5)
かっこの中に指定できるのは音だけです。縦棒 (|) は、チャンネルを区切ります。チャンネルは同時に再生されるため、テンポを含めて完全に独立しています。
特定のチャンネルのどこかに大文字の P が含まれている場合、そのチャンネルはパーカッション チャンネルになります。パーカッション チャンネルには、通常の表記で音符や休止を含めることができますが、パーカッションの音を数字で指定することもできます。たとえば、次の文字列はカウベルです。
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 プロパティを上記の例で示した形式の文字列に設定して、Play メソッドを呼び出します (必要に応じて、さらに Stop メソッドを呼び出してシーケンスが完了する前に停止します)。
MidiStringPlayer クラスには、要素が読み込まれたときにシーケンスを再生する PlayOnLoad プロパティ、および読み取り専用の IsPlaying プロパティもあります。要素では、シーケンスの再生が完了すると Ended イベントが発生し、MIDI 文字列の構文にエラーがある場合は Failed イベントが発生します。イベントには、問題のあるトークンを示すテキスト文字列のオフセット、およびテキストによるエラーの説明が含まれます。
ダウンロード可能なコードには、2 つの WPF プログラムも含まれています。MusicComposer プログラムを使用すると、MIDI 文字列を対話的に作成できます。WpfMusicDemo プログラムでは、図 1 のように、MIDI ファイルの単純なシーケンスをエンコードします。
図 1 いくつかの単純な MIDI 文字列をエンコードする WpfMusicDemo プログラムの Window1.xaml
<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 & 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 を使用しました。もちろん、これは最適な手法ではありません。別のプログラムが CPU を占有している場合、音楽の再生が不規則になります。また、DispatcherTimer では毎秒約 60 回を超える速度で Tick イベントを生成できません。この速度は単純な曲には十分ですが、より複雑なリズムの音楽に必要な精度は得られません。
Win32 API には、MIDI シーケンスの再生専用に設計された高精度のタイマーがありますが、NAudio ライブラリにはまだ組み込まれていません。おそらく、将来のどこかの時点で DispatcherTimer をもう少し正確で規則的なタイマーに置き換えることになるでしょうが、今のところは、この単純なソリューションで DispatcherTimer が問題なく動作することに満足しましょう。
Charles Petzold は MSDN Magazine の記事を長期にわたって担当している寄稿編集者です。最新の著書には『The Annotated Turing: A Guided Tour through Alan Turing's Historic Paper on Computability and the Turing Machine』(Wiley、2008 年) があります。Petzold のブログは、彼の Web サイト (charlespetzold.com、英語) で公開されています。
この記事のレビューに協力してくれた技術スタッフの Mark Heath に心より感謝いたします。