OSDN Git Service

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