OSDN Git Service

ローカルリポジトリ作成→初コミット
authorAkiyoshi Kamide <kamide@yk.rim.or.jp>
Sun, 4 Jan 2015 17:49:12 +0000 (02:49 +0900)
committerAkiyoshi Kamide <kamide@yk.rim.or.jp>
Sun, 4 Jan 2015 17:49:12 +0000 (02:49 +0900)
81 files changed:
.classpath [new file with mode: 0644]
.gitignore [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/camidion/chordhelper/ButtonIcon.java [new file with mode: 0644]
src/camidion/chordhelper/ChordDisplayLabel.java [new file with mode: 0644]
src/camidion/chordhelper/ChordHelperApplet.java [new file with mode: 0644]
src/camidion/chordhelper/ChordTextField.java [new file with mode: 0644]
src/camidion/chordhelper/InversionAndOmissionLabel.java [new file with mode: 0644]
src/camidion/chordhelper/MidiChordHelper.java [new file with mode: 0644]
src/camidion/chordhelper/anogakki/AnoGakkiPane.java [new file with mode: 0644]
src/camidion/chordhelper/chorddiagram/CapoSelecterView.java [new file with mode: 0644]
src/camidion/chordhelper/chorddiagram/ChordDiagram.java [new file with mode: 0644]
src/camidion/chordhelper/chorddiagram/ChordDiagramDisplay.java [new file with mode: 0644]
src/camidion/chordhelper/chordmatrix/ChordButtonLabel.java [new file with mode: 0644]
src/camidion/chordhelper/chordmatrix/ChordGuide.java [new file with mode: 0644]
src/camidion/chordhelper/chordmatrix/ChordMatrix.java [new file with mode: 0644]
src/camidion/chordhelper/chordmatrix/ChordMatrixListener.java [new file with mode: 0644]
src/camidion/chordhelper/midichordhelper.ico [new file with mode: 0644]
src/camidion/chordhelper/midichordhelper.png [new file with mode: 0644]
src/camidion/chordhelper/mididevice/AbstractMidiChannelStatus.java [new file with mode: 0644]
src/camidion/chordhelper/mididevice/AbstractMidiStatus.java [new file with mode: 0644]
src/camidion/chordhelper/mididevice/AbstractVirtualMidiDevice.java [new file with mode: 0644]
src/camidion/chordhelper/mididevice/MidiCablePane.java [new file with mode: 0644]
src/camidion/chordhelper/mididevice/MidiConnecterListModel.java [new file with mode: 0644]
src/camidion/chordhelper/mididevice/MidiConnecterListView.java [new file with mode: 0644]
src/camidion/chordhelper/mididevice/MidiDesktopPane.java [new file with mode: 0644]
src/camidion/chordhelper/mididevice/MidiDeviceDialog.java [new file with mode: 0644]
src/camidion/chordhelper/mididevice/MidiDeviceFrame.java [new file with mode: 0644]
src/camidion/chordhelper/mididevice/MidiDeviceInOutType.java [new file with mode: 0644]
src/camidion/chordhelper/mididevice/MidiDeviceModelList.java [new file with mode: 0644]
src/camidion/chordhelper/mididevice/MidiDeviceTree.java [new file with mode: 0644]
src/camidion/chordhelper/mididevice/MidiDeviceTreeModel.java [new file with mode: 0644]
src/camidion/chordhelper/mididevice/MidiSequencerModel.java [new file with mode: 0644]
src/camidion/chordhelper/mididevice/SequencerMeasureView.java [new file with mode: 0644]
src/camidion/chordhelper/mididevice/SequencerTimeView.java [new file with mode: 0644]
src/camidion/chordhelper/mididevice/VirtualMidiDevice.java [new file with mode: 0644]
src/camidion/chordhelper/midieditor/Base64Dialog.java [new file with mode: 0644]
src/camidion/chordhelper/midieditor/DefaultMidiChannelComboBoxModel.java [new file with mode: 0644]
src/camidion/chordhelper/midieditor/DurationForm.java [new file with mode: 0644]
src/camidion/chordhelper/midieditor/HexSelecter.java [new file with mode: 0644]
src/camidion/chordhelper/midieditor/HexTextForm.java [new file with mode: 0644]
src/camidion/chordhelper/midieditor/KeySignatureLabel.java [new file with mode: 0644]
src/camidion/chordhelper/midieditor/KeySignatureSelecter.java [new file with mode: 0644]
src/camidion/chordhelper/midieditor/MidiChannelButtonSelecter.java [new file with mode: 0644]
src/camidion/chordhelper/midieditor/MidiChannelComboBoxModel.java [new file with mode: 0644]
src/camidion/chordhelper/midieditor/MidiChannelComboSelecter.java [new file with mode: 0644]
src/camidion/chordhelper/midieditor/MidiEventDialog.java [new file with mode: 0644]
src/camidion/chordhelper/midieditor/MidiMessageForm.java [new file with mode: 0644]
src/camidion/chordhelper/midieditor/MidiProgramFamilySelecter.java [new file with mode: 0644]
src/camidion/chordhelper/midieditor/MidiProgramSelecter.java [new file with mode: 0644]
src/camidion/chordhelper/midieditor/MidiSequenceEditor.java [new file with mode: 0644]
src/camidion/chordhelper/midieditor/NewSequenceDialog.java [new file with mode: 0644]
src/camidion/chordhelper/midieditor/PlaylistTableModel.java [new file with mode: 0644]
src/camidion/chordhelper/midieditor/SequenceTickIndex.java [new file with mode: 0644]
src/camidion/chordhelper/midieditor/SequenceTrackListTableModel.java [new file with mode: 0644]
src/camidion/chordhelper/midieditor/SequencerSpeedSlider.java [new file with mode: 0644]
src/camidion/chordhelper/midieditor/TempoSelecter.java [new file with mode: 0644]
src/camidion/chordhelper/midieditor/TickPositionModel.java [new file with mode: 0644]
src/camidion/chordhelper/midieditor/TimeSignatureSelecter.java [new file with mode: 0644]
src/camidion/chordhelper/midieditor/TrackEventListTableModel.java [new file with mode: 0644]
src/camidion/chordhelper/midieditor/VelocitySelecter.java [new file with mode: 0644]
src/camidion/chordhelper/music/AbstractNoteTrackSpec.java [new file with mode: 0644]
src/camidion/chordhelper/music/AbstractTrackSpec.java [new file with mode: 0644]
src/camidion/chordhelper/music/Chord.java [new file with mode: 0644]
src/camidion/chordhelper/music/ChordProgression.java [new file with mode: 0644]
src/camidion/chordhelper/music/DrumTrackSpec.java [new file with mode: 0644]
src/camidion/chordhelper/music/FirstTrackSpec.java [new file with mode: 0644]
src/camidion/chordhelper/music/Key.java [new file with mode: 0644]
src/camidion/chordhelper/music/MIDISpec.java [new file with mode: 0644]
src/camidion/chordhelper/music/MelodyTrackSpec.java [new file with mode: 0644]
src/camidion/chordhelper/music/Music.java [new file with mode: 0644]
src/camidion/chordhelper/music/NoteSymbol.java [new file with mode: 0644]
src/camidion/chordhelper/music/Range.java [new file with mode: 0644]
src/camidion/chordhelper/music/SymbolLanguage.java [new file with mode: 0644]
src/camidion/chordhelper/pianokeyboard/MidiKeyboardPanel.java [new file with mode: 0644]
src/camidion/chordhelper/pianokeyboard/PianoKeyboard.java [new file with mode: 0644]
src/camidion/chordhelper/pianokeyboard/PianoKeyboardAdapter.java [new file with mode: 0644]
src/camidion/chordhelper/pianokeyboard/PianoKeyboardListener.java [new file with mode: 0644]
src/camidion/chordhelper/pianokeyboard/PianoKeyboardPanel.java [new file with mode: 0644]

diff --git a/.classpath b/.classpath
new file mode 100644 (file)
index 0000000..49ea16d
--- /dev/null
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+       <classpathentry kind="src" path="src"/>
+       <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.7"/>
+       <classpathentry kind="lib" path="lib/commons-codec-1.4.jar"/>
+       <classpathentry kind="output" path="bin"/>
+</classpath>
diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..ae3c172
--- /dev/null
@@ -0,0 +1 @@
+/bin/
diff --git a/.project b/.project
new file mode 100644 (file)
index 0000000..0a8a077
--- /dev/null
+++ b/.project
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+       <name>MIDIChordHelper</name>
+       <comment></comment>
+       <projects>
+       </projects>
+       <buildSpec>
+               <buildCommand>
+                       <name>org.eclipse.jdt.core.javabuilder</name>
+                       <arguments>
+                       </arguments>
+               </buildCommand>
+       </buildSpec>
+       <natures>
+               <nature>org.eclipse.jdt.core.javanature</nature>
+       </natures>
+</projectDescription>
diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs
new file mode 100644 (file)
index 0000000..7341ab1
--- /dev/null
@@ -0,0 +1,11 @@
+eclipse.preferences.version=1
+org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.7
+org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
+org.eclipse.jdt.core.compiler.compliance=1.7
+org.eclipse.jdt.core.compiler.debug.lineNumber=generate
+org.eclipse.jdt.core.compiler.debug.localVariable=generate
+org.eclipse.jdt.core.compiler.debug.sourceFile=generate
+org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
+org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
+org.eclipse.jdt.core.compiler.source=1.7
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/camidion/chordhelper/ButtonIcon.java b/src/camidion/chordhelper/ButtonIcon.java
new file mode 100644 (file)
index 0000000..dfaf069
--- /dev/null
@@ -0,0 +1,405 @@
+package camidion.chordhelper;
+
+import java.awt.Color;
+import java.awt.Component;
+import java.awt.Graphics;
+import java.awt.Graphics2D;
+
+import javax.swing.AbstractButton;
+import javax.swing.Icon;
+
+import camidion.chordhelper.music.Chord;
+
+/**
+ * カスタムペイントアイコン
+ */
+public class ButtonIcon implements Icon {
+       public static final int BLANK_ICON = 0;
+       public static final int REC_ICON = 1;
+       public static final int PLAY_ICON = 2;
+       public static final int STOP_ICON = 3;
+       public static final int EJECT_ICON = 4;
+       public static final int PAUSE_ICON = 5;
+       public static final int ANO_GAKKI_ICON = 6;
+       //
+       public static final int INVERSION_ICON = 8;
+       public static final int DARK_MODE_ICON = 9;
+       public static final int X_ICON = 10;
+       public static final int REPEAT_ICON = 11;
+       public static final int MIDI_CONNECTOR_ICON = 12;
+       public static final int NATURAL_ICON = 13;
+       public static final int EDIT_ICON = 14;
+       public static final int FORWARD_ICON = 15;
+       public static final int BACKWARD_ICON = 16;
+       public static final int TOP_ICON = 17;
+       public static final int BOTTOM_ICON = 18;
+       //
+       public static final int A128TH_NOTE_ICON = 128;
+       public static final int DOTTED_128TH_NOTE_ICON = 129;
+       public static final int A64TH_NOTE_ICON = 130;
+       public static final int DOTTED_64TH_NOTE_ICON = 131;
+       public static final int A32ND_NOTE_ICON = 132;
+       public static final int DOTTED_32ND_NOTE_ICON = 133;
+       public static final int A16TH_NOTE_ICON = 134;
+       public static final int DOTTED_16TH_NOTE_ICON = 135;
+       public static final int A8TH_NOTE_ICON = 136;
+       public static final int DOTTED_8TH_NOTE_ICON = 137;
+       public static final int QUARTER_NOTE_ICON = 138;
+       public static final int DOTTED_QUARTER_NOTE_ICON = 139;
+       public static final int HALF_NOTE_ICON = 140;
+       public static final int DOTTED_HALF_NOTE_ICON = 141;
+       public static final int WHOLE_NOTE_ICON = 142;
+       //
+       private int iconKind;
+       public int getIconKind() { return iconKind; }
+       //
+       public boolean isMusicalNote() {
+               return iconKind >= A128TH_NOTE_ICON && iconKind <= WHOLE_NOTE_ICON ;
+       }
+       public boolean isDottedMusicalNote() {
+               return isMusicalNote() && ((iconKind & 1) != 0) ;
+       }
+       public int getMusicalNoteValueIndex() { // Returns log2(n) of n-th note
+               return isMusicalNote() ? (WHOLE_NOTE_ICON + 1 - iconKind) / 2 : -1 ;
+       }
+       //
+       private int width = 16;
+       private static final int HEIGHT = 16;
+       private static final int MARGIN = 3;
+       //
+       // for notes
+       private static final int NOTE_HEAD_WIDTH = 8;
+       private static final int NOTE_HEAD_HEIGHT = 6;
+       //
+       // for eject button
+       private static final int EJECT_BOTTOM_LINE_WIDTH = 2;
+       //
+       // for play/eject button
+       private int xPoints[];
+       private int yPoints[];
+       //
+       public ButtonIcon(int kind) {
+               iconKind = kind;
+               switch( iconKind ) {
+               case PLAY_ICON:
+                       xPoints = new int[4]; yPoints = new int[4];
+                       xPoints[0] = MARGIN;       yPoints[0] = MARGIN;
+                       xPoints[1] = width-MARGIN; yPoints[1] = HEIGHT/2;
+                       xPoints[2] = MARGIN;       yPoints[2] = HEIGHT-MARGIN;
+                       xPoints[3] = MARGIN;       yPoints[3] = MARGIN;
+                       break;
+               case EJECT_ICON:
+                       xPoints = new int[4]; yPoints = new int[4];
+                       xPoints[0] = width/2;      yPoints[0] = MARGIN;
+                       xPoints[1] = width-MARGIN; yPoints[1] = HEIGHT - MARGIN - 2*EJECT_BOTTOM_LINE_WIDTH;
+                       xPoints[2] = MARGIN;       yPoints[2] = HEIGHT - MARGIN - 2*EJECT_BOTTOM_LINE_WIDTH;
+                       xPoints[3] = width/2;      yPoints[3] = MARGIN;
+                       break;
+               case TOP_ICON:
+               case BACKWARD_ICON:
+                       xPoints = new int[8]; yPoints = new int[8];
+                       xPoints[0] = width-MARGIN; yPoints[0] = MARGIN;
+                       xPoints[1] = width-MARGIN; yPoints[1] = HEIGHT-MARGIN;
+                       xPoints[2] = width/2;      yPoints[2] = HEIGHT/2;
+                       xPoints[3] = width/2;      yPoints[3] = HEIGHT-MARGIN;
+                       xPoints[4] = MARGIN;       yPoints[4] = HEIGHT/2;
+                       xPoints[5] = width/2;      yPoints[5] = MARGIN;
+                       xPoints[6] = width/2;      yPoints[6] = HEIGHT/2;
+                       xPoints[7] = width-MARGIN; yPoints[7] = MARGIN;
+                       break;
+               case BOTTOM_ICON:
+               case FORWARD_ICON:
+                       xPoints = new int[8]; yPoints = new int[8];
+                       xPoints[0] = MARGIN;       yPoints[0] = MARGIN;
+                       xPoints[1] = MARGIN;       yPoints[1] = HEIGHT-MARGIN;
+                       xPoints[2] = width/2;      yPoints[2] = HEIGHT/2;
+                       xPoints[3] = width/2;      yPoints[3] = HEIGHT-MARGIN;
+                       xPoints[4] = width-MARGIN;       yPoints[4] = HEIGHT/2;
+                       xPoints[5] = width/2;      yPoints[5] = MARGIN;
+                       xPoints[6] = width/2;      yPoints[6] = HEIGHT/2;
+                       xPoints[7] = MARGIN;       yPoints[7] = MARGIN;
+                       break;
+               case INVERSION_ICON:
+               case ANO_GAKKI_ICON:
+                       width = 32;
+                       break;
+               case REPEAT_ICON:
+                       xPoints = new int[4]; yPoints = new int[4];
+                       xPoints[0] = width/2 - 2;  yPoints[0] = MARGIN;
+                       xPoints[1] = width/2 + 2;  yPoints[1] = MARGIN - 4;
+                       xPoints[2] = width/2 + 2;  yPoints[2] = MARGIN + 5;
+                       xPoints[3] = width/2 - 2;  yPoints[3] = MARGIN + 1;
+                       break;
+               }
+       }
+       @Override
+       public int getIconWidth() { return width; }
+       @Override
+       public int getIconHeight() { return HEIGHT; }
+       @Override
+       public void paintIcon(Component c, Graphics g, int x, int y) {
+               Graphics2D g2 = (Graphics2D) g;
+               boolean is_selected = (
+                       (
+                               (c instanceof AbstractButton) && ((AbstractButton)c).isSelected()
+                               )||(
+                               (c instanceof InversionAndOmissionLabel) && (
+                                       ((InversionAndOmissionLabel)c).isAutoInversionMode()
+                               )
+                       )
+               );
+               int omitting_note = c instanceof InversionAndOmissionLabel ?
+                       ((InversionAndOmissionLabel)c).getOmissionNoteIndex() : -1;
+               g2.setColor( c.isEnabled() ? c.getForeground() : c.getBackground().darker() );
+               g2.translate(x, y);
+               switch(iconKind) {
+               case REC_ICON:
+                       if( c.isEnabled() ) g.setColor(Color.red);
+                       g2.fillOval( MARGIN, MARGIN, width - 2*MARGIN, HEIGHT - 2*MARGIN );
+                       break;
+               case TOP_ICON:
+                       g2.fillRect( MARGIN-1, MARGIN, 2, HEIGHT - 2*MARGIN );
+                       // No break;
+               case BACKWARD_ICON:
+               case FORWARD_ICON:
+               case PLAY_ICON:
+                       g2.fillPolygon( xPoints, yPoints, xPoints.length );
+                       break;
+               case BOTTOM_ICON:
+                       g2.fillRect( width-1-MARGIN, MARGIN, 2, HEIGHT - 2*MARGIN );
+                       g2.fillPolygon( xPoints, yPoints, xPoints.length );
+                       break;
+               case STOP_ICON:
+                       g2.fillRect( MARGIN+1, MARGIN+1, width - 2*(MARGIN+1), HEIGHT - 2*(MARGIN+1) );
+                       break;
+               case PAUSE_ICON:
+                       g2.fillRect( MARGIN+1, MARGIN+1, width/5, HEIGHT - 2*(MARGIN+1) );
+                       g2.fillRect( width-1-MARGIN-width/5, MARGIN+1, width/5, HEIGHT - 2*(MARGIN+1) );
+                       break;
+               case EJECT_ICON:
+                       g2.fillPolygon( xPoints, yPoints, xPoints.length );
+                       g2.fillRect(
+                               MARGIN+1,
+                               HEIGHT - MARGIN - EJECT_BOTTOM_LINE_WIDTH,
+                               width - 2*MARGIN - 1,
+                               EJECT_BOTTOM_LINE_WIDTH
+                       );
+                       break;
+
+               case ANO_GAKKI_ICON:
+                       g2.setBackground( c.getBackground() );
+                       g2.clearRect( 0,  0, width, HEIGHT );
+                       g2.setColor(Color.cyan);
+                       g2.drawRect(  4, 4, 10, 10 );
+                       g2.drawLine(  1, 14, 30, 4 );
+                       g2.drawOval(  18, 1, 12, 12 );
+                       if( ! is_selected ) {
+                               // g2.setStroke(new BasicStroke(2));
+                               g2.setColor(Color.red);
+                               g2.drawLine( 0, 0, width-1, HEIGHT-1 );
+                               g2.drawLine( 0, HEIGHT-1, width-1, 0 );
+                       }
+                       break;
+
+               case INVERSION_ICON:
+                       g2.setBackground( c.getBackground() );
+                       g2.clearRect( 0,  0, width, HEIGHT );
+                       g2.setColor( c.getBackground().darker() );
+                       g2.drawRect(  0,  0, width-1, HEIGHT-1 );
+                       g2.drawLine(  8,  0,  8, HEIGHT );
+                       g2.drawLine( 16,  0, 16, HEIGHT );
+                       g2.drawLine( 24,  0, 24, HEIGHT );
+                       g2.setColor( c.getForeground() );
+                       g2.fillRect(  6,  0,  5,  HEIGHT/2 );
+                       g2.fillRect( 14,  0,  5,  HEIGHT/2 );
+                       g2.fillRect( 22,  0,  5,  HEIGHT/2 );
+                       if( is_selected ) {
+                               g2.setColor( Chord.NOTE_INDEX_COLORS[1] );
+                               g2.fillOval( 2, 10, 4, 4 );
+                               if( omitting_note == 1 ) {
+                                       g2.setColor( c.getForeground() );
+                                       g2.drawLine( 1, 9, 7, 15 );
+                                       g2.drawLine( 1, 15, 7, 9 );
+                               }
+                               g2.setColor( Chord.NOTE_INDEX_COLORS[2] );
+                               g2.fillOval( 10, 10, 4, 4 );
+                               if( omitting_note == 2 ) {
+                                       g2.setColor( c.getForeground() );
+                                       g2.drawLine( 9, 9, 15, 15 );
+                                       g2.drawLine( 9, 15, 15, 9 );
+                               }
+                               g2.setColor( Chord.NOTE_INDEX_COLORS[0] );
+                               g2.fillOval( 26, 10, 4, 4 );
+                               if( omitting_note == 0 ) {
+                                       g2.setColor( c.getForeground() );
+                                       g2.drawLine( 25, 9, 31, 15 );
+                                       g2.drawLine( 25, 15, 31, 9 );
+                               }
+                       }
+                       else {
+                               g2.setColor( Chord.NOTE_INDEX_COLORS[0] );
+                               g2.fillOval( 1, 9, 6, 6 );
+                               if( omitting_note == 0 ) {
+                                       g2.setColor( c.getForeground() );
+                                       g2.drawLine( 1, 9, 7, 15 );
+                                       g2.drawLine( 1, 15, 7, 9 );
+                               }
+                               g2.setColor( Chord.NOTE_INDEX_COLORS[1] );
+                               g2.fillOval( 10, 10, 4, 4 );
+                               if( omitting_note == 1 ) {
+                                       g2.setColor( c.getForeground() );
+                                       g2.drawLine( 9, 9, 15, 15 );
+                                       g2.drawLine( 9, 15, 15, 9 );
+                               }
+                               g2.setColor( Chord.NOTE_INDEX_COLORS[2] );
+                               g2.fillOval( 18, 10, 4, 4 );
+                               if( omitting_note == 2 ) {
+                                       g2.setColor( c.getForeground() );
+                                       g2.drawLine( 17, 9, 23, 15 );
+                                       g2.drawLine( 17, 15, 23, 9 );
+                               }
+                       }
+                       break;
+               case DARK_MODE_ICON:
+                       if( is_selected ) {
+                               g2.setColor( c.getForeground().darker() );
+                               g2.fillRect( 0, 0, width, HEIGHT );
+                               g2.setColor( Color.gray );
+                               g2.fillRect( width-2, 0, 2, HEIGHT );
+                               g2.drawLine( 0, 0, width-1, 0 );
+                               g2.setColor( Color.gray.darker() );
+                               g2.drawLine( 0, 0, 0, HEIGHT-1 );
+                               g2.drawLine( 0, HEIGHT-1, width-1, HEIGHT-1 );
+                               g2.setColor( Color.orange.brighter() );
+                               g2.fillRect( width-6, HEIGHT/2-3, 2, 6 );
+                       }
+                       else {
+                               g2.setColor( c.getBackground().brighter() );
+                               g2.fillRect( 0, 0, width, HEIGHT );
+                               g2.setColor( Color.gray.brighter() );
+                               g2.drawLine( 0, 0, width-1, 0 );
+                               g2.drawLine( width-1, 0, width-1, HEIGHT-1 );
+                               g2.setColor( Color.gray );
+                               g2.fillRect( 0, 0, 2, HEIGHT );
+                               g2.drawLine( 0, HEIGHT-1, width-1, HEIGHT-1 );
+                               g2.setColor( Color.gray.brighter() );
+                               g2.fillRect( width-6, HEIGHT/2-4, 4, 7 );
+                               g2.setColor( c.getForeground() );
+                               g2.fillRect( width-5, HEIGHT/2-3, 2, 5 );
+                       }
+                       break;
+               case X_ICON:
+                       g2.drawLine( 4, 5, width-5, HEIGHT-4 );
+                       g2.drawLine( 4, 4, width-4, HEIGHT-4 );
+                       g2.drawLine( 5, 4, width-4, HEIGHT-5 );
+                       g2.drawLine( width-5, 4, 4, HEIGHT-5 );
+                       g2.drawLine( width-4, 4, 4, HEIGHT-4 );
+                       g2.drawLine( width-4, 5, 5, HEIGHT-4 );
+                       break;
+               case REPEAT_ICON:
+                       g2.drawArc( MARGIN, MARGIN, width - 2*MARGIN, HEIGHT - 2*MARGIN, 150, 300 );
+                       g2.fillPolygon( xPoints, yPoints, xPoints.length );
+                       break;
+               case MIDI_CONNECTOR_ICON:
+                       g2.drawOval( 0, 0, width - 2, HEIGHT - 2 );
+                       g2.fillRect( width/2-2, HEIGHT-4, 3, 3 );
+                       g2.fillOval( width/2-2, 2, 3, 3 );
+                       g2.fillOval( width/2-5, 4, 2, 2 );
+                       g2.fillOval( width/2+2, 4, 2, 2 );
+                       g2.fillOval( width/2-6, 7, 2, 2 );
+                       g2.fillOval( width/2+3, 7, 2, 2 );
+                       break;
+               case NATURAL_ICON:
+                       g2.drawLine( width/2-2, 1, width/2-2, HEIGHT-4 );
+                       g2.drawLine( width/2+1, 3, width/2+1, HEIGHT-2 );
+                       g2.drawLine( width/2-2, 4, width/2+1, 4 );
+                       g2.drawLine( width/2-2, 5, width/2+1, 5 );
+                       g2.drawLine( width/2-2, HEIGHT-6, width/2+1, HEIGHT-6 );
+                       g2.drawLine( width/2-2, HEIGHT-5, width/2+1, HEIGHT-5 );
+                       break;
+               case EDIT_ICON:
+                       g2.drawRect( 3, 1, 10, 14 );
+                       g2.drawLine( 5, 3, 11, 3 );
+                       g2.drawLine( 5, 5, 11, 5 );
+                       g2.drawLine( 5, 7, 11, 7 );
+                       g2.drawLine( 5, 9, 11, 9 );
+                       g2.drawLine( 5, 11, 11, 11 );
+                       if( c.isEnabled() ) g2.setColor( Color.red );
+                       g2.drawLine( width-1, 2, width-9, 10 );
+                       g2.drawLine( width-1, 3, width-9, 11 );
+                       break;
+
+               case DOTTED_HALF_NOTE_ICON:
+               case HALF_NOTE_ICON:
+                       drawMusicalNoteStem( g2 );
+                       // No break;
+               case WHOLE_NOTE_ICON:
+                       drawMusicalNoteHead( g2 );
+                       if( isDottedMusicalNote() ) drawMusicalNoteDot(g2);
+                       break;
+
+               case A128TH_NOTE_ICON:
+                       drawMusicalNoteFlag( g2, 4 );
+                       // No break;
+               case DOTTED_64TH_NOTE_ICON:
+               case A64TH_NOTE_ICON:
+                       drawMusicalNoteFlag( g2, 3 );
+                       // No break;
+               case DOTTED_32ND_NOTE_ICON:
+               case A32ND_NOTE_ICON:
+                       drawMusicalNoteFlag( g2, 2 );
+                       // No break;
+               case DOTTED_16TH_NOTE_ICON:
+               case A16TH_NOTE_ICON:
+                       drawMusicalNoteFlag( g2, 1 );
+                       // No break;
+               case DOTTED_8TH_NOTE_ICON:
+               case A8TH_NOTE_ICON:
+                       drawMusicalNoteFlag( g2, 0 );
+                       // No break;
+               case DOTTED_QUARTER_NOTE_ICON:
+               case QUARTER_NOTE_ICON:
+                       fillMusicalNoteHead(g2);
+                       drawMusicalNoteStem(g2);
+                       if( isDottedMusicalNote() ) drawMusicalNoteDot(g2);
+                       break;
+               }
+               g.translate(-x, -y);
+       }
+       private void drawMusicalNoteFlag( Graphics2D g2, int position ) {
+               g2.drawLine(
+                       width/2 + NOTE_HEAD_WIDTH/2 - 1,
+                       1 + position * 2,
+                       width/2 + NOTE_HEAD_WIDTH/2 + 4,
+                       6 + position * 2
+               );
+       }
+       private void drawMusicalNoteDot( Graphics2D g2 ) {
+               g2.fillRect(
+                       width/2 + NOTE_HEAD_WIDTH/2 + 2,
+                       HEIGHT - NOTE_HEAD_HEIGHT + 3,
+                       2, 2
+               );
+       }
+       private void drawMusicalNoteStem( Graphics2D g2 ) {
+               g2.fillRect(
+                       width/2 + NOTE_HEAD_WIDTH/2 - 1,
+                       1,
+                       1, HEIGHT - NOTE_HEAD_HEIGHT/2
+               );
+       }
+       private void drawMusicalNoteHead( Graphics2D g2 ) {
+               g2.drawOval(
+                       width/2 - NOTE_HEAD_WIDTH/2,
+                       HEIGHT - NOTE_HEAD_HEIGHT,
+                       NOTE_HEAD_WIDTH-1, NOTE_HEAD_HEIGHT-1
+               );
+       }
+       private void fillMusicalNoteHead( Graphics2D g2 ) {
+               g2.fillOval(
+                       width/2 - NOTE_HEAD_WIDTH/2,
+                       HEIGHT - NOTE_HEAD_HEIGHT,
+                       NOTE_HEAD_WIDTH, NOTE_HEAD_HEIGHT
+               );
+       }
+}
diff --git a/src/camidion/chordhelper/ChordDisplayLabel.java b/src/camidion/chordhelper/ChordDisplayLabel.java
new file mode 100644 (file)
index 0000000..a916273
--- /dev/null
@@ -0,0 +1,141 @@
+package camidion.chordhelper;
+
+import java.awt.Color;
+import java.awt.Dimension;
+import java.awt.Graphics;
+import java.awt.event.InputEvent;
+import java.awt.event.MouseEvent;
+import java.awt.event.MouseListener;
+
+import javax.swing.JLabel;
+
+import camidion.chordhelper.chordmatrix.ChordMatrix;
+import camidion.chordhelper.music.Chord;
+import camidion.chordhelper.music.MIDISpec;
+import camidion.chordhelper.music.Music;
+import camidion.chordhelper.music.NoteSymbol;
+import camidion.chordhelper.pianokeyboard.PianoKeyboard;
+
+/**
+ * 和音表示ラベル
+ */
+public class ChordDisplayLabel extends JLabel implements MouseListener {
+       private String defaultString = null;
+       private Chord chord = null;
+       private int noteNumber = -1;
+       private boolean isDark = false;
+       private boolean isMouseEntered = false;
+       /**
+        * 和音表示ラベルを構築します。
+        * @param defaultString 初期表示する文字列
+        * @param chordMatrix このラベルをクリックしたときに鳴らす和音ボタンマトリクス
+        * @param keyboard このラベルをクリックしたときに鳴らす鍵盤
+        */
+       public ChordDisplayLabel(String defaultString, ChordMatrix chordMatrix, PianoKeyboard keyboard) {
+               super(defaultString, JLabel.CENTER);
+               this.defaultString = defaultString;
+               this.keyboard = keyboard;
+               if( (this.chordMatrix = chordMatrix) != null ) {
+                       addMouseListener(this);
+                       addMouseWheelListener(chordMatrix);
+               }
+       }
+       @Override
+       public void paint(Graphics g) {
+               super.paint(g);
+               Dimension d = getSize();
+               if( isMouseEntered && (noteNumber >= 0 || chord != null) ) {
+                       g.setColor(Color.gray);
+                       g.drawRect( 0, 0, d.width-1, d.height-1 );
+               }
+       }
+       private PianoKeyboard keyboard = null;
+       private ChordMatrix chordMatrix = null;
+       @Override
+       public void mousePressed(MouseEvent e) {
+               if( chord != null ) { // コードが表示されている場合
+                       if( (e.getModifiersEx() & InputEvent.BUTTON3_DOWN_MASK) != 0 ) {
+                               // 右クリックでコードを止める
+                               chordMatrix.setSelectedChord((Chord)null);
+                       }
+                       else {
+                               // コードを鳴らす。
+                               //   キーボードが指定されている場合、オリジナルキー(カポ反映済)のコードを使う。
+                               if( keyboard == null )
+                                       chordMatrix.setSelectedChord(chord);
+                               else
+                                       chordMatrix.setSelectedChordCapo(chord);
+                       }
+               }
+               else if( noteNumber >= 0 ) { // 音階が表示されている場合
+                       keyboard.noteOn(noteNumber);
+               }
+       }
+       @Override
+       public void mouseReleased(MouseEvent e) {
+               if( noteNumber >= 0 ) keyboard.noteOff(noteNumber);
+       }
+       @Override
+       public void mouseEntered(MouseEvent e) { mouseEntered(true); }
+       @Override
+       public void mouseExited(MouseEvent e) { mouseEntered(false); }
+       @Override
+       public void mouseClicked(MouseEvent e) {
+       }
+       private void mouseEntered(boolean isMouseEntered) {
+               this.isMouseEntered = isMouseEntered;
+               if( noteNumber >= 0 || chord != null ) repaint();
+       }
+       /**
+        * 音階を表示します。
+        * @param noteNumber MIDIノート番号
+        * @param isRhythmPart リズムパートのときtrue
+        */
+       public void setNote(int noteNumber, boolean isRhythmPart) {
+               setToolTipText(null);
+               this.chord = null;
+               if( (this.noteNumber = noteNumber) < 0 ) {
+                       setText(defaultString);
+                       return;
+               }
+               if( isRhythmPart ) {
+                       String pn = MIDISpec.getPercussionName(noteNumber);
+                       setText("MIDI note No." + noteNumber + " : " + pn);
+               }
+               else {
+                       String ns = NoteSymbol.noteNoToSymbol(noteNumber);
+                       double f = Music.noteNumberToFrequency(noteNumber);
+                       setText("Note: "+ns+"  -  MIDI note No."+noteNumber+" : "+Math.round(f)+"Hz");
+               }
+       }
+       /**
+        * 和音(コード名)を表示します。
+        * @param chord 和音
+        */
+       public void setChord(Chord chord) {
+               this.noteNumber = -1;
+               if( (this.chord = chord) == null ) {
+                       setText(defaultString);
+                       setToolTipText(null);
+               }
+               else {
+                       setChordText();
+                       setToolTipText("Chord: "+chord.toName());
+               }
+       }
+       /**
+        * 表示をクリアします。
+        */
+       public void clear() { setNote(-1, false); }
+       /**
+        * ダークモードのON/OFFを切り替えます。
+        * @param isDark ダークモードONのときtrue
+        */
+       public void setDarkMode(boolean isDark) {
+               this.isDark = isDark;
+               if( chord != null ) setChordText();
+       }
+       private void setChordText() {
+               setText(chord.toHtmlString(isDark ? "#FFCC33" : "maroon"));
+       }
+}
diff --git a/src/camidion/chordhelper/ChordHelperApplet.java b/src/camidion/chordhelper/ChordHelperApplet.java
new file mode 100644 (file)
index 0000000..dc23eef
--- /dev/null
@@ -0,0 +1,894 @@
+package camidion.chordhelper;
+import java.awt.BorderLayout;
+import java.awt.Color;
+import java.awt.Desktop;
+import java.awt.Dimension;
+import java.awt.Image;
+import java.awt.Insets;
+import java.awt.dnd.DnDConstants;
+import java.awt.dnd.DropTarget;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.ComponentAdapter;
+import java.awt.event.ComponentEvent;
+import java.awt.event.InputEvent;
+import java.awt.event.ItemEvent;
+import java.awt.event.ItemListener;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.security.AccessControlException;
+import java.util.Arrays;
+import java.util.Vector;
+
+import javax.sound.midi.InvalidMidiDataException;
+import javax.sound.midi.MetaEventListener;
+import javax.sound.midi.MetaMessage;
+import javax.sound.midi.Sequence;
+import javax.sound.midi.Sequencer;
+import javax.swing.Box;
+import javax.swing.BoxLayout;
+import javax.swing.ImageIcon;
+import javax.swing.JApplet;
+import javax.swing.JButton;
+import javax.swing.JComponent;
+import javax.swing.JEditorPane;
+import javax.swing.JLabel;
+import javax.swing.JLayeredPane;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.JSlider;
+import javax.swing.JSplitPane;
+import javax.swing.JToggleButton;
+import javax.swing.SwingUtilities;
+import javax.swing.border.Border;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+import javax.swing.event.HyperlinkEvent;
+import javax.swing.event.HyperlinkListener;
+
+import camidion.chordhelper.anogakki.AnoGakkiPane;
+import camidion.chordhelper.chorddiagram.ChordDiagram;
+import camidion.chordhelper.chordmatrix.ChordButtonLabel;
+import camidion.chordhelper.chordmatrix.ChordMatrix;
+import camidion.chordhelper.chordmatrix.ChordMatrixListener;
+import camidion.chordhelper.mididevice.MidiDeviceDialog;
+import camidion.chordhelper.mididevice.MidiDeviceModelList;
+import camidion.chordhelper.mididevice.SequencerMeasureView;
+import camidion.chordhelper.mididevice.SequencerTimeView;
+import camidion.chordhelper.mididevice.VirtualMidiDevice;
+import camidion.chordhelper.midieditor.Base64Dialog;
+import camidion.chordhelper.midieditor.KeySignatureLabel;
+import camidion.chordhelper.midieditor.SequenceTickIndex;
+import camidion.chordhelper.midieditor.SequenceTrackListTableModel;
+import camidion.chordhelper.midieditor.TempoSelecter;
+import camidion.chordhelper.midieditor.TimeSignatureSelecter;
+import camidion.chordhelper.music.Chord;
+import camidion.chordhelper.music.Key;
+import camidion.chordhelper.music.Range;
+import camidion.chordhelper.pianokeyboard.MidiKeyboardPanel;
+import camidion.chordhelper.pianokeyboard.PianoKeyboardAdapter;
+
+/**
+ * MIDI Chord Helper - Circle-of-fifth oriented chord pad
+ * (アプレットクラス)
+ *
+ *     @auther
+ *             Copyright (C) 2004-2014 @きよし - Akiyoshi Kamide
+ *             http://www.yk.rim.or.jp/~kamide/music/chordhelper/
+ */
+public class ChordHelperApplet extends JApplet {
+       /////////////////////////////////////////////////////////////////////
+       //
+       // JavaScript などからの呼び出しインターフェース
+       //
+       /////////////////////////////////////////////////////////////////////
+       /**
+        * 未保存の修正済み MIDI ファイルがあるかどうか調べます。
+        * @return 未保存の修正済み MIDI ファイルがあれば true
+        */
+       public boolean isModified() {
+               return deviceModelList.editorDialog.sequenceListTable.getModel().isModified();
+       }
+       /**
+        * 指定された小節数の曲を、乱数で自動作曲してプレイリストへ追加します。
+        * @param measureLength 小節数
+        * @return 追加先のインデックス値(0から始まる)。追加できなかったときは -1
+        */
+       public int addRandomSongToPlaylist(int measureLength) {
+               deviceModelList.editorDialog.newSequenceDialog.setRandomChordProgression(measureLength);
+               Sequence sequence = deviceModelList.editorDialog.newSequenceDialog.getMidiSequence();
+               return deviceModelList.editorDialog.sequenceListTable.getModel().addSequenceAndPlay(sequence);
+       }
+       /**
+        * URLで指定されたMIDIファイルをプレイリストへ追加します。
+        *
+        * <p>URL の最後の / より後ろの部分がファイル名として取り込まれます。
+        * 指定できる MIDI ファイルには、param タグの midi_file パラメータと同様の制限があります。
+        * </p>
+        * @param midiFileUrl 追加するMIDIファイルのURL
+        * @return 追加先のインデックス値(0から始まる)。追加できなかったときは -1
+        */
+       public int addToPlaylist(String midiFileUrl) {
+               try {
+                       return deviceModelList.editorDialog.sequenceListTable.getModel().addSequenceFromURL(midiFileUrl);
+               } catch( URISyntaxException|IOException|InvalidMidiDataException e ) {
+                       deviceModelList.editorDialog.showWarning(e.getMessage());
+               } catch( AccessControlException e ) {
+                       e.printStackTrace();
+                       deviceModelList.editorDialog.showError(e.getMessage());
+               }
+               return -1;
+       }
+       /**
+        * Base64 エンコードされた MIDI ファイルをプレイリストへ追加します。
+        *
+        * @param base64EncodedText Base64エンコードされたMIDIファイル
+        * @return 追加先のインデックス値(0から始まる)。追加できなかったときは -1
+        */
+       public int addToPlaylistBase64(String base64EncodedText) {
+               return addToPlaylistBase64(base64EncodedText, null);
+       }
+       /**
+        * ファイル名を指定して、
+        * Base64エンコードされたMIDIファイルをプレイリストへ追加します。
+        *
+        * @param base64EncodedText Base64エンコードされたMIDIファイル
+        * @param filename ディレクトリ名を除いたファイル名
+        * @return 追加先のインデックス値(0から始まる)。追加できなかったときは -1
+        */
+       public int addToPlaylistBase64(String base64EncodedText, String filename) {
+               Base64Dialog d = deviceModelList.editorDialog.base64Dialog;
+               d.setBase64Data(base64EncodedText);
+               try {
+                       return deviceModelList.editorDialog.sequenceListTable.getModel().addSequence(d.getMIDIData(), filename);
+               } catch (IOException | InvalidMidiDataException e) {
+                       e.printStackTrace();
+                       deviceModelList.editorDialog.showWarning(e.getMessage());
+                       return -1;
+               }
+       }
+       /**
+        * プレイリスト上で現在選択されているMIDIシーケンスを、
+        * シーケンサへロードして再生します。
+        */
+       public void play() {
+               play(deviceModelList.editorDialog.sequenceListTable.getModel().sequenceListSelectionModel.getMinSelectionIndex());
+       }
+       /**
+        * 指定されたインデックス値が示すプレイリスト上のMIDIシーケンスを、
+        * シーケンサへロードして再生します。
+        * @param index インデックス値(0から始まる)
+        */
+       public void play(int index) {
+               deviceModelList.editorDialog.sequenceListTable.getModel().loadToSequencer(index);
+               deviceModelList.getSequencerModel().start();
+       }
+       /**
+        * シーケンサが実行中かどうかを返します。
+        * {@link Sequencer#isRunning()} の戻り値をそのまま返します。
+        *
+        * @return 実行中のときtrue
+        */
+       public boolean isRunning() {
+               return deviceModelList.getSequencerModel().getSequencer().isRunning();
+       }
+       /**
+        * シーケンサが再生中かどうかを返します。
+        * @return 再生中のときtrue
+        */
+       public boolean isPlaying() { return isRunning(); }
+       /**
+        * 現在シーケンサにロードされているMIDIデータを
+        * Base64テキストに変換した結果を返します。
+        * @return MIDIデータをBase64テキストに変換した結果
+        */
+       public String getMidiDataBase64() {
+               SequenceTrackListTableModel sequenceModel =
+                       deviceModelList.editorDialog.sequenceListTable.getModel().sequencerModel.getSequenceTrackListTableModel();
+               deviceModelList.editorDialog.base64Dialog.setMIDIData(sequenceModel.getMIDIdata());
+               return deviceModelList.editorDialog.base64Dialog.getBase64Data();
+       }
+       /**
+        * 現在シーケンサにロードされているMIDIファイルのファイル名を返します。
+        * @return MIDIファイル名(設定されていないときは空文字列)
+        */
+       public String getMidiFilename() {
+               SequenceTrackListTableModel seq_model = deviceModelList.getSequencerModel().getSequenceTrackListTableModel();
+               if( seq_model == null ) return null;
+               String fn = seq_model.getFilename();
+               return fn == null ? "" : fn ;
+       }
+       /**
+        * オクターブ位置を設定します。
+        * @param octavePosition オクターブ位置(デフォルト:4)
+        */
+       public void setOctavePosition(int octavePosition) {
+               keyboardPanel.keyboardCenterPanel.keyboard.octaveRangeModel.setValue(octavePosition);
+       }
+       /**
+        * 操作対象のMIDIチャンネルを変更します。
+        * @param ch チャンネル番号 - 1(チャンネル1のとき0、デフォルトは0)
+        */
+       public void setChannel(int ch) {
+               keyboardPanel.keyboardCenterPanel.keyboard.midiChComboboxModel.setSelectedChannel(ch);
+       }
+       /**
+        * 操作対象のMIDIチャンネルを返します。
+        * @return 操作対象のMIDIチャンネル
+        */
+       public int getChannel() {
+               return keyboardPanel.keyboardCenterPanel.keyboard.midiChComboboxModel.getSelectedChannel();
+       }
+       /**
+        * 操作対象のMIDIチャンネルに対してプログラム(音色)を設定します。
+        * @param program 音色(0~127:General MIDI に基づく)
+        */
+       public void programChange(int program) {
+               keyboardPanel.keyboardCenterPanel.keyboard.getSelectedChannel().programChange(program);
+       }
+       /**
+        * 操作対象のMIDIチャンネルに対してプログラム(音色)を設定します。
+        * 内部的には {@link #programChange(int)} を呼び出しているだけです。
+        * @param program 音色(0~127:General MIDI に基づく)
+        */
+       public void setProgram(int program) { programChange(program); }
+       /**
+        * 自動転回モードを変更します。初期値は true です。
+        * @param isAuto true:自動転回を行う false:自動転回を行わない
+        */
+       public void setAutoInversion(boolean isAuto) {
+               inversionOmissionButton.setAutoInversion(isAuto);
+       }
+       /**
+        * 省略したい構成音を指定します。
+        * @param index
+        * <ul>
+        * <li>-1:省略しない(デフォルト)</li>
+        * <li>0:ルート音を省略</li>
+        * <li>1:三度を省略</li>
+        * <li>2:五度を省略</li>
+        * </ul>
+        */
+       public void setOmissionNoteIndex(int index) {
+               inversionOmissionButton.setOmissionNoteIndex(index);
+       }
+       /**
+        * コードダイアグラムの表示・非表示を切り替えます。
+        * @param isVisible 表示するときtrue
+        */
+       public void setChordDiagramVisible(boolean isVisible) {
+               keyboardSplitPane.resetToPreferredSizes();
+               if( ! isVisible )
+                       keyboardSplitPane.setDividerLocation((double)1.0);
+       }
+       /**
+        * コードダイヤグラムをギターモードに変更します。
+        * 初期状態ではウクレレモードになっています。
+        */
+       public void setChordDiagramForGuitar() {
+               chordDiagram.setTargetInstrument(ChordDiagram.Instrument.Guitar);
+       }
+       /**
+        * ダークモード(暗い表示)と明るい表示とを切り替えます。
+        * @param isDark ダークモードのときtrue、明るい表示のときfalse(デフォルト)
+        */
+       public void setDarkMode(boolean isDark) {
+               darkModeToggleButton.setSelected(isDark);
+       }
+       /**
+        * バージョン情報
+        */
+       public static class VersionInfo {
+               public static final String      NAME = "MIDI Chord Helper";
+               public static final String      VERSION = "Ver.20150104.1";
+               public static final String      COPYRIGHT = "Copyright (C) 2004-2014";
+               public static final String      AUTHER = "@きよし - Akiyoshi Kamide";
+               public static final String      URL = "http://www.yk.rim.or.jp/~kamide/music/chordhelper/";
+               /**
+                * バージョン情報を返します。
+                * @return バージョン情報
+                */
+               public static String getInfo() {
+                       return NAME + " " + VERSION + " " + COPYRIGHT + " " + AUTHER + " " + URL;
+               }
+       }
+       @Override
+       public String getAppletInfo() { return VersionInfo.getInfo(); }
+       private class AboutMessagePane extends JEditorPane implements ActionListener {
+               URI uri = null;
+               public AboutMessagePane() { this(true); }
+               public AboutMessagePane(boolean link_enabled) {
+                       super( "text/html", "" );
+                       String link_string, tooltip = null;
+                       if( link_enabled && Desktop.isDesktopSupported() ) {
+                               tooltip = "Click this URL to open with your web browser - URLをクリックしてWebブラウザで開く";
+                               link_string =
+                                       "<a href=\"" + VersionInfo.URL + "\" title=\"" +
+                                       tooltip + "\">" + VersionInfo.URL + "</a>" ;
+                       }
+                       else {
+                               link_enabled = false; link_string = VersionInfo.URL;
+                       }
+                       setText(
+                               "<html><center><font size=\"+1\">" + VersionInfo.NAME + "</font>  " +
+                                               VersionInfo.VERSION + "<br/><br/>" +
+                                               VersionInfo.COPYRIGHT + " " + VersionInfo.AUTHER + "<br/>" +
+                                               link_string + "</center></html>"
+                       );
+                       setToolTipText(tooltip);
+                       setOpaque(false);
+                       putClientProperty(JEditorPane.HONOR_DISPLAY_PROPERTIES, Boolean.TRUE);
+                       setEditable(false);
+                       //
+                       // メッセージ内の <a href=""> ~ </a> によるリンクを
+                       // 実際に機能させる(ブラウザで表示されるようにする)ための設定
+                       //
+                       if( ! link_enabled ) return;
+                       try {
+                               uri = new URI(VersionInfo.URL);
+                       }catch( URISyntaxException use ) {
+                               use.printStackTrace();
+                               return;
+                       }
+                       addHyperlinkListener(new HyperlinkListener() {
+                               public void hyperlinkUpdate(HyperlinkEvent e) {
+                                       if(e.getEventType()==HyperlinkEvent.EventType.ACTIVATED) {
+                                               try{
+                                                       Desktop.getDesktop().browse(uri);
+                                               }catch(IOException ioe) {
+                                                       ioe.printStackTrace();
+                                               }
+                                       }
+                               }
+                       });
+               }
+               @Override
+               public void actionPerformed(ActionEvent e) {
+                       JOptionPane.showMessageDialog(
+                               null, this, "Version info",
+                               JOptionPane.INFORMATION_MESSAGE, imageIcon
+                       );
+               }
+       }
+       // 終了してよいか確認する
+       public boolean isConfirmedToExit() {
+               return ! isModified() || JOptionPane.showConfirmDialog(
+                       this,
+                       "MIDI file not saved, exit anyway ?\n保存されていないMIDIファイルがありますが、終了してよろしいですか?",
+                       VersionInfo.NAME,
+                       JOptionPane.YES_NO_OPTION,
+                       JOptionPane.WARNING_MESSAGE
+               ) == JOptionPane.YES_OPTION ;
+       }
+       /**
+        * アプリケーションのアイコンイメージ
+        */
+       public ImageIcon imageIcon;
+       /**
+        * ボタンの余白を詰めたいときに setMargin() の引数に指定するインセット
+        */
+       public static final Insets ZERO_INSETS = new Insets(0,0,0,0);
+       //
+       public ChordMatrix chordMatrix;
+       MidiDeviceModelList     deviceModelList;
+       //
+       private JPanel keyboardSequencerPanel;
+       private JPanel chordGuide;
+       private Color rootPaneDefaultBgcolor;
+       private Color lyricDisplayDefaultBgcolor;
+       private Border lyricDisplayDefaultBorder;
+       private JSplitPane mainSplitPane;
+       private JSplitPane keyboardSplitPane;
+       private ChordButtonLabel enterButtonLabel;
+       private ChordTextField  lyricDisplay;
+       private MidiKeyboardPanel keyboardPanel;
+       private InversionAndOmissionLabel inversionOmissionButton;
+       private JToggleButton darkModeToggleButton;
+       private MidiDeviceDialog midiConnectionDialog;
+       private ChordDiagram chordDiagram;
+       private TempoSelecter tempoSelecter;
+       private TimeSignatureSelecter timesigSelecter;
+       private KeySignatureLabel keysigLabel;
+       private JLabel songTitleLabel;
+       private AnoGakkiPane anoGakkiPane;
+       private JToggleButton anoGakkiToggleButton;
+
+       public void init() {
+               String imageIconPath = "midichordhelper.png";
+               URL imageIconUrl = getClass().getResource(imageIconPath);
+               if( imageIconUrl == null ) {
+                       System.out.println("Icon image "+imageIconPath+" not found");
+                       imageIcon = null;
+               }
+               else {
+                       imageIcon = new ImageIcon(imageIconUrl);
+               }
+               Image iconImage = (imageIcon == null) ? null : imageIcon.getImage();
+               rootPaneDefaultBgcolor = getContentPane().getBackground();
+               chordMatrix = new ChordMatrix() {{
+                       addChordMatrixListener(new ChordMatrixListener(){
+                               public void keySignatureChanged() {
+                                       Key capoKey = getKeySignatureCapo();
+                                       keyboardPanel.keySelecter.setKey(capoKey);
+                                       keyboardPanel.keyboardCenterPanel.keyboard.setKeySignature(capoKey);
+                               }
+                               public void chordChanged() { chordOn(); }
+                       });
+               }};
+               chordMatrix.capoSelecter.checkbox.addItemListener(
+                       new ItemListener() {
+                               public void itemStateChanged(ItemEvent e) {
+                                       chordOn();
+                                       keyboardPanel.keyboardCenterPanel.keyboard.chordDisplay.clear();
+                                       chordDiagram.clear();
+                               }
+                       }
+               );
+               chordMatrix.capoSelecter.valueSelecter.addActionListener(
+                       new ActionListener() {
+                               public void actionPerformed(ActionEvent e) {
+                                       chordOn();
+                                       keyboardPanel.keyboardCenterPanel.keyboard.chordDisplay.clear();
+                                       chordDiagram.clear();
+                               }
+                       }
+               );
+               keyboardPanel = new MidiKeyboardPanel(chordMatrix) {{
+                       keyboardCenterPanel.keyboard.addPianoKeyboardListener(
+                               new PianoKeyboardAdapter() {
+                                       @Override
+                                       public void pianoKeyPressed(int n, InputEvent e) {
+                                               chordDiagram.clear();
+                                       }
+                               }
+                       );
+                       keySelecter.keysigCombobox.addActionListener(
+                               new ActionListener() {
+                                       @Override
+                                       public void actionPerformed(ActionEvent e) {
+                                               Key key = keySelecter.getKey();
+                                               key.transpose( - chordMatrix.capoSelecter.getCapo() );
+                                               chordMatrix.setKeySignature(key);
+                                       }
+                               }
+                       );
+                       keyboardCenterPanel.keyboard.setPreferredSize(new Dimension(571, 80));
+               }};
+               deviceModelList = new MidiDeviceModelList(
+                       new Vector<VirtualMidiDevice>() {
+                               {
+                                       add(keyboardPanel.keyboardCenterPanel.keyboard.midiDevice);
+                               }
+                       }
+               );
+               deviceModelList.editorDialog.setIconImage(iconImage);
+               new DropTarget(this, DnDConstants.ACTION_COPY_OR_MOVE, deviceModelList.editorDialog, true);
+               keyboardPanel.setEventDialog(deviceModelList.editorDialog.eventDialog);
+               midiConnectionDialog = new MidiDeviceDialog(deviceModelList);
+               midiConnectionDialog.setIconImage(iconImage);
+               lyricDisplay = new ChordTextField(deviceModelList.getSequencerModel()) {{
+                       addActionListener(new ActionListener() {
+                               @Override
+                               public void actionPerformed(ActionEvent event) {
+                                       String symbol = event.getActionCommand().trim().split("[ \t\r\n]")[0];
+                                       chordMatrix.setSelectedChord(symbol);
+                               }
+                       });
+               }};
+               lyricDisplayDefaultBorder = lyricDisplay.getBorder();
+               lyricDisplayDefaultBgcolor = lyricDisplay.getBackground();
+               chordDiagram = new ChordDiagram(this);
+               tempoSelecter = new TempoSelecter() {{
+                       setEditable(false);
+                       deviceModelList.getSequencerModel().getSequencer().addMetaEventListener(this);
+               }};
+               timesigSelecter = new TimeSignatureSelecter() {{
+                       setEditable(false);
+                       deviceModelList.getSequencerModel().getSequencer().addMetaEventListener(this);
+               }};
+               keysigLabel = new KeySignatureLabel() {{
+                       addMouseListener(new MouseAdapter() {
+                               public void mousePressed(MouseEvent e) {
+                                       chordMatrix.setKeySignature(getKey());
+                               }
+                       });
+               }};
+               deviceModelList.getSequencerModel().getSequencer().addMetaEventListener(
+                       new MetaEventListener() {
+                               class SetKeySignatureRunnable implements Runnable {
+                                       Key key;
+                                       public SetKeySignatureRunnable(Key key) {
+                                               this.key = key;
+                                       }
+                                       @Override
+                                       public void run() { setKeySignature(key); }
+                               }
+                               @Override
+                               public void meta(MetaMessage msg) {
+                                       switch(msg.getType()) {
+                                       case 0x59: // Key signature (2 bytes) : 調号
+                                               Key key = new Key(msg.getData());
+                                               if( ! SwingUtilities.isEventDispatchThread() ) {
+                                                       SwingUtilities.invokeLater(
+                                                               new SetKeySignatureRunnable(key)
+                                                       );
+                                               }
+                                               setKeySignature(key);
+                                               break;
+                                       }
+                               }
+                               private void setKeySignature(Key key) {
+                                       keysigLabel.setKeySignature(key);
+                                       chordMatrix.setKeySignature(key);
+                               }
+                       }
+               );
+               songTitleLabel = new JLabel();
+               //シーケンサーの時間スライダーの値が変わったときのリスナーを登録
+               deviceModelList.getSequencerModel().addChangeListener(new ChangeListener() {
+                       @Override
+                       public void stateChanged(ChangeEvent e) {
+                               SequenceTrackListTableModel sequenceTableModel = deviceModelList.getSequencerModel().getSequenceTrackListTableModel();
+                               int loadedSequenceIndex = deviceModelList.editorDialog.sequenceListTable.getModel().indexOfSequenceOnSequencer();
+                               songTitleLabel.setText(
+                                       "<html>"+(
+                                               loadedSequenceIndex < 0 ? "[No MIDI file loaded]" :
+                                               "MIDI file " + loadedSequenceIndex + ": " + (
+                                                       sequenceTableModel == null ||
+                                                       sequenceTableModel.toString() == null ||
+                                                       sequenceTableModel.toString().isEmpty() ?
+                                                       "[Untitled]" :
+                                                       "<font color=maroon>"+sequenceTableModel+"</font>"
+                                               )
+                                       )+"</html>"
+                               );
+                               Sequencer sequencer = deviceModelList.getSequencerModel().getSequencer();
+                               chordMatrix.setPlaying(sequencer.isRunning());
+                               if( sequenceTableModel != null ) {
+                                       SequenceTickIndex tickIndex = sequenceTableModel.getSequenceTickIndex();
+                                       long tickPos = sequencer.getTickPosition();
+                                       tickIndex.tickToMeasure(tickPos);
+                                       chordMatrix.setBeat(tickIndex);
+                                       if(
+                                               deviceModelList.getSequencerModel().getValueIsAdjusting() ||
+                                               ! (sequencer.isRunning() || sequencer.isRecording())
+                                       ) {
+                                               MetaMessage msg;
+                                               msg = tickIndex.lastMetaMessageAt(
+                                                       SequenceTickIndex.MetaMessageType.TIME_SIGNATURE, tickPos
+                                               );
+                                               timesigSelecter.setValue(msg==null ? null : msg.getData());
+                                               msg = tickIndex.lastMetaMessageAt(
+                                                       SequenceTickIndex.MetaMessageType.TEMPO, tickPos
+                                               );
+                                               tempoSelecter.setTempo(msg==null ? null : msg.getData());
+                                               msg = tickIndex.lastMetaMessageAt(
+                                                       SequenceTickIndex.MetaMessageType.KEY_SIGNATURE, tickPos
+                                               );
+                                               if( msg == null ) {
+                                                       keysigLabel.clear();
+                                               }
+                                               else {
+                                                       Key key = new Key(msg.getData());
+                                                       keysigLabel.setKeySignature(key);
+                                                       chordMatrix.setKeySignature(key);
+                                               }
+                                       }
+                               }
+                       }
+               });
+               deviceModelList.getSequencerModel().fireStateChanged();
+               chordGuide = new JPanel() {
+                       {
+                               setLayout(new BoxLayout(this, BoxLayout.X_AXIS));
+                               add( Box.createHorizontalStrut(2) );
+                               add( chordMatrix.chordGuide );
+                               add( Box.createHorizontalStrut(2) );
+                               add( lyricDisplay );
+                               add( Box.createHorizontalStrut(2) );
+                               add( enterButtonLabel = new ChordButtonLabel("Enter",chordMatrix) {{
+                                       addMouseListener(new MouseAdapter() {
+                                               public void mousePressed(MouseEvent event) {
+                                                       if( (event.getModifiersEx() & InputEvent.BUTTON3_DOWN_MASK) != 0 ) // RightClicked
+                                                               chordMatrix.setSelectedChord((Chord)null);
+                                                       else {
+                                                               chordMatrix.setSelectedChord(lyricDisplay.getText());
+                                                       }
+                                               }
+                                       });
+                               }});
+                               add( Box.createHorizontalStrut(5) );
+                               add( chordMatrix.chordDisplay );
+                               add( Box.createHorizontalStrut(5) );
+                               add( darkModeToggleButton = new JToggleButton(new ButtonIcon(ButtonIcon.DARK_MODE_ICON)) {{
+                                       setMargin(ZERO_INSETS);
+                                       addItemListener(new ItemListener() {
+                                               public void itemStateChanged(ItemEvent e) {
+                                                       innerSetDarkMode(darkModeToggleButton.isSelected());
+                                               }
+                                       });
+                                       setToolTipText("Light / Dark - 明かりを点灯/消灯");
+                                       setBorder(null);
+                               }});
+                               add( Box.createHorizontalStrut(5) );
+                               add( anoGakkiToggleButton = new JToggleButton(
+                                       new ButtonIcon(ButtonIcon.ANO_GAKKI_ICON)
+                               ) {{
+                                       setOpaque(false);
+                                       setMargin(ZERO_INSETS);
+                                       setBorder( null );
+                                       setToolTipText("あの楽器");
+                                       addItemListener(
+                                               new ItemListener() {
+                                                       public void itemStateChanged(ItemEvent e) {
+                                                               keyboardPanel.keyboardCenterPanel.keyboard.anoGakkiPane
+                                                               = anoGakkiToggleButton.isSelected() ? anoGakkiPane : null ;
+                                                       }
+                                               }
+                                       );
+                               }} );
+                               add( Box.createHorizontalStrut(5) );
+                               add( inversionOmissionButton = new InversionAndOmissionLabel() );
+                               add( Box.createHorizontalStrut(5) );
+                               add( chordMatrix.capoSelecter );
+                               add( Box.createHorizontalStrut(2) );
+                       }
+               };
+               keyboardSequencerPanel = new JPanel() {{
+                       setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
+                       add(chordGuide);
+                       add(Box.createVerticalStrut(5));
+                       add(keyboardSplitPane = new JSplitPane(
+                               JSplitPane.HORIZONTAL_SPLIT, keyboardPanel, chordDiagram
+                       ) {{
+                               setOneTouchExpandable(true);
+                               setResizeWeight(1.0);
+                               setAlignmentX((float)0.5);
+                       }});
+                       add(Box.createVerticalStrut(5));
+                       add(new JPanel() {{
+                               setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
+                               add(new JPanel() {{
+                                       setLayout(new BoxLayout(this, BoxLayout.X_AXIS));
+                                       add( Box.createHorizontalStrut(12) );
+                                       add( keysigLabel );
+                                       add( Box.createHorizontalStrut(12) );
+                                       add( timesigSelecter );
+                                       add( Box.createHorizontalStrut(12) );
+                                       add( tempoSelecter );
+                                       add( Box.createHorizontalStrut(12) );
+                                       add( new SequencerMeasureView(deviceModelList.getSequencerModel()) );
+                                       add( Box.createHorizontalStrut(12) );
+                                       add( songTitleLabel );
+                                       add( Box.createHorizontalStrut(12) );
+                                       add( new JButton(deviceModelList.editorDialog.openAction) {{ setMargin(ZERO_INSETS); }});
+                               }});
+                               add(new JPanel() {{
+                                       setLayout(new BoxLayout(this, BoxLayout.X_AXIS));
+                                       add( Box.createHorizontalStrut(10) );
+                                       add( new JSlider(deviceModelList.getSequencerModel()) );
+                                       add( new SequencerTimeView(deviceModelList.getSequencerModel()) );
+                                       add( Box.createHorizontalStrut(5) );
+                                       add( new JButton(deviceModelList.editorDialog.sequenceListTable.getModel().moveToTopAction) {{
+                                               setMargin(ZERO_INSETS);
+                                       }});
+                                       add(new JButton(deviceModelList.getSequencerModel().moveBackwardAction) {{
+                                               setMargin(ZERO_INSETS);
+                                       }});
+                                       add(new JToggleButton(deviceModelList.getSequencerModel().startStopAction));
+                                       add(new JButton(deviceModelList.getSequencerModel().moveForwardAction) {{
+                                               setMargin(ZERO_INSETS);
+                                       }});
+                                       add(new JButton(deviceModelList.editorDialog.sequenceListTable.getModel().moveToBottomAction) {{
+                                               setMargin(ZERO_INSETS);
+                                       }});
+                                       add(new JToggleButton(deviceModelList.editorDialog.sequenceListTable.getModel().toggleRepeatAction) {{
+                                               setMargin(ZERO_INSETS);
+                                       }});
+                                       add( Box.createHorizontalStrut(10) );
+                               }});
+                               add(new JPanel() {{
+                                       add(new JButton(
+                                               "MIDI device connection",
+                                               new ButtonIcon( ButtonIcon.MIDI_CONNECTOR_ICON )
+                                       ) {{
+                                               addActionListener(midiConnectionDialog);
+                                       }});
+                                       add(new JButton("Version info") {{
+                                               setToolTipText(VersionInfo.NAME + " " + VersionInfo.VERSION);
+                                               addActionListener(new AboutMessagePane());
+                                       }});
+                               }});
+                       }});
+               }};
+               setContentPane(new JLayeredPane() {
+                       {
+                               add(anoGakkiPane = new AnoGakkiPane(), JLayeredPane.PALETTE_LAYER);
+                               addComponentListener(new ComponentAdapter() {
+                                       @Override
+                                       public void componentResized(ComponentEvent e) {
+                                               adjustSize();
+                                       }
+                                       @Override
+                                       public void componentShown(ComponentEvent e) {
+                                               adjustSize();
+                                       }
+                                       private void adjustSize() {
+                                               anoGakkiPane.setBounds(getBounds());
+                                       }
+                               });
+                               setLayout(new BorderLayout());
+                               setOpaque(true);
+                               add(mainSplitPane = new JSplitPane(
+                                       JSplitPane.VERTICAL_SPLIT,
+                                       chordMatrix, keyboardSequencerPanel
+                               ){
+                                       {
+                                               setResizeWeight(0.5);
+                                               setAlignmentX((float)0.5);
+                                               setDividerSize(5);
+                                       }
+                               });
+                       }
+               });
+               setPreferredSize(new Dimension(750,470));
+       }
+       @Override
+       public void start() {
+               //
+               // コードボタンで設定されている現在の調を
+               // ピアノキーボードに伝える
+               chordMatrix.fireKeySignatureChanged();
+               //
+               // アプレットのパラメータにMIDIファイルのURLが指定されていたら
+               // それを再生する
+               String midi_url = getParameter("midi_file");
+               System.gc();
+               if( midi_url != null ) {
+                       addToPlaylist(midi_url);
+                       play();
+               }
+       }
+       @Override
+       public void stop() {
+               deviceModelList.getSequencerModel().stop(); // MIDI再生を強制終了
+               System.gc();
+       }
+       private void innerSetDarkMode(boolean isDark) {
+               Color col = isDark ? Color.black : null;
+               getContentPane().setBackground(
+                       isDark ? Color.black : rootPaneDefaultBgcolor
+               );
+               mainSplitPane.setBackground(col);
+               keyboardSplitPane.setBackground(col);
+               enterButtonLabel.setDarkMode(isDark);
+               chordGuide.setBackground(col);
+               lyricDisplay.setBorder(isDark ? null : lyricDisplayDefaultBorder);
+               lyricDisplay.setBackground(isDark ?
+                       chordMatrix.darkModeColorset.backgrounds[2] :
+                       lyricDisplayDefaultBgcolor
+               );
+               lyricDisplay.setForeground(isDark ? Color.white : null);
+               inversionOmissionButton.setBackground(col);
+               anoGakkiToggleButton.setBackground(col);
+               keyboardSequencerPanel.setBackground(col);
+               chordDiagram.setBackground(col);
+               chordDiagram.titleLabel.setDarkMode(isDark);
+               chordMatrix.setDarkMode(isDark);
+               keyboardPanel.setDarkMode(isDark);
+       }
+
+       private int[] chordOnNotes = null;
+       /**
+        * 和音を発音します。
+        * <p>この関数を直接呼ぶとアルペジオが効かないので、
+        * chord_matrix.setSelectedChord() を使うことを推奨
+        * </p>
+        */
+       public void chordOn() {
+               Chord playChord = chordMatrix.getSelectedChord();
+               if(
+                       chordOnNotes != null &&
+                       chordMatrix.getNoteIndex() < 0 &&
+                       (! chordMatrix.isDragged() || playChord == null)
+               ) {
+                       // コードが鳴っている状態で、新たなコードを鳴らそうとしたり、
+                       // もう鳴らさないという信号が来た場合は、今鳴っている音を止める。
+                       //
+                       for( int n : chordOnNotes )
+                               keyboardPanel.keyboardCenterPanel.keyboard.noteOff(n);
+                       chordOnNotes = null;
+               }
+               if( playChord == null ) {
+                       // もう鳴らさないので、歌詞表示に通知して終了
+                       if( lyricDisplay != null )
+                               lyricDisplay.appendChord(null);
+                       return;
+               }
+               // あの楽器っぽい表示
+               if( keyboardPanel.keyboardCenterPanel.keyboard.anoGakkiPane != null ) {
+                       JComponent btn = chordMatrix.getSelectedButton();
+                       if( btn != null ) anoGakkiPane.start(chordMatrix, btn.getBounds());
+               }
+               // コードボタンからのコードを、カポつき演奏キーからオリジナルキーへ変換
+               Key originalKey = chordMatrix.getKeySignatureCapo();
+               Chord originalChord = playChord.clone().transpose(
+                       chordMatrix.capoSelecter.getCapo(),
+                       chordMatrix.getKeySignature()
+               );
+               // 変換後のコードをキーボード画面に設定
+               keyboardPanel.keyboardCenterPanel.keyboard.setChord(originalChord);
+               //
+               // 音域を決める。これにより鳴らす音が確定する。
+               Range chordRange = new Range(
+                       keyboardPanel.keyboardCenterPanel.keyboard.getChromaticOffset() + 10 +
+                       ( keyboardPanel.keyboardCenterPanel.keyboard.getOctaves() / 4 ) * 12,
+                       inversionOmissionButton.isAutoInversionMode() ?
+                       keyboardPanel.keyboardCenterPanel.keyboard.getChromaticOffset() + 21 :
+                       keyboardPanel.keyboardCenterPanel.keyboard.getChromaticOffset() + 33,
+                       -2,
+                       inversionOmissionButton.isAutoInversionMode()
+               );
+               int[] notes = originalChord.toNoteArray(chordRange, originalKey);
+               //
+               // 前回鳴らしたコード構成音を覚えておく
+               int[] prevChordOnNotes = null;
+               if( chordMatrix.isDragged() || chordMatrix.getNoteIndex() >= 0 )
+                       prevChordOnNotes = Arrays.copyOf(chordOnNotes, chordOnNotes.length);
+               //
+               // 次に鳴らす構成音を決める
+               chordOnNotes = new int[notes.length];
+               int i = 0;
+               for( int n : notes ) {
+                       if( inversionOmissionButton.getOmissionNoteIndex() == i ) {
+                               i++; continue;
+                       }
+                       chordOnNotes[i++] = n;
+                       //
+                       // その音が今鳴っているか調べる
+                       boolean isNoteOn = false;
+                       if( prevChordOnNotes != null ) {
+                               for( int prevN : prevChordOnNotes ) {
+                                       if( n == prevN ) {
+                                               isNoteOn = true;
+                                               break;
+                                       }
+                               }
+                       }
+                       // すでに鳴っているのに単音を鳴らそうとする場合、
+                       // 鳴らそうとしている音を一旦止める。
+                       if( isNoteOn && chordMatrix.getNoteIndex() >= 0 &&
+                               notes[chordMatrix.getNoteIndex()] - n == 0
+                       ) {
+                               keyboardPanel.keyboardCenterPanel.keyboard.noteOff(n);
+                               isNoteOn = false;
+                       }
+                       // その音が鳴っていなかったら鳴らす。
+                       if( ! isNoteOn )
+                               keyboardPanel.keyboardCenterPanel.keyboard.noteOn(n);
+               }
+               //
+               // コードを表示
+               keyboardPanel.keyboardCenterPanel.keyboard.setChord(originalChord);
+               chordMatrix.chordDisplay.setChord(playChord);
+               //
+               // コードダイアグラム用にもコードを表示
+               Chord diagramChord;
+               int chordDiagramCapo = chordDiagram.capoSelecterView.getCapo();
+               if( chordDiagramCapo == chordMatrix.capoSelecter.getCapo() )
+                       diagramChord = playChord.clone();
+               else
+                       diagramChord = originalChord.clone().transpose(
+                               - chordDiagramCapo, originalKey
+                       );
+               chordDiagram.setChord(diagramChord);
+               if( chordDiagram.recordTextButton.isSelected() )
+                       lyricDisplay.appendChord(diagramChord);
+       }
+
+}
+
diff --git a/src/camidion/chordhelper/ChordTextField.java b/src/camidion/chordhelper/ChordTextField.java
new file mode 100644 (file)
index 0000000..89c67e6
--- /dev/null
@@ -0,0 +1,148 @@
+package camidion.chordhelper;
+
+import java.io.UnsupportedEncodingException;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.sound.midi.MetaEventListener;
+import javax.sound.midi.MetaMessage;
+import javax.swing.JTextField;
+import javax.swing.SwingUtilities;
+
+import camidion.chordhelper.mididevice.MidiSequencerModel;
+import camidion.chordhelper.midieditor.SequenceTrackListTableModel;
+import camidion.chordhelper.music.Chord;
+
+public class ChordTextField extends JTextField implements MetaEventListener {
+       private MidiSequencerModel sequencerModel;
+       public ChordTextField(MidiSequencerModel sequencerModel) {
+               super(80);
+               //
+               // JTextField は、サイズ設定をしないとリサイズ時に縦に伸び過ぎてしまう。
+               // 1行しか入力できないので、縦に伸びすぎるのはスペースがもったいない。
+               // そこで、このような現象を防止するために、最大サイズを明示的に
+               // 画面サイズと同じに設定する。
+               //
+               // To reduce resized height, set maximum size to screen size.
+               //
+               setMaximumSize(
+                       java.awt.Toolkit.getDefaultToolkit().getScreenSize()
+               );
+               this.sequencerModel = sequencerModel;
+               sequencerModel.getSequencer().addMetaEventListener(this);
+       }
+       @Override
+       public void meta(MetaMessage msg) {
+               int t = msg.getType();
+               switch(t) {
+               case 0x01: // Text(任意のテキスト:コメントなど)
+               case 0x05: // Lyrics(歌詞)
+               case 0x02: // Copyright(著作権表示)
+               case 0x03: // Sequence Name / Track Name(曲名またはトラック名)
+               case 0x06: // Marker
+                       byte[] d = msg.getData();
+                       if( ! SwingUtilities.isEventDispatchThread() ) {
+                               // MIDIシーケンサの EDT から呼ばれた場合、
+                               // 表示処理を Swing の EDT に振り直す。
+                               SwingUtilities.invokeLater(new AddTextJob(t,d));
+                               return;
+                       }
+                       addText(t,d);
+                       break;
+               default:
+                       return;
+               }
+       }
+       /**
+        * 歌詞を追加するジョブ
+        */
+       public class AddTextJob implements Runnable {
+               private int type;
+               private byte[] data;
+               public AddTextJob(int type, byte[] data) {
+                       this.type = type;
+                       this.data = data;
+               }
+               @Override
+               public void run() { addText(type, data); }
+       }
+       /**
+        * 前回のタイムスタンプ
+        */
+       private long lastArrivedTime = System.nanoTime();
+       /**
+        * スキップするテキスト
+        */
+       private Map<Integer,String> skippingTextMap = new HashMap<>();
+       /**
+        * テキストを追加し、カーソルを末尾に移動します。
+        * @param data テキストの元データ
+        */
+       private void addText(int type, byte[] data) {
+               // 頻繁に来たかどうかだけとりあえずチェック
+               long arrivedTime = System.nanoTime();
+               boolean isSoon = (arrivedTime - lastArrivedTime < 1000000000L /* 1sec */);
+               lastArrivedTime = arrivedTime;
+               //
+               // 文字コード確認用シーケンス
+               SequenceTrackListTableModel m = sequencerModel.getSequenceTrackListTableModel();
+               //
+               // 追加するデータを適切な文字コードで文字列に変換
+               String additionalText;
+               if( m != null ) {
+                       additionalText = new String(data,m.charset);
+               }
+               else try {
+                       additionalText = new String(data,"JISAutoDetect");
+               }
+               catch( UnsupportedEncodingException e ) {
+                       additionalText = new String(data);
+               }
+               additionalText = additionalText.trim();
+               String lastAdditionalText = skippingTextMap.remove(type);
+               // 歌詞とテキストで同じもの同士がすぐに来た場合は追加しない
+               if( ! (isSoon && additionalText.equals(lastAdditionalText)) ) {
+                       // テキストと歌詞が同じかどうかチェックするための比較対象を記録
+                       switch(type) {
+                       case 0x01: skippingTextMap.put(0x05,additionalText);
+                       case 0x05: skippingTextMap.put(0x01,additionalText);
+                       }
+                       // 既存の歌詞
+                       String currentText = getText();
+                       if(
+                               currentText != null && ! currentText.isEmpty()
+                               && (
+                                       isSoon ||
+                                       ! additionalText.isEmpty() && additionalText.length() <= 8
+                               )
+                       ) {
+                               // 既存歌詞がある場合、頻繁に来たか短い歌詞だったら追加
+                               currentText += " " + additionalText;
+                       }
+                       else {
+                               // それ以外の場合は上書き
+                               currentText = additionalText;
+                       }
+                       setText(currentText);
+               }
+               // 入力カーソル(キャレット)をテキストの末尾へ
+               setCaretPosition(getText().length());
+       }
+       /**
+        * 現在のコード
+        */
+       private Chord currentChord = null;
+       /**
+        * コードを追加します。
+        * @param chord コード
+        */
+       public void appendChord(Chord chord) {
+               if( currentChord == null && chord == null )
+                       return;
+               if( currentChord != null && chord != null && chord.equals(currentChord) )
+                       return;
+               String delimiter = ""; // was "\n"
+               setText( getText() + (chord == null ? delimiter : chord + " ") );
+               currentChord = ( chord == null ? null : chord.clone() );
+       }
+}
\ No newline at end of file
diff --git a/src/camidion/chordhelper/InversionAndOmissionLabel.java b/src/camidion/chordhelper/InversionAndOmissionLabel.java
new file mode 100644 (file)
index 0000000..651f9e3
--- /dev/null
@@ -0,0 +1,95 @@
+package camidion.chordhelper;
+
+import java.awt.Component;
+import java.awt.event.MouseEvent;
+import java.awt.event.MouseListener;
+
+import javax.swing.ButtonGroup;
+import javax.swing.JCheckBoxMenuItem;
+import javax.swing.JLabel;
+import javax.swing.JPopupMenu;
+import javax.swing.JRadioButtonMenuItem;
+import javax.swing.event.PopupMenuEvent;
+import javax.swing.event.PopupMenuListener;
+
+/**
+ * 転回・省略音メニューボタン
+ */
+class InversionAndOmissionLabel extends JLabel
+       implements MouseListener, PopupMenuListener
+{
+       JPopupMenu popup_menu;
+       ButtonGroup omission_group = new ButtonGroup();
+       ButtonIcon icon = new ButtonIcon(ButtonIcon.INVERSION_ICON);
+       JRadioButtonMenuItem radioButtonitems[] = new JRadioButtonMenuItem[4];
+       JCheckBoxMenuItem cb_inversion;
+
+       public InversionAndOmissionLabel() {
+               setIcon(icon);
+               popup_menu = new JPopupMenu();
+               popup_menu.add(
+                       cb_inversion = new JCheckBoxMenuItem("Auto Inversion",true)
+               );
+               popup_menu.addSeparator();
+               omission_group.add(
+                       radioButtonitems[0] = new JRadioButtonMenuItem("All notes",true)
+               );
+               popup_menu.add(radioButtonitems[0]);
+               omission_group.add(
+                       radioButtonitems[1] = new JRadioButtonMenuItem("Omit 5th")
+               );
+               popup_menu.add(radioButtonitems[1]);
+               omission_group.add(
+                       radioButtonitems[2] = new JRadioButtonMenuItem("Omit 3rd (Power Chord)")
+               );
+               popup_menu.add(radioButtonitems[2]);
+               omission_group.add(
+                       radioButtonitems[3] = new JRadioButtonMenuItem("Omit root")
+               );
+               popup_menu.add(radioButtonitems[3]);
+               addMouseListener(this);
+               popup_menu.addPopupMenuListener(this);
+               setToolTipText("Automatic inversion and Note omission - 自動転回と省略音の設定");
+       }
+       public void mousePressed(MouseEvent e) {
+               Component c = e.getComponent();
+               if( c == this ) popup_menu.show( c, 0, getHeight() );
+       }
+       public void mouseReleased(MouseEvent e) { }
+       public void mouseEntered(MouseEvent e) { }
+       public void mouseExited(MouseEvent e) { }
+       public void mouseClicked(MouseEvent e) { }
+       public void popupMenuCanceled(PopupMenuEvent e) { }
+       public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
+               repaint(); // To repaint icon image
+       }
+       public void popupMenuWillBecomeVisible(PopupMenuEvent e) { }
+       public boolean isAutoInversionMode() {
+               return cb_inversion.isSelected();
+       }
+       public void setAutoInversion(boolean is_auto) {
+               cb_inversion.setSelected(is_auto);
+       }
+       public int getOmissionNoteIndex() {
+               if( radioButtonitems[3].isSelected() ) { // Root
+                       return 0;
+               }
+               else if( radioButtonitems[2].isSelected() ) { // 3rd
+                       return 1;
+               }
+               else if( radioButtonitems[1].isSelected() ) { // 5th
+                       return 2;
+               }
+               else { // No omission
+                       return -1;
+               }
+       }
+       public void setOmissionNoteIndex(int index) {
+               switch(index) {
+               case 0: radioButtonitems[3].setSelected(true); break;
+               case 1: radioButtonitems[2].setSelected(true); break;
+               case 2: radioButtonitems[1].setSelected(true); break;
+               default: radioButtonitems[0].setSelected(true); break;
+               }
+       }
+}
\ No newline at end of file
diff --git a/src/camidion/chordhelper/MidiChordHelper.java b/src/camidion/chordhelper/MidiChordHelper.java
new file mode 100644 (file)
index 0000000..e181fb6
--- /dev/null
@@ -0,0 +1,181 @@
+package camidion.chordhelper;
+
+import java.applet.Applet;
+import java.applet.AppletContext;
+import java.applet.AppletStub;
+import java.applet.AudioClip;
+import java.awt.BorderLayout;
+import java.awt.Font;
+import java.awt.Frame;
+import java.awt.Image;
+import java.awt.Toolkit;
+import java.awt.event.WindowAdapter;
+import java.awt.event.WindowEvent;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.util.Enumeration;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Vector;
+
+import javax.swing.JFrame;
+import javax.swing.JLabel;
+import javax.swing.SwingUtilities;
+import javax.swing.WindowConstants;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+import javax.swing.event.TableModelEvent;
+import javax.swing.event.TableModelListener;
+
+import camidion.chordhelper.mididevice.MidiSequencerModel;
+import camidion.chordhelper.midieditor.PlaylistTableModel;
+import camidion.chordhelper.midieditor.SequenceTrackListTableModel;
+
+/**
+ * MIDI Chord Helper を Java アプリとして起動します。
+ */
+public class MidiChordHelper {
+       private static int count = 0;
+       private static AppletFrame frame = null;
+       private static List<File> fileList = new Vector<File>();
+       /**
+        * MIDI Chord Helper を Java アプリとして起動します。
+        * @param args コマンドライン引数
+        * @throws Exception 何らかの異常が発生した場合にスローされる
+        */
+       public static void main(String[] args) throws Exception {
+               if( args.length > 0 ) {
+                       for( String arg : args ) fileList.add(new File(arg));
+               }
+               SwingUtilities.invokeLater(new Runnable(){
+                       @Override
+                       public void run() {
+                               ChordHelperApplet applet;
+                               if( count++ > 0 && frame != null) {
+                                       applet = frame.applet;
+                                       int windowState = frame.getExtendedState();
+                                       if( ( windowState & Frame.ICONIFIED ) == 0 ) {
+                                               frame.toFront();
+                                       } else {
+                                               frame.setExtendedState(windowState &= ~(Frame.ICONIFIED));
+                                       }
+                               } else {
+                                       frame = new AppletFrame(applet = new ChordHelperApplet());
+                               }
+                               applet.deviceModelList.editorDialog.loadAndPlay(fileList);
+                       }
+               });
+       }
+       private static class AppletFrame extends JFrame
+               implements AppletStub, AppletContext
+       {
+               JLabel status_;
+               ChordHelperApplet applet;
+               public AppletFrame(ChordHelperApplet applet) {
+                       setTitle(ChordHelperApplet.VersionInfo.NAME);
+                       (status_ = new JLabel()).setFont(
+                               status_.getFont().deriveFont(Font.PLAIN)
+                       );
+                       add( this.applet = applet, BorderLayout.CENTER );
+                       add( status_, BorderLayout.SOUTH );
+                       applet.setStub(this);
+                       applet.init();
+                       Image iconImage = applet.imageIcon == null ? null : applet.imageIcon.getImage();
+                       setIconImage(iconImage);
+                       setDefaultCloseOperation( WindowConstants.DO_NOTHING_ON_CLOSE );
+                       addWindowListener(new WindowAdapter() {
+                               @Override
+                               public void windowClosing(WindowEvent evt) {
+                                       if( AppletFrame.this.applet.isConfirmedToExit() )
+                                               System.exit(0);
+                               }
+                       });
+                       new TitleUpdater(applet);
+                       pack();
+                       setLocationRelativeTo(null);
+                       setVisible(true);
+                       applet.start();
+               }
+               /**
+                * タイトルバー更新器
+                */
+               private class TitleUpdater implements ChangeListener, TableModelListener {
+                       MidiSequencerModel sequencerModel;
+                       /**
+                        * タイトルバー更新器の構築
+                        * @param applet 対象アプレット
+                        */
+                       public TitleUpdater(ChordHelperApplet applet) {
+                               applet.deviceModelList.editorDialog.sequenceListTable.getModel().addTableModelListener(this);
+                               sequencerModel = applet.deviceModelList.getSequencerModel();
+                               sequencerModel.addChangeListener(this);
+                       }
+                       /**
+                        * プレイリスト上で変更されたファイル名をタイトルバーに反映します。
+                        */
+                       @Override
+                       public void tableChanged(TableModelEvent e) {
+                               int col = e.getColumn();
+                               if( col == PlaylistTableModel.Column.FILENAME.ordinal() ) {
+                                       setFilenameToTitle();
+                               }
+                               if( col == TableModelEvent.ALL_COLUMNS ) {
+                                       setFilenameToTitle();
+                               }
+                       }
+                       /**
+                        * 再生中にファイルが切り替わったら、そのファイル名をタイトルバーに反映します。
+                        */
+                       @Override
+                       public void stateChanged(ChangeEvent e) { setFilenameToTitle(); }
+                       /**
+                        * シーケンサーにロードされている曲のファイル名をタイトルバーに反映します。
+                        */
+                       private void setFilenameToTitle() {
+                               SequenceTrackListTableModel seq = sequencerModel.getSequenceTrackListTableModel();
+                               String filename = ( seq == null ? null : seq.getFilename() );
+                               String title = ChordHelperApplet.VersionInfo.NAME;
+                               if( filename != null && ! filename.isEmpty() ) {
+                                       title = filename + " - " + title;
+                               }
+                               setTitle(title);
+                       }
+               }
+               @Override
+               public boolean isActive() { return true; }
+               @Override
+               public URL getDocumentBase() { return null; }
+               @Override
+               public URL getCodeBase() { return null; }
+               @Override
+               public String getParameter(String name) { return null; }
+               @Override
+               public AppletContext getAppletContext() { return this; }
+               @Override
+               public void appletResize(int width, int height) {}
+               @Override
+               public AudioClip getAudioClip(URL url) { return null; }
+               @Override
+               public Image getImage(URL url) {
+                       return Toolkit.getDefaultToolkit().getImage(url);
+               }
+               @Override
+               public Applet getApplet(String name) { return null; }
+               @Override
+               public Enumeration<Applet> getApplets() { return (null); }
+               @Override
+               public void showDocument(URL url) {}
+               @Override
+               public void showDocument(URL url, String target) {}
+               @Override
+               public void showStatus(String status) { status_.setText(status); }
+               @Override
+               public InputStream getStream(String key) { return null; }
+               @Override
+               public Iterator<String> getStreamKeys() { return null; }
+               @Override
+               public void setStream(String key, InputStream stream) throws IOException {}
+       }
+}
diff --git a/src/camidion/chordhelper/anogakki/AnoGakkiPane.java b/src/camidion/chordhelper/anogakki/AnoGakkiPane.java
new file mode 100644 (file)
index 0000000..ec1ed4f
--- /dev/null
@@ -0,0 +1,274 @@
+package camidion.chordhelper.anogakki;
+
+import java.awt.BasicStroke;
+import java.awt.Color;
+import java.awt.Component;
+import java.awt.Graphics;
+import java.awt.Graphics2D;
+import java.awt.Point;
+import java.awt.Rectangle;
+import java.awt.Stroke;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.geom.AffineTransform;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+
+import javax.swing.JComponent;
+import javax.swing.JLayeredPane;
+import javax.swing.SwingUtilities;
+import javax.swing.Timer;
+
+/**
+ * Innocence「あの楽器」風の表示を行う拡張クラスです。
+ *
+ * <p>{@link #start()} メソッドで表示を開始でき、
+ * 時間が経過すると表示が自然に消えるようになっています。
+ * </p>
+ * <p>画面を「あの楽器」化するには、その画面とともに
+ * このクラスのインスタンスを {@link JLayeredPane} で重ね合わせます。
+ * </p>
+ */
+public class AnoGakkiPane extends JComponent {
+       /**
+        * 1ステップあたりの時間間隔(ミリ秒)
+        */
+       private static final int INTERVAL_MS = 15;
+       /**
+        * 表示終了までのステップ数
+        */
+       private static final int INITIAL_COUNT = 20;
+       /**
+        * 角速度ωの(絶対値の)最大
+        */
+       private static final double MAX_OMEGA = 0.005;
+       /**
+        * 図形の種類
+        */
+       private static enum Shape {
+               /** ○ */
+               CIRCLE {
+                       public void draw(Graphics2D g2, QueueEntry entry) {
+                               entry.drawCircle(g2);
+                       }
+               },
+               /** = */
+               LINES {
+                       public void draw(Graphics2D g2, QueueEntry entry) {
+                               entry.drawLines(g2);
+                       }
+               },
+               /** □ */
+               SQUARE {
+                       public void draw(Graphics2D g2, QueueEntry entry) {
+                               entry.drawSquare(g2);
+                       }
+               },
+               /** △ */
+               TRIANGLE {
+                       public void draw(Graphics2D g2, QueueEntry entry) {
+                               entry.drawTriangle(g2);
+                       }
+               };
+               /**
+                * 図形の種類の値をランダムに返します。
+                * @return ランダムな値
+                */
+               public static Shape randomShape() {
+                       return values()[(int)(Math.random() * values().length)];
+               }
+               /**
+                * この図形を描画します。
+                * @param g2 描画オブジェクト
+                * @param entry キューエントリ
+                */
+               public abstract void draw(Graphics2D g2, QueueEntry entry);
+       }
+       /**
+        * 色(RGBA、Aは「不透明度」を表すアルファ値)
+        */
+       private static final Color color = new Color(0,255,255,192);
+       /**
+        * 線の太さ
+        */
+       private static final Stroke stroke = new BasicStroke((float)5);
+       /**
+        * いま描画すべき図形を覚えておくためのキュー
+        */
+       private List<QueueEntry> queue = new LinkedList<QueueEntry>();
+       /**
+        * キューエントリ内容
+        */
+       private class QueueEntry {
+               /** 時間軸 */
+               private int countdown = INITIAL_COUNT;
+               /** スタートからの経過時間(ミリ秒) */
+               private int tms = 0;
+               //
+               /** 図形の種類 */
+               AnoGakkiPane.Shape shape = Shape.randomShape();
+               /** クリックされた場所(中心) */
+               private Point clickedPoint;
+               //
+               // 回転する場合
+               /** 現在の半径 */
+               private int r = 0;
+               /** 回転速度(時計回り) */
+               private double omega = 0;
+               /** 現在の回転角(アフィン変換) */
+               AffineTransform affineTransform = null;
+               /**
+                * 新しいキューエントリを構築します。
+                * @param clickedPoint クリックされた場所
+                */
+               public QueueEntry(Point clickedPoint) {
+                       this.clickedPoint = clickedPoint;
+                       if( shape != Shape.CIRCLE ) {
+                               // ○以外なら回転角を初期化
+                               //(○は回転しても見かけ上何も変わらなくて無駄なので除外)
+                               affineTransform = AffineTransform.getRotateInstance(
+                                       2 * Math.PI * Math.random(),
+                                       clickedPoint.x,
+                                       clickedPoint.y
+                               );
+                               omega = MAX_OMEGA * (1.0 - 2.0 * Math.random());
+                       }
+               }
+               /**
+                * このキューエントリをカウントダウンします。
+                * @return カウントダウン値(0 でタイムアウト)
+                */
+               public int countDown() {
+                       if( countdown > 0 ) {
+                               // 時間 t を進める
+                               countdown--;
+                               tms += INTERVAL_MS;
+                               // 半径 r = vt
+                               r = tms / 2;
+                               // 回転
+                               if( shape == Shape.SQUARE || shape == Shape.TRIANGLE ) {
+                                       // 角度を θ=ωt で求めると、移動距離 l=rθ が
+                                       // t の2乗のオーダーで伸びるため、加速しているように見えてしまう。
+                                       // 一定の速度に見せるために t を平方根にして角度を計算する。
+                                       affineTransform.rotate(
+                                               omega * Math.sqrt((double)tms),
+                                               clickedPoint.x,
+                                               clickedPoint.y
+                                       );
+                               }
+                       }
+                       return countdown;
+               }
+               /**
+                * ○を描画します。
+                * @param g2 描画オブジェクト
+                */
+               public void drawCircle(Graphics2D g2) {
+                       int d = 2 * r;
+                       g2.drawOval( clickedPoint.x-r, clickedPoint.y-r, d, d );
+               }
+               /**
+                * =を描画します。
+                * @param g2 描画オブジェクト
+                */
+               public void drawLines(Graphics2D g2) {
+                       int width2 = 2 * getSize().width;
+                       int y = clickedPoint.y;
+                       g2.transform(affineTransform);
+                       g2.drawLine( -width2, y-r, width2, y-r );
+                       g2.drawLine( -width2, y+r, width2, y+r );
+               }
+               /**
+                * □を描画します。
+                * @param g2 描画オブジェクト
+                */
+               public void drawSquare(Graphics2D g2) {
+                       int d = 2 * r;
+                       g2.transform(affineTransform);
+                       g2.drawRect( clickedPoint.x-r, clickedPoint.y-r, d, d );
+               }
+               /**
+                * △を描画します。
+                * @param g2 描画オブジェクト
+                */
+               public void drawTriangle(Graphics2D g2) {
+                       int x = clickedPoint.x;
+                       int y = clickedPoint.y;
+                       g2.transform(affineTransform);
+                       g2.drawLine( x-r, y, x+r, y-r );
+                       g2.drawLine( x-r, y, x+r, y+r );
+                       g2.drawLine( x+r, y-r, x+r, y+r );
+               }
+       }
+       /**
+        * キューを更新するアニメーション用タイマー
+        */
+       public AnoGakkiPane() {
+               setOpaque(false);
+               timer = new Timer(
+                       INTERVAL_MS,
+                       new ActionListener() {
+                               @Override
+                               public void actionPerformed(ActionEvent event) {
+                                       synchronized(queue) {
+                                               Iterator<QueueEntry> i = queue.iterator();
+                                               while( i.hasNext() )
+                                                       if( i.next().countDown() <= 0 )i.remove();
+                                       }
+                                       if(queue.isEmpty()) timer.stop();
+                                       repaint();
+                               }
+                       }
+               ) {
+                       {
+                               setCoalesce(true);
+                               setRepeats(true);
+                       }
+               };
+       }
+       private Timer timer;
+       @Override
+       public void paint(Graphics g) {
+               if(queue.isEmpty()) return;
+               Graphics2D g2 = (Graphics2D)g;
+               g2.setStroke(stroke);
+               g2.setColor(color);
+               synchronized(queue) {
+                       Iterator<QueueEntry> i = queue.iterator();
+                       while( i.hasNext() ) {
+                               QueueEntry entry = i.next();
+                               entry.shape.draw(g2, entry);
+                       }
+               }
+       }
+       /**
+        * 指定された長方形領域({@link Rectangle})の中央から図形の表示を開始します。
+        * @param source AWTコンポーネント
+        * @param rect AWTコンポーネント内の座標系を基準とした長方形領域
+        */
+       public void start(Component source, Rectangle rect) {
+               Point point = rect.getLocation();
+               point.translate( rect.width/2, rect.height/2 );
+               start(source, point);
+       }
+       private long prevStartedAt = System.nanoTime();
+       /**
+        * 指定された場所から図形の表示を開始します。
+        * @param source AWTコンポーネント
+        * @param point AWTコンポーネント内の座標系を基準とした場所
+        */
+       public void start(Component source, Point point) {
+               long startedAt = System.nanoTime();
+               if( startedAt - prevStartedAt < (INTERVAL_MS * 1000)*50 ) {
+                       // 頻繁すぎる場合は無視する
+                       return;
+               }
+               point = SwingUtilities.convertPoint(source, point, this);
+               synchronized (queue) {
+                       queue.add(new QueueEntry(point));
+               }
+               timer.start();
+               prevStartedAt = startedAt;
+       }
+}
\ No newline at end of file
diff --git a/src/camidion/chordhelper/chorddiagram/CapoSelecterView.java b/src/camidion/chordhelper/chorddiagram/CapoSelecterView.java
new file mode 100644 (file)
index 0000000..040de9e
--- /dev/null
@@ -0,0 +1,61 @@
+package camidion.chordhelper.chorddiagram;
+
+import java.awt.event.ItemEvent;
+import java.awt.event.ItemListener;
+
+import javax.swing.BoxLayout;
+import javax.swing.ComboBoxModel;
+import javax.swing.JCheckBox;
+import javax.swing.JComboBox;
+import javax.swing.JPanel;
+
+/**
+ * カポ選択ビュー
+ */
+public class CapoSelecterView extends JPanel implements ItemListener {
+       /**
+        * カポON/OFFチェックボックス
+        */
+       public JCheckBox checkbox = new JCheckBox("Capo") {
+               {
+                       setOpaque(false);
+               }
+       };
+       /**
+        * カポ位置選択コンボボックス
+        */
+       public JComboBox<Integer> valueSelecter = new JComboBox<Integer>() {
+               {
+                       setMaximumRowCount(12);
+                       setVisible(false);
+               }
+       };
+       /**
+        * カポ選択ビューを構築します。
+        */
+       public CapoSelecterView() {
+               checkbox.addItemListener(this);
+               setLayout(new BoxLayout(this, BoxLayout.X_AXIS));
+               add(checkbox);
+               add(valueSelecter);
+       }
+       /**
+        * 指定されたデータモデルを操作するカポ選択ビューを構築します。
+        * @param model データモデル
+        */
+       public CapoSelecterView(ComboBoxModel<Integer> model) {
+               this();
+               valueSelecter.setModel(model);
+       }
+       @Override
+       public void itemStateChanged(ItemEvent e) {
+               valueSelecter.setVisible(checkbox.isSelected());
+       }
+       /**
+        * カポ位置を返します。
+        * @return カポ位置
+        */
+       public int getCapo() {
+               return checkbox.isSelected() ? valueSelecter.getSelectedIndex()+1 : 0;
+       }
+}
\ No newline at end of file
diff --git a/src/camidion/chordhelper/chorddiagram/ChordDiagram.java b/src/camidion/chordhelper/chorddiagram/ChordDiagram.java
new file mode 100644 (file)
index 0000000..9b39fa0
--- /dev/null
@@ -0,0 +1,247 @@
+package camidion.chordhelper.chorddiagram;
+import java.awt.Color;
+import java.awt.Dimension;
+import java.awt.Insets;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.AdjustmentEvent;
+import java.awt.event.AdjustmentListener;
+import java.awt.event.ItemEvent;
+import java.awt.event.ItemListener;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.EnumMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+import javax.swing.Box;
+import javax.swing.BoxLayout;
+import javax.swing.ButtonGroup;
+import javax.swing.JPanel;
+import javax.swing.JRadioButton;
+import javax.swing.JScrollBar;
+import javax.swing.JToggleButton;
+import javax.swing.SwingConstants;
+
+import camidion.chordhelper.ButtonIcon;
+import camidion.chordhelper.ChordDisplayLabel;
+import camidion.chordhelper.ChordHelperApplet;
+import camidion.chordhelper.music.Chord;
+
+/**
+ * ChordDiagram class for MIDI Chord Helper
+ *
+ * @auther
+ *     Copyright (C) 2004-2014 Akiyoshi Kamide
+ *     http://www.yk.rim.or.jp/~kamide/music/chordhelper/
+ */
+public class ChordDiagram extends JPanel {
+       /**
+        * コードダイヤグラムの対象楽器
+        */
+       public static enum Instrument {
+               /** ウクレレ  */
+               Ukulele(Arrays.asList(9,4,0,7)), // AECG
+               /** ギター */
+               Guitar(Arrays.asList(4,11,7,2,9,4)); // EBGDAE
+               private Instrument(List<Integer> defaultOpenNotes) {
+                       this.defaultOpenNotes = Collections.unmodifiableList(defaultOpenNotes);
+               }
+               /**
+                * デフォルトの開放弦の音階を表すノート番号リストを返します。
+                * このリストは書き換えできません。
+                *
+                * @return 開放弦の音階(固定値)を表すノート番号リスト
+                */
+               public List<Integer> getDefaultOpenNotes() { return defaultOpenNotes; }
+               private List<Integer> defaultOpenNotes;
+               /**
+                * 開放弦の音階を表す、書き換え(チューニング)可能な
+                * ノート番号の配列を生成します。
+                *
+                * @return 開放弦の音階(デフォルト値)を表すノート番号の配列
+                */
+               public int[] createTunableOpenNotes() {
+                       int[] r = new int[defaultOpenNotes.size()];
+                       int i=0;
+                       for(int note : defaultOpenNotes) r[i++] = note;
+                       return r;
+               }
+       }
+       /**
+        * コードダイアグラムを構築します。
+        * @param applet 親となるアプレット
+        */
+       public ChordDiagram(ChordHelperApplet applet) {
+               capoSelecterView.valueSelecter.setModel(applet.chordMatrix.capoValueModel);
+               setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
+               add(new JPanel() {
+                       {
+                               setLayout(new BoxLayout(this, BoxLayout.X_AXIS));
+                               setOpaque(false);
+                               add(Box.createHorizontalStrut(2));
+                               add(recordTextButton);
+                               add(Box.createHorizontalStrut(2));
+                               add(capoSelecterView);
+                       }
+               });
+               add(Box.createHorizontalStrut(5));
+               add(new JPanel() {
+                       {
+                               setLayout(new BoxLayout(this, BoxLayout.X_AXIS));
+                               setOpaque(false);
+                               add(new JPanel() {
+                                       {
+                                               setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
+                                               setOpaque(false);
+                                               add(new JPanel() {
+                                                       {
+                                                               add(titleLabel);
+                                                               setOpaque(false);
+                                                               setAlignmentY((float)0);
+                                                       }
+                                               });
+                                               add(diagramDisplay);
+                                               fretRangeScrollbar.setAlignmentY((float)1.0);
+                                               add(fretRangeScrollbar);
+                                               add(new JPanel() {
+                                                       {
+                                                               setOpaque(false);
+                                                               for(JRadioButton rb : instButtons.values())
+                                                                       add(rb);
+                                                               setAlignmentY((float)1.0);
+                                                       }
+                                               });
+                                       }
+                               });
+                               add(variationScrollbar);
+                       }
+               });
+       }
+       /**
+        * コードをテキストに記録するボタン
+        */
+       public JToggleButton recordTextButton =
+               new JToggleButton("REC", new ButtonIcon(ButtonIcon.REC_ICON)) {
+                       {
+                               setMargin(new Insets(0,0,0,0));
+                               setToolTipText("Record to text ON/OFF");
+                       }
+               };
+       /**
+        * コードダイアグラムのタイトルラベル
+        */
+       public ChordDisplayLabel titleLabel =
+               new ChordDisplayLabel("Chord Diagram",null,null) {
+                       {
+                               setHorizontalAlignment(SwingConstants.CENTER);
+                               setVerticalAlignment(SwingConstants.BOTTOM);
+                       }
+               };
+       /**
+        * コードダイアグラム表示部
+        */
+       private ChordDiagramDisplay diagramDisplay =
+               new ChordDiagramDisplay(Instrument.Ukulele) {
+                       {
+                               setOpaque(false);
+                               setPreferredSize(new Dimension(120,120));
+                       }
+               };
+       /**
+        * 対象楽器選択ボタンのマップ
+        */
+       private Map<Instrument,JRadioButton> instButtons =
+               new EnumMap<Instrument,JRadioButton>(Instrument.class) {
+                       {
+                               ButtonGroup buttonGroup = new ButtonGroup();
+                               for( final Instrument instrument : Instrument.values() ) {
+                                       String label = instrument.toString();
+                                       JRadioButton radioButton = new JRadioButton(label) {
+                                               {
+                                                       setOpaque(false);
+                                                       addActionListener(new ActionListener() {
+                                                               @Override
+                                                               public void actionPerformed(ActionEvent e) {
+                                                                       diagramDisplay.tune(instrument);
+                                                               }
+                                                       });
+                                               }
+                                       };
+                                       buttonGroup.add(radioButton);
+                                       put(instrument, radioButton);
+                               }
+                               get(Instrument.Ukulele).setSelected(true);
+                       }
+               };
+
+       private JScrollBar variationScrollbar = new JScrollBar(JScrollBar.VERTICAL) {
+               {
+                       setModel(diagramDisplay.chordVariations.indexModel);
+                       addAdjustmentListener(
+                               new AdjustmentListener() {
+                                       @Override
+                                       public void adjustmentValueChanged(AdjustmentEvent e) {
+                                               setToolTipText(
+                                                       diagramDisplay.chordVariations.getIndexDescription()
+                                               );
+                                       }
+                               }
+                       );
+               }
+       };
+       private JScrollBar fretRangeScrollbar = new JScrollBar(JScrollBar.HORIZONTAL) {
+               {
+                       setModel(diagramDisplay.fretViewIndexModel);
+                       setBlockIncrement(diagramDisplay.fretViewIndexModel.getExtent());
+               }
+       };
+       /**
+        * カポ位置選択コンボボックス
+        */
+       public CapoSelecterView capoSelecterView = new CapoSelecterView() {
+               {
+                       checkbox.addItemListener(new ItemListener() {
+                               @Override
+                               public void itemStateChanged(ItemEvent e) { clear(); }
+                       });
+               }
+       };
+       @Override
+       public void setBackground(Color bgColor) {
+               super.setBackground(bgColor);
+               if( diagramDisplay == null ) return;
+               diagramDisplay.setBackground(bgColor);
+               capoSelecterView.setBackground(bgColor);
+               capoSelecterView.valueSelecter.setBackground(bgColor);
+               variationScrollbar.setBackground(bgColor);
+               fretRangeScrollbar.setBackground(bgColor);
+       }
+       /**
+        * コード(和音)をクリアします。
+        *
+        * <p>{@link #setChord(Chord)} の引数に null を指定して呼び出しているだけです。
+        * </p>
+        */
+       public void clear() { setChord(null); }
+       /**
+        * コード(和音)を設定します。
+        * 表示中でない場合、指定のコードに関係なく null が設定されます。
+        *
+        * @param chord コード(クリアする場合は null)
+        */
+       public void setChord(Chord chord) {
+               if( ! isVisible() ) chord = null;
+               titleLabel.setChord(chord);
+               diagramDisplay.setChord(chord);
+       }
+       /**
+        * 対象楽器を切り替えます。
+        * @param instrument 対象楽器
+        * @throws NullPointerException 対象楽器がnullの場合
+        */
+       public void setTargetInstrument(Instrument instrument) {
+               instButtons.get(Objects.requireNonNull(instrument)).doClick();
+       }
+}
diff --git a/src/camidion/chordhelper/chorddiagram/ChordDiagramDisplay.java b/src/camidion/chordhelper/chorddiagram/ChordDiagramDisplay.java
new file mode 100644 (file)
index 0000000..b02d58e
--- /dev/null
@@ -0,0 +1,647 @@
+package camidion.chordhelper.chorddiagram;
+
+import java.awt.Color;
+import java.awt.Dimension;
+import java.awt.FontMetrics;
+import java.awt.Graphics;
+import java.awt.Graphics2D;
+import java.awt.Point;
+import java.awt.Rectangle;
+import java.awt.event.ComponentAdapter;
+import java.awt.event.ComponentEvent;
+import java.awt.event.MouseEvent;
+import java.awt.event.MouseListener;
+import java.awt.event.MouseMotionListener;
+import java.util.LinkedList;
+
+import javax.swing.DefaultBoundedRangeModel;
+import javax.swing.JComponent;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+
+import camidion.chordhelper.chorddiagram.ChordDiagram.Instrument;
+import camidion.chordhelper.music.Chord;
+import camidion.chordhelper.music.Music;
+import camidion.chordhelper.music.NoteSymbol;
+
+/**
+ * コードダイアグラム表示部
+ */
+class ChordDiagramDisplay extends JComponent
+       implements MouseListener, MouseMotionListener {
+       /**
+        * 可視フレット数
+        */
+       public static final int VISIBLE_FRETS = 4;
+       /**
+        * 最大フレット数
+        */
+       public static final int MAX_FRETS = 16;
+       /**
+        * 弦の最大本数
+        */
+       public static final int MAX_STRINGS = 6;
+       /**
+        * 左マージン
+        */
+       public static final int LEFT_MARGIN_WIDTH = 10;
+       /**
+        * 右マージン
+        */
+       public static final int RIGHT_MARGIN_WIDTH = 10;
+       /**
+        * 上マージン
+        */
+       public static final int UPPER_MARGIN_WIDTH = 5;
+       /**
+        * 下マージン
+        */
+       public static final int LOWER_MARGIN_WIDTH = 5;
+       /**
+        * フレット方向の横スクロールバーで使用する境界つき値範囲
+        */
+       DefaultBoundedRangeModel fretViewIndexModel
+               = new DefaultBoundedRangeModel( 0, VISIBLE_FRETS, 0, MAX_FRETS );
+       /**
+        * チューニング対象楽器
+        */
+       private Instrument targetInstrument;
+       /**
+        * 開放弦チューニング音階(ずらしあり)
+        */
+       private int[] notesWhenOpen;
+       /**
+        * コードの押さえ方のバリエーション
+        */
+       ChordVariations chordVariations = new ChordVariations();
+       /**
+        * チューニングボタンの配列
+        */
+       private TuningButton tuningButtons[];
+       /**
+        * チューニングボタン
+        */
+       private class TuningButton extends Rectangle {
+               boolean isMouseEntered = false;
+               int stringIndex;
+               public TuningButton(int stringIndex) {
+                       super(
+                               LEFT_MARGIN_WIDTH,
+                               UPPER_MARGIN_WIDTH + stringIndex * stringDistance,
+                               CHAR_WIDTH,
+                               stringDistance
+                       );
+                       this.stringIndex = stringIndex;
+               }
+       }
+       /**
+        * 押さえる場所
+        */
+       private class PressingPoint {
+               /**
+                * 弦インデックス(0始まり)
+                */
+               int stringIndex;
+               /**
+                * フレットインデックス(0が開放弦、-1が弾かない弦)
+                */
+               int fretIndex;
+               /**
+                * コード構成音インデックス(0でルート音)
+                */
+               int chordNoteIndex;
+               /**
+                * 押さえる場所を示す矩形領域
+                */
+               Rectangle rect = null;
+               /**
+                * マウスカーソルが入ったらtrue
+                */
+               boolean isMouseEntered = false;
+               /**
+                * 指定した弦を弾かないことを表す {@link PressingPoint} を構築します。
+                * @param stringIndex 弦インデックス
+                */
+               public PressingPoint(int stringIndex) {
+                       this(-1,-1,stringIndex);
+               }
+               /**
+                * 指定した弦、フレットを押さえると
+                * 指定されたコード構成音が鳴ることを表す {@link PressingPoint} を構築します。
+                * @param fretIndex フレットインデックス
+                * @param chordNoteIndex コード構成音インデックス
+                * @param stringIndex 弦インデックス
+                */
+               public PressingPoint(int fretIndex, int chordNoteIndex, int stringIndex) {
+                       rect = new Rectangle(
+                               gridRect.x + (
+                                       fretIndex<1 ?
+                                               -(pointSize + 3) :
+                                               (fretIndex * fretDistance - pointSize/2 - fretDistance/2)
+                               ),
+                               gridRect.y - pointSize/2 + stringIndex * stringDistance,
+                               pointSize,
+                               pointSize
+                       );
+                       this.fretIndex = fretIndex;
+                       this.chordNoteIndex = chordNoteIndex;
+                       this.stringIndex = stringIndex;
+               }
+       }
+       /**
+        * 押さえる場所リスト(配列要素として使えるようにするための空の継承クラス)
+        */
+       private class PressingPointList extends LinkedList<PressingPoint> {
+       }
+       /**
+        * コードの押さえ方のバリエーション
+        */
+       class ChordVariations extends LinkedList<PressingPoint[]> {
+               /**
+                * 対象コード
+                */
+               public Chord chord = null;
+               /**
+                * 省略しないコード構成音が全部揃ったかをビットで確認するための値
+                */
+               private int checkBitsAllOn = 0;
+               /**
+                * 五度の構成音を省略できるコードのときtrue
+                */
+               private boolean fifthOmittable = false;
+               /**
+                * ルート音を省略できるコードのときtrue
+                */
+               private boolean rootOmittable = false;
+               /**
+                * バリエーションインデックスの範囲を表すモデル
+                */
+               public DefaultBoundedRangeModel indexModel = new DefaultBoundedRangeModel(0,0,0,0);
+               /**
+                * コード(和音)を設定します。
+                *
+                * 設定すると、そのコードの押さえ方がくまなく探索され、
+                * バリエーションが再構築されます。
+                *
+                * @param chord コード
+                */
+               public void setChord(Chord chord) {
+                       clear();
+                       if( (this.chord = chord) == null ) {
+                               possiblePressingPoints = null;
+                               indexModel.setRangeProperties(0,0,0,0,false);
+                               return;
+                       }
+                       int chordNoteCount = chord.numberOfNotes();
+                       rootOmittable = ( chordNoteCount == 5 );
+                       fifthOmittable = ( chord.symbolSuffix().equals("7") || rootOmittable );
+                       checkBitsAllOn = (1 << chordNoteCount) - 1;
+                       possiblePressingPoints = new PressingPointList[notesWhenOpen.length];
+                       for( int stringIndex=0; stringIndex<possiblePressingPoints.length; stringIndex++ ) {
+                               possiblePressingPoints[stringIndex] = new PressingPointList();
+                               for(
+                                       int fretIndex=0;
+                                       fretIndex <= fretViewIndexModel.getValue() + fretViewIndexModel.getExtent();
+                                       fretIndex++
+                               ) {
+                                       if( fretIndex == 0 || fretIndex > fretViewIndexModel.getValue() ) {
+                                               int chordNoteIndex = chord.indexOf(
+                                                       notesWhenOpen[stringIndex]+fretIndex
+                                               );
+                                               if( chordNoteIndex >= 0 ) {
+                                                       possiblePressingPoints[stringIndex].add(
+                                                               new PressingPoint(fretIndex,chordNoteIndex,stringIndex)
+                                                       );
+                                               }
+                                       }
+                               }
+                               // 'x'-marking string
+                               possiblePressingPoints[stringIndex].add(new PressingPoint(stringIndex));
+                       }
+                       validatingPoints = new PressingPoint[notesWhenOpen.length];
+                       scanFret(0);
+                       indexModel.setRangeProperties(-1,1,-1,size(),false);
+               }
+               /**
+                * 押さえる可能性のある場所のリスト
+                */
+               private PressingPointList possiblePressingPoints[] = null;
+               /**
+                * 押さえる可能性のある場所のリストを返します。
+                * @return 押さえる可能性のある場所のリスト
+                */
+               public PressingPointList[] getPossiblePressingPoints() {
+                       return possiblePressingPoints;
+               }
+               /**
+                * 検証対象の押さえ方
+                */
+               private PressingPoint validatingPoints[] = null;
+               /**
+                * 引数で指定された弦のフレットをスキャンします。
+                *
+                * <p>指定された弦について、
+                * コード構成音のどれか一つを鳴らすことのできる
+                * フレット位置を順にスキャンし、
+                * 新しい押さえ方にフレット位置を記録したうえで、
+                * 次の弦について再帰呼び出しを行います。
+                * </p>
+                * <p>最後の弦まで達すると(再起呼び出しツリーの葉)、
+                * その押さえ方でコード構成音が十分に揃うかどうか検証されます。
+                * 検証結果がOKだった場合、押さえ方がバリエーションリストに追加されます。
+                * </p>
+                *
+                * @param stringIndex 弦インデックス
+                */
+               private void scanFret(int stringIndex) {
+                       int endOfStringIndex = validatingPoints.length - 1;
+                       for( PressingPoint pp : possiblePressingPoints[stringIndex] ) {
+                               validatingPoints[stringIndex] = pp;
+                               if( stringIndex < endOfStringIndex ) {
+                                       scanFret( stringIndex + 1 );
+                                       continue;
+                               }
+                               if( hasValidNewVariation() ) {
+                                       add(validatingPoints.clone());
+                               }
+                       }
+               }
+               /**
+                * 新しい押さえ方のバリエーションが、
+                * そのコードを鳴らすのに十分であるか検証します。
+                *
+                * @return 省略しないコード構成音が全部揃っていたらtrue、
+                * 一つでも欠けていたらfalse
+                */
+               private boolean hasValidNewVariation() {
+                       int checkBits = 0;
+                       int iocn;
+                       for( PressingPoint pp : validatingPoints )
+                               if( (iocn = pp.chordNoteIndex) >= 0 ) checkBits |= 1 << iocn;
+                       return ( checkBits == checkBitsAllOn
+                               || checkBits == checkBitsAllOn -4 && fifthOmittable
+                               || (checkBits & (checkBitsAllOn -1-4)) == (checkBitsAllOn -1-4)
+                               && (checkBits & (1+4)) != 0 && rootOmittable
+                       );
+               }
+               /**
+                * バリエーションインデックスの説明を返します。
+                *
+                * <p>(インデックス値 / バリエーションの個数) のような説明です。
+                * インデックス値が未選択(-1)の場合、
+                * バリエーションの個数のみの説明を返します。
+                * </p>
+                * @return バリエーションインデックスの説明
+                */
+               public String getIndexDescription() {
+                       if( chord == null )
+                               return null;
+                       int val = indexModel.getValue();
+                       int max = indexModel.getMaximum();
+                       if( val < 0 ) { // 未選択時
+                               switch(max) {
+                               case 0: return "No variation found";
+                               case 1: return "1 variation found";
+                               default: return max + " variations found";
+                               }
+                       }
+                       return "Variation: " + (val+1) + " / " + max ;
+               }
+       }
+
+       public ChordDiagramDisplay(ChordDiagram.Instrument inst) {
+               addMouseListener(this);
+               addMouseMotionListener(this);
+               addComponentListener(
+                       new ComponentAdapter() {
+                               @Override
+                               public void componentResized(ComponentEvent e) {
+                                       tune();
+                               }
+                       }
+               );
+               chordVariations.indexModel.addChangeListener(
+                       new ChangeListener() {
+                               @Override
+                               public void stateChanged(ChangeEvent e) {
+                                       repaint();
+                               }
+                       }
+               );
+               fretViewIndexModel.addChangeListener(
+                       new ChangeListener() {
+                               @Override
+                               public void stateChanged(ChangeEvent e) {
+                                       setChord(); // To reconstruct chord variations
+                               }
+                       }
+               );
+               setMinimumSize(new Dimension(100,70));
+               tune(inst);
+       }
+       @Override
+       public void paint(Graphics g) {
+               Graphics2D g2 = (Graphics2D) g;
+               Dimension d = getSize();
+               Color fret_color = Color.gray; // getBackground().darker();
+               FontMetrics fm = g2.getFontMetrics();
+               //
+               // Copy background color
+               g2.setBackground(getBackground());
+               g2.clearRect(0, 0, d.width, d.height);
+               //
+               // Draw frets and its numbers
+               //
+               for( int i=1; i<=VISIBLE_FRETS; i++ ) {
+                       g2.setColor(fret_color);
+                       int fret_x = gridRect.x + (gridRect.width - 2) * i / VISIBLE_FRETS;
+                       g2.drawLine(
+                               fret_x, gridRect.y,
+                               fret_x, gridRect.y + stringDistance * (notesWhenOpen.length - 1)
+                       );
+                       g2.setColor(getForeground());
+                       String s = String.valueOf( i + fretViewIndexModel.getValue() );
+                       g2.drawString(
+                               s,
+                               gridRect.x
+                               + fretDistance/2 - fm.stringWidth(s)/2
+                               + gridRect.width * (i-1) / VISIBLE_FRETS,
+                               gridRect.y
+                               + stringDistance/2 + fm.getHeight()
+                               + stringDistance * (notesWhenOpen.length - 1) - 1
+                       );
+               }
+               //
+               // Draw strings and open notes
+               for( int i=0; i<notesWhenOpen.length; i++ ) {
+                       int string_y = gridRect.y + gridRect.height * i / (MAX_STRINGS - 1);
+                       g2.setColor(fret_color);
+                       g2.drawLine(
+                               gridRect.x,
+                               string_y,
+                               gridRect.x + (gridRect.width - 2),
+                               string_y
+                       );
+                       if( notesWhenOpen[i] != targetInstrument.getDefaultOpenNotes().get(i) ) {
+                               g2.setColor(Color.yellow);
+                               g2.fill(tuningButtons[i]);
+                       }
+                       g2.setColor(getForeground());
+                       g2.drawString(
+                               NoteSymbol.noteNumberToSymbol(notesWhenOpen[i], 2),
+                               LEFT_MARGIN_WIDTH,
+                               string_y + (fm.getHeight() - fm.getDescent())/2
+                       );
+                       g2.setColor(fret_color);
+                       if( tuningButtons[i].isMouseEntered ) {
+                               g2.draw(tuningButtons[i]);
+                       }
+               }
+               //
+               // Draw left-end of frets
+               if( fretViewIndexModel.getValue() == 0 ) {
+                       g2.setColor(getForeground());
+                       g2.fillRect(
+                               gridRect.x - 1,
+                               gridRect.y,
+                               3,
+                               stringDistance * (notesWhenOpen.length - 1) + 1
+                       );
+               }
+               else {
+                       g2.setColor(fret_color);
+                       g2.drawLine(
+                               gridRect.x,
+                               gridRect.y,
+                               gridRect.x,
+                               gridRect.y + stringDistance * (notesWhenOpen.length - 1)
+                       );
+               }
+               //
+               // Draw indicators
+               if( chordVariations.chord == null ) {
+                       return;
+               }
+               PressingPoint variation[] = null;
+               int ppIndex = chordVariations.indexModel.getValue();
+               if( ppIndex >= 0 ) {
+                       variation = chordVariations.get(ppIndex);
+                       for( PressingPoint pp : variation ) drawIndicator(g2, pp, false);
+               }
+               PressingPointList possiblePressingPoints[] = chordVariations.getPossiblePressingPoints();
+               if( possiblePressingPoints != null ) {
+                       for( PressingPointList pps : possiblePressingPoints ) {
+                               for( PressingPoint pp : pps ) {
+                                       if( pp.isMouseEntered ) {
+                                               drawIndicator( g2, pp, false );
+                                               if( variation != null ) {
+                                                       return;
+                                               }
+                                       }
+                                       else if( variation == null ) {
+                                               drawIndicator( g2, pp, true );
+                                       }
+                               }
+                       }
+               }
+       }
+       private void drawIndicator(
+               Graphics2D g2, PressingPoint pp, boolean drawAllPoints
+       ) {
+               Rectangle r;
+               int i_chord = pp.chordNoteIndex;
+               g2.setColor(
+                       i_chord < 0 ? getForeground() : Chord.NOTE_INDEX_COLORS[i_chord]
+               );
+               if( (r = pp.rect) == null ) {
+                       return;
+               }
+               int fretPoint = pp.fretIndex;
+               if( fretPoint < 0 ) {
+                       if( ! drawAllPoints ) {
+                               // Put 'x' mark
+                               g2.drawLine(
+                                       r.x + 1,
+                                       r.y + 1,
+                                       r.x + r.width - 1,
+                                       r.y + r.height - 1
+                               );
+                               g2.drawLine(
+                                       r.x + 1,
+                                       r.y + r.height - 1,
+                                       r.x + r.width - 1,
+                                       r.y + 1
+                               );
+                       }
+               }
+               else if( fretPoint == 0 ) {
+                       // Put 'o' mark
+                       g2.drawOval( r.x, r.y, r.width, r.height );
+               }
+               else { // Fret-pressing
+                       int x = r.x - fretViewIndexModel.getValue() * fretDistance ;
+                       if( drawAllPoints ) {
+                               g2.drawOval( x, r.y, r.width, r.height );
+                       }
+                       else {
+                               g2.fillOval( x, r.y, r.width, r.height );
+                       }
+               }
+       }
+       @Override
+       public void mousePressed(MouseEvent e) {
+               Point point = e.getPoint();
+               PressingPointList possiblePressingPoints[] = chordVariations.getPossiblePressingPoints();
+               if( possiblePressingPoints != null ) {
+                       for( PressingPointList pps : possiblePressingPoints ) {
+                               for( PressingPoint pp : pps ) {
+                                       boolean hit;
+                                       Rectangle rect = pp.rect;
+                                       if( pp.fretIndex > 0 ) {
+                                               int xOffset = -fretViewIndexModel.getValue()*fretDistance;
+                                               rect.translate( xOffset, 0 );
+                                               hit = rect.contains(point);
+                                               rect.translate( -xOffset, 0 );
+                                       }
+                                       else hit = rect.contains(point);
+                                       if( ! hit )
+                                               continue;
+                                       int variationIndex = 0;
+                                       for( PressingPoint[] variation : chordVariations ) {
+                                               if( variation[pp.stringIndex].fretIndex != pp.fretIndex ) {
+                                                       variationIndex++;
+                                                       continue;
+                                               }
+                                               chordVariations.indexModel.setValue(variationIndex);
+                                               return;
+                                       }
+                               }
+                       }
+               }
+               for( TuningButton button : tuningButtons ) {
+                       if( ! button.contains(point) )
+                               continue;
+                       int note = notesWhenOpen[button.stringIndex];
+                       note += (e.getButton()==MouseEvent.BUTTON3 ? 11 : 1);
+                       notesWhenOpen[button.stringIndex] = Music.mod12(note);
+                       setChord();
+                       return;
+               }
+       }
+       @Override
+       public void mouseReleased(MouseEvent e) { }
+       @Override
+       public void mouseEntered(MouseEvent e) {
+               mouseMoved(e);
+       }
+       @Override
+       public void mouseExited(MouseEvent e) {
+               mouseMoved(e);
+       }
+       @Override
+       public void mouseClicked(MouseEvent e) { }
+       @Override
+       public void mouseDragged(MouseEvent e) {
+       }
+       @Override
+       public void mouseMoved(MouseEvent e) {
+               Point point = e.getPoint();
+               boolean changed = false;
+               boolean hit;
+               for( TuningButton button : tuningButtons ) {
+                       hit = button.contains(point);
+                       if ( button.isMouseEntered != hit ) changed = true;
+                       button.isMouseEntered = hit;
+               }
+               PressingPointList possible_points[] = chordVariations.getPossiblePressingPoints();
+               if( possible_points != null ) {
+                       for( PressingPointList pps : possible_points ) {
+                               for( PressingPoint pp : pps ) {
+                                       if( pp.fretIndex > 0 ) {
+                                               int xOffset = -fretViewIndexModel.getValue()*fretDistance;
+                                               pp.rect.translate( xOffset, 0 );
+                                               hit = pp.rect.contains(point);
+                                               pp.rect.translate( -xOffset, 0 );
+                                       }
+                                       else hit = pp.rect.contains(point);
+                                       if ( pp.isMouseEntered != hit ) changed = true;
+                                       pp.isMouseEntered = hit;
+                               }
+                       }
+               }
+               if( changed ) repaint();
+       }
+       private static final int CHAR_WIDTH = 8; // FontMetrics.stringWidth("C#");
+       private static final int CHAR_HEIGHT = 16; // FontMetrics.getHeight();
+       /**
+        * 弦間距離(リサイズによって変化する)
+        */
+       private int stringDistance;
+       /**
+        * フレット間距離(リサイズによって変化する)
+        */
+       private int fretDistance;
+       /**
+        * 押さえるポイントの直径(リサイズによって変化する)
+        */
+       private int pointSize;
+       /**
+        * 可視部分の矩形(リサイズによって変化する)
+        */
+       private Rectangle gridRect;
+       /**
+        * 指定された楽器用にチューニングをリセットします。
+        * @param inst 対象楽器
+        */
+       public void tune(ChordDiagram.Instrument inst) {
+               notesWhenOpen = (targetInstrument = inst).createTunableOpenNotes();
+               tune();
+       }
+       /**
+        * チューニングを行います。
+        */
+       public void tune() {
+               Dimension sz = getSize();
+               stringDistance = (
+                       sz.height + 1
+                       - UPPER_MARGIN_WIDTH - LOWER_MARGIN_WIDTH
+                       - CHAR_HEIGHT
+               ) / MAX_STRINGS;
+
+               pointSize = stringDistance * 4 / 5;
+
+               fretDistance = (
+                       sz.width + 1 - (
+                               LEFT_MARGIN_WIDTH + RIGHT_MARGIN_WIDTH
+                               + CHAR_WIDTH + pointSize + 8
+                       )
+               ) / VISIBLE_FRETS;
+
+               gridRect = new Rectangle(
+                       LEFT_MARGIN_WIDTH + pointSize + CHAR_WIDTH + 8,
+                       UPPER_MARGIN_WIDTH + stringDistance / 2,
+                       fretDistance * VISIBLE_FRETS,
+                       stringDistance * (MAX_STRINGS - 1)
+               );
+
+               tuningButtons = new TuningButton[targetInstrument.getDefaultOpenNotes().size()];
+               for( int i=0; i<tuningButtons.length; i++ ) {
+                       tuningButtons[i] = new TuningButton(i);
+               }
+               setChord();
+       }
+       /**
+        * コード(和音)を再設定します。
+        */
+       public void setChord() {
+               setChord(chordVariations.chord);
+       }
+       /**
+        * コード(和音)を設定します。
+        * @param chord コード
+        */
+       public void setChord(Chord chord) {
+               chordVariations.setChord(chord);
+               repaint();
+       }
+}
\ No newline at end of file
diff --git a/src/camidion/chordhelper/chordmatrix/ChordButtonLabel.java b/src/camidion/chordhelper/chordmatrix/ChordButtonLabel.java
new file mode 100644 (file)
index 0000000..6d42123
--- /dev/null
@@ -0,0 +1,29 @@
+package camidion.chordhelper.chordmatrix;
+
+import java.awt.Font;
+
+import javax.swing.JLabel;
+
+/**
+ * コードボタン用のマトリクス外のラベル
+ */
+public class ChordButtonLabel extends JLabel {
+       private ChordMatrix cm;
+       public ChordButtonLabel(String txt, ChordMatrix cm) {
+               super(txt,CENTER);
+               this.cm = cm;
+               setOpaque(true);
+               setFont(getFont().deriveFont(Font.PLAIN));
+               setDarkMode(false);
+       }
+       public void setDarkMode(boolean isDark) {
+               setBackground( isDark ?
+                       cm.darkModeColorset.backgrounds[2] :
+                       cm.normalModeColorset.backgrounds[2]
+               );
+               setForeground( isDark ?
+                       cm.darkModeColorset.foregrounds[0] :
+                       cm.normalModeColorset.foregrounds[0]
+               );
+       }
+}
diff --git a/src/camidion/chordhelper/chordmatrix/ChordGuide.java b/src/camidion/chordhelper/chordmatrix/ChordGuide.java
new file mode 100644 (file)
index 0000000..c6be45b
--- /dev/null
@@ -0,0 +1,78 @@
+package camidion.chordhelper.chordmatrix;
+
+import java.awt.Color;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+
+import javax.swing.Box;
+import javax.swing.BoxLayout;
+import javax.swing.JMenuItem;
+import javax.swing.JPanel;
+import javax.swing.JPopupMenu;
+
+/**
+ * コードサフィックスのヘルプ
+ */
+public class ChordGuide extends JPanel {
+       private class ChordGuideLabel extends ChordButtonLabel {
+               private JPopupMenu popupMenu = new JPopupMenu();
+               public ChordGuideLabel(String txt, ChordMatrix cm) {
+                       super(txt,cm);
+                       addMouseListener(
+                               new MouseAdapter() {
+                                       public void mousePressed(MouseEvent e) {
+                                               popupMenu.show( e.getComponent(), 0, getHeight() );
+                                       }
+                               }
+                       );
+               }
+               public void addMenu(JMenuItem menuItem) { popupMenu.add(menuItem); }
+               public void addSeparator() { popupMenu.addSeparator(); }
+       }
+       private ChordGuideLabel guide76, guide5, guide9;
+       public ChordGuide(ChordMatrix cm) {
+               guide76 = new ChordGuideLabel(" 6  7  M7 ",cm) {
+                       {
+                               setToolTipText("How to add 7th, major 7th, 6th");
+                               addMenu(new JMenuItem("7        = <RightClick>"));
+                               addMenu(new JMenuItem("M7(maj7) = [Shift] <RightClick>"));
+                               addMenu(new JMenuItem("6        = [Shift]"));
+                       }
+               };
+               guide5 = new ChordGuideLabel(" -5 dim +5 aug ",cm){
+                       {
+                               setToolTipText("How to add -5, dim, +5, aug");
+                               addMenu(new JMenuItem("-5 (b5)      = [Alt]"));
+                               addMenu(new JMenuItem("+5 (#5/aug)  = [Alt] sus4"));
+                               addSeparator();
+                               addMenu(new JMenuItem("dim  (m-5)  = [Alt] minor"));
+                               addMenu(new JMenuItem("dim7 (m6-5) = [Alt] [Shift] minor"));
+                               addMenu(new JMenuItem("m7-5 = [Alt] minor <RightClick>"));
+                               addMenu(new JMenuItem("aug7 (7+5)  = [Alt] sus4 <RightClick>"));
+                       }
+               };
+               guide9 = new ChordGuideLabel(" add9 ",cm) {
+                       {
+                               setToolTipText("How to add 9th");
+                               addMenu(new JMenuItem("add9  = [Ctrl]"));
+                               addSeparator();
+                               addMenu(new JMenuItem("9     = [Ctrl] <RightClick>"));
+                               addMenu(new JMenuItem("M9    = [Ctrl] [Shift] <RightClick>"));
+                               addMenu(new JMenuItem("69    = [Ctrl] [Shift]"));
+                               addMenu(new JMenuItem("dim9  = [Ctrl] [Shift] [Alt] minor"));
+                       }
+               };
+               setLayout(new BoxLayout(this, BoxLayout.X_AXIS));
+               add(guide76);
+               add( Box.createHorizontalStrut(2) );
+               add(guide5);
+               add( Box.createHorizontalStrut(2) );
+               add(guide9);
+       }
+       public void setDarkMode(boolean is_dark) {
+               setBackground( is_dark ? Color.black : null );
+               guide76.setDarkMode( is_dark );
+               guide5.setDarkMode( is_dark );
+               guide9.setDarkMode( is_dark );
+       }
+}
\ No newline at end of file
diff --git a/src/camidion/chordhelper/chordmatrix/ChordMatrix.java b/src/camidion/chordhelper/chordmatrix/ChordMatrix.java
new file mode 100644 (file)
index 0000000..b0c3b63
--- /dev/null
@@ -0,0 +1,1175 @@
+package camidion.chordhelper.chordmatrix;
+
+import java.awt.Color;
+import java.awt.Component;
+import java.awt.Dimension;
+import java.awt.Font;
+import java.awt.Graphics;
+import java.awt.Graphics2D;
+import java.awt.GridLayout;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.FocusEvent;
+import java.awt.event.FocusListener;
+import java.awt.event.InputEvent;
+import java.awt.event.ItemEvent;
+import java.awt.event.ItemListener;
+import java.awt.event.KeyEvent;
+import java.awt.event.KeyListener;
+import java.awt.event.MouseEvent;
+import java.awt.event.MouseListener;
+import java.awt.event.MouseMotionListener;
+import java.awt.event.MouseWheelEvent;
+import java.awt.event.MouseWheelListener;
+import java.util.ArrayList;
+
+import javax.swing.ComboBoxModel;
+import javax.swing.DefaultComboBoxModel;
+import javax.swing.JComponent;
+import javax.swing.JLabel;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+
+import camidion.chordhelper.ButtonIcon;
+import camidion.chordhelper.ChordDisplayLabel;
+import camidion.chordhelper.chorddiagram.CapoSelecterView;
+import camidion.chordhelper.midieditor.SequenceTickIndex;
+import camidion.chordhelper.music.Chord;
+import camidion.chordhelper.music.Key;
+import camidion.chordhelper.music.Music;
+import camidion.chordhelper.music.NoteSymbol;
+import camidion.chordhelper.music.SymbolLanguage;
+
+/**
+ * MIDI Chord Helper 用のコードボタンマトリクス
+ *
+ * @author
+ *     Copyright (C) 2004-2013 Akiyoshi Kamide
+ *     http://www.yk.rim.or.jp/~kamide/music/chordhelper/
+ */
+public class ChordMatrix extends JPanel
+       implements MouseListener, KeyListener, MouseMotionListener, MouseWheelListener
+{
+       /**
+        * 列数
+        */
+       public static final int N_COLUMNS = Music.SEMITONES_PER_OCTAVE * 2 + 1;
+       /**
+        * 行数
+        */
+       public static final int CHORD_BUTTON_ROWS = 3;
+       /**
+        * 調号ボタン
+        */
+       public Co5Label keysigLabels[] = new Co5Label[ N_COLUMNS ];
+       /**
+        * コードボタン
+        */
+       public ChordLabel chordLabels[] = new ChordLabel[N_COLUMNS * CHORD_BUTTON_ROWS];
+       /**
+        * コードボタンの下のコード表示部
+        */
+       public ChordDisplayLabel chordDisplay = new ChordDisplayLabel("Chord Pad", this, null);
+
+       private static class ChordLabelSelection {
+               ChordLabel chordLabel;
+               int bitIndex;
+               boolean isSus4;
+               public ChordLabelSelection(ChordLabel chordLabel, int bitIndex) {
+                       this.chordLabel = chordLabel;
+                       this.bitIndex = bitIndex;
+                       this.isSus4 = chordLabel.isSus4;
+               }
+               public void setCheckBit(boolean isOn) {
+                       chordLabel.setCheckBit(isOn, bitIndex);
+               }
+               public boolean setBassCheckBit(boolean isOn) {
+                       if( bitIndex == 0 && ! isSus4 ) {
+                               chordLabel.setCheckBit(isOn, 6);
+                               return true;
+                       }
+                       return false;
+               }
+       }
+       private static class ChordLabelSelections {
+               int weight = 0;
+               int bass_weight = 0;
+               boolean is_active = false;
+               boolean is_bass_active = false;
+               private ChordLabelSelection acls[];
+               public ChordLabelSelections(ArrayList<ChordLabelSelection> al) {
+                       acls = al.toArray(new ChordLabelSelection[al.size()]);
+               }
+               void addWeight(int weight_diff) {
+                       if( (weight += weight_diff) < 0 ) weight = 0;
+                       if( (weight > 0) != is_active ) {
+                               is_active = !is_active;
+                               for( ChordLabelSelection cls : acls ) {
+                                       cls.setCheckBit(is_active);
+                               }
+                       }
+               }
+               void addBassWeight(int weight_diff) {
+                       if( (bass_weight += weight_diff) < 0 ) bass_weight = 0;
+                       if( (bass_weight > 0) != is_bass_active ) {
+                               is_bass_active = !is_bass_active;
+                               for( ChordLabelSelection cls : acls ) {
+                                       if( ! cls.setBassCheckBit(is_bass_active) ) {
+                                               // No more root major/minor
+                                               break;
+                                       }
+                               }
+                       }
+                       addWeight(weight_diff);
+               }
+               void clearWeight() {
+                       weight = bass_weight = 0;
+                       is_active = is_bass_active = false;
+                       for( ChordLabelSelection cls : acls ) {
+                               cls.setCheckBit(false);
+                               cls.setBassCheckBit(false);
+                       }
+               }
+       }
+       private ChordLabelSelections chordLabelSelections[] =
+               new ChordLabelSelections[Music.SEMITONES_PER_OCTAVE];
+       /**
+        * 発音中のノート表示をクリアします。
+        */
+       public void clearIndicators() {
+               for( int i=0; i<chordLabelSelections.length; i++ ) {
+                       chordLabelSelections[i].clearWeight();
+               }
+               repaint();
+       }
+       /**
+        * MIDIのノートイベント(ON/OFF)を受け取ります。
+        * @param isNoteOn ONのときtrue
+        * @param noteNumber ノート番号
+        */
+       public void note(boolean isNoteOn, int noteNumber) {
+               int weightDiff = (isNoteOn ? 1 : -1);
+               ChordLabelSelections cls = chordLabelSelections[Music.mod12(noteNumber)];
+               if( noteNumber < 49 )
+                       cls.addBassWeight(weightDiff);
+               else
+                       cls.addWeight(weightDiff);
+       }
+
+       /**
+        * 調号ボタン
+        */
+       private class Co5Label extends JLabel {
+               public boolean isSelected = false;
+               public int co5Value = 0;
+               private Color indicatorColor;
+               public Co5Label(int v) {
+                       Key key = new Key(co5Value = v);
+                       setOpaque(true);
+                       setBackground(false);
+                       setForeground( currentColorset.foregrounds[0] );
+                       setHorizontalAlignment( JLabel.CENTER );
+                       String tip = "Key signature: ";
+                       if( v != key.toCo5() ) {
+                               tip += "out of range" ;
+                       }
+                       else {
+                               tip += key.signatureDescription() + " " +
+                                       key.toStringIn(SymbolLanguage.IN_JAPANESE);
+                               if( v == 0 ) {
+                                       setIcon(new ButtonIcon(ButtonIcon.NATURAL_ICON));
+                               }
+                               else {
+                                       setFont( getFont().deriveFont(Font.PLAIN) );
+                                       setText( key.signature() );
+                               }
+                       }
+                       setToolTipText(tip);
+               }
+               public void paint(Graphics g) {
+                       super.paint(g);
+                       Dimension d = getSize();
+                       if( ChordMatrix.this.isFocusOwner() && isSelected ) {
+                               g.setColor( currentColorset.focus[1] );
+                               g.drawRect( 0, 0, d.width-1, d.height-1 );
+                       }
+                       if( !isSelected || !isPlaying || currentBeat+1 == timesigUpper ) {
+                               return;
+                       }
+                       if( currentBeat == 0 ) {
+                               g.setColor( indicatorColor );
+                               g.drawRect( 2, 2, d.width-5, d.height-5 );
+                               g.setColor( isDark ? indicatorColor.darker() : indicatorColor.brighter() );
+                               g.drawRect( 0, 0, d.width-1, d.height-1 );
+                               return;
+                       }
+                       Color color = currentColorset.indicators[0];
+                       g.setColor( color );
+                       if( currentBeat == 1 ) {
+                               //
+                               // ||__ii
+                               g.drawLine( 2, d.height-3, d.width-3, d.height-3 );
+                               g.drawLine( d.width-3, d.height*3/4, d.width-3, d.height-3 );
+                               g.drawLine( 2, 2, 2, d.height-3 );
+                               g.setColor( isDark ? color.darker() : color.brighter() );
+                               g.drawLine( 0, d.height-1, d.width-1, d.height-1 );
+                               g.drawLine( d.width-1, d.height*3/4, d.width-1, d.height-1 );
+                               g.drawLine( 0, 0, 0, d.height-1 );
+                       }
+                       else {
+                               //
+                               // ii__
+                               //
+                               int vertical_top = (d.height-1) * (currentBeat-1) / (timesigUpper-2) ;
+                               g.drawLine( 2, vertical_top == 0 ? 2 : vertical_top, 2, d.height-3 );
+                               g.setColor( isDark ? color.darker() : color.brighter() );
+                               g.drawLine( 0, vertical_top, 0, d.height-1 );
+                       }
+               }
+               public void setBackground(boolean isActive) {
+                       super.setBackground(currentColorset.backgrounds[isActive?2:0]);
+                       setIndicatorColor();
+                       setOpaque(true);
+               }
+               public void setSelection(boolean isSelected) {
+                       this.isSelected = isSelected;
+                       setSelection();
+               }
+               public void setSelection() {
+                       setForeground(currentColorset.foregrounds[isSelected?1:0]);
+               }
+               public void setIndicatorColor() {
+                       if( co5Value < 0 ) {
+                               indicatorColor = currentColorset.indicators[2];
+                       }
+                       else if( co5Value > 0 ) {
+                               indicatorColor = currentColorset.indicators[1];
+                       }
+                       else {
+                               indicatorColor = currentColorset.foregrounds[1];
+                       }
+               }
+       }
+       /**
+        * コードボタン
+        */
+       private class ChordLabel extends JLabel {
+               public byte checkBits = 0;
+               public int co5Value;
+               public boolean isMinor;
+               public boolean isSus4;
+               public boolean isSelected = false;
+               public Chord chord;
+
+               private boolean inActiveZone = true;
+               private Font boldFont;
+               private Font plainFont;
+               private int indicatorColorIndices[] = new int[5];
+               private byte indicatorBits = 0;
+
+               public ChordLabel(Chord chord) {
+                       this.chord = chord;
+                       isMinor = chord.isSet(Chord.Interval.MINOR);
+                       isSus4 = chord.isSet(Chord.Interval.SUS4);
+                       co5Value = chord.rootNoteSymbol().toCo5();
+                       if( isMinor ) co5Value -= 3;
+                       String labelText = ( isSus4 ? chord.symbolSuffix() : chord.toString() );
+                       if( isMinor && labelText.length() > 3 ) {
+                               float small_point_size = getFont().getSize2D() - 2;
+                               boldFont = getFont().deriveFont(Font.BOLD, small_point_size);
+                               plainFont = getFont().deriveFont(Font.PLAIN, small_point_size);
+                       }
+                       else {
+                               boldFont = getFont().deriveFont(Font.BOLD);
+                               plainFont = getFont().deriveFont(Font.PLAIN);
+                       }
+                       setOpaque(true);
+                       setBackground(0);
+                       setForeground( currentColorset.foregrounds[0] );
+                       setBold(false);
+                       setHorizontalAlignment( JLabel.CENTER );
+                       setText(labelText);
+                       setToolTipText( "Chord: " + chord.toName() );
+               }
+               public void paint(Graphics g) {
+                       super.paint(g);
+                       Dimension d = getSize();
+                       Graphics2D g2 = (Graphics2D) g;
+                       Color color = null;
+
+                       if( ! inActiveZone ) {
+                               g2.setColor( Color.gray );
+                       }
+
+                       if( (indicatorBits & 32) != 0 ) {
+                               //
+                               // Draw square  []  with 3rd/sus4th note color
+                               //
+                               if( inActiveZone ) {
+                                       color = currentColorset.indicators[indicatorColorIndices[1]];
+                                       g2.setColor( color );
+                               }
+                               g2.drawRect( 0, 0, d.width-1, d.height-1 );
+                               g2.drawRect( 2, 2, d.width-5, d.height-5 );
+                       }
+                       if( (indicatorBits & 1) != 0 ) {
+                               //
+                               // Draw  ||__  with root note color
+                               //
+                               if( inActiveZone ) {
+                                       color = currentColorset.indicators[indicatorColorIndices[0]];
+                                       g2.setColor( color );
+                               }
+                               g2.drawLine( 0, 0, 0, d.height-1 );
+                               g2.drawLine( 2, 2, 2, d.height-3 );
+                       }
+                       if( (indicatorBits & 64) != 0 ) {
+                               // Draw bass mark with root note color
+                               //
+                               if( inActiveZone ) {
+                                       color = currentColorset.indicators[indicatorColorIndices[0]];
+                                       g2.setColor( color );
+                               }
+                               g2.fillRect( 6, d.height-7, d.width-12, 2 );
+                       }
+                       if( (indicatorBits & 4) != 0 ) {
+                               //
+                               // Draw short  __ii  with parfect 5th color
+                               //
+                               if( inActiveZone ) {
+                                       color = currentColorset.indicators[indicatorColorIndices[2]];
+                                       g2.setColor( color );
+                               }
+                               g2.drawLine( d.width-1, d.height*3/4, d.width-1, d.height-1 );
+                               g2.drawLine( d.width-3, d.height*3/4, d.width-3, d.height-3 );
+                       }
+                       if( (indicatorBits & 2) != 0 ) {
+                               //
+                               // Draw  __  with 3rd note color
+                               //
+                               if( inActiveZone ) {
+                                       color = currentColorset.indicators[indicatorColorIndices[1]];
+                                       g2.setColor( color );
+                               }
+                               g2.drawLine( 0, d.height-1, d.width-1, d.height-1 );
+                               g2.drawLine( 2, d.height-3, d.width-3, d.height-3 );
+                       }
+                       if( (indicatorBits & 8) != 0 ) {
+                               //
+                               // Draw circle with diminished 5th color
+                               //
+                               if( inActiveZone ) {
+                                       g2.setColor( currentColorset.indicators[indicatorColorIndices[3]] );
+                               }
+                               g2.drawOval( 1, 1, d.width-2, d.height-2 );
+                       }
+                       if( (indicatorBits & 16) != 0 ) {
+                               //
+                               // Draw + with augument 5th color
+                               //
+                               if( inActiveZone ) {
+                                       g2.setColor( currentColorset.indicators[indicatorColorIndices[4]] );
+                               }
+                               g2.drawLine( 1, 3, d.width-3, 3 );
+                               g2.drawLine( 1, 4, d.width-3, 4 );
+                               g2.drawLine( d.width/2-1, 0, d.width/2-1, 7 );
+                               g2.drawLine( d.width/2, 0, d.width/2, 7 );
+                       }
+               }
+               public void setCheckBit( boolean is_on, int bit_index ) {
+                       //
+                       // Check bits: x6x43210
+                       //   6:BassRoot
+                       //   4:Augumented5th, 3:Diminished5th, 2:Parfect5th,
+                       //   1:Major3rd/minor3rd/sus4th, 0:Root
+                       //
+                       byte mask = ((byte)(1<<bit_index));
+                       byte old_check_bits = checkBits;
+                       if( is_on ) {
+                               checkBits |= mask;
+                       }
+                       else {
+                               checkBits &= ~mask;
+                       }
+                       if( old_check_bits == checkBits ) {
+                               // No bits changed
+                               return;
+                       }
+                       // Indicator bits: x6543210     6:Bass||_  5:[]  4:+  3:O  2:_ii  1:__  0:||_
+                       //
+                       byte indicator_bits = 0;
+                       if( (checkBits & 1) != 0 ) {
+                               if( (checkBits & 7) == 7 ) { // All triad notes appared
+                                       //
+                                       // Draw square
+                                       indicator_bits |= 0x20;
+                                       //
+                                       // Draw different-colored vertical lines
+                                       if( indicatorColorIndices[0] != indicatorColorIndices[1] ) {
+                                               indicator_bits |= 1;
+                                       }
+                                       if( indicatorColorIndices[2] != indicatorColorIndices[1] ) {
+                                               indicator_bits |= 4;
+                                       }
+                               }
+                               else if( !isSus4 ) {
+                                       //
+                                       // Draw vertical lines  || ii
+                                       indicator_bits |= 5;
+                                       //
+                                       if( (checkBits & 2) != 0 && (!isMinor || (checkBits & 0x18) != 0) ) {
+                                               //
+                                               // Draw horizontal bottom lines __
+                                               indicator_bits |= 2;
+                                       }
+                               }
+                               if( !isSus4 ) {
+                                       if( isMinor || (checkBits & 2) != 0 ) {
+                                               indicator_bits |= (byte)(checkBits & 0x18);  // Copy bit 3 and bit 4
+                                       }
+                                       if( (checkBits & 0x40) != 0 ) {
+                                               indicator_bits |= 0x40; // Bass
+                                       }
+                               }
+                       }
+                       if( this.indicatorBits == indicator_bits ) {
+                               // No shapes changed
+                               return;
+                       }
+                       this.indicatorBits = indicator_bits;
+                       repaint();
+               }
+               public void setBackground(int i) {
+                       switch( i ) {
+                       case  0:
+                       case  1:
+                       case  2:
+                       case  3:
+                               super.setBackground(currentColorset.backgrounds[i]);
+                               setOpaque(true);
+                               break;
+                       default: return;
+                       }
+               }
+               public void setSelection(boolean is_selected) {
+                       this.isSelected = is_selected;
+                       setSelection();
+               }
+               public void setSelection() {
+                       setForeground(currentColorset.foregrounds[this.isSelected?1:0]);
+               }
+               public void setBold(boolean is_bold) {
+                       setFont( is_bold ? boldFont : plainFont );
+               }
+               public void keyChanged() {
+                       int co5_key = capoKey.toCo5();
+                       int co5_offset = co5Value - co5_key;
+                       inActiveZone = (co5_offset <= 6 && co5_offset >= -6) ;
+                       int root_note = chord.rootNoteSymbol().toNoteNumber();
+                       //
+                       // Reconstruct color index
+                       //
+                       // Root
+                       indicatorColorIndices[0] = Music.isOnScale(
+                               root_note, co5_key
+                       ) ? 0 : co5_offset > 0 ? 1 : 2;
+                       //
+                       // 3rd / sus4
+                       indicatorColorIndices[1] = Music.isOnScale(
+                               root_note+(isMinor?3:isSus4?5:4), co5_key
+                       ) ? 0 : co5_offset > 0 ? 1 : 2;
+                       //
+                       // P5th
+                       indicatorColorIndices[2] = Music.isOnScale(
+                               root_note+7, co5_key
+                       ) ? 0 : co5_offset > 0 ? 1 : 2;
+                       //
+                       // dim5th
+                       indicatorColorIndices[3] = Music.isOnScale(
+                               root_note+6, co5_key
+                       ) ? 0 : co5_offset > 4 ? 1 : 2;
+                       //
+                       // aug5th
+                       indicatorColorIndices[4] = Music.isOnScale(
+                               root_note+8, co5_key
+                       ) ? 0 : co5_offset > -3 ? 1 : 2;
+               }
+       }
+
+       /**
+        * 色セット(ダークモード切替対応)
+        */
+       public class ColorSet {
+               Color[] focus = new Color[2];   // 0:lost 1:gained
+               Color[] foregrounds = new Color[2];     // 0:unselected 1:selected
+               public Color[] backgrounds = new Color[4]; // 0:remote 1:left 2:local 3:right
+               Color[] indicators = new Color[3];      // 0:natural 1:sharp 2:flat
+       }
+       public ColorSet normalModeColorset = new ColorSet() {
+               {
+                       foregrounds[0] = null;
+                       foregrounds[1] = new Color(0xFF,0x3F,0x3F);
+                       backgrounds[0] = new Color(0xCF,0xFF,0xCF);
+                       backgrounds[1] = new Color(0x9F,0xFF,0xFF);
+                       backgrounds[2] = new Color(0xFF,0xCF,0xCF);
+                       backgrounds[3] = new Color(0xFF,0xFF,0x9F);
+                       indicators[0] = new Color(0xFF,0x3F,0x3F);
+                       indicators[1] = new Color(0xCF,0x6F,0x00);
+                       indicators[2] = new Color(0x3F,0x3F,0xFF);
+                       focus[0] = null;
+                       focus[1] = getBackground().darker();
+               }
+       };
+       public ColorSet darkModeColorset = new ColorSet() {
+               {
+                       foregrounds[0] = Color.gray.darker();
+                       foregrounds[1] = Color.pink.brighter();
+                       backgrounds[0] = Color.black;
+                       backgrounds[1] = new Color(0x00,0x18,0x18);
+                       backgrounds[2] = new Color(0x20,0x00,0x00);
+                       backgrounds[3] = new Color(0x18,0x18,0x00);
+                       indicators[0] = Color.pink;
+                       indicators[1] = Color.yellow;
+                       indicators[2] = Color.cyan;
+                       focus[0] = Color.black;
+                       focus[1] = getForeground().brighter();
+               }
+       };
+       private ColorSet currentColorset = normalModeColorset;
+
+       /**
+        * カポ値選択コンボボックスのデータモデル
+        * (コードボタン側とコードダイアグラム側の両方から参照される)
+        */
+       public ComboBoxModel<Integer> capoValueModel =
+               new DefaultComboBoxModel<Integer>() {
+                       {
+                               for( int i=1; i<=Music.SEMITONES_PER_OCTAVE-1; i++ )
+                                       addElement(i);
+                       }
+               };
+       /**
+        * カポ値選択コンボボックス(コードボタン側ビュー)
+        */
+       public CapoSelecterView capoSelecter = new CapoSelecterView(capoValueModel) {
+               private void capoChanged() {
+                       ChordMatrix.this.capoChanged(getCapo());
+               }
+               {
+                       checkbox.addItemListener(
+                               new ItemListener() {
+                                       public void itemStateChanged(ItemEvent e) {capoChanged();}
+                               }
+                       );
+                       valueSelecter.addActionListener(
+                               new ActionListener() {
+                                       public void actionPerformed(ActionEvent e) {capoChanged();}
+                               }
+                       );
+               }
+       };
+
+       /**
+        * コードボタンマトリクスの構築
+        */
+       public ChordMatrix() {
+               int i, v;
+               Dimension buttonSize = new Dimension(28,26);
+               //
+               // Make key-signature labels and chord labels
+               Co5Label l;
+               for (i=0, v= -Music.SEMITONES_PER_OCTAVE; i<N_COLUMNS; i++, v++) {
+                       l = new Co5Label(v);
+                       l.addMouseListener(this);
+                       l.addMouseMotionListener(this);
+                       add( keysigLabels[i] = l );
+                       l.setPreferredSize(buttonSize);
+               }
+               int row;
+               for (i=0; i < N_COLUMNS * CHORD_BUTTON_ROWS; i++) {
+                       row = i / N_COLUMNS;
+                       v = i - (N_COLUMNS * row) - 12;
+                       Chord chord = new Chord(
+                               new NoteSymbol(row==2 ? v+3 : v)
+                       );
+                       if( row==0 ) chord.set(Chord.Interval.SUS4);
+                       else if( row==2 ) chord.set(Chord.Interval.MINOR);
+                       ChordLabel cl = new ChordLabel(chord);
+                       cl.addMouseListener(this);
+                       cl.addMouseMotionListener(this);
+                       cl.addMouseWheelListener(this);
+                       add(chordLabels[i] = cl);
+                       cl.setPreferredSize(buttonSize);
+               }
+               setFocusable(true);
+               setOpaque(true);
+               addKeyListener(this);
+               addFocusListener(new FocusListener() {
+                       public void focusGained(FocusEvent e) {
+                               repaint();
+                       }
+                       public void focusLost(FocusEvent e) {
+                               selectedChord = selectedChordCapo = null;
+                               fireChordChanged();
+                               repaint();
+                       }
+               });
+               setLayout(new GridLayout( 4, N_COLUMNS, 2, 2 ));
+               setKeySignature( new Key() );
+               //
+               // Make chord label selections index
+               //
+               int noteIndex;
+               ArrayList<ChordLabelSelection> al;
+               Chord chord;
+               for( int note_no=0; note_no<chordLabelSelections.length; note_no++ ) {
+                       al = new ArrayList<ChordLabelSelection>();
+                       //
+                       // Root major/minor chords
+                       for( ChordLabel cl : chordLabels ) {
+                               if( ! cl.isSus4 && cl.chord.indexOf(note_no) == 0 ) {
+                                       al.add(new ChordLabelSelection( cl, 0 )); // Root
+                               }
+                       }
+                       // Root sus4 chords
+                       for( ChordLabel cl : chordLabels ) {
+                               if( cl.isSus4 && cl.chord.indexOf(note_no) == 0 ) {
+                                       al.add(new ChordLabelSelection( cl, 0 )); // Root
+                               }
+                       }
+                       // 3rd,sus4th,5th included chords
+                       for( ChordLabel cl : chordLabels ) {
+                               noteIndex = cl.chord.indexOf(note_no);
+                               if( noteIndex == 1 || noteIndex == 2 ) {
+                                       al.add(new ChordLabelSelection( cl, noteIndex )); // 3rd,sus4,P5
+                               }
+                       }
+                       // Diminished chords (major/minor chord button only)
+                       for( ChordLabel cl : chordLabels ) {
+                               if( cl.isSus4 ) continue;
+                               (chord = cl.chord.clone()).set(Chord.Interval.FLAT5);
+                               if( chord.indexOf(note_no) == 2 ) {
+                                       al.add(new ChordLabelSelection( cl, 3 ));
+                               }
+                       }
+                       // Augumented chords (major chord button only)
+                       for( ChordLabel cl : chordLabels ) {
+                               if( cl.isSus4 || cl.isMinor ) continue;
+                               (chord = cl.chord.clone()).set(Chord.Interval.SHARP5);
+                               if( chord.indexOf(note_no) == 2 ) {
+                                       al.add(new ChordLabelSelection( cl, 4 ));
+                               }
+                       }
+                       chordLabelSelections[note_no] = new ChordLabelSelections(al);
+               }
+       }
+       //
+       // MouseListener
+       public void mousePressed(MouseEvent e) {
+               Component obj = e.getComponent();
+               if( obj instanceof ChordLabel ) {
+                       ChordLabel cl = (ChordLabel)obj;
+                       Chord chord = cl.chord.clone();
+                       if( (e.getModifiersEx() & InputEvent.BUTTON3_DOWN_MASK) != 0 ) {
+                               if( e.isShiftDown() )
+                                       chord.set(Chord.Interval.MAJOR_SEVENTH);
+                               else
+                                       chord.set(Chord.Interval.SEVENTH);
+                       }
+                       else if( e.isShiftDown() )
+                               chord.set(Chord.Interval.SIXTH);
+                       if( e.isControlDown() )
+                               chord.set(Chord.Interval.NINTH);
+                       else
+                               chord.clear(Chord.OffsetIndex.NINTH);
+
+                       if( e.isAltDown() ) {
+                               if( cl.isSus4 ) {
+                                       chord.set(Chord.Interval.MAJOR); // To cancel sus4
+                                       chord.set(Chord.Interval.SHARP5);
+                               }
+                               else chord.set(Chord.Interval.FLAT5);
+                       }
+                       if( selectedChordLabel != null ) {
+                               selectedChordLabel.setSelection(false);
+                       }
+                       (selectedChordLabel = cl).setSelection(true);
+                       setSelectedChord(chord);
+               }
+               else if( obj instanceof Co5Label ) {
+                       int v = ((Co5Label)obj).co5Value;
+                       if( (e.getModifiersEx() & InputEvent.BUTTON3_DOWN_MASK) != 0 ) {
+                               setKeySignature( new Key(Music.oppositeCo5(v)) );
+                       }
+                       else if ( v == key.toCo5() ) {
+                               //
+                               // Cancel selected chord
+                               //
+                               setSelectedChord( (Chord)null );
+                       }
+                       else {
+                               // Change key
+                               setKeySignature( new Key(v) );
+                       }
+               }
+               requestFocusInWindow();
+               repaint();
+       }
+       public void mouseReleased(MouseEvent e) { destinationChordLabel = null; }
+       public void mouseEntered(MouseEvent e) { }
+       public void mouseExited(MouseEvent e) { }
+       public void mouseClicked(MouseEvent e) { }
+       public void mouseDragged(MouseEvent e) {
+               Component obj = e.getComponent();
+               if( obj instanceof ChordLabel ) {
+                       ChordLabel l_src = (ChordLabel)obj;
+                       Component obj2 = this.getComponentAt(
+                               l_src.getX() + e.getX(),
+                               l_src.getY() + e.getY()
+                       );
+                       if( obj2 == this ) {
+                               //
+                               // Entered gap between chord buttons - do nothing
+                               //
+                               return;
+                       }
+                       ChordLabel l_dst =
+                               ( (obj2 instanceof ChordLabel ) ? (ChordLabel)obj2 : null );
+                       if( l_dst == l_src ) {
+                               //
+                               // Returned to original chord button
+                               //
+                               destinationChordLabel = null;
+                               return;
+                       }
+                       if( destinationChordLabel != null ) {
+                               //
+                               // Already touched another chord button
+                               //
+                               return;
+                       }
+                       Chord chord = l_src.chord.clone();
+                       if( l_src.isMinor ) {
+                               if( l_dst == null ) { // Out of chord buttons
+                                       // mM7
+                                       chord.set(Chord.Interval.MAJOR_SEVENTH);
+                               }
+                               else if( l_src.co5Value < l_dst.co5Value ) { // Right
+                                       // m6
+                                       chord.set(Chord.Interval.SIXTH);
+                               }
+                               else { // Left or up from minor to major
+                                       // m7
+                                       chord.set(Chord.Interval.SEVENTH);
+                               }
+                       }
+                       else if( l_src.isSus4 ) {
+                               if( l_dst == null ) { // Out of chord buttons
+                                       return;
+                               }
+                               else if( ! l_dst.isSus4 ) { // Down from sus4 to major
+                                       chord.set(Chord.Interval.MAJOR);
+                               }
+                               else if( l_src.co5Value < l_dst.co5Value ) { // Right
+                                       chord.set(Chord.Interval.NINTH);
+                               }
+                               else { // Left
+                                       // 7sus4
+                                       chord.set(Chord.Interval.SEVENTH);
+                               }
+                       }
+                       else {
+                               if( l_dst == null ) { // Out of chord buttons
+                                       return;
+                               }
+                               else if( l_dst.isSus4 ) { // Up from major to sus4
+                                       chord.set(Chord.Interval.NINTH);
+                               }
+                               else if( l_src.co5Value < l_dst.co5Value ) { // Right
+                                       // M7
+                                       chord.set(Chord.Interval.MAJOR_SEVENTH);
+                               }
+                               else if( l_dst.isMinor ) { // Down from major to minor
+                                       // 6
+                                       chord.set(Chord.Interval.SIXTH);
+                               }
+                               else { // Left
+                                       // 7
+                                       chord.set(Chord.Interval.SEVENTH);
+                               }
+                       }
+                       if( chord.isSet(Chord.OffsetIndex.NINTH) || (l_src.isSus4 && (l_dst == null || ! l_dst.isSus4) ) ) {
+                               if( (e.getModifiersEx() & InputEvent.BUTTON3_DOWN_MASK) != 0 ) {
+                                       if( e.isShiftDown() ) {
+                                               chord.set(Chord.Interval.MAJOR_SEVENTH);
+                                       }
+                                       else {
+                                               chord.set(Chord.Interval.SEVENTH);
+                                       }
+                               }
+                               else if( e.isShiftDown() ) {
+                                       chord.set(Chord.Interval.SIXTH);
+                               }
+                       }
+                       else {
+                               if( e.isControlDown() )
+                                       chord.set(Chord.Interval.NINTH);
+                               else
+                                       chord.clear(Chord.OffsetIndex.NINTH);
+                       }
+                       if( e.isAltDown() ) {
+                               if( l_src.isSus4 ) {
+                                       chord.set(Chord.Interval.MAJOR);
+                                       chord.set(Chord.Interval.SHARP5);
+                               }
+                               else {
+                                       chord.set(Chord.Interval.FLAT5);
+                               }
+                       }
+                       setSelectedChord(chord);
+                       destinationChordLabel = (l_dst == null ? l_src : l_dst ) ;
+               }
+               else if( obj instanceof Co5Label ) {
+                       Co5Label l_src = (Co5Label)obj;
+                       Component obj2 = this.getComponentAt(
+                               l_src.getX() + e.getX(),
+                               l_src.getY() + e.getY()
+                       );
+                       if( !(obj2 instanceof Co5Label) ) {
+                               return;
+                       }
+                       Co5Label l_dst = (Co5Label)obj2;
+                       int v = l_dst.co5Value;
+                       if( (e.getModifiersEx() & InputEvent.BUTTON3_DOWN_MASK) != 0 ) {
+                               setKeySignature( new Key(Music.oppositeCo5(v)) );
+                       }
+                       else {
+                               setKeySignature( new Key(v) );
+                       }
+                       repaint();
+               }
+       }
+       public void mouseMoved(MouseEvent e) { }
+       public void mouseWheelMoved(MouseWheelEvent e) {
+               if( selectedChord != null ) {
+                       if( e.getWheelRotation() > 0 ) { // Wheel moved down
+                               if( --selectedNoteIndex < 0 ) {
+                                       selectedNoteIndex = selectedChord.numberOfNotes() - 1;
+                               }
+                       }
+                       else { // Wheel moved up
+                               if( ++selectedNoteIndex >= selectedChord.numberOfNotes() ) {
+                                       selectedNoteIndex = 0;
+                               }
+                       }
+                       fireChordChanged();
+               }
+       }
+       private Chord.Interval pcKeyNextShift7;
+       public void keyPressed(KeyEvent e) {
+               int i = -1, i_col = -1, i_row = 1;
+               boolean shiftPressed = false; // True if Shift-key pressed or CapsLocked
+               char keyChar = e.getKeyChar();
+               int keyCode = e.getKeyCode();
+               ChordLabel cl = null;
+               Chord chord = null;
+               int key_co5 = key.toCo5();
+               // System.out.println( keyChar + " Pressed on chord matrix" );
+               //
+               if( (i = "6 ".indexOf(keyChar)) >= 0 ) {
+                       selectedChord = selectedChordCapo = null;
+                       fireChordChanged();
+                       pcKeyNextShift7 = null;
+                       return;
+               }
+               else if( (i = "asdfghjkl;:]".indexOf(keyChar)) >= 0 ) {
+                       i_col = i + key_co5 + 7;
+               }
+               else if( (i = "ASDFGHJKL+*}".indexOf(keyChar)) >= 0 ) {
+                       i_col = i + key_co5 + 7;
+                       shiftPressed = true;
+               }
+               else if( (i = "zxcvbnm,./\\".indexOf(keyChar)) >=0 ) {
+                       i_col = i + key_co5 + 7;
+                       i_row = 2;
+               }
+               else if( (i = "ZXCVBNM<>?_".indexOf(keyChar)) >=0 ) {
+                       i_col = i + key_co5 + 7;
+                       i_row = 2;
+                       shiftPressed = true;
+               }
+               else if( (i = "qwertyuiop@[".indexOf(keyChar)) >= 0 ) {
+                       i_col = i + key_co5 + 7;
+                       i_row = 0;
+               }
+               else if( (i = "QWERTYUIOP`{".indexOf(keyChar)) >= 0 ) {
+                       i_col = i + key_co5 + 7;
+                       i_row = 0;
+                       shiftPressed = true;
+               }
+               else if( keyChar == '5' ) {
+                       pcKeyNextShift7 = Chord.Interval.MAJOR_SEVENTH; return;
+               }
+               else if( keyChar == '7' ) {
+                       pcKeyNextShift7 = Chord.Interval.SEVENTH; return;
+               }
+               // Shift current key-signature
+               else if( keyCode == KeyEvent.VK_LEFT || keyCode == KeyEvent.VK_KP_LEFT ) {
+                       // Add a flat
+                       setKeySignature( new Key(key_co5-1) );
+                       return;
+               }
+               else if( keyCode == KeyEvent.VK_RIGHT || keyCode == KeyEvent.VK_KP_RIGHT ) {
+                       // Add a sharp
+                       setKeySignature( new Key(key_co5+1) );
+                       return;
+               }
+               else if( keyCode == KeyEvent.VK_DOWN || keyCode == KeyEvent.VK_KP_DOWN ) {
+                       // Semitone down
+                       Key key = new Key(key_co5);
+                       key.transpose(-1);
+                       setKeySignature(key);
+                       return;
+               }
+               else if( keyCode == KeyEvent.VK_UP || keyCode == KeyEvent.VK_KP_UP ) {
+                       // Semitone up
+                       Key key = new Key(key_co5);
+                       key.transpose(1);
+                       setKeySignature(key);
+                       return;
+               }
+               if( i < 0 ) // No key char found
+                       return;
+               if( i_col < 0 ) i_col += 12; else if( i_col > N_COLUMNS ) i_col -= 12;
+               cl = chordLabels[i_col + N_COLUMNS * i_row];
+               chord = cl.chord.clone();
+               if( shiftPressed ) {
+                       chord.set(Chord.Interval.SEVENTH);
+               }
+               // specify by previous key
+               else if( pcKeyNextShift7 == null ) {
+                       chord.clear(Chord.OffsetIndex.SEVENTH);
+               }
+               else {
+                       chord.set(pcKeyNextShift7);
+               }
+               if( e.isAltDown() ) {
+                       if( cl.isSus4 ) {
+                               chord.set(Chord.Interval.MAJOR); // To cancel sus4
+                               chord.set(Chord.Interval.SHARP5);
+                       }
+                       else {
+                               chord.set(Chord.Interval.FLAT5);
+                       }
+               }
+               if( e.isControlDown() ) { // Cannot use for ninth ?
+                       chord.set(Chord.Interval.NINTH);
+               }
+               if( selectedChordLabel != null ) clear();
+               (selectedChordLabel = cl).setSelection(true);
+               setSelectedChord(chord);
+               pcKeyNextShift7 = null;
+               return;
+       }
+       public void keyReleased(KeyEvent e) { }
+       public void keyTyped(KeyEvent e) { }
+
+       public void addChordMatrixListener(ChordMatrixListener l) {
+               listenerList.add(ChordMatrixListener.class, l);
+       }
+       public void removeChordMatrixListener(ChordMatrixListener l) {
+               listenerList.remove(ChordMatrixListener.class, l);
+       }
+       protected void fireChordChanged() {
+               Object[] listeners = listenerList.getListenerList();
+               for (int i = listeners.length-2; i>=0; i-=2) {
+                       if (listeners[i]==ChordMatrixListener.class) {
+                               ((ChordMatrixListener)listeners[i+1]).chordChanged();
+                       }
+               }
+               if( selectedChord == null ) clearIndicators();
+       }
+       public void fireKeySignatureChanged() {
+               Object[] listeners = listenerList.getListenerList();
+               for (int i = listeners.length-2; i>=0; i-=2) {
+                       if (listeners[i]==ChordMatrixListener.class) {
+                               ((ChordMatrixListener)listeners[i+1]).keySignatureChanged();
+                       }
+               }
+       }
+       private Key key = null;
+       private Key capoKey = null;
+       public Key getKeySignature() { return key; }
+       public Key getKeySignatureCapo() { return capoKey; }
+       public void setKeySignature( Key key ) {
+               if( key == null || this.key != null && key.equals(this.key) )
+                       return;
+               int i;
+               // Clear old value
+               if( this.key == null ) {
+                       for( i = 0; i < keysigLabels.length; i++ ) {
+                               keysigLabels[i].setBackground(false);
+                       }
+               }
+               else {
+                       keysigLabels[this.key.toCo5() + 12].setSelection(false);
+                       for( i = Music.mod12(this.key.toCo5()); i < N_COLUMNS; i+=12 ) {
+                               keysigLabels[i].setBackground(false);
+                       }
+               }
+               // Set new value
+               keysigLabels[i = key.toCo5() + 12].setSelection(true);
+               for( i = Music.mod12(key.toCo5()); i < N_COLUMNS; i+=12 ) {
+                       keysigLabels[i].setBackground(true);
+               }
+               // Change chord-label's color & font
+               int i_color, old_i_color;
+               for( ChordLabel cl : chordLabels ) {
+                       i_color = ((cl.co5Value - key.toCo5() + 31)/3) & 3;
+                       if( this.key != null ) {
+                               old_i_color = ((cl.co5Value - this.key.toCo5() + 31)/3) & 3;
+                               if( i_color != old_i_color ) {
+                                       cl.setBackground(i_color);
+                               }
+                       }
+                       else cl.setBackground(i_color);
+                       if( !(cl.isSus4) ) {
+                               if( this.key != null && Music.mod12(cl.co5Value - this.key.toCo5()) == 0)
+                                       cl.setBold(false);
+                               if( Music.mod12( cl.co5Value - key.toCo5() ) == 0 )
+                                       cl.setBold(true);
+                       }
+               }
+               this.capoKey = (this.key = key).clone().transpose(capoSelecter.getCapo());
+               for( ChordLabel cl : chordLabels ) cl.keyChanged();
+               fireKeySignatureChanged();
+       }
+       private int capo = 0;
+       /**
+        * カポ位置の変更処理
+        * @param newCapo 新しいカポ位置
+        */
+       protected void capoChanged(int newCapo) {
+               if( this.capo == newCapo )
+                       return;
+               (this.capoKey = this.key.clone()).transpose(this.capo = newCapo);
+               selectedChordCapo = (
+                       selectedChord == null ? null : selectedChord.clone().transpose(newCapo)
+               );
+               for( ChordLabel cl : chordLabels ) cl.keyChanged();
+               fireKeySignatureChanged();
+       }
+
+       /**
+        * コードサフィックスのヘルプ
+        */
+       public ChordGuide chordGuide = new ChordGuide(this);
+
+       /**
+        * ドラッグ先コードボタン
+        */
+       private ChordLabel      destinationChordLabel = null;
+       /**
+        * ドラッグされたかどうか調べます。
+        * @return ドラッグ先コードボタンがあればtrue
+        */
+       public boolean isDragged() {
+               return destinationChordLabel != null ;
+       }
+
+       private boolean isDark = false;
+       public void setDarkMode(boolean is_dark) {
+               this.isDark = is_dark;
+               currentColorset = (is_dark ? darkModeColorset : normalModeColorset);
+               setBackground( currentColorset.focus[0] );
+               Key prev_key = key;
+               key = null;
+               setKeySignature(prev_key);
+               for( int i=0; i < keysigLabels.length; i++ ) keysigLabels[i].setSelection();
+               for( int i=0; i <  chordLabels.length; i++ ) chordLabels[i].setSelection();
+               chordGuide.setDarkMode( is_dark );
+               chordDisplay.setDarkMode( is_dark );
+               Color col = is_dark ? Color.black : null;
+               capoSelecter.setBackground( col );
+               capoSelecter.valueSelecter.setBackground( col );
+       }
+
+       private boolean isPlaying = false;
+       public boolean isPlaying() { return isPlaying; }
+       public void setPlaying(boolean is_playing) {
+               this.isPlaying = is_playing;
+               repaint();
+       }
+
+       private byte currentBeat = 0;
+       private byte timesigUpper = 4;
+       public void setBeat(SequenceTickIndex sequenceTickIndex) {
+               byte beat = (byte)(sequenceTickIndex.lastBeat);
+               byte tsu = sequenceTickIndex.timesigUpper;
+               if( currentBeat == beat && timesigUpper == tsu )
+                       return;
+               timesigUpper = tsu;
+               currentBeat = beat;
+               keysigLabels[ key.toCo5() + 12 ].repaint();
+       }
+
+       private ChordLabel      selectedChordLabel = null;
+       public JComponent getSelectedButton() {
+               return selectedChordLabel;
+       }
+       private Chord   selectedChord = null;
+       public Chord getSelectedChord() {
+               return selectedChord;
+       }
+       private Chord   selectedChordCapo = null;
+       public Chord getSelectedChordCapo() {
+               return selectedChordCapo;
+       }
+       public void setSelectedChordCapo( Chord chord ) {
+               setNoteIndex(-1); // Cancel arpeggio mode
+               selectedChord = (chord == null ? null : chord.clone().transpose(-capo,capoKey));
+               selectedChordCapo = chord;
+               fireChordChanged();
+       }
+       public void setSelectedChord( Chord chord ) {
+               setNoteIndex(-1); // Cancel arpeggio mode
+               selectedChord = chord;
+               selectedChordCapo = (chord == null ? null : chord.clone().transpose(capo,key));
+               fireChordChanged();
+       }
+       /**
+        * コードを文字列で設定します。
+        * @param chordSymbol コード名
+        */
+       public void setSelectedChord(String chordSymbol) throws IllegalArgumentException {
+               Chord chord = null;
+               if( chordSymbol != null && ! chordSymbol.isEmpty() ) {
+                       try {
+                               chord = new Chord(chordSymbol);
+                       } catch( IllegalArgumentException e ) {
+                               JOptionPane.showMessageDialog(
+                                       null, e.getMessage(), "Input error",
+                                       JOptionPane.ERROR_MESSAGE
+                               );
+                               return;
+                       }
+               }
+               setSelectedChord(chord);
+       }
+
+       private int selectedNoteIndex = -1;
+       public int getNoteIndex() {
+               return selectedChord == null || selectedNoteIndex < 0 ? -1 : selectedNoteIndex;
+       }
+       public void setNoteIndex(int noteIndex) {
+               selectedNoteIndex = noteIndex;
+       }
+       public void clear() {
+               if( selectedChordLabel != null ) {
+                       selectedChordLabel.setSelection(false);
+                       selectedChordLabel = null;
+               }
+               selectedChord = null; selectedNoteIndex = -1;
+       }
+
+}
diff --git a/src/camidion/chordhelper/chordmatrix/ChordMatrixListener.java b/src/camidion/chordhelper/chordmatrix/ChordMatrixListener.java
new file mode 100644 (file)
index 0000000..3f0658e
--- /dev/null
@@ -0,0 +1,11 @@
+package camidion.chordhelper.chordmatrix;
+
+import java.util.EventListener;
+
+/**
+ * コードボタンマトリクスのイベントを受信するリスナー
+ */
+public interface ChordMatrixListener extends EventListener {
+       void chordChanged();
+       void keySignatureChanged();
+}
diff --git a/src/camidion/chordhelper/midichordhelper.ico b/src/camidion/chordhelper/midichordhelper.ico
new file mode 100644 (file)
index 0000000..9b5aa13
Binary files /dev/null and b/src/camidion/chordhelper/midichordhelper.ico differ
diff --git a/src/camidion/chordhelper/midichordhelper.png b/src/camidion/chordhelper/midichordhelper.png
new file mode 100644 (file)
index 0000000..1f92b25
Binary files /dev/null and b/src/camidion/chordhelper/midichordhelper.png differ
diff --git a/src/camidion/chordhelper/mididevice/AbstractMidiChannelStatus.java b/src/camidion/chordhelper/mididevice/AbstractMidiChannelStatus.java
new file mode 100644 (file)
index 0000000..31e1c06
--- /dev/null
@@ -0,0 +1,187 @@
+package camidion.chordhelper.mididevice;
+
+import javax.sound.midi.MidiChannel;
+
+import camidion.chordhelper.music.MIDISpec;
+
+/**
+ * MIDIチャンネルの状態を管理するクラスです。
+ */
+public abstract class AbstractMidiChannelStatus implements MidiChannel {
+       protected int channel;
+       protected int program = 0;
+       protected int pitchBend = MIDISpec.PITCH_BEND_NONE;
+       protected int controllerValues[] = new int[0x80];
+       protected boolean isRhythmPart = false;
+
+       protected static final int DATA_NONE = 0;
+       protected static final int DATA_FOR_RPN = 1;
+       protected static final int DATA_FOR_NRPN = 2;
+       protected int dataFor = DATA_NONE;
+
+       public AbstractMidiChannelStatus(int channel) {
+               this.channel = channel;
+               resetAllValues(true);
+       }
+       public int getChannel() { return channel; }
+       public boolean isRhythmPart() { return isRhythmPart; }
+       public void setRhythmPart(boolean isRhythmPart) {
+               this.isRhythmPart = isRhythmPart;
+       }
+       public void resetRhythmPart() {
+               isRhythmPart = (channel == 9);
+       }
+       public void resetAllValues() { resetAllValues(false); }
+       public void resetAllValues(boolean isGS) {
+               for( int i=0; i<controllerValues.length; i++ )
+                       controllerValues[i] = 0;
+               if( isGS ) resetRhythmPart();
+               resetAllControllers();
+               controllerValues[10] = 0x40; // Set pan to center
+       }
+       public void fireRpnChanged() {}
+       protected void changeRPNData( int dataDiff ) {
+               int dataMsb = controllerValues[0x06];
+               int dataLsb = controllerValues[0x26];
+               if( dataDiff != 0 ) {
+                       // Data increment or decrement
+                       dataLsb += dataDiff;
+                       if( dataLsb >= 100 ) {
+                               dataLsb = 0;
+                               controllerValues[0x26] = ++dataMsb;
+                       }
+                       else if( dataLsb < 0 ) {
+                               dataLsb = 0;
+                               controllerValues[0x26] = --dataMsb;
+                       }
+                       controllerValues[0x06] = dataLsb;
+               }
+               fireRpnChanged();
+       }
+       @Override
+       public void noteOff( int noteNumber ) {}
+       @Override
+       public void noteOff( int noteNumber, int velocity ) {}
+       @Override
+       public void noteOn( int noteNumber, int velocity ) {}
+       @Override
+       public int getController(int controller) {
+               return controllerValues[controller];
+       }
+       @Override
+       public void programChange( int program ) {
+               this.program = program;
+       }
+       @Override
+       public void programChange(int bank, int program) {
+               controlChange( 0x00, ((bank>>7) & 0x7F) );
+               controlChange( 0x20, (bank & 0x7F) );
+               programChange( program );
+       }
+       @Override
+       public int getProgram() { return program; }
+       @Override
+       public void setPitchBend(int bend) { pitchBend = bend; }
+       @Override
+       public int getPitchBend() { return pitchBend; }
+       @Override
+       public void setPolyPressure(int noteNumber, int pressure) {}
+       @Override
+       public int getPolyPressure(int noteNumber) { return 0x40; }
+       @Override
+       public void setChannelPressure(int pressure) {}
+       @Override
+       public int getChannelPressure() { return 0x40; }
+       @Override
+       public void allSoundOff() {}
+       @Override
+       public void allNotesOff() {}
+       @Override
+       public void resetAllControllers() {
+               //
+               // See also:
+               //   Recommended Practice (RP-015)
+               //   Response to Reset All Controllers
+               //   http://www.midi.org/techspecs/rp15.php
+               //
+               // modulation
+               controllerValues[0] = 0;
+               //
+               // pedals
+               for(int i=64; i<=67; i++) controllerValues[i] = 0;
+               //
+               // Set pitch bend to center
+               pitchBend = 8192;
+               //
+               // Set NRPN / RPN to null value
+               for(int i=98; i<=101; i++) controllerValues[i] = 127;
+       }
+       @Override
+       public boolean localControl(boolean on) {
+               controlChange( 0x7A, on ? 0x7F : 0x00 );
+               return false;
+       }
+       @Override
+       public void setOmni(boolean on) {
+               controlChange( on ? 0x7D : 0x7C, 0 );
+       }
+       @Override
+       public boolean getOmni() { return false; }
+       @Override
+       public void setMono(boolean on) {}
+       @Override
+       public boolean getMono() { return false; }
+       @Override
+       public void setMute(boolean mute) {}
+       @Override
+       public boolean getMute() { return false; }
+       @Override
+       public void setSolo(boolean soloState) {}
+       @Override
+       public boolean getSolo() { return false; }
+       @Override
+       public void controlChange(int controller, int value) {
+               controllerValues[controller] = value & 0x7F;
+               switch( controller ) {
+
+               case 0x78: // All Sound Off
+                       allSoundOff();
+                       break;
+
+               case 0x7B: // All Notes Off
+                       allNotesOff();
+                       break;
+
+               case 0x79: // Reset All Controllers
+                       resetAllControllers();
+                       break;
+
+               case 0x06: // Data Entry (MSB)
+               case 0x26: // Data Entry (LSB)
+                       changeRPNData(0);
+                       break;
+
+               case 0x60: // Data Increment
+                       changeRPNData(1);
+                       break;
+
+               case 0x61: // Data Decrement
+                       changeRPNData(-1);
+                       break;
+
+                       // Non-Registered Parameter Number
+               case 0x62: // NRPN (LSB)
+               case 0x63: // NRPN (MSB)
+                       dataFor = DATA_FOR_NRPN;
+                       // fireRpnChanged();
+                       break;
+
+                       // Registered Parameter Number
+               case 0x64: // RPN (LSB)
+               case 0x65: // RPN (MSB)
+                       dataFor = DATA_FOR_RPN;
+                       fireRpnChanged();
+                       break;
+               }
+       }
+}
\ No newline at end of file
diff --git a/src/camidion/chordhelper/mididevice/AbstractMidiStatus.java b/src/camidion/chordhelper/mididevice/AbstractMidiStatus.java
new file mode 100644 (file)
index 0000000..a6b5901
--- /dev/null
@@ -0,0 +1,110 @@
+package camidion.chordhelper.mididevice;
+
+import java.util.Vector;
+
+import javax.sound.midi.MidiMessage;
+import javax.sound.midi.Receiver;
+import javax.sound.midi.ShortMessage;
+import javax.sound.midi.SysexMessage;
+
+/**
+ * 仮想 MIDI デバイスからの MIDI 受信とチャンネル状態の管理
+ */
+public abstract class AbstractMidiStatus extends Vector<AbstractMidiChannelStatus>
+       implements Receiver
+{
+       private void resetStatus() { resetStatus(false); }
+       private void resetStatus(boolean is_GS) {
+               for( AbstractMidiChannelStatus mcs : this )
+                       mcs.resetAllValues(is_GS);
+       }
+       public void close() { }
+       public void send(MidiMessage message, long timeStamp) {
+               if ( message instanceof ShortMessage ) {
+                       ShortMessage sm = (ShortMessage)message;
+                       switch ( sm.getCommand() ) {
+                       case ShortMessage.NOTE_ON:
+                               get(sm.getChannel()).noteOn(sm.getData1(), sm.getData2());
+                               break;
+                       case ShortMessage.NOTE_OFF:
+                               get(sm.getChannel()).noteOff(sm.getData1(), sm.getData2());
+                               break;
+                       case ShortMessage.CONTROL_CHANGE:
+                               get(sm.getChannel()).controlChange(sm.getData1(), sm.getData2());
+                               break;
+                       case ShortMessage.PROGRAM_CHANGE:
+                               get(sm.getChannel()).programChange(sm.getData1());
+                               break;
+                       case ShortMessage.PITCH_BEND:
+                               {
+                                       int b = (sm.getData1() & 0x7F);
+                                       b += ((sm.getData2() & 0x7F) << 7);
+                                       get(sm.getChannel()).setPitchBend(b);
+                               }
+                               break;
+                       case ShortMessage.POLY_PRESSURE:
+                               get(sm.getChannel()).setPolyPressure(sm.getData1(), sm.getData2());
+                               break;
+                       case ShortMessage.CHANNEL_PRESSURE:
+                               get(sm.getChannel()).setChannelPressure(sm.getData1());
+                               break;
+                       }
+               }
+               else if ( message instanceof SysexMessage ) {
+                       SysexMessage sxm = (SysexMessage)message;
+                       switch ( sxm.getStatus() ) {
+
+                       case SysexMessage.SYSTEM_EXCLUSIVE:
+                               byte data[] = sxm.getData();
+                               switch( data[0] ) {
+                               case 0x7E: // Non-Realtime Universal System Exclusive Message
+                                       if( data[2] == 0x09 ) { // General MIDI (GM)
+                                               if( data[3] == 0x01 ) { // GM System ON
+                                                       resetStatus();
+                                               }
+                                               else if( data[3] == 0x02 ) { // GM System OFF
+                                                       resetStatus();
+                                               }
+                                       }
+                                       break;
+                               case 0x41: // Roland
+                                       if( data[2]==0x42 && data[3]==0x12 ) { // GS DT1
+                                               if( data[4]==0x40 && data[5]==0x00 && data[6]==0x7F &&
+                                                               data[7]==0x00 && data[8]==0x41
+                                                               ) {
+                                                       resetStatus(true);
+                                               }
+                                               else if( data[4]==0x40 && (data[5] & 0xF0)==0x10 && data[6]==0x15 ) {
+                                                       // Drum Map 1 or 2, otherwise Normal Part
+                                                       boolean is_rhythm_part = ( data[7]==1 || data[7]==2 );
+                                                       int ch = (data[5] & 0x0F);
+                                                       if( ch == 0 ) ch = 9; else if( ch <= 9 ) ch--;
+                                                       get(ch).setRhythmPart(is_rhythm_part);
+                                               }
+                                               else if( data[4]==0x00 && data[5]==0x00 && data[6]==0x7F ) {
+                                                       if( data[7]==0x00 && data[8]==0x01 ) {
+                                                               // GM System Mode Set (1)
+                                                               resetStatus(true);
+                                                       }
+                                                       if( data[7]==0x01 && data[8]==0x00 ) {
+                                                               // GM System Mode Set (2)
+                                                               resetStatus(true);
+                                                       }
+                                               }
+                                       }
+                                       break;
+                               case 0x43: // Yamaha
+                                       if( data[2] == 0x4C
+                                       && data[3]==0 && data[4]==0 && data[5]==0x7E
+                                       && data[6]==0
+                                                       ) {
+                                               // XG System ON
+                                               resetStatus();
+                                       }
+                                       break;
+                               }
+                               break;
+                       }
+               }
+       }
+}
\ No newline at end of file
diff --git a/src/camidion/chordhelper/mididevice/AbstractVirtualMidiDevice.java b/src/camidion/chordhelper/mididevice/AbstractVirtualMidiDevice.java
new file mode 100644 (file)
index 0000000..5116c55
--- /dev/null
@@ -0,0 +1,219 @@
+package camidion.chordhelper.mididevice;
+
+import java.util.List;
+import java.util.Vector;
+
+import javax.sound.midi.InvalidMidiDataException;
+import javax.sound.midi.MidiChannel;
+import javax.sound.midi.MidiMessage;
+import javax.sound.midi.MidiUnavailableException;
+import javax.sound.midi.Receiver;
+import javax.sound.midi.ShortMessage;
+import javax.sound.midi.Transmitter;
+
+import camidion.chordhelper.music.MIDISpec;
+
+/**
+ * 仮想MIDIデバイスの最小限の実装を提供するクラス
+ */
+public abstract class AbstractVirtualMidiDevice implements VirtualMidiDevice {
+       /**
+        * 仮想MIDIデバイスを構築します。
+        */
+       protected AbstractVirtualMidiDevice() {
+               channels = new VirtualDeviceMidiChannel[MIDISpec.MAX_CHANNELS];
+               for( int i=0; i<channels.length; i++ )
+                       channels[i] = new VirtualDeviceMidiChannel(i);
+       }
+       protected MidiChannel[] channels;
+       @Override
+       public MidiChannel[] getChannels() { return channels; }
+       @Override
+       public long getMicrosecondPosition() {
+               return (microsecondOrigin == -1 ? -1: System.nanoTime()/1000 - microsecondOrigin);
+       }
+       /**
+        * 先頭のマイクロ秒位置(-1 で不定)
+        */
+       protected long microsecondOrigin = -1;
+       @Override
+       public boolean isOpen() { return isOpen; }
+       protected boolean isOpen = false;
+       @Override
+       public void open() {
+               isOpen = true;
+               microsecondOrigin = System.nanoTime()/1000;
+       }
+       @Override
+       public void close() { txList.clear(); isOpen = false; }
+       /**
+        * レシーバのリスト
+        */
+       protected List<Receiver> rxList = new Vector<Receiver>();
+       @Override
+       public List<Receiver> getReceivers() { return rxList; }
+       private int maxReceivers = 1;
+       /**
+        * この MIDI デバイスで MIDI データを受信するのに使用可能な
+        *  MIDI IN 接続の最大数を設定します。デフォルト値は -1 です。
+        * @param maxReceivers MIDI IN 接続の最大数、または利用可能な接続数に制限がない場合は -1。
+        */
+       protected void setMaxReceivers(int maxReceivers) {
+               this.maxReceivers = maxReceivers;
+       }
+       @Override
+       public int getMaxReceivers() { return maxReceivers; }
+       @Override
+       public Receiver getReceiver() {
+               return rxList.isEmpty() ? null : rxList.get(0);
+       }
+       protected void setReceiver(Receiver rx) {
+               if( maxReceivers == 0 ) return;
+               if( ! rxList.isEmpty() ) rxList.clear();
+               rxList.add(rx);
+       }
+       /**
+        * トランスミッタのリスト
+        */
+       protected List<Transmitter> txList = new Vector<Transmitter>();
+       @Override
+       public List<Transmitter> getTransmitters() { return txList; }
+       private int maxTransmitters = -1;
+       @Override
+       public int getMaxTransmitters() { return maxTransmitters; }
+       /**
+        * この MIDI デバイスで MIDI データを転送するのに使用可能な
+        *  MIDI OUT 接続の最大数を設定します。デフォルト値は -1 です。
+        * @param maxTransmitters MIDI OUT 接続の最大数、または利用可能な接続数に制限がない場合は -1。
+        */
+       protected void setMaxTransmitters(int maxTransmitters) {
+               this.maxTransmitters = maxTransmitters;
+       }
+       @Override
+       public Transmitter getTransmitter() throws MidiUnavailableException {
+               if( maxTransmitters == 0 ) {
+                       throw new MidiUnavailableException();
+               }
+               Transmitter new_tx = new Transmitter() {
+                       private Receiver rx = null;
+                       @Override
+                       public void close() { txList.remove(this); }
+                       @Override
+                       public Receiver getReceiver() { return rx; }
+                       @Override
+                       public void setReceiver(Receiver rx) { this.rx = rx; }
+               };
+               txList.add(new_tx);
+               return new_tx;
+       }
+       @Override
+       public void sendMidiMessage(MidiMessage msg) {
+               long timestamp = getMicrosecondPosition();
+               for( Transmitter tx : txList ) {
+                       Receiver rx = tx.getReceiver();
+                       if(rx != null) rx.send(msg, timestamp);
+               }
+       }
+       /**
+        * チャンネルの実装
+        */
+       private class VirtualDeviceMidiChannel implements MidiChannel {
+               /**
+                * MIDIチャンネルインデックス(チャンネル 1 のとき 0)
+                */
+               private int channel;
+               /**
+                * 指定の仮想MIDIデバイスの指定のMIDIチャンネルの
+                * メッセージを送信するためのインスタンスを構築します。
+                * @param vmd 仮想MIDIデバイス
+                * @param channel MIDIチャンネルインデックス(チャンネル 1 のとき 0)
+                */
+               public VirtualDeviceMidiChannel(int channel) {
+                       this.channel = channel;
+               }
+               private void sendShortMessage(int command, int data1, int data2) {
+                       try {
+                               sendMidiMessage(new ShortMessage(command, channel, data1, data2));
+                       } catch (InvalidMidiDataException e) {
+                               e.printStackTrace();
+                       }
+               }
+               @Override
+               public void noteOff(int noteNumber) {
+                       noteOff(noteNumber, 64);
+               }
+               @Override
+               public void noteOff(int noteNumber, int velocity) {
+                       sendShortMessage(ShortMessage.NOTE_OFF, noteNumber, velocity);
+               }
+               @Override
+               public void noteOn(int noteNumber, int velocity) {
+                       sendShortMessage(ShortMessage.NOTE_ON, noteNumber, velocity);
+               }
+               @Override
+               public void setPolyPressure(int noteNumber, int pressure) {
+                       sendShortMessage(ShortMessage.POLY_PRESSURE, noteNumber, pressure);
+               }
+               @Override
+               public int getPolyPressure(int noteNumber) { return 0x40; }
+               @Override
+               public void controlChange(int controller, int value) {
+                       sendShortMessage(ShortMessage.CONTROL_CHANGE, controller, value);
+               }
+               @Override
+               public int getController(int controller) { return 0x40; }
+               @Override
+               public void programChange(int program) {
+                       sendShortMessage(ShortMessage.PROGRAM_CHANGE, program, 0);
+               }
+               @Override
+               public void programChange(int bank, int program) {
+                       controlChange(0x00, ((bank>>7) & 0x7F));
+                       controlChange(0x20, (bank & 0x7F));
+                       programChange(program);
+               }
+               @Override
+               public int getProgram() { return 0; }
+               @Override
+               public void setChannelPressure(int pressure) {
+                       sendShortMessage(ShortMessage.CHANNEL_PRESSURE, pressure, 0);
+               }
+               @Override
+               public int getChannelPressure() { return 0x40; }
+               @Override
+               public void setPitchBend(int bend) {
+                       // NOTE: Pitch Bend data byte order is Little Endian
+                       sendShortMessage(ShortMessage.PITCH_BEND, (bend & 0x7F), ((bend>>7) & 0x7F));
+               }
+               @Override
+               public int getPitchBend() { return MIDISpec.PITCH_BEND_NONE; }
+               @Override
+               public void allSoundOff() { controlChange(0x78, 0); }
+               @Override
+               public void resetAllControllers() { controlChange(0x79, 0); }
+               @Override
+               public boolean localControl(boolean on) {
+                       controlChange(0x7A, on?0x7F:0x00);
+                       return false;
+               }
+               @Override
+               public void allNotesOff() { controlChange( 0x7B, 0 ); }
+               @Override
+               public void setOmni(boolean on) { controlChange(on?0x7D:0x7C, 0);
+               }
+               @Override
+               public boolean getOmni() { return false; }
+               @Override
+               public void setMono(boolean on) {}
+               @Override
+               public boolean getMono() { return false; }
+               @Override
+               public void setMute(boolean mute) {}
+               @Override
+               public boolean getMute() { return false; }
+               @Override
+               public void setSolo(boolean soloState) {}
+               @Override
+               public boolean getSolo() { return false; }
+       }
+}
\ No newline at end of file
diff --git a/src/camidion/chordhelper/mididevice/MidiCablePane.java b/src/camidion/chordhelper/mididevice/MidiCablePane.java
new file mode 100644 (file)
index 0000000..53582f7
--- /dev/null
@@ -0,0 +1,151 @@
+package camidion.chordhelper.mididevice;
+
+import java.awt.BasicStroke;
+import java.awt.Color;
+import java.awt.Graphics;
+import java.awt.Graphics2D;
+import java.awt.Rectangle;
+import java.awt.Stroke;
+import java.awt.event.ComponentEvent;
+import java.awt.event.ComponentListener;
+import java.util.Hashtable;
+import java.util.List;
+
+import javax.sound.midi.Receiver;
+import javax.sound.midi.Transmitter;
+import javax.swing.JComponent;
+import javax.swing.JDesktopPane;
+import javax.swing.JInternalFrame;
+import javax.swing.JLayeredPane;
+import javax.swing.event.InternalFrameEvent;
+import javax.swing.event.InternalFrameListener;
+import javax.swing.event.ListDataEvent;
+import javax.swing.event.ListDataListener;
+
+/**
+ * MIDI ケーブル描画面
+ */
+public class MidiCablePane extends JComponent
+       implements ListDataListener, ComponentListener, InternalFrameListener
+{
+       private JDesktopPane desktopPane;
+       //private JTree tree;
+       public MidiCablePane(JDesktopPane desktopPane) {
+               this.desktopPane = desktopPane;
+               setOpaque(false);
+               setVisible(true);
+       }
+       //
+       // MidiDeviceFrame の開閉を検出
+       public void internalFrameActivated(InternalFrameEvent e) {}
+       public void internalFrameClosed(InternalFrameEvent e) { repaint(); }
+       public void internalFrameClosing(InternalFrameEvent e) {
+               JInternalFrame frame = e.getInternalFrame();
+               if( ! (frame instanceof MidiDeviceFrame) )
+                       return;
+               MidiDeviceFrame devFrame = (MidiDeviceFrame)frame;
+               MidiConnecterListModel devModel = devFrame.listView.getModel();
+               if( ! devModel.rxSupported() )
+                       return;
+               colorMap.remove(devModel.getMidiDevice().getReceivers().get(0));
+               repaint();
+       }
+       public void internalFrameDeactivated(InternalFrameEvent e) { repaint(); }
+       public void internalFrameDeiconified(InternalFrameEvent e) {}
+       public void internalFrameIconified(InternalFrameEvent e) {}
+       public void internalFrameOpened(InternalFrameEvent e) {}
+       //
+       // ウィンドウオペレーションの検出
+       public void componentHidden(ComponentEvent e) {}
+       public void componentMoved(ComponentEvent e) { repaint(); }
+       public void componentResized(ComponentEvent e) { repaint(); }
+       public void componentShown(ComponentEvent e) {}
+       //
+       // MidiConnecterListModel における Transmitter リストの更新を検出
+       public void contentsChanged(ListDataEvent e) { repaint(); }
+       public void intervalAdded(ListDataEvent e) { repaint(); }
+       public void intervalRemoved(ListDataEvent e) { repaint(); }
+       //
+       // ケーブル描画用
+       private static final Stroke CABLE_STROKE = new BasicStroke(
+               3, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND
+       );
+       private static final Color[] CABLE_COLORS = {
+               new Color(255, 0, 0,144),
+               new Color(0, 255, 0,144),
+               new Color(0, 0, 255,144),
+               new Color(191,191,0,144),
+               new Color(0,191,191,144),
+               new Color(191,0,191,144),
+       };
+       private int nextColorIndex = 0;
+       private Hashtable<Receiver,Color> colorMap = new Hashtable<>();
+       @Override
+       public void paint(Graphics g) {
+               super.paint(g);
+               Graphics2D g2 = (Graphics2D)g;
+               g2.setStroke(CABLE_STROKE);
+               JInternalFrame[] frames =
+                       desktopPane.getAllFramesInLayer(JLayeredPane.DEFAULT_LAYER);
+               for( JInternalFrame frame : frames ) {
+                       if( ! (frame instanceof MidiDeviceFrame) )
+                               continue;
+                       MidiDeviceFrame txDeviceFrame = (MidiDeviceFrame)frame;
+                       List<Transmitter> txList = txDeviceFrame.listView.getModel().getMidiDevice().getTransmitters();
+                       for( Transmitter tx : txList ) {
+                               //
+                               // 送信端子から接続されている受信端子の存在を確認
+                               Receiver rx = tx.getReceiver();
+                               if( rx == null )
+                                       continue;
+                               //
+                               // 送信端子の矩形を特定
+                               Rectangle txRect = txDeviceFrame.getListCellBounds(tx);
+                               if( txRect == null )
+                                       continue;
+                               //
+                               // 受信端子のあるMIDIデバイスを探す
+                               Rectangle rxRect = null;
+                               for( JInternalFrame anotherFrame : frames ) {
+                                       if( ! (anotherFrame instanceof MidiDeviceFrame) )
+                                               continue;
+                                       //
+                                       // 受信端子の矩形を探す
+                                       MidiDeviceFrame rxDeviceFrame = (MidiDeviceFrame)anotherFrame;
+                                       if((rxRect = rxDeviceFrame.getListCellBounds(rx)) == null)
+                                               continue;
+                                       rxRect.translate(rxDeviceFrame.getX(), rxDeviceFrame.getY());
+                                       break;
+                               }
+                               if( rxRect == null )
+                                       continue;
+                               txRect.translate(txDeviceFrame.getX(), txDeviceFrame.getY());
+                               //
+                               // 色を探す
+                               Color color = colorMap.get(rx);
+                               if( color == null ) {
+                                       colorMap.put(rx, color=CABLE_COLORS[nextColorIndex++]);
+                                       if( nextColorIndex >= CABLE_COLORS.length )
+                                               nextColorIndex = 0;
+                               }
+                               g2.setColor(color);
+                               //
+                               // Tx 始点
+                               int fromX = txRect.x;
+                               int fromY = txRect.y + 2;
+                               int d = txRect.height - 5;
+                               g2.fillOval(fromX, fromY, d, d);
+                               //
+                               // Tx → Rx 線
+                               int r = d / 2;
+                               fromX += r;
+                               fromY += r;
+                               d = rxRect.height - 5;
+                               r = d / 2;
+                               int toX = rxRect.x + r;
+                               int toY = rxRect.y + r + 2;
+                               g2.drawLine(fromX, fromY, toX, toY);
+                       }
+               }
+       }
+}
\ No newline at end of file
diff --git a/src/camidion/chordhelper/mididevice/MidiConnecterListModel.java b/src/camidion/chordhelper/mididevice/MidiConnecterListModel.java
new file mode 100644 (file)
index 0000000..0487e1d
--- /dev/null
@@ -0,0 +1,271 @@
+package camidion.chordhelper.mididevice;
+
+import java.util.List;
+import java.util.Vector;
+
+import javax.sound.midi.MidiDevice;
+import javax.sound.midi.MidiUnavailableException;
+import javax.sound.midi.Receiver;
+import javax.sound.midi.Sequencer;
+import javax.sound.midi.Transmitter;
+import javax.swing.AbstractListModel;
+
+/**
+ * 1個の MIDI デバイスに属する Transmitter/Receiver のリストモデル
+ */
+public class MidiConnecterListModel extends AbstractListModel<AutoCloseable> {
+       protected MidiDevice device;
+       private List<MidiConnecterListModel> modelList;
+       /**
+        * 指定のMIDIデバイスに属する
+        *  {@link Transmitter}/{@link Receiver} のリストモデルを構築します。
+        *
+        * @param device 対象MIDIデバイス
+        * @param modelList リストモデルのリスト
+        */
+       public MidiConnecterListModel(MidiDevice device, List<MidiConnecterListModel> modelList) {
+               this.device = device;
+               this.modelList = modelList;
+       }
+       /**
+        * 対象MIDIデバイスを返します。
+        * @return 対象MIDIデバイス
+        */
+       public MidiDevice getMidiDevice() {
+               return device;
+       }
+       /**
+        * 対象MIDIデバイスの名前を返します。
+        */
+       @Override
+       public String toString() {
+               return device.getDeviceInfo().toString();
+       }
+       @Override
+       public AutoCloseable getElementAt(int index) {
+               List<Receiver> rxList = device.getReceivers();
+               int rxSize = rxList.size();
+               if( index < rxSize ) return rxList.get(index);
+               index -= rxSize;
+               List<Transmitter> txList = device.getTransmitters();
+               return index < txList.size() ? txList.get(index) : null;
+       }
+       @Override
+       public int getSize() {
+               return
+                       device.getReceivers().size() +
+                       device.getTransmitters().size();
+       }
+       /**
+        * 指定の要素がこのリストモデルで最初に見つかった位置を返します。
+        *
+        * @param element 探したい要素
+        * @return 位置のインデックス(先頭が 0、見つからないとき -1)
+        */
+       public int indexOf(AutoCloseable element) {
+               List<Receiver> rxList = device.getReceivers();
+               int index = rxList.indexOf(element);
+               if( index < 0 ) {
+                       List<Transmitter> txList = device.getTransmitters();
+                       if( (index = txList.indexOf(element)) >= 0 )
+                               index += rxList.size();
+               }
+               return index;
+       }
+       /**
+        * このリストが {@link Transmitter} をサポートしているか調べます。
+        * @return {@link Transmitter} をサポートしていたら true
+        */
+       public boolean txSupported() {
+               return device.getMaxTransmitters() != 0;
+       }
+       /**
+        * このリストが {@link Receiver} をサポートしているか調べます。
+        * @return {@link Receiver} をサポートしていたら true
+        */
+       public boolean rxSupported() {
+               return device.getMaxReceivers() != 0;
+       }
+       /**
+        * このリストのMIDIデバイスの入出力タイプを返します。
+        * <p>レシーバからMIDI信号を受けて外部へ出力できるデバイスの場合は MIDI_OUT、
+        * 外部からMIDI信号を入力してトランスミッタからレシーバへ転送できるデバイスの場合は MIDI_IN、
+        * 両方できるデバイスの場合は MIDI_IN_OUT を返します。
+        * </p>
+        * @return このリストのMIDIデバイスの入出力タイプ
+        */
+       public MidiDeviceInOutType getMidiDeviceInOutType() {
+               if( rxSupported() ) {
+                       if( txSupported() )
+                               return MidiDeviceInOutType.MIDI_IN_OUT;
+                       else
+                               return MidiDeviceInOutType.MIDI_OUT;
+               }
+               else {
+                       if( txSupported() )
+                               return MidiDeviceInOutType.MIDI_IN;
+                       else
+                               return null;
+               }
+       }
+       /**
+        * 引数で指定されたトランスミッタを、最初のレシーバに接続します。
+        * <p>接続先のレシーバがない場合は無視されます。
+        * </p>
+        * @param tx トランスミッタ
+        */
+       public void ConnectToReceiver(Transmitter tx) {
+               List<Receiver> receivers = device.getReceivers();
+               if( receivers.size() == 0 )
+                       return;
+               tx.setReceiver(receivers.get(0));
+               fireContentsChanged(this,0,getSize());
+       }
+       /**
+        * 未接続のトランスミッタを、
+        * 引数で指定されたリストモデルの最初のレシーバに接続します。
+        * @param anotherModel 接続先レシーバを持つリストモデル
+        */
+       public void connectToReceiverOf(MidiConnecterListModel anotherModel) {
+               if( ! txSupported() )
+                       return;
+               if( anotherModel == null || ! anotherModel.rxSupported() )
+                       return;
+               List<Receiver> rxList = anotherModel.device.getReceivers();
+               if( rxList.isEmpty() )
+                       return;
+               getUnconnectedTransmitter().setReceiver(rxList.get(0));
+       }
+       /**
+        * レシーバに未接続の最初のトランスミッタを返します。
+        * @return 未接続のトランスミッタ
+        */
+       public Transmitter getUnconnectedTransmitter() {
+               if( ! txSupported() ) {
+                       return null;
+               }
+               List<Transmitter> txList = device.getTransmitters();
+               for( Transmitter tx : txList ) {
+                       if( tx.getReceiver() == null )
+                               return tx;
+               }
+               Transmitter tx;
+               try {
+                       tx = device.getTransmitter();
+               } catch( MidiUnavailableException e ) {
+                       e.printStackTrace();
+                       return null;
+               }
+               fireIntervalAdded(this,0,getSize());
+               return tx;
+       }
+       /**
+        * 指定のトランスミッタを閉じます。
+        * <p>このリストモデルにないトランスミッタが指定された場合、無視されます。
+        * </p>
+        * @param txToClose 閉じたいトランスミッタ
+        */
+       public void closeTransmitter(Transmitter txToClose) {
+               if( device.getTransmitters().contains(txToClose) ) {
+                       txToClose.close();
+                       fireIntervalRemoved(this,0,getSize());
+               }
+       }
+       /**
+        * 対象MIDIデバイスを開きます。
+        * @throws MidiUnavailableException デバイスを開くことができない場合
+        */
+       public void openDevice() throws MidiUnavailableException {
+               device.open();
+               if( rxSupported() && device.getReceivers().size() == 0 ) {
+                       device.getReceiver();
+               }
+       }
+       /**
+        * 対象MIDIデバイスを閉じます。
+        *
+        * <p>対象MIDIデバイスの Receiver を設定している
+        *  {@link Transmitter} があればすべて閉じます。
+        * </p>
+        */
+       public void closeDevice() {
+               if( rxSupported() ) {
+                       Receiver rx = device.getReceivers().get(0);
+                       for( MidiConnecterListModel m : modelList ) {
+                               if( m == this || ! m.txSupported() )
+                                       continue;
+                               for( int i=0; i<m.getSize(); i++ ) {
+                                       AutoCloseable ac = m.getElementAt(i);
+                                       if( ! (ac instanceof Transmitter) )
+                                               continue;
+                                       Transmitter tx = ((Transmitter)ac);
+                                       if( tx.getReceiver() == rx )
+                                               m.closeTransmitter(tx);
+                               }
+                       }
+               }
+               device.close();
+       }
+       /**
+        * マイクロ秒位置をリセットします。
+        * <p>マイクロ秒位置はMIDIデバイスを開いてからの時間で表されます。
+        * このメソッドではMIDIデバイスをいったん閉じて再び開くことによって
+        * 時間位置をリセットします。
+        * 接続相手のデバイスがあった場合、元通りに接続を復元します。
+        * </p>
+        * <p>MIDIデバイスからリアルタイムレコーディングを開始するときは、
+        * 必ずマイクロ秒位置をリセットする必要があります。
+        * (リセットされていないマイクロ秒位置がそのままシーケンサに記録されると、
+        * 大幅に後ろのほうにずれて記録されてしまいます)
+        * </p>
+        */
+       public void resetMicrosecondPosition() {
+               if( ! txSupported() || device instanceof Sequencer )
+                       return;
+               //
+               // デバイスを閉じる前に接続相手の情報を保存
+               List<Transmitter> txList = device.getTransmitters();
+               List<Receiver> peerRxList = new Vector<Receiver>();
+               for( Transmitter tx : txList ) {
+                       Receiver rx = tx.getReceiver();
+                       if( rx != null ) peerRxList.add(rx);
+               }
+               List<Transmitter> peerTxList = null;
+               Receiver rx = null;
+               if( rxSupported() ) {
+                       rx = device.getReceivers().get(0);
+                       peerTxList = new Vector<Transmitter>();
+                       for( MidiConnecterListModel m : modelList ) {
+                               if( m == this || ! m.txSupported() )
+                                       continue;
+                               for( int i=0; i<m.getSize(); i++ ) {
+                                       Object obj = m.getElementAt(i);
+                                       if( ! (obj instanceof Transmitter) )
+                                               continue;
+                                       Transmitter tx = ((Transmitter)obj);
+                                       if( tx.getReceiver() == rx )
+                                               peerTxList.add(tx);
+                               }
+                       }
+               }
+               // いったん閉じて開く(ここでマイクロ秒位置がリセットされる)
+               device.close();
+               try {
+                       device.open();
+               } catch( MidiUnavailableException e ) {
+                       e.printStackTrace();
+               }
+               // 元通りに接続し直す
+               for( Receiver peerRx : peerRxList ) {
+                       Transmitter tx = getUnconnectedTransmitter();
+                       if( tx == null ) continue;
+                       tx.setReceiver(peerRx);
+               }
+               if( peerTxList != null ) {
+                       rx = device.getReceivers().get(0);
+                       for( Transmitter peerTx : peerTxList ) {
+                               peerTx.setReceiver(rx);
+                       }
+               }
+       }
+}
\ No newline at end of file
diff --git a/src/camidion/chordhelper/mididevice/MidiConnecterListView.java b/src/camidion/chordhelper/mididevice/MidiConnecterListView.java
new file mode 100644 (file)
index 0000000..2121c7a
--- /dev/null
@@ -0,0 +1,151 @@
+package camidion.chordhelper.mididevice;
+
+import java.awt.Component;
+import java.awt.datatransfer.DataFlavor;
+import java.awt.datatransfer.Transferable;
+import java.awt.dnd.DnDConstants;
+import java.awt.dnd.DragGestureEvent;
+import java.awt.dnd.DragGestureListener;
+import java.awt.dnd.DragSource;
+import java.awt.dnd.DropTarget;
+import java.awt.dnd.DropTargetDragEvent;
+import java.awt.dnd.DropTargetDropEvent;
+import java.awt.dnd.DropTargetEvent;
+import java.awt.dnd.DropTargetListener;
+
+import javax.sound.midi.Receiver;
+import javax.sound.midi.Transmitter;
+import javax.swing.Icon;
+import javax.swing.JLabel;
+import javax.swing.JList;
+import javax.swing.ListCellRenderer;
+import javax.swing.ListSelectionModel;
+
+import camidion.chordhelper.ButtonIcon;
+
+/**
+ * Transmitter(Tx)/Receiver(Rx) のリスト(view)
+ *
+ * <p>マウスで Tx からドラッグして Rx へドロップする機能を備えた
+ * 仮想MIDI端子リストです。
+ * </p>
+ */
+public class MidiConnecterListView extends JList<AutoCloseable>
+       implements Transferable, DragGestureListener, DropTargetListener
+{
+       public static final Icon MIDI_CONNECTER_ICON =
+               new ButtonIcon(ButtonIcon.MIDI_CONNECTOR_ICON);
+       private class CellRenderer extends JLabel implements ListCellRenderer<AutoCloseable> {
+               public Component getListCellRendererComponent(
+                       JList<? extends AutoCloseable> list,
+                       AutoCloseable value,
+                       int index,
+                       boolean isSelected,
+                       boolean cellHasFocus
+               ) {
+                       String text;
+                       if( value instanceof Transmitter ) text = "Tx";
+                       else if( value instanceof Receiver ) text = "Rx";
+                       else text = (value==null ? null : value.toString());
+                       setText(text);
+                       setIcon(MIDI_CONNECTER_ICON);
+                       if (isSelected) {
+                               setBackground(list.getSelectionBackground());
+                               setForeground(list.getSelectionForeground());
+                       } else {
+                               setBackground(list.getBackground());
+                               setForeground(list.getForeground());
+                       }
+                       setEnabled(list.isEnabled());
+                       setFont(list.getFont());
+                       setOpaque(true);
+                       return this;
+               }
+       }
+       /**
+        * 仮想MIDI端子リストビューを生成します。
+        * @param model このビューから参照されるデータモデル
+        */
+       public MidiConnecterListView(MidiConnecterListModel model) {
+               super(model);
+               setCellRenderer(new CellRenderer());
+               setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
+               setLayoutOrientation(JList.HORIZONTAL_WRAP);
+               setVisibleRowCount(0);
+        (new DragSource()).createDefaultDragGestureRecognizer(
+               this, DnDConstants.ACTION_COPY_OR_MOVE, this
+        );
+               new DropTarget( this, DnDConstants.ACTION_COPY_OR_MOVE, this, true );
+       }
+       private static final DataFlavor transmitterFlavor =
+               new DataFlavor(Transmitter.class, "Transmitter");
+       private static final DataFlavor transmitterFlavors[] = {transmitterFlavor};
+       @Override
+       public Object getTransferData(DataFlavor flavor) {
+               return getModel().getElementAt(getSelectedIndex());
+       }
+       @Override
+       public DataFlavor[] getTransferDataFlavors() {
+               return transmitterFlavors;
+       }
+       @Override
+       public boolean isDataFlavorSupported(DataFlavor flavor) {
+               return flavor.equals(transmitterFlavor);
+       }
+       @Override
+       public void dragGestureRecognized(DragGestureEvent dge) {
+               int action = dge.getDragAction();
+               if( (action & DnDConstants.ACTION_COPY_OR_MOVE) == 0 )
+                       return;
+               int index = locationToIndex(dge.getDragOrigin());
+               AutoCloseable data = getModel().getElementAt(index);
+               if( data instanceof Transmitter ) {
+                       dge.startDrag(DragSource.DefaultLinkDrop, this, null);
+               }
+       }
+       @Override
+       public void dragEnter(DropTargetDragEvent event) {
+               if( event.isDataFlavorSupported(transmitterFlavor) )
+                       event.acceptDrag(DnDConstants.ACTION_COPY_OR_MOVE);
+       }
+       @Override
+       public void dragExit(DropTargetEvent dte) {}
+       @Override
+       public void dragOver(DropTargetDragEvent dtde) {}
+       @Override
+       public void dropActionChanged(DropTargetDragEvent dtde) {}
+       @Override
+       public void drop(DropTargetDropEvent event) {
+               event.acceptDrop(DnDConstants.ACTION_COPY_OR_MOVE);
+               try {
+                       int maskedBits = event.getDropAction() & DnDConstants.ACTION_COPY_OR_MOVE;
+                       if( maskedBits != 0 ) {
+                               Transferable t = event.getTransferable();
+                               Object data = t.getTransferData(transmitterFlavor);
+                               if( data instanceof Transmitter ) {
+                                       getModel().ConnectToReceiver((Transmitter)data);
+                                       event.dropComplete(true);
+                                       return;
+                               }
+                       }
+                       event.dropComplete(false);
+               }
+               catch (Exception ex) {
+                       ex.printStackTrace();
+                       event.dropComplete(false);
+               }
+       }
+       @Override
+       public MidiConnecterListModel getModel() {
+               return (MidiConnecterListModel)super.getModel();
+       }
+       /**
+        * 選択されているトランスミッタを閉じます。
+        * レシーバが選択されていた場合は無視されます。
+        */
+       public void closeSelectedTransmitter() {
+               AutoCloseable ac = getSelectedValue();
+               if( ac instanceof Transmitter )
+                       getModel().closeTransmitter((Transmitter)ac);
+       }
+}
\ No newline at end of file
diff --git a/src/camidion/chordhelper/mididevice/MidiDesktopPane.java b/src/camidion/chordhelper/mididevice/MidiDesktopPane.java
new file mode 100644 (file)
index 0000000..8fa0059
--- /dev/null
@@ -0,0 +1,158 @@
+package camidion.chordhelper.mididevice;
+
+import java.awt.Point;
+import java.awt.datatransfer.Transferable;
+import java.awt.dnd.DnDConstants;
+import java.awt.dnd.DropTarget;
+import java.awt.dnd.DropTargetDragEvent;
+import java.awt.dnd.DropTargetDropEvent;
+import java.awt.dnd.DropTargetEvent;
+import java.awt.dnd.DropTargetListener;
+import java.awt.event.ComponentAdapter;
+import java.awt.event.ComponentEvent;
+
+import javax.sound.midi.MidiUnavailableException;
+import javax.swing.JDesktopPane;
+import javax.swing.JInternalFrame;
+import javax.swing.JLayeredPane;
+import javax.swing.JOptionPane;
+import javax.swing.Timer;
+
+/**
+ * 開いている MIDI デバイスを置くためのデスクトップビュー
+ */
+public class MidiDesktopPane extends JDesktopPane implements DropTargetListener {
+       private MidiCablePane cablePane = new MidiCablePane(this);
+       public MidiDesktopPane(MidiDeviceTree deviceTree) {
+               add(cablePane, JLayeredPane.PALETTE_LAYER);
+               int i=0;
+               MidiDeviceTreeModel treeModel = (MidiDeviceTreeModel)deviceTree.getModel();
+               for( MidiConnecterListModel deviceModel : treeModel.deviceModelList ) {
+                       MidiDeviceFrame frame = new MidiDeviceFrame(deviceModel) {
+                               {
+                                       addInternalFrameListener(cablePane);
+                                       addComponentListener(cablePane);
+                               }
+                       };
+                       frame.addInternalFrameListener(deviceTree);
+                       deviceModel.addListDataListener(cablePane);
+                       add(frame);
+                       if( deviceModel.getMidiDevice().isOpen() ) {
+                               frame.setBounds( 10+(i%2)*260, 10+i*55, 250, 100 );
+                               frame.setVisible(true);
+                               i++;
+                       }
+               }
+               addComponentListener(new ComponentAdapter() {
+                       @Override
+                       public void componentResized(ComponentEvent e) {
+                               cablePane.setSize(getSize());
+                       }
+               });
+               new DropTarget( this, DnDConstants.ACTION_COPY_OR_MOVE, this, true );
+       }
+       @Override
+       public void dragEnter(DropTargetDragEvent dtde) {
+               Transferable trans = dtde.getTransferable();
+               if( trans.isDataFlavorSupported(MidiDeviceTree.TREE_MODEL_FLAVOR) ) {
+                       dtde.acceptDrag(DnDConstants.ACTION_COPY_OR_MOVE);
+               }
+       }
+       @Override
+       public void dragExit(DropTargetEvent dte) {}
+       @Override
+       public void dragOver(DropTargetDragEvent dtde) {}
+       @Override
+       public void dropActionChanged(DropTargetDragEvent dtde) {}
+       @Override
+       public void drop(DropTargetDropEvent dtde) {
+               dtde.acceptDrop(DnDConstants.ACTION_COPY_OR_MOVE);
+               try {
+                       int action = dtde.getDropAction() ;
+                       if( (action & DnDConstants.ACTION_COPY_OR_MOVE) != 0 ) {
+                               Transferable trans = dtde.getTransferable();
+                               Object data = trans.getTransferData(MidiDeviceTree.TREE_MODEL_FLAVOR);
+                               if( data instanceof MidiConnecterListModel ) {
+                                       MidiConnecterListModel deviceModel = (MidiConnecterListModel)data;
+                                       try {
+                                               deviceModel.openDevice();
+                                       } catch( MidiUnavailableException e ) {
+                                               //
+                                               // デバイスを開くのに失敗した場合
+                                               //
+                                               //   例えば、「Microsort MIDI マッパー」と
+                                               //   「Microsoft GS Wavetable SW Synth」を
+                                               //   同時に開こうとするとここに来る。
+                                               //
+                                               dtde.dropComplete(false);
+                                               String message = "MIDIデバイス "
+                                                               + deviceModel
+                                                               +" を開けません。\n"
+                                                               + "すでに開かれているデバイスが"
+                                                               + "このデバイスを連動して開いていないか確認してください。\n\n"
+                                                               + e.getMessage();
+                                               JOptionPane.showMessageDialog(
+                                                       null, message,
+                                                       "Cannot open MIDI device",
+                                                       JOptionPane.ERROR_MESSAGE
+                                               );
+                                               return;
+                                       }
+                                       if( deviceModel.getMidiDevice().isOpen() ) {
+                                               dtde.dropComplete(true);
+                                               //
+                                               // デバイスが正常に開かれたことを確認できたら
+                                               // ドロップした場所へフレームを配置して可視化する。
+                                               //
+                                               JInternalFrame frame = getFrameOf(deviceModel);
+                                               if( frame != null ) {
+                                                       Point loc = dtde.getLocation();
+                                                       loc.translate( -frame.getWidth()/2, 0 );
+                                                       frame.setLocation(loc);
+                                                       frame.setVisible(true);
+                                               }
+                                               return;
+                                       }
+                               }
+                       }
+               }
+               catch (Exception ex) {
+                       ex.printStackTrace();
+               }
+               dtde.dropComplete(false);
+       }
+       /**
+        * 指定されたMIDIデバイスモデルに対するMIDIデバイスフレームを返します。
+        *
+        * @param deviceModel MIDIデバイスモデル
+        * @return 対応するMIDIデバイスフレーム(ない場合 null)
+        */
+       public MidiDeviceFrame getFrameOf(MidiConnecterListModel deviceModel) {
+               JInternalFrame[] frames = getAllFramesInLayer(JLayeredPane.DEFAULT_LAYER);
+               for( JInternalFrame frame : frames ) {
+                       if( ! (frame instanceof MidiDeviceFrame) )
+                               continue;
+                       MidiDeviceFrame deviceFrame = (MidiDeviceFrame)frame;
+                       if( deviceFrame.listView.getModel() == deviceModel )
+                               return deviceFrame;
+               }
+               return null;
+       }
+       private boolean isTimerStarted;
+       /**
+        * タイムスタンプを更新するタイマーを開始または停止します。
+        * @param toStart trueで開始、falseで停止
+        */
+       public void setAllDeviceTimestampTimers(boolean toStart) {
+               if( isTimerStarted == toStart ) return;
+               isTimerStarted = toStart;
+               JInternalFrame[] frames = getAllFramesInLayer(JLayeredPane.DEFAULT_LAYER);
+               for( JInternalFrame frame : frames ) {
+                       if( ! (frame instanceof MidiDeviceFrame) )
+                               continue;
+                       MidiDeviceFrame deviceFrame = (MidiDeviceFrame)frame;
+                       Timer timer = deviceFrame.timer;
+                       if( toStart ) timer.start(); else timer.stop();
+               }
+       }
+}
\ No newline at end of file
diff --git a/src/camidion/chordhelper/mididevice/MidiDeviceDialog.java b/src/camidion/chordhelper/mididevice/MidiDeviceDialog.java
new file mode 100644 (file)
index 0000000..1d1c5ba
--- /dev/null
@@ -0,0 +1,100 @@
+package camidion.chordhelper.mididevice;
+
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.WindowAdapter;
+import java.awt.event.WindowEvent;
+import java.beans.PropertyVetoException;
+import java.util.List;
+
+import javax.sound.midi.MidiDevice;
+import javax.swing.JDialog;
+import javax.swing.JEditorPane;
+import javax.swing.JScrollPane;
+import javax.swing.JSplitPane;
+import javax.swing.event.TreeSelectionEvent;
+import javax.swing.event.TreeSelectionListener;
+
+/**
+ * MIDIデバイスダイアログ (View)
+ */
+public class MidiDeviceDialog extends JDialog
+       implements ActionListener, TreeSelectionListener
+{
+       private JEditorPane deviceInfoPane;
+       private MidiDesktopPane desktopPane;
+       private MidiDeviceTree deviceTree;
+       public MidiDeviceDialog(List<MidiConnecterListModel> deviceModelList) {
+               setTitle("MIDI device connection");
+               setBounds( 300, 300, 800, 500 );
+               deviceTree = new MidiDeviceTree(new MidiDeviceTreeModel(deviceModelList));
+               deviceTree.addTreeSelectionListener(this);
+               deviceInfoPane = new JEditorPane("text/html","<html></html>") {
+                       {
+                               setEditable(false);
+                       }
+               };
+               desktopPane = new MidiDesktopPane(deviceTree);
+               add(new JSplitPane(
+                       JSplitPane.HORIZONTAL_SPLIT,
+                       new JSplitPane(
+                               JSplitPane.VERTICAL_SPLIT,
+                               new JScrollPane(deviceTree),
+                               new JScrollPane(deviceInfoPane)
+                       ){{
+                               setDividerLocation(260);
+                       }},
+                       desktopPane
+               ){{
+                       setOneTouchExpandable(true);
+                       setDividerLocation(250);
+               }});
+               addWindowListener(new WindowAdapter() {
+                       @Override
+                       public void windowClosing(WindowEvent e) {
+                               desktopPane.setAllDeviceTimestampTimers(false);
+                       }
+                       @Override
+                       public void windowActivated(WindowEvent e) {
+                               desktopPane.setAllDeviceTimestampTimers(true);
+                       }
+               });
+       }
+       @Override
+       public void actionPerformed(ActionEvent event) {
+               setVisible(true);
+       }
+       @Override
+       public void valueChanged(TreeSelectionEvent e) {
+               Object lastSelected = deviceTree.getLastSelectedPathComponent();
+               String html = "<html><head></head><body>";
+               if( lastSelected instanceof MidiConnecterListModel ) {
+                       MidiConnecterListModel deviceModel = (MidiConnecterListModel)lastSelected;
+                       MidiDevice.Info info = deviceModel.getMidiDevice().getDeviceInfo();
+                       html += "<b>"+deviceModel+"</b><br/>"
+                               + "<table border=\"1\"><tbody>"
+                               + "<tr><th>Version</th><td>"+info.getVersion()+"</td></tr>"
+                               + "<tr><th>Description</th><td>"+info.getDescription()+"</td></tr>"
+                               + "<tr><th>Vendor</th><td>"+info.getVendor()+"</td></tr>"
+                               + "</tbody></table>";
+                       MidiDeviceFrame frame = desktopPane.getFrameOf(deviceModel);
+                       if( frame != null ) {
+                               try {
+                                       frame.setSelected(true);
+                               } catch( PropertyVetoException ex ) {
+                                       ex.printStackTrace();
+                               }
+                       }
+               }
+               else if( lastSelected instanceof MidiDeviceInOutType ) {
+                       MidiDeviceInOutType ioType = (MidiDeviceInOutType)lastSelected;
+                       html += "<b>"+ioType+"</b><br/>";
+                       html += ioType.getDescription()+"<br/>";
+               }
+               else if( lastSelected != null ) {
+                       html += lastSelected.toString();
+               }
+               html += "</body></html>";
+               deviceInfoPane.setText(html);
+       }
+}
diff --git a/src/camidion/chordhelper/mididevice/MidiDeviceFrame.java b/src/camidion/chordhelper/mididevice/MidiDeviceFrame.java
new file mode 100644 (file)
index 0000000..6f71124
--- /dev/null
@@ -0,0 +1,133 @@
+package camidion.chordhelper.mididevice;
+
+import java.awt.Insets;
+import java.awt.Rectangle;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+
+import javax.sound.midi.MidiDevice;
+import javax.swing.BoxLayout;
+import javax.swing.JButton;
+import javax.swing.JInternalFrame;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.Timer;
+import javax.swing.event.InternalFrameAdapter;
+import javax.swing.event.InternalFrameEvent;
+
+/**
+ * MIDIデバイスフレームビュー
+ */
+public class MidiDeviceFrame extends JInternalFrame {
+       private static Insets ZERO_INSETS = new Insets(0,0,0,0);
+       /**
+        * デバイスの仮想MIDI端子リストビュー
+        */
+       MidiConnecterListView listView;
+       /**
+        * デバイスのタイムスタンプを更新するタイマー
+        */
+       Timer timer;
+       /**
+        * MIDIデバイスのモデルからフレームビューを構築します。
+        * @param model MIDIデバイスのTransmitter/Receiverリストモデル
+        */
+       public MidiDeviceFrame(MidiConnecterListModel model) {
+               super( null, true, true, false, false );
+               //
+               // タイトルの設定
+               String title = model.toString();
+               if( model.txSupported() ) {
+                       title = (model.rxSupported()?"[I/O] ":"[IN] ")+title;
+               }
+               else {
+                       title = (model.rxSupported()?"[OUT] ":"[No I/O] ")+title;
+               }
+               setTitle(title);
+               listView = new MidiConnecterListView(model);
+               setDefaultCloseOperation(DO_NOTHING_ON_CLOSE);
+               addInternalFrameListener(
+                       new InternalFrameAdapter() {
+                               public void internalFrameOpened(InternalFrameEvent e) {
+                                       if( ! listView.getModel().getMidiDevice().isOpen() )
+                                               setVisible(false);
+                               }
+                               public void internalFrameClosing(InternalFrameEvent e) {
+                                       MidiConnecterListModel m = listView.getModel();
+                                       m.closeDevice();
+                                       if( ! m.getMidiDevice().isOpen() )
+                                               setVisible(false);
+                               }
+                       }
+               );
+               setLayout(new BoxLayout(getContentPane(), BoxLayout.Y_AXIS));
+               add(new JScrollPane(listView));
+               add(new JPanel() {{
+                       if( listView.getModel().txSupported() ) {
+                               add(new JButton("New Tx") {{
+                                       setMargin(ZERO_INSETS);
+                                       addActionListener(new ActionListener() {
+                                               @Override
+                                               public void actionPerformed(ActionEvent event) {
+                                                       listView.getModel().getUnconnectedTransmitter();
+                                               }
+                                       });
+                               }});
+                               add(new JButton("Close Tx") {{
+                                       setMargin(ZERO_INSETS);
+                                       addActionListener(new ActionListener() {
+                                               @Override
+                                               public void actionPerformed(ActionEvent event) {
+                                                       listView.closeSelectedTransmitter();
+                                               }
+                                       });
+                               }});
+                       }
+                       add(new JLabel() {{
+                               timer = new Timer(50, new ActionListener() {
+                                       private long sec = -2;
+                                       private MidiDevice dev = listView.getModel().getMidiDevice();
+                                       @Override
+                                       public void actionPerformed(ActionEvent e) {
+                                               long usec = dev.getMicrosecondPosition();
+                                               long sec = (usec == -1 ? -1 : usec/1000000);
+                                               if( sec == this.sec ) return;
+                                               String text;
+                                               if( (this.sec = sec) == -1 )
+                                                       text = "No TimeStamp";
+                                               else
+                                                       text = String.format("TimeStamp: %02d:%02d", sec/60, sec%60);
+                                               setText(text);
+                                       }
+                               });
+                       }});
+               }});
+               setSize(250,100);
+       }
+       /**
+        * 指定されたインデックスが示す仮想MIDI端子リストの要素のセル範囲を返します。
+        *
+        * @param index リスト要素のインデックス
+        * @return セル範囲の矩形
+        */
+       public Rectangle getListCellBounds(int index) {
+               Rectangle rect = listView.getCellBounds(index,index);
+               if( rect == null )
+                       return null;
+               rect.translate(
+                       getRootPane().getX() + getContentPane().getX(),
+                       getRootPane().getY() + getContentPane().getY()
+               );
+               return rect;
+       }
+       /**
+        * 仮想MIDI端子リストの指定された要素のセル範囲を返します。
+        *
+        * @param transciver 要素となるMIDI端子(Transmitter または Receiver)
+        * @return セル範囲の矩形
+        */
+       public Rectangle getListCellBounds(AutoCloseable transciver) {
+               return getListCellBounds(listView.getModel().indexOf(transciver));
+       }
+}
\ No newline at end of file
diff --git a/src/camidion/chordhelper/mididevice/MidiDeviceInOutType.java b/src/camidion/chordhelper/mididevice/MidiDeviceInOutType.java
new file mode 100644 (file)
index 0000000..a7b3fc1
--- /dev/null
@@ -0,0 +1,17 @@
+package camidion.chordhelper.mididevice;
+
+/**
+ * MIDIデバイス入出力タイプ
+ */
+public enum MidiDeviceInOutType {
+       MIDI_OUT("MIDI output devices (MIDI synthesizer etc.)"),
+       MIDI_IN("MIDI input devices (MIDI keyboard etc.)"),
+       MIDI_IN_OUT("MIDI input/output devices (MIDI sequencer etc.)");
+       private String description;
+       private MidiDeviceInOutType(String description) {
+               this.description = description;
+       }
+       public String getDescription() {
+               return description;
+       }
+}
\ No newline at end of file
diff --git a/src/camidion/chordhelper/mididevice/MidiDeviceModelList.java b/src/camidion/chordhelper/mididevice/MidiDeviceModelList.java
new file mode 100644 (file)
index 0000000..17b4167
--- /dev/null
@@ -0,0 +1,159 @@
+package camidion.chordhelper.mididevice;
+
+import java.util.List;
+import java.util.Vector;
+
+import javax.sound.midi.MidiDevice;
+import javax.sound.midi.MidiSystem;
+import javax.sound.midi.MidiUnavailableException;
+import javax.sound.midi.Sequencer;
+import javax.sound.midi.Synthesizer;
+
+import camidion.chordhelper.ChordHelperApplet;
+import camidion.chordhelper.midieditor.MidiSequenceEditor;
+
+/**
+ * MIDIデバイスモデルリスト
+ */
+public class MidiDeviceModelList extends Vector<MidiConnecterListModel> {
+       /**
+        * MIDIエディタ
+        */
+       public MidiSequenceEditor editorDialog;
+       /**
+        * MIDIエディタモデル
+        */
+       private MidiConnecterListModel editorDialogModel;
+       /**
+        * MIDIシンセサイザーモデル
+        */
+       private MidiConnecterListModel synthModel;
+       /**
+        * 最初のMIDI出力
+        */
+       private MidiConnecterListModel firstMidiOutModel;
+       /**
+        * MIDIデバイスモデルリストを生成します。
+        * @param vmdList 仮想MIDIデバイスのリスト
+        */
+       public MidiDeviceModelList(List<VirtualMidiDevice> vmdList) {
+               MidiDevice.Info[] devInfos = MidiSystem.getMidiDeviceInfo();
+               MidiConnecterListModel guiModels[] = new MidiConnecterListModel[vmdList.size()];
+               MidiConnecterListModel firstMidiInModel = null;
+               for( int i=0; i<vmdList.size(); i++ )
+                       guiModels[i] = addMidiDevice(vmdList.get(i));
+               Sequencer sequencer;
+               try {
+                       sequencer = MidiSystem.getSequencer(false);
+                       sequencerModel = (MidiSequencerModel)addMidiDevice(sequencer);
+               } catch( MidiUnavailableException e ) {
+                       System.out.println(
+                               ChordHelperApplet.VersionInfo.NAME +
+                               " : MIDI sequencer unavailable"
+                       );
+                       e.printStackTrace();
+               }
+               editorDialog = new MidiSequenceEditor(sequencerModel);
+               editorDialogModel = addMidiDevice(editorDialog.getVirtualMidiDevice());
+               for( MidiDevice.Info info : devInfos ) {
+                       MidiDevice device;
+                       try {
+                               device = MidiSystem.getMidiDevice(info);
+                       } catch( MidiUnavailableException e ) {
+                               e.printStackTrace(); continue;
+                       }
+                       if( device instanceof Sequencer ) continue;
+                       if( device instanceof Synthesizer ) {
+                               try {
+                                       synthModel = addMidiDevice(MidiSystem.getSynthesizer());
+                               } catch( MidiUnavailableException e ) {
+                                       System.out.println(
+                                               ChordHelperApplet.VersionInfo.NAME +
+                                               " : Java internal MIDI synthesizer unavailable"
+                                       );
+                                       e.printStackTrace();
+                               }
+                               continue;
+                       }
+                       MidiConnecterListModel m = addMidiDevice(device);
+                       if( m.rxSupported() && firstMidiOutModel == null )
+                               firstMidiOutModel = m;
+                       if( m.txSupported() && firstMidiInModel == null )
+                               firstMidiInModel = m;
+               }
+               // デバイスを開く。
+               //   NOTE: 必ず MIDI OUT Rx デバイスを先に開くこと。
+               //
+               //   そうすれば、後から開いた MIDI IN Tx デバイスからの
+               //   タイムスタンプのほうが「若く」なる。これにより、
+               //   先に開かれ「少し歳を食った」Rx デバイスは
+               //   「信号が遅れてやってきた」と認識するので、
+               //   遅れを取り戻そうとして即座に音を出してくれる。
+               //
+               //   開く順序が逆になると「進みすぎるから遅らせよう」として
+               //   無用なレイテンシーが発生する原因になる。
+               try {
+                       MidiConnecterListModel openModels[] = {
+                               synthModel,
+                               firstMidiOutModel,
+                               sequencerModel,
+                               firstMidiInModel,
+                               editorDialogModel,
+                       };
+                       for( MidiConnecterListModel m : openModels ) {
+                               if( m != null ) m.openDevice();
+                       }
+                       for( MidiConnecterListModel m : guiModels ) {
+                               m.openDevice();
+                       }
+               } catch( MidiUnavailableException ex ) {
+                       ex.printStackTrace();
+               }
+               // 初期接続
+               //
+               for( MidiConnecterListModel mtx : guiModels ) {
+                       for( MidiConnecterListModel mrx : guiModels )
+                               mtx.connectToReceiverOf(mrx);
+                       mtx.connectToReceiverOf(sequencerModel);
+                       mtx.connectToReceiverOf(synthModel);
+                       mtx.connectToReceiverOf(firstMidiOutModel);
+               }
+               if( firstMidiInModel != null ) {
+                       for( MidiConnecterListModel m : guiModels )
+                               firstMidiInModel.connectToReceiverOf(m);
+                       firstMidiInModel.connectToReceiverOf(sequencerModel);
+                       firstMidiInModel.connectToReceiverOf(synthModel);
+                       firstMidiInModel.connectToReceiverOf(firstMidiOutModel);
+               }
+               if( sequencerModel != null ) {
+                       for( MidiConnecterListModel m : guiModels )
+                               sequencerModel.connectToReceiverOf(m);
+                       sequencerModel.connectToReceiverOf(synthModel);
+                       sequencerModel.connectToReceiverOf(firstMidiOutModel);
+               }
+               if( editorDialogModel != null ) {
+                       editorDialogModel.connectToReceiverOf(synthModel);
+                       editorDialogModel.connectToReceiverOf(firstMidiOutModel);
+               }
+       }
+       /**
+        * このデバイスモデルリストに登録されたMIDIシーケンサーモデルを返します。
+        * @return MIDIシーケンサーモデル
+        */
+       public MidiSequencerModel getSequencerModel() { return sequencerModel; }
+       private MidiSequencerModel sequencerModel;
+       /**
+        * 指定のMIDIデバイスからMIDIデバイスモデルを生成して追加します。
+        * @param device MIDIデバイス
+        * @return 生成されたMIDIデバイスモデル
+        */
+       private MidiConnecterListModel addMidiDevice(MidiDevice device) {
+               MidiConnecterListModel m;
+               if( device instanceof Sequencer )
+                       m = new MidiSequencerModel(this,(Sequencer)device,this);
+               else
+                       m = new MidiConnecterListModel(device,this);
+               addElement(m);
+               return m;
+       }
+}
\ No newline at end of file
diff --git a/src/camidion/chordhelper/mididevice/MidiDeviceTree.java b/src/camidion/chordhelper/mididevice/MidiDeviceTree.java
new file mode 100644 (file)
index 0000000..645b858
--- /dev/null
@@ -0,0 +1,97 @@
+package camidion.chordhelper.mididevice;
+
+import java.awt.Component;
+import java.awt.datatransfer.DataFlavor;
+import java.awt.datatransfer.Transferable;
+import java.awt.dnd.DnDConstants;
+import java.awt.dnd.DragGestureEvent;
+import java.awt.dnd.DragGestureListener;
+import java.awt.dnd.DragSource;
+
+import javax.swing.JTree;
+import javax.swing.event.InternalFrameEvent;
+import javax.swing.event.InternalFrameListener;
+import javax.swing.tree.DefaultTreeCellRenderer;
+import javax.swing.tree.TreeModel;
+
+/**
+ * MIDIデバイスツリービュー
+ */
+public class MidiDeviceTree extends JTree
+       implements Transferable, DragGestureListener, InternalFrameListener
+{
+       /**
+        * MIDIデバイスツリービューを構築します。
+        * @param model このビューにデータを提供するモデル
+        */
+       public MidiDeviceTree(MidiDeviceTreeModel model) {
+               super(model);
+        (new DragSource()).createDefaultDragGestureRecognizer(
+               this, DnDConstants.ACTION_COPY_OR_MOVE, this
+        );
+        setCellRenderer(new DefaultTreeCellRenderer() {
+               @Override
+               public Component getTreeCellRendererComponent(
+                       JTree tree, Object value,
+                       boolean selected, boolean expanded, boolean leaf, int row,
+                       boolean hasFocus
+               ) {
+                       super.getTreeCellRendererComponent(
+                               tree, value, selected, expanded, leaf, row, hasFocus
+                       );
+                       if(leaf) {
+                                       setIcon(MidiConnecterListView.MIDI_CONNECTER_ICON);
+                                       setDisabledIcon(MidiConnecterListView.MIDI_CONNECTER_ICON);
+                               MidiConnecterListModel listModel = (MidiConnecterListModel)value;
+                               setEnabled( ! listModel.getMidiDevice().isOpen() );
+                       }
+                       return this;
+               }
+       });
+       }
+       /**
+        * このデバイスツリーからドラッグされるデータフレーバ
+        */
+       public static final DataFlavor
+               TREE_MODEL_FLAVOR = new DataFlavor(TreeModel.class, "TreeModel");
+       private static final DataFlavor
+               TREE_MODE_FLAVORS[] = {TREE_MODEL_FLAVOR};
+       @Override
+       public Object getTransferData(DataFlavor flavor) {
+               return getLastSelectedPathComponent();
+       }
+       @Override
+       public DataFlavor[] getTransferDataFlavors() {
+               return TREE_MODE_FLAVORS;
+       }
+       @Override
+       public boolean isDataFlavorSupported(DataFlavor flavor) {
+               return flavor.equals(TREE_MODEL_FLAVOR);
+       }
+       @Override
+       public void dragGestureRecognized(DragGestureEvent dge) {
+               int action = dge.getDragAction();
+               if( (action & DnDConstants.ACTION_COPY_OR_MOVE) != 0 ) {
+                       dge.startDrag(DragSource.DefaultMoveDrop, this, null);
+               }
+       }
+       @Override
+       public void internalFrameOpened(InternalFrameEvent e) {}
+       /**
+        *      MidiDeviceFrame のクローズ処理中に再描画リクエストを送ります。
+        */
+       @Override
+       public void internalFrameClosing(InternalFrameEvent e) {
+               repaint();
+       }
+       @Override
+       public void internalFrameClosed(InternalFrameEvent e) {}
+       @Override
+       public void internalFrameIconified(InternalFrameEvent e) {}
+       @Override
+       public void internalFrameDeiconified(InternalFrameEvent e) {}
+       @Override
+       public void internalFrameActivated(InternalFrameEvent e) {}
+       @Override
+       public void internalFrameDeactivated(InternalFrameEvent e) {}
+}
\ No newline at end of file
diff --git a/src/camidion/chordhelper/mididevice/MidiDeviceTreeModel.java b/src/camidion/chordhelper/mididevice/MidiDeviceTreeModel.java
new file mode 100644 (file)
index 0000000..dd0a846
--- /dev/null
@@ -0,0 +1,100 @@
+package camidion.chordhelper.mididevice;
+
+import java.util.List;
+
+import javax.swing.event.EventListenerList;
+import javax.swing.event.TreeModelEvent;
+import javax.swing.event.TreeModelListener;
+import javax.swing.tree.TreeModel;
+import javax.swing.tree.TreePath;
+
+/**
+ * MIDIデバイスツリーモデル
+ */
+public class MidiDeviceTreeModel implements TreeModel {
+       List<MidiConnecterListModel> deviceModelList;
+       public MidiDeviceTreeModel(List<MidiConnecterListModel> deviceModelList) {
+               this.deviceModelList = deviceModelList;
+       }
+       @Override
+       public Object getRoot() { return "MIDI devices"; }
+       @Override
+       public Object getChild(Object parent, int index) {
+               if( parent == getRoot() ) {
+                       return MidiDeviceInOutType.values()[index];
+               }
+               if( parent instanceof MidiDeviceInOutType ) {
+                       MidiDeviceInOutType ioType = (MidiDeviceInOutType)parent;
+                       for( MidiConnecterListModel deviceModel : deviceModelList )
+                               if( deviceModel.getMidiDeviceInOutType() == ioType ) {
+                                       if( index == 0 )
+                                               return deviceModel;
+                                       index--;
+                               }
+               }
+               return null;
+       }
+       @Override
+       public int getChildCount(Object parent) {
+               if( parent == getRoot() ) {
+                       return MidiDeviceInOutType.values().length;
+               }
+               int childCount = 0;
+               if( parent instanceof MidiDeviceInOutType ) {
+                       MidiDeviceInOutType ioType = (MidiDeviceInOutType)parent;
+                       for( MidiConnecterListModel deviceModel : deviceModelList )
+                               if( deviceModel.getMidiDeviceInOutType() == ioType )
+                                       childCount++;
+               }
+               return childCount;
+       }
+       @Override
+       public int getIndexOfChild(Object parent, Object child) {
+               if( parent == getRoot() ) {
+                       if( child instanceof MidiDeviceInOutType ) {
+                               MidiDeviceInOutType ioType = (MidiDeviceInOutType)child;
+                               return ioType.ordinal();
+                       }
+               }
+               if( parent instanceof MidiDeviceInOutType ) {
+                       MidiDeviceInOutType ioType = (MidiDeviceInOutType)parent;
+                       int index = 0;
+                       for( MidiConnecterListModel deviceModel : deviceModelList ) {
+                               if( deviceModel.getMidiDeviceInOutType() == ioType ) {
+                                       if( deviceModel == child )
+                                               return index;
+                                       index++;
+                               }
+                       }
+               }
+               return -1;
+       }
+       @Override
+       public boolean isLeaf(Object node) {
+               return node instanceof MidiConnecterListModel;
+       }
+       @Override
+       public void valueForPathChanged(TreePath path, Object newValue) {}
+       //
+       private EventListenerList listenerList = new EventListenerList();
+       @Override
+       public void addTreeModelListener(TreeModelListener listener) {
+               listenerList.add(TreeModelListener.class, listener);
+       }
+       @Override
+       public void removeTreeModelListener(TreeModelListener listener) {
+               listenerList.remove(TreeModelListener.class, listener);
+       }
+       public void fireTreeNodesChanged(
+               Object source, Object[] path, int[] childIndices, Object[] children
+       ) {
+               Object[] listeners = listenerList.getListenerList();
+               for (int i = listeners.length-2; i>=0; i-=2) {
+                       if (listeners[i]==TreeModelListener.class) {
+                               ((TreeModelListener)listeners[i+1]).treeNodesChanged(
+                                       new TreeModelEvent(source,path,childIndices,children)
+                               );
+                       }
+               }
+       }
+}
\ No newline at end of file
diff --git a/src/camidion/chordhelper/mididevice/MidiSequencerModel.java b/src/camidion/chordhelper/mididevice/MidiSequencerModel.java
new file mode 100644 (file)
index 0000000..6bddec0
--- /dev/null
@@ -0,0 +1,392 @@
+package camidion.chordhelper.mididevice;
+
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.sound.midi.InvalidMidiDataException;
+import javax.sound.midi.Sequence;
+import javax.sound.midi.Sequencer;
+import javax.swing.AbstractAction;
+import javax.swing.Action;
+import javax.swing.BoundedRangeModel;
+import javax.swing.ComboBoxModel;
+import javax.swing.DefaultBoundedRangeModel;
+import javax.swing.DefaultComboBoxModel;
+import javax.swing.Icon;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+import javax.swing.event.EventListenerList;
+import javax.swing.event.ListDataEvent;
+import javax.swing.event.ListDataListener;
+
+import camidion.chordhelper.ButtonIcon;
+import camidion.chordhelper.midieditor.SequenceTickIndex;
+import camidion.chordhelper.midieditor.SequenceTrackListTableModel;
+
+/**
+ * MIDIシーケンサモデル
+ */
+public class MidiSequencerModel extends MidiConnecterListModel
+       implements BoundedRangeModel
+{
+       /**
+        * MIDIシーケンサモデルを構築します。
+        * @param deviceModelList 親のMIDIデバイスモデルリスト
+        * @param sequencer シーケンサーMIDIデバイス
+        * @param modelList MIDIコネクタリストモデルのリスト(タイムスタンプリセット対象)
+        */
+       public MidiSequencerModel(
+               MidiDeviceModelList deviceModelList,
+               Sequencer sequencer,
+               List<MidiConnecterListModel> modelList
+       ) {
+               super(sequencer, modelList);
+               this.deviceModelList = deviceModelList;
+       }
+       /**
+        * このシーケンサーの再生スピード調整モデル
+        */
+       public BoundedRangeModel speedSliderModel = new DefaultBoundedRangeModel(0, 0, -7, 7) {{
+               addChangeListener(
+                       new ChangeListener() {
+                               @Override
+                               public void stateChanged(ChangeEvent e) {
+                                       int val = getValue();
+                                       getSequencer().setTempoFactor((float)(
+                                               val == 0 ? 1.0 : Math.pow( 2.0, ((double)val)/12.0 )
+                                       ));
+                               }
+                       }
+               );
+       }};
+       /**
+        * MIDIシーケンサを返します。
+        * @return MIDIシーケンサ
+        */
+       public Sequencer getSequencer() { return (Sequencer)device; }
+       /**
+        * 開始終了アクション
+        */
+       public StartStopAction startStopAction = new StartStopAction();
+       /**
+        * 開始終了アクション
+        */
+       class StartStopAction extends AbstractAction {
+               private Map<Boolean,Icon> iconMap = new HashMap<Boolean,Icon>() {
+                       {
+                               put(Boolean.FALSE, new ButtonIcon(ButtonIcon.PLAY_ICON));
+                               put(Boolean.TRUE, new ButtonIcon(ButtonIcon.PAUSE_ICON));
+                       }
+               };
+               {
+                       putValue(
+                               SHORT_DESCRIPTION,
+                               "Start/Stop recording or playing - 録音または再生の開始/停止"
+                       );
+                       setRunning(false);
+               }
+               @Override
+               public void actionPerformed(ActionEvent event) {
+                       if(timeRangeUpdater.isRunning()) stop(); else start();
+               }
+               /**
+                * 開始されているかどうかを設定します。
+                * @param isRunning 開始されていたらtrue
+                */
+               public void setRunning(boolean isRunning) {
+                       putValue(LARGE_ICON_KEY, iconMap.get(isRunning));
+                       putValue(SELECTED_KEY, isRunning);
+               }
+       }
+       /**
+        * シーケンサに合わせてミリ秒位置を更新するタイマー
+        */
+       private javax.swing.Timer timeRangeUpdater = new javax.swing.Timer(
+               20,
+               new ActionListener(){
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               if( valueIsAdjusting || ! getSequencer().isRunning() ) {
+                                       // 手動で移動中の場合や、シーケンサが止まっている場合は、
+                                       // タイマーによる更新は不要
+                                       return;
+                               }
+                               // リスナーに読み込みを促す
+                               fireStateChanged();
+                       }
+               }
+       );
+       /**
+        * MIDIデバイスモデルリスト(タイムスタンプリセット対象)
+        */
+       private MidiDeviceModelList deviceModelList;
+       /**
+        * このモデルのMIDIシーケンサを開始します。
+        *
+        * <p>録音するMIDIチャンネルがMIDIエディタで指定されている場合、
+        * 録音スタート時のタイムスタンプが正しく0になるよう、
+        * 各MIDIデバイスのタイムスタンプをすべてリセットします。
+        * </p>
+        */
+       public void start() {
+               Sequencer sequencer = getSequencer();
+               if( ! sequencer.isOpen() || sequencer.getSequence() == null ) {
+                       timeRangeUpdater.stop();
+                       startStopAction.setRunning(false);
+                       return;
+               }
+               startStopAction.setRunning(true);
+               timeRangeUpdater.start();
+               SequenceTrackListTableModel sequenceTableModel = getSequenceTrackListTableModel();
+               if( sequenceTableModel != null && sequenceTableModel.hasRecordChannel() ) {
+                       for(MidiConnecterListModel m : deviceModelList)
+                               m.resetMicrosecondPosition();
+                       System.gc();
+                       sequencer.startRecording();
+               }
+               else {
+                       System.gc();
+                       sequencer.start();
+               }
+               fireStateChanged();
+       }
+       /**
+        * このモデルのMIDIシーケンサを停止します。
+        */
+       public void stop() {
+               Sequencer sequencer = getSequencer();
+               if(sequencer.isOpen()) sequencer.stop();
+               timeRangeUpdater.stop();
+               startStopAction.setRunning(false);
+               fireStateChanged();
+       }
+       /**
+        * {@link Sequencer#getMicrosecondLength()} と同じです。
+        * @return マイクロ秒単位でのシーケンスの長さ
+        */
+       public long getMicrosecondLength() {
+               //
+               // Sequencer.getMicrosecondLength() returns NEGATIVE value
+               //  when over 0x7FFFFFFF microseconds (== 35.7913941166666... minutes),
+               //  should be corrected when negative
+               //
+               long usLength = getSequencer().getMicrosecondLength();
+               return usLength < 0 ? 0x100000000L + usLength : usLength ;
+       }
+       @Override
+       public int getMaximum() { return (int)(getMicrosecondLength()/1000L); }
+       @Override
+       public void setMaximum(int newMaximum) {}
+       @Override
+       public int getMinimum() { return 0; }
+       @Override
+       public void setMinimum(int newMinimum) {}
+       @Override
+       public int getExtent() { return 0; }
+       @Override
+       public void setExtent(int newExtent) {}
+       /**
+        * {@link Sequencer#getMicrosecondPosition()} と同じです。
+        * @return マイクロ秒単位での現在の位置
+        */
+       public long getMicrosecondPosition() {
+               long usPosition = getSequencer().getMicrosecondPosition();
+               return usPosition < 0 ? 0x100000000L + usPosition : usPosition ;
+       }
+       @Override
+       public int getValue() { return (int)(getMicrosecondPosition()/1000L); }
+       @Override
+       public void setValue(int newValue) {
+               getSequencer().setMicrosecondPosition(1000L * (long)newValue);
+               fireStateChanged();
+       }
+       /**
+        * 値調整中のときtrue
+        */
+       private boolean valueIsAdjusting = false;
+       @Override
+       public boolean getValueIsAdjusting() {
+               return valueIsAdjusting;
+       }
+       @Override
+       public void setValueIsAdjusting(boolean valueIsAdjusting) {
+               this.valueIsAdjusting = valueIsAdjusting;
+       }
+       @Override
+       public void setRangeProperties(int value, int extent, int min, int max, boolean valueIsAdjusting) {
+               getSequencer().setMicrosecondPosition(1000L * (long)value);
+               setValueIsAdjusting(valueIsAdjusting);
+               fireStateChanged();
+       }
+       /**
+        * イベントリスナーのリスト
+        */
+       protected EventListenerList listenerList = new EventListenerList();
+       /**
+        * {@inheritDoc}
+        * <p>このシーケンサーの再生時間位置変更通知を受けるリスナーを追加します。
+        * </p>
+        */
+       @Override
+       public void addChangeListener(ChangeListener listener) {
+               listenerList.add(ChangeListener.class, listener);
+       }
+       /**
+        * {@inheritDoc}
+        * <p>このシーケンサーの再生時間位置変更通知を受けるリスナーを除去します。
+        * </p>
+        */
+       @Override
+       public void removeChangeListener(ChangeListener listener) {
+               listenerList.remove(ChangeListener.class, listener);
+       }
+       /**
+        * 秒位置が変わったことをリスナーに通知します。
+        * <p>登録中のすべての {@link ChangeListener} について
+        * {@link ChangeListener#stateChanged(ChangeEvent)}
+        * を呼び出すことによって状態の変化を通知します。
+        * </p>
+        */
+       public void fireStateChanged() {
+               Object[] listeners = listenerList.getListenerList();
+               for (int i = listeners.length-2; i>=0; i-=2) {
+                       if (listeners[i]==ChangeListener.class) {
+                               ((ChangeListener)listeners[i+1]).stateChanged(new ChangeEvent(this));
+                       }
+               }
+       }
+       /**
+        * MIDIトラックリストテーブルモデル
+        */
+       private SequenceTrackListTableModel sequenceTableModel = null;
+       /**
+        * このシーケンサーに現在ロードされているシーケンスのMIDIトラックリストテーブルモデルを返します。
+        * @return MIDIトラックリストテーブルモデル(何もロードされていなければnull)
+        */
+       public SequenceTrackListTableModel getSequenceTrackListTableModel() {
+               return sequenceTableModel;
+       }
+       /**
+        * MIDIトラックリストテーブルモデルを
+        * このシーケンサーモデルにセットします。
+        * @param sequenceTableModel MIDIトラックリストテーブルモデル
+        * @return 成功したらtrue
+        */
+       public boolean setSequenceTrackListTableModel(
+               SequenceTrackListTableModel sequenceTableModel
+       ) {
+               // javax.sound.midi:Sequencer.setSequence() のドキュメントにある
+               // 「このメソッドは、Sequencer が閉じている場合でも呼び出すことができます。 」
+               // という記述は、null をセットする場合には当てはまらない。
+               // 連鎖的に stop() が呼ばれるために IllegalStateException sequencer not open が出る。
+               // この現象を回避するため、あらかじめチェックしてから setSequence() を呼び出している。
+               //
+               if( sequenceTableModel != null || getSequencer().isOpen() ) {
+                       Sequence sequence = null;
+                       if( sequenceTableModel != null )
+                               sequence = sequenceTableModel.getSequence();
+                       try {
+                               getSequencer().setSequence(sequence);
+                       } catch ( InvalidMidiDataException e ) {
+                               e.printStackTrace();
+                               return false;
+                       }
+               }
+               if( this.sequenceTableModel != null ) {
+                       this.sequenceTableModel.fireTableDataChanged();
+               }
+               if( sequenceTableModel != null ) {
+                       sequenceTableModel.fireTableDataChanged();
+               }
+               this.sequenceTableModel = sequenceTableModel;
+               fireStateChanged();
+               return true;
+       }
+
+       /**
+        * 小節単位で位置を移動します。
+        * @param measureOffset 何小節進めるか(戻したいときは負数を指定)
+        */
+       private void moveMeasure(int measureOffset) {
+               if( measureOffset == 0 || sequenceTableModel == null )
+                       return;
+               SequenceTickIndex seqIndex = sequenceTableModel.getSequenceTickIndex();
+               Sequencer sequencer = getSequencer();
+               int measurePosition = seqIndex.tickToMeasure(sequencer.getTickPosition());
+               long newTickPosition = seqIndex.measureToTick(measurePosition + measureOffset);
+               if( newTickPosition < 0 ) {
+                       // 下限
+                       newTickPosition = 0;
+               }
+               else {
+                       long tickLength = sequencer.getTickLength();
+                       if( newTickPosition > tickLength ) {
+                               // 上限
+                               newTickPosition = tickLength - 1;
+                       }
+               }
+               sequencer.setTickPosition(newTickPosition);
+               fireStateChanged();
+       }
+       /**
+        * 1小節戻るアクション
+        */
+       public Action moveBackwardAction = new AbstractAction() {
+               {
+                       putValue(SHORT_DESCRIPTION, "Move backward 1 measure - 1小節戻る");
+                       putValue(LARGE_ICON_KEY, new ButtonIcon(ButtonIcon.BACKWARD_ICON));
+               }
+               @Override
+               public void actionPerformed(ActionEvent event) { moveMeasure(-1); }
+       };
+       /**
+        *1小節進むアクション
+        */
+       public Action moveForwardAction = new AbstractAction() {
+               {
+                       putValue(SHORT_DESCRIPTION, "Move forward 1 measure - 1小節進む");
+                       putValue(LARGE_ICON_KEY, new ButtonIcon(ButtonIcon.FORWARD_ICON));
+               }
+               @Override
+               public void actionPerformed(ActionEvent event) { moveMeasure(1); }
+       };
+       /**
+        * マスター同期モードのコンボボックスモデル
+        */
+       public ComboBoxModel<Sequencer.SyncMode> masterSyncModeModel =
+               new DefaultComboBoxModel<Sequencer.SyncMode>(getSequencer().getMasterSyncModes()) {{
+                       addListDataListener(new ListDataListener() {
+                               @Override
+                               public void intervalAdded(ListDataEvent e) { }
+                               @Override
+                               public void intervalRemoved(ListDataEvent e) { }
+                               @Override
+                               public void contentsChanged(ListDataEvent e) {
+                                       getSequencer().setMasterSyncMode(
+                                               (Sequencer.SyncMode)getSelectedItem()
+                                       );
+                               }
+                       });
+               }};
+       /**
+        * スレーブ同期モードのコンボボックスモデル
+        */
+       public ComboBoxModel<Sequencer.SyncMode> slaveSyncModeModel =
+               new DefaultComboBoxModel<Sequencer.SyncMode>(getSequencer().getSlaveSyncModes()) {{
+                       addListDataListener(new ListDataListener() {
+                               @Override
+                               public void intervalAdded(ListDataEvent e) { }
+                               @Override
+                               public void intervalRemoved(ListDataEvent e) { }
+                               @Override
+                               public void contentsChanged(ListDataEvent e) {
+                                       getSequencer().setSlaveSyncMode(
+                                               (Sequencer.SyncMode)getSelectedItem()
+                                       );
+                               }
+                       });
+               }};
+}
\ No newline at end of file
diff --git a/src/camidion/chordhelper/mididevice/SequencerMeasureView.java b/src/camidion/chordhelper/mididevice/SequencerMeasureView.java
new file mode 100644 (file)
index 0000000..87502cb
--- /dev/null
@@ -0,0 +1,95 @@
+package camidion.chordhelper.mididevice;
+
+import java.awt.Color;
+
+import javax.sound.midi.Sequencer;
+import javax.swing.BoxLayout;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+
+import camidion.chordhelper.midieditor.SequenceTickIndex;
+import camidion.chordhelper.midieditor.SequenceTrackListTableModel;
+
+/**
+ * 小節表示ビュー
+ */
+public class SequencerMeasureView extends JPanel implements ChangeListener {
+       private SequencerMeasureView.MeasurePositionLabel measurePositionLabel;
+       private SequencerMeasureView.MeasureLengthLabel measureLengthLabel;
+       private MidiSequencerModel model;
+       /**
+        * シーケンサの現在の小節位置を表示するビューを構築します。
+        * @param model スライダー用の時間範囲データモデル
+        */
+       public SequencerMeasureView(MidiSequencerModel model) {
+               setLayout(new BoxLayout(this, BoxLayout.X_AXIS));
+               add(measurePositionLabel = new MeasurePositionLabel());
+               add(measureLengthLabel = new MeasureLengthLabel());
+               (this.model = model).addChangeListener(this);
+       }
+       @Override
+       public void stateChanged(ChangeEvent e) {
+               Sequencer sequencer = model.getSequencer();
+               SequenceTrackListTableModel sequenceTableModel = model.getSequenceTrackListTableModel();
+               SequenceTickIndex tickIndex = (
+                       sequenceTableModel == null ? null : sequenceTableModel.getSequenceTickIndex()
+               );
+               if( ! sequencer.isRunning() || sequencer.isRecording() ) {
+                       // 停止中または録音中の場合、長さが変わることがあるので表示を更新
+                       if( tickIndex == null ) {
+                               measureLengthLabel.setMeasure(0);
+                       }
+                       else {
+                               long tickLength = sequencer.getTickLength();
+                               int measureLength = tickIndex.tickToMeasure(tickLength);
+                               measureLengthLabel.setMeasure(measureLength);
+                       }
+               }
+               // 小節位置の表示を更新
+               if( tickIndex == null ) {
+                       measurePositionLabel.setMeasure(0, 0);
+               }
+               else {
+                       long tickPosition = sequencer.getTickPosition();
+                       int measurePosition = tickIndex.tickToMeasure(tickPosition);
+                       measurePositionLabel.setMeasure(measurePosition, tickIndex.lastBeat);
+               }
+       }
+       private static abstract class MeasureLabel extends JLabel {
+               protected int measure = -1;
+               public boolean setMeasure(int measure) {
+                       if( this.measure == measure ) return false;
+                       this.measure = measure;
+                       return true;
+               }
+       }
+       private static class MeasurePositionLabel extends SequencerMeasureView.MeasureLabel {
+               protected int beat = 0;
+               public MeasurePositionLabel() {
+                       setFont( getFont().deriveFont(getFont().getSize2D() + 4) );
+                       setForeground( new Color(0x80,0x00,0x00) );
+                       setText("0001:01");
+                       setToolTipText("Measure:beat position - 何小節目:何拍目");
+               }
+               public boolean setMeasure(int measure, int beat) {
+                       if( ! super.setMeasure(measure) && this.beat == beat )
+                               return false;
+                       setText(String.format("%04d:%02d", measure+1, beat+1));
+                       return true;
+               }
+       }
+       private static class MeasureLengthLabel extends SequencerMeasureView.MeasureLabel {
+               public MeasureLengthLabel() {
+                       setText("/0000");
+                       setToolTipText("Measure length - 小節の数");
+               }
+               public boolean setMeasure(int measure) {
+                       if( ! super.setMeasure(measure) )
+                               return false;
+                       setText(String.format("/%04d", measure));
+                       return true;
+               }
+       }
+}
\ No newline at end of file
diff --git a/src/camidion/chordhelper/mididevice/SequencerTimeView.java b/src/camidion/chordhelper/mididevice/SequencerTimeView.java
new file mode 100644 (file)
index 0000000..6f5dea5
--- /dev/null
@@ -0,0 +1,72 @@
+package camidion.chordhelper.mididevice;
+
+import java.awt.Color;
+
+import javax.swing.BoxLayout;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+
+/**
+ * シーケンサの現在位置(分:秒)を表示するビュー
+ */
+public class SequencerTimeView extends JPanel implements ChangeListener {
+       /**
+        * シーケンサの現在位置(分:秒)を表示するビューを構築します。
+        * @param model MIDIシーケンサモデル
+        */
+       public SequencerTimeView(MidiSequencerModel model) {
+               setLayout(new BoxLayout(this, BoxLayout.X_AXIS));
+               add(timePositionLabel);
+               add(timeLengthLabel);
+               (this.model = model).addChangeListener(this);
+       }
+       @Override
+       public void stateChanged(ChangeEvent e) {
+               timeLengthLabel.setTimeInSecond(model.getMaximum()/1000);
+               timePositionLabel.setTimeInSecond(model.getValue()/1000);
+       }
+       private MidiSequencerModel model;
+       private SequencerTimeView.TimeLabel timePositionLabel = new TimePositionLabel();
+       private SequencerTimeView.TimeLabel timeLengthLabel = new TimeLengthLabel();
+       private static abstract class TimeLabel extends JLabel {
+               /**
+                * 時間の値(秒)
+                */
+               private int valueInSec;
+               /**
+                * 時間の値を秒単位で設定します。
+                * @param sec 秒単位の時間
+                */
+               public void setTimeInSecond(int sec) {
+                       if(valueInSec != sec) setText(toTimeString(valueInSec = sec));
+               }
+               /**
+                * 時間の値を文字列に変換します。
+                * @param sec 秒単位の時間
+                * @return 変換結果(分:秒)
+                */
+               protected String toTimeString(int sec) {
+                       return String.format("%02d:%02d", sec/60, sec%60);
+               }
+       }
+       private static class TimePositionLabel extends SequencerTimeView.TimeLabel {
+               public TimePositionLabel() {
+                       setFont( getFont().deriveFont(getFont().getSize2D() + 4) );
+                       setForeground( new Color(0x80,0x00,0x00) );
+                       setToolTipText("Time position - 現在位置(分:秒)");
+                       setText(toTimeString(0));
+               }
+       }
+       private static class TimeLengthLabel extends SequencerTimeView.TimeLabel {
+               public TimeLengthLabel() {
+                       setToolTipText("Time length - 曲の長さ(分:秒)");
+                       setText(toTimeString(0));
+               }
+               @Override
+               protected String toTimeString(int sec) {
+                       return "/"+super.toTimeString(sec);
+               }
+       }
+}
\ No newline at end of file
diff --git a/src/camidion/chordhelper/mididevice/VirtualMidiDevice.java b/src/camidion/chordhelper/mididevice/VirtualMidiDevice.java
new file mode 100644 (file)
index 0000000..1507105
--- /dev/null
@@ -0,0 +1,21 @@
+package camidion.chordhelper.mididevice;
+
+import javax.sound.midi.MidiChannel;
+import javax.sound.midi.MidiDevice;
+import javax.sound.midi.MidiMessage;
+
+/**
+ * 仮想MIDIデバイス
+ */
+public interface VirtualMidiDevice extends MidiDevice {
+       /**
+        * この仮想MIDIデバイスのMIDIチャンネルを返します。
+        * @return MIDIチャンネル
+        */
+       MidiChannel[] getChannels();
+       /**
+        * MIDIメッセージを送信します。
+        * @param msg MIDIメッセージ
+        */
+       void sendMidiMessage(MidiMessage msg);
+}
diff --git a/src/camidion/chordhelper/midieditor/Base64Dialog.java b/src/camidion/chordhelper/midieditor/Base64Dialog.java
new file mode 100644 (file)
index 0000000..abf7769
--- /dev/null
@@ -0,0 +1,175 @@
+package camidion.chordhelper.midieditor;
+import java.awt.Dimension;
+import java.awt.event.ActionEvent;
+import java.io.IOException;
+import java.util.regex.Pattern;
+
+import javax.sound.midi.InvalidMidiDataException;
+import javax.swing.AbstractAction;
+import javax.swing.Action;
+import javax.swing.Box;
+import javax.swing.BoxLayout;
+import javax.swing.JButton;
+import javax.swing.JDialog;
+import javax.swing.JLabel;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.JTextArea;
+
+import org.apache.commons.codec.binary.Base64;
+
+import camidion.chordhelper.ButtonIcon;
+import camidion.chordhelper.ChordHelperApplet;
+
+/**
+ * Base64テキスト入力ダイアログ
+ */
+public class Base64Dialog extends JDialog {
+       private Base64TextArea base64TextArea = new Base64TextArea(8,56);
+       private MidiSequenceEditor midiEditor;
+       /**
+        * Base64デコードアクション
+        */
+       public Action addBase64Action = new AbstractAction(
+               "Base64 Decode & Add to PlayList",
+               new ButtonIcon(ButtonIcon.EJECT_ICON)
+       ) {
+               {
+                       putValue(
+                               Action.SHORT_DESCRIPTION,
+                               "Base64デコードして、プレイリストへ追加"
+                       );
+               }
+               @Override
+               public void actionPerformed(ActionEvent event) {
+                       byte[] data = getMIDIData();
+                       if( data == null || data.length == 0 ) {
+                               String message = "No data entered - データが入力されていません。";
+                               JOptionPane.showMessageDialog(
+                                       Base64Dialog.this, message,
+                                       ChordHelperApplet.VersionInfo.NAME,
+                                       JOptionPane.WARNING_MESSAGE
+                               );
+                               base64TextArea.requestFocusInWindow();
+                               return;
+                       }
+                       PlaylistTableModel sltm = midiEditor.sequenceListTable.getModel();
+                       try {
+                               sltm.addSequence(data, null);
+                       } catch(IOException | InvalidMidiDataException e) {
+                               String message = "例外 "+e+" が発生しました。"+e.getMessage();
+                               e.printStackTrace();
+                               midiEditor.showWarning(message);
+                               base64TextArea.requestFocusInWindow();
+                               return;
+                       }
+                       setVisible(false);
+               }
+       };
+       /**
+        * Base64テキストクリアアクション
+        */
+       public Action clearAction = new AbstractAction("Clear") {
+               @Override
+               public void actionPerformed(ActionEvent e) {
+                       base64TextArea.setText(null);
+               }
+       };
+       private static class Base64TextArea extends JTextArea {
+               private static final Pattern headerLine =
+                       Pattern.compile( "^.*:.*$", Pattern.MULTILINE );
+               public Base64TextArea(int rows, int columns) {
+                       super(rows,columns);
+               }
+               public byte[] getBinary() {
+                       String text = headerLine.matcher(getText()).replaceAll("");
+                       return Base64.decodeBase64(text.getBytes());
+               }
+               public void setBinary(byte[] binary_data, String content_type, String filename) {
+                       if( binary_data != null && binary_data.length > 0 ) {
+                               String header = "";
+                               if( content_type != null && filename != null ) {
+                                       header += "Content-Type: " + content_type + "; name=\"" + filename + "\"\n";
+                                       header += "Content-Transfer-Encoding: base64\n";
+                                       header += "\n";
+                               }
+                               setText(header + new String(Base64.encodeBase64Chunked(binary_data)) + "\n");
+                       }
+               }
+       }
+       /**
+        * Base64テキスト入力ダイアログを構築します。
+        * @param midiEditor 親画面となるMIDIエディタ
+        */
+       public Base64Dialog(MidiSequenceEditor midiEditor) {
+               this.midiEditor = midiEditor;
+               setTitle("Base64-encoded MIDI sequence - " + ChordHelperApplet.VersionInfo.NAME);
+               try {
+                       Base64.decodeBase64("".getBytes());
+                       base64Available = true;
+               } catch( NoClassDefFoundError e ) {
+                       base64Available = false;
+               }
+               if( base64Available ) {
+                       add(new JPanel() {{
+                               setLayout(new BoxLayout(this, BoxLayout.PAGE_AXIS));
+                               add(new JPanel() {{
+                                       setLayout(new BoxLayout(this, BoxLayout.LINE_AXIS));
+                                       add(new JLabel("Base64-encoded MIDI sequence:"));
+                                       add(Box.createRigidArea(new Dimension(10, 0)));
+                                       add(new JButton(addBase64Action){{setMargin(ChordHelperApplet.ZERO_INSETS);}});
+                                       add(new JButton(clearAction){{setMargin(ChordHelperApplet.ZERO_INSETS);}});
+                               }});
+                               add(new JScrollPane(base64TextArea));
+                       }});
+               }
+               setBounds( 300, 250, 660, 300 );
+       }
+       private boolean base64Available;
+       /**
+        * {@link Base64} が使用できるかどうかを返します。
+        * @return Apache Commons Codec ライブラリが利用できる状態ならtrue
+        */
+       public boolean isBase64Available() {
+               return base64Available;
+       }
+       /**
+        * バイナリー形式でMIDIデータを返します。
+        * @return バイナリー形式のMIDIデータ
+        */
+       public byte[] getMIDIData() {
+               return base64TextArea.getBinary();
+       }
+       /**
+        * バイナリー形式のMIDIデータを設定します。
+        * @param midiData バイナリー形式のMIDIデータ
+        */
+       public void setMIDIData( byte[] midiData ) {
+               base64TextArea.setBinary(midiData, null, null);
+       }
+       /**
+        * バイナリー形式のMIDIデータを、ファイル名をつけて設定します。
+        * @param midiData バイナリー形式のMIDIデータ
+        * @param filename ファイル名
+        */
+       public void setMIDIData( byte[] midiData, String filename ) {
+               base64TextArea.setBinary(midiData, "audio/midi", filename);
+               base64TextArea.selectAll();
+       }
+       /**
+        * Base64形式でMIDIデータを返します。
+        * @return  Base64形式のMIDIデータ
+        */
+       public String getBase64Data() {
+               return base64TextArea.getText();
+       }
+       /**
+        * Base64形式のMIDIデータを設定します。
+        * @param base64Data Base64形式のMIDIデータ
+        */
+       public void setBase64Data( String base64Data ) {
+               base64TextArea.setText(null);
+               base64TextArea.append(base64Data);
+       }
+}
diff --git a/src/camidion/chordhelper/midieditor/DefaultMidiChannelComboBoxModel.java b/src/camidion/chordhelper/midieditor/DefaultMidiChannelComboBoxModel.java
new file mode 100644 (file)
index 0000000..bc445db
--- /dev/null
@@ -0,0 +1,27 @@
+package camidion.chordhelper.midieditor;
+
+import javax.swing.DefaultComboBoxModel;
+
+import camidion.chordhelper.music.MIDISpec;
+
+/**
+ * MIDIチャンネル選択コンボボックスモデルのデフォルト実装
+ */
+public class DefaultMidiChannelComboBoxModel extends DefaultComboBoxModel<Integer>
+       implements MidiChannelComboBoxModel
+{
+       /**
+        * MIDIチャンネル選択コンボボックスモデルを構築します。
+        */
+       public DefaultMidiChannelComboBoxModel() {
+               for(int ch = 1; ch <= MIDISpec.MAX_CHANNELS ; ch++) addElement(ch);
+       }
+       @Override
+       public int getSelectedChannel() {
+               return getIndexOf(getSelectedItem());
+       }
+       @Override
+       public void setSelectedChannel(int channel) {
+               setSelectedItem(getElementAt(channel));
+       }
+}
diff --git a/src/camidion/chordhelper/midieditor/DurationForm.java b/src/camidion/chordhelper/midieditor/DurationForm.java
new file mode 100644 (file)
index 0000000..a203f64
--- /dev/null
@@ -0,0 +1,147 @@
+package camidion.chordhelper.midieditor;
+
+import java.awt.Component;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+
+import javax.swing.JComboBox;
+import javax.swing.JLabel;
+import javax.swing.JList;
+import javax.swing.JPanel;
+import javax.swing.JSpinner;
+import javax.swing.ListCellRenderer;
+import javax.swing.SpinnerNumberModel;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+
+import camidion.chordhelper.ButtonIcon;
+
+/**
+ * 音長入力フォーム
+ */
+public class DurationForm extends JPanel implements ActionListener, ChangeListener {
+       class NoteIcon extends ButtonIcon {
+               public NoteIcon( int kind ) { super(kind); }
+               public int getDuration() {
+                       if(  ! isMusicalNote() ) return -1;
+                       int duration = (ppq * 4) >> getMusicalNoteValueIndex();
+                       if( isDottedMusicalNote() )
+                               duration += duration / 2;
+                       return duration;
+               }
+       }
+       class NoteRenderer extends JLabel implements ListCellRenderer<NoteIcon> {
+               public NoteRenderer() { setOpaque(true); }
+               @Override
+               public Component getListCellRendererComponent(
+                       JList<? extends NoteIcon> list,
+                       NoteIcon icon,
+                       int index,
+                       boolean isSelected,
+                       boolean cellHasFocus
+               ) {
+                       setIcon( icon );
+                       int duration = icon.getDuration();
+                       setText( duration < 0 ? null : ("" + duration) );
+                       setFont( list.getFont() );
+                       if (isSelected) {
+                               setBackground( list.getSelectionBackground() );
+                               setForeground( list.getSelectionForeground() );
+                       } else {
+                               setBackground( list.getBackground() );
+                               setForeground( list.getForeground() );
+                       }
+                       return this;
+               }
+       }
+       class NoteComboBox extends JComboBox<NoteIcon> {
+               public NoteComboBox() {
+                       setRenderer( new NoteRenderer() );
+                       addItem( new NoteIcon(ButtonIcon.EDIT_ICON) );
+                       addItem( new NoteIcon(ButtonIcon.WHOLE_NOTE_ICON) );
+                       addItem( new NoteIcon(ButtonIcon.DOTTED_HALF_NOTE_ICON) );
+                       addItem( new NoteIcon(ButtonIcon.HALF_NOTE_ICON) );
+                       addItem( new NoteIcon(ButtonIcon.DOTTED_QUARTER_NOTE_ICON) );
+                       addItem( new NoteIcon(ButtonIcon.QUARTER_NOTE_ICON) );
+                       addItem( new NoteIcon(ButtonIcon.DOTTED_8TH_NOTE_ICON) );
+                       addItem( new NoteIcon(ButtonIcon.A8TH_NOTE_ICON) );
+                       addItem( new NoteIcon(ButtonIcon.DOTTED_16TH_NOTE_ICON) );
+                       addItem( new NoteIcon(ButtonIcon.A16TH_NOTE_ICON) );
+                       addItem( new NoteIcon(ButtonIcon.DOTTED_32ND_NOTE_ICON) );
+                       addItem( new NoteIcon(ButtonIcon.A32ND_NOTE_ICON) );
+                       addItem( new NoteIcon(ButtonIcon.A64TH_NOTE_ICON) );
+                       setMaximumRowCount(16);
+                       setSelectedIndex(5);
+               }
+               public int getDuration() {
+                       NoteIcon icon = (NoteIcon)getSelectedItem();
+                       return icon==null ? -1 : icon.getDuration();
+               }
+               public void setDuration(int duration) {
+                       int n_items = getItemCount();
+                       for( int i = 1; i < n_items; i++ ) {
+                               NoteIcon icon = getItemAt(i);
+                               int icon_duration = icon.getDuration();
+                               if( icon_duration < 0 || icon_duration != duration )
+                                       continue;
+                               setSelectedItem(icon);
+                               return;
+                       }
+                       setSelectedIndex(0);
+               }
+       }
+       class DurationModel extends SpinnerNumberModel {
+               public DurationModel() { super( ppq, 1, ppq*4*4, 1 ); }
+               public void setDuration( int value ) {
+                       setValue( new Integer(value) );
+               }
+               public int getDuration() {
+                       return getNumber().intValue();
+               }
+               public void setPPQ( int ppq ) {
+                       setMaximum( ppq*4*4 );
+                       setDuration( ppq );
+               }
+       }
+       DurationModel model;
+       JSpinner spinner;
+       NoteComboBox note_combo;
+       JLabel title_label, unit_label;
+       private int ppq = 960;
+       //
+       public DurationForm() {
+               (model = new DurationModel()).addChangeListener(this);
+               (note_combo = new NoteComboBox()).addActionListener(this);
+               add( title_label = new JLabel("Duration:") );
+               add( note_combo );
+               add( spinner = new JSpinner( model ) );
+               add( unit_label = new JLabel("[Ticks]") );
+       }
+       @Override
+       public void actionPerformed(ActionEvent e) {
+               int duration = note_combo.getDuration();
+               if( duration < 0 ) return;
+               model.setDuration( duration );
+       }
+       @Override
+       public void stateChanged(ChangeEvent e) {
+               note_combo.setDuration( model.getDuration() );
+       }
+       @Override
+       public void setEnabled( boolean enabled ) {
+               super.setEnabled(enabled);
+               title_label.setEnabled(enabled);
+               spinner.setEnabled(enabled);
+               note_combo.setEnabled(enabled);
+               unit_label.setEnabled(enabled);
+       }
+       public void setPPQ( int ppq ) {
+               model.setPPQ(this.ppq = ppq);
+       }
+       public int getDuration() {
+               return model.getDuration();
+       }
+       public void setDuration( int duration ) {
+               model.setDuration(duration);
+       }
+}
\ No newline at end of file
diff --git a/src/camidion/chordhelper/midieditor/HexSelecter.java b/src/camidion/chordhelper/midieditor/HexSelecter.java
new file mode 100644 (file)
index 0000000..89c33fc
--- /dev/null
@@ -0,0 +1,91 @@
+package camidion.chordhelper.midieditor;
+
+import java.awt.FlowLayout;
+import java.util.ArrayList;
+
+import javax.swing.JComboBox;
+import javax.swing.JLabel;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+
+/**
+ * 16進数選択 [0x00 0x00 0x00 ... ] v -> Select
+ */
+public class HexSelecter extends JPanel {
+       private JComboBox<String> comboBox = new JComboBox<String>() {{
+               setEditable(true);
+               setMaximumRowCount(16);
+       }};
+       private JLabel title;
+       public HexSelecter( String title ) {
+               if( title != null )
+                       add( this.title = new JLabel(title) );
+               add(comboBox);
+               setLayout(new FlowLayout());
+       }
+       public JComboBox<String> getComboBox() { return comboBox; }
+       public void setTitle(String title) { this.title.setText(title); }
+       public int getValue() {
+               ArrayList<Integer> ia = getIntegerList();
+               return ia.size() == 0 ? -1 : ia.get(0);
+       }
+       public ArrayList<Integer> getIntegerList() {
+               String words[];
+               String str = (String)(comboBox.getSelectedItem());
+               if( str == null )
+                       words = new String[0];
+               else
+                       words = str.replaceAll( ":.*$", "" ).trim().split(" +");
+               int i;
+               ArrayList<Integer> ia = new ArrayList<Integer>();
+               for( String w : words ) {
+                       if( w.length() == 0 ) continue;
+                       try {
+                               i = Integer.decode(w).intValue();
+                       } catch( NumberFormatException e ) {
+                               JOptionPane.showMessageDialog(
+                                       this,
+                                       w + " : is not a number",
+                                       "MIDI Chord Helper",
+                                       JOptionPane.ERROR_MESSAGE
+                               );
+                               return null;
+                       }
+                       ia.add(i);
+               }
+               return ia;
+       }
+       public byte[] getBytes() {
+               ArrayList<Integer> ia = getIntegerList();
+               byte[] ba = new byte[ia.size()];
+               int i = 0;
+               for( Integer ib : ia ) {
+                       ba[i++] = (byte)( ib.intValue() & 0xFF );
+               }
+               return ba;
+       }
+       public void setValue( int val ) {
+               setValue( (byte)(val & 0xFF) );
+       }
+       public void setValue( byte val ) {
+               int n_item = comboBox.getItemCount();
+               String item;
+               for( int i=0; i<n_item; i++ ) {
+                       item = (String)( comboBox.getItemAt(i) );
+                       if( Integer.decode( item.trim().split(" +")[0] ).byteValue() == val ) {
+                               comboBox.setSelectedIndex(i);
+                               return;
+                       }
+               }
+               comboBox.setSelectedItem(String.format(" 0x%02X",val));
+       }
+       public void setValue( byte ba[] ) {
+               String str = "";
+               for( byte b : ba )
+                       str += String.format( " 0x%02X", b );
+               comboBox.setSelectedItem(str);
+       }
+       public void clear() {
+               comboBox.setSelectedItem("");
+       }
+}
\ No newline at end of file
diff --git a/src/camidion/chordhelper/midieditor/HexTextForm.java b/src/camidion/chordhelper/midieditor/HexTextForm.java
new file mode 100644 (file)
index 0000000..fd65125
--- /dev/null
@@ -0,0 +1,79 @@
+package camidion.chordhelper.midieditor;
+
+import java.awt.FlowLayout;
+import java.util.ArrayList;
+
+import javax.swing.JLabel;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.JTextArea;
+
+/**
+ * 16進数テキスト入力フォーム [0x00 0x00 0x00 ... ]
+ */
+public class HexTextForm extends JPanel {
+       public JTextArea textArea;
+       public JLabel titleLabel;
+       public HexTextForm(String title) {
+               this(title,1,3);
+       }
+       public HexTextForm(String title, int rows, int columns) {
+               if( title != null )
+                       add(titleLabel = new JLabel(title));
+               textArea = new JTextArea(rows, columns) {{
+                       setLineWrap(true);
+               }};
+               add(new JScrollPane(textArea));
+               setLayout(new FlowLayout());
+       }
+       public String getString() {
+               return textArea.getText();
+       }
+       public byte[] getBytes() {
+               String words[] = getString().trim().split(" +");
+               ArrayList<Integer> tmp_ba = new ArrayList<Integer>();
+               int i;
+               for( String w : words ) {
+                       if( w.length() == 0 ) continue;
+                       try {
+                               i = Integer.decode(w).intValue();
+                       } catch( NumberFormatException e ) {
+                               JOptionPane.showMessageDialog(
+                                               this,
+                                               w + " : is not a number",
+                                               "MIDI Chord Helper",
+                                               JOptionPane.ERROR_MESSAGE
+                                               );
+                               return null;
+                       }
+                       tmp_ba.add(i);
+               }
+               byte[] ba = new byte[tmp_ba.size()];
+               i = 0;
+               for( Integer b : tmp_ba ) {
+                       ba[i++] = (byte)( b.intValue() & 0xFF );
+               }
+               return ba;
+       }
+       public void setTitle( String str ) {
+               titleLabel.setText( str );
+       }
+       public void setString( String str ) {
+               textArea.setText( str );
+       }
+       public void setValue( int val ) {
+               textArea.setText( String.format( " 0x%02X", val ) );
+       }
+       public void setValue( byte val ) {
+               textArea.setText( String.format( " 0x%02X", val ) );
+       }
+       public void setValue( byte ba[] ) {
+               String str = "";
+               for( byte b : ba ) {
+                       str += String.format( " 0x%02X", b );
+               }
+               textArea.setText(str);
+       }
+       public void clear() { textArea.setText(""); }
+}
\ No newline at end of file
diff --git a/src/camidion/chordhelper/midieditor/KeySignatureLabel.java b/src/camidion/chordhelper/midieditor/KeySignatureLabel.java
new file mode 100644 (file)
index 0000000..2eed35c
--- /dev/null
@@ -0,0 +1,32 @@
+package camidion.chordhelper.midieditor;
+
+import javax.swing.JLabel;
+
+import camidion.chordhelper.music.Key;
+import camidion.chordhelper.music.SymbolLanguage;
+
+/**
+ * 調表示ラベル
+ */
+public class KeySignatureLabel extends JLabel {
+       private Key key;
+       public KeySignatureLabel() { clear(); }
+       public Key getKey() { return key; }
+       public void setKeySignature( Key key ) {
+               this.key = key;
+               if( key == null ) {
+                       setText("Key:C");
+                       setToolTipText("Key: Unknown");
+                       setEnabled(false);
+                       return;
+               }
+               setText( "key:" + key.toString() );
+               setToolTipText(
+                       "Key: " + key.toStringIn(SymbolLanguage.NAME)
+                       + " "  + key.toStringIn(SymbolLanguage.IN_JAPANESE)
+                       + " (" + key.signatureDescription() + ")"
+               );
+               setEnabled(true);
+       }
+       public void clear() { setKeySignature( (Key)null ); }
+}
\ No newline at end of file
diff --git a/src/camidion/chordhelper/midieditor/KeySignatureSelecter.java b/src/camidion/chordhelper/midieditor/KeySignatureSelecter.java
new file mode 100644 (file)
index 0000000..48eff59
--- /dev/null
@@ -0,0 +1,82 @@
+package camidion.chordhelper.midieditor;
+
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+
+import javax.swing.JCheckBox;
+import javax.swing.JComboBox;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+
+import camidion.chordhelper.music.Key;
+import camidion.chordhelper.music.SymbolLanguage;
+
+/**
+ * 調性選択
+ */
+public class KeySignatureSelecter extends JPanel implements ActionListener {
+       public JComboBox<String> keysigCombobox = new JComboBox<String>() {
+               {
+                       String str;
+                       Key key;
+                       for( int i = -7 ; i <= 7 ; i++ ) {
+                               str = (key = new Key(i)).toString();
+                               if( i != 0 ) {
+                                       str = key.signature() + " : " + str ;
+                               }
+                               addItem(str);
+                       }
+                       setMaximumRowCount(15);
+               }
+       };
+       JCheckBox minorCheckbox = null;
+
+       public KeySignatureSelecter() { this(true); }
+       public KeySignatureSelecter(boolean useMinorCheckbox) {
+               add(new JLabel("Key:"));
+               add(keysigCombobox);
+               if(useMinorCheckbox) {
+                       add( minorCheckbox = new JCheckBox("minor") );
+                       minorCheckbox.addActionListener(this);
+               }
+               keysigCombobox.addActionListener(this);
+               clear();
+       }
+       public void actionPerformed(ActionEvent e) { updateToolTipText(); }
+       private void updateToolTipText() {
+               Key key = getKey();
+               keysigCombobox.setToolTipText(
+                       "Key: " + key.toStringIn( SymbolLanguage.NAME )
+                       + " "  + key.toStringIn( SymbolLanguage.IN_JAPANESE )
+                       + " (" + key.signatureDescription() + ")"
+               );
+       }
+       public void clear() { setKey(new Key("C")); }
+       public void setKey( Key key ) {
+               if( key == null ) {
+                       clear();
+                       return;
+               }
+               keysigCombobox.setSelectedIndex( key.toCo5() + 7 );
+               if( minorCheckbox == null )
+                       return;
+               switch( key.majorMinor() ) {
+               case Key.MINOR : minorCheckbox.setSelected(true); break;
+               case Key.MAJOR : minorCheckbox.setSelected(false); break;
+               }
+       }
+       public Key getKey() {
+               int minor = (
+                       minorCheckbox == null ? Key.MAJOR_OR_MINOR :
+                       isMinor() ? Key.MINOR :
+                       Key.MAJOR
+               );
+               return new Key(getKeyCo5(),minor);
+       }
+       public int getKeyCo5() {
+               return keysigCombobox.getSelectedIndex() - 7;
+       }
+       public boolean isMinor() {
+               return minorCheckbox != null && minorCheckbox.isSelected();
+       }
+}
\ No newline at end of file
diff --git a/src/camidion/chordhelper/midieditor/MidiChannelButtonSelecter.java b/src/camidion/chordhelper/midieditor/MidiChannelButtonSelecter.java
new file mode 100644 (file)
index 0000000..698e919
--- /dev/null
@@ -0,0 +1,94 @@
+package camidion.chordhelper.midieditor;
+
+import java.awt.Color;
+import java.awt.Component;
+import java.awt.Graphics;
+
+import javax.swing.JLabel;
+import javax.swing.JList;
+import javax.swing.ListCellRenderer;
+import javax.swing.event.ListDataEvent;
+import javax.swing.event.ListDataListener;
+import javax.swing.event.ListSelectionEvent;
+import javax.swing.event.ListSelectionListener;
+
+import camidion.chordhelper.pianokeyboard.PianoKeyboard;
+
+/**
+ * MIDIチャンネル選択ビュー(リストボタン)
+ */
+public class MidiChannelButtonSelecter extends JList<Integer>
+       implements ListDataListener, ListSelectionListener
+{
+       private PianoKeyboard keyboard = null;
+       public MidiChannelButtonSelecter(MidiChannelComboBoxModel model) {
+               super(model);
+               setLayoutOrientation(HORIZONTAL_WRAP);
+               setVisibleRowCount(1);
+               setCellRenderer(new MyCellRenderer());
+               setSelectedIndex(model.getSelectedChannel());
+               model.addListDataListener(this);
+               addListSelectionListener(this);
+       }
+       @Override
+       public void contentsChanged(ListDataEvent e) {
+               setSelectedIndex(getModel().getSelectedChannel());
+       }
+       @Override
+       public void intervalAdded(ListDataEvent e) {}
+       @Override
+       public void intervalRemoved(ListDataEvent e) {}
+       @Override
+       public void valueChanged(ListSelectionEvent e) {
+               getModel().setSelectedChannel(getSelectedIndex());
+       }
+       public MidiChannelButtonSelecter(PianoKeyboard keyboard) {
+               this(keyboard.midiChComboboxModel);
+               setPianoKeyboard(keyboard);
+       }
+       @Override
+       public MidiChannelComboBoxModel getModel() {
+               return (MidiChannelComboBoxModel)(super.getModel());
+       }
+       public void setPianoKeyboard(PianoKeyboard keyboard) {
+               (this.keyboard = keyboard).midiChannelButtonSelecter = this;
+       }
+       class MyCellRenderer extends JLabel implements ListCellRenderer<Integer> {
+               private boolean cellHasFocus = false;
+               public MyCellRenderer() {
+                       setOpaque(true);
+                       setHorizontalAlignment(CENTER);
+                       setSelectionBackground(Color.yellow);
+               }
+               @Override
+               public Component getListCellRendererComponent(
+                       JList<? extends Integer> list,
+                       Integer value, int index,
+                       boolean isSelected, boolean cellHasFocus
+               ) {
+                       this.cellHasFocus = cellHasFocus;
+                       setText(value.toString());
+                       if(isSelected) {
+                               setBackground(list.getSelectionBackground());
+                               setForeground(list.getSelectionForeground());
+                       } else {
+                               setBackground(
+                                       keyboard != null && keyboard.countKeyOn(index) > 0 ?
+                                       Color.pink : list.getBackground()
+                               );
+                               setForeground(list.getForeground());
+                       }
+                       setEnabled(list.isEnabled());
+                       setFont(list.getFont());
+                       return this;
+               }
+               @Override
+               protected void paintComponent(Graphics g) {
+                       super.paintComponent(g);
+                       if( cellHasFocus ) {
+                               g.setColor(Color.gray);
+                               g.drawRect(0, 0, this.getWidth() - 1, this.getHeight() - 1);
+                       }
+               }
+       }
+}
\ No newline at end of file
diff --git a/src/camidion/chordhelper/midieditor/MidiChannelComboBoxModel.java b/src/camidion/chordhelper/midieditor/MidiChannelComboBoxModel.java
new file mode 100644 (file)
index 0000000..cb7ab4f
--- /dev/null
@@ -0,0 +1,19 @@
+package camidion.chordhelper.midieditor;
+
+import javax.swing.ComboBoxModel;
+
+/**
+ * MIDIチャンネル選択コンボボックスモデル
+ */
+public interface MidiChannelComboBoxModel extends ComboBoxModel<Integer> {
+       /**
+        * 選択中のMIDIチャンネルを返します。
+        * @return 選択中のMIDIチャンネル
+        */
+       int getSelectedChannel();
+       /**
+        * MIDIチャンネルの選択を変更します。
+        * @param channel 選択するMIDIチャンネル
+        */
+       void setSelectedChannel(int channel);
+}
diff --git a/src/camidion/chordhelper/midieditor/MidiChannelComboSelecter.java b/src/camidion/chordhelper/midieditor/MidiChannelComboSelecter.java
new file mode 100644 (file)
index 0000000..f6777ba
--- /dev/null
@@ -0,0 +1,36 @@
+package camidion.chordhelper.midieditor;
+
+import java.awt.FlowLayout;
+
+import javax.swing.JComboBox;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+
+/**
+ * MIDIチャンネル選択ビュー(コンボボックス)
+ */
+public class MidiChannelComboSelecter extends JPanel {
+       public JComboBox<Integer> comboBox = new JComboBox<>();
+       public MidiChannelComboSelecter( String title ) {
+               this(title, new DefaultMidiChannelComboBoxModel());
+       }
+       public MidiChannelComboSelecter(String title, MidiChannelComboBoxModel model) {
+               setLayout(new FlowLayout());
+               if( title != null ) add( new JLabel(title) );
+               comboBox.setModel(model);
+               comboBox.setMaximumRowCount(16);
+               add(comboBox);
+       }
+       public JComboBox<Integer> getComboBox() {
+               return comboBox;
+       }
+       public MidiChannelComboBoxModel getModel() {
+               return (MidiChannelComboBoxModel)comboBox.getModel();
+       }
+       public int getSelectedChannel() {
+               return comboBox.getSelectedIndex();
+       }
+       public void setSelectedChannel(int channel) {
+               comboBox.setSelectedIndex(channel);
+       }
+}
\ No newline at end of file
diff --git a/src/camidion/chordhelper/midieditor/MidiEventDialog.java b/src/camidion/chordhelper/midieditor/MidiEventDialog.java
new file mode 100644 (file)
index 0000000..9dbc678
--- /dev/null
@@ -0,0 +1,106 @@
+package camidion.chordhelper.midieditor;
+
+import java.awt.FlowLayout;
+import java.awt.GridLayout;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+
+import javax.swing.Action;
+import javax.swing.JButton;
+import javax.swing.JDialog;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JSpinner;
+
+public class MidiEventDialog extends JDialog {
+       /**
+        * tick位置入力フォーム
+        */
+       public static class TickPositionInputForm extends JPanel {
+               private JSpinner tickSpinner = new JSpinner();
+               private JSpinner measureSpinner = new JSpinner();
+               private JSpinner beatSpinner = new JSpinner();
+               private JSpinner extraTickSpinner = new JSpinner();
+               public TickPositionInputForm() {
+                       setLayout(new GridLayout(2,4));
+                       add( new JLabel() );
+                       add( new JLabel() );
+                       add( new JLabel("Measure:") );
+                       add( new JLabel("Beat:") );
+                       add( new JLabel("ExTick:") );
+                       add( new JLabel("Tick position : ",JLabel.RIGHT) );
+                       add( tickSpinner );
+                       add( measureSpinner );
+                       add( beatSpinner );
+                       add( extraTickSpinner );
+               }
+               public void setModel(TickPositionModel model) {
+                       tickSpinner.setModel(model.tickModel);
+                       measureSpinner.setModel(model.measureModel);
+                       beatSpinner.setModel(model.beatModel);
+                       extraTickSpinner.setModel(model.extraTickModel);
+               }
+       }
+       /**
+        * tick位置入力フォーム
+        */
+       public MidiEventDialog.TickPositionInputForm tickPositionInputForm;
+       /**
+        * MIDIメッセージ入力フォーム
+        */
+       public MidiMessageForm midiMessageForm;
+       /**
+        * キャンセルボタン
+        */
+       private JButton cancelButton;
+       /**
+        * OKボタン(アクションによってラベルがOK以外に変わることがある)
+        */
+       private JButton okButton;
+       /**
+        * MIDIイベントダイアログの構築
+        */
+       public MidiEventDialog() {
+               setLayout(new FlowLayout());
+               add(tickPositionInputForm = new TickPositionInputForm());
+               add(midiMessageForm = new MidiMessageForm());
+               add(new JPanel(){{
+                       add(okButton = new JButton("OK"));
+                       add(cancelButton = new JButton("Cancel"));
+               }});
+               cancelButton.addActionListener(new ActionListener() {
+                       public void actionPerformed(ActionEvent e) {
+                               setVisible(false);
+                       }
+               });
+       }
+       public void openForm(
+               String title, Action okAction, int midiChannel,
+               boolean useTick, boolean useMessage
+       ) {
+               setTitle(title);
+               okButton.setAction(okAction);
+               if( useMessage && midiChannel >= 0 ) {
+                       midiMessageForm.channelText.setSelectedChannel(midiChannel);
+               }
+               tickPositionInputForm.setVisible(useTick);
+               midiMessageForm.setVisible(useMessage);
+               midiMessageForm.setDurationVisible(useMessage && useTick);
+               int width = useMessage ? 630 : 520;
+               int height = useMessage ? (useTick ? 370 : 300) : 150;
+               setBounds( 200, 300, width, height );
+               setVisible(true);
+       }
+       public void openTickForm(String title, Action okAction) {
+               openForm(title, okAction, -1, true, false);
+       }
+       public void openMessageForm(String title, Action okAction, int midiChannel) {
+               openForm(title, okAction, midiChannel, false, true);
+       }
+       public void openEventForm(String title, Action okAction, int midiChannel) {
+               openForm(title, okAction, midiChannel, true, true);
+       }
+       public void openEventForm(String title, Action okAction) {
+               openEventForm(title, okAction, -1);
+       }
+}
\ No newline at end of file
diff --git a/src/camidion/chordhelper/midieditor/MidiMessageForm.java b/src/camidion/chordhelper/midieditor/MidiMessageForm.java
new file mode 100644 (file)
index 0000000..bb567f0
--- /dev/null
@@ -0,0 +1,630 @@
+package camidion.chordhelper.midieditor;
+
+import java.awt.Dimension;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.InputEvent;
+import java.awt.event.ItemEvent;
+import java.awt.event.ItemListener;
+import java.nio.charset.Charset;
+
+import javax.sound.midi.InvalidMidiDataException;
+import javax.sound.midi.MetaMessage;
+import javax.sound.midi.MidiChannel;
+import javax.sound.midi.MidiMessage;
+import javax.sound.midi.ShortMessage;
+import javax.sound.midi.SysexMessage;
+import javax.swing.BoxLayout;
+import javax.swing.ComboBoxModel;
+import javax.swing.DefaultComboBoxModel;
+import javax.swing.JComboBox;
+import javax.swing.JPanel;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+
+import camidion.chordhelper.music.Key;
+import camidion.chordhelper.music.MIDISpec;
+import camidion.chordhelper.music.NoteSymbol;
+import camidion.chordhelper.pianokeyboard.PianoKeyboardAdapter;
+import camidion.chordhelper.pianokeyboard.PianoKeyboardPanel;
+
+/**
+ * MIDI Message Entry Form - MIDIメッセージ入力欄
+ */
+public class MidiMessageForm extends JPanel implements ActionListener {
+       /**
+        * MIDIステータス
+        */
+       private ComboBoxModel<String> statusComboBoxModel =
+               new DefaultComboBoxModel<String>() {
+                       {
+                               int i; String s;
+                               // チャンネルメッセージ
+                               for( i = 0x80; i <= 0xE0 ; i += 0x10 ) {
+                                       if( (s = MIDISpec.getStatusName(i)) == null )
+                                               continue;
+                                       addElement(String.format("0x%02X : %s", i, s));
+                               }
+                               // チャンネルを持たない SysEx やメタメッセージなど
+                               for( i = 0xF0; i <= 0xFF ; i++ ) {
+                                       if( (s = MIDISpec.getStatusName(i)) == null )
+                                               continue;
+                                       addElement(String.format("0x%02X : %s", i, s));
+                               }
+                       }
+               };
+       /**
+        * ノート番号
+        */
+       private ComboBoxModel<String> noteComboBoxModel =
+               new DefaultComboBoxModel<String>() {
+                       {
+                               for( int i = 0; i<=0x7F; i++ ) addElement(
+                                       String.format(
+                                               "0x%02X : %d : %s", i, i, NoteSymbol.noteNoToSymbol(i)
+                                       )
+                               );
+                               // Center note C
+                               setSelectedItem(getElementAt(60));
+                       }
+               };
+       /**
+        * 打楽器名
+        */
+       private ComboBoxModel<String> percussionComboBoxModel =
+               new DefaultComboBoxModel<String>() {
+                       {
+                               for( int i = 0; i<=0x7F; i++ ) addElement(
+                                       String.format(
+                                               "0x%02X : %d : %s", i, i, MIDISpec.getPercussionName(i)
+                                       )
+                               );
+                               setSelectedItem(getElementAt(35)); // Acoustic Bass Drum
+                       }
+               };
+       /**
+        * コントロールチェンジ
+        */
+       private ComboBoxModel<String> controlChangeComboBoxModel =
+               new DefaultComboBoxModel<String>() {
+                       {
+                               String s;
+                               for( int i = 0; i<=0x7F; i++ ) {
+                                       if( (s = MIDISpec.getControllerName(i)) == null )
+                                               continue;
+                                       addElement(String.format("0x%02X : %d : %s", i, i, s));
+                               }
+                       }
+               };
+       /**
+        * 楽器名(音色)
+        */
+       private ComboBoxModel<String> instrumentComboBoxModel =
+               new DefaultComboBoxModel<String>() {
+                       {
+                               for( int i = 0; i<=0x7F; i++ ) addElement(
+                                       String.format(
+                                               "0x%02X : %s", i, MIDISpec.instrumentNames[i]
+                                       )
+                               );
+                       }
+               };
+       /**
+        * MetaMessage Type
+        */
+       private ComboBoxModel<String> metaTypeComboBoxModel =
+               new DefaultComboBoxModel<String>() {
+                       {
+                               String s;
+                               String initial_type_string = null;
+                               for( int type = 0; type < 0x80 ; type++ ) {
+                                       if( (s = MIDISpec.getMetaName(type)) == null ) {
+                                               continue;
+                                       }
+                                       s = String.format("0x%02X : %s", type, s);
+                                       addElement(s);
+                                       if( type == 0x51 )
+                                               initial_type_string = s; // Tempo
+                               }
+                               setSelectedItem(initial_type_string);
+                       }
+               };
+       /**
+        * 16進数値のみの選択データモデル
+        */
+       private ComboBoxModel<String> hexData1ComboBoxModel =
+               new DefaultComboBoxModel<String>() {
+                       {
+                               for( int i = 0; i<=0x7F; i++ ) {
+                                       addElement(String.format("0x%02X : %d", i, i ));
+                               }
+                       }
+               };
+       /**
+        * 16進数値のみの選択データモデル(ShortMessageデータ2バイト目)
+        */
+       private ComboBoxModel<String> hexData2ComboBoxModel =
+               new DefaultComboBoxModel<String>() {
+                       {
+                               for( int i = 0; i<=0x7F; i++ ) {
+                                       addElement(String.format("0x%02X : %d", i, i ));
+                               }
+                       }
+               };
+       // データ選択操作部
+       private HexSelecter statusText = new HexSelecter("Status/Command");
+       private HexSelecter data1Text = new HexSelecter("[Data1] ");
+       private HexSelecter data2Text = new HexSelecter("[Data2] ");
+       MidiChannelComboSelecter channelText =
+               new MidiChannelComboSelecter("MIDI Channel");
+
+       private JComboBox<String> statusComboBox = statusText.getComboBox();
+       private JComboBox<String> data1ComboBox = data1Text.getComboBox();
+       private JComboBox<String> data2ComboBox = data2Text.getComboBox();
+       private JComboBox<Integer> channelComboBox = channelText.getComboBox();
+
+       /**
+        * 長い値(テキストまたは数値)の入力欄
+        */
+       private HexTextForm dataText = new HexTextForm("Data:",3,50);
+       /**
+        * 音階入力用ピアノキーボード
+        */
+       private PianoKeyboardPanel keyboardPanel = new PianoKeyboardPanel() {
+               {
+                       keyboard.setPreferredSize(new Dimension(300,40));
+                       keyboard.addPianoKeyboardListener(
+                               new PianoKeyboardAdapter() {
+                                       public void pianoKeyPressed(int n, InputEvent e) {
+                                               data1Text.setValue(n);
+                                               if( midiChannels != null )
+                                                       midiChannels[channelText.getSelectedChannel()].
+                                                       noteOn( n, data2Text.getValue() );
+                                       }
+                                       public void pianoKeyReleased(int n, InputEvent e) {
+                                               if( midiChannels != null ) {
+                                                       midiChannels[channelText.getSelectedChannel()].
+                                                       noteOff( n, data2Text.getValue() );
+                                               }
+                                       }
+                               }
+                       );
+               }
+       };
+       /**
+        * 音の長さ
+        */
+       public DurationForm durationForm = new DurationForm();
+       /**
+        * テンポ選択
+        */
+       private TempoSelecter tempoSelecter = new TempoSelecter() {
+               {
+                       tempoSpinnerModel.addChangeListener(
+                               new ChangeListener() {
+                                       @Override
+                                       public void stateChanged(ChangeEvent e) {
+                                               dataText.setValue(getTempoByteArray());
+                                       }
+                               }
+                       );
+               }
+       };
+       /**
+        * 拍子選択
+        */
+       private TimeSignatureSelecter timesigSelecter = new TimeSignatureSelecter() {
+               {
+                       upperTimesigSpinnerModel.addChangeListener(
+                               new ChangeListener() {
+                                       @Override
+                                       public void stateChanged(ChangeEvent e) {
+                                               dataText.setValue(getByteArray());
+                                       }
+                               }
+                       );
+                       lowerTimesigCombobox.addActionListener(
+                               new ActionListener() {
+                                       @Override
+                                       public void actionPerformed(ActionEvent e) {
+                                               dataText.setValue(getByteArray());
+                                       }
+                               }
+                       );
+               }
+       };
+       /**
+        * 調号選択
+        */
+       private KeySignatureSelecter keysigSelecter = new KeySignatureSelecter() {
+               {
+                       keysigCombobox.addActionListener(
+                               new ActionListener() {
+                                       public void actionPerformed(ActionEvent e) {
+                                               dataText.setValue(getKey().getBytes());
+                                       }
+                               }
+                       );
+                       minorCheckbox.addItemListener(
+                               new ItemListener() {
+                                       public void itemStateChanged(ItemEvent e) {
+                                               dataText.setValue(getKey().getBytes());
+                                       }
+                               }
+                       );
+               }
+       };
+
+       /**
+        * 音を鳴らす出力MIDIチャンネル
+        */
+       private MidiChannel[] midiChannels = null;
+
+       /**
+        * Note on/off のときに Duration フォームを表示するか
+        */
+       private boolean isDurationVisible = true;
+
+       public MidiMessageForm() {
+               statusComboBox.setModel(statusComboBoxModel);
+               statusComboBox.setSelectedIndex(1); // NoteOn
+               data2ComboBox.setModel(hexData2ComboBoxModel);
+               data2ComboBox.setSelectedIndex(64); // Center
+               statusComboBox.addActionListener(this);
+               channelComboBox.addActionListener(this);
+               data1ComboBox.addActionListener(this);
+               setLayout(new BoxLayout( this, BoxLayout.Y_AXIS ));
+               add(new JPanel() {{ add(statusText); add(channelText); }});
+               add(durationForm);
+               add(new JPanel() {{ add(data1Text); add(keyboardPanel); }});
+               add(new JPanel() {{ add(data2Text); }});
+               add( tempoSelecter );
+               add( timesigSelecter );
+               add( keysigSelecter );
+               add( dataText );
+               updateVisible();
+       }
+       @Override
+       public void actionPerformed(ActionEvent e) {
+               Object src = e.getSource();
+               if( src == data1ComboBox ) {
+                       int status = statusText.getValue();
+                       int data1 = data1Text.getValue();
+                       if( isNote(status) ) { // Data1 -> Note
+                               if( data1 >= 0 ) keyboardPanel.keyboard.setSelectedNote(data1);
+                       }
+                       else if( status == 0xFF ) {
+                               switch( data1 ) { // Data type -> Selecter
+                               case 0x51: dataText.setValue(tempoSelecter.getTempoByteArray()); break;
+                               case 0x58: dataText.setValue(timesigSelecter.getByteArray()); break;
+                               case 0x59: dataText.setValue(keysigSelecter.getKey().getBytes()); break;
+                               default: break;
+                               }
+                       }
+               }
+               updateVisible();
+       }
+       /**
+        * このMIDIメッセージフォームにMIDIチャンネルを設定します。
+        *
+        * <p>設定したMIDIチャンネルには、
+        * ダイアログ内のピアノキーボードで音階を入力したときに
+        * ノートON/OFFが出力されます。これにより実際に音として聞けるようになります。
+        * </p>
+        *
+        * @param midiChannels MIDIチャンネル
+        */
+       public void setOutputMidiChannels( MidiChannel midiChannels[] ) {
+               this.midiChannels = midiChannels;
+       }
+       /**
+        * 時間間隔入力の表示状態を変更します。
+        * @param isVisible trueで表示、falseで非表示
+        */
+       public void setDurationVisible(boolean isVisible) {
+               isDurationVisible = isVisible;
+               updateVisible();
+       }
+       /**
+        * 時間間隔入力の表示状態を返します。
+        * @return true:表示中 false:非表示中
+        */
+       public boolean isDurationVisible() {
+               return isDurationVisible;
+       }
+       /**
+        * 各入力欄の表示状態を更新します。
+        */
+       public void updateVisible() {
+               int msgStatus = statusText.getValue();
+               boolean is_ch_msg = MIDISpec.isChannelMessage(msgStatus);
+               channelText.setVisible(is_ch_msg);
+               statusText.setTitle("[Status] "+(is_ch_msg ? "Command" : ""));
+               durationForm.setVisible( isDurationVisible && isNote(msgStatus) );
+               keyboardPanel.setVisible( msgStatus <= 0xAF );
+               data1Text.setVisible(
+                       msgStatus <= 0xEF ||
+                       msgStatus >= 0xF1 && msgStatus <= 0xF3 ||
+                       msgStatus == 0xFF
+               );
+               data2Text.setVisible(
+                       !(
+                               (msgStatus >= 0xC0 && msgStatus <= 0xDF)
+                               ||
+                               msgStatus == 0xF0 || msgStatus == 0xF1
+                               ||
+                               msgStatus == 0xF3 || msgStatus >= 0xF6
+                       )
+               );
+               data2Text.setTitle("[Data2] "+(
+                       msgStatus <= 0x9F ? "Velocity" :
+                       msgStatus <= 0xAF ? "Pressure" :
+                       msgStatus <= 0xBF ? "Value" :
+                       (msgStatus & 0xF0) == 0xE0 ? "High 7bit value" : ""
+               ));
+               // Show if Sysex or Meta
+               dataText.setVisible(
+                       msgStatus == 0xF0 || msgStatus == 0xF7 || msgStatus == 0xFF
+               );
+               if( msgStatus != 0xFF ) {
+                       tempoSelecter.setVisible(false);
+                       timesigSelecter.setVisible(false);
+                       keysigSelecter.setVisible(false);
+               }
+               switch( msgStatus & 0xF0 ) {
+               // ステータスに応じて、1バイト目のデータモデルを切り替える。
+
+               case ShortMessage.NOTE_OFF:
+               case ShortMessage.NOTE_ON:
+               case ShortMessage.POLY_PRESSURE:
+                       int ch = channelText.getSelectedChannel();
+                       data1Text.setTitle("[Data1] "+(ch == 9 ? "Percussion" : "Note No."));
+                       data1ComboBox.setModel(ch == 9 ? percussionComboBoxModel : noteComboBoxModel);
+                       break;
+
+               case ShortMessage.CONTROL_CHANGE: // Control Change / Mode Change
+                       data1Text.setTitle("[Data1] Control/Mode No.");
+                       data1ComboBox.setModel(controlChangeComboBoxModel);
+                       break;
+
+               case ShortMessage.PROGRAM_CHANGE:
+                       data1Text.setTitle( "[Data1] Program No.");
+                       data1ComboBox.setModel(instrumentComboBoxModel);
+                       break;
+
+               case ShortMessage.CHANNEL_PRESSURE:
+                       data1Text.setTitle("[Data1] Pressure");
+                       data1ComboBox.setModel(hexData1ComboBoxModel);
+                       break;
+
+               case ShortMessage.PITCH_BEND:
+                       data1Text.setTitle("[Data1] Low 7bit value");
+                       data1ComboBox.setModel(hexData1ComboBoxModel);
+                       break;
+
+               default:
+                       if( msgStatus == 0xFF ) { // MetaMessage
+                               data1Text.setTitle("[Data1] MetaEvent Type");
+                               data1ComboBox.setModel(metaTypeComboBoxModel);
+                               int msgType = data1Text.getValue();
+                               tempoSelecter.setVisible( msgType == 0x51 );
+                               timesigSelecter.setVisible( msgType == 0x58 );
+                               keysigSelecter.setVisible( msgType == 0x59 );
+                               //
+                               if( MIDISpec.isEOT(msgType) ) {
+                                       dataText.clear();
+                                       dataText.setVisible(false);
+                               }
+                               else {
+                                       dataText.setTitle(MIDISpec.hasMetaText(msgType)?"Text:":"Data:");
+                               }
+                       }
+                       else {
+                               data1Text.setTitle("[Data1] ");
+                               data1ComboBox.setModel(hexData1ComboBoxModel);
+                       }
+                       break;
+               }
+       }
+       /**
+        * 入力している内容からMIDIメッセージを生成して返します。
+        * @param charset 文字コード
+        * @return 入力している内容から生成したMIDIメッセージ
+        */
+       public MidiMessage getMessage(Charset charset) {
+               int msgStatus = statusText.getValue();
+               if( msgStatus < 0 ) {
+                       return null;
+               }
+               else if( msgStatus == 0xFF ) {
+                       int msgType = data1Text.getValue();
+                       if( msgType < 0 ) return null;
+                       byte msgData[];
+                       if( MIDISpec.hasMetaText(msgType) ) {
+                               msgData = dataText.getString().getBytes(charset);
+                       }
+                       else if( msgType == 0x2F ) { // EOT
+                               // To avoid inserting un-removable EOT, ignore the data.
+                               msgData = new byte[0];
+                       }
+                       else {
+                               if( (msgData = dataText.getBytes() ) == null ) {
+                                       return null;
+                               }
+                       }
+                       MetaMessage msg = new MetaMessage();
+                       try {
+                               msg.setMessage( msgType, msgData, msgData.length );
+                       } catch( InvalidMidiDataException e ) {
+                               e.printStackTrace();
+                               return null;
+                       }
+                       return (MidiMessage)msg;
+               }
+               else if( msgStatus == 0xF0 || msgStatus == 0xF7 ) {
+                       SysexMessage msg = new SysexMessage();
+                       byte data[] = dataText.getBytes();
+                       if( data == null ) return null;
+                       try {
+                               msg.setMessage(
+                                               (int)(msgStatus & 0xFF), data, data.length
+                                               );
+                       } catch( InvalidMidiDataException e ) {
+                               e.printStackTrace();
+                               return null;
+                       }
+                       return (MidiMessage)msg;
+               }
+               ShortMessage msg = new ShortMessage();
+               int msg_data1 = data1Text.getValue();
+               if( msg_data1 < 0 ) msg_data1 = 0;
+               int msg_data2 = data2Text.getValue();
+               if( msg_data2 < 0 ) msg_data2 = 0;
+               try {
+                       if( MIDISpec.isChannelMessage( msgStatus ) ) {
+                               msg.setMessage(
+                                       (msgStatus & 0xF0),
+                                       channelText.getSelectedChannel(),
+                                       msg_data1, msg_data2
+                               );
+                       }
+                       else {
+                               msg.setMessage( msgStatus, msg_data1, msg_data2 );
+                       }
+               } catch( InvalidMidiDataException e ) {
+                       e.printStackTrace();
+                       return null;
+               }
+               return (MidiMessage)msg;
+       }
+       /**
+        * MIDIメッセージを入力欄に反映します。
+        * @param msg MIDIメッセージ
+        */
+       public void setMessage(MidiMessage msg, Charset charset) {
+               if( msg instanceof ShortMessage ) {
+                       ShortMessage smsg = (ShortMessage)msg;
+                       int msgChannel = 0;
+                       int msgStatus = smsg.getStatus();
+                       if( MIDISpec.isChannelMessage(msgStatus) ) {
+                               msgStatus = smsg.getCommand();
+                               msgChannel = smsg.getChannel();
+                       }
+                       statusText.setValue( msgStatus );
+                       channelText.setSelectedChannel( msgChannel );
+                       data1Text.setValue( smsg.getData1() );
+                       data2Text.setValue( smsg.getData2() );
+               }
+               else if( msg instanceof SysexMessage ) {
+                       SysexMessage sysexMsg = (SysexMessage)msg;
+                       statusText.setValue( sysexMsg.getStatus() );
+                       dataText.setValue( sysexMsg.getData() );
+               }
+               else if( msg instanceof MetaMessage ) {
+                       MetaMessage metaMsg = (MetaMessage)msg;
+                       int msgType = metaMsg.getType();
+                       byte data[] = metaMsg.getData();
+                       statusText.setValue( 0xFF );
+                       data1Text.setValue(msgType);
+                       switch(msgType) {
+                       case 0x51: tempoSelecter.setTempo(data); break;
+                       case 0x58: timesigSelecter.setValue(data[0], data[1]); break;
+                       case 0x59: keysigSelecter.setKey(new Key(data)); break;
+                       default: break;
+                       }
+                       if( MIDISpec.hasMetaText(msgType) ) {
+                               dataText.setString(new String(data,charset));
+                       }
+                       else {
+                               dataText.setValue(data);
+                       }
+                       updateVisible();
+               }
+       }
+       /**
+        * ノートメッセージを設定します。
+        * @param channel MIDIチャンネル
+        * @param noteNumber ノート番号
+        * @param velocity ベロシティ
+        * @return 常にtrue
+        */
+       public boolean setNote(int channel, int noteNumber, int velocity) {
+               channelText.setSelectedChannel(channel);
+               data1Text.setValue(noteNumber);
+               data2Text.setValue(velocity);
+               return true;
+       }
+       /**
+        * 入力内容がノートメッセージかどうか調べます。
+        * @return ノートメッセージのときtrue
+        */
+       public boolean isNote() {
+               return isNote(statusText.getValue());
+       }
+       /**
+        * 入力内容がノートメッセージかどうか調べます。
+        * @param status MIDIメッセージのステータス
+        * @return ノートメッセージのときtrue
+        */
+       public boolean isNote(int status) {
+               int cmd = status & 0xF0;
+               return (cmd == ShortMessage.NOTE_ON || cmd == ShortMessage.NOTE_OFF);
+       }
+       /**
+        * 入力内容が NoteOn または NoteOff かどうか調べます。
+        * @param isNoteOn NoteOnを調べるときtrue、NoteOffを調べるときfalse
+        * @return 該当するときtrue
+        */
+       public boolean isNote(boolean isNoteOn) {
+               int status = statusText.getValue();
+               int cmd = status & 0xF0;
+               return (
+                       isNoteOn && cmd == ShortMessage.NOTE_ON && data2Text.getValue() > 0
+                       ||
+                       !isNoteOn && (
+                               cmd == ShortMessage.NOTE_ON && data2Text.getValue() <= 0 ||
+                               cmd == ShortMessage.NOTE_OFF
+                       )
+               );
+       }
+       /**
+        * 入力されたMIDIメッセージがNoteOn/NoteOffのときに、
+        * そのパートナーとなるNoteOff/NoteOnメッセージを生成します。
+        *
+        * @return パートナーメッセージ(ない場合null)
+        */
+       public ShortMessage createPartnerMessage() {
+               ShortMessage sm = (ShortMessage)getMessage(Charset.defaultCharset());
+               if( sm == null ) return null;
+               ShortMessage partnerSm;
+               if( isNote(true) ) { // NoteOn
+                       partnerSm = new ShortMessage();
+                       try{
+                               partnerSm.setMessage(
+                                       ShortMessage.NOTE_OFF,
+                                       sm.getChannel(), sm.getData1(), sm.getData2()
+                               );
+                       } catch( InvalidMidiDataException e ) {
+                               e.printStackTrace();
+                               return null;
+                       }
+                       return partnerSm;
+               }
+               else if( isNote(false) ) { // NoteOff
+                       partnerSm = new ShortMessage();
+                       try{
+                               partnerSm.setMessage(
+                                       ShortMessage.NOTE_ON,
+                                       sm.getChannel(),
+                                       sm.getData1() == 0 ? 100 : sm.getData1(),
+                                       sm.getData2()
+                               );
+                       } catch( InvalidMidiDataException e ) {
+                               e.printStackTrace();
+                               return null;
+                       }
+                       return partnerSm;
+               }
+               return null;
+       }
+}
\ No newline at end of file
diff --git a/src/camidion/chordhelper/midieditor/MidiProgramFamilySelecter.java b/src/camidion/chordhelper/midieditor/MidiProgramFamilySelecter.java
new file mode 100644 (file)
index 0000000..b5e8cd2
--- /dev/null
@@ -0,0 +1,42 @@
+package camidion.chordhelper.midieditor;
+
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+
+import javax.swing.JComboBox;
+
+import camidion.chordhelper.music.MIDISpec;
+
+/**
+ * MIDI Instrument (Program) Family - 音色ファミリーの選択
+ */
+public class MidiProgramFamilySelecter extends JComboBox<String> implements ActionListener {
+       private MidiProgramSelecter programSelecter = null;
+       public MidiProgramFamilySelecter() { this(null); }
+       public MidiProgramFamilySelecter( MidiProgramSelecter mps ) {
+               programSelecter = mps;
+               setMaximumRowCount(17);
+               addItem("Program:");
+               for( int i=0; i < MIDISpec.instrumentFamilyNames.length; i++ ) {
+                       addItem( (i*8) + "-" + (i*8+7) + ": " + MIDISpec.instrumentFamilyNames[i] );
+               }
+               setSelectedIndex(0);
+               addActionListener(this);
+       }
+       public void actionPerformed(ActionEvent event) {
+               if( programSelecter == null ) return;
+               int i = getSelectedIndex();
+               programSelecter.setFamily( i < 0 ? i : i-1 );
+       }
+       public int getProgram() {
+               int i = getSelectedIndex();
+               if( i <= 0 ) return -1;
+               else return (i-1)*8;
+       }
+       public String getProgramFamilyName() { return (String)( getSelectedItem() ); }
+       public void setProgram( int programNumber ) {
+               if( programNumber < 0 ) programNumber = 0;
+               else programNumber = programNumber / 8 + 1;
+               setSelectedIndex( programNumber );
+       }
+}
\ No newline at end of file
diff --git a/src/camidion/chordhelper/midieditor/MidiProgramSelecter.java b/src/camidion/chordhelper/midieditor/MidiProgramSelecter.java
new file mode 100644 (file)
index 0000000..8a84584
--- /dev/null
@@ -0,0 +1,56 @@
+package camidion.chordhelper.midieditor;
+
+import javax.swing.JComboBox;
+
+import camidion.chordhelper.music.MIDISpec;
+
+/**
+ * MIDI Instrument (Program) - 音色選択
+ */
+public class MidiProgramSelecter extends JComboBox<String> {
+       private int family;
+       private MidiProgramFamilySelecter family_selecter = null;
+       public MidiProgramSelecter() {
+               setFamily(-1);
+       }
+       public void setFamilySelecter( MidiProgramFamilySelecter mpfs ) {
+               family_selecter = mpfs;
+       }
+       public void setFamily( int family ) {
+               int program_no = getProgram();
+               this.family = family;
+               removeAllItems();
+               if( family < 0 ) {
+                       setMaximumRowCount(16);
+                       for( int i=0; i < MIDISpec.instrumentNames.length; i++ ) {
+                               addItem(i+": " + MIDISpec.instrumentNames[i]);
+                       }
+                       setSelectedIndex(program_no);
+               }
+               else {
+                       setMaximumRowCount(8);
+                       for( int i=0; i < 8; i++ ) {
+                               program_no = i + family * 8;
+                               addItem( program_no + ": " + MIDISpec.instrumentNames[program_no] );
+                       }
+                       setSelectedIndex(0);
+               }
+       }
+       public int getProgram() {
+               int program_no = getSelectedIndex();
+               if( family > 0 && program_no >= 0 ) program_no += family * 8;
+               return program_no;
+       }
+       public String getProgramName() { return (String)( getSelectedItem() ); }
+       public void setProgram( int program_no ) {
+               if( getItemCount() == 0 ) return; // To ignore event triggered by removeAllItems()
+               if( family >= 0 && program_no >= 0 && family == program_no / 8 ) {
+                       setSelectedIndex(program_no % 8);
+               }
+               else {
+                       if( family >= 0 ) setFamily(-1);
+                       if( family_selecter != null ) family_selecter.setSelectedIndex(0);
+                       if( program_no < getItemCount() ) setSelectedIndex(program_no);
+               }
+       }
+}
\ No newline at end of file
diff --git a/src/camidion/chordhelper/midieditor/MidiSequenceEditor.java b/src/camidion/chordhelper/midieditor/MidiSequenceEditor.java
new file mode 100644 (file)
index 0000000..8c6b57c
--- /dev/null
@@ -0,0 +1,1354 @@
+package camidion.chordhelper.midieditor;
+
+import java.awt.Component;
+import java.awt.Container;
+import java.awt.Dimension;
+import java.awt.FlowLayout;
+import java.awt.Insets;
+import java.awt.datatransfer.DataFlavor;
+import java.awt.datatransfer.Transferable;
+import java.awt.dnd.DnDConstants;
+import java.awt.dnd.DropTarget;
+import java.awt.dnd.DropTargetDragEvent;
+import java.awt.dnd.DropTargetDropEvent;
+import java.awt.dnd.DropTargetEvent;
+import java.awt.dnd.DropTargetListener;
+import java.awt.event.ActionEvent;
+import java.awt.event.ComponentAdapter;
+import java.awt.event.ComponentEvent;
+import java.awt.event.ComponentListener;
+import java.awt.event.ItemEvent;
+import java.awt.event.ItemListener;
+import java.awt.event.MouseEvent;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.nio.charset.Charset;
+import java.security.AccessControlException;
+import java.util.EventObject;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.sound.midi.InvalidMidiDataException;
+import javax.sound.midi.MidiChannel;
+import javax.sound.midi.MidiEvent;
+import javax.sound.midi.MidiMessage;
+import javax.sound.midi.Sequencer;
+import javax.sound.midi.ShortMessage;
+import javax.swing.AbstractAction;
+import javax.swing.AbstractCellEditor;
+import javax.swing.Action;
+import javax.swing.Box;
+import javax.swing.BoxLayout;
+import javax.swing.DefaultCellEditor;
+import javax.swing.Icon;
+import javax.swing.JButton;
+import javax.swing.JCheckBox;
+import javax.swing.JComboBox;
+import javax.swing.JDialog;
+import javax.swing.JFileChooser;
+import javax.swing.JLabel;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.JSplitPane;
+import javax.swing.JTable;
+import javax.swing.JToggleButton;
+import javax.swing.ListSelectionModel;
+import javax.swing.event.ListSelectionEvent;
+import javax.swing.event.ListSelectionListener;
+import javax.swing.event.TableModelEvent;
+import javax.swing.filechooser.FileFilter;
+import javax.swing.filechooser.FileNameExtensionFilter;
+import javax.swing.table.JTableHeader;
+import javax.swing.table.TableCellEditor;
+import javax.swing.table.TableCellRenderer;
+import javax.swing.table.TableColumn;
+import javax.swing.table.TableColumnModel;
+import javax.swing.table.TableModel;
+
+import camidion.chordhelper.ButtonIcon;
+import camidion.chordhelper.ChordHelperApplet;
+import camidion.chordhelper.mididevice.AbstractVirtualMidiDevice;
+import camidion.chordhelper.mididevice.MidiSequencerModel;
+import camidion.chordhelper.mididevice.VirtualMidiDevice;
+import camidion.chordhelper.music.MIDISpec;
+
+/**
+ * MIDIエディタ(MIDI Editor/Playlist for MIDI Chord Helper)
+ *
+ * @author
+ *     Copyright (C) 2006-2014 Akiyoshi Kamide
+ *     http://www.yk.rim.or.jp/~kamide/music/chordhelper/
+ */
+public class MidiSequenceEditor extends JDialog implements DropTargetListener {
+       private static VirtualMidiDevice virtualMidiDevice = new AbstractVirtualMidiDevice() {
+               {
+                       info = new MyInfo();
+                       setMaxReceivers(0); // 送信専用とする(MIDI IN はサポートしない)
+               }
+               /**
+                * MIDIデバイス情報
+                */
+               protected MyInfo info;
+               @Override
+               public Info getDeviceInfo() {
+                       return info;
+               }
+               class MyInfo extends Info {
+                       protected MyInfo() {
+                               super("MIDI Editor","Unknown vendor","MIDI sequence editor","");
+                       }
+               }
+       };
+       /**
+        * このMIDIエディタの仮想MIDIデバイスを返します。
+        */
+       public VirtualMidiDevice getVirtualMidiDevice() {
+               return virtualMidiDevice;
+       }
+       /**
+        * このダイアログを表示するアクション
+        */
+       public Action openAction = new AbstractAction(
+               "Edit/Playlist/Speed", new ButtonIcon(ButtonIcon.EDIT_ICON)
+       ) {
+               {
+                       String tooltip = "MIDIシーケンスの編集/プレイリスト/再生速度調整";
+                       putValue(Action.SHORT_DESCRIPTION, tooltip);
+               }
+               @Override
+               public void actionPerformed(ActionEvent e) { open(); }
+       };
+       /**
+        * このダイアログを開きます。すでに開かれていた場合は前面に移動します。
+        */
+       public void open() {
+               if( isVisible() ) toFront(); else setVisible(true);
+       }
+       /**
+        * エラーメッセージダイアログを表示します。
+        * @param message エラーメッセージ
+        */
+       public void showError(String message) {
+               JOptionPane.showMessageDialog(
+                       this, message,
+                       ChordHelperApplet.VersionInfo.NAME,
+                       JOptionPane.ERROR_MESSAGE
+               );
+       }
+       /**
+        * 警告メッセージダイアログを表示します。
+        * @param message 警告メッセージ
+        */
+       public void showWarning(String message) {
+               JOptionPane.showMessageDialog(
+                       this, message,
+                       ChordHelperApplet.VersionInfo.NAME,
+                       JOptionPane.WARNING_MESSAGE
+               );
+       }
+       /**
+        * 確認ダイアログを表示します。
+        * @param message 確認メッセージ
+        * @return 確認OKのときtrue
+        */
+       boolean confirm(String message) {
+               return JOptionPane.showConfirmDialog(
+                       this, message,
+                       ChordHelperApplet.VersionInfo.NAME,
+                       JOptionPane.YES_NO_OPTION,
+                       JOptionPane.WARNING_MESSAGE
+               ) == JOptionPane.YES_OPTION ;
+       }
+       @Override
+       public void dragEnter(DropTargetDragEvent event) {
+               if( event.isDataFlavorSupported(DataFlavor.javaFileListFlavor) ) {
+                       event.acceptDrag(DnDConstants.ACTION_COPY_OR_MOVE);
+               }
+       }
+       @Override
+       public void dragExit(DropTargetEvent event) {}
+       @Override
+       public void dragOver(DropTargetDragEvent event) {}
+       @Override
+       public void dropActionChanged(DropTargetDragEvent event) {}
+       @Override
+       @SuppressWarnings("unchecked")
+       public void drop(DropTargetDropEvent event) {
+               event.acceptDrop(DnDConstants.ACTION_COPY_OR_MOVE);
+               try {
+                       int action = event.getDropAction();
+                       if ( (action & DnDConstants.ACTION_COPY_OR_MOVE) != 0 ) {
+                               Transferable t = event.getTransferable();
+                               Object data = t.getTransferData(DataFlavor.javaFileListFlavor);
+                               loadAndPlay((List<File>)data);
+                               event.dropComplete(true);
+                               return;
+                       }
+                       event.dropComplete(false);
+               }
+               catch (Exception ex) {
+                       ex.printStackTrace();
+                       event.dropComplete(false);
+               }
+       }
+       /**
+        * 複数のMIDIファイルを読み込み、再生されていなかったら再生します。
+        * すでに再生されていた場合、このエディタダイアログを表示します。
+        *
+        * @param fileList 読み込むMIDIファイルのリスト
+        */
+       public void loadAndPlay(List<File> fileList) {
+               int firstIndex = -1;
+               PlaylistTableModel playlist = sequenceListTable.getModel();
+               try {
+                       firstIndex = playlist.addSequences(fileList);
+               } catch(IOException|InvalidMidiDataException e) {
+                       showWarning(e.getMessage());
+               } catch(AccessControlException e) {
+                       showError(e.getMessage());
+                       e.printStackTrace();
+               }
+               if(playlist.sequencerModel.getSequencer().isRunning()) {
+                       open();
+               }
+               else if( firstIndex >= 0 ) {
+                       playlist.loadToSequencer(firstIndex);
+                       playlist.sequencerModel.start();
+               }
+       }
+       private static final Insets ZERO_INSETS = new Insets(0,0,0,0);
+       private static final Icon deleteIcon = new ButtonIcon(ButtonIcon.X_ICON);
+       /**
+        * 新しいMIDIシーケンスを生成するダイアログ
+        */
+       public NewSequenceDialog newSequenceDialog = new NewSequenceDialog(this);
+       /**
+        * BASE64テキスト入力ダイアログ
+        */
+       public Base64Dialog base64Dialog = new Base64Dialog(this);
+       /**
+        * プレイリストビュー(シーケンスリスト)
+        */
+       public SequenceListTable sequenceListTable;
+       /**
+        * MIDIトラックリストテーブルビュー(選択中のシーケンスの中身)
+        */
+       private TrackListTable trackListTable;
+       /**
+        * MIDIイベントリストテーブルビュー(選択中のトラックの中身)
+        */
+       private EventListTable eventListTable;
+       /**
+        * MIDIイベント入力ダイアログ(イベント入力とイベント送出で共用)
+        */
+       public MidiEventDialog  eventDialog = new MidiEventDialog();
+       /**
+        * プレイリストビュー(シーケンスリスト)
+        */
+       public class SequenceListTable extends JTable {
+               /**
+                * ファイル選択ダイアログ(アプレットでは使用不可)
+                */
+               private MidiFileChooser midiFileChooser;
+               /**
+                * BASE64エンコードアクション(ライブラリが見えている場合のみ有効)
+                */
+               private Action base64EncodeAction;
+               /**
+                * プレイリストビューを構築します。
+                * @param model プレイリストデータモデル
+                */
+               public SequenceListTable(PlaylistTableModel model) {
+                       super(model, null, model.sequenceListSelectionModel);
+                       try {
+                               midiFileChooser = new MidiFileChooser();
+                       }
+                       catch( ExceptionInInitializerError|NoClassDefFoundError|AccessControlException e ) {
+                               // アプレットの場合、Webクライアントマシンのローカルファイルには
+                               // アクセスできないので、ファイル選択ダイアログは使用不可。
+                               midiFileChooser = null;
+                       }
+                       // 再生ボタンを埋め込む
+                       new PlayButtonCellEditor();
+                       new PositionCellEditor();
+                       //
+                       // 文字コード選択をプルダウンにする
+                       int column = PlaylistTableModel.Column.CHARSET.ordinal();
+                       TableCellEditor ce = new DefaultCellEditor(new JComboBox<Charset>() {{
+                               Set<Map.Entry<String,Charset>> entrySet = Charset.availableCharsets().entrySet();
+                               for( Map.Entry<String,Charset> entry : entrySet ) addItem(entry.getValue());
+                       }});
+                       getColumnModel().getColumn(column).setCellEditor(ce);
+                       setAutoCreateColumnsFromModel(false);
+                       //
+                       // Base64エンコードアクションの生成
+                       if( base64Dialog.isBase64Available() ) {
+                               base64EncodeAction = new AbstractAction("Base64") {
+                                       {
+                                               String tooltip = "Base64 text conversion - Base64テキスト変換";
+                                               putValue(Action.SHORT_DESCRIPTION, tooltip);
+                                       }
+                                       @Override
+                                       public void actionPerformed(ActionEvent e) {
+                                               SequenceTrackListTableModel mstm = getModel().getSelectedSequenceModel();
+                                               byte[] data = null;
+                                               String filename = null;
+                                               if( mstm != null ) {
+                                                       data = mstm.getMIDIdata();
+                                                       filename = mstm.getFilename();
+                                               }
+                                               base64Dialog.setMIDIData(data, filename);
+                                               base64Dialog.setVisible(true);
+                                       }
+                               };
+                       }
+                       TableColumnModel colModel = getColumnModel();
+                       for( PlaylistTableModel.Column c : PlaylistTableModel.Column.values() ) {
+                               TableColumn tc = colModel.getColumn(c.ordinal());
+                               tc.setPreferredWidth(c.preferredWidth);
+                               if( c == PlaylistTableModel.Column.LENGTH ) {
+                                       lengthColumn = tc;
+                               }
+                       }
+               }
+               private TableColumn lengthColumn;
+               @Override
+               public void tableChanged(TableModelEvent event) {
+                       super.tableChanged(event);
+                       //
+                       // タイトルに合計シーケンス長を表示
+                       if( lengthColumn != null ) {
+                               int sec = getModel().getTotalSeconds();
+                               String title = PlaylistTableModel.Column.LENGTH.title;
+                               title = String.format(title+" [%02d:%02d]", sec/60, sec%60);
+                               lengthColumn.setHeaderValue(title);
+                       }
+                       //
+                       // シーケンス削除時など、合計シーケンス長が変わっても
+                       // 列モデルからではヘッダタイトルが再描画されないことがある。
+                       // そこで、ヘッダビューから repaint() で突っついて再描画させる。
+                       JTableHeader th = getTableHeader();
+                       if( th != null ) {
+                               th.repaint();
+                       }
+               }
+               /**
+                * 時間位置表示セルエディタ(ダブルクリック専用)
+                */
+               private class PositionCellEditor extends AbstractCellEditor
+                       implements TableCellEditor
+               {
+                       public PositionCellEditor() {
+                               int column = PlaylistTableModel.Column.POSITION.ordinal();
+                               TableColumn tc = getColumnModel().getColumn(column);
+                               tc.setCellEditor(this);
+                       }
+                       /**
+                        * セルをダブルクリックしたときだけ編集モードに入るようにします。
+                        * @param e イベント(マウスイベント)
+                        * @return 編集可能になったらtrue
+                        */
+                       @Override
+                       public boolean isCellEditable(EventObject e) {
+                               // マウスイベント以外のイベントでは編集不可
+                               if( ! (e instanceof MouseEvent) ) return false;
+                               return ((MouseEvent)e).getClickCount() == 2;
+                       }
+                       @Override
+                       public Object getCellEditorValue() { return null; }
+                       /**
+                        * 編集モード時のコンポーネントを返すタイミングで
+                        * そのシーケンスをシーケンサーにロードしたあと、
+                        * すぐに編集モードを解除します。
+                        * @return 常にnull
+                        */
+                       @Override
+                       public Component getTableCellEditorComponent(
+                               JTable table, Object value, boolean isSelected,
+                               int row, int column
+                       ) {
+                               getModel().loadToSequencer(row);
+                               fireEditingStopped();
+                               return null;
+                       }
+               }
+               /**
+                * プレイボタンを埋め込んだセルエディタ
+                */
+               private class PlayButtonCellEditor extends AbstractCellEditor
+                       implements TableCellEditor, TableCellRenderer
+               {
+                       private JToggleButton playButton = new JToggleButton(
+                               getModel().sequencerModel.startStopAction
+                       );
+                       public PlayButtonCellEditor() {
+                               playButton.setMargin(ZERO_INSETS);
+                               int column = PlaylistTableModel.Column.PLAY.ordinal();
+                               TableColumn tc = getColumnModel().getColumn(column);
+                               tc.setCellRenderer(this);
+                               tc.setCellEditor(this);
+                       }
+                       /**
+                        * {@inheritDoc}
+                        *
+                        * <p>この実装では、クリックしたセルのシーケンスが
+                        * シーケンサーにロードされている場合に
+                        * trueを返してプレイボタンを押せるようにします。
+                        * そうでない場合はプレイボタンのないセルなので、
+                        * ダブルクリックされたときだけtrueを返します。
+                        * </p>
+                        */
+                       @Override
+                       public boolean isCellEditable(EventObject e) {
+                               if( ! (e instanceof MouseEvent) ) {
+                                       // マウスイベント以外はデフォルトメソッドにお任せ
+                                       return super.isCellEditable(e);
+                               }
+                               fireEditingStopped();
+                               MouseEvent me = (MouseEvent)e;
+                               // クリックされたセルの行を特定
+                               int row = rowAtPoint(me.getPoint());
+                               if( row < 0 )
+                                       return false;
+                               PlaylistTableModel model = getModel();
+                               if( row >= model.getRowCount() )
+                                       return false;
+                               if( model.sequenceList.get(row).isOnSequencer() ) {
+                                       // プレイボタン表示中のセルはシングルクリックでもOK
+                                       return true;
+                               }
+                               // プレイボタンのないセルはダブルクリックのみを受け付ける
+                               return me.getClickCount() == 2;
+                       }
+                       @Override
+                       public Object getCellEditorValue() { return null; }
+                       /**
+                        * {@inheritDoc}
+                        *
+                        * <p>この実装では、行の表すシーケンスが
+                        * シーケンサーにロードされている場合にプレイボタンを返します。
+                        * そうでない場合は、
+                        * そのシーケンスをシーケンサーにロードしてnullを返します。
+                        * </p>
+                        */
+                       @Override
+                       public Component getTableCellEditorComponent(
+                               JTable table, Object value, boolean isSelected, int row, int column
+                       ) {
+                               fireEditingStopped();
+                               PlaylistTableModel model = getModel();
+                               if( model.sequenceList.get(row).isOnSequencer() ) {
+                                       return playButton;
+                               }
+                               model.loadToSequencer(row);
+                               return null;
+                       }
+                       @Override
+                       public Component getTableCellRendererComponent(
+                               JTable table, Object value, boolean isSelected,
+                               boolean hasFocus, int row, int column
+                       ) {
+                               PlaylistTableModel model = getModel();
+                               if(model.sequenceList.get(row).isOnSequencer()) return playButton;
+                               Class<?> cc = model.getColumnClass(column);
+                               TableCellRenderer defaultRenderer = table.getDefaultRenderer(cc);
+                               return defaultRenderer.getTableCellRendererComponent(
+                                       table, value, isSelected, hasFocus, row, column
+                               );
+                       }
+               }
+               /**
+                * このプレイリスト(シーケンスリスト)が表示するデータを提供する
+                * プレイリストモデルを返します。
+                * @return プレイリストモデル
+                */
+               @Override
+               public PlaylistTableModel getModel() {
+                       return (PlaylistTableModel) super.getModel();
+               }
+               /**
+                * シーケンスを削除するアクション
+                */
+               Action deleteSequenceAction = getModel().new SelectedSequenceAction(
+                       "Delete", MidiSequenceEditor.deleteIcon,
+                       "Delete selected MIDI sequence - 選択した曲をプレイリストから削除"
+               ) {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               PlaylistTableModel model = getModel();
+                               if( midiFileChooser != null ) {
+                                       // ファイルに保存できる場合(Javaアプレットではなく、Javaアプリとして動作している場合)
+                                       //
+                                       SequenceTrackListTableModel seqModel = model.getSelectedSequenceModel();
+                                       if( seqModel.isModified() ) {
+                                               // ファイル未保存の変更がある場合
+                                               //
+                                               String message =
+                                                       "Selected MIDI sequence not saved - delete it ?\n" +
+                                                       "選択したMIDIシーケンスはまだ保存されていません。削除しますか?";
+                                               if( ! confirm(message) ) {
+                                                       // 実は削除してほしくなかった場合
+                                                       return;
+                                               }
+                                       }
+                               }
+                               // 削除を実行
+                               model.removeSelectedSequence();
+                       }
+               };
+               /**
+                * ファイル選択ダイアログ(アプレットでは使用不可)
+                */
+               private class MidiFileChooser extends JFileChooser {
+                       {
+                               String description = "MIDI sequence (*.mid)";
+                               String extension = "mid";
+                               FileFilter filter = new FileNameExtensionFilter(description, extension);
+                               setFileFilter(filter);
+                       }
+                       /**
+                        * ファイル保存アクション
+                        */
+                       public Action saveMidiFileAction = getModel().new SelectedSequenceAction(
+                               "Save",
+                               "Save selected MIDI sequence to file - 選択したMIDIシーケンスをファイルに保存"
+                       ) {
+                               @Override
+                               public void actionPerformed(ActionEvent e) {
+                                       PlaylistTableModel model = getModel();
+                                       SequenceTrackListTableModel sequenceModel = model.getSelectedSequenceModel();
+                                       String filename = sequenceModel.getFilename();
+                                       File selectedFile;
+                                       if( filename != null && ! filename.isEmpty() ) {
+                                               // プレイリスト上でファイル名が入っていたら、それを初期選択
+                                               setSelectedFile(selectedFile = new File(filename));
+                                       }
+                                       int saveOption = showSaveDialog(MidiSequenceEditor.this);
+                                       if( saveOption != JFileChooser.APPROVE_OPTION ) {
+                                               // 保存ダイアログでキャンセルされた場合
+                                               return;
+                                       }
+                                       if( (selectedFile = getSelectedFile()).exists() ) {
+                                               // 指定されたファイルがすでにあった場合
+                                               String fn = selectedFile.getName();
+                                               String message = "Overwrite " + fn + " ?\n";
+                                               message += fn + " を上書きしてよろしいですか?";
+                                               if( ! confirm(message) ) {
+                                                       // 上書きしてほしくなかった場合
+                                                       return;
+                                               }
+                                       }
+                                       // 保存を実行
+                                       try ( FileOutputStream out = new FileOutputStream(selectedFile) ) {
+                                               out.write(sequenceModel.getMIDIdata());
+                                               sequenceModel.setModified(false);
+                                       }
+                                       catch( IOException ex ) {
+                                               showError( ex.getMessage() );
+                                               ex.printStackTrace();
+                                       }
+                               }
+                       };
+                       /**
+                        * ファイルを開くアクション
+                        */
+                       public Action openMidiFileAction = new AbstractAction("Open") {
+                               {
+                                       String tooltip = "Open MIDI file - MIDIファイルを開く";
+                                       putValue(Action.SHORT_DESCRIPTION, tooltip);
+                               }
+                               @Override
+                               public void actionPerformed(ActionEvent event) {
+                                       int openOption = showOpenDialog(MidiSequenceEditor.this);
+                                       if(openOption == JFileChooser.APPROVE_OPTION) {
+                                               try  {
+                                                       getModel().addSequence(getSelectedFile());
+                                               } catch( IOException|InvalidMidiDataException e ) {
+                                                       showWarning(e.getMessage());
+                                               } catch( AccessControlException e ) {
+                                                       showError(e.getMessage());
+                                                       e.printStackTrace();
+                                               }
+                                       }
+                               }
+                       };
+               };
+       }
+
+       /**
+        * シーケンス(トラックリスト)テーブルビュー
+        */
+       public class TrackListTable extends JTable {
+               /**
+                * トラックリストテーブルビューを構築します。
+                * @param model シーケンス(トラックリスト)データモデル
+                */
+               public TrackListTable(SequenceTrackListTableModel model) {
+                       super(model, null, model.trackListSelectionModel);
+                       //
+                       // 録音対象のMIDIチャンネルをコンボボックスで選択できるようにする
+                       int colIndex = SequenceTrackListTableModel.Column.RECORD_CHANNEL.ordinal();
+                       TableColumn tc = getColumnModel().getColumn(colIndex);
+                       tc.setCellEditor(new DefaultCellEditor(new JComboBox<String>(){{
+                               addItem("OFF");
+                               for(int i=1; i <= MIDISpec.MAX_CHANNELS; i++)
+                                       addItem(String.format("%d", i));
+                               addItem("ALL");
+                       }}));
+                       setAutoCreateColumnsFromModel(false);
+                       //
+                       trackSelectionListener = new TrackSelectionListener();
+                       titleLabel = new TitleLabel();
+                       model.sequenceListTableModel.sequenceListSelectionModel.addListSelectionListener(titleLabel);
+                       TableColumnModel colModel = getColumnModel();
+                       for( SequenceTrackListTableModel.Column c : SequenceTrackListTableModel.Column.values() )
+                               colModel.getColumn(c.ordinal()).setPreferredWidth(c.preferredWidth);
+               }
+               /**
+                * このテーブルビューが表示するデータを提供する
+                * シーケンス(トラックリスト)データモデルを返します。
+                * @return シーケンス(トラックリスト)データモデル
+                */
+               @Override
+               public SequenceTrackListTableModel getModel() {
+                       return (SequenceTrackListTableModel) super.getModel();
+               }
+               /**
+                * タイトルラベル
+                */
+               TitleLabel titleLabel;
+               /**
+                * 親テーブルの選択シーケンスの変更に反応する
+                * 曲番号表示付きタイトルラベル
+                */
+               private class TitleLabel extends JLabel implements ListSelectionListener {
+                       private static final String TITLE = "Tracks";
+                       public TitleLabel() { setText(TITLE); }
+                       @Override
+                       public void valueChanged(ListSelectionEvent event) {
+                               if( event.getValueIsAdjusting() )
+                                       return;
+                               SequenceTrackListTableModel oldModel = getModel();
+                               SequenceTrackListTableModel newModel = oldModel.sequenceListTableModel.getSelectedSequenceModel();
+                               if( oldModel == newModel )
+                                       return;
+                               //
+                               // MIDIチャンネル選択中のときはキャンセルする
+                               cancelCellEditing();
+                               //
+                               int index = oldModel.sequenceListTableModel.sequenceListSelectionModel.getMinSelectionIndex();
+                               String text = TITLE;
+                               if( index >= 0 ) {
+                                       text = String.format(text+" - MIDI file No.%d", index);
+                               }
+                               setText(text);
+                               if( newModel == null ) {
+                                       newModel = oldModel.sequenceListTableModel.emptyTrackListTableModel;
+                                       addTrackAction.setEnabled(false);
+                               }
+                               else {
+                                       addTrackAction.setEnabled(true);
+                               }
+                               oldModel.trackListSelectionModel.removeListSelectionListener(trackSelectionListener);
+                               setModel(newModel);
+                               setSelectionModel(newModel.trackListSelectionModel);
+                               newModel.trackListSelectionModel.addListSelectionListener(trackSelectionListener);
+                               trackSelectionListener.valueChanged(null);
+                       }
+               }
+               /**
+                * トラック選択リスナー
+                */
+               TrackSelectionListener trackSelectionListener;
+               /**
+                * 選択トラックの変更に反応するリスナー
+                */
+               private class TrackSelectionListener implements ListSelectionListener {
+                       @Override
+                       public void valueChanged(ListSelectionEvent e) {
+                               if( e != null && e.getValueIsAdjusting() )
+                                       return;
+                               ListSelectionModel tlsm = getModel().trackListSelectionModel;
+                               deleteTrackAction.setEnabled(! tlsm.isSelectionEmpty());
+                               eventListTable.titleLabel.update(tlsm, getModel());
+                       }
+               }
+               /**
+                * {@inheritDoc}
+                *
+                * <p>このトラックリストテーブルのデータが変わったときに編集を解除します。
+                * 例えば、イベントが編集された場合や、
+                * シーケンサーからこのモデルが外された場合がこれに該当します。
+                * </p>
+                */
+               @Override
+               public void tableChanged(TableModelEvent e) {
+                       super.tableChanged(e);
+                       cancelCellEditing();
+               }
+               /**
+                * このトラックリストテーブルが編集モードになっていたら解除します。
+                */
+               private void cancelCellEditing() {
+                       TableCellEditor currentCellEditor = getCellEditor();
+                       if( currentCellEditor != null )
+                               currentCellEditor.cancelCellEditing();
+               }
+               /**
+                * トラック追加アクション
+                */
+               Action addTrackAction = new AbstractAction("New") {
+                       {
+                               String tooltip = "Append new track - 新しいトラックの追加";
+                               putValue(Action.SHORT_DESCRIPTION, tooltip);
+                               setEnabled(false);
+                       }
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               getModel().createTrack();
+                       }
+               };
+               /**
+                * トラック削除アクション
+                */
+               Action deleteTrackAction = new AbstractAction("Delete", deleteIcon) {
+                       {
+                               String tooltip = "Delete selected track - 選択したトラックを削除";
+                               putValue(Action.SHORT_DESCRIPTION, tooltip);
+                               setEnabled(false);
+                       }
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               String message = "Do you want to delete selected track ?\n"
+                                       + "選択したトラックを削除しますか?";
+                               if( confirm(message) ) getModel().deleteSelectedTracks();
+                       }
+               };
+       }
+
+       /**
+        * MIDIイベントリストテーブルビュー(選択中のトラックの中身)
+        */
+       public class EventListTable extends JTable {
+               /**
+                * 新しいイベントリストテーブルを構築します。
+                * <p>データモデルとして一つのトラックのイベントリストを指定できます。
+                * トラックを切り替えたいときは {@link #setModel(TableModel)}
+                * でデータモデルを異なるトラックのものに切り替えます。
+                * </p>
+                *
+                * @param model トラック(イベントリスト)データモデル
+                */
+               public EventListTable(TrackEventListTableModel model) {
+                       super(model, null, model.eventSelectionModel);
+                       //
+                       // 列モデルにセルエディタを設定
+                       eventCellEditor = new MidiEventCellEditor();
+                       setAutoCreateColumnsFromModel(false);
+                       //
+                       eventSelectionListener = new EventSelectionListener();
+                       titleLabel = new TitleLabel();
+                       //
+                       TableColumnModel colModel = getColumnModel();
+                       for( TrackEventListTableModel.Column c : TrackEventListTableModel.Column.values() )
+                               colModel.getColumn(c.ordinal()).setPreferredWidth(c.preferredWidth);
+               }
+               /**
+                * このテーブルビューが表示するデータを提供する
+                * トラック(イベントリスト)データモデルを返します。
+                * @return トラック(イベントリスト)データモデル
+                */
+               @Override
+               public TrackEventListTableModel getModel() {
+                       return (TrackEventListTableModel) super.getModel();
+               }
+               /**
+                * タイトルラベル
+                */
+               TitleLabel titleLabel;
+               /**
+                * 親テーブルの選択トラックの変更に反応する
+                * トラック番号つきタイトルラベル
+                */
+               private class TitleLabel extends JLabel {
+                       private static final String TITLE = "MIDI Events";
+                       public TitleLabel() { super(TITLE); }
+                       public void update(ListSelectionModel tlsm, SequenceTrackListTableModel sequenceModel) {
+                               String text = TITLE;
+                               TrackEventListTableModel oldTrackModel = getModel();
+                               int index = tlsm.getMinSelectionIndex();
+                               if( index >= 0 ) {
+                                       text = String.format(TITLE+" - track No.%d", index);
+                               }
+                               setText(text);
+                               TrackEventListTableModel newTrackModel = sequenceModel.getSelectedTrackModel();
+                               if( oldTrackModel == newTrackModel )
+                                       return;
+                               if( newTrackModel == null ) {
+                                       newTrackModel = getModel().sequenceTrackListTableModel.sequenceListTableModel.emptyEventListTableModel;
+                                       queryJumpEventAction.setEnabled(false);
+                                       queryAddEventAction.setEnabled(false);
+
+                                       queryPasteEventAction.setEnabled(false);
+                                       copyEventAction.setEnabled(false);
+                                       deleteEventAction.setEnabled(false);
+                                       cutEventAction.setEnabled(false);
+                               }
+                               else {
+                                       queryJumpEventAction.setEnabled(true);
+                                       queryAddEventAction.setEnabled(true);
+                               }
+                               oldTrackModel.eventSelectionModel.removeListSelectionListener(eventSelectionListener);
+                               setModel(newTrackModel);
+                               setSelectionModel(newTrackModel.eventSelectionModel);
+                               newTrackModel.eventSelectionModel.addListSelectionListener(eventSelectionListener);
+                       }
+               }
+               /**
+                * イベント選択リスナー
+                */
+               private EventSelectionListener eventSelectionListener;
+               /**
+                * 選択イベントの変更に反応するリスナー
+                */
+               private class EventSelectionListener implements ListSelectionListener {
+                       public EventSelectionListener() {
+                               getModel().eventSelectionModel.addListSelectionListener(this);
+                       }
+                       @Override
+                       public void valueChanged(ListSelectionEvent e) {
+                               if( e.getValueIsAdjusting() )
+                                       return;
+                               if( getSelectionModel().isSelectionEmpty() ) {
+                                       queryPasteEventAction.setEnabled(false);
+                                       copyEventAction.setEnabled(false);
+                                       deleteEventAction.setEnabled(false);
+                                       cutEventAction.setEnabled(false);
+                               }
+                               else {
+                                       copyEventAction.setEnabled(true);
+                                       deleteEventAction.setEnabled(true);
+                                       cutEventAction.setEnabled(true);
+                                       TrackEventListTableModel trackModel = getModel();
+                                       int minIndex = getSelectionModel().getMinSelectionIndex();
+                                       MidiEvent midiEvent = trackModel.getMidiEvent(minIndex);
+                                       if( midiEvent != null ) {
+                                               MidiMessage msg = midiEvent.getMessage();
+                                               if( msg instanceof ShortMessage ) {
+                                                       ShortMessage sm = (ShortMessage)msg;
+                                                       int cmd = sm.getCommand();
+                                                       if( cmd == 0x80 || cmd == 0x90 || cmd == 0xA0 ) {
+                                                               // ノート番号を持つ場合、音を鳴らす。
+                                                               MidiChannel outMidiChannels[] = virtualMidiDevice.getChannels();
+                                                               int ch = sm.getChannel();
+                                                               int note = sm.getData1();
+                                                               int vel = sm.getData2();
+                                                               outMidiChannels[ch].noteOn(note, vel);
+                                                               outMidiChannels[ch].noteOff(note, vel);
+                                                       }
+                                               }
+                                       }
+                                       if( pairNoteOnOffModel.isSelected() ) {
+                                               int maxIndex = getSelectionModel().getMaxSelectionIndex();
+                                               int partnerIndex;
+                                               for( int i=minIndex; i<=maxIndex; i++ ) {
+                                                       if( ! getSelectionModel().isSelectedIndex(i) ) continue;
+                                                       partnerIndex = trackModel.getIndexOfPartnerFor(i);
+                                                       if( partnerIndex >= 0 && ! getSelectionModel().isSelectedIndex(partnerIndex) )
+                                                               getSelectionModel().addSelectionInterval(partnerIndex, partnerIndex);
+                                               }
+                                       }
+                               }
+                       }
+               }
+               /**
+                * Pair noteON/OFF トグルボタンモデル
+                */
+               private JToggleButton.ToggleButtonModel
+                       pairNoteOnOffModel = new JToggleButton.ToggleButtonModel() {
+                               {
+                                       addItemListener(
+                                               new ItemListener() {
+                                                       public void itemStateChanged(ItemEvent e) {
+                                                               eventDialog.midiMessageForm.durationForm.setEnabled(isSelected());
+                                                       }
+                                               }
+                                       );
+                                       setSelected(true);
+                               }
+                       };
+               private class EventEditContext {
+                       /**
+                        * 編集対象トラック
+                        */
+                       private TrackEventListTableModel trackModel;
+                       /**
+                        * tick位置入力モデル
+                        */
+                       private TickPositionModel tickPositionModel = new TickPositionModel();
+                       /**
+                        * 選択されたイベント
+                        */
+                       private MidiEvent selectedMidiEvent = null;
+                       /**
+                        * 選択されたイベントの場所
+                        */
+                       private int selectedIndex = -1;
+                       /**
+                        * 選択されたイベントのtick位置
+                        */
+                       private long currentTick = 0;
+                       /**
+                        * 上書きして削除対象にする変更前イベント(null可)
+                        */
+                       private MidiEvent[] midiEventsToBeOverwritten;
+                       /**
+                        * 選択したイベントを入力ダイアログなどに反映します。
+                        * @param model 対象データモデル
+                        */
+                       private void setSelectedEvent(TrackEventListTableModel trackModel) {
+                               this.trackModel = trackModel;
+                               SequenceTrackListTableModel sequenceTableModel = trackModel.sequenceTrackListTableModel;
+                               int ppq = sequenceTableModel.getSequence().getResolution();
+                               eventDialog.midiMessageForm.durationForm.setPPQ(ppq);
+                               tickPositionModel.setSequenceIndex(sequenceTableModel.getSequenceTickIndex());
+
+                               selectedIndex = trackModel.eventSelectionModel.getMinSelectionIndex();
+                               selectedMidiEvent = selectedIndex < 0 ? null : trackModel.getMidiEvent(selectedIndex);
+                               currentTick = selectedMidiEvent == null ? 0 : selectedMidiEvent.getTick();
+                               tickPositionModel.setTickPosition(currentTick);
+                       }
+                       public void setupForEdit(TrackEventListTableModel trackModel) {
+                               MidiEvent partnerEvent = null;
+                               eventDialog.midiMessageForm.setMessage(
+                                       selectedMidiEvent.getMessage(),
+                                       trackModel.sequenceTrackListTableModel.charset
+                               );
+                               if( eventDialog.midiMessageForm.isNote() ) {
+                                       int partnerIndex = trackModel.getIndexOfPartnerFor(selectedIndex);
+                                       if( partnerIndex < 0 ) {
+                                               eventDialog.midiMessageForm.durationForm.setDuration(0);
+                                       }
+                                       else {
+                                               partnerEvent = trackModel.getMidiEvent(partnerIndex);
+                                               long partnerTick = partnerEvent.getTick();
+                                               long duration = currentTick > partnerTick ?
+                                                       currentTick - partnerTick : partnerTick - currentTick ;
+                                               eventDialog.midiMessageForm.durationForm.setDuration((int)duration);
+                                       }
+                               }
+                               if(partnerEvent == null)
+                                       midiEventsToBeOverwritten = new MidiEvent[] {selectedMidiEvent};
+                               else
+                                       midiEventsToBeOverwritten = new MidiEvent[] {selectedMidiEvent, partnerEvent};
+                       }
+                       private Action jumpEventAction = new AbstractAction() {
+                               { putValue(NAME,"Jump"); }
+                               public void actionPerformed(ActionEvent e) {
+                                       long tick = tickPositionModel.getTickPosition();
+                                       scrollToEventAt(tick);
+                                       eventDialog.setVisible(false);
+                                       trackModel = null;
+                               }
+                       };
+                       private Action pasteEventAction = new AbstractAction() {
+                               { putValue(NAME,"Paste"); }
+                               public void actionPerformed(ActionEvent e) {
+                                       long tick = tickPositionModel.getTickPosition();
+                                       clipBoard.paste(trackModel, tick);
+                                       scrollToEventAt(tick);
+                                       // ペーストで曲の長さが変わったことをプレイリストに通知
+                                       SequenceTrackListTableModel seqModel = trackModel.sequenceTrackListTableModel;
+                                       seqModel.sequenceListTableModel.fireSequenceModified(seqModel);
+                                       eventDialog.setVisible(false);
+                                       trackModel = null;
+                               }
+                       };
+                       private boolean applyEvent() {
+                               long tick = tickPositionModel.getTickPosition();
+                               MidiMessageForm form = eventDialog.midiMessageForm;
+                               SequenceTrackListTableModel seqModel = trackModel.sequenceTrackListTableModel;
+                               MidiEvent newMidiEvent = new MidiEvent(form.getMessage(seqModel.charset), tick);
+                               if( midiEventsToBeOverwritten != null ) {
+                                       // 上書き消去するための選択済イベントがあった場合
+                                       trackModel.removeMidiEvents(midiEventsToBeOverwritten);
+                               }
+                               if( ! trackModel.addMidiEvent(newMidiEvent) ) {
+                                       System.out.println("addMidiEvent failure");
+                                       return false;
+                               }
+                               if(pairNoteOnOffModel.isSelected() && form.isNote()) {
+                                       ShortMessage sm = form.createPartnerMessage();
+                                       if(sm == null)
+                                               scrollToEventAt( tick );
+                                       else {
+                                               int duration = form.durationForm.getDuration();
+                                               if( form.isNote(false) ) {
+                                                       duration = -duration;
+                                               }
+                                               long partnerTick = tick + (long)duration;
+                                               if( partnerTick < 0L ) partnerTick = 0L;
+                                               MidiEvent partner = new MidiEvent((MidiMessage)sm, partnerTick);
+                                               if( ! trackModel.addMidiEvent(partner) ) {
+                                                       System.out.println("addMidiEvent failure (note on/off partner message)");
+                                               }
+                                               scrollToEventAt(partnerTick > tick ? partnerTick : tick);
+                                       }
+                               }
+                               seqModel.sequenceListTableModel.fireSequenceModified(seqModel);
+                               eventDialog.setVisible(false);
+                               return true;
+                       }
+               }
+               private EventEditContext editContext = new EventEditContext();
+               /**
+                * 指定のTick位置へジャンプするアクション
+                */
+               Action queryJumpEventAction = new AbstractAction() {
+                       {
+                               putValue(NAME,"Jump to ...");
+                               setEnabled(false);
+                       }
+                       public void actionPerformed(ActionEvent e) {
+                               editContext.setSelectedEvent(getModel());
+                               eventDialog.openTickForm("Jump selection to", editContext.jumpEventAction);
+                       }
+               };
+               /**
+                * 新しいイベントの追加を行うアクション
+                */
+               Action queryAddEventAction = new AbstractAction() {
+                       {
+                               putValue(NAME,"New");
+                               setEnabled(false);
+                       }
+                       public void actionPerformed(ActionEvent e) {
+                               TrackEventListTableModel model = getModel();
+                               editContext.setSelectedEvent(model);
+                               editContext.midiEventsToBeOverwritten = null;
+                               eventDialog.openEventForm(
+                                       "New MIDI event",
+                                       eventCellEditor.applyEventAction,
+                                       model.getChannel()
+                               );
+                       }
+               };
+               /**
+                * MIDIイベントのコピー&ペーストを行うためのクリップボード
+                */
+               private class LocalClipBoard {
+                       private MidiEvent copiedEventsToPaste[];
+                       private int copiedEventsPPQ = 0;
+                       public void copy(TrackEventListTableModel model, boolean withRemove) {
+                               copiedEventsToPaste = model.getSelectedMidiEvents();
+                               copiedEventsPPQ = model.sequenceTrackListTableModel.getSequence().getResolution();
+                               if( withRemove ) model.removeMidiEvents(copiedEventsToPaste);
+                               boolean en = (copiedEventsToPaste != null && copiedEventsToPaste.length > 0);
+                               queryPasteEventAction.setEnabled(en);
+                       }
+                       public void cut(TrackEventListTableModel model) {copy(model,true);}
+                       public void copy(TrackEventListTableModel model){copy(model,false);}
+                       public void paste(TrackEventListTableModel model, long tick) {
+                               model.addMidiEvents(copiedEventsToPaste, tick, copiedEventsPPQ);
+                       }
+               }
+               private LocalClipBoard clipBoard = new LocalClipBoard();
+               /**
+                * 指定のTick位置へ貼り付けるアクション
+                */
+               Action queryPasteEventAction = new AbstractAction() {
+                       {
+                               putValue(NAME,"Paste to ...");
+                               setEnabled(false);
+                       }
+                       public void actionPerformed(ActionEvent e) {
+                               editContext.setSelectedEvent(getModel());
+                               eventDialog.openTickForm("Paste to", editContext.pasteEventAction);
+                       }
+               };
+               /**
+                * イベントカットアクション
+                */
+               public Action cutEventAction = new AbstractAction("Cut") {
+                       {
+                               setEnabled(false);
+                       }
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               TrackEventListTableModel model = getModel();
+                               if( ! confirm("Do you want to cut selected event ?\n選択したMIDIイベントを切り取りますか?"))
+                                       return;
+                               clipBoard.cut(model);
+                       }
+               };
+               /**
+                * イベントコピーアクション
+                */
+               public Action copyEventAction = new AbstractAction("Copy") {
+                       {
+                               setEnabled(false);
+                       }
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               clipBoard.copy(getModel());
+                       }
+               };
+               /**
+                * イベント削除アクション
+                */
+               public Action deleteEventAction = new AbstractAction("Delete", deleteIcon) {
+                       {
+                               setEnabled(false);
+                       }
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               TrackEventListTableModel model = getModel();
+                               if( ! confirm("Do you want to delete selected event ?\n選択したMIDIイベントを削除しますか?"))
+                                       return;
+                               model.removeSelectedMidiEvents();
+                       }
+               };
+               /**
+                * MIDIイベント表のセルエディタ
+                */
+               private MidiEventCellEditor eventCellEditor;
+               /**
+                * MIDIイベント表のセルエディタ
+                */
+               class MidiEventCellEditor extends AbstractCellEditor implements TableCellEditor {
+                       /**
+                        * MIDIイベントセルエディタを構築します。
+                        */
+                       public MidiEventCellEditor() {
+                               eventDialog.midiMessageForm.setOutputMidiChannels(virtualMidiDevice.getChannels());
+                               eventDialog.tickPositionInputForm.setModel(editContext.tickPositionModel);
+                               int index = TrackEventListTableModel.Column.MESSAGE.ordinal();
+                               getColumnModel().getColumn(index).setCellEditor(this);
+                       }
+                       /**
+                        * セルをダブルクリックしないと編集できないようにします。
+                        * @param e イベント(マウスイベント)
+                        * @return 編集可能になったらtrue
+                        */
+                       @Override
+                       public boolean isCellEditable(EventObject e) {
+                               if( ! (e instanceof MouseEvent) )
+                                       return super.isCellEditable(e);
+                               return ((MouseEvent)e).getClickCount() == 2;
+                       }
+                       @Override
+                       public Object getCellEditorValue() { return null; }
+                       /**
+                        * MIDIメッセージダイアログが閉じたときにセル編集を中止するリスナー
+                        */
+                       private ComponentListener dialogComponentListener = new ComponentAdapter() {
+                               @Override
+                               public void componentHidden(ComponentEvent e) {
+                                       fireEditingCanceled();
+                                       // 用が済んだら当リスナーを除去
+                                       eventDialog.removeComponentListener(this);
+                               }
+                       };
+                       /**
+                        * 既存イベントを編集するアクション
+                        */
+                       private Action editEventAction = new AbstractAction() {
+                               public void actionPerformed(ActionEvent e) {
+                                       TrackEventListTableModel model = getModel();
+                                       editContext.setSelectedEvent(model);
+                                       if( editContext.selectedMidiEvent == null )
+                                               return;
+                                       editContext.setupForEdit(model);
+                                       eventDialog.addComponentListener(dialogComponentListener);
+                                       eventDialog.openEventForm("Change MIDI event", applyEventAction);
+                               }
+                       };
+                       /**
+                        * イベント編集ボタン
+                        */
+                       private JButton editEventButton = new JButton(editEventAction){{
+                               setHorizontalAlignment(JButton.LEFT);
+                       }};
+                       @Override
+                       public Component getTableCellEditorComponent(
+                               JTable table, Object value, boolean isSelected, int row, int column
+                       ) {
+                               editEventButton.setText(value.toString());
+                               return editEventButton;
+                       }
+                       /**
+                        * 入力したイベントを反映するアクション
+                        */
+                       private Action applyEventAction = new AbstractAction() {
+                               {
+                                       putValue(NAME,"OK");
+                               }
+                               public void actionPerformed(ActionEvent e) {
+                                       if( editContext.applyEvent() ) fireEditingStopped();
+                               }
+                       };
+               }
+               /**
+                * スクロール可能なMIDIイベントテーブルビュー
+                */
+               private JScrollPane scrollPane = new JScrollPane(this);
+               /**
+                * 指定の MIDI tick のイベントへスクロールします。
+                * @param tick MIDI tick
+                */
+               public void scrollToEventAt(long tick) {
+                       int index = getModel().tickToIndex(tick);
+                       scrollPane.getVerticalScrollBar().setValue(index * getRowHeight());
+                       getSelectionModel().setSelectionInterval(index, index);
+               }
+       }
+
+       /**
+        * 新しい {@link MidiSequenceEditor} を構築します。
+        * @param deviceModelList MIDIデバイスモデルリスト
+        */
+       public MidiSequenceEditor(MidiSequencerModel sequencerModel) {
+               // テーブルモデルとテーブルビューの生成
+               sequenceListTable = new SequenceListTable(
+                       new PlaylistTableModel(sequencerModel)
+               );
+               trackListTable = new TrackListTable(
+                       new SequenceTrackListTableModel(sequenceListTable.getModel(), null, null)
+               );
+               eventListTable = new EventListTable(
+                       new TrackEventListTableModel(trackListTable.getModel(), null)
+               );
+               // レイアウト
+               setTitle("MIDI Editor/Playlist - MIDI Chord Helper");
+               setBounds( 150, 200, 900, 500 );
+               setLayout(new FlowLayout());
+               new DropTarget(this, DnDConstants.ACTION_COPY_OR_MOVE, this, true);
+               JPanel playlistPanel = new JPanel() {{
+                       JPanel playlistOperationPanel = new JPanel() {{
+                               setLayout(new BoxLayout(this, BoxLayout.LINE_AXIS));
+                               add(Box.createRigidArea(new Dimension(10, 0)));
+                               add(new JButton(newSequenceDialog.openAction) {{
+                                       setMargin(ZERO_INSETS);
+                               }});
+                               if( sequenceListTable.midiFileChooser != null ) {
+                                       add( Box.createRigidArea(new Dimension(5, 0)) );
+                                       add(new JButton(
+                                               sequenceListTable.midiFileChooser.openMidiFileAction
+                                       ) {{
+                                               setMargin(ZERO_INSETS);
+                                       }});
+                               }
+                               if(sequenceListTable.base64EncodeAction != null) {
+                                       add(Box.createRigidArea(new Dimension(5, 0)));
+                                       add(new JButton(sequenceListTable.base64EncodeAction) {{
+                                               setMargin(ZERO_INSETS);
+                                       }});
+                               }
+                               add(Box.createRigidArea(new Dimension(5, 0)));
+                               PlaylistTableModel playlistTableModel = sequenceListTable.getModel();
+                               add(new JButton(playlistTableModel.moveToTopAction) {{
+                                       setMargin(ZERO_INSETS);
+                               }});
+                               add(Box.createRigidArea(new Dimension(5, 0)));
+                               add(new JButton(playlistTableModel.moveToBottomAction) {{
+                                       setMargin(ZERO_INSETS);
+                               }});
+                               if( sequenceListTable.midiFileChooser != null ) {
+                                       add(Box.createRigidArea(new Dimension(5, 0)));
+                                       add(new JButton(
+                                               sequenceListTable.midiFileChooser.saveMidiFileAction
+                                       ) {{
+                                               setMargin(ZERO_INSETS);
+                                       }});
+                               }
+                               add( Box.createRigidArea(new Dimension(5, 0)) );
+                               add(new JButton(sequenceListTable.deleteSequenceAction) {{
+                                       setMargin(ZERO_INSETS);
+                               }});
+                               add( Box.createRigidArea(new Dimension(5, 0)) );
+                               add(new SequencerSpeedSlider(
+                                       playlistTableModel.sequencerModel.speedSliderModel
+                               ));
+                               add( Box.createRigidArea(new Dimension(5, 0)) );
+                               add(new JPanel() {{
+                                       add(new JLabel("SyncMode:"));
+                                       add(new JLabel("Master"));
+                                       add(new JComboBox<Sequencer.SyncMode>(
+                                               sequenceListTable.getModel().sequencerModel.masterSyncModeModel
+                                       ));
+                                       add(new JLabel("Slave"));
+                                       add(new JComboBox<Sequencer.SyncMode>(
+                                               sequenceListTable.getModel().sequencerModel.slaveSyncModeModel
+                                       ));
+                               }});
+                       }};
+                       setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
+                       add(new JScrollPane(sequenceListTable));
+                       add(Box.createRigidArea(new Dimension(0, 10)));
+                       add(playlistOperationPanel);
+                       add(Box.createRigidArea(new Dimension(0, 10)));
+               }};
+               JPanel trackListPanel = new JPanel() {{
+                       JPanel trackListOperationPanel = new JPanel() {{
+                               add(new JButton(trackListTable.addTrackAction) {{
+                                       setMargin(ZERO_INSETS);
+                               }});
+                               add(new JButton(trackListTable.deleteTrackAction) {{
+                                       setMargin(ZERO_INSETS);
+                               }});
+                       }};
+                       setLayout(new BoxLayout(this, BoxLayout.PAGE_AXIS));
+                       add(trackListTable.titleLabel);
+                       add(Box.createRigidArea(new Dimension(0, 5)));
+                       add(new JScrollPane(trackListTable));
+                       add(Box.createRigidArea(new Dimension(0, 5)));
+                       add(trackListOperationPanel);
+               }};
+               JPanel eventListPanel = new JPanel() {{
+                       JPanel eventListOperationPanel = new JPanel() {{
+                               add(new JCheckBox("Pair NoteON/OFF") {{
+                                       setModel(eventListTable.pairNoteOnOffModel);
+                                       setToolTipText("NoteON/OFFをペアで同時選択する");
+                               }});
+                               add(new JButton(eventListTable.queryJumpEventAction) {{
+                                       setMargin(ZERO_INSETS);
+                               }});
+                               add(new JButton(eventListTable.queryAddEventAction) {{
+                                       setMargin(ZERO_INSETS);
+                               }});
+                               add(new JButton(eventListTable.copyEventAction) {{
+                                       setMargin(ZERO_INSETS);
+                               }});
+                               add(new JButton(eventListTable.cutEventAction) {{
+                                       setMargin(ZERO_INSETS);
+                               }});
+                               add(new JButton(eventListTable.queryPasteEventAction) {{
+                                       setMargin(ZERO_INSETS);
+                               }});
+                               add(new JButton(eventListTable.deleteEventAction) {{
+                                       setMargin(ZERO_INSETS);
+                               }});
+                       }};
+                       setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
+                       add(eventListTable.titleLabel);
+                       add(eventListTable.scrollPane);
+                       add(eventListOperationPanel);
+               }};
+               Container cp = getContentPane();
+               cp.setLayout(new BoxLayout(cp, BoxLayout.Y_AXIS));
+               cp.add(Box.createVerticalStrut(2));
+               cp.add(
+                       new JSplitPane(JSplitPane.VERTICAL_SPLIT, playlistPanel,
+                               new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, trackListPanel, eventListPanel) {{
+                                       setDividerLocation(300);
+                               }}
+                       ) {{
+                               setDividerLocation(160);
+                       }}
+               );
+       }
+
+}
diff --git a/src/camidion/chordhelper/midieditor/NewSequenceDialog.java b/src/camidion/chordhelper/midieditor/NewSequenceDialog.java
new file mode 100644 (file)
index 0000000..6e0a187
--- /dev/null
@@ -0,0 +1,677 @@
+package camidion.chordhelper.midieditor;
+
+import java.awt.Dimension;
+import java.awt.Graphics;
+import java.awt.Graphics2D;
+import java.awt.GridLayout;
+import java.awt.Insets;
+import java.awt.Point;
+import java.awt.Rectangle;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.ComponentEvent;
+import java.awt.event.ComponentListener;
+import java.awt.event.InputEvent;
+import java.awt.event.MouseEvent;
+import java.awt.event.MouseListener;
+import java.util.ArrayList;
+import java.util.Vector;
+
+import javax.sound.midi.MidiChannel;
+import javax.sound.midi.Sequence;
+import javax.swing.AbstractAction;
+import javax.swing.Action;
+import javax.swing.BoxLayout;
+import javax.swing.JButton;
+import javax.swing.JCheckBox;
+import javax.swing.JComboBox;
+import javax.swing.JComponent;
+import javax.swing.JDialog;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.JSpinner;
+import javax.swing.JTabbedPane;
+import javax.swing.JTextArea;
+import javax.swing.JTextField;
+import javax.swing.SpinnerNumberModel;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+
+import camidion.chordhelper.ButtonIcon;
+import camidion.chordhelper.ChordHelperApplet;
+import camidion.chordhelper.music.AbstractNoteTrackSpec;
+import camidion.chordhelper.music.ChordProgression;
+import camidion.chordhelper.music.DrumTrackSpec;
+import camidion.chordhelper.music.FirstTrackSpec;
+import camidion.chordhelper.music.MelodyTrackSpec;
+import camidion.chordhelper.music.Range;
+import camidion.chordhelper.pianokeyboard.PianoKeyboardListener;
+import camidion.chordhelper.pianokeyboard.PianoKeyboardPanel;
+
+/**
+ * 新しいMIDIシーケンスを生成するダイアログ
+ */
+public class NewSequenceDialog extends JDialog {
+       private static final Insets ZERO_INSETS = new Insets(0,0,0,0);
+       private static final Integer[] PPQList = {
+               48,60,80,96,120,160,192,240,320,384,480,960
+       };
+       private static final String INITIAL_CHORD_STRING =
+               "Key: C\nC G/B | Am Em/G | F C/E | Dm7 G7 C % | F G7 | Csus4 C\n";
+       private JTextArea chordText = new JTextArea(INITIAL_CHORD_STRING, 18, 30);
+       private JTextField seqNameText = new JTextField();
+       private JComboBox<Integer> ppqComboBox = new JComboBox<Integer>(PPQList);
+       private TimeSignatureSelecter timesigSelecter = new TimeSignatureSelecter();
+       private TempoSelecter tempoSelecter = new TempoSelecter();
+       private MeasureSelecter measureSelecter = new MeasureSelecter();
+       private TrackSpecPanel trackSpecPanel = new TrackSpecPanel() {{
+               DrumTrackSpec dts = new DrumTrackSpec(9, "Percussion track");
+               dts.velocity = 127;
+               addTrackSpec(dts);
+               MelodyTrackSpec mts;
+               mts = new MelodyTrackSpec(2, "Bass track", new Range(36,48));
+               mts.isBass = true;
+               mts.velocity = 96;
+               addTrackSpec(mts);
+               mts =  new MelodyTrackSpec(1, "Chord track", new Range(60,72));
+               addTrackSpec(mts);
+               mts = new MelodyTrackSpec(0, "Melody track", new Range(60,84));
+               mts.randomMelody = true;
+               mts.beatPattern = 0xFFFF;
+               mts.continuousBeatPattern = 0x820A;
+               addTrackSpec(mts);
+       }};
+       /**
+        * ダイアログを開くアクション
+        */
+       public Action openAction = new AbstractAction("New") {
+               {
+                       String tooltip = "Generate new song - 新しい曲を生成";
+                       putValue(Action.SHORT_DESCRIPTION, tooltip);
+               }
+               @Override
+               public void actionPerformed(ActionEvent e) { setVisible(true); }
+       };
+       private MidiSequenceEditor midiEditor;
+       /**
+        * MIDIシーケンス生成アクション
+        */
+       public Action generateAction = new AbstractAction(
+               "Generate & Add to PlayList",
+               new ButtonIcon(ButtonIcon.EJECT_ICON)
+       ) {
+               @Override
+               public void actionPerformed(ActionEvent e) {
+                       midiEditor.sequenceListTable.getModel().addSequenceAndPlay(getMidiSequence());
+                       NewSequenceDialog.this.setVisible(false);
+               }
+       };
+       /**
+        * 新しいMIDIシーケンスを生成するダイアログを構築します。
+        * @param midiEditor シーケンス追加先エディタ
+        */
+       public NewSequenceDialog(MidiSequenceEditor midiEditor) {
+               this.midiEditor = midiEditor;
+               trackSpecPanel.setChannels(midiEditor.getVirtualMidiDevice().getChannels());
+               setTitle("Generate new sequence - " + ChordHelperApplet.VersionInfo.NAME);
+               add(new JTabbedPane() {{
+                       add("Sequence", new JPanel() {{
+                               setLayout(new BoxLayout(this, BoxLayout.PAGE_AXIS));
+                               add(new JPanel() {{
+                                       setLayout(new BoxLayout(this, BoxLayout.LINE_AXIS));
+                                       add(new JLabel("Sequence name:"));
+                                       add(seqNameText);
+                               }});
+                               add(new JPanel() {{
+                                       setLayout(new BoxLayout(this, BoxLayout.LINE_AXIS));
+                                       add(new JLabel("Resolution in PPQ ="));
+                                       add(ppqComboBox);
+                                       add(measureSelecter);
+                               }});
+                               add(new JButton("Randomize (Tempo, Time signature, Chord progression)") {{
+                                       setMargin(ZERO_INSETS);
+                                       addActionListener(new ActionListener() {
+                                               @Override
+                                               public void actionPerformed(ActionEvent e) {
+                                                       setRandomChordProgression(
+                                                               measureSelecter.getMeasureDuration()
+                                                       );
+                                               }
+                                       });
+                               }});
+                               add(new JPanel() {{
+                                       setLayout(new BoxLayout(this, BoxLayout.LINE_AXIS));
+                                       add(tempoSelecter);
+                                       add(new JPanel() {{
+                                               add(new JLabel("Time signature ="));
+                                               add(timesigSelecter);
+                                       }});
+                               }});
+                               add(new JPanel() {{
+                                       setLayout(new BoxLayout(this, BoxLayout.LINE_AXIS));
+                                       add(new JLabel("Chord progression :"));
+                                       add(new JLabel("Transpose"));
+                                       add(new JButton(" + Up ") {{
+                                               setMargin(ZERO_INSETS);
+                                               addActionListener(new ActionListener() {
+                                                       @Override
+                                                       public void actionPerformed(ActionEvent e) {
+                                                               ChordProgression cp = createChordProgression();
+                                                               cp.transpose(1);
+                                                               setChordProgression(cp);
+                                                       }
+                                               });
+                                       }});
+                                       add(new JButton(" - Down ") {{
+                                               setMargin(ZERO_INSETS);
+                                               addActionListener(new ActionListener() {
+                                                       @Override
+                                                       public void actionPerformed(ActionEvent e) {
+                                                               ChordProgression cp = createChordProgression();
+                                                               cp.transpose(-1);
+                                                               setChordProgression(cp);
+                                                       }
+                                               });
+                                       }});
+                                       add(new JButton(" Enharmonic ") {{
+                                               setMargin(ZERO_INSETS);
+                                               addActionListener(new ActionListener() {
+                                                       @Override
+                                                       public void actionPerformed(ActionEvent e) {
+                                                               ChordProgression cp = createChordProgression();
+                                                               cp.toggleEnharmonically();
+                                                               setChordProgression(cp);
+                                                       }
+                                               });
+                                       }});
+                                       add(new JButton("Relative key") {{
+                                               setMargin(ZERO_INSETS);
+                                               addActionListener(new ActionListener() {
+                                                       @Override
+                                                       public void actionPerformed(ActionEvent e) {
+                                                               ChordProgression cp = createChordProgression();
+                                                               cp.toggleKeyMajorMinor();
+                                                               setChordProgression(cp);
+                                                       }
+                                               });
+                                       }});
+                               }});
+                               add(new JScrollPane(chordText));
+                               add(new JPanel() {{
+                                       setLayout(new BoxLayout(this, BoxLayout.LINE_AXIS));
+                                       add(new JButton(generateAction){{setMargin(ZERO_INSETS);}});
+                               }});
+                       }});
+                       add("Track", trackSpecPanel);
+               }});
+               setBounds( 250, 200, 600, 540 );
+       }
+       /**
+        * 新しいコード進行を生成して返します。
+        * @return 新しいコード進行
+        */
+       private ChordProgression createChordProgression() {
+               return new ChordProgression(chordText.getText());
+       }
+       /**
+        * MIDIシーケンスを生成して返します。
+        * @return MIDIシーケンス
+        */
+       public Sequence getMidiSequence() {
+               FirstTrackSpec firstTrackSpec = new FirstTrackSpec(
+                       seqNameText.getText(),
+                       tempoSelecter.getTempoByteArray(),
+                       timesigSelecter.getByteArray()
+               );
+               return createChordProgression().toMidiSequence(
+                       (int)ppqComboBox.getSelectedItem(),
+                       measureSelecter.getStartMeasurePosition(),
+                       measureSelecter.getEndMeasurePosition(),
+                       firstTrackSpec,
+                       trackSpecPanel.getTrackSpecs()
+               );
+       }
+       /**
+        * コード進行を設定します。テキスト欄に反映されます。
+        * @param cp コード進行
+        */
+       public void setChordProgression(ChordProgression cp) {
+               chordText.setText(cp.toString());
+       }
+       /**
+        * テンポ・拍子・コード進行をランダムに設定
+        * @param measureLength 小節数
+        */
+       public void setRandomChordProgression(int measureLength) {
+               tempoSelecter.setTempo( 80 + (int)(Math.random() * 100) );
+               int timesig_upper = 4;
+               int timesig_lower_index = 2;
+               switch( (int)(Math.random() * 10) ) {
+                       case 0: timesig_upper = 3; break; // 3/4
+               }
+               timesigSelecter.setValue((byte)timesig_upper, (byte)timesig_lower_index);
+               setChordProgression(new ChordProgression(measureLength, timesig_upper));
+       }
+       /**
+        * トラック設定画面
+        */
+       private static class TrackSpecPanel extends JPanel
+               implements PianoKeyboardListener, ActionListener, ChangeListener
+       {
+               JComboBox<AbstractNoteTrackSpec> trackSelecter = new JComboBox<>();
+               JLabel trackTypeLabel = new JLabel();
+               JTextField nameTextField = new JTextField(20);
+               MidiChannelComboSelecter chSelecter =
+                       new MidiChannelComboSelecter("MIDI Channel:");
+               MidiProgramSelecter pgSelecter = new MidiProgramSelecter();
+               MidiProgramFamilySelecter pgFamilySelecter =
+                       new MidiProgramFamilySelecter(pgSelecter) {{
+                               pgSelecter.setFamilySelecter(pgFamilySelecter);
+                       }};
+               PianoKeyboardPanel keyboardPanel = new PianoKeyboardPanel() {{
+                       keyboard.octaveSizeModel.setValue(6);
+                       keyboard.setPreferredSize(new Dimension(400,40));
+                       keyboard.setMaxSelectable(2);
+               }};
+               JPanel rangePanel = new JPanel() {{
+                       add( new JLabel("Range:") );
+                       add(keyboardPanel);
+               }};
+               JCheckBox randomMelodyCheckbox = new JCheckBox("Random melody");
+               JCheckBox bassCheckbox = new JCheckBox("Bass note");
+               JCheckBox randomLyricCheckbox = new JCheckBox("Random lyrics");
+               JCheckBox nsx39Checkbox = new JCheckBox("NSX-39");;
+               BeatPadPanel beatPadPanel = new BeatPadPanel(this);
+               private MidiChannel[] midiChannels;
+
+               public TrackSpecPanel() {
+                       nameTextField.addActionListener(this);
+                       keyboardPanel.keyboard.addPianoKeyboardListener(this);
+                       add(new JPanel() {{
+                               add(new JLabel("Track select:"));
+                               add(trackSelecter);
+                       }});
+                       add(trackTypeLabel);
+                       add(new JPanel() {{
+                               add(new JLabel("Track name (Press [Enter] key to change):"));
+                               add(nameTextField);
+                       }});
+                       add(chSelecter);
+                       add(new VelocitySelecter(keyboardPanel.keyboard.velocityModel));
+                       add(new JPanel() {{
+                               add(pgFamilySelecter);
+                               add(pgSelecter);
+                       }});
+                       add(rangePanel);
+                       bassCheckbox.addChangeListener(this);
+                       add(bassCheckbox);
+                       randomMelodyCheckbox.addChangeListener(this);
+                       add(randomMelodyCheckbox);
+                       randomLyricCheckbox.addChangeListener(this);
+                       add(randomLyricCheckbox);
+                       nsx39Checkbox.addChangeListener(this);
+                       add(nsx39Checkbox);
+                       add(beatPadPanel);
+                       trackSelecter.addActionListener(this);
+                       chSelecter.comboBox.addActionListener(this);
+                       keyboardPanel.keyboard.velocityModel.addChangeListener(
+                               new ChangeListener() {
+                                       public void stateChanged(ChangeEvent e) {
+                                               AbstractNoteTrackSpec ants = getTrackSpec();
+                                               ants.velocity = keyboardPanel.keyboard.velocityModel.getValue();
+                                       }
+                               }
+                       );
+                       pgSelecter.addActionListener(this);
+               }
+               @Override
+               public void stateChanged(ChangeEvent e) {
+                       Object src = e.getSource();
+                       if( src == bassCheckbox ) {
+                               AbstractNoteTrackSpec ants = getTrackSpec();
+                               if( ants instanceof MelodyTrackSpec ) {
+                                       MelodyTrackSpec mts = (MelodyTrackSpec)ants;
+                                       mts.isBass = bassCheckbox.isSelected();
+                               }
+                       }
+                       else if( src == randomMelodyCheckbox ) {
+                               AbstractNoteTrackSpec ants = getTrackSpec();
+                               if( ants instanceof MelodyTrackSpec ) {
+                                       MelodyTrackSpec mts = (MelodyTrackSpec)ants;
+                                       mts.randomMelody = randomMelodyCheckbox.isSelected();
+                               }
+                       }
+                       else if( src == randomLyricCheckbox ) {
+                               AbstractNoteTrackSpec ants = getTrackSpec();
+                               if( ants instanceof MelodyTrackSpec ) {
+                                       MelodyTrackSpec mts = (MelodyTrackSpec)ants;
+                                       mts.randomLyric = randomLyricCheckbox.isSelected();
+                               }
+                       }
+                       else if( src == nsx39Checkbox ) {
+                               AbstractNoteTrackSpec ants = getTrackSpec();
+                               if( ants instanceof MelodyTrackSpec ) {
+                                       MelodyTrackSpec mts = (MelodyTrackSpec)ants;
+                                       mts.nsx39 = nsx39Checkbox.isSelected();
+                               }
+                       }
+               }
+               @Override
+               public void actionPerformed(ActionEvent e) {
+                       Object src = e.getSource();
+                       AbstractNoteTrackSpec ants;
+                       if( src == nameTextField ) {
+                               getTrackSpec().name = nameTextField.getText();
+                       }
+                       else if( src == trackSelecter ) {
+                               ants = (AbstractNoteTrackSpec)(trackSelecter.getSelectedItem());
+                               String trackTypeString = "Track type: " + (
+                                       ants instanceof DrumTrackSpec ? "Percussion" :
+                                       ants instanceof MelodyTrackSpec ? "Melody" : "(Unknown)"
+                               );
+                               trackTypeLabel.setText(trackTypeString);
+                               nameTextField.setText(ants.name);
+                               chSelecter.setSelectedChannel(ants.midiChannel);
+                               keyboardPanel.keyboard.velocityModel.setValue(ants.velocity);
+                               pgSelecter.setProgram(ants.programNumber);
+                               keyboardPanel.keyboard.clear();
+                               if( ants instanceof DrumTrackSpec ) {
+                                       rangePanel.setVisible(false);
+                                       randomMelodyCheckbox.setVisible(false);
+                                       randomLyricCheckbox.setVisible(false);
+                                       nsx39Checkbox.setVisible(false);
+                                       bassCheckbox.setVisible(false);
+                               }
+                               else if( ants instanceof MelodyTrackSpec ) {
+                                       MelodyTrackSpec ts = (MelodyTrackSpec)ants;
+                                       rangePanel.setVisible(true);
+                                       keyboardPanel.keyboard.setSelectedNote(ts.range.min_note);
+                                       keyboardPanel.keyboard.setSelectedNote(ts.range.max_note);
+                                       keyboardPanel.keyboard.autoScroll(ts.range.min_note);
+                                       randomMelodyCheckbox.setSelected(ts.randomMelody);
+                                       randomLyricCheckbox.setSelected(ts.randomLyric);
+                                       bassCheckbox.setSelected(ts.isBass);
+                                       randomMelodyCheckbox.setVisible(true);
+                                       randomLyricCheckbox.setVisible(true);
+                                       nsx39Checkbox.setVisible(true);
+                                       bassCheckbox.setVisible(true);
+                               }
+                               beatPadPanel.setTrackSpec(ants);
+                       }
+                       else if( src == chSelecter.comboBox ) {
+                               getTrackSpec().midiChannel = chSelecter.getSelectedChannel();
+                       }
+                       else if( src == pgSelecter ) {
+                               getTrackSpec().programNumber = pgSelecter.getProgram();
+                       }
+               }
+               @Override
+               public void pianoKeyPressed(int n, InputEvent e) {
+                       noteOn(n);
+                       AbstractNoteTrackSpec ants = getTrackSpec();
+                       if( ants instanceof MelodyTrackSpec ) {
+                               MelodyTrackSpec ts = (MelodyTrackSpec)ants;
+                               ts.range = new Range(keyboardPanel.keyboard.getSelectedNotes());
+                       }
+               }
+               @Override
+               public void pianoKeyReleased(int n, InputEvent e) { noteOff(n); }
+               public void octaveMoved(ChangeEvent event) {}
+               public void octaveResized(ChangeEvent event) {}
+               public void noteOn(int n) {
+                       if( midiChannels == null ) return;
+                       MidiChannel mc = midiChannels[chSelecter.getSelectedChannel()];
+                       mc.noteOn( n, keyboardPanel.keyboard.velocityModel.getValue() );
+               }
+               public void noteOff(int n) {
+                       if( midiChannels == null ) return;
+                       MidiChannel mc = midiChannels[chSelecter.getSelectedChannel()];
+                       mc.noteOff( n, keyboardPanel.keyboard.velocityModel.getValue() );
+               }
+               public void setChannels( MidiChannel midiChannels[] ) {
+                       this.midiChannels = midiChannels;
+               }
+               public AbstractNoteTrackSpec getTrackSpec() {
+                       Object trackSpecObj = trackSelecter.getSelectedItem();
+                       AbstractNoteTrackSpec ants = (AbstractNoteTrackSpec)trackSpecObj;
+                       ants.name = nameTextField.getText();
+                       return ants;
+               }
+               public Vector<AbstractNoteTrackSpec> getTrackSpecs() {
+                       Vector<AbstractNoteTrackSpec> trackSpecs = new Vector<>();
+                       int i=0, n_items = trackSelecter.getItemCount();
+                       while( i < n_items ) {
+                               trackSpecs.add((AbstractNoteTrackSpec)trackSelecter.getItemAt(i++));
+                       }
+                       return trackSpecs;
+               }
+               public void addTrackSpec(AbstractNoteTrackSpec trackSpec) {
+                       trackSelecter.addItem(trackSpec);
+               }
+       }
+       private static class MeasureSelecter extends JPanel {
+               public MeasureSelecter() {
+                       setLayout(new GridLayout(2,3));
+                       add(new JLabel());
+                       add(new JLabel("Start",JLabel.CENTER));
+                       add(new JLabel("End",JLabel.CENTER));
+                       add(new JLabel("Measure",JLabel.RIGHT));
+                       add(new JSpinner(startModel));
+                       add(new JSpinner(endModel));
+               }
+               private SpinnerNumberModel startModel = new SpinnerNumberModel( 3, 1, 9999, 1 );
+               private SpinnerNumberModel endModel = new SpinnerNumberModel( 8, 1, 9999, 1 );
+               public int getStartMeasurePosition() {
+                       return startModel.getNumber().intValue();
+               }
+               public int getEndMeasurePosition() {
+                       return endModel.getNumber().intValue();
+               }
+               public int getMeasureDuration() {
+                       return getEndMeasurePosition() - getStartMeasurePosition() + 1;
+               }
+       }
+       //////////////////////////////////////////////////////////////////
+       //
+       // □=□=□=□=□=□=□=□=
+       // □=□=□=□=□=□=□=□=
+       //
+       private static class BeatPadPanel extends JPanel implements ActionListener {
+               PianoKeyboardListener piano_keyboard_listener;
+               JPanel percussion_selecters_panel;
+               java.util.List<JComboBox<String>> percussionSelecters =
+                       new ArrayList<JComboBox<String>>() {
+                               {
+                                       for( int i=0; i < DrumTrackSpec.defaultPercussions.length; i++  ) {
+                                               add(new JComboBox<String>());
+                                       }
+                               }
+                       };
+               BeatPad beat_pad;
+
+               public BeatPadPanel(PianoKeyboardListener pkl) {
+                       piano_keyboard_listener = pkl;
+                       percussion_selecters_panel = new JPanel();
+                       percussion_selecters_panel.setLayout(
+                               new BoxLayout( percussion_selecters_panel, BoxLayout.Y_AXIS )
+                       );
+                       for( JComboBox<String> cb : percussionSelecters ) {
+                               percussion_selecters_panel.add(cb);
+                               cb.addActionListener(this);
+                       }
+                       add( percussion_selecters_panel );
+                       add( beat_pad = new BeatPad(pkl) );
+                       beat_pad.setPreferredSize( new Dimension(400,200) );
+                       setLayout( new BoxLayout( this, BoxLayout.X_AXIS ) );
+               }
+               public void actionPerformed(ActionEvent e) {
+                       Object src = e.getSource();
+                       for( JComboBox<String> cb : percussionSelecters ) {
+                               if( src != cb ) continue;
+                               int note_no = (
+                                       (DrumTrackSpec.PercussionComboBoxModel)cb.getModel()
+                               ).getSelectedNoteNo();
+                               piano_keyboard_listener.pianoKeyPressed(note_no,(InputEvent)null);
+                       }
+               }
+               public void setTrackSpec( AbstractNoteTrackSpec ants ) {
+                       beat_pad.setTrackSpec(ants);
+                       if( ants instanceof DrumTrackSpec ) {
+                               DrumTrackSpec dts = (DrumTrackSpec)ants;
+                               int i=0;
+                               for( JComboBox<String> cb : percussionSelecters ) {
+                                       cb.setModel(dts.models[i++]);
+                               }
+                               percussion_selecters_panel.setVisible(true);
+                       }
+                       else if( ants instanceof MelodyTrackSpec ) {
+                               percussion_selecters_panel.setVisible(false);
+                       }
+               }
+       }
+       private static class BeatPad extends JComponent implements MouseListener, ComponentListener {
+               PianoKeyboardListener piano_keyboard_listener;
+               private int on_note_no = -1;
+               AbstractNoteTrackSpec track_spec;
+
+               public static final int MAX_BEATS = 16;
+               Rectangle beat_buttons[][];
+               Rectangle continuous_beat_buttons[][];
+
+               public BeatPad(PianoKeyboardListener pkl) {
+                       piano_keyboard_listener = pkl;
+                       addMouseListener(this);
+                       addComponentListener(this);
+                       // addMouseMotionListener(this);
+               }
+               public void paint(Graphics g) {
+                       super.paint(g);
+                       Graphics2D g2 = (Graphics2D) g;
+                       Rectangle r;
+                       int note, beat, mask;
+
+                       if( track_spec instanceof DrumTrackSpec ) {
+                               DrumTrackSpec dts = (DrumTrackSpec)track_spec;
+                               for( note=0; note<dts.beat_patterns.length; note++ ) {
+                                       for( beat=0, mask=0x8000; beat<MAX_BEATS; beat++, mask >>>= 1 ) {
+                                               r = beat_buttons[note][beat];
+                                               if( (dts.beat_patterns[note] & mask) != 0 )
+                                                       g2.fillRect( r.x, r.y, r.width, r.height );
+                                               else
+                                                       g2.drawRect( r.x, r.y, r.width, r.height );
+                                       }
+                               }
+                       }
+                       else if( track_spec instanceof MelodyTrackSpec ) {
+                               MelodyTrackSpec mts = (MelodyTrackSpec)track_spec;
+                               for( beat=0, mask=0x8000; beat<MAX_BEATS; beat++, mask >>>= 1 ) {
+                                       r = beat_buttons[0][beat];
+                                       if( (mts.beatPattern & mask) != 0 )
+                                               g2.fillRect( r.x, r.y, r.width, r.height );
+                                       else
+                                               g2.drawRect( r.x, r.y, r.width, r.height );
+                                       r = continuous_beat_buttons[0][beat];
+                                       if( (mts.continuousBeatPattern & mask) != 0 )
+                                               g2.fillRect( r.x, r.y, r.width, r.height );
+                                       else
+                                               g2.drawRect( r.x, r.y, r.width, r.height );
+                               }
+                       }
+
+               }
+               public void componentShown(ComponentEvent e) { }
+               public void componentHidden(ComponentEvent e) { }
+               public void componentMoved(ComponentEvent e) { }
+               public void componentResized(ComponentEvent e) {
+                       sizeChanged();
+               }
+               public void mousePressed(MouseEvent e) {
+                       catchEvent(e);
+                       if( on_note_no >= 0 ) {
+                               piano_keyboard_listener.pianoKeyPressed( on_note_no ,(InputEvent)e );
+                       }
+               }
+               public void mouseReleased(MouseEvent e) {
+                       if( on_note_no >= 0 ) {
+                               piano_keyboard_listener.pianoKeyReleased( on_note_no ,(InputEvent)e );
+                       }
+                       on_note_no = -1;
+               }
+               public void mouseEntered(MouseEvent e) {
+                       if((e.getModifiers() & InputEvent.BUTTON1_MASK) == InputEvent.BUTTON1_MASK) {
+                               catchEvent(e);
+                       }
+               }
+               public void mouseExited(MouseEvent e) { }
+               public void mouseClicked(MouseEvent e) { }
+               private void sizeChanged() {
+                       int beat, note, width, height;
+                       Dimension d = getSize();
+                       int num_notes = 1;
+                       if( track_spec instanceof DrumTrackSpec ) {
+                               DrumTrackSpec dts = (DrumTrackSpec)track_spec;
+                               num_notes = dts.models.length;
+                       }
+                       beat_buttons = new Rectangle[num_notes][];
+                       continuous_beat_buttons = new Rectangle[num_notes][];
+                       for( note=0; note<beat_buttons.length; note++ ) {
+                               beat_buttons[note] = new Rectangle[MAX_BEATS];
+                               continuous_beat_buttons[note] = new Rectangle[MAX_BEATS];
+                               for( beat=0; beat<MAX_BEATS; beat++ ) {
+                                       width = (d.width * 3) / (MAX_BEATS * 4);
+                                       height = d.height / num_notes - 1;
+                                       beat_buttons[note][beat] = new Rectangle(
+                                               beat * d.width / MAX_BEATS,
+                                               note * height,
+                                               width,
+                                               height
+                                       );
+                                       width = d.width / (MAX_BEATS * 3);
+                                       continuous_beat_buttons[note][beat] = new Rectangle(
+                                               (beat+1) * d.width / MAX_BEATS - width + 1,
+                                               note * height + height / 3,
+                                               width-1,
+                                               height / 3
+                                       );
+                               }
+                       }
+               }
+               private void catchEvent(MouseEvent e) {
+                       Point point = e.getPoint();
+                       int note, beat, mask;
+
+                       // ビートパターンのビットを反転
+                       if( track_spec instanceof DrumTrackSpec ) {
+                               DrumTrackSpec dts = (DrumTrackSpec)track_spec;
+                               for( note=0; note<dts.beat_patterns.length; note++ ) {
+                                       for( beat=0, mask=0x8000; beat<MAX_BEATS; beat++, mask >>>= 1 ) {
+                                               if( beat_buttons[note][beat].contains(point) ) {
+                                                       dts.beat_patterns[note] ^= mask;
+                                                       on_note_no = dts.models[note].getSelectedNoteNo();
+                                                       repaint(); return;
+                                               }
+                                       }
+                               }
+                       }
+                       else if( track_spec instanceof MelodyTrackSpec ) {
+                               MelodyTrackSpec mts = (MelodyTrackSpec)track_spec;
+                               for( beat=0, mask=0x8000; beat<MAX_BEATS; beat++, mask >>>= 1 ) {
+                                       if( beat_buttons[0][beat].contains(point) ) {
+                                               mts.beatPattern ^= mask;
+                                               repaint(); return;
+                                       }
+                                       if( continuous_beat_buttons[0][beat].contains(point) ) {
+                                               mts.continuousBeatPattern ^= mask;
+                                               repaint(); return;
+                                       }
+                               }
+                       }
+               }
+               public void setTrackSpec( AbstractNoteTrackSpec ants ) {
+                       track_spec = ants;
+                       sizeChanged();
+                       repaint();
+               }
+       }
+}
diff --git a/src/camidion/chordhelper/midieditor/PlaylistTableModel.java b/src/camidion/chordhelper/midieditor/PlaylistTableModel.java
new file mode 100644 (file)
index 0000000..db1e895
--- /dev/null
@@ -0,0 +1,645 @@
+package camidion.chordhelper.midieditor;
+
+import java.awt.event.ActionEvent;
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.nio.charset.Charset;
+import java.util.List;
+import java.util.Vector;
+
+import javax.sound.midi.InvalidMidiDataException;
+import javax.sound.midi.MetaEventListener;
+import javax.sound.midi.MetaMessage;
+import javax.sound.midi.MidiSystem;
+import javax.sound.midi.Sequence;
+import javax.swing.AbstractAction;
+import javax.swing.Action;
+import javax.swing.DefaultListSelectionModel;
+import javax.swing.Icon;
+import javax.swing.ListSelectionModel;
+import javax.swing.SwingUtilities;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+import javax.swing.event.ListSelectionEvent;
+import javax.swing.event.ListSelectionListener;
+import javax.swing.table.AbstractTableModel;
+
+import camidion.chordhelper.ButtonIcon;
+import camidion.chordhelper.mididevice.MidiSequencerModel;
+import camidion.chordhelper.music.ChordProgression;
+
+/**
+ * プレイリスト(MIDIシーケンスリスト)のテーブルデータモデル
+ */
+public class PlaylistTableModel extends AbstractTableModel {
+       /**
+        * MIDIシーケンサモデル
+        */
+       public MidiSequencerModel sequencerModel;
+       /**
+        * 空のトラックリストモデル
+        */
+       SequenceTrackListTableModel emptyTrackListTableModel;
+       /**
+        * 空のイベントリストモデル
+        */
+       TrackEventListTableModel emptyEventListTableModel;
+       /**
+        * 選択されているシーケンスのインデックス
+        */
+       public ListSelectionModel sequenceListSelectionModel = new DefaultListSelectionModel() {
+               {
+                       setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
+               }
+       };
+       /**
+        * 新しいプレイリストのテーブルモデルを構築します。
+        * @param sequencerModel MIDIシーケンサーモデル
+        */
+       public PlaylistTableModel(MidiSequencerModel sequencerModel) {
+               this.sequencerModel = sequencerModel;
+               //
+               // 秒位置を監視
+               sequencerModel.addChangeListener(secondPosition = new SecondPosition());
+               //
+               // メタイベントを監視
+               sequencerModel.getSequencer().addMetaEventListener(
+                       new MetaEventListener() {
+                               /**
+                                * {@inheritDoc}
+                                *
+                                * <p>EOT (End Of Track、type==0x2F) を受信したときの処理です。
+                                * </p>
+                                * <p>これは MetaEventListener のための実装なので、多くの場合
+                                * Swing EDT ではなく MIDI シーケンサの EDT から起動されます。
+                                * Swing EDT とは違うスレッドで動いていた場合は Swing EDT に振り直されます。
+                                * </p>
+                                */
+                               @Override
+                               public void meta(MetaMessage msg) {
+                                       if( msg.getType() == 0x2F ) {
+                                               if( ! SwingUtilities.isEventDispatchThread() ) {
+                                                       SwingUtilities.invokeLater(
+                                                               new Runnable() {
+                                                                       @Override
+                                                                       public void run() { goNext(); }
+                                                               }
+                                                       );
+                                                       return;
+                                               }
+                                               goNext();
+                                       }
+                               }
+                       }
+               );
+               emptyTrackListTableModel = new SequenceTrackListTableModel(this, null, null);
+               emptyEventListTableModel = new TrackEventListTableModel(emptyTrackListTableModel, null);
+       }
+       /**
+        * 次の曲へ進みます。
+        *
+        * <p>リピートモードの場合は同じ曲をもう一度再生、
+        * そうでない場合は次の曲へ進んで再生します。
+        * 次の曲がなければ、そこで停止します。
+        * いずれの場合も局の先頭へ戻ります。
+        * </p>
+        */
+       private void goNext() {
+               // とりあえず曲の先頭へ戻る
+               sequencerModel.getSequencer().setMicrosecondPosition(0);
+               if( (Boolean)toggleRepeatAction.getValue(Action.SELECTED_KEY) || loadNext(1)) {
+                       // リピートモードのときはもう一度同じ曲を、
+                       // そうでない場合は次の曲を再生開始
+                       sequencerModel.start();
+               }
+               else {
+                       // 最後の曲が終わったので、停止状態にする
+                       sequencerModel.stop();
+                       // ここでボタンが停止状態に変わったはずなので、
+                       // 通常であれば再生ボタンが自力で再描画するところだが、
+                       //
+                       // セルのレンダラーが描く再生ボタンには効かないようなので、
+                       // セルを突っついて再表示させる。
+                       int rowIndex = indexOfSequenceOnSequencer();
+                       int colIndex = Column.PLAY.ordinal();
+                       fireTableCellUpdated(rowIndex, colIndex);
+               }
+       }
+       /**
+        * シーケンスリスト
+        */
+       List<SequenceTrackListTableModel> sequenceList = new Vector<>();
+       /**
+        * 行が選択されているときだけイネーブルになるアクション
+        */
+       public abstract class SelectedSequenceAction extends AbstractAction
+               implements ListSelectionListener
+       {
+               public SelectedSequenceAction(String name, Icon icon, String tooltip) {
+                       super(name,icon); init(tooltip);
+               }
+               public SelectedSequenceAction(String name, String tooltip) {
+                       super(name); init(tooltip);
+               }
+               @Override
+               public void valueChanged(ListSelectionEvent e) {
+                       if( e.getValueIsAdjusting() ) return;
+                       setEnebledBySelection();
+               }
+               protected void setEnebledBySelection() {
+                       int index = sequenceListSelectionModel.getMinSelectionIndex();
+                       setEnabled(index >= 0);
+               }
+               private void init(String tooltip) {
+                       putValue(Action.SHORT_DESCRIPTION, tooltip);
+                       sequenceListSelectionModel.addListSelectionListener(this);
+                       setEnebledBySelection();
+               }
+       }
+       /**
+        * 繰り返し再生ON/OFF切り替えアクション
+        */
+       public Action toggleRepeatAction = new AbstractAction() {
+               {
+                       putValue(SHORT_DESCRIPTION, "Repeat - 繰り返し再生");
+                       putValue(LARGE_ICON_KEY, new ButtonIcon(ButtonIcon.REPEAT_ICON));
+                       putValue(SELECTED_KEY, false);
+               }
+               @Override
+               public void actionPerformed(ActionEvent event) { }
+       };
+       /**
+        * 再生中のシーケンサーの秒位置
+        */
+       private class SecondPosition implements ChangeListener {
+               private int value = 0;
+               /**
+                * 再生中のシーケンサーの秒位置が変わったときに表示を更新します。
+                * @param event 変更イベント
+                */
+               @Override
+               public void stateChanged(ChangeEvent event) {
+                       Object src = event.getSource();
+                       if( src instanceof MidiSequencerModel ) {
+                               int newValue = ((MidiSequencerModel)src).getValue() / 1000;
+                               if(value == newValue) return;
+                               value = newValue;
+                               int rowIndex = indexOfSequenceOnSequencer();
+                               fireTableCellUpdated(rowIndex, Column.POSITION.ordinal());
+                       }
+               }
+               @Override
+               public String toString() {
+                       return String.format("%02d:%02d", value/60, value%60);
+               }
+       }
+       /**
+        * 曲の先頭または前の曲へ戻るアクション
+        */
+       public Action moveToTopAction = new AbstractAction() {
+               {
+                       putValue(SHORT_DESCRIPTION,
+                               "Move to top or previous song - 曲の先頭または前の曲へ戻る"
+                       );
+                       putValue(LARGE_ICON_KEY, new ButtonIcon(ButtonIcon.TOP_ICON));
+               }
+               public void actionPerformed(ActionEvent event) {
+                       if( sequencerModel.getSequencer().getTickPosition() <= 40 )
+                               loadNext(-1);
+                       sequencerModel.setValue(0);
+               }
+       };
+       /**
+        * 次の曲へ進むアクション
+        */
+       public Action moveToBottomAction = new AbstractAction() {
+               {
+                       putValue(SHORT_DESCRIPTION, "Move to next song - 次の曲へ進む");
+                       putValue(LARGE_ICON_KEY, new ButtonIcon(ButtonIcon.BOTTOM_ICON));
+               }
+               public void actionPerformed(ActionEvent event) {
+                       if(loadNext(1)) sequencerModel.setValue(0);
+               }
+       };
+
+       /**
+        * 列の列挙型
+        */
+       public enum Column {
+               /** MIDIシーケンスの番号 */
+               NUMBER("No.", Integer.class, 20),
+               /** 再生ボタン */
+               PLAY("Play/Stop", String.class, 60) {
+                       @Override
+                       public boolean isCellEditable() { return true; }
+               },
+               /** 再生中の時間位置(分:秒) */
+               POSITION("Position", String.class, 60) {
+                       @Override
+                       public boolean isCellEditable() { return true; } // ダブルクリックだけ有効
+                       @Override
+                       public Object getValueOf(SequenceTrackListTableModel sequenceModel) {
+                               return sequenceModel.isOnSequencer()
+                                       ? sequenceModel.sequenceListTableModel.secondPosition : "";
+                       }
+               },
+               /** シーケンスの時間長(分:秒) */
+               LENGTH("Length", String.class, 80) {
+                       @Override
+                       public Object getValueOf(SequenceTrackListTableModel sequenceModel) {
+                               long usec = sequenceModel.getSequence().getMicrosecondLength();
+                               int sec = (int)( (usec < 0 ? usec += 0x100000000L : usec) / 1000L / 1000L );
+                               return String.format( "%02d:%02d", sec/60, sec%60 );
+                       }
+               },
+               /** ファイル名 */
+               FILENAME("Filename", String.class, 100) {
+                       @Override
+                       public boolean isCellEditable() { return true; }
+                       @Override
+                       public Object getValueOf(SequenceTrackListTableModel sequenceModel) {
+                               String filename = sequenceModel.getFilename();
+                               return filename == null ? "" : filename;
+                       }
+               },
+               /** 変更済みフラグ */
+               MODIFIED("Modified", Boolean.class, 50) {
+                       @Override
+                       public Object getValueOf(SequenceTrackListTableModel sequenceModel) {
+                               return sequenceModel.isModified();
+                       }
+               },
+               /** シーケンス名(最初のトラックの名前) */
+               NAME("Sequence name", String.class, 250) {
+                       @Override
+                       public boolean isCellEditable() { return true; }
+                       @Override
+                       public Object getValueOf(SequenceTrackListTableModel sequenceModel) {
+                               String name = sequenceModel.toString();
+                               return name == null ? "" : name;
+                       }
+               },
+               /** 文字コード */
+               CHARSET("CharSet", String.class, 80) {
+                       @Override
+                       public boolean isCellEditable() { return true; }
+                       @Override
+                       public Object getValueOf(SequenceTrackListTableModel sequenceModel) {
+                               return sequenceModel.charset;
+                       }
+               },
+               /** タイミング解像度 */
+               RESOLUTION("Resolution", Integer.class, 60) {
+                       @Override
+                       public Object getValueOf(SequenceTrackListTableModel sequenceModel) {
+                               return sequenceModel.getSequence().getResolution();
+                       }
+               },
+               /** トラック数 */
+               TRACKS("Tracks", Integer.class, 40) {
+                       @Override
+                       public Object getValueOf(SequenceTrackListTableModel sequenceModel) {
+                               return sequenceModel.getSequence().getTracks().length;
+                       }
+               },
+               /** タイミング分割形式 */
+               DIVISION_TYPE("DivType", String.class, 50) {
+                       @Override
+                       public Object getValueOf(SequenceTrackListTableModel sequenceModel) {
+                               float divType = sequenceModel.getSequence().getDivisionType();
+                               if( divType == Sequence.PPQ ) return "PPQ";
+                               else if( divType == Sequence.SMPTE_24 ) return "SMPTE_24";
+                               else if( divType == Sequence.SMPTE_25 ) return "SMPTE_25";
+                               else if( divType == Sequence.SMPTE_30 ) return "SMPTE_30";
+                               else if( divType == Sequence.SMPTE_30DROP ) return "SMPTE_30DROP";
+                               else return "[Unknown]";
+                       }
+               };
+               String title;
+               Class<?> columnClass;
+               int preferredWidth;
+               /**
+                * 列の識別子を構築します。
+                * @param title 列のタイトル
+                * @param columnClass 列のクラス
+                * @param perferredWidth 列の適切な幅
+                */
+               private Column(String title, Class<?> columnClass, int preferredWidth) {
+                       this.title = title;
+                       this.columnClass = columnClass;
+                       this.preferredWidth = preferredWidth;
+               }
+               public boolean isCellEditable() { return false; }
+               public Object getValueOf(SequenceTrackListTableModel sequenceModel) {
+                       return "";
+               }
+       }
+
+       @Override
+       public int getRowCount() { return sequenceList.size(); }
+       @Override
+       public int getColumnCount() { return Column.values().length; }
+       @Override
+       public String getColumnName(int column) {
+               return Column.values()[column].title;
+       }
+       @Override
+       public Class<?> getColumnClass(int column) {
+               return Column.values()[column].columnClass;
+       }
+       @Override
+       public boolean isCellEditable(int row, int column) {
+               return Column.values()[column].isCellEditable();
+       }
+       /** 再生中のシーケンサーの秒位置 */
+       private SecondPosition secondPosition;
+       @Override
+       public Object getValueAt(int row, int column) {
+               PlaylistTableModel.Column c = Column.values()[column];
+               return c == Column.NUMBER ? row : c.getValueOf(sequenceList.get(row));
+       }
+       @Override
+       public void setValueAt(Object val, int row, int column) {
+               PlaylistTableModel.Column c = Column.values()[column];
+               switch(c) {
+               case FILENAME:
+                       // ファイル名の変更
+                       sequenceList.get(row).setFilename((String)val);
+                       fireTableCellUpdated(row, column);
+                       break;
+               case NAME:
+                       // シーケンス名の設定または変更
+                       if( sequenceList.get(row).setName((String)val) )
+                               fireTableCellUpdated(row, Column.MODIFIED.ordinal());
+                       fireTableCellUpdated(row, column);
+                       break;
+               case CHARSET:
+                       // 文字コードの変更
+                       SequenceTrackListTableModel seq = sequenceList.get(row);
+                       seq.charset = Charset.forName(val.toString());
+                       fireTableCellUpdated(row, column);
+                       // シーケンス名の表示更新
+                       fireTableCellUpdated(row, Column.NAME.ordinal());
+                       // トラック名の表示更新
+                       seq.fireTableDataChanged();
+               default:
+                       break;
+               }
+       }
+       /**
+        * このプレイリストに読み込まれた全シーケンスの合計時間長を返します。
+        * @return 全シーケンスの合計時間長 [秒]
+        */
+       public int getTotalSeconds() {
+               int total = 0;
+               long usec;
+               for( SequenceTrackListTableModel m : sequenceList ) {
+                       usec = m.getSequence().getMicrosecondLength();
+                       total += (int)( (usec < 0 ? usec += 0x100000000L : usec)/1000L/1000L );
+               }
+               return total;
+       }
+       /**
+        * 未保存の修正内容を持つシーケンスがあるか調べます。
+        * @return 未保存の修正内容を持つシーケンスがあればtrue
+        */
+       public boolean isModified() {
+               for( SequenceTrackListTableModel m : sequenceList ) {
+                       if( m.isModified() ) return true;
+               }
+               return false;
+       }
+       /**
+        * 選択したシーケンスに未保存の修正内容があることを記録します。
+        * @param selModel 選択状態
+        * @param isModified 未保存の修正内容があるときtrue
+        */
+       public void setModified(boolean isModified) {
+               int minIndex = sequenceListSelectionModel.getMinSelectionIndex();
+               int maxIndex = sequenceListSelectionModel.getMaxSelectionIndex();
+               for( int i = minIndex; i <= maxIndex; i++ ) {
+                       if( sequenceListSelectionModel.isSelectedIndex(i) ) {
+                               sequenceList.get(i).setModified(isModified);
+                               fireTableCellUpdated(i, Column.MODIFIED.ordinal());
+                       }
+               }
+       }
+       /**
+        * 選択されたMIDIシーケンスのテーブルモデルを返します。
+        * @return 選択されたMIDIシーケンスのテーブルモデル(非選択時はnull)
+        */
+       public SequenceTrackListTableModel getSelectedSequenceModel() {
+               if( sequenceListSelectionModel.isSelectionEmpty() )
+                       return null;
+               int selectedIndex = sequenceListSelectionModel.getMinSelectionIndex();
+               if( selectedIndex >= sequenceList.size() )
+                       return null;
+               return sequenceList.get(selectedIndex);
+       }
+       /**
+        * 指定されたシーケンスが修正されたことを通知します。
+        * @param sequenceTableModel MIDIシーケンスモデル
+        */
+       public void fireSequenceModified(SequenceTrackListTableModel sequenceTableModel) {
+               int index = sequenceList.indexOf(sequenceTableModel);
+               if( index < 0 )
+                       return;
+               sequenceTableModel.setModified(true);
+               fireTableRowsUpdated(index, index);
+       }
+       /**
+        * 指定されている選択範囲のシーケンスが変更されたことを通知します。
+        * 更新済みフラグをセットし、選択されたシーケンスの全ての列を再表示します。
+        */
+       public void fireSelectedSequenceModified() {
+               if( sequenceListSelectionModel.isSelectionEmpty() )
+                       return;
+               int minIndex = sequenceListSelectionModel.getMinSelectionIndex();
+               int maxIndex = sequenceListSelectionModel.getMaxSelectionIndex();
+               for( int index = minIndex; index <= maxIndex; index++ ) {
+                       sequenceList.get(index).setModified(true);
+               }
+               fireTableRowsUpdated(minIndex, maxIndex);
+       }
+       /**
+        * バイト列とファイル名からMIDIシーケンスを追加します。
+        * バイト列が null の場合、空のMIDIシーケンスを追加します。
+        * @param data バイト列
+        * @param filename ファイル名
+        * @return 追加先インデックス(先頭が 0)
+        * @throws IOException ファイル読み込みに失敗した場合
+        * @throws InvalidMidiDataException MIDIデータが正しくない場合
+        */
+       public int addSequence(byte[] data, String filename)
+               throws IOException, InvalidMidiDataException
+       {
+               if( data == null ) return addDefaultSequence();
+               int lastIndex;
+               try (InputStream in = new ByteArrayInputStream(data)) {
+                       Sequence seq = MidiSystem.getSequence(in);
+                       lastIndex = addSequence(seq, filename);
+               } catch( IOException|InvalidMidiDataException e ) {
+                       throw e;
+               }
+               sequenceListSelectionModel.setSelectionInterval(lastIndex, lastIndex);
+               return lastIndex;
+       }
+       /**
+        * MIDIシーケンスを追加します。
+        * シーケンサーが停止中の場合、追加したシーケンスから再生を開始します。
+        * @param sequence MIDIシーケンス
+        * @return 追加先インデックス(先頭が 0)
+        */
+       public int addSequenceAndPlay(Sequence sequence) {
+               int lastIndex = addSequence(sequence,"");
+               if( ! sequencerModel.getSequencer().isRunning() ) {
+                       loadToSequencer(lastIndex);
+                       sequencerModel.start();
+               }
+               return lastIndex;
+       }
+       /**
+        * MIDIシーケンスを追加します。
+        * @param sequence MIDIシーケンス
+        * @param filename ファイル名
+        * @return 追加されたシーケンスのインデックス(先頭が 0)
+        */
+       public int addSequence(Sequence sequence, String filename) {
+               sequenceList.add(
+                       new SequenceTrackListTableModel(this, sequence, filename)
+               );
+               int lastIndex = sequenceList.size() - 1;
+               fireTableRowsInserted(lastIndex, lastIndex);
+               return lastIndex;
+       }
+       /**
+        * デフォルトの内容でMIDIシーケンスを作成して追加します。
+        * @return 追加されたMIDIシーケンスのインデックス(先頭が 0)
+        */
+       public int addDefaultSequence() {
+               Sequence seq = (new ChordProgression()).toMidiSequence();
+               return seq == null ? -1 : addSequence(seq,null);
+       }
+       /**
+        * MIDIファイルを追加します。
+        * ファイルが null の場合、空のMIDIシーケンスを追加します。
+        * @param midiFile MIDIファイル
+        * @return 追加先インデックス(先頭が 0)
+        * @throws InvalidMidiDataException ファイル内のMIDIデータが正しくない場合
+        * @throws IOException ファイル入出力に失敗した場合
+        */
+       public int addSequence(File midiFile) throws InvalidMidiDataException, IOException {
+               if( midiFile == null ) return addDefaultSequence();
+               int lastIndex;
+               try (FileInputStream in = new FileInputStream(midiFile)) {
+                       Sequence seq = MidiSystem.getSequence(in);
+                       String filename = midiFile.getName();
+                       lastIndex = addSequence(seq, filename);
+               } catch( InvalidMidiDataException|IOException e ) {
+                       throw e;
+               }
+               return lastIndex;
+       }
+       /**
+        * 複数のMIDIファイルを追加します。
+        * @param fileList 追加するMIDIファイルのリスト
+        * @return 追加先の最初のインデックス(先頭が 0、追加されなかった場合は -1)
+        * @throws InvalidMidiDataException ファイル内のMIDIデータが正しくない場合
+        * @throws IOException ファイル入出力に失敗した場合
+        */
+       public int addSequences(List<File> fileList)
+               throws InvalidMidiDataException, IOException
+       {
+               int firstIndex = -1;
+               for( File file : fileList ) {
+                       int lastIndex = addSequence(file);
+                       if( firstIndex == -1 )
+                               firstIndex = lastIndex;
+               }
+               return firstIndex;
+       }
+       /**
+        * URLから読み込んだMIDIシーケンスを追加します。
+        * @param midiFileUrl MIDIファイルのURL
+        * @return 追加先インデックス(先頭が 0、失敗した場合は -1)
+        * @throws URISyntaxException URLの形式に誤りがある場合
+        * @throws IOException 入出力に失敗した場合
+        * @throws InvalidMidiDataException MIDIデータが正しくない場合
+        */
+       public int addSequenceFromURL(String midiFileUrl)
+               throws URISyntaxException, IOException, InvalidMidiDataException
+       {
+               URI uri = new URI(midiFileUrl);
+               URL url = uri.toURL();
+               Sequence seq = MidiSystem.getSequence(url);
+               String filename = url.getFile().replaceFirst("^.*/","");
+               return addSequence(seq, filename);
+       }
+
+       /**
+        * 選択したシーケンスを除去します。
+        * @param listSelectionModel 選択状態
+        */
+       public void removeSelectedSequence() {
+               if( sequenceListSelectionModel.isSelectionEmpty() )
+                       return;
+               int selectedIndex = sequenceListSelectionModel.getMinSelectionIndex();
+               if( sequenceList.remove(selectedIndex).isOnSequencer() ) {
+                       // 削除したシーケンスが
+                       // シーケンサーにロード済みだった場合、アンロードする。
+                       sequencerModel.setSequenceTrackListTableModel(null);
+               }
+               fireTableRowsDeleted(selectedIndex, selectedIndex);
+       }
+       /**
+        * 指定したインデックス位置のシーケンスをシーケンサーにロードします。
+        * @param index シーケンスのインデックス位置(-1 を指定するとアンロードされます)
+        */
+       public void loadToSequencer(int index) {
+               SequenceTrackListTableModel oldSeq = sequencerModel.getSequenceTrackListTableModel();
+               SequenceTrackListTableModel newSeq = (index < 0 ? null : sequenceList.get(index));
+               if(oldSeq == newSeq)
+                       return;
+               sequencerModel.setSequenceTrackListTableModel(newSeq);
+               int columnIndices[] = {
+                       Column.PLAY.ordinal(),
+                       Column.POSITION.ordinal(),
+               };
+               if( oldSeq != null ) {
+                       int oldIndex = sequenceList.indexOf(oldSeq);
+                       for( int columnIndex : columnIndices )
+                               fireTableCellUpdated(oldIndex, columnIndex);
+               }
+               if( newSeq != null ) {
+                       for( int columnIndex : columnIndices )
+                               fireTableCellUpdated(index, columnIndex);
+               }
+       }
+       /**
+        * 現在シーケンサにロードされているシーケンスのインデックスを返します。
+        * ロードされていない場合は -1 を返します。
+        * @return 現在シーケンサにロードされているシーケンスのインデックス
+        */
+       public int indexOfSequenceOnSequencer() {
+               return sequenceList.indexOf(sequencerModel.getSequenceTrackListTableModel());
+       }
+       /**
+        * 引数で示された数だけ次へ進めたシーケンスをロードします。
+        * @param offset 進みたいシーケンス数
+        * @return 成功したらtrue
+        */
+       public boolean loadNext(int offset) {
+               int loadedIndex = indexOfSequenceOnSequencer();
+               int index = (loadedIndex < 0 ? 0 : loadedIndex + offset);
+               if( index < 0 || index >= sequenceList.size() )
+                       return false;
+               loadToSequencer(index);
+               return true;
+       }
+}
\ No newline at end of file
diff --git a/src/camidion/chordhelper/midieditor/SequenceTickIndex.java b/src/camidion/chordhelper/midieditor/SequenceTickIndex.java
new file mode 100644 (file)
index 0000000..4aa1139
--- /dev/null
@@ -0,0 +1,232 @@
+package camidion.chordhelper.midieditor;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.sound.midi.InvalidMidiDataException;
+import javax.sound.midi.MetaMessage;
+import javax.sound.midi.MidiEvent;
+import javax.sound.midi.MidiMessage;
+import javax.sound.midi.Sequence;
+import javax.sound.midi.Track;
+
+/**
+ *  MIDI シーケンスデータのtickインデックス
+ * <p>拍子、テンポ、調だけを抜き出したトラックを保持するためのインデックスです。
+ * 指定の MIDI tick の位置におけるテンポ、調、拍子を取得したり、
+ * 拍子情報から MIDI tick と小節位置との間の変換を行うために使います。
+ * </p>
+ */
+public class SequenceTickIndex {
+       /**
+        * メタメッセージタイプ
+        */
+       public static enum MetaMessageType {
+               /** テンポ */
+               TEMPO(0x51),
+               /** 拍子 */
+               TIME_SIGNATURE(0x58),
+               /** 調号 */
+               KEY_SIGNATURE(0x59);
+               private MetaMessageType(int typeNumber) {this.typeNumber = typeNumber;}
+               private int typeNumber;
+               /**
+                * 指定されたメタメッセージがどのタイプに該当するかを返します。
+                * @param metaMessage メタメッセージ
+                * @return 該当するタイプ(見つからなければnull)
+                */
+               public static MetaMessageType getByMessage(MetaMessage metaMessage) {
+                       int typeNumber = metaMessage.getType();
+                       for( MetaMessageType type : MetaMessageType.values() )
+                               if( type.typeNumber == typeNumber ) return type;
+                       return null;
+               }
+       }
+       /**
+        * 新しいMIDIシーケンスデータのインデックスを構築します。
+        * @param sourceSequence 元のMIDIシーケンス
+        */
+       public SequenceTickIndex(Sequence sourceSequence) {
+               try {
+                       int ppq = sourceSequence.getResolution();
+                       wholeNoteTickLength = ppq * 4;
+                       Sequence indexSeq = new Sequence(Sequence.PPQ, ppq, MetaMessageType.values().length);
+                       Track[] tracks = indexSeq.getTracks();
+                       for( MetaMessageType type : MetaMessageType.values() ) {
+                               trackMap.put(type, tracks[type.ordinal()]);
+                       }
+                       Track[] sourceTracks = sourceSequence.getTracks();
+                       for( Track tk : sourceTracks ) {
+                               for( int i_evt = 0 ; i_evt < tk.size(); i_evt++ ) {
+                                       MidiEvent evt = tk.get(i_evt);
+                                       MidiMessage msg = evt.getMessage();
+                                       if( ! (msg instanceof MetaMessage) ) continue;
+                                       MetaMessageType type = MetaMessageType.getByMessage((MetaMessage)msg);
+                                       if( type == null ) continue;
+                                       trackMap.get(type).add(evt);
+                               }
+                       }
+               }
+               catch ( InvalidMidiDataException e ) {
+                       e.printStackTrace();
+               }
+               this.sourceSequence = sourceSequence;
+       }
+       /**
+        * メタメッセージタイプからトラックへの変換マップ
+        */
+       private Map<MetaMessageType, Track> trackMap = new HashMap<>();
+       /**
+        * 元のMIDIシーケンス
+        */
+       private Sequence sourceSequence;
+       /**
+        * 元のMIDIシーケンスを返します。
+        * @return 元のMIDIシーケンス
+        */
+       public Sequence getSourceSequence() { return sourceSequence; }
+       /**
+        * 指定されたtick位置以前の最後のメタメッセージを返します。
+        * @param type メタメッセージの種類
+        * @param tickPosition tick位置
+        * @return 指定されたtick位置以前の最後のメタメッセージ(見つからなければnull)
+        */
+       public MetaMessage lastMetaMessageAt(MetaMessageType type, long tickPosition) {
+               Track track = trackMap.get(type);
+               for(int eventIndex = track.size()-1 ; eventIndex >= 0; eventIndex--) {
+                       MidiEvent event = track.get(eventIndex);
+                       if( event.getTick() > tickPosition )
+                               continue;
+                       MetaMessage metaMessage = (MetaMessage)(event.getMessage());
+                       if( metaMessage.getType() == 0x2F /* skip EOT (last event) */ )
+                               continue;
+                       return metaMessage;
+               }
+               return null;
+       }
+
+       private int wholeNoteTickLength;
+       public int lastBeat;
+       public int lastExtraTick;
+       public byte timesigUpper;
+       public byte timesigLowerIndex;
+       /**
+        * tick位置を小節位置に変換します。
+        * @param tickPosition tick位置
+        * @return 小節位置
+        */
+       public int tickToMeasure(long tickPosition) {
+               byte extraBeats = 0;
+               MidiEvent event = null;
+               MidiMessage message = null;
+               byte[] data = null;
+               long currentTick = 0L;
+               long nextTimesigTick = 0L;
+               long prevTick = 0L;
+               long duration = 0L;
+               int lastMeasure = 0;
+               int eventIndex = 0;
+               timesigUpper = 4;
+               timesigLowerIndex = 2; // =log2(4)
+               Track tst = trackMap.get(MetaMessageType.TIME_SIGNATURE);
+               if( tst != null ) {
+                       do {
+                               // Check current time-signature event
+                               if( eventIndex < tst.size() ) {
+                                       message = (event = tst.get(eventIndex)).getMessage();
+                                       currentTick = nextTimesigTick = event.getTick();
+                                       if(currentTick > tickPosition || (message.getStatus() == 0xFF && ((MetaMessage)message).getType() == 0x2F /* EOT */)) {
+                                               currentTick = tickPosition;
+                                       }
+                               }
+                               else { // No event
+                                       currentTick = nextTimesigTick = tickPosition;
+                               }
+                               // Add measure from last event
+                               //
+                               int beatTickLength = wholeNoteTickLength >> timesigLowerIndex;
+                               duration = currentTick - prevTick;
+                               int beats = (int)( duration / beatTickLength );
+                               lastExtraTick = (int)(duration % beatTickLength);
+                               int measures = beats / timesigUpper;
+                               extraBeats = (byte)(beats % timesigUpper);
+                               lastMeasure += measures;
+                               if( nextTimesigTick > tickPosition ) break;  // Not reached to next time signature
+                               //
+                               // Reached to the next time signature, so get it.
+                               if( ( data = ((MetaMessage)message).getData() ).length > 0 ) { // To skip EOT, check the data length.
+                                       timesigUpper = data[0];
+                                       timesigLowerIndex = data[1];
+                               }
+                               if( currentTick == tickPosition )  break;  // Calculation complete
+                               //
+                               // Calculation incomplete, so prepare for next
+                               //
+                               if( extraBeats > 0 ) {
+                                       //
+                                       // Extra beats are treated as 1 measure
+                                       lastMeasure++;
+                               }
+                               prevTick = currentTick;
+                               eventIndex++;
+                       } while( true );
+               }
+               lastBeat = extraBeats;
+               return lastMeasure;
+       }
+       /**
+        * 小節位置を MIDI tick に変換します。
+        * @param measure 小節位置
+        * @return MIDI tick
+        */
+       public long measureToTick(int measure) {
+               return measureToTick(measure, 0, 0);
+       }
+       /**
+        * 指定の小節位置、拍、拍内tickを、そのシーケンス全体の MIDI tick に変換します。
+        * @param measure 小節位置
+        * @param beat 拍
+        * @param extraTick 拍内tick
+        * @return そのシーケンス全体の MIDI tick
+        */
+       public long measureToTick(int measure, int beat, int extraTick) {
+               MidiEvent evt = null;
+               MidiMessage msg = null;
+               byte[] data = null;
+               long tick = 0L;
+               long prev_tick = 0L;
+               long duration = 0L;
+               long duration_sum = 0L;
+               long estimated_ticks;
+               int ticks_per_beat;
+               int i_evt = 0;
+               timesigUpper = 4;
+               timesigLowerIndex = 2; // =log2(4)
+               Track tst = trackMap.get(MetaMessageType.TIME_SIGNATURE);
+               do {
+                       ticks_per_beat = wholeNoteTickLength >> timesigLowerIndex;
+                       estimated_ticks = ((measure * timesigUpper) + beat) * ticks_per_beat + extraTick;
+                       if( tst == null || i_evt > tst.size() ) {
+                               return duration_sum + estimated_ticks;
+                       }
+                       msg = (evt = tst.get(i_evt)).getMessage();
+                       if( msg.getStatus() == 0xFF && ((MetaMessage)msg).getType() == 0x2F /* EOT */ ) {
+                               return duration_sum + estimated_ticks;
+                       }
+                       duration = (tick = evt.getTick()) - prev_tick;
+                       if( duration >= estimated_ticks ) {
+                               return duration_sum + estimated_ticks;
+                       }
+                       // Re-calculate measure (ignore extra beats/ticks)
+                       measure -= ( duration / (ticks_per_beat * timesigUpper) );
+                       duration_sum += duration;
+                       //
+                       // Get next time-signature
+                       data = ( (MetaMessage)msg ).getData();
+                       timesigUpper = data[0];
+                       timesigLowerIndex = data[1];
+                       prev_tick = tick;
+                       i_evt++;
+               } while( true );
+       }
+}
\ No newline at end of file
diff --git a/src/camidion/chordhelper/midieditor/SequenceTrackListTableModel.java b/src/camidion/chordhelper/midieditor/SequenceTrackListTableModel.java
new file mode 100644 (file)
index 0000000..bae324b
--- /dev/null
@@ -0,0 +1,428 @@
+package camidion.chordhelper.midieditor;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.sound.midi.MidiSystem;
+import javax.sound.midi.Sequence;
+import javax.sound.midi.Track;
+import javax.swing.DefaultListSelectionModel;
+import javax.swing.ListSelectionModel;
+import javax.swing.table.AbstractTableModel;
+
+import camidion.chordhelper.music.MIDISpec;
+
+/**
+ * MIDIシーケンス(トラックリスト)のテーブルデータモデル
+ */
+public class SequenceTrackListTableModel extends AbstractTableModel {
+       /**
+        * 列の列挙型
+        */
+       public enum Column {
+               /** トラック番号 */
+               TRACK_NUMBER("No.", Integer.class, 20),
+               /** イベント数 */
+               EVENTS("Events", Integer.class, 40),
+               /** Mute */
+               MUTE("Mute", Boolean.class, 30),
+               /** Solo */
+               SOLO("Solo", Boolean.class, 30),
+               /** 録音するMIDIチャンネル */
+               RECORD_CHANNEL("RecCh", String.class, 40),
+               /** MIDIチャンネル */
+               CHANNEL("Ch", String.class, 30),
+               /** トラック名 */
+               TRACK_NAME("Track name", String.class, 100);
+               String title;
+               Class<?> columnClass;
+               int preferredWidth;
+               /**
+                * 列の識別子を構築します。
+                * @param title 列のタイトル
+                * @param widthRatio 幅の割合
+                * @param columnClass 列のクラス
+                * @param perferredWidth 列の適切な幅
+                */
+               private Column(String title, Class<?> columnClass, int preferredWidth) {
+                       this.title = title;
+                       this.columnClass = columnClass;
+                       this.preferredWidth = preferredWidth;
+               }
+       }
+       /**
+        * 親のプレイリスト
+        */
+       PlaylistTableModel sequenceListTableModel;
+       /**
+        * ラップされたMIDIシーケンス
+        */
+       private Sequence sequence;
+       /**
+        * ラップされたMIDIシーケンスのtickインデックス
+        */
+       private SequenceTickIndex sequenceTickIndex;
+       /**
+        * MIDIファイル名
+        */
+       private String filename = "";
+       /**
+        * テキスト部分の文字コード(タイトル、歌詞など)
+        */
+       public Charset charset = Charset.defaultCharset();
+       /**
+        * トラックリスト
+        */
+       private List<TrackEventListTableModel> trackModelList = new ArrayList<>();
+       /**
+        * 選択されているトラックのインデックス
+        */
+       ListSelectionModel trackListSelectionModel = new DefaultListSelectionModel(){
+               {
+                       setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
+               }
+       };
+       /**
+        * MIDIシーケンスとファイル名から {@link SequenceTrackListTableModel} を構築します。
+        * @param sequenceListTableModel 親のプレイリスト
+        * @param sequence MIDIシーケンス
+        * @param filename ファイル名
+        */
+       public SequenceTrackListTableModel(
+               PlaylistTableModel sequenceListTableModel,
+               Sequence sequence,
+               String filename
+       ) {
+               this.sequenceListTableModel = sequenceListTableModel;
+               setSequence(sequence);
+               setFilename(filename);
+       }
+       @Override
+       public int getRowCount() {
+               return sequence == null ? 0 : sequence.getTracks().length;
+       }
+       @Override
+       public int getColumnCount() {
+               return Column.values().length;
+       }
+       /**
+        * 列名を返します。
+        * @return 列名
+        */
+       @Override
+       public String getColumnName(int column) {
+               return Column.values()[column].title;
+       }
+       /**
+        * 指定された列の型を返します。
+        * @return 指定された列の型
+        */
+       @Override
+       public Class<?> getColumnClass(int column) {
+               SequenceTrackListTableModel.Column c = Column.values()[column];
+               switch(c) {
+               case MUTE:
+               case SOLO: if( ! isOnSequencer() ) return String.class;
+                       // FALLTHROUGH
+               default: return c.columnClass;
+               }
+       }
+       @Override
+       public Object getValueAt(int row, int column) {
+               SequenceTrackListTableModel.Column c = Column.values()[column];
+               switch(c) {
+               case TRACK_NUMBER: return row;
+               case EVENTS: return sequence.getTracks()[row].size();
+               case MUTE:
+                       return isOnSequencer() ? sequenceListTableModel.sequencerModel.getSequencer().getTrackMute(row) : "";
+               case SOLO:
+                       return isOnSequencer() ? sequenceListTableModel.sequencerModel.getSequencer().getTrackSolo(row) : "";
+               case RECORD_CHANNEL:
+                       return isOnSequencer() ? trackModelList.get(row).getRecordingChannel() : "";
+               case CHANNEL: {
+                       int ch = trackModelList.get(row).getChannel();
+                       return ch < 0 ? "" : ch + 1 ;
+               }
+               case TRACK_NAME: return trackModelList.get(row).toString();
+               default: return "";
+               }
+       }
+       /**
+        * セルが編集可能かどうかを返します。
+        */
+       @Override
+       public boolean isCellEditable(int row, int column) {
+               SequenceTrackListTableModel.Column c = Column.values()[column];
+               switch(c) {
+               case MUTE:
+               case SOLO:
+               case RECORD_CHANNEL: return isOnSequencer();
+               case CHANNEL:
+               case TRACK_NAME: return true;
+               default: return false;
+               }
+       }
+       /**
+        * 列の値を設定します。
+        */
+       @Override
+       public void setValueAt(Object val, int row, int column) {
+               SequenceTrackListTableModel.Column c = Column.values()[column];
+               switch(c) {
+               case MUTE:
+                       sequenceListTableModel.sequencerModel.getSequencer().setTrackMute(row, ((Boolean)val).booleanValue());
+                       break;
+               case SOLO:
+                       sequenceListTableModel.sequencerModel.getSequencer().setTrackSolo(row, ((Boolean)val).booleanValue());
+                       break;
+               case RECORD_CHANNEL:
+                       trackModelList.get(row).setRecordingChannel((String)val);
+                       break;
+               case CHANNEL: {
+                       Integer ch;
+                       try {
+                               ch = new Integer((String)val);
+                       }
+                       catch( NumberFormatException e ) {
+                               ch = -1;
+                               break;
+                       }
+                       if( --ch <= 0 || ch > MIDISpec.MAX_CHANNELS )
+                               break;
+                       TrackEventListTableModel trackTableModel = trackModelList.get(row);
+                       if( ch == trackTableModel.getChannel() ) break;
+                       trackTableModel.setChannel(ch);
+                       setModified(true);
+                       fireTableCellUpdated(row, Column.EVENTS.ordinal());
+                       break;
+               }
+               case TRACK_NAME:
+                       trackModelList.get(row).setString((String)val);
+                       break;
+               default:
+                       break;
+               }
+               fireTableCellUpdated(row,column);
+       }
+       /**
+        * MIDIシーケンスを返します。
+        * @return MIDIシーケンス
+        */
+       public Sequence getSequence() { return sequence; }
+       /**
+        * シーケンスtickインデックスを返します。
+        * @return シーケンスtickインデックス
+        */
+       public SequenceTickIndex getSequenceTickIndex() {
+               return sequenceTickIndex;
+       }
+       /**
+        * MIDIシーケンスを設定します。
+        * @param sequence MIDIシーケンス(nullを指定するとトラックリストが空になる)
+        */
+       private void setSequence(Sequence sequence) {
+               //
+               // 旧シーケンスの録音モードを解除
+               sequenceListTableModel.sequencerModel.getSequencer().recordDisable(null); // The "null" means all tracks
+               //
+               // トラックリストをクリア
+               int oldSize = trackModelList.size();
+               if( oldSize > 0 ) {
+                       trackModelList.clear();
+                       fireTableRowsDeleted(0, oldSize-1);
+               }
+               // 新シーケンスに置き換える
+               if( (this.sequence = sequence) == null ) {
+                       // 新シーケンスがない場合
+                       sequenceTickIndex = null;
+                       return;
+               }
+               // tickインデックスを再構築
+               fireTimeSignatureChanged();
+               //
+               // トラックリストを再構築
+               Track tracks[] = sequence.getTracks();
+               for(Track track : tracks) {
+                       trackModelList.add(new TrackEventListTableModel(this, track));
+               }
+               // 文字コードの判定
+               byte b[] = MIDISpec.getNameBytesOf(sequence);
+               if( b != null && b.length > 0 ) {
+                       try {
+                               String autoDetectedName = new String(b, "JISAutoDetect");
+                               Set<Map.Entry<String,Charset>> entrySet;
+                               entrySet = Charset.availableCharsets().entrySet();
+                               for( Map.Entry<String,Charset> entry : entrySet ) {
+                                       Charset cs = entry.getValue();
+                                       if( ! autoDetectedName.equals(new String(b, cs)) )
+                                               continue;
+                                       charset = cs;
+                                       break;
+                               }
+                       } catch (UnsupportedEncodingException e) {
+                               e.printStackTrace();
+                       }
+               }
+               // トラックが挿入されたことを通知
+               fireTableRowsInserted(0, tracks.length-1);
+       }
+       /**
+        * 拍子が変更されたとき、シーケンスtickインデックスを再作成します。
+        */
+       public void fireTimeSignatureChanged() {
+               sequenceTickIndex = new SequenceTickIndex(sequence);
+       }
+       private boolean isModified = false;
+       /**
+        * 変更されたかどうかを返します。
+        * @return 変更済みのときtrue
+        */
+       public boolean isModified() { return isModified; }
+       /**
+        * 変更されたかどうかを設定します。
+        * @param isModified 変更されたときtrue
+        */
+       public void setModified(boolean isModified) { this.isModified = isModified; }
+       /**
+        * ファイル名を設定します。
+        * @param filename ファイル名
+        */
+       public void setFilename(String filename) { this.filename = filename; }
+       /**
+        * ファイル名を返します。
+        * @return ファイル名
+        */
+       public String getFilename() { return filename; }
+       @Override
+       public String toString() {
+               byte b[] = MIDISpec.getNameBytesOf(sequence);
+               return b == null ? "" : new String(b, charset);
+       }
+       /**
+        * シーケンス名を設定します。
+        * @param name シーケンス名
+        * @return 成功したらtrue
+        */
+       public boolean setName(String name) {
+               if( name.equals(toString()) )
+                       return false;
+               byte b[] = name.getBytes(charset);
+               if( ! MIDISpec.setNameBytesOf(sequence, b) )
+                       return false;
+               setModified(true);
+               fireTableDataChanged();
+               return true;
+       }
+       /**
+        * このシーケンスのMIDIデータのバイト列を返します。
+        * @return MIDIデータのバイト列(失敗した場合null)
+        */
+       public byte[] getMIDIdata() {
+               if( sequence == null || sequence.getTracks().length == 0 ) {
+                       return null;
+               }
+               try( ByteArrayOutputStream out = new ByteArrayOutputStream() ) {
+                       MidiSystem.write(sequence, 1, out);
+                       return out.toByteArray();
+               } catch ( IOException e ) {
+                       e.printStackTrace();
+                       return null;
+               }
+       }
+       /**
+        * 指定のトラックが変更されたことを通知します。
+        * @param track トラック
+        */
+       public void fireTrackChanged(Track track) {
+               int row = indexOf(track);
+               if( row < 0 ) return;
+               fireTableRowsUpdated(row, row);
+               sequenceListTableModel.fireSequenceModified(this);
+       }
+       /**
+        * 選択されているトラックモデルを返します。
+        * @param index トラックのインデックス
+        * @return トラックモデル(見つからない場合null)
+        */
+       public TrackEventListTableModel getSelectedTrackModel() {
+               if( trackListSelectionModel.isSelectionEmpty() )
+                       return null;
+               int index = trackListSelectionModel.getMinSelectionIndex();
+               Track tracks[] = sequence.getTracks();
+               if( tracks.length != 0 ) {
+                       Track track = tracks[index];
+                       for( TrackEventListTableModel model : trackModelList )
+                               if( model.getTrack() == track )
+                                       return model;
+               }
+               return null;
+       }
+       /**
+        * 指定のトラックがある位置のインデックスを返します。
+        * @param track トラック
+        * @return トラックのインデックス(先頭 0、トラックが見つからない場合 -1)
+        */
+       public int indexOf(Track track) {
+               Track tracks[] = sequence.getTracks();
+               for( int i=0; i<tracks.length; i++ )
+                       if( tracks[i] == track )
+                               return i;
+               return -1;
+       }
+       /**
+        * 新しいトラックを生成し、末尾に追加します。
+        * @return 追加したトラックのインデックス(先頭 0)
+        */
+       public int createTrack() {
+               Track newTrack = sequence.createTrack();
+               trackModelList.add(new TrackEventListTableModel(this, newTrack));
+               int lastRow = getRowCount() - 1;
+               fireTableRowsInserted(lastRow, lastRow);
+               sequenceListTableModel.fireSelectedSequenceModified();
+               trackListSelectionModel.setSelectionInterval(lastRow, lastRow);
+               return lastRow;
+       }
+       /**
+        * 選択されているトラックを削除します。
+        */
+       public void deleteSelectedTracks() {
+               if( trackListSelectionModel.isSelectionEmpty() )
+                       return;
+               int minIndex = trackListSelectionModel.getMinSelectionIndex();
+               int maxIndex = trackListSelectionModel.getMaxSelectionIndex();
+               Track tracks[] = sequence.getTracks();
+               for( int i = maxIndex; i >= minIndex; i-- ) {
+                       if( ! trackListSelectionModel.isSelectedIndex(i) )
+                               continue;
+                       sequence.deleteTrack(tracks[i]);
+                       trackModelList.remove(i);
+               }
+               fireTableRowsDeleted(minIndex, maxIndex);
+               sequenceListTableModel.fireSelectedSequenceModified();
+       }
+       /**
+        * このシーケンスモデルのシーケンスをシーケンサーが操作しているか調べます。
+        * @return シーケンサーが操作していたらtrue
+        */
+       public boolean isOnSequencer() {
+               return sequence == sequenceListTableModel.sequencerModel.getSequencer().getSequence();
+       }
+       /**
+        * 録音しようとしているチャンネルの設定されたトラックがあるか調べます。
+        * @return 該当トラックがあればtrue
+        */
+       public boolean hasRecordChannel() {
+               int rowCount = getRowCount();
+               for( int row=0; row < rowCount; row++ ) {
+                       Object value = getValueAt(row, Column.RECORD_CHANNEL.ordinal());
+                       if( ! "OFF".equals(value) ) return true;
+               }
+               return false;
+       }
+}
\ No newline at end of file
diff --git a/src/camidion/chordhelper/midieditor/SequencerSpeedSlider.java b/src/camidion/chordhelper/midieditor/SequencerSpeedSlider.java
new file mode 100644 (file)
index 0000000..73f23ff
--- /dev/null
@@ -0,0 +1,60 @@
+package camidion.chordhelper.midieditor;
+
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+
+import javax.swing.BoundedRangeModel;
+import javax.swing.BoxLayout;
+import javax.swing.JComboBox;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JSlider;
+
+/**
+ * シーケンサーの再生スピード調整スライダビュー
+ */
+public class SequencerSpeedSlider extends JPanel {
+       private static final String items[] = {
+               "x 1.0",
+               "x 1.5",
+               "x 2",
+               "x 4",
+               "x 8",
+               "x 16",
+       };
+       private JLabel titleLabel;
+       private JSlider slider;
+       public SequencerSpeedSlider(BoundedRangeModel model) {
+               setLayout(new BoxLayout(this, BoxLayout.X_AXIS));
+               add(titleLabel = new JLabel("Speed:"));
+               add(slider = new JSlider(model){{
+                       setPaintTicks(true);
+                       setMajorTickSpacing(12);
+                       setMinorTickSpacing(1);
+                       setVisible(false);
+               }});
+               add(new JComboBox<String>(items) {{
+                       addActionListener(new ActionListener() {
+                               @Override
+                               public void actionPerformed(ActionEvent e) {
+                                       int index = getSelectedIndex();
+                                       BoundedRangeModel model = slider.getModel();
+                                       if( index == 0 ) {
+                                               model.setValue(0);
+                                               slider.setVisible(false);
+                                               titleLabel.setVisible(true);
+                                       }
+                                       else {
+                                               int maxValue = ( index == 1 ? 7 : (index-1)*12 );
+                                               model.setMinimum(-maxValue);
+                                               model.setMaximum(maxValue);
+                                               slider.setMajorTickSpacing( index == 1 ? 7 : 12 );
+                                               slider.setMinorTickSpacing( index > 3 ? 12 : 1 );
+                                               slider.setVisible(true);
+                                               titleLabel.setVisible(false);
+                                       }
+                               }
+                       });
+               }});
+       }
+}
\ No newline at end of file
diff --git a/src/camidion/chordhelper/midieditor/TempoSelecter.java b/src/camidion/chordhelper/midieditor/TempoSelecter.java
new file mode 100644 (file)
index 0000000..2af643b
--- /dev/null
@@ -0,0 +1,142 @@
+package camidion.chordhelper.midieditor;
+
+import java.awt.Component;
+import java.awt.event.MouseEvent;
+import java.awt.event.MouseListener;
+
+import javax.sound.midi.MetaEventListener;
+import javax.sound.midi.MetaMessage;
+import javax.swing.Box;
+import javax.swing.BoxLayout;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JSpinner;
+import javax.swing.SpinnerNumberModel;
+import javax.swing.SwingUtilities;
+
+import camidion.chordhelper.ButtonIcon;
+import camidion.chordhelper.music.MIDISpec;
+
+/**
+ * テンポ選択(QPM: Quarter Per Minute)
+ */
+public class TempoSelecter extends JPanel implements MouseListener, MetaEventListener {
+       static final int DEFAULT_QPM = 120;
+       protected SpinnerNumberModel tempoSpinnerModel =
+               new SpinnerNumberModel(DEFAULT_QPM, 1, 999, 1);
+       private JLabel tempoLabel = new JLabel(
+               "=", new ButtonIcon(ButtonIcon.QUARTER_NOTE_ICON), JLabel.CENTER
+       ) {{
+               setVerticalAlignment(JLabel.CENTER);
+       }};
+       private JLabel tempoValueLabel = new JLabel(""+DEFAULT_QPM);
+       private JSpinner tempoSpinner = new JSpinner(tempoSpinnerModel);
+       public TempoSelecter() {
+               String tooltip = "Tempo in quatrers per minute - テンポ(1分あたりの四分音符の数)";
+               tempoSpinner.setToolTipText(tooltip);
+               tempoValueLabel.setToolTipText(tooltip);
+               setLayout(new BoxLayout(this,BoxLayout.X_AXIS));
+               add(tempoLabel);
+               add(Box.createHorizontalStrut(5));
+               add(tempoSpinner);
+               add(tempoValueLabel);
+               setEditable(true);
+               tempoLabel.addMouseListener(this);
+       }
+       private long prevBeatMicrosecondPosition = 0;
+       private class SetTempoRunnable implements Runnable {
+               byte[] qpm;
+               public SetTempoRunnable(byte[] qpm) { this.qpm = qpm; }
+               @Override
+               public void run() { setTempo(qpm);}
+       }
+       @Override
+       public void meta(MetaMessage msg) {
+               switch(msg.getType()) {
+               case 0x51: // Tempo (3 bytes) - テンポ
+                       if( ! SwingUtilities.isEventDispatchThread() ) {
+                               SwingUtilities.invokeLater(new SetTempoRunnable(msg.getData()));
+                               break;
+                       }
+                       setTempo(msg.getData());
+                       break;
+               }
+       }
+       @Override
+       public void mousePressed(MouseEvent e) {
+               Component obj = e.getComponent();
+               if(obj == tempoLabel && isEditable()) {
+                       //
+                       // Adjust tempo by interval time between two clicks
+                       //
+                       long currentMicrosecond = System.nanoTime()/1000;
+                       // midi_ch_selecter.noteOn( 9, 37, 100 );
+                       long interval_us = currentMicrosecond - prevBeatMicrosecondPosition;
+                       prevBeatMicrosecondPosition = currentMicrosecond;
+                       if( interval_us < 2000000L /* Shorter than 2 sec only */ ) {
+                               int tempo_in_bpm = (int)(240000000L / interval_us) >> 2; //  n/4拍子の場合のみを想定
+                       int old_tempo_in_bpm = getTempoInQpm();
+                       setTempo( ( tempo_in_bpm + old_tempo_in_bpm * 2 ) / 3 );
+                       }
+               }
+       }
+       public void mouseReleased(MouseEvent e) { }
+       public void mouseEntered(MouseEvent e) { }
+       public void mouseExited(MouseEvent e) { }
+       public void mouseClicked(MouseEvent e) { }
+       private boolean editable;
+       /**
+        * 編集可能かどうかを返します。
+        * @return 編集可能ならtrue
+        */
+       public boolean isEditable() { return editable; }
+       /**
+        * 編集可能かどうかを設定します。
+        * @param editable 編集可能ならtrue
+        */
+       public void setEditable( boolean editable ) {
+               this.editable = editable;
+               tempoSpinner.setVisible( editable );
+               tempoValueLabel.setVisible( !editable );
+               if( !editable ) {
+                       // Copy spinner's value to label
+                       tempoValueLabel.setText(
+                               ""+tempoSpinnerModel.getNumber().intValue()
+                       );
+               }
+               tempoLabel.setToolTipText(
+                       editable ?
+                       "Click rhythmically to adjust tempo - ここをクリックしてリズムをとるとテンポを合わせられます"
+                       : null
+               );
+       }
+       /**
+        * テンポを返します。
+        * @return テンポ [BPM](QPM)
+        */
+       public int getTempoInQpm() {
+               return tempoSpinnerModel.getNumber().intValue();
+       }
+       /**
+        * テンポをMIDIメタメッセージのバイト列として返します。
+        * @return MIDIメタメッセージのバイト列
+        */
+       public byte[] getTempoByteArray() {
+               return MIDISpec.qpmTempoToByteArray(getTempoInQpm());
+       }
+       /**
+        * テンポを設定します。
+        * @param qpm BPM(QPM)の値
+        */
+       public void setTempo(int qpm) {
+               tempoSpinnerModel.setValue(new Integer(qpm));
+               tempoValueLabel.setText(""+qpm);
+       }
+       /**
+        * MIDIメタメッセージのバイト列からテンポを設定します。
+        * @param msgdata MIDIメタメッセージのバイト列(null を指定した場合はデフォルトに戻る)
+        */
+       public void setTempo(byte msgdata[]) {
+               setTempo(msgdata==null ? DEFAULT_QPM: MIDISpec.byteArrayToQpmTempo(msgdata));
+       }
+}
\ No newline at end of file
diff --git a/src/camidion/chordhelper/midieditor/TickPositionModel.java b/src/camidion/chordhelper/midieditor/TickPositionModel.java
new file mode 100644 (file)
index 0000000..5da4aba
--- /dev/null
@@ -0,0 +1,60 @@
+package camidion.chordhelper.midieditor;
+
+import javax.swing.SpinnerNumberModel;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+
+/**
+ * tick位置入力モデル Mesausre:[xxxx] Beat:[xx] ExTick:[xxx]
+ */
+public class TickPositionModel implements ChangeListener {
+       public SpinnerNumberModel tickModel = new SpinnerNumberModel(0L, 0L, 999999L, 1L);
+       public SpinnerNumberModel measureModel = new SpinnerNumberModel(1, 1, 9999, 1);
+       public SpinnerNumberModel beatModel = new SpinnerNumberModel(1, 1, 32, 1);
+       public SpinnerNumberModel extraTickModel = new SpinnerNumberModel(0, 0, 4*960-1, 1);
+       /**
+        * 新しい {@link TickPositionModel} を構築します。
+        */
+       public TickPositionModel() {
+               tickModel.addChangeListener(this);
+               measureModel.addChangeListener(this);
+               beatModel.addChangeListener(this);
+               extraTickModel.addChangeListener(this);
+       }
+       private SequenceTickIndex sequenceTickIndex;
+       private boolean isChanging = false;
+       @Override
+       public void stateChanged(ChangeEvent e) {
+               if( sequenceTickIndex == null )
+                       return;
+               if( e.getSource() == tickModel ) {
+                       isChanging = true;
+                       long newTick = tickModel.getNumber().longValue();
+                       int newMeasure = 1 + sequenceTickIndex.tickToMeasure(newTick);
+                       measureModel.setValue(newMeasure);
+                       beatModel.setValue(sequenceTickIndex.lastBeat + 1);
+                       isChanging = false;
+                       extraTickModel.setValue(sequenceTickIndex.lastExtraTick);
+                       return;
+               }
+               if( isChanging )
+                       return;
+               long newTick = sequenceTickIndex.measureToTick(
+                       measureModel.getNumber().intValue() - 1,
+                       beatModel.getNumber().intValue() - 1,
+                       extraTickModel.getNumber().intValue()
+               );
+               tickModel.setValue(newTick);
+       }
+       public void setSequenceIndex(SequenceTickIndex sequenceTickIndex) {
+               this.sequenceTickIndex = sequenceTickIndex;
+               int resolution = sequenceTickIndex.getSourceSequence().getResolution();
+               extraTickModel.setMaximum( 4 * resolution - 1 );
+       }
+       public long getTickPosition() {
+               return tickModel.getNumber().longValue();
+       }
+       public void setTickPosition( long tick ) {
+               tickModel.setValue(tick);
+       }
+}
\ No newline at end of file
diff --git a/src/camidion/chordhelper/midieditor/TimeSignatureSelecter.java b/src/camidion/chordhelper/midieditor/TimeSignatureSelecter.java
new file mode 100644 (file)
index 0000000..c1d14ac
--- /dev/null
@@ -0,0 +1,115 @@
+package camidion.chordhelper.midieditor;
+
+import javax.sound.midi.MetaEventListener;
+import javax.sound.midi.MetaMessage;
+import javax.swing.JComboBox;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JSpinner;
+import javax.swing.SpinnerNumberModel;
+import javax.swing.SwingUtilities;
+
+/**
+ * 拍子選択ビュー
+ */
+public class TimeSignatureSelecter extends JPanel implements MetaEventListener {
+       SpinnerNumberModel upperTimesigSpinnerModel = new SpinnerNumberModel(4, 1, 32, 1);
+       private JSpinner upperTimesigSpinner = new JSpinner(
+               upperTimesigSpinnerModel
+       ) {
+               {
+                       setToolTipText("Time signature (upper digit) - 拍子の分子");
+               }
+       };
+       JComboBox<String> lowerTimesigCombobox = new JComboBox<String>() {
+               {
+                       setToolTipText("Time signature (lower digit) - 拍子の分母");
+                       for( int i=0; i<6; i++ ) addItem( "/" + (1<<i) );
+                       setSelectedIndex(2);
+               }
+       };
+       private class SetValueRunnable implements Runnable {
+               byte[] qpm;
+               public SetValueRunnable(byte[] qpm) { this.qpm = qpm; }
+               @Override
+               public void run() { setValue(qpm);}
+       }
+       @Override
+       public void meta(MetaMessage msg) {
+               switch(msg.getType()) {
+               case 0x58: // Time signature (4 bytes) - 拍子
+                       if( ! SwingUtilities.isEventDispatchThread() ) {
+                               SwingUtilities.invokeLater(new SetValueRunnable(msg.getData()));
+                               break;
+                       }
+                       setValue(msg.getData());
+                       break;
+               }
+       }
+       private class TimeSignatureLabel extends JLabel {
+               private byte upper = -1;
+               private byte lower_index = -1;
+               {
+                       setToolTipText("Time signature - 拍子");
+               }
+               public void setTimeSignature(byte upper, byte lower_index) {
+                       if( this.upper == upper && this.lower_index == lower_index ) {
+                               return;
+                       }
+                       setText("<html><font size=\"+1\">" + upper + "/" + (1 << lower_index) + "</font></html>");
+               }
+       }
+       private TimeSignatureLabel timesigValueLabel = new TimeSignatureLabel();
+       private boolean editable;
+       public TimeSignatureSelecter() {
+               add(upperTimesigSpinner);
+               add(lowerTimesigCombobox);
+               add(timesigValueLabel);
+               setEditable(true);
+       }
+       public void clear() {
+               upperTimesigSpinnerModel.setValue(4);
+               lowerTimesigCombobox.setSelectedIndex(2);
+       }
+       public int getUpperValue() {
+               return upperTimesigSpinnerModel.getNumber().intValue();
+       }
+       public byte getUpperByte() {
+               return upperTimesigSpinnerModel.getNumber().byteValue();
+       }
+       public int getLowerValueIndex() {
+               return lowerTimesigCombobox.getSelectedIndex();
+       }
+       public byte getLowerByte() {
+               return (byte)getLowerValueIndex();
+       }
+       public byte[] getByteArray() {
+               byte[] data = new byte[4];
+               data[0] = getUpperByte();
+               data[1] = getLowerByte();
+               data[2] = (byte)( 96 >> getLowerValueIndex() );
+               data[3] = 8;
+               return data;
+       }
+       public void setValue(byte upper, byte lowerIndex) {
+               upperTimesigSpinnerModel.setValue( upper );
+               lowerTimesigCombobox.setSelectedIndex( lowerIndex );
+               timesigValueLabel.setTimeSignature( upper, lowerIndex );
+       }
+       public void setValue(byte[] data) {
+               if(data == null)
+                       clear();
+               else
+                       setValue(data[0], data[1]);
+       }
+       public boolean isEditable() { return editable; }
+       public void setEditable( boolean editable ) {
+               this.editable = editable;
+               upperTimesigSpinner.setVisible(editable);
+               lowerTimesigCombobox.setVisible(editable);
+               timesigValueLabel.setVisible(!editable);
+               if( !editable ) {
+                       timesigValueLabel.setTimeSignature(getUpperByte(), getLowerByte());
+               }
+       }
+}
\ No newline at end of file
diff --git a/src/camidion/chordhelper/midieditor/TrackEventListTableModel.java b/src/camidion/chordhelper/midieditor/TrackEventListTableModel.java
new file mode 100644 (file)
index 0000000..e0870a7
--- /dev/null
@@ -0,0 +1,551 @@
+package camidion.chordhelper.midieditor;
+
+import java.nio.charset.Charset;
+import java.util.Vector;
+
+import javax.sound.midi.InvalidMidiDataException;
+import javax.sound.midi.MidiEvent;
+import javax.sound.midi.MidiMessage;
+import javax.sound.midi.Sequencer;
+import javax.sound.midi.ShortMessage;
+import javax.sound.midi.Track;
+import javax.swing.DefaultListSelectionModel;
+import javax.swing.ListSelectionModel;
+import javax.swing.table.AbstractTableModel;
+
+import camidion.chordhelper.music.MIDISpec;
+
+/**
+ * MIDIトラック(MIDIイベントリスト)テーブルモデル
+ */
+public class TrackEventListTableModel extends AbstractTableModel {
+       /**
+        * 列
+        */
+       public enum Column {
+               /** MIDIイベント番号 */
+               EVENT_NUMBER("No.", Integer.class, 15) {
+                       @Override
+                       public boolean isCellEditable() { return false; }
+               },
+               /** tick位置 */
+               TICK_POSITION("TickPos.", Long.class, 40) {
+                       @Override
+                       public Object getValue(MidiEvent event) {
+                               return event.getTick();
+                       }
+               },
+               /** tick位置に対応する小節 */
+               MEASURE_POSITION("Measure", Integer.class, 30) {
+                       public Object getValue(SequenceTrackListTableModel seq, MidiEvent event) {
+                               return seq.getSequenceTickIndex().tickToMeasure(event.getTick()) + 1;
+                       }
+               },
+               /** tick位置に対応する拍 */
+               BEAT_POSITION("Beat", Integer.class, 20) {
+                       @Override
+                       public Object getValue(SequenceTrackListTableModel seq, MidiEvent event) {
+                               SequenceTickIndex sti = seq.getSequenceTickIndex();
+                               sti.tickToMeasure(event.getTick());
+                               return sti.lastBeat + 1;
+                       }
+               },
+               /** tick位置に対応する余剰tick(拍に収まらずに余ったtick数) */
+               EXTRA_TICK_POSITION("ExTick", Integer.class, 20) {
+                       @Override
+                       public Object getValue(SequenceTrackListTableModel seq, MidiEvent event) {
+                               SequenceTickIndex sti = seq.getSequenceTickIndex();
+                               sti.tickToMeasure(event.getTick());
+                               return sti.lastExtraTick;
+                       }
+               },
+               /** MIDIメッセージ */
+               MESSAGE("MIDI Message", String.class, 300) {
+                       @Override
+                       public Object getValue(SequenceTrackListTableModel seq, MidiEvent event) {
+                               return MIDISpec.msgToString(event.getMessage(), seq.charset);
+                       }
+               };
+               private String title;
+               private Class<?> columnClass;
+               int preferredWidth;
+               /**
+                * 列の識別子を構築します。
+                * @param title 列のタイトル
+                * @param widthRatio 幅の割合
+                * @param columnClass 列のクラス
+                * @param perferredWidth 列の適切な幅
+                */
+               private Column(String title, Class<?> columnClass, int preferredWidth) {
+                       this.title = title;
+                       this.columnClass = columnClass;
+                       this.preferredWidth = preferredWidth;
+               }
+               /**
+                * セルを編集できるときtrue、編集できないときfalseを返します。
+                */
+               public boolean isCellEditable() { return true; }
+               /**
+                * 列の値を返します。
+                * @param event 対象イベント
+                * @return この列の対象イベントにおける値
+                */
+               public Object getValue(MidiEvent event) { return ""; }
+               /**
+                * 列の値を返します。
+                * @param sti 対象シーケンスモデル
+                * @param event 対象イベント
+                * @return この列の対象イベントにおける値
+                */
+               public Object getValue(SequenceTrackListTableModel seq, MidiEvent event) {
+                       return getValue(event);
+               }
+       }
+       /**
+        * ラップされているMIDIトラック
+        */
+       private Track track;
+       /**
+        * 親のシーケンスモデル
+        */
+       SequenceTrackListTableModel sequenceTrackListTableModel;
+       /**
+        * 選択されているイベントのインデックス
+        */
+       ListSelectionModel eventSelectionModel = new DefaultListSelectionModel() {
+               {
+                       setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
+               }
+       };
+       /**
+        * シーケンスを親にして、その特定のトラックに連動する
+        * MIDIトラックモデルを構築します。
+        *
+        * @param parent 親のシーケンスモデル
+        * @param track ラップするMIDIトラック(ない場合はnull)
+        */
+       public TrackEventListTableModel(
+               SequenceTrackListTableModel sequenceTrackListTableModel, Track track
+       ) {
+               this.track = track;
+               this.sequenceTrackListTableModel = sequenceTrackListTableModel;
+       }
+       @Override
+       public int getRowCount() {
+               return track == null ? 0 : track.size();
+       }
+       @Override
+       public int getColumnCount() {
+               return Column.values().length;
+       }
+       /**
+        * 列名を返します。
+        */
+       @Override
+       public String getColumnName(int column) {
+               return Column.values()[column].title;
+       }
+       /**
+        * 列のクラスを返します。
+        */
+       @Override
+       public Class<?> getColumnClass(int column) {
+               return Column.values()[column].columnClass;
+       }
+       @Override
+       public Object getValueAt(int row, int column) {
+               TrackEventListTableModel.Column c = Column.values()[column];
+               if( c == Column.EVENT_NUMBER ) return row;
+               MidiEvent event = track.get(row);
+               switch(c) {
+               case MEASURE_POSITION:
+               case BEAT_POSITION:
+               case EXTRA_TICK_POSITION:
+               case MESSAGE:
+                       return c.getValue(sequenceTrackListTableModel, event);
+               default:
+                       return c.getValue(event);
+               }
+       }
+       /**
+        * セルを編集できるときtrue、編集できないときfalseを返します。
+        */
+       @Override
+       public boolean isCellEditable(int row, int column) {
+               return Column.values()[column].isCellEditable();
+       }
+       /**
+        * セルの値を変更します。
+        */
+       @Override
+       public void setValueAt(Object value, int row, int column) {
+               long newTick;
+               switch(Column.values()[column]) {
+               case TICK_POSITION: newTick = (Long)value; break;
+               case MEASURE_POSITION:
+                       newTick = sequenceTrackListTableModel.getSequenceTickIndex().measureToTick(
+                               (Integer)value - 1,
+                               (Integer)getValueAt( row, Column.BEAT_POSITION.ordinal() ) - 1,
+                               (Integer)getValueAt( row, Column.EXTRA_TICK_POSITION.ordinal() )
+                       );
+                       break;
+               case BEAT_POSITION:
+                       newTick = sequenceTrackListTableModel.getSequenceTickIndex().measureToTick(
+                               (Integer)getValueAt( row, Column.MEASURE_POSITION.ordinal() ) - 1,
+                               (Integer)value - 1,
+                               (Integer)getValueAt( row, Column.EXTRA_TICK_POSITION.ordinal() )
+                       );
+                       break;
+               case EXTRA_TICK_POSITION:
+                       newTick = sequenceTrackListTableModel.getSequenceTickIndex().measureToTick(
+                               (Integer)getValueAt( row, Column.MEASURE_POSITION.ordinal() ) - 1,
+                               (Integer)getValueAt( row, Column.BEAT_POSITION.ordinal() ) - 1,
+                               (Integer)value
+                       );
+                       break;
+               default: return;
+               }
+               MidiEvent oldMidiEvent = track.get(row);
+               if( oldMidiEvent.getTick() == newTick ) {
+                       return;
+               }
+               MidiMessage msg = oldMidiEvent.getMessage();
+               MidiEvent newMidiEvent = new MidiEvent(msg,newTick);
+               track.remove(oldMidiEvent);
+               track.add(newMidiEvent);
+               fireTableDataChanged();
+               if( MIDISpec.isEOT(msg) ) {
+                       // EOTの場所が変わると曲の長さが変わるので、親モデルへ通知する。
+                       sequenceTrackListTableModel.sequenceListTableModel.fireSequenceModified(sequenceTrackListTableModel);
+               }
+       }
+       /**
+        * MIDIトラックを返します。
+        * @return MIDIトラック
+        */
+       public Track getTrack() { return track; }
+       /**
+        * トラック名を返します。
+        */
+       @Override
+       public String toString() {
+               byte b[] = MIDISpec.getNameBytesOf(track);
+               if( b == null ) return "";
+               Charset cs = Charset.defaultCharset();
+               if( sequenceTrackListTableModel != null )
+                       cs = sequenceTrackListTableModel.charset;
+               return new String(b, cs);
+       }
+       /**
+        * トラック名を設定します。
+        * @param name トラック名
+        * @return 設定が行われたらtrue
+        */
+       public boolean setString(String name) {
+               if(name.equals(toString()))
+                       return false;
+               byte b[] = name.getBytes(sequenceTrackListTableModel.charset);
+               if( ! MIDISpec.setNameBytesOf(track, b) )
+                       return false;
+               sequenceTrackListTableModel.setModified(true);
+               sequenceTrackListTableModel.sequenceListTableModel.fireSequenceModified(sequenceTrackListTableModel);
+               fireTableDataChanged();
+               return true;
+       }
+       private String recordingChannel = "OFF";
+       /**
+        * 録音中のMIDIチャンネルを返します。
+        * @return 録音中のMIDIチャンネル
+        */
+       public String getRecordingChannel() { return recordingChannel; }
+       /**
+        * 録音中のMIDIチャンネルを設定します。
+        * @param recordingChannel 録音中のMIDIチャンネル
+        */
+       public void setRecordingChannel(String recordingChannel) {
+               Sequencer sequencer = sequenceTrackListTableModel.sequenceListTableModel.sequencerModel.getSequencer();
+               if( recordingChannel.equals("OFF") ) {
+                       sequencer.recordDisable( track );
+               }
+               else if( recordingChannel.equals("ALL") ) {
+                       sequencer.recordEnable( track, -1 );
+               }
+               else {
+                       try {
+                               int ch = Integer.decode(recordingChannel).intValue() - 1;
+                               sequencer.recordEnable( track, ch );
+                       } catch( NumberFormatException nfe ) {
+                               sequencer.recordDisable( track );
+                               this.recordingChannel = "OFF";
+                               return;
+                       }
+               }
+               this.recordingChannel = recordingChannel;
+       }
+       /**
+        * このトラックの対象MIDIチャンネルを返します。
+        * <p>全てのチャンネルメッセージが同じMIDIチャンネルの場合、
+        * そのMIDIチャンネルを返します。
+        * MIDIチャンネルの異なるチャンネルメッセージが一つでも含まれていた場合、
+        * -1 を返します。
+        * </p>
+        * @return 対象MIDIチャンネル(不統一の場合 -1)
+        */
+       public int getChannel() {
+               int prevCh = -1;
+               int trackSize = track.size();
+               for( int index=0; index < trackSize; index++ ) {
+                       MidiMessage msg = track.get(index).getMessage();
+                       if( ! (msg instanceof ShortMessage) )
+                               continue;
+                       ShortMessage smsg = (ShortMessage)msg;
+                       if( ! MIDISpec.isChannelMessage(smsg) )
+                               continue;
+                       int ch = smsg.getChannel();
+                       if( prevCh >= 0 && prevCh != ch ) {
+                               return -1;
+                       }
+                       prevCh = ch;
+               }
+               return prevCh;
+       }
+       /**
+        * 指定されたMIDIチャンネルをすべてのチャンネルメッセージに対して設定します。
+        * @param channel MIDIチャンネル
+        */
+       public void setChannel(int channel) {
+               int track_size = track.size();
+               for( int index=0; index < track_size; index++ ) {
+                       MidiMessage msg = track.get(index).getMessage();
+                       if( ! (msg instanceof ShortMessage) )
+                               continue;
+                       ShortMessage smsg = (ShortMessage)msg;
+                       if( ! MIDISpec.isChannelMessage(smsg) )
+                               continue;
+                       if( smsg.getChannel() == channel )
+                               continue;
+                       try {
+                               smsg.setMessage(
+                                       smsg.getCommand(), channel,
+                                       smsg.getData1(), smsg.getData2()
+                               );
+                       }
+                       catch( InvalidMidiDataException e ) {
+                               e.printStackTrace();
+                       }
+                       sequenceTrackListTableModel.setModified(true);
+               }
+               sequenceTrackListTableModel.fireTrackChanged(track);
+               fireTableDataChanged();
+       }
+       /**
+        * 指定の MIDI tick 位置にあるイベントを二分探索し、
+        * そのイベントの行インデックスを返します。
+        * @param tick MIDI tick
+        * @return 行インデックス
+        */
+       public int tickToIndex(long tick) {
+               if( track == null )
+                       return 0;
+               int minIndex = 0;
+               int maxIndex = track.size() - 1;
+               while( minIndex < maxIndex ) {
+                       int currentIndex = (minIndex + maxIndex) / 2 ;
+                       long currentTick = track.get(currentIndex).getTick();
+                       if( tick > currentTick ) {
+                               minIndex = currentIndex + 1;
+                       }
+                       else if( tick < currentTick ) {
+                               maxIndex = currentIndex - 1;
+                       }
+                       else {
+                               return currentIndex;
+                       }
+               }
+               return (minIndex + maxIndex) / 2;
+       }
+       /**
+        * NoteOn/NoteOff ペアの一方の行インデックスから、
+        * もう一方(ペアの相手)の行インデックスを返します。
+        * @param index 行インデックス
+        * @return ペアを構成する相手の行インデックス(ない場合は -1)
+        */
+       public int getIndexOfPartnerFor(int index) {
+               if( track == null || index >= track.size() )
+                       return -1;
+               MidiMessage msg = track.get(index).getMessage();
+               if( ! (msg instanceof ShortMessage) ) return -1;
+               ShortMessage sm = (ShortMessage)msg;
+               int cmd = sm.getCommand();
+               int i;
+               int ch = sm.getChannel();
+               int note = sm.getData1();
+               MidiMessage partner_msg;
+               ShortMessage partner_sm;
+               int partner_cmd;
+
+               switch( cmd ) {
+               case 0x90: // NoteOn
+               if( sm.getData2() > 0 ) {
+                       // Search NoteOff event forward
+                       for( i = index + 1; i < track.size(); i++ ) {
+                               partner_msg = track.get(i).getMessage();
+                               if( ! (partner_msg instanceof ShortMessage ) ) continue;
+                               partner_sm = (ShortMessage)partner_msg;
+                               partner_cmd = partner_sm.getCommand();
+                               if( partner_cmd != 0x80 && partner_cmd != 0x90 ||
+                                               partner_cmd == 0x90 && partner_sm.getData2() > 0
+                                               ) {
+                                       // Not NoteOff
+                                       continue;
+                               }
+                               if( ch != partner_sm.getChannel() || note != partner_sm.getData1() ) {
+                                       // Not my partner
+                                       continue;
+                               }
+                               return i;
+                       }
+                       break;
+               }
+               // When velocity is 0, it means Note Off, so no break.
+               case 0x80: // NoteOff
+                       // Search NoteOn event backward
+                       for( i = index - 1; i >= 0; i-- ) {
+                               partner_msg = track.get(i).getMessage();
+                               if( ! (partner_msg instanceof ShortMessage ) ) continue;
+                               partner_sm = (ShortMessage)partner_msg;
+                               partner_cmd = partner_sm.getCommand();
+                               if( partner_cmd != 0x90 || partner_sm.getData2() <= 0 ) {
+                                       // Not NoteOn
+                                       continue;
+                               }
+                               if( ch != partner_sm.getChannel() || note != partner_sm.getData1() ) {
+                                       // Not my partner
+                                       continue;
+                               }
+                               return i;
+                       }
+                       break;
+               }
+               // Not found
+               return -1;
+       }
+       /**
+        * ノートメッセージかどうか調べます。
+        * @param index 行インデックス
+        * @return Note On または Note Off のとき true
+        */
+       public boolean isNote(int index) {
+               MidiEvent midiEvent = getMidiEvent(index);
+               MidiMessage msg = midiEvent.getMessage();
+               if( ! (msg instanceof ShortMessage) ) return false;
+               int cmd = ((ShortMessage)msg).getCommand();
+               return cmd == ShortMessage.NOTE_ON || cmd == ShortMessage.NOTE_OFF ;
+       }
+       /**
+        * 指定の行インデックスのMIDIイベントを返します。
+        * @param index 行インデックス
+        * @return MIDIイベント
+        */
+       public MidiEvent getMidiEvent(int index) {
+               return track==null ? null : track.get(index);
+       }
+       /**
+        * 選択されているMIDIイベントを返します。
+        * @return 選択されているMIDIイベント
+        */
+       public MidiEvent[] getSelectedMidiEvents() {
+               Vector<MidiEvent> events = new Vector<MidiEvent>();
+               if( ! eventSelectionModel.isSelectionEmpty() ) {
+                       int i = eventSelectionModel.getMinSelectionIndex();
+                       int max = eventSelectionModel.getMaxSelectionIndex();
+                       for( ; i <= max; i++ )
+                               if( eventSelectionModel.isSelectedIndex(i) )
+                                       events.add(track.get(i));
+               }
+               return events.toArray(new MidiEvent[1]);
+       }
+       /**
+        * MIDIイベントを追加します。
+        * @param midiEvent 追加するMIDIイベント
+        * @return 追加できたらtrue
+        */
+       public boolean addMidiEvent(MidiEvent midiEvent) {
+               if( track == null || !(track.add(midiEvent)) )
+                       return false;
+               if( MIDISpec.isTimeSignature(midiEvent.getMessage()) )
+                       sequenceTrackListTableModel.fireTimeSignatureChanged();
+               sequenceTrackListTableModel.fireTrackChanged(track);
+               int lastIndex = track.size() - 1;
+               fireTableRowsInserted( lastIndex-1, lastIndex-1 );
+               return true;
+       }
+       /**
+        * MIDIイベントを追加します。
+        * @param midiEvents 追加するMIDIイベント
+        * @param destinationTick 追加先tick
+        * @param sourcePPQ PPQ値(タイミング解像度)
+        * @return 追加できたらtrue
+        */
+       public boolean addMidiEvents(MidiEvent midiEvents[], long destinationTick, int sourcePPQ) {
+               if( track == null )
+                       return false;
+               int destinationPPQ = sequenceTrackListTableModel.getSequence().getResolution();
+               boolean done = false;
+               boolean hasTimeSignature = false;
+               long firstSourceEventTick = -1;
+               for( MidiEvent sourceEvent : midiEvents ) {
+                       long sourceEventTick = sourceEvent.getTick();
+                       MidiMessage msg = sourceEvent.getMessage();
+                       long newTick = destinationTick;
+                       if( firstSourceEventTick < 0 ) {
+                               firstSourceEventTick = sourceEventTick;
+                       }
+                       else {
+                               newTick += (sourceEventTick - firstSourceEventTick) * destinationPPQ / sourcePPQ;
+                       }
+                       if( ! track.add(new MidiEvent(msg, newTick)) ) continue;
+                       done = true;
+                       if( MIDISpec.isTimeSignature(msg) ) hasTimeSignature = true;
+               }
+               if( done ) {
+                       if( hasTimeSignature ) sequenceTrackListTableModel.fireTimeSignatureChanged();
+                       sequenceTrackListTableModel.fireTrackChanged(track);
+                       int lastIndex = track.size() - 1;
+                       int oldLastIndex = lastIndex - midiEvents.length;
+                       fireTableRowsInserted(oldLastIndex, lastIndex);
+               }
+               return done;
+       }
+       /**
+        * MIDIイベントを除去します。
+        * 曲の長さが変わることがあるので、プレイリストにも通知します。
+        * @param midiEvents 除去するMIDIイベント
+        */
+       public void removeMidiEvents(MidiEvent midiEvents[]) {
+               if( track == null )
+                       return;
+               boolean hadTimeSignature = false;
+               for( MidiEvent e : midiEvents ) {
+                       if( MIDISpec.isTimeSignature(e.getMessage()) )
+                               hadTimeSignature = true;
+                       track.remove(e);
+               }
+               if( hadTimeSignature ) {
+                       sequenceTrackListTableModel.fireTimeSignatureChanged();
+               }
+               sequenceTrackListTableModel.fireTrackChanged(track);
+               int lastIndex = track.size() - 1;
+               int oldLastIndex = lastIndex + midiEvents.length;
+               if(lastIndex < 0) lastIndex = 0;
+               fireTableRowsDeleted(oldLastIndex, lastIndex);
+               sequenceTrackListTableModel.sequenceListTableModel.fireSelectedSequenceModified();
+       }
+       /**
+        * 引数の選択内容が示すMIDIイベントを除去します。
+        * @param selectionModel 選択内容
+        */
+       public void removeSelectedMidiEvents() {
+               removeMidiEvents(getSelectedMidiEvents());
+       }
+}
diff --git a/src/camidion/chordhelper/midieditor/VelocitySelecter.java b/src/camidion/chordhelper/midieditor/VelocitySelecter.java
new file mode 100644 (file)
index 0000000..0a6d5cb
--- /dev/null
@@ -0,0 +1,42 @@
+package camidion.chordhelper.midieditor;
+
+import java.awt.Color;
+import java.awt.Label;
+
+import javax.swing.BoundedRangeModel;
+import javax.swing.BoxLayout;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JSlider;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+
+/**
+ * ベロシティ選択ビュー
+ */
+public class VelocitySelecter extends JPanel implements ChangeListener {
+       private static final String     LABEL_PREFIX = "Velocity=";
+       public JSlider slider;
+       public JLabel label;
+       public VelocitySelecter(BoundedRangeModel model) {
+               setLayout(new BoxLayout(this, BoxLayout.X_AXIS));
+               add(label = new JLabel(LABEL_PREFIX + model.getValue(), Label.RIGHT) {{
+                       setToolTipText("Velocity");
+               }});
+               add(slider = new JSlider(model) {{ setToolTipText("Velocity"); }});
+               slider.addChangeListener(this);
+       }
+       public void stateChanged(ChangeEvent e) {
+               label.setText( LABEL_PREFIX + getValue() );
+       }
+       @Override
+       public void setBackground(Color c) {
+               super.setBackground(c);
+               // このクラスが構築される前にスーパークラスの
+               // Look & Feel からここが呼ばれることがあるため
+               // null チェックが必要
+               if( slider != null ) slider.setBackground(c);
+       }
+       public int getValue() { return slider.getValue(); }
+       public void setValue(int velocity) { slider.setValue(velocity); }
+}
\ No newline at end of file
diff --git a/src/camidion/chordhelper/music/AbstractNoteTrackSpec.java b/src/camidion/chordhelper/music/AbstractNoteTrackSpec.java
new file mode 100644 (file)
index 0000000..0cedf6a
--- /dev/null
@@ -0,0 +1,80 @@
+package camidion.chordhelper.music;
+
+import javax.sound.midi.InvalidMidiDataException;
+import javax.sound.midi.MidiEvent;
+import javax.sound.midi.MidiMessage;
+import javax.sound.midi.Sequence;
+import javax.sound.midi.ShortMessage;
+import javax.sound.midi.Track;
+
+// 一般のトラック(メロディ、ドラム共通)
+//
+public abstract class AbstractNoteTrackSpec extends AbstractTrackSpec {
+       public int midiChannel = -1;
+       public int programNumber = -1;
+       public int velocity = 64;
+
+       public AbstractNoteTrackSpec() {}
+       public AbstractNoteTrackSpec(int ch) {
+               midiChannel = ch;
+       }
+       public AbstractNoteTrackSpec(int ch, String name) {
+               midiChannel = ch;
+               this.name = name;
+       }
+       public AbstractNoteTrackSpec(int ch, String name, int program_no) {
+               this(ch,name);
+               this.programNumber = program_no;
+       }
+       public AbstractNoteTrackSpec(int ch, String name, int program_no, int velocity) {
+               this(ch,name,program_no);
+               this.velocity = velocity;
+       }
+       public Track createTrack( Sequence seq, FirstTrackSpec first_track_spec ) {
+               Track track = super.createTrack( seq, first_track_spec );
+               if( programNumber >= 0 ) addProgram( programNumber, 0 );
+               return track;
+       }
+       public boolean addProgram( int program_no, long tick_pos ) {
+               ShortMessage short_msg;
+               try {
+                       (short_msg = new ShortMessage()).setMessage(
+                               ShortMessage.PROGRAM_CHANGE, midiChannel, program_no, 0
+                       );
+               } catch( InvalidMidiDataException ex ) {
+                       ex.printStackTrace();
+                       return false;
+               }
+               return track.add(new MidiEvent( (MidiMessage)short_msg, tick_pos ));
+       }
+       public boolean addNote(long start_tick_pos, long end_tick_pos, int note_no) {
+               return addNote(start_tick_pos, end_tick_pos, note_no, velocity);
+       }
+       public boolean addNote(
+               long start_tick_pos, long end_tick_pos,
+               int note_no, int velocity
+       ) {
+               ShortMessage short_msg;
+               //
+               try {
+                       (short_msg = new ShortMessage()).setMessage(
+                               ShortMessage.NOTE_ON, midiChannel, note_no, velocity
+                       );
+               } catch( InvalidMidiDataException ex ) {
+                       ex.printStackTrace();
+                       return false;
+               }
+               if( ! track.add(new MidiEvent( (MidiMessage)short_msg, start_tick_pos )) )
+                       return false;
+               //
+               try {
+                       (short_msg = new ShortMessage()).setMessage(
+                                       ShortMessage.NOTE_OFF, midiChannel, note_no, velocity
+                                       );
+               } catch( InvalidMidiDataException ex ) {
+                       ex.printStackTrace();
+                       return false;
+               }
+               return track.add( new MidiEvent( (MidiMessage)short_msg, end_tick_pos ) );
+       }
+}
\ No newline at end of file
diff --git a/src/camidion/chordhelper/music/AbstractTrackSpec.java b/src/camidion/chordhelper/music/AbstractTrackSpec.java
new file mode 100644 (file)
index 0000000..ce7f32f
--- /dev/null
@@ -0,0 +1,103 @@
+package camidion.chordhelper.music;
+
+import javax.sound.midi.InvalidMidiDataException;
+import javax.sound.midi.MetaMessage;
+import javax.sound.midi.MidiEvent;
+import javax.sound.midi.Sequence;
+import javax.sound.midi.SysexMessage;
+import javax.sound.midi.Track;
+
+/**
+ * MIDIトラックの仕様を表すクラス
+ */
+public abstract class AbstractTrackSpec {
+       public static final int BEAT_RESOLUTION = 2;
+       // 最短の音符の長さ(四分音符を何回半分にするか)
+       public String name = null;
+       Track track = null;
+       FirstTrackSpec first_track_spec = null;
+       Sequence sequence = null;
+       long minNoteTicks = 0;
+       int pre_measures = 2;
+       /**
+        * トラック名なしでMIDIトラック仕様を構築します。
+        */
+       public AbstractTrackSpec() { }
+       /**
+        * トラック名を指定してMIDIトラック仕様を構築します。
+        * @param name
+        */
+       public AbstractTrackSpec(String name) {
+               this.name = name;
+       }
+       /**
+        * このオブジェクトの文字列表現としてトラック名を返します。
+        * トラック名がない場合はスーパークラスの toString() と同じです。
+        */
+       public String toString() {
+               return name==null ? super.toString() : name;
+       }
+       /**
+        * トラックを生成して返します。
+        * @param seq MIDIシーケンス
+        * @param firstTrackSpec 最初のトラック仕様
+        * @return 生成したトラック
+        */
+       public Track createTrack( Sequence seq, FirstTrackSpec firstTrackSpec ) {
+               this.first_track_spec = firstTrackSpec;
+               track = (sequence = seq).createTrack();
+               if( name != null ) addStringTo( 0x03, name, 0 );
+               minNoteTicks = (long)( seq.getResolution() >> 2 );
+               return track;
+       }
+       /**
+        * メタイベントを追加します。
+        * @param type メタイベントのタイプ
+        * @param data メタイベントのデータ
+        * @param tickPos tick位置
+        * @return {@link Track#add(MidiEvent)} と同じ
+        */
+       public boolean addMetaEventTo( int type, byte data[], long tickPos  ) {
+               MetaMessage meta_msg = new MetaMessage();
+               try {
+                       meta_msg.setMessage( type, data, data.length );
+               } catch( InvalidMidiDataException ex ) {
+                       ex.printStackTrace();
+                       return false;
+               }
+               return track.add(new MidiEvent(meta_msg, tickPos));
+       }
+       /**
+        * 文字列をメタイベントとして追加します。
+        * @param type メタイベントのタイプ
+        * @param str 追加する文字列
+        * @param tickPos tick位置
+        * @return {@link #addMetaEventTo(int, byte[], long)} と同じ
+        */
+       public boolean addStringTo( int type, String str, long tickPos ) {
+               if( str == null ) str = "";
+               return addMetaEventTo( type, str.getBytes(), tickPos );
+       }
+       public boolean addStringTo( int type, ChordProgression.ChordStroke cs ) {
+               return addStringTo(type, cs.chord.toString(), cs.tick_range.start_tick_pos);
+       }
+       public boolean addStringTo( int type, ChordProgression.Lyrics lyrics ) {
+               return addStringTo(type, lyrics.text, lyrics.start_tick_pos);
+       }
+       public boolean addEOT( long tick_pos ) {
+               return addMetaEventTo( 0x2F, new byte[0], tick_pos );
+       }
+       public void setChordSymbolText( ChordProgression cp ) {
+               cp.setChordSymbolTextTo( this );
+       }
+       public boolean addSysEx(byte[] data, long tickPos) {
+               SysexMessage msg = new SysexMessage();
+               try {
+                       msg.setMessage( SysexMessage.SYSTEM_EXCLUSIVE, data, data.length );
+               } catch( InvalidMidiDataException ex ) {
+                       ex.printStackTrace();
+                       return false;
+               }
+               return track.add(new MidiEvent( msg, tickPos ));
+       }
+}
\ No newline at end of file
diff --git a/src/camidion/chordhelper/music/Chord.java b/src/camidion/chordhelper/music/Chord.java
new file mode 100644 (file)
index 0000000..294afc6
--- /dev/null
@@ -0,0 +1,679 @@
+package camidion.chordhelper.music;
+
+import java.awt.Color;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Vector;
+
+import javax.swing.JLabel;
+
+/**
+ * 和音(コード - musical chord)のクラス
+ */
+public class Chord implements Cloneable {
+       /**
+        * コード構成音の順序に対応する色
+        */
+       public static final Color NOTE_INDEX_COLORS[] = {
+               Color.red,
+               new Color(0x40,0x40,0xFF),
+               Color.orange.darker(),
+               new Color(0x20,0x99,0x00),
+               Color.magenta,
+               Color.orange,
+               Color.green
+       };
+       /**
+        * 音程差の半音オフセットのインデックス
+        */
+       public static enum OffsetIndex {
+               THIRD,
+               FIFTH,
+               SEVENTH,
+               NINTH,
+               ELEVENTH,
+               THIRTEENTH
+       }
+       /**
+        * 音程差
+        */
+       public static enum Interval {
+
+               /** 長2度(major 2nd / sus2) */
+               SUS2(2, OffsetIndex.THIRD),
+               /** 短3度または増2度 */
+               MINOR(3, OffsetIndex.THIRD),
+               /** 長3度 */
+               MAJOR(4, OffsetIndex.THIRD),
+               /** 完全4度(parfect 4th / sus4) */
+               SUS4(5, OffsetIndex.THIRD),
+
+               /** 減5度または増4度(トライトーン = 三全音 = 半オクターブ) */
+               FLAT5(6, OffsetIndex.FIFTH),
+               /** 完全5度 */
+               PARFECT5(7, OffsetIndex.FIFTH),
+               /** 増5度または短6度 */
+               SHARP5(8, OffsetIndex.FIFTH),
+
+               /** 長6度または減7度 */
+               SIXTH(9, OffsetIndex.SEVENTH),
+               /** 短7度 */
+               SEVENTH(10, OffsetIndex.SEVENTH),
+               /** 長7度 */
+               MAJOR_SEVENTH(11, OffsetIndex.SEVENTH),
+
+               /** 短9度(短2度の1オクターブ上) */
+               FLAT9(13, OffsetIndex.NINTH),
+               /** 長9度(長2度の1オクターブ上) */
+               NINTH(14, OffsetIndex.NINTH),
+               /** 増9度(増2度の1オクターブ上) */
+               SHARP9(15, OffsetIndex.NINTH),
+
+               /** 完全11度(完全4度の1オクターブ上) */
+               ELEVENTH(17, OffsetIndex.ELEVENTH),
+               /** 増11度(増4度の1オクターブ上) */
+               SHARP11(18, OffsetIndex.ELEVENTH),
+
+               /** 短13度(短6度の1オクターブ上) */
+               FLAT13(20, OffsetIndex.THIRTEENTH),
+               /** 長13度(長6度の1オクターブ上) */
+               THIRTEENTH(21, OffsetIndex.THIRTEENTH);
+
+               private Interval(int offset, OffsetIndex offsetIndex) {
+                       this.offset = offset;
+                       this.offsetIndex = offsetIndex;
+               }
+               private OffsetIndex offsetIndex;
+               private int offset;
+               /**
+                * 半音差を返します。
+                * @return 半音差
+                */
+               public int getChromaticOffset() { return offset; }
+               /**
+                * 対応するインデックスを返します。
+                * @return 対応するインデックス
+                */
+               public OffsetIndex getChromaticOffsetIndex() {
+                       return offsetIndex;
+               }
+       }
+       /**
+        * デフォルトの半音値(メジャーコード固定)
+        */
+       public static Map<OffsetIndex, Interval>
+               DEFAULT_OFFSETS = new HashMap<OffsetIndex, Interval>() {
+                       {
+                               Interval itv;
+                               itv = Interval.MAJOR; put(itv.getChromaticOffsetIndex(), itv);
+                               itv = Interval.PARFECT5; put(itv.getChromaticOffsetIndex(), itv);
+                       }
+               };
+       /**
+        * 現在有効な構成音の音程(ルート音を除く)
+        */
+       public Map<OffsetIndex, Interval> offsets = new HashMap<>(DEFAULT_OFFSETS);
+       /**
+        * このコードのルート音
+        */
+       private NoteSymbol rootNoteSymbol;
+       /**
+        * このコードのベース音(ルート音と異なる場合は分数コードの分母)
+        */
+       private NoteSymbol bassNoteSymbol;
+
+       /**
+        * コード C major を構築します。
+        */
+       public Chord() {
+               this(new NoteSymbol());
+       }
+       /**
+        * 指定した音名のメジャーコードを構築します。
+        * @param noteSymbol 音名
+        */
+       public Chord(NoteSymbol noteSymbol) {
+               setRoot(noteSymbol);
+               setBass(noteSymbol);
+       }
+       /**
+        * 指定された調と同名のコードを構築します。
+        * <p>元の調がマイナーキーの場合はマイナーコード、
+        * それ以外の場合はメジャーコードになります。
+        * </p>
+        * @param key 調
+        */
+       public Chord(Key key) {
+               int keyCo5 = key.toCo5();
+               if( key.majorMinor() == Key.MINOR ) {
+                       keyCo5 += 3;
+                       set(Interval.MINOR);
+               }
+               setRoot(new NoteSymbol(keyCo5));
+               setBass(new NoteSymbol(keyCo5));
+       }
+       /**
+        * コード名の文字列からコードを構築します。
+        * @param chordSymbol コード名の文字列
+        */
+       public Chord(String chordSymbol) {
+               setChordSymbol(chordSymbol);
+       }
+       /**
+        * このコードのクローンを作成します。
+        */
+       @Override
+       public Chord clone() {
+               Chord newChord = new Chord(rootNoteSymbol);
+               newChord.offsets = new HashMap<>(offsets);
+               newChord.setBass(bassNoteSymbol);
+               return newChord;
+       }
+       /**
+        * コードのルート音を指定された音階に置換します。
+        * @param rootNoteSymbol 音階
+        * @return このコード自身(置換後)
+        */
+       public Chord setRoot(NoteSymbol rootNoteSymbol) {
+               this.rootNoteSymbol = rootNoteSymbol;
+               return this;
+       }
+       /**
+        * コードのベース音を指定された音階に置換します。
+        * @param rootNoteSymbol 音階
+        * @return このコード自身(置換後)
+        */
+       public Chord setBass(NoteSymbol rootNoteSymbol) {
+               this.bassNoteSymbol = rootNoteSymbol;
+               return this;
+       }
+       /**
+        * コードの種類を設定します。
+        * @param itv 設定する音程
+        */
+       public void set(Interval itv) {
+               offsets.put(itv.getChromaticOffsetIndex(), itv);
+       }
+       /**
+        * コードに設定した音程をクリアします。
+        * @param index 半音差インデックス
+        */
+       public void clear(OffsetIndex index) {
+               offsets.remove(index);
+       }
+       //
+       // コードネームの文字列が示すコードに置き換えます。
+       public Chord setChordSymbol(String chordSymbol) {
+               //
+               // 分数コードの分子と分母に分ける
+               String parts[] = chordSymbol.trim().split("(/|on)");
+               if( parts.length == 0 ) {
+                       return this;
+               }
+               // ルート音とベース音を設定
+               setRoot(new NoteSymbol(parts[0]));
+               setBass(new NoteSymbol(parts[ parts.length > 1 ? 1 : 0 ]));
+               String suffix = parts[0].replaceFirst("^[A-G][#bx]*","");
+               //
+               // () があれば、その中身を取り出す
+               String suffixParts[] = suffix.split("[\\(\\)]");
+               if( suffixParts.length == 0 ) {
+                       return this;
+               }
+               String suffixParen = "";
+               if( suffixParts.length > 1 ) {
+                       suffixParen = suffixParts[1];
+                       suffix = suffixParts[0];
+               }
+               Interval itv;
+               //
+               // +5 -5 aug dim の判定
+               set(
+                       suffix.matches(".*(\\+5|aug|#5).*") ? Interval.SHARP5 :
+                       suffix.matches(".*(-5|dim|b5).*") ? Interval.FLAT5 :
+                       Interval.PARFECT5
+               );
+               //
+               // 6 7 M7 の判定
+               itv = suffix.matches(".*(M7|maj7|M9|maj9).*") ? Interval.MAJOR_SEVENTH :
+                       suffix.matches(".*(6|dim[79]).*") ? Interval.SIXTH :
+                       suffix.matches(".*7.*") ? Interval.SEVENTH :
+                       null;
+               if(itv==null)
+                       clear(OffsetIndex.SEVENTH);
+               else
+                       set(itv);
+               //
+               // マイナーの判定。maj7 と間違えないように比較
+               set(
+                       (suffix.matches(".*m.*") && ! suffix.matches(".*ma.*") ) ? Interval.MINOR :
+                       suffix.matches(".*sus4.*") ? Interval.SUS4 :
+                       Interval.MAJOR
+               );
+               //
+               // 9th の判定
+               if( suffix.matches(".*9.*") ) {
+                       set(Interval.NINTH);
+                       if( ! suffix.matches( ".*(add9|6|M9|maj9|dim9).*") ) {
+                               set(Interval.SEVENTH);
+                       }
+               }
+               else {
+                       offsets.remove(OffsetIndex.NINTH);
+                       offsets.remove(OffsetIndex.ELEVENTH);
+                       offsets.remove(OffsetIndex.THIRTEENTH);
+                       //
+                       // () の中を , で分ける
+                       String parts_in_paren[] = suffixParen.split(",");
+                       for( String p : parts_in_paren ) {
+                               if( p.matches("(\\+9|#9)") )
+                                       offsets.put(OffsetIndex.NINTH, Interval.SHARP9);
+                               else if( p.matches("(-9|b9)") )
+                                       offsets.put(OffsetIndex.NINTH, Interval.FLAT9);
+                               else if( p.matches("9") )
+                                       offsets.put(OffsetIndex.NINTH, Interval.NINTH);
+
+                               if( p.matches("(\\+11|#11)") )
+                                       offsets.put(OffsetIndex.ELEVENTH, Interval.SHARP11);
+                               else if( p.matches("11") )
+                                       offsets.put(OffsetIndex.ELEVENTH, Interval.ELEVENTH);
+
+                               if( p.matches("(-13|b13)") )
+                                       offsets.put(OffsetIndex.THIRTEENTH, Interval.FLAT13);
+                               else if( p.matches("13") )
+                                       offsets.put(OffsetIndex.THIRTEENTH, Interval.THIRTEENTH);
+
+                               // -5 や +5 が () の中にあっても解釈できるようにする
+                               if( p.matches("(-5|b5)") )
+                                       offsets.put(OffsetIndex.FIFTH, Interval.FLAT5);
+                               else if( p.matches("(\\+5|#5)") )
+                                       offsets.put(OffsetIndex.FIFTH, Interval.SHARP5);
+                       }
+               }
+               return this;
+       }
+       /**
+        * ルート音を返します。
+        * @return ルート音
+        */
+       public NoteSymbol rootNoteSymbol() { return rootNoteSymbol; }
+       /**
+        * ベース音を返します。分数コードの場合はルート音と異なります。
+        * @return ベース音
+        */
+       public NoteSymbol bassNoteSymbol() { return bassNoteSymbol; }
+       /**
+        * 指定した音程が設定されているか調べます。
+        * @param itv 音程
+        * @return 指定した音程が設定されていたらtrue
+        */
+       public boolean isSet(Interval itv) {
+               return offsets.get(itv.getChromaticOffsetIndex()) == itv;
+       }
+       /**
+        * 指定したインデックスに音程が設定されているか調べます。
+        * @param index インデックス
+        * @return 指定したインデックスに音程が設定されていたらtrue
+        */
+       public boolean isSet(OffsetIndex index) {
+               return offsets.containsKey(index);
+       }
+       /**
+        * コードが等しいかどうかを判定します。
+        * @return 等しければtrue
+        */
+       @Override
+       public boolean equals(Object anObject) {
+               if( this == anObject )
+                       return true;
+               if( anObject instanceof Chord ) {
+                       Chord another = (Chord) anObject;
+                       if( ! rootNoteSymbol.equals(another.rootNoteSymbol) )
+                               return false;
+                       if( ! bassNoteSymbol.equals(another.bassNoteSymbol) )
+                               return false;
+                       return offsets.equals(another.offsets);
+               }
+               return false;
+       }
+       @Override
+       public int hashCode() {
+               return toString().hashCode();
+       }
+       /**
+        * コードが等しいかどうかを、異名同音を無視して判定します。
+        * @param another 比較対象のコード
+        * @return 等しければtrue
+        */
+       public boolean equalsEnharmonically(Chord another) {
+               if( this == another )
+                       return true;
+               if( another == null )
+                       return false;
+               if( ! rootNoteSymbol.equalsEnharmonically(another.rootNoteSymbol) )
+                       return false;
+               if( ! bassNoteSymbol.equalsEnharmonically(another.bassNoteSymbol) )
+                       return false;
+               return offsets.equals(another.offsets);
+       }
+       /**
+        * コード構成音の数を返します
+        * (ルート音は含まれますが、ベース音は含まれません)。
+        *
+        * @return コード構成音の数
+        */
+       public int numberOfNotes() { return offsets.size() + 1; }
+       /**
+        * 指定された位置にあるノート番号を返します。
+        * @param index 位置(0をルート音とした構成音の順序)
+        * @return ノート番号(該当する音がない場合は -1)
+        */
+       public int noteAt(int index) {
+               int rootnote = rootNoteSymbol.toNoteNumber();
+               if( index == 0 )
+                       return rootnote;
+               Interval itv;
+               int i=0;
+               for( OffsetIndex offsetIndex : OffsetIndex.values() )
+                       if( (itv = offsets.get(offsetIndex)) != null && ++i == index )
+                               return rootnote + itv.getChromaticOffset();
+               return -1;
+       }
+       /**
+        * コード構成音を格納したノート番号の配列を返します。
+        * (ベース音は含まれません)
+        * 音域が指定された場合、その音域に合わせたノート番号を返します。
+        * @param range 音域(null可)
+        * @param key キー(null可)
+        * @return ノート番号の配列
+        */
+       public int[] toNoteArray(Range range, Key key) {
+               int rootnote = rootNoteSymbol.toNoteNumber();
+               int ia[] = new int[numberOfNotes()];
+               int i;
+               ia[i=0] = rootnote;
+               Interval itv;
+               for( OffsetIndex offsetIndex : OffsetIndex.values() )
+                       if( (itv = offsets.get(offsetIndex)) != null )
+                               ia[++i] = rootnote + itv.getChromaticOffset();
+               if( range != null )
+                       range.invertNotesOf(ia, key);
+               return ia;
+       }
+       /**
+        * MIDI ノート番号が、コードの構成音の何番目(0=ルート音)に
+        * あるかを表すインデックス値を返します。
+        * 構成音に該当しない場合は -1 を返します。
+        * ベース音は検索されません。
+        * @param noteNumber MIDIノート番号
+        * @return 構成音のインデックス値
+        */
+       public int indexOf(int noteNumber) {
+               int relativeNote = noteNumber - rootNoteSymbol.toNoteNumber();
+               if( Music.mod12(relativeNote) == 0 ) return 0;
+               Interval itv;
+               int i=0;
+               for( OffsetIndex offsetIndex : OffsetIndex.values() ) {
+                       if( (itv = offsets.get(offsetIndex)) != null ) {
+                               i++;
+                               if( Music.mod12(relativeNote - itv.getChromaticOffset()) == 0 )
+                                       return i;
+                       }
+               }
+               return -1;
+       }
+       /**
+        * 指定したキーのスケールを外れた構成音がないか調べます。
+        * @param key 調べるキー
+        * @return スケールを外れている構成音がなければtrue
+        */
+       public boolean isOnScaleInKey(Key key) {
+               return isOnScaleInKey(key.toCo5());
+       }
+       private boolean isOnScaleInKey(int keyCo5) {
+               int rootnote = rootNoteSymbol.toNoteNumber();
+               if( ! Music.isOnScale(rootnote, keyCo5) )
+                       return false;
+               Interval itv;
+               for( OffsetIndex offsetIndex : OffsetIndex.values() ) {
+                       if( (itv = offsets.get(offsetIndex)) == null )
+                               continue;
+                       if( ! Music.isOnScale(rootnote + itv.getChromaticOffset(), keyCo5) )
+                               return false;
+               }
+               return true;
+       }
+       /**
+        * コードを移調します。
+        * @param chromatic_offset 移調幅(半音単位)
+        * @return 移調した新しいコード(移調幅が0の場合は自分自身)
+        */
+       public Chord transpose(int chromatic_offset) {
+               return transpose(chromatic_offset, 0);
+       }
+       public Chord transpose(int chromatic_offset, Key original_key) {
+               return transpose(chromatic_offset, original_key.toCo5());
+       }
+       public Chord transpose(int chromatic_offset, int original_key_co5) {
+               if( chromatic_offset == 0 ) return this;
+               int offsetCo5 = Music.mod12(Music.reverseCo5(chromatic_offset));
+               if( offsetCo5 > 6 ) offsetCo5 -= 12;
+               int key_co5   = original_key_co5 + offsetCo5;
+               //
+               int newRootCo5 = rootNoteSymbol.toCo5() + offsetCo5;
+               int newBassCo5 = bassNoteSymbol.toCo5() + offsetCo5;
+               if( key_co5 > 6 ) {
+                       newRootCo5 -= 12;
+                       newBassCo5 -= 12;
+               }
+               else if( key_co5 < -5 ) {
+                       newRootCo5 += 12;
+                       newBassCo5 += 12;
+               }
+               setRoot(new NoteSymbol(newRootCo5));
+               return setBass(new NoteSymbol(newBassCo5));
+       }
+       /**
+        * この和音の文字列表現としてコード名を返します。
+        * @return この和音のコード名
+        */
+       @Override
+       public String toString() {
+               String chordSymbol = rootNoteSymbol + symbolSuffix();
+               if( ! rootNoteSymbol.equals(bassNoteSymbol) ) {
+                       chordSymbol += "/" + bassNoteSymbol;
+               }
+               return chordSymbol;
+       }
+       /**
+        * コード名を HTML で返します。
+        *
+        * Swing の {@link JLabel#setText(String)} は HTML で指定できるので、
+        * 文字の大きさに変化をつけることができます。
+        *
+        * @param color_name 色のHTML表現(色名または #RRGGBB 形式)
+        * @return コード名のHTML
+        */
+       public String toHtmlString(String color_name) {
+               String small_tag = "<span style=\"font-size: 120%\">";
+               String end_of_small_tag = "</span>";
+               String root = rootNoteSymbol.toString();
+               String formatted_root = (root.length() == 1) ? root + small_tag :
+                       root.replace("#",small_tag+"<sup>#</sup>").
+                       replace("b",small_tag+"<sup>b</sup>").
+                       replace("x",small_tag+"<sup>x</sup>");
+               String formatted_bass = "";
+               if( ! rootNoteSymbol.equals(bassNoteSymbol) ) {
+                       String bass = bassNoteSymbol.toString();
+                       formatted_bass = (bass.length() == 1) ? bass + small_tag :
+                               bass.replace("#",small_tag+"<sup>#</sup>").
+                               replace("b",small_tag+"<sup>b</sup>").
+                               replace("x",small_tag+"<sup>x</sup>");
+                       formatted_bass = "/" + formatted_bass + end_of_small_tag;
+               }
+               String suffix = symbolSuffix().
+                       replace("-5","<sup>-5</sup>").
+                       replace("+5","<sup>+5</sup>");
+               return
+                       "<html>" +
+                       "<span style=\"color: " + color_name + "; font-size: 170% ; white-space: nowrap ;\">" +
+                       formatted_root + suffix + end_of_small_tag + formatted_bass +
+                       "</span>" +
+                       "</html>" ;
+       }
+       /**
+        * コードの説明(英語)を返します。
+        * @return コードの説明(英語)
+        */
+       public String toName() {
+               String chord_name = rootNoteSymbol.toStringIn(SymbolLanguage.NAME) + nameSuffix() ;
+               if( ! rootNoteSymbol.equals(bassNoteSymbol) ) {
+                       chord_name += " on " + bassNoteSymbol.toStringIn(SymbolLanguage.NAME);
+               }
+               return chord_name;
+       }
+       /**
+        * コードネームの音名を除いた部分(サフィックス)を組み立てて返します。
+        * @return コードネームの音名を除いた部分
+        */
+       public String symbolSuffix() {
+               String suffix = (
+                       offsets.get(OffsetIndex.THIRD) == Interval.MINOR ? "m" : ""
+               );
+               Interval itv;
+               if( (itv = offsets.get(OffsetIndex.SEVENTH)) != null ) {
+                       switch(itv) {
+                       case SIXTH:         suffix += "6";  break;
+                       case SEVENTH:       suffix += "7";  break;
+                       case MAJOR_SEVENTH: suffix += "M7"; break;
+                       default: break;
+                       }
+               }
+               switch( offsets.get(OffsetIndex.THIRD) ) {
+               case SUS4: suffix += "sus4"; break;
+               case SUS2: suffix += "sus2"; break;
+               default: break;
+               }
+               switch( offsets.get(OffsetIndex.FIFTH) ) {
+               case FLAT5:  suffix += "-5"; break;
+               case SHARP5: suffix += "+5"; break;
+               default: break;
+               }
+               Vector<String> paren = new Vector<String>();
+               if( (itv = offsets.get(OffsetIndex.NINTH)) != null ) {
+                       switch(itv) {
+                       case NINTH:  paren.add("9"); break;
+                       case FLAT9:  paren.add("-9"); break;
+                       case SHARP9: paren.add("+9"); break;
+                       default: break;
+                       }
+               }
+               if( (itv = offsets.get(OffsetIndex.ELEVENTH)) != null ) {
+                       switch(itv) {
+                       case ELEVENTH: paren.add("11"); break;
+                       case SHARP11:  paren.add("+11"); break;
+                       default: break;
+                       }
+               }
+               if( (itv = offsets.get(OffsetIndex.THIRTEENTH)) != null ) {
+                       switch(itv) {
+                       case THIRTEENTH: paren.add("13"); break;
+                       case FLAT13:     paren.add("-13"); break;
+                       default: break;
+                       }
+               }
+               if( ! paren.isEmpty() ) {
+                       boolean is_first = true;
+                       suffix += "(";
+                       for( String p : paren ) {
+                               if( is_first )
+                                       is_first = false;
+                               else
+                                       suffix += ",";
+                               suffix += p;
+                       }
+                       suffix += ")";
+               }
+               if( suffix.equals("m-5") ) return "dim";
+               else if( suffix.equals("+5") ) return "aug";
+               else if( suffix.equals("m6-5") ) return "dim7";
+               else if( suffix.equals("(9)") ) return "add9";
+               else if( suffix.equals("7(9)") ) return "9";
+               else if( suffix.equals("M7(9)") ) return "M9";
+               else if( suffix.equals("7+5") ) return "aug7";
+               else if( suffix.equals("m6-5(9)") ) return "dim9";
+               else return suffix ;
+       }
+       /**
+        * コードの説明のうち、音名を除いた部分を組み立てて返します。
+        * @return コード説明の音名を除いた部分
+        */
+       public String nameSuffix() {
+               String suffix = "";
+               if( offsets.get(OffsetIndex.THIRD) == Interval.MINOR )
+                       suffix += " minor";
+               Interval itv;
+               if( (itv = offsets.get(OffsetIndex.SEVENTH)) != null ) {
+                       switch(itv) {
+                       case SIXTH:         suffix += " 6th"; break;
+                       case SEVENTH:       suffix += " 7th"; break;
+                       case MAJOR_SEVENTH: suffix += " major 7th"; break;
+                       default: break;
+                       }
+               }
+               switch( offsets.get(OffsetIndex.THIRD) ) {
+               case SUS4: suffix += " suspended 4th"; break;
+               case SUS2: suffix += " suspended 2nd"; break;
+               default: break;
+               }
+               switch( offsets.get(OffsetIndex.FIFTH) ) {
+               case FLAT5 : suffix += " flatted 5th"; break;
+               case SHARP5: suffix += " sharped 5th"; break;
+               default: break;
+               }
+               Vector<String> paren = new Vector<String>();
+               if( (itv = offsets.get(OffsetIndex.NINTH)) != null ) {
+                       switch(itv) {
+                       case NINTH:  paren.add("9th"); break;
+                       case FLAT9:  paren.add("flatted 9th"); break;
+                       case SHARP9: paren.add("sharped 9th"); break;
+                       default: break;
+                       }
+               }
+               if( (itv = offsets.get(OffsetIndex.ELEVENTH)) != null ) {
+                       switch(itv) {
+                       case ELEVENTH: paren.add("11th"); break;
+                       case SHARP11:  paren.add("sharped 11th"); break;
+                       default: break;
+                       }
+               }
+               if( (itv = offsets.get(OffsetIndex.THIRTEENTH)) != null ) {
+                       switch(itv) {
+                       case THIRTEENTH: paren.add("13th"); break;
+                       case FLAT13:     paren.add("flatted 13th"); break;
+                       default: break;
+                       }
+               }
+               if( ! paren.isEmpty() ) {
+                       boolean is_first = true;
+                       suffix += "(additional ";
+                       for( String p : paren ) {
+                               if( is_first )
+                                       is_first = false;
+                               else
+                                       suffix += ",";
+                               suffix += p;
+                       }
+                       suffix += ")";
+               }
+               if( suffix.equals(" minor flatted 5th") ) return " diminished (triad)";
+               else if( suffix.equals(" sharped 5th") ) return " augumented";
+               else if( suffix.equals(" minor 6th flatted 5th") ) return " diminished 7th";
+               else if( suffix.equals(" 7th(additional 9th)") ) return " 9th";
+               else if( suffix.equals(" major 7th(additional 9th)") ) return " major 9th";
+               else if( suffix.equals(" 7th sharped 5th") ) return " augumented 7th";
+               else if( suffix.equals(" minor 6th flatted 5th(additional 9th)") ) return " diminished 9th";
+               else if( suffix.isEmpty() ) return " major";
+               else return suffix ;
+       }
+}
\ No newline at end of file
diff --git a/src/camidion/chordhelper/music/ChordProgression.java b/src/camidion/chordhelper/music/ChordProgression.java
new file mode 100644 (file)
index 0000000..f157fe4
--- /dev/null
@@ -0,0 +1,527 @@
+package camidion.chordhelper.music;
+
+import java.util.Vector;
+import java.util.regex.Pattern;
+
+import javax.sound.midi.InvalidMidiDataException;
+import javax.sound.midi.Sequence;
+
+/**
+ * Chord Progression - コード進行のクラス
+ */
+public class ChordProgression {
+
+       public class TickRange implements Cloneable {
+               long start_tick_pos = 0, end_tick_pos = 0;
+               public TickRange( long tick_pos ) {
+                       end_tick_pos = start_tick_pos = tick_pos;
+               }
+               public TickRange( long start_tick_pos, long end_tick_pos ) {
+                       this.start_tick_pos = start_tick_pos;
+                       this.end_tick_pos = end_tick_pos;
+               }
+               protected TickRange clone() {
+                       return new TickRange( start_tick_pos, end_tick_pos );
+               }
+               public void moveForward() {
+                       start_tick_pos = end_tick_pos;
+               }
+               public void moveForward( long duration ) {
+                       start_tick_pos = end_tick_pos;
+                       end_tick_pos += duration;
+               }
+               public long duration() {
+                       return end_tick_pos - start_tick_pos;
+               }
+               public boolean contains( long tick ) {
+                       return ( tick >= start_tick_pos && tick < end_tick_pos );
+               }
+       }
+
+       class ChordStroke {
+               Chord chord; int beat_length; TickRange tick_range = null;
+               public ChordStroke(Chord chord) { this( chord, 1 ); }
+               public ChordStroke(Chord chord, int beat_length) {
+                       this.chord = chord;
+                       this.beat_length = beat_length;
+               }
+               public String toString() {
+                       String str = chord.toString();
+                       for( int i=2; i <= beat_length; i++ ) str += " %";
+                       return str;
+               }
+       }
+
+       // 時間位置付き歌詞
+       public class Lyrics {
+               String text = null;
+               Long start_tick_pos = null;
+               public Lyrics(String text) { this.text = text; }
+               public Lyrics(String text, long tick_pos) {
+                       this.text = text; start_tick_pos = tick_pos;
+               }
+               public String toString() { return text; }
+       }
+
+       class Measure extends Vector<Object> {
+               Long ticks_per_beat = null;
+               public int numberOfBeats() {
+                       int n = 0;
+                       for( Object obj : this ) {
+                               if( obj instanceof ChordStroke ) {
+                                       n += ((ChordStroke)obj).beat_length;
+                               }
+                       }
+                       return n;
+               }
+               // 小節内のコードストロークが時間的に等間隔かどうか調べる。
+               // もし等間隔の場合、テキスト出力時に % をつける必要がなくなる。
+               public boolean isEquallyDivided() {
+                       int l, l_prev = 0;
+                       for( Object obj : this ) {
+                               if( obj instanceof ChordStroke ) {
+                                       l = ((ChordStroke)obj).beat_length;
+                                       if( l_prev > 0 && l_prev != l ) {
+                                               return false;
+                                       }
+                                       l_prev = l;
+                               }
+                       }
+                       return true;
+               }
+               public int addBeat() { return addBeat(1); }
+               public int addBeat(int num_beats) {
+                       ChordStroke last_chord_stroke = null;
+                       for( Object obj : this ) {
+                               if( obj instanceof ChordStroke ) {
+                                       last_chord_stroke = (ChordStroke)obj;
+                               }
+                       }
+                       if( last_chord_stroke == null ) {
+                               return 0;
+                       }
+                       return last_chord_stroke.beat_length += num_beats;
+               }
+               public String toString() {
+                       String str = "";
+                       boolean is_eq_dev = isEquallyDivided();
+                       for( Object element : this ) {
+                               str += " ";
+                               if( element instanceof ChordStroke ) {
+                                       ChordStroke cs = (ChordStroke)element;
+                                       str += is_eq_dev ? cs.chord : cs;
+                               }
+                               else if( element instanceof Lyrics ) {
+                                       str += element.toString();
+                               }
+                       }
+                       return str;
+               }
+               public TickRange getRange() {
+                       long start_tick_pos = -1;
+                       long end_tick_pos = -1;
+                       for( Object element : this ) {
+                               if( ! (element instanceof ChordProgression.ChordStroke) )
+                                       continue;
+                               ChordProgression.ChordStroke chord_stroke
+                               = (ChordProgression.ChordStroke)element;
+                               // 小節の先頭と末尾の tick を求める
+                               if( start_tick_pos < 0 ) {
+                                       start_tick_pos = chord_stroke.tick_range.start_tick_pos;
+                               }
+                               end_tick_pos = chord_stroke.tick_range.end_tick_pos;
+                       }
+                       if( start_tick_pos < 0 || end_tick_pos < 0 ) {
+                               return null;
+                       }
+                       return new TickRange( start_tick_pos, end_tick_pos );
+               }
+               public ChordStroke chordStrokeAt( long tick ) {
+                       for( Object element : this ) {
+                               if( ! (element instanceof ChordProgression.ChordStroke) )
+                                       continue;
+                               ChordProgression.ChordStroke chord_stroke
+                               = (ChordProgression.ChordStroke)element;
+                               if( chord_stroke.tick_range.contains(tick) ) {
+                                       return chord_stroke;
+                               }
+                       }
+                       return null;
+               }
+       }
+       class Line extends Vector<Measure> {
+               public String toString() {
+                       String str = "";
+                       for( Measure measure : this ) str += measure + "|";
+                       return str;
+               }
+       }
+
+       // 内部変数
+       Vector<Line> lines = null;
+       Key key = null;
+       private Long ticks_per_measure = null;
+
+       public Key getKey() { return key; }
+       public void setKey(Key key) { this.key = key; }
+
+       public String toString() {
+               String str = "";
+               if( key != null ) str += "Key: " + key + "\n";
+               for( Line line : lines ) str += line + "\n";
+               return str;
+       }
+
+       /**
+        * デフォルトの設定でコード進行を構築します。
+        */
+       public ChordProgression() { }
+       /**
+        * 指定された小節数、キー、拍子に合わせたコード進行を構築します。
+        * コード進行の内容は、ランダムに自動生成されます。
+        * @param measureLength 小節の長さ
+        * @param timeSignatureUpper 拍子の分子
+        */
+       public ChordProgression( int measureLength, int timeSignatureUpper ) {
+               int key_co5 = (int)(Math.random() * 12) - 5;
+               key = new Key( key_co5, Key.MAJOR );
+               lines = new Vector<Line>();
+               Line line = new Line();
+               boolean is_end;
+               Chord chord, prev_chord = new Chord(new NoteSymbol(key_co5));
+               int co5_offset, prev_co5_offset;
+               double r;
+               for( int mp=0; mp<measureLength; mp++ ) {
+                       is_end = (mp == 0 || mp == measureLength - 1); // 最初または最後の小節かを覚えておく
+                       Measure measure = new Measure();
+                       ChordStroke lastChordStroke = null;
+                       for( int i=0; i<timeSignatureUpper; i++ ) {
+                               if(
+                                       i % 4 == 2 && Math.random() < 0.8
+                                       ||
+                                       i % 2 != 0 && Math.random() < 0.9
+                               ){
+                                       // もう一拍延長
+                                       lastChordStroke.beat_length++;
+                                       continue;
+                               }
+                               chord = new Chord(new NoteSymbol(key_co5));
+                               co5_offset = 0;
+                               prev_co5_offset = prev_chord.rootNoteSymbol().toCo5() - key_co5;
+                               if( ! is_end ) {
+                                       //
+                                       // 最初または最後の小節は常にトニックにする。
+                                       // 完全五度ずつ下がる進行を基本としつつ、時々そうでない進行も出現するようにする。
+                                       // サブドミナントを超えるとスケールを外れるので、超えそうになったらランダムに決め直す。
+                                       //
+                                       r = Math.random();
+                                       co5_offset = prev_co5_offset - 1;
+                                       if( co5_offset < -1 || (prev_co5_offset < 5 && r < 0.5) ) {
+                                               //
+                                               // 長7度がルートとなるコードの出現確率を半減させながらコードを決める
+                                               // (余りが6のときだけが長7度)
+                                               // なお、前回と同じコードは使わないようにする。
+                                               do {
+                                                       co5_offset = (int)(Math.random() * 13) % 7 - 1;
+                                               } while( co5_offset == prev_co5_offset );
+                                       }
+                                       int co5RootNote = key_co5 + co5_offset;
+                                       chord.setRoot(new NoteSymbol(co5RootNote));
+                                       chord.setBass(new NoteSymbol(co5RootNote));
+                               }
+                               switch( co5_offset ) {
+                               // ルート音ごとに、7th などの付加や、メジャーマイナー反転を行う確率を決める
+                               case 5: // VII
+                                       if( Math.random() < 0.5 ) {
+                                               // m7-5
+                                               chord.set(Chord.Interval.MINOR);
+                                               chord.set(Chord.Interval.FLAT5);
+                                       }
+                                       if( Math.random() < 0.8 )
+                                               chord.set(Chord.Interval.SEVENTH);
+                                       break;
+                               case 4: // Secondary dominant (III)
+                                       if( prev_co5_offset == 5 ) {
+                                               // ルートが長7度→長3度の進行のとき、反転確率を上げる。
+                                               // (ハ長調でいう Bm7-5 の次に E7 を出現しやすくする)
+                                               if( Math.random() < 0.2 ) chord.set(Chord.Interval.MINOR);
+                                       }
+                                       else {
+                                               if( Math.random() < 0.8 ) chord.set(Chord.Interval.MINOR);
+                                       }
+                                       if( Math.random() < 0.7 ) chord.set(Chord.Interval.SEVENTH);
+                                       break;
+                               case 3: // VI
+                                       if( Math.random() < 0.8 ) chord.set(Chord.Interval.MINOR);
+                                       if( Math.random() < 0.7 ) chord.set(Chord.Interval.SEVENTH);
+                                       break;
+                               case 2: // II
+                                       if( Math.random() < 0.8 ) chord.set(Chord.Interval.MINOR);
+                                       if( Math.random() < 0.7 ) chord.set(Chord.Interval.SEVENTH);
+                                       break;
+                               case 1: // Dominant (V)
+                                       if( Math.random() < 0.1 ) chord.set(Chord.Interval.MINOR);
+                                       if( Math.random() < 0.3 ) chord.set(Chord.Interval.SEVENTH);
+                                       if( Math.random() < 0.2 ) chord.set(Chord.Interval.NINTH);
+                                       break;
+                               case 0: // Tonic(ここでマイナーで終わるとさみしいので setMinorThird() はしない)
+                                       if( Math.random() < 0.2 ) chord.set(Chord.Interval.MAJOR_SEVENTH);
+                                       if( Math.random() < 0.2 ) chord.set(Chord.Interval.NINTH);
+                                       break;
+                               case -1: // Sub-dominant (IV)
+                                       if( Math.random() < 0.1 ) {
+                                               chord.set(Chord.Interval.MINOR);
+                                               if( Math.random() < 0.3 ) chord.set(Chord.Interval.SEVENTH);
+                                       }
+                                       else
+                                               if( Math.random() < 0.2 ) chord.set(Chord.Interval.MAJOR_SEVENTH);
+                                       if( Math.random() < 0.2 ) chord.set(Chord.Interval.NINTH);
+                                       break;
+                               }
+                               measure.add( lastChordStroke = new ChordStroke(chord) );
+                               prev_chord = chord;
+                       }
+                       line.add(measure);
+                       if( (mp+1) % 8 == 0 ) { // 8小節おきに改行
+                               lines.add(line);
+                               line = new Line();
+                       }
+               }
+               if( line.size() > 0 ) lines.add(line);
+       }
+       // テキストからコード進行を生成
+       public ChordProgression( String source_text ) {
+               if( source_text == null ) return;
+               Measure measure;
+               Line line;
+               String[] linesSrc, measuresSrc, elementsSrc;
+               Chord lastChord = null;
+               String keyHeaderRegex = "^Key(\\s*):(\\s*)";
+               String keyValueRegex = "[A-G]+.*$";
+               //
+               // キーであるかどうか見分けるためのパターン
+               Pattern keyMatchPattern = Pattern.compile(
+                       keyHeaderRegex + keyValueRegex,
+                       Pattern.CASE_INSENSITIVE
+               );
+               // キーのヘッダーを取り除くためのパターン
+               Pattern keyReplPattern = Pattern.compile(
+                       keyHeaderRegex, Pattern.CASE_INSENSITIVE
+               );
+               //
+               linesSrc = source_text.split("[\r\n]+");
+               lines = new Vector<Line>();
+               for( String line_src : linesSrc ) {
+                       measuresSrc = line_src.split("\\|");
+                       if( measuresSrc.length > 0 ) {
+                               String keyString = measuresSrc[0].trim();
+                               if( keyMatchPattern.matcher(keyString).matches() ) {
+                                       try {
+                                               key = new Key(keyReplPattern.matcher(keyString).replaceFirst(""));
+                                               continue;
+                                       } catch( Exception e ) {
+                                               e.printStackTrace();
+                                       }
+                               }
+                       }
+                       line = new Line();
+                       for( String measureSrc : measuresSrc ) {
+                               elementsSrc = measureSrc.split("[ \t]+");
+                               measure = new Measure();
+                               for( String elementSrc : elementsSrc ) {
+                                       if( elementSrc.isEmpty() ) continue;
+                                       if( elementSrc.equals("%") ) {
+                                               if( measure.addBeat() == 0 ) {
+                                                       measure.add( new ChordStroke(lastChord) );
+                                               }
+                                               continue;
+                                       }
+                                       try {
+                                               measure.add(new ChordStroke(lastChord = new Chord(elementSrc)));
+                                       } catch( IllegalArgumentException ex ) {
+                                               measure.add( new Lyrics(elementSrc) );
+                                       }
+                               }
+                               line.add(measure);
+                       }
+                       lines.add(line);
+               }
+       }
+
+       // Major/minor 切り替え
+       public void toggleKeyMajorMinor() {
+               key = key.relativeKey();
+       }
+
+       // コード進行の移調
+       public void transpose(int chromaticOffset) {
+               for( Line line : lines ) {
+                       for( Measure measure : line ) {
+                               for( int i=0; i<measure.size(); i++ ) {
+                                       Object element = measure.get(i);
+                                       if( element instanceof ChordStroke ) {
+                                               ChordStroke cs = (ChordStroke)element;
+                                               Chord new_chord = cs.chord.clone();
+                                               //
+                                               // キーが未設定のときは、最初のコードから推測して設定
+                                               if( key == null ) key = new Key( new_chord );
+                                               //
+                                               new_chord.transpose( chromaticOffset, key );
+                                               measure.set( i, new ChordStroke( new_chord, cs.beat_length ) );
+                                       }
+                               }
+                       }
+               }
+               key.transpose(chromaticOffset);
+       }
+       // 異名同音の♭と#を切り替える
+       public void toggleEnharmonically() {
+               if( key == null ) return;
+               int original_key_co5 = key.toCo5();
+               int co5Offset = 0;
+               if( original_key_co5 > 4 ) {
+                       co5Offset = -Music.SEMITONES_PER_OCTAVE;
+               }
+               else if( original_key_co5 < -4 ) {
+                       co5Offset = Music.SEMITONES_PER_OCTAVE;
+               }
+               else {
+                       return;
+               }
+               key.toggleEnharmonically();
+               for( Line line : lines ) {
+                       for( Measure measure : line ) {
+                               for( int i=0; i<measure.size(); i++ ) {
+                                       Object element = measure.get(i);
+                                       if( element instanceof ChordStroke ) {
+                                               ChordStroke cs = (ChordStroke)element;
+                                               Chord newChord = cs.chord.clone();
+                                               newChord.setRoot(new NoteSymbol(newChord.rootNoteSymbol().toCo5() + co5Offset));
+                                               newChord.setBass(new NoteSymbol(newChord.bassNoteSymbol().toCo5() + co5Offset));
+                                               measure.set( i, new ChordStroke( newChord, cs.beat_length ) );
+                                       }
+                               }
+                       }
+               }
+       }
+       // コード進行の中に時間軸(MIDI tick)を書き込む
+       //
+       public void setTickPositions( FirstTrackSpec first_track ) {
+               ticks_per_measure = first_track.ticks_per_measure;
+               TickRange tick_range = new TickRange(
+                               first_track.pre_measures * ticks_per_measure
+                               );
+               for( Line line : lines ) { // 行単位の処理
+                       for( Measure measure : line ) { // 小節単位の処理
+                               int n_beats = measure.numberOfBeats();
+                               if( n_beats == 0 ) continue;
+                               long tpb = measure.ticks_per_beat = ticks_per_measure / n_beats ;
+                               for( Object element : measure ) {
+                                       if( element instanceof Lyrics ) {
+                                               ((Lyrics)element).start_tick_pos = tick_range.start_tick_pos;
+                                               continue;
+                                       }
+                                       else if( element instanceof ChordStroke ) {
+                                               ChordStroke chord_stroke = (ChordStroke)element;
+                                               tick_range.moveForward( tpb * chord_stroke.beat_length );
+                                               chord_stroke.tick_range = tick_range.clone();
+                                       }
+                               }
+                       }
+               }
+       }
+       // コード文字列の書き込み
+       public void setChordSymbolTextTo( AbstractTrackSpec ts ) {
+               for( Line line : lines ) {
+                       for( Measure measure : line ) {
+                               if( measure.ticks_per_beat == null ) continue;
+                               for( Object element : measure ) {
+                                       if( element instanceof ChordStroke ) {
+                                               ts.addStringTo( 0x01, (ChordStroke)element );
+                                       }
+                               }
+                       }
+               }
+       }
+       // 歌詞の書き込み
+       public void setLyricsTo( AbstractTrackSpec ts ) {
+               for( Line line : lines ) {
+                       for( Measure measure : line ) {
+                               if( measure.ticks_per_beat == null ) continue;
+                               for( Object element : measure ) {
+                                       if( element instanceof Lyrics ) {
+                                               ts.addStringTo( 0x05, (Lyrics)element );
+                                       }
+                               }
+                       }
+               }
+       }
+       /**
+        * コード進行をもとに MIDI シーケンスを生成します。
+        * @return MIDIシーケンス
+        */
+       public Sequence toMidiSequence() {
+               return toMidiSequence(48);
+       }
+       /**
+        * 指定のタイミング解像度で、
+        * コード進行をもとに MIDI シーケンスを生成します。
+        * @return MIDIシーケンス
+        */
+       public Sequence toMidiSequence(int ppq) {
+               //
+               // PPQ = Pulse Per Quarter (TPQN = Tick Per Quearter Note)
+               //
+               return toMidiSequence( ppq, 0, 0, null, null );
+       }
+       /**
+        * 小節数、トラック仕様、コード進行をもとに MIDI シーケンスを生成します。
+        * @param ppq 分解能(pulse per quarter)
+        * @param startMeasure 開始小節位置
+        * @param endMeasure 終了小節位置
+        * @param firstTrack 最初のトラックの仕様
+        * @param trackSpecs 残りのトラックの仕様
+        * @return MIDIシーケンス
+        */
+       public Sequence toMidiSequence(
+               int ppq, int startMeasure, int endMeasure,
+               FirstTrackSpec firstTrack,
+               Vector<AbstractNoteTrackSpec> trackSpecs
+       ) {
+               Sequence seq;
+               try {
+                       seq = new Sequence(Sequence.PPQ, ppq);
+               } catch ( InvalidMidiDataException e ) {
+                       e.printStackTrace();
+                       return null;
+               }
+               // マスタートラックの生成
+               if( firstTrack == null ) {
+                       firstTrack = new FirstTrackSpec();
+               }
+               firstTrack.key = this.key;
+               firstTrack.createTrack( seq, startMeasure, endMeasure );
+               //
+               // 中身がなければここで終了
+               if( lines == null || trackSpecs == null ) return seq;
+               //
+               // コード進行の中に時間軸(MIDI tick)を書き込む
+               setTickPositions(firstTrack);
+               //
+               // コードのテキストと歌詞を書き込む
+               setChordSymbolTextTo(firstTrack);
+               setLyricsTo(firstTrack);
+               //
+               // 残りのトラックを生成
+               for( AbstractNoteTrackSpec ts : trackSpecs ) {
+                       ts.createTrack(seq, firstTrack);
+                       if( ts instanceof DrumTrackSpec ) {
+                               ((DrumTrackSpec)ts).addDrums(this);
+                       }
+                       else {
+                               ((MelodyTrackSpec)ts).addChords(this);
+                       }
+               }
+               return seq;
+       }
+}
\ No newline at end of file
diff --git a/src/camidion/chordhelper/music/DrumTrackSpec.java b/src/camidion/chordhelper/music/DrumTrackSpec.java
new file mode 100644 (file)
index 0000000..12ad3ff
--- /dev/null
@@ -0,0 +1,98 @@
+package camidion.chordhelper.music;
+
+import javax.swing.ComboBoxModel;
+import javax.swing.event.ListDataListener;
+
+public class DrumTrackSpec extends AbstractNoteTrackSpec {
+       public static int defaultPercussions[] = { // ドラムの音色リスト
+               36, // Bass Drum 1
+               44, // Pedal Hi-Hat
+               39, // Hand Clap
+               48, // Hi Mid Tom
+               50, // High Tom
+               38, // Acoustic Snare
+               62, // Mute Hi Conga
+               63, // Open Hi Conga
+       };
+       public PercussionComboBoxModel models[]
+               = new PercussionComboBoxModel[defaultPercussions.length];
+       public int[] beat_patterns = {
+               0x8888, 0x2222, 0x0008, 0x0800,
+               0, 0, 0, 0
+       };
+       public class PercussionComboBoxModel implements ComboBoxModel<String> {
+               private int noteNumber;
+               public PercussionComboBoxModel(int defaultNoteNumber) {
+                       noteNumber = defaultNoteNumber;
+               }
+               public int getSelectedNoteNo() {
+                       return noteNumber;
+               }
+               public void setSelectedNoteNo(int noteNumber) {
+                       this.noteNumber = noteNumber;
+               }
+               @Override
+               public Object getSelectedItem() {
+                       return noteNumber + ": " + MIDISpec.getPercussionName(noteNumber);
+               }
+               @Override
+               public void setSelectedItem(Object anItem) {
+                       String name = (String)anItem;
+                       int i = MIDISpec.MIN_PERCUSSION_NUMBER;
+                       for( String pname : MIDISpec.PERCUSSION_NAMES ) {
+                               if( name.equals(i + ": " + pname) ) {
+                                       noteNumber = i; return;
+                               }
+                               i++;
+                       }
+               }
+               @Override
+               public String getElementAt(int index) {
+                       return (index + MIDISpec.MIN_PERCUSSION_NUMBER) + ": "
+                                       + MIDISpec.PERCUSSION_NAMES[index];
+               }
+               @Override
+               public int getSize() {
+                       return MIDISpec.PERCUSSION_NAMES.length;
+               }
+               @Override
+               public void addListDataListener(ListDataListener l) { }
+               @Override
+               public void removeListDataListener(ListDataListener l) { }
+       }
+
+       public DrumTrackSpec(int ch, String name) {
+               super(ch,name);
+               for( int i=0; i<defaultPercussions.length; i++ ) {
+                       models[i] = new PercussionComboBoxModel(defaultPercussions[i]);
+               }
+       }
+
+       public void addDrums( ChordProgression cp ) {
+               int i;
+               long tick;
+               for( ChordProgression.Line line : cp.lines ) { // 行単位の処理
+                       for( ChordProgression.Measure measure : line ) { // 小節単位の処理
+                               if( measure.ticks_per_beat == null )
+                                       continue;
+                               ChordProgression.TickRange range = measure.getRange();
+                               int mask;
+                               for(
+                                               tick = range.start_tick_pos, mask = 0x8000;
+                                               tick < range.end_tick_pos;
+                                               tick += minNoteTicks, mask >>>= 1
+                                               ) {
+                                       for( i=0; i<beat_patterns.length; i++ ) {
+                                               if( (beat_patterns[i] & mask) == 0 )
+                                                       continue;
+                                               addNote(
+                                                               tick, tick+10,
+                                                               models[i].getSelectedNoteNo(),
+                                                               velocity
+                                                               );
+                                       }
+                               }
+                       }
+               }
+       }
+}
\ No newline at end of file
diff --git a/src/camidion/chordhelper/music/FirstTrackSpec.java b/src/camidion/chordhelper/music/FirstTrackSpec.java
new file mode 100644 (file)
index 0000000..55008a5
--- /dev/null
@@ -0,0 +1,66 @@
+package camidion.chordhelper.music;
+
+import javax.sound.midi.Sequence;
+import javax.sound.midi.Track;
+
+// 最初のトラック専用
+//
+public class FirstTrackSpec extends AbstractTrackSpec {
+       static byte default_tempo_data[] = { 0x07, (byte)0xA1, 0x20 };  // 120[QPM]
+       static byte default_timesig_data[] = { 0x04, 0x02, 0x18, 0x08 };        // 4/4
+       byte tempo_data[] = default_tempo_data;
+       byte timesig_data[] = default_timesig_data;
+       Key key = null;
+       long ticks_per_measure;
+       public FirstTrackSpec() { }
+       public FirstTrackSpec(String name) {
+               this.name = name;
+       }
+       public FirstTrackSpec(
+               String name, byte[] tempo_data, byte[] timesig_data
+       ) {
+               this.name = name;
+               if( tempo_data != null ) this.tempo_data = tempo_data;
+               if( timesig_data != null ) this.timesig_data = timesig_data;
+       }
+       public FirstTrackSpec(
+               String name, byte[] tempo_data, byte[] timesig_data, Key key
+       ) {
+               this(name,tempo_data,timesig_data);
+               this.key = key;
+       }
+       public Track createTrack(Sequence seq) {
+               return createTrack( seq, 0, 0 );
+       }
+       public Track createTrack(
+               Sequence seq, int start_measure_pos, int end_measure_pos
+       ) {
+               this.pre_measures = start_measure_pos - 1;
+               Track track = super.createTrack( seq, this );
+               addTempo(
+                       this.tempo_data = (
+                               tempo_data == null ? default_tempo_data : tempo_data
+                       ), 0
+               );
+               addTimeSignature(
+                       this.timesig_data = (
+                               timesig_data == null ? default_timesig_data : timesig_data
+                       ), 0
+               );
+               if( key != null ) addKeySignature( key, 0 );
+               ticks_per_measure = (long)(
+                       ( 4 * seq.getResolution() * this.timesig_data[0] ) >> this.timesig_data[1]
+               );
+               addEOT( end_measure_pos * ticks_per_measure );
+               return track;
+       }
+       public boolean addKeySignature( Key key, long tick_pos ) {
+               return addMetaEventTo( 0x59, key.getBytes(), tick_pos );
+       }
+       public boolean addTempo( byte data[], long tick_pos ) {
+               return addMetaEventTo( 0x51, data, tick_pos );
+       }
+       public boolean addTimeSignature( byte data[], long tick_pos ) {
+               return addMetaEventTo( 0x58, data, tick_pos );
+       }
+}
\ No newline at end of file
diff --git a/src/camidion/chordhelper/music/Key.java b/src/camidion/chordhelper/music/Key.java
new file mode 100644 (file)
index 0000000..fca48b6
--- /dev/null
@@ -0,0 +1,303 @@
+package camidion.chordhelper.music;
+
+
+/**
+ * 調(キー)を表すクラスです。
+ *
+ * <p>内部的には次の値を持っています。</p>
+ * <ul>
+ * <li>五度圏インデックス値。これは調号の♯の数(♭の数は負数)と同じです。</li>
+ * <li>メジャー/マイナーの区別(無指定ありの3値)</li>
+ * </ul>
+ * <p>これらの値はMIDIのメタメッセージにある調号のパラメータに対応します。
+ * </p>
+ */
+public class Key implements Cloneable {
+       /**
+        * メジャーかマイナーかが特定できていないことを示す値
+        */
+       public static final int MAJOR_OR_MINOR = 0;
+       /**
+        * メジャーキー(長調)
+        */
+       public static final int MAJOR = 1;
+       /**
+        * マイナーキー(短調)
+        */
+       public static final int MINOR = -1;
+       /**
+        * この調の五度圏インデックス値
+        */
+       private int co5;
+       /**
+        * メジャー・マイナーの種別
+        */
+       private int majorMinor;
+       /**
+        * 調号が空のキー(ハ長調またはイ短調)を構築します。
+        */
+       public Key() { setKey(0, MAJOR_OR_MINOR); }
+       /**
+        * 指定の五度圏インデックス値を持つ調を、
+        * メジャーとマイナーを指定せずに構築します。
+        *
+        * @param co5 五度圏インデックス値
+        */
+       public Key(int co5) { setKey(co5, MAJOR_OR_MINOR); }
+       /**
+        * 指定の五度圏インデックス値を持つ、
+        * メジャー/マイナーを指定した調を構築します。
+        *
+        * @param co5 五度圏インデックス値
+        * @param majorMinor {@link #MAJOR}、{@link #MINOR}、{@link #MAJOR_OR_MINOR} のいずれか
+        */
+       public Key(int co5, int majorMinor) {
+               setKey(co5, majorMinor);
+       }
+       /**
+        * 指定の五度圏インデックス値を持つ、
+        * メジャー/マイナーの明確な調を構築します。
+        *
+        * @param co5 五度圏インデックス値
+        * @param isMinor true:マイナー、false:メジャー
+        */
+       public Key(int co5, boolean isMinor) {
+               setKey(co5, isMinor);
+       }
+       /**
+        * MIDIの調データ(メタメッセージ2byte)から調を構築します。
+        * @param midiData MIDIの調データ
+        */
+       public Key(byte midiData[]) {
+               setBytes(midiData);
+       }
+       /**
+        * C、Am のような文字列から調を構築します。
+        * @param keySymbol 調を表す文字列
+        * @throw IllegalArgumentException 調を表す文字列が不正の場合
+        */
+       public Key(String keySymbol) throws IllegalArgumentException {
+               boolean isMinor = keySymbol.matches(".*m");
+               setKey((new NoteSymbol(keySymbol)).toCo5(isMinor), isMinor);
+       }
+       /**
+        * 指定されたコードと同名の調を構築します。
+        * @param chord コード(和音)
+        */
+       public Key(Chord chord) {
+               boolean isMinor = chord.isSet(Chord.Interval.MINOR);
+               setKey(chord.rootNoteSymbol().toCo5(isMinor), isMinor);
+       }
+       @Override
+       public Key clone() {
+               return new Key(co5, majorMinor);
+       }
+       @Override
+       public boolean equals(Object anObject) {
+               if( this == anObject )
+                       return true;
+               if( anObject instanceof Key ) {
+                       Key another = (Key) anObject;
+                       return
+                               co5 == another.toCo5() &&
+                               majorMinor == another.majorMinor() ;
+               }
+               return false;
+       }
+       @Override
+       public int hashCode() {
+               return majorMinor * 256 + co5 ;
+       }
+       private void setKey(int co5, boolean isMinor) {
+               setKey( co5, isMinor ? MINOR : MAJOR );
+       }
+       private void setKey(int co5, int majorMinor) {
+               this.co5 = co5;
+               this.majorMinor = majorMinor;
+               normalize();
+       }
+       /**
+        * MIDIの調データ(メタメッセージ2byte)を設定します。
+        * @param data MIDIの調データ
+        */
+       public void setBytes( byte[] data ) {
+               byte sharpFlat = data.length > 0 ? data[0] : 0;
+               byte isMinor = data.length > 1 ? data[1] : 0;
+               setKey( (int)sharpFlat, isMinor==1 );
+       }
+       /**
+        * MIDIの調データ(メタメッセージ2byte)を生成して返します。
+        * @return  MIDIの調データ
+        */
+       public byte[] getBytes() {
+               byte data[] = new byte[2];
+               data[0] = (byte)(co5 & 0xFF);
+               data[1] = (byte)(majorMinor == MINOR ? 1 : 0);
+               return data;
+       }
+       /**
+        * 五度圏インデックス値を返します。
+        * @return 五度圏インデックス値
+        */
+       public int toCo5() { return co5; }
+       /**
+        * メジャー/マイナーの区別を返します。
+        * @return {@link #MAJOR}、{@link #MINOR}、{@link #MAJOR_OR_MINOR} のいずれか
+        */
+       public int majorMinor() { return majorMinor; }
+       /**
+        * 相対ドの音階を返します。
+        * @return 相対ドの音階(0~11)
+        */
+       public int relativeDo() {
+               return NoteSymbol.toNoteNumber(co5);
+       }
+       /**
+        * この調のルート音を返します。
+        * メジャーキーの場合は相対ド、
+        * マイナーキーの場合は相対ラの音階です。
+        *
+        * @return キーのルート音(0~11)
+        */
+       public int rootNoteNumber() {
+               int n = relativeDo();
+               return majorMinor==MINOR ? Music.mod12(n-3) : n;
+       }
+       /**
+        * 指定されたノート番号の音が、この調のスケールの構成音か調べます。
+        * メジャーキーの場合はメジャースケール、
+        * マイナーキーの場合はナチュラルマイナースケールとして判断されます。
+        *
+        * @param noteNumber ノート番号
+        * @return 指定されたノート番号がこのキーのスケールの構成音ならtrue
+        */
+       public boolean isOnScale(int noteNumber) {
+               return Music.isOnScale(noteNumber, co5);
+       }
+       /**
+        * この調を、指定された半音オフセット値だけ移調します。
+        *
+        * @param chromaticOffset 半音オフセット値
+        * @return このオブジェクト自身(移調後)
+        */
+       public Key transpose(int chromaticOffset) {
+               co5 = Music.transposeCo5(co5, chromaticOffset);
+               return this;
+       }
+       /**
+        * この調に異名同音の調がある場合、その調に置換します。
+        * <p>例えば、♭5個(D♭メジャー)の場合は♯7個(C♯メジャー)に置換されます。
+        * 異名同音の調が存在しないキー(4♯~4♭)に対してこのメソッドを呼び出しても、
+        * 何も変化しません。
+        * </p>
+        */
+       public void toggleEnharmonically() {
+               if( co5 > 4 )
+                       co5 -= 12;
+               else if( co5 < -4 )
+                       co5 += 12;
+       }
+       /**
+        * この調を正規化します。
+        * 調が7♭~7♯の範囲に入っていない場合、
+        * その範囲に入るよう調整されます。
+        */
+       public void normalize() {
+               if( co5 < -7 || co5 > 7 ) {
+                       co5 = Music.mod12( co5 );
+                       if( co5 > 6 ) co5 -= Music.SEMITONES_PER_OCTAVE;
+               }
+       }
+       /**
+        * 平行調を生成して返します。
+        * これは元の調と同じ調号を持つ、メジャーとマイナーが異なる調です。
+        *
+        * <p>メジャーとマイナーの区別が不明な場合、クローンを生成したのと同じことになります。
+        * </p>
+        *
+        * @return 平行調
+        */
+       public Key relativeKey() {
+               return new Key(co5, majorMinor * (-1));
+       }
+       /**
+        * 同主調を生成して返します。
+        * これは元の調とルート音が同じで、メジャーとマイナーが異なる調です。
+        *
+        * <p>メジャーとマイナーの区別が不明な場合、クローンを生成したのと同じことになります。
+        * 元の調の♭、♯の数が5個以上の場合、
+        * 7♭~7♯の範囲をはみ出すことがあります(正規化は行われません)。
+        * </p>
+        *
+        * @return 同主調
+        */
+       public Key parallelKey() {
+               switch( majorMinor ) {
+               case MAJOR: return new Key( co5-3, MINOR );
+               case MINOR: return new Key( co5+3, MAJOR );
+               default: return new Key(co5);
+               }
+       }
+       /**
+        * 五度圏で真裏にあたる調を生成して返します。
+        * @return 五度圏で真裏にあたる調
+        */
+       public Key oppositeKey() {
+               return new Key(Music.oppositeCo5(co5), majorMinor);
+       }
+       /**
+        * この調の文字列表現を C、Am のような形式で返します。
+        * @return この調の文字列表現
+        */
+       @Override
+       public String toString() {
+               return toStringIn(SymbolLanguage.SYMBOL);
+       }
+       /**
+        * この調の文字列表現を、指定された形式で返します。
+        * @return この調の文字列表現
+        */
+       public String toStringIn(SymbolLanguage language) {
+               NoteSymbol note = new NoteSymbol(co5);
+               String major = note.toStringIn(language, false) + language.major;
+               if( majorMinor > 0 ) {
+                       return major;
+               }
+               else {
+                       String minor = note.toStringIn(language, true) + language.minor;
+                       return majorMinor < 0 ?
+                               minor : major + language.majorMinorDelimiter + minor ;
+               }
+       }
+       /**
+        * 調号を表す半角文字列を返します。
+        * 正規化された状態において最大2文字になるよう調整されます。
+        *
+        * @return 調号を表す半角文字列
+        */
+       public String signature() {
+               switch(co5) {
+               case  0: return "==";
+               case  1: return "#";
+               case -1: return "b";
+               case  2: return "##";
+               case -2: return "bb";
+               default:
+                       if( co5 >= 3 && co5 <= 7 ) return co5 + "#" ;
+                       else if( co5 <= -3 && co5 >= -7 ) return (-co5) + "b" ;
+                       return "";
+               }
+       }
+       /**
+        * 調号の説明(英語)を返します。
+        * @return 調号の説明
+        */
+       public String signatureDescription() {
+               switch(co5) {
+               case  0: return "no sharps or flats";
+               case  1: return "1 sharp";
+               case -1: return "1 flat";
+               default: return co5 < 0 ? (-co5) + " flats" : co5 + " sharps" ;
+               }
+       }
+}
\ No newline at end of file
diff --git a/src/camidion/chordhelper/music/MIDISpec.java b/src/camidion/chordhelper/music/MIDISpec.java
new file mode 100644 (file)
index 0000000..071dc69
--- /dev/null
@@ -0,0 +1,1115 @@
+package camidion.chordhelper.music;
+import java.nio.charset.Charset;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.sound.midi.InvalidMidiDataException;
+import javax.sound.midi.MetaMessage;
+import javax.sound.midi.MidiEvent;
+import javax.sound.midi.MidiMessage;
+import javax.sound.midi.Sequence;
+import javax.sound.midi.ShortMessage;
+import javax.sound.midi.SysexMessage;
+import javax.sound.midi.Track;
+/**
+ * MIDI仕様(システムエクスクルーシブ含む)
+ */
+public class MIDISpec {
+       public static final int MAX_CHANNELS = 16;
+       public static final int PITCH_BEND_NONE = 8192;
+       /**
+        * メタメッセージタイプ名マップ
+        */
+       private static final Map<Integer,String>
+               META_MESSAGE_TYPE_NAMES = new HashMap<Integer,String>() {
+                       {
+                               put(0x00, "Seq Number");
+                               put(0x01, "Text");
+                               put(0x02, "Copyright");
+                               put(0x03, "Seq/Track Name");
+                               put(0x04, "Instrument Name");
+                               put(0x05, "Lyric");
+                               put(0x06, "Marker");
+                               put(0x07, "Cue Point");
+                               put(0x08, "Program Name");
+                               put(0x09, "Device Name");
+                               put(0x20, "MIDI Ch.Prefix");
+                               put(0x21, "MIDI Output Port");
+                               put(0x2F, "End Of Track");
+                               put(0x51, "Tempo");
+                               put(0x54, "SMPTE Offset");
+                               put(0x58, "Time Signature");
+                               put(0x59, "Key Signature");
+                               put(0x7F, "Sequencer Specific");
+                       }
+               };
+       /**
+        * メタメッセージタイプの名前を返します。
+        * @param metaMessageType メタメッセージタイプ
+        * @return メタメッセージタイプの名前
+        */
+       public static String getMetaName(int metaMessageType) {
+               return META_MESSAGE_TYPE_NAMES.get(metaMessageType);
+       }
+       /**
+        * メタメッセージタイプがテキストのつくものか調べます。
+        * @param metaMessageType メタメッセージタイプ
+        * @return テキストがつくときtrue
+        */
+       public static boolean hasMetaText(int metaMessageType) {
+               return (metaMessageType > 0 && metaMessageType < 10);
+       }
+       /**
+        * メタメッセージタイプが拍子記号か調べます。
+        * @param metaMessageType メタメッセージタイプ
+        * @return 拍子記号ならtrue
+        */
+       public static boolean isTimeSignature(int metaMessageType) {
+               return metaMessageType == 0x58;
+       }
+       /**
+        * MIDIメッセージが拍子記号か調べます。
+        * @param msg MIDIメッセージ
+        * @return 拍子記号ならtrue
+        */
+       public static boolean isTimeSignature(MidiMessage midiMessage) {
+               if ( !(midiMessage instanceof MetaMessage) )
+                       return false;
+               return isTimeSignature( ((MetaMessage)midiMessage).getType() );
+       }
+       /**
+        * メタメッセージタイプが EOT (End Of Track) か調べます。
+        * @param metaMessageType メタメッセージタイプ
+        * @return EOTならtrue
+        */
+       public static boolean isEOT(int metaMessageType) {
+               return metaMessageType == 0x2F;
+       }
+       /**
+        * MIDIメッセージが EOT (End Of Track) か調べます。
+        * @param midiMessage MIDIメッセージ
+        * @return EOTならtrue
+        */
+       public static boolean isEOT(MidiMessage midiMessage) {
+               if ( !(midiMessage instanceof MetaMessage) )
+                       return false;
+               return isEOT( ((MetaMessage)midiMessage).getType() );
+       }
+       /**
+        * 1分のマイクロ秒数
+        */
+       public static final int MICROSECOND_PER_MINUTE = (60 * 1000 * 1000);
+       /**
+        * MIDIのテンポメッセージについているバイト列をQPM単位のテンポに変換します。
+        * @param b バイト列
+        * @return テンポ[QPM]
+        */
+       public static int byteArrayToQpmTempo(byte[] b) {
+               int tempoInUsPerQuarter
+                       = ((b[0] & 0xFF) << 16) | ((b[1] & 0xFF) << 8) | (b[2] & 0xFF);
+               return MICROSECOND_PER_MINUTE / tempoInUsPerQuarter;
+       }
+       /**
+        * QPM単位のテンポをMIDIのテンポメッセージ用バイト列に変換します。
+        * @param qpm テンポ[QPM]
+        * @return MIDIのテンポメッセージ用バイト列
+        */
+       public static byte[] qpmTempoToByteArray(int qpm) {
+               int tempoInUsPerQuarter = MICROSECOND_PER_MINUTE / qpm;
+               byte[] b = new byte[3];
+               b[0] = (byte)((tempoInUsPerQuarter >> 16) & 0xFF);
+               b[1] = (byte)((tempoInUsPerQuarter >> 8) & 0xFF);
+               b[2] = (byte)(tempoInUsPerQuarter & 0xFF);
+               return b;
+       }
+       /**
+        * トラック名のバイト列を返します。
+        * @param track MIDIトラック
+        * @return トラック名のバイト列
+        */
+       public static byte[] getNameBytesOf(Track track) {
+               MidiEvent midiEvent;
+               MidiMessage message;
+               MetaMessage metaMessage;
+               for( int i=0; i<track.size(); i++ ) {
+                       midiEvent = track.get(i);
+                       if( midiEvent.getTick() > 0 ) { // No more event at top, try next track
+                               break;
+                       }
+                       message = midiEvent.getMessage();
+                       if( ! (message instanceof MetaMessage) ) { // Not meta message
+                               continue;
+                       }
+                       metaMessage = (MetaMessage)message;
+                       if( metaMessage.getType() != 0x03 ) { // Not sequence name
+                               continue;
+                       }
+                       return metaMessage.getData();
+               }
+               return null;
+       }
+       /**
+        * トラック名のバイト列を設定します。
+        * @param track MIDIトラック
+        * @param name トラック名
+        * @return 成功:true、失敗:false
+        */
+       public static boolean setNameBytesOf(Track track, byte[] name) {
+               MidiEvent midiEvent = null;
+               MidiMessage msg = null;
+               MetaMessage metaMsg = null;
+               for( int i=0; i<track.size(); i++ ) {
+                       if(
+                               (midiEvent = track.get(i)).getTick() > 0
+                               ||
+                               (msg = midiEvent.getMessage()) instanceof MetaMessage
+                               &&
+                               (metaMsg = (MetaMessage)msg).getType() == 0x03
+                       ) {
+                               break;
+                       }
+                       metaMsg = null;
+               }
+               if( metaMsg == null ) {
+                       if( name.length == 0 ) return false;
+                       track.add(new MidiEvent(
+                               (MidiMessage)(metaMsg = new MetaMessage()), 0
+                       ));
+               }
+               try {
+                       metaMsg.setMessage(0x03, name, name.length);
+               }
+               catch( InvalidMidiDataException e ) {
+                       e.printStackTrace();
+                       return false;
+               }
+               return true;
+       }
+       /**
+        * シーケンス名のバイト列を返します。
+        * <p>トラック名の入った最初のトラックにあるトラック名を
+        * シーケンス名として返します。
+        * </p>
+        * @param seq MIDIシーケンス
+        * @return シーケンス名のバイト列
+        */
+       public static byte[] getNameBytesOf(Sequence seq) {
+               // Returns name of the MIDI sequence.
+               // A sequence name is placed at top of first track of the sequence.
+               //
+               Track tracks[] = seq.getTracks();
+               byte b[];
+               for( Track track : tracks )
+                       if( (b = getNameBytesOf(track)) != null ) return b;
+               return null;
+       }
+       /**
+        * シーケンス名のバイト列を設定します。
+        * <p>先頭のトラックに設定されます。
+        * 設定に失敗した場合、順に次のトラックへの設定を試みます。
+        * </p>
+        *
+        * @param seq MIDIシーケンス
+        * @param name シーケンス名のバイト列
+        * @return 成功:true、失敗:false
+        */
+       public static boolean setNameBytesOf(Sequence seq, byte[] name) {
+               Track tracks[] = seq.getTracks();
+               for( Track track : tracks )
+                       if( setNameBytesOf(track,name) ) return true;
+               return false;
+       }
+       ///////////////////////////////////////////////////////////////////
+       //
+       // Channel Message / System Message
+       //
+       /**
+        * MIDIステータス名を返します。
+        * @param status MIDIステータス
+        * @return MIDIステータス名
+        */
+       public static String getStatusName( int status ) {
+               if( status < 0x80 ) {
+                       // No such status
+                       return null;
+               }
+               else if ( status < 0xF0 ) {
+                       // Channel Message
+                       return ch_msg_status_names[ (status >> 4) - 0x08 ];
+               }
+               else if ( status <= 0xFF ) {
+                       // System Message
+                       return sys_msg_names[ status - 0xF0 ];
+               }
+               return null;
+       }
+       /**
+        * 指定のMIDIショートメッセージがチャンネルメッセージかどうか調べます。
+        * @param msg MIDIメッセージ
+        * @return MIDIチャンネルメッセージの場合true
+        */
+       public static boolean isChannelMessage( ShortMessage msg ) {
+               return isChannelMessage( msg.getStatus() );
+       }
+       /**
+        * MIDIステータスがチャンネルメッセージかどうか調べます。
+        * @param status MIDIステータス
+        * @return MIDIチャンネルメッセージの場合true
+        */
+       public static boolean isChannelMessage( int status ) {
+               return ( status < 0xF0 && status >= 0x80 );
+       }
+       private static final String ch_msg_status_names[] = {
+               // 0x80 - 0xE0 : Channel Voice Message
+               // 0xB0 : Channel Mode Message
+               "NoteOFF", "NoteON",
+               "Polyphonic Key Pressure", "Ctrl/Mode",
+               "Program", "Ch.Pressure", "Pitch Bend"
+       };
+       private static final String sys_msg_names[] = {
+               // 0xF0 : System Exclusive
+               "SysEx",
+               //
+               // 0xF1 - 0xF7 : System Common Message
+               "MIDI Time Code Quarter Frame",
+               "Song Position Pointer", "Song Select",
+               null, null, "Tune Request", "Special SysEx",
+               //
+               // 0xF8 - 0xFF : System Realtime Message
+               // 0xFF : Meta Message (SMF only, Not for wired MIDI message)
+               "Timing Clock", null, "Start", "Continue",
+               "Stop", null, "Active Sensing", "Meta / Sys.Reset",
+       };
+       ///////////////////////////////////////////////////////////////////
+       //
+       // Control Change / Channel Mode Message
+       //
+       /**
+        * コントロールチェンジの名前を返します。
+        * @param controllerNumber コントローラ番号
+        * @return コントロールチェンジの名前
+        */
+       public static String getControllerName( int controllerNumber ) {
+               if( controllerNumber < 0x00 ) {
+                       return null;
+               }
+               else if( controllerNumber < 0x20 ) {
+                       String s = controllerNames0[controllerNumber];
+                       if( s != null ) s += " (MSB)";
+                       return s;
+               }
+               else if( controllerNumber < 0x40 ) {
+                       String s = controllerNames0[controllerNumber - 0x20];
+                       if( s != null ) s += " (LSB)";
+                       return s;
+               }
+               else if( controllerNumber < 0x78 ) {
+                       return controllerMomentarySwitchNames[controllerNumber - 0x40];
+               }
+               else if( controllerNumber < 0x80 ) {
+                       return controllerModeMessageNames[controllerNumber - 0x78];
+               }
+               else {
+                       return null;
+               }
+       }
+       private static final String controllerNames0[] = {
+               //
+               // 0x00-0x0F (MSB)
+               "Bank Select", "Modulation Depth", "Breath Controller", null,
+               "Foot Controller", "Portamento Time", "Data Entry", "Volume",
+               "Balance", null, "Pan", "Expression",
+               "Effect Control 1", "Effect Control 2", null, null,
+               //
+               // 0x10-0x1F (MSB)
+               "General Purpose 1", "General Purpose 2",
+               "General Purpose 3", "General Purpose 4",
+               null, null, null, null,
+               null, null, null, null,
+               null, null, null, null,
+               //
+               // 0x20-0x3F (LSB)
+       };
+       private static final String controllerMomentarySwitchNames[] = {
+               //
+               // 0x40-0x4F
+               "Damper Pedal (Sustain)", "Portamento",
+               "Sustenuto", "Soft Pedal",
+               "Legato Footswitch", "Hold 2",
+               "Sound Controller 1 (Sound Variation)",
+               "Sound Controller 2 (Timbre/Harmonic Intens)",
+               "Sound Controller 3 (Release Time)",
+               "Sound Controller 4 (Attack Time)",
+               "Sound Controller 5 (Brightness)",
+               "Sound Controller 6 (Decay Time)",
+               "Sound Controller 7 (Vibrato Rate)",
+               "Sound Controller 8 (Vibrato Depth)",
+               "Sound Controller 9 (Vibrato Delay)",
+               "Sound Controller 10 (Undefined)",
+               //
+               // 0x50-0x5F
+               "General Purpose 5", "General Purpose 6 (Temp Change)",
+               "General Purpose 7", "General Purpose 8",
+               "Portamento Control", null, null, null,
+               null, null, null, "Reverb (Ext.Effects Depth)",
+               "Tremelo Depth", "Chorus Depth",
+               "Celeste (Detune) Depth", "Phaser Depth",
+               //
+               // 0x60-0x6F
+               "Data Increment", "Data Decrement",
+               "NRPN (LSB)", "NRPN (MSB)",
+               "RPN (LSB)", "RPN (MSB)", null, null,
+               null, null, null, null,
+               null, null, null, null,
+               //
+               // 0x70-0x77
+               null, null, null, null,
+               null, null, null, null
+       };
+       private static final String controllerModeMessageNames[] = {
+               // 0x78-0x7F
+               "All Sound OFF", "Reset All Controllers",
+               "Local Control", "All Notes OFF",
+               "Omni Mode OFF", "Omni Mode ON",
+               "Mono Mode ON", "Poly Mode ON"
+       };
+       ///////////////////////////////////////////////////////////////////
+       //
+       // System Exclusive
+       //
+       /**
+        * システムエクスクルーシブの製造者IDをキーにして製造者名を返すマップ
+        */
+       public static final Map<Integer,String>
+               SYSEX_MANUFACTURER_NAMES = new HashMap<Integer,String>() {
+                       {
+                               put(0x40,"KAWAI");
+                               put(0x41,"Roland");
+                               put(0x42,"KORG");
+                               put(0x43,"YAMAHA");
+                               put(0x44,"CASIO");
+                               put(0x7D,"Non-Commercial");
+                               put(0x7E,"Universal: Non-RealTime");
+                               put(0x7F,"Universal: RealTime");
+                       }
+               };
+       /**
+        * MIDIノート番号の最大値
+        */
+       public static final int MAX_NOTE_NO = 127;
+       /**
+        * General MIDI の楽器ファミリー名の配列
+        */
+       public static final String instrumentFamilyNames[] = {
+
+               "Piano",
+               "Chrom.Percussion",
+               "Organ",
+               "Guitar",
+               "Bass",
+               "Strings",
+               "Ensemble",
+               "Brass",
+
+               "Reed",
+               "Pipe",
+               "Synth Lead",
+               "Synth Pad",
+               "Synth Effects",
+               "Ethnic",
+               "Percussive",
+               "Sound Effects",
+       };
+       /**
+        * General MIDI の楽器名(プログラムチェンジのプログラム名)の配列
+        */
+       public static final String instrumentNames[] = {
+               "Acoustic Grand Piano",
+               "Bright Acoustic Piano",
+               "Electric Grand Piano",
+               "Honky-tonk Piano",
+               "Electric Piano 1",
+               "Electric Piano 2",
+               "Harpsichord",
+               "Clavi",
+               "Celesta",
+               "Glockenspiel",
+               "Music Box",
+               "Vibraphone",
+               "Marimba",
+               "Xylophone",
+               "Tubular Bells",
+               "Dulcimer",
+               "Drawbar Organ",
+               "Percussive Organ",
+               "Rock Organ",
+               "Church Organ",
+               "Reed Organ",
+               "Accordion",
+               "Harmonica",
+               "Tango Accordion",
+               "Acoustic Guitar (nylon)",
+               "Acoustic Guitar (steel)",
+               "Electric Guitar (jazz)",
+               "Electric Guitar (clean)",
+               "Electric Guitar (muted)",
+               "Overdriven Guitar",
+               "Distortion Guitar",
+               "Guitar harmonics",
+               "Acoustic Bass",
+               "Electric Bass (finger)",
+               "Electric Bass (pick)",
+               "Fretless Bass",
+               "Slap Bass 1",
+               "Slap Bass 2",
+               "Synth Bass 1",
+               "Synth Bass 2",
+               "Violin",
+               "Viola",
+               "Cello",
+               "Contrabass",
+               "Tremolo Strings",
+               "Pizzicato Strings",
+               "Orchestral Harp",
+               "Timpani",
+               "String Ensemble 1",
+               "String Ensemble 2",
+               "SynthStrings 1",
+               "SynthStrings 2",
+               "Choir Aahs",
+               "Voice Oohs",
+               "Synth Voice",
+               "Orchestra Hit",
+               "Trumpet",
+               "Trombone",
+               "Tuba",
+               "Muted Trumpet",
+               "French Horn",
+               "Brass Section",
+               "SynthBrass 1",
+               "SynthBrass 2",
+               "Soprano Sax",
+               "Alto Sax",
+               "Tenor Sax",
+               "Baritone Sax",
+               "Oboe",
+               "English Horn",
+               "Bassoon",
+               "Clarinet",
+               "Piccolo",
+               "Flute",
+               "Recorder",
+               "Pan Flute",
+               "Blown Bottle",
+               "Shakuhachi",
+               "Whistle",
+               "Ocarina",
+               "Lead 1 (square)",
+               "Lead 2 (sawtooth)",
+               "Lead 3 (calliope)",
+               "Lead 4 (chiff)",
+               "Lead 5 (charang)",
+               "Lead 6 (voice)",
+               "Lead 7 (fifths)",
+               "Lead 8 (bass + lead)",
+               "Pad 1 (new age)",
+               "Pad 2 (warm)",
+               "Pad 3 (polysynth)",
+               "Pad 4 (choir)",
+               "Pad 5 (bowed)",
+               "Pad 6 (metallic)",
+               "Pad 7 (halo)",
+               "Pad 8 (sweep)",
+               "FX 1 (rain)",
+               "FX 2 (soundtrack)",
+               "FX 3 (crystal)",
+               "FX 4 (atmosphere)",
+               "FX 5 (brightness)",
+               "FX 6 (goblins)",
+               "FX 7 (echoes)",
+               "FX 8 (sci-fi)",
+               "Sitar",
+               "Banjo",
+               "Shamisen",
+               "Koto",
+               "Kalimba",
+               "Bag pipe",
+               "Fiddle",
+               "Shanai",
+               "Tinkle Bell",
+               "Agogo",
+               "Steel Drums",
+               "Woodblock",
+               "Taiko Drum",
+               "Melodic Tom",
+               "Synth Drum",
+               "Reverse Cymbal",
+               "Guitar Fret Noise",
+               "Breath Noise",
+               "Seashore",
+               "Bird Tweet",
+               "Telephone Ring",
+               "Helicopter",
+               "Applause",
+               "Gunshot",
+       };
+       /**
+        * パーカッション用MIDIノート番号の最小値
+        */
+       public static final int MIN_PERCUSSION_NUMBER = 35;
+       /**
+        * パーカッション用のMIDIチャンネル(通常はCH.10)における
+        * ノート番号からパーカッション名を返します。
+        *
+        * @param note_no ノート番号
+        * @return パーカッション名
+        */
+       public static String getPercussionName(int note_no) {
+               int i = note_no - MIN_PERCUSSION_NUMBER ;
+               return i>=0 && i < PERCUSSION_NAMES.length ? PERCUSSION_NAMES[i] : "(Unknown)" ;
+       }
+       public static final String      PERCUSSION_NAMES[] = {
+               "Acoustic Bass Drum",
+               "Bass Drum 1",
+               "Side Stick",
+               "Acoustic Snare",
+               "Hand Clap",
+               "Electric Snare",
+               "Low Floor Tom",
+               "Closed Hi Hat",
+               "High Floor Tom",
+               "Pedal Hi-Hat",
+               "Low Tom",
+               "Open Hi-Hat",
+               "Low-Mid Tom",
+               "Hi Mid Tom",
+               "Crash Cymbal 1",
+               "High Tom",
+               "Ride Cymbal 1",
+               "Chinese Cymbal",
+               "Ride Bell",
+               "Tambourine",
+               "Splash Cymbal",
+               "Cowbell",
+               "Crash Cymbal 2",
+               "Vibraslap",
+               "Ride Cymbal 2",
+               "Hi Bongo",
+               "Low Bongo",
+               "Mute Hi Conga",
+               "Open Hi Conga",
+               "Low Conga",
+               "High Timbale",
+               "Low Timbale",
+               "High Agogo",
+               "Low Agogo",
+               "Cabasa",
+               "Maracas",
+               "Short Whistle",
+               "Long Whistle",
+               "Short Guiro",
+               "Long Guiro",
+               "Claves",
+               "Hi Wood Block",
+               "Low Wood Block",
+               "Mute Cuica",
+               "Open Cuica",
+               "Mute Triangle",
+               "Open Triangle",
+       };
+       public static final String nsx39LyricElements[] = {
+               "あ","い","う","え","お",
+               "か","き","く","け","こ",
+               "が","ぎ","ぐ","げ","ご",
+           "きゃ","きゅ","きょ",
+           "ぎゃ","ぎゅ","ぎょ",
+               "さ","すぃ","す","せ","そ",
+               "ざ","ずぃ","ず","ぜ","ぞ",
+           "しゃ","し","しゅ","しぇ","しょ",
+           "じゃ","じ","じゅ","じぇ","じょ",
+               "た","てぃ","とぅ","て","と",
+               "だ","でぃ","どぅ","で","ど",
+               "てゅ","でゅ",
+               "ちゃ","ち","ちゅ","ちぇ","ちょ",
+               "つぁ","つぃ","つ","つぇ","つぉ",
+               "な","に","ぬ","ね","の",
+           "にゃ","にゅ","にょ",
+               "は","ひ","ふ","へ","ほ",
+               "ば","び","ぶ","べ","ぼ",
+               "ぱ","ぴ","ぷ","ぺ","ぽ",
+               "ひゃ","ひゅ","ひょ",
+               "びゃ","びゅ","びょ",
+               "ぴゃ","ぴゅ","ぴょ",
+               "ふぁ","ふぃ","ふゅ","ふぇ","ふぉ",
+               "ま","み","む","め","も",
+               "みゃ","みゅ","みょ",
+               "や","ゆ","よ",
+               "ら","り","る","れ","ろ",
+               "りゃ","りゅ","りょ",
+               "わ","うぃ","うぇ","を",
+               "ん","ん","ん","ん","ん",
+       };
+       /**
+        * MIDIメッセージの内容を文字列で返します。
+        * @param msg MIDIメッセージ
+        * @param charset MIDIメタメッセージに含まれるテキストデータの文字コード
+        * @return MIDIメッセージの内容を表す文字列
+        */
+       public static String msgToString(MidiMessage msg, Charset charset) {
+               String str = "";
+               if( msg instanceof ShortMessage ) {
+                       ShortMessage shortmsg = (ShortMessage)msg;
+                       int status = msg.getStatus();
+                       String statusName = getStatusName(status);
+                       int data1 = shortmsg.getData1();
+                       int data2 = shortmsg.getData2();
+                       if( isChannelMessage(status) ) {
+                               int channel = shortmsg.getChannel();
+                               String channelPrefix = "Ch."+(channel+1) + ": ";
+                               String statusPrefix = (
+                                       statusName == null ? String.format("status=0x%02X",status) : statusName
+                               ) + ": ";
+                               int cmd = shortmsg.getCommand();
+                               switch( cmd ) {
+                               case ShortMessage.NOTE_OFF:
+                               case ShortMessage.NOTE_ON:
+                                       str += channelPrefix + statusPrefix + data1;
+                                       str += ":[";
+                                       if( MIDISpec.isRhythmPart(channel) ) {
+                                               str += getPercussionName(data1);
+                                       }
+                                       else {
+                                               str += NoteSymbol.noteNoToSymbol(data1);
+                                       }
+                                       str +="] Velocity=" + data2;
+                                       break;
+                               case ShortMessage.POLY_PRESSURE:
+                                       str += channelPrefix + statusPrefix + "Note=" + data1 + " Pressure=" + data2;
+                                       break;
+                               case ShortMessage.PROGRAM_CHANGE:
+                                       str += channelPrefix + statusPrefix + data1 + ":[" + instrumentNames[data1] + "]";
+                                       if( data2 != 0 ) str += " data2=" + data2;
+                                       break;
+                               case ShortMessage.CHANNEL_PRESSURE:
+                                       str += channelPrefix + statusPrefix + data1;
+                                       if( data2 != 0 ) str += " data2=" + data2;
+                                       break;
+                               case ShortMessage.PITCH_BEND:
+                               {
+                                       int val = ((data1 & 0x7F) | ((data2 & 0x7F) << 7));
+                                       str += channelPrefix + statusPrefix + ( (val-8192) * 100 / 8191) + "% (" + val + ")";
+                               }
+                               break;
+                               case ShortMessage.CONTROL_CHANGE:
+                               {
+                                       // Control / Mode message name
+                                       String ctrl_name = getControllerName(data1);
+                                       str += channelPrefix + (data1 < 0x78 ? "CtrlChg: " : "ModeMsg: ");
+                                       if( ctrl_name == null ) {
+                                               str += " No.=" + data1 + " Value=" + data2;
+                                               return str;
+                                       }
+                                       str += ctrl_name;
+                                       //
+                                       // Controller's value
+                                       switch( data1 ) {
+                                       case 0x40: case 0x41: case 0x42: case 0x43: case 0x45:
+                                               str += " " + ( data2==0x3F?"OFF":data2==0x40?"ON":data2 );
+                                               break;
+                                       case 0x44: // Legato Footswitch
+                                               str += " " + ( data2==0x3F?"Normal":data2==0x40?"Legato":data2 );
+                                               break;
+                                       case 0x7A: // Local Control
+                                               str += " " + ( data2==0x00?"OFF":data2==0x7F?"ON":data2 );
+                                               break;
+                                       default:
+                                               str += " " + data2;
+                                               break;
+                                       }
+                               }
+                               break;
+
+                               default:
+                                       // Never reached here
+                                       break;
+                               }
+                       }
+                       else { // System Message
+                               str += (statusName == null ? ("status="+status) : statusName );
+                               str += " (" + data1 + "," + data2 + ")";
+                       }
+                       return str;
+               }
+               else if( msg instanceof MetaMessage ) {
+                       MetaMessage metamsg = (MetaMessage)msg;
+                       byte[] msgdata = metamsg.getData();
+                       int msgtype = metamsg.getType();
+                       str += "Meta: ";
+                       String meta_name = getMetaName(msgtype);
+                       if( meta_name == null ) {
+                               str += "Unknown MessageType="+msgtype + " Values=(";
+                               for( byte b : msgdata ) str += String.format( " %02X", b );
+                               str += " )";
+                               return str;
+                       }
+                       // Add the message type name
+                       str += meta_name;
+                       //
+                       // Add the text data
+                       if( hasMetaText(msgtype) ) {
+                               str +=" ["+(new String(msgdata,charset))+"]";
+                               return str;
+                       }
+                       // Add the numeric data
+                       switch(msgtype) {
+                       case 0x00: // Sequence Number (for MIDI Format 2)
+                               if( msgdata.length == 2 ) {
+                                       str += String.format(
+                                               ": %04X",
+                                               ((msgdata[0] & 0xFF) << 8) | (msgdata[1] & 0xFF)
+                                       );
+                                       break;
+                               }
+                               str += ": Size not 2 byte : data=(";
+                               for( byte b : msgdata ) str += String.format( " %02X", b );
+                               str += " )";
+                               break;
+                       case 0x20: // MIDI Ch.Prefix
+                       case 0x21: // MIDI Output Port
+                               if( msgdata.length == 1 ) {
+                                       str += String.format( ": %02X", msgdata[0] & 0xFF );
+                                       break;
+                               }
+                               str += ": Size not 1 byte : data=(";
+                               for( byte b : msgdata ) str += String.format( " %02X", b );
+                               str += " )";
+                               break;
+                       case 0x51: // Tempo
+                               str += ": " + byteArrayToQpmTempo( msgdata ) + "[QPM] (";
+                               for( byte b : msgdata ) str += String.format( " %02X", b );
+                               str += " )";
+                               break;
+                       case 0x54: // SMPTE Offset
+                               if( msgdata.length == 5 ) {
+                                       str += ": "
+                                               + (msgdata[0] & 0xFF) + ":"
+                                               + (msgdata[1] & 0xFF) + ":"
+                                               + (msgdata[2] & 0xFF) + "."
+                                               + (msgdata[3] & 0xFF) + "."
+                                               + (msgdata[4] & 0xFF);
+                                       break;
+                               }
+                               str += ": Size not 5 byte : data=(";
+                               for( byte b : msgdata ) str += String.format( " %02X", b );
+                               str += " )";
+                               break;
+                       case 0x58: // Time Signature
+                               if( msgdata.length == 4 ) {
+                                       str +=": " + msgdata[0] + "/" + (1 << msgdata[1]);
+                                       str +=", "+msgdata[2]+"[clk/beat], "+msgdata[3]+"[32nds/24clk]";
+                                       break;
+                               }
+                               str += ": Size not 4 byte : data=(";
+                               for( byte b : msgdata ) str += String.format( " %02X", b );
+                               str += " )";
+                               break;
+                       case 0x59: // Key Signature
+                               if( msgdata.length == 2 ) {
+                                       Key key = new Key(msgdata);
+                                       str += ": " + key.signatureDescription();
+                                       str += " (" + key.toStringIn(SymbolLanguage.NAME) + ")";
+                                       break;
+                               }
+                               str += ": Size not 2 byte : data=(";
+                               for( byte b : msgdata ) str += String.format( " %02X", b );
+                               str += " )";
+                               break;
+                       case 0x7F: // Sequencer Specific Meta Event
+                               str += " (";
+                               for( byte b : msgdata ) str += String.format( " %02X", b );
+                               str += " )";
+                               break;
+                       }
+                       return str;
+               }
+               else if( msg instanceof SysexMessage ) {
+                       SysexMessage sysexmsg = (SysexMessage)msg;
+                       int status = sysexmsg.getStatus();
+                       byte[] msgdata = sysexmsg.getData();
+                       int dataBytePos = 1;
+                       switch( status ) {
+                       case SysexMessage.SYSTEM_EXCLUSIVE:
+                               str += "SysEx: ";
+                               break;
+                       case SysexMessage.SPECIAL_SYSTEM_EXCLUSIVE:
+                               str += "SysEx(Special): ";
+                               break;
+                       default:
+                               str += "SysEx: Invalid (status="+status+") ";
+                               break;
+                       }
+                       if( msgdata.length < 1 ) {
+                               str += " Invalid data size: " + msgdata.length;
+                               return str;
+                       }
+                       int manufacturerId = (int)(msgdata[0] & 0xFF);
+                       int deviceId = (int)(msgdata[1] & 0xFF);
+                       int modelId = (int)(msgdata[2] & 0xFF);
+                       String manufacturerName = SYSEX_MANUFACTURER_NAMES.get(manufacturerId);
+                       if( manufacturerName == null ) {
+                               manufacturerName = String.format("[Manufacturer code %02X]", msgdata[0]);
+                       }
+                       str += manufacturerName + String.format(" (DevID=0x%02X)", deviceId);
+                       switch( manufacturerId ) {
+                       case 0x7E: // Non-Realtime Universal
+                               dataBytePos++;
+                               int sub_id_1 = modelId;
+                               int sub_id_2 = (int)(msgdata[3] & 0xFF);
+                               switch( sub_id_1 ) {
+                               case 0x09: // General MIDI (GM)
+                                       switch( sub_id_2 ) {
+                                       case 0x01: str += " GM System ON"; return str;
+                                       case 0x02: str += " GM System OFF"; return str;
+                                       }
+                                       break;
+                               default:
+                                       break;
+                               }
+                               break;
+                               // case 0x7F: // Realtime Universal
+                       case 0x41: // Roland
+                               dataBytePos++;
+                               switch( modelId ) {
+                               case 0x42:
+                                       str += " [GS]"; dataBytePos++;
+                                       if( msgdata[3]==0x12 ) {
+                                               str += "DT1:"; dataBytePos++;
+                                               switch( msgdata[4] ) {
+                                               case 0x00:
+                                                       if( msgdata[5]==0x00 ) {
+                                                               if( msgdata[6]==0x7F ) {
+                                                                       if( msgdata[7]==0x00 ) {
+                                                                               str += " [88] System Mode Set (Mode 1: Single Module)"; return str;
+                                                                       }
+                                                                       else if( msgdata[7]==0x01 ) {
+                                                                               str += " [88] System Mode Set (Mode 2: Double Module)"; return str;
+                                                                       }
+                                                               }
+                                                       }
+                                                       else if( msgdata[5]==0x01 ) {
+                                                               int port = (msgdata[7] & 0xFF);
+                                                               str += String.format(
+                                                                               " [88] Ch.Msg Rx Port: Block=0x%02X, Port=%s",
+                                                                               msgdata[6],
+                                                                               port==0?"A":port==1?"B":String.format("0x%02X",port)
+                                                                               );
+                                                               return str;
+                                                       }
+                                                       break;
+                                               case 0x40:
+                                                       if( msgdata[5]==0x00 ) {
+                                                               switch( msgdata[6] ) {
+                                                               case 0x00: str += " Master Tune: "; dataBytePos += 3; break;
+                                                               case 0x04: str += " Master Volume: "; dataBytePos += 3; break;
+                                                               case 0x05: str += " Master Key Shift: "; dataBytePos += 3; break;
+                                                               case 0x06: str += " Master Pan: "; dataBytePos += 3; break;
+                                                               case 0x7F:
+                                                                       switch( msgdata[7] ) {
+                                                                       case 0x00: str += " GS Reset"; return str;
+                                                                       case 0x7F: str += " Exit GS Mode"; return str;
+                                                                       }
+                                                                       break;
+                                                               }
+                                                       }
+                                                       else if( msgdata[5]==0x01 ) {
+                                                               switch( msgdata[6] ) {
+                                                               // case 0x00: str += ""; break;
+                                                               // case 0x10: str += ""; break;
+                                                               case 0x30: str += " Reverb Macro: "; dataBytePos += 3; break;
+                                                               case 0x31: str += " Reverb Character: "; dataBytePos += 3; break;
+                                                               case 0x32: str += " Reverb Pre-LPF: "; dataBytePos += 3; break;
+                                                               case 0x33: str += " Reverb Level: "; dataBytePos += 3; break;
+                                                               case 0x34: str += " Reverb Time: "; dataBytePos += 3; break;
+                                                               case 0x35: str += " Reverb Delay FB: "; dataBytePos += 3; break;
+                                                               case 0x36: str += " Reverb Chorus Level: "; dataBytePos += 3; break;
+                                                               case 0x37: str += " [88] Reverb Predelay Time: "; dataBytePos += 3; break;
+                                                               case 0x38: str += " Chorus Macro: "; dataBytePos += 3; break;
+                                                               case 0x39: str += " Chorus Pre-LPF: "; dataBytePos += 3; break;
+                                                               case 0x3A: str += " Chorus Level: "; dataBytePos += 3; break;
+                                                               case 0x3B: str += " Chorus FB: "; dataBytePos += 3; break;
+                                                               case 0x3C: str += " Chorus Delay: "; dataBytePos += 3; break;
+                                                               case 0x3D: str += " Chorus Rate: "; dataBytePos += 3; break;
+                                                               case 0x3E: str += " Chorus Depth: "; dataBytePos += 3; break;
+                                                               case 0x3F: str += " Chorus Send Level To Reverb: "; dataBytePos += 3; break;
+                                                               case 0x40: str += " [88] Chorus Send Level To Delay: "; dataBytePos += 3; break;
+                                                               case 0x50: str += " [88] Delay Macro: "; dataBytePos += 3; break;
+                                                               case 0x51: str += " [88] Delay Pre-LPF: "; dataBytePos += 3; break;
+                                                               case 0x52: str += " [88] Delay Time Center: "; dataBytePos += 3; break;
+                                                               case 0x53: str += " [88] Delay Time Ratio Left: "; dataBytePos += 3; break;
+                                                               case 0x54: str += " [88] Delay Time Ratio Right: "; dataBytePos += 3; break;
+                                                               case 0x55: str += " [88] Delay Level Center: "; dataBytePos += 3; break;
+                                                               case 0x56: str += " [88] Delay Level Left: "; dataBytePos += 3; break;
+                                                               case 0x57: str += " [88] Delay Level Right: "; dataBytePos += 3; break;
+                                                               case 0x58: str += " [88] Delay Level: "; dataBytePos += 3; break;
+                                                               case 0x59: str += " [88] Delay FB: "; dataBytePos += 3; break;
+                                                               case 0x5A: str += " [88] Delay Send Level To Reverb: "; dataBytePos += 3; break;
+                                                               }
+                                                       }
+                                                       else if( msgdata[5]==0x02 ) {
+                                                               switch( msgdata[6] ) {
+                                                               case 0x00: str += " [88] EQ Low Freq: "; dataBytePos += 3; break;
+                                                               case 0x01: str += " [88] EQ Low Gain: "; dataBytePos += 3; break;
+                                                               case 0x02: str += " [88] EQ High Freq: "; dataBytePos += 3; break;
+                                                               case 0x03: str += " [88] EQ High Gain: "; dataBytePos += 3; break;
+                                                               }
+                                                       }
+                                                       else if( msgdata[5]==0x03 ) {
+                                                               if( msgdata[6] == 0x00 ) {
+                                                                       str += " [Pro] EFX Type: "; dataBytePos += 3;
+                                                               }
+                                                               else if( msgdata[6] >= 0x03 && msgdata[6] <= 0x16 ) {
+                                                                       str += String.format(" [Pro] EFX Param %d", msgdata[6]-2 );
+                                                                       dataBytePos += 3;
+                                                               }
+                                                               else if( msgdata[6] == 0x17 ) {
+                                                                       str += " [Pro] EFX Send Level To Reverb: "; dataBytePos += 3;
+                                                               }
+                                                               else if( msgdata[6] == 0x18 ) {
+                                                                       str += " [Pro] EFX Send Level To Chorus: "; dataBytePos += 3;
+                                                               }
+                                                               else if( msgdata[6] == 0x19 ) {
+                                                                       str += " [Pro] EFX Send Level To Delay: "; dataBytePos += 3;
+                                                               }
+                                                               else if( msgdata[6] == 0x1B ) {
+                                                                       str += " [Pro] EFX Ctrl Src1: "; dataBytePos += 3;
+                                                               }
+                                                               else if( msgdata[6] == 0x1C ) {
+                                                                       str += " [Pro] EFX Ctrl Depth1: "; dataBytePos += 3;
+                                                               }
+                                                               else if( msgdata[6] == 0x1D ) {
+                                                                       str += " [Pro] EFX Ctrl Src2: "; dataBytePos += 3;
+                                                               }
+                                                               else if( msgdata[6] == 0x1E ) {
+                                                                       str += " [Pro] EFX Ctrl Depth2: "; dataBytePos += 3;
+                                                               }
+                                                               else if( msgdata[6] == 0x1F ) {
+                                                                       str += " [Pro] EFX Send EQ Switch: "; dataBytePos += 3;
+                                                               }
+                                                       }
+                                                       else if( (msgdata[5] & 0xF0) == 0x10 ) {
+                                                               int ch = (msgdata[5] & 0x0F);
+                                                               if( ch <= 9 ) ch--; else if( ch == 0 ) ch = 9;
+                                                               if( msgdata[6]==0x02 ) {
+                                                                       str += String.format(
+                                                                                       " Rx Ch: Part=%d(0x%02X) Ch=0x%02X", (ch+1),  msgdata[5], msgdata[7]
+                                                                                       );
+                                                                       return str;
+                                                               }
+                                                               else if( msgdata[6]==0x15 ) {
+                                                                       String map;
+                                                                       switch( msgdata[7] ) {
+                                                                       case 0: map = " NormalPart"; break;
+                                                                       case 1: map = " DrumMap1"; break;
+                                                                       case 2: map = " DrumMap2"; break;
+                                                                       default: map = String.format("0x%02X",msgdata[7]); break;
+                                                                       }
+                                                                       str += String.format(
+                                                                               " Rhythm Part: Ch=%d(0x%02X) Map=%s",
+                                                                               (ch+1), msgdata[5],
+                                                                               map
+                                                                       );
+                                                                       return str;
+                                                               }
+                                                       }
+                                                       else if( (msgdata[5] & 0xF0) == 0x40 ) {
+                                                               int ch = (msgdata[5] & 0x0F);
+                                                               if( ch <= 9 ) ch--; else if( ch == 0 ) ch = 9;
+                                                               int dt = (msgdata[7] & 0xFF);
+                                                               if( msgdata[6]==0x20 ) {
+                                                                       str += String.format(
+                                                                               " [88] EQ: Ch=%d(0x%02X) %s",
+                                                                               (ch+1), msgdata[5],
+                                                                               dt==0 ? "OFF" : dt==1 ? "ON" : String.format("0x%02X",dt)
+                                                                       );
+                                                               }
+                                                               else if( msgdata[6]==0x22 ) {
+                                                                       str += String.format(
+                                                                               " [Pro] Part EFX Assign: Ch=%d(0x%02X) %s",
+                                                                               (ch+1), msgdata[5],
+                                                                               dt==0 ? "ByPass" : dt==1 ? "EFX" : String.format("0x%02X",dt)
+                                                                       );
+                                                               }
+                                                       }
+                                                       break;
+                                               } // [4]
+                                       } // [3] [DT1]
+                                       break; // [GS]
+                               case 0x45:
+                                       str += " [GS-LCD]"; dataBytePos++;
+                                       if( msgdata[3]==0x12 ) {
+                                               str += " [DT1]"; dataBytePos++;
+                                               if( msgdata[4]==0x10 && msgdata[5]==0x00 && msgdata[6]==0x00 ) {
+                                                       dataBytePos += 3;
+                                                       str += " Disp [" +(new String(
+                                                               msgdata, dataBytePos, msgdata.length - dataBytePos - 2
+                                                       ))+ "]";
+                                               }
+                                       } // [3] [DT1]
+                                       break;
+                               case 0x14: str += " [D-50]"; dataBytePos++; break;
+                               case 0x16: str += " [MT-32]"; dataBytePos++; break;
+                               } // [2] model_id
+                               break;
+                       case 0x43: // Yamaha
+                               if( (deviceId & 0xF0) == 0x10 && modelId == 0x4C ) {
+                                       str += " [XG]Dev#="+(deviceId & 0x0F);
+                                       dataBytePos += 2;
+                                       if( msgdata[3]==0 && msgdata[4]==0 && msgdata[5]==0x7E && msgdata[6]==0 ) {
+                                               str += " System ON";
+                                               return str;
+                                       }
+
+                               }
+                               else if( deviceId == 0x79 && modelId == 9 ) {
+                                       str += " [eVocaloid]";
+                                       dataBytePos += 2;
+                                       if( msgdata[3]==0x11 && msgdata[4]==0x0A && msgdata[5]==0 ) {
+                                               StringBuilder p = new StringBuilder();
+                                               for( int i=6; i<msgdata.length; i++ ) {
+                                                       int b = (msgdata[i] & 0xFF);
+                                                       if( b == 0xF7 ) break;
+                                                       String s = (
+                                                               b < 0 || b >= MIDISpec.nsx39LyricElements.length ?
+                                                               "?": MIDISpec.nsx39LyricElements[b]
+                                                       );
+                                                       p.append(s);
+                                               }
+                                               str += " pronounce["+p+"]";
+                                               return str;
+                                       }
+                               }
+                               break;
+                       default:
+                               break;
+                       }
+                       int i;
+                       str += " data=(";
+                       for( i = dataBytePos; i<msgdata.length-1; i++ ) {
+                               str += String.format( " %02X", msgdata[i] );
+                       }
+                       if( i < msgdata.length && (int)(msgdata[i] & 0xFF) != 0xF7 ) {
+                               str+=" [ Invalid EOX " + String.format( "%02X", msgdata[i] ) + " ]";
+                       }
+                       str += " )";
+                       return str;
+               }
+               byte[] msg_data = msg.getMessage();
+               str += "(";
+               for( byte b : msg_data ) {
+                       str += String.format( " %02X", b );
+               }
+               str += " )";
+               return str;
+       }
+       public static boolean isRhythmPart(int ch) { return (ch == 9); }
+}
diff --git a/src/camidion/chordhelper/music/MelodyTrackSpec.java b/src/camidion/chordhelper/music/MelodyTrackSpec.java
new file mode 100644 (file)
index 0000000..6ebeb38
--- /dev/null
@@ -0,0 +1,219 @@
+package camidion.chordhelper.music;
+
+
+
+/**
+ * メロディトラック仕様
+ */
+public class MelodyTrackSpec extends AbstractNoteTrackSpec {
+       /**
+        * 音域
+        */
+       public Range range;
+       /**
+        * 音を出すかどうかを表すビット列
+        */
+       public int beatPattern = 0xFFFF;
+       /**
+        * あとで音を出し続けるかどうかを表すビット列
+        */
+       public int continuousBeatPattern = 0xEEEE;
+       /**
+        * ベース音を使う場合 true、それ以外のコード構成音を使う場合 false
+        */
+       public boolean isBass = false;
+       /**
+        * 乱数メロディを作るかどうか
+        */
+       public boolean randomMelody = false;
+       /**
+        * 乱数歌詞を作るかどうか
+        */
+       public boolean randomLyric = false;
+       /**
+        * 乱数歌詞をNSX-39(ポケット・ミク)対応の
+        * システムエクスクルーシブを使って出力するかどうか
+        */
+       public boolean nsx39 = false;
+       /**
+        * メロディトラック仕様を構築
+        * @param ch MIDIチャンネル
+        * @param name トラック名
+        */
+       public MelodyTrackSpec(int ch, String name) {
+               super(ch,name);
+               range = new Range(
+                       Music.SEMITONES_PER_OCTAVE * 5, Music.SEMITONES_PER_OCTAVE * 6 );
+       }
+       /**
+        * 音域を指定してメロディトラック仕様を構築
+        * @param ch MIDIチャンネル
+        * @param name トラック名
+        * @param range 音域
+        */
+       public MelodyTrackSpec(int ch, String name, Range range) {
+               super(ch,name);
+               this.range = range;
+       }
+       /**
+        * コードの追加
+        * @param cp コード進行
+        */
+       public void addChords( ChordProgression cp ) {
+               int mask;
+               long tick;
+               long startTickPos;
+
+               // 音階ごとの生起確率を決める重みリスト(random_melody の場合)
+               int i, noteNumber, prevNoteNumber = 1;
+               int noteWeights[] = new int[range.max_note - range.min_note];
+               //
+               Key key = cp.key;
+               if( key == null ) key = new Key("C");
+
+               for( ChordProgression.Line line : cp.lines ) { // 行単位の処理
+                       for( ChordProgression.Measure measure : line ) { // 小節単位の処理
+                               if( measure.ticks_per_beat == null )
+                                       continue;
+                               ChordProgression.TickRange tickRange = measure.getRange();
+                               boolean isNoteOn = false;
+                               //
+                               // 各ビートごとに繰り返し
+                               for(
+                                       tick = startTickPos = tickRange.start_tick_pos, mask = 0x8000;
+                                       tick < tickRange.end_tick_pos;
+                                       tick += minNoteTicks, mask >>>= 1
+                               ) {
+                                       // そのtick地点のコードを調べる
+                                       Chord chord = measure.chordStrokeAt(tick).chord;
+                                       int notes[] = chord.toNoteArray(range, null);
+                                       //
+                                       // 各音階ごとに繰り返し
+                                       if( Math.random() < 0.9 ) {
+                                               if( (beatPattern & mask) == 0 ) {
+                                                       // 音を出さない
+                                                       continue;
+                                               }
+                                       }
+                                       else {
+                                               // ランダムに逆パターン
+                                               if( (beatPattern & mask) != 0 ) {
+                                                       continue;
+                                               }
+                                       }
+                                       if( ! isNoteOn ) {
+                                               // 前回のビートで継続していなかったので、
+                                               // この地点で音を出し始めることにする。
+                                               startTickPos = tick;
+                                               isNoteOn = true;
+                                       }
+                                       if( Math.random() < 0.9 ) {
+                                               if( (continuousBeatPattern & mask) != 0 ) {
+                                                       // 音を継続
+                                                       continue;
+                                               }
+                                       }
+                                       else {
+                                               // ランダムに逆パターン
+                                               if( (continuousBeatPattern & mask) == 0 ) {
+                                                       continue;
+                                               }
+                                       }
+                                       // このビートの終了tickで音を出し終わることにする。
+                                       if( randomMelody ) {
+                                               // 音階ごとに出現確率を決める
+                                               int totalWeight = 0;
+                                               for( i=0; i<noteWeights.length; i++ ) {
+                                                       noteNumber = range.min_note + i;
+                                                       int m12 = Music.mod12(noteNumber - chord.rootNoteSymbol().toNoteNumber());
+                                                       int w;
+                                                       if( chord.indexOf(noteNumber) >= 0 ) {
+                                                               // コード構成音は確率を上げる
+                                                               w = 255;
+                                                       }
+                                                       else {
+                                                               switch( m12 ) {
+                                                               case 2: // 長2度
+                                                               case 9: // 長6度
+                                                                       w = 63; break;
+                                                               case 5: // 完全4度
+                                                               case 11: // 長7度
+                                                                       w = 47; break;
+                                                               default:
+                                                                       w = 0; break;
+                                                               }
+                                                               if( ! key.isOnScale( noteNumber ) ) {
+                                                                       // スケールを外れている音は採用しない
+                                                                       w = 0;
+                                                               }
+                                                       }
+                                                       // 乱高下軽減のため、前回との差によって確率を調整する
+                                                       int diff = noteNumber - prevNoteNumber;
+                                                       if( diff < 0 ) diff = -diff;
+                                                       if( diff == 0 ) w /= 8;
+                                                       else if( diff > 7 ) w = 0;
+                                                       else if( diff > 4 ) w /= 8;
+                                                       totalWeight += (noteWeights[i] = w);
+                                               }
+                                               // さいころを振って音階を決定
+                                               noteNumber = range.invertedNoteOf(key.rootNoteNumber());
+                                               double r = Math.random();
+                                               totalWeight *= r;
+                                               for( i=0; i<noteWeights.length; i++ ) {
+                                                       if( (totalWeight -= noteWeights[i]) < 0 ) {
+                                                               noteNumber = range.min_note + i;
+                                                               break;
+                                                       }
+                                               }
+                                               if( randomLyric ) {
+                                                       // ランダムな歌詞を作成
+                                                       int index = (int)(Math.random() * MIDISpec.nsx39LyricElements.length);
+                                                       if( nsx39 ) {
+                                                               // ポケット・ミク用システムエクスクルーシブ
+                                                               byte nsx39SysEx[] = {
+                                                                       0x43, 0x79, 0x09, 0x11, 0x0A, 0x00, (byte)(index & 0x7F), (byte) 0xF7
+                                                               };
+                                                               addSysEx(nsx39SysEx, startTickPos);
+                                                       }
+                                                       // 決定された音符を追加
+                                                       addNote(
+                                                               startTickPos, tick + minNoteTicks,
+                                                               noteNumber, velocity
+                                                       );
+                                                       // 歌詞をテキストとして追加
+                                                       addStringTo(0x05, MIDISpec.nsx39LyricElements[index], startTickPos);
+                                               }
+                                               else {
+                                                       // 決定された音符を追加
+                                                       addNote(
+                                                               startTickPos, tick + minNoteTicks,
+                                                               noteNumber, velocity
+                                                       );
+                                               }
+                                               prevNoteNumber = noteNumber;
+                                       }
+                                       else if( isBass ) {
+                                               // ベース音を追加
+                                               int note = range.invertedNoteOf(chord.bassNoteSymbol().toNoteNumber());
+                                               addNote(
+                                                       startTickPos, tick + minNoteTicks,
+                                                       note, velocity
+                                               );
+                                       }
+                                       else {
+                                               // コード本体の音を追加
+                                               for( int note : notes ) {
+                                                       addNote(
+                                                               startTickPos, tick + minNoteTicks,
+                                                               note, velocity
+                                                       );
+                                               }
+                                       }
+                                       isNoteOn = false;
+                               }
+                       }
+               }
+
+       }
+
+}
\ No newline at end of file
diff --git a/src/camidion/chordhelper/music/Music.java b/src/camidion/chordhelper/music/Music.java
new file mode 100644 (file)
index 0000000..edd260f
--- /dev/null
@@ -0,0 +1,99 @@
+package camidion.chordhelper.music;
+
+/**
+ * 音楽理論ユーティリティ
+ *
+ * Circle-Of-Fifth based music theory functions
+ *
+ * @author Copyright (C) 2004-2014 @きよし - Akiyoshi Kamide
+ */
+public class Music {
+       /**
+        * 1オクターブの半音数
+        */
+       public static final int SEMITONES_PER_OCTAVE = 12;
+       /**
+        * MIDIノート番号と、
+        * メジャーキー基準の五度圏インデックス値との間の変換を行います。
+        *
+        * <p>双方向の変換に対応しています。
+        * 内部的には、元の値が奇数のときに6(半オクターブ)を足し、
+        * 偶数のときにそのまま返しているだけです。
+        * 値は0~11であるとは限りません。その範囲に補正したい場合は
+        *  {@link #mod12(int)} を併用します。
+        * </p>
+        *
+        * @param n 元の値
+        * @return 変換結果
+        */
+       public static int reverseCo5(int n) {
+               return (n & 1) == 0 ? n : n+6 ;
+       }
+       /**
+        * ノート番号からオクターブ成分を抜きます。
+        * <p>n % 12 と似ていますが、Java の % 演算子では、
+        * 左辺に負数を与えると答えも負数になってしまうため、n % 12 で計算しても
+        * 0~11 の範囲を外れてしまうことがあります。そこで、
+        * 負数の場合に12を足すことにより 0~11 の範囲に入るよう補正します。
+        * </p>
+        * @param n 元のノート番号
+        * @return オクターブ成分を抜いたノート番号(0~11)
+        */
+       public static int mod12(int n) {
+               int qn = n % SEMITONES_PER_OCTAVE;
+               return qn < 0 ? qn + 12 : qn ;
+       }
+       /**
+        * 指定されたMIDIノート番号の音の周波数を返します。
+        * チューニングは A=440Hz とします。
+        *
+        * @param noteNumber MIDIノート番号
+        * @return 音の周波数[Hz]
+        */
+       public static double noteNumberToFrequency(int noteNumber) {
+               return 55 * Math.pow( 2, (double)(noteNumber - 33)/12 );
+       }
+       /**
+        * MIDIノート番号の示す音階が、
+        * 指定された調(五度圏インデックス値)におけるメジャースケールまたは
+        * ナチュラルマイナースケールの構成音に該当するか調べます。
+        *
+        * <p>調の五度圏インデックス値に0(ハ長調またはイ短調)を指定すると、
+        * 白鍵のときにtrue、黒鍵のときにfalseを返します。
+        * </p>
+        *
+        * @param noteNumber 調べたい音階のノート番号
+        * @param keyCo5 調の五度圏インデックス値
+        * @return スケール構成音のときtrue、スケールを外れている場合false
+        */
+       public static boolean isOnScale(int noteNumber, int keyCo5) {
+               return mod12(reverseCo5(noteNumber) - keyCo5 + 1) < 7 ;
+       }
+       /**
+        * 五度圏インデックス値で表された音階を、
+        * 指定された半音数だけ移調した結果を返します。
+        *
+        * <p>移調する半音数が0の場合、指定の五度圏インデックス値をそのまま返します。
+        * それ以外の場合、移調結果を -5 ~ 6 の範囲で返します。
+        * </p>
+        *
+        * @param co5 五度圏インデックス値
+        * @param chromaticOffset 移調する半音数
+        * @return 移調結果
+        */
+       public static int transposeCo5(int co5, int chromaticOffset) {
+               if( chromaticOffset == 0 ) return co5;
+               int transposedCo5 = mod12( co5 + reverseCo5(chromaticOffset) );
+               if( transposedCo5 > 6 ) transposedCo5 -= Music.SEMITONES_PER_OCTAVE;
+               return transposedCo5;
+       }
+       /**
+        * 指定の五度圏インデックス値の真裏にあたる値を返します。
+        * @param co5 五度圏インデックス値
+        * @return 真裏の五度圏インデックス値
+        */
+       public static int oppositeCo5(int co5) {
+               return co5 > 0 ? co5 - 6 : co5 + 6;
+       }
+}
+
diff --git a/src/camidion/chordhelper/music/NoteSymbol.java b/src/camidion/chordhelper/music/NoteSymbol.java
new file mode 100644 (file)
index 0000000..9347649
--- /dev/null
@@ -0,0 +1,231 @@
+package camidion.chordhelper.music;
+
+/**
+ * 音名(オクターブ抜き)を表すクラスです。値は不変です。
+ *
+ * <p>この音名は、メジャーキーの調号にした場合に
+ * 「♭、#が何個つくか」という数値
+ * 「五度圏インデックス値」で保持することを基本としています。
+ * こうすれば異名同音を明確に区別でき、
+ * しかも音楽理論的な計算を極めて単純な数式で行えるようになります。
+ * この方式はMIDIメタメッセージで調号を指定するときにも使われていて、
+ * 非常に高い親和性を持ちます。
+ * </p>
+ */
+public class NoteSymbol implements Cloneable {
+       /**
+        * メジャーキー基準の五度圏インデックス値
+        */
+       private int majorCo5;
+       /**
+        * ノート番号(0~11)
+        */
+       private int noteNumber;
+       /**
+        * 音名 C(ハ音)を構築します。
+        */
+       public NoteSymbol() {
+       }
+       /**
+        * 五度圏インデックス値(メジャーキー基準)から音名を構築します。
+        * @param majorCo5 五度圏インデックス値
+        */
+       public NoteSymbol(int majorCo5) {
+               noteNumber = toNoteNumber(this.majorCo5 = majorCo5);
+       }
+       /**
+        * 音名を文字列から構築します。
+        * @param noteSymbol 音名の文字列
+        * @throws IllegalArgumentException
+        *  指定の音名が空、またはA~Gの範囲を外れている場合
+        */
+       public NoteSymbol(String noteSymbol) {
+               this(SymbolLanguage.SYMBOL.majorCo5Of(noteSymbol.trim()));
+       }
+       @Override
+       protected NoteSymbol clone() {
+               return new NoteSymbol(majorCo5);
+       }
+       /**
+        * この音階が指定されたオブジェクトと等しいか調べます。
+        *
+        * <p>双方の五度圏インデックス値が等しい場合のみtrueを返します。
+        * すなわち、異名同音は等しくないものとして判定されます。
+        * </p>
+        *
+        * @return この音階が指定されたオブジェクトと等しい場合true
+        */
+       @Override
+       public boolean equals(Object anObject) {
+               if( this == anObject )
+                       return true;
+               if( anObject instanceof NoteSymbol ) {
+                       NoteSymbol another = (NoteSymbol) anObject;
+                       return majorCo5 == another.majorCo5;
+               }
+               return false;
+       }
+       /**
+        * この音階のハッシュコード値として、
+        * 五度圏インデックス値をそのまま返します。
+        *
+        * @return この音階のハッシュコード値
+        */
+       @Override
+       public int hashCode() { return majorCo5; }
+       /**
+        * 音階が等しいかどうかを、異名同音を無視して判定します。
+        * @param another 比較対象の音階
+        * @return 等しければtrue
+        */
+       public boolean equalsEnharmonically(NoteSymbol another) {
+               return this == another || this.noteNumber == another.noteNumber;
+       }
+       /**
+        * 五度圏インデックス値(メジャーキー基準)を返します。
+        * @return 五度圏インデックス値
+        */
+       public int toCo5() { return majorCo5; }
+       /**
+        * メジャーかマイナーかを指定し、対応する五度圏インデックス値を返します。
+        * <p>マイナーの場合、
+        * メジャー基準の五度圏インデックス値から3が差し引かれます。
+        * 例えば、C major の場合は調号が0個なのに対し、
+        * C minor のときは調号の♭が3個に増えますが、
+        * 3を差し引くことによってこのズレが補正されます。
+        * </p>
+        *
+        * @param isMinor マイナーのときtrue
+        * @return 五度圏インデックス値
+        */
+       public int toCo5(boolean isMinor) {
+               return isMinor ? majorCo5 - 3 : majorCo5;
+       }
+       /**
+        * ノート番号(0~11)を返します。
+        * <p>これはMIDIノート番号からオクターブ情報を抜いた値と同じです。
+        * 五度圏インデックス値をノート番号に変換した場合、
+        * 異名同音、すなわち同じ音階が♭表記、♯表記のどちらだったか
+        * という情報は失われます。
+        * </p>
+        * @return ノート番号(0~11)
+        */
+       public int toNoteNumber() { return noteNumber; }
+       /**
+        * この音階の文字列表現として音名を返します。
+        * @return この音階の文字列表現
+        */
+       @Override
+       public String toString() {
+               return toStringIn(SymbolLanguage.SYMBOL, false);
+       }
+       /**
+        * 指定した言語モードにおける文字列表現を返します。
+        * @param language 言語モード
+        * @return 文字列表現
+        */
+       public String toStringIn(SymbolLanguage language) {
+               return toStringIn(language, false);
+       }
+       /**
+        * 指定した言語モードとメジャーマイナー種別における文字列表現を返します。
+        * <p>マイナーが指定された場合、
+        * 五度圏インデックス値を3つ進めた音階として返します。
+        * 例えば、{@link #toCo5()} の戻り値が0の場合、
+        * メジャーが指定されていれば C を返しますが、
+        * マイナーが指定されると A を返します。
+        * これにより、同じ五度圏インデックス値0で C と Am の両方のルート音を導出できます。
+        * </p>
+        * @param language 言語モード
+        * @param isMinor マイナーのときtrue
+        * @return 文字列表現
+        */
+       public String toStringIn(SymbolLanguage language, boolean isMinor) {
+               int co5_s771 = majorCo5 + 15; // Shift 7 + 7 + 1 = 15 steps
+               if( isMinor ) {
+                       // When co5 is for minor (key or chord), shift 3 steps more
+                       co5_s771 += 3;
+               }
+               if( co5_s771 < 0 || co5_s771 >= 35 ) {
+                       //
+                       // 35種類の音名の範囲に入らないような値が来てしまった場合は、
+                       // それを調号として見たときに 5b ~ 6# の範囲に収まるような異名同音(enharmonic)に置き換える。
+                       //
+                       co5_s771 = Music.mod12(co5_s771);  // returns 0(Fbb) ... 7(Fb) 8(Cb) 9(Gb) 10(Db) 11(Ab)
+                       if( isMinor ) {
+                               if( co5_s771 == 0 )
+                                       co5_s771 += Music.SEMITONES_PER_OCTAVE * 2; // 0(Fbbm)+24 = 24(D#m)
+                               else
+                                       co5_s771 += Music.SEMITONES_PER_OCTAVE;  // 1(Cbbm)+12 = 13(Bbm)
+                       }
+                       else {
+                               if( co5_s771 < 10 )
+                                       co5_s771 += Music.SEMITONES_PER_OCTAVE;  // 0(Fbb)+12 = 12(Eb), 9(Gb)+12 = 21(F#)
+                       }
+               }
+               int sharpFlatIndex = co5_s771 / 7;
+               int note_index = co5_s771 - sharpFlatIndex * 7;
+               String note = language.notes.substring( note_index, note_index+1 );
+               String sharp_flat = language.sharpFlatList.get(sharpFlatIndex);
+               return language.preSharpFlat ? sharp_flat + note : note + sharp_flat;
+       }
+       /**
+        * 指定の最大文字数の範囲で、MIDIノート番号が示す音名を返します。
+        * <p>ノート番号だけでは物理的な音階情報しか得られないため、
+        * 白鍵で#♭のついた音階表現(B#、Cb など)、
+        * ダブルシャープ、ダブルフラットを使った表現は返しません。
+        * </p>
+        * <p>白鍵の場合は A ~ G までの文字、黒鍵の場合は#と♭の両方の表現を返します。
+        * ただし、制限文字数の指定により、#と♭の両方を返せないことがわかった場合は、
+        * 五度圏で値0(キー C / Am)からの距離が、メジャー、マイナーの両方を含めて
+        * 近くにあるほうの表現(C# Eb F# Ab Bb)のみを返します。
+        * </p>
+        * @param noteNo MIDIノート番号
+        * @param maxChars 最大文字数
+        * @return MIDIノート番号が示す音名
+        */
+       public static String noteNumberToSymbol(int noteNo, int maxChars) {
+               int co5 = Music.mod12(Music.reverseCo5(noteNo));
+               if( co5 == 11 ) {
+                       return (new NoteSymbol(-1)).toString();
+               }
+               else if( co5 >= 6 ) {
+                       if( maxChars >= 7 ) {
+                               return
+                                       (new NoteSymbol(co5)).toString() + " / " +
+                                       (new NoteSymbol(co5 - 12)).toString();
+                       }
+                       else {
+                               // String capacity not enough
+                               // Select only one note (sharped or flatted)
+                               return (new NoteSymbol(co5 - ((co5 >= 8) ? 12 : 0))).toString();
+                       }
+               }
+               else return (new NoteSymbol(co5)).toString();
+       }
+       /**
+        * 最大256文字の範囲で、MIDIノート番号が示す音名を返します。
+        * <p>内部的には
+        * {@link #noteNumberToSymbol(int, int)} を呼び出しているだけです。
+        * </p>
+        * @param noteNo MIDIノート番号
+        * @return MIDIノート番号が示す音名
+        */
+       public static String noteNoToSymbol(int noteNo) {
+               return noteNumberToSymbol(noteNo, 256);
+       }
+       /**
+        * 指定された五度圏インデックス値(メジャーキー基準)を
+        * ノート番号(0~11)に変換します。
+        *
+        * <p>これはMIDIノート番号からオクターブ情報を抜いた値と同じです。
+        * 五度圏インデックス値をノート番号に変換した場合、
+        * 異名同音、すなわち同じ音階が♭表記、♯表記のどちらだったか
+        * という情報は失われます。
+        * </p>
+        * @return ノート番号(0~11)
+        */
+       public static int toNoteNumber(int majorCo5) {
+               return Music.mod12(Music.reverseCo5(majorCo5));
+       }
+}
\ No newline at end of file
diff --git a/src/camidion/chordhelper/music/Range.java b/src/camidion/chordhelper/music/Range.java
new file mode 100644 (file)
index 0000000..e3d44c2
--- /dev/null
@@ -0,0 +1,90 @@
+package camidion.chordhelper.music;
+
+
+/**
+ * 音域を表すクラスです。
+ */
+public class Range {
+       public int min_note = 0;
+       public int max_note = MIDISpec.MAX_NOTE_NO;
+       public int min_key_offset = 0;
+       public boolean is_inversion_mode = true;
+       public Range( int min_note, int max_note ) {
+               this.min_note = min_note;
+               this.max_note = max_note;
+       }
+       public Range( Integer[] notes ) {
+               if( notes == null ) return;
+               switch( notes.length ) {
+               case 0: return;
+               case 1:
+                       min_note = max_note = notes[0];
+                       break;
+               default:
+                       if( notes[0] > notes[1] ) {
+                               min_note = notes[1];
+                               max_note = notes[0];
+                       }
+                       else {
+                               min_note = notes[0];
+                               max_note = notes[1];
+                       }
+                       break;
+               }
+       }
+       public Range(
+               int min_note, int max_note,
+               int min_key_offset, boolean inv_mode
+       ) {
+               this.min_note = min_note;
+               this.max_note = max_note;
+               this.min_key_offset = min_key_offset;
+               this.is_inversion_mode = inv_mode;
+       }
+       public int invertedNoteOf(int note_no) {
+               return invertedNoteOf( note_no, null );
+       }
+       public int invertedNoteOf(int note_no, Key key) {
+               int min_note = this.min_note;
+               int max_note = this.max_note;
+               int offset = 0;
+               if( key != null ) {
+                       offset = key.relativeDo();
+                       if( min_key_offset < 0 && offset >= Music.mod12(min_key_offset) ) {
+                               offset -= 12;
+                       }
+                       else if( min_key_offset > 0 && offset < Music.mod12(min_key_offset) ) {
+                               offset += 12;
+                       }
+                       min_note += offset;
+                       max_note += offset;
+               }
+               int octave = min_note / Music.SEMITONES_PER_OCTAVE;
+               note_no += 12 * octave;
+               while( note_no > max_note )
+                       note_no -= Music.SEMITONES_PER_OCTAVE;
+               while( note_no > MIDISpec.MAX_NOTE_NO )
+                       note_no -= Music.SEMITONES_PER_OCTAVE;
+               while( note_no < min_note )
+                       note_no += Music.SEMITONES_PER_OCTAVE;
+               while( note_no < 0 )
+                       note_no += Music.SEMITONES_PER_OCTAVE;
+               return note_no;
+       }
+       public void invertNotesOf( int[] notes, Key key ) {
+               int i;
+               if( is_inversion_mode ) {
+                       for( i=0; i<notes.length; i++ ) {
+                               notes[i] = invertedNoteOf( notes[i], key );
+                       }
+               }
+               else {
+                       int n = invertedNoteOf( notes[0], new Key(min_key_offset) );
+                       int n_diff = n - notes[0];
+                       notes[0] = n;
+                       for( i=1; i<notes.length; i++ ) {
+                               notes[i] += n_diff;
+                       }
+               }
+       }
+}
\ No newline at end of file
diff --git a/src/camidion/chordhelper/music/SymbolLanguage.java b/src/camidion/chordhelper/music/SymbolLanguage.java
new file mode 100644 (file)
index 0000000..376c081
--- /dev/null
@@ -0,0 +1,109 @@
+package camidion.chordhelper.music;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * シンボルの言語モード(音階、調など)
+ */
+public enum SymbolLanguage {
+       /**
+        * シンボル表記(Bb, F#)
+        */
+       SYMBOL(
+               Arrays.asList("bb","b","","#","x"),
+               "FCGDAEB",false,"","m"," / "
+       ),
+       /**
+        * 英名表記(B flat, F sharp)
+        */
+       NAME(
+               Arrays.asList(" double flat"," flat",""," sharp"," double sharp"),
+               "FCGDAEB",false," major"," minor"," / "
+       ),
+       /**
+        * 日本名表記(変ロ, 嬰ヘ)
+        */
+       IN_JAPANESE(
+               Arrays.asList("重変","変","","嬰","重嬰"),
+               "ヘハトニイホロ",true,"長調","短調","/"
+       );
+       /**
+        * ♭や♯の表記を、半音下がる数が多いほうから順に並べたリスト
+        */
+       List<String> sharpFlatList;
+       /**
+        * 音名を五度圏順で並べた文字列(必ず7文字でなければならない)
+        */
+       String notes;
+       /**
+        * 変化記号が音名の前につく(true)か後ろにつく(false)か
+        * <p>英語の場合は B♭ のように♭が後ろ、
+        * 日本語の場合は「変ロ」のように「変」が前につくことを表します。
+        * </p>
+        */
+       boolean preSharpFlat;
+       /**
+        * メジャーを表す文字列
+        */
+       String major;
+       /**
+        * マイナーを表す文字列
+        */
+       String minor;
+       /**
+        * メジャーとマイナーを併記する場合の区切り文字
+        */
+       String majorMinorDelimiter;
+       private SymbolLanguage(
+               List<String> sharpFlatList,
+               String notes,
+               boolean preSharpFlat,
+               String major, String minor, String majorMinorDelimiter
+       ) {
+               this.sharpFlatList = sharpFlatList;
+               this.notes = notes;
+               this.preSharpFlat = preSharpFlat;
+               this.major = major;
+               this.minor = minor;
+               this.majorMinorDelimiter = majorMinorDelimiter;
+       }
+       /**
+        * 音名の文字列を、メジャーキー基準の五度圏インデックス値に変換します。
+        *
+        * @param noteSymbol 音名の文字列
+        * @return メジャーキー基準の五度圏インデックス値
+        * @throws IllegalArgumentException
+        *  指定の音名が空、またはA~Gの範囲を外れている場合
+        */
+       public int majorCo5Of(String noteSymbol) {
+               if( Objects.requireNonNull(
+                       noteSymbol,
+                       "Musical note symbol must not be null"
+               ).isEmpty() ) {
+                       throw new IllegalArgumentException(
+                               "Empty musical note symbol specified"
+                       );
+               }
+               char topChar = noteSymbol.charAt(0);
+               int co5 = notes.indexOf(topChar);
+               if( co5 < 0 ) {
+                       throw new IllegalArgumentException(
+                               "Invalid musical note symbol "+noteSymbol+", not in "+notes
+                       );
+               }
+               co5--;
+               int offset = -14;
+               for( String sharpFlat : sharpFlatList ) {
+                       if( ! sharpFlat.isEmpty() && noteSymbol.startsWith(sharpFlat,1) ) {
+                               // 変化記号を発見
+                               // bb のほうが b よりも先にマッチするので誤判定の心配なし
+                               co5 += offset;
+                               break;
+                       }
+                       offset += 7;
+               }
+               return co5;
+       }
+}
diff --git a/src/camidion/chordhelper/pianokeyboard/MidiKeyboardPanel.java b/src/camidion/chordhelper/pianokeyboard/MidiKeyboardPanel.java
new file mode 100644 (file)
index 0000000..397aae5
--- /dev/null
@@ -0,0 +1,111 @@
+package camidion.chordhelper.pianokeyboard;
+
+import java.awt.Color;
+import java.awt.Insets;
+import java.awt.event.ActionEvent;
+import java.nio.charset.Charset;
+
+import javax.sound.midi.MidiMessage;
+import javax.swing.AbstractAction;
+import javax.swing.Box;
+import javax.swing.BoxLayout;
+import javax.swing.JButton;
+import javax.swing.JPanel;
+
+import camidion.chordhelper.ChordDisplayLabel;
+import camidion.chordhelper.chordmatrix.ChordMatrix;
+import camidion.chordhelper.mididevice.VirtualMidiDevice;
+import camidion.chordhelper.midieditor.KeySignatureSelecter;
+import camidion.chordhelper.midieditor.MidiChannelButtonSelecter;
+import camidion.chordhelper.midieditor.MidiChannelComboSelecter;
+import camidion.chordhelper.midieditor.MidiEventDialog;
+import camidion.chordhelper.midieditor.VelocitySelecter;
+
+public class MidiKeyboardPanel extends JPanel {
+       private MidiEventDialog eventDialog;
+       public void setEventDialog(MidiEventDialog eventDialog) {
+               this.eventDialog = eventDialog;
+       }
+       JButton sendEventButton;
+       JPanel keyboardChordPanel;
+       JPanel keyboardSouthPanel;
+       public KeySignatureSelecter keySelecter;
+       public PianoKeyboardPanel keyboardCenterPanel;
+       MidiChannelComboSelecter midiChannelCombobox;
+       MidiChannelButtonSelecter midiChannelButtons;
+       VelocitySelecter velocitySelecter;
+
+       private static final Insets ZERO_INSETS = new Insets(0,0,0,0);
+
+       public MidiKeyboardPanel(ChordMatrix chordMatrix) {
+               keyboardCenterPanel = new PianoKeyboardPanel();
+               keyboardCenterPanel.keyboard.chordMatrix = chordMatrix;
+               keyboardCenterPanel.keyboard.chordDisplay =
+                       new ChordDisplayLabel(
+                               "MIDI Keyboard", chordMatrix, keyboardCenterPanel.keyboard
+                       );
+               //
+               setLayout( new BoxLayout( this, BoxLayout.Y_AXIS ) );
+               add(keyboardChordPanel = new JPanel() {
+                       {
+                               setLayout(new BoxLayout(this, BoxLayout.X_AXIS));
+                               add( Box.createHorizontalStrut(5) );
+                               add(velocitySelecter = new VelocitySelecter(
+                                       keyboardCenterPanel.keyboard.velocityModel)
+                               );
+                               add(keySelecter = new KeySignatureSelecter(false));
+                               add( keyboardCenterPanel.keyboard.chordDisplay );
+                               add( Box.createHorizontalStrut(5) );
+                       }
+               });
+               add(keyboardCenterPanel);
+               add(Box.createVerticalStrut(5));
+               add(keyboardSouthPanel = new JPanel() {{
+                       setLayout(new BoxLayout(this, BoxLayout.X_AXIS));
+                       add(midiChannelCombobox = new MidiChannelComboSelecter(
+                               "MIDI Channel", keyboardCenterPanel.keyboard.midiChComboboxModel
+                       ));
+                       add(midiChannelButtons = new MidiChannelButtonSelecter(
+                               keyboardCenterPanel.keyboard
+                       ));
+                       add(sendEventButton = new JButton(new AbstractAction() {
+                               { putValue(NAME,"Send MIDI event"); }
+                               @Override
+                               public void actionPerformed(ActionEvent e) {
+                                       eventDialog.openMessageForm(
+                                               "Send MIDI event",
+                                               new AbstractAction() {
+                                                       { putValue(NAME,"Send"); }
+                                                       @Override
+                                                       public void actionPerformed(ActionEvent e) {
+                                                               VirtualMidiDevice vmd = keyboardCenterPanel.keyboard.midiDevice;
+                                                               MidiMessage msg = eventDialog.midiMessageForm.getMessage(Charset.defaultCharset());
+                                                               vmd.sendMidiMessage(msg);
+                                                       }
+                                               },
+                                               keyboardCenterPanel.keyboard.midiChComboboxModel.getSelectedChannel()
+                                       );
+                               }
+                       }) {
+                               { setMargin(ZERO_INSETS); }
+                       });
+               }});
+       }
+
+       public void setDarkMode(boolean isDark) {
+               Color col = isDark ? Color.black : null;
+               setBackground(col);
+               keyboardCenterPanel.setDarkMode(isDark);
+               keyboardChordPanel.setBackground(col);
+               keyboardSouthPanel.setBackground(col);
+               midiChannelButtons.setBackground(col);
+               midiChannelCombobox.setBackground(col);
+               midiChannelCombobox.comboBox.setBackground(col);
+               keySelecter.setBackground(col);
+               keySelecter.keysigCombobox.setBackground(col);
+               velocitySelecter.setBackground(col);
+               keyboardCenterPanel.keyboard.chordDisplay.setDarkMode(isDark);
+               sendEventButton.setBackground(col);
+       }
+
+}
\ No newline at end of file
diff --git a/src/camidion/chordhelper/pianokeyboard/PianoKeyboard.java b/src/camidion/chordhelper/pianokeyboard/PianoKeyboard.java
new file mode 100644 (file)
index 0000000..d851e51
--- /dev/null
@@ -0,0 +1,865 @@
+package camidion.chordhelper.pianokeyboard;
+
+import java.awt.BorderLayout;
+import java.awt.Color;
+import java.awt.Dimension;
+import java.awt.Graphics;
+import java.awt.Graphics2D;
+import java.awt.Point;
+import java.awt.Rectangle;
+import java.awt.event.ComponentAdapter;
+import java.awt.event.ComponentEvent;
+import java.awt.event.FocusEvent;
+import java.awt.event.FocusListener;
+import java.awt.event.InputEvent;
+import java.awt.event.KeyEvent;
+import java.awt.event.KeyListener;
+import java.awt.event.MouseEvent;
+import java.awt.event.MouseListener;
+import java.awt.event.MouseMotionListener;
+import java.util.LinkedList;
+import java.util.Vector;
+
+import javax.sound.midi.MidiChannel;
+import javax.swing.BoundedRangeModel;
+import javax.swing.DefaultBoundedRangeModel;
+import javax.swing.JComponent;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+import javax.swing.event.ListDataEvent;
+import javax.swing.event.ListDataListener;
+
+import camidion.chordhelper.ChordDisplayLabel;
+import camidion.chordhelper.anogakki.AnoGakkiPane;
+import camidion.chordhelper.chordmatrix.ChordMatrix;
+import camidion.chordhelper.mididevice.AbstractMidiChannelStatus;
+import camidion.chordhelper.mididevice.AbstractMidiStatus;
+import camidion.chordhelper.mididevice.AbstractVirtualMidiDevice;
+import camidion.chordhelper.mididevice.VirtualMidiDevice;
+import camidion.chordhelper.midieditor.DefaultMidiChannelComboBoxModel;
+import camidion.chordhelper.midieditor.MidiChannelButtonSelecter;
+import camidion.chordhelper.midieditor.MidiChannelComboBoxModel;
+import camidion.chordhelper.music.Chord;
+import camidion.chordhelper.music.Key;
+import camidion.chordhelper.music.MIDISpec;
+import camidion.chordhelper.music.Music;
+
+/**
+ * Piano Keyboard class for MIDI Chord Helper
+ *
+ * @author
+ *     Copyright (C) 2004-2013 Akiyoshi Kamide
+ *     http://www.yk.rim.or.jp/~kamide/music/chordhelper/
+ */
+public class PianoKeyboard extends JComponent {
+       /**
+        * 最小オクターブ幅
+        */
+       public static final int MIN_OCTAVE_WIDTH = 3;
+       /**
+        * 最大オクターブ幅(切り上げ)
+        */
+       public static final int MAX_OCTAVE_WIDTH = MIDISpec.MAX_NOTE_NO / 12 + 1;
+       /**
+        * 濃いピンク
+        */
+       public static final Color DARK_PINK = new Color(0xFF,0x50,0x80);
+
+       /** 1オクターブあたりの幅 */
+       private static float WIDTH_PER_OCTAVE = Music.SEMITONES_PER_OCTAVE * 10;
+       /** 白鍵のサイズ */
+       private Dimension       whiteKeySize;
+       /** 黒鍵のサイズ */
+       private Dimension       blackKeySize;
+       /** ダークモードならtrue */
+       boolean         isDark = false;
+
+       /** 表示中のすべてのピアノキー */
+       private PianoKey[] keys;
+       /** 黒鍵 */
+       private PianoKey[] blackKeys;
+       /** 白鍵 */
+       private PianoKey[] whiteKeys;
+       /**
+        * オクターブ範囲モデル
+        */
+       public BoundedRangeModel octaveRangeModel;
+       /**
+        * オクターブ幅モデル
+        */
+       public BoundedRangeModel octaveSizeModel;
+       /**
+        * ベロシティモデル
+        */
+       public BoundedRangeModel velocityModel = new DefaultBoundedRangeModel(64, 0, 0, 127);
+       /**
+        * MIDIチャンネル選択コンボボックスモデル
+        */
+       public MidiChannelComboBoxModel
+               midiChComboboxModel = new DefaultMidiChannelComboBoxModel();
+
+       /**
+        * ノートのリスト。配列の要素として使えるようクラス名を割り当てます。
+        */
+       private class NoteList extends LinkedList<Integer> {
+               // 何もすることはない
+       }
+       /**
+        * 選択マーク●がついている鍵を表すノートリスト
+        */
+       private NoteList selectedKeyNoteList = new NoteList();
+       /**
+        * 調号(スケール判定用)
+        */
+       private Key     keySignature = null;
+       /**
+        * 表示中のコード
+        */
+       private Chord   chord = null;
+       /**
+        * コードボタンマトリクス
+        */
+       public ChordMatrix chordMatrix;
+       /**
+        * コード表示部
+        */
+       public ChordDisplayLabel chordDisplay;
+       /**
+        * Innocence「あの楽器」
+        */
+       public AnoGakkiPane anoGakkiPane;
+       /**
+        * MIDIチャンネルをボタンで選択
+        */
+       public MidiChannelButtonSelecter midiChannelButtonSelecter;
+
+       private NoteList[] channelNotes = new NoteList[MIDISpec.MAX_CHANNELS];
+       private int[] pitchBendValues = new int[MIDISpec.MAX_CHANNELS];
+       private int[] pitchBendSensitivities = new int[MIDISpec.MAX_CHANNELS];
+       private int[] modulations = new int[MIDISpec.MAX_CHANNELS];
+
+       private class MidiChannelStatus extends AbstractMidiChannelStatus {
+               public MidiChannelStatus(int channel) {
+                       super(channel);
+                       channelNotes[channel] = new NoteList();
+                       pitchBendSensitivities[channel] = 2; // Default is wholetone = 2 semitones
+               }
+               @Override
+               public void fireRpnChanged() {
+                       if( dataFor != DATA_FOR_RPN ) return;
+
+                       // RPN (MSB) - Accept 0x00 only
+                       if( controllerValues[0x65] != 0x00 ) return;
+
+                       // RPN (LSB)
+                       switch( controllerValues[0x64] ) {
+                       case 0x00: // Pitch Bend Sensitivity
+                               if( controllerValues[0x06] == 0 ) return;
+                               pitchBendSensitivities[channel] = controllerValues[0x06];
+                               break;
+                       }
+               }
+               @Override
+               public void noteOff(int noteNumber, int velocity) {
+                       noteOff(noteNumber);
+               }
+               @Override
+               public void noteOff(int noteNumber) {
+                       keyOff( channel, noteNumber );
+                       if( chordMatrix != null ) {
+                               if( ! isRhythmPart() )
+                                       chordMatrix.note(false, noteNumber);
+                       }
+                       if( midiChannelButtonSelecter != null ) {
+                               midiChannelButtonSelecter.repaint();
+                       }
+               }
+               @Override
+               public void noteOn(int noteNumber, int velocity) {
+                       if( velocity <= 0 ) {
+                               noteOff(noteNumber); return;
+                       }
+                       keyOn( channel, noteNumber );
+                       if( midiChComboboxModel.getSelectedChannel() == channel ) {
+                               if( chordDisplay != null ) {
+                                       if( chordMatrix != null && chordMatrix.isPlaying() )
+                                               chordDisplay.clear();
+                                       else
+                                               chordDisplay.setNote(noteNumber, isRhythmPart());
+                               }
+                               if( anoGakkiPane != null ) {
+                                       PianoKey pienoKey = getPianoKey(noteNumber);
+                                       if( pienoKey != null )
+                                               anoGakkiPane.start(PianoKeyboard.this, pienoKey.indicator);
+                               }
+                       }
+                       if( chordMatrix != null ) {
+                               if( ! isRhythmPart() )
+                                       chordMatrix.note(true, noteNumber);
+                       }
+                       if( midiChannelButtonSelecter != null ) {
+                               midiChannelButtonSelecter.repaint();
+                       }
+               }
+               @Override
+               public void allNotesOff() {
+                       allKeysOff( channel, -1 );
+                       if( chordMatrix != null )
+                               chordMatrix.clearIndicators();
+               }
+               @Override
+               public void setPitchBend(int bend) {
+                       super.setPitchBend(bend);
+                       pitchBendValues[channel] = bend;
+                       repaintNotes();
+               }
+               @Override
+               public void resetAllControllers() {
+                       super.resetAllControllers();
+                       //
+                       // See also: Response to Reset All Controllers
+                       //     http://www.midi.org/about-midi/rp15.shtml
+                       //
+                       pitchBendValues[channel] = MIDISpec.PITCH_BEND_NONE;
+                       modulations[channel] = 0;
+                       repaintNotes();
+               }
+               @Override
+               public void controlChange(int controller, int value) {
+                       super.controlChange(controller,value);
+                       switch( controller ) {
+                       case 0x01: // Moduration (MSB)
+                               modulations[channel] = value;
+                               repaintNotes();
+                               break;
+                       }
+               }
+               private void repaintNotes() {
+                       if( midiChComboboxModel.getSelectedChannel() != channel
+                               || channelNotes[channel] == null
+                       )
+                               return;
+                       if( channelNotes[channel].size() > 0 || selectedKeyNoteList.size() > 0 )
+                               repaint();
+               }
+       }
+       /**
+        * この鍵盤の仮想MIDIデバイスです。
+        * ノートオンなどのMIDIメッセージを受け取り、画面に反映します。
+        */
+       public VirtualMidiDevice midiDevice = new AbstractVirtualMidiDevice() {
+               class MyInfo extends Info {
+                       protected MyInfo() {
+                               super("MIDI Keyboard","Unknown vendor","Software MIDI keyboard","");
+                       }
+               }
+               /**
+                * MIDIデバイス情報
+                */
+               protected MyInfo info;
+               @Override
+               public Info getDeviceInfo() { return info; }
+               {
+                       info = new MyInfo();
+                       // 受信してMIDIチャンネルの状態を管理する
+                       setReceiver(new AbstractMidiStatus() {
+                               {
+                                       for( int i=0; i<MIDISpec.MAX_CHANNELS; i++ )
+                                               add(new MidiChannelStatus(i));
+                               }
+                       });
+               }
+       };
+
+       /**
+        * 現在選択中のMIDIチャンネルを返します。
+        * @return 現在選択中のMIDIチャンネル
+        */
+       public MidiChannel getSelectedChannel() {
+               return midiDevice.getChannels()[midiChComboboxModel.getSelectedChannel()];
+       }
+       /**
+        * 現在選択中のMIDIチャンネルにノートオンメッセージを送出します。
+        * ベロシティ値は現在画面で設定中の値となります。
+        * @param noteNumber ノート番号
+        */
+       public void noteOn(int noteNumber) {
+               getSelectedChannel().noteOn(noteNumber, velocityModel.getValue());
+       }
+       /**
+        * 現在選択中のMIDIチャンネルにノートオフメッセージを送出します。
+        * ベロシティ値は現在画面で設定中の値となります。
+        * @param noteNumber ノート番号
+        */
+       public void noteOff(int noteNumber) {
+               getSelectedChannel().noteOff(noteNumber, velocityModel.getValue());
+       }
+
+       /**
+        * 1個のピアノ鍵盤を表す矩形
+        */
+       private class PianoKey extends Rectangle {
+               private boolean isBlack = false;
+               private int position = 0;
+               private String bindedKeyChar = null;
+               private Rectangle indicator;
+               private boolean outOfBounds = false;
+               public PianoKey(Point p, Dimension d, Dimension indicatorSize) {
+                       super(p,d);
+                       Point indicatorPosition = new Point(
+                               p.x + (d.width - indicatorSize.width) / 2,
+                               p.y + d.height - indicatorSize.height - indicatorSize.height / 2 + 2
+                       );
+                       indicator = new Rectangle(indicatorPosition, indicatorSize);
+               }
+               int getNote(int chromaticOffset) {
+                       int n = position + chromaticOffset;
+                       return (outOfBounds = ( n > MIDISpec.MAX_NOTE_NO )) ? -1 : n;
+               }
+               boolean paintKey(Graphics2D g2, boolean isPressed) {
+                       if(outOfBounds) return false;
+                       g2.fill3DRect(x, y, width, height, !isPressed);
+                       return true;
+               }
+               boolean paintKey(Graphics2D g2) {
+                       return paintKey(g2,false);
+               }
+               boolean paintKeyBinding(Graphics2D g2) {
+                       if( bindedKeyChar == null ) return false;
+                       g2.drawString( bindedKeyChar, x + width/3, indicator.y - 2 );
+                       return true;
+               }
+               boolean paintIndicator(Graphics2D g2, boolean is_small, int pitch_bend_value) {
+                       if( is_small ) {
+                               g2.fillOval(
+                                       indicator.x + indicator.width/4,
+                                       indicator.y + indicator.height/4 + 1,
+                                       indicator.width/2,
+                                       indicator.height/2
+                               );
+                       }
+                       else {
+                               int current_channel = midiChComboboxModel.getSelectedChannel();
+                               int sens = pitchBendSensitivities[current_channel];
+                               if( sens == 0 ) {
+                                       sens = 2;
+                               }
+                               int x_offset = (
+                                       7 * whiteKeySize.width * sens * (pitch_bend_value - MIDISpec.PITCH_BEND_NONE)
+                               ) / (12 * 8192);
+                               int additional_height = indicator.height * modulations[current_channel] / 256 ;
+                               int y_offset = additional_height / 2 ;
+                               g2.fillOval(
+                                       indicator.x + ( x_offset < 0 ? x_offset : 0 ),
+                                       indicator.y - y_offset,
+                                       indicator.width + ( x_offset < 0 ? -x_offset : x_offset ),
+                                       indicator.height + additional_height
+                               );
+                       }
+                       return true;
+               }
+               boolean paintIndicator(Graphics2D g2, boolean is_small) {
+                       return paintIndicator( g2, is_small, 0 );
+               }
+       }
+
+       private class MouseKeyListener
+               implements MouseListener, MouseMotionListener, KeyListener
+       {
+               private void pressed(int c, int n, InputEvent e) {
+                       keyOn(c,n);
+                       noteOn(n);
+                       firePianoKeyPressed(n,e);
+               }
+               private void released(int c, int n, InputEvent e) {
+                       keyOff(c,n);
+                       noteOff(n);
+                       firePianoKeyReleased(n,e);
+               }
+               @Override
+               public void mousePressed(MouseEvent e) {
+                       int n = getNote(e.getPoint());
+                       if( n < 0 ) return;
+                       int c = midiChComboboxModel.getSelectedChannel();
+                       if( channelNotes[c].contains(n) ) return;
+                       chord = null;
+                       pressed(c,n,e);
+                       requestFocusInWindow();
+                       repaint();
+               }
+               @Override
+               public void mouseReleased(MouseEvent e) {
+                       int c = midiChComboboxModel.getSelectedChannel();
+                       NoteList nl = channelNotes[c];
+                       if( ! nl.isEmpty() )
+                               released(c, nl.poll(), e);
+               }
+               @Override
+               public void mouseEntered(MouseEvent e) {
+               }
+               @Override
+               public void mouseExited(MouseEvent e) {
+               }
+               @Override
+               public void mouseDragged(MouseEvent e) {
+                       int n = getNote(e.getPoint());
+                       if( n < 0 ) return;
+                       int c = midiChComboboxModel.getSelectedChannel();
+                       NoteList nl = channelNotes[c];
+                       if( nl.contains(n) ) return;
+                       if( ! nl.isEmpty() )
+                               released(c, nl.poll(), e);
+                       pressed(c,n,e);
+               }
+               @Override
+               public void mouseMoved(MouseEvent e) {
+               }
+               @Override
+               public void mouseClicked(MouseEvent e) {
+               }
+               @Override
+               public void keyPressed(KeyEvent e) {
+                       int kc = e.getKeyCode();
+                       if( kc == KeyEvent.VK_LEFT || kc == KeyEvent.VK_KP_LEFT ) {
+                               octaveRangeModel.setValue( octaveRangeModel.getValue() - 1 );
+                               return;
+                       }
+                       else if( kc == KeyEvent.VK_RIGHT || kc == KeyEvent.VK_KP_RIGHT ) {
+                               octaveRangeModel.setValue( octaveRangeModel.getValue() + 1 );
+                               return;
+                       }
+                       int n = getNote(e); if( n < 0 ) return;
+                       int c = midiChComboboxModel.getSelectedChannel();
+                       if( channelNotes[c].contains(n) ) return;
+                       chord = null;
+                       pressed(c,n,e);
+               }
+               @Override
+               public void keyReleased(KeyEvent e) {
+                       int c = midiChComboboxModel.getSelectedChannel();
+                       int n = getNote(e);
+                       if( n < 0 || ! channelNotes[c].contains(n) ) return;
+                       released(c,n,e);
+               }
+               @Override
+               public void keyTyped(KeyEvent e) {
+               }
+       }
+
+       /**
+        * 新しいピアノキーボードを構築します。
+        */
+       public PianoKeyboard() {
+               setLayout(new BorderLayout());
+               setFocusable(true);
+               addFocusListener(new FocusListener() {
+                       public void focusGained(FocusEvent e) { repaint(); }
+                       public void focusLost(FocusEvent e)   { repaint(); }
+               });
+               MouseKeyListener mkl = new MouseKeyListener();
+               addMouseListener(mkl);
+               addMouseMotionListener(mkl);
+               addKeyListener(mkl);
+               int octaves = getPerferredOctaves();
+               octaveSizeModel = new DefaultBoundedRangeModel(
+                       octaves, 0, MIN_OCTAVE_WIDTH, MAX_OCTAVE_WIDTH
+               ) {{
+                       addChangeListener(new ChangeListener() {
+                               public void stateChanged(ChangeEvent e) {
+                                       fireOctaveResized(e);
+                                       octaveSizeChanged();
+                               }
+                       });
+               }};
+               octaveRangeModel = new DefaultBoundedRangeModel(
+                       (MAX_OCTAVE_WIDTH - octaves) / 2, octaves, 0, MAX_OCTAVE_WIDTH
+               ) {{
+                       addChangeListener(new ChangeListener() {
+                               public void stateChanged(ChangeEvent e) {
+                                       fireOctaveMoved(e);
+                                       checkOutOfBounds();
+                                       repaint();
+                               }
+                       });
+               }};
+               addComponentListener(new ComponentAdapter() {
+                       @Override
+                       public void componentResized(ComponentEvent e) {
+                               octaveSizeModel.setValue( getPerferredOctaves() );
+                               octaveSizeChanged();
+                       }
+               });
+               midiChComboboxModel.addListDataListener(
+                       new ListDataListener() {
+                               public void contentsChanged(ListDataEvent e) {
+                                       int c = midiChComboboxModel.getSelectedChannel();
+                                       for( int n : channelNotes[c] )
+                                               if( autoScroll(n) ) break;
+                                       repaint();
+                               }
+                               public void intervalAdded(ListDataEvent e) {}
+                               public void intervalRemoved(ListDataEvent e) {}
+                       }
+               );
+       }
+       public void paint(Graphics g) {
+               if( keys == null ) return;
+               Graphics2D g2 = (Graphics2D) g;
+               Dimension d = getSize();
+               //
+               // 鍵盤をクリア
+               g2.setBackground( getBackground() );
+               g2.clearRect( 0, 0, d.width, d.height );
+               //
+               // 白鍵を描画
+               g2.setColor( isDark ? Color.gray : Color.white );
+               for( PianoKey k : whiteKeys ) k.paintKey(g2);
+
+               NoteList notesArray[] = {
+                       (NoteList)selectedKeyNoteList.clone(),
+                       (NoteList)channelNotes[midiChComboboxModel.getSelectedChannel()].clone()
+               };
+               PianoKey key;
+               //
+               // ノートオン状態の白鍵を塗り重ねる
+               for( int n : notesArray[1] )
+                       if( (key=getPianoKey(n)) != null && !(key.isBlack) )
+                               key.paintKey(g2,true);
+               //
+               // 黒鍵を描画
+               g2.setColor(getForeground());
+               for( PianoKey k : blackKeys ) k.paintKey(g2);
+               //
+               // ノートオン状態の黒鍵を塗り重ねる
+               g2.setColor( Color.gray );
+               for( int n : notesArray[1] )
+                       if( (key=getPianoKey(n)) != null && key.isBlack )
+                               key.paintKey(g2,true);
+               //
+               // インジケータの表示
+               for( NoteList nl : notesArray ) {
+                       if( nl == null ) continue;
+                       for( Integer ni : nl ) {
+                               if( ni == null ) continue;
+                               int n = ni;
+                               if( (key=getPianoKey(n)) == null ) continue;
+                               boolean isOnScale = (keySignature == null || keySignature.isOnScale(n));
+                               int chordIndex;
+                               if( chord != null && (chordIndex = chord.indexOf(n)) >=0 ) {
+                                       g2.setColor(Chord.NOTE_INDEX_COLORS[chordIndex]);
+                               }
+                               else {
+                                       g2.setColor(isDark && isOnScale ? Color.pink : DARK_PINK);
+                               }
+                               int c = midiChComboboxModel.getSelectedChannel();
+                               key.paintIndicator(g2, false, pitchBendValues[c]);
+                               if( ! isOnScale ) {
+                                       g2.setColor(Color.white);
+                                       key.paintIndicator(g2, true);
+                               }
+                       }
+               }
+               if( isFocusOwner() ) {
+                       // Show PC-key binding
+                       for( PianoKey k : bindedKeys ) {
+                               g2.setColor(
+                                       k.isBlack ? Color.gray.brighter() :
+                                       isDark ? getForeground() :
+                                       getForeground().brighter()
+                               );
+                               k.paintKeyBinding(g2);
+                       }
+               }
+       }
+       //
+       protected void firePianoKeyPressed(int note_no, InputEvent event) {
+               Object[] listeners = listenerList.getListenerList();
+               for (int i = listeners.length-2; i>=0; i-=2) {
+                       if (listeners[i]==PianoKeyboardListener.class) {
+                               ((PianoKeyboardListener)listeners[i+1]).pianoKeyPressed(note_no,event);
+                       }
+               }
+       }
+       protected void firePianoKeyReleased(int note_no, InputEvent event) {
+               Object[] listeners = listenerList.getListenerList();
+               for (int i = listeners.length-2; i>=0; i-=2) {
+                       if (listeners[i]==PianoKeyboardListener.class) {
+                               ((PianoKeyboardListener)listeners[i+1]).pianoKeyReleased(note_no,event);
+                       }
+               }
+       }
+       protected void fireOctaveMoved(ChangeEvent event) {
+               Object[] listeners = listenerList.getListenerList();
+               for (int i = listeners.length-2; i>=0; i-=2) {
+                       if (listeners[i]==PianoKeyboardListener.class) {
+                               ((PianoKeyboardListener)listeners[i+1]).octaveMoved(event);
+                       }
+               }
+       }
+       protected void fireOctaveResized(ChangeEvent event) {
+               Object[] listeners = listenerList.getListenerList();
+               for (int i = listeners.length-2; i>=0; i-=2) {
+                       if (listeners[i]==PianoKeyboardListener.class) {
+                               ((PianoKeyboardListener)listeners[i+1]).octaveResized(event);
+                       }
+               }
+       }
+       /**
+        * 現在のオクターブ位置における、
+        * 指定のノート番号に対する1個のピアノキーを返します。
+        * @param noteNumber ノート番号
+        * @return ピアノキー(範囲外の場合 null)
+        */
+       private PianoKey getPianoKey(int noteNumber) {
+               int i = noteNumber - octaveRangeModel.getValue() * 12 ;
+               return i>=0 && i<keys.length ? keys[i]: null;
+       }
+       /**
+        * 指定の座標におけるノート番号を返します。
+        * @param point 座標
+        * @return ノート番号(範囲外の場合 -1)
+        */
+       private int getNote(Point point) {
+               PianoKey k = getPianoKeyAt(point);
+               return k==null ? -1 : k.getNote(getChromaticOffset());
+       }
+       /**
+        * 指定の座標における1個のピアノキーを返します。
+        * @param point 座標
+        * @return ピアノキー(範囲外の場合 null)
+        */
+       private PianoKey getPianoKeyAt(Point point) {
+               int indexWhite = point.x / whiteKeySize.width;
+               int indexOctave = indexWhite / 7;
+               int i = (indexWhite -= indexOctave * 7) * 2 + indexOctave * 12;
+               if( indexWhite >= 3 ) i--;
+               if( i < 0 || i > keys.length-1 )
+                       return null;
+               if( point.y > blackKeySize.height )
+                       return keys[i];
+               PianoKey k;
+               if( i > 0 ) {
+                       k = keys[i-1];
+                       if( k.isBlack && !(k.outOfBounds) && k.contains(point) )
+                               return k;
+               }
+               if( i < keys.length-1 ) {
+                       k = keys[i+1];
+                       if( k.isBlack && !(k.outOfBounds) && k.contains(point) )
+                               return k;
+               }
+               return keys[i];
+       }
+
+       private PianoKey[] bindedKeys;
+       private int             bindedKeyPosition;
+       private String  bindedKeyChars;
+       private PianoKey getPianoKey(KeyEvent e) {
+               int i = bindedKeyChars.indexOf(e.getKeyChar());
+               return i >= 0 ? keys[bindedKeyPosition + i] : null;
+       }
+       private int getNote(KeyEvent e) {
+               PianoKey k = getPianoKey(e);
+               return k==null ? -1 : k.getNote(getChromaticOffset());
+       }
+       private void changeKeyBinding(int from, String keyChars) {
+               PianoKey k;
+               bindedKeys = new PianoKey[(bindedKeyChars = keyChars).length()];
+               bindedKeyPosition = from;
+               for( int i = 0; i < bindedKeyChars.length(); i++ ) {
+                       bindedKeys[i] = k = keys[ bindedKeyPosition + i ];
+                       k.bindedKeyChar = bindedKeyChars.substring( i, i+1 );
+               }
+               repaint();
+       }
+
+       private void checkOutOfBounds() {
+               if( keys == null ) return;
+               for( PianoKey k : keys ) k.getNote(getChromaticOffset());
+       }
+       private void keyOff(int ch, int noteNumber) {
+               if( noteNumber < 0 || ch < 0 || ch >= channelNotes.length ) return;
+               channelNotes[ch].remove((Object)noteNumber);
+               if( ch == midiChComboboxModel.getSelectedChannel() )
+                       repaint();
+       }
+       private void keyOn(int ch, int noteNumber) {
+               if( noteNumber < 0 || ch < 0 || ch >= channelNotes.length ) return;
+               channelNotes[ch].add(noteNumber);
+               setSelectedNote(ch,noteNumber);
+       }
+       public boolean autoScroll(int noteNumber) {
+               if( octaveRangeModel == null || keys == null )
+                       return false;
+               int i = noteNumber - getChromaticOffset();
+               if( i < 0 ) {
+                       octaveRangeModel.setValue(
+                               octaveRangeModel.getValue() - (-i)/Music.SEMITONES_PER_OCTAVE - 1
+                       );
+                       return true;
+               }
+               if( i >= keys.length ) {
+                       octaveRangeModel.setValue(
+                               octaveRangeModel.getValue() + (i-keys.length)/Music.SEMITONES_PER_OCTAVE + 1
+                       );
+                       return true;
+               }
+               return false;
+       }
+       public void addPianoKeyboardListener(PianoKeyboardListener l) {
+               listenerList.add(PianoKeyboardListener.class, l);
+       }
+       public void removePianoKeyboardListener(PianoKeyboardListener l) {
+               listenerList.remove(PianoKeyboardListener.class, l);
+       }
+       int countKeyOn() {
+               return channelNotes[midiChComboboxModel.getSelectedChannel()].size();
+       }
+       public int countKeyOn(int ch) {
+               return channelNotes[ch].size();
+       }
+       void allKeysOff(int ch, int numMarks) {
+               if( ! selectedKeyNoteList.isEmpty() ) return;
+               switch(numMarks) {
+               case -1:
+                       selectedKeyNoteList = (NoteList)(channelNotes[ch].clone());
+                       break;
+               case  1:
+                       selectedKeyNoteList.add(
+                               channelNotes[ch].get(channelNotes[ch].size()-1)
+                       );
+                       break;
+               default: break;
+               }
+               channelNotes[ch].clear();
+               if( midiChComboboxModel.getSelectedChannel() == ch )
+                       repaint();
+       }
+       public void clear() {
+               selectedKeyNoteList.clear();
+               channelNotes[midiChComboboxModel.getSelectedChannel()].clear();
+               chord = null;
+               repaint();
+       }
+       int getNote() {
+               int current_channel = midiChComboboxModel.getSelectedChannel();
+               switch( channelNotes[current_channel].size() ) {
+               case 1: return channelNotes[current_channel].get(0);
+               case 0:
+                       if( selectedKeyNoteList.size() == 1 )
+                               return selectedKeyNoteList.get(0);
+                       return -1;
+               default:
+                       return -1;
+               }
+       }
+       public void setSelectedNote(int noteNumber) {
+               setSelectedNote(midiChComboboxModel.getSelectedChannel(), noteNumber);
+       }
+       void setSelectedNote(int ch, int note_no) {
+               if( ch != midiChComboboxModel.getSelectedChannel() )
+                       return;
+               selectedKeyNoteList.add(note_no);
+               int maxSel = (chord == null ? maxSelectable : chord.numberOfNotes());
+               while( selectedKeyNoteList.size() > maxSel )
+                       selectedKeyNoteList.poll();
+               if( !autoScroll(note_no) ) {
+                       // When autoScroll() returned false, stateChanged() not invoked - need repaint()
+                       repaint();
+               }
+       }
+       public Integer[] getSelectedNotes() {
+               return selectedKeyNoteList.toArray(new Integer[0]);
+       }
+       Chord getChord() { return chord; }
+       public void setChord(Chord c) {
+               chordDisplay.setChord(chord = c);
+       }
+       public void setKeySignature(Key ks) {
+               keySignature = ks;
+               repaint();
+       }
+       private int     maxSelectable = 1;
+       public void setMaxSelectable( int maxSelectable ) {
+               this.maxSelectable = maxSelectable;
+       }
+       int getMaxSelectable() { return maxSelectable; }
+       public int getChromaticOffset() {
+               return octaveRangeModel.getValue() * Music.SEMITONES_PER_OCTAVE ;
+       }
+       public int getOctaves() { return octaveSizeModel.getValue(); }
+       private int getPerferredOctaves() {
+               int octaves = Math.round( (float)getWidth() / WIDTH_PER_OCTAVE );
+               if( octaves > MAX_OCTAVE_WIDTH ) {
+                       octaves = MAX_OCTAVE_WIDTH;
+               }
+               else if( octaves < MIN_OCTAVE_WIDTH ) {
+                       octaves = MIN_OCTAVE_WIDTH;
+               }
+               return octaves;
+       }
+       private void octaveSizeChanged() {
+               int octaves = octaveSizeModel.getValue();
+               String defaultBindedKeyChars = "zsxdcvgbhnjm,l.;/\\]";
+               Dimension keyboard_size = getSize();
+               if( keyboard_size.width == 0 ) {
+                       return;
+               }
+               whiteKeySize = new Dimension(
+                       (keyboard_size.width - 1) / (octaves * 7 + 1),
+                       keyboard_size.height - 1
+               );
+               blackKeySize = new Dimension(
+                       whiteKeySize.width * 3 / 4,
+                       whiteKeySize.height * 3 / 5
+               );
+               Dimension indicatorSize = new Dimension(
+                       whiteKeySize.width / 2,
+                       whiteKeySize.height / 6
+               );
+               octaveRangeModel.setExtent( octaves );
+               octaveRangeModel.setValue( (MAX_OCTAVE_WIDTH - octaves) / 2 );
+               WIDTH_PER_OCTAVE = keyboard_size.width / octaves;
+               //
+               // Construct piano-keys
+               //
+               keys = new PianoKey[ octaves * 12 + 1 ];
+               Vector<PianoKey> vBlackKeys = new Vector<PianoKey>();
+               Vector<PianoKey> vWhiteKeys = new Vector<PianoKey>();
+               Point keyPoint = new Point(1,1);
+               PianoKey k;
+               int i, i12;
+               boolean is_CDE = true;
+               for( i = i12 = 0; i < keys.length; i++, i12++ ) {
+                       switch(i12) {
+                       case 12: is_CDE = true; i12 = 0; break;
+                       case  5: is_CDE = false; break;
+                       default: break;
+                       }
+                       keyPoint.x = whiteKeySize.width * (
+                               i / Music.SEMITONES_PER_OCTAVE * 7 + (i12+(is_CDE?1:2))/2
+                       );
+                       if( Music.isOnScale(i12,0) ) {
+                               k = new PianoKey( keyPoint, whiteKeySize, indicatorSize );
+                               k.isBlack = false;
+                               vWhiteKeys.add(k);
+                       }
+                       else {
+                               keyPoint.x -= ( (is_CDE?5:12) - i12 )/2 * blackKeySize.width / (is_CDE?3:4);
+                               k = new PianoKey( keyPoint, blackKeySize, indicatorSize );
+                               k.isBlack = true;
+                               vBlackKeys.add(k);
+                       }
+                       (keys[i] = k).position = i;
+               }
+               whiteKeys = vWhiteKeys.toArray(new PianoKey[1]);
+               blackKeys = vBlackKeys.toArray(new PianoKey[1]);
+               changeKeyBinding(((octaves - 1) / 2) * 12, defaultBindedKeyChars);
+               checkOutOfBounds();
+       }
+       //
+       void setDarkMode(boolean isDark) {
+               this.isDark = isDark;
+               setBackground( isDark ? Color.black : null );
+       }
+}
diff --git a/src/camidion/chordhelper/pianokeyboard/PianoKeyboardAdapter.java b/src/camidion/chordhelper/pianokeyboard/PianoKeyboardAdapter.java
new file mode 100644 (file)
index 0000000..c0426a1
--- /dev/null
@@ -0,0 +1,12 @@
+package camidion.chordhelper.pianokeyboard;
+
+import java.awt.event.InputEvent;
+
+import javax.swing.event.ChangeEvent;
+
+public abstract class PianoKeyboardAdapter implements PianoKeyboardListener {
+       public void pianoKeyPressed(int n, InputEvent e) { }
+       public void pianoKeyReleased(int n, InputEvent e) { }
+       public void octaveMoved(ChangeEvent e) { }
+       public void octaveResized(ChangeEvent e) { }
+}
\ No newline at end of file
diff --git a/src/camidion/chordhelper/pianokeyboard/PianoKeyboardListener.java b/src/camidion/chordhelper/pianokeyboard/PianoKeyboardListener.java
new file mode 100644 (file)
index 0000000..71b47c5
--- /dev/null
@@ -0,0 +1,13 @@
+package camidion.chordhelper.pianokeyboard;
+
+import java.awt.event.InputEvent;
+import java.util.EventListener;
+
+import javax.swing.event.ChangeEvent;
+
+public interface PianoKeyboardListener extends EventListener {
+       void pianoKeyPressed(int noteNumber, InputEvent event);
+       void pianoKeyReleased(int noteNumber, InputEvent event);
+       void octaveMoved(ChangeEvent e);
+       void octaveResized(ChangeEvent e);
+}
\ No newline at end of file
diff --git a/src/camidion/chordhelper/pianokeyboard/PianoKeyboardPanel.java b/src/camidion/chordhelper/pianokeyboard/PianoKeyboardPanel.java
new file mode 100644 (file)
index 0000000..a0b0db0
--- /dev/null
@@ -0,0 +1,65 @@
+package camidion.chordhelper.pianokeyboard;
+
+import java.awt.Color;
+import java.awt.Dimension;
+
+import javax.swing.Box;
+import javax.swing.BoxLayout;
+import javax.swing.JPanel;
+import javax.swing.JScrollBar;
+import javax.swing.JSlider;
+import javax.swing.event.ChangeEvent;
+
+public class PianoKeyboardPanel extends JPanel {
+       public PianoKeyboardPanel() {
+               keyboard = new PianoKeyboard() {
+                       {
+                               addPianoKeyboardListener(new PianoKeyboardAdapter() {
+                                       @Override
+                                       public void octaveResized(ChangeEvent e) {
+                                               octaveSelecter.setBlockIncrement(getOctaves());
+                                       }
+                               });
+                       }
+               };
+               octaveSelecter = new JScrollBar(JScrollBar.HORIZONTAL) {
+                       {
+                               setToolTipText("Octave position");
+                               setModel(keyboard.octaveRangeModel);
+                               setBlockIncrement(keyboard.getOctaves());
+                       }
+               };
+               octaveSizeSlider = new JSlider() {
+                       {
+                               setToolTipText("Octave size");
+                               setModel(keyboard.octaveSizeModel);
+                               setMinimumSize(new Dimension(100, 18));
+                               setMaximumSize(new Dimension(100, 18));
+                               setPreferredSize(new Dimension(100, 18));
+                       }
+               };
+               octaveBar = new JPanel() {
+                       {
+                               setLayout(new BoxLayout(this, BoxLayout.X_AXIS));
+                               add(octaveSelecter);
+                               add(Box.createHorizontalStrut(5));
+                               add(octaveSizeSlider);
+                       }
+               };
+               setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
+               add(octaveBar);
+               add(keyboard);
+               setAlignmentX((float)0.5);
+       }
+       public PianoKeyboard keyboard;
+       private JScrollBar octaveSelecter;
+       private JSlider octaveSizeSlider;
+       private JPanel octaveBar;
+       public void setDarkMode(boolean isDark) {
+               Color col = isDark ? Color.black : null;
+               octaveSelecter.setBackground( col );
+               octaveSizeSlider.setBackground( col );
+               octaveBar.setBackground( col );
+               keyboard.setDarkMode( isDark );
+       }
+}
\ No newline at end of file