はじめに
本稿ではマウスをパッドの上でぐりぐりとドラッグすることで、音を発する不思議な自作楽器「MIDIテルミン」を製作します。
「テルミン(Theremin)」とは1920年、ロシアの物理学者レフ・セルゲイヴィッチ・テルミンによって発明された世界最古の電子楽器です。楽器に直接触れることなく、空間にかざした手の動きによって演奏するという不思議な楽器です。本稿は、これをMIDI音源とパソコンで再現しようというものです。
また、Windowsには、MIDIを利用するためのAPIが用意されていますが、なかなかMIDIに関する資料が少ないのが現状です。そこで、自作楽器を作ってみることで、WindowsでのMIDI制御の方法についても解説します。
対象読者
- MIDIに興味のある方
- 自作楽器を作ってみたい方
必要な環境
MIDI音源、もしくは、サウンドカードのあるパソコンが必要です(サウンドカードがついていれば、だいたい Microsoft GS Wavetable SW SynthというソフトウェアMIDI音源が入っているはずです)。
開発には、Delphi 7を使用しましたが、Delphi 2005(Win32)でも動作確認を行いました。
MIDIとは
パソコンとシンセサイザ(もしくは、シンセサイザ同士)を接続して、シンセサイザを演奏させるための規格です。音を波形データとして送ることはなく、純粋に演奏情報のみをやりとりします。最近のパソコンでは、MIDIをソフトウェアで再現するソフトウェアMIDI音源が入っているので、別途シンセサイザを用意することなくMIDI演奏を楽しむことができます。
[コントロールパネル]を開き、[サウンドとオーディオデバイス]を開き、以下のようにMIDIの項にデバイスが表示されていれば、MIDIで演奏が可能です。
今回作成するMIDIテルミンの概要
まずは、どんな楽器を作るのか考えてみます。本家テルミンには、アンテナが一本立っており、このアンテナと演奏者の手の距離によって音程や音量が変わります。これをヒントにして、マウスを左右に動かすと、音程が変わるようにし、マウスを上下に動かすと、音量が変わるようにしてみます。
つまり、パッド上でマウスを操作することにより演奏が可能な楽器ということです。マウス操作によるMIDI演奏は以下のようにします。
- マウスボタンを押した時、シンセサイザの発音(ノートオン情報を送信)をします。
- マウスボタンを離した時、消音(ノートオフ情報を送信)します。
- マウスのボタンを押したままドラッグした時には、マウスの座標から音程と音量を計算して、音程の変更(ピッチベンド情報)と音量の変更(ベロシティ情報)を送信します。
MIDI関連のAPI
自作楽器を作る前に、WindowsでMIDIを操作するために必要なAPIをまとめておきます。
MIDIデバイスの列挙
MIDIに演奏情報を送信する前に、MIDIデバイスを開く必要があります。WindowsにインストールされているMIDIデバイスを列挙するには、Windows APIのmidiOutGetDevCaps
関数を利用します。この関数は、「MMSystem.pas」(C言語なら「mmsystem.h」)で定義されています。ですので、プログラムのuses
節に、MMSystem
を追加します。
以下のプログラムは、出力MIDIデバイスを列挙して1つずつその名前を画面に表示する例です。まず、midiOutGetNumDevs
関数で、出力MIDIデバイスの数を得て、その数だけ繰り返しmidiOutGetDevCaps
関数を呼び出しています。
procedure PrintMidiDeviceList; var mp : MIDIOUTCAPS; s : string; id : Integer; cnt : Integer; begin cnt := midiOutGetNumDevs; // 出力MIDIデバイスの数を取得 for id := 0 to cnt - 1 do // デバイスの数だけ繰り返し begin midiOutGetDevCaps(id, @mp, SizeOf(MIDIOUTCAPS)); s := string(mp.szPname); ShowMessage(IntToStr(id) + ' - ' + s); end; end;
ドの音を発音する
次に発音の方法です。以下のプログラムは、1秒間ドの音を鳴らす例です。演奏を行うには、利用したいMIDIデバイスを開き、ハンドルを得ます。そして、ハンドルを指定して演奏を行い、最後にMIDIデバイスを閉じます。
MIDIの演奏情報には、ノートオン(発音:0x90)、ノートオフ(消音:0x80)、プログラムチェンジ(音色変更:0xC0)などがあり、1つのメッセージは、2バイトから3バイトで成り立ちます。
WindowsでMIDIに演奏情報を送信するには、midiOutShortMsg
関数を利用します。この関数の1つ目の引数には、midiOutOpen
関数で得たMIDIハンドルを、2つ目の引数には、4バイトから成るMIDIメッセージを指定します。
procedure PlayC; var h : HMIDIOUT; // ハンドル data : array [0..3] of Byte; // 演奏情報 begin // (1) MIDIデバイスを開く midiOutOpen(@h, MIDI_MAPPER, 0, 0, CALLBACK_NULL); // (2) 演奏情報を送信 // 発音 data[0] := $90 + 0; // 発音メッセージ + チャンネル data[1] := 60; // ノート番号(60=ド) data[2] := 127; // 音量 data[3] := 0; // 使われない midiOutShortMsg(h, Cardinal(data)); // メッセージを送信 sleep(1000); // 発音時間 // 消音 data[0] := $80 + 0; // 消音メッセージ + チャンネル data[1] := 60; // ノート番号 data[2] := 127; // 音量 data[3] := 0; // 使われない midiOutShortMsg(h, Cardinal(data)); // メッセージを送信 // (3) MIDIデバイスを閉じる midiOutClose(h); end;
プログラム中(1)では、開くデバイスの番号を、MIDI_MAPPER
としています。これは、コントロールパネルでユーザーが指定しているデバイスを利用するという意味になります。
(2)の部分では、midiOutShortMsg
関数を使ってMIDIメッセージを送信しています。今回の自作楽器作成に使うMIDIメッセージは以下の通りです。
メッセージ | 1バイト目 | 2バイト目 | 3バイト目 | 例 |
ノートオン(発音) | 0x9n | ノート番号(0-127) | ベロシティ(0-127) | 0x90 60 127 |
ノートオフ(消音) | 0x8n | ノート番号(0-127) | ベロシティ(0-127) | 0x80 60 127 |
プログラムチェンジ(音色変更) | 0xCn | 音色番号(0-127) | --- | 0xC0 80 |
エクスプレッション(音量) | 0xBn | 11 | 値(0-127) | 0xB0 11 127 |
ピッチベンド | 0xEn | 下位7ビット | 上位7ビット | 0xE0 0 0 |
ピッチベンド
ピッチベンドとは、音程を連続的に滑らかに変化させるためのものです。例えば、ギターのチョーキングやグリッサンドを表現するのに使います。今回、MIDIテルミンを作るにあたって、テルミンのような連続的な音程変化を付けるためにピッチベンドを利用します。
ピッチベンドでは、14ビットの整数(-8192から8191)の値を指定できます。そして、ピッチベンドを送る前にピッチベンドの利き具合を、半音階を単位とした音程の範囲で指定ができます。例えば、ピッチベンドの範囲を2半音に設定し、ピッチベンドの値を8191にして、ドを発音すると、実際にはレの音が鳴ります。また、範囲を12半音(1オクターブ)にし、ピッチベンドを2730にして、ドを発音すると、ミに近い音が鳴るという具合です。
以下のプログラムは、ピッチベンドの範囲を上下12半音(1オクターブ)に設定し、ピッチベンドの値0を送信するプログラムです。
procedure SendPitchBend; var h : HMIDIOUT; // ハンドル ch : Byte; // チャンネル番号 // MIDIショートメッセージを送信 ... (a) procedure sendMidiMsg(b1, b2, b3: Byte); var data : array [0..3] of Byte; begin data[0] := b1; data[1] := b2; data[2] := b3; data[3] := 0; midiOutShortMsg(h, Cardinal(data)); // メッセージを送信 end; // RPNの指定 ... (b) procedure sendRPN(ch, msb, lsb, value: Byte); begin sendMidiMsg($B0 + ch, 101, msb); sendMidiMsg($B0 + ch, 100, lsb); sendMidiMsg($B0 + ch, 6, value); end; // ピッチベンドを送信 ... (c) procedure sendPitchBend(ch: Byte; value: Integer); var lo, hi: Byte; begin Inc(value,8192); // -8192~8191が指定されるので0ベースに直す // ベンド値の計算 hi := (value shr 7) and $7F; // 上位 lo := value and $7F; // 下位 sendMidiMsg($E0 + ch, lo, hi); // 送信 end; begin // (1) MIDIデバイスを開く midiOutOpen(@h, MIDI_MAPPER, 0, 0, CALLBACK_NULL); // (2) 演奏情報を送信 // ピッチベンドの範囲を指定 ch := 0; // チャンネル番号を指定 sendRPN(ch, 0, 0, 12); // 12 半音を指定 // ピッチベンドを送信 sendPitchBend(ch, 0); // ドを発音 sendMidiMsg($90+ch, 60, 127); sleep(1000); sendMidiMsg($80+ch, 60, 127); // (3) MIDIデバイスを閉じる midiOutClose(h); end;
ピッチベンドの範囲を指定するには、RPNという特殊コマンドを送信します。これは、MSB/LSB/DataEntryの3つのメッセージを1セットとして送信します。プログラム中の(b)にあるsendRPN
関数を参考にしてください。
プログラム中(c)の部分では、ピッチベンドをMIDIメッセージとして送信しています。このとき、送信したい値に8192を足して、上位7ビット、下位7ビットに分けて送信します。14ビットの値を、上位7ビットを得るには、shr
というビットシフト演算子を使います。これは右方向にビットをずらすことができます。そして下位7ビットを得るには、and
演算子を使って0x7Fをマスクします。
MIDIメッセージ送信クラスを作る
ピッチベンドの送信例を見ると分かりますが、MIDI情報の送信には似た部分が多く、いちいち数値を指定してMIDIメッセージ送信するのは非常に退屈です。そこで、MIDIメッセージを手軽に送信できるクラスを作ってみました。それがソースファイルの「midi_out.pas」にあるTMidiOut
クラスです。
これは、以下のように手軽にMIDIメッセージを送信できるようにしました。
var mo: TMidiOut; ch: Byte; begin ch := 0; // チャンネル番号 0 を使用 mo := TMidiOut.Create; try mo.Open(-1); // デフォルトポートを開く mo.OutProgramChange(ch, 80); // 音色変更 mo.OutRPN(ch, 0, 0, 12); // ピッチベンドの範囲を12半音に mo.OutBned(ch, 0); // ピッチベンドを0に mo.OutNoteOn(ch, 60, 127); // 60(=ド)を発音 sleep(1000); mo.OutNoteOff(ch, 60, 0); // 60(=ド)を消音 mo.OutBned(ch, 500); // ピッチベンドを500に mo.OutNoteOn(ch, 60, 127); // 60(=ド)を発音 sleep(1000); mo.OutNoteOff(ch, 60, 0); // 60(=ド)を消音 finally mo.Free; end; end;
画面の設計
コンポーネントの配置
MIDIの制御方法が分かったところで、楽器の操作画面を設計してみましょう。以下のようにDelphi標準のコンポーネントを設置してみました。
画面上部でMIDIの設定を行い、それ以外は演奏用のパッドにします。このパッド上をマウスでドラッグすることによりMIDI演奏を行います。
画面描画
本家テルミンは、垂直に立っているアンテナと演奏者の手の距離で音程を調節します。そのため、絶対音感がないと、とても音痴な演奏になってしまいます。そこで、MIDIテルミンでは、ピアノの鍵盤に見立てた半音ごとの音程ガイドを描画しようと思います。フォームをリサイズするごとに、ガイドを再描画するようにしています。
以下のプログラムがピアノの鍵盤を描画している部分です。このプログラムでは、MIDIテルミンの利用可能な音域を2オクターブと限定しているので、2オクターブつまり24半音分のガイドラインを描画しています。
ピアノの鍵盤は、白黒が交互に並んでいるわけではないので、1オクターブ内の12音階について、配列変数「kenban」に白を1、黒を0として並び順を代入しています。
procedure TfrmMain.FormResize(Sender: TObject); const kenban: array [0..11] of Byte = (1, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1); var x, xc : Integer; w, w2 : Extended; cPen, cBrush, cCenter: TColor; begin //todo: キャンバスの描画 // 鍵盤の描画 with imgBoard.Canvas do begin Pen.Style := psSolid ; Pen.Color := clBlack ; Pen.Width := 1; xc := 24; w := imgBoard.Width / xc; w2 := w / 2; for x := 0 to xc do begin // 鍵盤の色の決定 if kenban[x mod 12] = 1 then begin cPen := RGB( 50, 50, 50); cBrush := RGB(255,255,255); cCenter := RGB(200,200,200); end else begin cPen := RGB( 50, 50, 50); cBrush := RGB(150,150,150); cCenter := RGB(100,100,100); end; // 鍵盤の描画 Pen.Color := cPen; Brush.Color := cBrush; Rectangle(Trunc(x * w - w2), 0, Trunc((x+1) * w + w2), imgBoard.Height); Pen.Color := cCenter; MoveTo(Trunc(x * w), 0); LineTo(Trunc(x * w), imgBoard.Height); end; end; end;
マウスイベントの実装
MIDIテルミンの動作は非常に単純です。マウスのボタンを押したらノートオン(発音)のイベントを送信し、ボタンを離したらノートオフ(消音)を送信します。
procedure TfrmMain.imgBoardMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); begin FMouse := True; // マウスフラグをTrueにする // 座標に応じたベンド音量を送信 imgBoardMouseMove(Sender, Shift, X, Y); MidiOut.OutNoteOn(ch, note, 127); // 発音する end;
procedure TfrmMain.imgBoardMouseUp(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); begin MidiOut.OutNoteOff(ch, note, 0); // 消音 FMouse := False; // マウスフラグをFalseにする end;
そして、マウスを動かした時にマウスの座標からピッチベンドとエクスプレッション(音量)の値を計算してMIDIイベントを送信します。
procedure TfrmMain.imgBoardMouseMove(Sender: TObject; Shift: TShiftState; X, Y: Integer); var w, h: Integer; ve, vp: Integer; begin if FMouse then begin w := imgBoard.Width ; h := imgBoard.Height ; // ピッチベンド vp := Trunc( (8192 * 2) / w * X ) - 8192; MidiOut.OutBned(ch, vp); // エクスプレッション ve := Trunc( 128 / h * Y); MidiOut.OutCtrlChg(ch, 11, ve); sbar.SimpleText := Format('Pitch=%d Expression=%d', [vp, ve]); end; end;
おわりに
このように、MIDIイベントの扱いさえ分かってしまえば、自作MIDI楽器の製作はそれほど難しいものではありません。ただし、ある程度MIDI規格についての知識は必要とされます。MIDI規格を説明している書籍やWebサイトは豊富にあるので、ちょっと凝ったMIDI楽器を作ろうと思ったらこれらを一読すると良いでしょう。
また、本文でも触れていますが、ソースコードの「midi_out」ユニットのTMidiOut
クラスを使うとMIDIイベントを手軽に扱うことが出来ます。ちなみに筆者はこのユニットを使って、テキスト音楽「サクラ」という文字ベースのMIDIシーケンサを開発しています。
このユニットは、自由に利用可能ですので、これを改造して、ぜひ自作楽器を作ってみてください。
参考資料
- ディレクト アート スタジオ 『MIDIユーザとプログラマのためのMIDI講座』
- テキスト音楽「サクラ」