OSDN Git Service

リファクタリング
[midichordhelper/MIDIChordHelper.git] / src / camidion / chordhelper / ChordHelperApplet.java
1 package camidion.chordhelper;
2 import java.awt.BorderLayout;
3 import java.awt.Color;
4 import java.awt.Dimension;
5 import java.awt.Image;
6 import java.awt.Insets;
7 import java.awt.event.ComponentAdapter;
8 import java.awt.event.ComponentEvent;
9 import java.awt.event.InputEvent;
10 import java.awt.event.MouseAdapter;
11 import java.awt.event.MouseEvent;
12 import java.io.IOException;
13 import java.net.URI;
14 import java.net.URISyntaxException;
15 import java.net.URL;
16 import java.nio.charset.Charset;
17 import java.util.Arrays;
18
19 import javax.sound.midi.InvalidMidiDataException;
20 import javax.sound.midi.MetaMessage;
21 import javax.sound.midi.MidiSystem;
22 import javax.sound.midi.Sequence;
23 import javax.sound.midi.Sequencer;
24 import javax.swing.Box;
25 import javax.swing.BoxLayout;
26 import javax.swing.ImageIcon;
27 import javax.swing.JApplet;
28 import javax.swing.JButton;
29 import javax.swing.JComponent;
30 import javax.swing.JLayeredPane;
31 import javax.swing.JOptionPane;
32 import javax.swing.JPanel;
33 import javax.swing.JSlider;
34 import javax.swing.JSplitPane;
35 import javax.swing.JToggleButton;
36 import javax.swing.SwingUtilities;
37 import javax.swing.border.Border;
38
39 import camidion.chordhelper.anogakki.AnoGakkiPane;
40 import camidion.chordhelper.chorddiagram.CapoComboBoxModel;
41 import camidion.chordhelper.chorddiagram.ChordDiagram;
42 import camidion.chordhelper.chordmatrix.ChordButtonLabel;
43 import camidion.chordhelper.chordmatrix.ChordMatrix;
44 import camidion.chordhelper.chordmatrix.ChordMatrixListener;
45 import camidion.chordhelper.mididevice.MidiDeviceDialog;
46 import camidion.chordhelper.mididevice.MidiDeviceTreeModel;
47 import camidion.chordhelper.mididevice.MidiSequencerModel;
48 import camidion.chordhelper.mididevice.SequencerMeasureView;
49 import camidion.chordhelper.mididevice.SequencerTimeView;
50 import camidion.chordhelper.mididevice.VirtualMidiDevice;
51 import camidion.chordhelper.midieditor.Base64Dialog;
52 import camidion.chordhelper.midieditor.KeySignatureLabel;
53 import camidion.chordhelper.midieditor.MidiEventDialog;
54 import camidion.chordhelper.midieditor.MidiSequenceEditorDialog;
55 import camidion.chordhelper.midieditor.NewSequenceDialog;
56 import camidion.chordhelper.midieditor.PlaylistTableModel;
57 import camidion.chordhelper.midieditor.SequenceTickIndex;
58 import camidion.chordhelper.midieditor.SequenceTrackListTableModel;
59 import camidion.chordhelper.midieditor.TempoSelecter;
60 import camidion.chordhelper.midieditor.TimeSignatureSelecter;
61 import camidion.chordhelper.music.Chord;
62 import camidion.chordhelper.music.Key;
63 import camidion.chordhelper.music.MIDISpec;
64 import camidion.chordhelper.music.Range;
65 import camidion.chordhelper.pianokeyboard.MidiKeyboardPanel;
66 import camidion.chordhelper.pianokeyboard.PianoKeyboardAdapter;
67
68 /**
69  * MIDI Chord Helper - Circle-of-fifth oriented chord pad
70  * (アプレットクラス)
71  *
72  *      @auther
73  *              Copyright (C) 2004-2017 @きよし - Akiyoshi Kamide
74  *              http://www.yk.rim.or.jp/~kamide/music/chordhelper/
75  */
76 public class ChordHelperApplet extends JApplet {
77         /////////////////////////////////////////////////////////////////////
78         //
79         // JavaScript などからの呼び出しインターフェース
80         //
81         /////////////////////////////////////////////////////////////////////
82         /**
83          * 未保存の修正済み MIDI ファイルがあるかどうか調べます。
84          * @return 未保存の修正済み MIDI ファイルがあれば true
85          */
86         public boolean isModified() {
87                 return playlistModel.getSequenceModelList().stream().anyMatch(m -> m.isModified());
88         }
89         /**
90          * 指定された小節数の曲を、乱数で自動作曲してプレイリストへ追加し、再生します。
91          * @param measureLength 小節数
92          * @return 追加先のインデックス値(0から始まる)。追加できなかったときは -1
93          * @throws InvalidMidiDataException {@link Sequencer#setSequence(Sequence)} を参照
94          * @throws IllegalStateException MIDIシーケンサデバイスが閉じている場合
95          */
96         public int addRandomSongToPlaylist(int measureLength) throws InvalidMidiDataException {
97                 NewSequenceDialog d = midiEditor.newSequenceDialog;
98                 d.setRandomChordProgression(measureLength);
99                 int index = playlistModel.play(d.getMidiSequence(), d.getSelectedCharset());
100                 midiEditor.playlistTable.getSelectionModel().setSelectionInterval(index, index);
101                 return index;
102         }
103         /**
104          * URLで指定されたMIDIファイルをプレイリストへ追加します。
105          *
106          * <p>URL の最後の / より後ろの部分がファイル名として取り込まれます。
107          * 指定できる MIDI ファイルには、param タグの midi_file パラメータと同様の制限があります。
108          * </p>
109          * @param midiFileUrl 追加するMIDIファイルのURL
110          * @return 追加先のインデックス値(0から始まる)。追加できなかったときは -1
111          */
112         public int addToPlaylist(String midiFileUrl) {
113                 try {
114                         URL url = (new URI(midiFileUrl)).toURL();
115                         String filename = url.getFile().replaceFirst("^.*/","");
116                         Sequence sequence = MidiSystem.getSequence(url);
117                         Charset charset = MIDISpec.getCharsetOf(sequence);
118                         if( charset == null ) charset = Charset.defaultCharset();
119                         int index = playlistModel.add(sequence, charset, filename);
120                         midiEditor.playlistTable.getSelectionModel().setSelectionInterval(index, index);
121                         return index;
122                 } catch( URISyntaxException|IOException|InvalidMidiDataException e ) {
123                         JOptionPane.showMessageDialog(null, e, VersionInfo.NAME, JOptionPane.WARNING_MESSAGE);
124                 } catch( Exception e ) {
125                         JOptionPane.showMessageDialog(null, e, VersionInfo.NAME, JOptionPane.ERROR_MESSAGE);
126                 }
127                 return -1;
128         }
129         /**
130          * Base64 エンコードされた MIDI ファイルをプレイリストへ追加します。
131          *
132          * @param base64EncodedText Base64エンコードされたMIDIファイル
133          * @return 追加先のインデックス値(0から始まる)。追加できなかったときは -1
134          */
135         public int addToPlaylistBase64(String base64EncodedText) {
136                 return addToPlaylistBase64(base64EncodedText, null);
137         }
138         /**
139          * ファイル名を指定して、Base64エンコードされたMIDIファイルをプレイリストへ追加します。
140          *
141          * @param base64EncodedText Base64エンコードされたMIDIファイル
142          * @param filename ディレクトリ名を除いたファイル名
143          * @return 追加先のインデックス値(0から始まる)。追加できなかったときは -1
144          */
145         public int addToPlaylistBase64(String base64EncodedText, String filename) {
146                 Base64Dialog d = midiEditor.playlistTable.base64Dialog;
147                 d.setBase64Data(base64EncodedText, filename);
148                 int index = d.addToPlaylist();
149                 midiEditor.playlistTable.getSelectionModel().setSelectionInterval(index, index);
150                 return index;
151         }
152         /**
153          * プレイリスト上で現在選択されているMIDIシーケンスをシーケンサへロードして再生します。
154          */
155         public void play() { midiEditor.play(); }
156         /**
157          * 指定されたインデックス値が示すプレイリスト上のMIDIシーケンスをシーケンサへロードして再生します。
158          * @param index インデックス値(0から始まる)
159          */
160         public void play(int index) { midiEditor.play(index); }
161         /**
162          * シーケンサが実行中かどうかを返します。
163          * {@link Sequencer#isRunning()} の戻り値をそのまま返します。
164          *
165          * @return 実行中のときtrue
166          */
167         public boolean isRunning() { return sequencerModel.getSequencer().isRunning(); }
168         /**
169          * シーケンサが再生中かどうかを返します。
170          * @return 再生中のときtrue
171          */
172         public boolean isPlaying() { return isRunning(); }
173         /**
174          * 現在シーケンサにロードされているMIDIデータをBase64テキストに変換した結果を返します。
175          * @return MIDIデータをBase64テキストに変換した結果(シーケンサにロードされていない場合null)
176          * @throws IOException MIDIデータの読み込みに失敗した場合
177          */
178         public String getMidiDataBase64() throws IOException {
179                 SequenceTrackListTableModel s = sequencerModel.getSequenceTrackListTableModel();
180                 if( s == null ) return null;
181                 Base64Dialog d = midiEditor.playlistTable.base64Dialog;
182                 d.setMIDIData(s.getMIDIdata());
183                 return d.getBase64Data();
184         }
185         /**
186          * 現在シーケンサにロードされているMIDIファイルのファイル名を返します。
187          * @return MIDIファイル名(設定されていないときは空文字列、シーケンサにロードされていない場合null)
188          */
189         public String getMidiFilename() {
190                 SequenceTrackListTableModel s = sequencerModel.getSequenceTrackListTableModel();
191                 if( s == null ) return null;
192                 String fn = s.getFilename();
193                 return fn == null ? "" : fn;
194         }
195         /**
196          * オクターブ位置を設定します。
197          * @param octavePosition オクターブ位置(デフォルト:4)
198          */
199         public void setOctavePosition(int octavePosition) {
200                 keyboardPanel.keyboardCenterPanel.keyboard.octaveRangeModel.setValue(octavePosition);
201         }
202         /**
203          * 操作対象のMIDIチャンネルを変更します。
204          * @param ch チャンネル番号 - 1(チャンネル1のとき0、デフォルトは0)
205          */
206         public void setChannel(int ch) {
207                 keyboardPanel.keyboardCenterPanel.keyboard.midiChComboboxModel.setSelectedChannel(ch);
208         }
209         /**
210          * 操作対象のMIDIチャンネルを返します。
211          * @return 操作対象のMIDIチャンネル
212          */
213         public int getChannel() {
214                 return keyboardPanel.keyboardCenterPanel.keyboard.midiChComboboxModel.getSelectedChannel();
215         }
216         /**
217          * 操作対象のMIDIチャンネルに対してプログラム(音色)を設定します。
218          * @param program 音色(0~127:General MIDI に基づく)
219          */
220         public void programChange(int program) {
221                 keyboardPanel.keyboardCenterPanel.keyboard.getSelectedChannel().programChange(program);
222         }
223         /**
224          * 操作対象のMIDIチャンネルに対してプログラム(音色)を設定します。
225          * 内部的には {@link #programChange(int)} を呼び出しているだけです。
226          * @param program 音色(0~127:General MIDI に基づく)
227          */
228         public void setProgram(int program) { programChange(program); }
229         /**
230          * 自動転回モードを変更します。初期値は true です。
231          * @param isAuto true:自動転回を行う false:自動転回を行わない
232          */
233         public void setAutoInversion(boolean isAuto) {
234                 inversionOmissionButton.setAutoInversion(isAuto);
235         }
236         /**
237          * 省略したい構成音を指定します。
238          * @param index
239          * <ul>
240          * <li>-1:省略しない(デフォルト)</li>
241          * <li>0:ルート音を省略</li>
242          * <li>1:三度を省略</li>
243          * <li>2:五度を省略</li>
244          * </ul>
245          */
246         public void setOmissionNoteIndex(int index) {
247                 inversionOmissionButton.setOmissionNoteIndex(index);
248         }
249         /**
250          * コードダイアグラムの表示・非表示を切り替えます。
251          * @param isVisible 表示するときtrue
252          */
253         public void setChordDiagramVisible(boolean isVisible) {
254                 keyboardSplitPane.resetToPreferredSizes();
255                 if( ! isVisible ) keyboardSplitPane.setDividerLocation((double)1.0);
256         }
257         /**
258          * コードダイヤグラムをギターモードに変更します。
259          * 初期状態ではウクレレモードになっています。
260          */
261         public void setChordDiagramForGuitar() {
262                 chordDiagram.setTargetInstrument(ChordDiagram.Instrument.Guitar);
263         }
264         /**
265          * ダークモード(暗い表示)と明るい表示とを切り替えます。
266          * @param isDark ダークモードのときtrue、明るい表示のときfalse(デフォルト)
267          */
268         public void setDarkMode(boolean isDark) {
269                 if( darkModeToggleButton.isSelected() != isDark )
270                         darkModeToggleButton.doClick();
271         }
272         /**
273          * バージョン情報
274          */
275         public static class VersionInfo {
276                 public static final String NAME = "MIDI Chord Helper";
277                 public static final String VERSION = "Ver.20170722.1";
278                 public static final String COPYRIGHT = "Copyright (C) 2004-2017";
279                 public static final String AUTHER = "@きよし - Akiyoshi Kamide";
280                 public static final String URL = "http://www.yk.rim.or.jp/~kamide/music/chordhelper/";
281         }
282         @Override
283         public String getAppletInfo() {
284                 return String.join(" ",
285                                 VersionInfo.NAME, VersionInfo.VERSION,
286                                 VersionInfo.COPYRIGHT ,VersionInfo.AUTHER, VersionInfo.URL);
287         }
288         /** ボタンの余白を詰めたいときに setMargin() の引数に指定するインセット */
289         public static final Insets ZERO_INSETS = new Insets(0,0,0,0);
290
291         // MIDIエディタダイアログ(Javaアプリメインからもアクセスできるようprivateにしていない)
292         MidiSequenceEditorDialog midiEditor;
293
294         // GUIコンポーネント(内部保存用)
295         private PlaylistTableModel playlistModel;
296         private MidiSequencerModel sequencerModel;
297         private ChordMatrix chordMatrix;
298         private JPanel keyboardSequencerPanel;
299         private JPanel chordGuide;
300         private JSplitPane mainSplitPane;
301         private JSplitPane keyboardSplitPane;
302         private ChordButtonLabel enterButtonLabel;
303         private ChordTextField  lyricDisplay;
304         private MidiKeyboardPanel keyboardPanel;
305         private InversionAndOmissionLabel inversionOmissionButton;
306         private JToggleButton darkModeToggleButton;
307         private ChordDiagram chordDiagram;
308         private KeySignatureLabel keysigLabel;
309         private AnoGakkiPane anoGakkiPane;
310         private JToggleButton anoGakkiToggleButton;
311         private MidiDeviceTreeModel deviceTreeModel;
312
313         // アイコン画像
314         private Image iconImage;
315         public Image getIconImage() { return iconImage; }
316         private ImageIcon imageIcon;
317         public ImageIcon getImageIcon() { return imageIcon; }
318
319         @Override
320         public void init() {
321                 // アイコン画像のロード
322                 URL imageIconUrl = getClass().getResource("midichordhelper.png");
323                 if( imageIconUrl != null ) {
324                         iconImage = (imageIcon = new ImageIcon(imageIconUrl)).getImage();
325                 }
326                 AboutMessagePane about = new AboutMessagePane(imageIcon);
327                 //
328                 // 背景色の取得
329                 Color rootPaneDefaultBgcolor = getContentPane().getBackground();
330                 //
331                 // コードダイアグラム、コードボタン、ピアノ鍵盤、およびそれらの仮想MIDIデバイスを生成
332                 CapoComboBoxModel capoComboBoxModel = new CapoComboBoxModel();
333                 chordDiagram = new ChordDiagram(capoComboBoxModel);
334                 chordMatrix = new ChordMatrix(capoComboBoxModel) {
335                         private void clearChord() {
336                                 chordOn();
337                                 keyboardPanel.keyboardCenterPanel.keyboard.chordDisplay.clear();
338                                 chordDiagram.clear();
339                         }
340                         {
341                                 addChordMatrixListener(new ChordMatrixListener(){
342                                         @Override
343                                         public void keySignatureChanged() {
344                                                 keyboardPanel.setCapoKey(getKeySignatureCapo());
345                                         }
346                                         @Override
347                                         public void chordChanged() { chordOn(); }
348                                 });
349                                 capoSelecter.checkbox.addItemListener(e->clearChord());
350                                 capoSelecter.valueSelecter.addActionListener(e->clearChord());
351                         }
352                 };
353                 keysigLabel = new KeySignatureLabel() {{
354                         addMouseListener(new MouseAdapter() {
355                                 @Override
356                                 public void mousePressed(MouseEvent e) {
357                                         chordMatrix.setKeySignature(getKey());
358                                 }
359                         });
360                 }};
361                 keyboardPanel = new MidiKeyboardPanel(chordMatrix) {{
362                         keyboardCenterPanel.keyboard.addPianoKeyboardListener(new PianoKeyboardAdapter() {
363                                 @Override
364                                 public void pianoKeyPressed(int n, InputEvent e) { chordDiagram.clear(); }
365                         });
366                         keySelecter.getKeysigCombobox().addActionListener(e->chordMatrix.setKeySignature(
367                                 keySelecter.getSelectedKey().transposedKey(-chordMatrix.capoSelecter.getCapo())
368                         ));
369                         keyboardCenterPanel.keyboard.setPreferredSize(new Dimension(571, 80));
370                 }};
371                 // MIDIデバイスツリーの構築
372                 VirtualMidiDevice guiMidiDevice = keyboardPanel.keyboardCenterPanel.keyboard.midiDevice;
373                 deviceTreeModel = new MidiDeviceTreeModel(guiMidiDevice);
374                 //
375                 // MIDIデバイスツリーを操作するダイアログの構築
376                 MidiDeviceDialog midiDeviceDialog = new MidiDeviceDialog(deviceTreeModel);
377                 midiDeviceDialog.setIconImage(iconImage);
378                 //
379                 // MIDIイベントダイアログの構築
380                 MidiEventDialog eventDialog = new MidiEventDialog();
381                 keyboardPanel.setEventDialog(eventDialog);
382                 //
383                 // MIDIエディタダイアログの構築・MIDIファイルのドロップ受付開始
384                 sequencerModel = deviceTreeModel.getSequencerModel();
385                 playlistModel = new PlaylistTableModel(sequencerModel);
386                 midiEditor = new MidiSequenceEditorDialog(playlistModel, eventDialog, guiMidiDevice, midiDeviceDialog.getOpenAction());
387                 midiEditor.setIconImage(iconImage);
388                 setTransferHandler(midiEditor.transferHandler);
389                 //
390                 // 歌詞表示/コード入力フィールド
391                 (lyricDisplay = new ChordTextField(sequencerModel)).addActionListener(
392                         e->chordMatrix.setSelectedChord(e.getActionCommand().trim().split("[ \t\r\n]")[0])
393                 );
394                 Border lyricDisplayDefaultBorder = lyricDisplay.getBorder();
395                 Color lyricDisplayDefaultBgcolor = lyricDisplay.getBackground();
396                 //
397                 // メタイベント(テンポ・拍子・調号)を受信して表示するリスナーを登録
398                 TempoSelecter tempoSelecter = new TempoSelecter() {{ setEditable(false); }};
399                 TimeSignatureSelecter timesigSelecter = new TimeSignatureSelecter() {{ setEditable(false); }};
400                 sequencerModel.getSequencer().addMetaEventListener(msg->{
401                         switch(msg.getType()) {
402                         case 0x51: // Tempo (3 bytes) - テンポ
403                                 SwingUtilities.invokeLater(()->tempoSelecter.setTempo(msg.getData()));
404                                 break;
405                         case 0x58: // Time signature (4 bytes) - 拍子
406                                 SwingUtilities.invokeLater(()->timesigSelecter.setValue(msg.getData()));
407                                 break;
408                         case 0x59: // Key signature (2 bytes) : 調号
409                                 SwingUtilities.invokeLater(()->setKeySignature(new Key(msg.getData())));
410                                 break;
411                         }
412                 });
413                 // 再生時間位置の移動、シーケンス名の変更、またはシーケンスの入れ替えが発生したときに呼び出されるリスナーを登録
414                 SongTitleLabel songTitleLabel = new SongTitleLabel();
415                 sequencerModel.addChangeListener(e->{
416                         Sequencer sequencer = sequencerModel.getSequencer();
417                         chordMatrix.setPlaying(sequencer.isRunning());
418                         SequenceTrackListTableModel sequenceModel = sequencerModel.getSequenceTrackListTableModel();
419                         if( sequenceModel == null ) {
420                                 songTitleLabel.clear();
421                                 timesigSelecter.clear();
422                                 tempoSelecter.clear();
423                                 keysigLabel.clear();
424                                 return;
425                         }
426                         int songIndex = playlistModel.getSequenceModelList().indexOf(sequenceModel);
427                         songTitleLabel.setSongTitle(songIndex, sequenceModel);
428                         SequenceTickIndex tickIndex = sequenceModel.getSequenceTickIndex();
429                         long tickPosition = sequencer.getTickPosition();
430                         tickIndex.tickToMeasure(tickPosition);
431                         chordMatrix.setBeat(tickIndex);
432                         if( sequencerModel.getValueIsAdjusting() || ! (sequencer.isRunning() || sequencer.isRecording()) ) {
433                                 timesigSelecter.setValueAt(tickIndex, tickPosition);
434                                 tempoSelecter.setTempoAt(tickIndex, tickPosition);
435                                 setKeySignatureAt(tickIndex, tickPosition);
436                         }
437                 });
438                 sequencerModel.fireStateChanged();
439                 chordGuide = new JPanel() {{
440                         setLayout(new BoxLayout(this, BoxLayout.X_AXIS));
441                         add( Box.createHorizontalStrut(2) );
442                         add( chordMatrix.chordGuide );
443                         add( Box.createHorizontalStrut(2) );
444                         add( lyricDisplay );
445                         add( Box.createHorizontalStrut(2) );
446                         add( enterButtonLabel = new ChordButtonLabel("Enter",chordMatrix) {{
447                                 addMouseListener(new MouseAdapter() {
448                                         public void mousePressed(MouseEvent event) {
449                                                 boolean rightClicked = (event.getModifiersEx() & InputEvent.BUTTON3_DOWN_MASK) != 0;
450                                                 String selectedChord = rightClicked ? lyricDisplay.getText() : null;
451                                                 chordMatrix.setSelectedChord(selectedChord);
452                                         }
453                                 });
454                         }});
455                         add( Box.createHorizontalStrut(5) );
456                         add( chordMatrix.chordDisplay );
457                         add( Box.createHorizontalStrut(5) );
458                         add( darkModeToggleButton = new JToggleButton(new ButtonIcon(ButtonIcon.DARK_MODE_ICON)) {{
459                                 setMargin(ZERO_INSETS);
460                                 addItemListener(e->{
461                                         boolean isDark = darkModeToggleButton.isSelected();
462                                         Color col = isDark ? Color.black : null;
463                                         getContentPane().setBackground(isDark ? Color.black : rootPaneDefaultBgcolor);
464                                         mainSplitPane.setBackground(col);
465                                         keyboardSplitPane.setBackground(col);
466                                         enterButtonLabel.setDarkMode(isDark);
467                                         chordGuide.setBackground(col);
468                                         lyricDisplay.setBorder(isDark ? null : lyricDisplayDefaultBorder);
469                                         lyricDisplay.setBackground(isDark ?
470                                                 chordMatrix.darkModeColorset.backgrounds[2] :
471                                                 lyricDisplayDefaultBgcolor
472                                         );
473                                         lyricDisplay.setForeground(isDark ? Color.white : null);
474                                         inversionOmissionButton.setBackground(col);
475                                         anoGakkiToggleButton.setBackground(col);
476                                         keyboardSequencerPanel.setBackground(col);
477                                         chordDiagram.setBackground(col);
478                                         chordDiagram.titleLabel.setDarkMode(isDark);
479                                         chordMatrix.setDarkMode(isDark);
480                                         keyboardPanel.setDarkMode(isDark);
481                                 });
482                                 setToolTipText("Light / Dark - 明かりを点灯/消灯");
483                                 setBorder(null);
484                         }});
485                         add( Box.createHorizontalStrut(5) );
486                         add( anoGakkiToggleButton = new JToggleButton(new ButtonIcon(ButtonIcon.ANO_GAKKI_ICON)) {{
487                                 setOpaque(false);
488                                 setMargin(ZERO_INSETS);
489                                 setBorder(null);
490                                 setToolTipText("あの楽器");
491                                 addItemListener(e->
492                                         keyboardPanel.keyboardCenterPanel.keyboard.anoGakkiPane
493                                         = anoGakkiToggleButton.isSelected() ? anoGakkiPane : null
494                                 );
495                         }} );
496                         add( Box.createHorizontalStrut(5) );
497                         add( inversionOmissionButton = new InversionAndOmissionLabel() );
498                         add( Box.createHorizontalStrut(5) );
499                         add( chordMatrix.capoSelecter );
500                         add( Box.createHorizontalStrut(2) );
501                 }};
502                 keyboardSequencerPanel = new JPanel() {{
503                         setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
504                         add(chordGuide);
505                         add(Box.createVerticalStrut(5));
506                         add(keyboardSplitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, keyboardPanel, chordDiagram) {{
507                                 setOneTouchExpandable(true);
508                                 setResizeWeight(1.0);
509                                 setAlignmentX((float)0.5);
510                         }});
511                         add(Box.createVerticalStrut(5));
512                         add(new JPanel() {{
513                                 setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
514                                 add(new JPanel() {{
515                                         setLayout(new BoxLayout(this, BoxLayout.X_AXIS));
516                                         add(Box.createHorizontalStrut(12));
517                                         add(keysigLabel);
518                                         add(Box.createHorizontalStrut(12));
519                                         add(timesigSelecter);
520                                         add(Box.createHorizontalStrut(12));
521                                         add(tempoSelecter);
522                                         add(Box.createHorizontalStrut(12));
523                                         add(new SequencerMeasureView(sequencerModel));
524                                         add(Box.createHorizontalStrut(12));
525                                         add(songTitleLabel);
526                                         add(Box.createHorizontalStrut(12));
527                                         add(new JButton(midiEditor.openAction) {{ setMargin(ZERO_INSETS); }});
528                                 }});
529                                 add(new JPanel() {{
530                                         setLayout(new BoxLayout(this, BoxLayout.X_AXIS));
531                                         add(Box.createHorizontalStrut(10));
532                                         add(new JSlider(sequencerModel));
533                                         add(new SequencerTimeView(sequencerModel));
534                                         add(Box.createHorizontalStrut(5));
535                                         add(new JButton(playlistModel.getMoveToTopAction()) {{ setMargin(ZERO_INSETS); }});
536                                         add(new JButton(sequencerModel.getMoveBackwardAction()) {{ setMargin(ZERO_INSETS); }});
537                                         add(new JToggleButton(sequencerModel.getStartStopAction()));
538                                         add(new JButton(sequencerModel.getMoveForwardAction()) {{ setMargin(ZERO_INSETS); }});
539                                         add(new JButton(playlistModel.getMoveToBottomAction()) {{ setMargin(ZERO_INSETS); }});
540                                         add(new JToggleButton(playlistModel.getToggleRepeatAction()) {{ setMargin(ZERO_INSETS); }});
541                                         add( Box.createHorizontalStrut(10) );
542                                 }});
543                                 add(new JPanel() {{
544                                         add(new JButton(midiDeviceDialog.getOpenAction()));
545                                         add(new JButton(about.getOpenAction()));
546                                 }});
547                         }});
548                 }};
549                 setContentPane(new JLayeredPane() {
550                         {
551                                 add(anoGakkiPane = new AnoGakkiPane(), JLayeredPane.PALETTE_LAYER);
552                                 addComponentListener(new ComponentAdapter() {
553                                         @Override
554                                         public void componentResized(ComponentEvent e) { adjustSize(); }
555                                         @Override
556                                         public void componentShown(ComponentEvent e) { adjustSize(); }
557                                         private void adjustSize() { anoGakkiPane.setBounds(getBounds()); }
558                                 });
559                                 setLayout(new BorderLayout());
560                                 setOpaque(true);
561                                 add(mainSplitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT, chordMatrix, keyboardSequencerPanel){
562                                         {
563                                                 setResizeWeight(0.5);
564                                                 setAlignmentX((float)0.5);
565                                                 setDividerSize(5);
566                                         }
567                                 });
568                         }
569                 });
570                 setPreferredSize(new Dimension(750,470));
571         }
572         @Override
573         public void destroy() { deviceTreeModel.forEach(m -> m.close()); }
574         @Override
575         public void start() {
576                 //
577                 // コードボタンで設定されている現在の調をピアノキーボードに伝える
578                 chordMatrix.fireKeySignatureChanged();
579                 //
580                 // アプレットのパラメータにMIDIファイルのURLが指定されていたらそれを再生する
581                 String midiUrl = getParameter("midi_file");
582                 if( midiUrl != null ) try {
583                         play(addToPlaylist(midiUrl));
584                 } catch (Exception e) {
585                         JOptionPane.showMessageDialog(null, e, VersionInfo.NAME, JOptionPane.WARNING_MESSAGE);
586                 }
587         }
588         @Override
589         public void stop() { sequencerModel.stop(); }
590
591         private void setKeySignature(Key key) {
592                 keysigLabel.setKey(key);
593                 chordMatrix.setKeySignature(key);
594         }
595         private void setKeySignatureAt(SequenceTickIndex tickIndex, long tickPosition) {
596                 MetaMessage msg = tickIndex.lastMetaMessageAt(
597                         SequenceTickIndex.MetaMessageType.KEY_SIGNATURE,
598                         tickPosition
599                 );
600                 if(msg == null) keysigLabel.clear(); else setKeySignature(new Key(msg.getData()));
601         }
602
603         private int[] chordOnNotes = null;
604         /**
605          * 和音を発音します。
606          * <p>この関数を直接呼ぶとアルペジオが効かないので、
607          * chordMatrix.setSelectedChord() を使うことを推奨。
608          * </p>
609          */
610         public void chordOn() {
611                 Chord playChord = chordMatrix.getSelectedChord();
612                 if(
613                         chordOnNotes != null &&
614                         chordMatrix.getNoteIndex() < 0 &&
615                         (! chordMatrix.isDragged() || playChord == null)
616                 ) {
617                         // コードが鳴っている状態で、新たなコードを鳴らそうとしたり、
618                         // もう鳴らさないという信号が来た場合は、今鳴っている音を止める。
619                         Arrays.stream(chordOnNotes).forEach(n->keyboardPanel.keyboardCenterPanel.keyboard.noteOff(n));
620                         chordOnNotes = null;
621                 }
622                 if( playChord == null ) {
623                         // もう鳴らさないので、歌詞表示に通知して終了
624                         if( lyricDisplay != null ) lyricDisplay.appendChord(null);
625                         return;
626                 }
627                 // あの楽器っぽい表示
628                 if( keyboardPanel.keyboardCenterPanel.keyboard.anoGakkiPane != null ) {
629                         JComponent btn = chordMatrix.getSelectedButton();
630                         if( btn != null ) anoGakkiPane.start(chordMatrix, btn.getBounds());
631                 }
632                 // コードボタンからのコードを、カポつき演奏キーからオリジナルキーへ変換
633                 Key originalKey = chordMatrix.getKeySignatureCapo();
634                 Chord originalChord = playChord.transposedNewChord(
635                         chordMatrix.capoSelecter.getCapo(),
636                         chordMatrix.getKeySignature()
637                 );
638                 // 変換後のコードをキーボード画面に設定
639                 keyboardPanel.keyboardCenterPanel.keyboard.setChord(originalChord);
640                 //
641                 // 音域を決める。これにより鳴らす音が確定する。
642                 Range chordRange = new Range(
643                         keyboardPanel.keyboardCenterPanel.keyboard.getChromaticOffset() + 10 +
644                         ( keyboardPanel.keyboardCenterPanel.keyboard.getOctaves() / 4 ) * 12,
645                         inversionOmissionButton.isAutoInversionMode() ?
646                         keyboardPanel.keyboardCenterPanel.keyboard.getChromaticOffset() + 21 :
647                         keyboardPanel.keyboardCenterPanel.keyboard.getChromaticOffset() + 33,
648                         -2,
649                         inversionOmissionButton.isAutoInversionMode()
650                 );
651                 int[] notes = originalChord.toNoteArray(chordRange, originalKey);
652                 //
653                 // 前回鳴らしたコード構成音を覚えておく
654                 int[] prevChordOnNotes = null;
655                 if( chordMatrix.isDragged() || chordMatrix.getNoteIndex() >= 0 )
656                         prevChordOnNotes = Arrays.copyOf(chordOnNotes, chordOnNotes.length);
657                 //
658                 // 次に鳴らす構成音を決める
659                 chordOnNotes = new int[notes.length];
660                 int i = 0;
661                 for( int n : notes ) {
662                         if( inversionOmissionButton.getOmissionNoteIndex() == i ) {
663                                 i++; continue;
664                         }
665                         chordOnNotes[i++] = n;
666                         //
667                         // その音が今鳴っているか調べる
668                         boolean isNoteOn = prevChordOnNotes != null && Arrays.stream(prevChordOnNotes).anyMatch(prevN -> prevN == n);
669                         // すでに鳴っているのに単音を鳴らそうとする場合、
670                         // 鳴らそうとしている音を一旦止める。
671                         if( isNoteOn && chordMatrix.getNoteIndex() >= 0 &&
672                                 notes[chordMatrix.getNoteIndex()] - n == 0
673                         ) {
674                                 keyboardPanel.keyboardCenterPanel.keyboard.noteOff(n);
675                                 isNoteOn = false;
676                         }
677                         // その音が鳴っていなかったら鳴らす。
678                         if( ! isNoteOn ) keyboardPanel.keyboardCenterPanel.keyboard.noteOn(n);
679                 }
680                 //
681                 // コードを表示
682                 keyboardPanel.keyboardCenterPanel.keyboard.setChord(originalChord);
683                 chordMatrix.chordDisplay.setChord(playChord);
684                 //
685                 // コードダイアグラム用にもコードを表示
686                 Chord diagramChord;
687                 int chordDiagramCapo = chordDiagram.capoSelecterView.getCapo();
688                 if( chordDiagramCapo == chordMatrix.capoSelecter.getCapo() )
689                         diagramChord = playChord;
690                 else
691                         diagramChord = originalChord.transposedNewChord(-chordDiagramCapo, originalKey);
692                 chordDiagram.setChord(diagramChord);
693                 if( chordDiagram.recordTextButton.isSelected() )
694                         lyricDisplay.appendChord(diagramChord);
695         }
696
697 }
698