OSDN Git Service

MIDI Editor の表のセル幅調整
[midichordhelper/MIDIChordHelper.git] / src / MIDIEditor.java
1 \r
2 import java.awt.Component;\r
3 import java.awt.Container;\r
4 import java.awt.Dimension;\r
5 import java.awt.FlowLayout;\r
6 import java.awt.Insets;\r
7 import java.awt.datatransfer.DataFlavor;\r
8 import java.awt.datatransfer.Transferable;\r
9 import java.awt.dnd.DnDConstants;\r
10 import java.awt.dnd.DropTarget;\r
11 import java.awt.dnd.DropTargetDragEvent;\r
12 import java.awt.dnd.DropTargetDropEvent;\r
13 import java.awt.dnd.DropTargetEvent;\r
14 import java.awt.dnd.DropTargetListener;\r
15 import java.awt.event.ActionEvent;\r
16 import java.awt.event.ActionListener;\r
17 import java.awt.event.ItemEvent;\r
18 import java.awt.event.ItemListener;\r
19 import java.awt.event.MouseEvent;\r
20 import java.io.ByteArrayInputStream;\r
21 import java.io.ByteArrayOutputStream;\r
22 import java.io.File;\r
23 import java.io.FileInputStream;\r
24 import java.io.FileOutputStream;\r
25 import java.io.IOException;\r
26 import java.io.InputStream;\r
27 import java.net.URI;\r
28 import java.net.URISyntaxException;\r
29 import java.net.URL;\r
30 import java.security.AccessControlException;\r
31 import java.util.ArrayList;\r
32 import java.util.EventObject;\r
33 import java.util.HashMap;\r
34 import java.util.List;\r
35 import java.util.Map;\r
36 import java.util.Vector;\r
37 \r
38 import javax.sound.midi.InvalidMidiDataException;\r
39 import javax.sound.midi.MetaEventListener;\r
40 import javax.sound.midi.MetaMessage;\r
41 import javax.sound.midi.MidiChannel;\r
42 import javax.sound.midi.MidiEvent;\r
43 import javax.sound.midi.MidiMessage;\r
44 import javax.sound.midi.MidiSystem;\r
45 import javax.sound.midi.Sequence;\r
46 import javax.sound.midi.Sequencer;\r
47 import javax.sound.midi.ShortMessage;\r
48 import javax.sound.midi.SysexMessage;\r
49 import javax.sound.midi.Track;\r
50 import javax.swing.AbstractAction;\r
51 import javax.swing.AbstractCellEditor;\r
52 import javax.swing.Action;\r
53 import javax.swing.BoundedRangeModel;\r
54 import javax.swing.Box;\r
55 import javax.swing.BoxLayout;\r
56 import javax.swing.DefaultCellEditor;\r
57 import javax.swing.DefaultListSelectionModel;\r
58 import javax.swing.Icon;\r
59 import javax.swing.JButton;\r
60 import javax.swing.JCheckBox;\r
61 import javax.swing.JComboBox;\r
62 import javax.swing.JDialog;\r
63 import javax.swing.JFileChooser;\r
64 import javax.swing.JLabel;\r
65 import javax.swing.JOptionPane;\r
66 import javax.swing.JPanel;\r
67 import javax.swing.JScrollPane;\r
68 import javax.swing.JSlider;\r
69 import javax.swing.JSplitPane;\r
70 import javax.swing.JTable;\r
71 import javax.swing.JToggleButton;\r
72 import javax.swing.ListSelectionModel;\r
73 import javax.swing.SwingUtilities;\r
74 import javax.swing.event.ChangeEvent;\r
75 import javax.swing.event.ChangeListener;\r
76 import javax.swing.event.ListSelectionEvent;\r
77 import javax.swing.event.ListSelectionListener;\r
78 import javax.swing.event.TableModelEvent;\r
79 import javax.swing.filechooser.FileNameExtensionFilter;\r
80 import javax.swing.table.AbstractTableModel;\r
81 import javax.swing.table.JTableHeader;\r
82 import javax.swing.table.TableCellEditor;\r
83 import javax.swing.table.TableCellRenderer;\r
84 import javax.swing.table.TableColumn;\r
85 import javax.swing.table.TableColumnModel;\r
86 import javax.swing.table.TableModel;\r
87 \r
88 /**\r
89  * MIDIエディタ(MIDI Editor/Playlist for MIDI Chord Helper)\r
90  *\r
91  * @author\r
92  *      Copyright (C) 2006-2013 Akiyoshi Kamide\r
93  *      http://www.yk.rim.or.jp/~kamide/music/chordhelper/\r
94  */\r
95 class MidiEditor extends JDialog implements DropTargetListener {\r
96         /**\r
97          * このMIDIエディタの仮想MIDIデバイス\r
98          */\r
99         VirtualMidiDevice virtualMidiDevice = new AbstractVirtualMidiDevice() {\r
100                 class MyInfo extends Info {\r
101                         protected MyInfo() {\r
102                                 super("MIDI Editor","Unknown vendor","MIDI sequence editor","");\r
103                         }\r
104                 }\r
105                 // 送信のみなので MIDI IN はサポートしない\r
106                 { info = new MyInfo(); setMaxReceivers(0); }\r
107         };\r
108         /**\r
109          * このダイアログを表示するアクション\r
110          */\r
111         public Action openAction = new AbstractAction(\r
112                 "Edit/Playlist/Speed", new ButtonIcon(ButtonIcon.EDIT_ICON)\r
113         ) {\r
114                 {\r
115                         String tooltip = "MIDIシーケンスの編集/プレイリスト/再生速度調整";\r
116                         putValue(Action.SHORT_DESCRIPTION, tooltip);\r
117                 }\r
118                 @Override\r
119                 public void actionPerformed(ActionEvent e) { open(); }\r
120         };\r
121         /**\r
122          * このダイアログを開きます。すでに開かれていた場合は前面に移動します。\r
123          */\r
124         public void open() {\r
125                 if( isVisible() ) toFront(); else setVisible(true);\r
126         }\r
127         /**\r
128          * エラーメッセージダイアログを表示します。\r
129          * @param message エラーメッセージ\r
130          */\r
131         void showError(String message) {\r
132                 JOptionPane.showMessageDialog(\r
133                         this, message,\r
134                         ChordHelperApplet.VersionInfo.NAME,\r
135                         JOptionPane.ERROR_MESSAGE\r
136                 );\r
137         }\r
138         /**\r
139          * 警告メッセージダイアログを表示します。\r
140          * @param message 警告メッセージ\r
141          */\r
142         void showWarning(String message) {\r
143                 JOptionPane.showMessageDialog(\r
144                         this, message,\r
145                         ChordHelperApplet.VersionInfo.NAME,\r
146                         JOptionPane.WARNING_MESSAGE\r
147                 );\r
148         }\r
149         /**\r
150          * 確認ダイアログを表示します。\r
151          * @param message 確認メッセージ\r
152          * @return 確認OKのときtrue\r
153          */\r
154         boolean confirm(String message) {\r
155                 return JOptionPane.showConfirmDialog(\r
156                         this, message,\r
157                         ChordHelperApplet.VersionInfo.NAME,\r
158                         JOptionPane.YES_NO_OPTION,\r
159                         JOptionPane.WARNING_MESSAGE\r
160                 ) == JOptionPane.YES_OPTION ;\r
161         }\r
162 \r
163         @Override\r
164         public void dragEnter(DropTargetDragEvent event) {\r
165                 if( event.isDataFlavorSupported(DataFlavor.javaFileListFlavor) ) {\r
166                         event.acceptDrag(DnDConstants.ACTION_COPY_OR_MOVE);\r
167                 }\r
168         }\r
169         @Override\r
170         public void dragExit(DropTargetEvent event) {}\r
171         @Override\r
172         public void dragOver(DropTargetDragEvent event) {}\r
173         @Override\r
174         public void dropActionChanged(DropTargetDragEvent event) {}\r
175         @Override\r
176         @SuppressWarnings("unchecked")\r
177         public void drop(DropTargetDropEvent event) {\r
178                 event.acceptDrop(DnDConstants.ACTION_COPY_OR_MOVE);\r
179                 try {\r
180                         int action = event.getDropAction();\r
181                         if ( (action & DnDConstants.ACTION_COPY_OR_MOVE) != 0 ) {\r
182                                 Transferable t = event.getTransferable();\r
183                                 Object data = t.getTransferData(DataFlavor.javaFileListFlavor);\r
184                                 loadAndPlay((List<File>)data);\r
185                                 event.dropComplete(true);\r
186                                 return;\r
187                         }\r
188                         event.dropComplete(false);\r
189                 }\r
190                 catch (Exception ex) {\r
191                         ex.printStackTrace();\r
192                         event.dropComplete(false);\r
193                 }\r
194         }\r
195 \r
196         public static final Insets ZERO_INSETS = new Insets(0,0,0,0);\r
197         static final Icon deleteIcon = new ButtonIcon(ButtonIcon.X_ICON);\r
198         /**\r
199          * 新しいMIDIシーケンスを生成するダイアログ\r
200          */\r
201         NewSequenceDialog newSequenceDialog = new NewSequenceDialog(this);\r
202         /**\r
203          * BASE64テキスト入力ダイアログ\r
204          */\r
205         Base64Dialog base64Dialog = new Base64Dialog(this);\r
206         /**\r
207          * プレイリストビュー(シーケンスリスト)\r
208          */\r
209         SequenceListTable sequenceListTable;\r
210         /**\r
211          * MIDIトラックリストテーブルビュー(選択中のシーケンスの中身)\r
212          */\r
213         private TrackListTable trackListTable;\r
214         /**\r
215          * MIDIイベントリストテーブルビュー(選択中のトラックの中身)\r
216          */\r
217         private EventListTable eventListTable;\r
218         /**\r
219          * MIDIイベント入力ダイアログ(イベント入力とイベント送出で共用)\r
220          */\r
221         MidiEventDialog eventDialog = new MidiEventDialog();\r
222 \r
223         /**\r
224          * プレイリストビュー(シーケンスリスト)\r
225          */\r
226         class SequenceListTable extends JTable {\r
227                 /**\r
228                  * ファイル選択ダイアログ(アプレットでは使用不可)\r
229                  */\r
230                 private MidiFileChooser midiFileChooser;\r
231                 /**\r
232                  * BASE64エンコードボタン(ライブラリが見えている場合のみ有効)\r
233                  */\r
234                 private Action base64EncodeAction;\r
235                 /**\r
236                  * プレイリストビューを構築します。\r
237                  * @param model プレイリストデータモデル\r
238                  */\r
239                 public SequenceListTable(SequenceListTableModel model) {\r
240                         super(model, null, model.sequenceListSelectionModel);\r
241                         try {\r
242                                 midiFileChooser = new MidiFileChooser();\r
243                         }\r
244                         catch( ExceptionInInitializerError|NoClassDefFoundError|AccessControlException e ) {\r
245                                 // アプレットの場合、Webクライアントマシンのローカルファイルには\r
246                                 // アクセスできないので、ファイル選択ダイアログは使用不可。\r
247                                 midiFileChooser = null;\r
248                         }\r
249                         // 列モデルに再生ボタンを埋め込む\r
250                         new PlayButtonCellEditor();\r
251                         setAutoCreateColumnsFromModel(false);\r
252                         //\r
253                         // Base64エンコードアクションの生成\r
254                         if( base64Dialog.isBase64Available() ) {\r
255                                 base64EncodeAction = getModel().new SelectedSequenceAction(\r
256                                         "Base64 Encode",\r
257                                         "Encode selected sequence to Base64 textdata - 選択した曲をBase64テキストにエンコード"\r
258                                 ) {\r
259                                         @Override\r
260                                         public void actionPerformed(ActionEvent e) {\r
261                                                 SequenceTrackListTableModel mstm = getModel().getSelectedSequenceModel();\r
262                                                 base64Dialog.setMIDIData(mstm.getMIDIdata(), mstm.getFilename());\r
263                                                 base64Dialog.setVisible(true);\r
264                                         }\r
265                                 };\r
266                         }\r
267                         TableColumnModel colModel = getColumnModel();\r
268                         for( SequenceListTableModel.Column c : SequenceListTableModel.Column.values() )\r
269                                 colModel.getColumn(c.ordinal()).setPreferredWidth(c.preferredWidth);\r
270                 }\r
271                 @Override\r
272                 public void tableChanged(TableModelEvent event) {\r
273                         super.tableChanged(event);\r
274                         int sec = getModel().getTotalSeconds();\r
275                         SequenceListTableModel.Column c = SequenceListTableModel.Column.SEQ_LENGTH;\r
276                         String title = String.format(c.title+" [%02d:%02d]", sec/60, sec%60);\r
277                         getColumnModel().getColumn(c.ordinal()).setHeaderValue(title);\r
278                         //\r
279                         // シーケンス削除時など、合計シーケンス長が変わっても\r
280                         // 列モデルからではヘッダタイトルが再描画されないことがある。\r
281                         // そこで、ヘッダビューから repaint() で突っついて再描画させる。\r
282                         JTableHeader th = getTableHeader();\r
283                         if( th != null ) {\r
284                                 th.repaint();\r
285                         }\r
286                 }\r
287                 /**\r
288                  * プレイボタンを埋め込んだセルエディタ\r
289                  */\r
290                 private class PlayButtonCellEditor extends AbstractCellEditor\r
291                         implements TableCellEditor, TableCellRenderer\r
292                 {\r
293                         private JToggleButton playButton = new JToggleButton(\r
294                                 getModel().sequencerModel.startStopAction\r
295                         );\r
296                         public PlayButtonCellEditor() {\r
297                                 playButton.setMargin(ZERO_INSETS);\r
298                                 int column = SequenceListTableModel.Column.SEQ_PLAY.ordinal();\r
299                                 TableColumn tc = getColumnModel().getColumn(column);\r
300                                 tc.setCellRenderer(this);\r
301                                 tc.setCellEditor(this);\r
302                         }\r
303                         @Override\r
304                         public Object getCellEditorValue() {\r
305                                 return playButton.getAction().getValue(Action.SELECTED_KEY);\r
306                         }\r
307                         @Override\r
308                         public Component getTableCellEditorComponent(\r
309                                 JTable table, Object value, boolean isSelected,\r
310                                 int row, int column\r
311                         ) {\r
312                                 SequenceListTableModel model = getModel();\r
313                                 if(model.sequenceList.get(row).isOnSequencer()) {\r
314                                         // すでにロードされていたらボタンをレンダリング\r
315                                         return playButton;\r
316                                 }\r
317                                 // ロードされていない場合、そのセルは編集不可\r
318                                 return null;\r
319                         }\r
320                         @Override\r
321                         public Component getTableCellRendererComponent(\r
322                                 JTable table, Object value, boolean isSelected,\r
323                                 boolean hasFocus, int row, int column\r
324                         ) {\r
325                                 SequenceListTableModel model = getModel();\r
326                                 if(model.sequenceList.get(row).isOnSequencer()) {\r
327                                         // すでにロードされていたらボタンをレンダリング\r
328                                         return playButton;\r
329                                 }\r
330                                 // ロードされていなかった場合、\r
331                                 // デフォルトレンダラーでレンダリングする。こうすれば\r
332                                 // レンダラーを設定しなかった場合と全く同じ動作になる。\r
333                                 Class<?> cc = model.getColumnClass(column);\r
334                                 TableCellRenderer defaultRenderer = table.getDefaultRenderer(cc);\r
335                                 return defaultRenderer.getTableCellRendererComponent(\r
336                                         table, value, isSelected, hasFocus, row, column\r
337                                 );\r
338                         }\r
339                 }\r
340                 /**\r
341                  * このプレイリスト(シーケンスリスト)が表示するデータを提供する\r
342                  * プレイリストモデルを返します。\r
343                  * @return プレイリストモデル\r
344                  */\r
345                 @Override\r
346                 public SequenceListTableModel getModel() {\r
347                         return (SequenceListTableModel) super.getModel();\r
348                 }\r
349                 /**\r
350                  * シーケンスを削除するアクション\r
351                  */\r
352                 Action deleteSequenceAction = getModel().new SelectedSequenceAction(\r
353                         "Delete", MidiEditor.deleteIcon,\r
354                         "Delete selected MIDI sequence - 選択した曲をプレイリストから削除"\r
355                 ) {\r
356                         @Override\r
357                         public void actionPerformed(ActionEvent e) {\r
358                                 SequenceListTableModel model = getModel();\r
359                                 if( midiFileChooser != null ) {\r
360                                         // ファイルに保存できる場合(Javaアプレットではなく、Javaアプリとして動作している場合)\r
361                                         //\r
362                                         SequenceTrackListTableModel seqModel = model.getSelectedSequenceModel();\r
363                                         if( seqModel.isModified() ) {\r
364                                                 // ファイル未保存の変更がある場合\r
365                                                 //\r
366                                                 String message =\r
367                                                         "Selected MIDI sequence not saved - delete it ?\n" +\r
368                                                         "選択したMIDIシーケンスはまだ保存されていません。削除しますか?";\r
369                                                 if( ! confirm(message) ) {\r
370                                                         // 実は削除してほしくなかった場合\r
371                                                         return;\r
372                                                 }\r
373                                         }\r
374                                 }\r
375                                 // 削除を実行\r
376                                 model.removeSelectedSequence();\r
377                         }\r
378                 };\r
379                 /**\r
380                  * ファイル選択ダイアログ(アプレットでは使用不可)\r
381                  */\r
382                 private class MidiFileChooser extends JFileChooser implements ListSelectionListener {\r
383                         /**\r
384                          * ファイル保存アクション\r
385                          */\r
386                         public Action saveMidiFileAction = new AbstractAction("Save") {\r
387                                 {\r
388                                         String tooltip = "Save selected MIDI sequence to file - 選択したMIDIシーケンスをファイルに保存";\r
389                                         putValue(Action.SHORT_DESCRIPTION, tooltip);\r
390                                 }\r
391                                 @Override\r
392                                 public void actionPerformed(ActionEvent e) {\r
393                                         SequenceTrackListTableModel sequenceTableModel =\r
394                                                 getModel().getSelectedSequenceModel();\r
395                                         String filename = sequenceTableModel.getFilename();\r
396                                         File midiFile;\r
397                                         if( filename != null && ! filename.isEmpty() ) {\r
398                                                 // プレイリスト上でファイル名が入っていたら、それを初期選択\r
399                                                 setSelectedFile(midiFile = new File(filename));\r
400                                         }\r
401                                         int response = showSaveDialog(MidiEditor.this);\r
402                                         if( response != JFileChooser.APPROVE_OPTION ) {\r
403                                                 // 保存ダイアログでキャンセルされた場合\r
404                                                 return;\r
405                                         }\r
406                                         if( (midiFile = getSelectedFile()).exists() ) {\r
407                                                 // 指定されたファイルがすでにあった場合\r
408                                                 String fn = midiFile.getName();\r
409                                                 String message = "Overwrite " + fn + " ?\n";\r
410                                                 message += fn + " を上書きしてよろしいですか?";\r
411                                                 if( ! confirm(message) ) {\r
412                                                         // 上書きしてほしくなかった場合\r
413                                                         return;\r
414                                                 }\r
415                                         }\r
416                                         // 保存を実行\r
417                                         try ( FileOutputStream out = new FileOutputStream(midiFile) ) {\r
418                                                 out.write(sequenceTableModel.getMIDIdata());\r
419                                                 sequenceTableModel.setModified(false);\r
420                                         }\r
421                                         catch( IOException ex ) {\r
422                                                 showError( ex.getMessage() );\r
423                                                 ex.printStackTrace();\r
424                                         }\r
425                                 }\r
426                         };\r
427                         {\r
428                                 // ファイルフィルタの設定\r
429                                 setFileFilter(new FileNameExtensionFilter("MIDI sequence (*.mid)", "mid"));\r
430                                 //\r
431                                 // 選択状態のリスニングを開始\r
432                                 getModel().sequenceListSelectionModel.addListSelectionListener(this);\r
433                                 updateEnabled();\r
434                         }\r
435                         /**\r
436                          * シーケンスの選択有無に応じて、保存ボタンのイネーブル状態を更新します。\r
437                          */\r
438                         private void updateEnabled() {\r
439                                 boolean en = (getModel().sequenceListSelectionModel.getMinSelectionIndex() >= 0);\r
440                                 saveMidiFileAction.setEnabled(en);\r
441                         }\r
442                         @Override\r
443                         public void valueChanged(ListSelectionEvent e) {\r
444                                 if( e.getValueIsAdjusting() )\r
445                                         return;\r
446                                 updateEnabled();\r
447                         }\r
448                         /**\r
449                          * ファイルを開くアクション\r
450                          */\r
451                         public Action openMidiFileAction = new AbstractAction("Open") {\r
452                                 {\r
453                                         String tooltip = "Open MIDI file - MIDIファイルを開く";\r
454                                         putValue(Action.SHORT_DESCRIPTION, tooltip);\r
455                                 }\r
456                                 @Override\r
457                                 public void actionPerformed(ActionEvent event) {\r
458                                         if(showOpenDialog(MidiEditor.this) == JFileChooser.APPROVE_OPTION) {\r
459                                                 try  {\r
460                                                         getModel().addSequence(getSelectedFile());\r
461                                                 } catch( IOException|InvalidMidiDataException e ) {\r
462                                                         showWarning(e.getMessage());\r
463                                                 } catch( AccessControlException e ) {\r
464                                                         showError(e.getMessage());\r
465                                                         e.printStackTrace();\r
466                                                 }\r
467                                         }\r
468                                 }\r
469                         };\r
470                 };\r
471         }\r
472 \r
473         /**\r
474          * シーケンス(トラックリスト)テーブルビュー\r
475          */\r
476         private class TrackListTable extends JTable {\r
477                 /**\r
478                  * トラックリストテーブルビューを構築します。\r
479                  * @param model シーケンス(トラックリスト)データモデル\r
480                  */\r
481                 public TrackListTable(SequenceTrackListTableModel model) {\r
482                         super(model, null, model.trackListSelectionModel);\r
483                         //\r
484                         // 録音対象のMIDIチャンネルをコンボボックスで選択できるようにする\r
485                         int colIndex = SequenceTrackListTableModel.Column.RECORD_CHANNEL.ordinal();\r
486                         TableColumn tc = getColumnModel().getColumn(colIndex);\r
487                         tc.setCellEditor(new DefaultCellEditor(new JComboBox<String>(){{\r
488                                 addItem("OFF");\r
489                                 for(int i=1; i <= MIDISpec.MAX_CHANNELS; i++)\r
490                                         addItem(String.format("%d", i));\r
491                                 addItem("ALL");\r
492                         }}));\r
493                         setAutoCreateColumnsFromModel(false);\r
494                         //\r
495                         trackSelectionListener = new TrackSelectionListener();\r
496                         titleLabel = new TitleLabel();\r
497                         model.sequenceListTableModel.sequenceListSelectionModel.addListSelectionListener(titleLabel);\r
498                         TableColumnModel colModel = getColumnModel();\r
499                         for( SequenceTrackListTableModel.Column c : SequenceTrackListTableModel.Column.values() )\r
500                                 colModel.getColumn(c.ordinal()).setPreferredWidth(c.preferredWidth);\r
501                 }\r
502                 /**\r
503                  * このテーブルビューが表示するデータを提供する\r
504                  * シーケンス(トラックリスト)データモデルを返します。\r
505                  * @return シーケンス(トラックリスト)データモデル\r
506                  */\r
507                 @Override\r
508                 public SequenceTrackListTableModel getModel() {\r
509                         return (SequenceTrackListTableModel) super.getModel();\r
510                 }\r
511                 /**\r
512                  * タイトルラベル\r
513                  */\r
514                 TitleLabel titleLabel;\r
515                 /**\r
516                  * 親テーブルの選択シーケンスの変更に反応する\r
517                  * 曲番号表示付きタイトルラベル\r
518                  */\r
519                 private class TitleLabel extends JLabel implements ListSelectionListener {\r
520                         private static final String TITLE = "Tracks";\r
521                         public TitleLabel() { setText(TITLE); }\r
522                         @Override\r
523                         public void valueChanged(ListSelectionEvent event) {\r
524                                 if( event.getValueIsAdjusting() )\r
525                                         return;\r
526                                 SequenceTrackListTableModel oldModel = getModel();\r
527                                 SequenceTrackListTableModel newModel = oldModel.sequenceListTableModel.getSelectedSequenceModel();\r
528                                 if( oldModel == newModel )\r
529                                         return;\r
530                                 int index = oldModel.sequenceListTableModel.sequenceListSelectionModel.getMinSelectionIndex();\r
531                                 String text = TITLE;\r
532                                 if( index >= 0 ) {\r
533                                         text = String.format(text+" - MIDI file No.%d", index);\r
534                                 }\r
535                                 setText(text);\r
536                                 if( newModel == null ) {\r
537                                         newModel = oldModel.sequenceListTableModel.emptyTrackListTableModel;\r
538                                         addTrackAction.setEnabled(false);\r
539                                 }\r
540                                 else {\r
541                                         addTrackAction.setEnabled(true);\r
542                                 }\r
543                                 oldModel.trackListSelectionModel.removeListSelectionListener(trackSelectionListener);\r
544                                 setModel(newModel);\r
545                                 setSelectionModel(newModel.trackListSelectionModel);\r
546                                 newModel.trackListSelectionModel.addListSelectionListener(trackSelectionListener);\r
547                                 trackSelectionListener.valueChanged(null);\r
548                         }\r
549                 }\r
550                 /**\r
551                  * トラック選択リスナー\r
552                  */\r
553                 TrackSelectionListener trackSelectionListener;\r
554                 /**\r
555                  * 選択トラックの変更に反応するリスナー\r
556                  */\r
557                 private class TrackSelectionListener implements ListSelectionListener {\r
558                         @Override\r
559                         public void valueChanged(ListSelectionEvent e) {\r
560                                 if( e != null && e.getValueIsAdjusting() )\r
561                                         return;\r
562                                 TrackListSelectionModel tlsm = getModel().trackListSelectionModel;\r
563                                 deleteTrackAction.setEnabled(! tlsm.isSelectionEmpty());\r
564                                 eventListTable.titleLabel.update(tlsm, getModel());\r
565                         }\r
566                 }\r
567                 /**\r
568                  * トラック追加アクション\r
569                  */\r
570                 Action addTrackAction = new AbstractAction("New") {\r
571                         {\r
572                                 String tooltip = "Append new track - 新しいトラックの追加";\r
573                                 putValue(Action.SHORT_DESCRIPTION, tooltip);\r
574                                 setEnabled(false);\r
575                         }\r
576                         @Override\r
577                         public void actionPerformed(ActionEvent e) {\r
578                                 getModel().createTrack();\r
579                         }\r
580                 };\r
581                 /**\r
582                  * トラック削除アクション\r
583                  */\r
584                 Action deleteTrackAction = new AbstractAction("Delete", deleteIcon) {\r
585                         {\r
586                                 String tooltip = "Delete selected track - 選択したトラックを削除";\r
587                                 putValue(Action.SHORT_DESCRIPTION, tooltip);\r
588                                 setEnabled(false);\r
589                         }\r
590                         @Override\r
591                         public void actionPerformed(ActionEvent e) {\r
592                                 String message = "Do you want to delete selected track ?\n"\r
593                                         + "選択したトラックを削除しますか?";\r
594                                 if( confirm(message) ) getModel().deleteSelectedTracks();\r
595                         }\r
596                 };\r
597         }\r
598 \r
599         /**\r
600          * MIDIイベントリストテーブルビュー(選択中のトラックの中身)\r
601          */\r
602         class EventListTable extends JTable {\r
603                 /**\r
604                  * 新しいイベントリストテーブルを構築します。\r
605                  * <p>データモデルとして一つのトラックのイベントリストを指定できます。\r
606                  * トラックを切り替えたいときは {@link #setModel(TableModel)}\r
607                  * でデータモデルを異なるトラックのものに切り替えます。\r
608                  * </p>\r
609                  *\r
610                  * @param model トラック(イベントリスト)データモデル\r
611                  */\r
612                 public EventListTable(TrackEventListTableModel model) {\r
613                         super(model, null, model.eventSelectionModel);\r
614                         //\r
615                         // 列モデルにセルエディタを設定\r
616                         eventCellEditor = new MidiEventCellEditor();\r
617                         setAutoCreateColumnsFromModel(false);\r
618                         //\r
619                         eventSelectionListener = new EventSelectionListener();\r
620                         titleLabel = new TitleLabel();\r
621                         //\r
622                         TableColumnModel colModel = getColumnModel();\r
623                         for( TrackEventListTableModel.Column c : TrackEventListTableModel.Column.values() )\r
624                                 colModel.getColumn(c.ordinal()).setPreferredWidth(c.preferredWidth);\r
625                 }\r
626                 /**\r
627                  * このテーブルビューが表示するデータを提供する\r
628                  * トラック(イベントリスト)データモデルを返します。\r
629                  * @return トラック(イベントリスト)データモデル\r
630                  */\r
631                 @Override\r
632                 public TrackEventListTableModel getModel() {\r
633                         return (TrackEventListTableModel) super.getModel();\r
634                 }\r
635                 /**\r
636                  * タイトルラベル\r
637                  */\r
638                 TitleLabel titleLabel;\r
639                 /**\r
640                  * 親テーブルの選択トラックの変更に反応する\r
641                  * トラック番号つきタイトルラベル\r
642                  */\r
643                 private class TitleLabel extends JLabel {\r
644                         private static final String TITLE = "MIDI Events";\r
645                         public TitleLabel() { super(TITLE); }\r
646                         public void update(TrackListSelectionModel tlsm, SequenceTrackListTableModel sequenceModel) {\r
647                                 String text = TITLE;\r
648                                 TrackEventListTableModel oldTrackModel = getModel();\r
649                                 int index = tlsm.getMinSelectionIndex();\r
650                                 if( index >= 0 ) {\r
651                                         text = String.format(TITLE+" - track No.%d", index);\r
652                                 }\r
653                                 setText(text);\r
654                                 TrackEventListTableModel newTrackModel = sequenceModel.getSelectedTrackModel();\r
655                                 if( oldTrackModel == newTrackModel )\r
656                                         return;\r
657                                 if( newTrackModel == null ) {\r
658                                         newTrackModel = getModel().sequenceTrackListTableModel.sequenceListTableModel.emptyEventListTableModel;\r
659                                         queryJumpEventAction.setEnabled(false);\r
660                                         queryAddEventAction.setEnabled(false);\r
661 \r
662                                         queryPasteEventAction.setEnabled(false);\r
663                                         copyEventAction.setEnabled(false);\r
664                                         deleteEventAction.setEnabled(false);\r
665                                         cutEventAction.setEnabled(false);\r
666                                 }\r
667                                 else {\r
668                                         queryJumpEventAction.setEnabled(true);\r
669                                         queryAddEventAction.setEnabled(true);\r
670                                 }\r
671                                 oldTrackModel.eventSelectionModel.removeListSelectionListener(eventSelectionListener);\r
672                                 setModel(newTrackModel);\r
673                                 setSelectionModel(newTrackModel.eventSelectionModel);\r
674                                 newTrackModel.eventSelectionModel.addListSelectionListener(eventSelectionListener);\r
675                         }\r
676                 }\r
677                 /**\r
678                  * イベント選択リスナー\r
679                  */\r
680                 private EventSelectionListener eventSelectionListener;\r
681                 /**\r
682                  * 選択イベントの変更に反応するリスナー\r
683                  */\r
684                 private class EventSelectionListener implements ListSelectionListener {\r
685                         public EventSelectionListener() {\r
686                                 getModel().eventSelectionModel.addListSelectionListener(this);\r
687                         }\r
688                         @Override\r
689                         public void valueChanged(ListSelectionEvent e) {\r
690                                 if( e.getValueIsAdjusting() )\r
691                                         return;\r
692                                 if( getSelectionModel().isSelectionEmpty() ) {\r
693                                         queryPasteEventAction.setEnabled(false);\r
694                                         copyEventAction.setEnabled(false);\r
695                                         deleteEventAction.setEnabled(false);\r
696                                         cutEventAction.setEnabled(false);\r
697                                 }\r
698                                 else {\r
699                                         copyEventAction.setEnabled(true);\r
700                                         deleteEventAction.setEnabled(true);\r
701                                         cutEventAction.setEnabled(true);\r
702                                         TrackEventListTableModel trackModel = getModel();\r
703                                         int minIndex = getSelectionModel().getMinSelectionIndex();\r
704                                         MidiEvent midiEvent = trackModel.getMidiEvent(minIndex);\r
705                                         MidiMessage msg = midiEvent.getMessage();\r
706                                         if( msg instanceof ShortMessage ) {\r
707                                                 ShortMessage sm = (ShortMessage)msg;\r
708                                                 int cmd = sm.getCommand();\r
709                                                 if( cmd == 0x80 || cmd == 0x90 || cmd == 0xA0 ) {\r
710                                                         // ノート番号を持つ場合、音を鳴らす。\r
711                                                         MidiChannel outMidiChannels[] = virtualMidiDevice.getChannels();\r
712                                                         int ch = sm.getChannel();\r
713                                                         int note = sm.getData1();\r
714                                                         int vel = sm.getData2();\r
715                                                         outMidiChannels[ch].noteOn(note, vel);\r
716                                                         outMidiChannels[ch].noteOff(note, vel);\r
717                                                 }\r
718                                         }\r
719                                         if( pairNoteOnOffModel.isSelected() ) {\r
720                                                 int maxIndex = getSelectionModel().getMaxSelectionIndex();\r
721                                                 int partnerIndex;\r
722                                                 for( int i=minIndex; i<=maxIndex; i++ ) {\r
723                                                         if( ! getSelectionModel().isSelectedIndex(i) ) continue;\r
724                                                         partnerIndex = trackModel.getIndexOfPartnerFor(i);\r
725                                                         if( partnerIndex >= 0 && ! getSelectionModel().isSelectedIndex(partnerIndex) )\r
726                                                                 getSelectionModel().addSelectionInterval(partnerIndex, partnerIndex);\r
727                                                 }\r
728                                         }\r
729                                 }\r
730                         }\r
731                 }\r
732                 /**\r
733                  * Pair noteON/OFF トグルボタンモデル\r
734                  */\r
735                 private JToggleButton.ToggleButtonModel\r
736                         pairNoteOnOffModel = new JToggleButton.ToggleButtonModel() {\r
737                                 {\r
738                                         addItemListener(\r
739                                                 new ItemListener() {\r
740                                                         public void itemStateChanged(ItemEvent e) {\r
741                                                                 eventDialog.midiMessageForm.durationForm.setEnabled(isSelected());\r
742                                                         }\r
743                                                 }\r
744                                         );\r
745                                         setSelected(true);\r
746                                 }\r
747                         };\r
748                 /**\r
749                  * tick位置入力モデル\r
750                  */\r
751                 private TickPositionModel tickPositionModel = new TickPositionModel();\r
752                 /**\r
753                  * 選択されたイベント\r
754                  */\r
755                 private MidiEvent selectedMidiEvent = null;\r
756                 /**\r
757                  * 選択されたイベントの場所\r
758                  */\r
759                 private int selectedIndex = -1;\r
760                 /**\r
761                  * 選択されたイベントのtick位置\r
762                  */\r
763                 private long currentTick = 0;\r
764                 private void setSelectedEvent() {\r
765                         SequenceTrackListTableModel sequenceTableModel = getModel().sequenceTrackListTableModel;\r
766                         eventDialog.midiMessageForm.durationForm.setPPQ(sequenceTableModel.getSequence().getResolution());\r
767                         tickPositionModel.setSequenceIndex(sequenceTableModel.getSequenceTickIndex());\r
768                         selectedIndex = -1;\r
769                         currentTick = 0;\r
770                         selectedMidiEvent = null;\r
771                         if( ! getSelectionModel().isSelectionEmpty() ) {\r
772                                 selectedIndex = getSelectionModel().getMinSelectionIndex();\r
773                                 selectedMidiEvent = getModel().getMidiEvent(selectedIndex);\r
774                                 currentTick = selectedMidiEvent.getTick();\r
775                                 tickPositionModel.setTickPosition(currentTick);\r
776                         }\r
777                 }\r
778                 /**\r
779                  * 指定のTick位置へジャンプするアクション\r
780                  */\r
781                 Action queryJumpEventAction = new AbstractAction() {\r
782                         {\r
783                                 putValue(NAME,"Jump to ...");\r
784                                 setEnabled(false);\r
785                         }\r
786                         private Action jumpEventAction = new AbstractAction() {\r
787                                 { putValue(NAME,"Jump"); }\r
788                                 public void actionPerformed(ActionEvent e) {\r
789                                         scrollToEventAt(tickPositionModel.getTickPosition());\r
790                                         eventDialog.setVisible(false);\r
791                                 }\r
792                         };\r
793                         public void actionPerformed(ActionEvent e) {\r
794                                 setSelectedEvent();\r
795                                 eventDialog.setTitle("Jump selection to");\r
796                                 eventDialog.okButton.setAction(jumpEventAction);\r
797                                 eventDialog.openTickForm();\r
798                         }\r
799                 };\r
800                 /**\r
801                  * 指定のTick位置へ貼り付けるアクション\r
802                  */\r
803                 Action queryPasteEventAction = new AbstractAction() {\r
804                         {\r
805                                 putValue(NAME,"Paste to ...");\r
806                                 setEnabled(false);\r
807                         }\r
808                         private Action pasteEventAction = new AbstractAction() {\r
809                                 { putValue(NAME,"Paste"); }\r
810                                 public void actionPerformed(ActionEvent e) {\r
811                                         long tick = tickPositionModel.getTickPosition();\r
812                                         getModel().addMidiEvents(copiedEventsToPaste, tick, copiedEventsPPQ);\r
813                                         scrollToEventAt(tick);\r
814                                         // プレイリストの曲の長さ表示を更新\r
815                                         getModel().sequenceTrackListTableModel.sequenceListTableModel.fireSelectedSequenceChanged();\r
816                                         eventDialog.setVisible(false);\r
817                                 }\r
818                         };\r
819                         public void actionPerformed(ActionEvent e) {\r
820                                 setSelectedEvent();\r
821                                 eventDialog.setTitle("Paste to");\r
822                                 eventDialog.okButton.setAction(pasteEventAction);\r
823                                 eventDialog.openTickForm();\r
824                         }\r
825                 };\r
826                 /**\r
827                  * 新しいイベントの追加を行うアクション\r
828                  */\r
829                 Action queryAddEventAction = new AbstractAction() {\r
830                         {\r
831                                 putValue(NAME,"New");\r
832                                 setEnabled(false);\r
833                         }\r
834                         public void actionPerformed(ActionEvent e) {\r
835                                 setSelectedEvent();\r
836                                 midiEventsToBeOverwritten = null; // 追加なので、上書きされるイベントなし\r
837                                 eventDialog.setTitle("Add a new MIDI event");\r
838                                 eventDialog.okButton.setAction(eventCellEditor.applyEventAction);\r
839                                 int ch = getModel().getChannel();\r
840                                 if( ch >= 0 ) {\r
841                                         eventDialog.midiMessageForm.channelText.setSelectedChannel(ch);\r
842                                 }\r
843                                 eventDialog.openEventForm();\r
844                         }\r
845                 };\r
846                 /**\r
847                  * 上書きして削除対象にする変更前イベント(null可)\r
848                  */\r
849                 private MidiEvent[] midiEventsToBeOverwritten;\r
850                 /**\r
851                  * ペースト用にコピーされたMIDIイベントの配列\r
852                  */\r
853                 private MidiEvent copiedEventsToPaste[];\r
854                 /**\r
855                  * ペースト用にコピーされたMIDIイベントのタイミング解像度\r
856                  */\r
857                 private int copiedEventsPPQ = 0;\r
858                 /**\r
859                  * イベントカットアクション\r
860                  */\r
861                 public Action cutEventAction = new AbstractAction("Cut") {\r
862                         {\r
863                                 setEnabled(false);\r
864                         }\r
865                         @Override\r
866                         public void actionPerformed(ActionEvent e) {\r
867                                 if( ! confirm("Do you want to cut selected event ?\n選択したMIDIイベントを切り取りますか?"))\r
868                                         return;\r
869                                 copiedEventsToPaste = getModel().getSelectedMidiEvents();\r
870                                 copiedEventsPPQ = getModel().sequenceTrackListTableModel.sequenceListTableModel.getSelectedSequenceModel().getSequence().getResolution();\r
871                                 getModel().removeMidiEvents(copiedEventsToPaste);\r
872                                 queryPasteEventAction.setEnabled(\r
873                                         copiedEventsToPaste != null &&\r
874                                         copiedEventsToPaste.length > 0\r
875                                 );\r
876                         }\r
877                 };\r
878                 /**\r
879                  * イベントコピーアクション\r
880                  */\r
881                 public Action copyEventAction = new AbstractAction("Copy") {\r
882                         {\r
883                                 setEnabled(false);\r
884                         }\r
885                         @Override\r
886                         public void actionPerformed(ActionEvent e) {\r
887                                 copiedEventsToPaste = getModel().getSelectedMidiEvents();\r
888                                 copiedEventsPPQ = getModel().sequenceTrackListTableModel.sequenceListTableModel.getSelectedSequenceModel().getSequence().getResolution();\r
889                                 queryPasteEventAction.setEnabled(\r
890                                         copiedEventsToPaste != null &&\r
891                                         copiedEventsToPaste.length > 0\r
892                                 );\r
893                         }\r
894                 };\r
895                 /**\r
896                  * イベント削除アクション\r
897                  */\r
898                 public Action deleteEventAction = new AbstractAction("Delete", deleteIcon) {\r
899                         {\r
900                                 setEnabled(false);\r
901                         }\r
902                         @Override\r
903                         public void actionPerformed(ActionEvent e) {\r
904                                 if( ! confirm("Do you want to delete selected event ?\n選択したMIDIイベントを削除しますか?"))\r
905                                         return;\r
906                                 getModel().removeSelectedMidiEvents();\r
907                         }\r
908                 };\r
909                 /**\r
910                  * MIDIイベント表のセルエディタ\r
911                  */\r
912                 private MidiEventCellEditor eventCellEditor;\r
913                 /**\r
914                  * MIDIイベント表のセルエディタ\r
915                  */\r
916                 class MidiEventCellEditor extends AbstractCellEditor implements TableCellEditor {\r
917                         /**\r
918                          * MIDIイベントセルエディタを構築します。\r
919                          */\r
920                         public MidiEventCellEditor() {\r
921                                 eventDialog.midiMessageForm.setOutputMidiChannels(virtualMidiDevice.getChannels());\r
922                                 eventDialog.tickPositionInputForm.setModel(tickPositionModel);\r
923                                 int index = TrackEventListTableModel.Column.MESSAGE.ordinal();\r
924                                 getColumnModel().getColumn(index).setCellEditor(this);\r
925                         }\r
926                         /**\r
927                          * セルをダブルクリックしないと編集できないようにします。\r
928                          * @param e イベント(マウスイベント)\r
929                          * @return 編集可能になったらtrue\r
930                          */\r
931                         @Override\r
932                         public boolean isCellEditable(EventObject e) {\r
933                                 if( ! (e instanceof MouseEvent) ) return false;\r
934                                 return ((MouseEvent)e).getClickCount() == 2;\r
935                         }\r
936                         @Override\r
937                         public Object getCellEditorValue() { return ""; }\r
938                         /**\r
939                          * 既存イベントを編集するアクション\r
940                          */\r
941                         private Action editEventAction = new AbstractAction() {\r
942                                 public void actionPerformed(ActionEvent e) {\r
943                                         setSelectedEvent();\r
944                                         if( selectedMidiEvent == null )\r
945                                                 return;\r
946                                         MidiEvent partnerEvent = null;\r
947                                         eventDialog.midiMessageForm.setMessage(selectedMidiEvent.getMessage());\r
948                                         if( eventDialog.midiMessageForm.isNote() ) {\r
949                                                 int partnerIndex = getModel().getIndexOfPartnerFor(selectedIndex);\r
950                                                 if( partnerIndex < 0 ) {\r
951                                                         eventDialog.midiMessageForm.durationForm.setDuration(0);\r
952                                                 }\r
953                                                 else {\r
954                                                         partnerEvent = getModel().getMidiEvent(partnerIndex);\r
955                                                         long partnerTick = partnerEvent.getTick();\r
956                                                         long duration = currentTick > partnerTick ?\r
957                                                                 currentTick - partnerTick : partnerTick - currentTick ;\r
958                                                         eventDialog.midiMessageForm.durationForm.setDuration((int)duration);\r
959                                                 }\r
960                                         }\r
961                                         if(partnerEvent == null)\r
962                                                 midiEventsToBeOverwritten = new MidiEvent[] {selectedMidiEvent};\r
963                                         else\r
964                                                 midiEventsToBeOverwritten = new MidiEvent[] {selectedMidiEvent, partnerEvent};\r
965                                         eventDialog.setTitle("Change MIDI event");\r
966                                         eventDialog.okButton.setAction(applyEventAction);\r
967                                         eventDialog.cancelButton.addActionListener(cancelActionListener);\r
968                                         eventDialog.openEventForm();\r
969                                 }\r
970                         };\r
971                         /**\r
972                          * イベント編集ボタン\r
973                          */\r
974                         private JButton editEventButton = new JButton(editEventAction){{\r
975                                 setHorizontalAlignment(JButton.LEFT);\r
976                         }};\r
977                         @Override\r
978                         public Component getTableCellEditorComponent(\r
979                                 JTable table, Object value, boolean isSelected, int row, int column\r
980                         ) {\r
981                                 editEventButton.setText((String)value);\r
982                                 return editEventButton;\r
983                         }\r
984                         /**\r
985                          * イベント入力をキャンセルするアクションリスナーです。\r
986                          *\r
987                          * <p>セル編集によって表示されたMIDIメッセージダイアログを\r
988                          * キャンセルする場合、セル編集を中止する処理の追加が必要です。\r
989                          * その追加処理をこのリスナーでカバーします。\r
990                          * </p>\r
991                          */\r
992                         private ActionListener cancelActionListener = new ActionListener() {\r
993                                 public void actionPerformed(ActionEvent e) {\r
994                                         fireEditingCanceled();\r
995                                         // 用が済んだら当リスナーを除去\r
996                                         eventDialog.cancelButton.removeActionListener(this);\r
997                                 }\r
998                         };\r
999                         /**\r
1000                          * 入力したイベントを反映するアクション\r
1001                          */\r
1002                         private Action applyEventAction = new AbstractAction() {\r
1003                                 {\r
1004                                         putValue(NAME,"OK");\r
1005                                 }\r
1006                                 public void actionPerformed(ActionEvent e) {\r
1007                                         long tick = tickPositionModel.getTickPosition();\r
1008                                         MidiMessage msg = eventDialog.midiMessageForm.getMessage();\r
1009                                         MidiEvent newMidiEvent = new MidiEvent(msg,tick);\r
1010                                         if( midiEventsToBeOverwritten != null ) {\r
1011                                                 // 上書き消去するための選択済イベントがあった場合\r
1012                                                 getModel().removeMidiEvents(midiEventsToBeOverwritten);\r
1013                                         }\r
1014                                         if( ! getModel().addMidiEvent(newMidiEvent) ) {\r
1015                                                 System.out.println("addMidiEvent failure");\r
1016                                                 return;\r
1017                                         }\r
1018                                         if(pairNoteOnOffModel.isSelected() && eventDialog.midiMessageForm.isNote()) {\r
1019                                                 ShortMessage sm = eventDialog.midiMessageForm.getPartnerMessage();\r
1020                                                 if( sm == null ) scrollToEventAt( tick );\r
1021                                                 else {\r
1022                                                         int duration = eventDialog.midiMessageForm.durationForm.getDuration();\r
1023                                                         if( eventDialog.midiMessageForm.isNote(false) ) { // Note Off\r
1024                                                                 duration = -duration;\r
1025                                                         }\r
1026                                                         long partnerTick = tick + (long)duration;\r
1027                                                         if( partnerTick < 0L ) partnerTick = 0L;\r
1028                                                         MidiEvent partner_midi_event =\r
1029                                                                         new MidiEvent( (MidiMessage)sm, partnerTick );\r
1030                                                         if( ! getModel().addMidiEvent(partner_midi_event) ) {\r
1031                                                                 System.out.println("addMidiEvent failure (note on/off partner message)");\r
1032                                                         }\r
1033                                                         scrollToEventAt( partnerTick > tick ? partnerTick : tick );\r
1034                                                 }\r
1035                                         }\r
1036                                         getModel().sequenceTrackListTableModel.sequenceListTableModel.fireSequenceModified(getModel().sequenceTrackListTableModel);\r
1037                                         eventDialog.setVisible(false);\r
1038                                         eventCellEditor.fireEditingStopped();\r
1039                                 }\r
1040                         };\r
1041                 }\r
1042                 /**\r
1043                  * スクロール可能なMIDIイベントテーブルビュー\r
1044                  */\r
1045                 private JScrollPane scrollPane = new JScrollPane(this);\r
1046                 /**\r
1047                  * 指定の MIDI tick のイベントへスクロールします。\r
1048                  * @param tick MIDI tick\r
1049                  */\r
1050                 public void scrollToEventAt(long tick) {\r
1051                         int index = getModel().tickToIndex(tick);\r
1052                         scrollPane.getVerticalScrollBar().setValue(index * getRowHeight());\r
1053                         getSelectionModel().setSelectionInterval(index, index);\r
1054                 }\r
1055         }\r
1056 \r
1057         /**\r
1058          * 新しい {@link MidiEditor} を構築します。\r
1059          * @param deviceModelList MIDIデバイスモデルリスト\r
1060          */\r
1061         public MidiEditor(MidiSequencerModel sequencerModel) {\r
1062                 // テーブルモデルとテーブルビューの生成\r
1063                 sequenceListTable = new SequenceListTable(\r
1064                         new SequenceListTableModel(sequencerModel)\r
1065                 );\r
1066                 trackListTable = new TrackListTable(\r
1067                         new SequenceTrackListTableModel(\r
1068                                 sequenceListTable.getModel(), null, null\r
1069                         )\r
1070                 );\r
1071                 eventListTable = new EventListTable(\r
1072                         new TrackEventListTableModel(trackListTable.getModel(), null)\r
1073                 );\r
1074                 // レイアウト\r
1075                 setTitle("MIDI Editor/Playlist - MIDI Chord Helper");\r
1076                 setBounds( 150, 200, 850, 500 );\r
1077                 setLayout(new FlowLayout());\r
1078                 new DropTarget(this, DnDConstants.ACTION_COPY_OR_MOVE, this, true);\r
1079                 JPanel playlistPanel = new JPanel() {{\r
1080                         JPanel playlistOperationPanel = new JPanel() {{\r
1081                                 setLayout(new BoxLayout(this, BoxLayout.LINE_AXIS));\r
1082                                 add(Box.createRigidArea(new Dimension(10, 0)));\r
1083                                 add(new JButton(newSequenceDialog.openAction) {{\r
1084                                         setMargin(ZERO_INSETS);\r
1085                                 }});\r
1086                                 if( sequenceListTable.midiFileChooser != null ) {\r
1087                                         add( Box.createRigidArea(new Dimension(5, 0)) );\r
1088                                         add(new JButton(\r
1089                                                 sequenceListTable.midiFileChooser.openMidiFileAction\r
1090                                         ) {{\r
1091                                                 setMargin(ZERO_INSETS);\r
1092                                         }});\r
1093                                 }\r
1094                                 add(Box.createRigidArea(new Dimension(5, 0)));\r
1095                                 SequenceListTableModel sequenceListTableModel = sequenceListTable.getModel();\r
1096                                 add(new JButton(sequenceListTableModel.moveToTopAction) {{\r
1097                                         setMargin(ZERO_INSETS);\r
1098                                 }});\r
1099                                 add(Box.createRigidArea(new Dimension(5, 0)));\r
1100                                 add(new JButton(sequenceListTableModel.loadToSequencerAction){{\r
1101                                         setMargin(ZERO_INSETS);\r
1102                                 }});\r
1103                                 add(Box.createRigidArea(new Dimension(5, 0)));\r
1104                                 add(new JButton(sequenceListTableModel.moveToBottomAction) {{\r
1105                                         setMargin(ZERO_INSETS);\r
1106                                 }});\r
1107                                 if( sequenceListTable.midiFileChooser != null ) {\r
1108                                         add(Box.createRigidArea(new Dimension(5, 0)));\r
1109                                         add(new JButton(\r
1110                                                 sequenceListTable.midiFileChooser.saveMidiFileAction\r
1111                                         ) {{\r
1112                                                 setMargin(ZERO_INSETS);\r
1113                                         }});\r
1114                                 }\r
1115                                 if(sequenceListTable.base64EncodeAction != null) {\r
1116                                         add(Box.createRigidArea(new Dimension(5, 0)));\r
1117                                         add(new JButton(sequenceListTable.base64EncodeAction) {{\r
1118                                                 setMargin(ZERO_INSETS);\r
1119                                         }});\r
1120                                 }\r
1121                                 add( Box.createRigidArea(new Dimension(5, 0)) );\r
1122                                 add(new JButton(sequenceListTable.deleteSequenceAction) {{\r
1123                                         setMargin(ZERO_INSETS);\r
1124                                 }});\r
1125                                 add( Box.createRigidArea(new Dimension(5, 0)) );\r
1126                                 add(new SequencerSpeedSlider(\r
1127                                         sequenceListTableModel.sequencerModel.speedSliderModel\r
1128                                 ));\r
1129                         }};\r
1130                         setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));\r
1131                         add(new JScrollPane(sequenceListTable));\r
1132                         add(Box.createRigidArea(new Dimension(0, 10)));\r
1133                         add(playlistOperationPanel);\r
1134                         add(Box.createRigidArea(new Dimension(0, 10)));\r
1135                 }};\r
1136                 JPanel trackListPanel = new JPanel() {{\r
1137                         JPanel trackListOperationPanel = new JPanel() {{\r
1138                                 add(new JButton(trackListTable.addTrackAction) {{\r
1139                                         setMargin(ZERO_INSETS);\r
1140                                 }});\r
1141                                 add(new JButton(trackListTable.deleteTrackAction) {{\r
1142                                         setMargin(ZERO_INSETS);\r
1143                                 }});\r
1144                         }};\r
1145                         setLayout(new BoxLayout(this, BoxLayout.PAGE_AXIS));\r
1146                         add(trackListTable.titleLabel);\r
1147                         add(Box.createRigidArea(new Dimension(0, 5)));\r
1148                         add(new JScrollPane(trackListTable));\r
1149                         add(Box.createRigidArea(new Dimension(0, 5)));\r
1150                         add(trackListOperationPanel);\r
1151                 }};\r
1152                 JPanel eventListPanel = new JPanel() {{\r
1153                         JPanel eventListOperationPanel = new JPanel() {{\r
1154                                 add(new JCheckBox("Pair NoteON/OFF") {{\r
1155                                         setModel(eventListTable.pairNoteOnOffModel);\r
1156                                         setToolTipText("NoteON/OFFをペアで同時選択する");\r
1157                                 }});\r
1158                                 add(new JButton(eventListTable.queryJumpEventAction) {{\r
1159                                         setMargin(ZERO_INSETS);\r
1160                                 }});\r
1161                                 add(new JButton(eventListTable.queryAddEventAction) {{\r
1162                                         setMargin(ZERO_INSETS);\r
1163                                 }});\r
1164                                 add(new JButton(eventListTable.copyEventAction) {{\r
1165                                         setMargin(ZERO_INSETS);\r
1166                                 }});\r
1167                                 add(new JButton(eventListTable.cutEventAction) {{\r
1168                                         setMargin(ZERO_INSETS);\r
1169                                 }});\r
1170                                 add(new JButton(eventListTable.queryPasteEventAction) {{\r
1171                                         setMargin(ZERO_INSETS);\r
1172                                 }});\r
1173                                 add(new JButton(eventListTable.deleteEventAction) {{\r
1174                                         setMargin(ZERO_INSETS);\r
1175                                 }});\r
1176                         }};\r
1177                         setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));\r
1178                         add(eventListTable.titleLabel);\r
1179                         add(eventListTable.scrollPane);\r
1180                         add(eventListOperationPanel);\r
1181                 }};\r
1182                 Container cp = getContentPane();\r
1183                 cp.setLayout(new BoxLayout(cp, BoxLayout.Y_AXIS));\r
1184                 cp.add(Box.createVerticalStrut(2));\r
1185                 cp.add(\r
1186                         new JSplitPane(JSplitPane.VERTICAL_SPLIT, playlistPanel,\r
1187                                 new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, trackListPanel, eventListPanel) {{\r
1188                                         setDividerLocation(300);\r
1189                                 }}\r
1190                         ) {{\r
1191                                 setDividerLocation(160);\r
1192                         }}\r
1193                 );\r
1194         }\r
1195 \r
1196         /**\r
1197          * 複数のMIDIファイルを読み込み、再生されていなかったら再生します。\r
1198          * すでに再生されていた場合、このエディタダイアログを表示します。\r
1199          *\r
1200          * @param fileList 読み込むMIDIファイルのリスト\r
1201          */\r
1202         public void loadAndPlay(List<File> fileList) {\r
1203                 int firstIndex = -1;\r
1204                 SequenceListTableModel sequenceListTableModel = sequenceListTable.getModel();\r
1205                 try {\r
1206                         firstIndex = sequenceListTableModel.addSequences(fileList);\r
1207                 } catch( IOException|InvalidMidiDataException e ) {\r
1208                         showWarning(e.getMessage());\r
1209                 } catch( AccessControlException e ) {\r
1210                         showError(e.getMessage());\r
1211                         e.printStackTrace();\r
1212                 }\r
1213                 if(sequenceListTableModel.sequencerModel.getSequencer().isRunning()) {\r
1214                         open();\r
1215                 }\r
1216                 else if( firstIndex >= 0 ) {\r
1217                         sequenceListTableModel.loadToSequencer(firstIndex);\r
1218                         sequenceListTableModel.sequencerModel.start();\r
1219                 }\r
1220         }\r
1221 }\r
1222 \r
1223 /**\r
1224  * シーケンサーの再生スピード調整スライダビュー\r
1225  */\r
1226 class SequencerSpeedSlider extends JPanel {\r
1227         private static final String items[] = {\r
1228                 "x 1.0",\r
1229                 "x 1.5",\r
1230                 "x 2",\r
1231                 "x 4",\r
1232                 "x 8",\r
1233                 "x 16",\r
1234         };\r
1235         private JLabel titleLabel;\r
1236         private JSlider slider;\r
1237         public SequencerSpeedSlider(BoundedRangeModel model) {\r
1238                 add(titleLabel = new JLabel("Speed:"));\r
1239                 add(slider = new JSlider(model){{\r
1240                         setPaintTicks(true);\r
1241                         setMajorTickSpacing(12);\r
1242                         setMinorTickSpacing(1);\r
1243                         setVisible(false);\r
1244                 }});\r
1245                 add(new JComboBox<String>(items) {{\r
1246                         addActionListener(new ActionListener() {\r
1247                                 @Override\r
1248                                 public void actionPerformed(ActionEvent e) {\r
1249                                         int index = getSelectedIndex();\r
1250                                         BoundedRangeModel model = slider.getModel();\r
1251                                         if( index == 0 ) {\r
1252                                                 model.setValue(0);\r
1253                                                 slider.setVisible(false);\r
1254                                                 titleLabel.setVisible(true);\r
1255                                         }\r
1256                                         else {\r
1257                                                 int maxValue = ( index == 1 ? 7 : (index-1)*12 );\r
1258                                                 model.setMinimum(-maxValue);\r
1259                                                 model.setMaximum(maxValue);\r
1260                                                 slider.setMajorTickSpacing( index == 1 ? 7 : 12 );\r
1261                                                 slider.setMinorTickSpacing( index > 3 ? 12 : 1 );\r
1262                                                 slider.setVisible(true);\r
1263                                                 titleLabel.setVisible(false);\r
1264                                         }\r
1265                                 }\r
1266                         });\r
1267                 }});\r
1268         }\r
1269 }\r
1270 \r
1271 /**\r
1272  * 選択されているシーケンスのインデックス\r
1273  */\r
1274 class SequenceListSelectionModel extends DefaultListSelectionModel {\r
1275         {\r
1276                 setSelectionMode(ListSelectionModel.SINGLE_SELECTION);\r
1277         }\r
1278 }\r
1279 /**\r
1280  * プレイリスト(MIDIシーケンスリスト)のテーブルデータモデル\r
1281  */\r
1282 class SequenceListTableModel extends AbstractTableModel {\r
1283         /**\r
1284          * MIDIシーケンサモデル\r
1285          */\r
1286         MidiSequencerModel sequencerModel;\r
1287         /**\r
1288          * 空のトラックリストモデル\r
1289          */\r
1290         SequenceTrackListTableModel emptyTrackListTableModel;\r
1291         /**\r
1292          * 空のイベントリストモデル\r
1293          */\r
1294         TrackEventListTableModel emptyEventListTableModel;\r
1295         /**\r
1296          * 新しいプレイリストのテーブルモデルを構築します。\r
1297          * @param sequencerModel MIDIシーケンサーモデル\r
1298          */\r
1299         public SequenceListTableModel(MidiSequencerModel sequencerModel) {\r
1300                 this.sequencerModel = sequencerModel;\r
1301                 //\r
1302                 // 秒位置を監視\r
1303                 sequencerModel.addChangeListener(secondPosition = new SecondPosition());\r
1304                 //\r
1305                 // メタイベントを監視\r
1306                 sequencerModel.getSequencer().addMetaEventListener(\r
1307                         new MetaEventListener() {\r
1308                                 /**\r
1309                                  * {@inheritDoc}\r
1310                                  *\r
1311                                  * <p>EOT (End Of Track、type==0x2F) を受信したときの処理です。\r
1312                                  * </p>\r
1313                                  * <p>これは MetaEventListener のための実装なので、多くの場合\r
1314                                  * Swing EDT ではなく MIDI シーケンサの EDT から起動されます。\r
1315                                  * Swing EDT とは違うスレッドで動いていた場合は Swing EDT に振り直されます。\r
1316                                  * </p>\r
1317                                  */\r
1318                                 @Override\r
1319                                 public void meta(MetaMessage msg) {\r
1320                                         if( msg.getType() == 0x2F ) {\r
1321                                                 if( ! SwingUtilities.isEventDispatchThread() ) {\r
1322                                                         SwingUtilities.invokeLater(\r
1323                                                                 new Runnable() {\r
1324                                                                         @Override\r
1325                                                                         public void run() { goNext(); }\r
1326                                                                 }\r
1327                                                         );\r
1328                                                         return;\r
1329                                                 }\r
1330                                                 goNext();\r
1331                                         }\r
1332                                 }\r
1333                         }\r
1334                 );\r
1335                 emptyTrackListTableModel = new SequenceTrackListTableModel(this, null, null);\r
1336                 emptyEventListTableModel = new TrackEventListTableModel(emptyTrackListTableModel, null);\r
1337         }\r
1338         /**\r
1339          * 次の曲へ進みます。\r
1340          *\r
1341          * <p>リピートモードの場合は同じ曲をもう一度再生、\r
1342          * そうでない場合は次の曲へ進んで再生します。\r
1343          * 次の曲がなければ、そこで停止します。\r
1344          * いずれの場合も局の先頭へ戻ります。\r
1345          * </p>\r
1346          */\r
1347         private void goNext() {\r
1348                 // とりあえず曲の先頭へ戻る\r
1349                 sequencerModel.getSequencer().setMicrosecondPosition(0);\r
1350                 if( (Boolean)toggleRepeatAction.getValue(Action.SELECTED_KEY) || loadNext(1)) {\r
1351                         // リピートモードのときはもう一度同じ曲を、\r
1352                         // そうでない場合は次の曲を再生開始\r
1353                         sequencerModel.start();\r
1354                 }\r
1355                 else {\r
1356                         // 最後の曲が終わったので、停止状態にする\r
1357                         sequencerModel.stop();\r
1358                         // ここでボタンが停止状態に変わったはずなので、\r
1359                         // 通常であれば再生ボタンが自力で再描画するところだが、\r
1360                         //\r
1361                         // セルのレンダラーが描く再生ボタンには効かないようなので、\r
1362                         // セルを突っついて再表示させる。\r
1363                         int rowIndex = indexOfSequenceOnSequencer();\r
1364                         int colIndex = Column.SEQ_PLAY.ordinal();\r
1365                         fireTableCellUpdated(rowIndex, colIndex);\r
1366                 }\r
1367         }\r
1368         /**\r
1369          * シーケンスリスト\r
1370          */\r
1371         List<SequenceTrackListTableModel> sequenceList = new Vector<>();\r
1372         /**\r
1373          * 選択されているシーケンスのインデックス\r
1374          */\r
1375         SequenceListSelectionModel sequenceListSelectionModel = new SequenceListSelectionModel();\r
1376         /**\r
1377          * 行が選択されているときだけイネーブルになるアクション\r
1378          */\r
1379         public abstract class SelectedSequenceAction extends AbstractAction\r
1380                 implements ListSelectionListener\r
1381         {\r
1382                 public SelectedSequenceAction(String name, Icon icon, String tooltip) {\r
1383                         super(name,icon); init(tooltip);\r
1384                 }\r
1385                 public SelectedSequenceAction(String name, String tooltip) {\r
1386                         super(name); init(tooltip);\r
1387                 }\r
1388                 @Override\r
1389                 public void valueChanged(ListSelectionEvent e) {\r
1390                         if( e.getValueIsAdjusting() ) return;\r
1391                         setEnebledBySelection();\r
1392                 }\r
1393                 protected void setEnebledBySelection() {\r
1394                         int index = sequenceListSelectionModel.getMinSelectionIndex();\r
1395                         setEnabled(index >= 0);\r
1396                 }\r
1397                 private void init(String tooltip) {\r
1398                         putValue(Action.SHORT_DESCRIPTION, tooltip);\r
1399                         sequenceListSelectionModel.addListSelectionListener(this);\r
1400                         setEnebledBySelection();\r
1401                 }\r
1402         }\r
1403         /**\r
1404          * 選択されたシーケンスへジャンプするアクション\r
1405          */\r
1406         public Action loadToSequencerAction = new SelectedSequenceAction(\r
1407                 "Load to sequencer",\r
1408                 "Load selected MIDI sequence to sequencer - 選択した曲をシーケンサへロード"\r
1409         ) {\r
1410                 @Override\r
1411                 public void actionPerformed(ActionEvent e) {\r
1412                         loadToSequencer(sequenceListSelectionModel.getMinSelectionIndex());\r
1413                 }\r
1414         };\r
1415         /**\r
1416          * 繰り返し再生ON/OFF切り替えアクション\r
1417          */\r
1418         public Action toggleRepeatAction = new AbstractAction() {\r
1419                 {\r
1420                         putValue(SHORT_DESCRIPTION, "Repeat - 繰り返し再生");\r
1421                         putValue(LARGE_ICON_KEY, new ButtonIcon(ButtonIcon.REPEAT_ICON));\r
1422                         putValue(SELECTED_KEY, false);\r
1423                 }\r
1424                 @Override\r
1425                 public void actionPerformed(ActionEvent event) { }\r
1426         };\r
1427         /**\r
1428          * 再生中のシーケンサーの秒位置\r
1429          */\r
1430         private class SecondPosition implements ChangeListener {\r
1431                 private int value = 0;\r
1432                 /**\r
1433                  * 再生中のシーケンサーの秒位置が変わったときに表示を更新します。\r
1434                  * @param event 変更イベント\r
1435                  */\r
1436                 @Override\r
1437                 public void stateChanged(ChangeEvent event) {\r
1438                         Object src = event.getSource();\r
1439                         if( src instanceof MidiSequencerModel ) {\r
1440                                 int newValue = ((MidiSequencerModel)src).getValue() / 1000;\r
1441                                 if(value == newValue) return;\r
1442                                 value = newValue;\r
1443                                 int rowIndex = indexOfSequenceOnSequencer();\r
1444                                 fireTableCellUpdated(rowIndex, Column.SEQ_POSITION.ordinal());\r
1445                         }\r
1446                 }\r
1447                 @Override\r
1448                 public String toString() {\r
1449                         return String.format("%02d:%02d", value/60, value%60);\r
1450                 }\r
1451         }\r
1452         /**\r
1453          * 曲の先頭または前の曲へ戻るアクション\r
1454          */\r
1455         public Action moveToTopAction = new AbstractAction() {\r
1456                 {\r
1457                         putValue(SHORT_DESCRIPTION,\r
1458                                 "Move to top or previous song - 曲の先頭または前の曲へ戻る"\r
1459                         );\r
1460                         putValue(LARGE_ICON_KEY, new ButtonIcon(ButtonIcon.TOP_ICON));\r
1461                 }\r
1462                 public void actionPerformed(ActionEvent event) {\r
1463                         if( sequencerModel.getSequencer().getTickPosition() <= 40 )\r
1464                                 loadNext(-1);\r
1465                         sequencerModel.setValue(0);\r
1466                 }\r
1467         };\r
1468         /**\r
1469          * 次の曲へ進むアクション\r
1470          */\r
1471         public Action moveToBottomAction = new AbstractAction() {\r
1472                 {\r
1473                         putValue(SHORT_DESCRIPTION, "Move to next song - 次の曲へ進む");\r
1474                         putValue(LARGE_ICON_KEY, new ButtonIcon(ButtonIcon.BOTTOM_ICON));\r
1475                 }\r
1476                 public void actionPerformed(ActionEvent event) {\r
1477                         if(loadNext(1)) sequencerModel.setValue(0);\r
1478                 }\r
1479         };\r
1480 \r
1481         /**\r
1482          * 列の列挙型\r
1483          */\r
1484         public enum Column {\r
1485                 /** MIDIシーケンスの番号 */\r
1486                 SEQ_NUMBER("No.", Integer.class, 20),\r
1487                 /** 変更済みフラグ */\r
1488                 MODIFIED("Modified", Boolean.class, 50) {\r
1489                         @Override\r
1490                         public Object getValueOf(SequenceTrackListTableModel sequenceModel) {\r
1491                                 return sequenceModel.isModified();\r
1492                         }\r
1493                 },\r
1494                 /** 再生ボタン */\r
1495                 SEQ_PLAY("Play/Stop", String.class, 60) {\r
1496                         @Override\r
1497                         public boolean isCellEditable() { return true; }\r
1498                 },\r
1499                 /** 再生中の時間位置(分:秒) */\r
1500                 SEQ_POSITION("Position", String.class, 60) {\r
1501                         @Override\r
1502                         public Object getValueOf(SequenceTrackListTableModel sequenceModel) {\r
1503                                 return sequenceModel.isOnSequencer()\r
1504                                         ? sequenceModel.sequenceListTableModel.secondPosition : "";\r
1505                         }\r
1506                 },\r
1507                 /** シーケンスの時間長(分:秒) */\r
1508                 SEQ_LENGTH("Length", String.class, 80) {\r
1509                         @Override\r
1510                         public Object getValueOf(SequenceTrackListTableModel sequenceModel) {\r
1511                                 long usec = sequenceModel.getSequence().getMicrosecondLength();\r
1512                                 int sec = (int)( (usec < 0 ? usec += 0x100000000L : usec) / 1000L / 1000L );\r
1513                                 return String.format( "%02d:%02d", sec/60, sec%60 );\r
1514                         }\r
1515                 },\r
1516                 /** ファイル名 */\r
1517                 FILENAME("Filename", String.class, 100) {\r
1518                         @Override\r
1519                         public boolean isCellEditable() { return true; }\r
1520                         public Object getValueOf(SequenceTrackListTableModel sequenceModel) {\r
1521                                 String filename = sequenceModel.getFilename();\r
1522                                 return filename == null ? "" : filename;\r
1523                         }\r
1524                 },\r
1525                 /** シーケンス名(最初のトラックの名前) */\r
1526                 SEQ_NAME("Sequence name", String.class, 250) {\r
1527                         @Override\r
1528                         public boolean isCellEditable() { return true; }\r
1529                         public Object getValueOf(SequenceTrackListTableModel sequenceModel) {\r
1530                                 String name = sequenceModel.toString();\r
1531                                 return name == null ? "" : name;\r
1532                         }\r
1533                 },\r
1534                 /** タイミング解像度 */\r
1535                 RESOLUTION("Resolution", Integer.class, 60) {\r
1536                         @Override\r
1537                         public Object getValueOf(SequenceTrackListTableModel sequenceModel) {\r
1538                                 return sequenceModel.getSequence().getResolution();\r
1539                         }\r
1540                 },\r
1541                 /** トラック数 */\r
1542                 TRACKS("Tracks", Integer.class, 40) {\r
1543                         @Override\r
1544                         public Object getValueOf(SequenceTrackListTableModel sequenceModel) {\r
1545                                 return sequenceModel.getSequence().getTracks().length;\r
1546                         }\r
1547                 },\r
1548                 /** タイミング分割形式 */\r
1549                 DIVISION_TYPE("DivType", String.class, 50) {\r
1550                         @Override\r
1551                         public Object getValueOf(SequenceTrackListTableModel sequenceModel) {\r
1552                                 float divType = sequenceModel.getSequence().getDivisionType();\r
1553                                 if( divType == Sequence.PPQ ) return "PPQ";\r
1554                                 else if( divType == Sequence.SMPTE_24 ) return "SMPTE_24";\r
1555                                 else if( divType == Sequence.SMPTE_25 ) return "SMPTE_25";\r
1556                                 else if( divType == Sequence.SMPTE_30 ) return "SMPTE_30";\r
1557                                 else if( divType == Sequence.SMPTE_30DROP ) return "SMPTE_30DROP";\r
1558                                 else return "[Unknown]";\r
1559                         }\r
1560                 };\r
1561                 String title;\r
1562                 Class<?> columnClass;\r
1563                 int preferredWidth;\r
1564                 /**\r
1565                  * 列の識別子を構築します。\r
1566                  * @param title 列のタイトル\r
1567                  * @param columnClass 列のクラス\r
1568                  */\r
1569                 private Column(String title, Class<?> columnClass, int preferredWidth) {\r
1570                         this.title = title;\r
1571                         this.columnClass = columnClass;\r
1572                         this.preferredWidth = preferredWidth;\r
1573                 }\r
1574                 public boolean isCellEditable() { return false; }\r
1575                 public Object getValueOf(SequenceTrackListTableModel sequenceModel) {\r
1576                         return "";\r
1577                 }\r
1578         }\r
1579 \r
1580         @Override\r
1581         public int getRowCount() { return sequenceList.size(); }\r
1582         @Override\r
1583         public int getColumnCount() { return Column.values().length; }\r
1584         @Override\r
1585         public String getColumnName(int column) {\r
1586                 return Column.values()[column].title;\r
1587         }\r
1588         @Override\r
1589         public Class<?> getColumnClass(int column) {\r
1590                 return Column.values()[column].columnClass;\r
1591         }\r
1592         @Override\r
1593         public boolean isCellEditable(int row, int column) {\r
1594                 return Column.values()[column].isCellEditable();\r
1595         }\r
1596         /** 再生中のシーケンサーの秒位置 */\r
1597         private SecondPosition secondPosition;\r
1598         @Override\r
1599         public Object getValueAt(int row, int column) {\r
1600                 Column c = Column.values()[column];\r
1601                 return c == Column.SEQ_NUMBER ? row : c.getValueOf(sequenceList.get(row));\r
1602         }\r
1603         @Override\r
1604         public void setValueAt(Object val, int row, int column) {\r
1605                 Column c = Column.values()[column];\r
1606                 switch(c) {\r
1607                 case FILENAME:\r
1608                         // ファイル名の変更\r
1609                         sequenceList.get(row).setFilename((String)val);\r
1610                         fireTableCellUpdated(row, column);\r
1611                         break;\r
1612                 case SEQ_NAME:\r
1613                         // シーケンス名の設定または変更\r
1614                         if( sequenceList.get(row).setName((String)val) )\r
1615                                 fireTableCellUpdated(row, Column.MODIFIED.ordinal());\r
1616                         fireTableCellUpdated(row, column);\r
1617                         break;\r
1618                 default:\r
1619                         break;\r
1620                 }\r
1621         }\r
1622         /**\r
1623          * このプレイリストに読み込まれた全シーケンスの合計時間長を返します。\r
1624          * @return 全シーケンスの合計時間長 [秒]\r
1625          */\r
1626         public int getTotalSeconds() {\r
1627                 int total = 0;\r
1628                 long usec;\r
1629                 for( SequenceTrackListTableModel m : sequenceList ) {\r
1630                         usec = m.getSequence().getMicrosecondLength();\r
1631                         total += (int)( (usec < 0 ? usec += 0x100000000L : usec)/1000L/1000L );\r
1632                 }\r
1633                 return total;\r
1634         }\r
1635         /**\r
1636          * 未保存の修正内容を持つシーケンスがあるか調べます。\r
1637          * @return 未保存の修正内容を持つシーケンスがあればtrue\r
1638          */\r
1639         public boolean isModified() {\r
1640                 for( SequenceTrackListTableModel m : sequenceList ) {\r
1641                         if( m.isModified() ) return true;\r
1642                 }\r
1643                 return false;\r
1644         }\r
1645         /**\r
1646          * 選択したシーケンスに未保存の修正内容があることを記録します。\r
1647          * @param selModel 選択状態\r
1648          * @param isModified 未保存の修正内容があるときtrue\r
1649          */\r
1650         public void setModified(boolean isModified) {\r
1651                 int minIndex = sequenceListSelectionModel.getMinSelectionIndex();\r
1652                 int maxIndex = sequenceListSelectionModel.getMaxSelectionIndex();\r
1653                 for( int i = minIndex; i <= maxIndex; i++ ) {\r
1654                         if( sequenceListSelectionModel.isSelectedIndex(i) ) {\r
1655                                 sequenceList.get(i).setModified(isModified);\r
1656                                 fireTableCellUpdated(i, Column.MODIFIED.ordinal());\r
1657                         }\r
1658                 }\r
1659         }\r
1660         /**\r
1661          * 選択されたMIDIシーケンスのテーブルモデルを返します。\r
1662          * @return 選択されたMIDIシーケンスのテーブルモデル(非選択時はnull)\r
1663          */\r
1664         public SequenceTrackListTableModel getSelectedSequenceModel() {\r
1665                 if( sequenceListSelectionModel.isSelectionEmpty() )\r
1666                         return null;\r
1667                 int selectedIndex = sequenceListSelectionModel.getMinSelectionIndex();\r
1668                 if( selectedIndex >= sequenceList.size() )\r
1669                         return null;\r
1670                 return sequenceList.get(selectedIndex);\r
1671         }\r
1672         /**\r
1673          * 指定されたシーケンスが修正されたことを通知します。\r
1674          * @param sequenceTableModel MIDIシーケンスモデル\r
1675          */\r
1676         public void fireSequenceModified(SequenceTrackListTableModel sequenceTableModel) {\r
1677                 int index = sequenceList.indexOf(sequenceTableModel);\r
1678                 if( index < 0 )\r
1679                         return;\r
1680                 sequenceTableModel.setModified(true);\r
1681                 fireTableRowsUpdated(index, index);\r
1682         }\r
1683         /**\r
1684          * 指定されている選択範囲のシーケンスが変更されたことを通知します。\r
1685          * 更新済みフラグをセットし、選択されたシーケンスの全ての列を再表示します。\r
1686          */\r
1687         public void fireSelectedSequenceChanged() {\r
1688                 if( sequenceListSelectionModel.isSelectionEmpty() )\r
1689                         return;\r
1690                 int minIndex = sequenceListSelectionModel.getMinSelectionIndex();\r
1691                 int maxIndex = sequenceListSelectionModel.getMaxSelectionIndex();\r
1692                 for( int index = minIndex; index <= maxIndex; index++ ) {\r
1693                         sequenceList.get(index).setModified(true);\r
1694                 }\r
1695                 fireTableRowsUpdated(minIndex, maxIndex);\r
1696         }\r
1697         /**\r
1698          * バイト列とファイル名からMIDIシーケンスを追加します。\r
1699          * バイト列が null の場合、空のMIDIシーケンスを追加します。\r
1700          * @param data バイト列\r
1701          * @param filename ファイル名\r
1702          * @return 追加先インデックス(先頭が 0)\r
1703          * @throws IOException ファイル読み込みに失敗した場合\r
1704          * @throws InvalidMidiDataException MIDIデータが正しくない場合\r
1705          */\r
1706         public int addSequence(byte[] data, String filename)\r
1707                 throws IOException, InvalidMidiDataException\r
1708         {\r
1709                 if( data == null ) return addDefaultSequence();\r
1710                 int lastIndex;\r
1711                 try (InputStream in = new ByteArrayInputStream(data)) {\r
1712                         Sequence seq = MidiSystem.getSequence(in);\r
1713                         lastIndex = addSequence(seq, filename);\r
1714                 } catch( IOException|InvalidMidiDataException e ) {\r
1715                         throw e;\r
1716                 }\r
1717                 sequenceListSelectionModel.setSelectionInterval(lastIndex, lastIndex);\r
1718                 return lastIndex;\r
1719         }\r
1720         /**\r
1721          * MIDIシーケンスを追加します。\r
1722          * シーケンサーが停止中の場合、追加したシーケンスから再生を開始します。\r
1723          * @param sequence MIDIシーケンス\r
1724          * @return 追加先インデックス(先頭が 0)\r
1725          */\r
1726         public int addSequenceAndPlay(Sequence sequence) {\r
1727                 int lastIndex = addSequence(sequence,"");\r
1728                 if( ! sequencerModel.getSequencer().isRunning() ) {\r
1729                         loadToSequencer(lastIndex);\r
1730                         sequencerModel.start();\r
1731                 }\r
1732                 return lastIndex;\r
1733         }\r
1734         /**\r
1735          * MIDIシーケンスを追加します。\r
1736          * @param sequence MIDIシーケンス\r
1737          * @param filename ファイル名\r
1738          * @return 追加されたシーケンスのインデックス(先頭が 0)\r
1739          */\r
1740         public int addSequence(Sequence sequence, String filename) {\r
1741                 sequenceList.add(\r
1742                         new SequenceTrackListTableModel(this, sequence, filename)\r
1743                 );\r
1744                 int lastIndex = sequenceList.size() - 1;\r
1745                 fireTableRowsInserted(lastIndex, lastIndex);\r
1746                 return lastIndex;\r
1747         }\r
1748         /**\r
1749          * デフォルトの内容でMIDIシーケンスを作成して追加します。\r
1750          * @return 追加されたMIDIシーケンスのインデックス(先頭が 0)\r
1751          */\r
1752         public int addDefaultSequence() {\r
1753                 Sequence seq = (new Music.ChordProgression()).toMidiSequence();\r
1754                 return seq == null ? -1 : addSequence(seq,null);\r
1755         }\r
1756         /**\r
1757          * MIDIファイルを追加します。\r
1758          * ファイルが null の場合、空のMIDIシーケンスを追加します。\r
1759          * @param midiFile MIDIファイル\r
1760          * @return 追加先インデックス(先頭が 0)\r
1761          * @throws InvalidMidiDataException ファイル内のMIDIデータが正しくない場合\r
1762          * @throws IOException ファイル入出力に失敗した場合\r
1763          */\r
1764         public int addSequence(File midiFile) throws InvalidMidiDataException, IOException {\r
1765                 if( midiFile == null ) return addDefaultSequence();\r
1766                 int lastIndex;\r
1767                 try (FileInputStream in = new FileInputStream(midiFile)) {\r
1768                         Sequence seq = MidiSystem.getSequence(in);\r
1769                         String filename = midiFile.getName();\r
1770                         lastIndex = addSequence(seq, filename);\r
1771                 } catch( InvalidMidiDataException|IOException e ) {\r
1772                         throw e;\r
1773                 }\r
1774                 return lastIndex;\r
1775         }\r
1776         /**\r
1777          * 複数のMIDIファイルを追加します。\r
1778          * @param fileList 追加するMIDIファイルのリスト\r
1779          * @return 追加先の最初のインデックス(先頭が 0、追加されなかった場合は -1)\r
1780          * @throws InvalidMidiDataException ファイル内のMIDIデータが正しくない場合\r
1781          * @throws IOException ファイル入出力に失敗した場合\r
1782          */\r
1783         public int addSequences(List<File> fileList)\r
1784                 throws InvalidMidiDataException, IOException\r
1785         {\r
1786                 int firstIndex = -1;\r
1787                 for( File file : fileList ) {\r
1788                         int lastIndex = addSequence(file);\r
1789                         if( firstIndex == -1 )\r
1790                                 firstIndex = lastIndex;\r
1791                 }\r
1792                 return firstIndex;\r
1793         }\r
1794         /**\r
1795          * URLから読み込んだMIDIシーケンスを追加します。\r
1796          * @param midiFileUrl MIDIファイルのURL\r
1797          * @return 追加先インデックス(先頭が 0、失敗した場合は -1)\r
1798          * @throws URISyntaxException URLの形式に誤りがある場合\r
1799          * @throws IOException 入出力に失敗した場合\r
1800          * @throws InvalidMidiDataException MIDIデータが正しくない場合\r
1801          */\r
1802         public int addSequenceFromURL(String midiFileUrl)\r
1803                 throws URISyntaxException, IOException, InvalidMidiDataException\r
1804         {\r
1805                 URI uri = new URI(midiFileUrl);\r
1806                 URL url = uri.toURL();\r
1807                 Sequence seq = MidiSystem.getSequence(url);\r
1808                 String filename = url.getFile().replaceFirst("^.*/","");\r
1809                 return addSequence(seq, filename);\r
1810         }\r
1811 \r
1812         /**\r
1813          * 選択したシーケンスを除去します。\r
1814          * @param listSelectionModel 選択状態\r
1815          */\r
1816         public void removeSelectedSequence() {\r
1817                 if( sequenceListSelectionModel.isSelectionEmpty() )\r
1818                         return;\r
1819                 int selectedIndex = sequenceListSelectionModel.getMinSelectionIndex();\r
1820                 if( sequenceList.remove(selectedIndex).isOnSequencer() ) {\r
1821                         // 削除したシーケンスが\r
1822                         // シーケンサーにロード済みだった場合、アンロードする。\r
1823                         sequencerModel.setSequenceTrackListTableModel(null);\r
1824                 }\r
1825                 fireTableRowsDeleted(selectedIndex, selectedIndex);\r
1826         }\r
1827         /**\r
1828          * 指定したインデックス位置のシーケンスをシーケンサーにロードします。\r
1829          * @param index シーケンスのインデックス位置(-1 を指定するとアンロードされます)\r
1830          */\r
1831         public void loadToSequencer(int index) {\r
1832                 SequenceTrackListTableModel oldSeq = sequencerModel.getSequenceTableModel();\r
1833                 SequenceTrackListTableModel newSeq = (index < 0 ? null : sequenceList.get(index));\r
1834                 if(oldSeq == newSeq)\r
1835                         return;\r
1836                 sequencerModel.setSequenceTrackListTableModel(newSeq);\r
1837                 int columnIndices[] = {\r
1838                         Column.SEQ_PLAY.ordinal(),\r
1839                         Column.SEQ_POSITION.ordinal(),\r
1840                 };\r
1841                 if( oldSeq != null ) {\r
1842                         int oldIndex = sequenceList.indexOf(oldSeq);\r
1843                         for( int columnIndex : columnIndices )\r
1844                                 fireTableCellUpdated(oldIndex, columnIndex);\r
1845                 }\r
1846                 if( newSeq != null )\r
1847                         for( int columnIndex : columnIndices )\r
1848                                 fireTableCellUpdated(index, columnIndex);\r
1849         }\r
1850         /**\r
1851          * 現在シーケンサにロードされているシーケンスのインデックスを返します。\r
1852          * ロードされていない場合は -1 を返します。\r
1853          * @return 現在シーケンサにロードされているシーケンスのインデックス\r
1854          */\r
1855         public int indexOfSequenceOnSequencer() {\r
1856                 return sequenceList.indexOf(sequencerModel.getSequenceTableModel());\r
1857         }\r
1858         /**\r
1859          * 引数で示された数だけ次へ進めたシーケンスをロードします。\r
1860          * @param offset 進みたいシーケンス数\r
1861          * @return 成功したらtrue\r
1862          */\r
1863         public boolean loadNext(int offset) {\r
1864                 int loadedIndex = indexOfSequenceOnSequencer();\r
1865                 int index = (loadedIndex < 0 ? 0 : loadedIndex + offset);\r
1866                 if( index < 0 || index >= sequenceList.size() )\r
1867                         return false;\r
1868                 loadToSequencer(index);\r
1869                 return true;\r
1870         }\r
1871 }\r
1872 \r
1873 /**\r
1874  * 選択されているトラックのインデックス\r
1875  */\r
1876 class TrackListSelectionModel extends DefaultListSelectionModel {\r
1877         {\r
1878                 setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);\r
1879         }\r
1880 }\r
1881 /**\r
1882  * MIDIシーケンス(トラックリスト)のテーブルデータモデル\r
1883  */\r
1884 class SequenceTrackListTableModel extends AbstractTableModel {\r
1885         /**\r
1886          * 列の列挙型\r
1887          */\r
1888         public enum Column {\r
1889                 /** トラック番号 */\r
1890                 TRACK_NUMBER("No.", Integer.class, 20),\r
1891                 /** イベント数 */\r
1892                 EVENTS("Events", Integer.class, 40),\r
1893                 /** Mute */\r
1894                 MUTE("Mute", Boolean.class, 30),\r
1895                 /** Solo */\r
1896                 SOLO("Solo", Boolean.class, 30),\r
1897                 /** 録音するMIDIチャンネル */\r
1898                 RECORD_CHANNEL("RecCh", String.class, 40),\r
1899                 /** MIDIチャンネル */\r
1900                 CHANNEL("Ch", String.class, 30),\r
1901                 /** トラック名 */\r
1902                 TRACK_NAME("Track name", String.class, 100);\r
1903                 String title;\r
1904                 Class<?> columnClass;\r
1905                 int preferredWidth;\r
1906                 /**\r
1907                  * 列の識別子を構築します。\r
1908                  * @param title 列のタイトル\r
1909                  * @param widthRatio 幅の割合\r
1910                  * @param columnClass 列のクラス\r
1911                  */\r
1912                 private Column(String title, Class<?> columnClass, int preferredWidth) {\r
1913                         this.title = title;\r
1914                         this.columnClass = columnClass;\r
1915                         this.preferredWidth = preferredWidth;\r
1916                 }\r
1917         }\r
1918         /**\r
1919          * 親のプレイリスト\r
1920          */\r
1921         SequenceListTableModel sequenceListTableModel;\r
1922         /**\r
1923          * ラップされたMIDIシーケンス\r
1924          */\r
1925         private Sequence sequence;\r
1926         /**\r
1927          * ラップされたMIDIシーケンスのtickインデックス\r
1928          */\r
1929         private SequenceTickIndex sequenceTickIndex;\r
1930         /**\r
1931          * MIDIファイル名\r
1932          */\r
1933         private String filename = "";\r
1934         /**\r
1935          * トラックリスト\r
1936          */\r
1937         private List<TrackEventListTableModel> trackModelList = new ArrayList<>();\r
1938         /**\r
1939          * 選択されているトラックのインデックス\r
1940          */\r
1941         TrackListSelectionModel trackListSelectionModel = new TrackListSelectionModel();\r
1942         /**\r
1943          * MIDIシーケンスとファイル名から {@link SequenceTrackListTableModel} を構築します。\r
1944          * @param sequenceListTableModel 親のプレイリスト\r
1945          * @param sequence MIDIシーケンス\r
1946          * @param filename ファイル名\r
1947          */\r
1948         public SequenceTrackListTableModel(\r
1949                 SequenceListTableModel sequenceListTableModel,\r
1950                 Sequence sequence,\r
1951                 String filename\r
1952         ) {\r
1953                 this.sequenceListTableModel = sequenceListTableModel;\r
1954                 setSequence(sequence);\r
1955                 setFilename(filename);\r
1956         }\r
1957         @Override\r
1958         public int getRowCount() {\r
1959                 return sequence == null ? 0 : sequence.getTracks().length;\r
1960         }\r
1961         @Override\r
1962         public int getColumnCount() {\r
1963                 return Column.values().length;\r
1964         }\r
1965         /**\r
1966          * 列名を返します。\r
1967          * @return 列名\r
1968          */\r
1969         @Override\r
1970         public String getColumnName(int column) {\r
1971                 return Column.values()[column].title;\r
1972         }\r
1973         /**\r
1974          * 指定された列の型を返します。\r
1975          * @return 指定された列の型\r
1976          */\r
1977         @Override\r
1978         public Class<?> getColumnClass(int column) {\r
1979                 Column c = Column.values()[column];\r
1980                 switch(c) {\r
1981                 case MUTE:\r
1982                 case SOLO: if( ! isOnSequencer() ) return String.class;\r
1983                         // FALLTHROUGH\r
1984                 default: return c.columnClass;\r
1985                 }\r
1986         }\r
1987         @Override\r
1988         public Object getValueAt(int row, int column) {\r
1989                 Column c = Column.values()[column];\r
1990                 switch(c) {\r
1991                 case TRACK_NUMBER: return row;\r
1992                 case EVENTS: return sequence.getTracks()[row].size();\r
1993                 case MUTE:\r
1994                         return isOnSequencer() ? sequenceListTableModel.sequencerModel.getSequencer().getTrackMute(row) : "";\r
1995                 case SOLO:\r
1996                         return isOnSequencer() ? sequenceListTableModel.sequencerModel.getSequencer().getTrackSolo(row) : "";\r
1997                 case RECORD_CHANNEL:\r
1998                         return isOnSequencer() ? trackModelList.get(row).getRecordingChannel() : "";\r
1999                 case CHANNEL: {\r
2000                         int ch = trackModelList.get(row).getChannel();\r
2001                         return ch < 0 ? "" : ch + 1 ;\r
2002                 }\r
2003                 case TRACK_NAME: return trackModelList.get(row).toString();\r
2004                 default: return "";\r
2005                 }\r
2006         }\r
2007         /**\r
2008          * セルが編集可能かどうかを返します。\r
2009          */\r
2010         @Override\r
2011         public boolean isCellEditable(int row, int column) {\r
2012                 Column c = Column.values()[column];\r
2013                 switch(c) {\r
2014                 case MUTE:\r
2015                 case SOLO:\r
2016                 case RECORD_CHANNEL: return isOnSequencer();\r
2017                 case CHANNEL:\r
2018                 case TRACK_NAME: return true;\r
2019                 default: return false;\r
2020                 }\r
2021         }\r
2022         /**\r
2023          * 列の値を設定します。\r
2024          */\r
2025         @Override\r
2026         public void setValueAt(Object val, int row, int column) {\r
2027                 Column c = Column.values()[column];\r
2028                 switch(c) {\r
2029                 case MUTE:\r
2030                         sequenceListTableModel.sequencerModel.getSequencer().setTrackMute(row, ((Boolean)val).booleanValue());\r
2031                         break;\r
2032                 case SOLO:\r
2033                         sequenceListTableModel.sequencerModel.getSequencer().setTrackSolo(row, ((Boolean)val).booleanValue());\r
2034                         break;\r
2035                 case RECORD_CHANNEL:\r
2036                         trackModelList.get(row).setRecordingChannel((String)val);\r
2037                         break;\r
2038                 case CHANNEL: {\r
2039                         Integer ch;\r
2040                         try {\r
2041                                 ch = new Integer((String)val);\r
2042                         }\r
2043                         catch( NumberFormatException e ) {\r
2044                                 ch = -1;\r
2045                                 break;\r
2046                         }\r
2047                         if( --ch <= 0 || ch > MIDISpec.MAX_CHANNELS )\r
2048                                 break;\r
2049                         TrackEventListTableModel trackTableModel = trackModelList.get(row);\r
2050                         if( ch == trackTableModel.getChannel() ) break;\r
2051                         trackTableModel.setChannel(ch);\r
2052                         setModified(true);\r
2053                         fireTableCellUpdated(row, Column.EVENTS.ordinal());\r
2054                         break;\r
2055                 }\r
2056                 case TRACK_NAME:\r
2057                         trackModelList.get(row).setString((String)val);\r
2058                         break;\r
2059                 default:\r
2060                         break;\r
2061                 }\r
2062                 fireTableCellUpdated(row,column);\r
2063         }\r
2064         /**\r
2065          * MIDIシーケンスを返します。\r
2066          * @return MIDIシーケンス\r
2067          */\r
2068         public Sequence getSequence() { return sequence; }\r
2069         /**\r
2070          * シーケンスtickインデックスを返します。\r
2071          * @return シーケンスtickインデックス\r
2072          */\r
2073         public SequenceTickIndex getSequenceTickIndex() {\r
2074                 return sequenceTickIndex;\r
2075         }\r
2076         /**\r
2077          * MIDIシーケンスを設定します。\r
2078          * @param sequence MIDIシーケンス(nullを指定するとトラックリストが空になる)\r
2079          */\r
2080         private void setSequence(Sequence sequence) {\r
2081                 // シーケンサーの録音を中止\r
2082                 sequenceListTableModel.sequencerModel.getSequencer().recordDisable(null); // The "null" means all tracks\r
2083                 // トラックリストをクリア\r
2084                 int oldSize = trackModelList.size();\r
2085                 if( oldSize > 0 ) {\r
2086                         trackModelList.clear();\r
2087                         fireTableRowsDeleted(0, oldSize-1);\r
2088                 }\r
2089                 if( (this.sequence = sequence) == null ) {\r
2090                         sequenceTickIndex = null;\r
2091                         return;\r
2092                 }\r
2093                 // 新しいシーケンスからtickインデックスとトラックリストを再構築\r
2094                 fireTimeSignatureChanged();\r
2095                 Track tracks[] = sequence.getTracks();\r
2096                 for(Track track : tracks) {\r
2097                         trackModelList.add(new TrackEventListTableModel(this, track));\r
2098                 }\r
2099                 fireTableRowsInserted(0, tracks.length-1);\r
2100         }\r
2101         /**\r
2102          * 拍子が変更されたとき、シーケンスtickインデックスを再作成します。\r
2103          */\r
2104         public void fireTimeSignatureChanged() {\r
2105                 sequenceTickIndex = new SequenceTickIndex(sequence);\r
2106         }\r
2107         private boolean isModified = false;\r
2108         /**\r
2109          * 変更されたかどうかを返します。\r
2110          * @return 変更済みのときtrue\r
2111          */\r
2112         public boolean isModified() { return isModified; }\r
2113         /**\r
2114          * 変更されたかどうかを設定します。\r
2115          * @param isModified 変更されたときtrue\r
2116          */\r
2117         public void setModified(boolean isModified) { this.isModified = isModified; }\r
2118         /**\r
2119          * ファイル名を設定します。\r
2120          * @param filename ファイル名\r
2121          */\r
2122         public void setFilename(String filename) { this.filename = filename; }\r
2123         /**\r
2124          * ファイル名を返します。\r
2125          * @return ファイル名\r
2126          */\r
2127         public String getFilename() { return filename; }\r
2128         @Override\r
2129         public String toString() { return MIDISpec.getNameOf(sequence); }\r
2130         /**\r
2131          * シーケンス名を設定します。\r
2132          * @param name シーケンス名\r
2133          * @return 成功したらtrue\r
2134          */\r
2135         public boolean setName(String name) {\r
2136                 if( name.equals(toString()) || ! MIDISpec.setNameOf(sequence,name) )\r
2137                         return false;\r
2138                 setModified(true);\r
2139                 fireTableDataChanged();\r
2140                 return true;\r
2141         }\r
2142         /**\r
2143          * このシーケンスのMIDIデータのバイト列を返します。\r
2144          * @return MIDIデータのバイト列(失敗した場合null)\r
2145          */\r
2146         public byte[] getMIDIdata() {\r
2147                 if( sequence == null || sequence.getTracks().length == 0 ) {\r
2148                         return null;\r
2149                 }\r
2150                 try( ByteArrayOutputStream out = new ByteArrayOutputStream() ) {\r
2151                         MidiSystem.write(sequence, 1, out);\r
2152                         return out.toByteArray();\r
2153                 } catch ( IOException e ) {\r
2154                         e.printStackTrace();\r
2155                         return null;\r
2156                 }\r
2157         }\r
2158         /**\r
2159          * 指定のトラックが変更されたことを通知します。\r
2160          * @param track トラック\r
2161          */\r
2162         public void fireTrackChanged(Track track) {\r
2163                 int row = indexOf(track);\r
2164                 if( row < 0 ) return;\r
2165                 fireTableRowsUpdated(row, row);\r
2166                 sequenceListTableModel.fireSequenceModified(this);\r
2167         }\r
2168         /**\r
2169          * 選択されているトラックモデルを返します。\r
2170          * @param index トラックのインデックス\r
2171          * @return トラックモデル(見つからない場合null)\r
2172          */\r
2173         public TrackEventListTableModel getSelectedTrackModel() {\r
2174                 if( trackListSelectionModel.isSelectionEmpty() )\r
2175                         return null;\r
2176                 int index = trackListSelectionModel.getMinSelectionIndex();\r
2177                 Track tracks[] = sequence.getTracks();\r
2178                 if( tracks.length != 0 ) {\r
2179                         Track track = tracks[index];\r
2180                         for( TrackEventListTableModel model : trackModelList )\r
2181                                 if( model.getTrack() == track )\r
2182                                         return model;\r
2183                 }\r
2184                 return null;\r
2185         }\r
2186         /**\r
2187          * 指定のトラックがある位置のインデックスを返します。\r
2188          * @param track トラック\r
2189          * @return トラックのインデックス(先頭 0、トラックが見つからない場合 -1)\r
2190          */\r
2191         public int indexOf(Track track) {\r
2192                 Track tracks[] = sequence.getTracks();\r
2193                 for( int i=0; i<tracks.length; i++ )\r
2194                         if( tracks[i] == track )\r
2195                                 return i;\r
2196                 return -1;\r
2197         }\r
2198         /**\r
2199          * 新しいトラックを生成し、末尾に追加します。\r
2200          * @return 追加したトラックのインデックス(先頭 0)\r
2201          */\r
2202         public int createTrack() {\r
2203                 Track newTrack = sequence.createTrack();\r
2204                 trackModelList.add(new TrackEventListTableModel(this, newTrack));\r
2205                 int lastRow = getRowCount() - 1;\r
2206                 fireTableRowsInserted(lastRow, lastRow);\r
2207                 sequenceListTableModel.fireSelectedSequenceChanged();\r
2208                 trackListSelectionModel.setSelectionInterval(lastRow, lastRow);\r
2209                 return lastRow;\r
2210         }\r
2211         /**\r
2212          * 選択されているトラックを削除します。\r
2213          */\r
2214         public void deleteSelectedTracks() {\r
2215                 if( trackListSelectionModel.isSelectionEmpty() )\r
2216                         return;\r
2217                 int minIndex = trackListSelectionModel.getMinSelectionIndex();\r
2218                 int maxIndex = trackListSelectionModel.getMaxSelectionIndex();\r
2219                 Track tracks[] = sequence.getTracks();\r
2220                 for( int i = maxIndex; i >= minIndex; i-- ) {\r
2221                         if( ! trackListSelectionModel.isSelectedIndex(i) )\r
2222                                 continue;\r
2223                         sequence.deleteTrack(tracks[i]);\r
2224                         trackModelList.remove(i);\r
2225                 }\r
2226                 fireTableRowsDeleted(minIndex, maxIndex);\r
2227                 sequenceListTableModel.fireSelectedSequenceChanged();\r
2228         }\r
2229         /**\r
2230          * このシーケンスモデルのシーケンスをシーケンサーが操作しているか調べます。\r
2231          * @return シーケンサーが操作していたらtrue\r
2232          */\r
2233         public boolean isOnSequencer() {\r
2234                 return sequence == sequenceListTableModel.sequencerModel.getSequencer().getSequence();\r
2235         }\r
2236         /**\r
2237          * 録音しようとしているチャンネルの設定されたトラックがあるか調べます。\r
2238          * @return 該当トラックがあればtrue\r
2239          */\r
2240         public boolean hasRecordChannel() {\r
2241                 int rowCount = getRowCount();\r
2242                 for( int row=0; row < rowCount; row++ ) {\r
2243                         Object value = getValueAt(row, Column.RECORD_CHANNEL.ordinal());\r
2244                         if( ! "OFF".equals(value) ) return true;\r
2245                 }\r
2246                 return false;\r
2247         }\r
2248 }\r
2249 \r
2250 /**\r
2251  * 選択されているイベントのインデックス\r
2252  */\r
2253 class EventListSelectionModel extends DefaultListSelectionModel {\r
2254         {\r
2255                 setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);\r
2256         }\r
2257 }\r
2258 /**\r
2259  * MIDIトラック(MIDIイベントリスト)テーブルモデル\r
2260  */\r
2261 class TrackEventListTableModel extends AbstractTableModel {\r
2262         /**\r
2263          * 列\r
2264          */\r
2265         public enum Column {\r
2266                 /** MIDIイベント番号 */\r
2267                 EVENT_NUMBER("No.", Integer.class, 20),\r
2268                 /** tick位置 */\r
2269                 TICK_POSITION("TickPos.", Long.class, 40),\r
2270                 /** tick位置に対応する小節 */\r
2271                 MEASURE_POSITION("Measure", Integer.class, 40),\r
2272                 /** tick位置に対応する拍 */\r
2273                 BEAT_POSITION("Beat", Integer.class, 20),\r
2274                 /** tick位置に対応する余剰tick(拍に収まらずに余ったtick数) */\r
2275                 EXTRA_TICK_POSITION("ExTick", Integer.class, 20),\r
2276                 /** MIDIメッセージ */\r
2277                 MESSAGE("MIDI Message", String.class, 200);\r
2278                 private String title;\r
2279                 private Class<?> columnClass;\r
2280                 int preferredWidth;\r
2281                 /**\r
2282                  * 列の識別子を構築します。\r
2283                  * @param title 列のタイトル\r
2284                  * @param widthRatio 幅の割合\r
2285                  * @param columnClass 列のクラス\r
2286                  */\r
2287                 private Column(String title, Class<?> columnClass, int preferredWidth) {\r
2288                         this.title = title;\r
2289                         this.columnClass = columnClass;\r
2290                         this.preferredWidth = preferredWidth;\r
2291                 }\r
2292         }\r
2293         /**\r
2294          * ラップされているMIDIトラック\r
2295          */\r
2296         private Track track;\r
2297         /**\r
2298          * 親のシーケンスモデル\r
2299          */\r
2300         SequenceTrackListTableModel sequenceTrackListTableModel;\r
2301         /**\r
2302          * 選択されているイベントのインデックス\r
2303          */\r
2304         EventListSelectionModel eventSelectionModel = new EventListSelectionModel();\r
2305         /**\r
2306          * シーケンスを親にして、その特定のトラックに連動する\r
2307          * MIDIトラックモデルを構築します。\r
2308          *\r
2309          * @param parent 親のシーケンスモデル\r
2310          * @param track ラップするMIDIトラック(ない場合はnull)\r
2311          */\r
2312         public TrackEventListTableModel(\r
2313                 SequenceTrackListTableModel sequenceTrackListTableModel, Track track\r
2314         ) {\r
2315                 this.track = track;\r
2316                 this.sequenceTrackListTableModel = sequenceTrackListTableModel;\r
2317         }\r
2318         @Override\r
2319         public int getRowCount() {\r
2320                 return track == null ? 0 : track.size();\r
2321         }\r
2322         @Override\r
2323         public int getColumnCount() {\r
2324                 return Column.values().length;\r
2325         }\r
2326         /**\r
2327          * 列名を返します。\r
2328          */\r
2329         @Override\r
2330         public String getColumnName(int column) {\r
2331                 return Column.values()[column].title;\r
2332         }\r
2333         /**\r
2334          * 列のクラスを返します。\r
2335          */\r
2336         @Override\r
2337         public Class<?> getColumnClass(int column) {\r
2338                 return Column.values()[column].columnClass;\r
2339         }\r
2340         @Override\r
2341         public Object getValueAt(int row, int column) {\r
2342                 switch(Column.values()[column]) {\r
2343                 case EVENT_NUMBER: return row;\r
2344                 case TICK_POSITION: return track.get(row).getTick();\r
2345                 case MEASURE_POSITION:\r
2346                         return sequenceTrackListTableModel.getSequenceTickIndex().tickToMeasure(track.get(row).getTick()) + 1;\r
2347                 case BEAT_POSITION:\r
2348                         sequenceTrackListTableModel.getSequenceTickIndex().tickToMeasure(track.get(row).getTick());\r
2349                         return sequenceTrackListTableModel.getSequenceTickIndex().lastBeat + 1;\r
2350                 case EXTRA_TICK_POSITION:\r
2351                         sequenceTrackListTableModel.getSequenceTickIndex().tickToMeasure(track.get(row).getTick());\r
2352                         return sequenceTrackListTableModel.getSequenceTickIndex().lastExtraTick;\r
2353                 case MESSAGE: return msgToString(track.get(row).getMessage());\r
2354                 default: return "";\r
2355                 }\r
2356         }\r
2357         /**\r
2358          * セルを編集できるときtrue、編集できないときfalseを返します。\r
2359          */\r
2360         @Override\r
2361         public boolean isCellEditable(int row, int column) {\r
2362                 switch(Column.values()[column]) {\r
2363                 case TICK_POSITION:\r
2364                 case MEASURE_POSITION:\r
2365                 case BEAT_POSITION:\r
2366                 case EXTRA_TICK_POSITION:\r
2367                 case MESSAGE: return true;\r
2368                 default: return false;\r
2369                 }\r
2370         }\r
2371         /**\r
2372          * セルの値を変更します。\r
2373          */\r
2374         @Override\r
2375         public void setValueAt(Object value, int row, int column) {\r
2376                 long newTick;\r
2377                 switch(Column.values()[column]) {\r
2378                 case TICK_POSITION: newTick = (Long)value; break;\r
2379                 case MEASURE_POSITION:\r
2380                         newTick = sequenceTrackListTableModel.getSequenceTickIndex().measureToTick(\r
2381                                 (Integer)value - 1,\r
2382                                 (Integer)getValueAt( row, Column.BEAT_POSITION.ordinal() ) - 1,\r
2383                                 (Integer)getValueAt( row, Column.EXTRA_TICK_POSITION.ordinal() )\r
2384                         );\r
2385                         break;\r
2386                 case BEAT_POSITION:\r
2387                         newTick = sequenceTrackListTableModel.getSequenceTickIndex().measureToTick(\r
2388                                 (Integer)getValueAt( row, Column.MEASURE_POSITION.ordinal() ) - 1,\r
2389                                 (Integer)value - 1,\r
2390                                 (Integer)getValueAt( row, Column.EXTRA_TICK_POSITION.ordinal() )\r
2391                         );\r
2392                         break;\r
2393                 case EXTRA_TICK_POSITION:\r
2394                         newTick = sequenceTrackListTableModel.getSequenceTickIndex().measureToTick(\r
2395                                 (Integer)getValueAt( row, Column.MEASURE_POSITION.ordinal() ) - 1,\r
2396                                 (Integer)getValueAt( row, Column.BEAT_POSITION.ordinal() ) - 1,\r
2397                                 (Integer)value\r
2398                         );\r
2399                         break;\r
2400                 default: return;\r
2401                 }\r
2402                 MidiEvent oldMidiEvent = track.get(row);\r
2403                 if( oldMidiEvent.getTick() == newTick ) {\r
2404                         return;\r
2405                 }\r
2406                 MidiMessage msg = oldMidiEvent.getMessage();\r
2407                 MidiEvent newMidiEvent = new MidiEvent(msg,newTick);\r
2408                 track.remove(oldMidiEvent);\r
2409                 track.add(newMidiEvent);\r
2410                 fireTableDataChanged();\r
2411                 if( MIDISpec.isEOT(msg) ) {\r
2412                         // EOTの場所が変わると曲の長さが変わるので、親モデルへ通知する。\r
2413                         sequenceTrackListTableModel.sequenceListTableModel.fireSequenceModified(sequenceTrackListTableModel);\r
2414                 }\r
2415         }\r
2416         /**\r
2417          * MIDIトラックを返します。\r
2418          * @return MIDIトラック\r
2419          */\r
2420         public Track getTrack() { return track; }\r
2421         /**\r
2422          * トラック名を返します。\r
2423          */\r
2424         @Override\r
2425         public String toString() { return MIDISpec.getNameOf(track); }\r
2426         /**\r
2427          * トラック名を設定します。\r
2428          * @param name トラック名\r
2429          * @return 設定が行われたらtrue\r
2430          */\r
2431         public boolean setString(String name) {\r
2432                 if(name.equals(toString()) || ! MIDISpec.setNameOf(track, name))\r
2433                         return false;\r
2434                 sequenceTrackListTableModel.setModified(true);\r
2435                 sequenceTrackListTableModel.sequenceListTableModel.fireSequenceModified(sequenceTrackListTableModel);\r
2436                 fireTableDataChanged();\r
2437                 return true;\r
2438         }\r
2439         private String recordingChannel = "OFF";\r
2440         /**\r
2441          * 録音中のMIDIチャンネルを返します。\r
2442          * @return 録音中のMIDIチャンネル\r
2443          */\r
2444         public String getRecordingChannel() { return recordingChannel; }\r
2445         /**\r
2446          * 録音中のMIDIチャンネルを設定します。\r
2447          * @param recordingChannel 録音中のMIDIチャンネル\r
2448          */\r
2449         public void setRecordingChannel(String recordingChannel) {\r
2450                 Sequencer sequencer = sequenceTrackListTableModel.sequenceListTableModel.sequencerModel.getSequencer();\r
2451                 if( recordingChannel.equals("OFF") ) {\r
2452                         sequencer.recordDisable( track );\r
2453                 }\r
2454                 else if( recordingChannel.equals("ALL") ) {\r
2455                         sequencer.recordEnable( track, -1 );\r
2456                 }\r
2457                 else {\r
2458                         try {\r
2459                                 int ch = Integer.decode(recordingChannel).intValue() - 1;\r
2460                                 sequencer.recordEnable( track, ch );\r
2461                         } catch( NumberFormatException nfe ) {\r
2462                                 sequencer.recordDisable( track );\r
2463                                 this.recordingChannel = "OFF";\r
2464                                 return;\r
2465                         }\r
2466                 }\r
2467                 this.recordingChannel = recordingChannel;\r
2468         }\r
2469         /**\r
2470          * このトラックの対象MIDIチャンネルを返します。\r
2471          * <p>全てのチャンネルメッセージが同じMIDIチャンネルの場合、\r
2472          * そのMIDIチャンネルを返します。\r
2473          * MIDIチャンネルの異なるチャンネルメッセージが一つでも含まれていた場合、\r
2474          * -1 を返します。\r
2475          * </p>\r
2476          * @return 対象MIDIチャンネル(不統一の場合 -1)\r
2477          */\r
2478         public int getChannel() {\r
2479                 int prevCh = -1;\r
2480                 int trackSize = track.size();\r
2481                 for( int index=0; index < trackSize; index++ ) {\r
2482                         MidiMessage msg = track.get(index).getMessage();\r
2483                         if( ! (msg instanceof ShortMessage) )\r
2484                                 continue;\r
2485                         ShortMessage smsg = (ShortMessage)msg;\r
2486                         if( ! MIDISpec.isChannelMessage(smsg) )\r
2487                                 continue;\r
2488                         int ch = smsg.getChannel();\r
2489                         if( prevCh >= 0 && prevCh != ch ) {\r
2490                                 return -1;\r
2491                         }\r
2492                         prevCh = ch;\r
2493                 }\r
2494                 return prevCh;\r
2495         }\r
2496         /**\r
2497          * 指定されたMIDIチャンネルをすべてのチャンネルメッセージに対して設定します。\r
2498          * @param channel MIDIチャンネル\r
2499          */\r
2500         public void setChannel(int channel) {\r
2501                 int track_size = track.size();\r
2502                 for( int index=0; index < track_size; index++ ) {\r
2503                         MidiMessage msg = track.get(index).getMessage();\r
2504                         if( ! (msg instanceof ShortMessage) )\r
2505                                 continue;\r
2506                         ShortMessage smsg = (ShortMessage)msg;\r
2507                         if( ! MIDISpec.isChannelMessage(smsg) )\r
2508                                 continue;\r
2509                         if( smsg.getChannel() == channel )\r
2510                                 continue;\r
2511                         try {\r
2512                                 smsg.setMessage(\r
2513                                         smsg.getCommand(), channel,\r
2514                                         smsg.getData1(), smsg.getData2()\r
2515                                 );\r
2516                         }\r
2517                         catch( InvalidMidiDataException e ) {\r
2518                                 e.printStackTrace();\r
2519                         }\r
2520                         sequenceTrackListTableModel.setModified(true);\r
2521                 }\r
2522                 sequenceTrackListTableModel.fireTrackChanged(track);\r
2523                 fireTableDataChanged();\r
2524         }\r
2525         /**\r
2526          * 指定の MIDI tick 位置にあるイベントを二分探索し、\r
2527          * そのイベントの行インデックスを返します。\r
2528          * @param tick MIDI tick\r
2529          * @return 行インデックス\r
2530          */\r
2531         public int tickToIndex(long tick) {\r
2532                 if( track == null )\r
2533                         return 0;\r
2534                 int minIndex = 0;\r
2535                 int maxIndex = track.size() - 1;\r
2536                 while( minIndex < maxIndex ) {\r
2537                         int currentIndex = (minIndex + maxIndex) / 2 ;\r
2538                         long currentTick = track.get(currentIndex).getTick();\r
2539                         if( tick > currentTick ) {\r
2540                                 minIndex = currentIndex + 1;\r
2541                         }\r
2542                         else if( tick < currentTick ) {\r
2543                                 maxIndex = currentIndex - 1;\r
2544                         }\r
2545                         else {\r
2546                                 return currentIndex;\r
2547                         }\r
2548                 }\r
2549                 return (minIndex + maxIndex) / 2;\r
2550         }\r
2551         /**\r
2552          * NoteOn/NoteOff ペアの一方の行インデックスから、\r
2553          * もう一方(ペアの相手)の行インデックスを返します。\r
2554          * @param index 行インデックス\r
2555          * @return ペアを構成する相手の行インデックス(ない場合は -1)\r
2556          */\r
2557         public int getIndexOfPartnerFor(int index) {\r
2558                 if( track == null || index >= track.size() )\r
2559                         return -1;\r
2560                 MidiMessage msg = track.get(index).getMessage();\r
2561                 if( ! (msg instanceof ShortMessage) ) return -1;\r
2562                 ShortMessage sm = (ShortMessage)msg;\r
2563                 int cmd = sm.getCommand();\r
2564                 int i;\r
2565                 int ch = sm.getChannel();\r
2566                 int note = sm.getData1();\r
2567                 MidiMessage partner_msg;\r
2568                 ShortMessage partner_sm;\r
2569                 int partner_cmd;\r
2570 \r
2571                 switch( cmd ) {\r
2572                 case 0x90: // NoteOn\r
2573                 if( sm.getData2() > 0 ) {\r
2574                         // Search NoteOff event forward\r
2575                         for( i = index + 1; i < track.size(); i++ ) {\r
2576                                 partner_msg = track.get(i).getMessage();\r
2577                                 if( ! (partner_msg instanceof ShortMessage ) ) continue;\r
2578                                 partner_sm = (ShortMessage)partner_msg;\r
2579                                 partner_cmd = partner_sm.getCommand();\r
2580                                 if( partner_cmd != 0x80 && partner_cmd != 0x90 ||\r
2581                                                 partner_cmd == 0x90 && partner_sm.getData2() > 0\r
2582                                                 ) {\r
2583                                         // Not NoteOff\r
2584                                         continue;\r
2585                                 }\r
2586                                 if( ch != partner_sm.getChannel() || note != partner_sm.getData1() ) {\r
2587                                         // Not my partner\r
2588                                         continue;\r
2589                                 }\r
2590                                 return i;\r
2591                         }\r
2592                         break;\r
2593                 }\r
2594                 // When velocity is 0, it means Note Off, so no break.\r
2595                 case 0x80: // NoteOff\r
2596                         // Search NoteOn event backward\r
2597                         for( i = index - 1; i >= 0; i-- ) {\r
2598                                 partner_msg = track.get(i).getMessage();\r
2599                                 if( ! (partner_msg instanceof ShortMessage ) ) continue;\r
2600                                 partner_sm = (ShortMessage)partner_msg;\r
2601                                 partner_cmd = partner_sm.getCommand();\r
2602                                 if( partner_cmd != 0x90 || partner_sm.getData2() <= 0 ) {\r
2603                                         // Not NoteOn\r
2604                                         continue;\r
2605                                 }\r
2606                                 if( ch != partner_sm.getChannel() || note != partner_sm.getData1() ) {\r
2607                                         // Not my partner\r
2608                                         continue;\r
2609                                 }\r
2610                                 return i;\r
2611                         }\r
2612                         break;\r
2613                 }\r
2614                 // Not found\r
2615                 return -1;\r
2616         }\r
2617         /**\r
2618          * ノートメッセージかどうか調べます。\r
2619          * @param index 行インデックス\r
2620          * @return Note On または Note Off のとき true\r
2621          */\r
2622         public boolean isNote(int index) {\r
2623                 MidiEvent midi_evt = getMidiEvent(index);\r
2624                 MidiMessage msg = midi_evt.getMessage();\r
2625                 if( ! (msg instanceof ShortMessage) ) return false;\r
2626                 int cmd = ((ShortMessage)msg).getCommand();\r
2627                 return cmd == ShortMessage.NOTE_ON || cmd == ShortMessage.NOTE_OFF ;\r
2628         }\r
2629         /**\r
2630          * 指定の行インデックスのMIDIイベントを返します。\r
2631          * @param index 行インデックス\r
2632          * @return MIDIイベント\r
2633          */\r
2634         public MidiEvent getMidiEvent(int index) { return track.get(index); }\r
2635         /**\r
2636          * 選択されているMIDIイベントを返します。\r
2637          * @return 選択されているMIDIイベント\r
2638          */\r
2639         public MidiEvent[] getSelectedMidiEvents() {\r
2640                 Vector<MidiEvent> events = new Vector<MidiEvent>();\r
2641                 if( ! eventSelectionModel.isSelectionEmpty() ) {\r
2642                         int i = eventSelectionModel.getMinSelectionIndex();\r
2643                         int max = eventSelectionModel.getMaxSelectionIndex();\r
2644                         for( ; i <= max; i++ )\r
2645                                 if( eventSelectionModel.isSelectedIndex(i) )\r
2646                                         events.add(track.get(i));\r
2647                 }\r
2648                 return events.toArray(new MidiEvent[1]);\r
2649         }\r
2650         /**\r
2651          * MIDIイベントを追加します。\r
2652          * @param midiEvent 追加するMIDIイベント\r
2653          * @return 追加できたらtrue\r
2654          */\r
2655         public boolean addMidiEvent(MidiEvent midiEvent) {\r
2656                 if( !(track.add(midiEvent)) )\r
2657                         return false;\r
2658                 if( MIDISpec.isTimeSignature(midiEvent.getMessage()) )\r
2659                         sequenceTrackListTableModel.fireTimeSignatureChanged();\r
2660                 sequenceTrackListTableModel.fireTrackChanged(track);\r
2661                 int last_index = track.size() - 1;\r
2662                 fireTableRowsInserted( last_index-1, last_index-1 );\r
2663                 return true;\r
2664         }\r
2665         /**\r
2666          * MIDIイベントを追加します。\r
2667          * @param midiEvents 追加するMIDIイベント\r
2668          * @param destinationTick 追加先tick\r
2669          * @param sourcePPQ PPQ値(タイミング解像度)\r
2670          * @return 追加できたらtrue\r
2671          */\r
2672         public boolean addMidiEvents(MidiEvent midiEvents[], long destinationTick, int sourcePPQ) {\r
2673                 int destinationPPQ = sequenceTrackListTableModel.getSequence().getResolution();\r
2674                 boolean done = false;\r
2675                 boolean hasTimeSignature = false;\r
2676                 long firstSourceEventTick = -1;\r
2677                 for( MidiEvent sourceEvent : midiEvents ) {\r
2678                         long sourceEventTick = sourceEvent.getTick();\r
2679                         MidiMessage msg = sourceEvent.getMessage();\r
2680                         long newTick = destinationTick;\r
2681                         if( firstSourceEventTick < 0 ) {\r
2682                                 firstSourceEventTick = sourceEventTick;\r
2683                         }\r
2684                         else {\r
2685                                 newTick += (sourceEventTick - firstSourceEventTick) * destinationPPQ / sourcePPQ;\r
2686                         }\r
2687                         if( ! track.add(new MidiEvent(msg, newTick)) ) continue;\r
2688                         done = true;\r
2689                         if( MIDISpec.isTimeSignature(msg) ) hasTimeSignature = true;\r
2690                 }\r
2691                 if( done ) {\r
2692                         if( hasTimeSignature ) sequenceTrackListTableModel.fireTimeSignatureChanged();\r
2693                         sequenceTrackListTableModel.fireTrackChanged(track);\r
2694                         int lastIndex = track.size() - 1;\r
2695                         int oldLastIndex = lastIndex - midiEvents.length;\r
2696                         fireTableRowsInserted(oldLastIndex, lastIndex);\r
2697                 }\r
2698                 return done;\r
2699         }\r
2700         /**\r
2701          * MIDIイベントを除去します。\r
2702          * 曲の長さが変わることがあるので、プレイリストにも通知します。\r
2703          * @param midiEvents 除去するMIDIイベント\r
2704          */\r
2705         public void removeMidiEvents(MidiEvent midiEvents[]) {\r
2706                 boolean hadTimeSignature = false;\r
2707                 for( MidiEvent e : midiEvents ) {\r
2708                         if( MIDISpec.isTimeSignature(e.getMessage()) )\r
2709                                 hadTimeSignature = true;\r
2710                         track.remove(e);\r
2711                 }\r
2712                 if( hadTimeSignature ) {\r
2713                         sequenceTrackListTableModel.fireTimeSignatureChanged();\r
2714                 }\r
2715                 sequenceTrackListTableModel.fireTrackChanged(track);\r
2716                 int lastIndex = track.size() - 1;\r
2717                 int oldLastIndex = lastIndex + midiEvents.length;\r
2718                 if(lastIndex < 0) lastIndex = 0;\r
2719                 fireTableRowsDeleted(oldLastIndex, lastIndex);\r
2720                 sequenceTrackListTableModel.sequenceListTableModel.fireSelectedSequenceChanged();\r
2721         }\r
2722         /**\r
2723          * 引数の選択内容が示すMIDIイベントを除去します。\r
2724          * @param selectionModel 選択内容\r
2725          */\r
2726         public void removeSelectedMidiEvents() {\r
2727                 removeMidiEvents(getSelectedMidiEvents());\r
2728         }\r
2729         private boolean isRhythmPart(int ch) { return (ch == 9); }\r
2730         /**\r
2731          * MIDIメッセージの内容を文字列で返します。\r
2732          * @param msg MIDIメッセージ\r
2733          * @return MIDIメッセージの内容を表す文字列\r
2734          */\r
2735         public String msgToString(MidiMessage msg) {\r
2736                 String str = "";\r
2737                 if( msg instanceof ShortMessage ) {\r
2738                         ShortMessage shortmsg = (ShortMessage)msg;\r
2739                         int status = msg.getStatus();\r
2740                         String status_name = MIDISpec.getStatusName(status);\r
2741                         int data1 = shortmsg.getData1();\r
2742                         int data2 = shortmsg.getData2();\r
2743                         if( MIDISpec.isChannelMessage(status) ) {\r
2744                                 int ch = shortmsg.getChannel();\r
2745                                 String ch_prefix = "Ch."+(ch+1) + ": ";\r
2746                                 String status_prefix = (\r
2747                                                 status_name == null ? String.format("status=0x%02X",status) : status_name\r
2748                                                 ) + ": ";\r
2749                                 int cmd = shortmsg.getCommand();\r
2750                                 switch( cmd ) {\r
2751                                 case ShortMessage.NOTE_OFF:\r
2752                                 case ShortMessage.NOTE_ON:\r
2753                                         str += ch_prefix + status_prefix + data1;\r
2754                                         str += ":[";\r
2755                                         if( isRhythmPart(ch) ) {\r
2756                                                 str += MIDISpec.getPercussionName(data1);\r
2757                                         }\r
2758                                         else {\r
2759                                                 str += Music.NoteSymbol.noteNoToSymbol(data1);\r
2760                                         }\r
2761                                         str +="] Velocity=" + data2;\r
2762                                         break;\r
2763                                 case ShortMessage.POLY_PRESSURE:\r
2764                                         str += ch_prefix + status_prefix + "Note=" + data1 + " Pressure=" + data2;\r
2765                                         break;\r
2766                                 case ShortMessage.PROGRAM_CHANGE:\r
2767                                         str += ch_prefix + status_prefix + data1 + ":[" + MIDISpec.instrument_names[data1] + "]";\r
2768                                         if( data2 != 0 ) str += " data2=" + data2;\r
2769                                         break;\r
2770                                 case ShortMessage.CHANNEL_PRESSURE:\r
2771                                         str += ch_prefix + status_prefix + data1;\r
2772                                         if( data2 != 0 ) str += " data2=" + data2;\r
2773                                         break;\r
2774                                 case ShortMessage.PITCH_BEND:\r
2775                                 {\r
2776                                         int val = ((data1 & 0x7F) | ((data2 & 0x7F) << 7));\r
2777                                         str += ch_prefix + status_prefix + ( (val-8192) * 100 / 8191) + "% (" + val + ")";\r
2778                                 }\r
2779                                 break;\r
2780                                 case ShortMessage.CONTROL_CHANGE:\r
2781                                 {\r
2782                                         // Control / Mode message name\r
2783                                         String ctrl_name = MIDISpec.getControllerName(data1);\r
2784                                         str += ch_prefix + (data1 < 0x78 ? "CtrlChg: " : "ModeMsg: ");\r
2785                                         if( ctrl_name == null ) {\r
2786                                                 str += " No.=" + data1 + " Value=" + data2;\r
2787                                                 return str;\r
2788                                         }\r
2789                                         str += ctrl_name;\r
2790                                         //\r
2791                                         // Controller's value\r
2792                                         switch( data1 ) {\r
2793                                         case 0x40: case 0x41: case 0x42: case 0x43: case 0x45:\r
2794                                                 str += " " + ( data2==0x3F?"OFF":data2==0x40?"ON":data2 );\r
2795                                                 break;\r
2796                                         case 0x44: // Legato Footswitch\r
2797                                                 str += " " + ( data2==0x3F?"Normal":data2==0x40?"Legato":data2 );\r
2798                                                 break;\r
2799                                         case 0x7A: // Local Control\r
2800                                                 str += " " + ( data2==0x00?"OFF":data2==0x7F?"ON":data2 );\r
2801                                                 break;\r
2802                                         default:\r
2803                                                 str += " " + data2;\r
2804                                                 break;\r
2805                                         }\r
2806                                 }\r
2807                                 break;\r
2808 \r
2809                                 default:\r
2810                                         // Never reached here\r
2811                                         break;\r
2812                                 }\r
2813                         }\r
2814                         else { // System Message\r
2815                                 str += (status_name == null ? ("status="+status) : status_name );\r
2816                                 str += " (" + data1 + "," + data2 + ")";\r
2817                         }\r
2818                         return str;\r
2819                 }\r
2820                 else if( msg instanceof MetaMessage ) {\r
2821                         MetaMessage metamsg = (MetaMessage)msg;\r
2822                         byte[] msgdata = metamsg.getData();\r
2823                         int msgtype = metamsg.getType();\r
2824                         str += "Meta: ";\r
2825                         String meta_name = MIDISpec.getMetaName(msgtype);\r
2826                         if( meta_name == null ) {\r
2827                                 str += "Unknown MessageType="+msgtype + " Values=(";\r
2828                                 for( byte b : msgdata ) str += String.format( " %02X", b );\r
2829                                 str += " )";\r
2830                                 return str;\r
2831                         }\r
2832                         // Add the message type name\r
2833                         str += meta_name;\r
2834                         //\r
2835                         // Add the text data\r
2836                         if( MIDISpec.hasMetaText(msgtype) ) {\r
2837                                 str +=" ["+(new String(msgdata))+"]";\r
2838                                 return str;\r
2839                         }\r
2840                         // Add the numeric data\r
2841                         switch(msgtype) {\r
2842                         case 0x00: // Sequence Number (for MIDI Format 2)\r
2843                                 if( msgdata.length == 2 ) {\r
2844                                         str += String.format(\r
2845                                                 ": %04X",\r
2846                                                 ((msgdata[0] & 0xFF) << 8) | (msgdata[1] & 0xFF)\r
2847                                         );\r
2848                                         break;\r
2849                                 }\r
2850                                 str += ": Size not 2 byte : data=(";\r
2851                                 for( byte b : msgdata ) str += String.format( " %02X", b );\r
2852                                 str += " )";\r
2853                                 break;\r
2854                         case 0x20: // MIDI Ch.Prefix\r
2855                         case 0x21: // MIDI Output Port\r
2856                                 if( msgdata.length == 1 ) {\r
2857                                         str += String.format( ": %02X", msgdata[0] & 0xFF );\r
2858                                         break;\r
2859                                 }\r
2860                                 str += ": Size not 1 byte : data=(";\r
2861                                 for( byte b : msgdata ) str += String.format( " %02X", b );\r
2862                                 str += " )";\r
2863                                 break;\r
2864                         case 0x51: // Tempo\r
2865                                 str += ": " + MIDISpec.byteArrayToQpmTempo( msgdata ) + "[QPM] (";\r
2866                                 for( byte b : msgdata ) str += String.format( " %02X", b );\r
2867                                 str += " )";\r
2868                                 break;\r
2869                         case 0x54: // SMPTE Offset\r
2870                                 if( msgdata.length == 5 ) {\r
2871                                         str += ": "\r
2872                                                 + (msgdata[0] & 0xFF) + ":"\r
2873                                                 + (msgdata[1] & 0xFF) + ":"\r
2874                                                 + (msgdata[2] & 0xFF) + "."\r
2875                                                 + (msgdata[3] & 0xFF) + "."\r
2876                                                 + (msgdata[4] & 0xFF);\r
2877                                         break;\r
2878                                 }\r
2879                                 str += ": Size not 5 byte : data=(";\r
2880                                 for( byte b : msgdata ) str += String.format( " %02X", b );\r
2881                                 str += " )";\r
2882                                 break;\r
2883                         case 0x58: // Time Signature\r
2884                                 if( msgdata.length == 4 ) {\r
2885                                         str +=": " + msgdata[0] + "/" + (1 << msgdata[1]);\r
2886                                         str +=", "+msgdata[2]+"[clk/beat], "+msgdata[3]+"[32nds/24clk]";\r
2887                                         break;\r
2888                                 }\r
2889                                 str += ": Size not 4 byte : data=(";\r
2890                                 for( byte b : msgdata ) str += String.format( " %02X", b );\r
2891                                 str += " )";\r
2892                                 break;\r
2893                         case 0x59: // Key Signature\r
2894                                 if( msgdata.length == 2 ) {\r
2895                                         Music.Key key = new Music.Key(msgdata);\r
2896                                         str += ": " + key.signatureDescription();\r
2897                                         str += " (" + key.toStringIn(Music.SymbolLanguage.NAME) + ")";\r
2898                                         break;\r
2899                                 }\r
2900                                 str += ": Size not 2 byte : data=(";\r
2901                                 for( byte b : msgdata ) str += String.format( " %02X", b );\r
2902                                 str += " )";\r
2903                                 break;\r
2904                         case 0x7F: // Sequencer Specific Meta Event\r
2905                                 str += " (";\r
2906                                 for( byte b : msgdata ) str += String.format( " %02X", b );\r
2907                                 str += " )";\r
2908                                 break;\r
2909                         }\r
2910                         return str;\r
2911                 }\r
2912                 else if( msg instanceof SysexMessage ) {\r
2913                         SysexMessage sysexmsg = (SysexMessage)msg;\r
2914                         int status = sysexmsg.getStatus();\r
2915                         byte[] msgdata = sysexmsg.getData();\r
2916                         int data_byte_pos = 1;\r
2917                         switch( status ) {\r
2918                         case SysexMessage.SYSTEM_EXCLUSIVE:\r
2919                                 str += "SysEx: ";\r
2920                                 break;\r
2921                         case SysexMessage.SPECIAL_SYSTEM_EXCLUSIVE:\r
2922                                 str += "SysEx(Special): ";\r
2923                                 break;\r
2924                         default:\r
2925                                 str += "SysEx: Invalid (status="+status+") ";\r
2926                                 break;\r
2927                         }\r
2928                         if( msgdata.length < 1 ) {\r
2929                                 str += " Invalid data size: " + msgdata.length;\r
2930                                 return str;\r
2931                         }\r
2932                         int manufacturer_id = (int)(msgdata[0] & 0xFF );\r
2933                         int device_id = (int)(msgdata[1] & 0xFF);\r
2934                         int model_id = (int)(msgdata[2] & 0xFF);\r
2935                         String manufacturer_name = MIDISpec.getSysExManufacturerName(manufacturer_id);\r
2936                         if( manufacturer_name == null ) {\r
2937                                 manufacturer_name = String.format( "[Manufacturer code %02X]", msgdata[0] );\r
2938                         }\r
2939                         str += manufacturer_name + String.format( " (DevID=0x%02X)", device_id );\r
2940                         switch( manufacturer_id ) {\r
2941                         case 0x7E: // Non-Realtime Universal\r
2942                                 data_byte_pos++;\r
2943                                 int sub_id_1 = (int)(msgdata[2] & 0xFF);\r
2944                                 int sub_id_2 = (int)(msgdata[3] & 0xFF);\r
2945                                 switch( sub_id_1 ) {\r
2946                                 case 0x09: // General MIDI (GM)\r
2947                                         switch( sub_id_2 ) {\r
2948                                         case 0x01: str += " GM System ON"; return str;\r
2949                                         case 0x02: str += " GM System OFF"; return str;\r
2950                                         }\r
2951                                         break;\r
2952                                 default:\r
2953                                         break;\r
2954                                 }\r
2955                                 break;\r
2956                                 // case 0x7F: // Realtime Universal\r
2957                         case 0x41: // Roland\r
2958                                 data_byte_pos++;\r
2959                                 switch( model_id ) {\r
2960                                 case 0x42:\r
2961                                         str += " [GS]"; data_byte_pos++;\r
2962                                         if( msgdata[3]==0x12 ) {\r
2963                                                 str += "DT1:"; data_byte_pos++;\r
2964                                                 switch( msgdata[4] ) {\r
2965                                                 case 0x00:\r
2966                                                         if( msgdata[5]==0x00 ) {\r
2967                                                                 if( msgdata[6]==0x7F ) {\r
2968                                                                         if( msgdata[7]==0x00 ) {\r
2969                                                                                 str += " [88] System Mode Set (Mode 1: Single Module)"; return str;\r
2970                                                                         }\r
2971                                                                         else if( msgdata[7]==0x01 ) {\r
2972                                                                                 str += " [88] System Mode Set (Mode 2: Double Module)"; return str;\r
2973                                                                         }\r
2974                                                                 }\r
2975                                                         }\r
2976                                                         else if( msgdata[5]==0x01 ) {\r
2977                                                                 int port = (msgdata[7] & 0xFF);\r
2978                                                                 str += String.format(\r
2979                                                                                 " [88] Ch.Msg Rx Port: Block=0x%02X, Port=%s",\r
2980                                                                                 msgdata[6],\r
2981                                                                                 port==0?"A":port==1?"B":String.format("0x%02X",port)\r
2982                                                                                 );\r
2983                                                                 return str;\r
2984                                                         }\r
2985                                                         break;\r
2986                                                 case 0x40:\r
2987                                                         if( msgdata[5]==0x00 ) {\r
2988                                                                 switch( msgdata[6] ) {\r
2989                                                                 case 0x00: str += " Master Tune: "; data_byte_pos += 3; break;\r
2990                                                                 case 0x04: str += " Master Volume: "; data_byte_pos += 3; break;\r
2991                                                                 case 0x05: str += " Master Key Shift: "; data_byte_pos += 3; break;\r
2992                                                                 case 0x06: str += " Master Pan: "; data_byte_pos += 3; break;\r
2993                                                                 case 0x7F:\r
2994                                                                         switch( msgdata[7] ) {\r
2995                                                                         case 0x00: str += " GS Reset"; return str;\r
2996                                                                         case 0x7F: str += " Exit GS Mode"; return str;\r
2997                                                                         }\r
2998                                                                         break;\r
2999                                                                 }\r
3000                                                         }\r
3001                                                         else if( msgdata[5]==0x01 ) {\r
3002                                                                 switch( msgdata[6] ) {\r
3003                                                                 // case 0x00: str += ""; break;\r
3004                                                                 // case 0x10: str += ""; break;\r
3005                                                                 case 0x30: str += " Reverb Macro: "; data_byte_pos += 3; break;\r
3006                                                                 case 0x31: str += " Reverb Character: "; data_byte_pos += 3; break;\r
3007                                                                 case 0x32: str += " Reverb Pre-LPF: "; data_byte_pos += 3; break;\r
3008                                                                 case 0x33: str += " Reverb Level: "; data_byte_pos += 3; break;\r
3009                                                                 case 0x34: str += " Reverb Time: "; data_byte_pos += 3; break;\r
3010                                                                 case 0x35: str += " Reverb Delay FB: "; data_byte_pos += 3; break;\r
3011                                                                 case 0x36: str += " Reverb Chorus Level: "; data_byte_pos += 3; break;\r
3012                                                                 case 0x37: str += " [88] Reverb Predelay Time: "; data_byte_pos += 3; break;\r
3013                                                                 case 0x38: str += " Chorus Macro: "; data_byte_pos += 3; break;\r
3014                                                                 case 0x39: str += " Chorus Pre-LPF: "; data_byte_pos += 3; break;\r
3015                                                                 case 0x3A: str += " Chorus Level: "; data_byte_pos += 3; break;\r
3016                                                                 case 0x3B: str += " Chorus FB: "; data_byte_pos += 3; break;\r
3017                                                                 case 0x3C: str += " Chorus Delay: "; data_byte_pos += 3; break;\r
3018                                                                 case 0x3D: str += " Chorus Rate: "; data_byte_pos += 3; break;\r
3019                                                                 case 0x3E: str += " Chorus Depth: "; data_byte_pos += 3; break;\r
3020                                                                 case 0x3F: str += " Chorus Send Level To Reverb: "; data_byte_pos += 3; break;\r
3021                                                                 case 0x40: str += " [88] Chorus Send Level To Delay: "; data_byte_pos += 3; break;\r
3022                                                                 case 0x50: str += " [88] Delay Macro: "; data_byte_pos += 3; break;\r
3023                                                                 case 0x51: str += " [88] Delay Pre-LPF: "; data_byte_pos += 3; break;\r
3024                                                                 case 0x52: str += " [88] Delay Time Center: "; data_byte_pos += 3; break;\r
3025                                                                 case 0x53: str += " [88] Delay Time Ratio Left: "; data_byte_pos += 3; break;\r
3026                                                                 case 0x54: str += " [88] Delay Time Ratio Right: "; data_byte_pos += 3; break;\r
3027                                                                 case 0x55: str += " [88] Delay Level Center: "; data_byte_pos += 3; break;\r
3028                                                                 case 0x56: str += " [88] Delay Level Left: "; data_byte_pos += 3; break;\r
3029                                                                 case 0x57: str += " [88] Delay Level Right: "; data_byte_pos += 3; break;\r
3030                                                                 case 0x58: str += " [88] Delay Level: "; data_byte_pos += 3; break;\r
3031                                                                 case 0x59: str += " [88] Delay FB: "; data_byte_pos += 3; break;\r
3032                                                                 case 0x5A: str += " [88] Delay Send Level To Reverb: "; data_byte_pos += 3; break;\r
3033                                                                 }\r
3034                                                         }\r
3035                                                         else if( msgdata[5]==0x02 ) {\r
3036                                                                 switch( msgdata[6] ) {\r
3037                                                                 case 0x00: str += " [88] EQ Low Freq: "; data_byte_pos += 3; break;\r
3038                                                                 case 0x01: str += " [88] EQ Low Gain: "; data_byte_pos += 3; break;\r
3039                                                                 case 0x02: str += " [88] EQ High Freq: "; data_byte_pos += 3; break;\r
3040                                                                 case 0x03: str += " [88] EQ High Gain: "; data_byte_pos += 3; break;\r
3041                                                                 }\r
3042                                                         }\r
3043                                                         else if( msgdata[5]==0x03 ) {\r
3044                                                                 if( msgdata[6] == 0x00 ) {\r
3045                                                                         str += " [Pro] EFX Type: "; data_byte_pos += 3;\r
3046                                                                 }\r
3047                                                                 else if( msgdata[6] >= 0x03 && msgdata[6] <= 0x16 ) {\r
3048                                                                         str += String.format(" [Pro] EFX Param %d", msgdata[6]-2 );\r
3049                                                                         data_byte_pos += 3;\r
3050                                                                 }\r
3051                                                                 else if( msgdata[6] == 0x17 ) {\r
3052                                                                         str += " [Pro] EFX Send Level To Reverb: "; data_byte_pos += 3;\r
3053                                                                 }\r
3054                                                                 else if( msgdata[6] == 0x18 ) {\r
3055                                                                         str += " [Pro] EFX Send Level To Chorus: "; data_byte_pos += 3;\r
3056                                                                 }\r
3057                                                                 else if( msgdata[6] == 0x19 ) {\r
3058                                                                         str += " [Pro] EFX Send Level To Delay: "; data_byte_pos += 3;\r
3059                                                                 }\r
3060                                                                 else if( msgdata[6] == 0x1B ) {\r
3061                                                                         str += " [Pro] EFX Ctrl Src1: "; data_byte_pos += 3;\r
3062                                                                 }\r
3063                                                                 else if( msgdata[6] == 0x1C ) {\r
3064                                                                         str += " [Pro] EFX Ctrl Depth1: "; data_byte_pos += 3;\r
3065                                                                 }\r
3066                                                                 else if( msgdata[6] == 0x1D ) {\r
3067                                                                         str += " [Pro] EFX Ctrl Src2: "; data_byte_pos += 3;\r
3068                                                                 }\r
3069                                                                 else if( msgdata[6] == 0x1E ) {\r
3070                                                                         str += " [Pro] EFX Ctrl Depth2: "; data_byte_pos += 3;\r
3071                                                                 }\r
3072                                                                 else if( msgdata[6] == 0x1F ) {\r
3073                                                                         str += " [Pro] EFX Send EQ Switch: "; data_byte_pos += 3;\r
3074                                                                 }\r
3075                                                         }\r
3076                                                         else if( (msgdata[5] & 0xF0) == 0x10 ) {\r
3077                                                                 int ch = (msgdata[5] & 0x0F);\r
3078                                                                 if( ch <= 9 ) ch--; else if( ch == 0 ) ch = 9;\r
3079                                                                 if( msgdata[6]==0x02 ) {\r
3080                                                                         str += String.format(\r
3081                                                                                         " Rx Ch: Part=%d(0x%02X) Ch=0x%02X", (ch+1),  msgdata[5], msgdata[7]\r
3082                                                                                         );\r
3083                                                                         return str;\r
3084                                                                 }\r
3085                                                                 else if( msgdata[6]==0x15 ) {\r
3086                                                                         String map;\r
3087                                                                         switch( msgdata[7] ) {\r
3088                                                                         case 0: map = " NormalPart"; break;\r
3089                                                                         case 1: map = " DrumMap1"; break;\r
3090                                                                         case 2: map = " DrumMap2"; break;\r
3091                                                                         default: map = String.format("0x%02X",msgdata[7]); break;\r
3092                                                                         }\r
3093                                                                         str += String.format(\r
3094                                                                                         " Rhythm Part: Ch=%d(0x%02X) Map=%s",\r
3095                                                                                         (ch+1), msgdata[5],\r
3096                                                                                         map\r
3097                                                                                         );\r
3098                                                                         return str;\r
3099                                                                 }\r
3100                                                         }\r
3101                                                         else if( (msgdata[5] & 0xF0) == 0x40 ) {\r
3102                                                                 int ch = (msgdata[5] & 0x0F);\r
3103                                                                 if( ch <= 9 ) ch--; else if( ch == 0 ) ch = 9;\r
3104                                                                 int dt = (msgdata[7] & 0xFF);\r
3105                                                                 if( msgdata[6]==0x20 ) {\r
3106                                                                         str += String.format(\r
3107                                                                                         " [88] EQ: Ch=%d(0x%02X) %s",\r
3108                                                                                         (ch+1), msgdata[5],\r
3109                                                                                         dt==0 ? "OFF" : dt==1 ? "ON" : String.format("0x%02X",dt)\r
3110                                                                                         );\r
3111                                                                 }\r
3112                                                                 else if( msgdata[6]==0x22 ) {\r
3113                                                                         str += String.format(\r
3114                                                                                         " [Pro] Part EFX Assign: Ch=%d(0x%02X) %s",\r
3115                                                                                         (ch+1), msgdata[5],\r
3116                                                                                         dt==0 ? "ByPass" : dt==1 ? "EFX" : String.format("0x%02X",dt)\r
3117                                                                                         );\r
3118                                                                 }\r
3119                                                         }\r
3120                                                         break;\r
3121                                                 } // [4]\r
3122                                         } // [3] [DT1]\r
3123                                         break; // [GS]\r
3124                                 case 0x45:\r
3125                                         str += " [GS-LCD]"; data_byte_pos++;\r
3126                                         if( msgdata[3]==0x12 ) {\r
3127                                                 str += " [DT1]"; data_byte_pos++;\r
3128                                                 if( msgdata[4]==0x10 && msgdata[5]==0x00 && msgdata[6]==0x00 ) {\r
3129                                                         data_byte_pos += 3;\r
3130                                                         str += " Disp [" +(new String(\r
3131                                                                         msgdata, data_byte_pos, msgdata.length - data_byte_pos - 2\r
3132                                                                         ))+ "]";\r
3133                                                 }\r
3134                                         } // [3] [DT1]\r
3135                                         break;\r
3136                                 case 0x14: str += " [D-50]"; data_byte_pos++; break;\r
3137                                 case 0x16: str += " [MT-32]"; data_byte_pos++; break;\r
3138                                 } // [2] model_id\r
3139                                 break;\r
3140                         case 0x43: // Yamaha (XG)\r
3141                                 data_byte_pos++;\r
3142                                 if( model_id == 0x4C ) {\r
3143                                         str += " [XG]";\r
3144                                         if( msgdata[3]==0 && msgdata[4]==0 && msgdata[5]==0x7E && msgdata[6]==0 ) {\r
3145                                                 str += " XG System ON"; return str;\r
3146                                         }\r
3147                                         data_byte_pos++;\r
3148                                 }\r
3149                                 break;\r
3150                         default:\r
3151                                 break;\r
3152                         }\r
3153                         int i;\r
3154                         str += " data=(";\r
3155                         for( i = data_byte_pos; i<msgdata.length-1; i++ ) {\r
3156                                 str += String.format( " %02X", msgdata[i] );\r
3157                         }\r
3158                         if( i < msgdata.length && (int)(msgdata[i] & 0xFF) != 0xF7 ) {\r
3159                                 str+=" [ Invalid EOX " + String.format( "%02X", msgdata[i] ) + " ]";\r
3160                         }\r
3161                         str += " )";\r
3162                         return str;\r
3163                 }\r
3164                 byte[] msg_data = msg.getMessage();\r
3165                 str += "(";\r
3166                 for( byte b : msg_data ) {\r
3167                         str += String.format( " %02X", b );\r
3168                 }\r
3169                 str += " )";\r
3170                 return str;\r
3171         }\r
3172 }\r
3173 \r
3174 /**\r
3175  *  MIDI シーケンスデータのtickインデックス\r
3176  * <p>拍子、テンポ、調だけを抜き出したトラックを保持するためのインデックスです。\r
3177  * 指定の MIDI tick の位置におけるテンポ、調、拍子を取得したり、\r
3178  * 拍子情報から MIDI tick と小節位置との間の変換を行うために使います。\r
3179  * </p>\r
3180  */\r
3181 class SequenceTickIndex {\r
3182         /**\r
3183          * メタメッセージの種類:テンポ\r
3184          */\r
3185         public static final int TEMPO = 0;\r
3186         /**\r
3187          * メタメッセージの種類:拍子\r
3188          */\r
3189         public static final int TIME_SIGNATURE = 1;\r
3190         /**\r
3191          * メタメッセージの種類:調号\r
3192          */\r
3193         public static final int KEY_SIGNATURE = 2;\r
3194         /**\r
3195          * メタメッセージタイプ → メタメッセージの種類 変換マップ\r
3196          */\r
3197         private static final Map<Integer,Integer> INDEX_META_TO_TRACK =\r
3198                 new HashMap<Integer,Integer>() {\r
3199                         {\r
3200                                 put(0x51, TEMPO);\r
3201                                 put(0x58, TIME_SIGNATURE);\r
3202                                 put(0x59, KEY_SIGNATURE);\r
3203                         }\r
3204                 };\r
3205         /**\r
3206          * 新しいMIDIシーケンスデータのインデックスを構築します。\r
3207          * @param sourceSequence 元のMIDIシーケンス\r
3208          */\r
3209         public SequenceTickIndex(Sequence sourceSequence) {\r
3210                 try {\r
3211                         int ppq = sourceSequence.getResolution();\r
3212                         wholeNoteTickLength = ppq * 4;\r
3213                         tmpSequence = new Sequence(Sequence.PPQ, ppq, 3);\r
3214                         tracks = tmpSequence.getTracks();\r
3215                         Track[] sourceTracks = sourceSequence.getTracks();\r
3216                         for( Track tk : sourceTracks ) {\r
3217                                 for( int i_evt = 0 ; i_evt < tk.size(); i_evt++ ) {\r
3218                                         MidiEvent evt = tk.get(i_evt);\r
3219                                         MidiMessage msg = evt.getMessage();\r
3220                                         if( ! (msg instanceof MetaMessage) )\r
3221                                                 continue;\r
3222                                         MetaMessage metaMsg = (MetaMessage)msg;\r
3223                                         int metaType = metaMsg.getType();\r
3224                                         Integer metaIndex = INDEX_META_TO_TRACK.get(metaType);\r
3225                                         if( metaIndex != null ) tracks[metaIndex].add(evt);\r
3226                                 }\r
3227                         }\r
3228                 }\r
3229                 catch ( InvalidMidiDataException e ) {\r
3230                         e.printStackTrace();\r
3231                 }\r
3232         }\r
3233         private Sequence tmpSequence;\r
3234         /**\r
3235          * このtickインデックスのタイミング解像度を返します。\r
3236          * @return このtickインデックスのタイミング解像度\r
3237          */\r
3238         public int getResolution() {\r
3239                 return tmpSequence.getResolution();\r
3240         }\r
3241         private Track[] tracks;\r
3242         /**\r
3243          * 指定されたtick位置以前の最後のメタメッセージを返します。\r
3244          * @param trackIndex メタメッセージの種類()\r
3245          * @param tickPosition\r
3246          * @return\r
3247          */\r
3248         public MetaMessage lastMetaMessageAt(int trackIndex, long tickPosition) {\r
3249                 Track track = tracks[trackIndex];\r
3250                 for(int eventIndex = track.size()-1 ; eventIndex >= 0; eventIndex--) {\r
3251                         MidiEvent event = track.get(eventIndex);\r
3252                         if( event.getTick() > tickPosition )\r
3253                                 continue;\r
3254                         MetaMessage metaMessage = (MetaMessage)(event.getMessage());\r
3255                         if( metaMessage.getType() == 0x2F /* skip EOT (last event) */ )\r
3256                                 continue;\r
3257                         return metaMessage;\r
3258                 }\r
3259                 return null;\r
3260         }\r
3261 \r
3262         private int wholeNoteTickLength;\r
3263         public int lastBeat;\r
3264         public int lastExtraTick;\r
3265         public byte timesigUpper;\r
3266         public byte timesigLowerIndex;\r
3267         /**\r
3268          * tick位置を小節位置に変換します。\r
3269          * @param tickPosition tick位置\r
3270          * @return 小節位置\r
3271          */\r
3272         int tickToMeasure(long tickPosition) {\r
3273                 byte extraBeats = 0;\r
3274                 MidiEvent event = null;\r
3275                 MidiMessage message = null;\r
3276                 byte[] data = null;\r
3277                 long currentTick = 0L;\r
3278                 long nextTimesigTick = 0L;\r
3279                 long prevTick = 0L;\r
3280                 long duration = 0L;\r
3281                 int lastMeasure = 0;\r
3282                 int eventIndex = 0;\r
3283                 timesigUpper = 4;\r
3284                 timesigLowerIndex = 2; // =log2(4)\r
3285                 if( tracks[TIME_SIGNATURE] != null ) {\r
3286                         do {\r
3287                                 // Check current time-signature event\r
3288                                 if( eventIndex < tracks[TIME_SIGNATURE].size() ) {\r
3289                                         message = (event = tracks[TIME_SIGNATURE].get(eventIndex)).getMessage();\r
3290                                         currentTick = nextTimesigTick = event.getTick();\r
3291                                         if(currentTick > tickPosition || (message.getStatus() == 0xFF && ((MetaMessage)message).getType() == 0x2F /* EOT */)) {\r
3292                                                 currentTick = tickPosition;\r
3293                                         }\r
3294                                 }\r
3295                                 else { // No event\r
3296                                         currentTick = nextTimesigTick = tickPosition;\r
3297                                 }\r
3298                                 // Add measure from last event\r
3299                                 //\r
3300                                 int beatTickLength = wholeNoteTickLength >> timesigLowerIndex;\r
3301                                 duration = currentTick - prevTick;\r
3302                                 int beats = (int)( duration / beatTickLength );\r
3303                                 lastExtraTick = (int)(duration % beatTickLength);\r
3304                                 int measures = beats / timesigUpper;\r
3305                                 extraBeats = (byte)(beats % timesigUpper);\r
3306                                 lastMeasure += measures;\r
3307                                 if( nextTimesigTick > tickPosition ) break;  // Not reached to next time signature\r
3308                                 //\r
3309                                 // Reached to the next time signature, so get it.\r
3310                                 if( ( data = ((MetaMessage)message).getData() ).length > 0 ) { // To skip EOT, check the data length.\r
3311                                         timesigUpper = data[0];\r
3312                                         timesigLowerIndex = data[1];\r
3313                                 }\r
3314                                 if( currentTick == tickPosition )  break;  // Calculation complete\r
3315                                 //\r
3316                                 // Calculation incomplete, so prepare for next\r
3317                                 //\r
3318                                 if( extraBeats > 0 ) {\r
3319                                         //\r
3320                                         // Extra beats are treated as 1 measure\r
3321                                         lastMeasure++;\r
3322                                 }\r
3323                                 prevTick = currentTick;\r
3324                                 eventIndex++;\r
3325                         } while( true );\r
3326                 }\r
3327                 lastBeat = extraBeats;\r
3328                 return lastMeasure;\r
3329         }\r
3330         /**\r
3331          * 小節位置を MIDI tick に変換します。\r
3332          * @param measure 小節位置\r
3333          * @return MIDI tick\r
3334          */\r
3335         public long measureToTick(int measure) {\r
3336                 return measureToTick(measure, 0, 0);\r
3337         }\r
3338         /**\r
3339          * 指定の小節位置、拍、拍内tickを、そのシーケンス全体の MIDI tick に変換します。\r
3340          * @param measure 小節位置\r
3341          * @param beat 拍\r
3342          * @param extraTick 拍内tick\r
3343          * @return そのシーケンス全体の MIDI tick\r
3344          */\r
3345         public long measureToTick(int measure, int beat, int extraTick) {\r
3346                 MidiEvent evt = null;\r
3347                 MidiMessage msg = null;\r
3348                 byte[] data = null;\r
3349                 long tick = 0L;\r
3350                 long prev_tick = 0L;\r
3351                 long duration = 0L;\r
3352                 long duration_sum = 0L;\r
3353                 long estimated_ticks;\r
3354                 int ticks_per_beat;\r
3355                 int i_evt = 0;\r
3356                 timesigUpper = 4;\r
3357                 timesigLowerIndex = 2; // =log2(4)\r
3358                 do {\r
3359                         ticks_per_beat = wholeNoteTickLength >> timesigLowerIndex;\r
3360                         estimated_ticks = ((measure * timesigUpper) + beat) * ticks_per_beat + extraTick;\r
3361                         if( tracks[TIME_SIGNATURE] == null || i_evt > tracks[TIME_SIGNATURE].size() ) {\r
3362                                 return duration_sum + estimated_ticks;\r
3363                         }\r
3364                         msg = (evt = tracks[TIME_SIGNATURE].get(i_evt)).getMessage();\r
3365                         if( msg.getStatus() == 0xFF && ((MetaMessage)msg).getType() == 0x2F /* EOT */ ) {\r
3366                                 return duration_sum + estimated_ticks;\r
3367                         }\r
3368                         duration = (tick = evt.getTick()) - prev_tick;\r
3369                         if( duration >= estimated_ticks ) {\r
3370                                 return duration_sum + estimated_ticks;\r
3371                         }\r
3372                         // Re-calculate measure (ignore extra beats/ticks)\r
3373                         measure -= ( duration / (ticks_per_beat * timesigUpper) );\r
3374                         duration_sum += duration;\r
3375                         //\r
3376                         // Get next time-signature\r
3377                         data = ( (MetaMessage)msg ).getData();\r
3378                         timesigUpper = data[0];\r
3379                         timesigLowerIndex = data[1];\r
3380                         prev_tick = tick;\r
3381                         i_evt++;\r
3382                 } while( true );\r
3383         }\r
3384 }\r