OSDN Git Service

・eclipse導入に伴うリファクタリング(Java7対応など)
authorAkiyoshi Kamide <kamide@yk.rim.or.jp>
Wed, 30 Oct 2013 16:09:10 +0000 (16:09 +0000)
committerAkiyoshi Kamide <kamide@yk.rim.or.jp>
Wed, 30 Oct 2013 16:09:10 +0000 (16:09 +0000)
・開いているMIDIデバイスをツリー上でdisable表示するよう改良

git-svn-id: https://svn.sourceforge.jp/svnroot/midichordhelper/MIDIChordHelper@4 302f1594-2db2-43b1-aaa4-6307b5a2a2de

21 files changed:
.classpath [new file with mode: 0644]
.project [new file with mode: 0644]
.settings/org.eclipse.jdt.core.prefs [new file with mode: 0644]
lib/commons-codec-1.4.jar [new file with mode: 0644]
src/AnoGakki.java [new file with mode: 0644]
src/Base64Dialog.java [new file with mode: 0644]
src/ButtonIcon.java [new file with mode: 0644]
src/ChordDiagram.java [new file with mode: 0644]
src/ChordHelperApplet.java [new file with mode: 0644]
src/ChordMatrix.java [new file with mode: 0644]
src/MIDIDevice.java [new file with mode: 0644]
src/MIDIEditor.java [new file with mode: 0644]
src/MIDIMsgForm.java [new file with mode: 0644]
src/MIDISequencer.java [new file with mode: 0644]
src/MIDISpec.java [new file with mode: 0644]
src/MidiChordHelper.java [new file with mode: 0644]
src/Music.java [new file with mode: 0644]
src/NewSequenceDialog.java [new file with mode: 0644]
src/PianoKeyboard.java [new file with mode: 0644]
src/images/midichordhelper.ico [new file with mode: 0644]
src/images/midichordhelper.png [new file with mode: 0644]

diff --git a/.classpath b/.classpath
new file mode 100644 (file)
index 0000000..5c26a12
--- /dev/null
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>\r
+<classpath>\r
+       <classpathentry kind="src" path="src"/>\r
+       <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.7"/>\r
+       <classpathentry kind="lib" path="lib/commons-codec-1.4.jar"/>\r
+       <classpathentry kind="output" path="bin"/>\r
+</classpath>\r
diff --git a/.project b/.project
new file mode 100644 (file)
index 0000000..b8443f2
--- /dev/null
+++ b/.project
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>\r
+<projectDescription>\r
+       <name>MIDIChordHelper</name>\r
+       <comment></comment>\r
+       <projects>\r
+       </projects>\r
+       <buildSpec>\r
+               <buildCommand>\r
+                       <name>org.eclipse.jdt.core.javabuilder</name>\r
+                       <arguments>\r
+                       </arguments>\r
+               </buildCommand>\r
+       </buildSpec>\r
+       <natures>\r
+               <nature>org.eclipse.jdt.core.javanature</nature>\r
+       </natures>\r
+</projectDescription>\r
diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs
new file mode 100644 (file)
index 0000000..838bd9d
--- /dev/null
@@ -0,0 +1,11 @@
+eclipse.preferences.version=1\r
+org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled\r
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.7\r
+org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve\r
+org.eclipse.jdt.core.compiler.compliance=1.7\r
+org.eclipse.jdt.core.compiler.debug.lineNumber=generate\r
+org.eclipse.jdt.core.compiler.debug.localVariable=generate\r
+org.eclipse.jdt.core.compiler.debug.sourceFile=generate\r
+org.eclipse.jdt.core.compiler.problem.assertIdentifier=error\r
+org.eclipse.jdt.core.compiler.problem.enumIdentifier=error\r
+org.eclipse.jdt.core.compiler.source=1.7\r
diff --git a/lib/commons-codec-1.4.jar b/lib/commons-codec-1.4.jar
new file mode 100644 (file)
index 0000000..458d432
Binary files /dev/null and b/lib/commons-codec-1.4.jar differ
diff --git a/src/AnoGakki.java b/src/AnoGakki.java
new file mode 100644 (file)
index 0000000..c59519a
--- /dev/null
@@ -0,0 +1,300 @@
+\r
+import java.awt.BasicStroke;\r
+import java.awt.BorderLayout;\r
+import java.awt.Color;\r
+import java.awt.Component;\r
+import java.awt.Graphics;\r
+import java.awt.Graphics2D;\r
+import java.awt.Point;\r
+import java.awt.Rectangle;\r
+import java.awt.Stroke;\r
+import java.awt.event.ActionEvent;\r
+import java.awt.event.ActionListener;\r
+import java.awt.event.ComponentAdapter;\r
+import java.awt.event.ComponentEvent;\r
+import java.awt.geom.AffineTransform;\r
+import java.util.Iterator;\r
+import java.util.LinkedList;\r
+\r
+import javax.swing.JComponent;\r
+import javax.swing.JLayeredPane;\r
+import javax.swing.SwingUtilities;\r
+/**\r
+ * Innocence「あの楽器」風の表示を行う {@link JLayeredPane} 拡張クラスです。\r
+ * start() メソッドで表示を開始でき、\r
+ * 時間が経過すると表示が自然に消えるようになっています。\r
+ */\r
+class AnoGakkiLayeredPane extends JLayeredPane {\r
+       AnoGakkiPane anoGakkiPane = new AnoGakkiPane();\r
+       /**\r
+        * 新しい {@link AnoGakkiLayeredPane} を構築します。\r
+        */\r
+       public AnoGakkiLayeredPane() {\r
+               add(anoGakkiPane, JLayeredPane.PALETTE_LAYER);\r
+               addComponentListener(\r
+                       new ComponentAdapter() {\r
+                               private void adjustSize() {\r
+                                       anoGakkiPane.setBounds(getBounds());\r
+                               }\r
+                               @Override\r
+                               public void componentResized(ComponentEvent e) {\r
+                                       adjustSize();\r
+                               }\r
+                               @Override\r
+                               public void componentShown(ComponentEvent e) {\r
+                                       adjustSize();\r
+                               }\r
+                       }\r
+               );\r
+               setLayout(new BorderLayout());\r
+               setOpaque(true);\r
+       }\r
+       /**\r
+        * 指定された点から図形の表示を開始します。\r
+        * @param source 元のAWTコンポーネント\r
+        * @param point 点の位置\r
+        */\r
+       public void start(Component source, Point point) {\r
+               anoGakkiPane.start(source,point);\r
+       }\r
+       /**\r
+        * 指定された長方形領域({@link Rectangle})の中央から図形の表示を開始します。\r
+        * @param source 元のAWTコンポーネント\r
+        * @param rect 長方形領域\r
+        */\r
+       public void start(Component source, Rectangle rect) {\r
+               Point p = rect.getLocation();\r
+               p.translate( rect.width/2, rect.height/2 );\r
+               start(source,p);\r
+       }\r
+       /**\r
+        * クリックされたコンポーネントの中央から図形の表示を開始します。\r
+        * @param source 元のAWTコンポーネント\r
+        * @param clickedComponent クリックされたコンポーネント\r
+        */\r
+       public void start(Component source, Component clickedComponent) {\r
+               start(source,clickedComponent.getBounds());\r
+       }\r
+}\r
+\r
+class AnoGakkiPane extends JComponent {\r
+       /**\r
+        * 1ステップあたりの時間間隔(ミリ秒)\r
+        */\r
+       static final int INTERVAL_MS = 15;\r
+       /**\r
+        * 表示終了までのステップ数\r
+        */\r
+       static final int INITIAL_COUNT = 20;\r
+       /**\r
+        * 角速度ωの(絶対値の)最大\r
+        */\r
+       static final double MAX_OMEGA = 0.005;\r
+       /**\r
+        * 図形の種類\r
+        */\r
+       enum Shape {\r
+               /** ○ */\r
+               CIRCLE {\r
+                       public void draw(Graphics2D g2, QueueEntry entry) {\r
+                               entry.drawCircle(g2);\r
+                       }\r
+               },\r
+               /** = */\r
+               LINES {\r
+                       public void draw(Graphics2D g2, QueueEntry entry) {\r
+                               entry.drawLines(g2);\r
+                       }\r
+               },\r
+               /** □ */\r
+               SQUARE {\r
+                       public void draw(Graphics2D g2, QueueEntry entry) {\r
+                               entry.drawSquare(g2);\r
+                       }\r
+               },\r
+               /** △ */\r
+               TRIANGLE {\r
+                       public void draw(Graphics2D g2, QueueEntry entry) {\r
+                               entry.drawTriangle(g2);\r
+                       }\r
+               };\r
+               /**\r
+                * 図形の種類の値をランダムに返します。\r
+                * @return ランダムな値\r
+                */\r
+               public static Shape randomShape() {\r
+                       return values()[(int)(Math.random() * values().length)];\r
+               }\r
+               /**\r
+                * この図形を描画します。\r
+                * @param g2 描画オブジェクト\r
+                * @param entry キューエントリ\r
+                */\r
+               public abstract void draw(Graphics2D g2, QueueEntry entry);\r
+       }\r
+       /**\r
+        * 色(RGBA、Aは「不透明度」を表すアルファ値)\r
+        */\r
+       static final Color color = new Color(0,255,255,192);\r
+       /**\r
+        * 線の太さ\r
+        */\r
+       static final Stroke stroke = new BasicStroke((float)5);\r
+       /**\r
+        * いま描画すべき図形を覚えておくためのキュー\r
+        */\r
+       LinkedList<QueueEntry> queue = new LinkedList<QueueEntry>();\r
+       /**\r
+        * キューエントリ内容\r
+        */\r
+       class QueueEntry {\r
+               /** 時間軸 */\r
+               private int countdown = INITIAL_COUNT;\r
+               /** スタートからの経過時間(ミリ秒) */\r
+               private int tms = 0;\r
+               //\r
+               /** 図形の種類 */\r
+               Shape shape = Shape.randomShape();\r
+               /** クリックされた場所(中心) */\r
+               private Point clickedPoint;\r
+               //\r
+               // 回転する場合\r
+               /** 現在の半径 */\r
+               private int r = 0;\r
+               /** 回転速度(時計回り) */\r
+               private double omega = 0;\r
+               /** 現在の回転角(アフィン変換) */\r
+               AffineTransform affineTransform = null;\r
+               /**\r
+                * 新しいキューエントリを構築します。\r
+                * @param clickedPoint クリックされた場所\r
+                */\r
+               public QueueEntry(Point clickedPoint) {\r
+                       this.clickedPoint = clickedPoint;\r
+                       if( shape != Shape.CIRCLE ) {\r
+                               // ○以外なら回転角を初期化\r
+                               //(○は回転しても見かけ上何も変わらなくて無駄なので除外)\r
+                               affineTransform = AffineTransform.getRotateInstance(\r
+                                       2 * Math.PI * Math.random(),\r
+                                       clickedPoint.x,\r
+                                       clickedPoint.y\r
+                               );\r
+                               omega = MAX_OMEGA * (1.0 - 2.0 * Math.random());\r
+                       }\r
+               }\r
+               /**\r
+                * このキューエントリをカウントダウンします。\r
+                * @return カウントダウン値(0 でタイムアウト)\r
+                */\r
+               public int countDown() {\r
+                       if( countdown > 0 ) {\r
+                               // 時間 t を進める\r
+                               countdown--;\r
+                               tms += INTERVAL_MS;\r
+                               // 半径 r = vt\r
+                               r = tms / 2;\r
+                               // 回転\r
+                               if( shape == Shape.SQUARE || shape == Shape.TRIANGLE ) {\r
+                                       // 角度を θ=ωt で求めると、移動距離 l=rθ が\r
+                                       // t の2乗のオーダーで伸びるため、加速しているように見えてしまう。\r
+                                       // 一定の速度に見せるために t を平方根にして角度を計算する。\r
+                                       affineTransform.rotate(\r
+                                               omega * Math.sqrt((double)tms),\r
+                                               clickedPoint.x,\r
+                                               clickedPoint.y\r
+                                       );\r
+                               }\r
+                       }\r
+                       return countdown;\r
+               }\r
+               /**\r
+                * ○を描画します。\r
+                * @param g2 描画オブジェクト\r
+                */\r
+               public void drawCircle(Graphics2D g2) {\r
+                       int d = 2 * r;\r
+                       g2.drawOval( clickedPoint.x-r, clickedPoint.y-r, d, d );\r
+               }\r
+               /**\r
+                * =を描画します。\r
+                * @param g2 描画オブジェクト\r
+                */\r
+               public void drawLines(Graphics2D g2) {\r
+                       int width2 = 2 * getSize().width;\r
+                       int y = clickedPoint.y;\r
+                       g2.transform(affineTransform);\r
+                       g2.drawLine( -width2, y-r, width2, y-r );\r
+                       g2.drawLine( -width2, y+r, width2, y+r );\r
+               }\r
+               /**\r
+                * □を描画します。\r
+                * @param g2 描画オブジェクト\r
+                */\r
+               public void drawSquare(Graphics2D g2) {\r
+                       int d = 2 * r;\r
+                       g2.transform(affineTransform);\r
+                       g2.drawRect( clickedPoint.x-r, clickedPoint.y-r, d, d );\r
+               }\r
+               /**\r
+                * △を描画します。\r
+                * @param g2 描画オブジェクト\r
+                */\r
+               public void drawTriangle(Graphics2D g2) {\r
+                       int x = clickedPoint.x;\r
+                       int y = clickedPoint.y;\r
+                       g2.transform(affineTransform);\r
+                       g2.drawLine( x-r, y, x+r, y-r );\r
+                       g2.drawLine( x-r, y, x+r, y+r );\r
+                       g2.drawLine( x+r, y-r, x+r, y+r );\r
+               }\r
+       }\r
+       /**\r
+        * キューを更新するアニメーション用タイマー\r
+        */\r
+       javax.swing.Timer timer = new javax.swing.Timer(\r
+               INTERVAL_MS,\r
+               new ActionListener() {\r
+                       @Override\r
+                       public void actionPerformed(ActionEvent event) {\r
+                               Iterator<QueueEntry> i = queue.iterator();\r
+                               while( i.hasNext() )\r
+                                       if( i.next().countDown() <= 0 ) i.remove();\r
+                               if( queue.isEmpty() ) timer.stop();\r
+                               repaint();\r
+                       }\r
+               }\r
+       );\r
+       public AnoGakkiPane() {\r
+               super();\r
+               setOpaque(false);\r
+               timer.setCoalesce(true);\r
+               timer.setRepeats(true);\r
+       }\r
+       @Override\r
+       public void paint(Graphics g) {\r
+               if( queue.isEmpty() )\r
+                       return;\r
+               Graphics2D g2 = (Graphics2D)g;\r
+               g2.setStroke(stroke);\r
+               g2.setColor(color);\r
+               Iterator<QueueEntry> i = queue.iterator();\r
+               while( i.hasNext() ) {\r
+                       QueueEntry entry = i.next();\r
+                       entry.shape.draw(g2, entry);\r
+               }\r
+       }\r
+       private long prevStartedAt = System.nanoTime();\r
+       public void start(Component source, Point clicked_point) {\r
+               long startedAt = System.nanoTime();\r
+               if( startedAt - prevStartedAt < (INTERVAL_MS * 1000)*50 ) {\r
+                       // 頻繁すぎる場合は無視する\r
+                       return;\r
+               }\r
+               queue.add(new QueueEntry(\r
+                       SwingUtilities.convertPoint(source, clicked_point, this)\r
+               ));\r
+               timer.start();\r
+               prevStartedAt = startedAt;\r
+       }\r
+}\r
+\r
diff --git a/src/Base64Dialog.java b/src/Base64Dialog.java
new file mode 100644 (file)
index 0000000..f4236ea
--- /dev/null
@@ -0,0 +1,147 @@
+import java.awt.Dimension;\r
+import java.awt.Insets;\r
+import java.awt.event.ActionEvent;\r
+import java.awt.event.ActionListener;\r
+import java.util.regex.Pattern;\r
+\r
+import javax.swing.Box;\r
+import javax.swing.BoxLayout;\r
+import javax.swing.JButton;\r
+import javax.swing.JDialog;\r
+import javax.swing.JLabel;\r
+import javax.swing.JPanel;\r
+import javax.swing.JScrollPane;\r
+import javax.swing.JTextArea;\r
+\r
+import org.apache.commons.codec.binary.Base64;\r
+\r
+/**\r
+ * Base64テキスト入力ダイアログ\r
+ */\r
+public class Base64Dialog extends JDialog implements ActionListener {\r
+       private static final Insets ZERO_INSETS = new Insets(0,0,0,0);\r
+       private Base64TextArea base64TextArea = null;\r
+       private JButton addBase64Button;\r
+       private JButton clearButton;\r
+       private JPanel base64Panel;\r
+       private MidiEditor midiEditor;\r
+       private boolean base64Available;\r
+       private static class Base64TextArea extends JTextArea {\r
+               private static final Pattern headerLine =\r
+                       Pattern.compile( "^.*:.*$", Pattern.MULTILINE );\r
+               public Base64TextArea(int rows, int columns) {\r
+                       super(rows,columns);\r
+               }\r
+               public byte[] getBinary() {\r
+                       String text = headerLine.matcher(getText()).replaceAll("");\r
+                       return Base64.decodeBase64(text.getBytes());\r
+               }\r
+               public void setBinary(byte[] binary_data, String content_type, String filename) {\r
+                       if( binary_data != null && binary_data.length > 0 ) {\r
+                               String header = "";\r
+                               if( content_type != null && filename != null ) {\r
+                                       header += "Content-Type: " + content_type + "; name=\"" + filename + "\"\n";\r
+                                       header += "Content-Transfer-Encoding: base64\n";\r
+                                       header += "\n";\r
+                               }\r
+                               setText(header + new String(Base64.encodeBase64Chunked(binary_data)) + "\n");\r
+                       }\r
+               }\r
+       }\r
+       public Base64Dialog(MidiEditor midiEditor) {\r
+               this.midiEditor = midiEditor;\r
+               setTitle("Base64-encoded MIDI sequence - " + ChordHelperApplet.VersionInfo.NAME);\r
+               try {\r
+                       Base64.decodeBase64( "".getBytes() );\r
+                       base64Available = true;\r
+               } catch( NoClassDefFoundError e ) {\r
+                       base64Available = false;\r
+               }\r
+               if( base64Available ) {\r
+                       base64TextArea = new Base64TextArea(8,56);\r
+                       JScrollPane scrollable_media_text = new JScrollPane(base64TextArea);\r
+                       addBase64Button = new JButton( "Base64 Decode & Add to PlayList", new ButtonIcon(ButtonIcon.EJECT_ICON) );\r
+                       addBase64Button.setMargin(ZERO_INSETS);\r
+                       addBase64Button.addActionListener(this);\r
+                       addBase64Button.setToolTipText("Base64デコードして、プレイリストへ追加");\r
+                       clearButton = new JButton( "Clear" );\r
+                       clearButton.setMargin(ZERO_INSETS);\r
+                       clearButton.addActionListener(this);\r
+\r
+                       base64Panel = new JPanel();\r
+                       base64Panel.setLayout( new BoxLayout( base64Panel, BoxLayout.PAGE_AXIS ) );\r
+                       JPanel base64_button_panel = new JPanel();\r
+                       base64_button_panel.setLayout( new BoxLayout( base64_button_panel, BoxLayout.LINE_AXIS ) );\r
+                       base64_button_panel.add( new JLabel("Base64-encoded MIDI sequence:") );\r
+                       base64_button_panel.add( Box.createRigidArea(new Dimension(10, 0)) );\r
+                       base64_button_panel.add( addBase64Button );\r
+                       base64_button_panel.add( clearButton );\r
+                       base64Panel.add( base64_button_panel );\r
+                       base64Panel.add( scrollable_media_text );\r
+                       add(base64Panel);\r
+               }\r
+               // setLocationRelativeTo(applet);\r
+               setBounds( 300, 250, 660, 300 );\r
+       }\r
+       @Override\r
+       public void actionPerformed(ActionEvent event) {\r
+               Object obj = event.getSource();\r
+               if( obj == addBase64Button ) {\r
+                       int last_index = midiEditor.addSequenceFromMidiData( getMIDIData(), null );\r
+                       if( last_index < 0 ) {\r
+                               base64TextArea.requestFocusInWindow();\r
+                               last_index = midiEditor.seqListModel.getRowCount() - 1;\r
+                       }\r
+                       midiEditor.seq_selection_model.setSelectionInterval( last_index, last_index );\r
+                       setVisible(false);\r
+               }\r
+               else if( obj == clearButton ) {\r
+                       base64TextArea.setText(null);\r
+               }\r
+       }\r
+       /**\r
+        * {@link Base64} が使用できるかどうかを返します。\r
+        * @return Apache Commons Codec ライブラリが利用できる状態ならtrue\r
+        */\r
+       public boolean isBase64Available() {\r
+               return base64Available;\r
+       }\r
+       /**\r
+        * バイナリー形式でMIDIデータを返します。\r
+        * @return バイナリー形式のMIDIデータ\r
+        */\r
+       public byte[] getMIDIData() {\r
+               return base64TextArea.getBinary();\r
+       }\r
+       /**\r
+        * バイナリー形式のMIDIデータを設定します。\r
+        * @param midiData バイナリー形式のMIDIデータ\r
+        */\r
+       public void setMIDIData( byte[] midiData ) {\r
+               base64TextArea.setBinary(midiData, null, null);\r
+       }\r
+       /**\r
+        * バイナリー形式のMIDIデータを、ファイル名をつけて設定します。\r
+        * @param midiData バイナリー形式のMIDIデータ\r
+        * @param filename ファイル名\r
+        */\r
+       public void setMIDIData( byte[] midiData, String filename ) {\r
+               base64TextArea.setBinary(midiData, "audio/midi", filename);\r
+               base64TextArea.selectAll();\r
+       }\r
+       /**\r
+        * Base64形式でMIDIデータを返します。\r
+        * @return  Base64形式のMIDIデータ\r
+        */\r
+       public String getBase64Data() {\r
+               return base64TextArea.getText();\r
+       }\r
+       /**\r
+        * Base64形式のMIDIデータを設定します。\r
+        * @param base64Data Base64形式のMIDIデータ\r
+        */\r
+       public void setBase64Data( String base64Data ) {\r
+               base64TextArea.setText(null);\r
+               base64TextArea.append(base64Data);\r
+       }\r
+}\r
diff --git a/src/ButtonIcon.java b/src/ButtonIcon.java
new file mode 100644 (file)
index 0000000..8afc4cd
--- /dev/null
@@ -0,0 +1,400 @@
+\r
+import java.awt.Color;\r
+import java.awt.Component;\r
+import java.awt.Graphics;\r
+import java.awt.Graphics2D;\r
+\r
+import javax.swing.AbstractButton;\r
+import javax.swing.Icon;\r
+\r
+/**\r
+ * カスタムペイントアイコン\r
+ */\r
+class ButtonIcon implements Icon {\r
+       public static final int BLANK_ICON = 0;\r
+       public static final int REC_ICON = 1;\r
+       public static final int PLAY_ICON = 2;\r
+       public static final int STOP_ICON = 3;\r
+       public static final int EJECT_ICON = 4;\r
+       public static final int PAUSE_ICON = 5;\r
+       public static final int ANO_GAKKI_ICON = 6;\r
+       //\r
+       public static final int INVERSION_ICON = 8;\r
+       public static final int DARK_MODE_ICON = 9;\r
+       public static final int X_ICON = 10;\r
+       public static final int REPEAT_ICON = 11;\r
+       public static final int MIDI_CONNECTOR_ICON = 12;\r
+       public static final int NATURAL_ICON = 13;\r
+       public static final int EDIT_ICON = 14;\r
+       public static final int FORWARD_ICON = 15;\r
+       public static final int BACKWARD_ICON = 16;\r
+       public static final int TOP_ICON = 17;\r
+       public static final int BOTTOM_ICON = 18;\r
+       //\r
+       public static final int A128TH_NOTE_ICON = 128;\r
+       public static final int DOTTED_128TH_NOTE_ICON = 129;\r
+       public static final int A64TH_NOTE_ICON = 130;\r
+       public static final int DOTTED_64TH_NOTE_ICON = 131;\r
+       public static final int A32ND_NOTE_ICON = 132;\r
+       public static final int DOTTED_32ND_NOTE_ICON = 133;\r
+       public static final int A16TH_NOTE_ICON = 134;\r
+       public static final int DOTTED_16TH_NOTE_ICON = 135;\r
+       public static final int A8TH_NOTE_ICON = 136;\r
+       public static final int DOTTED_8TH_NOTE_ICON = 137;\r
+       public static final int QUARTER_NOTE_ICON = 138;\r
+       public static final int DOTTED_QUARTER_NOTE_ICON = 139;\r
+       public static final int HALF_NOTE_ICON = 140;\r
+       public static final int DOTTED_HALF_NOTE_ICON = 141;\r
+       public static final int WHOLE_NOTE_ICON = 142;\r
+       //\r
+       private int iconKind;\r
+       public int getIconKind() { return iconKind; }\r
+       //\r
+       public boolean isMusicalNote() {\r
+               return iconKind >= A128TH_NOTE_ICON && iconKind <= WHOLE_NOTE_ICON ;\r
+       }\r
+       public boolean isDottedMusicalNote() {\r
+               return isMusicalNote() && ((iconKind & 1) != 0) ;\r
+       }\r
+       public int getMusicalNoteValueIndex() { // Returns log2(n) of n-th note\r
+               return isMusicalNote() ? (WHOLE_NOTE_ICON + 1 - iconKind) / 2 : -1 ;\r
+       }\r
+       //\r
+       private int width = 16;\r
+       private static final int HEIGHT = 16;\r
+       private static final int MARGIN = 3;\r
+       //\r
+       // for notes\r
+       private static final int NOTE_HEAD_WIDTH = 8;\r
+       private static final int NOTE_HEAD_HEIGHT = 6;\r
+       //\r
+       // for eject button\r
+       private static final int EJECT_BOTTOM_LINE_WIDTH = 2;\r
+       //\r
+       // for play/eject button\r
+       private int xPoints[];\r
+       private int yPoints[];\r
+       //\r
+       public ButtonIcon(int kind) {\r
+               iconKind = kind;\r
+               switch( iconKind ) {\r
+               case PLAY_ICON:\r
+                       xPoints = new int[4]; yPoints = new int[4];\r
+                       xPoints[0] = MARGIN;       yPoints[0] = MARGIN;\r
+                       xPoints[1] = width-MARGIN; yPoints[1] = HEIGHT/2;\r
+                       xPoints[2] = MARGIN;       yPoints[2] = HEIGHT-MARGIN;\r
+                       xPoints[3] = MARGIN;       yPoints[3] = MARGIN;\r
+                       break;\r
+               case EJECT_ICON:\r
+                       xPoints = new int[4]; yPoints = new int[4];\r
+                       xPoints[0] = width/2;      yPoints[0] = MARGIN;\r
+                       xPoints[1] = width-MARGIN; yPoints[1] = HEIGHT - MARGIN - 2*EJECT_BOTTOM_LINE_WIDTH;\r
+                       xPoints[2] = MARGIN;       yPoints[2] = HEIGHT - MARGIN - 2*EJECT_BOTTOM_LINE_WIDTH;\r
+                       xPoints[3] = width/2;      yPoints[3] = MARGIN;\r
+                       break;\r
+               case TOP_ICON:\r
+               case BACKWARD_ICON:\r
+                       xPoints = new int[8]; yPoints = new int[8];\r
+                       xPoints[0] = width-MARGIN; yPoints[0] = MARGIN;\r
+                       xPoints[1] = width-MARGIN; yPoints[1] = HEIGHT-MARGIN;\r
+                       xPoints[2] = width/2;      yPoints[2] = HEIGHT/2;\r
+                       xPoints[3] = width/2;      yPoints[3] = HEIGHT-MARGIN;\r
+                       xPoints[4] = MARGIN;       yPoints[4] = HEIGHT/2;\r
+                       xPoints[5] = width/2;      yPoints[5] = MARGIN;\r
+                       xPoints[6] = width/2;      yPoints[6] = HEIGHT/2;\r
+                       xPoints[7] = width-MARGIN; yPoints[7] = MARGIN;\r
+                       break;\r
+               case BOTTOM_ICON:\r
+               case FORWARD_ICON:\r
+                       xPoints = new int[8]; yPoints = new int[8];\r
+                       xPoints[0] = MARGIN;       yPoints[0] = MARGIN;\r
+                       xPoints[1] = MARGIN;       yPoints[1] = HEIGHT-MARGIN;\r
+                       xPoints[2] = width/2;      yPoints[2] = HEIGHT/2;\r
+                       xPoints[3] = width/2;      yPoints[3] = HEIGHT-MARGIN;\r
+                       xPoints[4] = width-MARGIN;       yPoints[4] = HEIGHT/2;\r
+                       xPoints[5] = width/2;      yPoints[5] = MARGIN;\r
+                       xPoints[6] = width/2;      yPoints[6] = HEIGHT/2;\r
+                       xPoints[7] = MARGIN;       yPoints[7] = MARGIN;\r
+                       break;\r
+               case INVERSION_ICON:\r
+               case ANO_GAKKI_ICON:\r
+                       width = 32;\r
+                       break;\r
+               case REPEAT_ICON:\r
+                       xPoints = new int[4]; yPoints = new int[4];\r
+                       xPoints[0] = width/2 - 2;  yPoints[0] = MARGIN;\r
+                       xPoints[1] = width/2 + 2;  yPoints[1] = MARGIN - 4;\r
+                       xPoints[2] = width/2 + 2;  yPoints[2] = MARGIN + 5;\r
+                       xPoints[3] = width/2 - 2;  yPoints[3] = MARGIN + 1;\r
+                       break;\r
+               }\r
+       }\r
+       // Icon interface\r
+       public int getIconWidth() { return width; }\r
+       public int getIconHeight() { return HEIGHT; }\r
+       public void paintIcon(Component c, Graphics g, int x, int y) {\r
+               Graphics2D g2 = (Graphics2D) g;\r
+               boolean is_selected = (\r
+                       (\r
+                               (c instanceof AbstractButton) && ((AbstractButton)c).isSelected()\r
+                               )||(\r
+                               (c instanceof InversionAndOmissionLabel) && (\r
+                                       ((InversionAndOmissionLabel)c).isAutoInversionMode()\r
+                               )\r
+                       )\r
+               );\r
+               int omitting_note = c instanceof InversionAndOmissionLabel ?\r
+                       ((InversionAndOmissionLabel)c).getOmissionNoteIndex() : -1;\r
+               g2.setColor( c.isEnabled() ? c.getForeground() : c.getBackground().darker() );\r
+               g2.translate(x, y);\r
+               switch(iconKind) {\r
+               case REC_ICON:\r
+                       if( c.isEnabled() ) g.setColor(Color.red);\r
+                       g2.fillOval( MARGIN, MARGIN, width - 2*MARGIN, HEIGHT - 2*MARGIN );\r
+                       break;\r
+               case TOP_ICON:\r
+                       g2.fillRect( MARGIN-1, MARGIN, 2, HEIGHT - 2*MARGIN );\r
+                       // No break;\r
+               case BACKWARD_ICON:\r
+               case FORWARD_ICON:\r
+               case PLAY_ICON:\r
+                       g2.fillPolygon( xPoints, yPoints, xPoints.length );\r
+                       break;\r
+               case BOTTOM_ICON:\r
+                       g2.fillRect( width-1-MARGIN, MARGIN, 2, HEIGHT - 2*MARGIN );\r
+                       g2.fillPolygon( xPoints, yPoints, xPoints.length );\r
+                       break;\r
+               case STOP_ICON:\r
+                       g2.fillRect( MARGIN+1, MARGIN+1, width - 2*(MARGIN+1), HEIGHT - 2*(MARGIN+1) );\r
+                       break;\r
+               case PAUSE_ICON:\r
+                       g2.fillRect( MARGIN+1, MARGIN+1, width/5, HEIGHT - 2*(MARGIN+1) );\r
+                       g2.fillRect( width-1-MARGIN-width/5, MARGIN+1, width/5, HEIGHT - 2*(MARGIN+1) );\r
+                       break;\r
+               case EJECT_ICON:\r
+                       g2.fillPolygon( xPoints, yPoints, xPoints.length );\r
+                       g2.fillRect(\r
+                               MARGIN+1,\r
+                               HEIGHT - MARGIN - EJECT_BOTTOM_LINE_WIDTH,\r
+                               width - 2*MARGIN - 1,\r
+                               EJECT_BOTTOM_LINE_WIDTH\r
+                       );\r
+                       break;\r
+\r
+               case ANO_GAKKI_ICON:\r
+                       g2.setBackground( c.getBackground() );\r
+                       g2.clearRect( 0,  0, width, HEIGHT );\r
+                       g2.setColor(Color.cyan);\r
+                       g2.drawRect(  4, 4, 10, 10 );\r
+                       g2.drawLine(  1, 14, 30, 4 );\r
+                       g2.drawOval(  18, 1, 12, 12 );\r
+                       if( ! is_selected ) {\r
+                               // g2.setStroke(new BasicStroke(2));\r
+                               g2.setColor(Color.red);\r
+                               g2.drawLine( 0, 0, width-1, HEIGHT-1 );\r
+                               g2.drawLine( 0, HEIGHT-1, width-1, 0 );\r
+                       }\r
+                       break;\r
+\r
+               case INVERSION_ICON:\r
+                       g2.setBackground( c.getBackground() );\r
+                       g2.clearRect( 0,  0, width, HEIGHT );\r
+                       g2.setColor( c.getBackground().darker() );\r
+                       g2.drawRect(  0,  0, width-1, HEIGHT-1 );\r
+                       g2.drawLine(  8,  0,  8, HEIGHT );\r
+                       g2.drawLine( 16,  0, 16, HEIGHT );\r
+                       g2.drawLine( 24,  0, 24, HEIGHT );\r
+                       g2.setColor( c.getForeground() );\r
+                       g2.fillRect(  6,  0,  5,  HEIGHT/2 );\r
+                       g2.fillRect( 14,  0,  5,  HEIGHT/2 );\r
+                       g2.fillRect( 22,  0,  5,  HEIGHT/2 );\r
+                       if( is_selected ) {\r
+                               g2.setColor( Music.Chord.NOTE_INDEX_COLORS[1] );\r
+                               g2.fillOval( 2, 10, 4, 4 );\r
+                               if( omitting_note == 1 ) {\r
+                                       g2.setColor( c.getForeground() );\r
+                                       g2.drawLine( 1, 9, 7, 15 );\r
+                                       g2.drawLine( 1, 15, 7, 9 );\r
+                               }\r
+                               g2.setColor( Music.Chord.NOTE_INDEX_COLORS[2] );\r
+                               g2.fillOval( 10, 10, 4, 4 );\r
+                               if( omitting_note == 2 ) {\r
+                                       g2.setColor( c.getForeground() );\r
+                                       g2.drawLine( 9, 9, 15, 15 );\r
+                                       g2.drawLine( 9, 15, 15, 9 );\r
+                               }\r
+                               g2.setColor( Music.Chord.NOTE_INDEX_COLORS[0] );\r
+                               g2.fillOval( 26, 10, 4, 4 );\r
+                               if( omitting_note == 0 ) {\r
+                                       g2.setColor( c.getForeground() );\r
+                                       g2.drawLine( 25, 9, 31, 15 );\r
+                                       g2.drawLine( 25, 15, 31, 9 );\r
+                               }\r
+                       }\r
+                       else {\r
+                               g2.setColor( Music.Chord.NOTE_INDEX_COLORS[0] );\r
+                               g2.fillOval( 1, 9, 6, 6 );\r
+                               if( omitting_note == 0 ) {\r
+                                       g2.setColor( c.getForeground() );\r
+                                       g2.drawLine( 1, 9, 7, 15 );\r
+                                       g2.drawLine( 1, 15, 7, 9 );\r
+                               }\r
+                               g2.setColor( Music.Chord.NOTE_INDEX_COLORS[1] );\r
+                               g2.fillOval( 10, 10, 4, 4 );\r
+                               if( omitting_note == 1 ) {\r
+                                       g2.setColor( c.getForeground() );\r
+                                       g2.drawLine( 9, 9, 15, 15 );\r
+                                       g2.drawLine( 9, 15, 15, 9 );\r
+                               }\r
+                               g2.setColor( Music.Chord.NOTE_INDEX_COLORS[2] );\r
+                               g2.fillOval( 18, 10, 4, 4 );\r
+                               if( omitting_note == 2 ) {\r
+                                       g2.setColor( c.getForeground() );\r
+                                       g2.drawLine( 17, 9, 23, 15 );\r
+                                       g2.drawLine( 17, 15, 23, 9 );\r
+                               }\r
+                       }\r
+                       break;\r
+               case DARK_MODE_ICON:\r
+                       if( is_selected ) {\r
+                               g2.setColor( c.getForeground().darker() );\r
+                               g2.fillRect( 0, 0, width, HEIGHT );\r
+                               g2.setColor( Color.gray );\r
+                               g2.fillRect( width-2, 0, 2, HEIGHT );\r
+                               g2.drawLine( 0, 0, width-1, 0 );\r
+                               g2.setColor( Color.gray.darker() );\r
+                               g2.drawLine( 0, 0, 0, HEIGHT-1 );\r
+                               g2.drawLine( 0, HEIGHT-1, width-1, HEIGHT-1 );\r
+                               g2.setColor( Color.orange.brighter() );\r
+                               g2.fillRect( width-6, HEIGHT/2-3, 2, 6 );\r
+                       }\r
+                       else {\r
+                               g2.setColor( c.getBackground().brighter() );\r
+                               g2.fillRect( 0, 0, width, HEIGHT );\r
+                               g2.setColor( Color.gray.brighter() );\r
+                               g2.drawLine( 0, 0, width-1, 0 );\r
+                               g2.drawLine( width-1, 0, width-1, HEIGHT-1 );\r
+                               g2.setColor( Color.gray );\r
+                               g2.fillRect( 0, 0, 2, HEIGHT );\r
+                               g2.drawLine( 0, HEIGHT-1, width-1, HEIGHT-1 );\r
+                               g2.setColor( Color.gray.brighter() );\r
+                               g2.fillRect( width-6, HEIGHT/2-4, 4, 7 );\r
+                               g2.setColor( c.getForeground() );\r
+                               g2.fillRect( width-5, HEIGHT/2-3, 2, 5 );\r
+                       }\r
+                       break;\r
+               case X_ICON:\r
+                       g2.drawLine( 4, 5, width-5, HEIGHT-4 );\r
+                       g2.drawLine( 4, 4, width-4, HEIGHT-4 );\r
+                       g2.drawLine( 5, 4, width-4, HEIGHT-5 );\r
+                       g2.drawLine( width-5, 4, 4, HEIGHT-5 );\r
+                       g2.drawLine( width-4, 4, 4, HEIGHT-4 );\r
+                       g2.drawLine( width-4, 5, 5, HEIGHT-4 );\r
+                       break;\r
+               case REPEAT_ICON:\r
+                       g2.drawArc( MARGIN, MARGIN, width - 2*MARGIN, HEIGHT - 2*MARGIN, 150, 300 );\r
+                       g2.fillPolygon( xPoints, yPoints, xPoints.length );\r
+                       break;\r
+               case MIDI_CONNECTOR_ICON:\r
+                       g2.drawOval( 0, 0, width - 2, HEIGHT - 2 );\r
+                       g2.fillRect( width/2-2, HEIGHT-4, 3, 3 );\r
+                       g2.fillOval( width/2-2, 2, 3, 3 );\r
+                       g2.fillOval( width/2-5, 4, 2, 2 );\r
+                       g2.fillOval( width/2+2, 4, 2, 2 );\r
+                       g2.fillOval( width/2-6, 7, 2, 2 );\r
+                       g2.fillOval( width/2+3, 7, 2, 2 );\r
+                       break;\r
+               case NATURAL_ICON:\r
+                       g2.drawLine( width/2-2, 1, width/2-2, HEIGHT-4 );\r
+                       g2.drawLine( width/2+1, 3, width/2+1, HEIGHT-2 );\r
+                       g2.drawLine( width/2-2, 4, width/2+1, 4 );\r
+                       g2.drawLine( width/2-2, 5, width/2+1, 5 );\r
+                       g2.drawLine( width/2-2, HEIGHT-6, width/2+1, HEIGHT-6 );\r
+                       g2.drawLine( width/2-2, HEIGHT-5, width/2+1, HEIGHT-5 );\r
+                       break;\r
+               case EDIT_ICON:\r
+                       g2.drawRect( 3, 1, 10, 14 );\r
+                       g2.drawLine( 5, 3, 11, 3 );\r
+                       g2.drawLine( 5, 5, 11, 5 );\r
+                       g2.drawLine( 5, 7, 11, 7 );\r
+                       g2.drawLine( 5, 9, 11, 9 );\r
+                       g2.drawLine( 5, 11, 11, 11 );\r
+                       if( c.isEnabled() ) g2.setColor( Color.red );\r
+                       g2.drawLine( width-1, 2, width-9, 10 );\r
+                       g2.drawLine( width-1, 3, width-9, 11 );\r
+                       break;\r
+\r
+               case DOTTED_HALF_NOTE_ICON:\r
+               case HALF_NOTE_ICON:\r
+                       drawMusicalNoteStem( g2 );\r
+                       // No break;\r
+               case WHOLE_NOTE_ICON:\r
+                       drawMusicalNoteHead( g2 );\r
+                       if( isDottedMusicalNote() ) drawMusicalNoteDot(g2);\r
+                       break;\r
+\r
+               case A128TH_NOTE_ICON:\r
+                       drawMusicalNoteFlag( g2, 4 );\r
+                       // No break;\r
+               case DOTTED_64TH_NOTE_ICON:\r
+               case A64TH_NOTE_ICON:\r
+                       drawMusicalNoteFlag( g2, 3 );\r
+                       // No break;\r
+               case DOTTED_32ND_NOTE_ICON:\r
+               case A32ND_NOTE_ICON:\r
+                       drawMusicalNoteFlag( g2, 2 );\r
+                       // No break;\r
+               case DOTTED_16TH_NOTE_ICON:\r
+               case A16TH_NOTE_ICON:\r
+                       drawMusicalNoteFlag( g2, 1 );\r
+                       // No break;\r
+               case DOTTED_8TH_NOTE_ICON:\r
+               case A8TH_NOTE_ICON:\r
+                       drawMusicalNoteFlag( g2, 0 );\r
+                       // No break;\r
+               case DOTTED_QUARTER_NOTE_ICON:\r
+               case QUARTER_NOTE_ICON:\r
+                       fillMusicalNoteHead(g2);\r
+                       drawMusicalNoteStem(g2);\r
+                       if( isDottedMusicalNote() ) drawMusicalNoteDot(g2);\r
+                       break;\r
+               }\r
+               g.translate(-x, -y);\r
+       }\r
+       private void drawMusicalNoteFlag( Graphics2D g2, int position ) {\r
+               g2.drawLine(\r
+                       width/2 + NOTE_HEAD_WIDTH/2 - 1,\r
+                       1 + position * 2,\r
+                       width/2 + NOTE_HEAD_WIDTH/2 + 4,\r
+                       6 + position * 2\r
+               );\r
+       }\r
+       private void drawMusicalNoteDot( Graphics2D g2 ) {\r
+               g2.fillRect(\r
+                       width/2 + NOTE_HEAD_WIDTH/2 + 2,\r
+                       HEIGHT - NOTE_HEAD_HEIGHT + 3,\r
+                       2, 2\r
+               );\r
+       }\r
+       private void drawMusicalNoteStem( Graphics2D g2 ) {\r
+               g2.fillRect(\r
+                       width/2 + NOTE_HEAD_WIDTH/2 - 1,\r
+                       1,\r
+                       1, HEIGHT - NOTE_HEAD_HEIGHT/2\r
+               );\r
+       }\r
+       private void drawMusicalNoteHead( Graphics2D g2 ) {\r
+               g2.drawOval(\r
+                       width/2 - NOTE_HEAD_WIDTH/2,\r
+                       HEIGHT - NOTE_HEAD_HEIGHT,\r
+                       NOTE_HEAD_WIDTH-1, NOTE_HEAD_HEIGHT-1\r
+               );\r
+       }\r
+       private void fillMusicalNoteHead( Graphics2D g2 ) {\r
+               g2.fillOval(\r
+                       width/2 - NOTE_HEAD_WIDTH/2,\r
+                       HEIGHT - NOTE_HEAD_HEIGHT,\r
+                       NOTE_HEAD_WIDTH, NOTE_HEAD_HEIGHT\r
+               );\r
+       }\r
+}\r
diff --git a/src/ChordDiagram.java b/src/ChordDiagram.java
new file mode 100644 (file)
index 0000000..61676a7
--- /dev/null
@@ -0,0 +1,939 @@
+import java.awt.Color;\r
+import java.awt.Dimension;\r
+import java.awt.FontMetrics;\r
+import java.awt.Graphics;\r
+import java.awt.Graphics2D;\r
+import java.awt.Insets;\r
+import java.awt.Point;\r
+import java.awt.Rectangle;\r
+import java.awt.event.ActionEvent;\r
+import java.awt.event.ActionListener;\r
+import java.awt.event.AdjustmentEvent;\r
+import java.awt.event.AdjustmentListener;\r
+import java.awt.event.ComponentAdapter;\r
+import java.awt.event.ComponentEvent;\r
+import java.awt.event.ItemEvent;\r
+import java.awt.event.ItemListener;\r
+import java.awt.event.MouseEvent;\r
+import java.awt.event.MouseListener;\r
+import java.awt.event.MouseMotionListener;\r
+import java.util.Arrays;\r
+import java.util.Collections;\r
+import java.util.EnumMap;\r
+import java.util.LinkedList;\r
+import java.util.List;\r
+import java.util.Map;\r
+import java.util.Objects;\r
+\r
+import javax.swing.Box;\r
+import javax.swing.BoxLayout;\r
+import javax.swing.ButtonGroup;\r
+import javax.swing.ComboBoxModel;\r
+import javax.swing.DefaultBoundedRangeModel;\r
+import javax.swing.JCheckBox;\r
+import javax.swing.JComboBox;\r
+import javax.swing.JComponent;\r
+import javax.swing.JPanel;\r
+import javax.swing.JRadioButton;\r
+import javax.swing.JScrollBar;\r
+import javax.swing.JToggleButton;\r
+import javax.swing.SwingConstants;\r
+import javax.swing.event.ChangeEvent;\r
+import javax.swing.event.ChangeListener;\r
+\r
+/**\r
+ * ChordDiagram class for MIDI Chord Helper\r
+ *\r
+ * @auther\r
+ *     Copyright (C) 2004-2013 Akiyoshi Kamide\r
+ *     http://www.yk.rim.or.jp/~kamide/music/chordhelper/\r
+ */\r
+public class ChordDiagram extends JPanel {\r
+       /**\r
+        * コードダイヤグラムの対象楽器\r
+        */\r
+       public enum TargetInstrument {\r
+               /** ウクレレ  */\r
+               Ukulele(Arrays.asList(9,4,0,7),true), // AECG\r
+               /** ギター */\r
+               Guitar(Arrays.asList(4,11,7,2,9,4),false); // EBGDAE\r
+               /**\r
+                * 開放弦の音階を表す、変更不可能なノート番号リストを返します。\r
+                * @return 開放弦の音階(固定値)を表すノート番号リスト\r
+                */\r
+               public List<Integer> getOpenNotes() { return openNotes; }\r
+               private List<Integer> openNotes;\r
+               JRadioButton radioButton;\r
+               private TargetInstrument(List<Integer> openNotes, boolean isUkulele) {\r
+                       this.openNotes = Collections.unmodifiableList(openNotes);\r
+                       radioButton = new JRadioButton(toString(), isUkulele);\r
+                       radioButton.setOpaque(false);\r
+               }\r
+               /**\r
+                * 開放弦の音階を表すノート番号の配列を生成します。\r
+                * この配列の音階はチューニングのために書き換えることができます。\r
+                *\r
+                * @return 開放弦の音階(デフォルト値)を表すノート番号の配列\r
+                */\r
+               public int[] createTunableOpenNotes() {\r
+                       int[] r = new int[openNotes.size()];\r
+                       int i=0;\r
+                       for(int note : openNotes) r[i++] = note;\r
+                       return r;\r
+               }\r
+       }\r
+       /**\r
+        * コードをテキストに記録するボタン\r
+        */\r
+       public JToggleButton recordTextButton =\r
+               new JToggleButton("REC", new ButtonIcon(ButtonIcon.REC_ICON)) {\r
+                       {\r
+                               setMargin(new Insets(0,0,0,0));\r
+                               setToolTipText("Record to text ON/OFF");\r
+                       }\r
+               };\r
+       /**\r
+        * コードダイアグラムのタイトルラベル\r
+        */\r
+       public ChordDisplay titleLabel =\r
+               new ChordDisplay("Chord Diagram",null,null) {\r
+                       {\r
+                               setHorizontalAlignment(SwingConstants.CENTER);\r
+                               setVerticalAlignment(SwingConstants.BOTTOM);\r
+                       }\r
+               };\r
+       /**\r
+        * コードダイアグラム表示部\r
+        */\r
+       private ChordDiagramDisplay diagramDisplay =\r
+               new ChordDiagramDisplay(TargetInstrument.Ukulele) {\r
+                       {\r
+                               setOpaque(false);\r
+                               setPreferredSize(new Dimension(120,120));\r
+                       }\r
+               };\r
+       /**\r
+        * 対象楽器選択ボタンのマップ\r
+        */\r
+       private Map<TargetInstrument,JRadioButton> instButtons =\r
+               new EnumMap<TargetInstrument,JRadioButton>(TargetInstrument.class) {\r
+                       {\r
+                               ButtonGroup buttonGroup = new ButtonGroup();\r
+                               for( final TargetInstrument instrument : TargetInstrument.values() ) {\r
+                                       instrument.radioButton.addActionListener(\r
+                                               new ActionListener() {\r
+                                                       @Override\r
+                                                       public void actionPerformed(ActionEvent e) {\r
+                                                               diagramDisplay.tune(instrument);\r
+                                                       }\r
+                                               }\r
+                                       );\r
+                                       buttonGroup.add(instrument.radioButton);\r
+                                       put(instrument, instrument.radioButton);\r
+                               }\r
+                       }\r
+               };\r
+\r
+       private JScrollBar variationScrollbar = new JScrollBar(JScrollBar.VERTICAL) {\r
+               {\r
+                       setModel(diagramDisplay.chordVariations.indexModel);\r
+                       addAdjustmentListener(\r
+                               new AdjustmentListener() {\r
+                                       @Override\r
+                                       public void adjustmentValueChanged(AdjustmentEvent e) {\r
+                                               setToolTipText(\r
+                                                       diagramDisplay.chordVariations.getIndexDescription()\r
+                                               );\r
+                                       }\r
+                               }\r
+                       );\r
+               }\r
+       };\r
+       private JScrollBar fretRangeScrollbar = new JScrollBar(JScrollBar.HORIZONTAL) {\r
+               {\r
+                       setModel(diagramDisplay.fretViewIndexModel);\r
+                       setBlockIncrement(diagramDisplay.fretViewIndexModel.getExtent());\r
+               }\r
+       };\r
+       /**\r
+        * カポ位置選択コンボボックス\r
+        */\r
+       public CapoSelecterView capoSelecterView = new CapoSelecterView() {\r
+               {\r
+                       checkbox.addItemListener(\r
+                               new ItemListener() {\r
+                                       @Override\r
+                                       public void itemStateChanged(ItemEvent e) {\r
+                                               clear();\r
+                                       }\r
+                               }\r
+                       );\r
+               }\r
+       };\r
+       /**\r
+        * コードダイアグラムを構築します。\r
+        * @param applet 親となるアプレット\r
+        */\r
+       public ChordDiagram(ChordHelperApplet applet) {\r
+               capoSelecterView.valueSelecter.setModel(\r
+                       applet.chordMatrix.capoValueModel\r
+               );\r
+               JPanel mainPanel = new JPanel();\r
+               mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.X_AXIS));\r
+               mainPanel.setOpaque(false);\r
+               mainPanel.add(new JPanel() {\r
+                       {\r
+                               setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));\r
+                               setOpaque(false);\r
+                               add(\r
+                                       new JPanel() {\r
+                                               {\r
+                                                       add(titleLabel);\r
+                                                       setOpaque(false);\r
+                                                       setAlignmentY((float)0);\r
+                                               }\r
+                                       }\r
+                               );\r
+                               add(diagramDisplay);\r
+                               fretRangeScrollbar.setAlignmentY((float)1.0);\r
+                               add(fretRangeScrollbar);\r
+                               add(\r
+                                       new JPanel() {\r
+                                               {\r
+                                                       setOpaque(false);\r
+                                                       for(JRadioButton rb : instButtons.values())\r
+                                                               add(rb);\r
+                                                       setAlignmentY((float)1.0);\r
+                                               }\r
+                                       }\r
+                               );\r
+                       }\r
+               });\r
+               mainPanel.add(variationScrollbar);\r
+               setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));\r
+               add(\r
+                       new JPanel() {\r
+                               {\r
+                                       setLayout(new BoxLayout(this, BoxLayout.X_AXIS));\r
+                                       setOpaque(false);\r
+                                       add(Box.createHorizontalStrut(2));\r
+                                       add(recordTextButton);\r
+                                       add(Box.createHorizontalStrut(2));\r
+                                       add(capoSelecterView);\r
+                               }\r
+                       }\r
+               );\r
+               add(Box.createHorizontalStrut(5));\r
+               add(mainPanel);\r
+       }\r
+       @Override\r
+       public void setBackground(Color bgColor) {\r
+               super.setBackground(bgColor);\r
+               if( diagramDisplay == null )\r
+                       return;\r
+               diagramDisplay.setBackground(bgColor);\r
+               capoSelecterView.setBackground(bgColor);\r
+               capoSelecterView.valueSelecter.setBackground(bgColor);\r
+               variationScrollbar.setBackground(bgColor);\r
+               fretRangeScrollbar.setBackground(bgColor);\r
+       }\r
+       /**\r
+        * コード(和音)をクリアします。\r
+        *\r
+        * <p>{@link #setChord(Chord)} の引数に null を指定して呼び出しているだけです。\r
+        * </p>\r
+        */\r
+       public void clear() { setChord(null); }\r
+       /**\r
+        * コード(和音)を設定します。\r
+        * 表示中でない場合、指定のコードに関係なく null が設定されます。\r
+        *\r
+        * @param chord コード(クリアする場合は null)\r
+        */\r
+       public void setChord(Music.Chord chord) {\r
+               if( ! isVisible() ) chord = null;\r
+               titleLabel.setChord(chord);\r
+               diagramDisplay.setChord(chord);\r
+       }\r
+       /**\r
+        * 対象楽器を切り替えます。\r
+        * @param instrument 対象楽器\r
+        * @throws NullPointerException 対象楽器がnullの場合\r
+        */\r
+       public void setTargetInstrument(TargetInstrument instrument) {\r
+               instButtons.get(Objects.requireNonNull(instrument)).doClick();\r
+       }\r
+}\r
+\r
+/**\r
+ * カポ選択ビュー\r
+ */\r
+class CapoSelecterView extends JPanel implements ItemListener {\r
+       /**\r
+        * カポON/OFFチェックボックス\r
+        */\r
+       public JCheckBox checkbox = new JCheckBox("Capo") {\r
+               {\r
+                       setOpaque(false);\r
+               }\r
+       };\r
+       /**\r
+        * カポ位置選択コンボボックス\r
+        */\r
+       public JComboBox<Integer> valueSelecter = new JComboBox<Integer>() {\r
+               {\r
+                       setMaximumRowCount(12);\r
+                       setVisible(false);\r
+               }\r
+       };\r
+       /**\r
+        * カポ選択ビューを構築します。\r
+        */\r
+       public CapoSelecterView() {\r
+               checkbox.addItemListener(this);\r
+               setLayout(new BoxLayout(this, BoxLayout.X_AXIS));\r
+               add(checkbox);\r
+               add(valueSelecter);\r
+       }\r
+       /**\r
+        * 指定されたデータモデルを操作するカポ選択ビューを構築します。\r
+        * @param model データモデル\r
+        */\r
+       public CapoSelecterView(ComboBoxModel<Integer> model) {\r
+               this();\r
+               valueSelecter.setModel(model);\r
+       }\r
+       @Override\r
+       public void itemStateChanged(ItemEvent e) {\r
+               valueSelecter.setVisible(checkbox.isSelected());\r
+       }\r
+       /**\r
+        * カポ位置を返します。\r
+        * @return カポ位置\r
+        */\r
+       public int getCapo() {\r
+               return checkbox.isSelected() ? valueSelecter.getSelectedIndex()+1 : 0;\r
+       }\r
+}\r
+\r
+/**\r
+ * コードダイアグラム表示部\r
+ */\r
+class ChordDiagramDisplay extends JComponent\r
+       implements MouseListener, MouseMotionListener {\r
+       /**\r
+        * 可視フレット数\r
+        */\r
+       public static final int VISIBLE_FRETS = 4;\r
+       /**\r
+        * 最大フレット数\r
+        */\r
+       public static final int MAX_FRETS = 16;\r
+       /**\r
+        * 弦の最大本数\r
+        */\r
+       public static final int MAX_STRINGS = 6;\r
+       /**\r
+        * 左マージン\r
+        */\r
+       public static final int LEFT_MARGIN_WIDTH = 10;\r
+       /**\r
+        * 右マージン\r
+        */\r
+       public static final int RIGHT_MARGIN_WIDTH = 10;\r
+       /**\r
+        * 上マージン\r
+        */\r
+       public static final int UPPER_MARGIN_WIDTH = 5;\r
+       /**\r
+        * 下マージン\r
+        */\r
+       public static final int LOWER_MARGIN_WIDTH = 5;\r
+       /**\r
+        * フレット方向の横スクロールバーで使用する境界つき値範囲\r
+        */\r
+       DefaultBoundedRangeModel fretViewIndexModel\r
+               = new DefaultBoundedRangeModel( 0, VISIBLE_FRETS, 0, MAX_FRETS );\r
+       /**\r
+        * チューニング対象楽器\r
+        */\r
+       private ChordDiagram.TargetInstrument targetInstrument;\r
+       /**\r
+        * 開放弦チューニング音階(ずらしあり)\r
+        */\r
+       private int[] notesWhenOpen;\r
+       /**\r
+        * コードの押さえ方のバリエーション\r
+        */\r
+       ChordVariations chordVariations = new ChordVariations();\r
+       /**\r
+        * チューニングボタンの配列\r
+        */\r
+       private TuningButton tuningButtons[];\r
+       /**\r
+        * チューニングボタン\r
+        */\r
+       private class TuningButton extends Rectangle {\r
+               boolean isMouseEntered = false;\r
+               int stringIndex;\r
+               public TuningButton(int stringIndex) {\r
+                       super(\r
+                               LEFT_MARGIN_WIDTH,\r
+                               UPPER_MARGIN_WIDTH + stringIndex * stringDistance,\r
+                               CHAR_WIDTH,\r
+                               stringDistance\r
+                       );\r
+                       this.stringIndex = stringIndex;\r
+               }\r
+       }\r
+       /**\r
+        * 押さえる場所\r
+        */\r
+       private class PressingPoint {\r
+               /**\r
+                * 弦インデックス(0始まり)\r
+                */\r
+               int stringIndex;\r
+               /**\r
+                * フレットインデックス(0が開放弦、-1が弾かない弦)\r
+                */\r
+               int fretIndex;\r
+               /**\r
+                * コード構成音インデックス(0でルート音)\r
+                */\r
+               int chordNoteIndex;\r
+               /**\r
+                * 押さえる場所を示す矩形領域\r
+                */\r
+               Rectangle rect = null;\r
+               /**\r
+                * マウスカーソルが入ったらtrue\r
+                */\r
+               boolean isMouseEntered = false;\r
+               /**\r
+                * 指定した弦を弾かないことを表す {@link PressingPoint} を構築します。\r
+                * @param stringIndex 弦インデックス\r
+                */\r
+               public PressingPoint(int stringIndex) {\r
+                       this(-1,-1,stringIndex);\r
+               }\r
+               /**\r
+                * 指定した弦、フレットを押さえると\r
+                * 指定されたコード構成音が鳴ることを表す {@link PressingPoint} を構築します。\r
+                * @param fretIndex フレットインデックス\r
+                * @param chordNoteIndex コード構成音インデックス\r
+                * @param stringIndex 弦インデックス\r
+                */\r
+               public PressingPoint(int fretIndex, int chordNoteIndex, int stringIndex) {\r
+                       rect = new Rectangle(\r
+                               gridRect.x + (\r
+                                       fretIndex<1 ?\r
+                                               -(pointSize + 3) :\r
+                                               (fretIndex * fretDistance - pointSize/2 - fretDistance/2)\r
+                               ),\r
+                               gridRect.y - pointSize/2 + stringIndex * stringDistance,\r
+                               pointSize,\r
+                               pointSize\r
+                       );\r
+                       this.fretIndex = fretIndex;\r
+                       this.chordNoteIndex = chordNoteIndex;\r
+                       this.stringIndex = stringIndex;\r
+               }\r
+       }\r
+       /**\r
+        * 押さえる場所リスト(配列要素として使えるようにするための空の継承クラス)\r
+        */\r
+       class PressingPointList extends LinkedList<PressingPoint> {\r
+       }\r
+       /**\r
+        * コードの押さえ方のバリエーション\r
+        */\r
+       class ChordVariations extends LinkedList<PressingPoint[]> {\r
+               /**\r
+                * 対象コード\r
+                */\r
+               public Music.Chord chord = null;\r
+               /**\r
+                * 省略しないコード構成音が全部揃ったかをビットで確認するための値\r
+                */\r
+               private int checkBitsAllOn = 0;\r
+               /**\r
+                * 五度の構成音を省略できるコードのときtrue\r
+                */\r
+               private boolean fifthOmittable = false;\r
+               /**\r
+                * ルート音を省略できるコードのときtrue\r
+                */\r
+               private boolean rootOmittable = false;\r
+               /**\r
+                * バリエーションインデックスの範囲を表すモデル\r
+                */\r
+               public DefaultBoundedRangeModel indexModel = new DefaultBoundedRangeModel(0,0,0,0);\r
+               /**\r
+                * コード(和音)を設定します。\r
+                *\r
+                * 設定すると、そのコードの押さえ方がくまなく探索され、\r
+                * バリエーションが再構築されます。\r
+                *\r
+                * @param chord コード\r
+                */\r
+               public void setChord(Music.Chord chord) {\r
+                       clear();\r
+                       if( (this.chord = chord) == null ) {\r
+                               possiblePressingPoints = null;\r
+                               indexModel.setRangeProperties(0,0,0,0,false);\r
+                               return;\r
+                       }\r
+                       int chordNoteCount = chord.numberOfNotes();\r
+                       rootOmittable = ( chordNoteCount == 5 );\r
+                       fifthOmittable = ( chord.symbolSuffix().equals("7") || rootOmittable );\r
+                       checkBitsAllOn = (1 << chordNoteCount) - 1;\r
+                       possiblePressingPoints = new PressingPointList[notesWhenOpen.length];\r
+                       for( int stringIndex=0; stringIndex<possiblePressingPoints.length; stringIndex++ ) {\r
+                               possiblePressingPoints[stringIndex] = new PressingPointList();\r
+                               for(\r
+                                       int fretIndex=0;\r
+                                       fretIndex <= fretViewIndexModel.getValue() + fretViewIndexModel.getExtent();\r
+                                       fretIndex++\r
+                               ) {\r
+                                       if( fretIndex == 0 || fretIndex > fretViewIndexModel.getValue() ) {\r
+                                               int chordNoteIndex = chord.indexOf(\r
+                                                       notesWhenOpen[stringIndex]+fretIndex\r
+                                               );\r
+                                               if( chordNoteIndex >= 0 ) {\r
+                                                       possiblePressingPoints[stringIndex].add(\r
+                                                               new PressingPoint(fretIndex,chordNoteIndex,stringIndex)\r
+                                                       );\r
+                                               }\r
+                                       }\r
+                               }\r
+                               // 'x'-marking string\r
+                               possiblePressingPoints[stringIndex].add(new PressingPoint(stringIndex));\r
+                       }\r
+                       validatingPoints = new PressingPoint[notesWhenOpen.length];\r
+                       scanFret(0);\r
+                       indexModel.setRangeProperties(-1,1,-1,size(),false);\r
+               }\r
+               /**\r
+                * 押さえる可能性のある場所のリスト\r
+                */\r
+               private PressingPointList possiblePressingPoints[] = null;\r
+               /**\r
+                * 押さえる可能性のある場所のリストを返します。\r
+                * @return 押さえる可能性のある場所のリスト\r
+                */\r
+               public PressingPointList[] getPossiblePressingPoints() {\r
+                       return possiblePressingPoints;\r
+               }\r
+               /**\r
+                * 検証対象の押さえ方\r
+                */\r
+               private PressingPoint validatingPoints[] = null;\r
+               /**\r
+                * 引数で指定された弦のフレットをスキャンします。\r
+                *\r
+                * <p>指定された弦について、\r
+                * コード構成音のどれか一つを鳴らすことのできる\r
+                * フレット位置を順にスキャンし、\r
+                * 新しい押さえ方にフレット位置を記録したうえで、\r
+                * 次の弦について再帰呼び出しを行います。\r
+                * </p>\r
+                * <p>最後の弦まで達すると(再起呼び出しツリーの葉)、\r
+                * その押さえ方でコード構成音が十分に揃うかどうか検証されます。\r
+                * 検証結果がOKだった場合、押さえ方がバリエーションリストに追加されます。\r
+                * </p>\r
+                *\r
+                * @param stringIndex 弦インデックス\r
+                */\r
+               private void scanFret(int stringIndex) {\r
+                       int endOfStringIndex = validatingPoints.length - 1;\r
+                       for( PressingPoint pp : possiblePressingPoints[stringIndex] ) {\r
+                               validatingPoints[stringIndex] = pp;\r
+                               if( stringIndex < endOfStringIndex ) {\r
+                                       scanFret( stringIndex + 1 );\r
+                                       continue;\r
+                               }\r
+                               if( hasValidNewVariation() ) {\r
+                                       add(validatingPoints.clone());\r
+                               }\r
+                       }\r
+               }\r
+               /**\r
+                * 新しい押さえ方のバリエーションが、\r
+                * そのコードを鳴らすのに十分であるか検証します。\r
+                *\r
+                * @return 省略しないコード構成音が全部揃っていたらtrue、\r
+                * 一つでも欠けていたらfalse\r
+                */\r
+               private boolean hasValidNewVariation() {\r
+                       int checkBits = 0;\r
+                       int iocn;\r
+                       for( PressingPoint pp : validatingPoints )\r
+                               if( (iocn = pp.chordNoteIndex) >= 0 ) checkBits |= 1 << iocn;\r
+                       return ( checkBits == checkBitsAllOn\r
+                               || checkBits == checkBitsAllOn -4 && fifthOmittable\r
+                               || (checkBits & (checkBitsAllOn -1-4)) == (checkBitsAllOn -1-4)\r
+                               && (checkBits & (1+4)) != 0 && rootOmittable\r
+                       );\r
+               }\r
+               /**\r
+                * バリエーションインデックスの説明を返します。\r
+                *\r
+                * <p>(インデックス値 / バリエーションの個数) のような説明です。\r
+                * インデックス値が未選択(-1)の場合、\r
+                * バリエーションの個数のみの説明を返します。\r
+                * </p>\r
+                * @return バリエーションインデックスの説明\r
+                */\r
+               public String getIndexDescription() {\r
+                       if( chord == null )\r
+                               return null;\r
+                       int val = indexModel.getValue();\r
+                       int max = indexModel.getMaximum();\r
+                       if( val < 0 ) { // 未選択時\r
+                               switch(max) {\r
+                               case 0: return "No variation found";\r
+                               case 1: return "1 variation found";\r
+                               default: return max + " variations found";\r
+                               }\r
+                       }\r
+                       return "Variation: " + (val+1) + " / " + max ;\r
+               }\r
+       }\r
+\r
+       public ChordDiagramDisplay(ChordDiagram.TargetInstrument inst) {\r
+               addMouseListener(this);\r
+               addMouseMotionListener(this);\r
+               addComponentListener(\r
+                       new ComponentAdapter() {\r
+                               @Override\r
+                               public void componentResized(ComponentEvent e) {\r
+                                       tune();\r
+                               }\r
+                       }\r
+               );\r
+               chordVariations.indexModel.addChangeListener(\r
+                       new ChangeListener() {\r
+                               @Override\r
+                               public void stateChanged(ChangeEvent e) {\r
+                                       repaint();\r
+                               }\r
+                       }\r
+               );\r
+               fretViewIndexModel.addChangeListener(\r
+                       new ChangeListener() {\r
+                               @Override\r
+                               public void stateChanged(ChangeEvent e) {\r
+                                       setChord(); // To reconstruct chord variations\r
+                               }\r
+                       }\r
+               );\r
+               setMinimumSize(new Dimension(100,70));\r
+               tune(inst);\r
+       }\r
+       @Override\r
+       public void paint(Graphics g) {\r
+               Graphics2D g2 = (Graphics2D) g;\r
+               Dimension d = getSize();\r
+               Color fret_color = Color.gray; // getBackground().darker();\r
+               FontMetrics fm = g2.getFontMetrics();\r
+               //\r
+               // Copy background color\r
+               g2.setBackground(getBackground());\r
+               g2.clearRect(0, 0, d.width, d.height);\r
+               //\r
+               // Draw frets and its numbers\r
+               //\r
+               for( int i=1; i<=VISIBLE_FRETS; i++ ) {\r
+                       g2.setColor(fret_color);\r
+                       int fret_x = gridRect.x + (gridRect.width - 2) * i / VISIBLE_FRETS;\r
+                       g2.drawLine(\r
+                               fret_x, gridRect.y,\r
+                               fret_x, gridRect.y + stringDistance * (notesWhenOpen.length - 1)\r
+                       );\r
+                       g2.setColor(getForeground());\r
+                       String s = String.valueOf( i + fretViewIndexModel.getValue() );\r
+                       g2.drawString(\r
+                               s,\r
+                               gridRect.x\r
+                               + fretDistance/2 - fm.stringWidth(s)/2\r
+                               + gridRect.width * (i-1) / VISIBLE_FRETS,\r
+                               gridRect.y\r
+                               + stringDistance/2 + fm.getHeight()\r
+                               + stringDistance * (notesWhenOpen.length - 1) - 1\r
+                       );\r
+               }\r
+               //\r
+               // Draw strings and open notes\r
+               for( int i=0; i<notesWhenOpen.length; i++ ) {\r
+                       int string_y = gridRect.y + gridRect.height * i / (MAX_STRINGS - 1);\r
+                       g2.setColor(fret_color);\r
+                       g2.drawLine(\r
+                               gridRect.x,\r
+                               string_y,\r
+                               gridRect.x + (gridRect.width - 2),\r
+                               string_y\r
+                       );\r
+                       if( notesWhenOpen[i] != targetInstrument.getOpenNotes().get(i) ) {\r
+                               g2.setColor(Color.yellow);\r
+                               g2.fill(tuningButtons[i]);\r
+                       }\r
+                       g2.setColor(getForeground());\r
+                       g2.drawString(\r
+                               Music.NoteSymbol.noteNumberToSymbol(notesWhenOpen[i], 2),\r
+                               LEFT_MARGIN_WIDTH,\r
+                               string_y + (fm.getHeight() - fm.getDescent())/2\r
+                       );\r
+                       g2.setColor(fret_color);\r
+                       if( tuningButtons[i].isMouseEntered ) {\r
+                               g2.draw(tuningButtons[i]);\r
+                       }\r
+               }\r
+               //\r
+               // Draw left-end of frets\r
+               if( fretViewIndexModel.getValue() == 0 ) {\r
+                       g2.setColor(getForeground());\r
+                       g2.fillRect(\r
+                               gridRect.x - 1,\r
+                               gridRect.y,\r
+                               3,\r
+                               stringDistance * (notesWhenOpen.length - 1) + 1\r
+                       );\r
+               }\r
+               else {\r
+                       g2.setColor(fret_color);\r
+                       g2.drawLine(\r
+                               gridRect.x,\r
+                               gridRect.y,\r
+                               gridRect.x,\r
+                               gridRect.y + stringDistance * (notesWhenOpen.length - 1)\r
+                       );\r
+               }\r
+               //\r
+               // Draw indicators\r
+               if( chordVariations.chord == null ) {\r
+                       return;\r
+               }\r
+               PressingPoint variation[] = null;\r
+               int ppIndex = chordVariations.indexModel.getValue();\r
+               if( ppIndex >= 0 ) {\r
+                       variation = chordVariations.get(ppIndex);\r
+                       for( PressingPoint pp : variation ) drawIndicator(g2, pp, false);\r
+               }\r
+               PressingPointList possiblePressingPoints[] = chordVariations.getPossiblePressingPoints();\r
+               if( possiblePressingPoints != null ) {\r
+                       for( PressingPointList pps : possiblePressingPoints ) {\r
+                               for( PressingPoint pp : pps ) {\r
+                                       if( pp.isMouseEntered ) {\r
+                                               drawIndicator( g2, pp, false );\r
+                                               if( variation != null ) {\r
+                                                       return;\r
+                                               }\r
+                                       }\r
+                                       else if( variation == null ) {\r
+                                               drawIndicator( g2, pp, true );\r
+                                       }\r
+                               }\r
+                       }\r
+               }\r
+       }\r
+       private void drawIndicator(\r
+               Graphics2D g2, PressingPoint pp, boolean drawAllPoints\r
+       ) {\r
+               Rectangle r;\r
+               int i_chord = pp.chordNoteIndex;\r
+               g2.setColor(\r
+                       i_chord < 0 ? getForeground() : Music.Chord.NOTE_INDEX_COLORS[i_chord]\r
+               );\r
+               if( (r = pp.rect) == null ) {\r
+                       return;\r
+               }\r
+               int fretPoint = pp.fretIndex;\r
+               if( fretPoint < 0 ) {\r
+                       if( ! drawAllPoints ) {\r
+                               // Put 'x' mark\r
+                               g2.drawLine(\r
+                                       r.x + 1,\r
+                                       r.y + 1,\r
+                                       r.x + r.width - 1,\r
+                                       r.y + r.height - 1\r
+                               );\r
+                               g2.drawLine(\r
+                                       r.x + 1,\r
+                                       r.y + r.height - 1,\r
+                                       r.x + r.width - 1,\r
+                                       r.y + 1\r
+                               );\r
+                       }\r
+               }\r
+               else if( fretPoint == 0 ) {\r
+                       // Put 'o' mark\r
+                       g2.drawOval( r.x, r.y, r.width, r.height );\r
+               }\r
+               else { // Fret-pressing\r
+                       int x = r.x - fretViewIndexModel.getValue() * fretDistance ;\r
+                       if( drawAllPoints ) {\r
+                               g2.drawOval( x, r.y, r.width, r.height );\r
+                       }\r
+                       else {\r
+                               g2.fillOval( x, r.y, r.width, r.height );\r
+                       }\r
+               }\r
+       }\r
+       @Override\r
+       public void mousePressed(MouseEvent e) {\r
+               Point point = e.getPoint();\r
+               PressingPointList possiblePressingPoints[] = chordVariations.getPossiblePressingPoints();\r
+               if( possiblePressingPoints != null ) {\r
+                       for( PressingPointList pps : possiblePressingPoints ) {\r
+                               for( PressingPoint pp : pps ) {\r
+                                       boolean hit;\r
+                                       Rectangle rect = pp.rect;\r
+                                       if( pp.fretIndex > 0 ) {\r
+                                               int xOffset = -fretViewIndexModel.getValue()*fretDistance;\r
+                                               rect.translate( xOffset, 0 );\r
+                                               hit = rect.contains(point);\r
+                                               rect.translate( -xOffset, 0 );\r
+                                       }\r
+                                       else hit = rect.contains(point);\r
+                                       if( ! hit )\r
+                                               continue;\r
+                                       int variationIndex = 0;\r
+                                       for( PressingPoint[] variation : chordVariations ) {\r
+                                               if( variation[pp.stringIndex].fretIndex != pp.fretIndex ) {\r
+                                                       variationIndex++;\r
+                                                       continue;\r
+                                               }\r
+                                               chordVariations.indexModel.setValue(variationIndex);\r
+                                               return;\r
+                                       }\r
+                               }\r
+                       }\r
+               }\r
+               for( TuningButton button : tuningButtons ) {\r
+                       if( ! button.contains(point) )\r
+                               continue;\r
+                       int note = notesWhenOpen[button.stringIndex];\r
+                       note += (e.getButton()==MouseEvent.BUTTON3 ? 11 : 1);\r
+                       notesWhenOpen[button.stringIndex] = Music.mod12(note);\r
+                       setChord();\r
+                       return;\r
+               }\r
+       }\r
+       @Override\r
+       public void mouseReleased(MouseEvent e) { }\r
+       @Override\r
+       public void mouseEntered(MouseEvent e) {\r
+               mouseMoved(e);\r
+       }\r
+       @Override\r
+       public void mouseExited(MouseEvent e) {\r
+               mouseMoved(e);\r
+       }\r
+       @Override\r
+       public void mouseClicked(MouseEvent e) { }\r
+       @Override\r
+       public void mouseDragged(MouseEvent e) {\r
+       }\r
+       @Override\r
+       public void mouseMoved(MouseEvent e) {\r
+               Point point = e.getPoint();\r
+               boolean changed = false;\r
+               boolean hit;\r
+               for( TuningButton button : tuningButtons ) {\r
+                       hit = button.contains(point);\r
+                       if ( button.isMouseEntered != hit ) changed = true;\r
+                       button.isMouseEntered = hit;\r
+               }\r
+               PressingPointList possible_points[] = chordVariations.getPossiblePressingPoints();\r
+               if( possible_points != null ) {\r
+                       for( PressingPointList pps : possible_points ) {\r
+                               for( PressingPoint pp : pps ) {\r
+                                       if( pp.fretIndex > 0 ) {\r
+                                               int xOffset = -fretViewIndexModel.getValue()*fretDistance;\r
+                                               pp.rect.translate( xOffset, 0 );\r
+                                               hit = pp.rect.contains(point);\r
+                                               pp.rect.translate( -xOffset, 0 );\r
+                                       }\r
+                                       else hit = pp.rect.contains(point);\r
+                                       if ( pp.isMouseEntered != hit ) changed = true;\r
+                                       pp.isMouseEntered = hit;\r
+                               }\r
+                       }\r
+               }\r
+               if( changed ) repaint();\r
+       }\r
+       private static final int CHAR_WIDTH = 8; // FontMetrics.stringWidth("C#");\r
+       private static final int CHAR_HEIGHT = 16; // FontMetrics.getHeight();\r
+       /**\r
+        * 弦間距離(リサイズによって変化する)\r
+        */\r
+       private int stringDistance;\r
+       /**\r
+        * フレット間距離(リサイズによって変化する)\r
+        */\r
+       private int fretDistance;\r
+       /**\r
+        * 押さえるポイントの直径(リサイズによって変化する)\r
+        */\r
+       private int pointSize;\r
+       /**\r
+        * 可視部分の矩形(リサイズによって変化する)\r
+        */\r
+       private Rectangle gridRect;\r
+       /**\r
+        * 指定された楽器用にチューニングをリセットします。\r
+        * @param inst 対象楽器\r
+        */\r
+       public void tune(ChordDiagram.TargetInstrument inst) {\r
+               notesWhenOpen = (targetInstrument = inst).createTunableOpenNotes();\r
+               tune();\r
+       }\r
+       /**\r
+        * チューニングを行います。\r
+        */\r
+       public void tune() {\r
+               Dimension sz = getSize();\r
+               stringDistance = (\r
+                       sz.height + 1\r
+                       - UPPER_MARGIN_WIDTH - LOWER_MARGIN_WIDTH\r
+                       - CHAR_HEIGHT\r
+               ) / MAX_STRINGS;\r
+\r
+               pointSize = stringDistance * 4 / 5;\r
+\r
+               fretDistance = (\r
+                       sz.width + 1 - (\r
+                               LEFT_MARGIN_WIDTH + RIGHT_MARGIN_WIDTH\r
+                               + CHAR_WIDTH + pointSize + 8\r
+                       )\r
+               ) / VISIBLE_FRETS;\r
+\r
+               gridRect = new Rectangle(\r
+                       LEFT_MARGIN_WIDTH + pointSize + CHAR_WIDTH + 8,\r
+                       UPPER_MARGIN_WIDTH + stringDistance / 2,\r
+                       fretDistance * VISIBLE_FRETS,\r
+                       stringDistance * (MAX_STRINGS - 1)\r
+               );\r
+\r
+               tuningButtons = new TuningButton[targetInstrument.getOpenNotes().size()];\r
+               for( int i=0; i<tuningButtons.length; i++ ) {\r
+                       tuningButtons[i] = new TuningButton(i);\r
+               }\r
+               setChord();\r
+       }\r
+       /**\r
+        * コード(和音)を再設定します。\r
+        */\r
+       public void setChord() {\r
+               setChord(chordVariations.chord);\r
+       }\r
+       /**\r
+        * コード(和音)を設定します。\r
+        * @param chord コード\r
+        */\r
+       public void setChord(Music.Chord chord) {\r
+               chordVariations.setChord(chord);\r
+               repaint();\r
+       }\r
+}\r
diff --git a/src/ChordHelperApplet.java b/src/ChordHelperApplet.java
new file mode 100644 (file)
index 0000000..fe01ab3
--- /dev/null
@@ -0,0 +1,1168 @@
+import java.awt.Color;\r
+import java.awt.Component;\r
+import java.awt.Desktop;\r
+import java.awt.Dimension;\r
+import java.awt.Graphics;\r
+import java.awt.Image;\r
+import java.awt.Insets;\r
+import java.awt.dnd.DnDConstants;\r
+import java.awt.dnd.DropTarget;\r
+import java.awt.event.ActionEvent;\r
+import java.awt.event.ActionListener;\r
+import java.awt.event.InputEvent;\r
+import java.awt.event.ItemEvent;\r
+import java.awt.event.ItemListener;\r
+import java.awt.event.MouseAdapter;\r
+import java.awt.event.MouseEvent;\r
+import java.awt.event.MouseListener;\r
+import java.io.IOException;\r
+import java.io.UnsupportedEncodingException;\r
+import java.net.URI;\r
+import java.net.URISyntaxException;\r
+import java.net.URL;\r
+import java.util.Arrays;\r
+import java.util.Vector;\r
+\r
+import javax.sound.midi.MetaEventListener;\r
+import javax.sound.midi.MetaMessage;\r
+import javax.sound.midi.Sequencer;\r
+import javax.swing.Box;\r
+import javax.swing.BoxLayout;\r
+import javax.swing.ButtonGroup;\r
+import javax.swing.ImageIcon;\r
+import javax.swing.JApplet;\r
+import javax.swing.JButton;\r
+import javax.swing.JCheckBoxMenuItem;\r
+import javax.swing.JComponent;\r
+import javax.swing.JEditorPane;\r
+import javax.swing.JLabel;\r
+import javax.swing.JOptionPane;\r
+import javax.swing.JPanel;\r
+import javax.swing.JPopupMenu;\r
+import javax.swing.JRadioButtonMenuItem;\r
+import javax.swing.JSlider;\r
+import javax.swing.JSplitPane;\r
+import javax.swing.JTextField;\r
+import javax.swing.JToggleButton;\r
+import javax.swing.border.Border;\r
+import javax.swing.event.ChangeEvent;\r
+import javax.swing.event.ChangeListener;\r
+import javax.swing.event.HyperlinkEvent;\r
+import javax.swing.event.HyperlinkListener;\r
+import javax.swing.event.PopupMenuEvent;\r
+import javax.swing.event.PopupMenuListener;\r
+\r
+/**\r
+ * MIDI Chord Helper - Circle-of-fifth oriented chord pad\r
+ * (アプレットクラス)\r
+ *\r
+ *     @auther\r
+ *             Copyright (C) 2004-2013 @きよし - Akiyoshi Kamide\r
+ *             http://www.yk.rim.or.jp/~kamide/music/chordhelper/\r
+ */\r
+public class ChordHelperApplet extends JApplet implements MetaEventListener {\r
+       /////////////////////////////////////////////////////////////////////\r
+       //\r
+       // JavaScript などからの呼び出しインターフェース\r
+       //\r
+       /////////////////////////////////////////////////////////////////////\r
+       /**\r
+        * 未保存の修正済み MIDI ファイルがあるかどうか調べます。\r
+        * @return 未保存の修正済み MIDI ファイルがあれば true\r
+        */\r
+       public boolean isModified() {\r
+               return editorDialog.isModified();\r
+       }\r
+       /**\r
+        * 指定された小節数の曲を、乱数で自動作曲してプレイリストへ追加します。\r
+        * @param measureLength 小節数\r
+        * @return 追加先のインデックス値(0から始まる)。追加できなかったときは -1\r
+        */\r
+       public int addRandomSongToPlaylist(int measureLength) {\r
+               editorDialog.new_seq_dialog.setRandomChordProgression(measureLength);\r
+               return editorDialog.addSequence();\r
+       }\r
+       /**\r
+        * URLで指定されたMIDIファイルをプレイリストへ追加します。\r
+        *\r
+        * <p>URL の最後の / より後ろの部分がファイル名として取り込まれます。\r
+        * 指定できる MIDI ファイルには、param タグの midi_file パラメータと同様の制限があります。\r
+        * </p>\r
+        * @param midiFileUrl 追加するMIDIファイルのURL\r
+        * @return 追加先のインデックス値(0から始まる)。追加できなかったときは -1\r
+        */\r
+       public int addToPlaylist(String midiFileUrl) {\r
+               return editorDialog.addSequenceFromURL(midiFileUrl);\r
+       }\r
+       /**\r
+        * Base64 エンコードされた MIDI ファイルをプレイリストへ追加します。\r
+        *\r
+        * @param base64EncodedText Base64エンコードされたMIDIファイル\r
+        * @return 追加先のインデックス値(0から始まる)。追加できなかったときは -1\r
+        */\r
+       public int addToPlaylistBase64(String base64EncodedText) {\r
+               return addToPlaylistBase64(base64EncodedText, null);\r
+       }\r
+       /**\r
+        * ファイル名を指定して、\r
+        * Base64エンコードされたMIDIファイルをプレイリストへ追加します。\r
+        *\r
+        * @param base64EncodedText Base64エンコードされたMIDIファイル\r
+        * @param filename ディレクトリ名を除いたファイル名\r
+        * @return 追加先のインデックス値(0から始まる)。追加できなかったときは -1\r
+        */\r
+       public int addToPlaylistBase64(String base64EncodedText, String filename) {\r
+               return editorDialog.addSequenceFromBase64Text(\r
+                       base64EncodedText, filename\r
+               );\r
+       }\r
+       /**\r
+        * プレイリスト上で現在選択されているMIDIシーケンスを、\r
+        * シーケンサへロードして再生します。\r
+        */\r
+       public void play() { editorDialog.loadAndPlay(); }\r
+       /**\r
+        * 指定されたインデックス値が示すプレイリスト上のMIDIシーケンスを、\r
+        * シーケンサへロードして再生します。\r
+        * @param index インデックス値(0から始まる)\r
+        */\r
+       public void play(int index) { editorDialog.loadAndPlay(index); }\r
+       /**\r
+        * シーケンサが実行中かどうかを返します。\r
+        * {@link Sequencer#isRunning()} の戻り値をそのまま返します。\r
+        *\r
+        * @return 実行中のときtrue\r
+        */\r
+       public boolean isRunning() {\r
+               return deviceManager.getSequencer().isRunning();\r
+       }\r
+       /**\r
+        * シーケンサが再生中かどうかを返します。\r
+        * @return 再生中のときtrue\r
+        */\r
+       public boolean isPlaying() { return isRunning(); }\r
+       /**\r
+        * 現在シーケンサにロードされているMIDIデータを\r
+        * Base64テキストに変換した結果を返します。\r
+        * @return MIDIデータをBase64テキストに変換した結果\r
+        */\r
+       public String getMidiDataBase64() {\r
+               return editorDialog.getMIDIdataBase64();\r
+       }\r
+       /**\r
+        * 現在シーケンサにロードされているMIDIファイルのファイル名を返します。\r
+        * @return MIDIファイル名(設定されていないときは空文字列)\r
+        */\r
+       public String getMidiFilename() {\r
+               MidiSequenceModel seq_model = deviceManager.timeRangeModel.getSequenceModel();\r
+               if( seq_model == null ) return null;\r
+               String fn = seq_model.getFilename();\r
+               return fn == null ? "" : fn ;\r
+       }\r
+       /**\r
+        * オクターブ位置を設定します。\r
+        * @param octavePosition オクターブ位置(デフォルト:4)\r
+        */\r
+       public void setOctavePosition(int octavePosition) {\r
+               keyboardPanel.keyboardCenterPanel.keyboard.octaveRangeModel.setValue(octavePosition);\r
+       }\r
+       /**\r
+        * 操作対象のMIDIチャンネルを変更します。\r
+        * @param ch チャンネル番号 - 1(チャンネル1のとき0、デフォルトは0)\r
+        */\r
+       public void setChannel(int ch) {\r
+               keyboardPanel.keyboardCenterPanel.keyboard.midiChComboboxModel.setSelectedChannel(ch);\r
+       }\r
+       /**\r
+        * 操作対象のMIDIチャンネルを返します。\r
+        * @return 操作対象のMIDIチャンネル\r
+        */\r
+       public int getChannel() {\r
+               return keyboardPanel.keyboardCenterPanel.keyboard.midiChComboboxModel.getSelectedChannel();\r
+       }\r
+       /**\r
+        * 操作対象のMIDIチャンネルに対してプログラム(音色)を設定します。\r
+        * @param program 音色(0~127:General MIDI に基づく)\r
+        */\r
+       public void programChange(int program) {\r
+               keyboardPanel.keyboardCenterPanel.keyboard.getSelectedChannel().programChange(program);\r
+       }\r
+       /**\r
+        * 操作対象のMIDIチャンネルに対してプログラム(音色)を設定します。\r
+        * 内部的には {@link #programChange(int)} を呼び出しているだけです。\r
+        * @param program 音色(0~127:General MIDI に基づく)\r
+        */\r
+       public void setProgram(int program) { programChange(program); }\r
+       /**\r
+        * 自動転回モードを変更します。初期値は true です。\r
+        * @param isAuto true:自動転回を行う false:自動転回を行わない\r
+        */\r
+       public void setAutoInversion(boolean isAuto) {\r
+               inversionOmissionButton.setAutoInversion(isAuto);\r
+       }\r
+       /**\r
+        * 省略したい構成音を指定します。\r
+        * @param index\r
+        * <ul>\r
+        * <li>-1:省略しない(デフォルト)</li>\r
+        * <li>0:ルート音を省略</li>\r
+        * <li>1:三度を省略</li>\r
+        * <li>2:五度を省略</li>\r
+        * </ul>\r
+        */\r
+       public void setOmissionNoteIndex(int index) {\r
+               inversionOmissionButton.setOmissionNoteIndex(index);\r
+       }\r
+       /**\r
+        * コードダイアグラムの表示・非表示を切り替えます。\r
+        * @param isVisible 表示するときtrue\r
+        */\r
+       public void setChordDiagramVisible(boolean isVisible) {\r
+               keyboard_split_pane.resetToPreferredSizes();\r
+               if( ! isVisible )\r
+                       keyboard_split_pane.setDividerLocation((double)1.0);\r
+       }\r
+       /**\r
+        * コードダイヤグラムをギターモードに変更します。\r
+        * 初期状態ではウクレレモードになっています。\r
+        */\r
+       public void setChordDiagramForGuitar() {\r
+               chordDiagram.setTargetInstrument(ChordDiagram.TargetInstrument.Guitar);\r
+       }\r
+       /**\r
+        * ダークモード(暗い表示)と明るい表示とを切り替えます。\r
+        * @param isDark ダークモードのときtrue、明るい表示のときfalse(デフォルト)\r
+        */\r
+       public void setDarkMode(boolean isDark) {\r
+               dark_mode_toggle_button.setSelected(isDark);\r
+       }\r
+       /**\r
+        * バージョン情報\r
+        */\r
+       public static class VersionInfo {\r
+               public static final String      NAME = "MIDI Chord Helper";\r
+               public static final String      VERSION = "Ver.20131028.1";\r
+               public static final String      COPYRIGHT = "Copyright (C) 2004-2013";\r
+               public static final String      AUTHER = "@きよし - Akiyoshi Kamide";\r
+               public static final String      URL = "http://www.yk.rim.or.jp/~kamide/music/chordhelper/";\r
+               public static String getInfo() {\r
+                       return NAME + " " + VERSION + " " + COPYRIGHT + " " + AUTHER + " " + URL;\r
+               }\r
+       }\r
+       @Override\r
+       public String getAppletInfo() {\r
+               return VersionInfo.getInfo();\r
+       }\r
+       class AboutMessagePane extends JEditorPane {\r
+               URI uri = null;\r
+               public AboutMessagePane() { this(true); }\r
+               public AboutMessagePane(boolean link_enabled) {\r
+                       super( "text/html", "" );\r
+                       String link_string, tooltip = null;\r
+                       if( link_enabled && Desktop.isDesktopSupported() ) {\r
+                               tooltip = "Click this URL to open with your web browser - URLをクリックしてWebブラウザで開く";\r
+                               link_string =\r
+                                               "<a href=\"" + VersionInfo.URL + "\" title=\"" +\r
+                                                               tooltip + "\">" + VersionInfo.URL + "</a>" ;\r
+                       }\r
+                       else {\r
+                               link_enabled = false; link_string = VersionInfo.URL;\r
+                       }\r
+                       setText(\r
+                               "<html><center><font size=\"+1\">" + VersionInfo.NAME + "</font>  " +\r
+                                               VersionInfo.VERSION + "<br/><br/>" +\r
+                                               VersionInfo.COPYRIGHT + " " + VersionInfo.AUTHER + "<br/>" +\r
+                                               link_string + "</center></html>"\r
+                       );\r
+                       setToolTipText(tooltip);\r
+                       setOpaque(false);\r
+                       putClientProperty(JEditorPane.HONOR_DISPLAY_PROPERTIES, Boolean.TRUE);\r
+                       setEditable(false);\r
+                       //\r
+                       // メッセージ内の <a href=""> ~ </a> によるリンクを\r
+                       // 実際に機能させる(ブラウザで表示されるようにする)ための設定\r
+                       //\r
+                       if( ! link_enabled ) return;\r
+                       try {\r
+                               uri = new URI(VersionInfo.URL);\r
+                       }catch( URISyntaxException use ) {\r
+                               use.printStackTrace();\r
+                               return;\r
+                       }\r
+                       addHyperlinkListener(new HyperlinkListener() {\r
+                               public void hyperlinkUpdate(HyperlinkEvent e) {\r
+                                       if(e.getEventType()==HyperlinkEvent.EventType.ACTIVATED) {\r
+                                               try{\r
+                                                       Desktop.getDesktop().browse(uri);\r
+                                               }catch(IOException ioe) {\r
+                                                       ioe.printStackTrace();\r
+                                               }\r
+                                       }\r
+                               }\r
+                       });\r
+               }\r
+               // バージョン情報を表示\r
+               public void showMessage() {\r
+                       JOptionPane.showMessageDialog( null, this, "Version info",\r
+                               JOptionPane.INFORMATION_MESSAGE,\r
+                               imageIcon\r
+                       );\r
+               }\r
+       }\r
+       // 終了してよいか確認する\r
+       public boolean isConfirmedToExit() {\r
+               return ! isModified() || JOptionPane.showConfirmDialog(\r
+                       this,\r
+                       "MIDI file not saved, exit anyway ?\n保存されていないMIDIファイルがありますが、終了してよろしいですか?",\r
+                       VersionInfo.NAME,\r
+                       JOptionPane.YES_NO_OPTION,\r
+                       JOptionPane.WARNING_MESSAGE\r
+               ) == JOptionPane.YES_OPTION ;\r
+       }\r
+       // アプリケーションのアイコンイメージ\r
+       public ImageIcon imageIcon;\r
+       // ボタンの余白を詰めたいときは setMargin() の引数にこれを指定する\r
+       Insets  zero_insets = new Insets(0,0,0,0);\r
+       //\r
+       JPanel\r
+       keyboard_sequencer_panel,\r
+       chord_guide,\r
+       sequencer_panel,\r
+       sequencer_upper_panel,\r
+       sequencer_lower_panel,\r
+       midi_io_panel;\r
+       Color\r
+       root_pane_default_bgcolor,\r
+       lyric_display_default_bgcolor;\r
+       Border\r
+       lyric_display_default_border,\r
+       dark_mode_toggle_border;\r
+       AboutMessagePane        aboutMessagePane = new AboutMessagePane();\r
+       //\r
+       JSplitPane              main_split_pane, keyboard_split_pane;\r
+       ChordTextField  lyric_display;\r
+       MidiKeyboardPanel       keyboardPanel;\r
+       ChordMatrix             chordMatrix;\r
+       ChordButtonLabel        enterButtonLabel;\r
+       InversionAndOmissionLabel       inversionOmissionButton;\r
+       JToggleButton                   dark_mode_toggle_button;\r
+       MidiDeviceManager       deviceManager;\r
+       MidiDeviceDialog        midiConnectionDialog;\r
+       MidiEditor              editorDialog;\r
+       ChordDiagram            chordDiagram;\r
+       //\r
+       // Tempo, Time signature, Key signature\r
+       //\r
+       TempoSelecter           tempo_selecter = new TempoSelecter();\r
+       TimeSignatureSelecter   timesig_selecter = new TimeSignatureSelecter();\r
+       KeySignatureLabel       keysig_label = new KeySignatureLabel();\r
+       JLabel          song_title_label = new JLabel();\r
+       //\r
+       // あの楽器\r
+       AnoGakkiLayeredPane anoGakkiLayeredPane;\r
+       JToggleButton ano_gakki_toggle_button;\r
+\r
+       public void init() {\r
+               String imageIconPath = "images/midichordhelper.png";\r
+               URL imageIconUrl = getClass().getResource(imageIconPath);\r
+               if( imageIconUrl == null ) {\r
+                       // System.out.println("icon "+imageIconPath+" not found");\r
+                       imageIcon = null;\r
+               }\r
+               else {\r
+                       imageIcon = new ImageIcon(imageIconUrl);\r
+               }\r
+               root_pane_default_bgcolor = getContentPane().getBackground();\r
+               //\r
+               // About\r
+               JButton aboutButton = new JButton("Version info");\r
+               aboutButton.setToolTipText( VersionInfo.NAME + " " + VersionInfo.VERSION );\r
+               aboutButton.addActionListener( new ActionListener() {\r
+                       public void actionPerformed(ActionEvent e) {\r
+                               aboutMessagePane.showMessage();\r
+                       }\r
+               });\r
+               //\r
+               // Chord matrix\r
+               //\r
+               chordMatrix = new ChordMatrix();\r
+               chordMatrix.addChordMatrixListener(new ChordMatrixListener(){\r
+                       public void keySignatureChanged() {\r
+                               Music.Key capo_key = chordMatrix.getKeySignatureCapo();\r
+                               keyboardPanel.keySelecter.setKey(capo_key);\r
+                               keyboardPanel.keyboardCenterPanel.keyboard.setKeySignature(capo_key);\r
+                       }\r
+                       public void chordChanged() { chordOn(); }\r
+               });\r
+               enterButtonLabel = new ChordButtonLabel("Enter",chordMatrix);\r
+               enterButtonLabel.addMouseListener(new MouseAdapter() {\r
+                       public void mousePressed(MouseEvent e) {\r
+                               if( (e.getModifiersEx() & InputEvent.BUTTON3_DOWN_MASK) != 0 ) // RightClicked\r
+                                       chordMatrix.setSelectedChord( (Music.Chord)null );\r
+                               else\r
+                                       chordMatrix.setSelectedChord( lyric_display.getText() );\r
+                       }\r
+               });\r
+               chordMatrix.capoSelecter.checkbox.addItemListener(\r
+                       new ItemListener() {\r
+                               public void itemStateChanged(ItemEvent e) {\r
+                                       chordOn();\r
+                                       keyboardPanel.keyboardCenterPanel.keyboard.chordDisplay.setNote(-1);\r
+                                       chordDiagram.clear();\r
+                               }\r
+                       }\r
+               );\r
+               chordMatrix.capoSelecter.valueSelecter.addActionListener(\r
+                       new ActionListener() {\r
+                               public void actionPerformed(ActionEvent e) {\r
+                                       chordOn();\r
+                                       keyboardPanel.keyboardCenterPanel.keyboard.chordDisplay.setNote(-1);\r
+                                       chordDiagram.clear();\r
+                               }\r
+                       }\r
+               );\r
+               // Piano keyboard\r
+               //\r
+               keyboardPanel = new MidiKeyboardPanel(chordMatrix);\r
+               keyboardPanel.keyboardCenterPanel.keyboard.addPianoKeyboardListener(\r
+                       new PianoKeyboardAdapter() {\r
+                               public void pianoKeyPressed(int n, InputEvent e) {\r
+                                       chordDiagram.clear();\r
+                               }\r
+                       }\r
+               );\r
+               keyboardPanel.keySelecter.keysigCombobox.addActionListener(\r
+                       new ActionListener() {\r
+                               public void actionPerformed(ActionEvent e) {\r
+                                       Music.Key key = keyboardPanel.keySelecter.getKey();\r
+                                       key.transpose( - chordMatrix.capoSelecter.getCapo() );\r
+                                       chordMatrix.setKeySignature(key);\r
+                               }\r
+                       }\r
+               );\r
+               keyboardPanel.keyboardCenterPanel.keyboard.setPreferredSize(\r
+                       new Dimension( 571, 80 )\r
+               );\r
+               // MIDI connection and MIDI editor\r
+               //\r
+               Vector<VirtualMidiDevice> vmdList = new Vector<VirtualMidiDevice>();\r
+               vmdList.add(keyboardPanel.keyboardCenterPanel.keyboard.midiDevice);\r
+               deviceManager = new MidiDeviceManager(vmdList);\r
+               editorDialog = new MidiEditor(deviceManager);\r
+               Image iconImage = imageIcon == null ? null : imageIcon.getImage();\r
+               editorDialog.setIconImage(iconImage);\r
+               JButton editButton = new JButton(\r
+                       "Edit/Playlist/Speed", new ButtonIcon(ButtonIcon.EDIT_ICON)\r
+               );\r
+               editButton.setMargin(zero_insets);\r
+               editButton.addActionListener(\r
+                       new ActionListener() {\r
+                               public void actionPerformed(ActionEvent e) {\r
+                                       editorDialog.setVisible(true);\r
+                               }\r
+                       }\r
+               );\r
+               new DropTarget(\r
+                       this, DnDConstants.ACTION_COPY_OR_MOVE, editorDialog, true\r
+               );\r
+               deviceManager.setMidiEditor(editorDialog);\r
+               keyboardPanel.eventDialog = editorDialog.eventDialog;\r
+               //\r
+               midiConnectionDialog = new MidiDeviceDialog(deviceManager);\r
+               midiConnectionDialog.setIconImage(iconImage);\r
+               JButton midiConnectionButton = new JButton(\r
+                       "MIDI device connection",\r
+                       new ButtonIcon( ButtonIcon.MIDI_CONNECTOR_ICON )\r
+               );\r
+               midiConnectionButton.addActionListener(new ActionListener() {\r
+                       public void actionPerformed( ActionEvent event ) {\r
+                               midiConnectionDialog.setVisible(true);\r
+                       }\r
+               });\r
+               //\r
+               // Displays\r
+               //\r
+               lyric_display = new ChordTextField();\r
+               lyric_display.addActionListener( new ActionListener() {\r
+                       public void actionPerformed(ActionEvent event) {\r
+                               chordMatrix.setSelectedChord(\r
+                                       event.getActionCommand().trim().split("[ \t\r\n]")[0]\r
+                               );\r
+                       }\r
+               });\r
+               lyric_display_default_border = lyric_display.getBorder();\r
+               lyric_display_default_bgcolor = lyric_display.getBackground();\r
+               //\r
+               // Dark mode\r
+               //\r
+               dark_mode_toggle_button = new JToggleButton(\r
+                               new ButtonIcon(ButtonIcon.DARK_MODE_ICON)\r
+                               );\r
+               dark_mode_toggle_button.setMargin(zero_insets);\r
+               dark_mode_toggle_button.addItemListener(new ItemListener() {\r
+                       public void itemStateChanged(ItemEvent e) {\r
+                               innerSetDarkMode(dark_mode_toggle_button.isSelected());\r
+                       }\r
+               });\r
+               dark_mode_toggle_button.setToolTipText("Light / Dark - 明かりを点灯/消灯");\r
+               dark_mode_toggle_border = dark_mode_toggle_button.getBorder();\r
+               dark_mode_toggle_button.setBorder( null );\r
+               //\r
+               // Inversion/Omission(転回・省略音)\r
+               //\r
+               inversionOmissionButton = new InversionAndOmissionLabel();\r
+               //\r
+               // あの楽器 切り替えボタン\r
+               //\r
+               ano_gakki_toggle_button = new JToggleButton(\r
+                       new ButtonIcon(ButtonIcon.ANO_GAKKI_ICON)\r
+               );\r
+               ano_gakki_toggle_button.setOpaque(false);\r
+               ano_gakki_toggle_button.setMargin(zero_insets);\r
+               ano_gakki_toggle_button.setBorder( null );\r
+               ano_gakki_toggle_button.setToolTipText("あの楽器");\r
+               ano_gakki_toggle_button.addItemListener(\r
+                       new ItemListener() {\r
+                               public void itemStateChanged(ItemEvent e) {\r
+                                       keyboardPanel.keyboardCenterPanel.keyboard.anoGakkiLayeredPane\r
+                                       = ano_gakki_toggle_button.isSelected() ? anoGakkiLayeredPane : null ;\r
+                               }\r
+                       }\r
+               );\r
+               //\r
+               // Chord diagram\r
+               //\r
+               chordDiagram = new ChordDiagram(this);\r
+               //\r
+               // MIDI parts\r
+               //\r
+               tempo_selecter.setEditable(false);\r
+               timesig_selecter.setEditable(false);\r
+               keysig_label.addMouseListener(new MouseAdapter() {\r
+                       public void mousePressed(MouseEvent e) {\r
+                               chordMatrix.setKeySignature( keysig_label.getKey() );\r
+                       }\r
+               });\r
+               deviceManager.getSequencer().addMetaEventListener(this);\r
+               deviceManager.timeRangeModel.addChangeListener(\r
+                       new ChangeListener() {\r
+                               public void stateChanged(ChangeEvent e) {\r
+                                       MidiSequenceModel seq_model = deviceManager.timeRangeModel.getSequenceModel();\r
+                                       SequenceListModel seq_list_model = editorDialog.seqListModel;\r
+                                       int i = seq_list_model.getLoadedIndex();\r
+                                       song_title_label.setText(\r
+                                               "<html>"+(\r
+                                                       i < 0 ? "[No MIDI file loaded]" :\r
+                                                       "MIDI file " + i + ": " + (\r
+                                                       seq_model == null ||\r
+                                                       seq_model.toString() == null ||\r
+                                                       seq_model.toString().isEmpty() ?\r
+                                                               "[Untitled]" :\r
+                                                               "<font color=maroon>"+seq_model+"</font>"\r
+                                                       )\r
+                                               )+"</html>"\r
+                                       );\r
+                                       chordMatrix.setPlaying(deviceManager.timeRangeModel.timer.isRunning());\r
+                                       long current_tick_position = deviceManager.getSequencer().getTickPosition();\r
+                                       SequenceIndex seq_index = null;\r
+                                       if( seq_model != null ) {\r
+                                               seq_index = seq_model.getSequenceIndex();\r
+                                               seq_index.tickToMeasure( current_tick_position );\r
+                                               chordMatrix.setBeat(\r
+                                                       (byte)(seq_index.last_beat), seq_index.timesig_upper\r
+                                               );\r
+                                               if(\r
+                                                       deviceManager.timeRangeModel.getValueIsAdjusting()\r
+                                                       || (\r
+                                                               ! deviceManager.getSequencer().isRunning()\r
+                                                               &&\r
+                                                               ! deviceManager.getSequencer().isRecording()\r
+                                                       )\r
+                                               ) {\r
+                                                       MetaMessage msg = seq_index.lastTimeSignatureAt( current_tick_position );\r
+                                                       if( msg == null ) timesig_selecter.clear(); else meta(msg);\r
+                                                       msg = seq_index.lastTempoAt( current_tick_position );\r
+                                                       if( msg == null ) tempo_selecter.clear(); else meta(msg);\r
+                                                       msg = seq_index.lastKeySignatureAt( current_tick_position );\r
+                                                       if( msg == null ) keysig_label.clear(); else meta(msg);\r
+                                               }\r
+                                       }\r
+                               }\r
+                       }\r
+               );\r
+               deviceManager.timeRangeModel.fireStateChanged();\r
+               //\r
+               // Construct tree of panels\r
+               //\r
+               chord_guide = new JPanel();\r
+               chord_guide.setLayout(new BoxLayout( chord_guide, BoxLayout.X_AXIS ));\r
+               chord_guide.add( Box.createHorizontalStrut(2) );\r
+               chord_guide.add( chordMatrix.chordGuide );\r
+               chord_guide.add( Box.createHorizontalStrut(2) );\r
+               chord_guide.add( lyric_display );\r
+               chord_guide.add( Box.createHorizontalStrut(2) );\r
+               chord_guide.add( enterButtonLabel );\r
+               chord_guide.add( Box.createHorizontalStrut(5) );\r
+               chord_guide.add( chordMatrix.chordDisplay );\r
+               chord_guide.add( Box.createHorizontalStrut(5) );\r
+               chord_guide.add( dark_mode_toggle_button );\r
+               chord_guide.add( Box.createHorizontalStrut(5) );\r
+               chord_guide.add( ano_gakki_toggle_button );\r
+               chord_guide.add( Box.createHorizontalStrut(5) );\r
+               chord_guide.add( inversionOmissionButton );\r
+               chord_guide.add( Box.createHorizontalStrut(5) );\r
+               chord_guide.add( chordMatrix.capoSelecter );\r
+               chord_guide.add( Box.createHorizontalStrut(2) );\r
+               //\r
+               sequencer_upper_panel = new JPanel();\r
+               sequencer_upper_panel.setLayout(\r
+                       new BoxLayout( sequencer_upper_panel, BoxLayout.X_AXIS )\r
+               );\r
+               sequencer_upper_panel.add( Box.createHorizontalStrut(12) );\r
+               sequencer_upper_panel.add( keysig_label );\r
+               sequencer_upper_panel.add( Box.createHorizontalStrut(12) );\r
+               sequencer_upper_panel.add( timesig_selecter );\r
+               sequencer_upper_panel.add( Box.createHorizontalStrut(12) );\r
+               sequencer_upper_panel.add( tempo_selecter );\r
+               sequencer_upper_panel.add( Box.createHorizontalStrut(12) );\r
+               sequencer_upper_panel.add(\r
+                       new MeasureIndicator(deviceManager.timeRangeModel)\r
+               );\r
+               sequencer_upper_panel.add( Box.createHorizontalStrut(12) );\r
+               sequencer_upper_panel.add( song_title_label );\r
+               sequencer_upper_panel.add( Box.createHorizontalStrut(12) );\r
+               sequencer_upper_panel.add( editButton );\r
+               //\r
+               JButton top_button, bottom_button, backward_button, forward_button;\r
+               JToggleButton repeat_button;\r
+               sequencer_lower_panel = new JPanel();\r
+               sequencer_lower_panel.setLayout(\r
+                       new BoxLayout( sequencer_lower_panel, BoxLayout.X_AXIS )\r
+               );\r
+               sequencer_lower_panel.add( Box.createHorizontalStrut(10) );\r
+               sequencer_lower_panel.add(\r
+                       new JSlider(deviceManager.timeRangeModel)\r
+               );\r
+               sequencer_lower_panel.add(\r
+                       new TimeIndicator(deviceManager.timeRangeModel)\r
+               );\r
+               sequencer_lower_panel.add( Box.createHorizontalStrut(5) );\r
+               sequencer_lower_panel.add(\r
+                       top_button = new JButton(editorDialog.move_to_top_action)\r
+               );\r
+               sequencer_lower_panel.add(\r
+                       backward_button = new JButton(\r
+                               deviceManager.timeRangeModel.move_backward_action\r
+                       )\r
+               );\r
+               sequencer_lower_panel.add(\r
+                       new JToggleButton(deviceManager.timeRangeModel.startStopAction)\r
+               );\r
+               sequencer_lower_panel.add(\r
+                       forward_button = new JButton(\r
+                               deviceManager.timeRangeModel.move_forward_action\r
+                       )\r
+               );\r
+               sequencer_lower_panel.add(\r
+                       bottom_button = new JButton(\r
+                               editorDialog.move_to_bottom_action\r
+                       )\r
+               );\r
+               sequencer_lower_panel.add(\r
+                       repeat_button = new JToggleButton(\r
+                               deviceManager.timeRangeModel.toggle_repeat_action\r
+                       )\r
+               );\r
+               sequencer_lower_panel.add( Box.createHorizontalStrut(10) );\r
+               top_button.setMargin(zero_insets);\r
+               bottom_button.setMargin(zero_insets);\r
+               backward_button.setMargin(zero_insets);\r
+               forward_button.setMargin(zero_insets);\r
+               repeat_button.setMargin(zero_insets);\r
+               //\r
+               midi_io_panel = new JPanel();\r
+               midi_io_panel.add( midiConnectionButton );\r
+               midi_io_panel.add( aboutButton );\r
+               //\r
+               sequencer_panel = new JPanel();\r
+               sequencer_panel.setLayout(\r
+                       new BoxLayout( sequencer_panel, BoxLayout.Y_AXIS )\r
+               );\r
+               sequencer_panel.add( sequencer_upper_panel );\r
+               sequencer_panel.add( sequencer_lower_panel );\r
+               sequencer_panel.add( midi_io_panel );\r
+               //\r
+               keyboard_split_pane = new JSplitPane(\r
+                       JSplitPane.HORIZONTAL_SPLIT, keyboardPanel, chordDiagram\r
+               );\r
+               keyboard_split_pane.setOneTouchExpandable(true);\r
+               keyboard_split_pane.setResizeWeight(1.0);\r
+               keyboard_split_pane.setAlignmentX((float)0.5);\r
+               //\r
+               keyboard_sequencer_panel = new JPanel();\r
+               keyboard_sequencer_panel.setLayout(\r
+                       new BoxLayout( keyboard_sequencer_panel, BoxLayout.Y_AXIS )\r
+               );\r
+               keyboard_sequencer_panel.add(chord_guide);\r
+               keyboard_sequencer_panel.add(Box.createVerticalStrut(5));\r
+               keyboard_sequencer_panel.add(keyboard_split_pane);\r
+               keyboard_sequencer_panel.add(Box.createVerticalStrut(5));\r
+               keyboard_sequencer_panel.add(sequencer_panel);\r
+               //\r
+               main_split_pane = new JSplitPane(\r
+                       JSplitPane.VERTICAL_SPLIT,\r
+                       chordMatrix, keyboard_sequencer_panel\r
+               );\r
+               main_split_pane.setResizeWeight(0.5);\r
+               main_split_pane.setAlignmentX((float)0.5);\r
+               main_split_pane.setDividerSize(5);\r
+               //\r
+               anoGakkiLayeredPane = new AnoGakkiLayeredPane();\r
+               anoGakkiLayeredPane.add(main_split_pane);\r
+               setContentPane(anoGakkiLayeredPane);\r
+               setPreferredSize( new Dimension(750,470) );\r
+       }\r
+       /////////////////////////////////////////\r
+       //\r
+       // アプレット開始(その2)\r
+       //\r
+       public void start() {\r
+               //\r
+               // コードボタンで設定されている現在の調を\r
+               // ピアノキーボードに伝える\r
+               chordMatrix.fireKeySignatureChanged();\r
+               //\r
+               // アプレットのパラメータにMIDIファイルのURLが指定されていたら\r
+               // それを再生する\r
+               String midi_url = getParameter("midi_file");\r
+               System.gc();\r
+               if( midi_url != null ) {\r
+                       addToPlaylist(midi_url);\r
+                       play();\r
+               }\r
+       }\r
+       // アプレット終了\r
+       public void stop() {\r
+               deviceManager.timeRangeModel.stop(); // MIDI再生を強制終了\r
+               System.gc();\r
+       }\r
+       /////////////////////////////////////////\r
+       //\r
+       // MetaEventListener\r
+       //\r
+       public void meta(MetaMessage msg) {\r
+               int msgtype = msg.getType();\r
+               switch( msgtype ) {\r
+\r
+               case 0x01: // Text(任意のテキスト:コメントなど)\r
+               case 0x02: // Copyright(著作権表示)\r
+               case 0x05: // Lyrics(歌詞)\r
+               case 0x06: // Marker\r
+               case 0x03: // Sequence Name / Track Name(曲名またはトラック名)\r
+                       lyric_display.addLyric(msg.getData());\r
+                       break;\r
+\r
+               case 0x51: // Tempo (3 bytes) - テンポ\r
+                       tempo_selecter.setTempo(msg.getData());\r
+                       break;\r
+               case 0x58: // Time signature (4 bytes) - 拍子\r
+                       timesig_selecter.setValue(msg.getData());\r
+                       break;\r
+               case 0x59: // Key signature (2 bytes) : 調号\r
+                       keysig_label.setKeySignature( new Music.Key(msg.getData()) );\r
+                       chordMatrix.setKeySignature( new Music.Key(msg.getData()) );\r
+                       break;\r
+\r
+               }\r
+       }\r
+       //\r
+       ///////////////////////////////////////////////////////////////\r
+       //\r
+       // Methods\r
+       //\r
+       ///////////////////////////////////////////////////////////////\r
+       private void innerSetDarkMode(boolean is_dark) {\r
+               Color col = is_dark ? Color.black : null;\r
+               // Color fgcol = is_dark ? Color.pink : null;\r
+               getContentPane().setBackground(\r
+                               is_dark ? Color.black : root_pane_default_bgcolor\r
+                               );\r
+               main_split_pane.setBackground( col );\r
+               keyboard_split_pane.setBackground( col );\r
+               enterButtonLabel.setDarkMode( is_dark );\r
+               chord_guide.setBackground( col );\r
+               lyric_display.setBorder( is_dark ? null : lyric_display_default_border );\r
+               lyric_display.setBackground( is_dark ?\r
+                               chordMatrix.darkModeColorset.backgrounds[2] : lyric_display_default_bgcolor\r
+                               );\r
+               lyric_display.setForeground( is_dark ? Color.white : null );\r
+               inversionOmissionButton.setBackground( col );\r
+               ano_gakki_toggle_button.setBackground( col );\r
+               keyboard_sequencer_panel.setBackground( col );\r
+               chordDiagram.setBackground( col );\r
+               chordDiagram.titleLabel.setDarkMode( is_dark );\r
+               chordMatrix.setDarkMode( is_dark );\r
+               keyboardPanel.setDarkMode( is_dark );\r
+       }\r
+       /////////////////////////////////////////////////////////////////\r
+       //\r
+       // 発音(和音)\r
+       //\r
+       // 注:この関数を直接呼ぶとアルペジオが効かないので、\r
+       // chord_matrix.setSelectedChord() を使うことを推奨\r
+       //\r
+       int[] chordOnNotes = null;\r
+       public void chordOn() {\r
+               Music.Chord playChord = chordMatrix.getSelectedChord();\r
+               if(\r
+                       chordOnNotes != null &&\r
+                       chordMatrix.getNoteIndex() < 0 &&\r
+                       (! chordMatrix.isDragged() || playChord == null)\r
+               ) {\r
+                       // コードが鳴っている状態で、新たなコードを鳴らそうとしたり、\r
+                       // もう鳴らさないという信号が来た場合は、今鳴っている音を止める。\r
+                       //\r
+                       for( int n : chordOnNotes )\r
+                               keyboardPanel.keyboardCenterPanel.keyboard.noteOff(n);\r
+                       chordOnNotes = null;\r
+               }\r
+               if( playChord == null ) {\r
+                       // もう鳴らさないので、歌詞表示に通知して終了\r
+                       if( lyric_display != null )\r
+                               lyric_display.currentChord = null;\r
+                       return;\r
+               }\r
+               // あの楽器っぽい表示\r
+               if( keyboardPanel.keyboardCenterPanel.keyboard.anoGakkiLayeredPane != null ) {\r
+                       JComponent btn = chordMatrix.getSelectedButton();\r
+                       if( btn != null )\r
+                               anoGakkiLayeredPane.start(chordMatrix,btn);\r
+               }\r
+               // コードボタンからのコードを、カポつき演奏キーからオリジナルキーへ変換\r
+               Music.Key originalKey = chordMatrix.getKeySignatureCapo();\r
+               Music.Chord originalChord = playChord.clone().transpose(\r
+                       chordMatrix.capoSelecter.getCapo(),\r
+                       chordMatrix.getKeySignature()\r
+               );\r
+               // 変換後のコードをキーボード画面に設定\r
+               keyboardPanel.keyboardCenterPanel.keyboard.setChord(originalChord);\r
+               //\r
+               // 音域を決める。これにより鳴らす音が確定する。\r
+               Music.Range chordRange = new Music.Range(\r
+                       keyboardPanel.keyboardCenterPanel.keyboard.getChromaticOffset() + 10 +\r
+                       ( keyboardPanel.keyboardCenterPanel.keyboard.getOctaves() / 4 ) * 12,\r
+                       inversionOmissionButton.isAutoInversionMode() ?\r
+                       keyboardPanel.keyboardCenterPanel.keyboard.getChromaticOffset() + 21 :\r
+                       keyboardPanel.keyboardCenterPanel.keyboard.getChromaticOffset() + 33,\r
+                       -2,\r
+                       inversionOmissionButton.isAutoInversionMode()\r
+               );\r
+               int[] notes = originalChord.toNoteArray(chordRange, originalKey);\r
+               //\r
+               // 前回鳴らしたコード構成音を覚えておく\r
+               int[] prevChordOnNotes = null;\r
+               if( chordMatrix.isDragged() || chordMatrix.getNoteIndex() >= 0 )\r
+                       prevChordOnNotes = Arrays.copyOf(chordOnNotes, chordOnNotes.length);\r
+               //\r
+               // 次に鳴らす構成音を決める\r
+               chordOnNotes = new int[notes.length];\r
+               int i = 0;\r
+               for( int n : notes ) {\r
+                       if( inversionOmissionButton.getOmissionNoteIndex() == i ) {\r
+                               i++; continue;\r
+                       }\r
+                       chordOnNotes[i++] = n;\r
+                       //\r
+                       // その音が今鳴っているか調べる\r
+                       boolean isNoteOn = false;\r
+                       if( prevChordOnNotes != null ) {\r
+                               for( int prevN : prevChordOnNotes ) {\r
+                                       if( n == prevN ) {\r
+                                               isNoteOn = true;\r
+                                               break;\r
+                                       }\r
+                               }\r
+                       }\r
+                       // すでに鳴っているのに単音を鳴らそうとする場合、\r
+                       // 鳴らそうとしている音を一旦止める。\r
+                       if( isNoteOn && chordMatrix.getNoteIndex() >= 0 &&\r
+                               notes[chordMatrix.getNoteIndex()] - n == 0\r
+                       ) {\r
+                               keyboardPanel.keyboardCenterPanel.keyboard.noteOff(n);\r
+                               isNoteOn = false;\r
+                       }\r
+                       // その音が鳴っていなかったら鳴らす。\r
+                       if( ! isNoteOn )\r
+                               keyboardPanel.keyboardCenterPanel.keyboard.noteOn(n);\r
+               }\r
+               //\r
+               // コードを表示\r
+               keyboardPanel.keyboardCenterPanel.keyboard.setChord(originalChord);\r
+               chordMatrix.chordDisplay.setChord(playChord);\r
+               //\r
+               // コードダイアグラム用にもコードを表示\r
+               Music.Chord diagramChord;\r
+               int chordDiagramCapo = chordDiagram.capoSelecterView.getCapo();\r
+               if( chordDiagramCapo == chordMatrix.capoSelecter.getCapo() )\r
+                       diagramChord = playChord.clone();\r
+               else\r
+                       diagramChord = originalChord.clone().transpose(\r
+                               - chordDiagramCapo, originalKey\r
+                       );\r
+               chordDiagram.setChord(diagramChord);\r
+               if( chordDiagram.recordTextButton.isSelected() )\r
+                       lyric_display.appendChord(diagramChord);\r
+       }\r
+}\r
+\r
+/***************************************************************************\r
+ *\r
+ *     GUI parts\r
+ *\r
+ ***************************************************************************/\r
+\r
+class ChordDisplay extends JLabel {\r
+       Music.Chord chord = null;\r
+       PianoKeyboard keyboard = null;\r
+       ChordMatrix chordMatrix = null;\r
+       String defaultString = null;\r
+       int noteNumber = -1;\r
+       private boolean isDark = false;\r
+       private boolean isMouseEntered = false;\r
+\r
+       public ChordDisplay(String defaultString, ChordMatrix chordMatrix, PianoKeyboard keyboard) {\r
+               super(defaultString, JLabel.CENTER);\r
+               this.defaultString = defaultString;\r
+               this.keyboard = keyboard;\r
+               this.chordMatrix = chordMatrix;\r
+               if( chordMatrix != null ) {\r
+                       addMouseListener(new MouseAdapter() {\r
+                               public void mousePressed(MouseEvent e) {\r
+                                       if( chord != null ) { // コードが表示されている場合\r
+                                               if( (e.getModifiersEx() & InputEvent.BUTTON3_DOWN_MASK) != 0 ) {\r
+                                                       // 右クリックでコードを止める\r
+                                                       ChordDisplay.this.chordMatrix.setSelectedChord((Music.Chord)null);\r
+                                               }\r
+                                               else {\r
+                                                       // コードを鳴らす。\r
+                                                       //   キーボードが指定されている場合、オリジナルキー(カポ反映済)のコードを使う。\r
+                                                       if( ChordDisplay.this.keyboard == null )\r
+                                                               ChordDisplay.this.chordMatrix.setSelectedChord(chord);\r
+                                                       else\r
+                                                               ChordDisplay.this.chordMatrix.setSelectedChordCapo(chord);\r
+                                               }\r
+                                       }\r
+                                       else if( noteNumber >= 0 ) { // 音階が表示されている場合\r
+                                               ChordDisplay.this.keyboard.noteOn(noteNumber);\r
+                                       }\r
+                               }\r
+                               public void mouseReleased(MouseEvent e) {\r
+                                       if( noteNumber >= 0 )\r
+                                               ChordDisplay.this.keyboard.noteOff(noteNumber);\r
+                               }\r
+                               public void mouseEntered(MouseEvent e) {\r
+                                       mouseEntered(true);\r
+                               }\r
+                               public void mouseExited(MouseEvent e) {\r
+                                       mouseEntered(false);\r
+                               }\r
+                               private void mouseEntered(boolean isMouseEntered) {\r
+                                       ChordDisplay.this.isMouseEntered = isMouseEntered;\r
+                                       if( noteNumber >= 0 || chord != null )\r
+                                               repaint();\r
+                               }\r
+                       });\r
+                       addMouseWheelListener(this.chordMatrix);\r
+               }\r
+       }\r
+       public void paint(Graphics g) {\r
+               super.paint(g);\r
+               Dimension d = getSize();\r
+               if( isMouseEntered && (noteNumber >= 0 || chord != null) ) {\r
+                       g.setColor(Color.gray);\r
+                       g.drawRect( 0, 0, d.width-1, d.height-1 );\r
+               }\r
+       }\r
+       private void setChordText() {\r
+               setText( chord.toHtmlString(isDark ? "#FFCC33" : "maroon") );\r
+       }\r
+       void setNote(int note_no) { setNote( note_no, false ); }\r
+       void setNote(int note_no, boolean is_rhythm_part) {\r
+               setToolTipText(null);\r
+               this.chord = null;\r
+               this.noteNumber = note_no;\r
+               if( note_no < 0 ) {\r
+                       //\r
+                       // Clear\r
+                       //\r
+                       setText(defaultString);\r
+                       return;\r
+               }\r
+               if( is_rhythm_part ) {\r
+                       setText(\r
+                                       "MIDI note No." + note_no + " : "\r
+                                                       + MIDISpec.getPercussionName(note_no)\r
+                                       );\r
+               }\r
+               else {\r
+                       setText(\r
+                                       "Note: " + Music.NoteSymbol.noteNoToSymbol(note_no)\r
+                                       + "  -  MIDI note No." + note_no + " : "\r
+                                       + Math.round(Music.noteNoToFrequency(note_no)) + "Hz" );\r
+               }\r
+       }\r
+       void setChord(Music.Chord chord) {\r
+               this.chord = chord;\r
+               this.noteNumber = -1;\r
+               if( chord == null ) {\r
+                       setText( defaultString );\r
+                       setToolTipText( null );\r
+               }\r
+               else {\r
+                       setChordText();\r
+                       setToolTipText( "Chord: " + chord.toName() );\r
+               }\r
+       }\r
+       void setDarkMode(boolean is_dark) {\r
+               this.isDark = is_dark;\r
+               if( chord != null ) setChordText();\r
+       }\r
+}\r
+\r
+// Inversion and Omission menu button\r
+//\r
+class InversionAndOmissionLabel extends JLabel\r
+implements MouseListener, PopupMenuListener\r
+{\r
+       JPopupMenu popup_menu;\r
+       ButtonGroup omission_group = new ButtonGroup();\r
+       ButtonIcon icon = new ButtonIcon(ButtonIcon.INVERSION_ICON);\r
+       JRadioButtonMenuItem radioButtonitems[] = new JRadioButtonMenuItem[4];\r
+       JCheckBoxMenuItem cb_inversion;\r
+\r
+       public InversionAndOmissionLabel() {\r
+               setIcon(icon);\r
+               popup_menu = new JPopupMenu();\r
+               popup_menu.add(\r
+                       cb_inversion = new JCheckBoxMenuItem("Auto Inversion",true)\r
+               );\r
+               popup_menu.addSeparator();\r
+               omission_group.add(\r
+                       radioButtonitems[0] = new JRadioButtonMenuItem("All notes",true)\r
+               );\r
+               popup_menu.add(radioButtonitems[0]);\r
+               omission_group.add(\r
+                       radioButtonitems[1] = new JRadioButtonMenuItem("Omit 5th")\r
+               );\r
+               popup_menu.add(radioButtonitems[1]);\r
+               omission_group.add(\r
+                       radioButtonitems[2] = new JRadioButtonMenuItem("Omit 3rd (Power Chord)")\r
+               );\r
+               popup_menu.add(radioButtonitems[2]);\r
+               omission_group.add(\r
+                       radioButtonitems[3] = new JRadioButtonMenuItem("Omit root")\r
+               );\r
+               popup_menu.add(radioButtonitems[3]);\r
+               addMouseListener(this);\r
+               popup_menu.addPopupMenuListener(this);\r
+               setToolTipText("Automatic inversion and Note omission - 自動転回と省略音の設定");\r
+       }\r
+       public void mousePressed(MouseEvent e) {\r
+               Component c = e.getComponent();\r
+               if( c == this ) popup_menu.show( c, 0, getHeight() );\r
+       }\r
+       public void mouseReleased(MouseEvent e) { }\r
+       public void mouseEntered(MouseEvent e) { }\r
+       public void mouseExited(MouseEvent e) { }\r
+       public void mouseClicked(MouseEvent e) { }\r
+       public void popupMenuCanceled(PopupMenuEvent e) { }\r
+       public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {\r
+               repaint(); // To repaint icon image\r
+       }\r
+       public void popupMenuWillBecomeVisible(PopupMenuEvent e) { }\r
+       public boolean isAutoInversionMode() {\r
+               return cb_inversion.isSelected();\r
+       }\r
+       public void setAutoInversion(boolean is_auto) {\r
+               cb_inversion.setSelected(is_auto);\r
+       }\r
+       public int getOmissionNoteIndex() {\r
+               if( radioButtonitems[3].isSelected() ) { // Root\r
+                       return 0;\r
+               }\r
+               else if( radioButtonitems[2].isSelected() ) { // 3rd\r
+                       return 1;\r
+               }\r
+               else if( radioButtonitems[1].isSelected() ) { // 5th\r
+                       return 2;\r
+               }\r
+               else { // No omission\r
+                       return -1;\r
+               }\r
+       }\r
+       public void setOmissionNoteIndex(int index) {\r
+               switch(index) {\r
+               case 0: radioButtonitems[3].setSelected(true); break;\r
+               case 1: radioButtonitems[2].setSelected(true); break;\r
+               case 2: radioButtonitems[1].setSelected(true); break;\r
+               default: radioButtonitems[0].setSelected(true); break;\r
+               }\r
+       }\r
+}\r
+\r
+class ChordTextField extends JTextField {\r
+       Music.Chord currentChord = null;\r
+       private long lyricArrivedTime = System.nanoTime();\r
+       public ChordTextField() {\r
+               super(80);\r
+               //\r
+               // JTextField は、サイズ設定をしないとリサイズ時に縦に伸び過ぎてしまう。\r
+               // 1行しか入力できないので、縦に伸びすぎるのはスペースがもったいない。\r
+               // そこで、このような現象を防止するために、最大サイズを明示的に\r
+               // 画面サイズと同じに設定する。\r
+               //\r
+               // To reduce resized height, set maximum size to screen size.\r
+               //\r
+               setMaximumSize(\r
+                       java.awt.Toolkit.getDefaultToolkit().getScreenSize()\r
+               );\r
+       }\r
+       public void appendChord(Music.Chord chord) {\r
+               if( currentChord == null && chord == null )\r
+                       return;\r
+               if( currentChord != null && chord != null && chord.equals(currentChord) )\r
+                       return;\r
+               String delimiter = ""; // was "\n"\r
+               setText( getText() + (chord == null ? delimiter : chord + " ") );\r
+               currentChord = ( chord == null ? null : chord.clone() );\r
+       }\r
+       public void addLyric(byte[] data) {\r
+               long startTime = System.nanoTime();\r
+               // 歌詞を表示\r
+               String additionalLyric;\r
+               try {\r
+                       additionalLyric = (new String(data,"JISAutoDetect")).trim();\r
+               } catch( UnsupportedEncodingException e ) {\r
+                       additionalLyric = (new String(data)).trim();\r
+               }\r
+               String lyric = getText();\r
+               if( startTime - lyricArrivedTime > 1000000000L /* 1sec */\r
+                       && (\r
+                               additionalLyric.length() > 8 || additionalLyric.isEmpty()\r
+                               || lyric == null || lyric.isEmpty()\r
+                       )\r
+               ) {\r
+                       // 長い歌詞や空白が来たり、追加先に歌詞がなかった場合は上書きする。\r
+                       // ただし、前回から充分に時間が経っていない場合は上書きしない。\r
+                       setText(additionalLyric);\r
+               }\r
+               else {\r
+                       // 短い歌詞だった場合は、既存の歌詞に追加する\r
+                       setText( lyric + " " + additionalLyric );\r
+               }\r
+               setCaretPosition(getText().length());\r
+               lyricArrivedTime = startTime;\r
+       }\r
+}\r
+\r
+\r
diff --git a/src/ChordMatrix.java b/src/ChordMatrix.java
new file mode 100644 (file)
index 0000000..6406cc1
--- /dev/null
@@ -0,0 +1,1241 @@
+\r
+import java.awt.Color;\r
+import java.awt.Component;\r
+import java.awt.Dimension;\r
+import java.awt.Font;\r
+import java.awt.Graphics;\r
+import java.awt.Graphics2D;\r
+import java.awt.GridLayout;\r
+import java.awt.event.ActionEvent;\r
+import java.awt.event.ActionListener;\r
+import java.awt.event.FocusEvent;\r
+import java.awt.event.FocusListener;\r
+import java.awt.event.InputEvent;\r
+import java.awt.event.ItemEvent;\r
+import java.awt.event.ItemListener;\r
+import java.awt.event.KeyEvent;\r
+import java.awt.event.KeyListener;\r
+import java.awt.event.MouseAdapter;\r
+import java.awt.event.MouseEvent;\r
+import java.awt.event.MouseListener;\r
+import java.awt.event.MouseMotionListener;\r
+import java.awt.event.MouseWheelEvent;\r
+import java.awt.event.MouseWheelListener;\r
+import java.util.ArrayList;\r
+import java.util.EventListener;\r
+\r
+import javax.swing.Box;\r
+import javax.swing.BoxLayout;\r
+import javax.swing.ComboBoxModel;\r
+import javax.swing.DefaultComboBoxModel;\r
+import javax.swing.JComponent;\r
+import javax.swing.JLabel;\r
+import javax.swing.JMenuItem;\r
+import javax.swing.JPanel;\r
+import javax.swing.JPopupMenu;\r
+\r
+interface ChordMatrixListener extends EventListener {\r
+       void chordChanged();\r
+       void keySignatureChanged();\r
+}\r
+/**\r
+ * MIDI Chord Helper 用のコードボタンマトリクス\r
+ *\r
+ * @author\r
+ *     Copyright (C) 2004-2013 Akiyoshi Kamide\r
+ *     http://www.yk.rim.or.jp/~kamide/music/chordhelper/\r
+ */\r
+public class ChordMatrix extends JPanel\r
+       implements MouseListener, KeyListener, MouseMotionListener, MouseWheelListener\r
+{\r
+       /**\r
+        * 列数\r
+        */\r
+       public static final int N_COLUMNS = Music.SEMITONES_PER_OCTAVE * 2 + 1;\r
+       /**\r
+        * 行数\r
+        */\r
+       public static final int CHORD_BUTTON_ROWS = 3;\r
+       /**\r
+        * 調号ボタン\r
+        */\r
+       public Co5Label keysigLabels[] = new Co5Label[ N_COLUMNS ];\r
+       /**\r
+        * コードボタン\r
+        */\r
+       public ChordLabel chordLabels[] = new ChordLabel[N_COLUMNS * CHORD_BUTTON_ROWS];\r
+       /**\r
+        * コードボタンの下のコード表示部\r
+        */\r
+       public ChordDisplay chordDisplay = new ChordDisplay("Chord Pad", this, null);\r
+\r
+       private class ChordLabelSelection {\r
+               ChordLabel chordLabel;\r
+               int bitIndex;\r
+               boolean isSus4;\r
+               public ChordLabelSelection(ChordLabel chordLabel, int bitIndex) {\r
+                       this.chordLabel = chordLabel;\r
+                       this.bitIndex = bitIndex;\r
+                       this.isSus4 = chordLabel.isSus4;\r
+               }\r
+               public void setCheckBit(boolean isOn) {\r
+                       chordLabel.setCheckBit(isOn, bitIndex);\r
+               }\r
+               public boolean setBassCheckBit(boolean isOn) {\r
+                       if( bitIndex == 0 && ! isSus4 ) {\r
+                               chordLabel.setCheckBit(isOn, 6);\r
+                               return true;\r
+                       }\r
+                       return false;\r
+               }\r
+       }\r
+       private class ChordLabelSelections {\r
+               int weight = 0;\r
+               int bass_weight = 0;\r
+               boolean is_active = false;\r
+               boolean is_bass_active = false;\r
+               private ChordLabelSelection acls[];\r
+               public ChordLabelSelections(ArrayList<ChordLabelSelection> al) {\r
+                       acls = al.toArray(new ChordLabelSelection[al.size()]);\r
+               }\r
+               void addWeight(int weight_diff) {\r
+                       if( (weight += weight_diff) < 0 ) weight = 0;\r
+                       if( (weight > 0) != is_active ) {\r
+                               is_active = !is_active;\r
+                               for( ChordLabelSelection cls : acls ) {\r
+                                       cls.setCheckBit(is_active);\r
+                               }\r
+                       }\r
+               }\r
+               void addBassWeight(int weight_diff) {\r
+                       if( (bass_weight += weight_diff) < 0 ) bass_weight = 0;\r
+                       if( (bass_weight > 0) != is_bass_active ) {\r
+                               is_bass_active = !is_bass_active;\r
+                               for( ChordLabelSelection cls : acls ) {\r
+                                       if( ! cls.setBassCheckBit(is_bass_active) ) {\r
+                                               // No more root major/minor\r
+                                               break;\r
+                                       }\r
+                               }\r
+                       }\r
+                       addWeight(weight_diff);\r
+               }\r
+               void clearWeight() {\r
+                       weight = bass_weight = 0;\r
+                       is_active = is_bass_active = false;\r
+                       for( ChordLabelSelection cls : acls ) {\r
+                               cls.setCheckBit(false);\r
+                               cls.setBassCheckBit(false);\r
+                       }\r
+               }\r
+       }\r
+       ChordLabelSelections chordLabelSelections[] =\r
+               new ChordLabelSelections[Music.SEMITONES_PER_OCTAVE];\r
+       /**\r
+        * 発音中のノート表示をクリアします。\r
+        */\r
+       public void clearIndicators() {\r
+               for( int i=0; i<chordLabelSelections.length; i++ ) {\r
+                       chordLabelSelections[i].clearWeight();\r
+               }\r
+               repaint();\r
+       }\r
+       /**\r
+        * MIDIのノートイベント(ON/OFF)を受け取ります。\r
+        * @param isNoteOn ONのときtrue\r
+        * @param noteNumber ノート番号\r
+        */\r
+       public void note(boolean isNoteOn, int noteNumber) {\r
+               int weightDiff = (isNoteOn ? 1 : -1);\r
+               ChordLabelSelections cls = chordLabelSelections[Music.mod12(noteNumber)];\r
+               if( noteNumber < 49 )\r
+                       cls.addBassWeight(weightDiff);\r
+               else\r
+                       cls.addWeight(weightDiff);\r
+       }\r
+\r
+       /**\r
+        * 調号ボタン\r
+        */\r
+       class Co5Label extends JLabel {\r
+               public boolean isSelected = false;\r
+               public int co5Value = 0;\r
+               private Color indicatorColor;\r
+               public Co5Label(int v) {\r
+                       Music.Key key = new Music.Key(co5Value = v);\r
+                       setOpaque(true);\r
+                       setBackground(false);\r
+                       setForeground( currentColorset.foregrounds[0] );\r
+                       setHorizontalAlignment( JLabel.CENTER );\r
+                       String tip = "Key signature: ";\r
+                       if( v != key.toCo5() ) {\r
+                               tip += "out of range" ;\r
+                       }\r
+                       else {\r
+                               tip += key.signatureDescription() + " " +\r
+                                       key.toStringIn(Music.SymbolLanguage.IN_JAPANESE);\r
+                               if( v == 0 ) {\r
+                                       setIcon(new ButtonIcon(ButtonIcon.NATURAL_ICON));\r
+                               }\r
+                               else {\r
+                                       setFont( getFont().deriveFont(Font.PLAIN) );\r
+                                       setText( key.signature() );\r
+                               }\r
+                       }\r
+                       setToolTipText(tip);\r
+               }\r
+               public void paint(Graphics g) {\r
+                       super.paint(g);\r
+                       Dimension d = getSize();\r
+                       if( ChordMatrix.this.isFocusOwner() && isSelected ) {\r
+                               g.setColor( currentColorset.focus[1] );\r
+                               g.drawRect( 0, 0, d.width-1, d.height-1 );\r
+                       }\r
+                       if( !isSelected || !isPlaying || currentBeat+1 == timesigUpper ) {\r
+                               return;\r
+                       }\r
+                       if( currentBeat == 0 ) {\r
+                               g.setColor( indicatorColor );\r
+                               g.drawRect( 2, 2, d.width-5, d.height-5 );\r
+                               g.setColor( isDark ? indicatorColor.darker() : indicatorColor.brighter() );\r
+                               g.drawRect( 0, 0, d.width-1, d.height-1 );\r
+                               return;\r
+                       }\r
+                       Color color = currentColorset.indicators[0];\r
+                       g.setColor( color );\r
+                       if( currentBeat == 1 ) {\r
+                               //\r
+                               // ||__ii\r
+                               g.drawLine( 2, d.height-3, d.width-3, d.height-3 );\r
+                               g.drawLine( d.width-3, d.height*3/4, d.width-3, d.height-3 );\r
+                               g.drawLine( 2, 2, 2, d.height-3 );\r
+                               g.setColor( isDark ? color.darker() : color.brighter() );\r
+                               g.drawLine( 0, d.height-1, d.width-1, d.height-1 );\r
+                               g.drawLine( d.width-1, d.height*3/4, d.width-1, d.height-1 );\r
+                               g.drawLine( 0, 0, 0, d.height-1 );\r
+                       }\r
+                       else {\r
+                               //\r
+                               // ii__\r
+                               //\r
+                               int vertical_top = (d.height-1) * (currentBeat-1) / (timesigUpper-2) ;\r
+                               g.drawLine( 2, vertical_top == 0 ? 2 : vertical_top, 2, d.height-3 );\r
+                               g.setColor( isDark ? color.darker() : color.brighter() );\r
+                               g.drawLine( 0, vertical_top, 0, d.height-1 );\r
+                       }\r
+               }\r
+               public void setBackground(boolean isActive) {\r
+                       super.setBackground(currentColorset.backgrounds[isActive?2:0]);\r
+                       setIndicatorColor();\r
+                       setOpaque(true);\r
+               }\r
+               public void setSelection(boolean isSelected) {\r
+                       this.isSelected = isSelected;\r
+                       setSelection();\r
+               }\r
+               public void setSelection() {\r
+                       setForeground(currentColorset.foregrounds[isSelected?1:0]);\r
+               }\r
+               public void setIndicatorColor() {\r
+                       if( co5Value < 0 ) {\r
+                               indicatorColor = currentColorset.indicators[2];\r
+                       }\r
+                       else if( co5Value > 0 ) {\r
+                               indicatorColor = currentColorset.indicators[1];\r
+                       }\r
+                       else {\r
+                               indicatorColor = currentColorset.foregrounds[1];\r
+                       }\r
+               }\r
+       }\r
+       /**\r
+        * コードボタン\r
+        */\r
+       class ChordLabel extends JLabel {\r
+               public byte check_bits = 0;\r
+               public int co5_value;\r
+               public boolean is_minor;\r
+               public boolean isSus4;\r
+               public boolean is_selected = false;\r
+               public Music.Chord chord;\r
+\r
+               private boolean in_active_zone = true;\r
+               private Font bold_font, plain_font;\r
+               private int indicator_color_indices[] = new int[5];\r
+               private byte indicator_bits = 0;\r
+\r
+               public ChordLabel( Music.Chord chord ) {\r
+                       this.chord = chord;\r
+                       is_minor = chord.isMinor();\r
+                       isSus4 = chord.isSus4();\r
+                       co5_value = chord.rootNoteSymbol().toCo5();\r
+                       if( is_minor ) co5_value -= 3;\r
+                       String label_string = ( isSus4 ? chord.symbolSuffix() : chord.toString() );\r
+                       if( is_minor && label_string.length() > 3 ) {\r
+                               float small_point_size = getFont().getSize2D() - 2;\r
+                               bold_font = getFont().deriveFont(Font.BOLD, small_point_size);\r
+                               plain_font = getFont().deriveFont(Font.PLAIN, small_point_size);\r
+                       }\r
+                       else {\r
+                               bold_font = getFont().deriveFont(Font.BOLD);\r
+                               plain_font = getFont().deriveFont(Font.PLAIN);\r
+                       }\r
+                       setOpaque(true);\r
+                       setBackground(0);\r
+                       setForeground( currentColorset.foregrounds[0] );\r
+                       setBold(false);\r
+                       setHorizontalAlignment( JLabel.CENTER );\r
+                       setText( label_string );\r
+                       setToolTipText( "Chord: " + chord.toName() );\r
+               }\r
+               public void paint(Graphics g) {\r
+                       super.paint(g);\r
+                       Dimension d = getSize();\r
+                       Graphics2D g2 = (Graphics2D) g;\r
+                       Color color = null;\r
+\r
+                       if( ! in_active_zone ) {\r
+                               g2.setColor( Color.gray );\r
+                       }\r
+\r
+                       if( (indicator_bits & 32) != 0 ) {\r
+                               //\r
+                               // Draw square  []  with 3rd/sus4th note color\r
+                               //\r
+                               if( in_active_zone ) {\r
+                                       color = currentColorset.indicators[indicator_color_indices[1]];\r
+                                       g2.setColor( color );\r
+                               }\r
+                               g2.drawRect( 0, 0, d.width-1, d.height-1 );\r
+                               g2.drawRect( 2, 2, d.width-5, d.height-5 );\r
+                       }\r
+                       if( (indicator_bits & 1) != 0 ) {\r
+                               //\r
+                               // Draw  ||__  with root note color\r
+                               //\r
+                               if( in_active_zone ) {\r
+                                       color = currentColorset.indicators[indicator_color_indices[0]];\r
+                                       g2.setColor( color );\r
+                               }\r
+                               g2.drawLine( 0, 0, 0, d.height-1 );\r
+                               g2.drawLine( 2, 2, 2, d.height-3 );\r
+                       }\r
+                       if( (indicator_bits & 64) != 0 ) {\r
+                               // Draw bass mark with root note color\r
+                               //\r
+                               if( in_active_zone ) {\r
+                                       color = currentColorset.indicators[indicator_color_indices[0]];\r
+                                       g2.setColor( color );\r
+                               }\r
+                               g2.fillRect( 6, d.height-7, d.width-12, 2 );\r
+                       }\r
+                       if( (indicator_bits & 4) != 0 ) {\r
+                               //\r
+                               // Draw short  __ii  with parfect 5th color\r
+                               //\r
+                               if( in_active_zone ) {\r
+                                       color = currentColorset.indicators[indicator_color_indices[2]];\r
+                                       g2.setColor( color );\r
+                               }\r
+                               g2.drawLine( d.width-1, d.height*3/4, d.width-1, d.height-1 );\r
+                               g2.drawLine( d.width-3, d.height*3/4, d.width-3, d.height-3 );\r
+                       }\r
+                       if( (indicator_bits & 2) != 0 ) {\r
+                               //\r
+                               // Draw  __  with 3rd note color\r
+                               //\r
+                               if( in_active_zone ) {\r
+                                       color = currentColorset.indicators[indicator_color_indices[1]];\r
+                                       g2.setColor( color );\r
+                               }\r
+                               g2.drawLine( 0, d.height-1, d.width-1, d.height-1 );\r
+                               g2.drawLine( 2, d.height-3, d.width-3, d.height-3 );\r
+                       }\r
+                       if( (indicator_bits & 8) != 0 ) {\r
+                               //\r
+                               // Draw circle with diminished 5th color\r
+                               //\r
+                               if( in_active_zone ) {\r
+                                       g2.setColor( currentColorset.indicators[indicator_color_indices[3]] );\r
+                               }\r
+                               g2.drawOval( 1, 1, d.width-2, d.height-2 );\r
+                       }\r
+                       if( (indicator_bits & 16) != 0 ) {\r
+                               //\r
+                               // Draw + with augument 5th color\r
+                               //\r
+                               if( in_active_zone ) {\r
+                                       g2.setColor( currentColorset.indicators[indicator_color_indices[4]] );\r
+                               }\r
+                               g2.drawLine( 1, 3, d.width-3, 3 );\r
+                               g2.drawLine( 1, 4, d.width-3, 4 );\r
+                               g2.drawLine( d.width/2-1, 0, d.width/2-1, 7 );\r
+                               g2.drawLine( d.width/2, 0, d.width/2, 7 );\r
+                       }\r
+               }\r
+               public void clearCheckBit() {\r
+                       check_bits = indicator_bits = 0;\r
+               }\r
+               public void setCheckBit( boolean is_on, int bit_index ) {\r
+                       //\r
+                       // Check bits: x6x43210\r
+                       //   6:BassRoot\r
+                       //   4:Augumented5th, 3:Diminished5th, 2:Parfect5th,\r
+                       //   1:Major3rd/minor3rd/sus4th, 0:Root\r
+                       //\r
+                       byte mask = ((byte)(1<<bit_index));\r
+                       byte old_check_bits = check_bits;\r
+                       if( is_on ) {\r
+                               check_bits |= mask;\r
+                       }\r
+                       else {\r
+                               check_bits &= ~mask;\r
+                       }\r
+                       if( old_check_bits == check_bits ) {\r
+                               // No bits changed\r
+                               return;\r
+                       }\r
+                       // Indicator bits: x6543210     6:Bass||_  5:[]  4:+  3:O  2:_ii  1:__  0:||_\r
+                       //\r
+                       byte indicator_bits = 0;\r
+                       if( (check_bits & 1) != 0 ) {\r
+                               if( (check_bits & 7) == 7 ) { // All triad notes appared\r
+                                       //\r
+                                       // Draw square\r
+                                       indicator_bits |= 0x20;\r
+                                       //\r
+                                       // Draw different-colored vertical lines\r
+                                       if( indicator_color_indices[0] != indicator_color_indices[1] ) {\r
+                                               indicator_bits |= 1;\r
+                                       }\r
+                                       if( indicator_color_indices[2] != indicator_color_indices[1] ) {\r
+                                               indicator_bits |= 4;\r
+                                       }\r
+                               }\r
+                               else if( !isSus4 ) {\r
+                                       //\r
+                                       // Draw vertical lines  || ii\r
+                                       indicator_bits |= 5;\r
+                                       //\r
+                                       if( (check_bits & 2) != 0 && (!is_minor || (check_bits & 0x18) != 0) ) {\r
+                                               //\r
+                                               // Draw horizontal bottom lines __\r
+                                               indicator_bits |= 2;\r
+                                       }\r
+                               }\r
+                               if( !isSus4 ) {\r
+                                       if( is_minor || (check_bits & 2) != 0 ) {\r
+                                               indicator_bits |= (byte)(check_bits & 0x18);  // Copy bit 3 and bit 4\r
+                                       }\r
+                                       if( (check_bits & 0x40) != 0 ) {\r
+                                               indicator_bits |= 0x40; // Bass\r
+                                       }\r
+                               }\r
+                       }\r
+                       if( this.indicator_bits == indicator_bits ) {\r
+                               // No shapes changed\r
+                               return;\r
+                       }\r
+                       this.indicator_bits = indicator_bits;\r
+                       repaint();\r
+               }\r
+               public void setBackground(int i) {\r
+                       switch( i ) {\r
+                       case  0:\r
+                       case  1:\r
+                       case  2:\r
+                       case  3:\r
+                               super.setBackground(currentColorset.backgrounds[i]);\r
+                               setOpaque(true);\r
+                               break;\r
+                       default: return;\r
+                       }\r
+               }\r
+               public void setSelection(boolean is_selected) {\r
+                       this.is_selected = is_selected;\r
+                       setSelection();\r
+               }\r
+               public void setSelection() {\r
+                       setForeground(currentColorset.foregrounds[this.is_selected?1:0]);\r
+               }\r
+               public void setBold(boolean is_bold) {\r
+                       setFont( is_bold ? bold_font : plain_font );\r
+               }\r
+               public void keyChanged() {\r
+                       int co5_key = capoKey.toCo5();\r
+                       int co5_offset = co5_value - co5_key;\r
+                       in_active_zone = (co5_offset <= 6 && co5_offset >= -6) ;\r
+                       int root_note = chord.rootNoteSymbol().toNoteNumber();\r
+                       //\r
+                       // Reconstruct color index\r
+                       //\r
+                       // Root\r
+                       indicator_color_indices[0] = Music.isOnScale(\r
+                               root_note, co5_key\r
+                       ) ? 0 : co5_offset > 0 ? 1 : 2;\r
+                       //\r
+                       // 3rd / sus4\r
+                       indicator_color_indices[1] = Music.isOnScale(\r
+                               root_note+(is_minor?3:isSus4?5:4), co5_key\r
+                       ) ? 0 : co5_offset > 0 ? 1 : 2;\r
+                       //\r
+                       // P5th\r
+                       indicator_color_indices[2] = Music.isOnScale(\r
+                               root_note+7, co5_key\r
+                       ) ? 0 : co5_offset > 0 ? 1 : 2;\r
+                       //\r
+                       // dim5th\r
+                       indicator_color_indices[3] = Music.isOnScale(\r
+                               root_note+6, co5_key\r
+                       ) ? 0 : co5_offset > 4 ? 1 : 2;\r
+                       //\r
+                       // aug5th\r
+                       indicator_color_indices[4] = Music.isOnScale(\r
+                               root_note+8, co5_key\r
+                       ) ? 0 : co5_offset > -3 ? 1 : 2;\r
+               }\r
+       }\r
+\r
+       /**\r
+        * 色セット(ダークモード切替対応)\r
+        */\r
+       class ColorSet {\r
+               Color[] focus = new Color[2];   // 0:lost 1:gained\r
+               Color[] foregrounds = new Color[2];     // 0:unselected 1:selected\r
+               Color[] backgrounds = new Color[4]; // 0:remote 1:left 2:local 3:right\r
+               Color[] indicators = new Color[3];      // 0:natural 1:sharp 2:flat\r
+       }\r
+       ColorSet normalModeColorset = new ColorSet() {\r
+               {\r
+                       foregrounds[0] = null;\r
+                       foregrounds[1] = new Color(0xFF,0x3F,0x3F);\r
+                       backgrounds[0] = new Color(0xCF,0xFF,0xCF);\r
+                       backgrounds[1] = new Color(0x9F,0xFF,0xFF);\r
+                       backgrounds[2] = new Color(0xFF,0xCF,0xCF);\r
+                       backgrounds[3] = new Color(0xFF,0xFF,0x9F);\r
+                       indicators[0] = new Color(0xFF,0x3F,0x3F);\r
+                       indicators[1] = new Color(0xCF,0x6F,0x00);\r
+                       indicators[2] = new Color(0x3F,0x3F,0xFF);\r
+                       focus[0] = null;\r
+                       focus[1] = getBackground().darker();\r
+               }\r
+       };\r
+       ColorSet darkModeColorset = new ColorSet() {\r
+               {\r
+                       foregrounds[0] = Color.gray.darker();\r
+                       foregrounds[1] = Color.pink.brighter();\r
+                       backgrounds[0] = Color.black;\r
+                       backgrounds[1] = new Color(0x00,0x18,0x18);\r
+                       backgrounds[2] = new Color(0x20,0x00,0x00);\r
+                       backgrounds[3] = new Color(0x18,0x18,0x00);\r
+                       indicators[0] = Color.pink;\r
+                       indicators[1] = Color.yellow;\r
+                       indicators[2] = Color.cyan;\r
+                       focus[0] = Color.black;\r
+                       focus[1] = getForeground().brighter();\r
+               }\r
+       };\r
+       ColorSet currentColorset = normalModeColorset;\r
+\r
+       /**\r
+        * カポ値選択コンボボックスのデータモデル\r
+        * (コードボタン側とコードダイアグラム側の両方から参照される)\r
+        */\r
+       ComboBoxModel<Integer> capoValueModel =\r
+               new DefaultComboBoxModel<Integer>() {\r
+                       {\r
+                               for( int i=1; i<=Music.SEMITONES_PER_OCTAVE-1; i++ )\r
+                                       addElement(i);\r
+                       }\r
+               };\r
+       /**\r
+        * カポ値選択コンボボックス(コードボタン側ビュー)\r
+        */\r
+       CapoSelecterView capoSelecter = new CapoSelecterView(capoValueModel) {\r
+               private void capoChanged() {\r
+                       ChordMatrix.this.capoChanged(getCapo());\r
+               }\r
+               {\r
+                       checkbox.addItemListener(\r
+                               new ItemListener() {\r
+                                       public void itemStateChanged(ItemEvent e) {capoChanged();}\r
+                               }\r
+                       );\r
+                       valueSelecter.addActionListener(\r
+                               new ActionListener() {\r
+                                       public void actionPerformed(ActionEvent e) {capoChanged();}\r
+                               }\r
+                       );\r
+               }\r
+       };\r
+\r
+       public ChordMatrix() {\r
+               int i, v;\r
+               Dimension buttonSize = new Dimension(28,26);\r
+               //\r
+               // Make key-signature labels and chord labels\r
+               Co5Label l;\r
+               for (i=0, v= -Music.SEMITONES_PER_OCTAVE; i<N_COLUMNS; i++, v++) {\r
+                       l = new Co5Label(v);\r
+                       l.addMouseListener(this);\r
+                       l.addMouseMotionListener(this);\r
+                       add( keysigLabels[i] = l );\r
+                       l.setPreferredSize(buttonSize);\r
+               }\r
+               int row;\r
+               for (i=0; i < N_COLUMNS * CHORD_BUTTON_ROWS; i++) {\r
+                       row = i / N_COLUMNS;\r
+                       v = i - (N_COLUMNS * row) - 12;\r
+                       Music.Chord chord = new Music.Chord(\r
+                               new Music.NoteSymbol(row==2 ? v+3 : v)\r
+                       );\r
+                       if( row==0 ) chord.setSus4();\r
+                       else if( row==2 ) chord.setMinorThird();\r
+                       ChordLabel cl = new ChordLabel(chord);\r
+                       cl.addMouseListener(this);\r
+                       cl.addMouseMotionListener(this);\r
+                       cl.addMouseWheelListener(this);\r
+                       add(chordLabels[i] = cl);\r
+                       cl.setPreferredSize(buttonSize);\r
+               }\r
+               setFocusable(true);\r
+               setOpaque(true);\r
+               addKeyListener(this);\r
+               addFocusListener(new FocusListener() {\r
+                       public void focusGained(FocusEvent e) {\r
+                               repaint();\r
+                       }\r
+                       public void focusLost(FocusEvent e) {\r
+                               selectedChord = selectedChordCapo = null;\r
+                               fireChordChanged();\r
+                               repaint();\r
+                       }\r
+               });\r
+               setLayout(new GridLayout( 4, N_COLUMNS, 2, 2 ));\r
+               setKeySignature( new Music.Key() );\r
+               //\r
+               // Make chord label selections index\r
+               //\r
+               int noteIndex;\r
+               ArrayList<ChordLabelSelection> al;\r
+               Music.Chord chord;\r
+               for( int note_no=0; note_no<chordLabelSelections.length; note_no++ ) {\r
+                       al = new ArrayList<ChordLabelSelection>();\r
+                       //\r
+                       // Root major/minor chords\r
+                       for( ChordLabel cl : chordLabels ) {\r
+                               if( ! cl.isSus4 && cl.chord.indexOf(note_no) == 0 ) {\r
+                                       al.add(new ChordLabelSelection( cl, 0 )); // Root\r
+                               }\r
+                       }\r
+                       // Root sus4 chords\r
+                       for( ChordLabel cl : chordLabels ) {\r
+                               if( cl.isSus4 && cl.chord.indexOf(note_no) == 0 ) {\r
+                                       al.add(new ChordLabelSelection( cl, 0 )); // Root\r
+                               }\r
+                       }\r
+                       // 3rd,sus4th,5th included chords\r
+                       for( ChordLabel cl : chordLabels ) {\r
+                               noteIndex = cl.chord.indexOf(note_no);\r
+                               if( noteIndex == 1 || noteIndex == 2 ) {\r
+                                       al.add(new ChordLabelSelection( cl, noteIndex )); // 3rd,sus4,P5\r
+                               }\r
+                       }\r
+                       // Diminished chords (major/minor chord button only)\r
+                       for( ChordLabel cl : chordLabels ) {\r
+                               if( cl.isSus4 ) continue;\r
+                               (chord = cl.chord.clone()).setFlattedFifth();\r
+                               if( chord.indexOf(note_no) == 2 ) {\r
+                                       al.add(new ChordLabelSelection( cl, 3 ));\r
+                               }\r
+                       }\r
+                       // Augumented chords (major chord button only)\r
+                       for( ChordLabel cl : chordLabels ) {\r
+                               if( cl.isSus4 || cl.is_minor ) continue;\r
+                               (chord = cl.chord.clone()).setSharpedFifth();\r
+                               if( chord.indexOf(note_no) == 2 ) {\r
+                                       al.add(new ChordLabelSelection( cl, 4 ));\r
+                               }\r
+                       }\r
+                       chordLabelSelections[note_no] = new ChordLabelSelections(al);\r
+               }\r
+       }\r
+       //\r
+       // MouseListener\r
+       public void mousePressed(MouseEvent e) {\r
+               Component obj = e.getComponent();\r
+               if( obj instanceof ChordLabel ) {\r
+                       ChordLabel cl = (ChordLabel)obj;\r
+                       Music.Chord chord = cl.chord.clone();\r
+                       if( (e.getModifiersEx() & InputEvent.BUTTON3_DOWN_MASK) != 0 ) {\r
+                               if( e.isShiftDown() ) chord.setMajorSeventh();\r
+                               else chord.setSeventh();\r
+                       }\r
+                       else if( e.isShiftDown() ) chord.setSixth();\r
+\r
+                       if( e.isControlDown() ) chord.setNinth();\r
+                       else chord.clearNinth();\r
+\r
+                       if( e.isAltDown() ) {\r
+                               if( cl.isSus4 ) {\r
+                                       chord.setMajorThird(); // To cancel sus4\r
+                                       chord.setSharpedFifth();\r
+                               }\r
+                               else chord.setFlattedFifth();\r
+                       }\r
+                       if( selectedChordLabel != null ) {\r
+                               selectedChordLabel.setSelection(false);\r
+                       }\r
+                       (selectedChordLabel = cl).setSelection(true);\r
+                       setSelectedChord(chord);\r
+               }\r
+               else if( obj instanceof Co5Label ) {\r
+                       int v = ((Co5Label)obj).co5Value;\r
+                       if( (e.getModifiersEx() & InputEvent.BUTTON3_DOWN_MASK) != 0 ) {\r
+                               setKeySignature( new Music.Key(Music.oppositeCo5(v)) );\r
+                       }\r
+                       else if ( v == key.toCo5() ) {\r
+                               //\r
+                               // Cancel selected chord\r
+                               //\r
+                               setSelectedChord( (Music.Chord)null );\r
+                       }\r
+                       else {\r
+                               // Change key\r
+                               setKeySignature( new Music.Key(v) );\r
+                       }\r
+               }\r
+               requestFocusInWindow();\r
+               repaint();\r
+       }\r
+       public void mouseReleased(MouseEvent e) {\r
+               destinationChordLabel = null;\r
+       }\r
+       public void mouseEntered(MouseEvent e) { }\r
+       public void mouseExited(MouseEvent e) { }\r
+       public void mouseClicked(MouseEvent e) { }\r
+       //\r
+       // MouseMotionListener\r
+       public void mouseDragged(MouseEvent e) {\r
+               Component obj = e.getComponent();\r
+               if( obj instanceof ChordLabel ) {\r
+                       ChordLabel l_src = (ChordLabel)obj;\r
+                       Component obj2 = this.getComponentAt(\r
+                               l_src.getX() + e.getX(),\r
+                               l_src.getY() + e.getY()\r
+                       );\r
+                       if( obj2 == this ) {\r
+                               //\r
+                               // Entered gap between chord buttons - do nothing\r
+                               //\r
+                               return;\r
+                       }\r
+                       ChordLabel l_dst =\r
+                               ( (obj2 instanceof ChordLabel ) ? (ChordLabel)obj2 : null );\r
+                       if( l_dst == l_src ) {\r
+                               //\r
+                               // Returned to original chord button\r
+                               //\r
+                               destinationChordLabel = null;\r
+                               return;\r
+                       }\r
+                       if( destinationChordLabel != null ) {\r
+                               //\r
+                               // Already touched another chord button\r
+                               //\r
+                               return;\r
+                       }\r
+                       Music.Chord chord = l_src.chord.clone();\r
+                       if( l_src.is_minor ) {\r
+                               if( l_dst == null ) { // Out of chord buttons\r
+                                       // mM7\r
+                                       chord.setMajorSeventh();\r
+                               }\r
+                               else if( l_src.co5_value < l_dst.co5_value ) { // Right\r
+                                       // m6\r
+                                       chord.setSixth();\r
+                               }\r
+                               else { // Left or up from minor to major\r
+                                       // m7\r
+                                       chord.setSeventh();\r
+                               }\r
+                       }\r
+                       else if( l_src.isSus4 ) {\r
+                               if( l_dst == null ) { // Out of chord buttons\r
+                                       return;\r
+                               }\r
+                               else if( ! l_dst.isSus4 ) { // Down from sus4 to major\r
+                                       chord.setMajorThird();\r
+                               }\r
+                               else if( l_src.co5_value < l_dst.co5_value ) { // Right\r
+                                       chord.setNinth();\r
+                               }\r
+                               else { // Left\r
+                                       // 7sus4\r
+                                       chord.setSeventh();\r
+                               }\r
+                       }\r
+                       else {\r
+                               if( l_dst == null ) { // Out of chord buttons\r
+                                       return;\r
+                               }\r
+                               else if( l_dst.isSus4 ) { // Up from major to sus4\r
+                                       chord.setNinth();\r
+                               }\r
+                               else if( l_src.co5_value < l_dst.co5_value ) { // Right\r
+                                       // M7\r
+                                       chord.setMajorSeventh();\r
+                               }\r
+                               else if( l_dst.is_minor ) { // Down from major to minor\r
+                                       // 6\r
+                                       chord.setSixth();\r
+                               }\r
+                               else { // Left\r
+                                       // 7\r
+                                       chord.setSeventh();\r
+                               }\r
+                       }\r
+                       if( chord.hasNinth() || (l_src.isSus4 && (l_dst == null || ! l_dst.isSus4) ) ) {\r
+                               if( (e.getModifiersEx() & InputEvent.BUTTON3_DOWN_MASK) != 0 ) {\r
+                                       if( e.isShiftDown() ) {\r
+                                               chord.setMajorSeventh();\r
+                                       }\r
+                                       else {\r
+                                               chord.setSeventh();\r
+                                       }\r
+                               }\r
+                               else if( e.isShiftDown() ) {\r
+                                       chord.setSixth();\r
+                               }\r
+                       }\r
+                       else {\r
+                               if( e.isControlDown() ) chord.setNinth(); else chord.clearNinth();\r
+                       }\r
+                       if( e.isAltDown() ) {\r
+                               if( l_src.isSus4 ) {\r
+                                       chord.setMajorThird();\r
+                                       chord.setSharpedFifth();\r
+                               }\r
+                               else {\r
+                                       chord.setFlattedFifth();\r
+                               }\r
+                       }\r
+                       setSelectedChord(chord);\r
+                       destinationChordLabel = (l_dst == null ? l_src : l_dst ) ;\r
+               }\r
+               else if( obj instanceof Co5Label ) {\r
+                       Co5Label l_src = (Co5Label)obj;\r
+                       Component obj2 = this.getComponentAt(\r
+                               l_src.getX() + e.getX(),\r
+                               l_src.getY() + e.getY()\r
+                       );\r
+                       if( !(obj2 instanceof Co5Label) ) {\r
+                               return;\r
+                       }\r
+                       Co5Label l_dst = (Co5Label)obj2;\r
+                       int v = l_dst.co5Value;\r
+                       if( (e.getModifiersEx() & InputEvent.BUTTON3_DOWN_MASK) != 0 ) {\r
+                               setKeySignature( new Music.Key(Music.oppositeCo5(v)) );\r
+                       }\r
+                       else {\r
+                               setKeySignature( new Music.Key(v) );\r
+                       }\r
+                       repaint();\r
+               }\r
+       }\r
+       public void mouseMoved(MouseEvent e) { }\r
+       public void mouseWheelMoved(MouseWheelEvent e) {\r
+               if( selectedChord != null ) {\r
+                       if( e.getWheelRotation() > 0 ) { // Wheel moved down\r
+                               if( --selectedNoteIndex < 0 ) {\r
+                                       selectedNoteIndex = selectedChord.numberOfNotes() - 1;\r
+                               }\r
+                       }\r
+                       else { // Wheel moved up\r
+                               if( ++selectedNoteIndex >= selectedChord.numberOfNotes() ) {\r
+                                       selectedNoteIndex = 0;\r
+                               }\r
+                       }\r
+                       fireChordChanged();\r
+               }\r
+       }\r
+       private int pcKeyNextShift7 = Music.Chord.ROOT;\r
+       public void keyPressed(KeyEvent e) {\r
+               int i = -1, i_col = -1, i_row = 1;\r
+               boolean shift_pressed = false; // True if Shift-key pressed or CapsLocked\r
+               char keyChar = e.getKeyChar();\r
+               int keyCode = e.getKeyCode();\r
+               ChordLabel cl = null;\r
+               Music.Chord chord = null;\r
+               int key_co5 = key.toCo5();\r
+               // System.out.println( keyChar + " Pressed on chord matrix" );\r
+               //\r
+               if( (i = "6 ".indexOf(keyChar)) >= 0 ) {\r
+                       selectedChord = selectedChordCapo = null;\r
+                       fireChordChanged();\r
+                       pcKeyNextShift7 = Music.Chord.ROOT;\r
+                       return;\r
+               }\r
+               else if( (i = "asdfghjkl;:]".indexOf(keyChar)) >= 0 ) {\r
+                       i_col = i + key_co5 + 7;\r
+               }\r
+               else if( (i = "ASDFGHJKL+*}".indexOf(keyChar)) >= 0 ) {\r
+                       i_col = i + key_co5 + 7;\r
+                       shift_pressed = true;\r
+               }\r
+               else if( (i = "zxcvbnm,./\\".indexOf(keyChar)) >=0 ) {\r
+                       i_col = i + key_co5 + 7;\r
+                       i_row = 2;\r
+               }\r
+               else if( (i = "ZXCVBNM<>?_".indexOf(keyChar)) >=0 ) {\r
+                       i_col = i + key_co5 + 7;\r
+                       i_row = 2;\r
+                       shift_pressed = true;\r
+               }\r
+               else if( (i = "qwertyuiop@[".indexOf(keyChar)) >= 0 ) {\r
+                       i_col = i + key_co5 + 7;\r
+                       i_row = 0;\r
+               }\r
+               else if( (i = "QWERTYUIOP`{".indexOf(keyChar)) >= 0 ) {\r
+                       i_col = i + key_co5 + 7;\r
+                       i_row = 0;\r
+                       shift_pressed = true;\r
+               }\r
+               else if( keyChar == '5' ) {\r
+                       pcKeyNextShift7 = Music.Chord.MAJOR_SEVENTH; return;\r
+               }\r
+               else if( keyChar == '7' ) {\r
+                       pcKeyNextShift7 = Music.Chord.SEVENTH; return;\r
+               }\r
+               // Shift current key-signature\r
+               else if( keyCode == KeyEvent.VK_LEFT || keyCode == KeyEvent.VK_KP_LEFT ) {\r
+                       // Add a flat\r
+                       setKeySignature( new Music.Key(key_co5-1) );\r
+                       return;\r
+               }\r
+               else if( keyCode == KeyEvent.VK_RIGHT || keyCode == KeyEvent.VK_KP_RIGHT ) {\r
+                       // Add a sharp\r
+                       setKeySignature( new Music.Key(key_co5+1) );\r
+                       return;\r
+               }\r
+               else if( keyCode == KeyEvent.VK_DOWN || keyCode == KeyEvent.VK_KP_DOWN ) {\r
+                       // Semitone down\r
+                       Music.Key key = new Music.Key(key_co5);\r
+                       key.transpose(-1);\r
+                       setKeySignature(key);\r
+                       return;\r
+               }\r
+               else if( keyCode == KeyEvent.VK_UP || keyCode == KeyEvent.VK_KP_UP ) {\r
+                       // Semitone up\r
+                       Music.Key key = new Music.Key(key_co5);\r
+                       key.transpose(1);\r
+                       setKeySignature(key);\r
+                       return;\r
+               }\r
+               if( i < 0 ) // No key char found\r
+                       return;\r
+               if( i_col < 0 ) i_col += 12; else if( i_col > N_COLUMNS ) i_col -= 12;\r
+               cl = chordLabels[i_col + N_COLUMNS * i_row];\r
+               chord = cl.chord.clone();\r
+               if( shift_pressed )\r
+                       chord.setSeventh();\r
+               else\r
+                       chord.offsets[Music.Chord.SEVENTH_OFFSET] = pcKeyNextShift7; // specify by previous key\r
+               if( e.isAltDown() ) {\r
+                       if( cl.isSus4 ) {\r
+                               chord.setMajorThird(); // To cancel sus4\r
+                               chord.setSharpedFifth();\r
+                       }\r
+                       else chord.setFlattedFifth();\r
+               }\r
+               if( e.isControlDown() ) { // Cannot use for ninth ?\r
+                       chord.setNinth();\r
+               }\r
+               if( selectedChordLabel != null ) clear();\r
+               (selectedChordLabel = cl).setSelection(true);\r
+               setSelectedChord(chord);\r
+               pcKeyNextShift7 = Music.Chord.ROOT;\r
+               return;\r
+       }\r
+       public void keyReleased(KeyEvent e) { }\r
+       public void keyTyped(KeyEvent e) { }\r
+\r
+       public void addChordMatrixListener(ChordMatrixListener l) {\r
+               listenerList.add(ChordMatrixListener.class, l);\r
+       }\r
+       public void removeChordMatrixListener(ChordMatrixListener l) {\r
+               listenerList.remove(ChordMatrixListener.class, l);\r
+       }\r
+       protected void fireChordChanged() {\r
+               Object[] listeners = listenerList.getListenerList();\r
+               for (int i = listeners.length-2; i>=0; i-=2) {\r
+                       if (listeners[i]==ChordMatrixListener.class) {\r
+                               ((ChordMatrixListener)listeners[i+1]).chordChanged();\r
+                       }\r
+               }\r
+               if( selectedChord == null ) clearIndicators();\r
+       }\r
+       protected void fireKeySignatureChanged() {\r
+               Object[] listeners = listenerList.getListenerList();\r
+               for (int i = listeners.length-2; i>=0; i-=2) {\r
+                       if (listeners[i]==ChordMatrixListener.class) {\r
+                               ((ChordMatrixListener)listeners[i+1]).keySignatureChanged();\r
+                       }\r
+               }\r
+       }\r
+       private Music.Key key = null;\r
+       private Music.Key capoKey = null;\r
+       public Music.Key getKeySignature() { return key; }\r
+       public Music.Key getKeySignatureCapo() { return capoKey; }\r
+       public void setKeySignature( Music.Key key ) {\r
+               if( key == null || this.key != null && key.equals(this.key) )\r
+                       return;\r
+               int i;\r
+               // Clear old value\r
+               if( this.key == null ) {\r
+                       for( i = 0; i < keysigLabels.length; i++ ) {\r
+                               keysigLabels[i].setBackground(false);\r
+                       }\r
+               }\r
+               else {\r
+                       keysigLabels[this.key.toCo5() + 12].setSelection(false);\r
+                       for( i = Music.mod12(this.key.toCo5()); i < N_COLUMNS; i+=12 ) {\r
+                               keysigLabels[i].setBackground(false);\r
+                       }\r
+               }\r
+               // Set new value\r
+               keysigLabels[i = key.toCo5() + 12].setSelection(true);\r
+               for( i = Music.mod12(key.toCo5()); i < N_COLUMNS; i+=12 ) {\r
+                       keysigLabels[i].setBackground(true);\r
+               }\r
+               // Change chord-label's color & font\r
+               int i_color, old_i_color;\r
+               for( ChordLabel cl : chordLabels ) {\r
+                       i_color = ((cl.co5_value - key.toCo5() + 31)/3) & 3;\r
+                       if( this.key != null ) {\r
+                               old_i_color = ((cl.co5_value - this.key.toCo5() + 31)/3) & 3;\r
+                               if( i_color != old_i_color ) {\r
+                                       cl.setBackground(i_color);\r
+                               }\r
+                       }\r
+                       else cl.setBackground(i_color);\r
+                       if( !(cl.isSus4) ) {\r
+                               if( this.key != null && Music.mod12(cl.co5_value - this.key.toCo5()) == 0)\r
+                                       cl.setBold(false);\r
+                               if( Music.mod12( cl.co5_value - key.toCo5() ) == 0 )\r
+                                       cl.setBold(true);\r
+                       }\r
+               }\r
+               this.capoKey = (this.key = key).clone().transpose(capoSelecter.getCapo());\r
+               for( ChordLabel cl : chordLabels ) cl.keyChanged();\r
+               fireKeySignatureChanged();\r
+       }\r
+       private int capo = 0;\r
+       /**\r
+        * カポ位置の変更処理\r
+        * @param newCapo 新しいカポ位置\r
+        */\r
+       protected void capoChanged(int newCapo) {\r
+               if( this.capo == newCapo )\r
+                       return;\r
+               (this.capoKey = this.key.clone()).transpose(this.capo = newCapo);\r
+               selectedChordCapo = (\r
+                       selectedChord == null ? null : selectedChord.clone().transpose(newCapo)\r
+               );\r
+               for( ChordLabel cl : chordLabels ) cl.keyChanged();\r
+               fireKeySignatureChanged();\r
+       }\r
+\r
+       /**\r
+        * コードサフィックスのヘルプ\r
+        */\r
+       public ChordGuide chordGuide = new ChordGuide(this);\r
+\r
+       /**\r
+        * ドラッグ先コードボタン\r
+        */\r
+       private ChordLabel      destinationChordLabel = null;\r
+       /**\r
+        * ドラッグされたかどうか調べます。\r
+        * @return ドラッグ先コードボタンがあればtrue\r
+        */\r
+       public boolean isDragged() {\r
+               return destinationChordLabel != null ;\r
+       }\r
+\r
+       private boolean isDark = false;\r
+       public void setDarkMode(boolean is_dark) {\r
+               this.isDark = is_dark;\r
+               currentColorset = (is_dark ? darkModeColorset : normalModeColorset);\r
+               setBackground( currentColorset.focus[0] );\r
+               Music.Key prev_key = key;\r
+               key = null;\r
+               setKeySignature(prev_key);\r
+               for( int i=0; i < keysigLabels.length; i++ ) keysigLabels[i].setSelection();\r
+               for( int i=0; i <  chordLabels.length; i++ ) chordLabels[i].setSelection();\r
+               chordGuide.setDarkMode( is_dark );\r
+               chordDisplay.setDarkMode( is_dark );\r
+               Color col = is_dark ? Color.black : null;\r
+               capoSelecter.setBackground( col );\r
+               capoSelecter.valueSelecter.setBackground( col );\r
+       }\r
+\r
+       private boolean isPlaying = false;\r
+       public boolean isPlaying() { return isPlaying; }\r
+       public void setPlaying(boolean is_playing) {\r
+               this.isPlaying = is_playing;\r
+               repaint();\r
+       }\r
+\r
+       private byte currentBeat = 0;\r
+       private byte timesigUpper = 4;\r
+       public void setBeat(byte beat, byte tsu) {\r
+               if( currentBeat == beat && timesigUpper == tsu )\r
+                       return;\r
+               timesigUpper = tsu;\r
+               currentBeat = beat;\r
+               keysigLabels[ key.toCo5() + 12 ].repaint();\r
+       }\r
+\r
+       private ChordLabel      selectedChordLabel = null;\r
+       public JComponent getSelectedButton() {\r
+               return selectedChordLabel;\r
+       }\r
+       private Music.Chord     selectedChord = null;\r
+       public Music.Chord getSelectedChord() {\r
+               return selectedChord;\r
+       }\r
+       private Music.Chord     selectedChordCapo = null;\r
+       public Music.Chord getSelectedChordCapo() {\r
+               return selectedChordCapo;\r
+       }\r
+       public void setSelectedChordCapo( Music.Chord chord ) {\r
+               setNoteIndex(-1); // Cancel arpeggio mode\r
+               selectedChord = (chord == null ? null : chord.clone().transpose(-capo,capoKey));\r
+               selectedChordCapo = chord;\r
+               fireChordChanged();\r
+       }\r
+       public void setSelectedChord( Music.Chord chord ) {\r
+               setNoteIndex(-1); // Cancel arpeggio mode\r
+               selectedChord = chord;\r
+               selectedChordCapo = (chord == null ? null : chord.clone().transpose(capo,key));\r
+               fireChordChanged();\r
+       }\r
+       public void setSelectedChord( String chordSymbol ) {\r
+               Music.Chord chord = null;\r
+               if( chordSymbol != null && ! chordSymbol.isEmpty() ) {\r
+                       try {\r
+                               chord = new Music.Chord(chordSymbol);\r
+                       } catch( IllegalArgumentException ex ) {\r
+                               // Ignored\r
+                       }\r
+               }\r
+               setSelectedChord(chord);\r
+       }\r
+\r
+       private int selectedNoteIndex = -1;\r
+       public int getNoteIndex() {\r
+               return selectedChord == null || selectedNoteIndex < 0 ? -1 : selectedNoteIndex;\r
+       }\r
+       public void setNoteIndex(int noteIndex) {\r
+               selectedNoteIndex = noteIndex;\r
+       }\r
+       public void clear() {\r
+               if( selectedChordLabel != null ) {\r
+                       selectedChordLabel.setSelection(false);\r
+                       selectedChordLabel = null;\r
+               }\r
+               selectedChord = null; selectedNoteIndex = -1;\r
+       }\r
+}\r
+\r
+/**\r
+ * コードボタン用のマトリクス外のラベル\r
+ */\r
+class ChordButtonLabel extends JLabel {\r
+       private ChordMatrix cm;\r
+       public ChordButtonLabel(String txt, ChordMatrix cm) {\r
+               super(txt,CENTER);\r
+               this.cm = cm;\r
+               setOpaque(true);\r
+               setFont(getFont().deriveFont(Font.PLAIN));\r
+               setDarkMode(false);\r
+       }\r
+       public void setDarkMode(boolean isDark) {\r
+               setBackground( isDark ?\r
+                       cm.darkModeColorset.backgrounds[2] :\r
+                       cm.normalModeColorset.backgrounds[2]\r
+               );\r
+               setForeground( isDark ?\r
+                       cm.darkModeColorset.foregrounds[0] :\r
+                       cm.normalModeColorset.foregrounds[0]\r
+               );\r
+       }\r
+}\r
+/**\r
+ * コードサフィックスのヘルプ\r
+ */\r
+class ChordGuide extends JPanel {\r
+       private class ChordGuideLabel extends ChordButtonLabel {\r
+               private JPopupMenu popupMenu = new JPopupMenu();\r
+               public ChordGuideLabel(String txt, ChordMatrix cm) {\r
+                       super(txt,cm);\r
+                       addMouseListener(\r
+                               new MouseAdapter() {\r
+                                       public void mousePressed(MouseEvent e) {\r
+                                               popupMenu.show( e.getComponent(), 0, getHeight() );\r
+                                       }\r
+                               }\r
+                       );\r
+               }\r
+               public void addMenu(JMenuItem menuItem) { popupMenu.add(menuItem); }\r
+               public void addSeparator() { popupMenu.addSeparator(); }\r
+       }\r
+       private ChordGuideLabel guide76, guide5, guide9;\r
+       public ChordGuide(ChordMatrix cm) {\r
+               guide76 = new ChordGuideLabel(" 6  7  M7 ",cm) {\r
+                       {\r
+                               setToolTipText("How to add 7th, major 7th, 6th");\r
+                               addMenu(new JMenuItem("7        = <RightClick>"));\r
+                               addMenu(new JMenuItem("M7(maj7) = [Shift] <RightClick>"));\r
+                               addMenu(new JMenuItem("6        = [Shift]"));\r
+                       }\r
+               };\r
+               guide5 = new ChordGuideLabel(" -5 dim +5 aug ",cm){\r
+                       {\r
+                               setToolTipText("How to add -5, dim, +5, aug");\r
+                               addMenu(new JMenuItem("-5 (b5)      = [Alt]"));\r
+                               addMenu(new JMenuItem("+5 (#5/aug)  = [Alt] sus4"));\r
+                               addSeparator();\r
+                               addMenu(new JMenuItem("dim  (m-5)  = [Alt] minor"));\r
+                               addMenu(new JMenuItem("dim7 (m6-5) = [Alt] [Shift] minor"));\r
+                               addMenu(new JMenuItem("m7-5 = [Alt] minor <RightClick>"));\r
+                               addMenu(new JMenuItem("aug7 (7+5)  = [Alt] sus4 <RightClick>"));\r
+                       }\r
+               };\r
+               guide9 = new ChordGuideLabel(" add9 ",cm) {\r
+                       {\r
+                               setToolTipText("How to add 9th");\r
+                               addMenu(new JMenuItem("add9  = [Ctrl]"));\r
+                               addSeparator();\r
+                               addMenu(new JMenuItem("9     = [Ctrl] <RightClick>"));\r
+                               addMenu(new JMenuItem("M9    = [Ctrl] [Shift] <RightClick>"));\r
+                               addMenu(new JMenuItem("69    = [Ctrl] [Shift]"));\r
+                               addMenu(new JMenuItem("dim9  = [Ctrl] [Shift] [Alt] minor"));\r
+                       }\r
+               };\r
+               setLayout(new BoxLayout(this, BoxLayout.X_AXIS));\r
+               add(guide76);\r
+               add( Box.createHorizontalStrut(2) );\r
+               add(guide5);\r
+               add( Box.createHorizontalStrut(2) );\r
+               add(guide9);\r
+       }\r
+       public void setDarkMode(boolean is_dark) {\r
+               setBackground( is_dark ? Color.black : null );\r
+               guide76.setDarkMode( is_dark );\r
+               guide5.setDarkMode( is_dark );\r
+               guide9.setDarkMode( is_dark );\r
+       }\r
+}\r
+\r
diff --git a/src/MIDIDevice.java b/src/MIDIDevice.java
new file mode 100644 (file)
index 0000000..a1caed4
--- /dev/null
@@ -0,0 +1,1665 @@
+\r
+import java.awt.BasicStroke;\r
+import java.awt.Color;\r
+import java.awt.Component;\r
+import java.awt.Graphics;\r
+import java.awt.Graphics2D;\r
+import java.awt.Insets;\r
+import java.awt.Point;\r
+import java.awt.Rectangle;\r
+import java.awt.Stroke;\r
+import java.awt.datatransfer.DataFlavor;\r
+import java.awt.datatransfer.Transferable;\r
+import java.awt.dnd.DnDConstants;\r
+import java.awt.dnd.DragGestureEvent;\r
+import java.awt.dnd.DragGestureListener;\r
+import java.awt.dnd.DragSource;\r
+import java.awt.dnd.DropTarget;\r
+import java.awt.dnd.DropTargetDragEvent;\r
+import java.awt.dnd.DropTargetDropEvent;\r
+import java.awt.dnd.DropTargetEvent;\r
+import java.awt.dnd.DropTargetListener;\r
+import java.awt.event.ActionEvent;\r
+import java.awt.event.ActionListener;\r
+import java.awt.event.ComponentAdapter;\r
+import java.awt.event.ComponentEvent;\r
+import java.awt.event.ComponentListener;\r
+import java.beans.PropertyVetoException; // PropertyVetoException\r
+import java.util.Hashtable;\r
+import java.util.List;\r
+import java.util.Vector; // Vector\r
+\r
+import javax.sound.midi.InvalidMidiDataException;\r
+import javax.sound.midi.MidiChannel;\r
+import javax.sound.midi.MidiDevice;\r
+import javax.sound.midi.MidiMessage;\r
+import javax.sound.midi.MidiSystem;\r
+import javax.sound.midi.MidiUnavailableException;\r
+import javax.sound.midi.Receiver;\r
+import javax.sound.midi.Sequencer;\r
+import javax.sound.midi.ShortMessage;\r
+import javax.sound.midi.Synthesizer;\r
+import javax.sound.midi.SysexMessage;\r
+import javax.sound.midi.Transmitter;\r
+import javax.swing.AbstractListModel;\r
+import javax.swing.BoxLayout;\r
+import javax.swing.Icon;\r
+import javax.swing.JButton;\r
+import javax.swing.JComponent;\r
+import javax.swing.JDesktopPane;\r
+import javax.swing.JDialog;\r
+import javax.swing.JEditorPane;\r
+import javax.swing.JInternalFrame;\r
+import javax.swing.JLabel;\r
+import javax.swing.JLayeredPane;\r
+import javax.swing.JList;\r
+import javax.swing.JOptionPane;\r
+import javax.swing.JPanel;\r
+import javax.swing.JScrollPane;\r
+import javax.swing.JSplitPane;\r
+import javax.swing.JTree;\r
+import javax.swing.ListCellRenderer;\r
+import javax.swing.ListSelectionModel;\r
+import javax.swing.event.EventListenerList;\r
+import javax.swing.event.InternalFrameAdapter;\r
+import javax.swing.event.InternalFrameEvent;\r
+import javax.swing.event.InternalFrameListener;\r
+import javax.swing.event.ListDataEvent;\r
+import javax.swing.event.ListDataListener;\r
+import javax.swing.event.TreeModelEvent;\r
+import javax.swing.event.TreeModelListener;\r
+import javax.swing.event.TreeSelectionEvent;\r
+import javax.swing.event.TreeSelectionListener;\r
+import javax.swing.tree.DefaultTreeCellRenderer;\r
+import javax.swing.tree.TreeModel;\r
+import javax.swing.tree.TreePath;\r
+\r
+/**\r
+ * 仮想MIDIデバイス\r
+ */\r
+interface VirtualMidiDevice extends MidiDevice {\r
+       MidiChannel[] getChannels();\r
+       void sendMidiMessage( MidiMessage msg );\r
+       void setReceiver(Receiver rx);\r
+}\r
+/**\r
+ * 仮想MIDIデバイスの最小限の実装を提供するクラス\r
+ */\r
+abstract class AbstractVirtualMidiDevice implements VirtualMidiDevice {\r
+       protected boolean is_open = false;\r
+       protected long top_microsecond = -1;\r
+       protected Info info;\r
+\r
+       private int maxTransmitters = -1;\r
+       protected List<Transmitter> txList = new Vector<Transmitter>();\r
+       protected MidiChannelMessageSender[]\r
+               channels = new MidiChannelMessageSender[MIDISpec.MAX_CHANNELS];\r
+\r
+       private int maxReceivers = 1;\r
+       protected List<Receiver> rxList = new Vector<Receiver>();\r
+\r
+       protected AbstractVirtualMidiDevice() {\r
+               for( int i=0; i<channels.length; i++ )\r
+                       channels[i] = new MidiChannelMessageSender(this,i);\r
+       }\r
+       protected void setMaxReceivers(int max_rx) {\r
+               maxReceivers = max_rx;\r
+       }\r
+       protected void setMaxTransmitters(int max_tx) {\r
+               maxTransmitters = max_tx;\r
+       }\r
+       public void open() {\r
+               is_open = true;\r
+               top_microsecond = System.nanoTime()/1000;\r
+       }\r
+       public void close() {\r
+               txList.clear();\r
+               is_open = false;\r
+       }\r
+       public boolean isOpen() { return is_open; }\r
+       public Info getDeviceInfo() { return info; }\r
+       public long getMicrosecondPosition() {\r
+               return (top_microsecond == -1 ? -1: System.nanoTime()/1000 - top_microsecond);\r
+       }\r
+       public int getMaxReceivers() { return maxReceivers; }\r
+       public Receiver getReceiver() {\r
+               return rxList.isEmpty() ? null : rxList.get(0);\r
+       }\r
+       public List<Receiver> getReceivers() { return rxList; }\r
+       public int getMaxTransmitters() { return maxTransmitters; }\r
+       public Transmitter getTransmitter() throws MidiUnavailableException {\r
+               if( maxTransmitters == 0 ) {\r
+                       throw new MidiUnavailableException();\r
+               }\r
+               Transmitter new_tx = new Transmitter() {\r
+                       private Receiver rx = null;\r
+                       public void close() { txList.remove(this); }\r
+                       public Receiver getReceiver() { return rx; }\r
+                       public void setReceiver(Receiver rx) { this.rx = rx; }\r
+               };\r
+               txList.add(new_tx);\r
+               return new_tx;\r
+       }\r
+       public List<Transmitter> getTransmitters() { return txList; }\r
+       public MidiChannel[] getChannels() { return channels; }\r
+       public void sendMidiMessage( MidiMessage msg ) {\r
+               long time_stamp = getMicrosecondPosition();\r
+               for( Transmitter tx : txList ) {\r
+                       Receiver rx = tx.getReceiver();\r
+                       if( rx != null )\r
+                               rx.send( msg, time_stamp );\r
+               }\r
+       }\r
+       public void setReceiver(Receiver rx) {\r
+               if( maxReceivers == 0 )\r
+                       return;\r
+               if( ! rxList.isEmpty() )\r
+                       rxList.clear();\r
+               rxList.add(rx);\r
+       }\r
+}\r
+\r
+/**\r
+ * 仮想MIDIデバイスからのMIDIチャンネルメッセージ送信クラス\r
+ */\r
+class MidiChannelMessageSender implements MidiChannel {\r
+       /**\r
+        * このMIDIチャンネルの親となる仮想MIDIデバイス\r
+        */\r
+       private VirtualMidiDevice vmd;\r
+       /**\r
+        * MIDIチャンネルインデックス(チャンネル 1 のとき 0)\r
+        */\r
+       private int channel;\r
+       /**\r
+        * 指定の仮想MIDIデバイスの指定のMIDIチャンネルの\r
+        * メッセージを送信するためのインスタンスを構築します。\r
+        * @param vmd 仮想MIDIデバイス\r
+        * @param channel MIDIチャンネルインデックス(チャンネル 1 のとき 0)\r
+        */\r
+       public MidiChannelMessageSender(VirtualMidiDevice vmd, int channel) {\r
+               this.vmd = vmd;\r
+               this.channel = channel;\r
+       }\r
+       /**\r
+        * 仮想MIDIデバイスからこのMIDIチャンネルのショートメッセージを送信します。\r
+        * @param command このメッセージで表される MIDI コマンド\r
+        * @param data1 第 1 データバイト\r
+        * @param data2 第 2 データバイト\r
+        * @see ShortMessage#setMessage(int, int, int, int)\r
+        */\r
+       public void sendShortMessage(int command, int data1, int data2) {\r
+               ShortMessage short_msg = new ShortMessage();\r
+               try {\r
+                       short_msg.setMessage( command, channel, data1, data2 );\r
+               } catch(InvalidMidiDataException e) {\r
+                       e.printStackTrace();\r
+                       return;\r
+               }\r
+               vmd.sendMidiMessage((MidiMessage)short_msg);\r
+       }\r
+       public void noteOff( int note_no ) { noteOff( note_no, 64 ); }\r
+       public void noteOff( int note_no, int velocity ) {\r
+               sendShortMessage( ShortMessage.NOTE_OFF, note_no, velocity );\r
+       }\r
+       public void noteOn( int note_no, int velocity ) {\r
+               sendShortMessage( ShortMessage.NOTE_ON, note_no, velocity );\r
+       }\r
+       public void setPolyPressure(int note_no, int pressure) {\r
+               sendShortMessage( ShortMessage.POLY_PRESSURE, note_no, pressure );\r
+       }\r
+       public int getPolyPressure(int noteNumber) { return 0x40; }\r
+       public void controlChange(int controller, int value) {\r
+               sendShortMessage( ShortMessage.CONTROL_CHANGE, controller, value );\r
+       }\r
+       public int getController(int controller) { return 0x40; }\r
+       public void programChange( int program ) {\r
+               sendShortMessage( ShortMessage.PROGRAM_CHANGE, program, 0 );\r
+       }\r
+       public void programChange(int bank, int program) {\r
+               controlChange( 0x00, ((bank>>7) & 0x7F) );\r
+               controlChange( 0x20, (bank & 0x7F) );\r
+               programChange( program );\r
+       }\r
+       public int getProgram() { return 0; }\r
+       public void setChannelPressure(int pressure) {\r
+               sendShortMessage( ShortMessage.CHANNEL_PRESSURE, pressure, 0 );\r
+       }\r
+       public int getChannelPressure() { return 0x40; }\r
+       public void setPitchBend(int bend) {\r
+               // NOTE: Pitch Bend data byte order is Little Endian\r
+               sendShortMessage(\r
+                       ShortMessage.PITCH_BEND,\r
+                       (bend & 0x7F), ((bend>>7) & 0x7F)\r
+               );\r
+       }\r
+       public int getPitchBend() { return MIDISpec.PITCH_BEND_NONE; }\r
+       public void allSoundOff() { controlChange( 0x78, 0 ); }\r
+       public void resetAllControllers() { controlChange( 0x79, 0 ); }\r
+       public boolean localControl(boolean on) {\r
+               controlChange( 0x7A, on ? 0x7F : 0x00 );\r
+               return false;\r
+       }\r
+       public void allNotesOff() { controlChange( 0x7B, 0 ); }\r
+       public void setOmni(boolean on) {\r
+               controlChange( on ? 0x7D : 0x7C, 0 );\r
+       }\r
+       public boolean getOmni() { return false; }\r
+       public void setMono(boolean on) {}\r
+       public boolean getMono() { return false; }\r
+       public void setMute(boolean mute) {}\r
+       public boolean getMute() { return false; }\r
+       public void setSolo(boolean soloState) {}\r
+       public boolean getSolo() { return false; }\r
+}\r
+\r
+/**\r
+ * 仮想 MIDI デバイスからの MIDI 受信とチャンネル状態の管理\r
+ */\r
+abstract class AbstractMidiStatus extends Vector<AbstractMidiChannelStatus>\r
+       implements Receiver\r
+{\r
+       private void resetStatus() { resetStatus(false); }\r
+       private void resetStatus(boolean is_GS) {\r
+               for( AbstractMidiChannelStatus mcs : this )\r
+                       mcs.resetAllValues(is_GS);\r
+       }\r
+       public void close() { }\r
+       public void send(MidiMessage message, long timeStamp) {\r
+               if ( message instanceof ShortMessage ) {\r
+                       ShortMessage sm = (ShortMessage)message;\r
+                       switch ( sm.getCommand() ) {\r
+\r
+                       case ShortMessage.NOTE_ON:\r
+                               get(sm.getChannel()).noteOn( sm.getData1(), sm.getData2() );\r
+                               break;\r
+\r
+                       case ShortMessage.NOTE_OFF:\r
+                               get(sm.getChannel()).noteOff( sm.getData1(), sm.getData2() );\r
+                               break;\r
+\r
+                       case ShortMessage.CONTROL_CHANGE:\r
+                               get(sm.getChannel()).controlChange( sm.getData1(), sm.getData2() );\r
+                               break;\r
+\r
+                       case ShortMessage.PROGRAM_CHANGE:\r
+                               get(sm.getChannel()).programChange( sm.getData1() );\r
+                               break;\r
+\r
+                       case ShortMessage.PITCH_BEND:\r
+                               get(sm.getChannel()).setPitchBend(\r
+                                       ( sm.getData1() & 0x7F ) + ( (sm.getData2() & 0x7F) << 7 )\r
+                               );\r
+                               break;\r
+\r
+                               /* Pressure系も受信したい場合、この部分を有効にする\r
+      case ShortMessage.POLY_PRESSURE:\r
+        get(sm.getChannel()).setPolyPressure( sm.getData1(), sm.getData2() );\r
+        break;\r
+      case ShortMessage.CHANNEL_PRESSURE:\r
+        get(sm.getChannel()).setChannelPressure( sm.getData1() );\r
+        break;\r
+                                */\r
+                       }\r
+               }\r
+               else if ( message instanceof SysexMessage ) {\r
+                       SysexMessage sxm = (SysexMessage)message;\r
+                       switch ( sxm.getStatus() ) {\r
+\r
+                       case SysexMessage.SYSTEM_EXCLUSIVE:\r
+                               byte data[] = sxm.getData();\r
+                               switch( data[0] ) {\r
+                               case 0x7E: // Non-Realtime Universal System Exclusive Message\r
+                                       if( data[2] == 0x09 ) { // General MIDI (GM)\r
+                                               if( data[3] == 0x01 ) { // GM System ON\r
+                                                       resetStatus();\r
+                                               }\r
+                                               else if( data[3] == 0x02 ) { // GM System OFF\r
+                                                       resetStatus();\r
+                                               }\r
+                                       }\r
+                                       break;\r
+                               case 0x41: // Roland\r
+                                       if( data[2]==0x42 && data[3]==0x12 ) { // GS DT1\r
+                                               if( data[4]==0x40 && data[5]==0x00 && data[6]==0x7F &&\r
+                                                               data[7]==0x00 && data[8]==0x41\r
+                                                               ) {\r
+                                                       resetStatus(true);\r
+                                               }\r
+                                               else if( data[4]==0x40 && (data[5] & 0xF0)==0x10 && data[6]==0x15 ) {\r
+                                                       // Drum Map 1 or 2, otherwise Normal Part\r
+                                                       boolean is_rhythm_part = ( data[7]==1 || data[7]==2 );\r
+                                                       int ch = (data[5] & 0x0F);\r
+                                                       if( ch == 0 ) ch = 9; else if( ch <= 9 ) ch--;\r
+                                                       get(ch).setRhythmPart(is_rhythm_part);\r
+                                               }\r
+                                               else if( data[4]==0x00 && data[5]==0x00 && data[6]==0x7F ) {\r
+                                                       if( data[7]==0x00 && data[8]==0x01 ) {\r
+                                                               // GM System Mode Set (1)\r
+                                                               resetStatus(true);\r
+                                                       }\r
+                                                       if( data[7]==0x01 && data[8]==0x00 ) {\r
+                                                               // GM System Mode Set (2)\r
+                                                               resetStatus(true);\r
+                                                       }\r
+                                               }\r
+                                       }\r
+                                       break;\r
+                               case 0x43: // Yamaha\r
+                                       if( data[2] == 0x4C\r
+                                       && data[3]==0 && data[4]==0 && data[5]==0x7E\r
+                                       && data[6]==0\r
+                                                       ) {\r
+                                               // XG System ON\r
+                                               resetStatus();\r
+                                       }\r
+                                       break;\r
+                               }\r
+                               break;\r
+                       }\r
+               }\r
+       }\r
+}\r
+abstract class AbstractMidiChannelStatus implements MidiChannel {\r
+       protected int channel;\r
+       protected int program = 0;\r
+       protected int pitch_bend = MIDISpec.PITCH_BEND_NONE;\r
+       protected int controller_values[] = new int[0x80];\r
+       protected boolean is_rhythm_part = false;\r
+\r
+       protected static final int DATA_NONE = 0;\r
+       protected static final int DATA_FOR_RPN = 1;\r
+       protected final int DATA_FOR_NRPN = 2;\r
+       protected int data_for = DATA_NONE;\r
+\r
+       public AbstractMidiChannelStatus(int channel) {\r
+               this.channel = channel;\r
+               resetAllValues(true);\r
+       }\r
+       public int getChannel() { return channel; }\r
+       public boolean isRhythmPart() { return is_rhythm_part; }\r
+       public void setRhythmPart(boolean is_rhythm_part) {\r
+               this.is_rhythm_part = is_rhythm_part;\r
+       }\r
+       public void resetRhythmPart() {\r
+               is_rhythm_part = (channel == 9);\r
+       }\r
+       public void resetAllValues() { resetAllValues(false); }\r
+       public void resetAllValues(boolean is_GS) {\r
+               for( int i=0; i<controller_values.length; i++ )\r
+                       controller_values[i] = 0;\r
+               if( is_GS ) resetRhythmPart();\r
+               resetAllControllers();\r
+               controller_values[10] = 0x40; // Set pan to center\r
+       }\r
+       public void fireRpnChanged() {}\r
+       protected void changeRPNData( int data_diff ) {\r
+               int data_msb = controller_values[0x06];\r
+               int data_lsb = controller_values[0x26];\r
+               if( data_diff != 0 ) {\r
+                       // Data increment or decrement\r
+                       data_lsb += data_diff;\r
+                       if( data_lsb >= 100 ) {\r
+                               data_lsb = 0;\r
+                               controller_values[0x26] = ++data_msb;\r
+                       }\r
+                       else if( data_lsb < 0 ) {\r
+                               data_lsb = 0;\r
+                               controller_values[0x26] = --data_msb;\r
+                       }\r
+                       controller_values[0x06] = data_lsb;\r
+               }\r
+               fireRpnChanged();\r
+       }\r
+       @Override\r
+       public void noteOff( int note_no ) {}\r
+       @Override\r
+       public void noteOff( int note_no, int velocity ) {}\r
+       @Override\r
+       public void noteOn( int note_no, int velocity ) {}\r
+       @Override\r
+       public int getController(int controller) {\r
+               return controller_values[controller];\r
+       }\r
+       @Override\r
+       public void programChange( int program ) {\r
+               this.program = program;\r
+       }\r
+       @Override\r
+       public void programChange(int bank, int program) {\r
+               controlChange( 0x00, ((bank>>7) & 0x7F) );\r
+               controlChange( 0x20, (bank & 0x7F) );\r
+               programChange( program );\r
+       }\r
+       @Override\r
+       public int getProgram() { return program; }\r
+       @Override\r
+       public void setPitchBend(int bend) { pitch_bend = bend; }\r
+       @Override\r
+       public int getPitchBend() { return pitch_bend; }\r
+       @Override\r
+       public void setPolyPressure(int note_no, int pressure) {}\r
+       @Override\r
+       public int getPolyPressure(int noteNumber) { return 0x40; }\r
+       @Override\r
+       public void setChannelPressure(int pressure) {}\r
+       @Override\r
+       public int getChannelPressure() { return 0x40; }\r
+       @Override\r
+       public void allSoundOff() {}\r
+       @Override\r
+       public void allNotesOff() {}\r
+       @Override\r
+       public void resetAllControllers() {\r
+               //\r
+               // See also:\r
+               //   Recommended Practice (RP-015)\r
+               //   Response to Reset All Controllers\r
+               //   http://www.midi.org/techspecs/rp15.php\r
+               //\r
+               // modulation\r
+               controller_values[0] = 0;\r
+               //\r
+               // pedals\r
+               for(int i=64; i<=67; i++) controller_values[i] = 0;\r
+               //\r
+               // Set pitch bend to center\r
+               pitch_bend = 8192;\r
+               //\r
+               // Set NRPN / RPN to null value\r
+               for(int i=98; i<=101; i++) controller_values[i] = 127;\r
+       }\r
+       @Override\r
+       public boolean localControl(boolean on) {\r
+               controlChange( 0x7A, on ? 0x7F : 0x00 );\r
+               return false;\r
+       }\r
+       @Override\r
+       public void setOmni(boolean on) {\r
+               controlChange( on ? 0x7D : 0x7C, 0 );\r
+       }\r
+       @Override\r
+       public boolean getOmni() { return false; }\r
+       @Override\r
+       public void setMono(boolean on) {}\r
+       @Override\r
+       public boolean getMono() { return false; }\r
+       @Override\r
+       public void setMute(boolean mute) {}\r
+       @Override\r
+       public boolean getMute() { return false; }\r
+       @Override\r
+       public void setSolo(boolean soloState) {}\r
+       @Override\r
+       public boolean getSolo() { return false; }\r
+       @Override\r
+       public void controlChange(int controller, int value) {\r
+               controller_values[controller] = value & 0x7F;\r
+               switch( controller ) {\r
+\r
+               case 0x78: // All Sound Off\r
+                       allSoundOff();\r
+                       break;\r
+\r
+               case 0x7B: // All Notes Off\r
+                       allNotesOff();\r
+                       break;\r
+\r
+               case 0x79: // Reset All Controllers\r
+                       resetAllControllers();\r
+                       break;\r
+\r
+               case 0x06: // Data Entry (MSB)\r
+               case 0x26: // Data Entry (LSB)\r
+                       changeRPNData(0);\r
+                       break;\r
+\r
+               case 0x60: // Data Increment\r
+                       changeRPNData(1);\r
+                       break;\r
+\r
+               case 0x61: // Data Decrement\r
+                       changeRPNData(-1);\r
+                       break;\r
+\r
+                       // Non-Registered Parameter Number\r
+               case 0x62: // NRPN (LSB)\r
+               case 0x63: // NRPN (MSB)\r
+                       data_for = DATA_FOR_NRPN;\r
+                       // fireRpnChanged();\r
+                       break;\r
+\r
+                       // Registered Parameter Number\r
+               case 0x64: // RPN (LSB)\r
+               case 0x65: // RPN (MSB)\r
+                       data_for = DATA_FOR_RPN;\r
+                       fireRpnChanged();\r
+                       break;\r
+               }\r
+       }\r
+}\r
+\r
+/**\r
+ * Transmitter(Tx)/Receiver(Rx) のリスト(view)\r
+ *\r
+ * <p>マウスで Tx からドラッグして Rx へドロップする機能を備えた\r
+ * 仮想MIDI端子リストです。\r
+ * </p>\r
+ */\r
+class MidiConnecterListView extends JList<AutoCloseable>\r
+       implements Transferable, DragGestureListener, DropTargetListener\r
+{\r
+       public static final Icon MIDI_CONNECTER_ICON =\r
+               new ButtonIcon(ButtonIcon.MIDI_CONNECTOR_ICON);\r
+       private class CellRenderer extends JLabel implements ListCellRenderer<AutoCloseable> {\r
+               public Component getListCellRendererComponent(\r
+                       JList<? extends AutoCloseable> list,\r
+                       AutoCloseable value,\r
+                       int index,\r
+                       boolean isSelected,\r
+                       boolean cellHasFocus\r
+               ) {\r
+                       String text;\r
+                       if( value instanceof Transmitter ) text = "Tx";\r
+                       else if( value instanceof Receiver ) text = "Rx";\r
+                       else text = (value==null ? null : value.toString());\r
+                       setText(text);\r
+                       setIcon(MIDI_CONNECTER_ICON);\r
+                       if (isSelected) {\r
+                               setBackground(list.getSelectionBackground());\r
+                               setForeground(list.getSelectionForeground());\r
+                       } else {\r
+                               setBackground(list.getBackground());\r
+                               setForeground(list.getForeground());\r
+                       }\r
+                       setEnabled(list.isEnabled());\r
+                       setFont(list.getFont());\r
+                       setOpaque(true);\r
+                       return this;\r
+               }\r
+       }\r
+       /**\r
+        * 仮想MIDI端子リストビューを生成します。\r
+        * @param model このビューから参照されるデータモデル\r
+        */\r
+       public MidiConnecterListView(MidiConnecterListModel model) {\r
+               super(model);\r
+               setCellRenderer(new CellRenderer());\r
+               setSelectionMode(ListSelectionModel.SINGLE_SELECTION);\r
+               setLayoutOrientation(JList.HORIZONTAL_WRAP);\r
+               setVisibleRowCount(0);\r
+        (new DragSource()).createDefaultDragGestureRecognizer(\r
+               this, DnDConstants.ACTION_COPY_OR_MOVE, this\r
+        );\r
+               new DropTarget( this, DnDConstants.ACTION_COPY_OR_MOVE, this, true );\r
+       }\r
+       public static final DataFlavor transmitterFlavor =\r
+               new DataFlavor(Transmitter.class, "Transmitter");\r
+       public static final DataFlavor transmitterFlavors[] = {transmitterFlavor};\r
+       public Object getTransferData(DataFlavor flavor) {\r
+               return getModel().getElementAt(getSelectedIndex());\r
+       }\r
+       public DataFlavor[] getTransferDataFlavors() {\r
+               return transmitterFlavors;\r
+       }\r
+       public boolean isDataFlavorSupported(DataFlavor flavor) {\r
+               return flavor.equals(transmitterFlavor);\r
+       }\r
+       public void dragGestureRecognized(DragGestureEvent dge) {\r
+               int action = dge.getDragAction();\r
+               if( (action & DnDConstants.ACTION_COPY_OR_MOVE) == 0 )\r
+                       return;\r
+               int index = locationToIndex(dge.getDragOrigin());\r
+               AutoCloseable data = getModel().getElementAt(index);\r
+               if( data instanceof Transmitter ) {\r
+                       dge.startDrag(DragSource.DefaultLinkDrop, this, null);\r
+               }\r
+       }\r
+       public void dragEnter(DropTargetDragEvent event) {\r
+               if( event.isDataFlavorSupported(transmitterFlavor) )\r
+                       event.acceptDrag(DnDConstants.ACTION_COPY_OR_MOVE);\r
+       }\r
+       public void dragExit(DropTargetEvent dte) {}\r
+       public void dragOver(DropTargetDragEvent dtde) {}\r
+       public void dropActionChanged(DropTargetDragEvent dtde) {}\r
+       public void drop(DropTargetDropEvent event) {\r
+               event.acceptDrop(DnDConstants.ACTION_COPY_OR_MOVE);\r
+               try {\r
+                       int maskedBits = event.getDropAction() & DnDConstants.ACTION_COPY_OR_MOVE;\r
+                       if( maskedBits != 0 ) {\r
+                               Transferable t = event.getTransferable();\r
+                               Object data = t.getTransferData(transmitterFlavor);\r
+                               if( data instanceof Transmitter ) {\r
+                                       getModel().ConnectToReceiver((Transmitter)data);\r
+                                       event.dropComplete(true);\r
+                                       return;\r
+                               }\r
+                       }\r
+                       event.dropComplete(false);\r
+               }\r
+               catch (Exception ex) {\r
+                       ex.printStackTrace();\r
+                       event.dropComplete(false);\r
+               }\r
+       }\r
+       @Override\r
+       public MidiConnecterListModel getModel() {\r
+               return (MidiConnecterListModel)super.getModel();\r
+       }\r
+}\r
+\r
+/**\r
+ * 1個の MIDI デバイスに属する Transmitter/Receiver のリストモデル\r
+ */\r
+class MidiConnecterListModel extends AbstractListModel<AutoCloseable> {\r
+       private MidiDevice device;\r
+       private List<MidiConnecterListModel> modelList;\r
+       /**\r
+        * 指定のMIDIデバイスに属する\r
+        *  {@link Transmitter}/{@link Receiver} のリストモデルを構築します。\r
+        *\r
+        * @param device 対象MIDIデバイス\r
+        * @param modelList リストモデルのリスト\r
+        */\r
+       public MidiConnecterListModel(\r
+               MidiDevice device,\r
+               List<MidiConnecterListModel> modelList\r
+       ) {\r
+               this.device = device;\r
+               this.modelList = modelList;\r
+       }\r
+       /**\r
+        * 対象MIDIデバイスを返します。\r
+        * @return 対象MIDIデバイス\r
+        */\r
+       public MidiDevice getMidiDevice() {\r
+               return device;\r
+       }\r
+       /**\r
+        * 対象MIDIデバイスの名前を返します。\r
+        */\r
+       public String toString() {\r
+               return device.getDeviceInfo().toString();\r
+       }\r
+       @Override\r
+       public AutoCloseable getElementAt(int index) {\r
+               List<Receiver> rxList = device.getReceivers();\r
+               int rxSize = rxList.size();\r
+               if( index < rxSize ) return rxList.get(index);\r
+               index -= rxSize;\r
+               List<Transmitter> txList = device.getTransmitters();\r
+               return index < txList.size() ? txList.get(index) : null;\r
+       }\r
+       @Override\r
+       public int getSize() {\r
+               return\r
+                       device.getReceivers().size() +\r
+                       device.getTransmitters().size();\r
+       }\r
+       /**\r
+        * 指定の要素がこのリストモデルで最初に見つかった位置を返します。\r
+        *\r
+        * @param element 探したい要素\r
+        * @return 位置のインデックス(先頭が 0、見つからないとき -1)\r
+        */\r
+       public int indexOf(AutoCloseable element) {\r
+               List<Receiver> rxList = device.getReceivers();\r
+               int index = rxList.indexOf(element);\r
+               if( index < 0 ) {\r
+                       List<Transmitter> txList = device.getTransmitters();\r
+                       if( (index = txList.indexOf(element)) >= 0 )\r
+                               index += rxList.size();\r
+               }\r
+               return index;\r
+       }\r
+       /**\r
+        * このリストが {@link Transmitter} をサポートしているか調べます。\r
+        * @return {@link Transmitter} をサポートしていたら true\r
+        */\r
+       public boolean txSupported() {\r
+               return device.getMaxTransmitters() != 0;\r
+       }\r
+       /**\r
+        * このリストが {@link Receiver} をサポートしているか調べます。\r
+        * @return {@link Receiver} をサポートしていたら true\r
+        */\r
+       public boolean rxSupported() {\r
+               return device.getMaxReceivers() != 0;\r
+       }\r
+       /**\r
+        * このリストのMIDIデバイスの入出力タイプを返します。\r
+        * <p>レシーバからMIDI信号を受けて外部へ出力できるデバイスの場合は MIDI_OUT、\r
+        * 外部からMIDI信号を入力してトランスミッタからレシーバへ転送できるデバイスの場合は MIDI_IN、\r
+        * 両方できるデバイスの場合は MIDI_IN_OUT を返します。\r
+        * </p>\r
+        * @return このリストのMIDIデバイスの入出力タイプ\r
+        */\r
+       public MidiDeviceInOutType getMidiDeviceInOutType() {\r
+               if( rxSupported() ) {\r
+                       if( txSupported() )\r
+                               return MidiDeviceInOutType.MIDI_IN_OUT;\r
+                       else\r
+                               return MidiDeviceInOutType.MIDI_OUT;\r
+               }\r
+               else {\r
+                       if( txSupported() )\r
+                               return MidiDeviceInOutType.MIDI_IN;\r
+                       else\r
+                               return null;\r
+               }\r
+       }\r
+       /**\r
+        * 引数で指定されたトランスミッタを、最初のレシーバに接続します。\r
+        * <p>接続先のレシーバがない場合は無視されます。\r
+        * </p>\r
+        * @param tx トランスミッタ\r
+        */\r
+       public void ConnectToReceiver(Transmitter tx) {\r
+               List<Receiver> receivers = device.getReceivers();\r
+               if( receivers.size() == 0 )\r
+                       return;\r
+               tx.setReceiver(receivers.get(0));\r
+               fireContentsChanged(this,0,getSize());\r
+       }\r
+       /**\r
+        * 未接続のトランスミッタを、\r
+        * 引数で指定されたリストモデルの最初のレシーバに接続します。\r
+        * @param anotherModel 接続先レシーバを持つリストモデル\r
+        */\r
+       public void connectToReceiverOf(MidiConnecterListModel anotherModel) {\r
+               if( ! txSupported() )\r
+                       return;\r
+               if( anotherModel == null || ! anotherModel.rxSupported() )\r
+                       return;\r
+               List<Receiver> rxList = anotherModel.device.getReceivers();\r
+               if( rxList.isEmpty() )\r
+                       return;\r
+               getUnconnectedTransmitter().setReceiver(rxList.get(0));\r
+       }\r
+       /**\r
+        * レシーバに未接続の最初のトランスミッタを返します。\r
+        * @return 未接続のトランスミッタ\r
+        */\r
+       public Transmitter getUnconnectedTransmitter() {\r
+               if( ! txSupported() ) {\r
+                       return null;\r
+               }\r
+               List<Transmitter> txList = device.getTransmitters();\r
+               for( Transmitter tx : txList ) {\r
+                       if( tx.getReceiver() == null )\r
+                               return tx;\r
+               }\r
+               Transmitter tx;\r
+               try {\r
+                       tx = device.getTransmitter();\r
+               } catch( MidiUnavailableException e ) {\r
+                       e.printStackTrace();\r
+                       return null;\r
+               }\r
+               fireIntervalAdded(this,0,getSize());\r
+               return tx;\r
+       }\r
+       /**\r
+        * 指定のトランスミッタを閉じます。\r
+        * <p>このリストモデルにないトランスミッタが指定された場合、無視されます。\r
+        * </p>\r
+        * @param txToClose 閉じたいトランスミッタ\r
+        */\r
+       public void closeTransmitter(Transmitter txToClose) {\r
+               List<Transmitter> txList = device.getTransmitters();\r
+               if( ! txList.contains(txToClose) ) {\r
+                       return;\r
+               }\r
+               txToClose.close();\r
+               fireIntervalRemoved(this,0,getSize());\r
+       }\r
+       /**\r
+        * 対象MIDIデバイスを開きます。\r
+        * @throws MidiUnavailableException デバイスを開くことができない場合\r
+        */\r
+       public void openDevice() throws MidiUnavailableException {\r
+               device.open();\r
+               if( rxSupported() && device.getReceivers().size() == 0 ) {\r
+                       device.getReceiver();\r
+               }\r
+       }\r
+       /**\r
+        * 対象MIDIデバイスを閉じます。\r
+        *\r
+        * <p>対象MIDIデバイスの Receiver を設定している\r
+        *  {@link Transmitter} があればすべて閉じます。\r
+        * </p>\r
+        */\r
+       public void closeDevice() {\r
+               if( rxSupported() ) {\r
+                       Receiver rx = device.getReceivers().get(0);\r
+                       for( MidiConnecterListModel m : modelList ) {\r
+                               if( m == this || ! m.txSupported() )\r
+                                       continue;\r
+                               for( int i=0; i<m.getSize(); i++ ) {\r
+                                       AutoCloseable ac = m.getElementAt(i);\r
+                                       if( ! (ac instanceof Transmitter) )\r
+                                               continue;\r
+                                       Transmitter tx = ((Transmitter)ac);\r
+                                       if( tx.getReceiver() == rx )\r
+                                               m.closeTransmitter(tx);\r
+                               }\r
+                       }\r
+               }\r
+               device.close();\r
+       }\r
+       /**\r
+        * マイクロ秒位置をリセットします。\r
+        * <p>これはMIDIデバイスからリアルタイムレコーディングを開始するタイミングで\r
+        * 必ず行う必要があります。\r
+        * (マイクロ秒位置がリセットされていないと、そのままシーケンサに記録され、\r
+        * 記録位置が大幅に後ろのほうにずれてしまいます)\r
+        * </p>\r
+        */\r
+       public void resetMicrosecondPosition() {\r
+               if( ! txSupported() || device instanceof Sequencer )\r
+                       return;\r
+               //\r
+               // デバイスを閉じる前に接続相手の情報を保存\r
+               List<Transmitter> txList = device.getTransmitters();\r
+               List<Receiver> peerRxList = new Vector<Receiver>();\r
+               for( Transmitter tx : txList ) {\r
+                       Receiver rx = tx.getReceiver();\r
+                       if( rx != null ) peerRxList.add(rx);\r
+               }\r
+               List<Transmitter> peerTxList = null;\r
+               Receiver rx = null;\r
+               if( rxSupported() ) {\r
+                       rx = device.getReceivers().get(0);\r
+                       peerTxList = new Vector<Transmitter>();\r
+                       for( MidiConnecterListModel m : modelList ) {\r
+                               if( m == this || ! m.txSupported() )\r
+                                       continue;\r
+                               for( int i=0; i<m.getSize(); i++ ) {\r
+                                       Object obj = m.getElementAt(i);\r
+                                       if( ! (obj instanceof Transmitter) )\r
+                                               continue;\r
+                                       Transmitter tx = ((Transmitter)obj);\r
+                                       if( tx.getReceiver() == rx )\r
+                                               peerTxList.add(tx);\r
+                               }\r
+                       }\r
+               }\r
+               // デバイスを一旦閉じてまた開くことにより\r
+               // マイクロ秒位置をリセットする\r
+               device.close();\r
+               try {\r
+                       device.open();\r
+               } catch( MidiUnavailableException e ) {\r
+                       e.printStackTrace();\r
+               }\r
+               // 元通りに接続し直す\r
+               for( Receiver peerRx : peerRxList ) {\r
+                       Transmitter tx = getUnconnectedTransmitter();\r
+                       if( tx == null ) continue;\r
+                       tx.setReceiver(peerRx);\r
+               }\r
+               if( peerTxList != null ) {\r
+                       rx = device.getReceivers().get(0);\r
+                       for( Transmitter peerTx : peerTxList ) {\r
+                               peerTx.setReceiver(rx);\r
+                       }\r
+               }\r
+       }\r
+}\r
+\r
+/**\r
+ * MIDIデバイスフレームビュー\r
+ */\r
+class MidiDeviceFrame extends JInternalFrame {\r
+       private static Insets ZERO_INSETS = new Insets(0,0,0,0);\r
+       /**\r
+        * デバイスの仮想MIDI端子リストビュー\r
+        */\r
+       MidiConnecterListView listView;\r
+       /**\r
+        * MIDIデバイスのモデルからフレームビューを構築します。\r
+        * @param model MIDIデバイスのTransmitter/Receiverリストモデル\r
+        */\r
+       public MidiDeviceFrame( MidiConnecterListModel model ) {\r
+               super( null, true, true, false, false );\r
+               //\r
+               // タイトルの設定\r
+               String title = model.toString();\r
+               if( model.txSupported() ) {\r
+                       if( ! model.rxSupported() ) title = "[IN] "+title;\r
+               }\r
+               else {\r
+                       title = (model.rxSupported()?"[OUT] ":"[No IN/OUT] ")+title;\r
+               }\r
+               setTitle(title);\r
+               listView = new MidiConnecterListView(model);\r
+               setDefaultCloseOperation(DO_NOTHING_ON_CLOSE);\r
+               addInternalFrameListener(\r
+                       new InternalFrameAdapter() {\r
+                               public void internalFrameOpened(InternalFrameEvent e) {\r
+                                       if( ! listView.getModel().getMidiDevice().isOpen() )\r
+                                               setVisible(false);\r
+                               }\r
+                               public void internalFrameClosing(InternalFrameEvent e) {\r
+                                       MidiConnecterListModel m = listView.getModel();\r
+                                       m.closeDevice();\r
+                                       if( ! m.getMidiDevice().isOpen() )\r
+                                               setVisible(false);\r
+                               }\r
+                       }\r
+               );\r
+               setLayout( new BoxLayout( getContentPane(), BoxLayout.Y_AXIS ) );\r
+               add( new JScrollPane(listView) );\r
+               if( model.txSupported() ) {\r
+                       JPanel button_panel = new JPanel();\r
+                       button_panel.add(\r
+                               new JButton("New Tx") {\r
+                                       {\r
+                                               setMargin(ZERO_INSETS);\r
+                                               addActionListener(\r
+                                                       new ActionListener() {\r
+                                                               public void actionPerformed(ActionEvent event) {\r
+                                                                       listView.getModel().getUnconnectedTransmitter();\r
+                                                               }\r
+                                                       }\r
+                                               );\r
+                                       }\r
+                               }\r
+                       );\r
+                       button_panel.add(\r
+                               new JButton("Close Tx") {\r
+                                       {\r
+                                               setMargin(ZERO_INSETS);\r
+                                               addActionListener(\r
+                                                       new ActionListener() {\r
+                                                               public void actionPerformed(ActionEvent event) {\r
+                                                                       listView.getModel().closeTransmitter(\r
+                                                                               (Transmitter)listView.getSelectedValue()\r
+                                                                       );\r
+                                                               }\r
+                                                       }\r
+                                               );\r
+                                       }\r
+                               }\r
+                       );\r
+                       add(button_panel);\r
+               }\r
+               setSize(250,100);\r
+       }\r
+       /**\r
+        * 指定されたインデックスが示す仮想MIDI端子リストの要素のセル範囲を返します。\r
+        *\r
+        * @param index リスト要素のインデックス\r
+        * @return セル範囲の矩形\r
+        */\r
+       public Rectangle getListCellBounds(int index) {\r
+               Rectangle rect = listView.getCellBounds(index,index);\r
+               if( rect == null )\r
+                       return null;\r
+               rect.translate(\r
+                       getRootPane().getX() + getContentPane().getX(),\r
+                       getRootPane().getY() + getContentPane().getY()\r
+               );\r
+               return rect;\r
+       }\r
+       /**\r
+        * 仮想MIDI端子リストの指定された要素のセル範囲を返します。\r
+        *\r
+        * @param transciver 要素となるMIDI端子(Transmitter または Receiver)\r
+        * @return セル範囲の矩形\r
+        */\r
+       public Rectangle getListCellBounds(AutoCloseable transciver) {\r
+               return getListCellBounds(listView.getModel().indexOf(transciver));\r
+       }\r
+}\r
+\r
+/**\r
+ * MIDIデバイス入出力タイプ\r
+ */\r
+enum MidiDeviceInOutType {\r
+       MIDI_OUT("MIDI output devices (MIDI synthesizer etc.)"),\r
+       MIDI_IN("MIDI input devices (MIDI keyboard etc.)"),\r
+       MIDI_IN_OUT("MIDI input/output devices (MIDI sequencer etc.)");\r
+       private String description;\r
+       private MidiDeviceInOutType(String description) {\r
+               this.description = description;\r
+       }\r
+       public String getDescription() {\r
+               return description;\r
+       }\r
+}\r
+\r
+/**\r
+ * MIDIデバイスツリーモデル\r
+ */\r
+class MidiDeviceTreeModel implements TreeModel {\r
+       List<MidiConnecterListModel> deviceModelList;\r
+       public MidiDeviceTreeModel(List<MidiConnecterListModel> deviceModelList) {\r
+               this.deviceModelList = deviceModelList;\r
+       }\r
+       public Object getRoot() {\r
+               return "MIDI devices";\r
+       }\r
+       public Object getChild(Object parent, int index) {\r
+               if( parent == getRoot() ) {\r
+                       return MidiDeviceInOutType.values()[index];\r
+               }\r
+               if( parent instanceof MidiDeviceInOutType ) {\r
+                       MidiDeviceInOutType ioType = (MidiDeviceInOutType)parent;\r
+                       for( MidiConnecterListModel deviceModel : deviceModelList )\r
+                               if( deviceModel.getMidiDeviceInOutType() == ioType ) {\r
+                                       if( index == 0 )\r
+                                               return deviceModel;\r
+                                       index--;\r
+                               }\r
+               }\r
+               return null;\r
+       }\r
+       public int getChildCount(Object parent) {\r
+               if( parent == getRoot() ) {\r
+                       return MidiDeviceInOutType.values().length;\r
+               }\r
+               int childCount = 0;\r
+               if( parent instanceof MidiDeviceInOutType ) {\r
+                       MidiDeviceInOutType ioType = (MidiDeviceInOutType)parent;\r
+                       for( MidiConnecterListModel deviceModel : deviceModelList )\r
+                               if( deviceModel.getMidiDeviceInOutType() == ioType )\r
+                                       childCount++;\r
+               }\r
+               return childCount;\r
+       }\r
+       public int getIndexOfChild(Object parent, Object child) {\r
+               if( parent == getRoot() ) {\r
+                       if( child instanceof MidiDeviceInOutType ) {\r
+                               MidiDeviceInOutType ioType = (MidiDeviceInOutType)child;\r
+                               return ioType.ordinal();\r
+                       }\r
+               }\r
+               if( parent instanceof MidiDeviceInOutType ) {\r
+                       MidiDeviceInOutType ioType = (MidiDeviceInOutType)parent;\r
+                       int index = 0;\r
+                       for( MidiConnecterListModel deviceModel : deviceModelList ) {\r
+                               if( deviceModel.getMidiDeviceInOutType() == ioType ) {\r
+                                       if( deviceModel == child )\r
+                                               return index;\r
+                                       index++;\r
+                               }\r
+                       }\r
+               }\r
+               return -1;\r
+       }\r
+       public boolean isLeaf(Object node) {\r
+               return node instanceof MidiConnecterListModel;\r
+       }\r
+       public void valueForPathChanged(TreePath path, Object newValue) {}\r
+       private EventListenerList listenerList = new EventListenerList();\r
+       public void addTreeModelListener(TreeModelListener listener) {\r
+               listenerList.add(TreeModelListener.class, listener);\r
+       }\r
+       public void removeTreeModelListener(TreeModelListener listener) {\r
+               listenerList.remove(TreeModelListener.class, listener);\r
+       }\r
+       public void fireTreeNodesChanged(\r
+               Object source, Object[] path, int[] childIndices, Object[] children\r
+       ) {\r
+               Object[] listeners = listenerList.getListenerList();\r
+               for (int i = listeners.length-2; i>=0; i-=2) {\r
+                       if (listeners[i]==TreeModelListener.class) {\r
+                               ((TreeModelListener)listeners[i+1]).treeNodesChanged(\r
+                                       new TreeModelEvent(source,path,childIndices,children)\r
+                               );\r
+                       }\r
+               }\r
+       }\r
+/*\r
+       private static final Object[] midiOutPath =\r
+               {rootNode, MidiDeviceInOutType.MIDI_OUT};\r
+       private static final Object[] midiInPath =\r
+               {rootNode, MidiDeviceInOutType.MIDI_IN};\r
+       public void fireDeviceStatusChanged(MidiConnecterListModel deviceModel) {\r
+               Object[] path = deviceModel.rxSupported() ? midiOutPath : midiInPath;\r
+               MidiDeviceInOutType parent = deviceModel.getMidiDeviceInOutType();\r
+               fireTreeNodesChanged(\r
+                       this, path,\r
+                       new int[]{ getIndexOfChild(parent, deviceModel) },\r
+                       new Object[]{deviceModel}\r
+               );\r
+       }\r
+*/\r
+}\r
+\r
+/**\r
+ * MIDIデバイスツリー (View)\r
+ */\r
+class MidiDeviceTree extends JTree\r
+       implements Transferable, DragGestureListener, InternalFrameListener\r
+{\r
+       /**\r
+        * MIDIデバイスツリーのビューを構築します。\r
+        * @param model このビューにデータを提供するモデル\r
+        */\r
+       public MidiDeviceTree(MidiDeviceTreeModel model) {\r
+               super(model);\r
+        (new DragSource()).createDefaultDragGestureRecognizer(\r
+               this, DnDConstants.ACTION_COPY_OR_MOVE, this\r
+        );\r
+        setCellRenderer(new DefaultTreeCellRenderer() {\r
+               @Override\r
+               public Component getTreeCellRendererComponent(\r
+                       JTree tree, Object value,\r
+                       boolean selected, boolean expanded, boolean leaf, int row,\r
+                       boolean hasFocus\r
+               ) {\r
+                       super.getTreeCellRendererComponent(\r
+                               tree, value, selected, expanded, leaf, row, hasFocus\r
+                       );\r
+                       if(leaf) {\r
+                                       setIcon(MidiConnecterListView.MIDI_CONNECTER_ICON);\r
+                                       setDisabledIcon(MidiConnecterListView.MIDI_CONNECTER_ICON);\r
+                               MidiConnecterListModel listModel = (MidiConnecterListModel)value;\r
+                               setEnabled( ! listModel.getMidiDevice().isOpen() );\r
+                       }\r
+                       return this;\r
+               }\r
+       });\r
+       }\r
+       /**\r
+        * このデバイスツリーからドラッグされるデータフレーバ\r
+        */\r
+       public static final DataFlavor\r
+               treeModelFlavor = new DataFlavor(TreeModel.class, "TreeModel");\r
+       private static final DataFlavor treeModelFlavors[] = {treeModelFlavor};\r
+       @Override\r
+       public Object getTransferData(DataFlavor flavor) {\r
+               return getLastSelectedPathComponent();\r
+       }\r
+       @Override\r
+       public DataFlavor[] getTransferDataFlavors() {\r
+               return treeModelFlavors;\r
+       }\r
+       @Override\r
+       public boolean isDataFlavorSupported(DataFlavor flavor) {\r
+               return flavor.equals(treeModelFlavor);\r
+       }\r
+       @Override\r
+       public void dragGestureRecognized(DragGestureEvent dge) {\r
+               int action = dge.getDragAction();\r
+               if( (action & DnDConstants.ACTION_COPY_OR_MOVE) != 0 ) {\r
+                       dge.startDrag(DragSource.DefaultMoveDrop, this, null);\r
+               }\r
+       }\r
+       @Override\r
+       public void internalFrameOpened(InternalFrameEvent e) {}\r
+       /**\r
+        *      MidiDeviceFrame のクローズ処理中に再描画リクエストを送ります。\r
+        */\r
+       @Override\r
+       public void internalFrameClosing(InternalFrameEvent e) {\r
+               repaint();\r
+       }\r
+       @Override\r
+       public void internalFrameClosed(InternalFrameEvent e) {}\r
+       @Override\r
+       public void internalFrameIconified(InternalFrameEvent e) {}\r
+       @Override\r
+       public void internalFrameDeiconified(InternalFrameEvent e) {}\r
+       @Override\r
+       public void internalFrameActivated(InternalFrameEvent e) {}\r
+       @Override\r
+       public void internalFrameDeactivated(InternalFrameEvent e) {}\r
+}\r
+\r
+/**\r
+ * MIDIデバイスマネージャ (Model)\r
+ */\r
+class MidiDeviceManager extends Vector<MidiConnecterListModel> {\r
+       private Sequencer sequencer = null;\r
+       SpeedSliderModel speedSliderModel = null;\r
+       SequencerTimeRangeModel timeRangeModel = null;\r
+       MidiEditor editorDialog = null;\r
+       MidiConnecterListModel firstMidiOutModel = null;\r
+       public MidiDeviceManager( Vector<VirtualMidiDevice> vmds ) {\r
+               MidiDevice.Info[] devInfos = MidiSystem.getMidiDeviceInfo();\r
+               MidiConnecterListModel\r
+               guiModels[] = new MidiConnecterListModel[vmds.size()],\r
+               sequencerModel = null,\r
+               firstMidiInModel = null;\r
+               for( int i=0; i<vmds.size(); i++ )\r
+                       guiModels[i] = addMidiDevice(vmds.get(i));\r
+\r
+               try {\r
+                       sequencer = MidiSystem.getSequencer(false);\r
+                       sequencerModel = addMidiDevice(sequencer);\r
+                       speedSliderModel = new SpeedSliderModel(sequencer);\r
+                       timeRangeModel = new SequencerTimeRangeModel(this);\r
+               } catch( MidiUnavailableException e ) {\r
+                       System.out.println(\r
+                               ChordHelperApplet.VersionInfo.NAME +\r
+                               " : MIDI sequencer unavailable"\r
+                       );\r
+                       e.printStackTrace();\r
+               }\r
+               for( MidiDevice.Info info : devInfos ) {\r
+                       MidiDevice device;\r
+                       try {\r
+                               device = MidiSystem.getMidiDevice(info);\r
+                       } catch( MidiUnavailableException e ) {\r
+                               e.printStackTrace(); continue;\r
+                       }\r
+                       if( device instanceof Sequencer )  continue;\r
+                       if( device instanceof Synthesizer ) {\r
+                               try {\r
+                                       addMidiDevice(MidiSystem.getSynthesizer());\r
+                               } catch( MidiUnavailableException e ) {\r
+                                       System.out.println(\r
+                                               ChordHelperApplet.VersionInfo.NAME +\r
+                                               " : Java internal MIDI synthesizer unavailable"\r
+                                       );\r
+                                       e.printStackTrace();\r
+                               }\r
+                               continue;\r
+                       }\r
+                       MidiConnecterListModel m = addMidiDevice(device);\r
+                       if( m.rxSupported() && firstMidiOutModel == null )\r
+                               firstMidiOutModel = m;\r
+                       if( m.txSupported() && firstMidiInModel == null )\r
+                               firstMidiInModel = m;\r
+               }\r
+               // デバイスを開く\r
+               try {\r
+                       for( MidiConnecterListModel m : guiModels )\r
+                               m.openDevice();\r
+                       if( firstMidiInModel != null )\r
+                               firstMidiInModel.openDevice();\r
+                       if( sequencerModel != null )\r
+                               sequencerModel.openDevice();\r
+                       if( firstMidiOutModel != null )\r
+                               firstMidiOutModel.openDevice();\r
+               } catch( MidiUnavailableException ex ) {\r
+                       ex.printStackTrace();\r
+               }\r
+               //\r
+               // 初期接続\r
+               //\r
+               for( MidiConnecterListModel mtx : guiModels ) {\r
+                       for( MidiConnecterListModel mrx : guiModels )\r
+                               mtx.connectToReceiverOf(mrx);\r
+                       mtx.connectToReceiverOf(sequencerModel);\r
+                       mtx.connectToReceiverOf(firstMidiOutModel);\r
+               }\r
+               if( firstMidiInModel != null ) {\r
+                       for( MidiConnecterListModel m : guiModels )\r
+                               firstMidiInModel.connectToReceiverOf(m);\r
+                       firstMidiInModel.connectToReceiverOf(sequencerModel);\r
+                       firstMidiInModel.connectToReceiverOf(firstMidiOutModel);\r
+               }\r
+               if( sequencerModel != null ) {\r
+                       for( MidiConnecterListModel m : guiModels )\r
+                               sequencerModel.connectToReceiverOf(m);\r
+                       sequencerModel.connectToReceiverOf(firstMidiOutModel);\r
+               }\r
+       }\r
+       private MidiConnecterListModel addMidiDevice( MidiDevice device ) {\r
+               MidiConnecterListModel m = new MidiConnecterListModel(device,this);\r
+               addElement(m);\r
+               return m;\r
+       }\r
+       public void setMidiEditor( MidiEditor editorDialog ) {\r
+               editorDialog.deviceManager = this;\r
+               MidiConnecterListModel mclm = addMidiDevice(\r
+                       (this.editorDialog = editorDialog).midiDevice\r
+               );\r
+               try {\r
+                       mclm.openDevice();\r
+               } catch( MidiUnavailableException ex ) {\r
+                       ex.printStackTrace();\r
+               }\r
+               mclm.connectToReceiverOf(firstMidiOutModel);\r
+       }\r
+       public Sequencer getSequencer() { return sequencer; }\r
+       public boolean isRecordable() {\r
+               return editorDialog != null && editorDialog.isRecordable();\r
+       }\r
+       public void resetMicrosecondPosition() {\r
+               for( MidiConnecterListModel model : this )\r
+                       model.resetMicrosecondPosition();\r
+       }\r
+}\r
+\r
+/**\r
+ * MIDIデバイスダイアログ (View)\r
+ */\r
+class MidiDeviceDialog extends JDialog {\r
+       MidiDeviceTree deviceTree;\r
+       JEditorPane deviceInfoPane = new JEditorPane("text/html","<html></html>") {\r
+               {\r
+                       setEditable(false);\r
+               }\r
+       };\r
+       MidiDesktopPane desktopPane;\r
+       public MidiDeviceDialog(List<MidiConnecterListModel> deviceModelList) {\r
+               setTitle("MIDI device connection");\r
+               setBounds( 300, 300, 800, 500 );\r
+               desktopPane = new MidiDesktopPane(\r
+                       deviceTree = new MidiDeviceTree(\r
+                               new MidiDeviceTreeModel(deviceModelList)\r
+                       )\r
+               );\r
+               deviceTree.addTreeSelectionListener(\r
+                       new TreeSelectionListener() {\r
+                               public void valueChanged(TreeSelectionEvent e) {\r
+                                       String html = "<html><head></head><body>";\r
+                                       Object obj = deviceTree.getLastSelectedPathComponent();\r
+                                       if( obj instanceof MidiConnecterListModel ) {\r
+                                               MidiConnecterListModel deviceModel = (MidiConnecterListModel)obj;\r
+                                               MidiDevice device = deviceModel.getMidiDevice();\r
+                                               MidiDevice.Info info = device.getDeviceInfo();\r
+                                               html += "<b>"+deviceModel+"</b><br/>";\r
+                                               html += "<table border=\"1\"><tbody>";\r
+                                               html += "<tr><th>Version</th><td>"+info.getVersion()+"</td></tr>";\r
+                                               html += "<tr><th>Description</th><td>"+info.getDescription()+"</td></tr>";\r
+                                               html += "<tr><th>Vendor</th><td>"+info.getVendor()+"</td></tr>";\r
+                                               html += "</tbody></table>";\r
+                                               MidiDeviceFrame frame = desktopPane.getFrameOf(deviceModel);\r
+                                               if( frame != null ) {\r
+                                                       try {\r
+                                                               frame.setSelected(true);\r
+                                                       } catch( PropertyVetoException ex ) {\r
+                                                               ex.printStackTrace();\r
+                                                       }\r
+                                               }\r
+                                       }\r
+                                       else if( obj instanceof MidiDeviceInOutType ) {\r
+                                               MidiDeviceInOutType ioType = (MidiDeviceInOutType)obj;\r
+                                               html += "<b>"+ioType+"</b><br/>";\r
+                                               html += ioType.getDescription()+"<br/>";\r
+                                       }\r
+                                       else if( obj != null ) {\r
+                                               html += obj.toString();\r
+                                       }\r
+                                       html += "</body></html>";\r
+                                       deviceInfoPane.setText(html);\r
+                               }\r
+                       }\r
+               );\r
+               JSplitPane sideSplitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT,\r
+                       new JScrollPane(deviceTree),\r
+                       new JScrollPane(deviceInfoPane)\r
+               ) {\r
+                       {\r
+                               setDividerLocation(300);\r
+                       }\r
+               };\r
+               add(new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, sideSplitPane, desktopPane) {\r
+                       {\r
+                               setOneTouchExpandable(true);\r
+                               setDividerLocation(250);\r
+                       }\r
+               });\r
+       }\r
+}\r
+\r
+/**\r
+ * 開いている MIDI デバイスを置くためのデスクトップ (View)\r
+ */\r
+class MidiDesktopPane extends JDesktopPane implements DropTargetListener {\r
+       MidiCablePane cablePane = new MidiCablePane(this);\r
+       public MidiDesktopPane(MidiDeviceTree deviceTree) {\r
+               add( cablePane, JLayeredPane.PALETTE_LAYER );\r
+               int i=0;\r
+               MidiDeviceTreeModel treeModel = (MidiDeviceTreeModel)deviceTree.getModel();\r
+               List<MidiConnecterListModel> deviceModelList = treeModel.deviceModelList;\r
+               for( MidiConnecterListModel deviceModel : deviceModelList ) {\r
+                       MidiDeviceFrame frame = new MidiDeviceFrame(deviceModel) {\r
+                               {\r
+                                       addInternalFrameListener(cablePane);\r
+                                       addComponentListener(cablePane);\r
+                               }\r
+                       };\r
+                       frame.addInternalFrameListener(deviceTree);\r
+                       deviceModel.addListDataListener(cablePane);\r
+                       add(frame);\r
+                       if( deviceModel.getMidiDevice().isOpen() ) {\r
+                               frame.setBounds( 10+(i%2)*260, 10+i*55, 250, 100 );\r
+                               frame.setVisible(true);\r
+                               i++;\r
+                       }\r
+               }\r
+               addComponentListener(\r
+                       new ComponentAdapter() {\r
+                               public void componentResized(ComponentEvent e) {\r
+                                       cablePane.setSize(getSize());\r
+                               }\r
+                       }\r
+               );\r
+               new DropTarget( this, DnDConstants.ACTION_COPY_OR_MOVE, this, true );\r
+       }\r
+       public void dragEnter(DropTargetDragEvent dtde) {\r
+               Transferable trans = dtde.getTransferable();\r
+               if( trans.isDataFlavorSupported(MidiDeviceTree.treeModelFlavor) ) {\r
+                       dtde.acceptDrag(DnDConstants.ACTION_COPY_OR_MOVE);\r
+               }\r
+       }\r
+       public void dragExit(DropTargetEvent dte) {}\r
+       public void dragOver(DropTargetDragEvent dtde) {}\r
+       public void dropActionChanged(DropTargetDragEvent dtde) {}\r
+       public void drop(DropTargetDropEvent dtde) {\r
+               dtde.acceptDrop(DnDConstants.ACTION_COPY_OR_MOVE);\r
+               try {\r
+                       int action = dtde.getDropAction() ;\r
+                       if( (action & DnDConstants.ACTION_COPY_OR_MOVE) != 0 ) {\r
+                               Transferable trans = dtde.getTransferable();\r
+                               Object data = trans.getTransferData(MidiDeviceTree.treeModelFlavor);\r
+                               if( data instanceof MidiConnecterListModel ) {\r
+                                       MidiConnecterListModel deviceModel = (MidiConnecterListModel)data;\r
+                                       try {\r
+                                               deviceModel.openDevice();\r
+                                       } catch( MidiUnavailableException e ) {\r
+                                               //\r
+                                               // デバイスを開くのに失敗した場合\r
+                                               //\r
+                                               //   例えば、「Microsort MIDI マッパー」と\r
+                                               //   「Microsoft GS Wavetable SW Synth」を\r
+                                               //   同時に開こうとするとここに来る。\r
+                                               //\r
+                                               dtde.dropComplete(false);\r
+                                               String message = "MIDIデバイス "\r
+                                                               + deviceModel\r
+                                                               +" を開けません。\n"\r
+                                                               + "すでに開かれているデバイスが"\r
+                                                               + "このデバイスを連動して開いていないか確認してください。\n\n"\r
+                                                               + e.getMessage();\r
+                                               JOptionPane.showMessageDialog(\r
+                                                       null, message,\r
+                                                       "Cannot open MIDI device",\r
+                                                       JOptionPane.ERROR_MESSAGE\r
+                                               );\r
+                                               return;\r
+                                       }\r
+                                       if( deviceModel.getMidiDevice().isOpen() ) {\r
+                                               dtde.dropComplete(true);\r
+                                               //\r
+                                               // デバイスが正常に開かれたことを確認できたら\r
+                                               // ドロップした場所へフレームを配置して可視化する。\r
+                                               //\r
+                                               JInternalFrame frame = getFrameOf(deviceModel);\r
+                                               if( frame != null ) {\r
+                                                       Point loc = dtde.getLocation();\r
+                                                       loc.translate( -frame.getWidth()/2, 0 );\r
+                                                       frame.setLocation(loc);\r
+                                                       frame.setVisible(true);\r
+                                               }\r
+                                               return;\r
+                                       }\r
+                               }\r
+                       }\r
+               }\r
+               catch (Exception ex) {\r
+                       ex.printStackTrace();\r
+               }\r
+               dtde.dropComplete(false);\r
+       }\r
+       /**\r
+        * 指定されたMIDIデバイスモデルに対するMIDIデバイスフレームを返します。\r
+        *\r
+        * @param deviceModel MIDIデバイスモデル\r
+        * @return 対応するMIDIデバイスフレーム(ない場合 null)\r
+        */\r
+       public MidiDeviceFrame getFrameOf(MidiConnecterListModel deviceModel) {\r
+               JInternalFrame[] frames = getAllFramesInLayer(JLayeredPane.DEFAULT_LAYER);\r
+               for( JInternalFrame frame : frames ) {\r
+                       if( frame instanceof MidiDeviceFrame ) {\r
+                               MidiDeviceFrame deviceFrame = (MidiDeviceFrame)frame;\r
+                               if( deviceFrame.listView.getModel() == deviceModel )\r
+                                       return deviceFrame;\r
+                       }\r
+               }\r
+               return null;\r
+       }\r
+}\r
+\r
+/**\r
+ * MIDI ケーブル描画面\r
+ */\r
+class MidiCablePane extends JComponent\r
+       implements ListDataListener, ComponentListener, InternalFrameListener\r
+{\r
+       private JDesktopPane desktopPane;\r
+       //private JTree tree;\r
+       public MidiCablePane(JDesktopPane desktopPane) {\r
+               this.desktopPane = desktopPane;\r
+               setOpaque(false);\r
+               setVisible(true);\r
+       }\r
+       //\r
+       // MidiDeviceFrame の開閉を検出\r
+       public void internalFrameActivated(InternalFrameEvent e) {}\r
+       public void internalFrameClosed(InternalFrameEvent e) { repaint(); }\r
+       public void internalFrameClosing(InternalFrameEvent e) {\r
+               JInternalFrame frame = e.getInternalFrame();\r
+               if( ! (frame instanceof MidiDeviceFrame) )\r
+                       return;\r
+               MidiDeviceFrame devFrame = (MidiDeviceFrame)frame;\r
+               MidiConnecterListModel devModel = devFrame.listView.getModel();\r
+               if( ! devModel.rxSupported() )\r
+                       return;\r
+               colorMap.remove(devModel.getMidiDevice().getReceivers().get(0));\r
+               repaint();\r
+       }\r
+       public void internalFrameDeactivated(InternalFrameEvent e) { repaint(); }\r
+       public void internalFrameDeiconified(InternalFrameEvent e) {}\r
+       public void internalFrameIconified(InternalFrameEvent e) {}\r
+       public void internalFrameOpened(InternalFrameEvent e) {}\r
+       //\r
+       // ウィンドウオペレーションの検出\r
+       public void componentHidden(ComponentEvent e) {}\r
+       public void componentMoved(ComponentEvent e) { repaint(); }\r
+       public void componentResized(ComponentEvent e) { repaint(); }\r
+       public void componentShown(ComponentEvent e) {}\r
+       //\r
+       // MidiConnecterListModel における Transmitter リストの更新を検出\r
+       public void contentsChanged(ListDataEvent e) { repaint(); }\r
+       public void intervalAdded(ListDataEvent e) { repaint(); }\r
+       public void intervalRemoved(ListDataEvent e) { repaint(); }\r
+       //\r
+       // ケーブル描画用\r
+       private static final int ARROW_SIZE = 15;\r
+       private static final double ARROW_ANGLE = Math.PI / 6.0;\r
+       private static final Stroke CABLE_STROKE = new BasicStroke(\r
+               3, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND\r
+       );\r
+       private static final Color[] CABLE_COLORS = {\r
+               new Color(255,0,0,191),\r
+               new Color(0,255,0,191),\r
+               new Color(0,0,255,191),\r
+               new Color(191,191,0,191),\r
+               new Color(0,191,191,191),\r
+               new Color(191,0,191,191),\r
+       };\r
+       private int nextColorIndex = 0;\r
+       private Hashtable<Receiver,Color> colorMap = new Hashtable<>();\r
+       public void paint(Graphics g) {\r
+               super.paint(g);\r
+               Graphics2D g2 = (Graphics2D)g;\r
+               g2.setStroke(CABLE_STROKE);\r
+               JInternalFrame[] frames =\r
+                       desktopPane.getAllFramesInLayer(JLayeredPane.DEFAULT_LAYER);\r
+               for( JInternalFrame frame : frames ) {\r
+                       if( ! (frame instanceof MidiDeviceFrame) )\r
+                               continue;\r
+                       MidiDeviceFrame txDeviceFrame = (MidiDeviceFrame)frame;\r
+                       List<Transmitter> txList = txDeviceFrame.listView.getModel().getMidiDevice().getTransmitters();\r
+                       for( Transmitter tx : txList ) {\r
+                               //\r
+                               // 送信端子から接続されている受信端子の存在を確認\r
+                               Receiver rx = tx.getReceiver();\r
+                               if( rx == null )\r
+                                       continue;\r
+                               //\r
+                               // 送信端子の矩形を特定\r
+                               Rectangle txRect = txDeviceFrame.getListCellBounds(tx);\r
+                               if( txRect == null )\r
+                                       continue;\r
+                               //\r
+                               // 受信端子のあるMIDIデバイスを探す\r
+                               Rectangle rxRect = null;\r
+                               for( JInternalFrame anotherFrame : frames ) {\r
+                                       if( ! (anotherFrame instanceof MidiDeviceFrame) )\r
+                                               continue;\r
+                                       //\r
+                                       // 受信端子の矩形を探す\r
+                                       MidiDeviceFrame rxDeviceFrame = (MidiDeviceFrame)anotherFrame;\r
+                                       if((rxRect = rxDeviceFrame.getListCellBounds(rx)) == null)\r
+                                               continue;\r
+                                       rxRect.translate(rxDeviceFrame.getX(), rxDeviceFrame.getY());\r
+                                       break;\r
+                               }\r
+                               if( rxRect == null )\r
+                                       continue;\r
+                               txRect.translate(txDeviceFrame.getX(), txDeviceFrame.getY());\r
+                               //\r
+                               // 色を探す\r
+                               Color color = colorMap.get(rx);\r
+                               if( color == null ) {\r
+                                       colorMap.put(rx, color=CABLE_COLORS[nextColorIndex++]);\r
+                                       if( nextColorIndex >= CABLE_COLORS.length )\r
+                                               nextColorIndex = 0;\r
+                               }\r
+                               g2.setColor(color);\r
+                               //\r
+                               // 始点\r
+                               int fromX = txRect.x;\r
+                               int fromY = txRect.y;\r
+                               int d = txRect.height - 2;\r
+                               g2.fillOval( fromX, fromY, d, d );\r
+                               // 線\r
+                               int halfHeight = d / 2;\r
+                               fromX += halfHeight;\r
+                               fromY += halfHeight;\r
+                               halfHeight = (rxRect.height / 2) - 1;\r
+                               int toX = rxRect.x + halfHeight;\r
+                               int toY = rxRect.y + halfHeight;\r
+                               g2.drawLine( fromX, fromY, toX, toY );\r
+                               // 矢印\r
+                               double lineAngle = Math.atan2(\r
+                                       (double)(toY - fromY),\r
+                                       (double)(toX - fromX)\r
+                               );\r
+                               double arrowAngle = lineAngle-ARROW_ANGLE;\r
+                               g2.drawLine(\r
+                                       toX, toY,\r
+                                       toX - (int)(ARROW_SIZE * Math.cos(arrowAngle)),\r
+                                       toY - (int)(ARROW_SIZE * Math.sin(arrowAngle))\r
+                               );\r
+                               arrowAngle = lineAngle+ARROW_ANGLE;\r
+                               g2.drawLine(\r
+                                       toX, toY,\r
+                                       toX - (int)(ARROW_SIZE * Math.cos(arrowAngle)),\r
+                                       toY - (int)(ARROW_SIZE * Math.sin(arrowAngle))\r
+                               );\r
+                       }\r
+               }\r
+       }\r
+}\r
+\r
diff --git a/src/MIDIEditor.java b/src/MIDIEditor.java
new file mode 100644 (file)
index 0000000..3250ab9
--- /dev/null
@@ -0,0 +1,2809 @@
+\r
+import java.awt.Component;\r
+import java.awt.Container;\r
+import java.awt.Dimension;\r
+import java.awt.FlowLayout;\r
+import java.awt.Insets;\r
+import java.awt.datatransfer.DataFlavor;\r
+import java.awt.datatransfer.Transferable;\r
+import java.awt.dnd.DnDConstants;\r
+import java.awt.dnd.DropTarget;\r
+import java.awt.dnd.DropTargetDragEvent;\r
+import java.awt.dnd.DropTargetDropEvent;\r
+import java.awt.dnd.DropTargetEvent;\r
+import java.awt.dnd.DropTargetListener;\r
+import java.awt.event.ActionEvent;\r
+import java.awt.event.ActionListener;\r
+import java.awt.event.ItemEvent;\r
+import java.awt.event.ItemListener;\r
+import java.awt.event.MouseEvent;\r
+import java.io.ByteArrayInputStream;\r
+import java.io.ByteArrayOutputStream;\r
+import java.io.EOFException;\r
+import java.io.File;\r
+import java.io.FileInputStream;\r
+import java.io.FileNotFoundException;\r
+import java.io.FileOutputStream;\r
+import java.io.IOException;\r
+import java.io.InputStream;\r
+import java.net.MalformedURLException;\r
+import java.net.URI;\r
+import java.net.URISyntaxException;\r
+import java.net.URL;\r
+import java.security.AccessControlException;\r
+import java.util.ArrayList;\r
+import java.util.EventObject;\r
+import java.util.Vector;\r
+\r
+import javax.sound.midi.InvalidMidiDataException;\r
+import javax.sound.midi.MetaMessage;\r
+import javax.sound.midi.MidiChannel;\r
+import javax.sound.midi.MidiEvent;\r
+import javax.sound.midi.MidiMessage;\r
+import javax.sound.midi.MidiSystem;\r
+import javax.sound.midi.Sequence;\r
+import javax.sound.midi.Sequencer;\r
+import javax.sound.midi.ShortMessage;\r
+import javax.sound.midi.SysexMessage;\r
+import javax.sound.midi.Track;\r
+import javax.swing.AbstractAction;\r
+import javax.swing.AbstractCellEditor;\r
+import javax.swing.Action;\r
+import javax.swing.Box;\r
+import javax.swing.BoxLayout;\r
+import javax.swing.DefaultCellEditor;\r
+import javax.swing.Icon;\r
+import javax.swing.JButton;\r
+import javax.swing.JCheckBox;\r
+import javax.swing.JComboBox;\r
+import javax.swing.JDialog;\r
+import javax.swing.JFileChooser;\r
+import javax.swing.JLabel;\r
+import javax.swing.JOptionPane;\r
+import javax.swing.JPanel;\r
+import javax.swing.JScrollPane;\r
+import javax.swing.JSplitPane;\r
+import javax.swing.JTable;\r
+import javax.swing.JToggleButton;\r
+import javax.swing.ListSelectionModel;\r
+import javax.swing.event.ChangeEvent;\r
+import javax.swing.event.ChangeListener;\r
+import javax.swing.event.ListSelectionEvent;\r
+import javax.swing.event.ListSelectionListener;\r
+import javax.swing.filechooser.FileNameExtensionFilter;\r
+import javax.swing.table.AbstractTableModel;\r
+import javax.swing.table.TableCellEditor;\r
+import javax.swing.table.TableColumnModel;\r
+import javax.swing.table.TableModel;\r
+\r
+/**\r
+ * MIDI Editor/Playlist for MIDI Chord Helper\r
+ *\r
+ * @author\r
+ *     Copyright (C) 2006-2013 Akiyoshi Kamide\r
+ *     http://www.yk.rim.or.jp/~kamide/music/chordhelper/\r
+ */\r
+class MidiEditor extends JDialog implements DropTargetListener, ListSelectionListener {\r
+       Insets  zero_insets = new Insets(0,0,0,0);\r
+\r
+       MidiDeviceManager deviceManager;\r
+\r
+       SequenceListModel seqListModel;\r
+       JFileChooser    file_chooser = null;\r
+       Base64Dialog    base64_dialog = null;\r
+       NewSequenceDialog       new_seq_dialog;\r
+       MidiEventDialog eventDialog = new MidiEventDialog();\r
+\r
+       MidiEvent copied_events[] = null;\r
+       int copied_events_PPQ = 0;\r
+\r
+       ListSelectionModel\r
+       seq_selection_model,\r
+       track_selection_model,\r
+       event_selection_model;\r
+\r
+       JTable\r
+       sequence_table_view,\r
+       track_table_view,\r
+       event_table_view;\r
+\r
+       JScrollPane\r
+       scrollable_sequence_table,\r
+       scrollable_track_table_view,\r
+       scrollable_event_table_view;\r
+\r
+       JLabel\r
+       total_time_label,\r
+       tracks_label,\r
+       midi_events_label;\r
+\r
+       JButton\r
+       add_new_sequence_button, delete_sequence_button,\r
+       base64_encode_button = null,\r
+       add_midi_file_button,\r
+       save_midi_file_button,\r
+       jump_sequence_button,\r
+       add_track_button, remove_track_button,\r
+       add_event_button, jump_event_button,\r
+       cut_event_button, copy_event_button,\r
+       paste_event_button, remove_event_button;\r
+\r
+       JCheckBox pair_note_checkbox;\r
+\r
+       JButton forward_button, backward_button;\r
+       JToggleButton play_pause_button;\r
+\r
+       JSplitPane\r
+       sequence_split_pane, track_split_pane;\r
+\r
+       VirtualMidiDevice midiDevice = new AbstractVirtualMidiDevice() {\r
+               {\r
+                       info = new MyInfo();\r
+                       setMaxReceivers(0);\r
+               }\r
+               class MyInfo extends Info {\r
+                       protected MyInfo() {\r
+                               super(\r
+                                       "MIDI Editor",\r
+                                       "Unknown vendor",\r
+                                       "MIDI sequence editor",\r
+                                       ""\r
+                               );\r
+                       }\r
+               }\r
+       };\r
+\r
+       MidiEventCellEditor event_cell_editor;\r
+\r
+       class MidiEventCellEditor extends AbstractCellEditor implements TableCellEditor {\r
+               MidiEvent[] midi_events_to_be_removed; // 削除対象にする変更前イベント(null可)\r
+               MidiTrackModel midi_track_model; // 対象トラック\r
+               MidiSequenceModel seq_model;   // 対象シーケンス\r
+               MidiEvent sel_midi_evt = null; // 選択されたイベント\r
+               int sel_index = -1; // 選択されたイベントの場所\r
+               long current_tick = 0; // 選択されたイベントのtick位置\r
+\r
+               TickPositionModel tick_position_model = new TickPositionModel();\r
+               JToggleButton.ToggleButtonModel\r
+               pair_note_on_off_model = new JToggleButton.ToggleButtonModel();\r
+\r
+               JButton edit_event_button = new JButton();\r
+\r
+               Action cancel_action = new AbstractAction() {\r
+                       { putValue(NAME,"Cancel"); }\r
+                       public void actionPerformed(ActionEvent e) {\r
+                               fireEditingCanceled();\r
+                               eventDialog.setVisible(false);\r
+                       }\r
+               };\r
+\r
+               private void setSelectedEvent() {\r
+                       seq_model = seqListModel.getSequenceModel(seq_selection_model);\r
+                       eventDialog.midi_message_form.durationForm.setPPQ(\r
+                                       seq_model.getSequence().getResolution()\r
+                                       );\r
+                       tick_position_model.setSequenceIndex(\r
+                                       seq_model.getSequenceIndex()\r
+                                       );\r
+                       sel_index = -1;\r
+                       current_tick = 0;\r
+                       sel_midi_evt = null;\r
+                       midi_track_model = (MidiTrackModel)event_table_view.getModel();\r
+                       if( ! event_selection_model.isSelectionEmpty() ) {\r
+                               sel_index = event_selection_model.getMinSelectionIndex();\r
+                               sel_midi_evt = midi_track_model.getMidiEvent(sel_index);\r
+                               current_tick = sel_midi_evt.getTick();\r
+                               tick_position_model.setTickPosition(current_tick);\r
+                       }\r
+               }\r
+\r
+               // 指定のTick位置へジャンプ\r
+               Action query_jump_event_action = new AbstractAction() {\r
+                       { putValue(NAME,"Jump to ..."); }\r
+                       public void actionPerformed(ActionEvent e) {\r
+                               setSelectedEvent();\r
+                               eventDialog.setTitle("Jump selection to");\r
+                               eventDialog.ok_button.setAction(jump_event_action);\r
+                               eventDialog.openTickForm();\r
+                       }\r
+               };\r
+               Action jump_event_action = new AbstractAction() {\r
+                       { putValue(NAME,"Jump"); }\r
+                       public void actionPerformed(ActionEvent e) {\r
+                               scrollToEventAt(\r
+                                               tick_position_model.getTickPosition()\r
+                                               );\r
+                               eventDialog.setVisible(false);\r
+                       }\r
+               };\r
+\r
+               // 指定のTick位置へ貼り付け\r
+               Action query_paste_event_action = new AbstractAction() {\r
+                       { putValue(NAME,"Paste to ..."); }\r
+                       public void actionPerformed(ActionEvent e) {\r
+                               setSelectedEvent();\r
+                               eventDialog.setTitle("Paste to");\r
+                               eventDialog.ok_button.setAction(paste_event_action);\r
+                               eventDialog.openTickForm();\r
+                       }\r
+               };\r
+               Action paste_event_action = new AbstractAction() {\r
+                       { putValue(NAME,"Paste"); }\r
+                       public void actionPerformed(ActionEvent e) {\r
+                               long tick = tick_position_model.getTickPosition();\r
+                               ((MidiTrackModel)event_table_view.getModel()).addMidiEvents(\r
+                                               copied_events, tick, copied_events_PPQ\r
+                                               );\r
+                               scrollToEventAt( tick );\r
+                               seqListModel.fireSequenceChanged(seq_selection_model);\r
+                               eventDialog.setVisible(false);\r
+                       }\r
+               };\r
+\r
+               // イベントの追加(または変更)\r
+               Action query_add_event_action = new AbstractAction() {\r
+                       { putValue(NAME,"New"); }\r
+                       public void actionPerformed(ActionEvent e) {\r
+                               setSelectedEvent();\r
+                               midi_events_to_be_removed = null;\r
+                               eventDialog.setTitle("Add a new MIDI event");\r
+                               eventDialog.ok_button.setAction(add_event_action);\r
+                               int ch = midi_track_model.getChannel();\r
+                               if( ch >= 0 ) {\r
+                                       eventDialog.midi_message_form.channelText.setSelectedChannel(ch);\r
+                               }\r
+                               eventDialog.openEventForm();\r
+                       }\r
+               };\r
+               Action add_event_action = new AbstractAction() {\r
+                       { putValue(NAME,"OK"); }\r
+                       public void actionPerformed(ActionEvent e) {\r
+                               long tick = tick_position_model.getTickPosition();\r
+                               MidiMessage midi_msg = eventDialog.midi_message_form.getMessage();\r
+                               MidiEvent new_midi_event = new MidiEvent(midi_msg,tick);\r
+                               if( midi_events_to_be_removed != null ) {\r
+                                       midi_track_model.removeMidiEvents(midi_events_to_be_removed);\r
+                               }\r
+                               if( ! midi_track_model.addMidiEvent(new_midi_event) ) {\r
+                                       System.out.println("addMidiEvent failure");\r
+                                       return;\r
+                               }\r
+                               if(\r
+                                       pair_note_on_off_model.isSelected() &&\r
+                                       eventDialog.midi_message_form.isNote()\r
+                               ) {\r
+                                       ShortMessage sm = eventDialog.midi_message_form.getPartnerMessage();\r
+                                       if( sm == null ) scrollToEventAt( tick );\r
+                                       else {\r
+                                               int duration = eventDialog.midi_message_form.durationForm.getDuration();\r
+                                               if( eventDialog.midi_message_form.isNote(false) ) { // Note Off\r
+                                                       duration = -duration;\r
+                                               }\r
+                                               long partner_tick = tick + (long)duration;\r
+                                               if( partner_tick < 0L ) partner_tick = 0L;\r
+                                               MidiEvent partner_midi_event =\r
+                                                               new MidiEvent( (MidiMessage)sm, partner_tick );\r
+                                               if( ! midi_track_model.addMidiEvent(partner_midi_event) ) {\r
+                                                       System.out.println("addMidiEvent failure (note on/off partner message)");\r
+                                               }\r
+                                               scrollToEventAt( partner_tick > tick ? partner_tick : tick );\r
+                                       }\r
+                               }\r
+                               seqListModel.fireSequenceChanged(seq_model);\r
+                               eventDialog.setVisible(false);\r
+                               fireEditingStopped();\r
+                       }\r
+               };\r
+\r
+               // Constructor\r
+               //\r
+               public MidiEventCellEditor() {\r
+                       edit_event_button.setHorizontalAlignment(JButton.LEFT);\r
+                       eventDialog.cancel_button.setAction(cancel_action);\r
+                       eventDialog.midi_message_form.setOutputMidiChannels(\r
+                                       midiDevice.getChannels()\r
+                                       );\r
+                       eventDialog.tick_position_form.setModel(tick_position_model);\r
+                       edit_event_button.addActionListener(\r
+                               new ActionListener() {\r
+                                       public void actionPerformed(ActionEvent e) {\r
+                                               setSelectedEvent();\r
+                                               if( sel_midi_evt == null ) return;\r
+                                               MidiEvent partner_event = null;\r
+                                               eventDialog.midi_message_form.setMessage( sel_midi_evt.getMessage() );\r
+                                               if( eventDialog.midi_message_form.isNote() ) {\r
+                                                       int partner_index = midi_track_model.getIndexOfPartnerFor(sel_index);\r
+                                                       if( partner_index < 0 ) {\r
+                                                               eventDialog.midi_message_form.durationForm.setDuration(0);\r
+                                                       }\r
+                                                       else {\r
+                                                               partner_event = midi_track_model.getMidiEvent(partner_index);\r
+                                                               long partner_tick = partner_event.getTick();\r
+                                                               long duration = current_tick > partner_tick ?\r
+                                                                               current_tick - partner_tick : partner_tick - current_tick ;\r
+                                                               eventDialog.midi_message_form.durationForm.setDuration((int)duration);\r
+                                                       }\r
+                                               }\r
+                                               MidiEvent events[];\r
+                                               if( partner_event == null ) {\r
+                                                       events = new MidiEvent[1];\r
+                                                       events[0] = sel_midi_evt;\r
+                                               }\r
+                                               else {\r
+                                                       events = new MidiEvent[2];\r
+                                                       events[0] = sel_midi_evt;\r
+                                                       events[1] = partner_event;\r
+                                               }\r
+                                               midi_events_to_be_removed = events;\r
+                                               eventDialog.setTitle("Change MIDI event");\r
+                                               eventDialog.ok_button.setAction(add_event_action);\r
+                                               eventDialog.openEventForm();\r
+                                       }\r
+                               }\r
+                       );\r
+                       pair_note_on_off_model.addItemListener(new ItemListener() {\r
+                               public void itemStateChanged(ItemEvent e) {\r
+                                       eventDialog.midi_message_form.durationForm.setEnabled(\r
+                                                       pair_note_on_off_model.isSelected()\r
+                                                       );\r
+                               }\r
+                       });\r
+                       pair_note_on_off_model.setSelected(true);\r
+               }\r
+               // TableCellEditor\r
+               //\r
+               public boolean isCellEditable(EventObject e) {\r
+                       // ダブルクリックで編集\r
+                       return e instanceof MouseEvent && ((MouseEvent)e).getClickCount() == 2;\r
+               }\r
+               public Object getCellEditorValue() {\r
+                       return "";\r
+               }\r
+               public Component getTableCellEditorComponent(\r
+                               JTable table, Object value, boolean isSelected,\r
+                               int row, int column\r
+                               ) {\r
+                       edit_event_button.setText((String)value);\r
+                       return edit_event_button;\r
+               }\r
+       }\r
+\r
+       public Action move_to_top_action = new AbstractAction() {\r
+               {\r
+                       putValue( SHORT_DESCRIPTION,\r
+                                       "Move to top or previous song - 曲の先頭または前の曲へ戻る"\r
+                                       );\r
+                       putValue( LARGE_ICON_KEY, new ButtonIcon(ButtonIcon.TOP_ICON) );\r
+               }\r
+               public void actionPerformed( ActionEvent event ) {\r
+                       if( deviceManager.getSequencer().getTickPosition() <= 40 )\r
+                               loadNext(-1);\r
+                       deviceManager.timeRangeModel.setValue(0);\r
+               }\r
+       };\r
+       public Action move_to_bottom_action = new AbstractAction() {\r
+               {\r
+                       putValue( SHORT_DESCRIPTION, "Move to next song - 次の曲へ進む" );\r
+                       putValue( LARGE_ICON_KEY, new ButtonIcon(ButtonIcon.BOTTOM_ICON) );\r
+               }\r
+               public void actionPerformed( ActionEvent event ) {\r
+                       if( loadNext(1) ) deviceManager.timeRangeModel.setValue(0);\r
+               }\r
+       };\r
+       //\r
+       // Constructor\r
+       //\r
+       public MidiEditor(MidiDeviceManager deviceManager) {\r
+               this.deviceManager = deviceManager;\r
+               setTitle("MIDI Editor/Playlist - MIDI Chord Helper");\r
+               setBounds( 150, 200, 850, 500 );\r
+               setLayout(new FlowLayout());\r
+               Icon delete_icon = new ButtonIcon(ButtonIcon.X_ICON);\r
+               new DropTarget(\r
+                       this, DnDConstants.ACTION_COPY_OR_MOVE, this, true\r
+               );\r
+               total_time_label = new JLabel();\r
+               //\r
+               // Buttons (Sequence)\r
+               //\r
+               add_new_sequence_button = new JButton("New");\r
+               add_new_sequence_button.setToolTipText("Generate new song - 新しい曲を生成");\r
+               add_new_sequence_button.setMargin(zero_insets);\r
+               add_new_sequence_button.addActionListener(\r
+                       new ActionListener() {\r
+                               public void actionPerformed(ActionEvent e) {\r
+                                       new_seq_dialog.setVisible(true);\r
+                               }\r
+                       }\r
+               );\r
+               add_midi_file_button = new JButton("Open");\r
+               add_midi_file_button.setMargin(zero_insets);\r
+               add_midi_file_button.addActionListener(\r
+                       new ActionListener() {\r
+                               public void actionPerformed(ActionEvent e) {\r
+                                       if(\r
+                                                       file_chooser == null ||\r
+                                                       file_chooser.showOpenDialog(MidiEditor.this) != JFileChooser.APPROVE_OPTION\r
+                                                       ) return;\r
+                                       addSequenceFromMidiFile(file_chooser.getSelectedFile());\r
+                               }\r
+                       }\r
+               );\r
+               //\r
+               play_pause_button = new JToggleButton(\r
+                               deviceManager.timeRangeModel.startStopAction\r
+                               );\r
+               backward_button = new JButton(move_to_top_action);\r
+               backward_button.setMargin(zero_insets);\r
+               forward_button = new JButton(move_to_bottom_action);\r
+               forward_button.setMargin(zero_insets);\r
+               //\r
+               jump_sequence_button = new JButton("Jump");\r
+               jump_sequence_button.setToolTipText("Move to selected song - 選択した曲へ進む");\r
+               jump_sequence_button.setMargin(zero_insets);\r
+               jump_sequence_button.addActionListener(\r
+                       new ActionListener() {\r
+                               public void actionPerformed(ActionEvent e) {\r
+                                       load( seq_selection_model.getMinSelectionIndex() );\r
+                               }\r
+                       }\r
+               );\r
+               save_midi_file_button = new JButton("Save");\r
+               save_midi_file_button.setMargin(zero_insets);\r
+               save_midi_file_button.addActionListener(\r
+                       new ActionListener() {\r
+                               public void actionPerformed(ActionEvent e) {\r
+                                       if( file_chooser == null ) return;\r
+                                       File midi_file;\r
+                                       MidiSequenceModel seq_model =\r
+                                                       seqListModel.getSequenceModel(seq_selection_model);\r
+                                       String filename = seq_model.getFilename();\r
+                                       if( filename != null && ! filename.isEmpty() ) {\r
+                                               midi_file = new File(filename);\r
+                                               file_chooser.setSelectedFile(midi_file);\r
+                                       }\r
+                                       if( file_chooser.showSaveDialog(MidiEditor.this) != JFileChooser.APPROVE_OPTION ) {\r
+                                               return;\r
+                                       }\r
+                                       midi_file = file_chooser.getSelectedFile();\r
+                                       if( midi_file.exists() && ! confirm(\r
+                                                       "Overwrite " + midi_file.getName() + " ?\n"\r
+                                                                       + midi_file.getName() + " を上書きしてよろしいですか?"\r
+                                                       ) ) {\r
+                                               return;\r
+                                       }\r
+                                       FileOutputStream fos;\r
+                                       try {\r
+                                               fos = new FileOutputStream(midi_file);\r
+                                       }\r
+                                       catch( FileNotFoundException ex ) {\r
+                                               showError( midi_file.getName() + ": Cannot open to write" );\r
+                                               ex.printStackTrace();\r
+                                               return;\r
+                                       }\r
+                                       try {\r
+                                               fos.write(seq_model.getMIDIdata());\r
+                                               fos.close();\r
+                                               seq_model.setModified(false);\r
+                                       }\r
+                                       catch( IOException ex ) {\r
+                                               showError( midi_file.getName() + ": I/O Error" );\r
+                                               ex.printStackTrace();\r
+                                       }\r
+                               }\r
+                       }\r
+               );\r
+               delete_sequence_button = new JButton("Delete", delete_icon);\r
+               delete_sequence_button.setMargin(zero_insets);\r
+               delete_sequence_button.addActionListener(\r
+                       new ActionListener() {\r
+                               public void actionPerformed(ActionEvent e) {\r
+                                       if(\r
+                                                       file_chooser != null &&\r
+                                                       seqListModel.getSequenceModel(seq_selection_model).isModified() &&\r
+                                                       ! confirm(\r
+                                                                       "Selected MIDI sequence not saved - delete it ?\n" +\r
+                                                                                       "選択したMIDIシーケンスは保存されていませんが、削除しますか?"\r
+                                                                       )\r
+                                                       ) return;\r
+                                       seqListModel.removeSequence(seq_selection_model);\r
+                                       total_time_label.setText( seqListModel.getTotalLength() );\r
+                               }\r
+                       }\r
+               );\r
+               //\r
+               // Buttons (Track)\r
+               //\r
+               tracks_label = new JLabel("Tracks");\r
+               add_track_button = new JButton("New");\r
+               add_track_button.setMargin(zero_insets);\r
+               add_track_button.addActionListener(\r
+                               new ActionListener() {\r
+                                       public void actionPerformed(ActionEvent e) {\r
+                                               seqListModel.getSequenceModel(seq_selection_model).createTrack();\r
+                                               int n_tracks = seqListModel.getSequenceModel(seq_selection_model).getRowCount();\r
+                                               if( n_tracks > 0 ) {\r
+                                                       // Select a created track\r
+                                                       track_selection_model.setSelectionInterval(\r
+                                                                       n_tracks - 1, n_tracks - 1\r
+                                                                       );\r
+                                               }\r
+                                               seqListModel.fireSequenceChanged(seq_selection_model);\r
+                                       }\r
+                               }\r
+                               );\r
+               remove_track_button = new JButton("Delete", delete_icon);\r
+               remove_track_button.setMargin(zero_insets);\r
+               remove_track_button.addActionListener(\r
+                               new ActionListener() {\r
+                                       public void actionPerformed(ActionEvent e) {\r
+                                               if( ! confirm(\r
+                                                               "Do you want to delete selected track ?\n選択したトラックを削除しますか?"\r
+                                                               )) return;\r
+                                               seqListModel.getSequenceModel(\r
+                                                               seq_selection_model\r
+                                                               ).deleteTracks( track_selection_model );\r
+                                               seqListModel.fireSequenceChanged(seq_selection_model);\r
+                                       }\r
+                               }\r
+                               );\r
+               JPanel track_button_panel = new JPanel();\r
+               track_button_panel.add(add_track_button);\r
+               track_button_panel.add(remove_track_button);\r
+               //\r
+               // Buttons (Event)\r
+               //\r
+               event_cell_editor = new MidiEventCellEditor();\r
+               add_event_button = new JButton(event_cell_editor.query_add_event_action);\r
+               add_event_button.setMargin(zero_insets);\r
+               jump_event_button = new JButton(event_cell_editor.query_jump_event_action);\r
+               jump_event_button.setMargin(zero_insets);\r
+               paste_event_button = new JButton(event_cell_editor.query_paste_event_action);\r
+               paste_event_button.setMargin(zero_insets);\r
+               remove_event_button = new JButton("Delete", delete_icon);\r
+               remove_event_button.setMargin(zero_insets);\r
+               remove_event_button.addActionListener(\r
+                       new ActionListener() {\r
+                               public void actionPerformed(ActionEvent e) {\r
+                                       if( ! confirm(\r
+                                                       "Do you want to delete selected event ?\n選択したMIDIイベントを削除しますか?"\r
+                                                       )) return;\r
+                                       ((MidiTrackModel)event_table_view.getModel()).removeMidiEvents( event_selection_model );\r
+                                       seqListModel.fireSequenceChanged(seq_selection_model);\r
+                               }\r
+                       }\r
+               );\r
+               cut_event_button = new JButton("Cut");\r
+               cut_event_button.setMargin(zero_insets);\r
+               cut_event_button.addActionListener(\r
+                       new ActionListener() {\r
+                               public void actionPerformed(ActionEvent e) {\r
+                                       if( ! confirm(\r
+                                                       "Do you want to cut selected event ?\n選択したMIDIイベントを切り取りますか?"\r
+                                                       )) return;\r
+                                       MidiTrackModel track_model = (MidiTrackModel)event_table_view.getModel();\r
+                                       copied_events = track_model.getMidiEvents( event_selection_model );\r
+                                       copied_events_PPQ = seqListModel.getSequenceModel(\r
+                                                       seq_selection_model\r
+                                                       ).getSequence().getResolution();\r
+                                       track_model.removeMidiEvents( copied_events );\r
+                                       seqListModel.fireSequenceChanged(seq_selection_model);\r
+                               }\r
+                       }\r
+               );\r
+               copy_event_button = new JButton("Copy");\r
+               copy_event_button.setMargin(zero_insets);\r
+               copy_event_button.addActionListener(\r
+                       new ActionListener() {\r
+                               public void actionPerformed(ActionEvent e) {\r
+                                       copied_events = ((MidiTrackModel)event_table_view.getModel()).getMidiEvents(\r
+                                                       event_selection_model\r
+                                                       );\r
+                                       copied_events_PPQ = seqListModel.getSequenceModel(\r
+                                                       seq_selection_model\r
+                                                       ).getSequence().getResolution();\r
+                                       updateButtonStatus();\r
+                               }\r
+                       }\r
+               );\r
+               pair_note_checkbox = new JCheckBox( "Pair NoteON/OFF" );\r
+               pair_note_checkbox.setModel(event_cell_editor.pair_note_on_off_model);\r
+               //\r
+               // Tables\r
+               //\r
+               MidiSequenceModel empty_track_table_model = new MidiSequenceModel(\r
+                       seqListModel = new SequenceListModel(deviceManager)\r
+               );\r
+               sequence_table_view = new JTable( seqListModel );\r
+               track_table_view = new JTable( empty_track_table_model );\r
+               event_table_view = new JTable( new MidiTrackModel() );\r
+               //\r
+               seqListModel.sizeColumnWidthToFit( sequence_table_view );\r
+               //\r
+               TableColumnModel track_column_model = track_table_view.getColumnModel();\r
+               empty_track_table_model.sizeColumnWidthToFit(track_column_model);\r
+               //\r
+               seq_selection_model = sequence_table_view.getSelectionModel();\r
+               seq_selection_model.setSelectionMode( ListSelectionModel.SINGLE_SELECTION );\r
+               seq_selection_model.addListSelectionListener(\r
+                       new ListSelectionListener() {\r
+                               public void valueChanged(ListSelectionEvent e) {\r
+                                       if( e.getValueIsAdjusting() ) return;\r
+                                       sequenceSelectionChanged();\r
+                                       track_selection_model.setSelectionInterval(0,0);\r
+                               }\r
+                       }\r
+               );\r
+               JScrollPane scrollable_sequence_table = new JScrollPane(sequence_table_view);\r
+               //\r
+               track_selection_model = track_table_view.getSelectionModel();\r
+               track_selection_model.setSelectionMode(\r
+                               ListSelectionModel.MULTIPLE_INTERVAL_SELECTION\r
+                               );\r
+               track_selection_model.addListSelectionListener(this);\r
+               JScrollPane scrollable_track_table_view\r
+               = new JScrollPane(track_table_view);\r
+               //\r
+               event_selection_model = event_table_view.getSelectionModel();\r
+               event_selection_model.setSelectionMode(\r
+                               ListSelectionModel.MULTIPLE_INTERVAL_SELECTION\r
+                               );\r
+               event_selection_model.addListSelectionListener(this);\r
+               scrollable_event_table_view\r
+               = new JScrollPane(event_table_view);\r
+\r
+               base64_dialog = new Base64Dialog(this);\r
+               if( base64_dialog.isBase64Available() ) {\r
+                       base64_encode_button = new JButton( "Base64 Encode" );\r
+                       base64_encode_button.setMargin(zero_insets);\r
+                       base64_encode_button.addActionListener(\r
+                               new ActionListener() {\r
+                                       public void actionPerformed(ActionEvent e) {\r
+                                               MidiSequenceModel seq_model =\r
+                                                               seqListModel.getSequenceModel(seq_selection_model);\r
+                                               base64_dialog.setMIDIData(\r
+                                                               seq_model.getMIDIdata(), seq_model.getFilename()\r
+                                                               );\r
+                                               base64_dialog.setVisible(true);\r
+                                       }\r
+                               }\r
+                       );\r
+               }\r
+               new_seq_dialog = new NewSequenceDialog(this);\r
+               new_seq_dialog.setChannels( midiDevice.getChannels() );\r
+\r
+               JPanel button_panel = new JPanel();\r
+               button_panel.setLayout( new BoxLayout( button_panel, BoxLayout.LINE_AXIS ) );\r
+               button_panel.add( total_time_label );\r
+               button_panel.add( Box.createRigidArea(new Dimension(10, 0)) );\r
+               button_panel.add( add_new_sequence_button );\r
+               button_panel.add( Box.createRigidArea(new Dimension(5, 0)) );\r
+               button_panel.add( add_midi_file_button );\r
+               button_panel.add( Box.createRigidArea(new Dimension(5, 0)) );\r
+               button_panel.add( backward_button );\r
+               button_panel.add( Box.createRigidArea(new Dimension(5, 0)) );\r
+               button_panel.add( play_pause_button );\r
+               button_panel.add( Box.createRigidArea(new Dimension(5, 0)) );\r
+               button_panel.add( forward_button );\r
+               button_panel.add( Box.createRigidArea(new Dimension(5, 0)) );\r
+               button_panel.add( jump_sequence_button );\r
+               button_panel.add( Box.createRigidArea(new Dimension(5, 0)) );\r
+               button_panel.add( save_midi_file_button );\r
+               if( base64_encode_button != null ) {\r
+                       button_panel.add( Box.createRigidArea(new Dimension(5, 0)) );\r
+                       button_panel.add( base64_encode_button );\r
+               }\r
+               button_panel.add( Box.createRigidArea(new Dimension(5, 0)) );\r
+               button_panel.add( delete_sequence_button );\r
+               button_panel.add( Box.createRigidArea(new Dimension(5, 0)) );\r
+               button_panel.add( new SpeedSlider(deviceManager.speedSliderModel) );\r
+\r
+               JPanel playlist_panel = new JPanel();\r
+               playlist_panel.setLayout(\r
+                               new BoxLayout( playlist_panel, BoxLayout.Y_AXIS )\r
+                               );\r
+               playlist_panel.add( scrollable_sequence_table );\r
+               playlist_panel.add( Box.createRigidArea(new Dimension(0, 10)) );\r
+               playlist_panel.add( button_panel );\r
+               playlist_panel.add( Box.createRigidArea(new Dimension(0, 10)) );\r
+\r
+               sequenceSelectionChanged();\r
+               total_time_label.setText( seqListModel.getTotalLength() );\r
+\r
+               try {\r
+                       file_chooser = new JFileChooser();\r
+                       FileNameExtensionFilter filter = new FileNameExtensionFilter(\r
+                                       "MIDI sequence (*.mid)", "mid"\r
+                                       );\r
+                       file_chooser.setFileFilter(filter);\r
+               }\r
+               catch( ExceptionInInitializerError e ) {\r
+                       file_chooser = null;\r
+               }\r
+               catch( NoClassDefFoundError e ) {\r
+                       file_chooser = null;\r
+               }\r
+               catch( AccessControlException e ) {\r
+                       file_chooser = null;\r
+               }\r
+               if( file_chooser == null ) {\r
+                       // Applet cannot access local files\r
+                       add_midi_file_button.setVisible(false);\r
+                       save_midi_file_button.setVisible(false);\r
+               }\r
+               //\r
+               // Lists and input panel\r
+               //\r
+               JPanel track_list_panel = new JPanel();\r
+               track_list_panel.setLayout(new BoxLayout( track_list_panel, BoxLayout.PAGE_AXIS ));\r
+               track_list_panel.add( tracks_label );\r
+               track_list_panel.add( Box.createRigidArea(new Dimension(0, 5)) );\r
+               track_list_panel.add( scrollable_track_table_view );\r
+               track_list_panel.add( Box.createRigidArea(new Dimension(0, 5)) );\r
+               track_list_panel.add( track_button_panel );\r
+               //\r
+               JPanel event_list_panel = new JPanel();\r
+               event_list_panel.add( midi_events_label = new JLabel("No track selected") );\r
+               event_list_panel.add(scrollable_event_table_view);\r
+               //\r
+               JPanel event_button_panel = new JPanel();\r
+               event_button_panel.add(pair_note_checkbox);\r
+               event_button_panel.add(jump_event_button);\r
+               event_button_panel.add(add_event_button);\r
+               event_button_panel.add(copy_event_button);\r
+               event_button_panel.add(cut_event_button);\r
+               event_button_panel.add(paste_event_button);\r
+               event_button_panel.add(remove_event_button);\r
+               //\r
+               event_list_panel.add( event_button_panel );\r
+               event_list_panel.setLayout(\r
+                               new BoxLayout( event_list_panel, BoxLayout.Y_AXIS )\r
+                               );\r
+               //\r
+               track_split_pane = new JSplitPane(\r
+                       JSplitPane.HORIZONTAL_SPLIT,\r
+                       track_list_panel, event_list_panel\r
+               );\r
+               track_split_pane.setDividerLocation(300);\r
+               sequence_split_pane = new JSplitPane(\r
+                       JSplitPane.VERTICAL_SPLIT,\r
+                       playlist_panel, track_split_pane\r
+               );\r
+               sequence_split_pane.setDividerLocation(160);\r
+               Container cp = getContentPane();\r
+               cp.setLayout( new BoxLayout( cp, BoxLayout.Y_AXIS ) );\r
+               cp.add(Box.createVerticalStrut(2));\r
+               cp.add(sequence_split_pane);\r
+               //\r
+               seq_selection_model.setSelectionInterval(0,0);\r
+               updateButtonStatus();\r
+       }\r
+       public void dragEnter(DropTargetDragEvent event) {\r
+               if( event.isDataFlavorSupported(DataFlavor.javaFileListFlavor) )\r
+                       event.acceptDrag(DnDConstants.ACTION_COPY_OR_MOVE);\r
+       }\r
+       public void dragExit(DropTargetEvent event) {}\r
+       public void dragOver(DropTargetDragEvent event) {}\r
+       public void dropActionChanged(DropTargetDragEvent event) {}\r
+       @SuppressWarnings("unchecked")\r
+       public void drop(DropTargetDropEvent event) {\r
+               event.acceptDrop(DnDConstants.ACTION_COPY_OR_MOVE);\r
+               try {\r
+                       int action = event.getDropAction();\r
+                       if ( (action & DnDConstants.ACTION_COPY_OR_MOVE) != 0 ) {\r
+                               Transferable t = event.getTransferable();\r
+                               Object data = t.getTransferData(DataFlavor.javaFileListFlavor);\r
+                               loadAndPlay((java.util.List<File>)data);\r
+                               event.dropComplete(true);\r
+                               return;\r
+                       }\r
+                       event.dropComplete(false);\r
+               }\r
+               catch (Exception ex) {\r
+                       ex.printStackTrace();\r
+                       event.dropComplete(false);\r
+               }\r
+       }\r
+       public void valueChanged(ListSelectionEvent e) {\r
+               boolean is_adjusting = e.getValueIsAdjusting();\r
+               if( is_adjusting ) return;\r
+               Object src = e.getSource();\r
+               if( src == track_selection_model ) {\r
+                       if(\r
+                               seqListModel.getSequenceModel(seq_selection_model) == null\r
+                               ||\r
+                               track_selection_model.isSelectionEmpty()\r
+                       ) {\r
+                               midi_events_label.setText("MIDI Events (No track selected)");\r
+                               event_table_view.setModel(new MidiTrackModel());\r
+                       }\r
+                       else {\r
+                               int sel_index = track_selection_model.getMinSelectionIndex();\r
+                               MidiTrackModel track_model\r
+                               = seqListModel.getSequenceModel(seq_selection_model).getTrackModel(sel_index);\r
+                               if( track_model == null ) {\r
+                                       midi_events_label.setText("MIDI Events (No track selected)");\r
+                                       event_table_view.setModel(new MidiTrackModel());\r
+                               }\r
+                               else {\r
+                                       midi_events_label.setText(\r
+                                               String.format("MIDI Events (in track No.%d)", sel_index)\r
+                                       );\r
+                                       event_table_view.setModel(track_model);\r
+                                       TableColumnModel tcm = event_table_view.getColumnModel();\r
+                                       track_model.sizeColumnWidthToFit(tcm);\r
+                                       tcm.getColumn( MidiTrackModel.COLUMN_MESSAGE ).setCellEditor(event_cell_editor);\r
+                               }\r
+                       }\r
+                       updateButtonStatus();\r
+                       event_selection_model.setSelectionInterval(0,0);\r
+               }\r
+               else if( src == event_selection_model ) {\r
+                       if( ! event_selection_model.isSelectionEmpty() ) {\r
+                               MidiTrackModel track_model\r
+                               = (MidiTrackModel)event_table_view.getModel();\r
+                               int min_index = event_selection_model.getMinSelectionIndex();\r
+                               if( track_model.hasTrack() ) {\r
+                                       MidiEvent midi_event = track_model.getMidiEvent(min_index);\r
+                                       MidiMessage msg = midi_event.getMessage();\r
+                                       if( msg instanceof ShortMessage ) {\r
+                                               ShortMessage sm = (ShortMessage)msg;\r
+                                               int cmd = sm.getCommand();\r
+                                               if( cmd == 0x80 || cmd == 0x90 || cmd == 0xA0 ) {\r
+                                                       // ノート番号を持つ場合、音を鳴らす。\r
+                                                       MidiChannel out_midi_channels[] = midiDevice.getChannels();\r
+                                                       int ch = sm.getChannel();\r
+                                                       int note = sm.getData1();\r
+                                                       int vel = sm.getData2();\r
+                                                       out_midi_channels[ch].noteOn( note, vel );\r
+                                                       out_midi_channels[ch].noteOff( note, vel );\r
+                                               }\r
+                                       }\r
+                               }\r
+                               if( pair_note_checkbox.isSelected() ) {\r
+                                       int max_index = event_selection_model.getMaxSelectionIndex();\r
+                                       int partner_index;\r
+                                       for( int i=min_index; i<=max_index; i++ ) {\r
+                                               if(\r
+                                                               event_selection_model.isSelectedIndex(i)\r
+                                                               &&\r
+                                                               (partner_index = track_model.getIndexOfPartnerFor(i)) >= 0\r
+                                                               &&\r
+                                                               ! event_selection_model.isSelectedIndex(partner_index)\r
+                                                               ) {\r
+                                                       event_selection_model.addSelectionInterval(\r
+                                                                       partner_index, partner_index\r
+                                                                       );\r
+                                               }\r
+                                       }\r
+                               }\r
+                       }\r
+                       updateButtonStatus();\r
+               }\r
+       }\r
+       private void showError( String message ) {\r
+               JOptionPane.showMessageDialog(\r
+                       this, message,\r
+                       ChordHelperApplet.VersionInfo.NAME,\r
+                       JOptionPane.ERROR_MESSAGE\r
+               );\r
+       }\r
+       private void showWarning( String message ) {\r
+               JOptionPane.showMessageDialog(\r
+                       this, message,\r
+                       ChordHelperApplet.VersionInfo.NAME,\r
+                       JOptionPane.WARNING_MESSAGE\r
+               );\r
+       }\r
+       private boolean confirm( String message ) {\r
+               return JOptionPane.showConfirmDialog(\r
+                       this, message,\r
+                       ChordHelperApplet.VersionInfo.NAME,\r
+                       JOptionPane.YES_NO_OPTION,\r
+                       JOptionPane.WARNING_MESSAGE\r
+               ) == JOptionPane.YES_OPTION ;\r
+       }\r
+       public void setVisible(boolean is_to_visible) {\r
+               if( is_to_visible && isVisible() ) toFront();\r
+               else super.setVisible(is_to_visible);\r
+       }\r
+       public void sequenceSelectionChanged() {\r
+               MidiSequenceModel seq_model\r
+               = seqListModel.getSequenceModel(seq_selection_model);\r
+               jump_sequence_button.setEnabled( seq_model != null );\r
+               save_midi_file_button.setEnabled( seq_model != null );\r
+               add_track_button.setEnabled( seq_model != null );\r
+               if( base64_encode_button != null )\r
+                       base64_encode_button.setEnabled( seq_model != null );\r
+\r
+               if( seq_model != null ) {\r
+                       int sel_index = seq_selection_model.getMinSelectionIndex();\r
+                       delete_sequence_button.setEnabled(true);\r
+                       track_table_view.setModel(seq_model);\r
+                       TableColumnModel tcm = track_table_view.getColumnModel();\r
+                       seq_model.sizeColumnWidthToFit(tcm);\r
+                       tcm.getColumn(MidiSequenceModel.COLUMN_RECORD_CHANNEL).setCellEditor(\r
+                                       seq_model.new RecordChannelCellEditor()\r
+                                       );\r
+                       track_selection_model.setSelectionInterval(0,0);\r
+                       tracks_label.setText(\r
+                                       String.format("Tracks (in MIDI file No.%d)", sel_index)\r
+                                       );\r
+                       // event_cell_editor.setSequenceModel(seq_model);\r
+               }\r
+               else {\r
+                       delete_sequence_button.setEnabled(false);\r
+                       track_table_view.setModel(new MidiSequenceModel(seqListModel));\r
+                       tracks_label.setText("Tracks (No MIDI file selected)");\r
+               }\r
+               updateButtonStatus();\r
+       }\r
+       public void updateButtonStatus() {\r
+               boolean is_track_selected = (\r
+                               ! track_selection_model.isSelectionEmpty()\r
+                               &&\r
+                               seqListModel.getSequenceModel(seq_selection_model) != null\r
+                               &&\r
+                               seqListModel.getSequenceModel(seq_selection_model).getRowCount() > 0\r
+                               );\r
+               //\r
+               // Track list\r
+               remove_track_button.setEnabled( is_track_selected );\r
+               //\r
+               TableModel tm = event_table_view.getModel();\r
+               if( ! (tm instanceof MidiTrackModel) ) return;\r
+               //\r
+               MidiTrackModel track_model = (MidiTrackModel)tm;\r
+               jump_sequence_button.setEnabled(\r
+                               track_model != null && track_model.getRowCount() > 0\r
+                               );\r
+               // Event list\r
+               boolean is_event_selected = (\r
+                               !(\r
+                                               event_selection_model.isSelectionEmpty() ||\r
+                                               track_model == null || track_model.getRowCount() == 0\r
+                                               ) && is_track_selected\r
+                               );\r
+               copy_event_button.setEnabled( is_event_selected );\r
+               remove_event_button.setEnabled( is_event_selected );\r
+               cut_event_button.setEnabled( is_event_selected );\r
+               jump_event_button.setEnabled(\r
+                               track_model != null && is_track_selected\r
+                               );\r
+               add_event_button.setEnabled(\r
+                               track_model != null && is_track_selected\r
+                               );\r
+               paste_event_button.setEnabled(\r
+                               track_model != null && is_track_selected &&\r
+                               copied_events != null && copied_events.length > 0\r
+                               );\r
+       }\r
+       public String getMIDIdataBase64() {\r
+               base64_dialog.setMIDIData(\r
+                               deviceManager.timeRangeModel.getSequenceModel().getMIDIdata()\r
+                               );\r
+               return base64_dialog.getBase64Data();\r
+       }\r
+       public int addSequence() {\r
+               return addSequence(new_seq_dialog.getMidiSequence());\r
+       }\r
+       public int addSequence(Sequence seq) {\r
+               int last_index = seqListModel.addSequence(seq);\r
+               total_time_label.setText( seqListModel.getTotalLength() );\r
+               if( ! deviceManager.getSequencer().isRunning() ) {\r
+                       loadAndPlay(last_index);\r
+               }\r
+               return last_index;\r
+       }\r
+       public int addSequenceFromBase64Text(String base64_encoded_text, String filename) {\r
+               base64_dialog.setBase64Data( base64_encoded_text );\r
+               return addSequenceFromMidiData( base64_dialog.getMIDIData(), filename );\r
+       }\r
+       public int addSequenceFromBase64Text() {\r
+               return addSequenceFromMidiData( base64_dialog.getMIDIData(), null );\r
+       }\r
+       public int addSequenceFromMidiData(byte[] data, String filename) {\r
+               int last_index;\r
+               try {\r
+                       last_index = seqListModel.addSequence(data,filename);\r
+               } catch( InvalidMidiDataException e ) {\r
+                       showWarning("MIDI data invalid");\r
+                       return -1;\r
+               }\r
+               total_time_label.setText( seqListModel.getTotalLength() );\r
+               return last_index;\r
+       }\r
+       public int addSequenceFromMidiFile(File midi_file) {\r
+               int last_index;\r
+               try {\r
+                       last_index = seqListModel.addSequence(midi_file);\r
+               } catch( FileNotFoundException e ) {\r
+                       showWarning( midi_file.getName() + " : not found" );\r
+                       return -1;\r
+               } catch( InvalidMidiDataException e ) {\r
+                       showWarning( midi_file.getName() + " : MIDI data invalid" );\r
+                       return -1;\r
+               } catch( AccessControlException e ) {\r
+                       showError( midi_file.getName() + ": Cannot access" );\r
+                       e.printStackTrace();\r
+                       return -1;\r
+               }\r
+               total_time_label.setText( seqListModel.getTotalLength() );\r
+               return last_index;\r
+       }\r
+       public int addSequenceFromURL(String midi_file_url) {\r
+               int last_index;\r
+               try {\r
+                       last_index = seqListModel.addSequence(midi_file_url);\r
+               } catch( InvalidMidiDataException e ) {\r
+                       showWarning( midi_file_url + " : MIDI data invalid" );\r
+                       return -1;\r
+               } catch( AccessControlException e ) {\r
+                       showError( midi_file_url + ": Cannot access" );\r
+                       e.printStackTrace();\r
+                       return -1;\r
+               }\r
+               total_time_label.setText( seqListModel.getTotalLength() );\r
+               return last_index;\r
+       }\r
+       public void load(int index) {\r
+               seqListModel.loadToSequencer(index);\r
+               sequenceSelectionChanged();\r
+       }\r
+       public boolean loadNext(int offset) {\r
+               boolean retval = seqListModel.loadNext(offset);\r
+               sequenceSelectionChanged();\r
+               return retval;\r
+       }\r
+       public void loadAndPlay(int index) {\r
+               load(index);\r
+               deviceManager.timeRangeModel.start();\r
+       }\r
+       public void loadAndPlay() {\r
+               loadAndPlay( seq_selection_model.getMinSelectionIndex() );\r
+       }\r
+       public void loadAndPlay( java.util.List<File> fileList ) {\r
+               int lastIndex = -1;\r
+               int nextIndex = -1;\r
+               for( File f : fileList ) {\r
+                       lastIndex = addSequenceFromMidiFile(f);\r
+                       if( nextIndex == -1 ) nextIndex = lastIndex;\r
+               }\r
+               if( deviceManager.getSequencer().isRunning() ) {\r
+                       setVisible(true);\r
+               }\r
+               else if( nextIndex >= 0 ) {\r
+                       loadAndPlay(nextIndex);\r
+               }\r
+       }\r
+       public boolean isModified() {\r
+               return seqListModel.isModified();\r
+       }\r
+       public boolean isRecordable() {\r
+               MidiSequenceModel seq_model =\r
+                               seqListModel.getSequenceModel(seq_selection_model);\r
+               return seq_model == null ? false : seq_model.isRecordable();\r
+       }\r
+       public void scrollToEventAt( long tick ) {\r
+               MidiTrackModel track_model = (MidiTrackModel)event_table_view.getModel();\r
+               scrollToEventAt( track_model.tickToIndex(tick) );\r
+       }\r
+       public void scrollToEventAt( int index ) {\r
+               scrollable_event_table_view.getVerticalScrollBar().setValue(\r
+                               index * event_table_view.getRowHeight()\r
+                               );\r
+               event_selection_model.setSelectionInterval( index, index );\r
+       }\r
+}\r
+\r
+/////////////////////////////////////////////////////////////\r
+//\r
+// プレイリスト\r
+//\r
+class SequenceListModel extends AbstractTableModel\r
+{\r
+       public static final int COLUMN_SEQ_NUMBER       = 0;\r
+       public static final int COLUMN_MODIFIED = 1;\r
+       public static final int COLUMN_DIVISION_TYPE    = 2;\r
+       public static final int COLUMN_RESOLUTION       = 3;\r
+       public static final int COLUMN_TRACKS           = 4;\r
+       public static final int COLUMN_SEQ_POSITION     = 5;\r
+       public static final int COLUMN_SEQ_LENGTH       = 6;\r
+       public static final int COLUMN_FILENAME = 7;\r
+       public static final int COLUMN_SEQ_NAME = 8;\r
+       static String column_titles[] = {\r
+               "No.",\r
+               "Modified",\r
+               "DivType",\r
+               "Resolution",\r
+               "Tracks",\r
+               "Position",\r
+               "Length",\r
+               "Filename",\r
+               "Sequence name",\r
+       };\r
+       static int column_width_ratios[] = {\r
+               2, 6, 6, 6, 6, 6, 6, 16, 40,\r
+       };\r
+\r
+       private ArrayList<MidiSequenceModel>\r
+       sequences = new ArrayList<MidiSequenceModel>();\r
+\r
+       MidiDeviceManager device_manager;\r
+       int second_position = 0;\r
+\r
+       public SequenceListModel( MidiDeviceManager device_manager ) {\r
+               (this.device_manager = device_manager).timeRangeModel.addChangeListener(\r
+                               new ChangeListener() {\r
+                                       public void stateChanged(ChangeEvent e) {\r
+                                               int sec_pos = SequenceListModel.this.device_manager.timeRangeModel.getValue() / 1000;\r
+                                               if( second_position == sec_pos ) return;\r
+                                               second_position = sec_pos;\r
+                                               fireTableCellUpdated( getLoadedIndex(), COLUMN_SEQ_POSITION );\r
+                                       }\r
+                               }\r
+                               );\r
+       }\r
+\r
+       // TableModel\r
+       //\r
+       public int getRowCount() { return sequences.size(); }\r
+       public int getColumnCount() { return column_titles.length; }\r
+       public String getColumnName(int column) {\r
+               return column_titles[column];\r
+       }\r
+       public Class<?> getColumnClass(int column) {\r
+               switch(column) {\r
+               case COLUMN_MODIFIED: return Boolean.class;\r
+               case COLUMN_SEQ_NUMBER:\r
+               case COLUMN_RESOLUTION:\r
+               case COLUMN_TRACKS: return Integer.class;\r
+               default: return String.class;\r
+               }\r
+       }\r
+       public Object getValueAt(int row, int column) {\r
+               switch(column) {\r
+               case COLUMN_SEQ_NUMBER: return row;\r
+               case COLUMN_MODIFIED: return sequences.get(row).isModified();\r
+               case COLUMN_DIVISION_TYPE: {\r
+                       float div_type = sequences.get(row).getSequence().getDivisionType();\r
+                       if( div_type == Sequence.PPQ ) return "PPQ";\r
+                       else if( div_type == Sequence.SMPTE_24 ) return "SMPTE_24";\r
+                       else if( div_type == Sequence.SMPTE_25 ) return "SMPTE_25";\r
+                       else if( div_type == Sequence.SMPTE_30 ) return "SMPTE_30";\r
+                       else if( div_type == Sequence.SMPTE_30DROP ) return "SMPTE_30DROP";\r
+                       else return "[Unknown]";\r
+               }\r
+               case COLUMN_RESOLUTION: return sequences.get(row).getSequence().getResolution();\r
+               case COLUMN_TRACKS: return sequences.get(row).getSequence().getTracks().length;\r
+               case COLUMN_SEQ_POSITION: {\r
+                       Sequence loaded_seq = device_manager.getSequencer().getSequence();\r
+                       if( loaded_seq != null && loaded_seq == sequences.get(row).getSequence() )\r
+                               return String.format( "%02d:%02d", second_position/60, second_position%60 );\r
+                       else\r
+                               return "";\r
+               }\r
+               case COLUMN_SEQ_LENGTH: {\r
+                       long usec = sequences.get(row).getSequence().getMicrosecondLength();\r
+                       int sec = (int)( (usec < 0 ? usec += 0x100000000L : usec) / 1000L / 1000L );\r
+                       return String.format( "%02d:%02d", sec/60, sec%60 );\r
+               }\r
+               case COLUMN_FILENAME: {\r
+                       String filename = sequences.get(row).getFilename();\r
+                       return filename == null ? "" : filename;\r
+               }\r
+               case COLUMN_SEQ_NAME: {\r
+                       String seq_name = sequences.get(row).toString();\r
+                       return seq_name == null ? "" : seq_name;\r
+               }\r
+               default: return "";\r
+               }\r
+       }\r
+       public boolean isCellEditable( int row, int column ) {\r
+               return column == COLUMN_FILENAME || column == COLUMN_SEQ_NAME ;\r
+       }\r
+       public void setValueAt(Object val, int row, int column) {\r
+               switch(column) {\r
+               case COLUMN_FILENAME:\r
+                       // ファイル名の変更\r
+                       String filename = (String)val;\r
+                       sequences.get(row).setFilename(filename);\r
+                       fireTableCellUpdated(row, COLUMN_FILENAME);\r
+                       break;\r
+               case COLUMN_SEQ_NAME:\r
+                       // シーケンス名の設定または変更\r
+                       if( sequences.get(row).setName((String)val) )\r
+                               fireTableCellUpdated(row, COLUMN_MODIFIED);\r
+                       break;\r
+               }\r
+       }\r
+       // Methods\r
+       //\r
+       public void sizeColumnWidthToFit( JTable table_view ) {\r
+               TableColumnModel column_model = table_view.getColumnModel();\r
+               int total_width = column_model.getTotalColumnWidth();\r
+               int i, total_width_ratio;\r
+               for( i=0, total_width_ratio = 0; i<column_width_ratios.length; i++ ) {\r
+                       total_width_ratio += column_width_ratios[i];\r
+               }\r
+               for( i=0; i<column_width_ratios.length; i++ ) {\r
+                       column_model.getColumn(i).setPreferredWidth(\r
+                                       total_width * column_width_ratios[i] / total_width_ratio\r
+                                       );\r
+               }\r
+       }\r
+       public boolean isModified() {\r
+               for( MidiSequenceModel seq_model : sequences ) {\r
+                       if( seq_model.isModified() ) return true;\r
+               }\r
+               return false;\r
+       }\r
+       public void setModified( ListSelectionModel sel_model, boolean is_modified ) {\r
+               int min_index = sel_model.getMinSelectionIndex();\r
+               int max_index = sel_model.getMaxSelectionIndex();\r
+               for( int i = min_index; i <= max_index; i++ ) {\r
+                       if( sel_model.isSelectedIndex(i) ) {\r
+                               sequences.get(i).setModified(is_modified);\r
+                               fireTableCellUpdated(i, COLUMN_MODIFIED);\r
+                       }\r
+               }\r
+       }\r
+       public MidiSequenceModel getSequenceModel(ListSelectionModel sel_model) {\r
+               if( sel_model.isSelectionEmpty() ) return null;\r
+               int sel_index = sel_model.getMinSelectionIndex();\r
+               if( sel_index >= sequences.size() ) return null;\r
+               return sequences.get(sel_index);\r
+       }\r
+       public void fireSequenceChanged( ListSelectionModel sel_model ) {\r
+               if( sel_model.isSelectionEmpty() ) return;\r
+               fireSequenceChanged(\r
+                               sel_model.getMinSelectionIndex(),\r
+                               sel_model.getMaxSelectionIndex()\r
+                               );\r
+       }\r
+       public void fireSequenceChanged( MidiSequenceModel seq_model ) {\r
+               for( int index=0; index<sequences.size(); index++ )\r
+                       if( sequences.get(index) == seq_model )\r
+                               fireSequenceChanged(index,index);\r
+       }\r
+       public void fireSequenceChanged( int min_index, int max_index ) {\r
+               for( int index = min_index; index <= max_index; index++ ) {\r
+                       MidiSequenceModel seq_model = sequences.get(index);\r
+                       seq_model.setModified(true);\r
+                       if( device_manager.getSequencer().getSequence() == seq_model.getSequence() ) {\r
+                               // シーケンサーに対して、同じシーケンスを再度セットする。\r
+                               // (これをやらないと更新が反映されないため)\r
+                               device_manager.timeRangeModel.setSequenceModel(seq_model);\r
+                       }\r
+               }\r
+               fireTableRowsUpdated( min_index, max_index );\r
+       }\r
+       public int addSequence() {\r
+               Sequence seq = (new Music.ChordProgression()).toMidiSequence();\r
+               return seq == null ? -1 : addSequence(seq,null);\r
+       }\r
+       public int addSequence( Sequence seq ) {\r
+               return addSequence( seq, "" );\r
+       }\r
+       public int addSequence( Sequence seq, String filename ) {\r
+               MidiSequenceModel seq_model = new MidiSequenceModel(this);\r
+               seq_model.setSequence(seq);\r
+               seq_model.setFilename(filename);\r
+               sequences.add(seq_model);\r
+               int last_index = sequences.size() - 1;\r
+               fireTableRowsInserted( last_index, last_index );\r
+               return last_index;\r
+       }\r
+       public int addSequence( byte[] midiData, String filename )\r
+                       throws InvalidMidiDataException\r
+                       {\r
+               return ( midiData == null ) ?\r
+                               addSequence() :\r
+                                       addSequence( new ByteArrayInputStream(midiData), filename ) ;\r
+                       }\r
+       public int addSequence( File midi_file )\r
+                       throws InvalidMidiDataException, FileNotFoundException\r
+                       {\r
+               FileInputStream fis = new FileInputStream(midi_file);\r
+               int retval = addSequence( fis, midi_file.getName() );\r
+               try {\r
+                       fis.close();\r
+               } catch( IOException ex ) {\r
+                       ex.printStackTrace();\r
+               }\r
+               return retval;\r
+                       }\r
+       public int addSequence( InputStream in, String filename )\r
+                       throws InvalidMidiDataException\r
+                       {\r
+               if( in == null ) return addSequence();\r
+               Sequence seq;\r
+               try {\r
+                       seq = MidiSystem.getSequence(in);\r
+               } catch ( InvalidMidiDataException e ) {\r
+                       throw e;\r
+               } catch ( EOFException e ) {\r
+                       // No MIDI data\r
+                       return -1;\r
+               } catch ( IOException e ) {\r
+                       e.printStackTrace();\r
+                       return -1;\r
+               }\r
+               return addSequence( seq, filename );\r
+                       }\r
+       public int addSequence( String midi_file_url )\r
+                       throws InvalidMidiDataException, AccessControlException\r
+                       {\r
+               URL url = toURL( midi_file_url );\r
+               if( url == null ) {\r
+                       return -1;\r
+               }\r
+               Sequence seq;\r
+               try {\r
+                       seq = MidiSystem.getSequence(url);\r
+               } catch ( InvalidMidiDataException e ) {\r
+                       throw e;\r
+               } catch( EOFException e ) {\r
+                       // No MIDI data\r
+                       return -1;\r
+               } catch( IOException e ) {\r
+                       e.printStackTrace();\r
+                       return -1;\r
+               } catch( AccessControlException e ) {\r
+                       throw e;\r
+               }\r
+               return addSequence( seq, url.getFile().replaceFirst("^.*/","") );\r
+                       }\r
+       public void removeSequence( ListSelectionModel sel_model ) {\r
+               if( sel_model.isSelectionEmpty() ) return;\r
+               int sel_index = sel_model.getMinSelectionIndex();\r
+               if( sequences.get(sel_index) == device_manager.timeRangeModel.getSequenceModel() )\r
+                       device_manager.timeRangeModel.setSequenceModel(null);\r
+               sequences.remove(sel_index);\r
+               fireTableRowsDeleted( sel_index, sel_index );\r
+       }\r
+       public void loadToSequencer( int index ) {\r
+               int loaded_index = getLoadedIndex();\r
+               if( loaded_index == index ) return;\r
+               MidiSequenceModel seq_model = sequences.get(index);\r
+               device_manager.timeRangeModel.setSequenceModel(seq_model);\r
+               seq_model.fireTableDataChanged();\r
+               fireTableCellUpdated( loaded_index, COLUMN_SEQ_POSITION );\r
+               fireTableCellUpdated( index, COLUMN_SEQ_POSITION );\r
+       }\r
+       public int getLoadedIndex() {\r
+               MidiSequenceModel seq_model = device_manager.timeRangeModel.getSequenceModel();\r
+               for( int i=0; i<sequences.size(); i++ )\r
+                       if( sequences.get(i) == seq_model ) return i;\r
+               return -1;\r
+       }\r
+       public boolean loadNext( int offset ) {\r
+               int loaded_index = getLoadedIndex();\r
+               int index = (loaded_index < 0 ? 0 : loaded_index + offset);\r
+               if( index < 0 || index >= sequences.size() ) return false;\r
+               loadToSequencer( index );\r
+               return true;\r
+       }\r
+       public int getTotalSeconds() {\r
+               int total_sec = 0;\r
+               long usec;\r
+               for( MidiSequenceModel seq_model : sequences ) {\r
+                       usec = seq_model.getSequence().getMicrosecondLength();\r
+                       total_sec += (int)( (usec < 0 ? usec += 0x100000000L : usec) / 1000L / 1000L );\r
+               }\r
+               return total_sec;\r
+       }\r
+       public String getTotalLength() {\r
+               int sec = getTotalSeconds();\r
+               return String.format( "MIDI file playlist - Total length = %02d:%02d", sec/60, sec%60 );\r
+       }\r
+       //\r
+       // 文字列を URL オブジェクトに変換\r
+       //\r
+       public URL toURL( String url_string ) {\r
+               if( url_string == null || url_string.isEmpty() ) {\r
+                       return null;\r
+               }\r
+               URI uri = null;\r
+               URL url = null;\r
+               try {\r
+                       uri = new URI(url_string);\r
+                       url = uri.toURL();\r
+               } catch( URISyntaxException e ) {\r
+                       e.printStackTrace();\r
+               } catch( MalformedURLException e ) {\r
+                       e.printStackTrace();\r
+               }\r
+               return url;\r
+       }\r
+}\r
+\r
+//////////////////////////////////////////////////////////\r
+//\r
+// Track List (MIDI Sequence) Model\r
+//\r
+//////////////////////////////////////////////////////////\r
+class MidiSequenceModel extends AbstractTableModel\r
+{\r
+       public static final int COLUMN_TRACK_NUMBER     = 0;\r
+       public static final int COLUMN_EVENTS           = 1;\r
+       public static final int COLUMN_MUTE             = 2;\r
+       public static final int COLUMN_SOLO             = 3;\r
+       public static final int COLUMN_RECORD_CHANNEL   = 4;\r
+       public static final int COLUMN_CHANNEL  = 5;\r
+       public static final int COLUMN_TRACK_NAME       = 6;\r
+       public static final String column_titles[] = {\r
+               "No.", "Events", "Mute", "Solo", "RecCh", "Ch", "Track name"\r
+       };\r
+       public static final int column_width_ratios[] = {\r
+               30, 60, 40, 40, 60, 40, 200\r
+       };\r
+       private SequenceListModel seq_list_model;\r
+       private Sequence seq;\r
+       private SequenceIndex seq_index;\r
+       private String filename = "";\r
+       private boolean is_modified = false;\r
+       private ArrayList<MidiTrackModel> track_models\r
+       = new ArrayList<MidiTrackModel>();\r
+\r
+       class RecordChannelCellEditor extends DefaultCellEditor {\r
+               public RecordChannelCellEditor() {\r
+                       super(new JComboBox<String>() {\r
+                               {\r
+                                       addItem("OFF");\r
+                                       for( int i=1; i <= MIDISpec.MAX_CHANNELS; i++ )\r
+                                               addItem( String.format( "%d", i ) );\r
+                                       addItem("ALL");\r
+                               }\r
+                       });\r
+               }\r
+       }\r
+\r
+       public MidiSequenceModel( SequenceListModel slm ) {\r
+               seq_list_model = slm;\r
+       }\r
+       //\r
+       // TableModel interface\r
+       //\r
+       public int getRowCount() {\r
+               return seq == null ? 0 : seq.getTracks().length;\r
+       }\r
+       public int getColumnCount() {\r
+               return column_titles.length;\r
+       }\r
+       public String getColumnName(int column) {\r
+               return column_titles[column];\r
+       }\r
+       public Class<?> getColumnClass(int column) {\r
+               switch(column) {\r
+               case COLUMN_TRACK_NUMBER:\r
+               case COLUMN_EVENTS:\r
+                       return Integer.class;\r
+               case COLUMN_MUTE:\r
+               case COLUMN_SOLO:\r
+                       return\r
+                                       (seq == getSequencer().getSequence()) ?\r
+                                                       Boolean.class : String.class;\r
+               case COLUMN_RECORD_CHANNEL:\r
+               case COLUMN_CHANNEL:\r
+               case COLUMN_TRACK_NAME:\r
+                       return String.class;\r
+               default: return super.getColumnClass(column);\r
+               }\r
+       }\r
+       public Object getValueAt(int row, int column) {\r
+               switch(column) {\r
+               case COLUMN_TRACK_NUMBER: return row;\r
+               case COLUMN_EVENTS:\r
+                       return seq.getTracks()[row].size();\r
+               case COLUMN_MUTE:\r
+                       return (seq == getSequencer().getSequence()) ?\r
+                                       getSequencer().getTrackMute(row) : "";\r
+               case COLUMN_SOLO:\r
+                       return (seq == getSequencer().getSequence()) ?\r
+                                       getSequencer().getTrackSolo(row) : "";\r
+               case COLUMN_RECORD_CHANNEL:\r
+                       return (seq == getSequencer().getSequence()) ?\r
+                                       track_models.get(row).getRecordingChannel() : "";\r
+               case COLUMN_CHANNEL: {\r
+                       int ch = track_models.get(row).getChannel();\r
+                       return ch < 0 ? "" : ch + 1 ;\r
+               }\r
+               case COLUMN_TRACK_NAME:\r
+                       return track_models.get(row).toString();\r
+               default: return "";\r
+               }\r
+       }\r
+       public boolean isCellEditable( int row, int column ) {\r
+               switch(column) {\r
+               case COLUMN_MUTE:\r
+               case COLUMN_SOLO:\r
+               case COLUMN_RECORD_CHANNEL:\r
+                       return seq == getSequencer().getSequence();\r
+               case COLUMN_CHANNEL:\r
+               case COLUMN_TRACK_NAME:\r
+                       return true;\r
+               default:\r
+                       return false;\r
+               }\r
+       }\r
+       public void setValueAt(Object val, int row, int column) {\r
+               switch(column) {\r
+               case COLUMN_MUTE:\r
+                       getSequencer().setTrackMute( row, ((Boolean)val).booleanValue() );\r
+                       break;\r
+               case COLUMN_SOLO:\r
+                       getSequencer().setTrackSolo( row, ((Boolean)val).booleanValue() );\r
+                       break;\r
+               case COLUMN_RECORD_CHANNEL:\r
+                       track_models.get(row).setRecordingChannel((String)val);\r
+                       break;\r
+               case COLUMN_CHANNEL: {\r
+                       Integer ch;\r
+                       try {\r
+                               ch = new Integer((String)val);\r
+                       }\r
+                       catch( NumberFormatException e ) {\r
+                               ch = -1;\r
+                               break;\r
+                       }\r
+                       if( --ch <= 0 || ch > MIDISpec.MAX_CHANNELS )\r
+                               break;\r
+                       MidiTrackModel track_model = track_models.get(row);\r
+                       int old_ch = track_model.getChannel();\r
+                       if( ch == old_ch ) break;\r
+                       track_model.setChannel(ch);\r
+                       setModified(true);\r
+                       fireTableCellUpdated(row,COLUMN_EVENTS);\r
+                       break;\r
+               }\r
+               case COLUMN_TRACK_NAME:\r
+                       track_models.get(row).setString((String)val);\r
+                       break;\r
+               }\r
+               fireTableCellUpdated(row,column);\r
+       }\r
+       // Methods (Table view)\r
+       //\r
+       public void sizeColumnWidthToFit( TableColumnModel column_model ) {\r
+               int total_width = column_model.getTotalColumnWidth();\r
+               int i, total_width_ratio = 0;\r
+               for( i=0; i<column_width_ratios.length; i++ ) {\r
+                       total_width_ratio += column_width_ratios[i];\r
+               }\r
+               for( i=0; i<column_width_ratios.length; i++ ) {\r
+                       column_model.getColumn(i).setPreferredWidth(\r
+                                       total_width * column_width_ratios[i] / total_width_ratio\r
+                                       );\r
+               }\r
+       }\r
+       // Methods (sequence)\r
+       //\r
+       public Sequence getSequence() { return this.seq; }\r
+       public void setSequence( Sequence seq ) {\r
+               //\r
+               getSequencer().recordDisable(null); // The "null" means all tracks\r
+               //\r
+               this.seq = seq;\r
+               int old_size = track_models.size();\r
+               if( old_size > 0 ) {\r
+                       track_models.clear();\r
+                       fireTableRowsDeleted(0, old_size-1);\r
+               }\r
+               if( seq == null ) {\r
+                       seq_index = null;\r
+               }\r
+               else {\r
+                       seq_index = new SequenceIndex( seq );\r
+                       Track tklist[] = seq.getTracks();\r
+                       for( Track tk : tklist )\r
+                               track_models.add( new MidiTrackModel( tk, this ) );\r
+                       fireTableRowsInserted(0, tklist.length-1);\r
+               }\r
+       }\r
+       public SequenceIndex getSequenceIndex() {\r
+               return this.seq_index;\r
+       }\r
+       public void setModified(boolean is_modified) {\r
+               this.is_modified = is_modified;\r
+       }\r
+       public boolean isModified() { return is_modified; }\r
+       public void setFilename(String filename) {\r
+               this.filename = filename;\r
+       }\r
+       public String getFilename() { return filename; }\r
+       public String toString() {\r
+               return MIDISpec.getNameOf(seq);\r
+       }\r
+       public boolean setName( String name ) {\r
+               if( name.equals(toString()) )\r
+                       return false;\r
+               if( ! MIDISpec.setNameOf(seq,name) )\r
+                       return false;\r
+               setModified(true);\r
+               fireTableDataChanged();\r
+               return true;\r
+       }\r
+       public byte[] getMIDIdata() {\r
+               if( seq == null || seq.getTracks().length == 0 ) {\r
+                       return null;\r
+               }\r
+               /*\r
+    int[] file_types = MidiSystem.getMidiFileTypes(seq);\r
+    for( int i : file_types )\r
+      System.out.println( "Supported MIDI file type : " + i );\r
+                */\r
+               ByteArrayOutputStream out = new ByteArrayOutputStream();\r
+               try {\r
+                       MidiSystem.write(seq, 1, out);\r
+                       return out.toByteArray();\r
+               } catch ( IOException e ) {\r
+                       e.printStackTrace();\r
+                       return null;\r
+               }\r
+       }\r
+       public void fireTimeSignatureChanged() {\r
+               seq_index = new SequenceIndex( seq );\r
+       }\r
+       public void fireTrackChanged( Track tk ) {\r
+               int row = getTrackRow(tk);\r
+               if( row < 0 ) return;\r
+               fireTableRowsUpdated( row, row );\r
+               fireSequenceChanged();\r
+       }\r
+       public void fireSequenceChanged() {\r
+               seq_list_model.fireSequenceChanged(this);\r
+       }\r
+       public MidiTrackModel getTrackModel( int index ) {\r
+               Track tracks[] = seq.getTracks();\r
+               if( tracks.length == 0 ) return null;\r
+               Track tk = tracks[index];\r
+               for( MidiTrackModel model : track_models )\r
+                       if( model.getTrack() == tk )\r
+                               return model;\r
+               return null;\r
+       }\r
+       public int getTrackRow( Track tk ) {\r
+               Track tracks[] = seq.getTracks();\r
+               for( int i=0; i<tracks.length; i++ )\r
+                       if( tracks[i] == tk )\r
+                               return i;\r
+               return -1;\r
+       }\r
+       public void createTrack() {\r
+               Track tk = seq.createTrack();\r
+               track_models.add( new MidiTrackModel( tk, this ) );\r
+               int last_row = seq.getTracks().length - 1;\r
+               fireTableRowsInserted( last_row, last_row );\r
+       }\r
+       public void deleteTracks( ListSelectionModel selection_model ) {\r
+               if( selection_model.isSelectionEmpty() )\r
+                       return;\r
+               int min_sel_index = selection_model.getMinSelectionIndex();\r
+               int max_sel_index = selection_model.getMaxSelectionIndex();\r
+               Track tklist[] = seq.getTracks();\r
+               for( int i = max_sel_index; i >= min_sel_index; i-- ) {\r
+                       if( ! selection_model.isSelectedIndex(i) )\r
+                               continue;\r
+                       seq.deleteTrack( tklist[i] );\r
+                       track_models.remove(i);\r
+               }\r
+               fireTableRowsDeleted( min_sel_index, max_sel_index );\r
+       }\r
+       //\r
+       // Methods (sequencer)\r
+       //\r
+       public Sequencer getSequencer() {\r
+               return seq_list_model.device_manager.getSequencer();\r
+       }\r
+       public boolean isRecordable() {\r
+               if( seq != getSequencer().getSequence() ) return false;\r
+               int num_row = getRowCount();\r
+               String s;\r
+               for( int row=0; row<num_row; row++ ) {\r
+                       s = (String)getValueAt(\r
+                                       row, COLUMN_RECORD_CHANNEL\r
+                                       );\r
+                       if( s.equals("OFF") ) continue;\r
+                       return true;\r
+               }\r
+               return false;\r
+       }\r
+}\r
+\r
+////////////////////////////////////////////////////////\r
+//\r
+// Event List (Track) Model\r
+//\r
+////////////////////////////////////////////////////////\r
+class MidiTrackModel extends AbstractTableModel\r
+{\r
+       public static final int COLUMN_EVENT_NUMBER     = 0;\r
+       public static final int COLUMN_TICK_POSITION    = 1;\r
+       public static final int COLUMN_MEASURE_POSITION = 2;\r
+       public static final int COLUMN_BEAT_POSITION            = 3;\r
+       public static final int COLUMN_EXTRA_TICK_POSITION      = 4;\r
+       public static final int COLUMN_MESSAGE  = 5;\r
+       public static final String column_titles[] = {\r
+               "No.", "TickPos.", "Measure", "Beat", "ExTick", "MIDI Message",\r
+       };\r
+       public static final int column_width_ratios[] = {\r
+               30, 40, 20,20,20, 280,\r
+       };\r
+       private Track track;\r
+       private MidiSequenceModel parent_model;\r
+       //\r
+       // Constructor\r
+       //\r
+       public MidiTrackModel() { } // To create empty model\r
+       public MidiTrackModel( MidiSequenceModel parent_model ) {\r
+               this.parent_model = parent_model;\r
+       }\r
+       public MidiTrackModel( Track tk, MidiSequenceModel parent_model ) {\r
+               this.track = tk;\r
+               this.parent_model = parent_model;\r
+       }\r
+       // TableModel interface\r
+       //\r
+       public int getRowCount() {\r
+               return track == null ? 0 : track.size();\r
+       }\r
+       public int getColumnCount() {\r
+               return column_titles.length;\r
+       }\r
+       public String getColumnName(int column) {\r
+               return column_titles[column];\r
+       }\r
+       public Class<?> getColumnClass(int column) {\r
+               switch(column) {\r
+               case COLUMN_EVENT_NUMBER:\r
+                       return Integer.class;\r
+               case COLUMN_TICK_POSITION:\r
+                       return Long.class;\r
+               case COLUMN_MEASURE_POSITION:\r
+               case COLUMN_BEAT_POSITION:\r
+               case COLUMN_EXTRA_TICK_POSITION:\r
+                       return Integer.class;\r
+                       // case COLUMN_MESSAGE:\r
+                       default:\r
+                               return String.class;\r
+               }\r
+               // return getValueAt(0,column).getClass();\r
+       }\r
+       public Object getValueAt(int row, int column) {\r
+               switch(column) {\r
+               case COLUMN_EVENT_NUMBER:\r
+                       return row;\r
+\r
+               case COLUMN_TICK_POSITION:\r
+                       return track.get(row).getTick();\r
+\r
+               case COLUMN_MEASURE_POSITION:\r
+                       return parent_model.getSequenceIndex().tickToMeasure(\r
+                                       track.get(row).getTick()\r
+                                       ) + 1;\r
+\r
+               case COLUMN_BEAT_POSITION:\r
+                       parent_model.getSequenceIndex().tickToMeasure(\r
+                                       track.get(row).getTick()\r
+                                       );\r
+                       return parent_model.getSequenceIndex().last_beat + 1;\r
+\r
+               case COLUMN_EXTRA_TICK_POSITION:\r
+                       parent_model.getSequenceIndex().tickToMeasure(\r
+                                       track.get(row).getTick()\r
+                                       );\r
+                       return parent_model.getSequenceIndex().last_extra_tick;\r
+\r
+               case COLUMN_MESSAGE:\r
+                       return msgToString(\r
+                                       track.get(row).getMessage()\r
+                                       );\r
+               default: return "";\r
+               }\r
+       }\r
+       public boolean isCellEditable(int row, int column) {\r
+               switch(column) {\r
+               // case COLUMN_EVENT_NUMBER:\r
+               case COLUMN_TICK_POSITION:\r
+               case COLUMN_MEASURE_POSITION:\r
+               case COLUMN_BEAT_POSITION:\r
+               case COLUMN_EXTRA_TICK_POSITION:\r
+               case COLUMN_MESSAGE:\r
+                       return true;\r
+               default: return false;\r
+               }\r
+       }\r
+       public void setValueAt(Object value, int row, int column) {\r
+               long tick;\r
+               switch(column) {\r
+               // case COLUMN_EVENT_NUMBER:\r
+               case COLUMN_TICK_POSITION:\r
+                       tick = (Long)value;\r
+                       break;\r
+               case COLUMN_MEASURE_POSITION:\r
+                       tick = parent_model.getSequenceIndex().measureToTick(\r
+                                       (Integer)value - 1,\r
+                                       (Integer)getValueAt( row, COLUMN_BEAT_POSITION ) - 1,\r
+                                       (Integer)getValueAt( row, COLUMN_EXTRA_TICK_POSITION )\r
+                                       );\r
+                       break;\r
+               case COLUMN_BEAT_POSITION:\r
+                       tick = parent_model.getSequenceIndex().measureToTick(\r
+                                       (Integer)getValueAt( row, COLUMN_MEASURE_POSITION ) - 1,\r
+                                       (Integer)value - 1,\r
+                                       (Integer)getValueAt( row, COLUMN_EXTRA_TICK_POSITION )\r
+                                       );\r
+                       break;\r
+               case COLUMN_EXTRA_TICK_POSITION:\r
+                       tick = parent_model.getSequenceIndex().measureToTick(\r
+                                       (Integer)getValueAt( row, COLUMN_MEASURE_POSITION ) - 1,\r
+                                       (Integer)getValueAt( row, COLUMN_BEAT_POSITION ) - 1,\r
+                                       (Integer)value\r
+                                       );\r
+                       break;\r
+               case COLUMN_MESSAGE:\r
+                       return;\r
+               default: return;\r
+               }\r
+               changeEventTick(row,tick);\r
+       }\r
+\r
+       // Methods (Table view)\r
+       //\r
+       public void sizeColumnWidthToFit( TableColumnModel column_model ) {\r
+               int total_width = column_model.getTotalColumnWidth();\r
+               int i, total_width_ratio = 0;\r
+               for( i=0; i<column_width_ratios.length; i++ ) {\r
+                       total_width_ratio += column_width_ratios[i];\r
+               }\r
+               for( i=0; i<column_width_ratios.length; i++ ) {\r
+                       column_model.getColumn(i).setPreferredWidth(\r
+                                       total_width * column_width_ratios[i] / total_width_ratio\r
+                                       );\r
+               }\r
+       }\r
+       // Methods\r
+       //\r
+       private boolean isRhythmPart(int ch) {\r
+               return (ch == 9);\r
+       }\r
+       // トラックオブジェクトの取得\r
+       public Track getTrack() { return track; }\r
+       //\r
+       // 文字列としてトラック名を返す\r
+       public String toString() {\r
+               return MIDISpec.getNameOf(track);\r
+       }\r
+       // トラック名をセットする\r
+       public boolean setString( String name ) {\r
+               if(\r
+                               name.equals(toString())\r
+                               ||\r
+                               ! MIDISpec.setNameOf( track, name )\r
+                               )\r
+                       return false;\r
+               parent_model.setModified(true);\r
+               parent_model.fireSequenceChanged();\r
+               fireTableDataChanged();\r
+               return true;\r
+       }\r
+       //\r
+       // 録音中の MIDI チャンネル\r
+       private String rec_ch = "OFF";\r
+       public String getRecordingChannel() {\r
+               return rec_ch;\r
+       }\r
+       public void setRecordingChannel(String ch_str) {\r
+               Sequencer sequencer = parent_model.getSequencer();\r
+               if( ch_str.equals("OFF") ) {\r
+                       sequencer.recordDisable( track );\r
+               }\r
+               else if( ch_str.equals("ALL") ) {\r
+                       sequencer.recordEnable( track, -1 );\r
+               }\r
+               else {\r
+                       try {\r
+                               int ch = Integer.decode(ch_str).intValue() - 1;\r
+                               sequencer.recordEnable( track, ch );\r
+                       } catch( NumberFormatException nfe ) {\r
+                               sequencer.recordDisable( track );\r
+                               rec_ch = "OFF";\r
+                               return;\r
+                       }\r
+               }\r
+               rec_ch = ch_str;\r
+       }\r
+       //\r
+       // 対象MIDIチャンネル\r
+       public int getChannel() {\r
+               MidiMessage msg;\r
+               ShortMessage smsg;\r
+               int index, ch, prev_ch = -1, track_size = track.size();\r
+               for( index=0; index < track_size; index++ ) {\r
+                       msg = track.get(index).getMessage();\r
+                       if( ! (msg instanceof ShortMessage) )\r
+                               continue;\r
+                       smsg = (ShortMessage)msg;\r
+                       if( ! MIDISpec.isChannelMessage(smsg) )\r
+                               continue;\r
+                       ch = smsg.getChannel();\r
+                       if( prev_ch >= 0 && prev_ch != ch ) {\r
+                               // MIDIチャンネルが統一されていない場合\r
+                               return -1;\r
+                       }\r
+                       prev_ch = ch;\r
+               }\r
+               // すべてのMIDIチャンネルが同じならそれを返す\r
+               return prev_ch;\r
+       }\r
+       public void setChannel(int ch) {\r
+               // すべてのチャンネルメッセージに対して\r
+               // 同一のMIDIチャンネルをセットする\r
+               MidiMessage msg;\r
+               ShortMessage smsg;\r
+               int index, track_size = track.size();\r
+               for( index=0; index < track_size; index++ ) {\r
+                       msg = track.get(index).getMessage();\r
+                       if(\r
+                                       ! (msg instanceof ShortMessage)\r
+                                       ||\r
+                                       ! MIDISpec.isChannelMessage(smsg = (ShortMessage)msg)\r
+                                       ||\r
+                                       smsg.getChannel() == ch\r
+                                       )\r
+                               continue;\r
+                       try {\r
+                               smsg.setMessage(\r
+                                               smsg.getCommand(), ch,\r
+                                               smsg.getData1(), smsg.getData2()\r
+                                               );\r
+                       }\r
+                       catch( InvalidMidiDataException e ) {\r
+                               e.printStackTrace();\r
+                       }\r
+                       parent_model.setModified(true);\r
+               }\r
+               parent_model.fireTrackChanged( track );\r
+               parent_model.fireSequenceChanged();\r
+               fireTableDataChanged();\r
+       }\r
+       //\r
+       // MIDI イベントの tick 位置変更\r
+       public void changeEventTick(int row, long new_tick) {\r
+               MidiEvent old_midi_event = track.get(row);\r
+               if( old_midi_event.getTick() == new_tick ) {\r
+                       return;\r
+               }\r
+               MidiMessage msg = old_midi_event.getMessage();\r
+               MidiEvent new_midi_event = new MidiEvent(msg,new_tick);\r
+               track.remove(old_midi_event);\r
+               track.add(new_midi_event);\r
+               fireTableDataChanged();\r
+               //\r
+               if( MIDISpec.isEOT(msg) ) {\r
+                       // EOTの場所が変わると曲の長さが変わるので、親モデルへ通知する。\r
+                       parent_model.fireSequenceChanged();\r
+               }\r
+       }\r
+       //\r
+       // MIDI tick から位置を取得(バイナリーサーチ)\r
+       public int tickToIndex( long tick ) {\r
+               if( track == null ) return 0;\r
+               int min_index = 0;\r
+               int max_index = track.size() - 1;\r
+               long current_tick;\r
+               int current_index;\r
+               while( min_index < max_index ) {\r
+                       current_index = (min_index + max_index) / 2 ;\r
+                       current_tick = track.get(current_index).getTick();\r
+                       if( tick > current_tick ) {\r
+                               min_index = current_index + 1;\r
+                       }\r
+                       else if( tick < current_tick ) {\r
+                               max_index = current_index - 1;\r
+                       }\r
+                       else {\r
+                               return current_index;\r
+                       }\r
+               }\r
+               return (min_index + max_index) / 2;\r
+       }\r
+       // NoteOn/NoteOff ペアの一方のインデックスから、\r
+       // その相手を返す\r
+       public int getIndexOfPartnerFor( int index ) {\r
+               if( track == null || index >= track.size() ) return -1;\r
+               MidiMessage msg = track.get(index).getMessage();\r
+               if( ! (msg instanceof ShortMessage) ) return -1;\r
+               ShortMessage sm = (ShortMessage)msg;\r
+               int cmd = sm.getCommand();\r
+               int i;\r
+               int ch = sm.getChannel();\r
+               int note = sm.getData1();\r
+               MidiMessage partner_msg;\r
+               ShortMessage partner_sm;\r
+               int partner_cmd;\r
+\r
+               switch( cmd ) {\r
+               case 0x90: // NoteOn\r
+               if( sm.getData2() > 0 ) {\r
+                       // Search NoteOff event forward\r
+                       for( i = index + 1; i < track.size(); i++ ) {\r
+                               partner_msg = track.get(i).getMessage();\r
+                               if( ! (partner_msg instanceof ShortMessage ) ) continue;\r
+                               partner_sm = (ShortMessage)partner_msg;\r
+                               partner_cmd = partner_sm.getCommand();\r
+                               if( partner_cmd != 0x80 && partner_cmd != 0x90 ||\r
+                                               partner_cmd == 0x90 && partner_sm.getData2() > 0\r
+                                               ) {\r
+                                       // Not NoteOff\r
+                                       continue;\r
+                               }\r
+                               if( ch != partner_sm.getChannel() || note != partner_sm.getData1() ) {\r
+                                       // Not my partner\r
+                                       continue;\r
+                               }\r
+                               return i;\r
+                       }\r
+                       break;\r
+               }\r
+               // When velocity is 0, it means Note Off, so no break.\r
+               case 0x80: // NoteOff\r
+                       // Search NoteOn event backward\r
+                       for( i = index - 1; i >= 0; i-- ) {\r
+                               partner_msg = track.get(i).getMessage();\r
+                               if( ! (partner_msg instanceof ShortMessage ) ) continue;\r
+                               partner_sm = (ShortMessage)partner_msg;\r
+                               partner_cmd = partner_sm.getCommand();\r
+                               if( partner_cmd != 0x90 || partner_sm.getData2() <= 0 ) {\r
+                                       // Not NoteOn\r
+                                       continue;\r
+                               }\r
+                               if( ch != partner_sm.getChannel() || note != partner_sm.getData1() ) {\r
+                                       // Not my partner\r
+                                       continue;\r
+                               }\r
+                               return i;\r
+                       }\r
+                       break;\r
+               }\r
+               // Not found\r
+               return -1;\r
+       }\r
+       //\r
+       public boolean isTimeSignature( MidiMessage msg ) {\r
+               // 拍子記号のとき True を返す\r
+               return\r
+                               (msg instanceof MetaMessage)\r
+                               &&\r
+                               ((MetaMessage)msg).getType() == 0x58;\r
+       }\r
+       public boolean isNote( int index ) { // Note On または Note Off のとき True を返す\r
+               MidiEvent midi_evt = getMidiEvent(index);\r
+               MidiMessage msg = midi_evt.getMessage();\r
+               if( ! (msg instanceof ShortMessage) ) return false;\r
+               int cmd = ((ShortMessage)msg).getCommand();\r
+               return cmd == ShortMessage.NOTE_ON || cmd == ShortMessage.NOTE_OFF ;\r
+       }\r
+       public boolean hasTrack() { return track != null; }\r
+       //\r
+       // イベントの取得\r
+       //\r
+       public MidiEvent getMidiEvent( int index ) {\r
+               return track.get(index);\r
+       }\r
+       public MidiEvent[] getMidiEvents( ListSelectionModel sel_model ) {\r
+               Vector<MidiEvent> events = new Vector<MidiEvent>();\r
+               if( ! sel_model.isSelectionEmpty() ) {\r
+                       int min_sel_index = sel_model.getMinSelectionIndex();\r
+                       int max_sel_index = sel_model.getMaxSelectionIndex();\r
+                       for( int i = min_sel_index; i <= max_sel_index; i++ )\r
+                               if( sel_model.isSelectedIndex(i) )\r
+                                       events.add(track.get(i));\r
+               }\r
+               return events.toArray(new MidiEvent[1]);\r
+       }\r
+       //\r
+       // イベントの追加\r
+       //\r
+       public boolean addMidiEvent( MidiEvent midi_event ) {\r
+               if( !(track.add(midi_event)) )\r
+                       return false;\r
+               if( isTimeSignature(midi_event.getMessage()) )\r
+                       parent_model.fireTimeSignatureChanged();\r
+               parent_model.fireTrackChanged( track );\r
+               int last_index = track.size() - 1;\r
+               fireTableRowsInserted( last_index-1, last_index-1 );\r
+               return true;\r
+       }\r
+       public boolean addMidiEvents(\r
+                       MidiEvent midi_events[],\r
+                       long destination_tick,\r
+                       int midi_events_ppq\r
+                       ) {\r
+               int dest_ppq = parent_model.getSequence().getResolution();\r
+               boolean done = false, has_time_signature = false;\r
+               long event_tick = 0;\r
+               long first_event_tick = -1;\r
+               MidiEvent new_midi_event;\r
+               MidiMessage msg;\r
+               for( MidiEvent midi_event : midi_events ) {\r
+                       event_tick = midi_event.getTick();\r
+                       msg = midi_event.getMessage();\r
+                       if( first_event_tick < 0 ) {\r
+                               first_event_tick = event_tick;\r
+                               new_midi_event = new MidiEvent(\r
+                                               msg, destination_tick\r
+                                               );\r
+                       }\r
+                       else {\r
+                               new_midi_event = new MidiEvent(\r
+                                               msg,\r
+                                               destination_tick + (event_tick - first_event_tick) * dest_ppq / midi_events_ppq\r
+                                               );\r
+                       }\r
+                       if( ! track.add(new_midi_event) ) continue;\r
+                       done = true;\r
+                       if( isTimeSignature(msg) ) has_time_signature = true;\r
+               }\r
+               if( done ) {\r
+                       if( has_time_signature )\r
+                               parent_model.fireTimeSignatureChanged();\r
+                       parent_model.fireTrackChanged( track );\r
+                       int last_index = track.size() - 1;\r
+                       int old_last_index = last_index - midi_events.length;\r
+                       fireTableRowsInserted( old_last_index, last_index );\r
+               }\r
+               return done;\r
+       }\r
+       //\r
+       // イベントの削除\r
+       //\r
+       public void removeMidiEvents( MidiEvent midi_events[] ) {\r
+               boolean had_time_signature = false;\r
+               for( MidiEvent midi_event : midi_events ) {\r
+                       if( isTimeSignature(midi_event.getMessage()) )\r
+                               had_time_signature = true;\r
+                       track.remove(midi_event);\r
+               }\r
+               if( had_time_signature )\r
+                       parent_model.fireTimeSignatureChanged();\r
+               parent_model.fireTrackChanged( track );\r
+               int last_index = track.size() - 1;\r
+               int old_last_index = last_index + midi_events.length;\r
+               if( last_index < 0 ) last_index = 0;\r
+               fireTableRowsDeleted( old_last_index, last_index );\r
+       }\r
+       public void removeMidiEvents( ListSelectionModel sel_model ) {\r
+               removeMidiEvents( getMidiEvents(sel_model) );\r
+       }\r
+       //\r
+       // イベントの表示\r
+       //\r
+       public String msgToString(MidiMessage msg) {\r
+               String str = "";\r
+               if( msg instanceof ShortMessage ) {\r
+                       ShortMessage shortmsg = (ShortMessage)msg;\r
+                       int status = msg.getStatus();\r
+                       String status_name = MIDISpec.getStatusName(status);\r
+                       int data1 = shortmsg.getData1();\r
+                       int data2 = shortmsg.getData2();\r
+                       if( MIDISpec.isChannelMessage(status) ) {\r
+                               int ch = shortmsg.getChannel();\r
+                               String ch_prefix = "Ch."+(ch+1) + ": ";\r
+                               String status_prefix = (\r
+                                               status_name == null ? String.format("status=0x%02X",status) : status_name\r
+                                               ) + ": ";\r
+                               int cmd = shortmsg.getCommand();\r
+                               switch( cmd ) {\r
+                               case ShortMessage.NOTE_OFF:\r
+                               case ShortMessage.NOTE_ON:\r
+                                       str += ch_prefix + status_prefix + data1;\r
+                                       str += ":[";\r
+                                       if( isRhythmPart(ch) ) {\r
+                                               str += MIDISpec.getPercussionName(data1);\r
+                                       }\r
+                                       else {\r
+                                               str += Music.NoteSymbol.noteNoToSymbol(data1);\r
+                                       }\r
+                                       str +="] Velocity=" + data2;\r
+                                       break;\r
+                               case ShortMessage.POLY_PRESSURE:\r
+                                       str += ch_prefix + status_prefix + "Note=" + data1 + " Pressure=" + data2;\r
+                                       break;\r
+                               case ShortMessage.PROGRAM_CHANGE:\r
+                                       str += ch_prefix + status_prefix + data1 + ":[" + MIDISpec.instrument_names[data1] + "]";\r
+                                       if( data2 != 0 ) str += " data2=" + data2;\r
+                                       break;\r
+                               case ShortMessage.CHANNEL_PRESSURE:\r
+                                       str += ch_prefix + status_prefix + data1;\r
+                                       if( data2 != 0 ) str += " data2=" + data2;\r
+                                       break;\r
+                               case ShortMessage.PITCH_BEND:\r
+                               {\r
+                                       int val = (\r
+                                                       (data1 & 0x7F) | ( (data2 & 0x7F) << 7 )\r
+                                                       );\r
+                                       str += ch_prefix + status_prefix + ( (val-8192) * 100 / 8191) + "% (" + val + ")";\r
+                               }\r
+                               break;\r
+                               case ShortMessage.CONTROL_CHANGE:\r
+                               {\r
+                                       // Control / Mode message name\r
+                                       String ctrl_name = MIDISpec.getControllerName(data1);\r
+                                       str += ch_prefix + (data1 < 0x78 ? "CtrlChg: " : "ModeMsg: ");\r
+                                       if( ctrl_name == null ) {\r
+                                               str += " No.=" + data1 + " Value=" + data2;\r
+                                               return str;\r
+                                       }\r
+                                       str += ctrl_name;\r
+                                       //\r
+                                       // Controller's value\r
+                                       switch( data1 ) {\r
+                                       case 0x40: case 0x41: case 0x42: case 0x43: case 0x45:\r
+                                               str += " " + ( data2==0x3F?"OFF":data2==0x40?"ON":data2 );\r
+                                               break;\r
+                                       case 0x44: // Legato Footswitch\r
+                                               str += " " + ( data2==0x3F?"Normal":data2==0x40?"Legato":data2 );\r
+                                               break;\r
+                                       case 0x7A: // Local Control\r
+                                               str += " " + ( data2==0x00?"OFF":data2==0x7F?"ON":data2 );\r
+                                               break;\r
+                                       default:\r
+                                               str += " " + data2;\r
+                                               break;\r
+                                       }\r
+                               }\r
+                               break;\r
+\r
+                               default:\r
+                                       // Never reached here\r
+                                       break;\r
+                               }\r
+                       }\r
+                       else { // System Message\r
+                               str += (status_name == null ? ("status="+status) : status_name );\r
+                               str += " (" + data1 + "," + data2 + ")";\r
+                       }\r
+                       return str;\r
+               }\r
+               else if( msg instanceof MetaMessage ) {\r
+                       MetaMessage metamsg = (MetaMessage)msg;\r
+                       byte[] msgdata = metamsg.getData();\r
+                       int msgtype = metamsg.getType();\r
+                       str += "Meta: ";\r
+                       String meta_name = MIDISpec.getMetaName(msgtype);\r
+                       if( meta_name == null ) {\r
+                               str += "Unknown MessageType="+msgtype + " Values=(";\r
+                               for( byte b : msgdata ) str += String.format( " %02X", b );\r
+                               str += " )";\r
+                               return str;\r
+                       }\r
+                       // Add the message type name\r
+                       str += meta_name;\r
+                       //\r
+                       // Add the text data\r
+                       if( MIDISpec.hasMetaText(msgtype) ) {\r
+                               str +=" ["+(new String(msgdata))+"]";\r
+                               return str;\r
+                       }\r
+                       // Add the numeric data\r
+                       switch(msgtype) {\r
+                       case 0x00: // Sequence Number (for MIDI Format 2)\r
+                               if( msgdata.length == 2 ) {\r
+                                       str += String.format(\r
+                                                       ": %04X",\r
+                                                       ((msgdata[0] & 0xFF) << 8) | (msgdata[1] & 0xFF)\r
+                                                       );\r
+                                       break;\r
+                               }\r
+                               str += ": Size not 2 byte : data=(";\r
+                               for( byte b : msgdata ) str += String.format( " %02X", b );\r
+                               str += " )";\r
+                               break;\r
+                       case 0x20: // MIDI Ch.Prefix\r
+                       case 0x21: // MIDI Output Port\r
+                               if( msgdata.length == 1 ) {\r
+                                       str += String.format( ": %02X", msgdata[0] & 0xFF );\r
+                                       break;\r
+                               }\r
+                               str += ": Size not 1 byte : data=(";\r
+                               for( byte b : msgdata ) str += String.format( " %02X", b );\r
+                               str += " )";\r
+                               break;\r
+                       case 0x51: // Tempo\r
+                               str += ": " + MIDISpec.byteArrayToQpmTempo( msgdata ) + "[QPM] (";\r
+                               for( byte b : msgdata ) str += String.format( " %02X", b );\r
+                               str += " )";\r
+                               break;\r
+                       case 0x54: // SMPTE Offset\r
+                               if( msgdata.length == 5 ) {\r
+                                       str += ": "\r
+                                                       + (msgdata[0] & 0xFF) + ":"\r
+                                                       + (msgdata[1] & 0xFF) + ":"\r
+                                                       + (msgdata[2] & 0xFF) + "."\r
+                                                       + (msgdata[3] & 0xFF) + "."\r
+                                                       + (msgdata[4] & 0xFF);\r
+                                       break;\r
+                               }\r
+                               str += ": Size not 5 byte : data=(";\r
+                               for( byte b : msgdata ) str += String.format( " %02X", b );\r
+                               str += " )";\r
+                               break;\r
+                       case 0x58: // Time Signature\r
+                               if( msgdata.length == 4 ) {\r
+                                       str +=": " + msgdata[0] + "/" + (1 << msgdata[1]);\r
+                                       str +=", "+msgdata[2]+"[clk/beat], "+msgdata[3]+"[32nds/24clk]";\r
+                                       break;\r
+                               }\r
+                               str += ": Size not 4 byte : data=(";\r
+                               for( byte b : msgdata ) str += String.format( " %02X", b );\r
+                               str += " )";\r
+                               break;\r
+                       case 0x59: // Key Signature\r
+                               if( msgdata.length == 2 ) {\r
+                                       Music.Key key = new Music.Key(msgdata);\r
+                                       str += ": " + key.signatureDescription();\r
+                                       str += " (" + key.toStringIn(Music.SymbolLanguage.NAME) + ")";\r
+                                       break;\r
+                               }\r
+                               str += ": Size not 2 byte : data=(";\r
+                               for( byte b : msgdata ) str += String.format( " %02X", b );\r
+                               str += " )";\r
+                               break;\r
+                       case 0x7F: // Sequencer Specific Meta Event\r
+                               str += " (";\r
+                               for( byte b : msgdata ) str += String.format( " %02X", b );\r
+                               str += " )";\r
+                               break;\r
+                       }\r
+                       return str;\r
+               }\r
+               else if( msg instanceof SysexMessage ) {\r
+                       SysexMessage sysexmsg = (SysexMessage)msg;\r
+                       int status = sysexmsg.getStatus();\r
+                       byte[] msgdata = sysexmsg.getData();\r
+                       int data_byte_pos = 1;\r
+                       switch( status ) {\r
+                       case SysexMessage.SYSTEM_EXCLUSIVE:\r
+                               str += "SysEx: ";\r
+                               break;\r
+                       case SysexMessage.SPECIAL_SYSTEM_EXCLUSIVE:\r
+                               str += "SysEx(Special): ";\r
+                               break;\r
+                       default:\r
+                               str += "SysEx: Invalid (status="+status+") ";\r
+                               break;\r
+                       }\r
+                       if( msgdata.length < 1 ) {\r
+                               str += " Invalid data size: " + msgdata.length;\r
+                               return str;\r
+                       }\r
+                       int manufacturer_id = (int)(msgdata[0] & 0xFF );\r
+                       int device_id = (int)(msgdata[1] & 0xFF);\r
+                       int model_id = (int)(msgdata[2] & 0xFF);\r
+                       String manufacturer_name\r
+                       = MIDISpec.getSysExManufacturerName(manufacturer_id);\r
+                       if( manufacturer_name == null ) {\r
+                               manufacturer_name = String.format( "[Manufacturer code %02X]", msgdata[0] );\r
+                       }\r
+                       str += manufacturer_name + String.format( " (DevID=0x%02X)", device_id );\r
+                       switch( manufacturer_id ) {\r
+                       case 0x7E: // Non-Realtime Universal\r
+                               data_byte_pos++;\r
+                               int sub_id_1 = (int)(msgdata[2] & 0xFF);\r
+                               int sub_id_2 = (int)(msgdata[3] & 0xFF);\r
+                               switch( sub_id_1 ) {\r
+                               case 0x09: // General MIDI (GM)\r
+                                       switch( sub_id_2 ) {\r
+                                       case 0x01: str += " GM System ON"; return str;\r
+                                       case 0x02: str += " GM System OFF"; return str;\r
+                                       }\r
+                                       break;\r
+                               default:\r
+                                       break;\r
+                               }\r
+                               break;\r
+                               // case 0x7F: // Realtime Universal\r
+                       case 0x41: // Roland\r
+                               data_byte_pos++;\r
+                               switch( model_id ) {\r
+                               case 0x42:\r
+                                       str += " [GS]"; data_byte_pos++;\r
+                                       if( msgdata[3]==0x12 ) {\r
+                                               str += "DT1:"; data_byte_pos++;\r
+                                               switch( msgdata[4] ) {\r
+                                               case 0x00:\r
+                                                       if( msgdata[5]==0x00 ) {\r
+                                                               if( msgdata[6]==0x7F ) {\r
+                                                                       if( msgdata[7]==0x00 ) {\r
+                                                                               str += " [88] System Mode Set (Mode 1: Single Module)"; return str;\r
+                                                                       }\r
+                                                                       else if( msgdata[7]==0x01 ) {\r
+                                                                               str += " [88] System Mode Set (Mode 2: Double Module)"; return str;\r
+                                                                       }\r
+                                                               }\r
+                                                       }\r
+                                                       else if( msgdata[5]==0x01 ) {\r
+                                                               int port = (msgdata[7] & 0xFF);\r
+                                                               str += String.format(\r
+                                                                               " [88] Ch.Msg Rx Port: Block=0x%02X, Port=%s",\r
+                                                                               msgdata[6],\r
+                                                                               port==0?"A":port==1?"B":String.format("0x%02X",port)\r
+                                                                               );\r
+                                                               return str;\r
+                                                       }\r
+                                                       break;\r
+                                               case 0x40:\r
+                                                       if( msgdata[5]==0x00 ) {\r
+                                                               switch( msgdata[6] ) {\r
+                                                               case 0x00: str += " Master Tune: "; data_byte_pos += 3; break;\r
+                                                               case 0x04: str += " Master Volume: "; data_byte_pos += 3; break;\r
+                                                               case 0x05: str += " Master Key Shift: "; data_byte_pos += 3; break;\r
+                                                               case 0x06: str += " Master Pan: "; data_byte_pos += 3; break;\r
+                                                               case 0x7F:\r
+                                                                       switch( msgdata[7] ) {\r
+                                                                       case 0x00: str += " GS Reset"; return str;\r
+                                                                       case 0x7F: str += " Exit GS Mode"; return str;\r
+                                                                       }\r
+                                                                       break;\r
+                                                               }\r
+                                                       }\r
+                                                       else if( msgdata[5]==0x01 ) {\r
+                                                               switch( msgdata[6] ) {\r
+                                                               // case 0x00: str += ""; break;\r
+                                                               // case 0x10: str += ""; break;\r
+                                                               case 0x30: str += " Reverb Macro: "; data_byte_pos += 3; break;\r
+                                                               case 0x31: str += " Reverb Character: "; data_byte_pos += 3; break;\r
+                                                               case 0x32: str += " Reverb Pre-LPF: "; data_byte_pos += 3; break;\r
+                                                               case 0x33: str += " Reverb Level: "; data_byte_pos += 3; break;\r
+                                                               case 0x34: str += " Reverb Time: "; data_byte_pos += 3; break;\r
+                                                               case 0x35: str += " Reverb Delay FB: "; data_byte_pos += 3; break;\r
+                                                               case 0x36: str += " Reverb Chorus Level: "; data_byte_pos += 3; break;\r
+                                                               case 0x37: str += " [88] Reverb Predelay Time: "; data_byte_pos += 3; break;\r
+                                                               case 0x38: str += " Chorus Macro: "; data_byte_pos += 3; break;\r
+                                                               case 0x39: str += " Chorus Pre-LPF: "; data_byte_pos += 3; break;\r
+                                                               case 0x3A: str += " Chorus Level: "; data_byte_pos += 3; break;\r
+                                                               case 0x3B: str += " Chorus FB: "; data_byte_pos += 3; break;\r
+                                                               case 0x3C: str += " Chorus Delay: "; data_byte_pos += 3; break;\r
+                                                               case 0x3D: str += " Chorus Rate: "; data_byte_pos += 3; break;\r
+                                                               case 0x3E: str += " Chorus Depth: "; data_byte_pos += 3; break;\r
+                                                               case 0x3F: str += " Chorus Send Level To Reverb: "; data_byte_pos += 3; break;\r
+                                                               case 0x40: str += " [88] Chorus Send Level To Delay: "; data_byte_pos += 3; break;\r
+                                                               case 0x50: str += " [88] Delay Macro: "; data_byte_pos += 3; break;\r
+                                                               case 0x51: str += " [88] Delay Pre-LPF: "; data_byte_pos += 3; break;\r
+                                                               case 0x52: str += " [88] Delay Time Center: "; data_byte_pos += 3; break;\r
+                                                               case 0x53: str += " [88] Delay Time Ratio Left: "; data_byte_pos += 3; break;\r
+                                                               case 0x54: str += " [88] Delay Time Ratio Right: "; data_byte_pos += 3; break;\r
+                                                               case 0x55: str += " [88] Delay Level Center: "; data_byte_pos += 3; break;\r
+                                                               case 0x56: str += " [88] Delay Level Left: "; data_byte_pos += 3; break;\r
+                                                               case 0x57: str += " [88] Delay Level Right: "; data_byte_pos += 3; break;\r
+                                                               case 0x58: str += " [88] Delay Level: "; data_byte_pos += 3; break;\r
+                                                               case 0x59: str += " [88] Delay FB: "; data_byte_pos += 3; break;\r
+                                                               case 0x5A: str += " [88] Delay Send Level To Reverb: "; data_byte_pos += 3; break;\r
+                                                               }\r
+                                                       }\r
+                                                       else if( msgdata[5]==0x02 ) {\r
+                                                               switch( msgdata[6] ) {\r
+                                                               case 0x00: str += " [88] EQ Low Freq: "; data_byte_pos += 3; break;\r
+                                                               case 0x01: str += " [88] EQ Low Gain: "; data_byte_pos += 3; break;\r
+                                                               case 0x02: str += " [88] EQ High Freq: "; data_byte_pos += 3; break;\r
+                                                               case 0x03: str += " [88] EQ High Gain: "; data_byte_pos += 3; break;\r
+                                                               }\r
+                                                       }\r
+                                                       else if( msgdata[5]==0x03 ) {\r
+                                                               if( msgdata[6] == 0x00 ) {\r
+                                                                       str += " [Pro] EFX Type: "; data_byte_pos += 3;\r
+                                                               }\r
+                                                               else if( msgdata[6] >= 0x03 && msgdata[6] <= 0x16 ) {\r
+                                                                       str += String.format(" [Pro] EFX Param %d", msgdata[6]-2 );\r
+                                                                       data_byte_pos += 3;\r
+                                                               }\r
+                                                               else if( msgdata[6] == 0x17 ) {\r
+                                                                       str += " [Pro] EFX Send Level To Reverb: "; data_byte_pos += 3;\r
+                                                               }\r
+                                                               else if( msgdata[6] == 0x18 ) {\r
+                                                                       str += " [Pro] EFX Send Level To Chorus: "; data_byte_pos += 3;\r
+                                                               }\r
+                                                               else if( msgdata[6] == 0x19 ) {\r
+                                                                       str += " [Pro] EFX Send Level To Delay: "; data_byte_pos += 3;\r
+                                                               }\r
+                                                               else if( msgdata[6] == 0x1B ) {\r
+                                                                       str += " [Pro] EFX Ctrl Src1: "; data_byte_pos += 3;\r
+                                                               }\r
+                                                               else if( msgdata[6] == 0x1C ) {\r
+                                                                       str += " [Pro] EFX Ctrl Depth1: "; data_byte_pos += 3;\r
+                                                               }\r
+                                                               else if( msgdata[6] == 0x1D ) {\r
+                                                                       str += " [Pro] EFX Ctrl Src2: "; data_byte_pos += 3;\r
+                                                               }\r
+                                                               else if( msgdata[6] == 0x1E ) {\r
+                                                                       str += " [Pro] EFX Ctrl Depth2: "; data_byte_pos += 3;\r
+                                                               }\r
+                                                               else if( msgdata[6] == 0x1F ) {\r
+                                                                       str += " [Pro] EFX Send EQ Switch: "; data_byte_pos += 3;\r
+                                                               }\r
+                                                       }\r
+                                                       else if( (msgdata[5] & 0xF0) == 0x10 ) {\r
+                                                               int ch = (msgdata[5] & 0x0F);\r
+                                                               if( ch <= 9 ) ch--; else if( ch == 0 ) ch = 9;\r
+                                                               if( msgdata[6]==0x02 ) {\r
+                                                                       str += String.format(\r
+                                                                                       " Rx Ch: Part=%d(0x%02X) Ch=0x%02X", (ch+1),  msgdata[5], msgdata[7]\r
+                                                                                       );\r
+                                                                       return str;\r
+                                                               }\r
+                                                               else if( msgdata[6]==0x15 ) {\r
+                                                                       String map;\r
+                                                                       switch( msgdata[7] ) {\r
+                                                                       case 0: map = " NormalPart"; break;\r
+                                                                       case 1: map = " DrumMap1"; break;\r
+                                                                       case 2: map = " DrumMap2"; break;\r
+                                                                       default: map = String.format("0x%02X",msgdata[7]); break;\r
+                                                                       }\r
+                                                                       str += String.format(\r
+                                                                                       " Rhythm Part: Ch=%d(0x%02X) Map=%s",\r
+                                                                                       (ch+1), msgdata[5],\r
+                                                                                       map\r
+                                                                                       );\r
+                                                                       return str;\r
+                                                               }\r
+                                                       }\r
+                                                       else if( (msgdata[5] & 0xF0) == 0x40 ) {\r
+                                                               int ch = (msgdata[5] & 0x0F);\r
+                                                               if( ch <= 9 ) ch--; else if( ch == 0 ) ch = 9;\r
+                                                               int dt = (msgdata[7] & 0xFF);\r
+                                                               if( msgdata[6]==0x20 ) {\r
+                                                                       str += String.format(\r
+                                                                                       " [88] EQ: Ch=%d(0x%02X) %s",\r
+                                                                                       (ch+1), msgdata[5],\r
+                                                                                       dt==0 ? "OFF" : dt==1 ? "ON" : String.format("0x%02X",dt)\r
+                                                                                       );\r
+                                                               }\r
+                                                               else if( msgdata[6]==0x22 ) {\r
+                                                                       str += String.format(\r
+                                                                                       " [Pro] Part EFX Assign: Ch=%d(0x%02X) %s",\r
+                                                                                       (ch+1), msgdata[5],\r
+                                                                                       dt==0 ? "ByPass" : dt==1 ? "EFX" : String.format("0x%02X",dt)\r
+                                                                                       );\r
+                                                               }\r
+                                                       }\r
+                                                       break;\r
+                                               } // [4]\r
+                                       } // [3] [DT1]\r
+                                       break; // [GS]\r
+                               case 0x45:\r
+                                       str += " [GS-LCD]"; data_byte_pos++;\r
+                                       if( msgdata[3]==0x12 ) {\r
+                                               str += " [DT1]"; data_byte_pos++;\r
+                                               if( msgdata[4]==0x10 && msgdata[5]==0x00 && msgdata[6]==0x00 ) {\r
+                                                       data_byte_pos += 3;\r
+                                                       str += " Disp [" +(new String(\r
+                                                                       msgdata, data_byte_pos, msgdata.length - data_byte_pos - 2\r
+                                                                       ))+ "]";\r
+                                               }\r
+                                       } // [3] [DT1]\r
+                                       break;\r
+                               case 0x14: str += " [D-50]"; data_byte_pos++; break;\r
+                               case 0x16: str += " [MT-32]"; data_byte_pos++; break;\r
+                               } // [2] model_id\r
+                               break;\r
+                       case 0x43: // Yamaha (XG)\r
+                               data_byte_pos++;\r
+                               if( model_id == 0x4C ) {\r
+                                       str += " [XG]";\r
+                                       if( msgdata[3]==0 && msgdata[4]==0 && msgdata[5]==0x7E && msgdata[6]==0 ) {\r
+                                               str += " XG System ON"; return str;\r
+                                       }\r
+                                       data_byte_pos++;\r
+                               }\r
+                               break;\r
+                       default:\r
+                               break;\r
+                       }\r
+                       int i;\r
+                       str += " data=(";\r
+                       for( i = data_byte_pos; i<msgdata.length-1; i++ ) {\r
+                               str += String.format( " %02X", msgdata[i] );\r
+                       }\r
+                       if( i < msgdata.length && (int)(msgdata[i] & 0xFF) != 0xF7 ) {\r
+                               str+=" [ Invalid EOX " + String.format( "%02X", msgdata[i] ) + " ]";\r
+                       }\r
+                       str += " )";\r
+                       return str;\r
+               }\r
+               byte[] msg_data = msg.getMessage();\r
+               str += "(";\r
+               for( byte b : msg_data ) {\r
+                       str += String.format( " %02X", b );\r
+               }\r
+               str += " )";\r
+               return str;\r
+       }\r
+}\r
+\r
+///////////////////////////////////////////////////////////////////////////\r
+//\r
+// MIDI シーケンスデータのインデックス\r
+//\r
+// 拍子、テンポ、調だけを抜き出したトラックを保持するためのインデックス。\r
+//\r
+// 指定の MIDI tick の位置におけるテンポ、調、拍子を取得したり、\r
+// 拍子情報から MIDI tick と小節位置との間の変換を行うために使います。\r
+//\r
+class SequenceIndex {\r
+\r
+       private Track timesig_positions;\r
+       private Track tempo_positions;\r
+       private Track keysig_positions;\r
+       private Sequence tmp_seq;\r
+\r
+       public int       ticks_per_whole_note;\r
+\r
+       public SequenceIndex( Sequence source_seq ) {\r
+               try {\r
+                       int ppq = source_seq.getResolution();\r
+                       ticks_per_whole_note = ppq * 4;\r
+                       tmp_seq = new Sequence(Sequence.PPQ, ppq, 3);\r
+                       Track[] tmp_tracks = tmp_seq.getTracks();\r
+                       timesig_positions = tmp_tracks[0];\r
+                       tempo_positions = tmp_tracks[1];\r
+                       keysig_positions = tmp_tracks[2];\r
+                       Track[] tracks = source_seq.getTracks();\r
+                       for( Track tk : tracks ) {\r
+                               for( int i_evt = 0 ; i_evt < tk.size(); i_evt++ ) {\r
+                                       MidiEvent evt = tk.get(i_evt);\r
+                                       MidiMessage msg = evt.getMessage();\r
+                                       if( ! (msg instanceof MetaMessage) ) continue;\r
+                                       switch( ((MetaMessage)msg).getType() ) {\r
+                                       case 0x51: tempo_positions.add(evt); break;\r
+                                       case 0x58: timesig_positions.add(evt); break;\r
+                                       case 0x59: keysig_positions.add(evt); break;\r
+                                       default: break;\r
+                                       }\r
+                               }\r
+                       }\r
+               }\r
+               catch ( InvalidMidiDataException e ) {\r
+                       e.printStackTrace();\r
+               }\r
+       }\r
+\r
+       private MetaMessage lastMessageAt( Track tk, long tick_position ) {\r
+               if( tk == null ) return null;\r
+               MidiEvent evt;\r
+               MetaMessage msg;\r
+               for( int i_evt = tk.size() - 1 ; i_evt >= 0; i_evt-- ) {\r
+                       evt = tk.get(i_evt);\r
+                       if( evt.getTick() > tick_position ) continue;\r
+                       msg = (MetaMessage)( evt.getMessage() );\r
+                       if( msg.getType() != 0x2F /* EOT */ ) return msg;\r
+               }\r
+               return null;\r
+       }\r
+       public MetaMessage lastTimeSignatureAt( long tick_position ) {\r
+               return lastMessageAt( timesig_positions, tick_position );\r
+       }\r
+       public MetaMessage lastKeySignatureAt( long tick_position ) {\r
+               return lastMessageAt( keysig_positions, tick_position );\r
+       }\r
+       public MetaMessage lastTempoAt( long tick_position ) {\r
+               return lastMessageAt( tempo_positions, tick_position );\r
+       }\r
+       public int getResolution() { return tmp_seq.getResolution(); }\r
+\r
+       // MIDI tick を小節位置に変換\r
+       public int last_measure;\r
+       public int last_beat;\r
+       public int last_extra_tick;\r
+       public int ticks_per_beat;\r
+       public byte timesig_upper;\r
+       public byte timesig_lower_index;\r
+       int tickToMeasure(long tick_position) {\r
+               byte extra_beats = 0;\r
+               MidiEvent evt = null;\r
+               MidiMessage msg = null;\r
+               byte[] data = null;\r
+               long current_tick = 0L;\r
+               long next_timesig_tick = 0L;\r
+               long prev_tick = 0L;\r
+               long duration = 0L;\r
+               last_measure = 0;\r
+               int measures, beats;\r
+               int i_evt = 0;\r
+               timesig_upper = 4;\r
+               timesig_lower_index = 2; // =log2(4)\r
+                               if( timesig_positions != null ) {\r
+                                       do {\r
+                                               // Check current time-signature event\r
+                                               if( i_evt < timesig_positions.size() ) {\r
+                                                       msg = (evt = timesig_positions.get(i_evt)).getMessage();\r
+                                                       current_tick = next_timesig_tick = evt.getTick();\r
+                                                       if(\r
+                                                                       current_tick > tick_position || (\r
+                                                                                       msg.getStatus() == 0xFF && ((MetaMessage)msg).getType() == 0x2F /* EOT */\r
+                                                                                       )\r
+                                                                       ) {\r
+                                                               current_tick = tick_position;\r
+                                                       }\r
+                                               }\r
+                                               else { // No event\r
+                                                       current_tick = next_timesig_tick = tick_position;\r
+                                               }\r
+                                               // Add measure from last event\r
+                                               //\r
+                                               ticks_per_beat = ticks_per_whole_note >> timesig_lower_index;\r
+                       duration = current_tick - prev_tick;\r
+                       beats = (int)( duration / ticks_per_beat );\r
+                       last_extra_tick = (int)(duration % ticks_per_beat);\r
+                       measures = beats / timesig_upper;\r
+                       extra_beats = (byte)(beats % timesig_upper);\r
+                       last_measure += measures;\r
+                       if( next_timesig_tick > tick_position ) break;  // Not reached to next time signature\r
+                       //\r
+                       // Reached to the next time signature, so get it.\r
+                       if( ( data = ((MetaMessage)msg).getData() ).length > 0 ) { // To skip EOT, check the data length.\r
+                               timesig_upper = data[0];\r
+                               timesig_lower_index = data[1];\r
+                       }\r
+                       if( current_tick == tick_position )  break;  // Calculation complete\r
+                       //\r
+                       // Calculation incomplete, so prepare for next\r
+                       //\r
+                       if( extra_beats > 0 ) {\r
+                               //\r
+                               // Extra beats are treated as 1 measure\r
+                               last_measure++;\r
+                       }\r
+                       prev_tick = current_tick;\r
+                       i_evt++;\r
+                                       } while( true );\r
+                               }\r
+                               last_beat = extra_beats;\r
+                               return last_measure;\r
+       }\r
+\r
+       // 小節位置を MIDI tick に変換\r
+       public long measureToTick( int measure ) {\r
+               return measureToTick( measure, 0, 0 );\r
+       }\r
+       public long measureToTick( int measure, int beat, int extra_tick ) {\r
+               MidiEvent evt = null;\r
+               MidiMessage msg = null;\r
+               byte[] data = null;\r
+               long tick = 0L;\r
+               long prev_tick = 0L;\r
+               long duration = 0L;\r
+               long duration_sum = 0L;\r
+               long estimated_ticks;\r
+               int ticks_per_beat;\r
+               int i_evt = 0;\r
+               timesig_upper = 4;\r
+               timesig_lower_index = 2; // =log2(4)\r
+                               do {\r
+                                       ticks_per_beat = ticks_per_whole_note >> timesig_lower_index;\r
+                                       estimated_ticks = ((measure * timesig_upper) + beat) * ticks_per_beat + extra_tick;\r
+                                       if( timesig_positions == null || i_evt > timesig_positions.size() ) {\r
+                                               return duration_sum + estimated_ticks;\r
+                                       }\r
+                                       msg = (evt = timesig_positions.get(i_evt)).getMessage();\r
+                                       if( msg.getStatus() == 0xFF && ((MetaMessage)msg).getType() == 0x2F /* EOT */ ) {\r
+                                               return duration_sum + estimated_ticks;\r
+                                       }\r
+                                       duration = (tick = evt.getTick()) - prev_tick;\r
+                                       if( duration >= estimated_ticks ) {\r
+                                               return duration_sum + estimated_ticks;\r
+                                       }\r
+                                       // Re-calculate measure (ignore extra beats/ticks)\r
+                                       measure -= ( duration / (ticks_per_beat * timesig_upper) );\r
+                                       duration_sum += duration;\r
+                                       //\r
+                                       // Get next time-signature\r
+                                       data = ( (MetaMessage)msg ).getData();\r
+                                       timesig_upper = data[0];\r
+                                       timesig_lower_index = data[1];\r
+                                       prev_tick = tick;\r
+                                       i_evt++;\r
+                               } while( true );\r
+       }\r
+}\r
diff --git a/src/MIDIMsgForm.java b/src/MIDIMsgForm.java
new file mode 100644 (file)
index 0000000..f64747d
--- /dev/null
@@ -0,0 +1,1623 @@
+import java.awt.Color;\r
+import java.awt.Component;\r
+import java.awt.Dimension;\r
+import java.awt.FlowLayout;\r
+import java.awt.Graphics;\r
+import java.awt.GridLayout;\r
+import java.awt.Label;\r
+import java.awt.event.ActionEvent;\r
+import java.awt.event.ActionListener;\r
+import java.awt.event.InputEvent;\r
+import java.awt.event.ItemEvent;\r
+import java.awt.event.ItemListener;\r
+import java.awt.event.MouseEvent;\r
+import java.awt.event.MouseListener;\r
+import java.util.ArrayList;\r
+\r
+import javax.sound.midi.InvalidMidiDataException;\r
+import javax.sound.midi.MetaMessage;\r
+import javax.sound.midi.MidiChannel;\r
+import javax.sound.midi.MidiMessage;\r
+import javax.sound.midi.ShortMessage;\r
+import javax.sound.midi.SysexMessage;\r
+import javax.swing.Box;\r
+import javax.swing.BoxLayout;\r
+import javax.swing.ComboBoxModel;\r
+import javax.swing.DefaultBoundedRangeModel;\r
+import javax.swing.DefaultComboBoxModel;\r
+import javax.swing.JButton;\r
+import javax.swing.JCheckBox;\r
+import javax.swing.JComboBox;\r
+import javax.swing.JDialog;\r
+import javax.swing.JLabel;\r
+import javax.swing.JList;\r
+import javax.swing.JOptionPane;\r
+import javax.swing.JPanel;\r
+import javax.swing.JScrollPane;\r
+import javax.swing.JSlider;\r
+import javax.swing.JSpinner;\r
+import javax.swing.JTextArea;\r
+import javax.swing.ListCellRenderer;\r
+import javax.swing.SpinnerNumberModel;\r
+import javax.swing.event.ChangeEvent;\r
+import javax.swing.event.ChangeListener;\r
+import javax.swing.event.ListDataEvent;\r
+import javax.swing.event.ListDataListener;\r
+import javax.swing.event.ListSelectionEvent;\r
+import javax.swing.event.ListSelectionListener;\r
+\r
+/**\r
+ * MIDI Message Form for MIDI Chord Helper\r
+ *\r
+ * @auther\r
+ *     Copyright (C) 2006-2013 @きよし - Akiyoshi Kamide\r
+ *     http://www.yk.rim.or.jp/~kamide/music/chordhelper/\r
+ */\r
+class MidiEventDialog extends JDialog {\r
+       MidiMessageForm midi_message_form = new MidiMessageForm();\r
+       TickPositionForm tick_position_form = new TickPositionForm();\r
+       JButton ok_button = new JButton("OK");\r
+       JButton cancel_button = new JButton("Cancel");\r
+       public MidiEventDialog() {\r
+               setLayout(new FlowLayout());\r
+               add( tick_position_form );\r
+               add( midi_message_form );\r
+               JPanel ok_cancel_panel = new JPanel();\r
+               ok_cancel_panel.add( ok_button );\r
+               ok_cancel_panel.add( cancel_button );\r
+               add( ok_cancel_panel );\r
+               cancel_button.addActionListener(\r
+                       new ActionListener() {\r
+                               public void actionPerformed(ActionEvent e) {\r
+                                       setVisible(false);\r
+                               }\r
+                       }\r
+               );\r
+       }\r
+       public void openTickForm() {\r
+               tick_position_form.setVisible(true);\r
+               midi_message_form.setVisible(false);\r
+               setBounds( 200, 300, 500, 120 );\r
+               setVisible(true);\r
+       }\r
+       public void openEventForm() {\r
+               tick_position_form.setVisible(true);\r
+               midi_message_form.setVisible(true);\r
+               midi_message_form.setDurationVisible(true);\r
+               setBounds( 200, 300, 630, 320 );\r
+               setVisible(true);\r
+       }\r
+       public void openMessageForm() {\r
+               tick_position_form.setVisible(false);\r
+               midi_message_form.setVisible(true);\r
+               midi_message_form.setDurationVisible(false);\r
+               setBounds( 200, 300, 630, 270 );\r
+               setVisible(true);\r
+       }\r
+}\r
+\r
+/**\r
+ * MIDI Message Entry Form - MIDIメッセージ入力欄\r
+ */\r
+class MidiMessageForm extends JPanel implements ActionListener {\r
+       /**\r
+        * MIDIステータス\r
+        */\r
+       DefaultComboBoxModel<String> statusComboBoxModel =\r
+               new DefaultComboBoxModel<String>() {\r
+                       {\r
+                               int i; String s;\r
+                               // チャンネルメッセージ\r
+                               for( i = 0x80; i <= 0xE0 ; i += 0x10 ) {\r
+                                       if( (s = MIDISpec.getStatusName(i)) == null )\r
+                                               continue;\r
+                                       addElement(String.format("0x%02X : %s", i, s));\r
+                               }\r
+                               // チャンネルを持たない SysEx やメタメッセージなど\r
+                               for( i = 0xF0; i <= 0xFF ; i++ ) {\r
+                                       if( (s = MIDISpec.getStatusName(i)) == null )\r
+                                               continue;\r
+                                       addElement(String.format("0x%02X : %s", i, s));\r
+                               }\r
+                       }\r
+               };\r
+       /**\r
+        * ノート番号\r
+        */\r
+       DefaultComboBoxModel<String> noteComboBoxModel =\r
+               new DefaultComboBoxModel<String>() {\r
+                       {\r
+                               for( int i = 0; i<=0x7F; i++ ) addElement(\r
+                                       String.format(\r
+                                               "0x%02X : %d : %s", i, i, Music.NoteSymbol.noteNoToSymbol(i)\r
+                                       )\r
+                               );\r
+                               // Center note C\r
+                               setSelectedItem(getElementAt(60));\r
+                       }\r
+               };\r
+       /**\r
+        * 打楽器名\r
+        */\r
+       DefaultComboBoxModel<String> percussionComboBoxModel =\r
+               new DefaultComboBoxModel<String>() {\r
+                       {\r
+                               for( int i = 0; i<=0x7F; i++ ) addElement(\r
+                                       String.format(\r
+                                               "0x%02X : %d : %s", i, i, MIDISpec.getPercussionName(i)\r
+                                       )\r
+                               );\r
+                               setSelectedItem(getElementAt(35)); // Acoustic Bass Drum\r
+                       }\r
+               };\r
+       /**\r
+        * コントロールチェンジ\r
+        */\r
+       DefaultComboBoxModel<String> controlChangeComboBoxModel =\r
+               new DefaultComboBoxModel<String>() {\r
+                       {\r
+                               String s;\r
+                               for( int i = 0; i<=0x7F; i++ ) {\r
+                                       if( (s = MIDISpec.getControllerName(i)) == null )\r
+                                               continue;\r
+                                       addElement(String.format("0x%02X : %d : %s", i, i, s));\r
+                               }\r
+                       }\r
+               };\r
+       /**\r
+        * 楽器名(音色)\r
+        */\r
+       DefaultComboBoxModel<String> instrumentComboBoxModel =\r
+               new DefaultComboBoxModel<String>() {\r
+                       {\r
+                               for( int i = 0; i<=0x7F; i++ ) addElement(\r
+                                       String.format(\r
+                                               "0x%02X : %s", i, MIDISpec.instrument_names[i]\r
+                                       )\r
+                               );\r
+                       }\r
+               };\r
+       /**\r
+        * MetaMessage Type\r
+        */\r
+       DefaultComboBoxModel<String> metaTypeComboBoxModel =\r
+               new DefaultComboBoxModel<String>() {\r
+                       {\r
+                               String s;\r
+                               String initial_type_string = null;\r
+                               for( int type = 0; type < 0x80 ; type++ ) {\r
+                                       if( (s = MIDISpec.getMetaName(type)) == null ) {\r
+                                               continue;\r
+                                       }\r
+                                       s = String.format("0x%02X : %s", type, s);\r
+                                       addElement(s);\r
+                                       if( type == 0x51 )\r
+                                               initial_type_string = s; // Tempo\r
+                               }\r
+                               setSelectedItem(initial_type_string);\r
+                       }\r
+               };\r
+       /**\r
+        * 16進数値のみの選択データモデル\r
+        */\r
+       DefaultComboBoxModel<String> hexData1ComboBoxModel =\r
+               new DefaultComboBoxModel<String>() {\r
+                       {\r
+                               for( int i = 0; i<=0x7F; i++ ) {\r
+                                       addElement(String.format("0x%02X : %d", i, i ));\r
+                               }\r
+                       }\r
+               };\r
+       /**\r
+        * 16進数値のみの選択データモデル(ShortMessageデータ2バイト目)\r
+        */\r
+       DefaultComboBoxModel<String> hexData2ComboBoxModel =\r
+               new DefaultComboBoxModel<String>() {\r
+                       {\r
+                               for( int i = 0; i<=0x7F; i++ ) {\r
+                                       addElement(String.format("0x%02X : %d", i, i ));\r
+                               }\r
+                       }\r
+               };\r
+       // データ選択操作部\r
+       HexSelecter statusText = new HexSelecter("Status/Command");\r
+       HexSelecter data1Text = new HexSelecter("[Data1] ");\r
+       HexSelecter data2Text = new HexSelecter("[Data2] ");\r
+       MidiChannelComboSelecter channelText =\r
+               new MidiChannelComboSelecter("MIDI Channel");\r
+\r
+       JComboBox<String> statusComboBox = statusText.getComboBox();\r
+       JComboBox<String> data1ComboBox = data1Text.getComboBox();\r
+       JComboBox<String> data2ComboBox = data2Text.getComboBox();\r
+       JComboBox<Integer> channelComboBox = channelText.getComboBox();\r
+\r
+       /**\r
+        * 長い値(テキストまたは数値)の入力欄\r
+        */\r
+       HexTextForm dataText = new HexTextForm("Data:",3,50);\r
+       /**\r
+        * 音階入力用ピアノキーボード\r
+        */\r
+       PianoKeyboardPanel keyboardPanel = new PianoKeyboardPanel() {\r
+               {\r
+                       keyboard.setPreferredSize(new Dimension(300,40));\r
+                       keyboard.addPianoKeyboardListener(\r
+                               new PianoKeyboardAdapter() {\r
+                                       public void pianoKeyPressed(int n, InputEvent e) {\r
+                                               data1Text.setValue(n);\r
+                                               if( midi_channels != null )\r
+                                                       midi_channels[channelText.getSelectedChannel()].\r
+                                                       noteOn( n, data2Text.getValue() );\r
+                                       }\r
+                                       public void pianoKeyReleased(int n, InputEvent e) {\r
+                                               if( midi_channels != null ) {\r
+                                                       midi_channels[channelText.getSelectedChannel()].\r
+                                                       noteOff( n, data2Text.getValue() );\r
+                                               }\r
+                                       }\r
+                               }\r
+                       );\r
+               }\r
+       };\r
+       /**\r
+        * 音の長さ\r
+        */\r
+       DurationForm durationForm = new DurationForm();\r
+\r
+       // メタイベント\r
+       /**\r
+        * テンポ選択\r
+        */\r
+       TempoSelecter tempoSelecter = new TempoSelecter() {\r
+               {\r
+                       tempoSpinnerModel.addChangeListener(\r
+                               new ChangeListener() {\r
+                                       @Override\r
+                                       public void stateChanged(ChangeEvent e) {\r
+                                               dataText.setValue(getTempoByteArray());\r
+                                       }\r
+                               }\r
+                       );\r
+               }\r
+       };\r
+       /**\r
+        * 拍子選択\r
+        */\r
+       TimeSignatureSelecter timesigSelecter = new TimeSignatureSelecter() {\r
+               {\r
+                       upperTimesigSpinnerModel.addChangeListener(\r
+                               new ChangeListener() {\r
+                                       @Override\r
+                                       public void stateChanged(ChangeEvent e) {\r
+                                               dataText.setValue(getByteArray());\r
+                                       }\r
+                               }\r
+                       );\r
+                       lowerTimesigCombobox.addActionListener(\r
+                               new ActionListener() {\r
+                                       @Override\r
+                                       public void actionPerformed(ActionEvent e) {\r
+                                               dataText.setValue(getByteArray());\r
+                                       }\r
+                               }\r
+                       );\r
+               }\r
+       };\r
+       /**\r
+        * 調号選択\r
+        */\r
+       KeySignatureSelecter keysigSelecter = new KeySignatureSelecter() {\r
+               {\r
+                       keysigCombobox.addActionListener(\r
+                               new ActionListener() {\r
+                                       public void actionPerformed(ActionEvent e) {\r
+                                               dataText.setValue(getKey().getBytes());\r
+                                       }\r
+                               }\r
+                       );\r
+                       minor_checkbox.addItemListener(\r
+                               new ItemListener() {\r
+                                       public void itemStateChanged(ItemEvent e) {\r
+                                               dataText.setValue(getKey().getBytes());\r
+                                       }\r
+                               }\r
+                       );\r
+               }\r
+       };\r
+\r
+       /**\r
+        * 音を鳴らす出力MIDIチャンネル\r
+        */\r
+       private MidiChannel[] midi_channels = null;\r
+\r
+       /**\r
+        * Note on/off のときに Duration フォームを表示するか\r
+        */\r
+       private boolean is_duration_visible = true;\r
+\r
+       public MidiMessageForm() {\r
+               //\r
+               // Set models\r
+               //\r
+               statusComboBox.setModel( statusComboBoxModel );\r
+               statusComboBox.setSelectedIndex(1); // NoteOn\r
+               data2ComboBox.setModel( hexData2ComboBoxModel );\r
+               data2ComboBox.setSelectedIndex(64); // Center\r
+               //\r
+               // Add listener\r
+               //\r
+               statusComboBox.addActionListener(this);\r
+               channelComboBox.addActionListener(this);\r
+               data1ComboBox.addActionListener(this);\r
+               //\r
+               // Layout\r
+               //\r
+               JPanel panel1 = new JPanel();\r
+               panel1.add( statusText );\r
+               panel1.add( channelText );\r
+\r
+               JPanel panel2 = new JPanel();\r
+               panel2.add( data1Text );\r
+               panel2.add( keyboardPanel );\r
+\r
+               JPanel panel3 = new JPanel();\r
+               panel3.add( data2Text );\r
+\r
+               setLayout(new BoxLayout( this, BoxLayout.Y_AXIS ));\r
+               add( panel1 );\r
+               add( durationForm );\r
+               add( panel2 );\r
+               add( panel3 );\r
+               add( tempoSelecter );\r
+               add( timesigSelecter );\r
+               add( keysigSelecter );\r
+               add( dataText );\r
+\r
+               updateVisible();\r
+       }\r
+       // ActionListener\r
+       //\r
+       public void actionPerformed(ActionEvent e) {\r
+               Object src = e.getSource();\r
+               if( src == data1ComboBox ) {\r
+                       int status = statusText.getValue();\r
+                       int data1 = data1Text.getValue();\r
+                       if( isNote(status) ) { // Data1 -> Note\r
+                               if( data1 >= 0 ) keyboardPanel.keyboard.setSelectedNote(data1);\r
+                       }\r
+                       else if( status == 0xFF ) {\r
+                               switch( data1 ) { // Data type -> Selecter\r
+                               case 0x51: dataText.setValue( tempoSelecter.getTempoByteArray() ); break;\r
+                               case 0x58: dataText.setValue( timesigSelecter.getByteArray() ); break;\r
+                               case 0x59: dataText.setValue( keysigSelecter.getKey().getBytes() ); break;\r
+                               default: break;\r
+                               }\r
+                       }\r
+               }\r
+               updateVisible();\r
+       }\r
+       //\r
+       // Methods\r
+       //\r
+       public void setOutputMidiChannels( MidiChannel midi_channels[] ) {\r
+               this.midi_channels = midi_channels;\r
+       }\r
+       public void setDurationVisible(boolean is_visible) {\r
+               is_duration_visible = is_visible;\r
+               updateVisible();\r
+       }\r
+       public boolean isDurationVisible() {\r
+               return is_duration_visible;\r
+       }\r
+       public void updateVisible() {\r
+               int msg_status = statusText.getValue();\r
+               boolean is_ch_msg = MIDISpec.isChannelMessage(msg_status);\r
+               channelText.setVisible(is_ch_msg);\r
+               statusText.setTitle(\r
+                               "[Status] "+(is_ch_msg ? "Command" : "")\r
+                               );\r
+               durationForm.setVisible( is_duration_visible && isNote(msg_status) );\r
+               keyboardPanel.setVisible( msg_status <= 0xAF );\r
+\r
+               if(\r
+                               msg_status <= 0xEF\r
+                               ||\r
+                               msg_status >= 0xF1 && msg_status <= 0xF3\r
+                               ||\r
+                               msg_status == 0xFF\r
+                               ) {\r
+                       data1Text.setVisible( true );\r
+               }\r
+               else {\r
+                       data1Text.setVisible( false );\r
+               }\r
+\r
+               if(\r
+                               (msg_status >= 0xC0 && msg_status <= 0xDF) ||\r
+                               msg_status == 0xF0 || msg_status == 0xF1 ||\r
+                               msg_status == 0xF3 || msg_status >= 0xF6\r
+                               ) {\r
+                       data2Text.setVisible( false );\r
+               }\r
+               else {\r
+                       data2Text.setVisible( true );\r
+               }\r
+               data2Text.setTitle("[Data2] "+(\r
+                               msg_status <= 0x9F ? "Velocity" :\r
+                                       msg_status <= 0xAF ? "Pressure" :\r
+                                               msg_status <= 0xBF ? "Value" :\r
+                                                       (msg_status & 0xF0) == 0xE0 ? "High 7bit value" : ""\r
+                               ));\r
+\r
+               // Show if Sysex or Meta\r
+               dataText.setVisible(\r
+                               msg_status == 0xF0 ||\r
+                               msg_status == 0xF7 ||\r
+                               msg_status == 0xFF\r
+                               );\r
+\r
+               if( msg_status != 0xFF ) {\r
+                       tempoSelecter.setVisible(false);\r
+                       timesigSelecter.setVisible(false);\r
+                       keysigSelecter.setVisible(false);\r
+               }\r
+\r
+               switch( msg_status & 0xF0 ) {\r
+               // ステータスに応じて、1バイト目のデータモデルを切り替える。\r
+\r
+               case 0x80: // Note Off\r
+               case 0x90: // Note On\r
+               case 0xA0: // Polyphonic Key Pressure\r
+                       int ch = channelText.getSelectedChannel();\r
+                       data1Text.setTitle(\r
+                                       "[Data1] "+( ch == 9 ? "Percussion" : "Note No." )\r
+                                       );\r
+                       data1ComboBox.setModel(\r
+                                       ch == 9 ? percussionComboBoxModel : noteComboBoxModel\r
+                                       );\r
+                       break;\r
+\r
+               case 0xB0: // Control Change / Mode Change\r
+                       data1Text.setTitle("[Data1] Control/Mode No.");\r
+                       data1ComboBox.setModel(controlChangeComboBoxModel);\r
+                       break;\r
+\r
+               case 0xC0: // Program Change\r
+                       data1Text.setTitle( "[Data1] Program No.");\r
+                       data1ComboBox.setModel(instrumentComboBoxModel);\r
+                       break;\r
+\r
+               case 0xD0: // Channel Pressure\r
+                       data1Text.setTitle("[Data1] Pressure");\r
+                       data1ComboBox.setModel(hexData1ComboBoxModel);\r
+                       break;\r
+\r
+               case 0xE0: // Pitch Bend\r
+                       data1Text.setTitle("[Data1] Low 7bit value");\r
+                       data1ComboBox.setModel(hexData1ComboBoxModel);\r
+                       break;\r
+\r
+               default:\r
+                       if( msg_status == 0xFF ) { // MetaMessage\r
+                               data1Text.setTitle("[Data1] MetaEvent Type");\r
+                               data1ComboBox.setModel(metaTypeComboBoxModel);\r
+                               int msg_type = data1Text.getValue();\r
+                               tempoSelecter.setVisible( msg_type == 0x51 );\r
+                               timesigSelecter.setVisible( msg_type == 0x58 );\r
+                               keysigSelecter.setVisible( msg_type == 0x59 );\r
+                               //\r
+                               if( MIDISpec.isEOT(msg_type) ) {\r
+                                       dataText.clear();\r
+                                       dataText.setVisible(false);\r
+                               }\r
+                               else {\r
+                                       dataText.setTitle(\r
+                                                       MIDISpec.hasMetaText( msg_type ) ? "Text:":"Data:"\r
+                                                       );\r
+                               }\r
+                       }\r
+                       else {\r
+                               data1Text.setTitle("[Data1] ");\r
+                               data1ComboBox.setModel(hexData1ComboBoxModel);\r
+                       }\r
+                       break;\r
+               }\r
+       }\r
+       public MidiMessage getMessage() {\r
+               int msg_status = statusText.getValue();\r
+               if( msg_status < 0 ) {\r
+                       return null;\r
+               }\r
+               else if( msg_status == 0xFF ) {\r
+                       int msg_type = data1Text.getValue();\r
+                       if( msg_type < 0 ) return null;\r
+                       byte msg_data[];\r
+                       if( MIDISpec.hasMetaText( msg_type ) ) {\r
+                               msg_data = dataText.getBytesFromString();\r
+                       }\r
+                       else if( msg_type == 0x2F ) { // EOT\r
+                               // To avoid inserting un-removable EOT, ignore the data.\r
+                               msg_data = new byte[0];\r
+                       }\r
+                       else {\r
+                               if( (msg_data = dataText.getBytes() ) == null ) {\r
+                                       return null;\r
+                               }\r
+                       }\r
+                       MetaMessage msg = new MetaMessage();\r
+                       try {\r
+                               msg.setMessage( msg_type, msg_data, msg_data.length );\r
+                       } catch( InvalidMidiDataException e ) {\r
+                               e.printStackTrace();\r
+                               return null;\r
+                       }\r
+                       return (MidiMessage)msg;\r
+               }\r
+               else if( msg_status == 0xF0 || msg_status == 0xF7 ) {\r
+                       SysexMessage msg = new SysexMessage();\r
+                       byte data[] = dataText.getBytes();\r
+                       if( data == null ) return null;\r
+                       try {\r
+                               msg.setMessage(\r
+                                               (int)(msg_status & 0xFF), data, data.length\r
+                                               );\r
+                       } catch( InvalidMidiDataException e ) {\r
+                               e.printStackTrace();\r
+                               return null;\r
+                       }\r
+                       return (MidiMessage)msg;\r
+               }\r
+               ShortMessage msg = new ShortMessage();\r
+               int msg_data1 = data1Text.getValue();\r
+               if( msg_data1 < 0 ) msg_data1 = 0;\r
+               int msg_data2 = data2Text.getValue();\r
+               if( msg_data2 < 0 ) msg_data2 = 0;\r
+               try {\r
+                       if( MIDISpec.isChannelMessage( msg_status ) ) {\r
+                               msg.setMessage(\r
+                                               (msg_status & 0xF0),\r
+                                               channelText.getSelectedChannel(),\r
+                                               msg_data1, msg_data2\r
+                                               );\r
+                       }\r
+                       else {\r
+                               msg.setMessage( msg_status, msg_data1, msg_data2 );\r
+                       }\r
+               } catch( InvalidMidiDataException e ) {\r
+                       e.printStackTrace();\r
+                       return null;\r
+               }\r
+               return (MidiMessage)msg;\r
+       }\r
+       public void setMessage( MidiMessage msg ) {\r
+               if( msg instanceof ShortMessage ) {\r
+                       ShortMessage smsg = (ShortMessage)msg;\r
+                       int msg_ch = 0;\r
+                       int msg_status = smsg.getStatus();\r
+                       if( MIDISpec.isChannelMessage(msg_status) ) {\r
+                               msg_status = smsg.getCommand();\r
+                               msg_ch = smsg.getChannel();\r
+                       }\r
+                       statusText.setValue( msg_status );\r
+                       channelText.setSelectedChannel( msg_ch );\r
+                       data1Text.setValue( smsg.getData1() );\r
+                       data2Text.setValue( smsg.getData2() );\r
+               }\r
+               else if( msg instanceof SysexMessage ) {\r
+                       SysexMessage sysex_msg = (SysexMessage)msg;\r
+                       statusText.setValue( sysex_msg.getStatus() );\r
+                       dataText.setValue( sysex_msg.getData() );\r
+               }\r
+               else if( msg instanceof MetaMessage ) {\r
+                       MetaMessage meta_msg = (MetaMessage)msg;\r
+                       int msg_type = meta_msg.getType();\r
+                       byte data[] = meta_msg.getData();\r
+                       statusText.setValue( 0xFF );\r
+                       data1Text.setValue( msg_type );\r
+                       switch( msg_type ) {\r
+                       case 0x51: tempoSelecter.setTempo( data ); break;\r
+                       case 0x58: timesigSelecter.setValue( data[0], data[1] ); break;\r
+                       case 0x59: keysigSelecter.setKey( new Music.Key(data) ); break;\r
+                       default: break;\r
+                       }\r
+                       if( MIDISpec.hasMetaText( msg_type ) ) {\r
+                               dataText.setString( new String(data) );\r
+                       }\r
+                       else {\r
+                               dataText.setValue( data );\r
+                       }\r
+                       updateVisible();\r
+               }\r
+       }\r
+       public boolean setNote( int ch, int note_no, int velocity ) {\r
+               channelText.setSelectedChannel(ch);\r
+               data1Text.setValue(note_no);\r
+               data2Text.setValue(velocity);\r
+               return true;\r
+       }\r
+       public boolean isNote() {\r
+               return isNote( statusText.getValue() );\r
+       }\r
+       public boolean isNote( int status ) {\r
+               int cmd = status & 0xF0;\r
+               return ( cmd == ShortMessage.NOTE_ON || cmd == ShortMessage.NOTE_OFF );\r
+       }\r
+       public boolean isNote( boolean note_on ) {\r
+               return isNote( note_on, statusText.getValue() );\r
+       }\r
+       public boolean isNote( boolean note_on, int status ) {\r
+               int cmd = status & 0xF0;\r
+               return (\r
+                               note_on && cmd == ShortMessage.NOTE_ON && data2Text.getValue() > 0\r
+                               ||\r
+                               !note_on && (\r
+                                               cmd == ShortMessage.NOTE_ON && data2Text.getValue() <= 0\r
+                                               ||\r
+                                               cmd == ShortMessage.NOTE_OFF\r
+                                               )\r
+                               );\r
+       }\r
+       public ShortMessage getPartnerMessage() {\r
+               ShortMessage sm = (ShortMessage)getMessage();\r
+               if( sm == null ) return null;\r
+               ShortMessage partner_sm;\r
+               if( isNote(true) ) { // NoteOn\r
+                       partner_sm = new ShortMessage();\r
+               try{\r
+                       partner_sm.setMessage(\r
+                                       ShortMessage.NOTE_OFF,\r
+                                       sm.getChannel(),\r
+                                       sm.getData1(), sm.getData2()\r
+                                       );\r
+               } catch( InvalidMidiDataException e ) {\r
+                       e.printStackTrace();\r
+                       return null;\r
+               }\r
+               return partner_sm;\r
+               }\r
+               else if( isNote(false) ) { // NoteOff\r
+                       partner_sm = new ShortMessage();\r
+                       try{\r
+                               partner_sm.setMessage(\r
+                                               ShortMessage.NOTE_ON,\r
+                                               sm.getChannel(),\r
+                                               sm.getData1() == 0 ? 100 : sm.getData1(),\r
+                                                               sm.getData2()\r
+                                               );\r
+                       } catch( InvalidMidiDataException e ) {\r
+                               e.printStackTrace();\r
+                               return null;\r
+                       }\r
+                       return partner_sm;\r
+               }\r
+               return null;\r
+       }\r
+}\r
+\r
+\r
+///////////////////////////////////////\r
+//\r
+// Hex value: [0x00 0x00 0x00 ... ]\r
+//\r
+///////////////////////////////////////\r
+class HexTextForm extends JPanel {\r
+       public JTextArea text_area;\r
+       public JLabel title_label;\r
+       public HexTextForm(String title) {\r
+               this(title,1,3);\r
+       }\r
+       public HexTextForm(String title, int rows, int columns) {\r
+               if( title != null ) add(title_label = new JLabel(title));\r
+               text_area = new JTextArea(rows, columns);\r
+               text_area.setLineWrap(true);\r
+               JScrollPane text_scroll_pane = new JScrollPane(text_area);\r
+               add(text_scroll_pane);\r
+               setLayout(new FlowLayout());\r
+       }\r
+       public String getString() {\r
+               return text_area.getText();\r
+       }\r
+       public byte[] getBytesFromString() {\r
+               return getString().getBytes();\r
+       }\r
+       public byte[] getBytes() {\r
+               String words[] = getString().trim().split(" +");\r
+               ArrayList<Integer> tmp_ba = new ArrayList<Integer>();\r
+               int i;\r
+               for( String w : words ) {\r
+                       if( w.length() == 0 ) continue;\r
+                       try {\r
+                               i = Integer.decode(w).intValue();\r
+                       } catch( NumberFormatException e ) {\r
+                               JOptionPane.showMessageDialog(\r
+                                               this,\r
+                                               w + " : is not a number",\r
+                                               "MIDI Chord Helper",\r
+                                               JOptionPane.ERROR_MESSAGE\r
+                                               );\r
+                               return null;\r
+                       }\r
+                       tmp_ba.add(i);\r
+               }\r
+               byte[] ba = new byte[tmp_ba.size()];\r
+               i = 0;\r
+               for( Integer b : tmp_ba ) {\r
+                       ba[i++] = (byte)( b.intValue() & 0xFF );\r
+               }\r
+               return ba;\r
+       }\r
+       public void setTitle( String str ) {\r
+               title_label.setText( str );\r
+       }\r
+       public void setString( String str ) {\r
+               text_area.setText( str );\r
+       }\r
+       public void setValue( int val ) {\r
+               text_area.setText( String.format( " 0x%02X", val ) );\r
+       }\r
+       public void setValue( byte val ) {\r
+               text_area.setText( String.format( " 0x%02X", val ) );\r
+       }\r
+       public void setValue( byte ba[] ) {\r
+               String str = "";\r
+               for( byte b : ba ) {\r
+                       str += String.format( " 0x%02X", b );\r
+               }\r
+               text_area.setText(str);\r
+       }\r
+       public void clear() { text_area.setText(""); }\r
+}\r
+\r
+///////////////////////////////////////\r
+//\r
+// Hex value: [0x00 0x00 0x00 ... ] v -> Select\r
+//\r
+///////////////////////////////////////\r
+class HexSelecter extends JPanel {\r
+       private JComboBox<String> comboBox = new JComboBox<String>();\r
+       private JLabel title;\r
+       public HexSelecter( String title ) {\r
+               if( title != null )\r
+                       add( this.title = new JLabel(title) );\r
+               add(comboBox);\r
+               setLayout(new FlowLayout());\r
+               comboBox.setEditable(true);\r
+               comboBox.setMaximumRowCount(16);\r
+       }\r
+       public JComboBox<String> getComboBox() {\r
+               return comboBox;\r
+       }\r
+       public void setTitle( String title ) {\r
+               this.title.setText(title);\r
+       }\r
+       public String getString() {\r
+               return (String)( comboBox.getSelectedItem() );\r
+       }\r
+       public int getValue() {\r
+               ArrayList<Integer> ia = getIntegerList();\r
+               return ia.size() == 0 ? -1 : ia.get(0);\r
+       }\r
+       public ArrayList<Integer> getIntegerList() {\r
+               String words[], str = getString();\r
+               if( str == null )\r
+                       words = new String[0];\r
+               else\r
+                       words = str.replaceAll( ":.*$", "" ).trim().split(" +");\r
+               int i;\r
+               ArrayList<Integer> ia = new ArrayList<Integer>();\r
+               for( String w : words ) {\r
+                       if( w.length() == 0 ) continue;\r
+                       try {\r
+                               i = Integer.decode(w).intValue();\r
+                       } catch( NumberFormatException e ) {\r
+                               JOptionPane.showMessageDialog(\r
+                                       this,\r
+                                       w + " : is not a number",\r
+                                       "MIDI Chord Helper",\r
+                                       JOptionPane.ERROR_MESSAGE\r
+                               );\r
+                               return null;\r
+                       }\r
+                       ia.add(i);\r
+               }\r
+               return ia;\r
+       }\r
+       public byte[] getBytes() {\r
+               ArrayList<Integer> ia = getIntegerList();\r
+               byte[] ba = new byte[ia.size()];\r
+               int i = 0;\r
+               for( Integer ib : ia ) {\r
+                       ba[i++] = (byte)( ib.intValue() & 0xFF );\r
+               }\r
+               return ba;\r
+       }\r
+       public void setValue( int val ) {\r
+               setValue( (byte)(val & 0xFF) );\r
+       }\r
+       public void setValue( byte val ) {\r
+               int n_item = comboBox.getItemCount();\r
+               String item;\r
+               for( int i=0; i<n_item; i++ ) {\r
+                       item = (String)( comboBox.getItemAt(i) );\r
+                       if( Integer.decode( item.trim().split(" +")[0] ).byteValue() == val ) {\r
+                               comboBox.setSelectedIndex(i);\r
+                               return;\r
+                       }\r
+               }\r
+               comboBox.setSelectedItem(String.format(" 0x%02X",val));\r
+       }\r
+       public void setValue( byte ba[] ) {\r
+               String str = "";\r
+               for( byte b : ba )\r
+                       str += String.format( " 0x%02X", b );\r
+               comboBox.setSelectedItem(str);\r
+       }\r
+       public void clear() {\r
+               comboBox.setSelectedItem("");\r
+       }\r
+}\r
+\r
+///////////////////////////////////////\r
+//\r
+// MIDI Channel Selecter (ComboBox or List)\r
+//\r
+///////////////////////////////////////\r
+interface MidiChannelComboBoxModel extends ComboBoxModel<Integer> {\r
+       int getSelectedChannel();\r
+       void setSelectedChannel(int channel);\r
+}\r
+class DefaultMidiChannelComboBoxModel extends DefaultComboBoxModel<Integer>\r
+       implements MidiChannelComboBoxModel\r
+{\r
+       public DefaultMidiChannelComboBoxModel() {\r
+               for( int ch = 0; ch < MIDISpec.MAX_CHANNELS ; ch++ )\r
+                       addElement(ch+1);\r
+       }\r
+       public int getSelectedChannel() {\r
+               return getIndexOf(getSelectedItem());\r
+       }\r
+       public void setSelectedChannel(int channel) {\r
+               setSelectedItem(getElementAt(channel));\r
+       }\r
+}\r
+class MidiChannelComboSelecter extends JPanel {\r
+       JComboBox<Integer> comboBox = new JComboBox<>();\r
+       public MidiChannelComboSelecter( String title ) {\r
+               this( title, new DefaultMidiChannelComboBoxModel() );\r
+       }\r
+       public MidiChannelComboSelecter(\r
+               String title, MidiChannelComboBoxModel model\r
+       ) {\r
+               setLayout(new FlowLayout());\r
+               if( title != null ) add( new JLabel(title) );\r
+               comboBox.setModel(model);\r
+               comboBox.setMaximumRowCount(16);\r
+               add(comboBox);\r
+       }\r
+       public JComboBox<Integer> getComboBox() {\r
+               return comboBox;\r
+       }\r
+       public MidiChannelComboBoxModel getModel() {\r
+               return (MidiChannelComboBoxModel)comboBox.getModel();\r
+       }\r
+       public int getSelectedChannel() {\r
+               return comboBox.getSelectedIndex();\r
+       }\r
+       public void setSelectedChannel(int channel) {\r
+               comboBox.setSelectedIndex(channel);\r
+       }\r
+}\r
+class MidiChannelButtonSelecter extends JList<Integer> {\r
+       private PianoKeyboard keyboard = null;\r
+       public MidiChannelButtonSelecter(MidiChannelComboBoxModel model) {\r
+               super(model);\r
+               setLayoutOrientation(HORIZONTAL_WRAP);\r
+               setVisibleRowCount(1);\r
+               setCellRenderer(new MyCellRenderer());\r
+               setSelectedIndex(model.getSelectedChannel());\r
+               model.addListDataListener(new ListDataListener() {\r
+                       public void contentsChanged(ListDataEvent e) {\r
+                               MidiChannelButtonSelecter.this.setSelectedIndex(\r
+                                               MidiChannelButtonSelecter.this.getModel().getSelectedChannel()\r
+                                               );\r
+                       }\r
+                       public void intervalAdded(ListDataEvent e) {}\r
+                       public void intervalRemoved(ListDataEvent e) {}\r
+               });\r
+               addListSelectionListener(new ListSelectionListener() {\r
+                       public void valueChanged(ListSelectionEvent e) {\r
+                               MidiChannelButtonSelecter.this.getModel().setSelectedChannel(\r
+                                               MidiChannelButtonSelecter.this.getSelectedIndex()\r
+                                               );\r
+                       }\r
+               });\r
+       }\r
+       public MidiChannelButtonSelecter(PianoKeyboard keyboard) {\r
+               this(keyboard.midiChComboboxModel);\r
+               setPianoKeyboard(keyboard);\r
+       }\r
+       public MidiChannelComboBoxModel getModel() {\r
+               return (MidiChannelComboBoxModel)(super.getModel());\r
+       }\r
+       public void setPianoKeyboard(PianoKeyboard keyboard) {\r
+               (this.keyboard = keyboard).midi_ch_button_selecter = this;\r
+       }\r
+       class MyCellRenderer extends JLabel implements ListCellRenderer<Integer> {\r
+               private boolean cellHasFocus = false;\r
+               public MyCellRenderer() {\r
+                       setOpaque(true);\r
+                       setHorizontalAlignment(CENTER);\r
+                       setSelectionBackground(Color.yellow);\r
+               }\r
+               public Component getListCellRendererComponent(\r
+                       JList<? extends Integer> list,\r
+                       Integer value, int index,\r
+                       boolean isSelected, boolean cellHasFocus\r
+               ) {\r
+                       this.cellHasFocus = cellHasFocus;\r
+                       setText(value.toString());\r
+                       if(isSelected) {\r
+                               setBackground(list.getSelectionBackground());\r
+                               setForeground(list.getSelectionForeground());\r
+                       } else {\r
+                               setBackground(\r
+                                               keyboard != null && keyboard.countKeyOn(index) > 0 ?\r
+                                                               Color.pink : list.getBackground()\r
+                                               );\r
+                               setForeground(list.getForeground());\r
+                       }\r
+                       setEnabled(list.isEnabled());\r
+                       setFont(list.getFont());\r
+                       return this;\r
+               }\r
+               protected void paintComponent(Graphics g) {\r
+                       super.paintComponent(g);\r
+                       if( cellHasFocus ) {\r
+                               g.setColor(Color.gray);\r
+                               g.drawRect(0, 0, this.getWidth() - 1, this.getHeight() - 1);\r
+                       }\r
+               }\r
+       }\r
+}\r
+\r
+////////////////////////////////////\r
+//\r
+// Tick Position\r
+//\r
+// Mesausre:[xxxx] Beat:[xx] ExTick:[xxx]\r
+//\r
+////////////////////////////////////\r
+class TickPositionModel implements ChangeListener {\r
+       private SequenceIndex seq_index;\r
+       private boolean is_changing = false;\r
+       public SpinnerNumberModel\r
+       tick_model, measure_model, beat_model, extra_tick_model;\r
+       //\r
+       // Constuctor\r
+       //\r
+       public TickPositionModel() {\r
+               tick_model = new SpinnerNumberModel(0L, 0L, 999999L, 1L);\r
+               tick_model.addChangeListener(this);\r
+               measure_model = new SpinnerNumberModel(1, 1, 9999, 1);\r
+               measure_model.addChangeListener(this);\r
+               beat_model = new SpinnerNumberModel(1, 1, 32, 1);\r
+               beat_model.addChangeListener(this);\r
+               extra_tick_model = new SpinnerNumberModel(0, 0, 4*960-1, 1);\r
+               extra_tick_model.addChangeListener(this);\r
+       }\r
+       // ChangeListener\r
+       //\r
+       public void stateChanged(ChangeEvent e) {\r
+               if( seq_index == null ) return;\r
+               Object src = e.getSource();\r
+               if( src == tick_model ) {\r
+                       is_changing = true;\r
+                       measure_model.setValue(\r
+                                       1 + seq_index.tickToMeasure(\r
+                                                       tick_model.getNumber().longValue()\r
+                                                       )\r
+                                       );\r
+                       beat_model.setValue( seq_index.last_beat + 1 );\r
+                       is_changing = false;\r
+                       extra_tick_model.setValue( seq_index.last_extra_tick );\r
+                       return;\r
+               }\r
+               if( is_changing ) return;\r
+               tick_model.setValue(\r
+                               seq_index.measureToTick(\r
+                                               measure_model.getNumber().intValue() - 1,\r
+                                               beat_model.getNumber().intValue() - 1,\r
+                                               extra_tick_model.getNumber().intValue()\r
+                                               )\r
+                               );\r
+       }\r
+       // Methods\r
+       //\r
+       public SequenceIndex getSequenceIndex() {\r
+               return seq_index;\r
+       }\r
+       public void setSequenceIndex( SequenceIndex seq_index ) {\r
+               this.seq_index = seq_index;\r
+               extra_tick_model.setMaximum( 4 * seq_index.getResolution() - 1 );\r
+       }\r
+       public long getTickPosition() {\r
+               return tick_model.getNumber().longValue();\r
+       }\r
+       public void setTickPosition( long tick ) {\r
+               tick_model.setValue(tick);\r
+       }\r
+}\r
+\r
+class TickPositionForm extends JPanel {\r
+       JSpinner\r
+       tick_spinner = new JSpinner(),\r
+       measure_spinner = new JSpinner(),\r
+       beat_spinner = new JSpinner(),\r
+       extra_tick_spinner = new JSpinner();\r
+\r
+       public TickPositionForm() {\r
+               setLayout(new GridLayout(2,4));\r
+               add( new JLabel() );\r
+               add( new JLabel() );\r
+               add( new JLabel("Measure:") );\r
+               add( new JLabel("Beat:") );\r
+               add( new JLabel("ExTick:") );\r
+               add( new JLabel("Tick position : ",JLabel.RIGHT) );\r
+               add( tick_spinner );\r
+               add( measure_spinner );\r
+               add( beat_spinner );\r
+               add( extra_tick_spinner );\r
+       }\r
+       public TickPositionForm( TickPositionModel tpm ) {\r
+               this(); setModel(tpm);\r
+       }\r
+       public void setModel( TickPositionModel tpm ) {\r
+               tick_spinner.setModel(tpm.tick_model);\r
+               measure_spinner.setModel(tpm.measure_model);\r
+               beat_spinner.setModel(tpm.beat_model);\r
+               extra_tick_spinner.setModel(tpm.extra_tick_model);\r
+       }\r
+}\r
+\r
+/**\r
+ * 音の長さフォーム\r
+ */\r
+class DurationForm extends JPanel implements ActionListener, ChangeListener {\r
+       class NoteIcon extends ButtonIcon {\r
+               public NoteIcon( int kind ) { super(kind); }\r
+               public int getDuration() {\r
+                       if(  ! isMusicalNote() ) return -1;\r
+                       int duration = (ppq * 4) >> getMusicalNoteValueIndex();\r
+                       if( isDottedMusicalNote() )\r
+                               duration += duration / 2;\r
+                       return duration;\r
+               }\r
+       }\r
+       class NoteRenderer extends JLabel implements ListCellRenderer<NoteIcon> {\r
+               public NoteRenderer() { setOpaque(true); }\r
+               public Component getListCellRendererComponent(\r
+                       JList<? extends NoteIcon> list,\r
+                       NoteIcon icon,\r
+                       int index,\r
+                       boolean isSelected,\r
+                       boolean cellHasFocus\r
+               ) {\r
+                       setIcon( icon );\r
+                       int duration = icon.getDuration();\r
+                       setText( duration < 0 ? null : ("" + duration) );\r
+                       setFont( list.getFont() );\r
+                       if (isSelected) {\r
+                               setBackground( list.getSelectionBackground() );\r
+                               setForeground( list.getSelectionForeground() );\r
+                       } else {\r
+                               setBackground( list.getBackground() );\r
+                               setForeground( list.getForeground() );\r
+                       }\r
+                       return this;\r
+               }\r
+       }\r
+       class NoteComboBox extends JComboBox<NoteIcon> {\r
+               public NoteComboBox() {\r
+                       setRenderer( new NoteRenderer() );\r
+                       addItem( new NoteIcon(ButtonIcon.EDIT_ICON) );\r
+                       addItem( new NoteIcon(ButtonIcon.WHOLE_NOTE_ICON) );\r
+                       addItem( new NoteIcon(ButtonIcon.DOTTED_HALF_NOTE_ICON) );\r
+                       addItem( new NoteIcon(ButtonIcon.HALF_NOTE_ICON) );\r
+                       addItem( new NoteIcon(ButtonIcon.DOTTED_QUARTER_NOTE_ICON) );\r
+                       addItem( new NoteIcon(ButtonIcon.QUARTER_NOTE_ICON) );\r
+                       addItem( new NoteIcon(ButtonIcon.DOTTED_8TH_NOTE_ICON) );\r
+                       addItem( new NoteIcon(ButtonIcon.A8TH_NOTE_ICON) );\r
+                       addItem( new NoteIcon(ButtonIcon.DOTTED_16TH_NOTE_ICON) );\r
+                       addItem( new NoteIcon(ButtonIcon.A16TH_NOTE_ICON) );\r
+                       addItem( new NoteIcon(ButtonIcon.DOTTED_32ND_NOTE_ICON) );\r
+                       addItem( new NoteIcon(ButtonIcon.A32ND_NOTE_ICON) );\r
+                       addItem( new NoteIcon(ButtonIcon.A64TH_NOTE_ICON) );\r
+                       setMaximumRowCount(16);\r
+                       setSelectedIndex(5);\r
+               }\r
+               public int getDuration() {\r
+                       NoteIcon icon = (NoteIcon)getSelectedItem();\r
+                       return icon==null ? -1 : icon.getDuration();\r
+               }\r
+               public void setDuration(int duration) {\r
+                       int n_items = getItemCount();\r
+                       for( int i = 1; i < n_items; i++ ) {\r
+                               NoteIcon icon = getItemAt(i);\r
+                               int icon_duration = icon.getDuration();\r
+                               if( icon_duration < 0 || icon_duration != duration )\r
+                                       continue;\r
+                               setSelectedItem(icon);\r
+                               return;\r
+                       }\r
+                       setSelectedIndex(0);\r
+               }\r
+       }\r
+       class DurationModel extends SpinnerNumberModel {\r
+               public DurationModel() { super( ppq, 1, ppq*4*4, 1 ); }\r
+               public void setDuration( int value ) {\r
+                       setValue( new Integer(value) );\r
+               }\r
+               public int getDuration() {\r
+                       return getNumber().intValue();\r
+               }\r
+               public void setPPQ( int ppq ) {\r
+                       setMaximum( ppq*4*4 );\r
+                       setDuration( ppq );\r
+               }\r
+       }\r
+       DurationModel model;\r
+       JSpinner spinner;\r
+       NoteComboBox note_combo;\r
+       JLabel title_label, unit_label;\r
+       private int ppq = 960;\r
+       //\r
+       public DurationForm() {\r
+               (model = new DurationModel()).addChangeListener(this);\r
+               (note_combo = new NoteComboBox()).addActionListener(this);\r
+               add( title_label = new JLabel("Duration:") );\r
+               add( note_combo );\r
+               add( spinner = new JSpinner( model ) );\r
+               add( unit_label = new JLabel("[Ticks]") );\r
+       }\r
+       // ActionListener\r
+       //\r
+       public void actionPerformed(ActionEvent e) {\r
+               int duration = note_combo.getDuration();\r
+               if( duration < 0 ) return;\r
+               model.setDuration( duration );\r
+       }\r
+       // ChangeListener\r
+       //\r
+       public void stateChanged(ChangeEvent e) {\r
+               note_combo.setDuration( model.getDuration() );\r
+       }\r
+       // Methods\r
+       //\r
+       public void setEnabled( boolean enabled ) {\r
+               super.setEnabled(enabled);\r
+               title_label.setEnabled(enabled);\r
+               spinner.setEnabled(enabled);\r
+               note_combo.setEnabled(enabled);\r
+               unit_label.setEnabled(enabled);\r
+       }\r
+       public void setPPQ( int ppq ) {\r
+               model.setPPQ( this.ppq = ppq );\r
+       }\r
+       public int getDuration() {\r
+               return model.getDuration();\r
+       }\r
+       public void setDuration( int duration ) {\r
+               model.setDuration(duration);\r
+       }\r
+}\r
+\r
+//////////////////////////////\r
+//\r
+//  Tempo in QPM\r
+//\r
+//////////////////////////////\r
+class TempoSelecter extends JPanel implements MouseListener {\r
+       JSpinner                tempo_spinner;\r
+       SpinnerNumberModel      tempoSpinnerModel;\r
+       JLabel          tempo_label, tempo_value_label;\r
+       private boolean editable;\r
+       static final int        DEFAULT_QPM = 120;\r
+       private long    prev_beat_us_pos = 0;\r
+\r
+       public TempoSelecter() {\r
+               String tool_tip = "Tempo in quatrers per minute - テンポ(1分あたりの四分音符の数)";\r
+               tempo_label = new JLabel(\r
+                       "=",\r
+                       new ButtonIcon( ButtonIcon.QUARTER_NOTE_ICON ),\r
+                       JLabel.CENTER\r
+               );\r
+               tempo_label.setVerticalAlignment( JLabel.CENTER );\r
+               tempoSpinnerModel = new SpinnerNumberModel(DEFAULT_QPM, 1, 999, 1);\r
+               tempo_spinner = new JSpinner( tempoSpinnerModel );\r
+               tempo_spinner.setToolTipText( tool_tip );\r
+               tempo_value_label = new JLabel( ""+DEFAULT_QPM );\r
+               tempo_value_label.setToolTipText( tool_tip );\r
+               setLayout( new BoxLayout(this,BoxLayout.X_AXIS) );\r
+               add( tempo_label );\r
+               add( Box.createHorizontalStrut(5) );\r
+               add( tempo_spinner );\r
+               add( tempo_value_label );\r
+               setEditable(true);\r
+               tempo_label.addMouseListener(this);\r
+       }\r
+       //\r
+       // MouseListener\r
+       //\r
+       public void mousePressed(MouseEvent e) {\r
+               Component obj = e.getComponent();\r
+               if( obj == tempo_label && isEditable() ) {\r
+                       //\r
+                       // Adjust tempo by interval time between two clicks\r
+                       //\r
+                       long current_us = System.nanoTime()/1000;\r
+                       // midi_ch_selecter.noteOn( 9, 37, 100 );\r
+                       long interval_us = current_us - prev_beat_us_pos;\r
+                       prev_beat_us_pos = current_us;\r
+                       if( interval_us < 2000000L /* Shorter than 2 sec only */ ) {\r
+                               int tempo_in_bpm = (int)(240000000L / interval_us) >> 2; //  n/4拍子の場合のみを想定\r
+                       int old_tempo_in_bpm = getTempoInQpm();\r
+                       setTempo( ( tempo_in_bpm + old_tempo_in_bpm * 2 ) / 3 );\r
+                       }\r
+               }\r
+       }\r
+       public void mouseReleased(MouseEvent e) { }\r
+       public void mouseEntered(MouseEvent e) { }\r
+       public void mouseExited(MouseEvent e) { }\r
+       public void mouseClicked(MouseEvent e) { }\r
+       //\r
+       // Methods\r
+       //\r
+       public boolean isEditable() {\r
+               return editable;\r
+       }\r
+       public void setEditable( boolean editable ) {\r
+               this.editable = editable;\r
+               tempo_spinner.setVisible( editable );\r
+               tempo_value_label.setVisible( !editable );\r
+               if( !editable ) {\r
+                       // Copy spinner's value to label\r
+                       tempo_value_label.setText(\r
+                               ""+tempoSpinnerModel.getNumber().intValue()\r
+                       );\r
+               }\r
+               tempo_label.setToolTipText(\r
+                       editable ?\r
+                       "Click rhythmically to adjust tempo - ここをクリックしてリズムをとるとテンポを合わせられます"\r
+                       : null\r
+               );\r
+       }\r
+       public int getTempoInQpm() {\r
+               return tempoSpinnerModel.getNumber().intValue();\r
+       }\r
+       public byte[] getTempoByteArray() {\r
+               return MIDISpec.qpmTempoToByteArray( getTempoInQpm() );\r
+       }\r
+       public void setTempo( int qpm ) {\r
+               tempoSpinnerModel.setValue(new Integer(qpm));\r
+               tempo_value_label.setText(""+qpm);\r
+       }\r
+       public void setTempo( byte msgdata[] ) {\r
+               setTempo( MIDISpec.byteArrayToQpmTempo( msgdata ) );\r
+       }\r
+       public void clear() { setTempo( DEFAULT_QPM ); }\r
+}\r
+\r
+/////////////////////////////\r
+//\r
+// Time Signature - 拍子選択\r
+//\r
+/////////////////////////////\r
+class TimeSignatureLabel extends JLabel {\r
+       private byte upper = -1;\r
+       private byte lower_index = -1;\r
+       public void setTimeSignature(byte upper, byte lower_index) {\r
+               if( this.upper == upper && this.lower_index == lower_index ) {\r
+                       return;\r
+               }\r
+               setText(\r
+                               "<html><font size=\"+1\">" +\r
+                                               upper + "/" + (1 << lower_index) +\r
+                                               "</font></html>"\r
+                               );\r
+       }\r
+}\r
+class TimeSignatureSelecter extends JPanel {\r
+       SpinnerNumberModel upperTimesigSpinnerModel;\r
+       JSpinner upper_timesig_spinner;\r
+       JComboBox<String> lowerTimesigCombobox;\r
+       TimeSignatureLabel      timesig_value_label;\r
+       private boolean editable;\r
+\r
+       public TimeSignatureSelecter() {\r
+               upperTimesigSpinnerModel = new SpinnerNumberModel( 4, 1, 32, 1 );\r
+               upper_timesig_spinner = new JSpinner( upperTimesigSpinnerModel );\r
+               upper_timesig_spinner.setToolTipText("Time signature (upper digit) - 拍子の分子");\r
+               lowerTimesigCombobox = new JComboBox<String>();\r
+               lowerTimesigCombobox.setToolTipText("Time signature (lower digit) - 拍子の分母");\r
+               for( int i=0; i<6; i++ ) {\r
+                       lowerTimesigCombobox.addItem( "/" + (1<<i) );\r
+               }\r
+               timesig_value_label = new TimeSignatureLabel();\r
+               timesig_value_label.setToolTipText("Time signature - 拍子");\r
+               lowerTimesigCombobox.setSelectedIndex(2);\r
+               //\r
+               add( upper_timesig_spinner );\r
+               add( lowerTimesigCombobox );\r
+               add( timesig_value_label );\r
+               setEditable(true);\r
+       }\r
+       public void clear() {\r
+               upperTimesigSpinnerModel.setValue(4);\r
+               lowerTimesigCombobox.setSelectedIndex(2);\r
+       }\r
+       public int getUpperValue() {\r
+               return upperTimesigSpinnerModel.getNumber().intValue();\r
+       }\r
+       public byte getUpperByte() {\r
+               return upperTimesigSpinnerModel.getNumber().byteValue();\r
+       }\r
+       public int getLowerValueIndex() {\r
+               return lowerTimesigCombobox.getSelectedIndex();\r
+       }\r
+       public byte getLowerByte() {\r
+               return (byte)getLowerValueIndex();\r
+       }\r
+       public byte[] getByteArray() {\r
+               byte[] data = new byte[4];\r
+               data[0] = getUpperByte();\r
+               data[1] = getLowerByte();\r
+               data[2] = (byte)( 96 >> getLowerValueIndex() );\r
+               data[3] = 8;\r
+               return data;\r
+       }\r
+       public void setValue( byte upper, byte lower_index ) {\r
+               upperTimesigSpinnerModel.setValue( upper );\r
+               lowerTimesigCombobox.setSelectedIndex( lower_index );\r
+               timesig_value_label.setTimeSignature( upper, lower_index );\r
+       }\r
+       public void setValue( byte[] data ) {\r
+               setValue( data[0], data[1] );\r
+       }\r
+       public boolean isEditable() { return editable; }\r
+       public void setEditable( boolean editable ) {\r
+               this.editable = editable;\r
+               upper_timesig_spinner.setVisible(editable);\r
+               lowerTimesigCombobox.setVisible(editable);\r
+               timesig_value_label.setVisible(!editable);\r
+               if( !editable ) {\r
+                       timesig_value_label.setTimeSignature(\r
+                                       getUpperByte(), getLowerByte()\r
+                                       );\r
+               }\r
+       }\r
+}\r
+\r
+/////////////////////////////\r
+//\r
+// Key Signature - 調性選択\r
+//\r
+/////////////////////////////\r
+class KeySignatureSelecter extends JPanel implements ActionListener {\r
+       JComboBox<String> keysigCombobox = new JComboBox<String>() {\r
+               {\r
+                       String str;\r
+                       Music.Key key;\r
+                       for( int i = -7 ; i <= 7 ; i++ ) {\r
+                               str = (key = new Music.Key(i)).toString();\r
+                               if( i != 0 ) {\r
+                                       str = key.signature() + " : " + str ;\r
+                               }\r
+                               addItem(str);\r
+                       }\r
+                       setMaximumRowCount(15);\r
+               }\r
+       };\r
+       JCheckBox minor_checkbox = null;\r
+\r
+       public KeySignatureSelecter() {\r
+               this(true);\r
+       }\r
+       public KeySignatureSelecter(boolean use_minor_checkbox) {\r
+               add(new JLabel("Key:"));\r
+               add(keysigCombobox);\r
+               if( use_minor_checkbox ) {\r
+                       add( minor_checkbox = new JCheckBox("minor") );\r
+                       minor_checkbox.addActionListener(this);\r
+               }\r
+               keysigCombobox.addActionListener(this);\r
+               clear();\r
+       }\r
+       // ActionListener\r
+       //\r
+       public void actionPerformed(ActionEvent e) {\r
+               updateToolTipText();\r
+       }\r
+       //\r
+       // Methods\r
+       //\r
+       private void updateToolTipText() {\r
+               Music.Key key = getKey();\r
+               keysigCombobox.setToolTipText(\r
+                       "Key: " + key.toStringIn( Music.SymbolLanguage.NAME )\r
+                       + " "  + key.toStringIn( Music.SymbolLanguage.IN_JAPANESE )\r
+                       + " (" + key.signatureDescription() + ")"\r
+               );\r
+       }\r
+       public void clear() {\r
+               setKey( new Music.Key("C") );\r
+       }\r
+       public void setKey( Music.Key key ) {\r
+               if( key == null ) {\r
+                       clear();\r
+                       return;\r
+               }\r
+               keysigCombobox.setSelectedIndex( key.toCo5() + 7 );\r
+               if( minor_checkbox == null )\r
+                       return;\r
+               switch( key.majorMinor() ) {\r
+               case Music.Key.MINOR : minor_checkbox.setSelected(true); break;\r
+               case Music.Key.MAJOR : minor_checkbox.setSelected(false); break;\r
+               }\r
+       }\r
+       public Music.Key getKey() {\r
+               int minor = (\r
+                       minor_checkbox == null ? Music.Key.MAJOR_OR_MINOR :\r
+                       isMinor() ? Music.Key.MINOR : Music.Key.MAJOR\r
+               );\r
+               return new Music.Key(getKeyCo5(),minor);\r
+       }\r
+       public int getKeyCo5() {\r
+               return keysigCombobox.getSelectedIndex() - 7;\r
+       }\r
+       public boolean isMinor() {\r
+               return ( minor_checkbox != null && minor_checkbox.isSelected() );\r
+       }\r
+}\r
+class KeySignatureLabel extends JLabel {\r
+       private Music.Key key;\r
+       public KeySignatureLabel() { clear(); }\r
+       public Music.Key getKey() { return key; }\r
+       public void setKeySignature( Music.Key key ) {\r
+               this.key = key;\r
+               if( key == null ) {\r
+                       setText("Key:C");\r
+                       setToolTipText("Key: Unknown");\r
+                       setEnabled(false);\r
+                       return;\r
+               }\r
+               setText( "key:" + key.toString() );\r
+               setToolTipText(\r
+                               "Key: " + key.toStringIn(Music.SymbolLanguage.NAME)\r
+                               + " "  + key.toStringIn(Music.SymbolLanguage.IN_JAPANESE)\r
+                               + " (" + key.signatureDescription() + ")"\r
+                               );\r
+               setEnabled(true);\r
+       }\r
+       public void clear() { setKeySignature( (Music.Key)null ); }\r
+}\r
+\r
+///////////////////////////////////////////////\r
+//\r
+// Velocity\r
+//\r
+///////////////////////////////////////////////\r
+class VelocityModel extends DefaultBoundedRangeModel {\r
+       public VelocityModel() { super( 64, 0, 0, 127 ); }\r
+}\r
+class VelocitySelecter extends JPanel {\r
+       private static final String     LABEL_PREFIX = "Velocity=";\r
+       public JSlider slider = null;\r
+       public JLabel label;\r
+       public VelocitySelecter( VelocityModel model ) {\r
+               slider = new JSlider(model);\r
+               slider.addChangeListener(new ChangeListener() {\r
+                       public void stateChanged(ChangeEvent e) {\r
+                               label.setText( LABEL_PREFIX + getValue() );\r
+                       }\r
+               });\r
+               slider.setToolTipText("Velocity");\r
+               label = new JLabel( LABEL_PREFIX + model.getValue(), Label.RIGHT );\r
+               label.setToolTipText("Velocity");\r
+               setLayout( new BoxLayout( this, BoxLayout.X_AXIS ) );\r
+               add(label);\r
+               add(slider);\r
+       }\r
+       public void setBackground(Color c) {\r
+               super.setBackground(c);\r
+               if( slider != null ) slider.setBackground(c);\r
+       }\r
+       public int getValue() {\r
+               return slider.getValue();\r
+       }\r
+       public void setValue(int velocity) {\r
+               slider.setValue(velocity);\r
+       }\r
+}\r
+\r
+///////////////////////////////////////////////\r
+//\r
+// MIDI Instrument (Program) - 音色選択\r
+//\r
+///////////////////////////////////////////////\r
+class MidiProgramSelecter extends JComboBox<String> {\r
+       private int family;\r
+       private MidiProgramFamilySelecter family_selecter = null;\r
+       public MidiProgramSelecter() {\r
+               setFamily(-1);\r
+       }\r
+       public void setFamilySelecter( MidiProgramFamilySelecter mpfs ) {\r
+               family_selecter = mpfs;\r
+       }\r
+       public void setFamily( int family ) {\r
+               int program_no = getProgram();\r
+               this.family = family;\r
+               removeAllItems();\r
+               if( family < 0 ) {\r
+                       setMaximumRowCount(16);\r
+                       for( int i=0; i < MIDISpec.instrument_names.length; i++ ) {\r
+                               addItem(i+": " + MIDISpec.instrument_names[i]);\r
+                       }\r
+                       setSelectedIndex(program_no);\r
+               }\r
+               else {\r
+                       setMaximumRowCount(8);\r
+                       for( int i=0; i < 8; i++ ) {\r
+                               program_no = i + family * 8;\r
+                               addItem( program_no + ": " + MIDISpec.instrument_names[program_no] );\r
+                       }\r
+                       setSelectedIndex(0);\r
+               }\r
+       }\r
+       public int getProgram() {\r
+               int program_no = getSelectedIndex();\r
+               if( family > 0 && program_no >= 0 ) program_no += family * 8;\r
+               return program_no;\r
+       }\r
+       public String getProgramName() { return (String)( getSelectedItem() ); }\r
+       public void setProgram( int program_no ) {\r
+               if( getItemCount() == 0 ) return; // To ignore event triggered by removeAllItems()\r
+               if( family >= 0 && program_no >= 0 && family == program_no / 8 ) {\r
+                       setSelectedIndex(program_no % 8);\r
+               }\r
+               else {\r
+                       if( family >= 0 ) setFamily(-1);\r
+                       if( family_selecter != null ) family_selecter.setSelectedIndex(0);\r
+                       if( program_no < getItemCount() ) setSelectedIndex(program_no);\r
+               }\r
+       }\r
+}\r
+class MidiProgramFamilySelecter extends JComboBox<String> implements ActionListener {\r
+       private MidiProgramSelecter program_selecter = null;\r
+       public MidiProgramFamilySelecter() { this(null); }\r
+       public MidiProgramFamilySelecter( MidiProgramSelecter mps ) {\r
+               program_selecter = mps;\r
+               setMaximumRowCount(17);\r
+               addItem("Program:");\r
+               for( int i=0; i < MIDISpec.instrument_family_names.length; i++ ) {\r
+                       addItem( (i*8) + "-" + (i*8+7) + ": " + MIDISpec.instrument_family_names[i] );\r
+               }\r
+               setSelectedIndex(0);\r
+               addActionListener(this);\r
+       }\r
+       public void actionPerformed(ActionEvent evt) {\r
+               if( program_selecter == null ) return;\r
+               int i = getSelectedIndex();\r
+               program_selecter.setFamily( i < 0 ? i : i-1 );\r
+       }\r
+       public int getProgram() {\r
+               int i = getSelectedIndex();\r
+               if( i <= 0 ) return -1;\r
+               else return (i-1)*8;\r
+       }\r
+       public String getProgramFamilyName() { return (String)( getSelectedItem() ); }\r
+       public void setProgram( int program_no ) {\r
+               if( program_no < 0 ) program_no = 0;\r
+               else program_no = program_no / 8 + 1;\r
+               setSelectedIndex( program_no );\r
+       }\r
+}\r
diff --git a/src/MIDISequencer.java b/src/MIDISequencer.java
new file mode 100644 (file)
index 0000000..96da953
--- /dev/null
@@ -0,0 +1,480 @@
+\r
+import java.awt.Color;\r
+import java.awt.event.ActionEvent;\r
+import java.awt.event.ActionListener;\r
+\r
+import javax.sound.midi.InvalidMidiDataException;\r
+import javax.sound.midi.MetaEventListener;\r
+import javax.sound.midi.MetaMessage;\r
+import javax.sound.midi.Sequencer;\r
+import javax.swing.AbstractAction;\r
+import javax.swing.Action;\r
+import javax.swing.BoundedRangeModel;\r
+import javax.swing.BoxLayout;\r
+import javax.swing.DefaultBoundedRangeModel;\r
+import javax.swing.Icon;\r
+import javax.swing.JComboBox;\r
+import javax.swing.JLabel;\r
+import javax.swing.JPanel;\r
+import javax.swing.JSlider;\r
+import javax.swing.event.ChangeEvent;\r
+import javax.swing.event.ChangeListener;\r
+import javax.swing.event.EventListenerList;\r
+\r
+/**\r
+ * 時間または小節の表示形式\r
+ */\r
+enum TimeLabelFormat {\r
+       /**\r
+        * 時間位置\r
+        */\r
+       POSITION,\r
+       /**\r
+        * 時間の長さ\r
+        */\r
+       LENGTH {\r
+               @Override\r
+               public String toTimeString(int sec) {\r
+                       return "/"+super.toTimeString(sec);\r
+               }\r
+       };\r
+       /**\r
+        * この形式で時間の文字列を返します。\r
+        * @param sec 時間の秒数\r
+        * @return この形式での時間の文字列\r
+        */\r
+       public String toTimeString(int sec) {\r
+               return String.format("%02d:%02d", sec/60, sec%60);\r
+       }\r
+}\r
+\r
+/**\r
+ * シーケンサの現在位置(分:秒)を表示するビュー\r
+ */\r
+class TimeIndicator extends JPanel {\r
+       private TimeLabel timePositionLabel;\r
+       private TimeLabel timeLengthLabel;\r
+       private SequencerTimeRangeModel model;\r
+       public TimeIndicator() {\r
+               timePositionLabel = new TimeLabel(TimeLabelFormat.POSITION);\r
+               timeLengthLabel = new TimeLabel(TimeLabelFormat.LENGTH);\r
+               setLayout(new BoxLayout(this, BoxLayout.X_AXIS));\r
+               add(timePositionLabel);\r
+               add(timeLengthLabel);\r
+       }\r
+       public TimeIndicator(SequencerTimeRangeModel model) {\r
+               this();\r
+               (this.model = model).addChangeListener(\r
+                       new ChangeListener() {\r
+                               public void stateChanged(ChangeEvent e) {\r
+                                       SequencerTimeRangeModel model = TimeIndicator.this.model;\r
+                                       timeLengthLabel.setTimeInSecond(model.getMaximum()/1000);\r
+                                       timePositionLabel.setTimeInSecond(model.getValue()/1000);\r
+                               }\r
+                       }\r
+               );\r
+       }\r
+}\r
+\r
+/**\r
+ * 時間(分:秒)表示ラベル\r
+ */\r
+class TimeLabel extends JLabel {\r
+       /**\r
+        * 時間表示の形式\r
+        */\r
+       private TimeLabelFormat formatType;\r
+       /**\r
+        * 時間の値(秒)\r
+        */\r
+       private int valueInSec = -1;\r
+       /**\r
+        * 時間表示ラベルを構築します。\r
+        * @param formatType 表示形式\r
+        */\r
+       public TimeLabel(TimeLabelFormat formatType) {\r
+               super();\r
+               if( (this.formatType = formatType) == TimeLabelFormat.POSITION ) {\r
+                       float largePointSize = getFont().getSize2D() + 4;\r
+                       setFont( getFont().deriveFont(largePointSize) );\r
+                       setForeground( new Color(0x80,0x00,0x00) );\r
+                       setToolTipText("Time position - 現在位置(分:秒)");\r
+               }\r
+               else {\r
+                       setToolTipText("Time length - 曲の長さ(分:秒)");\r
+               }\r
+               setText(formatType.toTimeString(0));\r
+       }\r
+       /**\r
+        * 時間の値をマイクロ秒単位で設定します。\r
+        * @param us マイクロ秒単位の時間\r
+        */\r
+       public void setTimeInMicrosecond(long us) {\r
+               setTimeInSecond( (int)(us/1000000) );\r
+       }\r
+       /**\r
+        * 時間の値を秒単位で設定します。\r
+        * @param sec 秒単位の時間\r
+        */\r
+       public void setTimeInSecond(int sec) {\r
+               if( valueInSec == sec )\r
+                       return;\r
+               if( (valueInSec = sec) < 0 )\r
+                       setText(null);\r
+               else\r
+                       setText(formatType.toTimeString(sec));\r
+       }\r
+}\r
+\r
+/**\r
+ * 小節表示ビュー\r
+ */\r
+class MeasureIndicator extends JPanel {\r
+       private SequencerTimeRangeModel model;\r
+       private MeasureLabel measurePositionLabel;\r
+       private MeasureLabel measureLengthLabel;\r
+       public MeasureIndicator() {\r
+               measurePositionLabel = new MeasureLabel(TimeLabelFormat.POSITION);\r
+               measureLengthLabel = new MeasureLabel(TimeLabelFormat.LENGTH);\r
+               setLayout( new BoxLayout( this, BoxLayout.X_AXIS ) );\r
+               add( measurePositionLabel );\r
+               add( measureLengthLabel );\r
+       }\r
+       public MeasureIndicator(SequencerTimeRangeModel model) {\r
+               this();\r
+               (this.model = model).addChangeListener(new ChangeListener() {\r
+                       @Override\r
+                       public void stateChanged(ChangeEvent e) {\r
+                               SequencerTimeRangeModel model = MeasureIndicator.this.model;\r
+                               Sequencer sequencer = model.deviceManager.getSequencer();\r
+                               MidiSequenceModel seqModel =\r
+                                       model.deviceManager.timeRangeModel.getSequenceModel();\r
+                               SequenceIndex seqIndex = (\r
+                                       seqModel == null ? null : seqModel.getSequenceIndex()\r
+                               );\r
+                               if( ! sequencer.isRunning() || sequencer.isRecording() ) {\r
+                                       measureLengthLabel.setMeasure(\r
+                                               seqIndex == null ? 0 : seqIndex.tickToMeasure(\r
+                                                       sequencer.getTickLength()\r
+                                               )\r
+                                       );\r
+                               }\r
+                               if( seqIndex == null ) {\r
+                                       measurePositionLabel.setMeasure( 0, 0 );\r
+                               }\r
+                               else {\r
+                                       int measurePosition = seqIndex.tickToMeasure(\r
+                                               sequencer.getTickPosition()\r
+                                       );\r
+                                       measurePositionLabel.setMeasure(\r
+                                               measurePosition, seqIndex.last_beat\r
+                                       );\r
+                               }\r
+                       }\r
+               });\r
+       }\r
+}\r
+/**\r
+ * 小節表示ラベル\r
+ */\r
+class MeasureLabel extends JLabel {\r
+       private TimeLabelFormat formatType;\r
+       private int measure = -1;\r
+       private int beat = 0;\r
+       public MeasureLabel(TimeLabelFormat formatType) {\r
+               if( (this.formatType = formatType) == TimeLabelFormat.POSITION ) {\r
+                       float large_point_size = getFont().getSize2D() + 4;\r
+                       setFont( getFont().deriveFont(large_point_size) );\r
+                       setForeground( new Color(0x80,0x00,0x00) );\r
+                       setText( "0001:01" );\r
+                       setToolTipText("Measure:beat position - 何小節目:何拍目");\r
+               }\r
+               else {\r
+                       setText( "/0000" );\r
+                       setToolTipText("Measure length - 小節の数");\r
+               }\r
+       }\r
+       public void setMeasure(int measure) {\r
+               setMeasure(measure,0);\r
+       }\r
+       public void setMeasure(int measure, int beat) {\r
+               if( this.measure == measure && this.beat == beat ) {\r
+                       return;\r
+               }\r
+               this.beat = beat;\r
+               if( (this.measure = measure) < 0 )\r
+                       setText( null );\r
+               else if( formatType == TimeLabelFormat.LENGTH )\r
+                       setText( String.format("/%04d", measure) );\r
+               else\r
+                       setText( String.format("%04d:%02d", measure+1, beat+1) );\r
+       }\r
+}\r
+\r
+/**\r
+ * シーケンサーの再生スピード調整モデル\r
+ */\r
+class SpeedSliderModel extends DefaultBoundedRangeModel implements ChangeListener {\r
+       private Sequencer sequencer;\r
+       public SpeedSliderModel( Sequencer sequencer ) {\r
+               super( 0, 0, -7, 7 );\r
+               this.sequencer = sequencer;\r
+               addChangeListener(this);\r
+       }\r
+       public void stateChanged(ChangeEvent e) {\r
+               int val = getValue();\r
+               sequencer.setTempoFactor((float)(\r
+                       val == 0 ? 1.0 : Math.pow( 2.0, ((double)val)/12.0 )\r
+               ));\r
+       }\r
+}\r
+\r
+/**\r
+ * シーケンサーの再生スピード調整スライダビュー\r
+ */\r
+class SpeedSlider extends JPanel implements ActionListener {\r
+       static String items[] = {\r
+               "x 1.0",\r
+               "x 1.5",\r
+               "x 2",\r
+               "x 4",\r
+               "x 8",\r
+               "x 16",\r
+       };\r
+       JSlider slider;\r
+       JLabel title_label;\r
+       JComboBox<String> scale_combo_box;\r
+       public SpeedSlider( SpeedSliderModel model ) {\r
+               add( title_label = new JLabel("Speed:") );\r
+               add( slider = new JSlider(model) );\r
+               add( scale_combo_box = new JComboBox<String>(items) );\r
+               scale_combo_box.addActionListener(this);\r
+               slider.setPaintTicks(true);\r
+               slider.setMajorTickSpacing(12);\r
+               slider.setMinorTickSpacing(1);\r
+               slider.setVisible(false);\r
+       }\r
+       public void actionPerformed(ActionEvent e) {\r
+               int index = scale_combo_box.getSelectedIndex();\r
+               SpeedSliderModel model = (SpeedSliderModel)slider.getModel();\r
+               if( index == 0 ) {\r
+                       model.setValue(0);\r
+                       slider.setVisible(false);\r
+                       title_label.setVisible(true);\r
+               }\r
+               else {\r
+                       int max_val = ( index == 1 ? 7 : (index-1)*12 );\r
+                       model.setMinimum(-max_val);\r
+                       model.setMaximum(max_val);\r
+                       slider.setMajorTickSpacing( index == 1 ? 7 : 12 );\r
+                       slider.setMinorTickSpacing( index > 3 ? 12 : 1 );\r
+                       slider.setVisible(true);\r
+                       title_label.setVisible(false);\r
+               }\r
+       }\r
+}\r
+\r
+/**\r
+ * 時間範囲データモデル\r
+ */\r
+class SequencerTimeRangeModel implements BoundedRangeModel {\r
+       static final int INTERVAL_MS = 20;\r
+       MidiDeviceManager deviceManager;\r
+       private Sequencer sequencer;\r
+       javax.swing.Timer       timer;\r
+       private boolean valueIsAdjusting = false;\r
+       private EventListenerList listenerList = new EventListenerList();\r
+       public StartStopAction startStopAction = new StartStopAction();\r
+       class StartStopAction extends AbstractAction {\r
+               Icon play_icon = new ButtonIcon(ButtonIcon.PLAY_ICON);\r
+               Icon pause_icon = new ButtonIcon(ButtonIcon.PAUSE_ICON);\r
+               {\r
+                       putValue(\r
+                               SHORT_DESCRIPTION,\r
+                               "Start/Stop recording or playing - 録音または再生の開始/停止"\r
+                       );\r
+                       putValue( LARGE_ICON_KEY, play_icon );\r
+                       putValue( SELECTED_KEY, false );\r
+               }\r
+               public void actionPerformed(ActionEvent event) {\r
+                       if( timer.isRunning() ) stop(); else start();\r
+               }\r
+               public void setRunning(boolean is_running) {\r
+                       putValue( LARGE_ICON_KEY, is_running ? pause_icon : play_icon );\r
+                       putValue( SELECTED_KEY, is_running );\r
+               }\r
+       }\r
+       public Action move_backward_action = new AbstractAction() {\r
+               {\r
+                       putValue( SHORT_DESCRIPTION, "Move backward 1 measure - 1小節戻る" );\r
+                       putValue( LARGE_ICON_KEY, new ButtonIcon(ButtonIcon.BACKWARD_ICON) );\r
+               }\r
+               public void actionPerformed( ActionEvent event ) {\r
+                       moveMeasure(-1);\r
+               }\r
+       };\r
+       public Action move_forward_action = new AbstractAction() {\r
+               {\r
+                       putValue( SHORT_DESCRIPTION, "Move forward 1 measure - 1小節進む" );\r
+                       putValue( LARGE_ICON_KEY, new ButtonIcon(ButtonIcon.FORWARD_ICON) );\r
+               }\r
+               public void actionPerformed( ActionEvent event ) {\r
+                       moveMeasure(1);\r
+               }\r
+       };\r
+       public Action toggle_repeat_action = new AbstractAction() {\r
+               {\r
+                       putValue( SHORT_DESCRIPTION, "Repeat - 繰り返し再生" );\r
+                       putValue( LARGE_ICON_KEY, new ButtonIcon(ButtonIcon.REPEAT_ICON) );\r
+                       putValue( SELECTED_KEY, false );\r
+               }\r
+               public void actionPerformed(ActionEvent event) { }\r
+       };\r
+       public SequencerTimeRangeModel( MidiDeviceManager device_manager ) {\r
+               this.deviceManager = device_manager;\r
+               this.sequencer = device_manager.getSequencer();\r
+               timer = new javax.swing.Timer(\r
+                       INTERVAL_MS,\r
+                       new ActionListener() {\r
+                               public void actionPerformed(ActionEvent e) {\r
+                                       if( ! valueIsAdjusting ) fireStateChanged();\r
+                               }\r
+                       }\r
+               );\r
+               timer.setCoalesce(true);\r
+               sequencer.addMetaEventListener(\r
+                       new MetaEventListener() {\r
+                               public void meta(MetaMessage msg) {\r
+                                       if( msg.getType() != 0x2F /* End-Of-Track */)\r
+                                               return;\r
+                                       timer.stop();\r
+                                       startStopAction.setRunning(false);\r
+                                       sequencer.setMicrosecondPosition(0);\r
+                                       if(\r
+                                               (Boolean)toggle_repeat_action.getValue( Action.SELECTED_KEY ) ||\r
+                                               SequencerTimeRangeModel.this.deviceManager.editorDialog.loadNext(1)\r
+                                       ) start();\r
+                                       else fireStateChanged();\r
+                               }\r
+                       }\r
+               );\r
+       }\r
+       public int getExtent() { return 0; }\r
+       public int getMaximum() { return (int)(getMicrosecondLength()/1000L); }\r
+       public int getMinimum() { return 0; }\r
+       public int getValue() { return (int)(getMicrosecondPosition()/1000L); }\r
+       public boolean getValueIsAdjusting() { return valueIsAdjusting; }\r
+       public void setExtent(int new_extent) {}\r
+       public void setMaximum(int new_maximum) {}\r
+       public void setMinimum(int new_minimum) {}\r
+       public void setRangeProperties(int value, int extent, int min, int max, boolean adjusting) {\r
+               sequencer.setMicrosecondPosition( 1000L * (long)value );\r
+               valueIsAdjusting = adjusting;\r
+               fireStateChanged();\r
+       }\r
+       public void setValue(int new_value) {\r
+               sequencer.setMicrosecondPosition( 1000L * (long)new_value );\r
+               fireStateChanged();\r
+       }\r
+       public void setValueIsAdjusting(boolean b) {\r
+               valueIsAdjusting = b;\r
+       }\r
+       public void addChangeListener(ChangeListener listener) {\r
+               listenerList.add(ChangeListener.class, listener);\r
+       }\r
+       public void removeChangeListener(ChangeListener listener) {\r
+               listenerList.remove(ChangeListener.class, listener);\r
+       }\r
+       public void fireStateChanged() {\r
+               Object[] listeners = listenerList.getListenerList();\r
+               for (int i = listeners.length-2; i>=0; i-=2) {\r
+                       if (listeners[i]==ChangeListener.class) {\r
+                               ((ChangeListener)listeners[i+1]).stateChanged(new ChangeEvent(this));\r
+                       }\r
+               }\r
+       }\r
+       long getMicrosecondLength() {\r
+               //\r
+               // Sequencer.getMicrosecondLength() returns NEGATIVE value\r
+               //  when over 0x7FFFFFFF microseconds (== 35.7913941166666... minutes),\r
+               //  should be corrected when negative\r
+               //\r
+               long us_len = sequencer.getMicrosecondLength();\r
+               return us_len < 0 ? 0x100000000L + us_len : us_len ;\r
+       }\r
+       long getMicrosecondPosition() {\r
+               long us_pos = sequencer.getMicrosecondPosition();\r
+               return us_pos < 0 ? 0x100000000L + us_pos : us_pos ;\r
+       }\r
+       private MidiSequenceModel seq_model = null;\r
+       public MidiSequenceModel getSequenceModel() { return seq_model; }\r
+       public boolean setSequenceModel( MidiSequenceModel seq_model ) {\r
+               //\r
+               // javax.sound.midi:Sequencer.setSequence() のドキュメントにある\r
+               // 「このメソッドは、Sequencer が閉じている場合でも呼び出すことができます。 」\r
+               // という記述は、null をセットする場合には当てはまらない。\r
+               // 連鎖的に stop() が呼ばれるために IllegalStateException sequencer not open が出る。\r
+               // この現象を回避するため、あらかじめチェックしてから setSequence() を呼び出している。\r
+               //\r
+               if( seq_model != null || sequencer.isOpen() ) {\r
+                       try {\r
+                               // Set new MIDI data\r
+                               sequencer.setSequence(\r
+                                               seq_model == null ? null : seq_model.getSequence()\r
+                                               );\r
+                       } catch ( InvalidMidiDataException e ) {\r
+                               e.printStackTrace();\r
+                               return false;\r
+                       }\r
+               }\r
+               this.seq_model = seq_model;\r
+               fireStateChanged();\r
+               return true;\r
+       }\r
+       // 小節位置の相対移動\r
+       public void moveMeasure( int measure_offset ) {\r
+               if( measure_offset == 0 || seq_model == null ) return;\r
+               SequenceIndex seq_index = seq_model.getSequenceIndex();\r
+               long new_tick_pos =\r
+                               seq_index.measureToTick(\r
+                                               measure_offset + seq_index.tickToMeasure(\r
+                                                               sequencer.getTickPosition()\r
+                                                               )\r
+                                               );\r
+               if( new_tick_pos < 0 ) new_tick_pos = 0;\r
+               else {\r
+                       long tick_len = sequencer.getTickLength();\r
+                       if( new_tick_pos > tick_len ) new_tick_pos = tick_len - 1;\r
+               }\r
+               sequencer.setTickPosition( new_tick_pos );\r
+               fireStateChanged();\r
+       }\r
+\r
+       // 開始/終了\r
+       public boolean isStartable() {\r
+               return sequencer.isOpen() && sequencer.getSequence() != null ;\r
+       }\r
+       public void start() {\r
+               if( ! isStartable() ) {\r
+                       startStopAction.setRunning(false);\r
+                       return;\r
+               }\r
+               startStopAction.setRunning(true);\r
+               timer.start();\r
+               if( deviceManager.isRecordable() ) {\r
+                       deviceManager.resetMicrosecondPosition();\r
+                       System.gc();\r
+                       sequencer.startRecording();\r
+               }\r
+               else {\r
+                       System.gc(); sequencer.start();\r
+               }\r
+               fireStateChanged();\r
+       }\r
+       public void stop() {\r
+               if( sequencer.isOpen() ) sequencer.stop();\r
+               timer.stop();\r
+               startStopAction.setRunning(false);\r
+               fireStateChanged();\r
+       }\r
+}\r
diff --git a/src/MIDISpec.java b/src/MIDISpec.java
new file mode 100644 (file)
index 0000000..c9beadf
--- /dev/null
@@ -0,0 +1,593 @@
+import javax.sound.midi.InvalidMidiDataException;\r
+import javax.sound.midi.MetaMessage;\r
+import javax.sound.midi.MidiEvent;\r
+import javax.sound.midi.MidiMessage;\r
+import javax.sound.midi.Sequence;\r
+import javax.sound.midi.ShortMessage;\r
+import javax.sound.midi.Track;\r
+/**\r
+ * MIDI仕様\r
+ */\r
+public class MIDISpec {\r
+       public static final int MAX_CHANNELS = 16;\r
+       public static final int PITCH_BEND_NONE = 8192;\r
+       /**\r
+        * メタメッセージタイプの名前を返します。\r
+        * @param metaMessageType メタメッセージタイプ\r
+        * @return メタメッセージタイプの名前\r
+        */\r
+       public static String getMetaName( int metaMessageType ) {\r
+               if( metaMessageType < 0x10 ) {\r
+                       return META_MESSAGE_TYPE_NAMES[metaMessageType];\r
+               }\r
+               switch( metaMessageType ) {\r
+               case 0x20: return "MIDI Ch.Prefix";\r
+               case 0x21: return "MIDI Output Port";\r
+               case 0x2F: return "End Of Track";\r
+               case 0x51: return "Tempo";\r
+               case 0x54: return "SMPTE Offset";\r
+               case 0x58: return "Time Signature";\r
+               case 0x59: return "Key Signature";\r
+               case 0x7F: return "Sequencer Specific";\r
+               }\r
+               return null;\r
+       }\r
+       /**\r
+        * メタメッセージタイプがテキストのつくものか調べます。\r
+        * @param metaMessageType メタメッセージタイプ\r
+        * @return テキストがつくときtrue\r
+        */\r
+       public static boolean hasMetaText( int metaMessageType ) {\r
+               return (metaMessageType > 0 && metaMessageType < 10);\r
+       }\r
+       /**\r
+        * メタメッセージタイプが EOT (End Of Track) か調べます。\r
+        * @param metaMessageType メタメッセージタイプ\r
+        * @return EOTならtrue\r
+        */\r
+       public static boolean isEOT( int metaMessageType ) {\r
+               return metaMessageType == 0x2F;\r
+       }\r
+       /**\r
+        * MIDIメッセージが EOT (End Of Track) か調べます。\r
+        * @param midiMessage MIDIメッセージ\r
+        * @return EOTならtrue\r
+        */\r
+       public static boolean isEOT( MidiMessage midiMessage ) {\r
+               if ( !(midiMessage instanceof MetaMessage) )\r
+                       return false;\r
+               return isEOT( ((MetaMessage)midiMessage).getType() );\r
+       }\r
+       /**\r
+        * 1分のマイクロ秒数\r
+        */\r
+       public static final int MICROSECOND_PER_MINUTE = (60 * 1000 * 1000);\r
+       /**\r
+        * MIDIのテンポメッセージについているバイト列をQPM単位のテンポに変換します。\r
+        * @param b バイト列\r
+        * @return テンポ[QPM]\r
+        */\r
+       public static int byteArrayToQpmTempo( byte[] b ) {\r
+               int tempoInUsPerQuarter\r
+                       = ((b[0] & 0xFF) << 16) | ((b[1] & 0xFF) << 8) | (b[2] & 0xFF);\r
+               return MICROSECOND_PER_MINUTE / tempoInUsPerQuarter;\r
+       }\r
+       /**\r
+        * QPM単位のテンポをMIDIのテンポメッセージ用バイト列に変換します。\r
+        * @param qpm テンポ[QPM]\r
+        * @return MIDIのテンポメッセージ用バイト列\r
+        */\r
+       public static byte[] qpmTempoToByteArray( int qpm ) {\r
+               int tempo_in_us_per_quarter = MICROSECOND_PER_MINUTE / qpm;\r
+               byte[] b = new byte[3];\r
+               b[0] = (byte)((tempo_in_us_per_quarter >> 16) & 0xFF);\r
+               b[1] = (byte)((tempo_in_us_per_quarter >> 8) & 0xFF);\r
+               b[2] = (byte)(tempo_in_us_per_quarter & 0xFF);\r
+               return b;\r
+       }\r
+       /**\r
+        * トラック名を返します。\r
+        * @param track MIDIトラック\r
+        * @return トラック名\r
+        */\r
+       public static String getNameOf( Track track ) {\r
+               MidiEvent midi_event;\r
+               MidiMessage msg;\r
+               MetaMessage meta_msg;\r
+               for( int i=0; i<track.size(); i++ ) {\r
+                       midi_event = track.get(i);\r
+                       if( midi_event.getTick() > 0 ) { // No more event at top, try next track\r
+                               break;\r
+                       }\r
+                       msg = midi_event.getMessage();\r
+                       if( ! (msg instanceof MetaMessage) ) { // Not meta message\r
+                               continue;\r
+                       }\r
+                       meta_msg = (MetaMessage)msg;\r
+                       if( meta_msg.getType() != 0x03 ) { // Not sequence name\r
+                               continue;\r
+                       }\r
+                       return new String(meta_msg.getData());\r
+               }\r
+               return null;\r
+       }\r
+       /**\r
+        * トラック名を設定します。\r
+        * @param track MIDIトラック\r
+        * @param name トラック名\r
+        * @return 成功:true、失敗:false\r
+        */\r
+       public static boolean setNameOf( Track track, String name ) {\r
+               MidiEvent midiEvent = null;\r
+               MidiMessage msg = null;\r
+               MetaMessage metaMsg = null;\r
+               for( int i=0; i<track.size(); i++ ) {\r
+                       if(\r
+                               (midiEvent = track.get(i)).getTick() > 0\r
+                               ||\r
+                               (msg = midiEvent.getMessage()) instanceof MetaMessage\r
+                               &&\r
+                               (metaMsg = (MetaMessage)msg).getType() == 0x03\r
+                       ) {\r
+                               break;\r
+                       }\r
+                       metaMsg = null;\r
+               }\r
+               if( metaMsg == null ) {\r
+                       if( name.isEmpty() ) return false;\r
+                       track.add(new MidiEvent(\r
+                               (MidiMessage)(metaMsg = new MetaMessage()), 0\r
+                       ));\r
+               }\r
+               byte ub[] = name.getBytes();\r
+               try {\r
+                       metaMsg.setMessage( 0x03, ub, ub.length );\r
+               }\r
+               catch( InvalidMidiDataException e ) {\r
+                       e.printStackTrace();\r
+                       return false;\r
+               }\r
+               return true;\r
+       }\r
+       /**\r
+        * シーケンス名を返します。\r
+        * <p>トラック名の入った最初のトラックにあるトラック名を\r
+        * シーケンス名として返します。\r
+        * </p>\r
+        * @param seq MIDIシーケンス\r
+        * @return シーケンス名\r
+        */\r
+       public static String getNameOf(Sequence seq) {\r
+               // Returns name of the MIDI sequence.\r
+               // A sequence name is placed at top of first track of the sequence.\r
+               //\r
+               Track tracks[] = seq.getTracks();\r
+               String s;\r
+               for( Track track : tracks )\r
+                       if( (s = getNameOf(track)) != null )\r
+                               return s;\r
+               return null;\r
+       }\r
+       /**\r
+        * シーケンス名を設定します。\r
+        * <p>先頭のトラックに設定されます。\r
+        * 設定に失敗した場合、順に次のトラックへの設定を試みます。\r
+        * </p>\r
+        *\r
+        * @param seq MIDIシーケンス\r
+        * @param name シーケンス名\r
+        * @return 成功:true、失敗:false\r
+        */\r
+       public static boolean setNameOf( Sequence seq, String name ) {\r
+               Track tracks[] = seq.getTracks();\r
+               for( Track track : tracks )\r
+                       if( setNameOf(track,name) ) return true;\r
+               return false;\r
+       }\r
+       private static final String META_MESSAGE_TYPE_NAMES[] = {\r
+               "Seq Number", "Text", "Copyright", "Seq/Track Name",\r
+               "Instrument Name", "Lyric", "Marker","Cue Point",\r
+               "Program Name", "Device Name", null, null,\r
+               null, null, null, null\r
+       };\r
+       ///////////////////////////////////////////////////////////////////\r
+       //\r
+       // Channel Message / System Message\r
+       //\r
+       /**\r
+        * MIDIステータス名を返します。\r
+        * @param status MIDIステータス\r
+        * @return MIDIステータス名\r
+        */\r
+       public static String getStatusName( int status ) {\r
+               if( status < 0x80 ) {\r
+                       // No such status\r
+                       return null;\r
+               }\r
+               else if ( status < 0xF0 ) {\r
+                       // Channel Message\r
+                       return ch_msg_status_names[ (status >> 4) - 0x08 ];\r
+               }\r
+               else if ( status <= 0xFF ) {\r
+                       // System Message\r
+                       return sys_msg_names[ status - 0xF0 ];\r
+               }\r
+               return null;\r
+       }\r
+       /**\r
+        * 指定のMIDIショートメッセージがチャンネルメッセージかどうか調べます。\r
+        * @param msg MIDIメッセージ\r
+        * @return MIDIチャンネルメッセージの場合true\r
+        */\r
+       public static boolean isChannelMessage( ShortMessage msg ) {\r
+               return isChannelMessage( msg.getStatus() );\r
+       }\r
+       /**\r
+        * MIDIステータスがチャンネルメッセージかどうか調べます。\r
+        * @param status MIDIステータス\r
+        * @return MIDIチャンネルメッセージの場合true\r
+        */\r
+       public static boolean isChannelMessage( int status ) {\r
+               return ( status < 0xF0 && status >= 0x80 );\r
+       }\r
+       private static final String ch_msg_status_names[] = {\r
+               // 0x80 - 0xE0 : Channel Voice Message\r
+               // 0xB0 : Channel Mode Message\r
+               "NoteOFF", "NoteON",\r
+               "Polyphonic Key Pressure", "Ctrl/Mode",\r
+               "Program", "Ch.Pressure", "Pitch Bend"\r
+       };\r
+       private static final String sys_msg_names[] = {\r
+               // 0xF0 : System Exclusive\r
+               "SysEx",\r
+               //\r
+               // 0xF1 - 0xF7 : System Common Message\r
+               "MIDI Time Code Quarter Frame",\r
+               "Song Position Pointer", "Song Select",\r
+               null, null, "Tune Request", "Special SysEx",\r
+               //\r
+               // 0xF8 - 0xFF : System Realtime Message\r
+               // 0xFF : Meta Message (SMF only, Not for wired MIDI message)\r
+               "Timing Clock", null, "Start", "Continue",\r
+               "Stop", null, "Active Sensing", "Meta / Sys.Reset",\r
+       };\r
+       ///////////////////////////////////////////////////////////////////\r
+       //\r
+       // Control Change / Channel Mode Message\r
+       //\r
+       public static String getControllerName( int controller_number ) {\r
+               if( controller_number < 0x00 ) {\r
+                       return null;\r
+               }\r
+               else if( controller_number < 0x20 ) {\r
+                       String s = controller_names_0[controller_number];\r
+                       if( s != null ) s += " (MSB)";\r
+                       return s;\r
+               }\r
+               else if( controller_number < 0x40 ) {\r
+                       String s = controller_names_0[controller_number - 0x20];\r
+                       if( s != null ) s += " (LSB)";\r
+                       return s;\r
+               }\r
+               else if( controller_number < 0x78 ) {\r
+                       return controller_momentary_switch_names[controller_number - 0x40];\r
+               }\r
+               else if( controller_number < 0x80 ) {\r
+                       return controller_mode_message_names[controller_number - 0x78];\r
+               }\r
+               else {\r
+                       return null;\r
+               }\r
+       }\r
+       private static final String controller_names_0[] = {\r
+               //\r
+               // 0x00-0x0F (MSB)\r
+               "Bank Select", "Modulation Depth", "Breath Controller", null,\r
+               "Foot Controller", "Portamento Time", "Data Entry", "Volume",\r
+               "Balance", null, "Pan", "Expression",\r
+               "Effect Control 1", "Effect Control 2", null, null,\r
+               //\r
+               // 0x10-0x1F (MSB)\r
+               "General Purpose 1", "General Purpose 2",\r
+               "General Purpose 3", "General Purpose 4",\r
+               null, null, null, null,\r
+               null, null, null, null,\r
+               null, null, null, null,\r
+               //\r
+               // 0x20-0x3F (LSB)\r
+       };\r
+       private static final String controller_momentary_switch_names[] = {\r
+               //\r
+               // 0x40-0x4F\r
+               "Damper Pedal (Sustain)", "Portamento",\r
+               "Sustenuto", "Soft Pedal",\r
+               "Legato Footswitch", "Hold 2",\r
+               "Sound Controller 1 (Sound Variation)",\r
+               "Sound Controller 2 (Timbre/Harmonic Intens)",\r
+               "Sound Controller 3 (Release Time)",\r
+               "Sound Controller 4 (Attack Time)",\r
+               "Sound Controller 5 (Brightness)",\r
+               "Sound Controller 6 (Decay Time)",\r
+               "Sound Controller 7 (Vibrato Rate)",\r
+               "Sound Controller 8 (Vibrato Depth)",\r
+               "Sound Controller 9 (Vibrato Delay)",\r
+               "Sound Controller 10 (Undefined)",\r
+               //\r
+               // 0x50-0x5F\r
+               "General Purpose 5", "General Purpose 6 (Temp Change)",\r
+               "General Purpose 7", "General Purpose 8",\r
+               "Portamento Control", null, null, null,\r
+               null, null, null, "Reverb (Ext.Effects Depth)",\r
+               "Tremelo Depth", "Chorus Depth",\r
+               "Celeste (Detune) Depth", "Phaser Depth",\r
+               //\r
+               // 0x60-0x6F\r
+               "Data Increment", "Data Decrement",\r
+               "NRPN (LSB)", "NRPN (MSB)",\r
+               "RPN (LSB)", "RPN (MSB)", null, null,\r
+               null, null, null, null,\r
+               null, null, null, null,\r
+               //\r
+               // 0x70-0x77\r
+               null, null, null, null,\r
+               null, null, null, null\r
+       };\r
+       private static final String controller_mode_message_names[] = {\r
+               // 0x78-0x7F\r
+               "All Sound OFF", "Reset All Controllers",\r
+               "Local Control", "All Notes OFF",\r
+               "Omni Mode OFF", "Omni Mode ON",\r
+               "Mono Mode ON", "Poly Mode ON"\r
+       };\r
+       ///////////////////////////////////////////////////////////////////\r
+       //\r
+       // System Exclusive\r
+       //\r
+       /**\r
+        * システムエクスクルーシブの製造者IDを名前に変換します。\r
+        * @param id 製造者ID\r
+        * @return 製造者名(不明な場合はnull)\r
+        */\r
+       public static String getSysExManufacturerName( int id ) {\r
+               switch( id ) {\r
+               //\r
+               // Manufacturer\r
+               case 0x40: return "KAWAI";\r
+               case 0x41: return "Roland";\r
+               case 0x42: return "KORG";\r
+               case 0x43: return "YAMAHA";\r
+               case 0x44: return "CASIO";\r
+               //\r
+               // Special\r
+               case 0x7D: return "Non-Commercial";\r
+               case 0x7E: return "Universal: Non-RealTime";\r
+               case 0x7F: return "Universal: RealTime";\r
+               //\r
+               default: return null;\r
+               }\r
+       }\r
+       /**\r
+        * MIDIノート番号の最大値\r
+        */\r
+       public static final int MAX_NOTE_NO = 127;\r
+       /**\r
+        * General MIDI の楽器ファミリー名の配列\r
+        */\r
+       public static final String instrument_family_names[] = {\r
+\r
+               "Piano",\r
+               "Chrom.Percussion",\r
+               "Organ",\r
+               "Guitar",\r
+               "Bass",\r
+               "Strings",\r
+               "Ensemble",\r
+               "Brass",\r
+\r
+               "Reed",\r
+               "Pipe",\r
+               "Synth Lead",\r
+               "Synth Pad",\r
+               "Synth Effects",\r
+               "Ethnic",\r
+               "Percussive",\r
+               "Sound Effects",\r
+       };\r
+       /**\r
+        * General MIDI の楽器名(プログラムチェンジのプログラム名)の配列\r
+        */\r
+       public static final String instrument_names[] = {\r
+               "Acoustic Grand Piano",\r
+               "Bright Acoustic Piano",\r
+               "Electric Grand Piano",\r
+               "Honky-tonk Piano",\r
+               "Electric Piano 1",\r
+               "Electric Piano 2",\r
+               "Harpsichord",\r
+               "Clavi",\r
+               "Celesta",\r
+               "Glockenspiel",\r
+               "Music Box",\r
+               "Vibraphone",\r
+               "Marimba",\r
+               "Xylophone",\r
+               "Tubular Bells",\r
+               "Dulcimer",\r
+               "Drawbar Organ",\r
+               "Percussive Organ",\r
+               "Rock Organ",\r
+               "Church Organ",\r
+               "Reed Organ",\r
+               "Accordion",\r
+               "Harmonica",\r
+               "Tango Accordion",\r
+               "Acoustic Guitar (nylon)",\r
+               "Acoustic Guitar (steel)",\r
+               "Electric Guitar (jazz)",\r
+               "Electric Guitar (clean)",\r
+               "Electric Guitar (muted)",\r
+               "Overdriven Guitar",\r
+               "Distortion Guitar",\r
+               "Guitar harmonics",\r
+               "Acoustic Bass",\r
+               "Electric Bass (finger)",\r
+               "Electric Bass (pick)",\r
+               "Fretless Bass",\r
+               "Slap Bass 1",\r
+               "Slap Bass 2",\r
+               "Synth Bass 1",\r
+               "Synth Bass 2",\r
+               "Violin",\r
+               "Viola",\r
+               "Cello",\r
+               "Contrabass",\r
+               "Tremolo Strings",\r
+               "Pizzicato Strings",\r
+               "Orchestral Harp",\r
+               "Timpani",\r
+               "String Ensemble 1",\r
+               "String Ensemble 2",\r
+               "SynthStrings 1",\r
+               "SynthStrings 2",\r
+               "Choir Aahs",\r
+               "Voice Oohs",\r
+               "Synth Voice",\r
+               "Orchestra Hit",\r
+               "Trumpet",\r
+               "Trombone",\r
+               "Tuba",\r
+               "Muted Trumpet",\r
+               "French Horn",\r
+               "Brass Section",\r
+               "SynthBrass 1",\r
+               "SynthBrass 2",\r
+               "Soprano Sax",\r
+               "Alto Sax",\r
+               "Tenor Sax",\r
+               "Baritone Sax",\r
+               "Oboe",\r
+               "English Horn",\r
+               "Bassoon",\r
+               "Clarinet",\r
+               "Piccolo",\r
+               "Flute",\r
+               "Recorder",\r
+               "Pan Flute",\r
+               "Blown Bottle",\r
+               "Shakuhachi",\r
+               "Whistle",\r
+               "Ocarina",\r
+               "Lead 1 (square)",\r
+               "Lead 2 (sawtooth)",\r
+               "Lead 3 (calliope)",\r
+               "Lead 4 (chiff)",\r
+               "Lead 5 (charang)",\r
+               "Lead 6 (voice)",\r
+               "Lead 7 (fifths)",\r
+               "Lead 8 (bass + lead)",\r
+               "Pad 1 (new age)",\r
+               "Pad 2 (warm)",\r
+               "Pad 3 (polysynth)",\r
+               "Pad 4 (choir)",\r
+               "Pad 5 (bowed)",\r
+               "Pad 6 (metallic)",\r
+               "Pad 7 (halo)",\r
+               "Pad 8 (sweep)",\r
+               "FX 1 (rain)",\r
+               "FX 2 (soundtrack)",\r
+               "FX 3 (crystal)",\r
+               "FX 4 (atmosphere)",\r
+               "FX 5 (brightness)",\r
+               "FX 6 (goblins)",\r
+               "FX 7 (echoes)",\r
+               "FX 8 (sci-fi)",\r
+               "Sitar",\r
+               "Banjo",\r
+               "Shamisen",\r
+               "Koto",\r
+               "Kalimba",\r
+               "Bag pipe",\r
+               "Fiddle",\r
+               "Shanai",\r
+               "Tinkle Bell",\r
+               "Agogo",\r
+               "Steel Drums",\r
+               "Woodblock",\r
+               "Taiko Drum",\r
+               "Melodic Tom",\r
+               "Synth Drum",\r
+               "Reverse Cymbal",\r
+               "Guitar Fret Noise",\r
+               "Breath Noise",\r
+               "Seashore",\r
+               "Bird Tweet",\r
+               "Telephone Ring",\r
+               "Helicopter",\r
+               "Applause",\r
+               "Gunshot",\r
+       };\r
+       /**\r
+        * パーカッション用MIDIノート番号の最小値\r
+        */\r
+       public static final int MIN_PERCUSSION_NUMBER = 35;\r
+       /**\r
+        * パーカッション用のMIDIチャンネル(通常はCH.10)における\r
+        * ノート番号からパーカッション名を返します。\r
+        *\r
+        * @param note_no ノート番号\r
+        * @return パーカッション名\r
+        */\r
+       public static String getPercussionName(int note_no) {\r
+               int i = note_no - MIN_PERCUSSION_NUMBER ;\r
+               return i>=0 && i < PERCUSSION_NAMES.length ? PERCUSSION_NAMES[i] : "(Unknown)" ;\r
+       }\r
+       public static final String      PERCUSSION_NAMES[] = {\r
+               "Acoustic Bass Drum",\r
+               "Bass Drum 1",\r
+               "Side Stick",\r
+               "Acoustic Snare",\r
+               "Hand Clap",\r
+               "Electric Snare",\r
+               "Low Floor Tom",\r
+               "Closed Hi Hat",\r
+               "High Floor Tom",\r
+               "Pedal Hi-Hat",\r
+               "Low Tom",\r
+               "Open Hi-Hat",\r
+               "Low-Mid Tom",\r
+               "Hi Mid Tom",\r
+               "Crash Cymbal 1",\r
+               "High Tom",\r
+               "Ride Cymbal 1",\r
+               "Chinese Cymbal",\r
+               "Ride Bell",\r
+               "Tambourine",\r
+               "Splash Cymbal",\r
+               "Cowbell",\r
+               "Crash Cymbal 2",\r
+               "Vibraslap",\r
+               "Ride Cymbal 2",\r
+               "Hi Bongo",\r
+               "Low Bongo",\r
+               "Mute Hi Conga",\r
+               "Open Hi Conga",\r
+               "Low Conga",\r
+               "High Timbale",\r
+               "Low Timbale",\r
+               "High Agogo",\r
+               "Low Agogo",\r
+               "Cabasa",\r
+               "Maracas",\r
+               "Short Whistle",\r
+               "Long Whistle",\r
+               "Short Guiro",\r
+               "Long Guiro",\r
+               "Claves",\r
+               "Hi Wood Block",\r
+               "Low Wood Block",\r
+               "Mute Cuica",\r
+               "Open Cuica",\r
+               "Mute Triangle",\r
+               "Open Triangle",\r
+       };\r
+}\r
diff --git a/src/MidiChordHelper.java b/src/MidiChordHelper.java
new file mode 100644 (file)
index 0000000..20a354a
--- /dev/null
@@ -0,0 +1,132 @@
+\r
+import java.applet.Applet;\r
+import java.applet.AppletContext;\r
+import java.applet.AppletStub;\r
+import java.applet.AudioClip;\r
+import java.awt.BorderLayout;\r
+import java.awt.Font;\r
+import java.awt.Frame;\r
+import java.awt.Image;\r
+import java.awt.Toolkit;\r
+import java.awt.event.WindowAdapter;\r
+import java.awt.event.WindowEvent;\r
+import java.io.File;\r
+import java.io.IOException;\r
+import java.io.InputStream;\r
+import java.net.URL;\r
+import java.util.Enumeration;\r
+import java.util.Iterator;\r
+import java.util.Vector;\r
+\r
+import javax.swing.JFrame;\r
+import javax.swing.JLabel;\r
+import javax.swing.WindowConstants;\r
+import javax.swing.event.ChangeEvent;\r
+import javax.swing.event.ChangeListener;\r
+import javax.swing.event.TableModelEvent;\r
+import javax.swing.event.TableModelListener;\r
+\r
+/**\r
+ * MIDI Chord Helper を Java アプリとして起動します。\r
+ */\r
+public class MidiChordHelper {\r
+       static int count = 0;\r
+       static AppletFrame frame = null;\r
+       /**\r
+        * MIDI Chord Helper を Java アプリとして起動します。\r
+        * @param args コマンドライン引数\r
+        * @throws Exception 何らかの異常が発生した場合にスローされる\r
+        */\r
+       public static void main(String[] args) throws Exception {\r
+               ChordHelperApplet applet;\r
+               if( count++ > 0 && frame != null) {\r
+                       applet = frame.applet;\r
+                       int window_state = frame.getExtendedState();\r
+                       if( ( window_state & Frame.ICONIFIED ) == 0 ) {\r
+                               frame.toFront();\r
+                       } else {\r
+                               window_state &= ~(Frame.ICONIFIED) ;\r
+                               frame.setExtendedState( window_state );\r
+                       }\r
+               } else {\r
+                       frame = new AppletFrame(applet = new ChordHelperApplet());\r
+               }\r
+               if( args.length > 0 ) {\r
+                       Vector<File> fileList = new Vector<File>();\r
+                       for( String arg : args ) {\r
+                               fileList.add(new File(arg));\r
+                       }\r
+                       applet.editorDialog.loadAndPlay(fileList);\r
+               }\r
+       }\r
+}\r
+\r
+class AppletFrame extends JFrame implements AppletStub, AppletContext {\r
+       JLabel status_;\r
+       ChordHelperApplet applet = null;\r
+       public AppletFrame(ChordHelperApplet applet) {\r
+               setTitle(ChordHelperApplet.VersionInfo.NAME);\r
+               (status_ = new JLabel()).setFont(\r
+                       status_.getFont().deriveFont(Font.PLAIN)\r
+               );\r
+               add( this.applet = applet, BorderLayout.CENTER );\r
+               add( status_, BorderLayout.SOUTH );\r
+               applet.setStub(this);\r
+               applet.init();\r
+               Image iconImage = applet.imageIcon == null ? null : applet.imageIcon.getImage();\r
+               setIconImage(iconImage);\r
+               setDefaultCloseOperation( WindowConstants.DO_NOTHING_ON_CLOSE );\r
+               addWindowListener( new WindowAdapter() {\r
+                       public void windowClosing(WindowEvent evt) {\r
+                               if( AppletFrame.this.applet.isConfirmedToExit() )\r
+                                       System.exit(0);\r
+                       }\r
+               });\r
+               applet.editorDialog.seqListModel.addTableModelListener(\r
+                       new TableModelListener() {\r
+                               public void tableChanged(TableModelEvent e) {\r
+                                       if( e.getColumn() == SequenceListModel.COLUMN_FILENAME )\r
+                                               setFilenameToTitle();\r
+                               }\r
+                       }\r
+               );\r
+               applet.deviceManager.timeRangeModel.addChangeListener(\r
+                       new ChangeListener() {\r
+                               public void stateChanged(ChangeEvent e) {\r
+                                       setFilenameToTitle();\r
+                               }\r
+                       }\r
+               );\r
+               pack();\r
+               setLocationRelativeTo(null);\r
+               setVisible(true);\r
+               applet.start();\r
+       }\r
+       private void setFilenameToTitle() {\r
+               MidiSequenceModel seqModel = applet.deviceManager.timeRangeModel.getSequenceModel();\r
+               String filename = ( seqModel == null ? null : seqModel.getFilename() );\r
+               String title = ChordHelperApplet.VersionInfo.NAME;\r
+               if( filename != null && ! filename.isEmpty() ) {\r
+                       title = filename + " - " + title;\r
+               }\r
+               setTitle(title);\r
+       }\r
+       public boolean isActive() { return true; }\r
+       public URL getDocumentBase() { return null; }\r
+       public URL getCodeBase() { return null; }\r
+       public String getParameter(String name) { return null; }\r
+       public AppletContext getAppletContext() { return this; }\r
+       public void appletResize(int width, int height) {}\r
+       public AudioClip getAudioClip(URL url) { return null; }\r
+       public Image getImage(URL url) {\r
+               return Toolkit.getDefaultToolkit().getImage(url);\r
+       }\r
+       public Applet getApplet(String name) { return null; }\r
+       public Enumeration<Applet> getApplets() { return (null); }\r
+       public void showDocument(URL url) {}\r
+       public void showDocument(URL url, String target) {}\r
+       public void showStatus(String status) { status_.setText(status); }\r
+       public InputStream getStream(String key) { return null; }\r
+       public Iterator<String> getStreamKeys() { return null; }\r
+       public void setStream(String key, InputStream stream) throws IOException {}\r
+}\r
diff --git a/src/Music.java b/src/Music.java
new file mode 100644 (file)
index 0000000..119a3d1
--- /dev/null
@@ -0,0 +1,2506 @@
+import java.awt.Color; // Color\r
+import java.util.Arrays;\r
+import java.util.List;\r
+import java.util.Objects;\r
+import java.util.Vector;\r
+import java.util.regex.Pattern;\r
+\r
+import javax.sound.midi.InvalidMidiDataException;\r
+import javax.sound.midi.MetaMessage;\r
+import javax.sound.midi.MidiEvent;\r
+import javax.sound.midi.MidiMessage;\r
+import javax.sound.midi.Sequence;\r
+import javax.sound.midi.ShortMessage;\r
+import javax.sound.midi.Track;\r
+// for ComboBoxModel implementation\r
+import javax.swing.ComboBoxModel;\r
+import javax.swing.JLabel;\r
+import javax.swing.event.ListDataListener;\r
+\r
+/**\r
+ * 音楽理論・自動作曲アルゴリズムの Java 実装\r
+ *\r
+ * Circle-Of-Fifth based music theory functions\r
+ *\r
+ * @author Copyright (C) 2004-2013 @きよし - Akiyoshi Kamide\r
+ */\r
+public class Music {\r
+       /**\r
+        * 1オクターブの半音数\r
+        */\r
+       public static final int SEMITONES_PER_OCTAVE = 12;\r
+       /**\r
+        * シンボルの言語モード(音階、調など)\r
+        */\r
+       public enum SymbolLanguage {\r
+               /**\r
+                * シンボル表記(Bb, F#)\r
+                */\r
+               SYMBOL(\r
+                       Arrays.asList("bb","b","","#","x"),\r
+                       "FCGDAEB",false,"","m"," / "\r
+               ),\r
+               /**\r
+                * 英名表記(B flat, F sharp)\r
+                */\r
+               NAME(\r
+                       Arrays.asList(" double flat"," flat",""," sharp"," double sharp"),\r
+                       "FCGDAEB",false," major"," minor"," / "\r
+               ),\r
+               /**\r
+                * 日本名表記(変ロ, 嬰ヘ)\r
+                */\r
+               IN_JAPANESE(\r
+                       Arrays.asList("重変","変","","嬰","重嬰"),\r
+                       "ヘハトニイホロ",true,"長調","短調","/"\r
+               );\r
+               /**\r
+                * ♭や♯の表記を、半音下がる数が多いほうから順に並べたリスト\r
+                */\r
+               List<String> sharpFlatList;\r
+               /**\r
+                * 音名を五度圏順で並べた文字列(必ず7文字でなければならない)\r
+                */\r
+               String notes;\r
+               /**\r
+                * 変化記号が音名の前につく(true)か後ろにつく(false)か\r
+                * <p>英語の場合は B♭ のように♭が後ろ、\r
+                * 日本語の場合は「変ロ」のように「変」が前につくことを表します。\r
+                * </p>\r
+                */\r
+               boolean preSharpFlat;\r
+               /**\r
+                * メジャーを表す文字列\r
+                */\r
+               String major;\r
+               /**\r
+                * マイナーを表す文字列\r
+                */\r
+               String minor;\r
+               /**\r
+                * メジャーとマイナーを併記する場合の区切り文字\r
+                */\r
+               String majorMinorDelimiter;\r
+               private SymbolLanguage(\r
+                       List<String> sharpFlatList,\r
+                       String notes,\r
+                       boolean preSharpFlat,\r
+                       String major, String minor, String majorMinorDelimiter\r
+               ) {\r
+                       this.sharpFlatList = sharpFlatList;\r
+                       this.notes = notes;\r
+                       this.preSharpFlat = preSharpFlat;\r
+                       this.major = major;\r
+                       this.minor = minor;\r
+                       this.majorMinorDelimiter = majorMinorDelimiter;\r
+               }\r
+               /**\r
+                * 音名の文字列を、メジャーキー基準の五度圏インデックス値に変換します。\r
+                *\r
+                * @param noteSymbol 音名の文字列\r
+                * @return メジャーキー基準の五度圏インデックス値\r
+                * @throws IllegalArgumentException\r
+                *  指定の音名が空、またはA~Gの範囲を外れている場合\r
+                */\r
+               public int majorCo5Of(String noteSymbol) {\r
+                       if( Objects.requireNonNull(\r
+                               noteSymbol,\r
+                               "Note symbol must not be null"\r
+                       ).isEmpty() ) {\r
+                               throw new IllegalArgumentException(\r
+                                       "Empty note symbol specified"\r
+                               );\r
+                       }\r
+                       char topChar = noteSymbol.charAt(0);\r
+                       int co5 = notes.indexOf(topChar);\r
+                       if( co5 < 0 ) {\r
+                               throw new IllegalArgumentException(\r
+                                       "Unknown note symbol " + noteSymbol\r
+                               );\r
+                       }\r
+                       co5--;\r
+                       int offset = -14;\r
+                       for( String sharpFlat : sharpFlatList ) {\r
+                               if( ! sharpFlat.isEmpty() && noteSymbol.startsWith(sharpFlat,1) ) {\r
+                                       // 変化記号を発見\r
+                                       // bb のほうが b よりも先にマッチするので誤判定の心配なし\r
+                                       co5 += offset;\r
+                                       break;\r
+                               }\r
+                               offset += 7;\r
+                       }\r
+                       return co5;\r
+               }\r
+       }\r
+       /**\r
+        * 音名(オクターブ抜き)を表すクラスです。値は不変です。\r
+        *\r
+        * <p>この音名は、メジャーキーの調号にした場合に\r
+        * 「♭、#が何個つくか」という数値\r
+        * 「五度圏インデックス値」で保持することを基本としています。\r
+        * こうすれば異名同音を明確に区別でき、\r
+        * しかも音楽理論的な計算を極めて単純な数式で行えるようになります。\r
+        * この方式はMIDIメタメッセージで調号を指定するときにも使われていて、\r
+        * 非常に高い親和性を持ちます。\r
+        * </p>\r
+        */\r
+       public static class NoteSymbol implements Cloneable {\r
+               /**\r
+                * メジャーキー基準の五度圏インデックス値\r
+                */\r
+               private int majorCo5;\r
+               /**\r
+                * ノート番号(0~11)\r
+                */\r
+               private int noteNumber;\r
+               /**\r
+                * 音名 C(ハ音)を構築します。\r
+                */\r
+               public NoteSymbol() {\r
+               }\r
+               /**\r
+                * 五度圏インデックス値(メジャーキー基準)から音名を構築します。\r
+                * @param majorCo5 五度圏インデックス値\r
+                */\r
+               public NoteSymbol(int majorCo5) {\r
+                       noteNumber = toNoteNumber(this.majorCo5 = majorCo5);\r
+               }\r
+               /**\r
+                * 音名を文字列から構築します。\r
+                * @param noteSymbol 音名の文字列\r
+                * @throws IllegalArgumentException\r
+                *  指定の音名が空、またはA~Gの範囲を外れている場合\r
+                */\r
+               public NoteSymbol(String noteSymbol) {\r
+                       this(SymbolLanguage.SYMBOL.majorCo5Of(noteSymbol.trim()));\r
+               }\r
+               @Override\r
+               protected NoteSymbol clone() {\r
+                       return new NoteSymbol(majorCo5);\r
+               }\r
+               /**\r
+                * この音階が指定されたオブジェクトと等しいか調べます。\r
+                *\r
+                * <p>双方の五度圏インデックス値が等しい場合のみtrueを返します。\r
+                * すなわち、異名同音は等しくないものとして判定されます。\r
+                * </p>\r
+                *\r
+                * @return この音階が指定されたオブジェクトと等しい場合true\r
+                */\r
+               @Override\r
+               public boolean equals(Object anObject) {\r
+                       if( this == anObject )\r
+                               return true;\r
+                       if( anObject instanceof NoteSymbol ) {\r
+                               NoteSymbol another = (NoteSymbol) anObject;\r
+                               return majorCo5 == another.majorCo5;\r
+                       }\r
+                       return false;\r
+               }\r
+               /**\r
+                * この音階のハッシュコード値として、\r
+                * 五度圏インデックス値をそのまま返します。\r
+                *\r
+                * @return この音階のハッシュコード値\r
+                */\r
+               @Override\r
+               public int hashCode() { return majorCo5; }\r
+               /**\r
+                * 音階が等しいかどうかを、異名同音を無視して判定します。\r
+                * @param another 比較対象の音階\r
+                * @return 等しければtrue\r
+                */\r
+               public boolean equalsEnharmonically(NoteSymbol another) {\r
+                       return this == another || this.noteNumber == another.noteNumber;\r
+               }\r
+               /**\r
+                * 五度圏インデックス値(メジャーキー基準)を返します。\r
+                * @return 五度圏インデックス値\r
+                */\r
+               public int toCo5() { return majorCo5; }\r
+               /**\r
+                * メジャーかマイナーかを指定し、対応する五度圏インデックス値を返します。\r
+                * <p>マイナーの場合、\r
+                * メジャー基準の五度圏インデックス値から3が差し引かれます。\r
+                * 例えば、C major の場合は調号が0個なのに対し、\r
+                * C minor のときは調号の♭が3個に増えますが、\r
+                * 3を差し引くことによってこのズレが補正されます。\r
+                * </p>\r
+                *\r
+                * @param isMinor マイナーのときtrue\r
+                * @return 五度圏インデックス値\r
+                */\r
+               public int toCo5(boolean isMinor) {\r
+                       return isMinor ? majorCo5 - 3 : majorCo5;\r
+               }\r
+               /**\r
+                * ノート番号(0~11)を返します。\r
+                * <p>これはMIDIノート番号からオクターブ情報を抜いた値と同じです。\r
+                * 五度圏インデックス値をノート番号に変換した場合、\r
+                * 異名同音、すなわち同じ音階が♭表記、♯表記のどちらだったか\r
+                * という情報は失われます。\r
+                * </p>\r
+                * @return ノート番号(0~11)\r
+                */\r
+               public int toNoteNumber() { return noteNumber; }\r
+               /**\r
+                * この音階の文字列表現として音名を返します。\r
+                * @return この音階の文字列表現\r
+                */\r
+               @Override\r
+               public String toString() {\r
+                       return toStringIn(SymbolLanguage.SYMBOL, false);\r
+               }\r
+               /**\r
+                * 指定した言語モードにおける文字列表現を返します。\r
+                * @param language 言語モード\r
+                * @return 文字列表現\r
+                */\r
+               public String toStringIn(SymbolLanguage language) {\r
+                       return toStringIn(language, false);\r
+               }\r
+               /**\r
+                * 指定した言語モードとメジャーマイナー種別における文字列表現を返します。\r
+                * <p>マイナーが指定された場合、\r
+                * 五度圏インデックス値を3つ進めた音階として返します。\r
+                * 例えば、{@link #toCo5()} の戻り値が0の場合、\r
+                * メジャーが指定されていれば C を返しますが、\r
+                * マイナーが指定されると A を返します。\r
+                * これにより、同じ五度圏インデックス値0で C と Am の両方のルート音を導出できます。\r
+                * </p>\r
+                * @param language 言語モード\r
+                * @param isMinor マイナーのときtrue\r
+                * @return 文字列表現\r
+                */\r
+               public String toStringIn(SymbolLanguage language, boolean isMinor) {\r
+                       int co5_s771 = majorCo5 + 15; // Shift 7 + 7 + 1 = 15 steps\r
+                       if( isMinor ) {\r
+                               // When co5 is for minor (key or chord), shift 3 steps more\r
+                               co5_s771 += 3;\r
+                       }\r
+                       if( co5_s771 < 0 || co5_s771 >= 35 ) {\r
+                               //\r
+                               // 35種類の音名の範囲に入らないような値が来てしまった場合は、\r
+                               // それを調号として見たときに 5b ~ 6# の範囲に収まるような異名同音(enharmonic)に置き換える。\r
+                               //\r
+                               co5_s771 = mod12(co5_s771);  // returns 0(Fbb) ... 7(Fb) 8(Cb) 9(Gb) 10(Db) 11(Ab)\r
+                               if( isMinor ) {\r
+                                       if( co5_s771 == 0 )\r
+                                               co5_s771 += SEMITONES_PER_OCTAVE * 2; // 0(Fbbm)+24 = 24(D#m)\r
+                                       else\r
+                                               co5_s771 += SEMITONES_PER_OCTAVE;  // 1(Cbbm)+12 = 13(Bbm)\r
+                               }\r
+                               else {\r
+                                       if( co5_s771 < 10 )\r
+                                               co5_s771 += SEMITONES_PER_OCTAVE;  // 0(Fbb)+12 = 12(Eb), 9(Gb)+12 = 21(F#)\r
+                               }\r
+                       }\r
+                       int sharpFlatIndex = co5_s771 / 7;\r
+                       int note_index = co5_s771 - sharpFlatIndex * 7;\r
+                       String note = language.notes.substring( note_index, note_index+1 );\r
+                       String sharp_flat = language.sharpFlatList.get(sharpFlatIndex);\r
+                       return language.preSharpFlat ? sharp_flat + note : note + sharp_flat;\r
+               }\r
+               /**\r
+                * 指定の最大文字数の範囲で、MIDIノート番号が示す音名を返します。\r
+                * <p>ノート番号だけでは物理的な音階情報しか得られないため、\r
+                * 白鍵で#♭のついた音階表現(B#、Cb など)、\r
+                * ダブルシャープ、ダブルフラットを使った表現は返しません。\r
+                * </p>\r
+                * <p>白鍵の場合は A ~ G までの文字、黒鍵の場合は#と♭の両方の表現を返します。\r
+                * ただし、制限文字数の指定により、#と♭の両方を返せないことがわかった場合は、\r
+                * 五度圏で値0(キー C / Am)からの距離が、メジャー、マイナーの両方を含めて\r
+                * 近くにあるほうの表現(C# Eb F# Ab Bb)のみを返します。\r
+                * </p>\r
+                * @param noteNo MIDIノート番号\r
+                * @param maxChars 最大文字数\r
+                * @return MIDIノート番号が示す音名\r
+                */\r
+               public static String noteNumberToSymbol(int noteNo, int maxChars) {\r
+                       int co5 = mod12(reverseCo5(noteNo));\r
+                       if( co5 == 11 ) {\r
+                               return (new NoteSymbol(-1)).toString();\r
+                       }\r
+                       else if( co5 >= 6 ) {\r
+                               if( maxChars >= 7 ) {\r
+                                       return\r
+                                               (new NoteSymbol(co5)).toString() + " / " +\r
+                                               (new NoteSymbol(co5 - 12)).toString();\r
+                               }\r
+                               else {\r
+                                       // String capacity not enough\r
+                                       // Select only one note (sharped or flatted)\r
+                                       return (new NoteSymbol(co5 - ((co5 >= 8) ? 12 : 0))).toString();\r
+                               }\r
+                       }\r
+                       else return (new NoteSymbol(co5)).toString();\r
+               }\r
+               /**\r
+                * 最大256文字の範囲で、MIDIノート番号が示す音名を返します。\r
+                * <p>内部的には\r
+                * {@link #noteNumberToSymbol(int, int)} を呼び出しているだけです。\r
+                * </p>\r
+                * @param noteNo MIDIノート番号\r
+                * @return MIDIノート番号が示す音名\r
+                */\r
+               public static String noteNoToSymbol(int noteNo) {\r
+                       return noteNumberToSymbol(noteNo, 256);\r
+               }\r
+               /**\r
+                * 指定された五度圏インデックス値(メジャーキー基準)を\r
+                * ノート番号(0~11)に変換します。\r
+                *\r
+                * <p>これはMIDIノート番号からオクターブ情報を抜いた値と同じです。\r
+                * 五度圏インデックス値をノート番号に変換した場合、\r
+                * 異名同音、すなわち同じ音階が♭表記、♯表記のどちらだったか\r
+                * という情報は失われます。\r
+                * </p>\r
+                * @return ノート番号(0~11)\r
+                */\r
+               public static int toNoteNumber(int majorCo5) {\r
+                       return mod12(reverseCo5(majorCo5));\r
+               }\r
+       }\r
+\r
+       /**\r
+        * MIDIノート番号と、\r
+        * メジャーキー基準の五度圏インデックス値との間で変換を行います。\r
+        * <p>このメソッドで両方向の変換に対応しています。\r
+        * 内部的には、元の値が奇数のときに6(1オクターブ12半音の半分)を足し、\r
+        * 偶数のときにそのまま返しているだけです。\r
+        * 値は0~11であるとは限りません。その範囲に補正したい場合は\r
+        *  {@link #mod12(int)} を併用します。\r
+        * </p>\r
+        *\r
+        * @param n 元の値\r
+        * @return 変換結果\r
+        */\r
+       public static int reverseCo5(int n) { // Swap C D E <-> Gb Ab Bb\r
+               return (n & 1) == 0 ? n : n+6 ;\r
+       }\r
+       /**\r
+        * ノート番号からオクターブ成分を抜きます。\r
+        * <p>n % 12 と似ていますが、Java の % 演算子では、\r
+        * 左辺に負数を与えると答えも負数になってしまうため、n % 12 で計算しても\r
+        * 0~11 の範囲を外れてしまうことがあります。そこで、\r
+        * 負数の場合に12を足すことにより 0~11 の範囲に入るよう補正します。\r
+        * </p>\r
+        * @param n 元のノート番号\r
+        * @return オクターブ成分を抜いたノート番号(0~11)\r
+        */\r
+       public static int mod12(int n) {\r
+               int qn = n % SEMITONES_PER_OCTAVE;\r
+               return qn < 0 ? qn + 12 : qn ;\r
+       }\r
+       /**\r
+        * 指定のMIDIノート番号に対応する、A=440Hzとした場合の音の周波数を返します。\r
+        * @param noteNo MIDIノート番号\r
+        * @return A=440Hzとした場合の音の周波数[Hz]\r
+        */\r
+       public static double noteNoToFrequency(int noteNo) {\r
+               // Returns frequency when A=440Hz\r
+               return 55 * Math.pow( 2, (double)(noteNo - 33)/12 );\r
+       }\r
+       /**\r
+        * MIDIノート番号の示す音階が、\r
+        * 指定された調(五度圏インデックス値)におけるスケール構成音に\r
+        * 該当するか調べます。\r
+        *\r
+        * <p>例えば、調の五度圏インデックス値を0にした場合(ハ長調またはイ短調)、\r
+        * 白鍵のときにtrue、黒鍵のときにfalseを返すので、鍵盤の描画にも便利です。\r
+        * </p>\r
+        *\r
+        * <p>キーは五度圏インデックス値しか指定しないので、\r
+        * マイナーキーの場合は\r
+        * 平行調のメジャーキーと同じスケール構成音をもつ\r
+        * ナチュラルマイナースケールとして判断されます。\r
+        * ハーモニックマイナー、メロディックマイナースケールについては、\r
+        * 別途それを指定する手段がないと判別できないので対応していません。\r
+        * </p>\r
+        *\r
+        * @param noteNo 調べたい音階のノート番号\r
+        * @param keyCo5 キーの五度圏インデックス値\r
+        * @return スケール構成音のときtrue、スケールを外れている場合false\r
+        */\r
+       public static boolean isOnScale(int noteNo, int keyCo5) {\r
+               return mod12(reverseCo5(noteNo) - keyCo5 + 1) < 7 ;\r
+       }\r
+       /**\r
+        * 五度圏インデックス値で表された音階を、\r
+        * 指定された半音数だけ移調した結果を返します。\r
+        *\r
+        * <p>移調する半音数が0の場合、指定の五度圏インデックス値をそのまま返します。\r
+        * 例えば五度圏インデックス値が +7 の場合、-5 ではなく +7 を返します。\r
+        * </p>\r
+        *\r
+        * @param co5 五度圏インデックス値\r
+        * @param chromaticOffset 移調する半音数\r
+        * @return 移調結果(-5 ~ 6)\r
+        */\r
+       public static int transposeCo5(int co5, int chromaticOffset) {\r
+               if( chromaticOffset == 0 ) return co5;\r
+               int transposed_co5 = mod12( co5 + reverseCo5(chromaticOffset) );\r
+               if( transposed_co5 > 6 ) transposed_co5 -= Music.SEMITONES_PER_OCTAVE;\r
+               return transposed_co5; // range: -5 to +6\r
+       }\r
+       /**\r
+        * 指定の五度圏インデックス値の真裏にあたる値を返します。\r
+        * @param co5 五度圏インデックス値\r
+        * @return 真裏の五度圏インデックス値\r
+        */\r
+       public static int oppositeCo5(int co5) {\r
+               return co5 > 0 ? co5 - 6 : co5 + 6;\r
+       }\r
+\r
+       /**\r
+        * 音域を表すクラスです。\r
+        */\r
+       public static class Range {\r
+               public int min_note = 0;\r
+               public int max_note = MIDISpec.MAX_NOTE_NO;\r
+               public int min_key_offset = 0;\r
+               public boolean is_inversion_mode = true;\r
+               public Range( int min_note, int max_note ) {\r
+                       this.min_note = min_note;\r
+                       this.max_note = max_note;\r
+               }\r
+               public Range( Integer[] notes ) {\r
+                       if( notes == null ) return;\r
+                       switch( notes.length ) {\r
+                       case 0: return;\r
+                       case 1:\r
+                               min_note = max_note = notes[0];\r
+                               break;\r
+                       default:\r
+                               if( notes[0] > notes[1] ) {\r
+                                       min_note = notes[1];\r
+                                       max_note = notes[0];\r
+                               }\r
+                               else {\r
+                                       min_note = notes[0];\r
+                                       max_note = notes[1];\r
+                               }\r
+                               break;\r
+                       }\r
+               }\r
+               public Range(\r
+                       int min_note, int max_note,\r
+                       int min_key_offset, boolean inv_mode\r
+               ) {\r
+                       this.min_note = min_note;\r
+                       this.max_note = max_note;\r
+                       this.min_key_offset = min_key_offset;\r
+                       this.is_inversion_mode = inv_mode;\r
+               }\r
+               public int invertedNoteOf(int note_no) {\r
+                       return invertedNoteOf( note_no, null );\r
+               }\r
+               public int invertedNoteOf(int note_no, Key key) {\r
+                       int min_note = this.min_note;\r
+                       int max_note = this.max_note;\r
+                       int offset = 0;\r
+                       if( key != null ) {\r
+                               offset = key.relativeDo();\r
+                               if( min_key_offset < 0 && offset >= mod12(min_key_offset) ) {\r
+                                       offset -= 12;\r
+                               }\r
+                               else if( min_key_offset > 0 && offset < mod12(min_key_offset) ) {\r
+                                       offset += 12;\r
+                               }\r
+                               min_note += offset;\r
+                               max_note += offset;\r
+                       }\r
+                       int octave = min_note / SEMITONES_PER_OCTAVE;\r
+                       note_no += 12 * octave;\r
+                       while( note_no > max_note )\r
+                               note_no -= SEMITONES_PER_OCTAVE;\r
+                       while( note_no > MIDISpec.MAX_NOTE_NO )\r
+                               note_no -= SEMITONES_PER_OCTAVE;\r
+                       while( note_no < min_note )\r
+                               note_no += SEMITONES_PER_OCTAVE;\r
+                       while( note_no < 0 )\r
+                               note_no += SEMITONES_PER_OCTAVE;\r
+                       return note_no;\r
+               }\r
+               public void invertNotesOf( int[] notes, Key key ) {\r
+                       int i;\r
+                       if( is_inversion_mode ) {\r
+                               for( i=0; i<notes.length; i++ ) {\r
+                                       notes[i] = invertedNoteOf( notes[i], key );\r
+                               }\r
+                       }\r
+                       else {\r
+                               int n = invertedNoteOf( notes[0], new Key(min_key_offset) );\r
+                               int n_diff = n - notes[0];\r
+                               notes[0] = n;\r
+                               for( i=1; i<notes.length; i++ ) {\r
+                                       notes[i] += n_diff;\r
+                               }\r
+                       }\r
+               }\r
+       }\r
+\r
+       /**\r
+        * 調(キー)を表すクラスです。\r
+        *\r
+        * <p>内部的には次の値を持っています。</p>\r
+        * <ul>\r
+        * <li>五度圏インデックス値。これは調号の♯の数(♭の数は負数)と同じです。</li>\r
+        * <li>メジャー/マイナーの区別(無指定ありの3値)</li>\r
+        * </ul>\r
+        * <p>これらの値はMIDIのメタメッセージにある調号のパラメータに対応します。\r
+        * </p>\r
+        */\r
+       public static class Key implements Cloneable {\r
+               /**\r
+                * メジャーかマイナーかが特定できていないことを示す値\r
+                */\r
+               public static final int MAJOR_OR_MINOR = 0;\r
+               /**\r
+                * メジャーキー(長調)\r
+                */\r
+               public static final int MAJOR = 1;\r
+               /**\r
+                * マイナーキー(短調)\r
+                */\r
+               public static final int MINOR = -1;\r
+               /**\r
+                * この調の五度圏インデックス値\r
+                */\r
+               private int co5;\r
+               /**\r
+                * メジャー・マイナーの種別\r
+                */\r
+               private int majorMinor;\r
+               /**\r
+                * 調号が空のキー(ハ長調またはイ短調)を構築します。\r
+                */\r
+               public Key() { setKey(0, MAJOR_OR_MINOR); }\r
+               /**\r
+                * 指定の五度圏インデックス値を持つ調を、\r
+                * メジャーとマイナーを指定せずに構築します。\r
+                *\r
+                * @param co5 五度圏インデックス値\r
+                */\r
+               public Key(int co5) { setKey(co5, MAJOR_OR_MINOR); }\r
+               /**\r
+                * 指定の五度圏インデックス値を持つ、\r
+                * メジャー/マイナーを指定した調を構築します。\r
+                *\r
+                * @param co5 五度圏インデックス値\r
+                * @param majorMinor {@link #MAJOR}、{@link #MINOR}、{@link #MAJOR_OR_MINOR} のいずれか\r
+                */\r
+               public Key(int co5, int majorMinor) {\r
+                       setKey(co5, majorMinor);\r
+               }\r
+               /**\r
+                * 指定の五度圏インデックス値を持つ、\r
+                * メジャー/マイナーの明確な調を構築します。\r
+                *\r
+                * @param co5 五度圏インデックス値\r
+                * @param isMinor true:マイナー、false:メジャー\r
+                */\r
+               public Key(int co5, boolean isMinor) {\r
+                       setKey(co5, isMinor);\r
+               }\r
+               /**\r
+                * MIDIの調データ(メタメッセージ2byte)から調を構築します。\r
+                * @param midiData MIDIの調データ\r
+                */\r
+               public Key(byte midiData[]) {\r
+                       setBytes(midiData);\r
+               }\r
+               /**\r
+                * C、Am のような文字列から調を構築します。\r
+                * @param keySymbol キーを表す文字列\r
+                */\r
+               public Key(String keySymbol) {\r
+                       boolean isMinor = keySymbol.matches(".*m");\r
+                       setKey((new NoteSymbol(keySymbol)).toCo5(isMinor), isMinor);\r
+               }\r
+               /**\r
+                * 指定されたコードと同名の調を構築します。\r
+                * @param chord コード(和音)\r
+                */\r
+               public Key(Chord chord) {\r
+                       boolean isMinor = chord.isMinor();\r
+                       setKey(chord.rootNoteSymbol().toCo5(isMinor), isMinor);\r
+               }\r
+               @Override\r
+               protected Key clone() {\r
+                       return new Key(co5, majorMinor);\r
+               }\r
+               @Override\r
+               public boolean equals(Object anObject) {\r
+                       if( this == anObject )\r
+                               return true;\r
+                       if( anObject instanceof Key ) {\r
+                               Key another = (Key) anObject;\r
+                               return\r
+                                       co5 == another.toCo5() &&\r
+                                       majorMinor == another.majorMinor() ;\r
+                       }\r
+                       return false;\r
+               }\r
+               @Override\r
+               public int hashCode() {\r
+                       return majorMinor * 256 + co5 ;\r
+               }\r
+               private void setKey(int co5, boolean isMinor) {\r
+                       setKey( co5, isMinor ? MINOR : MAJOR );\r
+               }\r
+               private void setKey(int co5, int majorMinor) {\r
+                       this.co5 = co5;\r
+                       this.majorMinor = majorMinor;\r
+                       normalize();\r
+               }\r
+               /**\r
+                * MIDIの調データ(メタメッセージ2byte)を設定します。\r
+                * @param data MIDIの調データ\r
+                */\r
+               public void setBytes( byte[] data ) {\r
+                       byte sharpFlat = data.length > 0 ? data[0] : 0;\r
+                       byte isMinor = data.length > 1 ? data[1] : 0;\r
+                       setKey( (int)sharpFlat, isMinor==1 );\r
+               }\r
+               /**\r
+                * MIDIの調データ(メタメッセージ2byte)を生成して返します。\r
+                * @return  MIDIの調データ\r
+                */\r
+               public byte[] getBytes() {\r
+                       byte data[] = new byte[2];\r
+                       data[0] = (byte)(co5 & 0xFF);\r
+                       data[1] = (byte)(majorMinor == MINOR ? 1 : 0);\r
+                       return data;\r
+               }\r
+               /**\r
+                * 五度圏インデックス値を返します。\r
+                * @return 五度圏インデックス値\r
+                */\r
+               public int toCo5() { return co5; }\r
+               /**\r
+                * メジャー/マイナーの区別を返します。\r
+                * @return {@link #MAJOR}、{@link #MINOR}、{@link #MAJOR_OR_MINOR} のいずれか\r
+                */\r
+               public int majorMinor() { return majorMinor; }\r
+               /**\r
+                * 相対ドの音階を返します。\r
+                * @return 相対ドの音階(0~11)\r
+                */\r
+               public int relativeDo() {\r
+                       return NoteSymbol.toNoteNumber(co5);\r
+               }\r
+               /**\r
+                * この調のルート音を返します。\r
+                * メジャーキーの場合は相対ド、\r
+                * マイナーキーの場合は相対ラの音階です。\r
+                *\r
+                * @return キーのルート音(0~11)\r
+                */\r
+               public int rootNoteNumber() {\r
+                       int n = relativeDo();\r
+                       return majorMinor==MINOR ? mod12(n-3) : n;\r
+               }\r
+               /**\r
+                * 指定されたノート番号の音が、この調のスケールの構成音か調べます。\r
+                * メジャーキーの場合はメジャースケール、\r
+                * マイナーキーの場合はナチュラルマイナースケールとして判断されます。\r
+                *\r
+                * @param noteNumber ノート番号\r
+                * @return 指定されたノート番号がこのキーのスケールの構成音ならtrue\r
+                */\r
+               public boolean isOnScale(int noteNumber) {\r
+                       return Music.isOnScale(noteNumber, co5);\r
+               }\r
+               /**\r
+                * この調を、指定された半音オフセット値だけ移調します。\r
+                *\r
+                * @param chromaticOffset 半音オフセット値\r
+                * @return このオブジェクト自身(移調後)\r
+                */\r
+               public Key transpose(int chromaticOffset) {\r
+                       co5 = transposeCo5(co5, chromaticOffset);\r
+                       return this;\r
+               }\r
+               /**\r
+                * この調に異名同音の調がある場合、その調に置換します。\r
+                * <p>例えば、♭5個(D♭メジャー)の場合は♯7個(C♯メジャー)に置換されます。\r
+                * 異名同音の調が存在しないキー(4♯~4♭)に対してこのメソッドを呼び出しても、\r
+                * 何も変化しません。\r
+                * </p>\r
+                */\r
+               public void toggleEnharmonically() {\r
+                       if( co5 > 4 )\r
+                               co5 -= 12;\r
+                       else if( co5 < -4 )\r
+                               co5 += 12;\r
+               }\r
+               /**\r
+                * この調を正規化します。\r
+                * 調が7♭~7♯の範囲に入っていない場合、\r
+                * その範囲に入るよう調整されます。\r
+                */\r
+               public void normalize() {\r
+                       if( co5 < -7 || co5 > 7 ) {\r
+                               co5 = Music.mod12( co5 );\r
+                               if( co5 > 6 ) co5 -= SEMITONES_PER_OCTAVE;\r
+                       }\r
+               }\r
+               /**\r
+                * 平行調を生成して返します。\r
+                * これは元の調と同じ調号を持つ、メジャーとマイナーが異なる調です。\r
+                *\r
+                * <p>メジャーとマイナーの区別が不明な場合、クローンを生成したのと同じことになります。\r
+                * </p>\r
+                *\r
+                * @return 平行調\r
+                */\r
+               public Key relativeKey() {\r
+                       return new Key(co5, majorMinor * (-1));\r
+               }\r
+               /**\r
+                * 同主調を生成して返します。\r
+                * これは元の調とルート音が同じで、メジャーとマイナーが異なる調です。\r
+                *\r
+                * <p>メジャーとマイナーの区別が不明な場合、クローンを生成したのと同じことになります。\r
+                * 元の調の♭、♯の数が5個以上の場合、\r
+                * 7♭~7♯の範囲をはみ出すことがあります(正規化は行われません)。\r
+                * </p>\r
+                *\r
+                * @return 同主調\r
+                */\r
+               public Key parallelKey() {\r
+                       switch( majorMinor ) {\r
+                       case MAJOR: return new Key( co5-3, MINOR );\r
+                       case MINOR: return new Key( co5+3, MAJOR );\r
+                       default: return new Key(co5);\r
+                       }\r
+               }\r
+               /**\r
+                * 五度圏で真裏にあたる調を生成して返します。\r
+                * @return 五度圏で真裏にあたる調\r
+                */\r
+               public Key oppositeKey() {\r
+                       return new Key(Music.oppositeCo5(co5), majorMinor);\r
+               }\r
+               /**\r
+                * この調の文字列表現を C、Am のような形式で返します。\r
+                * @return この調の文字列表現\r
+                */\r
+               @Override\r
+               public String toString() {\r
+                       return toStringIn(SymbolLanguage.SYMBOL);\r
+               }\r
+               /**\r
+                * この調の文字列表現を、指定された形式で返します。\r
+                * @return この調の文字列表現\r
+                */\r
+               public String toStringIn(SymbolLanguage language) {\r
+                       NoteSymbol note = new NoteSymbol(co5);\r
+                       String major = note.toStringIn(language, false) + language.major;\r
+                       if( majorMinor > 0 ) {\r
+                               return major;\r
+                       }\r
+                       else {\r
+                               String minor = note.toStringIn(language, true) + language.minor;\r
+                               return majorMinor < 0 ?\r
+                                       minor : major + language.majorMinorDelimiter + minor ;\r
+                       }\r
+               }\r
+               /**\r
+                * 調号を表す半角文字列を返します。\r
+                * 正規化された状態において最大2文字になるよう調整されます。\r
+                *\r
+                * @return 調号を表す半角文字列\r
+                */\r
+               public String signature() {\r
+                       switch(co5) {\r
+                       case  0: return "==";\r
+                       case  1: return "#";\r
+                       case -1: return "b";\r
+                       case  2: return "##";\r
+                       case -2: return "bb";\r
+                       default:\r
+                               if( co5 >= 3 && co5 <= 7 ) return co5 + "#" ;\r
+                               else if( co5 <= -3 && co5 >= -7 ) return (-co5) + "b" ;\r
+                               return "";\r
+                       }\r
+               }\r
+               /**\r
+                * 調号の説明(英語)を返します。\r
+                * @return 調号の説明\r
+                */\r
+               public String signatureDescription() {\r
+                       switch(co5) {\r
+                       case  0: return "no sharps or flats";\r
+                       case  1: return "1 sharp";\r
+                       case -1: return "1 flat";\r
+                       default: return co5 < 0 ? (-co5) + " flats" : co5 + " sharps" ;\r
+                       }\r
+               }\r
+       }\r
+\r
+       /**\r
+        * 和音(コード - musical chord)のクラス\r
+        */\r
+       public static class Chord implements Cloneable {\r
+               /**\r
+                * コード構成音の順序に対応する色\r
+                */\r
+               public static final Color NOTE_INDEX_COLORS[] = {\r
+                       Color.red,\r
+                       new Color(0x40,0x40,0xFF),\r
+                       Color.orange.darker(),\r
+                       new Color(0x20,0x99,0x00),\r
+                       Color.magenta,\r
+                       Color.orange,\r
+                       Color.green\r
+               };\r
+               /**\r
+                * ルート音の半音差(ないのと同じ)\r
+                */\r
+               public static final int ROOT = 0;\r
+               /**\r
+                * 長2度の半音差\r
+                */\r
+               public static final int SUS2 = 2;\r
+               /**\r
+                * 短3度または増2度の半音差\r
+                */\r
+               public static final int MINOR = 3;\r
+               /**\r
+                * 長3度の半音差\r
+                */\r
+               public static final int MAJOR = 4;\r
+               /**\r
+                * 完全4度の半音差\r
+                */\r
+               public static final int SUS4 = 5;       //\r
+               /**\r
+                * 減5度または増4度の半音差(トライトーン = 三全音 = 半オクターブ)\r
+                */\r
+               public static final int FLAT5 = 6;\r
+               /**\r
+                * 完全5度の半音差\r
+                */\r
+               public static final int PARFECT5 = 7;\r
+               /**\r
+                * 増5度または短6度の半音差\r
+                */\r
+               public static final int SHARP5 = 8;\r
+               /**\r
+                * 長6度または減7度の半音差\r
+                */\r
+               public static final int SIXTH = 9;\r
+               /**\r
+                * 短7度の半音差\r
+                */\r
+               public static final int SEVENTH = 10;\r
+               /**\r
+                * 長7度の半音差\r
+                */\r
+               public static final int MAJOR_SEVENTH = 11;\r
+               /**\r
+                * 短9度(短2度の1オクターブ上)の半音差\r
+                */\r
+               public static final int FLAT9 = 13;\r
+               /**\r
+                * 長9度(長2度の1オクターブ上)の半音差\r
+                */\r
+               public static final int NINTH = 14;\r
+               /**\r
+                * 増9度(増2度の1オクターブ上)の半音差\r
+                */\r
+               public static final int SHARP9 = 15;\r
+               /**\r
+                * 完全11度(完全4度の1オクターブ上)の半音差\r
+                */\r
+               public static final int ELEVENTH = 17;\r
+               /**\r
+                * 増11度(増4度の1オクターブ上)の半音差\r
+                */\r
+               public static final int SHARP11 = 18;\r
+               /**\r
+                * 短13度(短6度の1オクターブ上)の半音差\r
+                */\r
+               public static final int FLAT13 = 20;\r
+               /**\r
+                * 長13度(長6度の1オクターブ上)の半音差\r
+                */\r
+               public static final int THIRTEENTH = 21;\r
+               //\r
+               // index\r
+               public static final int THIRD_OFFSET    = 0;    // 3度\r
+               public static final int FIFTH_OFFSET    = 1;    // 5度\r
+               public static final int SEVENTH_OFFSET  = 2;    // 7度\r
+               public static final int NINTH_OFFSET    = 3;    // 9度\r
+               public static final int ELEVENTH_OFFSET = 4;    // 11度\r
+               public static final int THIRTEENTH_OFFSET       = 5;    // 13度\r
+               /**\r
+                * デフォルトの半音値(メジャーコード固定)\r
+                */\r
+               public static int DEFAULT_OFFSETS[] = {\r
+                       MAJOR, PARFECT5, ROOT, ROOT, ROOT, ROOT,\r
+               };\r
+               /**\r
+                * このコードのルート音\r
+                */\r
+               private NoteSymbol rootNoteSymbol;\r
+               /**\r
+                * このコードのベース音(ルート音と異なる場合は分数コードの分母)\r
+                */\r
+               private NoteSymbol bassNoteSymbol;\r
+               /**\r
+                * 現在の半音値\r
+                */\r
+               public int offsets[] =\r
+                       Arrays.copyOf(DEFAULT_OFFSETS, DEFAULT_OFFSETS.length);\r
+               /**\r
+                * コード C major を構築します。\r
+                */\r
+               public Chord() {\r
+                       this(new NoteSymbol());\r
+               }\r
+               /**\r
+                * 指定した音名のメジャーコードを構築します。\r
+                * @param noteSymbol 音名\r
+                */\r
+               public Chord(NoteSymbol noteSymbol) {\r
+                       setRoot(noteSymbol);\r
+                       setBass(noteSymbol);\r
+               }\r
+               /**\r
+                * 指定された調と同名のコードを構築します。\r
+                * <p>元の調がマイナーキーの場合はマイナーコード、\r
+                * それ以外の場合はメジャーコードになります。\r
+                * </p>\r
+                * @param key 調\r
+                */\r
+               public Chord(Key key) {\r
+                       int keyCo5 = key.toCo5();\r
+                       if( key.majorMinor() == MINOR ) {\r
+                               keyCo5 += 3;\r
+                               setMinorThird();\r
+                       }\r
+                       setRoot(new NoteSymbol(keyCo5));\r
+                       setBass(new NoteSymbol(keyCo5));\r
+               }\r
+               /**\r
+                * コード名の文字列からコードを構築します。\r
+                * @param chordSymbol コード名の文字列\r
+                */\r
+               public Chord(String chordSymbol) {\r
+                       setChordSymbol(chordSymbol);\r
+               }\r
+               /**\r
+                * このコードのクローンを作成します。\r
+                */\r
+               @Override\r
+               protected Chord clone() {\r
+                       Chord newChord = new Chord(rootNoteSymbol);\r
+                       newChord.offsets = Arrays.copyOf( offsets, offsets.length );\r
+                       newChord.setBass(bassNoteSymbol);\r
+                       return newChord;\r
+               }\r
+               /**\r
+                * コードのルート音を指定された音階に置換します。\r
+                * @param rootNoteSymbol 音階\r
+                * @return このコード自身(置換後)\r
+                */\r
+               public Chord setRoot(NoteSymbol rootNoteSymbol) {\r
+                       this.rootNoteSymbol = rootNoteSymbol;\r
+                       return this;\r
+               }\r
+               /**\r
+                * コードのベース音を指定された音階に置換します。\r
+                * @param rootNoteSymbol 音階\r
+                * @return このコード自身(置換後)\r
+                */\r
+               public Chord setBass(NoteSymbol rootNoteSymbol) {\r
+                       this.bassNoteSymbol = rootNoteSymbol;\r
+                       return this;\r
+               }\r
+               // コードの種類を設定します。\r
+               //\r
+               public void setMajorThird() { offsets[THIRD_OFFSET] = MAJOR; }\r
+               public void setMinorThird() { offsets[THIRD_OFFSET] = MINOR; }\r
+               public void setSus4() { offsets[THIRD_OFFSET] = SUS4; }\r
+               public void setSus2() { offsets[THIRD_OFFSET] = SUS2; }\r
+               //\r
+               public void setParfectFifth() { offsets[FIFTH_OFFSET] = PARFECT5; }\r
+               public void setFlattedFifth() { offsets[FIFTH_OFFSET] = FLAT5; }\r
+               public void setSharpedFifth() { offsets[FIFTH_OFFSET] = SHARP5; }\r
+               //\r
+               public void clearSeventh() { offsets[SEVENTH_OFFSET] = ROOT; }\r
+               public void setMajorSeventh() { offsets[SEVENTH_OFFSET] = MAJOR_SEVENTH; }\r
+               public void setSeventh() { offsets[SEVENTH_OFFSET] = SEVENTH; }\r
+               public void setSixth() { offsets[SEVENTH_OFFSET] = SIXTH; }\r
+               //\r
+               public void clearNinth() { offsets[NINTH_OFFSET] = ROOT; }\r
+               public void setNinth() { offsets[NINTH_OFFSET] = NINTH; }\r
+               public void setSharpedNinth() { offsets[NINTH_OFFSET] = SHARP9; }\r
+               public void setFlattedNinth() { offsets[NINTH_OFFSET] = FLAT9; }\r
+               //\r
+               public void clearEleventh() { offsets[ELEVENTH_OFFSET] = ROOT; }\r
+               public void setEleventh() { offsets[ELEVENTH_OFFSET] = ELEVENTH; }\r
+               public void setSharpedEleventh() { offsets[ELEVENTH_OFFSET] = SHARP11; }\r
+               //\r
+               public void clearThirteenth() { offsets[THIRTEENTH_OFFSET] = ROOT; }\r
+               public void setThirteenth() { offsets[THIRTEENTH_OFFSET] = THIRTEENTH; }\r
+               public void setFlattedThirteenth() { offsets[THIRTEENTH_OFFSET] = FLAT13; }\r
+               //\r
+               // コードネームの文字列が示すコードに置き換えます。\r
+               public Chord setChordSymbol(String chord_symbol) {\r
+                       //\r
+                       // 分数コードの分子と分母に分ける\r
+                       String parts[] = chord_symbol.trim().split("(/|on)");\r
+                       if( parts.length == 0 ) {\r
+                               return this;\r
+                       }\r
+                       // ルート音とベース音を設定\r
+                       setRoot(new NoteSymbol(parts[0]));\r
+                       setBass(new NoteSymbol(parts[ parts.length > 1 ? 1 : 0 ]));\r
+                       String suffix = parts[0].replaceFirst("^[A-G][#bx]*","");\r
+                       //\r
+                       // () があれば、その中身を取り出す\r
+                       String suffixParts[] = suffix.split("[\\(\\)]");\r
+                       if( suffixParts.length == 0 ) {\r
+                               return this;\r
+                       }\r
+                       String suffixParen = "";\r
+                       if( suffixParts.length > 1 ) {\r
+                               suffixParen = suffixParts[1];\r
+                               suffix = suffixParts[0];\r
+                       }\r
+                       //\r
+                       // +5 -5 aug dim の判定\r
+                       offsets[FIFTH_OFFSET] = (\r
+                               suffix.matches( ".*(\\+5|aug|#5).*" ) ? SHARP5 :\r
+                               suffix.matches( ".*(-5|dim|b5).*" ) ? FLAT5 :\r
+                               PARFECT5\r
+                       );\r
+                       // 6 7 M7 の判定\r
+                       offsets[SEVENTH_OFFSET] = (\r
+                               suffix.matches( ".*(M7|maj7|M9|maj9).*" ) ? MAJOR_SEVENTH :\r
+                               suffix.matches( ".*(6|dim[79]).*" ) ? SIXTH :\r
+                               suffix.matches( ".*7.*" ) ? SEVENTH :\r
+                               ROOT\r
+                       );\r
+                       // マイナーの判定。maj7 と間違えないように比較\r
+                       offsets[THIRD_OFFSET] = (\r
+                               (suffix.matches( ".*m.*" ) && ! suffix.matches(".*ma.*") ) ? MINOR :\r
+                               suffix.matches( ".*sus4.*" ) ? SUS4 :\r
+                               MAJOR\r
+                       );\r
+                       // 9th の判定\r
+                       if( suffix.matches( ".*9.*" ) ) {\r
+                               offsets[NINTH_OFFSET] = NINTH;\r
+                               if( ! suffix.matches( ".*(add9|6|M9|maj9|dim9).*") ) {\r
+                                       offsets[SEVENTH_OFFSET] = SEVENTH;\r
+                               }\r
+                       }\r
+                       else {\r
+                               offsets[NINTH_OFFSET] =\r
+                               offsets[ELEVENTH_OFFSET] =\r
+                               offsets[THIRTEENTH_OFFSET] = ROOT;\r
+                               //\r
+                               // () の中を , で分ける\r
+                               String parts_in_paren[] = suffixParen.split(",");\r
+                               for( String p : parts_in_paren ) {\r
+                                       if( p.matches("(\\+9|#9)") ) offsets[NINTH_OFFSET] = SHARP9;\r
+                                       else if( p.matches("(-9|b9)") ) offsets[NINTH_OFFSET] = FLAT9;\r
+                                       else if( p.matches("9") ) offsets[NINTH_OFFSET] = NINTH;\r
+\r
+                                       if( p.matches("(\\+11|#11)") ) offsets[ELEVENTH_OFFSET] = SHARP11;\r
+                                       else if( p.matches("11") ) offsets[ELEVENTH_OFFSET] = ELEVENTH;\r
+\r
+                                       if( p.matches("(-13|b13)") ) offsets[THIRTEENTH_OFFSET] = FLAT13;\r
+                                       else if( p.matches("13") ) offsets[THIRTEENTH_OFFSET] = THIRTEENTH;\r
+\r
+                                       // -5 や +5 が () の中にあっても解釈できるようにする\r
+                                       if( p.matches("(-5|b5)") ) offsets[FIFTH_OFFSET] = FLAT5;\r
+                                       else if( p.matches("(\\+5|#5)") ) offsets[FIFTH_OFFSET] = SHARP5;\r
+                               }\r
+                       }\r
+                       return this;\r
+               }\r
+               /**\r
+                * ルート音を返します。\r
+                * @return ルート音\r
+                */\r
+               public NoteSymbol rootNoteSymbol() { return rootNoteSymbol; }\r
+               /**\r
+                * ベース音を返します。分数コードの場合はルート音と異なります。\r
+                * @return ベース音\r
+                */\r
+               public NoteSymbol bassNoteSymbol() { return bassNoteSymbol; }\r
+               //\r
+               // コードの種類を調べます。\r
+               public boolean isMajor() { return offsets[THIRD_OFFSET] == MAJOR; }\r
+               public boolean isMinor() { return offsets[THIRD_OFFSET] == MINOR; }\r
+               public boolean isSus4() { return offsets[THIRD_OFFSET] == SUS4; }\r
+               public boolean isSus2() { return offsets[THIRD_OFFSET] == SUS2; }\r
+               //\r
+               public boolean hasParfectFifth() { return offsets[FIFTH_OFFSET] == PARFECT5; }\r
+               public boolean hasFlattedFifth() { return offsets[FIFTH_OFFSET] == FLAT5; }\r
+               public boolean hasSharpedFifth() { return offsets[FIFTH_OFFSET] == SHARP5; }\r
+               //\r
+               public boolean hasNoSeventh() { return offsets[SEVENTH_OFFSET] == ROOT; }\r
+               public boolean hasSeventh() { return offsets[SEVENTH_OFFSET] == SEVENTH; }\r
+               public boolean hasMajorSeventh() { return offsets[SEVENTH_OFFSET] == MAJOR_SEVENTH; }\r
+               public boolean hasSixth() { return offsets[SEVENTH_OFFSET] == SIXTH; }\r
+               //\r
+               public boolean hasNoNinth() { return offsets[NINTH_OFFSET] == ROOT; }\r
+               public boolean hasNinth() { return offsets[NINTH_OFFSET] == NINTH; }\r
+               public boolean hasFlattedNinth() { return offsets[NINTH_OFFSET] == FLAT9; }\r
+               public boolean hasSharpedNinth() { return offsets[NINTH_OFFSET] == SHARP9; }\r
+               //\r
+               public boolean hasNoEleventh() { return offsets[ELEVENTH_OFFSET] == ROOT; }\r
+               public boolean hasEleventh() { return offsets[ELEVENTH_OFFSET] == ELEVENTH; }\r
+               public boolean hasSharpedEleventh() { return offsets[ELEVENTH_OFFSET] == SHARP11; }\r
+               //\r
+               public boolean hasNoThirteenth() { return offsets[THIRTEENTH_OFFSET] == ROOT; }\r
+               public boolean hasThirteenth() { return offsets[THIRTEENTH_OFFSET] == THIRTEENTH; }\r
+               public boolean hasFlattedThirteenth() { return offsets[THIRTEENTH_OFFSET] == FLAT13; }\r
+               /**\r
+                * コードが等しいかどうかを判定します。\r
+                * @return 等しければtrue\r
+                */\r
+               @Override\r
+               public boolean equals(Object anObject) {\r
+                       if( this == anObject )\r
+                               return true;\r
+                       if( anObject instanceof Chord ) {\r
+                               Chord another = (Chord) anObject;\r
+                               if( ! rootNoteSymbol.equals(another.rootNoteSymbol) )\r
+                                       return false;\r
+                               if( ! bassNoteSymbol.equals(another.bassNoteSymbol) )\r
+                                       return false;\r
+                               return Arrays.equals(offsets, another.offsets);\r
+                       }\r
+                       return false;\r
+               }\r
+               @Override\r
+               public int hashCode() {\r
+                       return toString().hashCode();\r
+               }\r
+               /**\r
+                * コードが等しいかどうかを、異名同音を無視して判定します。\r
+                * @param another 比較対象のコード\r
+                * @return 等しければtrue\r
+                */\r
+               public boolean equalsEnharmonically(Chord another) {\r
+                       if( this == another )\r
+                               return true;\r
+                       if( another == null )\r
+                               return false;\r
+                       if( ! rootNoteSymbol.equalsEnharmonically(another.rootNoteSymbol) )\r
+                               return false;\r
+                       if( ! bassNoteSymbol.equalsEnharmonically(another.bassNoteSymbol) )\r
+                               return false;\r
+                       return Arrays.equals(offsets, another.offsets);\r
+               }\r
+               /**\r
+                * コード構成音の数を返します(ベース音は含まれません)。\r
+                * @return コード構成音の数\r
+                */\r
+               public int numberOfNotes() {\r
+                       int n=1;\r
+                       for( int offset : offsets ) if( offset != ROOT ) n++;\r
+                       return n;\r
+               }\r
+               /**\r
+                * 指定された位置にあるノート番号を返します。\r
+                * @param index 位置(0をルート音とした構成音の順序)\r
+                * @return ノート番号(該当する音がない場合は -1)\r
+                */\r
+               public int noteAt(int index) {\r
+                       int rootnote = rootNoteSymbol.toNoteNumber();\r
+                       if( index == 0 )\r
+                               return rootnote;\r
+                       int i=0;\r
+                       for( int offset : offsets )\r
+                               if( offset != ROOT && ++i == index )\r
+                                       return rootnote + offset;\r
+                       return -1;\r
+               }\r
+               // コード構成音を格納したノート番号の配列を返します。\r
+               //(ベース音は含まれません)\r
+               // 音域が指定された場合、その音域に合わせたノート番号を返します。\r
+               //\r
+               public int[] toNoteArray() {\r
+                       return toNoteArray( (Range)null, (Key)null );\r
+               }\r
+               public int[] toNoteArray(Range range) {\r
+                       return toNoteArray( range, (Key)null );\r
+               }\r
+               public int[] toNoteArray(Range range, Key key) {\r
+                       int rootnote = rootNoteSymbol.toNoteNumber();\r
+                       int ia[] = new int[numberOfNotes()];\r
+                       int i;\r
+                       ia[i=0] = rootnote;\r
+                       for( int offset : offsets )\r
+                               if( offset != ROOT )\r
+                                       ia[++i] = rootnote + offset;\r
+                       if( range != null ) range.invertNotesOf( ia, key );\r
+                       return ia;\r
+               }\r
+               /**\r
+                * MIDI ノート番号が、コードの構成音の何番目(0=ルート音)に\r
+                * あるかを表すインデックス値を返します。\r
+                * 構成音に該当しない場合は -1 を返します。\r
+                * ベース音は検索されません。\r
+                * @param noteNo MIDIノート番号\r
+                * @return 構成音のインデックス値\r
+                */\r
+               public int indexOf(int noteNo) {\r
+                       int relative_note = noteNo - rootNoteSymbol.toNoteNumber();\r
+                       if( mod12(relative_note) == 0 ) return 0;\r
+                       int i=0;\r
+                       for( int offset : offsets ) if( offset != ROOT ) {\r
+                               i++;\r
+                               if( mod12(relative_note - offset) == 0 )\r
+                                       return i;\r
+                       }\r
+                       return -1;\r
+               }\r
+               // 構成音がそのキーのスケールを外れていないか調べます。\r
+               public boolean isOnScaleInKey( Key key ) {\r
+                       return isOnScaleInKey( key.toCo5() );\r
+               }\r
+               public boolean isOnScaleInKey( int key_co5 ) {\r
+                       int rootnote = rootNoteSymbol.toNoteNumber();\r
+                       if( ! isOnScale( rootnote, key_co5 ) )\r
+                               return false;\r
+                       for( int offset : offsets )\r
+                               if( offset != ROOT && ! isOnScale( rootnote + offset, key_co5 ) )\r
+                                       return false;\r
+                       return true;\r
+               }\r
+               //\r
+               // コードを移調します。\r
+               // 移調幅は chromatic_offset で半音単位に指定します。\r
+               // 移調幅が0の場合、自分自身をそのまま返します。\r
+               //\r
+               public Chord transpose(int chromatic_offset) {\r
+                       return transpose(chromatic_offset, 0);\r
+               }\r
+               public Chord transpose(int chromatic_offset, Key original_key) {\r
+                       return transpose(chromatic_offset, original_key.toCo5());\r
+               }\r
+               public Chord transpose(int chromatic_offset, int original_key_co5) {\r
+                       if( chromatic_offset == 0 ) return this;\r
+                       int offsetCo5 = mod12(reverseCo5(chromatic_offset));\r
+                       if( offsetCo5 > 6 ) offsetCo5 -= 12;\r
+                       int key_co5   = original_key_co5 + offsetCo5;\r
+                       //\r
+                       int newRootCo5 = rootNoteSymbol.toCo5() + offsetCo5;\r
+                       int newBassCo5 = bassNoteSymbol.toCo5() + offsetCo5;\r
+                       if( key_co5 > 6 ) {\r
+                               newRootCo5 -= 12;\r
+                               newBassCo5 -= 12;\r
+                       }\r
+                       else if( key_co5 < -5 ) {\r
+                               newRootCo5 += 12;\r
+                               newBassCo5 += 12;\r
+                       }\r
+                       setRoot(new NoteSymbol(newRootCo5));\r
+                       return setBass(new NoteSymbol(newBassCo5));\r
+               }\r
+               /**\r
+                * この和音の文字列表現としてコード名を返します。\r
+                * @return この和音のコード名\r
+                */\r
+               public String toString() {\r
+                       String chordSymbol = rootNoteSymbol + symbolSuffix();\r
+                       if( ! rootNoteSymbol.equals(bassNoteSymbol) ) {\r
+                               chordSymbol += "/" + bassNoteSymbol;\r
+                       }\r
+                       return chordSymbol;\r
+               }\r
+               /**\r
+                * コード名を HTML で返します。\r
+                *\r
+                * Swing の {@link JLabel#setText(String)} は HTML で指定できるので、\r
+                * 文字の大きさに変化をつけることができます。\r
+                *\r
+                * @param color_name 色のHTML表現(色名または #RRGGBB 形式)\r
+                * @return コード名のHTML\r
+                */\r
+               public String toHtmlString(String color_name) {\r
+                       String small_tag = "<span style=\"font-size: 120%\">";\r
+                       String end_of_small_tag = "</span>";\r
+                       String root = rootNoteSymbol.toString();\r
+                       String formatted_root = (root.length() == 1) ? root + small_tag :\r
+                               root.replace("#",small_tag+"<sup>#</sup>").\r
+                               replace("b",small_tag+"<sup>b</sup>").\r
+                               replace("x",small_tag+"<sup>x</sup>");\r
+                       String formatted_bass = "";\r
+                       if( ! rootNoteSymbol.equals(bassNoteSymbol) ) {\r
+                               String bass = bassNoteSymbol.toString();\r
+                               formatted_bass = (bass.length() == 1) ? bass + small_tag :\r
+                                       bass.replace("#",small_tag+"<sup>#</sup>").\r
+                                       replace("b",small_tag+"<sup>b</sup>").\r
+                                       replace("x",small_tag+"<sup>x</sup>");\r
+                               formatted_bass = "/" + formatted_bass + end_of_small_tag;\r
+                       }\r
+                       String suffix = symbolSuffix().\r
+                               replace("-5","<sup>-5</sup>").\r
+                               replace("+5","<sup>+5</sup>");\r
+                       return\r
+                               "<html>" +\r
+                               "<span style=\"color: " + color_name + "; font-size: 170% ; white-space: nowrap ;\">" +\r
+                               formatted_root + suffix + end_of_small_tag + formatted_bass +\r
+                               "</span>" +\r
+                               "</html>" ;\r
+               }\r
+               /**\r
+                * コードの説明(英語)を返します。\r
+                * @return コードの説明(英語)\r
+                */\r
+               public String toName() {\r
+                       String chord_name = rootNoteSymbol.toStringIn(SymbolLanguage.NAME) + nameSuffix() ;\r
+                       if( ! rootNoteSymbol.equals(bassNoteSymbol) ) {\r
+                               chord_name += " on " + bassNoteSymbol.toStringIn(SymbolLanguage.NAME);\r
+                       }\r
+                       return chord_name;\r
+               }\r
+               /**\r
+                * コードネームの音名を除いた部分(サフィックス)を組み立てて返します。\r
+                * @return コードネームの音名を除いた部分\r
+                */\r
+               public String symbolSuffix() {\r
+                       String suffix = ( ( offsets[THIRD_OFFSET] == MINOR ) ? "m" : "" );\r
+                       switch( offsets[SEVENTH_OFFSET] ) {\r
+                       case SIXTH:         suffix += "6";  break;\r
+                       case SEVENTH:       suffix += "7";  break;\r
+                       case MAJOR_SEVENTH: suffix += "M7"; break;\r
+                       }\r
+                       switch( offsets[THIRD_OFFSET] ) {\r
+                       case SUS4: suffix += "sus4"; break;\r
+                       case SUS2: suffix += "sus2"; break;\r
+                       default: break;\r
+                       }\r
+                       switch( offsets[FIFTH_OFFSET] ) {\r
+                       case FLAT5:  suffix += "-5"; break;\r
+                       case SHARP5: suffix += "+5"; break;\r
+                       default: break;\r
+                       }\r
+                       Vector<String> paren = new Vector<String>();\r
+                       switch( offsets[NINTH_OFFSET] ) {\r
+                       case NINTH:  paren.add("9"); break;\r
+                       case FLAT9:  paren.add("-9"); break;\r
+                       case SHARP9: paren.add("+9"); break;\r
+                       }\r
+                       switch( offsets[ELEVENTH_OFFSET] ) {\r
+                       case ELEVENTH: paren.add("11"); break;\r
+                       case SHARP11:  paren.add("+11"); break;\r
+                       }\r
+                       switch( offsets[THIRTEENTH_OFFSET] ) {\r
+                       case THIRTEENTH: paren.add("13"); break;\r
+                       case FLAT13:     paren.add("-13"); break;\r
+                       }\r
+                       if( paren.size() > 0 ) {\r
+                               boolean is_first = true;\r
+                               suffix += "(";\r
+                               for( String p : paren ) {\r
+                                       if( is_first )\r
+                                               is_first = false;\r
+                                       else\r
+                                               suffix += ",";\r
+                                       suffix += p;\r
+                               }\r
+                               suffix += ")";\r
+                       }\r
+                       if( suffix.equals("m-5") ) return "dim";\r
+                       else if( suffix.equals("+5") ) return "aug";\r
+                       else if( suffix.equals("m6-5") ) return "dim7";\r
+                       else if( suffix.equals("(9)") ) return "add9";\r
+                       else if( suffix.equals("7(9)") ) return "9";\r
+                       else if( suffix.equals("M7(9)") ) return "M9";\r
+                       else if( suffix.equals("7+5") ) return "aug7";\r
+                       else if( suffix.equals("m6-5(9)") ) return "dim9";\r
+                       else return suffix ;\r
+               }\r
+               /**\r
+                * コードの説明のうち、音名を除いた部分を組み立てて返します。\r
+                * @return コード説明の音名を除いた部分\r
+                */\r
+               public String nameSuffix() {\r
+                       String suffix = "";\r
+                       if( offsets[THIRD_OFFSET] == MINOR ) suffix += " minor";\r
+                       switch( offsets[SEVENTH_OFFSET] ) {\r
+                       case SIXTH:         suffix += " 6th"; break;\r
+                       case SEVENTH:       suffix += " 7th"; break;\r
+                       case MAJOR_SEVENTH: suffix += " major 7th"; break;\r
+                       }\r
+                       switch( offsets[THIRD_OFFSET] ) {\r
+                       case SUS4: suffix += " suspended 4th"; break;\r
+                       case SUS2: suffix += " suspended 2nd"; break;\r
+                       default: break;\r
+                       }\r
+                       switch( offsets[FIFTH_OFFSET] ) {\r
+                       case FLAT5 : suffix += " flatted 5th"; break;\r
+                       case SHARP5: suffix += " sharped 5th"; break;\r
+                       default: break;\r
+                       }\r
+                       Vector<String> paren = new Vector<String>();\r
+                       switch( offsets[NINTH_OFFSET] ) {\r
+                       case NINTH:  paren.add("9th"); break;\r
+                       case FLAT9:  paren.add("flatted 9th"); break;\r
+                       case SHARP9: paren.add("sharped 9th"); break;\r
+                       }\r
+                       switch( offsets[ELEVENTH_OFFSET] ) {\r
+                       case ELEVENTH: paren.add("11th"); break;\r
+                       case SHARP11:  paren.add("sharped 11th"); break;\r
+                       }\r
+                       switch( offsets[THIRTEENTH_OFFSET] ) {\r
+                       case THIRTEENTH: paren.add("13th"); break;\r
+                       case FLAT13:     paren.add("flatted 13th"); break;\r
+                       }\r
+                       if( paren.size() > 0 ) {\r
+                               boolean is_first = true;\r
+                               suffix += "(additional ";\r
+                               for( String p : paren ) {\r
+                                       if( is_first )\r
+                                               is_first = false;\r
+                                       else\r
+                                               suffix += ",";\r
+                                       suffix += p;\r
+                               }\r
+                               suffix += ")";\r
+                       }\r
+                       if( suffix.equals(" minor flatted 5th") ) return " diminished (triad)";\r
+                       else if( suffix.equals(" sharped 5th") ) return " augumented";\r
+                       else if( suffix.equals(" minor 6th flatted 5th") ) return " diminished 7th";\r
+                       else if( suffix.equals(" 7th(additional 9th)") ) return " 9th";\r
+                       else if( suffix.equals(" major 7th(additional 9th)") ) return " major 9th";\r
+                       else if( suffix.equals(" 7th sharped 5th") ) return " augumented 7th";\r
+                       else if( suffix.equals(" minor 6th flatted 5th(additional 9th)") ) return " diminished 9th";\r
+                       else if( suffix.isEmpty() ) return " major";\r
+                       else return suffix ;\r
+               }\r
+       }\r
+\r
+       /**\r
+        * Chord Progression - コード進行のクラス\r
+        */\r
+       public static class ChordProgression {\r
+\r
+               public class TickRange implements Cloneable {\r
+                       long start_tick_pos = 0, end_tick_pos = 0;\r
+                       public TickRange( long tick_pos ) {\r
+                               end_tick_pos = start_tick_pos = tick_pos;\r
+                       }\r
+                       public TickRange( long start_tick_pos, long end_tick_pos ) {\r
+                               this.start_tick_pos = start_tick_pos;\r
+                               this.end_tick_pos = end_tick_pos;\r
+                       }\r
+                       protected TickRange clone() {\r
+                               return new TickRange( start_tick_pos, end_tick_pos );\r
+                       }\r
+                       public void moveForward() {\r
+                               start_tick_pos = end_tick_pos;\r
+                       }\r
+                       public void moveForward( long duration ) {\r
+                               start_tick_pos = end_tick_pos;\r
+                               end_tick_pos += duration;\r
+                       }\r
+                       public long duration() {\r
+                               return end_tick_pos - start_tick_pos;\r
+                       }\r
+                       public boolean contains( long tick ) {\r
+                               return ( tick >= start_tick_pos && tick < end_tick_pos );\r
+                       }\r
+               }\r
+\r
+               private class ChordStroke {\r
+                       Chord chord; int beat_length; TickRange tick_range = null;\r
+                       public ChordStroke(Chord chord) { this( chord, 1 ); }\r
+                       public ChordStroke(Chord chord, int beat_length) {\r
+                               this.chord = chord;\r
+                               this.beat_length = beat_length;\r
+                       }\r
+                       public String toString() {\r
+                               String str = chord.toString();\r
+                               for( int i=2; i <= beat_length; i++ ) str += " %";\r
+                               return str;\r
+                       }\r
+               }\r
+\r
+               // 時間位置付き歌詞\r
+               public class Lyrics {\r
+                       String text = null;\r
+                       Long start_tick_pos = null;\r
+                       public Lyrics(String text) { this.text = text; }\r
+                       public Lyrics(String text, long tick_pos) {\r
+                               this.text = text; start_tick_pos = tick_pos;\r
+                       }\r
+                       public String toString() { return text; }\r
+               }\r
+\r
+               private class Measure extends Vector<Object> {\r
+                       Long ticks_per_beat = null;\r
+                       public int numberOfBeats() {\r
+                               int n = 0;\r
+                               for( Object obj : this ) {\r
+                                       if( obj instanceof ChordStroke ) {\r
+                                               n += ((ChordStroke)obj).beat_length;\r
+                                       }\r
+                               }\r
+                               return n;\r
+                       }\r
+                       // 小節内のコードストロークが時間的に等間隔かどうか調べる。\r
+                       // もし等間隔の場合、テキスト出力時に % をつける必要がなくなる。\r
+                       public boolean isEquallyDivided() {\r
+                               int l, l_prev = 0;\r
+                               for( Object obj : this ) {\r
+                                       if( obj instanceof ChordStroke ) {\r
+                                               l = ((ChordStroke)obj).beat_length;\r
+                                               if( l_prev > 0 && l_prev != l ) {\r
+                                                       return false;\r
+                                               }\r
+                                               l_prev = l;\r
+                                       }\r
+                               }\r
+                               return true;\r
+                       }\r
+                       public int addBeat() { return addBeat(1); }\r
+                       public int addBeat(int num_beats) {\r
+                               ChordStroke last_chord_stroke = null;\r
+                               for( Object obj : this ) {\r
+                                       if( obj instanceof ChordStroke ) {\r
+                                               last_chord_stroke = (ChordStroke)obj;\r
+                                       }\r
+                               }\r
+                               if( last_chord_stroke == null ) {\r
+                                       return 0;\r
+                               }\r
+                               return last_chord_stroke.beat_length += num_beats;\r
+                       }\r
+                       public String toString() {\r
+                               String str = "";\r
+                               boolean is_eq_dev = isEquallyDivided();\r
+                               for( Object element : this ) {\r
+                                       str += " ";\r
+                                       if( element instanceof ChordStroke ) {\r
+                                               ChordStroke cs = (ChordStroke)element;\r
+                                               str += is_eq_dev ? cs.chord : cs;\r
+                                       }\r
+                                       else if( element instanceof Lyrics ) {\r
+                                               str += element.toString();\r
+                                       }\r
+                               }\r
+                               return str;\r
+                       }\r
+                       public TickRange getRange() {\r
+                               long start_tick_pos = -1;\r
+                               long end_tick_pos = -1;\r
+                               for( Object element : this ) {\r
+                                       if( ! (element instanceof ChordProgression.ChordStroke) )\r
+                                               continue;\r
+                                       ChordProgression.ChordStroke chord_stroke\r
+                                       = (ChordProgression.ChordStroke)element;\r
+                                       // 小節の先頭と末尾の tick を求める\r
+                                       if( start_tick_pos < 0 ) {\r
+                                               start_tick_pos = chord_stroke.tick_range.start_tick_pos;\r
+                                       }\r
+                                       end_tick_pos = chord_stroke.tick_range.end_tick_pos;\r
+                               }\r
+                               if( start_tick_pos < 0 || end_tick_pos < 0 ) {\r
+                                       return null;\r
+                               }\r
+                               return new TickRange( start_tick_pos, end_tick_pos );\r
+                       }\r
+                       public ChordStroke chordStrokeAt( long tick ) {\r
+                               for( Object element : this ) {\r
+                                       if( ! (element instanceof ChordProgression.ChordStroke) )\r
+                                               continue;\r
+                                       ChordProgression.ChordStroke chord_stroke\r
+                                       = (ChordProgression.ChordStroke)element;\r
+                                       if( chord_stroke.tick_range.contains(tick) ) {\r
+                                               return chord_stroke;\r
+                                       }\r
+                               }\r
+                               return null;\r
+                       }\r
+               }\r
+               private class Line extends Vector<Measure> {\r
+                       public String toString() {\r
+                               String str = "";\r
+                               for( Measure measure : this ) str += measure + "|";\r
+                               return str;\r
+                       }\r
+               }\r
+\r
+               // 内部変数\r
+               private Vector<Line> lines = null;\r
+               private Key key = null;\r
+               private Long ticks_per_measure = null;\r
+\r
+               public Key getKey() { return key; }\r
+               public void setKey(Key key) { this.key = key; }\r
+\r
+               public String toString() {\r
+                       String str = "";\r
+                       if( key != null ) str += "Key: " + key + "\n";\r
+                       for( Line line : lines ) str += line + "\n";\r
+                       return str;\r
+               }\r
+\r
+               // 指定された小節数、キー、拍子に合わせたコード進行をランダムに自動生成\r
+               //\r
+               public ChordProgression() { }\r
+               public ChordProgression( int measure_length, int timesig_upper ) {\r
+                       int key_co5 = (int)(Math.random() * 12) - 5;\r
+                       key = new Key( key_co5, Key.MAJOR );\r
+                       lines = new Vector<Line>();\r
+                       Line line = new Line();\r
+                       boolean is_end;\r
+                       Chord chord, prev_chord = new Chord(new NoteSymbol(key_co5));\r
+                       int co5_offset, prev_co5_offset;\r
+                       double r;\r
+                       for( int mp=0; mp<measure_length; mp++ ) {\r
+                               is_end = (mp == 0 || mp == measure_length - 1); // 最初または最後の小節かを覚えておく\r
+                               Measure measure = new Measure();\r
+                               ChordStroke last_chord_stroke = null;\r
+                               for( int i=0; i<timesig_upper; i++ ) {\r
+                                       if(\r
+                                               i % 4 == 2 && Math.random() < 0.8\r
+                                               ||\r
+                                               i % 2 != 0 && Math.random() < 0.9\r
+                                       ){\r
+                                               // もう一拍延長\r
+                                               last_chord_stroke.beat_length++;\r
+                                               continue;\r
+                                       }\r
+                                       chord = new Chord(new NoteSymbol(key_co5));\r
+                                       co5_offset = 0;\r
+                                       prev_co5_offset = prev_chord.rootNoteSymbol().toCo5() - key_co5;\r
+                                       if( ! is_end ) {\r
+                                               //\r
+                                               // 最初または最後の小節は常にトニックにする。\r
+                                               // 完全五度ずつ下がる進行を基本としつつ、時々そうでない進行も出現するようにする。\r
+                                               // サブドミナントを超えるとスケールを外れるので、超えそうになったらランダムに決め直す。\r
+                                               //\r
+                                               r = Math.random();\r
+                                               co5_offset = prev_co5_offset - 1;\r
+                                               if( co5_offset < -1 || (prev_co5_offset < 5 && r < 0.5) ) {\r
+                                                       //\r
+                                                       // 長7度がルートとなるコードの出現確率を半減させながらコードを決める\r
+                                                       // (余りが6のときだけが長7度)\r
+                                                       // なお、前回と同じコードは使わないようにする。\r
+                                                       do {\r
+                                                               co5_offset = (int)(Math.random() * 13) % 7 - 1;\r
+                                                       } while( co5_offset == prev_co5_offset );\r
+                                               }\r
+                                               int co5RootNote = key_co5 + co5_offset;\r
+                                               chord.setRoot(new NoteSymbol(co5RootNote));\r
+                                               chord.setBass(new NoteSymbol(co5RootNote));\r
+                                       }\r
+                                       switch( co5_offset ) {\r
+                                       // ルート音ごとに、7th などの付加や、メジャーマイナー反転を行う確率を決める\r
+                                       case 5: // VII\r
+                                               if( Math.random() < 0.5 ) {\r
+                                                       // m7-5\r
+                                                       chord.setMinorThird();\r
+                                                       chord.setFlattedFifth();\r
+                                               }\r
+                                               if( Math.random() < 0.8 ) chord.setSeventh();\r
+                                               break;\r
+                                       case 4: // Secondary dominant (III)\r
+                                               if( prev_co5_offset == 5 ) {\r
+                                                       // ルートが長7度→長3度の進行のとき、反転確率を上げる。\r
+                                                       // (ハ長調でいう Bm7-5 の次に E7 を出現しやすくする)\r
+                                                       if( Math.random() < 0.2 ) chord.setMinorThird();\r
+                                               }\r
+                                               else {\r
+                                                       if( Math.random() < 0.8 ) chord.setMinorThird();\r
+                                               }\r
+                                               if( Math.random() < 0.7 ) chord.setSeventh();\r
+                                               break;\r
+                                       case 3: // VI\r
+                                               if( Math.random() < 0.8 ) chord.setMinorThird();\r
+                                               if( Math.random() < 0.7 ) chord.setSeventh();\r
+                                               break;\r
+                                       case 2: // II\r
+                                               if( Math.random() < 0.8 ) chord.setMinorThird();\r
+                                               if( Math.random() < 0.7 ) chord.setSeventh();\r
+                                               break;\r
+                                       case 1: // Dominant (V)\r
+                                               if( Math.random() < 0.1 ) chord.setMinorThird();\r
+                                               if( Math.random() < 0.3 ) chord.setSeventh();\r
+                                               if( Math.random() < 0.2 ) chord.setNinth();\r
+                                               break;\r
+                                       case 0: // Tonic(ここでマイナーで終わるとさみしいので setMinorThird() はしない)\r
+                                               if( Math.random() < 0.2 ) chord.setMajorSeventh();\r
+                                               if( Math.random() < 0.2 ) chord.setNinth();\r
+                                               break;\r
+                                       case -1: // Sub-dominant (IV)\r
+                                               if( Math.random() < 0.1 ) {\r
+                                                       chord.setMinorThird();\r
+                                                       if( Math.random() < 0.3 ) chord.setSeventh();\r
+                                               }\r
+                                               else\r
+                                                       if( Math.random() < 0.2 ) chord.setMajorSeventh();\r
+                                               if( Math.random() < 0.2 ) chord.setNinth();\r
+                                               break;\r
+                                       }\r
+                                       measure.add( last_chord_stroke = new ChordStroke(chord) );\r
+                                       prev_chord = chord;\r
+                               }\r
+                               line.add(measure);\r
+                               if( (mp+1) % 8 == 0 ) { // 8小節おきに改行\r
+                                       lines.add(line);\r
+                                       line = new Line();\r
+                               }\r
+                       }\r
+                       if( line.size() > 0 ) lines.add(line);\r
+               }\r
+               // テキストからコード進行を生成\r
+               public ChordProgression( String source_text ) {\r
+                       if( source_text == null ) return;\r
+                       Measure measure;\r
+                       Line line;\r
+                       String[] lines_src, measures_src, elements_src;\r
+                       Chord last_chord = null;\r
+                       String key_regex = "^Key(\\s*):(\\s*)";\r
+                       //\r
+                       // キーであるかどうか見分けるためのパターン\r
+                       Pattern key_match_pattern = Pattern.compile(\r
+                                       key_regex+".*$", Pattern.CASE_INSENSITIVE\r
+                                       );\r
+                       // キーのヘッダーを取り除くためのパターン\r
+                       Pattern key_repl_pattern = Pattern.compile(\r
+                                       key_regex, Pattern.CASE_INSENSITIVE\r
+                                       );\r
+                       //\r
+                       lines_src = source_text.split("[\r\n]+");\r
+                       lines = new Vector<Line>();\r
+                       for( String line_src : lines_src ) {\r
+                               measures_src = line_src.split("\\|");\r
+                               if( measures_src.length > 0 ) {\r
+                                       String key_string = measures_src[0].trim();\r
+                                       if( key_match_pattern.matcher(key_string).matches() ) {\r
+                                               key = new Key(\r
+                                                               key_repl_pattern.matcher(key_string).replaceFirst("")\r
+                                                               );\r
+                                               // System.out.println("Key = " + key);\r
+                                               continue;\r
+                                       }\r
+                               }\r
+                               line = new Line();\r
+                               for( String measure_src : measures_src ) {\r
+                                       elements_src = measure_src.split("[ \t]+");\r
+                                       measure = new Measure();\r
+                                       for( String element_src : elements_src ) {\r
+                                               if( element_src.isEmpty() ) continue;\r
+                                               if( element_src.equals("%") ) {\r
+                                                       if( measure.addBeat() == 0 ) {\r
+                                                               measure.add( new ChordStroke(last_chord) );\r
+                                                       }\r
+                                                       continue;\r
+                                               }\r
+                                               try {\r
+                                                       measure.add( new ChordStroke(\r
+                                                                       last_chord = new Music.Chord(element_src)\r
+                                                                       ));\r
+                                               } catch( IllegalArgumentException ex ) {\r
+                                                       measure.add( new Lyrics(element_src) );\r
+                                               }\r
+                                       }\r
+                                       line.add(measure);\r
+                               }\r
+                               lines.add(line);\r
+                       }\r
+               }\r
+\r
+               // Major/minor 切り替え\r
+               public void toggleKeyMajorMinor() {\r
+                       key = key.relativeKey();\r
+               }\r
+\r
+               // コード進行の移調\r
+               public void transpose(int chromatic_offset) {\r
+                       for( Line line : lines ) {\r
+                               for( Measure measure : line ) {\r
+                                       for( int i=0; i<measure.size(); i++ ) {\r
+                                               Object element = measure.get(i);\r
+                                               if( element instanceof ChordStroke ) {\r
+                                                       ChordStroke cs = (ChordStroke)element;\r
+                                                       Chord new_chord = cs.chord.clone();\r
+                                                       //\r
+                                                       // キーが未設定のときは、最初のコードから推測して設定\r
+                                                       if( key == null ) key = new Key( new_chord );\r
+                                                       //\r
+                                                       new_chord.transpose( chromatic_offset, key );\r
+                                                       measure.set( i, new ChordStroke( new_chord, cs.beat_length ) );\r
+                                               }\r
+                                       }\r
+                               }\r
+                       }\r
+                       key.transpose(chromatic_offset);\r
+               }\r
+               // 異名同音の♭と#を切り替える\r
+               public void toggleEnharmonically() {\r
+                       if( key == null ) return;\r
+                       int original_key_co5 = key.toCo5();\r
+                       int co5Offset = 0;\r
+                       if( original_key_co5 > 4 ) {\r
+                               co5Offset = -SEMITONES_PER_OCTAVE;\r
+                       }\r
+                       else if( original_key_co5 < -4 ) {\r
+                               co5Offset = SEMITONES_PER_OCTAVE;\r
+                       }\r
+                       else {\r
+                               return;\r
+                       }\r
+                       key.toggleEnharmonically();\r
+                       for( Line line : lines ) {\r
+                               for( Measure measure : line ) {\r
+                                       for( int i=0; i<measure.size(); i++ ) {\r
+                                               Object element = measure.get(i);\r
+                                               if( element instanceof ChordStroke ) {\r
+                                                       ChordStroke cs = (ChordStroke)element;\r
+                                                       Chord newChord = cs.chord.clone();\r
+                                                       newChord.setRoot(new NoteSymbol(newChord.rootNoteSymbol().toCo5() + co5Offset));\r
+                                                       newChord.setBass(new NoteSymbol(newChord.bassNoteSymbol().toCo5() + co5Offset));\r
+                                                       measure.set( i, new ChordStroke( newChord, cs.beat_length ) );\r
+                                               }\r
+                                       }\r
+                               }\r
+                       }\r
+               }\r
+               // コード進行の中に時間軸(MIDI tick)を書き込む\r
+               //\r
+               public void setTickPositions( FirstTrackSpec first_track ) {\r
+                       ticks_per_measure = first_track.ticks_per_measure;\r
+                       TickRange tick_range = new TickRange(\r
+                                       first_track.pre_measures * ticks_per_measure\r
+                                       );\r
+                       for( Line line : lines ) { // 行単位の処理\r
+                               for( Measure measure : line ) { // 小節単位の処理\r
+                                       int n_beats = measure.numberOfBeats();\r
+                                       if( n_beats == 0 ) continue;\r
+                                       long tpb = measure.ticks_per_beat = ticks_per_measure / n_beats ;\r
+                                       for( Object element : measure ) {\r
+                                               if( element instanceof Lyrics ) {\r
+                                                       ((Lyrics)element).start_tick_pos = tick_range.start_tick_pos;\r
+                                                       continue;\r
+                                               }\r
+                                               else if( element instanceof ChordStroke ) {\r
+                                                       ChordStroke chord_stroke = (ChordStroke)element;\r
+                                                       tick_range.moveForward( tpb * chord_stroke.beat_length );\r
+                                                       chord_stroke.tick_range = tick_range.clone();\r
+                                               }\r
+                                       }\r
+                               }\r
+                       }\r
+               }\r
+               // コード文字列の書き込み\r
+               public void setChordSymbolTextTo( AbstractTrackSpec ts ) {\r
+                       for( Line line : lines ) {\r
+                               for( Measure measure : line ) {\r
+                                       if( measure.ticks_per_beat == null ) continue;\r
+                                       for( Object element : measure ) {\r
+                                               if( element instanceof ChordStroke ) {\r
+                                                       ts.addStringTo( 0x01, (ChordStroke)element );\r
+                                               }\r
+                                       }\r
+                               }\r
+                       }\r
+               }\r
+               // 歌詞の書き込み\r
+               public void setLyricsTo( AbstractTrackSpec ts ) {\r
+                       for( Line line : lines ) {\r
+                               for( Measure measure : line ) {\r
+                                       if( measure.ticks_per_beat == null ) continue;\r
+                                       for( Object element : measure ) {\r
+                                               if( element instanceof Lyrics ) {\r
+                                                       ts.addStringTo( 0x05, (Lyrics)element );\r
+                                               }\r
+                                       }\r
+                               }\r
+                       }\r
+               }\r
+               // コード進行をもとに MIDI シーケンスを生成\r
+               //\r
+               public Sequence toMidiSequence() {\r
+                       return toMidiSequence(48);\r
+               }\r
+               public Sequence toMidiSequence( int ppq ) {\r
+                       //\r
+                       // PPQ = Pulse Per Quarter (TPQN = Tick Per Quearter Note)\r
+                       //\r
+                       return toMidiSequence( ppq, 0, 0, null, null );\r
+               }\r
+               public Sequence toMidiSequence(\r
+                               int ppq, int start_measure_pos, int end_measure_pos,\r
+                               FirstTrackSpec first_track,\r
+                               Vector<AbstractNoteTrackSpec> track_specs\r
+                               ) {\r
+                       // MIDIシーケンスの生成\r
+                       Sequence seq;\r
+                       try {\r
+                               seq = new Sequence(Sequence.PPQ, ppq);\r
+                       } catch ( InvalidMidiDataException e ) {\r
+                               e.printStackTrace();\r
+                               return null;\r
+                       }\r
+                       // マスタートラックの生成\r
+                       if( first_track == null ) {\r
+                               first_track = new FirstTrackSpec();\r
+                       }\r
+                       first_track.key = this.key;\r
+                       first_track.createTrack( seq, start_measure_pos, end_measure_pos );\r
+                       //\r
+                       // 中身がなければここで終了\r
+                       if( lines == null || track_specs == null ) return seq;\r
+                       //\r
+                       // コード進行の中に時間軸(MIDI tick)を書き込む\r
+                       setTickPositions( first_track );\r
+                       //\r
+                       // コードのテキストと歌詞を書き込む\r
+                       setChordSymbolTextTo( first_track );\r
+                       setLyricsTo( first_track );\r
+                       //\r
+                       // 残りのトラックを生成\r
+                       for( AbstractNoteTrackSpec ts : track_specs ) {\r
+                               ts.createTrack( seq, first_track );\r
+                               if( ts instanceof DrumTrackSpec ) {\r
+                                       ((DrumTrackSpec)ts).addDrums(this);\r
+                               }\r
+                               else {\r
+                                       ((MelodyTrackSpec)ts).addChords(this);\r
+                               }\r
+                       }\r
+                       return seq;\r
+               }\r
+       }\r
+\r
+       /**\r
+        * MIDIトラックの仕様を表すクラス\r
+        */\r
+       public static abstract class AbstractTrackSpec {\r
+               public static final int BEAT_RESOLUTION = 2;\r
+               // 最短の音符の長さ(四分音符を何回半分にするか)\r
+               String name = null;\r
+               Track track = null;\r
+               FirstTrackSpec first_track_spec = null;\r
+               Sequence sequence = null;\r
+               long min_note_ticks = 0;\r
+               int pre_measures = 2;\r
+               public AbstractTrackSpec() { }\r
+               public AbstractTrackSpec(String name) {\r
+                       this.name = name;\r
+               }\r
+               public String toString() {\r
+                       return name;\r
+               }\r
+               public Track createTrack( Sequence seq, FirstTrackSpec first_track_spec ) {\r
+                       this.first_track_spec = first_track_spec;\r
+                       track = (sequence = seq).createTrack();\r
+                       if( name != null ) addStringTo( 0x03, name, 0 );\r
+                       min_note_ticks = (long)( seq.getResolution() >> 2 );\r
+                       return track;\r
+               }\r
+               public boolean addMetaEventTo( int type, byte data[], long tick_pos  ) {\r
+                       MetaMessage meta_msg = new MetaMessage();\r
+                       try {\r
+                               meta_msg.setMessage( type, data, data.length );\r
+                       } catch( InvalidMidiDataException ex ) {\r
+                               ex.printStackTrace();\r
+                               return false;\r
+                       }\r
+                       return track.add(new MidiEvent( (MidiMessage)meta_msg, tick_pos ));\r
+               }\r
+               public boolean addStringTo( int type, String str, long tick_pos ) {\r
+                       if( str == null ) str = "";\r
+                       return addMetaEventTo( type, str.getBytes(), tick_pos );\r
+               }\r
+               public boolean addStringTo( int type, ChordProgression.ChordStroke cs ) {\r
+                       return addStringTo(\r
+                                       type,\r
+                                       cs.chord.toString(),\r
+                                       cs.tick_range.start_tick_pos\r
+                                       );\r
+               }\r
+               public boolean addStringTo( int type, ChordProgression.Lyrics lyrics ) {\r
+                       return addStringTo(\r
+                                       type, lyrics.text, lyrics.start_tick_pos\r
+                                       );\r
+               }\r
+               public boolean addEOT( long tick_pos ) {\r
+                       return addMetaEventTo( 0x2F, new byte[0], tick_pos );\r
+               }\r
+               public void setChordSymbolText( ChordProgression cp ) {\r
+                       cp.setChordSymbolTextTo( this );\r
+               }\r
+       }\r
+\r
+       // 最初のトラック専用\r
+       //\r
+       public static class FirstTrackSpec extends AbstractTrackSpec {\r
+               static byte default_tempo_data[] = { 0x07, (byte)0xA1, 0x20 };  // 120[QPM]\r
+               static byte default_timesig_data[] = { 0x04, 0x02, 0x18, 0x08 };        // 4/4\r
+               byte tempo_data[] = default_tempo_data;\r
+               byte timesig_data[] = default_timesig_data;\r
+               Key key = null;\r
+               long ticks_per_measure;\r
+               public FirstTrackSpec() { }\r
+               public FirstTrackSpec(String name) {\r
+                       this.name = name;\r
+               }\r
+               public FirstTrackSpec(\r
+                       String name, byte[] tempo_data, byte[] timesig_data\r
+               ) {\r
+                       this.name = name;\r
+                       if( tempo_data != null ) this.tempo_data = tempo_data;\r
+                       if( timesig_data != null ) this.timesig_data = timesig_data;\r
+               }\r
+               public FirstTrackSpec(\r
+                       String name, byte[] tempo_data, byte[] timesig_data, Key key\r
+               ) {\r
+                       this(name,tempo_data,timesig_data);\r
+                       this.key = key;\r
+               }\r
+               public Track createTrack(Sequence seq) {\r
+                       return createTrack( seq, 0, 0 );\r
+               }\r
+               public Track createTrack(\r
+                       Sequence seq, int start_measure_pos, int end_measure_pos\r
+               ) {\r
+                       this.pre_measures = start_measure_pos - 1;\r
+                       Track track = super.createTrack( seq, this );\r
+                       addTempo(\r
+                               this.tempo_data = (\r
+                                       tempo_data == null ? default_tempo_data : tempo_data\r
+                               ), 0\r
+                       );\r
+                       addTimeSignature(\r
+                               this.timesig_data = (\r
+                                       timesig_data == null ? default_timesig_data : timesig_data\r
+                               ), 0\r
+                       );\r
+                       if( key != null ) addKeySignature( key, 0 );\r
+                       ticks_per_measure = (long)(\r
+                               ( 4 * seq.getResolution() * this.timesig_data[0] ) >> this.timesig_data[1]\r
+                       );\r
+                       addEOT( end_measure_pos * ticks_per_measure );\r
+                       return track;\r
+               }\r
+               public boolean addKeySignature( Key key, long tick_pos ) {\r
+                       return addMetaEventTo( 0x59, key.getBytes(), tick_pos );\r
+               }\r
+               public boolean addTempo( byte data[], long tick_pos ) {\r
+                       return addMetaEventTo( 0x51, data, tick_pos );\r
+               }\r
+               public boolean addTimeSignature( byte data[], long tick_pos ) {\r
+                       return addMetaEventTo( 0x58, data, tick_pos );\r
+               }\r
+       }\r
+\r
+       // 一般のトラック(メロディ、ドラム共通)\r
+       //\r
+       public static abstract class AbstractNoteTrackSpec extends AbstractTrackSpec {\r
+               int midi_channel = -1;\r
+               int program_no = -1;\r
+               int velocity = 64;\r
+\r
+               public AbstractNoteTrackSpec() {}\r
+               public AbstractNoteTrackSpec(int ch) {\r
+                       midi_channel = ch;\r
+               }\r
+               public AbstractNoteTrackSpec(int ch, String name) {\r
+                       midi_channel = ch;\r
+                       this.name = name;\r
+               }\r
+               public AbstractNoteTrackSpec(int ch, String name, int program_no) {\r
+                       this(ch,name);\r
+                       this.program_no = program_no;\r
+               }\r
+               public AbstractNoteTrackSpec(int ch, String name, int program_no, int velocity) {\r
+                       this(ch,name,program_no);\r
+                       this.velocity = velocity;\r
+               }\r
+               public Track createTrack( Sequence seq, FirstTrackSpec first_track_spec ) {\r
+                       Track track = super.createTrack( seq, first_track_spec );\r
+                       if( program_no >= 0 ) addProgram( program_no, 0 );\r
+                       return track;\r
+               }\r
+               public boolean addProgram( int program_no, long tick_pos ) {\r
+                       ShortMessage short_msg;\r
+                       try {\r
+                               (short_msg = new ShortMessage()).setMessage(\r
+                                       ShortMessage.PROGRAM_CHANGE, midi_channel, program_no, 0\r
+                               );\r
+                       } catch( InvalidMidiDataException ex ) {\r
+                               ex.printStackTrace();\r
+                               return false;\r
+                       }\r
+                       return track.add(new MidiEvent( (MidiMessage)short_msg, tick_pos ));\r
+               }\r
+               public boolean addNote(long start_tick_pos, long end_tick_pos, int note_no) {\r
+                       return addNote(start_tick_pos, end_tick_pos, note_no, velocity);\r
+               }\r
+               public boolean addNote(\r
+                       long start_tick_pos, long end_tick_pos,\r
+                       int note_no, int velocity\r
+               ) {\r
+                       ShortMessage short_msg;\r
+                       //\r
+                       try {\r
+                               (short_msg = new ShortMessage()).setMessage(\r
+                                       ShortMessage.NOTE_ON, midi_channel, note_no, velocity\r
+                               );\r
+                       } catch( InvalidMidiDataException ex ) {\r
+                               ex.printStackTrace();\r
+                               return false;\r
+                       }\r
+                       if( ! track.add(new MidiEvent( (MidiMessage)short_msg, start_tick_pos )) )\r
+                               return false;\r
+                       //\r
+                       try {\r
+                               (short_msg = new ShortMessage()).setMessage(\r
+                                               ShortMessage.NOTE_OFF, midi_channel, note_no, velocity\r
+                                               );\r
+                       } catch( InvalidMidiDataException ex ) {\r
+                               ex.printStackTrace();\r
+                               return false;\r
+                       }\r
+                       return track.add( new MidiEvent( (MidiMessage)short_msg, end_tick_pos ) );\r
+               }\r
+       }\r
+\r
+       public static class DrumTrackSpec extends AbstractNoteTrackSpec {\r
+               static int default_percussions[] = { // ドラムの音色リスト\r
+                       36, // Bass Drum 1\r
+                       44, // Pedal Hi-Hat\r
+                       39, // Hand Clap\r
+                       48, // Hi Mid Tom\r
+                       50, // High Tom\r
+                       38, // Acoustic Snare\r
+                       62, // Mute Hi Conga\r
+                       63, // Open Hi Conga\r
+               };\r
+               PercussionComboBoxModel models[]\r
+                       = new PercussionComboBoxModel[default_percussions.length];\r
+               int[] beat_patterns = {\r
+                       0x8888, 0x2222, 0x0008, 0x0800,\r
+                       0, 0, 0, 0\r
+               };\r
+               public class PercussionComboBoxModel implements ComboBoxModel<String> {\r
+                       private int note_no;\r
+                       public PercussionComboBoxModel(int default_note_no) {\r
+                               note_no = default_note_no;\r
+                       }\r
+                       // ComboBoxModel\r
+                       public Object getSelectedItem() {\r
+                               return note_no + ": " +\r
+                                       MIDISpec.PERCUSSION_NAMES[note_no - MIDISpec.MIN_PERCUSSION_NUMBER];\r
+                       }\r
+                       public void setSelectedItem(Object anItem) {\r
+                               String name = (String)anItem;\r
+                               int i = MIDISpec.MIN_PERCUSSION_NUMBER;\r
+                               for( String pname : MIDISpec.PERCUSSION_NAMES ) {\r
+                                       if( name.equals(i + ": " + pname) ) {\r
+                                               note_no = i; return;\r
+                                       }\r
+                                       i++;\r
+                               }\r
+                       }\r
+                       // ListModel\r
+                       public String getElementAt(int index) {\r
+                               return (index + MIDISpec.MIN_PERCUSSION_NUMBER) + ": "\r
+                                               + MIDISpec.PERCUSSION_NAMES[index];\r
+                       }\r
+                       public int getSize() {\r
+                               return MIDISpec.PERCUSSION_NAMES.length;\r
+                       }\r
+                       public void addListDataListener(ListDataListener l) { }\r
+                       public void removeListDataListener(ListDataListener l) { }\r
+                       // Methods\r
+                       public int getSelectedNoteNo() {\r
+                               return note_no;\r
+                       }\r
+                       public void setSelectedNoteNo(int note_no) {\r
+                               this.note_no = note_no;\r
+                       }\r
+               }\r
+\r
+               public DrumTrackSpec(int ch, String name) {\r
+                       super(ch,name);\r
+                       for( int i=0; i<default_percussions.length; i++ ) {\r
+                               models[i] = new PercussionComboBoxModel(default_percussions[i]);\r
+                       }\r
+               }\r
+\r
+               public void addDrums( ChordProgression cp ) {\r
+                       int i;\r
+                       long tick;\r
+                       for( ChordProgression.Line line : cp.lines ) { // 行単位の処理\r
+                               for( ChordProgression.Measure measure : line ) { // 小節単位の処理\r
+                                       if( measure.ticks_per_beat == null )\r
+                                               continue;\r
+                                       ChordProgression.TickRange range = measure.getRange();\r
+                                       int mask;\r
+                                       for(\r
+                                                       tick = range.start_tick_pos, mask = 0x8000;\r
+                                                       tick < range.end_tick_pos;\r
+                                                       tick += min_note_ticks, mask >>>= 1\r
+                                                       ) {\r
+                                               for( i=0; i<beat_patterns.length; i++ ) {\r
+                                                       if( (beat_patterns[i] & mask) == 0 )\r
+                                                               continue;\r
+                                                       addNote(\r
+                                                                       tick, tick+10,\r
+                                                                       models[i].getSelectedNoteNo(),\r
+                                                                       velocity\r
+                                                                       );\r
+                                               }\r
+                                       }\r
+                               }\r
+                       }\r
+               }\r
+       }\r
+\r
+       public static class MelodyTrackSpec extends AbstractNoteTrackSpec {\r
+\r
+               Range range; // 音域\r
+\r
+               // 音を出すかどうかを表すビット列\r
+               int beat_pattern = 0xFFFF;\r
+\r
+               // あとで音を出し続けるかどうかを表すビット列\r
+               int continuous_beat_pattern = 0xEEEE;\r
+\r
+               // ベース音を使う場合 true\r
+               // それ以外のコード構成音を使う場合 false\r
+               boolean is_bass = false;\r
+\r
+               // 乱数メロディを作るかどうか\r
+               boolean random_melody = false;\r
+\r
+               // 乱数歌詞を作るかどうか\r
+               boolean random_lyric = false;\r
+\r
+               public MelodyTrackSpec(int ch, String name) {\r
+                       super(ch,name);\r
+                       range = new Range(\r
+                               SEMITONES_PER_OCTAVE * 5, SEMITONES_PER_OCTAVE * 6 );\r
+               }\r
+               public MelodyTrackSpec(int ch, String name, Range range) {\r
+                       super(ch,name);\r
+                       this.range = range;\r
+               }\r
+\r
+               public void addChords( ChordProgression cp ) {\r
+                       int mask;\r
+                       long tick;\r
+                       long start_tick_pos;\r
+\r
+                       // 音階ごとの生起確率を決める重みリスト(random_melody の場合)\r
+                       int i, note_no, prev_note_no = 1;\r
+                       int note_weights[] = new int[range.max_note - range.min_note];\r
+                       //\r
+                       Key key = cp.key;\r
+                       if( key == null ) key = new Key("C");\r
+\r
+                       for( ChordProgression.Line line : cp.lines ) { // 行単位の処理\r
+                               for( ChordProgression.Measure measure : line ) { // 小節単位の処理\r
+                                       if( measure.ticks_per_beat == null )\r
+                                               continue;\r
+                                       ChordProgression.TickRange tick_range = measure.getRange();\r
+                                       boolean is_note_on = false;\r
+                                       //\r
+                                       // 各ビートごとに繰り返し\r
+                                       for(\r
+                                                       tick = start_tick_pos = tick_range.start_tick_pos, mask = 0x8000;\r
+                                                       tick < tick_range.end_tick_pos;\r
+                                                       tick += min_note_ticks, mask >>>= 1\r
+                                                       ) {\r
+                                               // そのtick地点のコードを調べる\r
+                                               Chord chord = measure.chordStrokeAt(tick).chord;\r
+                                               int notes[] = chord.toNoteArray(range);\r
+                                               //\r
+                                               // 各音階ごとに繰り返し\r
+                                               if( Math.random() < 0.9 ) {\r
+                                                       if( (beat_pattern & mask) == 0 ) {\r
+                                                               // 音を出さない\r
+                                                               continue;\r
+                                                       }\r
+                                               }\r
+                                               else {\r
+                                                       // ランダムに逆パターン\r
+                                                       if( (beat_pattern & mask) != 0 ) {\r
+                                                               continue;\r
+                                                       }\r
+                                               }\r
+                                               if( ! is_note_on ) {\r
+                                                       // 前回のビートで継続していなかったので、\r
+                                                       // この地点で音を出し始めることにする。\r
+                                                       start_tick_pos = tick;\r
+                                                       is_note_on = true;\r
+                                               }\r
+                                               if( Math.random() < 0.9 ) {\r
+                                                       if( (continuous_beat_pattern & mask) != 0 ) {\r
+                                                               // 音を継続\r
+                                                               continue;\r
+                                                       }\r
+                                               }\r
+                                               else {\r
+                                                       // ランダムに逆パターン\r
+                                                       if( (continuous_beat_pattern & mask) == 0 ) {\r
+                                                               continue;\r
+                                                       }\r
+                                               }\r
+                                               // このビートの終了tickで音を出し終わることにする。\r
+                                               if( random_melody ) {\r
+                                                       // 音階ごとに出現確率を決める\r
+                                                       int total_weight = 0;\r
+                                                       for( i=0; i<note_weights.length; i++ ) {\r
+                                                               note_no = range.min_note + i;\r
+                                                               int m12 = mod12(note_no - chord.rootNoteSymbol().toNoteNumber());\r
+                                                               int w;\r
+                                                               if( chord.indexOf(note_no) >= 0 ) {\r
+                                                                       // コード構成音は確率を上げる\r
+                                                                       w = 255;\r
+                                                               }\r
+                                                               else {\r
+                                                                       switch( m12 ) {\r
+                                                                       case 2: // 長2度\r
+                                                                       case 9: // 長6度\r
+                                                                               w = 63; break;\r
+                                                                       case 5: // 完全4度\r
+                                                                       case 11: // 長7度\r
+                                                                               w = 47; break;\r
+                                                                       default:\r
+                                                                               w = 0; break;\r
+                                                                       }\r
+                                                                       if( ! key.isOnScale( note_no ) ) {\r
+                                                                               // スケールを外れている音は採用しない\r
+                                                                               w = 0;\r
+                                                                       }\r
+                                                               }\r
+                                                               // 乱高下軽減のため、前回との差によって確率を調整する\r
+                                                               int diff = note_no - prev_note_no;\r
+                                                               if( diff < 0 ) diff = -diff;\r
+                                                               if( diff == 0 ) w /= 8;\r
+                                                               else if( diff > 7 ) w = 0;\r
+                                                               else if( diff > 4 ) w /= 8;\r
+                                                               total_weight += (note_weights[i] = w);\r
+                                                       }\r
+                                                       // さいころを振って音階を決定\r
+                                                       note_no = range.invertedNoteOf(key.rootNoteNumber());\r
+                                                       double r = Math.random();\r
+                                                       total_weight *= r;\r
+                                                       for( i=0; i<note_weights.length; i++ ) {\r
+                                                               if( (total_weight -= note_weights[i]) < 0 ) {\r
+                                                                       note_no = range.min_note + i;\r
+                                                                       break;\r
+                                                               }\r
+                                                       }\r
+                                                       // 決定された音符を追加\r
+                                                       addNote(\r
+                                                               start_tick_pos, tick + min_note_ticks,\r
+                                                               note_no, velocity\r
+                                                       );\r
+                                                       if( random_lyric ) {\r
+                                                               // ランダムな歌詞を追加\r
+                                                               addStringTo(\r
+                                                                       0x05,\r
+                                                                       VocaloidLyricGenerator.getRandomLyric(),\r
+                                                                       start_tick_pos\r
+                                                               );\r
+                                                       }\r
+                                                       prev_note_no = note_no;\r
+                                               }\r
+                                               else if( is_bass ) {\r
+                                                       // ベース音を追加\r
+                                                       int note = range.invertedNoteOf(chord.bassNoteSymbol().toNoteNumber());\r
+                                                       addNote(\r
+                                                               start_tick_pos, tick + min_note_ticks,\r
+                                                               note, velocity\r
+                                                       );\r
+                                               }\r
+                                               else {\r
+                                                       // コード本体の音を追加\r
+                                                       for( int note : notes ) {\r
+                                                               addNote(\r
+                                                                       start_tick_pos, tick + min_note_ticks,\r
+                                                                       note, velocity\r
+                                                               );\r
+                                                       }\r
+                                               }\r
+                                               is_note_on = false;\r
+                                       }\r
+                               }\r
+                       }\r
+\r
+               }\r
+\r
+       }\r
+\r
+       //\r
+       // VOCALOID互換の音素単位で歌詞を生成するクラス\r
+       //\r
+       public static class VocaloidLyricGenerator {\r
+               private static String lyric_elements[] = {\r
+                       /*\r
+                     "きゃ","きゅ","きょ",\r
+                     "しゃ","しゅ","しょ",\r
+                     "ちゃ","ちゅ","ちょ",\r
+                     "にゃ","にゅ","にょ",\r
+                     "ひゃ","ひゅ","ひょ",\r
+                     "みゃ","みゅ","みょ",\r
+                     "りゃ","りゅ","りょ",\r
+                     "ぎゃ","ぎゅ","ぎょ",\r
+                     "じゃ","じゅ","じょ",\r
+                     "ぢゃ","ぢゅ","ぢょ",\r
+                     "びゃ","びゅ","びょ",\r
+                     "ぴゃ","ぴゅ","ぴょ",\r
+                        */\r
+                       "あ","い","う","え","お",\r
+                       "か","き","く","け","こ",\r
+                       "さ","し","す","せ","そ",\r
+                       "た","ち","つ","て","と",\r
+                       "な","に","ぬ","ね","の",\r
+                       "は","ひ","ふ","へ","ほ",\r
+                       "ま","み","む","め","も",\r
+                       "や","ゆ","よ",\r
+                       "ら","り","る","れ","ろ",\r
+                       "わ","を","ん",\r
+                       "が","ぎ","ぐ","げ","ご",\r
+                       "ざ","じ","ず","ぜ","ぞ",\r
+                       "だ","ぢ","づ","で","ど",\r
+                       "ば","び","ぶ","べ","ぼ",\r
+                       "ぱ","ぴ","ぷ","ぺ","ぽ",\r
+               };\r
+               //\r
+               // ランダムに音素を返す\r
+               public static String getRandomLyric() {\r
+                       return lyric_elements[(int)(Math.random() * lyric_elements.length)];\r
+               }\r
+               /*\r
+    // テキストを音素に分解する\r
+    public static Vector<String> split(String text) {\r
+      Vector<String> sv = new Vector<String>();\r
+      String s, prev_s;\r
+      int i;\r
+      for( i=0; i < text.length(); i++ ) {\r
+        s = text.substring(i,i+1);\r
+        if( "ゃゅょ".indexOf(s) < 0 ) {\r
+          sv.add(s);\r
+        }\r
+        else {\r
+          prev_s = sv.remove(sv.size()-1);\r
+          sv.add( prev_s + s );\r
+        }\r
+      }\r
+      return sv;\r
+    }\r
+                */\r
+       }\r
+\r
+}\r
+\r
diff --git a/src/NewSequenceDialog.java b/src/NewSequenceDialog.java
new file mode 100644 (file)
index 0000000..cbeacc4
--- /dev/null
@@ -0,0 +1,761 @@
+\r
+import java.awt.Component;\r
+import java.awt.Dimension;\r
+import java.awt.Graphics;\r
+import java.awt.Graphics2D;\r
+import java.awt.GridLayout;\r
+import java.awt.Insets;\r
+import java.awt.Point;\r
+import java.awt.Rectangle;\r
+import java.awt.event.ActionEvent;\r
+import java.awt.event.ActionListener;\r
+import java.awt.event.ComponentEvent;\r
+import java.awt.event.ComponentListener;\r
+import java.awt.event.InputEvent;\r
+import java.awt.event.MouseEvent;\r
+import java.awt.event.MouseListener;\r
+import java.util.ArrayList;\r
+import java.util.Vector;\r
+\r
+import javax.sound.midi.MidiChannel;\r
+import javax.sound.midi.Sequence;\r
+import javax.swing.BoxLayout;\r
+import javax.swing.JButton;\r
+import javax.swing.JCheckBox;\r
+import javax.swing.JComboBox;\r
+import javax.swing.JComponent;\r
+import javax.swing.JDialog;\r
+import javax.swing.JLabel;\r
+import javax.swing.JPanel;\r
+import javax.swing.JScrollPane;\r
+import javax.swing.JSpinner;\r
+import javax.swing.JTabbedPane;\r
+import javax.swing.JTextArea;\r
+import javax.swing.JTextField;\r
+import javax.swing.SpinnerNumberModel;\r
+import javax.swing.event.ChangeEvent;\r
+import javax.swing.event.ChangeListener;\r
+\r
+class NewSequenceDialog extends JDialog\r
+implements ActionListener\r
+{\r
+       Insets  zero_insets = new Insets(0,0,0,0);\r
+\r
+       JTextArea               chord_text;\r
+       JTextField              seq_name_text;\r
+       PPQSelectionComboBox    ppq_combo_box;\r
+       TimeSignatureSelecter   timesig_selecter;\r
+       TempoSelecter           tempo_selecter;\r
+       MeasureSelecter measure_selecter;\r
+       JButton\r
+       add_new_button,\r
+       transpose_up_button, transpose_down_button, enharmonic_button,\r
+       random_chord_button, toggle_major_minor_button;\r
+       JPanel new_file_panel;\r
+       TrackSpecPanel track_spec_panel;\r
+       JTabbedPane tabbed_pane;\r
+       MidiEditor midi_editor;\r
+\r
+       public NewSequenceDialog( MidiEditor midi_editor ) {\r
+               this.midi_editor = midi_editor;\r
+               setTitle("Generate new sequence - " + ChordHelperApplet.VersionInfo.NAME);\r
+               tabbed_pane = new JTabbedPane();\r
+\r
+               ppq_combo_box = new PPQSelectionComboBox();\r
+               seq_name_text = new JTextField();\r
+               timesig_selecter = new TimeSignatureSelecter();\r
+               tempo_selecter = new TempoSelecter();\r
+               measure_selecter = new MeasureSelecter();\r
+               String initial_string =\r
+                               "Key: C\nC G/B | Am Em/G | F C/E | Dm7 G7 C % | F G7 | Csus4 C\n";\r
+               chord_text = new JTextArea( initial_string, 18, 30 );\r
+               JScrollPane chord_text_scroll_area =\r
+                               new JScrollPane( (Component)chord_text );\r
+\r
+               add_new_button = new JButton(\r
+                               "Generate & Add to PlayList",\r
+                               new ButtonIcon(ButtonIcon.EJECT_ICON)\r
+                               );\r
+               add_new_button.setMargin(zero_insets);\r
+               add_new_button.addActionListener(this);\r
+               transpose_up_button = new JButton(" + Up ");\r
+               transpose_up_button.setMargin(zero_insets);\r
+               transpose_up_button.addActionListener(this);\r
+               transpose_down_button = new JButton(" - Down ");\r
+               transpose_down_button.setMargin(zero_insets);\r
+               transpose_down_button.addActionListener(this);\r
+               enharmonic_button = new JButton(" Enharmonic ");\r
+               enharmonic_button.setMargin(zero_insets);\r
+               enharmonic_button.addActionListener(this);\r
+               random_chord_button = new JButton("Randomize (Tempo, Time signature, Chord progression)");\r
+               random_chord_button.setMargin(zero_insets);\r
+               random_chord_button.addActionListener(this);\r
+               toggle_major_minor_button = new JButton("Relative key");\r
+               toggle_major_minor_button.setMargin(zero_insets);\r
+               toggle_major_minor_button.addActionListener(this);\r
+\r
+               JPanel sequence_name_panel = new JPanel();\r
+               sequence_name_panel.setLayout(\r
+                               new BoxLayout( sequence_name_panel, BoxLayout.LINE_AXIS )\r
+                               );\r
+               sequence_name_panel.add( new JLabel("Sequence name:") );\r
+               sequence_name_panel.add( seq_name_text );\r
+\r
+               JPanel new_file_panel_2 = new JPanel();\r
+               new_file_panel_2.setLayout( new BoxLayout( new_file_panel_2, BoxLayout.LINE_AXIS ) );\r
+               new_file_panel_2.add( new JLabel("Resolution in PPQ =") );\r
+               new_file_panel_2.add( ppq_combo_box );\r
+               new_file_panel_2.add( measure_selecter );\r
+\r
+               JPanel timesig_panel = new JPanel();\r
+               timesig_panel.add( new JLabel("Time signature =") );\r
+               timesig_panel.add( timesig_selecter );\r
+\r
+               JPanel new_file_panel_5 = new JPanel();\r
+               new_file_panel_5.setLayout( new BoxLayout( new_file_panel_5, BoxLayout.LINE_AXIS ) );\r
+               new_file_panel_5.add( tempo_selecter );\r
+               new_file_panel_5.add( timesig_panel );\r
+\r
+               JPanel new_file_panel_6 = new JPanel();\r
+               new_file_panel_6.setLayout( new BoxLayout( new_file_panel_6, BoxLayout.LINE_AXIS ) );\r
+               new_file_panel_6.add( new JLabel("Chord progression :") );\r
+               new_file_panel_6.add( new JLabel("Transpose") );\r
+               new_file_panel_6.add( transpose_up_button );\r
+               new_file_panel_6.add( transpose_down_button );\r
+               new_file_panel_6.add( enharmonic_button );\r
+               new_file_panel_6.add( toggle_major_minor_button );\r
+\r
+               JPanel new_file_panel_10 = new JPanel();\r
+               new_file_panel_10.setLayout( new BoxLayout( new_file_panel_10, BoxLayout.LINE_AXIS ) );\r
+               new_file_panel_10.add( add_new_button );\r
+\r
+               new_file_panel = new JPanel();\r
+               new_file_panel.setLayout( new BoxLayout( new_file_panel, BoxLayout.PAGE_AXIS ) );\r
+               new_file_panel.add( sequence_name_panel );\r
+               new_file_panel.add( new_file_panel_2 );\r
+               new_file_panel.add( random_chord_button );\r
+               new_file_panel.add( new_file_panel_5 );\r
+               new_file_panel.add( new_file_panel_6 );\r
+               new_file_panel.add( chord_text_scroll_area );\r
+               new_file_panel.add( new_file_panel_10 );\r
+\r
+               track_spec_panel = new TrackSpecPanel();\r
+\r
+               tabbed_pane.add( "Sequence", new_file_panel );\r
+               tabbed_pane.add( "Track", track_spec_panel );\r
+               add(tabbed_pane);\r
+               // setLocationRelativeTo(applet);\r
+               setBounds( 250, 200, 600, 540 );\r
+               //\r
+               // Create track specs\r
+               //\r
+               Music.MelodyTrackSpec mts;\r
+               Music.DrumTrackSpec dts;\r
+               //\r
+               dts = new Music.DrumTrackSpec( 9, "Percussion track" );\r
+               dts.velocity = 127;\r
+               track_spec_panel.addTrackSpec(dts);\r
+               //\r
+               mts = new Music.MelodyTrackSpec(\r
+                               0, "Bass track", new Music.Range(36,48)\r
+                               );\r
+               mts.is_bass = true;\r
+               mts.velocity = 96;\r
+               track_spec_panel.addTrackSpec(mts);\r
+               //\r
+               mts =  new Music.MelodyTrackSpec(\r
+                               1, "Chord track", new Music.Range(60,72)\r
+                               );\r
+               track_spec_panel.addTrackSpec(mts);\r
+               //\r
+               mts = new Music.MelodyTrackSpec(\r
+                               2, "Melody track", new Music.Range(60,84)\r
+                               );\r
+               mts.random_melody = true;\r
+               mts.beat_pattern = 0xFFFF;\r
+               mts.continuous_beat_pattern = 0x820A;\r
+               track_spec_panel.addTrackSpec(mts);\r
+       }\r
+       //\r
+       // ActionListener for JButton\r
+       //\r
+       public void actionPerformed(ActionEvent event) {\r
+               Object obj = event.getSource();\r
+               if( obj == add_new_button ) {\r
+                       midi_editor.addSequence(getMidiSequence());\r
+                       setVisible(false);\r
+               }\r
+               else if( obj == transpose_up_button ) { transpose(1); }\r
+               else if( obj == transpose_down_button ) { transpose(-1); }\r
+               else if( obj == enharmonic_button ) { enharmonic(); }\r
+               else if( obj == toggle_major_minor_button ) {\r
+                       toggleKeyMajorMinor();\r
+               }\r
+               else if( obj == random_chord_button ) {\r
+                       setRandomChordProgression(\r
+                                       measure_selecter.getMeasureDuration()\r
+                                       );\r
+               }\r
+       }\r
+       // Methods\r
+       //\r
+       public void setChannels( MidiChannel[] midi_channels ) {\r
+               track_spec_panel.setChannels(midi_channels);\r
+       }\r
+       public Music.ChordProgression getChordProgression() {\r
+               return new Music.ChordProgression( chord_text.getText() );\r
+       }\r
+       public Sequence getMidiSequence() {\r
+               Music.FirstTrackSpec first_track_spec = new Music.FirstTrackSpec(\r
+                               seq_name_text.getText(),\r
+                               tempo_selecter.getTempoByteArray(),\r
+                               timesig_selecter.getByteArray()\r
+                               );\r
+               return getChordProgression().toMidiSequence(\r
+                               ppq_combo_box.getPPQ(),\r
+                               measure_selecter.getStartMeasurePosition(),\r
+                               measure_selecter.getEndMeasurePosition(),\r
+                               first_track_spec,\r
+                               track_spec_panel.getTrackSpecs()\r
+                               );\r
+       }\r
+       public void setChordProgression( Music.ChordProgression cp ) {\r
+               chord_text.setText( cp.toString() );\r
+       }\r
+       public void setRandomChordProgression( int measure_length ) {\r
+               //\r
+               // テンポ・拍子・コード進行をランダムに設定\r
+               //\r
+               tempo_selecter.setTempo( 80 + (int)(Math.random() * 100) );\r
+               int timesig_upper = 4;\r
+               int timesig_lower_index = 2;\r
+               switch( (int)(Math.random() * 10) ) {\r
+               case 0: timesig_upper = 3; break; // 3/4\r
+               }\r
+               timesig_selecter.setValue(\r
+                               (byte)timesig_upper,\r
+                               (byte)timesig_lower_index\r
+                               );\r
+               setChordProgression(\r
+                               new Music.ChordProgression( measure_length, timesig_upper )\r
+                               );\r
+       }\r
+       public void transpose(int chromatic_offset) {\r
+               Music.ChordProgression cp = getChordProgression();\r
+               cp.transpose( chromatic_offset );\r
+               setChordProgression( cp );\r
+       }\r
+       public void enharmonic() {\r
+               Music.ChordProgression cp = getChordProgression();\r
+               cp.toggleEnharmonically();\r
+               setChordProgression( cp );\r
+       }\r
+       public void toggleKeyMajorMinor() {\r
+               Music.ChordProgression cp = getChordProgression();\r
+               cp.toggleKeyMajorMinor();\r
+               setChordProgression( cp );\r
+       }\r
+}\r
+\r
+// トラック設定画面\r
+//\r
+class TrackSpecPanel extends JPanel\r
+implements PianoKeyboardListener, ActionListener, ChangeListener\r
+{\r
+       JComboBox<Music.AbstractNoteTrackSpec> trackSelecter;\r
+       JLabel track_type_label;\r
+       JTextField name_text_field;\r
+       MidiChannelComboSelecter ch_selecter;\r
+       MidiProgramSelecter pg_selecter;\r
+       MidiProgramFamilySelecter pg_family_selecter;\r
+       PianoKeyboardPanel keyboard_panel;\r
+       JPanel range_panel;\r
+       JCheckBox random_melody_checkbox;\r
+       JCheckBox bass_checkbox;\r
+       JCheckBox random_lyric_checkbox;\r
+       BeatPadPanel beat_pad_panel;\r
+       private MidiChannel[] midi_channels;\r
+\r
+       public TrackSpecPanel() {\r
+               //\r
+               name_text_field = new JTextField(20);\r
+               name_text_field.addActionListener(this);\r
+               //\r
+               // 音色(プログラム)設定\r
+               pg_family_selecter = new MidiProgramFamilySelecter(\r
+                               pg_selecter = new MidiProgramSelecter()\r
+                               );\r
+               pg_selecter.setFamilySelecter(\r
+                               pg_family_selecter\r
+                               );\r
+               // 音域指定\r
+               //\r
+               keyboard_panel = new PianoKeyboardPanel();\r
+               keyboard_panel.keyboard.octaveSizeModel.setValue(6);\r
+               keyboard_panel.keyboard.setPreferredSize(new Dimension(400,40));\r
+               keyboard_panel.keyboard.setMaxSelectable(2);\r
+               keyboard_panel.keyboard.addPianoKeyboardListener(this);\r
+               //\r
+               // ビート設定\r
+               beat_pad_panel = new BeatPadPanel(this);\r
+               //\r
+               JPanel track_selecter_panel = new JPanel();\r
+               track_selecter_panel.add( new JLabel("Track select:") );\r
+               track_selecter_panel.add(\r
+                               trackSelecter = new JComboBox<Music.AbstractNoteTrackSpec>()\r
+                               );\r
+               add( track_selecter_panel );\r
+\r
+               add( track_type_label = new JLabel() );\r
+\r
+               JPanel track_name_panel = new JPanel();\r
+               track_name_panel.add( new JLabel(\r
+                               "Track name (Press [Enter] key to change):"\r
+                               ) );\r
+               track_name_panel.add( name_text_field );\r
+               add( track_name_panel );\r
+\r
+               add( ch_selecter = new MidiChannelComboSelecter(\r
+                               "MIDI Channel:"\r
+                               ) );\r
+               add(new VelocitySelecter(\r
+                               keyboard_panel.keyboard.velocityModel\r
+                               ));\r
+\r
+               JPanel pg_panel = new JPanel();\r
+               pg_panel.add( pg_family_selecter );\r
+               pg_panel.add( pg_selecter );\r
+               add(pg_panel);\r
+\r
+               range_panel = new JPanel();\r
+               range_panel.add( new JLabel("Range:") );\r
+               range_panel.add( keyboard_panel );\r
+               add(range_panel);\r
+\r
+               bass_checkbox = new JCheckBox("Bass note");\r
+               bass_checkbox.addChangeListener(this);\r
+               add(bass_checkbox);\r
+\r
+               random_melody_checkbox = new JCheckBox("Random melody");\r
+               random_melody_checkbox.addChangeListener(this);\r
+               add(random_melody_checkbox);\r
+\r
+               random_lyric_checkbox = new JCheckBox("Random lyrics");\r
+               random_lyric_checkbox.addChangeListener(this);\r
+               add(random_lyric_checkbox);\r
+\r
+               add(beat_pad_panel);\r
+\r
+               trackSelecter.addActionListener(this);\r
+               ch_selecter.comboBox.addActionListener(this);\r
+               keyboard_panel.keyboard.velocityModel.addChangeListener(\r
+                               new ChangeListener() {\r
+                                       public void stateChanged(ChangeEvent e) {\r
+                                               Music.AbstractNoteTrackSpec ants = getTrackSpec();\r
+                                               ants.velocity = keyboard_panel.keyboard.velocityModel.getValue();\r
+                                       }\r
+                               }\r
+                               );\r
+               pg_selecter.addActionListener(this);\r
+       }\r
+\r
+       // ChangeListener\r
+       //\r
+       public void stateChanged(ChangeEvent e) {\r
+               Object src = e.getSource();\r
+               if( src == bass_checkbox ) {\r
+                       Music.AbstractNoteTrackSpec ants = getTrackSpec();\r
+                       if( ants instanceof Music.MelodyTrackSpec ) {\r
+                               Music.MelodyTrackSpec mts = (Music.MelodyTrackSpec)ants;\r
+                               mts.is_bass = bass_checkbox.isSelected();\r
+                       }\r
+               }\r
+               else if( src == random_melody_checkbox ) {\r
+                       Music.AbstractNoteTrackSpec ants = getTrackSpec();\r
+                       if( ants instanceof Music.MelodyTrackSpec ) {\r
+                               Music.MelodyTrackSpec mts = (Music.MelodyTrackSpec)ants;\r
+                               mts.random_melody = random_melody_checkbox.isSelected();\r
+                       }\r
+               }\r
+               else if( src == random_lyric_checkbox ) {\r
+                       Music.AbstractNoteTrackSpec ants = getTrackSpec();\r
+                       if( ants instanceof Music.MelodyTrackSpec ) {\r
+                               Music.MelodyTrackSpec mts = (Music.MelodyTrackSpec)ants;\r
+                               mts.random_lyric = random_lyric_checkbox.isSelected();\r
+                       }\r
+               }\r
+       }\r
+       // ActionListener\r
+       //\r
+       public void actionPerformed(ActionEvent e) {\r
+               Object src = e.getSource();\r
+               Music.AbstractNoteTrackSpec ants;\r
+               if( src == name_text_field ) {\r
+                       getTrackSpec().name = name_text_field.getText();\r
+               }\r
+               else if( src == trackSelecter ) {\r
+                       ants = (Music.AbstractNoteTrackSpec)(\r
+                                       trackSelecter.getSelectedItem()\r
+                                       );\r
+                       String track_type_string = "Track type: " + (\r
+                                       ants instanceof Music.DrumTrackSpec ? "Percussion" :\r
+                                               ants instanceof Music.MelodyTrackSpec ? "Melody" :\r
+                                                       "(Unknown)"\r
+                                       );\r
+                       track_type_label.setText(track_type_string);\r
+                       name_text_field.setText( ants.name );\r
+                       ch_selecter.setSelectedChannel( ants.midi_channel );\r
+                       keyboard_panel.keyboard.velocityModel.setValue( ants.velocity );\r
+                       pg_selecter.setProgram( ants.program_no );\r
+                       keyboard_panel.keyboard.clear();\r
+                       if( ants instanceof Music.DrumTrackSpec ) {\r
+                               range_panel.setVisible(false);\r
+                               random_melody_checkbox.setVisible(false);\r
+                               random_lyric_checkbox.setVisible(false);\r
+                               bass_checkbox.setVisible(false);\r
+                       }\r
+                       else if( ants instanceof Music.MelodyTrackSpec ) {\r
+                               Music.MelodyTrackSpec ts = (Music.MelodyTrackSpec)ants;\r
+                               range_panel.setVisible(true);\r
+                               keyboard_panel.keyboard.setSelectedNote(ts.range.min_note);\r
+                               keyboard_panel.keyboard.setSelectedNote(ts.range.max_note);\r
+                               keyboard_panel.keyboard.autoScroll(ts.range.min_note);\r
+                               random_melody_checkbox.setSelected(ts.random_melody);\r
+                               random_lyric_checkbox.setSelected(ts.random_lyric);\r
+                               bass_checkbox.setSelected(ts.is_bass);\r
+                               random_melody_checkbox.setVisible(true);\r
+                               random_lyric_checkbox.setVisible(true);\r
+                               bass_checkbox.setVisible(true);\r
+                       }\r
+                       beat_pad_panel.setTrackSpec(ants);\r
+               }\r
+               else if( src == ch_selecter.comboBox ) {\r
+                       getTrackSpec().midi_channel = ch_selecter.getSelectedChannel();\r
+               }\r
+               else if( src == pg_selecter ) {\r
+                       getTrackSpec().program_no = pg_selecter.getProgram();\r
+               }\r
+       }\r
+       // PianoKeyboardListener\r
+       //\r
+       public void pianoKeyPressed(int n, InputEvent e) {\r
+               noteOn(n);\r
+               Music.AbstractNoteTrackSpec ants = getTrackSpec();\r
+               if( ants instanceof Music.MelodyTrackSpec ) {\r
+                       Music.MelodyTrackSpec ts = (Music.MelodyTrackSpec)ants;\r
+                       ts.range = new Music.Range(\r
+                                       keyboard_panel.keyboard.getSelectedNotes()\r
+                                       );\r
+               }\r
+       }\r
+       public void pianoKeyReleased(int n, InputEvent e) {\r
+               noteOff(n);\r
+       }\r
+       public void octaveMoved(ChangeEvent event) {}\r
+       public void octaveResized(ChangeEvent event) {}\r
+       //\r
+       public void noteOn(int n) {\r
+               if( midi_channels != null ) {\r
+                       midi_channels[ch_selecter.getSelectedChannel()].\r
+                       noteOn( n, keyboard_panel.keyboard.velocityModel.getValue() );\r
+               }\r
+       }\r
+       public void noteOff(int n) {\r
+               if( midi_channels != null ) {\r
+                       midi_channels[ch_selecter.getSelectedChannel()].\r
+                       noteOff( n, keyboard_panel.keyboard.velocityModel.getValue() );\r
+               }\r
+       }\r
+       public void setChannels( MidiChannel midi_channels[] ) {\r
+               this.midi_channels = midi_channels;\r
+       }\r
+       public Music.AbstractNoteTrackSpec getTrackSpec() {\r
+               Object track_spec_obj = trackSelecter.getSelectedItem();\r
+               Music.AbstractNoteTrackSpec ants = (Music.AbstractNoteTrackSpec)track_spec_obj;\r
+               ants.name = name_text_field.getText();\r
+               return ants;\r
+       }\r
+       public Vector<Music.AbstractNoteTrackSpec> getTrackSpecs() {\r
+               Vector<Music.AbstractNoteTrackSpec> track_specs = new Vector<>();\r
+               int i=0, n_items = trackSelecter.getItemCount();\r
+               while( i < n_items ) {\r
+                       track_specs.add(\r
+                                       (Music.AbstractNoteTrackSpec)trackSelecter.getItemAt(i++)\r
+                                       );\r
+               }\r
+               return track_specs;\r
+       }\r
+       public void addTrackSpec( Music.AbstractNoteTrackSpec track_spec ) {\r
+               trackSelecter.addItem(track_spec);\r
+       }\r
+}\r
+\r
+class PPQSelectionComboBox extends JComboBox<Integer> {\r
+       private static final int[] PPQList = {\r
+               48,60,80,96,120,160,192,240,320,384,480,960\r
+       };\r
+       public PPQSelectionComboBox() {\r
+               for( int ppq : PPQList ) addItem(ppq);\r
+       }\r
+       public int getPPQ() {\r
+               return (Integer) getSelectedItem();\r
+               // Integer.decode( (String) ).intValue();\r
+       }\r
+}\r
+\r
+class MeasureSelecter extends JPanel {\r
+       SpinnerNumberModel\r
+       start_model = new SpinnerNumberModel( 3, 1, 9999, 1 ),\r
+       end_model   = new SpinnerNumberModel( 8, 1, 9999, 1 );\r
+       public MeasureSelecter() {\r
+               JSpinner start_spinner = new JSpinner(start_model);\r
+               JSpinner end_spinner = new JSpinner(end_model);\r
+               setLayout( new GridLayout(2,3) );\r
+               add( new JLabel() );\r
+               add( new JLabel("Start",JLabel.CENTER) );\r
+               add( new JLabel("End",JLabel.CENTER) );\r
+               add( new JLabel("Measure",JLabel.RIGHT) );\r
+               add(start_spinner);\r
+               add(end_spinner);\r
+       }\r
+       public int getStartMeasurePosition() {\r
+               return start_model.getNumber().intValue();\r
+       }\r
+       public int getEndMeasurePosition() {\r
+               return end_model.getNumber().intValue();\r
+       }\r
+       public int getMeasureDuration() {\r
+               return\r
+                               end_model.getNumber().intValue()\r
+                               - start_model.getNumber().intValue() + 1;\r
+       }\r
+}\r
+\r
+//////////////////////////////////////////////////////////////////\r
+//\r
+// □=□=□=□=□=□=□=□=\r
+// □=□=□=□=□=□=□=□=\r
+//\r
+class BeatPadPanel extends JPanel implements ActionListener {\r
+       PianoKeyboardListener piano_keyboard_listener;\r
+       Music.AbstractNoteTrackSpec track_spec;\r
+       JPanel percussion_selecters_panel;\r
+       java.util.List<JComboBox<String>> percussionSelecters =\r
+               new ArrayList<JComboBox<String>>() {\r
+                       {\r
+                               for( int i=0; i < Music.DrumTrackSpec.default_percussions.length; i++  ) {\r
+                                       add(new JComboBox<String>());\r
+                               }\r
+                       }\r
+               };\r
+       BeatPad beat_pad;\r
+\r
+       public BeatPadPanel(PianoKeyboardListener pkl) {\r
+               piano_keyboard_listener = pkl;\r
+               percussion_selecters_panel = new JPanel();\r
+               percussion_selecters_panel.setLayout(\r
+                       new BoxLayout( percussion_selecters_panel, BoxLayout.Y_AXIS )\r
+               );\r
+               for( JComboBox<String> cb : percussionSelecters ) {\r
+                       percussion_selecters_panel.add(cb);\r
+                       cb.addActionListener(this);\r
+               }\r
+               add( percussion_selecters_panel );\r
+               add( beat_pad = new BeatPad(pkl) );\r
+               beat_pad.setPreferredSize( new Dimension(400,200) );\r
+               setLayout( new BoxLayout( this, BoxLayout.X_AXIS ) );\r
+       }\r
+       public void actionPerformed(ActionEvent e) {\r
+               Object src = e.getSource();\r
+               for( JComboBox<String> cb : percussionSelecters ) {\r
+                       if( src != cb ) continue;\r
+                       int note_no = (\r
+                               (Music.DrumTrackSpec.PercussionComboBoxModel)cb.getModel()\r
+                       ).getSelectedNoteNo();\r
+                       piano_keyboard_listener.pianoKeyPressed(note_no,(InputEvent)null);\r
+               }\r
+       }\r
+       public void setTrackSpec( Music.AbstractNoteTrackSpec ants ) {\r
+               track_spec = ants;\r
+               beat_pad.setTrackSpec(ants);\r
+               if( ants instanceof Music.DrumTrackSpec ) {\r
+                       Music.DrumTrackSpec dts = (Music.DrumTrackSpec)ants;\r
+                       int i=0;\r
+                       for( JComboBox<String> cb : percussionSelecters ) {\r
+                               cb.setModel(dts.models[i++]);\r
+                       }\r
+                       percussion_selecters_panel.setVisible(true);\r
+               }\r
+               else if( ants instanceof Music.MelodyTrackSpec ) {\r
+                       percussion_selecters_panel.setVisible(false);\r
+               }\r
+       }\r
+}\r
+\r
+class BeatPad extends JComponent\r
+implements MouseListener, ComponentListener\r
+{\r
+       PianoKeyboardListener piano_keyboard_listener;\r
+       private int on_note_no = -1;\r
+       Music.AbstractNoteTrackSpec track_spec;\r
+\r
+       public static final int MAX_BEATS = 16;\r
+       public static final int MAX_NOTES = 8;\r
+       Rectangle beat_buttons[][];\r
+       Rectangle continuous_beat_buttons[][];\r
+\r
+       public BeatPad(PianoKeyboardListener pkl) {\r
+               piano_keyboard_listener = pkl;\r
+               addMouseListener(this);\r
+               addComponentListener(this);\r
+               // addMouseMotionListener(this);\r
+       }\r
+       public void paint(Graphics g) {\r
+               super.paint(g);\r
+               Graphics2D g2 = (Graphics2D) g;\r
+               Rectangle r;\r
+               int note, beat, mask;\r
+\r
+               if( track_spec instanceof Music.DrumTrackSpec ) {\r
+                       Music.DrumTrackSpec dts = (Music.DrumTrackSpec)track_spec;\r
+                       for( note=0; note<dts.beat_patterns.length; note++ ) {\r
+                               for( beat=0, mask=0x8000; beat<MAX_BEATS; beat++, mask >>>= 1 ) {\r
+                                       r = beat_buttons[note][beat];\r
+                                       if( (dts.beat_patterns[note] & mask) != 0 )\r
+                                               g2.fillRect( r.x, r.y, r.width, r.height );\r
+                                       else\r
+                                               g2.drawRect( r.x, r.y, r.width, r.height );\r
+                               }\r
+                       }\r
+               }\r
+               else if( track_spec instanceof Music.MelodyTrackSpec ) {\r
+                       Music.MelodyTrackSpec mts = (Music.MelodyTrackSpec)track_spec;\r
+                       for( beat=0, mask=0x8000; beat<MAX_BEATS; beat++, mask >>>= 1 ) {\r
+                               r = beat_buttons[0][beat];\r
+                               if( (mts.beat_pattern & mask) != 0 )\r
+                                       g2.fillRect( r.x, r.y, r.width, r.height );\r
+                               else\r
+                                       g2.drawRect( r.x, r.y, r.width, r.height );\r
+                               r = continuous_beat_buttons[0][beat];\r
+                               if( (mts.continuous_beat_pattern & mask) != 0 )\r
+                                       g2.fillRect( r.x, r.y, r.width, r.height );\r
+                               else\r
+                                       g2.drawRect( r.x, r.y, r.width, r.height );\r
+                       }\r
+               }\r
+\r
+       }\r
+       // ComponentListener\r
+       //\r
+       public void componentShown(ComponentEvent e) { }\r
+       public void componentHidden(ComponentEvent e) { }\r
+       public void componentMoved(ComponentEvent e) { }\r
+       public void componentResized(ComponentEvent e) {\r
+               sizeChanged();\r
+       }\r
+       // MouseListener\r
+       //\r
+       public void mousePressed(MouseEvent e) {\r
+               catchEvent(e);\r
+               if( on_note_no >= 0 ) {\r
+                       piano_keyboard_listener.pianoKeyPressed( on_note_no ,(InputEvent)e );\r
+               }\r
+       }\r
+       public void mouseReleased(MouseEvent e) {\r
+               if( on_note_no >= 0 ) {\r
+                       piano_keyboard_listener.pianoKeyReleased( on_note_no ,(InputEvent)e );\r
+               }\r
+               on_note_no = -1;\r
+       }\r
+       public void mouseEntered(MouseEvent e) {\r
+               if( (e.getModifiers() & InputEvent.BUTTON1_MASK)\r
+                               == InputEvent.BUTTON1_MASK\r
+                               ) {\r
+                       catchEvent(e);\r
+               }\r
+       }\r
+       public void mouseExited(MouseEvent e) { }\r
+       public void mouseClicked(MouseEvent e) { }\r
+       //\r
+       // MouseMotionListener\r
+       //\r
+       /*\r
+  public void mouseDragged(MouseEvent e) {\r
+    catchEvent(e);\r
+  }\r
+  public void mouseMoved(MouseEvent e) { }\r
+        */\r
+       //\r
+       // Methods\r
+       //\r
+       private void sizeChanged() {\r
+               int beat, note, width, height;\r
+               Dimension d = getSize();\r
+\r
+               int num_notes = 1;\r
+               if( track_spec instanceof Music.DrumTrackSpec ) {\r
+                       Music.DrumTrackSpec dts = (Music.DrumTrackSpec)track_spec;\r
+                       num_notes = dts.models.length;\r
+               }\r
+\r
+               beat_buttons = new Rectangle[num_notes][];\r
+               continuous_beat_buttons = new Rectangle[num_notes][];\r
+               for( note=0; note<beat_buttons.length; note++ ) {\r
+                       beat_buttons[note] = new Rectangle[MAX_BEATS];\r
+                       continuous_beat_buttons[note] = new Rectangle[MAX_BEATS];\r
+                       for( beat=0; beat<MAX_BEATS; beat++ ) {\r
+                               width = (d.width * 3) / (MAX_BEATS * 4);\r
+                               height = d.height / num_notes - 1;\r
+                               beat_buttons[note][beat] = new Rectangle(\r
+                                               beat * d.width / MAX_BEATS,\r
+                                               note * height,\r
+                                               width,\r
+                                               height\r
+                                               );\r
+                               width = d.width / (MAX_BEATS * 3);\r
+                               continuous_beat_buttons[note][beat] = new Rectangle(\r
+                                               (beat+1) * d.width / MAX_BEATS - width + 1,\r
+                                               note * height + height / 3,\r
+                                               width-1,\r
+                                               height / 3\r
+                                               );\r
+                       }\r
+               }\r
+       }\r
+       private void catchEvent(MouseEvent e) {\r
+               Point point = e.getPoint();\r
+               int note, beat, mask;\r
+\r
+               // ビートパターンのビットを反転\r
+               if( track_spec instanceof Music.DrumTrackSpec ) {\r
+                       Music.DrumTrackSpec dts = (Music.DrumTrackSpec)track_spec;\r
+                       for( note=0; note<dts.beat_patterns.length; note++ ) {\r
+                               for( beat=0, mask=0x8000; beat<MAX_BEATS; beat++, mask >>>= 1 ) {\r
+                                       if( beat_buttons[note][beat].contains(point) ) {\r
+                                               dts.beat_patterns[note] ^= mask;\r
+                                               on_note_no = dts.models[note].getSelectedNoteNo();\r
+                                               repaint(); return;\r
+                                       }\r
+                               }\r
+                       }\r
+               }\r
+               else if( track_spec instanceof Music.MelodyTrackSpec ) {\r
+                       Music.MelodyTrackSpec mts = (Music.MelodyTrackSpec)track_spec;\r
+                       for( beat=0, mask=0x8000; beat<MAX_BEATS; beat++, mask >>>= 1 ) {\r
+                               if( beat_buttons[0][beat].contains(point) ) {\r
+                                       mts.beat_pattern ^= mask;\r
+                                       repaint(); return;\r
+                               }\r
+                               if( continuous_beat_buttons[0][beat].contains(point) ) {\r
+                                       mts.continuous_beat_pattern ^= mask;\r
+                                       repaint(); return;\r
+                               }\r
+                       }\r
+               }\r
+       }\r
+       public void setTrackSpec( Music.AbstractNoteTrackSpec ants ) {\r
+               track_spec = ants;\r
+               sizeChanged();\r
+               repaint();\r
+       }\r
+}\r
diff --git a/src/PianoKeyboard.java b/src/PianoKeyboard.java
new file mode 100644 (file)
index 0000000..8943422
--- /dev/null
@@ -0,0 +1,948 @@
+\r
+import java.awt.BorderLayout;\r
+import java.awt.Color;\r
+import java.awt.Dimension;\r
+import java.awt.Graphics;\r
+import java.awt.Graphics2D;\r
+import java.awt.Insets;\r
+import java.awt.Point;\r
+import java.awt.Rectangle;\r
+import java.awt.event.ActionEvent;\r
+import java.awt.event.ComponentAdapter;\r
+import java.awt.event.ComponentEvent;\r
+import java.awt.event.FocusEvent;\r
+import java.awt.event.FocusListener;\r
+import java.awt.event.InputEvent;\r
+import java.awt.event.KeyAdapter;\r
+import java.awt.event.KeyEvent;\r
+import java.awt.event.MouseAdapter;\r
+import java.awt.event.MouseEvent;\r
+import java.awt.event.MouseMotionAdapter;\r
+import java.util.EventListener;\r
+import java.util.LinkedList;\r
+import java.util.Vector;\r
+\r
+import javax.sound.midi.MidiChannel;\r
+import javax.swing.AbstractAction;\r
+import javax.swing.Action;\r
+import javax.swing.Box;\r
+import javax.swing.BoxLayout;\r
+import javax.swing.DefaultBoundedRangeModel;\r
+import javax.swing.JButton;\r
+import javax.swing.JComponent;\r
+import javax.swing.JPanel;\r
+import javax.swing.JScrollBar;\r
+import javax.swing.JSlider;\r
+import javax.swing.event.ChangeEvent;\r
+import javax.swing.event.ChangeListener;\r
+import javax.swing.event.ListDataEvent;\r
+import javax.swing.event.ListDataListener;\r
+\r
+interface PianoKeyboardListener extends EventListener {\r
+       void pianoKeyPressed(int note_no, InputEvent event);\r
+       void pianoKeyReleased(int note_no, InputEvent event);\r
+       void octaveMoved(ChangeEvent e);\r
+       void octaveResized(ChangeEvent e);\r
+}\r
+abstract class PianoKeyboardAdapter implements PianoKeyboardListener {\r
+       public void pianoKeyPressed(int n, InputEvent e) { }\r
+       public void pianoKeyReleased(int n, InputEvent e) { }\r
+       public void octaveMoved(ChangeEvent e) { }\r
+       public void octaveResized(ChangeEvent e) { }\r
+}\r
+\r
+/**\r
+ * Piano Keyboard class for MIDI Chord Helper\r
+ *\r
+ * @author\r
+ *     Copyright (C) 2004-2013 Akiyoshi Kamide\r
+ *     http://www.yk.rim.or.jp/~kamide/music/chordhelper/\r
+ */\r
+public class PianoKeyboard extends JComponent {\r
+       /**\r
+        * 最小オクターブ幅\r
+        */\r
+       public static final int MIN_OCTAVES = 3;\r
+       /**\r
+        * 最大オクターブ幅\r
+        */\r
+       public static final int MAX_OCTAVES = MIDISpec.MAX_NOTE_NO / 12 + 1;\r
+       /**\r
+        * 濃いピンク\r
+        */\r
+       public static final Color DARK_PINK = new Color(0xFF,0x50,0x80);\r
+\r
+       Dimension       whiteKeySize;\r
+       Dimension       blackKeySize;\r
+       boolean         isDark = false;\r
+       float           widthPerOctave = 120;\r
+\r
+       PianoKey[] keys;\r
+       PianoKey[] blackKeys;\r
+       PianoKey[] whiteKeys;\r
+\r
+       DefaultBoundedRangeModel octaveRangeModel;\r
+       DefaultBoundedRangeModel octaveSizeModel;\r
+\r
+       VelocityModel velocityModel = new VelocityModel();\r
+\r
+       DefaultMidiChannelComboBoxModel\r
+               midiChComboboxModel = new DefaultMidiChannelComboBoxModel();\r
+\r
+       NoteList selectedKeyNoteList = new NoteList();\r
+       private int     max_selectable = 1;\r
+       Music.Key       key_signature = null;\r
+       Music.Chord     chord = null;\r
+\r
+       class NoteList extends LinkedList<Integer> { }\r
+\r
+       public ChordMatrix chord_matrix;\r
+       public ChordDisplay chordDisplay;\r
+       public AnoGakkiLayeredPane anoGakkiLayeredPane;\r
+       public MidiChannelButtonSelecter midi_ch_button_selecter;\r
+\r
+       NoteList[] channel_notes = new NoteList[MIDISpec.MAX_CHANNELS];\r
+       int[] pitch_bend_values = new int[MIDISpec.MAX_CHANNELS];\r
+       int[] pitch_bend_sensitivities = new int[MIDISpec.MAX_CHANNELS];\r
+       int[] modulations = new int[MIDISpec.MAX_CHANNELS];\r
+       VirtualMidiDevice midiDevice = new AbstractVirtualMidiDevice() {\r
+               {\r
+                       info = new MyInfo();\r
+                       setReceiver( new AbstractMidiStatus() {\r
+                               {\r
+                                       for( int i=0; i<MIDISpec.MAX_CHANNELS; i++ )\r
+                                               add(new MidiChannelStatus(i));\r
+                               }\r
+                       });\r
+               }\r
+               class MyInfo extends Info {\r
+                       protected MyInfo() {\r
+                               super(\r
+                                       "MIDI Keyboard",\r
+                                       "Unknown vendor",\r
+                                       "Software MIDI keyboard",\r
+                                       ""\r
+                               );\r
+                       }\r
+               }\r
+       };\r
+       class MidiChannelStatus extends AbstractMidiChannelStatus {\r
+               public MidiChannelStatus(int channel) {\r
+                       super(channel);\r
+                       channel_notes[channel] = new NoteList();\r
+                       pitch_bend_sensitivities[channel] = 2; // Default is wholetone = 2 semitones\r
+               }\r
+               public void fireRpnChanged() {\r
+                       if( data_for != DATA_FOR_RPN ) return;\r
+\r
+                       // RPN (MSB) - Accept 0x00 only\r
+                       if( controller_values[0x65] != 0x00 ) return;\r
+\r
+                       // RPN (LSB)\r
+                       switch( controller_values[0x64] ) {\r
+                       case 0x00: // Pitch Bend Sensitivity\r
+                               if( controller_values[0x06] == 0 ) return;\r
+                               pitch_bend_sensitivities[channel] = controller_values[0x06];\r
+                               break;\r
+                       }\r
+               }\r
+               //\r
+               // MidiChannel interface\r
+               //\r
+               public void noteOff( int note_no, int velocity ) {\r
+                       noteOff(note_no);\r
+               }\r
+               public void noteOff( int note_no ) {\r
+                       keyOff( channel, note_no );\r
+                       if( chord_matrix != null ) {\r
+                               if( ! isRhythmPart() )\r
+                                       chord_matrix.note(false, note_no);\r
+                       }\r
+                       if( midi_ch_button_selecter != null ) {\r
+                               midi_ch_button_selecter.repaint();\r
+                       }\r
+               }\r
+               public void noteOn( int note_no, int velocity ) {\r
+                       if( velocity <= 0 ) {\r
+                               noteOff(note_no); return;\r
+                       }\r
+                       keyOn( channel, note_no );\r
+                       if( midiChComboboxModel.getSelectedChannel() == channel ) {\r
+                               if( chordDisplay != null ) {\r
+                                       if( chord_matrix != null && chord_matrix.isPlaying() )\r
+                                               chordDisplay.setNote(-1);\r
+                                       else\r
+                                               chordDisplay.setNote( note_no, isRhythmPart() );\r
+                               }\r
+                               if( anoGakkiLayeredPane != null ) {\r
+                                       PianoKey piano_key = getPianoKey(note_no);\r
+                                       if( piano_key != null )\r
+                                               anoGakkiLayeredPane.start(\r
+                                                               PianoKeyboard.this, piano_key.indicator\r
+                                                               );\r
+                               }\r
+                       }\r
+                       if( chord_matrix != null ) {\r
+                               if( ! isRhythmPart() )\r
+                                       chord_matrix.note(true, note_no);\r
+                       }\r
+                       if( midi_ch_button_selecter != null ) {\r
+                               midi_ch_button_selecter.repaint();\r
+                       }\r
+               }\r
+               public void allNotesOff() {\r
+                       allKeysOff( channel, -1 );\r
+                       if( chord_matrix != null )\r
+                               chord_matrix.clearIndicators();\r
+               }\r
+               public void setPitchBend(int bend) {\r
+                       super.setPitchBend(bend);\r
+                       pitch_bend_values[channel] = bend;\r
+                       repaintNotes();\r
+               }\r
+               public void resetAllControllers() {\r
+                       super.resetAllControllers();\r
+                       //\r
+                       // See also: Response to Reset All Controllers\r
+                       //     http://www.midi.org/about-midi/rp15.shtml\r
+                       //\r
+                       pitch_bend_values[channel] = MIDISpec.PITCH_BEND_NONE;\r
+                       modulations[channel] = 0;\r
+                       repaintNotes();\r
+               }\r
+               public void controlChange(int controller, int value) {\r
+                       super.controlChange(controller,value);\r
+                       switch( controller ) {\r
+                       case 0x01: // Moduration (MSB)\r
+                               modulations[channel] = value;\r
+                               repaintNotes();\r
+                               break;\r
+                       }\r
+               }\r
+               private void repaintNotes() {\r
+                       if( midiChComboboxModel.getSelectedChannel() != channel\r
+                                       || channel_notes[channel] == null\r
+                                       )\r
+                               return;\r
+                       if( channel_notes[channel].size() > 0 || selectedKeyNoteList.size() > 0 )\r
+                               repaint();\r
+               }\r
+       }\r
+       public MidiChannel getSelectedChannel() {\r
+               return midiDevice.getChannels()[\r
+                                                midiChComboboxModel.getSelectedChannel()\r
+                                                ];\r
+       }\r
+       public void note(boolean is_on, int note_no) {\r
+               MidiChannel ch = getSelectedChannel();\r
+               int velocity = velocityModel.getValue();\r
+               if( is_on )\r
+                       ch.noteOn(note_no,velocity);\r
+               else\r
+                       ch.noteOff(note_no,velocity);\r
+       }\r
+       public void noteOn(int note_no) { note(true,note_no); }\r
+       public void noteOff(int note_no) { note(false,note_no); }\r
+\r
+       class PianoKey extends Rectangle {\r
+               public boolean is_black = false;\r
+               public int position = 0;\r
+               public String binded_key_char = null;\r
+               public Rectangle indicator;\r
+               public boolean out_of_bounds = false;\r
+               public PianoKey( Point p, Dimension d, Dimension indicator_size ) {\r
+                       super(p,d);\r
+                       Point indicator_position = new Point(\r
+                                       p.x + (d.width - indicator_size.width) / 2,\r
+                                       p.y + d.height - indicator_size.height - indicator_size.height / 2 + 2\r
+                                       );\r
+                       indicator = new Rectangle( indicator_position, indicator_size );\r
+               }\r
+               int getNote(int chromatic_offset) {\r
+                       int n = position + chromatic_offset;\r
+                       return (out_of_bounds = ( n > MIDISpec.MAX_NOTE_NO )) ? -1 : n;\r
+               }\r
+               boolean paintKey(Graphics2D g2, boolean is_pressed) {\r
+                       if( out_of_bounds ) return false;\r
+                       g2.fill3DRect( x, y, width, height, !is_pressed );\r
+                       return true;\r
+               }\r
+               boolean paintKey(Graphics2D g2) {\r
+                       return paintKey(g2,false);\r
+               }\r
+               boolean paintKeyBinding(Graphics2D g2) {\r
+                       if( binded_key_char == null ) return false;\r
+                       g2.drawString( binded_key_char, x + width/3, indicator.y - 2 );\r
+                       return true;\r
+               }\r
+               boolean paintIndicator(Graphics2D g2, boolean is_small, int pitch_bend_value) {\r
+                       if( is_small ) {\r
+                               g2.fillOval(\r
+                                               indicator.x + indicator.width/4,\r
+                                               indicator.y + indicator.height/4 + 1,\r
+                                               indicator.width/2,\r
+                                               indicator.height/2\r
+                                               );\r
+                       }\r
+                       else {\r
+                               int current_channel = midiChComboboxModel.getSelectedChannel();\r
+                               int sens = pitch_bend_sensitivities[current_channel];\r
+                               if( sens == 0 ) {\r
+                                       sens = 2;\r
+                               }\r
+                               int x_offset = (\r
+                                       7 * whiteKeySize.width * sens * (pitch_bend_value - MIDISpec.PITCH_BEND_NONE)\r
+                               ) / (12 * 8192);\r
+                               int additional_height = indicator.height * modulations[current_channel] / 256 ;\r
+                               int y_offset = additional_height / 2 ;\r
+                               g2.fillOval(\r
+                                       indicator.x + ( x_offset < 0 ? x_offset : 0 ),\r
+                                       indicator.y - y_offset,\r
+                                       indicator.width + ( x_offset < 0 ? -x_offset : x_offset ),\r
+                                       indicator.height + additional_height\r
+                               );\r
+                       }\r
+                       return true;\r
+               }\r
+               boolean paintIndicator(Graphics2D g2, boolean is_small) {\r
+                       return paintIndicator( g2, is_small, 0 );\r
+               }\r
+       }\r
+       //\r
+       // Constructors\r
+       //\r
+       public PianoKeyboard() {\r
+               setLayout(new BorderLayout());\r
+               setFocusable(true);\r
+               addFocusListener( new FocusListener() {\r
+                       public void focusGained(FocusEvent e) { repaint(); }\r
+                       public void focusLost(FocusEvent e)   { repaint(); }\r
+               });\r
+               addMouseListener( new MouseAdapter() {\r
+                       public void mousePressed(MouseEvent e) {\r
+                               int n = getNote(e.getPoint()); if( n < 0 ) return;\r
+                               int current_channel = midiChComboboxModel.getSelectedChannel();\r
+                               if( channel_notes[current_channel].contains(n) ) return;\r
+                               chord = null;\r
+                               keyOn( current_channel, n );\r
+                               noteOn(n);\r
+                               firePianoKeyPressed( n, e );\r
+                               requestFocusInWindow();\r
+                               repaint();\r
+                       }\r
+                       public void mouseReleased(MouseEvent e) {\r
+                               int current_channel = midiChComboboxModel.getSelectedChannel();\r
+                               if( channel_notes[current_channel].isEmpty() ) return;\r
+                               int n = channel_notes[current_channel].poll();\r
+                               keyOff( current_channel, n );\r
+                               noteOff(n);\r
+                               firePianoKeyReleased( n, e );\r
+                       }\r
+               });\r
+               addMouseMotionListener( new MouseMotionAdapter() {\r
+                       public void mouseDragged(MouseEvent e) {\r
+                               int n = getNote(e.getPoint()); if( n < 0 ) return;\r
+                               int current_channel = midiChComboboxModel.getSelectedChannel();\r
+                               if( channel_notes[current_channel].contains(n) ) return;\r
+                               if( channel_notes[current_channel].size() > 0 ) {\r
+                                       int old_n = channel_notes[current_channel].poll();\r
+                                       keyOff( current_channel, old_n );\r
+                                       noteOff(old_n);\r
+                                       firePianoKeyReleased( old_n, e );\r
+                               }\r
+                               keyOn( current_channel, n );\r
+                               noteOn(n);\r
+                               firePianoKeyPressed( n, e );\r
+                       }\r
+               });\r
+               addKeyListener( new KeyAdapter() {\r
+                       public void keyPressed(KeyEvent e) {\r
+                               int key_code = e.getKeyCode();\r
+                               if( key_code == KeyEvent.VK_LEFT || key_code == KeyEvent.VK_KP_LEFT ) {\r
+                                       octaveRangeModel.setValue( octaveRangeModel.getValue() - 1 );\r
+                                       return;\r
+                               }\r
+                               else if( key_code == KeyEvent.VK_RIGHT || key_code == KeyEvent.VK_KP_RIGHT ) {\r
+                                       octaveRangeModel.setValue( octaveRangeModel.getValue() + 1 );\r
+                                       return;\r
+                               }\r
+                               int n = getNote(e); if( n < 0 ) return;\r
+                               int current_channel = midiChComboboxModel.getSelectedChannel();\r
+                               if( channel_notes[current_channel].contains(n) ) return;\r
+                               chord = null;\r
+                               keyOn( current_channel, n );\r
+                               noteOn(n);\r
+                               firePianoKeyPressed( n, e );\r
+                       }\r
+                       public void keyReleased(KeyEvent e) {\r
+                               int current_channel = midiChComboboxModel.getSelectedChannel();\r
+                               int n = getNote(e);\r
+                               if( n < 0 || ! channel_notes[current_channel].contains(n) ) return;\r
+                               keyOff( current_channel, n );\r
+                               noteOff(n);\r
+                               firePianoKeyReleased( n, e );\r
+                       }\r
+               });\r
+               int octaves = getPerferredOctaves();\r
+               octaveSizeModel = new DefaultBoundedRangeModel(\r
+                               octaves, 0, MIN_OCTAVES, MAX_OCTAVES\r
+                               );\r
+               octaveSizeModel.addChangeListener( new ChangeListener() {\r
+                       public void stateChanged(ChangeEvent e) {\r
+                               fireOctaveResized(e);\r
+                               octaveSizeChanged();\r
+                       }\r
+               });\r
+               octaveRangeModel = new DefaultBoundedRangeModel(\r
+                               (MAX_OCTAVES - octaves) / 2, octaves, 0, MAX_OCTAVES\r
+                               );\r
+               octaveRangeModel.addChangeListener( new ChangeListener() {\r
+                       public void stateChanged(ChangeEvent e) {\r
+                               fireOctaveMoved(e);\r
+                               checkOutOfBounds();\r
+                               repaint();\r
+                       }\r
+               });\r
+               addComponentListener( new ComponentAdapter() {\r
+                       public void componentResized(ComponentEvent e) {\r
+                               octaveSizeModel.setValue( getPerferredOctaves() );\r
+                               octaveSizeChanged();\r
+                       }\r
+               });\r
+               midiChComboboxModel.addListDataListener(\r
+                       new ListDataListener() {\r
+                               public void contentsChanged(ListDataEvent e) {\r
+                                       int current_channel = midiChComboboxModel.getSelectedChannel();\r
+                                       for( int n : channel_notes[current_channel] )\r
+                                               if( autoScroll(n) ) break;\r
+                                       repaint();\r
+                               }\r
+                               public void intervalAdded(ListDataEvent e) {}\r
+                               public void intervalRemoved(ListDataEvent e) {}\r
+                       }\r
+               );\r
+       }\r
+       //\r
+       // Callback\r
+       //\r
+       public void paint(Graphics g) {\r
+               //\r
+               if( keys == null ) return;\r
+               Graphics2D g2 = (Graphics2D) g;\r
+               Dimension d = getSize();\r
+               g2.setBackground( getBackground() );\r
+               g2.clearRect( 0, 0, d.width, d.height );\r
+               PianoKey key;\r
+\r
+               // White keys\r
+               g2.setColor( isDark ? Color.gray : Color.white );\r
+               for( PianoKey k : whiteKeys ) k.paintKey(g2);\r
+\r
+               // To avoid ConcurrentModificationException when sequencer running,\r
+               // copy the note-on list\r
+               NoteList notes = (NoteList)channel_notes[\r
+                                                        midiChComboboxModel.getSelectedChannel()\r
+                                                        ].clone();\r
+               NoteList selected_notes = (NoteList)selectedKeyNoteList.clone();\r
+\r
+               // Note-on white keys\r
+               for( int n : notes )\r
+                       if( (key=getPianoKey(n)) != null && !(key.is_black) )\r
+                               key.paintKey(g2,true);\r
+\r
+               // Black keys\r
+               g2.setColor(getForeground());\r
+               for( PianoKey k : blackKeys ) k.paintKey(g2);\r
+\r
+               // Note-on black keys\r
+               g2.setColor( Color.gray );\r
+               for( int n : notes )\r
+                       if( (key=getPianoKey(n)) != null && key.is_black )\r
+                               key.paintKey(g2,true);\r
+\r
+               // Selected pianokey indicators\r
+               for( int n : selected_notes ) {\r
+                       if( (key=getPianoKey(n)) == null ) continue;\r
+                       boolean is_on_scale = (\r
+                                       key_signature == null || key_signature.isOnScale(n)\r
+                                       );\r
+                       int i_chord;\r
+                       if( chord != null && (i_chord = chord.indexOf(n)) >=0 ) {\r
+                               g2.setColor(Music.Chord.NOTE_INDEX_COLORS[i_chord]);\r
+                       }\r
+                       else {\r
+                               g2.setColor(\r
+                                               isDark && is_on_scale ? Color.pink : DARK_PINK\r
+                                               );\r
+                       }\r
+                       key.paintIndicator( g2, false,\r
+                                       pitch_bend_values[midiChComboboxModel.getSelectedChannel()]\r
+                                       );\r
+                       if( ! is_on_scale ) {\r
+                               g2.setColor(Color.white);\r
+                               key.paintIndicator( g2, true );\r
+                       }\r
+               }\r
+               // Note-on key indicators\r
+               for( int n : notes ) {\r
+                       if( (key=getPianoKey(n)) == null ) continue;\r
+                       boolean is_on_scale = (\r
+                                       key_signature == null || key_signature.isOnScale(n)\r
+                                       );\r
+                       int i_chord;\r
+                       if( chord != null && (i_chord = chord.indexOf(n)) >=0 ) {\r
+                               g2.setColor(Music.Chord.NOTE_INDEX_COLORS[i_chord]);\r
+                       }\r
+                       else {\r
+                               g2.setColor(\r
+                                               isDark && is_on_scale ? Color.pink : DARK_PINK\r
+                                               );\r
+                       }\r
+                       key.paintIndicator( g2, false,\r
+                                       pitch_bend_values[midiChComboboxModel.getSelectedChannel()]\r
+                                       );\r
+                       if( ! is_on_scale ) {\r
+                               g2.setColor( Color.white );\r
+                               key.paintIndicator( g2, true );\r
+                       }\r
+               }\r
+               // Focus\r
+               if( isFocusOwner() ) {\r
+                       // Show PC-key binding\r
+                       for( PianoKey k : bindedKeys ) {\r
+                               g2.setColor(\r
+                                               k.is_black ? Color.gray.brighter() :\r
+                                                       isDark ? getForeground() :\r
+                                                               getForeground().brighter()\r
+                                               );\r
+                               k.paintKeyBinding(g2);\r
+                       }\r
+               }\r
+       }\r
+       //\r
+       protected void firePianoKeyPressed(int note_no, InputEvent event) {\r
+               Object[] listeners = listenerList.getListenerList();\r
+               for (int i = listeners.length-2; i>=0; i-=2) {\r
+                       if (listeners[i]==PianoKeyboardListener.class) {\r
+                               ((PianoKeyboardListener)listeners[i+1]).pianoKeyPressed(note_no,event);\r
+                       }\r
+               }\r
+       }\r
+       protected void firePianoKeyReleased(int note_no, InputEvent event) {\r
+               Object[] listeners = listenerList.getListenerList();\r
+               for (int i = listeners.length-2; i>=0; i-=2) {\r
+                       if (listeners[i]==PianoKeyboardListener.class) {\r
+                               ((PianoKeyboardListener)listeners[i+1]).pianoKeyReleased(note_no,event);\r
+                       }\r
+               }\r
+       }\r
+       protected void fireOctaveMoved(ChangeEvent event) {\r
+               Object[] listeners = listenerList.getListenerList();\r
+               for (int i = listeners.length-2; i>=0; i-=2) {\r
+                       if (listeners[i]==PianoKeyboardListener.class) {\r
+                               ((PianoKeyboardListener)listeners[i+1]).octaveMoved(event);\r
+                       }\r
+               }\r
+       }\r
+       protected void fireOctaveResized(ChangeEvent event) {\r
+               Object[] listeners = listenerList.getListenerList();\r
+               for (int i = listeners.length-2; i>=0; i-=2) {\r
+                       if (listeners[i]==PianoKeyboardListener.class) {\r
+                               ((PianoKeyboardListener)listeners[i+1]).octaveResized(event);\r
+                       }\r
+               }\r
+       }\r
+       public PianoKey getPianoKey(int note_no) {\r
+               int i = note_no - octaveRangeModel.getValue() * 12 ;\r
+               return i>=0 && i<keys.length ? keys[i]: null;\r
+       }\r
+       private int getNote(Point point) {\r
+               PianoKey k = getPianoKey(point);\r
+               return k==null ? -1 : k.getNote(getChromaticOffset());\r
+       }\r
+       private PianoKey getPianoKey(Point point) {\r
+               int i_white_key = point.x / whiteKeySize.width;\r
+               int i_octave = i_white_key / 7;\r
+               int i = (i_white_key -= i_octave * 7) * 2 + i_octave * 12;\r
+               if( i_white_key >= 3 ) i--;\r
+\r
+               if( i < 0 || i > keys.length-1 ) return null;\r
+\r
+               if( point.y > blackKeySize.height )\r
+                       return keys[i];\r
+\r
+               PianoKey k;\r
+               if( i > 0 ) {\r
+                       k = keys[i-1];\r
+                       if( k.is_black && !(k.out_of_bounds) && k.contains(point) ) return k;\r
+               }\r
+               if( i < keys.length-1 ) {\r
+                       k = keys[i+1];\r
+                       if( k.is_black && !(k.out_of_bounds) && k.contains(point) ) return k;\r
+               }\r
+               return keys[i];\r
+       }\r
+\r
+       PianoKey[]              bindedKeys;\r
+       private int             bindedKeyPosition;\r
+       private String  bindedKeyChars;\r
+       private PianoKey getPianoKey(KeyEvent e) {\r
+               int i = bindedKeyChars.indexOf(e.getKeyChar());\r
+               return i >= 0 ? keys[bindedKeyPosition + i] : null;\r
+       }\r
+       private int getNote(KeyEvent e) {\r
+               PianoKey k = getPianoKey(e);\r
+               return k==null ? -1 : k.getNote(getChromaticOffset());\r
+       }\r
+       void changeKeyBinding( int from, String key_chars ) {\r
+               PianoKey k;\r
+               bindedKeys = new PianoKey[(bindedKeyChars = key_chars).length()];\r
+               bindedKeyPosition = from;\r
+               for( int i = 0; i < bindedKeyChars.length(); i++ ) {\r
+                       bindedKeys[i] = k = keys[ bindedKeyPosition + i ];\r
+                       k.binded_key_char = bindedKeyChars.substring( i, i+1 );\r
+               }\r
+               repaint();\r
+       }\r
+\r
+       private void checkOutOfBounds() {\r
+               if( keys == null ) return;\r
+               for( PianoKey k : keys ) k.getNote(getChromaticOffset());\r
+       }\r
+       void keyOff(int ch, int note_no) {\r
+               if( note_no < 0 || ch < 0 || ch >= channel_notes.length ) return;\r
+               channel_notes[ch].remove((Object)note_no);\r
+               if( ch == midiChComboboxModel.getSelectedChannel() )\r
+                       repaint();\r
+       }\r
+       void keyOn(int ch, int note_no) {\r
+               if( note_no < 0 || ch < 0 || ch >= channel_notes.length ) return;\r
+               channel_notes[ch].add(note_no);\r
+               setSelectedNote(ch,note_no);\r
+       }\r
+       boolean autoScroll(int note_no) {\r
+               if( octaveRangeModel == null || keys == null )\r
+                       return false;\r
+               int i = note_no - getChromaticOffset();\r
+               if( i < 0 ) {\r
+                       octaveRangeModel.setValue(\r
+                                       octaveRangeModel.getValue() - ( (-i) / 12 ) - 1\r
+                                       );\r
+                       return true;\r
+               }\r
+               if( i >= keys.length ) {\r
+                       octaveRangeModel.setValue(\r
+                                       octaveRangeModel.getValue() + ( (i - keys.length) / 12 ) + 1\r
+                                       );\r
+                       return true;\r
+               }\r
+               return false;\r
+       }\r
+       void addPianoKeyboardListener(PianoKeyboardListener l) {\r
+               listenerList.add(PianoKeyboardListener.class, l);\r
+       }\r
+       void removePianoKeyboardListener(PianoKeyboardListener l) {\r
+               listenerList.remove(PianoKeyboardListener.class, l);\r
+       }\r
+       int countKeyOn() {\r
+               return channel_notes[\r
+                                    midiChComboboxModel.getSelectedChannel()\r
+                                    ].size();\r
+       }\r
+       int countKeyOn(int ch) {\r
+               return channel_notes[ch].size();\r
+       }\r
+       void allKeysOff(int ch, int n_marks) {\r
+               if( ! selectedKeyNoteList.isEmpty() ) return;\r
+               switch(n_marks) {\r
+               case -1:\r
+                       selectedKeyNoteList = (NoteList)(channel_notes[ch].clone());\r
+                       break;\r
+               case  1:\r
+                       selectedKeyNoteList.add(\r
+                                       channel_notes[ch].get(channel_notes[ch].size()-1)\r
+                                       );\r
+                       break;\r
+               default: break;\r
+               }\r
+               channel_notes[ch].clear();\r
+               if( midiChComboboxModel.getSelectedChannel() == ch )\r
+                       repaint();\r
+       }\r
+       void clear() {\r
+               selectedKeyNoteList.clear();\r
+               channel_notes[\r
+                             midiChComboboxModel.getSelectedChannel()\r
+                             ].clear();\r
+               chord = null;\r
+               repaint();\r
+       }\r
+       int getNote() {\r
+               int current_channel = midiChComboboxModel.getSelectedChannel();\r
+               switch( channel_notes[current_channel].size() ) {\r
+               case 1: return channel_notes[current_channel].get(0);\r
+               case 0:\r
+                       if( selectedKeyNoteList.size() == 1 )\r
+                               return selectedKeyNoteList.get(0);\r
+                       return -1;\r
+               default:\r
+                       return -1;\r
+               }\r
+       }\r
+       void setSelectedNote(int note_no) {\r
+               setSelectedNote(\r
+                               midiChComboboxModel.getSelectedChannel(), note_no\r
+                               );\r
+       }\r
+       void setSelectedNote(int ch, int note_no) {\r
+               if( ch != midiChComboboxModel.getSelectedChannel() )\r
+                       return;\r
+               selectedKeyNoteList.add(note_no);\r
+               int max_sel = (chord == null ? max_selectable : chord.numberOfNotes());\r
+               while( selectedKeyNoteList.size() > max_sel )\r
+                       selectedKeyNoteList.poll();\r
+               if( !autoScroll(note_no) ) {\r
+                       // When autoScroll() returned false, stateChanged() not invoked - need repaint()\r
+                       repaint();\r
+               }\r
+       }\r
+       Integer[] getSelectedNotes() {\r
+               return\r
+                               selectedKeyNoteList.toArray(new Integer[0]);\r
+       }\r
+       //\r
+       Music.Chord getChord() { return chord; }\r
+       void setChord(Music.Chord c) {\r
+               chordDisplay.setChord(chord = c);\r
+       }\r
+       void setKeySignature(Music.Key ks) {\r
+               key_signature = ks;\r
+               repaint();\r
+       }\r
+       //\r
+       void setMaxSelectable( int max_selectable ) {\r
+               this.max_selectable = max_selectable;\r
+       }\r
+       int getMaxSelectable() { return max_selectable; }\r
+       //\r
+       int getChromaticOffset() {\r
+               return octaveRangeModel.getValue() * 12 ;\r
+       }\r
+       int getOctaves() {\r
+               return octaveSizeModel.getValue();\r
+       }\r
+       private int getPerferredOctaves() {\r
+               int octaves = Math.round( (float)getWidth() / widthPerOctave );\r
+               if( octaves > MAX_OCTAVES ) {\r
+                       octaves = MAX_OCTAVES;\r
+               }\r
+               else if( octaves < MIN_OCTAVES ) {\r
+                       octaves = MIN_OCTAVES;\r
+               }\r
+               return octaves;\r
+       }\r
+       private void octaveSizeChanged() {\r
+               int octaves = octaveSizeModel.getValue();\r
+               String default_binded_key_chars = "zsxdcvgbhnjm,l.;/\\]";\r
+               Dimension keyboard_size = getSize();\r
+               if( keyboard_size.width == 0 ) {\r
+                       return;\r
+               }\r
+               whiteKeySize = new Dimension(\r
+                               (keyboard_size.width - 1) / (octaves * 7 + 1),\r
+                               keyboard_size.height - 1\r
+                               );\r
+               blackKeySize = new Dimension(\r
+                               whiteKeySize.width * 3 / 4,\r
+                               whiteKeySize.height * 3 / 5\r
+                               );\r
+               Dimension indicator_size = new Dimension(\r
+                               whiteKeySize.width / 2,\r
+                               whiteKeySize.height / 6\r
+                               );\r
+               octaveRangeModel.setExtent( octaves );\r
+               octaveRangeModel.setValue( (MAX_OCTAVES - octaves) / 2 );\r
+               widthPerOctave = keyboard_size.width / octaves;\r
+               //\r
+               // Construct piano-keys\r
+               //\r
+               keys = new PianoKey[ octaves * 12 + 1 ];\r
+               Vector<PianoKey> v_black_keys = new Vector<PianoKey>();\r
+               Vector<PianoKey> v_white_keys = new Vector<PianoKey>();\r
+               Point key_point = new Point(1,1);\r
+               PianoKey k;\r
+               int i, i12;\r
+               boolean is_CDE = true;\r
+               for( i = i12 = 0; i < keys.length; i++, i12++ ) {\r
+                       switch(i12) {\r
+                       case 12: is_CDE = true; i12 = 0; break;\r
+                       case  5: is_CDE = false; break;\r
+                       default: break;\r
+                       }\r
+                       key_point.x = whiteKeySize.width * (\r
+                                       i/12*7 + (i12+(is_CDE?1:2))/2\r
+                                       );\r
+                       if( Music.isOnScale(i12,0) ) {\r
+                               k = new PianoKey( key_point, whiteKeySize, indicator_size );\r
+                               k.is_black = false;\r
+                               v_white_keys.add(k);\r
+                       }\r
+                       else {\r
+                               key_point.x -=\r
+                                               ( (is_CDE?5:12) - i12 )/2 * blackKeySize.width / (is_CDE?3:4);\r
+                               k = new PianoKey( key_point, blackKeySize, indicator_size );\r
+                               k.is_black = true;\r
+                               v_black_keys.add(k);\r
+                       }\r
+                       (keys[i] = k).position = i;\r
+               }\r
+               whiteKeys = v_white_keys.toArray(new PianoKey[1]);\r
+               blackKeys = v_black_keys.toArray(new PianoKey[1]);\r
+               changeKeyBinding(\r
+                               ((octaves - 1) / 2) * 12,\r
+                               default_binded_key_chars\r
+                               );\r
+               checkOutOfBounds();\r
+       }\r
+       //\r
+       void setDarkMode(boolean is_dark) {\r
+               this.isDark = is_dark;\r
+               setBackground( is_dark ? Color.black : null );\r
+       }\r
+}\r
+\r
+class PianoKeyboardPanel extends JPanel\r
+{\r
+       PianoKeyboard   keyboard = new PianoKeyboard();\r
+       JSlider octave_size_slider = new JSlider();\r
+       JScrollBar      octave_selecter = new JScrollBar( JScrollBar.HORIZONTAL );\r
+       JPanel  octave_bar = new JPanel();\r
+       public PianoKeyboardPanel() {\r
+               octave_size_slider.setToolTipText("Octave size");\r
+               octave_selecter.setToolTipText("Octave position");\r
+               keyboard.addPianoKeyboardListener(\r
+                               new PianoKeyboardAdapter() {\r
+                                       public void octaveResized(ChangeEvent e) {\r
+                                               octave_selecter.setBlockIncrement( keyboard.getOctaves() );\r
+                                       }\r
+                               }\r
+                               );\r
+               octave_selecter.setModel( keyboard.octaveRangeModel );\r
+               octave_selecter.setBlockIncrement( keyboard.getOctaves() );\r
+               octave_size_slider.setModel( keyboard.octaveSizeModel );\r
+               octave_size_slider.setMinimumSize( new Dimension( 100, 18 ) );\r
+               octave_size_slider.setMaximumSize( new Dimension( 100, 18 ) );\r
+               octave_size_slider.setPreferredSize( new Dimension( 100, 18 ) );\r
+               octave_bar.setLayout( new BoxLayout( octave_bar, BoxLayout.X_AXIS ) );\r
+               octave_bar.add(octave_selecter);\r
+               octave_bar.add(Box.createHorizontalStrut(5));\r
+               octave_bar.add(octave_size_slider);\r
+               setLayout( new BoxLayout( this, BoxLayout.Y_AXIS ) );\r
+               add( octave_bar );\r
+               add( keyboard );\r
+               setAlignmentX((float)0.5);\r
+       }\r
+       // Methods\r
+       //\r
+       public void setDarkMode(boolean is_dark) {\r
+               Color col = is_dark ? Color.black : null;\r
+               octave_selecter.setBackground( col );\r
+               octave_size_slider.setBackground( col );\r
+               octave_bar.setBackground( col );\r
+               keyboard.setDarkMode( is_dark );\r
+       }\r
+}\r
+\r
+class MidiKeyboardPanel extends JPanel {\r
+       MidiEventDialog eventDialog;\r
+       Action query_send_event_action = new AbstractAction() {\r
+               { putValue(NAME,"Send MIDI event"); }\r
+               public void actionPerformed(ActionEvent e) {\r
+                       eventDialog.setTitle("Send MIDI event");\r
+                       eventDialog.ok_button.setAction(send_event_action);\r
+                       eventDialog.midi_message_form.channelText.setSelectedChannel(\r
+                                       keyboardCenterPanel.keyboard.midiChComboboxModel.getSelectedChannel()\r
+                                       );\r
+                       eventDialog.openMessageForm();\r
+               }\r
+       };\r
+       Action send_event_action = new AbstractAction() {\r
+               { putValue(NAME,"Send"); }\r
+               public void actionPerformed(ActionEvent e) {\r
+                       keyboardCenterPanel.keyboard.midiDevice.sendMidiMessage(\r
+                                       eventDialog.midi_message_form.getMessage()\r
+                                       );\r
+               }\r
+       };\r
+       Insets  zero_insets = new Insets(0,0,0,0);\r
+       KeySignatureSelecter    keySelecter = new KeySignatureSelecter(false);\r
+       JButton send_event_button = new JButton(query_send_event_action);\r
+\r
+       JPanel keyboard_chord_panel, keyboard_south_panel;\r
+\r
+       PianoKeyboardPanel\r
+       keyboardCenterPanel = new PianoKeyboardPanel();\r
+\r
+       MidiChannelComboSelecter midi_ch_combobox =\r
+                       new MidiChannelComboSelecter(\r
+                                       "MIDI Channel",\r
+                                       keyboardCenterPanel.keyboard.midiChComboboxModel\r
+                                       );\r
+       MidiChannelButtonSelecter midi_ch_buttons =\r
+                       new MidiChannelButtonSelecter(keyboardCenterPanel.keyboard);\r
+       VelocitySelecter velocity_selecter =\r
+                       new VelocitySelecter(keyboardCenterPanel.keyboard.velocityModel);\r
+\r
+       public MidiKeyboardPanel( ChordMatrix chord_matrix ) {\r
+               keyboardCenterPanel.keyboard.chord_matrix = chord_matrix;\r
+               keyboardCenterPanel.keyboard.chordDisplay =\r
+                               new ChordDisplay(\r
+                                               "MIDI Keyboard", chord_matrix,\r
+                                               keyboardCenterPanel.keyboard\r
+                                               );\r
+               keyboard_chord_panel = new JPanel();\r
+               keyboard_chord_panel.setLayout(\r
+                               new BoxLayout( keyboard_chord_panel, BoxLayout.X_AXIS )\r
+                               );\r
+               keyboard_chord_panel.add( Box.createHorizontalStrut(5) );\r
+               keyboard_chord_panel.add( velocity_selecter );\r
+               keyboard_chord_panel.add( keySelecter );\r
+               keyboard_chord_panel.add( keyboardCenterPanel.keyboard.chordDisplay );\r
+               keyboard_chord_panel.add( Box.createHorizontalStrut(5) );\r
+               //\r
+               send_event_button.setMargin(zero_insets);\r
+               //\r
+               keyboard_south_panel = new JPanel();\r
+               keyboard_south_panel.setLayout(\r
+                               new BoxLayout( keyboard_south_panel, BoxLayout.X_AXIS )\r
+                               );\r
+               keyboard_south_panel.add( midi_ch_combobox );\r
+               keyboard_south_panel.add( midi_ch_buttons );\r
+               keyboard_south_panel.add( send_event_button );\r
+               //\r
+               setLayout( new BoxLayout( this, BoxLayout.Y_AXIS ) );\r
+               add( keyboard_chord_panel );\r
+               add( keyboardCenterPanel );\r
+               add( Box.createVerticalStrut(5) );\r
+               add( keyboard_south_panel );\r
+       }\r
+\r
+       // Methods\r
+       //\r
+       public void setDarkMode(boolean is_dark) {\r
+               Color col = is_dark ? Color.black : null;\r
+               setBackground( col );\r
+               keyboardCenterPanel.setDarkMode( is_dark );\r
+               keyboard_chord_panel.setBackground( col );\r
+               keyboard_south_panel.setBackground( col );\r
+               midi_ch_buttons.setBackground( col );\r
+               midi_ch_combobox.setBackground( col );\r
+               midi_ch_combobox.comboBox.setBackground( col );\r
+               keySelecter.setBackground( col );\r
+               keySelecter.keysigCombobox.setBackground( col );\r
+               velocity_selecter.setBackground( col );\r
+               keyboardCenterPanel.keyboard.chordDisplay.setDarkMode( is_dark );\r
+               send_event_button.setBackground( col );\r
+       }\r
+\r
+}\r
diff --git a/src/images/midichordhelper.ico b/src/images/midichordhelper.ico
new file mode 100644 (file)
index 0000000..9b5aa13
Binary files /dev/null and b/src/images/midichordhelper.ico differ
diff --git a/src/images/midichordhelper.png b/src/images/midichordhelper.png
new file mode 100644 (file)
index 0000000..1f92b25
Binary files /dev/null and b/src/images/midichordhelper.png differ