ホーム > 音楽 > MIDI Chord Helper > 内部実装

内部実装

Javaソースコード

コード(Chord)を弾くために書いたコード(Code)です。 かなりカオスなソースになってて読みにくいかも知れませんw

ソース本体はOSDN の Git リポジトリのほうにあります。

MIDIサウンド

音を出すところは Java のサウンド API(javax.sound.midi パッケージ)を使っています。

MIDI キーボードからのイベントを拾う方法は、Java のドキュメントを見ただけではわかりにくいですが、 こんな風にして自分自身に MIDI Receiver を実装すれば可能です。
import javax.sound.midi.*;

public class MyApplet
  extends JApplet
  implements Receiver // <- アプレット自身に MIDI Receiver を実装していることを明示する
{
    MidiDevice.Info[] dev_infos = MidiSystem.getMidiDeviceInfo(); // MIDIデバイス一覧を取得
    int i = 0; // MIDI デバイスの通し番号(実際は選べるようにする必要がある)
    MidiDevice input_device = MidiSystem.getMidiDevice(dev_infos[i]);
    Transmitter tx;
    try {
      tx = input_device.getTransmitter();
      tx.setReceiver(this); // アプレット自身をMIDIレシーバとして登録する
    } catch(MidiUnavailableException e) {
      e.printStackTrace();
    }

    // MIDI Receiver の実装
    //   Transmitter に登録したので、MIDI イベントが来るたびに send() が
    //   自動的に呼ばれる。
    //
    public void close() {}
    public void send(MidiMessage message, long timeStamp) {
      // ここで message の中身を解析すればよい
    }
}
AWT や Swing を使うとき、AddなんとかListener() を使って自分自身を登録しますが、 これと全く同じように setReceiver() を使って自分自身を登録すればよいのです。

send() の中に時間のかかる処理を書くと、MIDI 信号が出始めるときの遅延が大きくなり、 再生される音楽が「もたついて」聞こえてしまう原因になります。 そこで、javax.swing.Timer のタイマーを使って画面関連の処理を「予約」したうえで、 一旦 send() を抜けて発音をできるだけ早く開始させます。 予約は立て続けに来ることがあるため、上書きされないよう待ち行列に蓄える必要がありますが、 これは java.util.Queue インターフェースを実装したクラスで実現できます(ここでは LinkedList を使っています)。

javax.sound.midi には Sequencer インターフェースがあり、これを操作することで MIDI データの録音や再生ができます。 この機能を使って、MIDI Chord Helper で MIDI データの録音再生を実現しています。 Sequencer を使う場合は、MIDI の信号として流れない「メタイベント」(シーケンサへの指示を出すためのイベント)を 受信するための MetaEventListener があるので、これを使って曲の終わり(トラックの終わり)、 拍子やテンポの変更、調号の変更、といったイベントを受信しています。 ちなみに、拍子の分母は「n分音符」のnと同じなので「2の整数乗」しかありえません。 そのため、メタイベントでは2のべき乗の値として入っています。表示のために本来の数値に戻すときのみ、 ビット単位でシフトする演算子 << を使っています(2のべき乗のままのほうが、何かと処理が書きやすいので)。

音階の表し方と五度圏計算

