OSDN Git Service

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