OSDN Git Service

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