音階を数値で表す方法には、大きく分けて以下の2通りがあります。
  1. MIDI ノート番号(物理的な音階)
    12平均律を前提とした物理的な音階で、半音単位で0〜127まであります。 白黒含めたピアノの鍵盤の順序なのでわかりやすく、オクターブを含めた物理的な音階を表現できます。 12で割った余りを使って0〜11までの値で表すことで、オクターブ情報を除いた音階を表現できます。 ただし、この表現方法では異名同音(例えば F# と G♭)を区別できないという問題があります。

  2. 五度圏での位置(論理的な音階)
    音階を半音単位ではなく完全五度の単位、すなわち FCGDAEB の並びで どこに位置するかという値で表現します。このようにすると、ダブルシャープやダブルフラットまで含めた 異名同音を明確に区別できます。
この2つの値の相互変換を行うには、7 倍して 12 で割った余りを取ります (Wikipedia の Circle of fifths で、 この方法による計算結果を示したデモンストレーションが行われています)。 なお、この変換は、値が偶数のときは実質変化しません。 7 倍する代わりに、奇数のときだけ 6 を足すという方法でもOKです。

2. の方法で表現することで、さまざまなメリットが生まれます。

例えば、C♭ major(調号:♭7個)と B major(調号:#5個)を 1. の方法で表現すると 両者を区別できなくなってしまいますが、2. の方法で表現すればこうした問題は起こりません。 また、MIDI (SMF) のメタイベントの一つとして、調性を指定するものがありますが、 これは調号の#や♭の数で指定するので、2. の方法による数値表現をそのまま使えます。 ただし、マイナーの場合はメジャーと比べて3つずれることに注意が必要です。

ある音階がその調のスケールを外れていないかチェックしたい場合も、 2. の方法で表現しておけば、白鍵と黒鍵がきれいに分離され、大小比較だけで簡単に判定できます。

GUI部品

Swing の部品を使っています。 以前は、Base64 保存用のテキスト欄だけ AWT のクラスを使っていました。 AWT のテキスト部品はメモ帳などと同じように右クリックでコピーなどのメニューが出ますが、 Swing のテキスト部品はメニューが出ないためです。しかし、 Swing のプルダウンメニューは AWT の部品に隠れてしまうため、 現在ではほぼすべて Swing 部品で実装しています。 Swing テキストでのコピー&ペーストは、メモ帳と同様に Ctrl+C や Ctrl+V で行えます。

ボタンアイコンは、画像ファイルを別途用意して読み込むのが面倒なので、グラフィック描画で作っています。 例えば四分音符なら丸と直線、プレイボタンなら三角、のような感じです。 Icon インターフェースを実装したクラス(class ButtonIcon)を自分で作って、その中で描画しています。

Java アプレットの Java アプリケーション化

こちらのページが参考になります。ポイントとしては、通常ブラウザから呼ばれる アプレットオブジェクトを、メインプログラムから同じインターフェースで呼ぶようにすることです。

ピアノキーボード

PianoKeyboard クラスとして実装しています。
黒鍵の位置を黒鍵のない部分に寄せるため、白鍵の上端の幅が均等になるよう、 黒鍵の横位置を調整しています。本物のピアノやキーボードもそうなっているはずです。 位置の計算についてはソースを見てください。
以前のバージョンではキーの位置を返すだけでしたが、現在では オクターブ範囲を管理するデータモデルを内蔵していて、直接 MIDI ノート番号を返せるようになっています。 このデータモデルと接続するためのスクロールバーは呼び出し側で作成します。

コードボタンへの図形描画

鳴っている音にあわせてコードボタンをリアルタイムに光らせていますが、 その裏では、無駄な処理を排除するために色々な工夫を行っています。

まず、起動時に12音階別インデックスを作っておきます。 このインデックスには、どのコードボタンに知らせるべきか、そのコードボタンの何番目の構成音か、 といった情報を登録しておきます。

実際に音が鳴ると、コードボタンのオブジェクトに「その音階が鳴った/消えた」という通知が来ます (ChordMatrix.indicateNoteOn() メソッド)。 この通知を受けると、まずその音階がいくつ鳴っているかという「重み」(Weight) を更新します。 重みの「0」か「非0」かが切り替わった場合に限り、インデックスを引いて関連するコードボタンを探し、 特定のコードボタンのみに通知を送ります。 通知を受け取ったコードボタンでは、構成音の順序値から、図形の内容を書き換える必要が あるかどうかを判断します。書き換える必要がある場合に限り repaint() で描画を予約します。

このように、75個のコードボタンがそれぞれ独自に判断し、必要なときだけ図形を描画するようになっています。

一般のコード判定ソフトでは、機械が明示的にコードネームを判定しようとするものが多いですが、 MIDI Chord Helper ではそのようなことは一切行いません。 図形表示であれば、複数の候補があったり、余分な音が混じったりしても、人間が柔軟に判断できるのです。 機械で判断したとしても、最終的にコード判定するのは人間であることに変わりはありません。

コードダイアグラムの探索方法

コードダイアグラムの表示・探索部分は、ChordDiagramDisplay クラスとして実装しています。 探索は、一見難しそうに見えますが、実は単純な木構造探索アルゴリズムで簡単に実現できます。

まず、一定のフレット範囲内で、コードの構成音が出るポイント(PressingPoint クラス)を全部拾います。 ポイントの中身は、弦番号、フレット番号、コード構成音の順序番号(=画面上の●や○の色)です。

拾ったポイントすべてについて、scanFret() メソッドが自分自身を呼び出す(再帰呼び出し)形で 木構造探索を行い、すべての組み合わせをもれなくチェックします。

まず最初の弦に対して scanFret() メソッドでフレットごとに見ていきます。 途中、フレットが変化するごとに次の弦に対して scanFret() を呼び出し、 さらにその中で次の弦に対して scanFret() を呼び出し...というふうに 再帰呼び出しをしています。最後の弦に達したら、構成音が揃っているかチェックして OKであればバリエーションの一つとして登録します。

構成音が揃っているかのチェックは、高速化を図るため二進数のビットでチェックしています。 ビットが 0 のところは、構成音のその位置にある音が抜けていることを意味します。 抜けていないときの正しい値と比較すれば、有効な押さえ方かどうかを簡単に判定できます。

他の言語での実現可能性

Flash で実装したコンテンツは、 右クリックが Flash のメニューになってしまうため、当アプレットみたいに 右クリックを多用しているアプリケーションは組めないようです。 さらに、Java には MIDI 音源を直接叩く機能もあるので、やはり Java にしておいて 正解だったかも知れません。

参考ページ


[ページ先頭へ戻る]