OSDN Git Service

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