内部実装
Javaソースコード
- 本体
- ChordHelperApplet.java - Java アプレットおよび MIDI 関連制御部
- MidiChordHelper.java - Java アプリケーションとして使うためのメインプログラム
- GUI部品
- ChordMatrix.java - 五度圏コードボタン
- PianoKeyboard.java - ピアノキーボード
- ChordDiagram.java - コードダイアグラム
- ButtonIcon.java - ボタンアイコン
- MIDIEditor.java - MIDIデータ編集&プレイリスト
- MIDIMsgForm.java - MIDIメッセージ入力フォーム
- NewSequenceDialog.java - MIDIデータ生成ダイアログ
- Base64Dialog.java - Base64ダイアログ
- AnoGakki.java - あの楽器
- MIDI&作曲部品
- MIDISequencer.java - MIDI シーケンサー関連
- MIDIDevice.java - MIDI デバイス選択用の抽象クラス
- MIDISpec.java - MIDI 仕様を記述した部分(音色の名前など)
- Music.java - 音楽理論と自動作曲の実装(Music クラス)
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通りがあります。
- MIDI ノート番号(物理的な音階)
12平均律を前提とした物理的な音階で、半音単位で0〜127まであります。
白黒含めたピアノの鍵盤の順序なのでわかりやすく、オクターブを含めた物理的な音階を表現できます。
12で割った余りを使って0〜11までの値で表すことで、オクターブ情報を除いた音階を表現できます。
ただし、この表現方法では異名同音(例えば F# と G♭)を区別できないという問題があります。
- 五度圏での位置(論理的な音階)
音階を半音単位ではなく完全五度の単位、すなわち 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(0)
- 1弦・最初のフレット → scanFret(1)
- 2弦・最初のフレット → scanFret(2)
- 3弦・最初のフレット → scanFret(3)
- 最後の弦・最初のフレット → 構成音が揃っていたら登録
- 次のフレット → 構成音が揃っていたら登録
- 2弦・次のフレット → scanFret(2)
- 3弦・最初のフレット → scanFret(3)
- 最後の弦・最初のフレット → 構成音が揃っていたら登録
- 次のフレット → 構成音が揃っていたら登録
- 3弦・次のフレット → scanFret(3)
- 最後の弦・最初のフレット → 構成音が揃っていたら登録
- 次のフレット → 構成音が揃っていたら登録
- 1弦・次のフレット → scanFret(1) ...
まず最初の弦に対して scanFret() メソッドでフレットごとに見ていきます。
途中、フレットが変化するごとに次の弦に対して scanFret() を呼び出し、
さらにその中で次の弦に対して scanFret() を呼び出し...というふうに
再帰呼び出しをしています。最後の弦に達したら、構成音が揃っているかチェックして
OKであればバリエーションの一つとして登録します。
構成音が揃っているかのチェックは、高速化を図るため二進数のビットでチェックしています。
ビットが 0 のところは、構成音のその位置にある音が抜けていることを意味します。
抜けていないときの正しい値と比較すれば、有効な押さえ方かどうかを簡単に判定できます。
他の言語での実現可能性
Flash で実装したコンテンツは、
右クリックが Flash のメニューになってしまうため、当アプレットみたいに
右クリックを多用しているアプリケーションは組めないようです。
さらに、Java には MIDI 音源を直接叩く機能もあるので、やはり Java にしておいて
正解だったかも知れません。
参考ページ
[ページ先頭へ戻る]