OSDN Git Service

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