OSDN Git Service

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