OSDN Git Service

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