CodeZine(コードジン)

特集ページ一覧

自作楽器「MIDIテルミン」の製作

自作楽器製作を通してMIDIの制御方法を学ぶ

  • ブックマーク
  • LINEで送る
  • このエントリーをはてなブックマークに追加
2005/09/29 12:00

ダウンロード バイナリ (230.1 KB)
ダウンロード ソースファイル (293.0 KB)

マウスをパッドの上でぐりぐりとドラッグすることで、連続音を発する不思議な自作楽器「MIDIテルミン」を製作します。自作楽器製作を通じてWindowsでのMIDI制御の方法についても解説します。

はじめに

 本稿ではマウスをパッドの上でぐりぐりとドラッグすることで、音を発する不思議な自作楽器「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テルミンの概要

 まずは、どんな楽器を作るのか考えてみます。本家テルミンには、アンテナが一本立っており、このアンテナと演奏者の手の距離によって音程や音量が変わります。これをヒントにして、マウスを左右に動かすと、音程が変わるようにし、マウスを上下に動かすと、音量が変わるようにしてみます。

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関数を呼び出しています。

MIDIデバイスの列挙
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メッセージは以下の通りです。

代表的な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
エクスプレッション(音量)0xBn11値(0-127) 0xB0 11 127
ピッチベンド0xEn下位7ビット上位7ビット0xE0 0 0
注意
 nにはチャンネル番号(0-15)を指定します。

ピッチベンド

 ピッチベンドとは、音程を連続的に滑らかに変化させるためのものです。例えば、ギターのチョーキングやグリッサンドを表現するのに使います。今回、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メッセージを送信できるようにしました。

簡易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シーケンサを開発しています。

 このユニットは、自由に利用可能ですので、これを改造して、ぜひ自作楽器を作ってみてください。

参考資料

  • ブックマーク
  • LINEで送る
  • このエントリーをはてなブックマークに追加

著者プロフィール

  • クジラ飛行机(クジラヒコウヅクエ)

    ソフト企画「くじらはんど」にて、多数のフリーソフトを公開しています。日本語プログラミング言語「なでしこ」、テキスト音楽「サクラ」、日本語Wiki記法が特徴の「KonaWiki」などを公開しています。

All contents copyright © 2005-2020 Shoeisha Co., Ltd. All rights reserved. ver.1.5