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
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
85 * MIDIエディタ(MIDI Editor/Playlist for MIDI Chord Helper)
\r
88 * Copyright (C) 2006-2013 Akiyoshi Kamide
\r
89 * http://www.yk.rim.or.jp/~kamide/music/chordhelper/
\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
95 * このMIDIエディタの仮想MIDIデバイス
\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
104 info = new MyInfo();
\r
105 // 送信のみなので MIDI IN はサポートしない
\r
106 setMaxReceivers(0);
\r
110 * 新しいMIDIシーケンスを生成するダイアログ
\r
112 NewSequenceDialog newSequenceDialog = new NewSequenceDialog(this) {
\r
113 { setChannels(virtualMidiDevice.getChannels()); }
\r
119 SequenceListTableModel sequenceListTableModel;
\r
121 * プレイリストのMIDIシーケンス選択状態
\r
123 ListSelectionModel seqSelectionModel = new DefaultListSelectionModel() {{
\r
124 setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
\r
125 addListSelectionListener(new ListSelectionListener() {
\r
127 public void valueChanged(ListSelectionEvent e) {
\r
128 if( e.getValueIsAdjusting() ) return;
\r
129 sequenceSelectionChanged();
\r
130 trackSelectionModel.setSelectionInterval(0,0);
\r
135 * 選択されたシーケンスへジャンプするアクション
\r
137 public Action jumpSequenceAction = new AbstractAction("Jump") {
\r
140 Action.SHORT_DESCRIPTION,
\r
141 "Move to selected song - 選択した曲へ進む"
\r
145 public void actionPerformed(ActionEvent e) {
\r
146 load(seqSelectionModel.getMinSelectionIndex());
\r
152 public Action deleteSequenceAction = new AbstractAction("Delete",deleteIcon) {
\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
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
170 sequenceListTableModel.removeSequence(seqSelectionModel);
\r
174 * BASE64テキスト入力ダイアログ
\r
176 Base64Dialog base64Dialog = new Base64Dialog(this);
\r
178 * BASE64エンコードボタン(ライブラリが見えている場合のみ有効)
\r
180 public Action base64EncodeAction;
\r
182 * ファイル選択ダイアログ(アプレットでは使用不可)
\r
184 private class MidiFileChooser extends JFileChooser {
\r
187 new FileNameExtensionFilter("MIDI sequence (*.mid)", "mid")
\r
193 public Action addMidiFileAction = new AbstractAction("Open") {
\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
204 public Action saveMidiFileAction = new AbstractAction("Save") {
\r
206 public void actionPerformed(ActionEvent e) {
\r
207 MidiSequenceTableModel sequenceTableModel =
\r
208 sequenceListTableModel.getSequenceModel(seqSelectionModel);
\r
209 String filename = sequenceTableModel.getFilename();
\r
211 if( filename != null && ! filename.isEmpty() ) {
\r
212 midiFile = new File(filename);
\r
213 midiFileChooser.setSelectedFile(midiFile);
\r
215 int resp = midiFileChooser.showSaveDialog(MidiEditor.this);
\r
216 if( resp != JFileChooser.APPROVE_OPTION ) {
\r
219 midiFile = midiFileChooser.getSelectedFile();
\r
220 if( midiFile.exists() && ! confirm(
\r
221 "Overwrite " + midiFile.getName() + " ?\n"
\r
222 + midiFile.getName()
\r
223 + " を上書きしてよろしいですか?"
\r
227 try ( FileOutputStream out = new FileOutputStream(midiFile) ) {
\r
228 out.write(sequenceTableModel.getMIDIdata());
\r
229 sequenceTableModel.setModified(false);
\r
231 catch( IOException ex ) {
\r
232 showError( ex.getMessage() );
\r
233 ex.printStackTrace();
\r
239 * ファイル選択ダイアログ(アプレットでは使用不可)
\r
241 private MidiFileChooser midiFileChooser;
\r
246 private ListSelectionModel trackSelectionModel = new DefaultListSelectionModel() {{
\r
247 setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
\r
248 addListSelectionListener(new ListSelectionListener() {
\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
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
265 midiEventsLabel.setText(
\r
266 String.format("MIDI Events (in track No.%d)", selIndex)
\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
274 updateButtonStatus();
\r
275 eventSelectionModel.setSelectionInterval(0,0);
\r
282 public Action addTrackAction = new AbstractAction("New") {
\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
293 public Action removeTrackAction = new AbstractAction("Delete", deleteIcon) {
\r
295 public void actionPerformed(ActionEvent e) {
\r
296 if( ! confirm("Do you want to delete selected track ?\n選択したトラックを削除しますか?"))
\r
298 sequenceListTableModel.getSequenceModel(seqSelectionModel).deleteTracks(trackSelectionModel);
\r
299 sequenceListTableModel.fireSequenceChanged(seqSelectionModel);
\r
303 * MIDIトラックリストテーブルビュー
\r
305 private JTable trackListTableView;
\r
310 private ListSelectionModel eventSelectionModel = new DefaultListSelectionModel() {{
\r
311 setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
\r
312 addListSelectionListener(new ListSelectionListener() {
\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
336 if( pairNoteCheckbox.isSelected() ) {
\r
337 int maxIndex = getMaxSelectionIndex();
\r
339 for( int i=minIndex; i<=maxIndex; i++ )
\r
341 isSelectedIndex(i) &&
\r
342 (partnerIndex = trackModel.getIndexOfPartnerFor(i)) >= 0 &&
\r
343 ! isSelectedIndex(partnerIndex)
\r
344 ) addSelectionInterval(partnerIndex, partnerIndex);
\r
347 updateButtonStatus();
\r
352 * MIDIイベントリストテーブルビュー
\r
354 private JTable eventListTableView;
\r
356 * スクロール可能なMIDIイベントテーブルビュー
\r
358 private JScrollPane scrollableEventTableView;
\r
362 private JLabel tracksLabel = new JLabel("Tracks");
\r
366 private JLabel midiEventsLabel = new JLabel("No track selected");
\r
370 MidiEventDialog eventDialog = new MidiEventDialog();
\r
374 private JButton removeEventButton;
\r
376 * Pair note on/off チェックボックス
\r
378 private JCheckBox pairNoteCheckbox;
\r
383 class MidiEventCellEditor extends AbstractCellEditor implements TableCellEditor {
\r
385 * 削除対象にする変更前イベント(null可)
\r
387 private MidiEvent[] midiEventsToBeRemoved;
\r
391 private MidiTrackTableModel midiTrackTableModel;
\r
395 private MidiSequenceTableModel sequenceTableModel;
\r
399 private MidiEvent selectedMidiEvent = null;
\r
403 private int selectedIndex = -1;
\r
407 private long currentTick = 0;
\r
411 private TickPositionModel tickPositionModel = new TickPositionModel();
\r
413 * Pair noteON/OFF トグルボタンモデル
\r
415 private JToggleButton.ToggleButtonModel pairNoteOnOffModel =
\r
416 new JToggleButton.ToggleButtonModel();
\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
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
434 * イベント入力をキャンセルするアクション
\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
444 * 指定のTick位置へジャンプするアクション
\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
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
463 * 指定のTick位置へ貼り付けるアクション
\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
474 scrollToEventAt(tick);
\r
475 sequenceListTableModel.fireSequenceChanged(seqSelectionModel);
\r
476 eventDialog.setVisible(false);
\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
487 * 新しいイベントの追加を行うアクション
\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
498 eventDialog.midiMessageForm.channelText.setSelectedChannel(ch);
\r
500 eventDialog.openEventForm();
\r
504 * イベントの追加(または変更)を行うアクション
\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
515 if( ! midiTrackTableModel.addMidiEvent(newMidiEvent) ) {
\r
516 System.out.println("addMidiEvent failure");
\r
519 if(pairNoteOnOffModel.isSelected() && eventDialog.midiMessageForm.isNote()) {
\r
520 ShortMessage sm = eventDialog.midiMessageForm.getPartnerMessage();
\r
521 if( sm == null ) scrollToEventAt( tick );
\r
523 int duration = eventDialog.midiMessageForm.durationForm.getDuration();
\r
524 if( eventDialog.midiMessageForm.isNote(false) ) { // Note Off
\r
525 duration = -duration;
\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
534 scrollToEventAt( partnerTick > tick ? partnerTick : tick );
\r
537 sequenceListTableModel.fireSequenceChanged(sequenceTableModel);
\r
538 eventDialog.setVisible(false);
\r
539 fireEditingStopped();
\r
545 private JButton editEventButton = new JButton() {{
\r
546 setHorizontalAlignment(JButton.LEFT);
\r
549 * MIDIイベント表のセルエディタを構築します。
\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
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
575 MidiEvent events[];
\r
576 if( partnerEvent == null ) {
\r
577 events = new MidiEvent[1];
\r
578 events[0] = selectedMidiEvent;
\r
581 events = new MidiEvent[2];
\r
582 events[0] = selectedMidiEvent;
\r
583 events[1] = partnerEvent;
\r
585 midiEventsToBeRemoved = events;
\r
586 eventDialog.setTitle("Change MIDI event");
\r
587 eventDialog.okButton.setAction(addEventAction);
\r
588 eventDialog.openEventForm();
\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
601 pairNoteOnOffModel.setSelected(true);
\r
603 public boolean isCellEditable(EventObject e) {
\r
605 return e instanceof MouseEvent && ((MouseEvent)e).getClickCount() == 2;
\r
607 public Object getCellEditorValue() {
\r
610 public Component getTableCellEditorComponent(
\r
611 JTable table, Object value, boolean isSelected,
\r
612 int row, int column
\r
614 editEventButton.setText((String)value);
\r
615 return editEventButton;
\r
622 private MidiEventCellEditor eventCellEditor = new MidiEventCellEditor();
\r
626 private JButton addEventButton = new JButton(eventCellEditor.queryAddEventAction) {{
\r
627 setMargin(ZERO_INSETS);
\r
632 private JButton jumpEventButton = new JButton(eventCellEditor.queryJumpEventAction) {{
\r
633 setMargin(ZERO_INSETS);
\r
638 private JButton pasteEventButton = new JButton(eventCellEditor.queryPasteEventAction) {{
\r
639 setMargin(ZERO_INSETS);
\r
642 * ペースト用にコピーされたMIDIイベントの配列
\r
644 private MidiEvent copiedEventsToPaste[];
\r
646 * ペースト用にコピーされたMIDIイベントのタイミング解像度
\r
648 private int copiedEventsPPQ = 0;
\r
652 private JButton cutEventButton = new JButton("Cut") {{
\r
653 setMargin(ZERO_INSETS);
\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
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
671 private JButton copyEventButton = new JButton("Copy") {{
\r
672 setMargin(ZERO_INSETS);
\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
685 * 新しい {@link MidiEditor} を構築します。
\r
686 * @param deviceModelList MIDIデバイスモデルリスト
\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
698 midiFileChooser = new MidiFileChooser();
\r
700 catch( ExceptionInInitializerError|NoClassDefFoundError|AccessControlException e ) {
\r
701 // アプレットの場合、Webクライアントマシンのローカルファイルには
\r
702 // アクセスできないので、ファイル選択ダイアログは使用不可。
\r
703 midiFileChooser = null;
\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
712 add(Box.createRigidArea(new Dimension(0, 10)));
\r
713 add(new JPanel() {{
\r
714 setLayout( new BoxLayout(this, BoxLayout.LINE_AXIS ));
\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
725 sequenceListTableModel.addTableModelListener(
\r
726 new TableModelListener() {
\r
728 * プレイリスト上でシーケンスが増減した場合、
\r
729 * 合計時間が変わるので表示を更新します。
\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
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
749 new ActionListener() {
\r
750 public void actionPerformed(ActionEvent e) {
\r
751 newSequenceDialog.setVisible(true);
\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
762 add(Box.createRigidArea(new Dimension(5, 0)));
\r
763 add(new JButton(sequenceListTableModel.moveToTopAction) {{
\r
764 setMargin(ZERO_INSETS);
\r
766 add(Box.createRigidArea(new Dimension(5, 0)));
\r
767 add(new JToggleButton(
\r
768 sequenceListTableModel.sequencerModel.startStopAction
\r
770 add(Box.createRigidArea(new Dimension(5, 0)));
\r
771 add(new JButton(sequenceListTableModel.moveToBottomAction) {{
\r
772 setMargin(ZERO_INSETS);
\r
774 add( Box.createRigidArea(new Dimension(5, 0)) );
\r
775 add(new JButton(jumpSequenceAction){{
\r
776 setMargin(ZERO_INSETS);
\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
784 if( base64Dialog.isBase64Available() ) {
\r
785 base64EncodeAction = new AbstractAction("Base64 Encode") {
\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
793 add(Box.createRigidArea(new Dimension(5, 0)));
\r
794 add(new JButton(base64EncodeAction) {{
\r
795 setMargin(ZERO_INSETS);
\r
798 add( Box.createRigidArea(new Dimension(5, 0)) );
\r
799 add(new JButton(deleteSequenceAction) {{
\r
800 setMargin(ZERO_INSETS);
\r
802 add( Box.createRigidArea(new Dimension(5, 0)) );
\r
803 add(new SequencerSpeedSlider(
\r
804 sequenceListTableModel.sequencerModel.speedSliderModel
\r
807 add( Box.createRigidArea(new Dimension(0, 10)) );
\r
809 JPanel trackListPanel = new JPanel() {{
\r
810 setLayout(new BoxLayout(this, BoxLayout.PAGE_AXIS));
\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
817 trackSelectionModel
\r
819 ((MidiSequenceTableModel)getModel()).sizeColumnWidthToFit(getColumnModel());
\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
827 add(new JButton(removeTrackAction) {{
\r
828 setMargin(ZERO_INSETS);
\r
832 removeEventButton = new JButton("Delete", deleteIcon) {{
\r
833 setMargin(ZERO_INSETS);
\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
839 ((MidiTrackTableModel)eventListTableView.getModel()).removeMidiEvents(eventSelectionModel);
\r
840 sequenceListTableModel.fireSequenceChanged(seqSelectionModel);
\r
845 JPanel eventListPanel = new JPanel() {{
\r
846 add(midiEventsLabel);
\r
847 add(scrollableEventTableView = new JScrollPane(eventListTableView));
\r
848 add(new JPanel() {{
\r
850 pairNoteCheckbox = new JCheckBox("Pair NoteON/OFF") {{
\r
851 setModel(eventCellEditor.pairNoteOnOffModel);
\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
861 setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
\r
863 Container cp = getContentPane();
\r
864 cp.setLayout(new BoxLayout(cp, BoxLayout.Y_AXIS));
\r
865 cp.add(Box.createVerticalStrut(2));
\r
867 new JSplitPane(JSplitPane.VERTICAL_SPLIT, playlistPanel,
\r
868 new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, trackListPanel, eventListPanel) {{
\r
869 setDividerLocation(300);
\r
872 setDividerLocation(160);
\r
875 seqSelectionModel.setSelectionInterval(0,0);
\r
876 sequenceSelectionChanged();
\r
878 public void dragEnter(DropTargetDragEvent event) {
\r
879 if( event.isDataFlavorSupported(DataFlavor.javaFileListFlavor) )
\r
880 event.acceptDrag(DnDConstants.ACTION_COPY_OR_MOVE);
\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
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
897 event.dropComplete(false);
\r
899 catch (Exception ex) {
\r
900 ex.printStackTrace();
\r
901 event.dropComplete(false);
\r
904 private void showError(String message) {
\r
905 JOptionPane.showMessageDialog(
\r
907 ChordHelperApplet.VersionInfo.NAME,
\r
908 JOptionPane.ERROR_MESSAGE
\r
911 private void showWarning(String message) {
\r
912 JOptionPane.showMessageDialog(
\r
914 ChordHelperApplet.VersionInfo.NAME,
\r
915 JOptionPane.WARNING_MESSAGE
\r
918 private boolean confirm(String message) {
\r
919 return JOptionPane.showConfirmDialog(
\r
921 ChordHelperApplet.VersionInfo.NAME,
\r
922 JOptionPane.YES_NO_OPTION,
\r
923 JOptionPane.WARNING_MESSAGE
\r
924 ) == JOptionPane.YES_OPTION ;
\r
927 public void setVisible(boolean isToVisible) {
\r
928 if( isToVisible && isVisible() )
\r
931 super.setVisible(isToVisible);
\r
934 public void actionPerformed(ActionEvent e) {
\r
938 * 選択されたシーケンスが変わったことを通知します。
\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
949 int selectedIndex = seqSelectionModel.getMinSelectionIndex();
\r
950 trackListTableView.setModel(sequenceTableModel);
\r
951 TableColumnModel tcm = trackListTableView.getColumnModel();
\r
952 sequenceTableModel.sizeColumnWidthToFit(tcm);
\r
954 MidiSequenceTableModel.Column.RECORD_CHANNEL.ordinal()
\r
956 sequenceTableModel.new RecordChannelCellEditor()
\r
958 trackSelectionModel.setSelectionInterval(0,0);
\r
959 tracksLabel.setText(String.format("Tracks (in MIDI file No.%d)", selectedIndex));
\r
962 trackListTableView.setModel(new MidiSequenceTableModel(sequenceListTableModel));
\r
963 tracksLabel.setText("Tracks (No MIDI file selected)");
\r
965 updateButtonStatus();
\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
976 removeTrackAction.setEnabled(isTrackSelected);
\r
977 TableModel tm = eventListTableView.getModel();
\r
978 if( ! (tm instanceof MidiTrackTableModel) )
\r
980 MidiTrackTableModel trackTableModel = (MidiTrackTableModel)tm;
\r
981 jumpSequenceAction.setEnabled(
\r
982 trackTableModel != null && trackTableModel.getRowCount() > 0
\r
984 boolean isEventSelected = (
\r
986 eventSelectionModel.isSelectionEmpty() ||
\r
987 trackTableModel == null || trackTableModel.getRowCount() == 0
\r
988 ) && isTrackSelected
\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
1000 public String getMIDIdataBase64() {
\r
1001 base64Dialog.setMIDIData(
\r
1002 sequenceListTableModel.sequencerModel.getSequenceTableModel().getMIDIdata()
\r
1004 return base64Dialog.getBase64Data();
\r
1007 * MIDIシーケンスを追加します。
\r
1008 * シーケンサーが停止中の場合、追加したシーケンスから再生を開始します。
\r
1009 * @param sequence MIDIシーケンス
\r
1010 * @return 追加先インデックス(先頭が 0)
\r
1012 public int addSequenceAndPlay(Sequence sequence) {
\r
1013 int lastIndex = sequenceListTableModel.addSequence(sequence,"");
\r
1014 if( ! sequenceListTableModel.sequencerModel.getSequencer().isRunning() ) {
\r
1016 sequenceListTableModel.sequencerModel.start();
\r
1021 * バイト列とファイル名からMIDIシーケンスを追加します。
\r
1022 * バイト列が null の場合、空のMIDIシーケンスを追加します。
\r
1023 * @param data バイト列
\r
1024 * @param filename ファイル名
\r
1025 * @return 追加先インデックス(先頭が 0、失敗した場合は -1)
\r
1027 public int addSequence(byte[] data, String filename) {
\r
1028 if( data == null ) {
\r
1029 return sequenceListTableModel.addDefaultSequence();
\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
1042 * MIDIファイルから読み込んだシーケンスを追加します。
\r
1043 * ファイルが null の場合、空のMIDIシーケンスを追加します。
\r
1044 * @param midiFile MIDIファイル
\r
1045 * @return 追加先インデックス(先頭が 0、失敗した場合は -1)
\r
1047 public int addSequence(File midiFile) {
\r
1048 if( midiFile == null ) {
\r
1049 return sequenceListTableModel.addDefaultSequence();
\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
1059 } catch( AccessControlException e ) {
\r
1060 showError(e.getMessage());
\r
1061 e.printStackTrace();
\r
1067 * URLから読み込んだMIDIシーケンスを追加します。
\r
1068 * @param midiFileUrl MIDIファイルのURL
\r
1069 * @return 追加先インデックス(先頭が 0、失敗した場合は -1)
\r
1071 public int addSequenceFromURL(String midiFileUrl) {
\r
1072 Sequence seq = null;
\r
1073 String filename = null;
\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
1085 if( seq == null ) return -1;
\r
1086 return sequenceListTableModel.addSequence(seq, filename);
\r
1090 * 指定のインデックス位置にあるMIDIシーケンスをシーケンサーにロードします。
\r
1091 * @param index MIDIシーケンスのインデックス(先頭が 0)
\r
1093 public void load(int index) {
\r
1094 sequenceListTableModel.loadToSequencer(index);
\r
1095 sequenceSelectionChanged();
\r
1099 * @param offset 何曲次へ進むかを表す数値
\r
1100 * @return 成功したらtrue
\r
1102 public boolean loadNext(int offset) {
\r
1103 boolean retval = sequenceListTableModel.loadNext(offset);
\r
1104 sequenceSelectionChanged();
\r
1108 * 複数のMIDIファイルを読み込み、再生されていなかったら再生します。
\r
1109 * すでに再生されていた場合、このエディタダイアログを表示します。
\r
1111 * @param fileList 読み込むMIDIファイルのリスト
\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
1120 if(sequenceListTableModel.sequencerModel.getSequencer().isRunning()) {
\r
1123 else if( firstIndex >= 0 ) {
\r
1125 sequenceListTableModel.sequencerModel.start();
\r
1130 * ユーザ操作により録音可能な設定になったかどうか調べます。
\r
1131 * @return 選択されているシーケンスが録音可能な設定ならtrue
\r
1133 public boolean isRecordable() {
\r
1134 MidiSequenceTableModel sequenceTableModel =
\r
1135 sequenceListTableModel.getSequenceModel(seqSelectionModel);
\r
1136 return sequenceTableModel == null ? false : sequenceTableModel.isRecordable();
\r
1139 * 指定の MIDI tick のイベントへスクロールします。
\r
1140 * @param tick MIDI tick
\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
1148 eventSelectionModel.setSelectionInterval(index, index);
\r
1153 * シーケンサーの再生スピード調整スライダビュー
\r
1155 class SequencerSpeedSlider extends JPanel {
\r
1156 private static final String items[] = {
\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
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
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
1189 public SequencerSpeedSlider(BoundedRangeModel model) {
\r
1191 add(slider = new JSlider(model){{
\r
1192 setPaintTicks(true);
\r
1193 setMajorTickSpacing(12);
\r
1194 setMinorTickSpacing(1);
\r
1195 setVisible(false);
\r
1197 add(scaleComboBox);
\r
1202 * プレイリスト(MIDIシーケンスリスト)のテーブルモデル
\r
1204 class SequenceListTableModel extends AbstractTableModel implements ChangeListener {
\r
1208 public enum Column {
\r
1209 /** MIDIシーケンスの番号 */
\r
1210 SEQ_NUMBER("No.", 2, Integer.class),
\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
1218 FILENAME("Filename", 16, String.class),
\r
1219 /** シーケンス名(最初のトラックの名前) */
\r
1220 SEQ_NAME("Sequence name", 40, String.class),
\r
1222 RESOLUTION("Resolution", 6, Integer.class),
\r
1224 TRACKS("Tracks", 6, Integer.class),
\r
1226 DIVISION_TYPE("DivType", 6, String.class);
\r
1227 private String title;
\r
1228 private int widthRatio;
\r
1229 private Class<?> columnClass;
\r
1232 * @param title 列のタイトル
\r
1233 * @param widthRatio 幅の割合
\r
1234 * @param columnClass 列のクラス
\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
1245 public static int totalWidthRatio() {
\r
1247 for( Column c : values() ) total += c.widthRatio;
\r
1254 MidiSequencerModel sequencerModel;
\r
1256 * 曲の先頭または前の曲へ戻るアクション
\r
1258 public Action moveToTopAction = new AbstractAction() {
\r
1260 putValue( SHORT_DESCRIPTION,
\r
1261 "Move to top or previous song - 曲の先頭または前の曲へ戻る"
\r
1263 putValue( LARGE_ICON_KEY, new ButtonIcon(ButtonIcon.TOP_ICON) );
\r
1265 public void actionPerformed(ActionEvent event) {
\r
1266 if( sequencerModel.getSequencer().getTickPosition() <= 40 )
\r
1268 sequencerModel.setValue(0);
\r
1274 public Action moveToBottomAction = new AbstractAction() {
\r
1276 putValue( SHORT_DESCRIPTION, "Move to next song - 次の曲へ進む" );
\r
1277 putValue( LARGE_ICON_KEY, new ButtonIcon(ButtonIcon.BOTTOM_ICON) );
\r
1279 public void actionPerformed(ActionEvent event) {
\r
1280 if(loadNext(1)) sequencerModel.setValue(0);
\r
1284 * 新しいプレイリストのテーブルモデルを構築します。
\r
1285 * @param sequencerModel MIDIシーケンサーモデル
\r
1287 public SequenceListTableModel(MidiSequencerModel sequencerModel) {
\r
1288 (this.sequencerModel = sequencerModel).addChangeListener(this);
\r
1293 private int secondPosition = 0;
\r
1295 * 再生中のシーケンサーの秒位置が変わったときに表示を更新します。
\r
1298 public void stateChanged(ChangeEvent e) {
\r
1299 int sec = sequencerModel.getValue() / 1000;
\r
1300 if( secondPosition == sec )
\r
1302 secondPosition = sec;
\r
1303 fireTableCellUpdated(getLoadedIndex(), Column.SEQ_POSITION.ordinal());
\r
1305 private List<MidiSequenceTableModel> sequenceList = new ArrayList<>();
\r
1307 public int getRowCount() {
\r
1308 return sequenceList.size();
\r
1311 public int getColumnCount() {
\r
1312 return Column.values().length;
\r
1315 public String getColumnName(int column) {
\r
1316 return Column.values()[column].title;
\r
1319 public Class<?> getColumnClass(int column) {
\r
1320 return Column.values()[column].columnClass;
\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
1337 return sequenceList.get(row).getSequence().getResolution();
\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
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
1353 String filename = sequenceList.get(row).getFilename();
\r
1354 return filename == null ? "" : filename;
\r
1357 String name = sequenceList.get(row).toString();
\r
1358 return name == null ? "" : name;
\r
1360 default: return "";
\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
1369 public void setValueAt(Object val, int row, int column) {
\r
1370 switch(Column.values()[column]) {
\r
1373 String filename = (String)val;
\r
1374 sequenceList.get(row).setFilename(filename);
\r
1375 fireTableCellUpdated(row, column);
\r
1379 if( sequenceList.get(row).setName((String)val) )
\r
1380 fireTableCellUpdated(row, Column.MODIFIED.ordinal());
\r
1388 * @param tableView テーブルビュー
\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
1400 * このプレイリストに読み込まれた全シーケンスの合計時間長を返します。
\r
1401 * @return 全シーケンスの合計時間長 [秒]
\r
1403 public int getTotalSeconds() {
\r
1406 for( MidiSequenceTableModel m : sequenceList ) {
\r
1407 usec = m.getSequence().getMicrosecondLength();
\r
1408 total += (int)( (usec < 0 ? usec += 0x100000000L : usec)/1000L/1000L );
\r
1413 * 未保存の修正内容を持つシーケンスがあるか調べます。
\r
1414 * @return 未保存の修正内容を持つシーケンスがあればtrue
\r
1416 public boolean isModified() {
\r
1417 for( MidiSequenceTableModel m : sequenceList ) {
\r
1418 if( m.isModified() ) return true;
\r
1423 * 選択したシーケンスに未保存の修正内容があることを記録します。
\r
1424 * @param selModel 選択状態
\r
1425 * @param isModified 未保存の修正内容があるときtrue
\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
1438 * 選択されたMIDIシーケンスのテーブルモデルを返します。
\r
1439 * @param selectionModel 選択状態
\r
1440 * @return 選択されたMIDIシーケンスのテーブルモデル
\r
1442 public MidiSequenceTableModel getSequenceModel(ListSelectionModel selectionModel) {
\r
1443 if( selectionModel.isSelectionEmpty() )
\r
1445 int selectedIndex = selectionModel.getMinSelectionIndex();
\r
1446 if( selectedIndex >= sequenceList.size() )
\r
1448 return sequenceList.get(selectedIndex);
\r
1451 * 指定されたシーケンスが変更されたことを通知します。
\r
1452 * @param sequenceTableModel MIDIシーケンスモデル
\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
1460 * 指定された選択範囲のシーケンスが変更されたことを通知します。
\r
1461 * @param selectionModel 選択状態
\r
1463 public void fireSequenceChanged(ListSelectionModel selectionModel) {
\r
1464 if( ! selectionModel.isSelectionEmpty() ) fireSequenceChanged(
\r
1465 selectionModel.getMinSelectionIndex(),
\r
1466 selectionModel.getMaxSelectionIndex()
\r
1470 * 指定された範囲のシーケンスが変更されたことを通知します。
\r
1471 * @param minIndex 範囲の最小インデックス
\r
1472 * @param maxIndex 範囲の最大インデックス
\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
1484 fireTableRowsUpdated(minIndex, maxIndex);
\r
1487 * デフォルトの内容でシーケンスを作成して追加します。
\r
1488 * @return 追加されたシーケンスのインデックス(先頭が 0)
\r
1490 public int addDefaultSequence() {
\r
1491 Sequence seq = (new Music.ChordProgression()).toMidiSequence();
\r
1492 return seq == null ? -1 : addSequence(seq,null);
\r
1496 * @param seq MIDIシーケンス
\r
1497 * @param filename ファイル名
\r
1498 * @return 追加されたシーケンスのインデックス(先頭が 0)
\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
1507 * 選択したシーケンスを除去します。
\r
1508 * @param listSelectionModel 選択状態
\r
1510 public void removeSequence(ListSelectionModel listSelectionModel) {
\r
1511 if( listSelectionModel.isSelectionEmpty() )
\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
1520 * 指定したインデックス位置のシーケンスをシーケンサーにロードします。
\r
1521 * @param index シーケンスのインデックス位置
\r
1523 public void loadToSequencer(int index) {
\r
1524 int oldIndex = getLoadedIndex();
\r
1525 if(index == oldIndex)
\r
1527 MidiSequenceTableModel sequenceTableModel = sequenceList.get(index);
\r
1528 sequencerModel.setSequenceTableModel(sequenceTableModel);
\r
1531 sequenceTableModel.fireTableDataChanged();
\r
1532 int columnIndex = Column.SEQ_POSITION.ordinal();
\r
1533 fireTableCellUpdated(oldIndex, columnIndex);
\r
1534 fireTableCellUpdated(index, columnIndex);
\r
1537 * 現在シーケンサにロードされているシーケンスのインデックスを返します。
\r
1538 * ロードされていない場合は -1 を返します。
\r
1539 * @return 現在シーケンサにロードされているシーケンスのインデックス
\r
1541 public int getLoadedIndex() {
\r
1542 return sequenceList.indexOf(sequencerModel.getSequenceTableModel());
\r
1545 * 引数で示された数だけ次へ進めたシーケンスをロードします。
\r
1546 * @param offset 進みたいシーケンス数
\r
1547 * @return 成功したらtrue
\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
1554 loadToSequencer( index );
\r
1560 * MIDIシーケンス(トラックリスト)を表すテーブルモデル
\r
1562 class MidiSequenceTableModel extends AbstractTableModel {
\r
1566 public enum Column {
\r
1568 TRACK_NUMBER("No.", 30, Integer.class),
\r
1570 EVENTS("Events", 60, Integer.class),
\r
1572 MUTE("Mute", 40, Boolean.class),
\r
1574 SOLO("Solo", 40, Boolean.class),
\r
1575 /** 録音するMIDIチャンネル */
\r
1576 RECORD_CHANNEL("RecCh", 60, String.class),
\r
1578 CHANNEL("Ch", 60, String.class),
\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
1586 * @param title 列のタイトル
\r
1587 * @param widthRatio 幅の割合
\r
1588 * @param columnClass 列のクラス
\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
1599 public static int totalWidthRatio() {
\r
1601 for( Column c : values() ) total += c.widthRatio;
\r
1608 private Sequence sequence;
\r
1609 private SequenceTickIndex sequenceTickIndex;
\r
1613 private String filename = "";
\r
1617 private List<MidiTrackTableModel> trackModelList = new ArrayList<>();
\r
1621 class RecordChannelCellEditor extends DefaultCellEditor {
\r
1622 public RecordChannelCellEditor() {
\r
1624 new JComboBox<String>() {
\r
1627 for( int i=1; i <= MIDISpec.MAX_CHANNELS; i++ )
\r
1628 addItem( String.format("%d", i) );
\r
1638 private SequenceListTableModel sequenceListTableModel;
\r
1640 * 空の {@link MidiSequenceTableModel} を構築します。
\r
1641 * @param sequenceListTableModel 親のプレイリスト
\r
1643 public MidiSequenceTableModel(SequenceListTableModel sequenceListTableModel) {
\r
1644 this.sequenceListTableModel = sequenceListTableModel;
\r
1647 * MIDIシーケンスとファイル名から {@link MidiSequenceTableModel} を構築します。
\r
1648 * @param sequenceListTableModel 親のプレイリスト
\r
1649 * @param sequence MIDIシーケンス
\r
1650 * @param filename ファイル名
\r
1652 public MidiSequenceTableModel(
\r
1653 SequenceListTableModel sequenceListTableModel,
\r
1654 Sequence sequence,
\r
1657 this(sequenceListTableModel);
\r
1658 setSequence(sequence);
\r
1659 setFilename(filename);
\r
1662 public int getRowCount() {
\r
1663 return sequence == null ? 0 : sequence.getTracks().length;
\r
1666 public int getColumnCount() { return Column.values().length; }
\r
1672 public String getColumnName(int column) {
\r
1673 return Column.values()[column].title;
\r
1677 * @return 指定された列の型
\r
1680 public Class<?> getColumnClass(int column) {
\r
1681 Column c = Column.values()[column];
\r
1684 case SOLO: if( ! isOnSequencer() ) return String.class;
\r
1686 default: return c.columnClass;
\r
1690 public Object getValueAt(int row, int column) {
\r
1691 Column c = Column.values()[column];
\r
1693 case TRACK_NUMBER: return row;
\r
1694 case EVENTS: return sequence.getTracks()[row].size();
\r
1696 return isOnSequencer() ? getSequencer().getTrackMute(row) : "";
\r
1698 return isOnSequencer() ? getSequencer().getTrackSolo(row) : "";
\r
1699 case RECORD_CHANNEL:
\r
1700 return isOnSequencer() ? trackModelList.get(row).getRecordingChannel() : "";
\r
1702 int ch = trackModelList.get(row).getChannel();
\r
1703 return ch < 0 ? "" : ch + 1 ;
\r
1705 case TRACK_NAME: return trackModelList.get(row).toString();
\r
1706 default: return "";
\r
1710 * セルが編集可能かどうかを返します。
\r
1713 public boolean isCellEditable(int row, int column) {
\r
1714 Column c = Column.values()[column];
\r
1718 case RECORD_CHANNEL: return isOnSequencer();
\r
1720 case TRACK_NAME: return true;
\r
1721 default: return false;
\r
1728 public void setValueAt(Object val, int row, int column) {
\r
1729 Column c = Column.values()[column];
\r
1732 getSequencer().setTrackMute(row, ((Boolean)val).booleanValue());
\r
1735 getSequencer().setTrackSolo(row, ((Boolean)val).booleanValue());
\r
1737 case RECORD_CHANNEL:
\r
1738 trackModelList.get(row).setRecordingChannel((String)val);
\r
1743 ch = new Integer((String)val);
\r
1745 catch( NumberFormatException e ) {
\r
1749 if( --ch <= 0 || ch > MIDISpec.MAX_CHANNELS )
\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
1759 trackModelList.get(row).setString((String)val);
\r
1764 fireTableCellUpdated(row,column);
\r
1768 * @param columnModel テーブル列モデル
\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
1780 * @return MIDIシーケンス
\r
1782 public Sequence getSequence() { return sequence; }
\r
1784 * シーケンスtickインデックスを返します。
\r
1785 * @return シーケンスtickインデックス
\r
1787 public SequenceTickIndex getSequenceTickIndex() {
\r
1788 return sequenceTickIndex;
\r
1791 * MIDIシーケンスを設定します。
\r
1792 * @param sequence MIDIシーケンス
\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
1802 if( sequence == null ) {
\r
1803 sequenceTickIndex = null;
\r
1806 fireTimeSignatureChanged();
\r
1807 Track tracks[] = sequence.getTracks();
\r
1808 for(Track track : tracks) {
\r
1809 trackModelList.add(new MidiTrackTableModel(track, this));
\r
1811 fireTableRowsInserted(0, tracks.length-1);
\r
1815 * 拍子が変更されたとき、シーケンスtickインデックスを再作成します。
\r
1817 public void fireTimeSignatureChanged() {
\r
1818 sequenceTickIndex = new SequenceTickIndex(sequence);
\r
1820 private boolean isModified = false;
\r
1823 * @return 変更済みのときtrue
\r
1825 public boolean isModified() { return isModified; }
\r
1827 * 変更されたかどうかを設定します。
\r
1828 * @param isModified 変更されたときtrue
\r
1830 public void setModified(boolean isModified) { this.isModified = isModified; }
\r
1833 * @param filename ファイル名
\r
1835 public void setFilename(String filename) { this.filename = filename; }
\r
1840 public String getFilename() { return filename; }
\r
1842 public String toString() { return MIDISpec.getNameOf(sequence); }
\r
1845 * @param name シーケンス名
\r
1846 * @return 成功したらtrue
\r
1848 public boolean setName(String name) {
\r
1849 if( name.equals(toString()) || ! MIDISpec.setNameOf(sequence,name) )
\r
1851 setModified(true);
\r
1852 fireTableDataChanged();
\r
1856 * このシーケンスのMIDIデータのバイト列を返します。
\r
1857 * @return MIDIデータのバイト列(失敗した場合null)
\r
1859 public byte[] getMIDIdata() {
\r
1860 if( sequence == null || sequence.getTracks().length == 0 ) {
\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
1872 * 指定のトラックが変更されたことを通知します。
\r
1873 * @param track トラック
\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
1882 * このシーケンスが変更されたことを通知します。
\r
1884 public void fireSequenceChanged() {
\r
1885 sequenceListTableModel.fireSequenceChanged(this);
\r
1888 * 指定のインデックスのトラックモデルを返します。
\r
1889 * @param index トラックのインデックス
\r
1890 * @return トラックモデル(見つからない場合null)
\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
1903 * 指定のトラックがある位置のインデックスを返します。
\r
1904 * @param track トラック
\r
1905 * @return トラックのインデックス(先頭 0、トラックが見つからない場合 -1)
\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
1915 * 新しいトラックを生成し、末尾に追加します。
\r
1916 * @return 追加したトラックのインデックス(先頭 0)
\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
1925 * 選択されているトラックを削除します。
\r
1926 * @param selectionModel 選択状態
\r
1928 public void deleteTracks(ListSelectionModel selectionModel) {
\r
1929 if( selectionModel.isSelectionEmpty() )
\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
1937 sequence.deleteTrack(tracks[i]);
\r
1938 trackModelList.remove(i);
\r
1940 fireTableRowsDeleted(minIndex, maxIndex);
\r
1944 * @return MIDIシーケンサ
\r
1946 public Sequencer getSequencer() {
\r
1947 return sequenceListTableModel.sequencerModel.getSequencer();
\r
1950 * このシーケンスモデルのシーケンスをシーケンサーが操作しているか調べます。
\r
1951 * @return シーケンサーが操作していたらtrue
\r
1953 public boolean isOnSequencer() {
\r
1954 return sequence == getSequencer().getSequence();
\r
1959 * <p>シーケンサーにロード済みで、
\r
1960 * かつ録音しようとしているチャンネルの設定されたトラックが一つでもあれば、
\r
1963 * @return 録音可能であればtrue
\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
1977 * MIDIトラック(MIDIイベントリスト)テーブルモデル
\r
1979 class MidiTrackTableModel extends AbstractTableModel {
\r
1983 public enum Column {
\r
1985 EVENT_NUMBER("No.", 30, Integer.class),
\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
1995 MESSAGE("MIDI Message", 280, String.class);
\r
1996 private String title;
\r
1997 private int widthRatio;
\r
1998 private Class<?> columnClass;
\r
2001 * @param title 列のタイトル
\r
2002 * @param widthRatio 幅の割合
\r
2003 * @param columnClass 列のクラス
\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
2014 public static int totalWidthRatio() {
\r
2016 for( Column c : values() ) total += c.widthRatio;
\r
2021 * ラップされているMIDIトラック
\r
2023 private Track track;
\r
2027 private MidiSequenceTableModel parent;
\r
2029 * 空のMIDIトラックモデルを構築します。
\r
2031 public MidiTrackTableModel() { }
\r
2033 * シーケンスに連動する空のMIDIトラックモデルを構築します。
\r
2034 * @parent 親のシーケンステーブルモデル
\r
2036 public MidiTrackTableModel(MidiSequenceTableModel parent) {
\r
2037 this.parent = parent;
\r
2040 * シーケンスを親にして、その特定のトラックに連動する
\r
2041 * MIDIトラックモデルを構築します。
\r
2043 * @param track ラップするMIDIトラック
\r
2044 * @param parent 親のシーケンスモデル
\r
2046 public MidiTrackTableModel(Track track, MidiSequenceTableModel parent) {
\r
2047 this.track = track;
\r
2048 this.parent = parent;
\r
2051 public int getRowCount() {
\r
2052 return track == null ? 0 : track.size();
\r
2055 public int getColumnCount() {
\r
2056 return Column.values().length;
\r
2062 public String getColumnName(int column) {
\r
2063 return Column.values()[column].title;
\r
2069 public Class<?> getColumnClass(int column) {
\r
2070 return Column.values()[column].columnClass;
\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
2090 * セルを編集できるときtrue、編集できないときfalseを返します。
\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
2107 public void setValueAt(Object value, int row, int column) {
\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
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
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
2134 MidiEvent oldMidiEvent = track.get(row);
\r
2135 if( oldMidiEvent.getTick() == newTick ) {
\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
2150 * @param columnModel テーブル列モデル
\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
2162 * @return MIDIトラック
\r
2164 public Track getTrack() { return track; }
\r
2169 public String toString() { return MIDISpec.getNameOf(track); }
\r
2172 * @param name トラック名
\r
2173 * @return 設定が行われたらtrue
\r
2175 public boolean setString(String name) {
\r
2176 if(name.equals(toString()) || ! MIDISpec.setNameOf(track, name))
\r
2178 parent.setModified(true);
\r
2179 parent.fireSequenceChanged();
\r
2180 fireTableDataChanged();
\r
2183 private String recordingChannel = "OFF";
\r
2185 * 録音中のMIDIチャンネルを返します。
\r
2186 * @return 録音中のMIDIチャンネル
\r
2188 public String getRecordingChannel() { return recordingChannel; }
\r
2190 * 録音中のMIDIチャンネルを設定します。
\r
2191 * @param recordingChannel 録音中のMIDIチャンネル
\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
2198 else if( recordingChannel.equals("ALL") ) {
\r
2199 sequencer.recordEnable( track, -1 );
\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
2211 this.recordingChannel = recordingChannel;
\r
2214 * このトラックの対象MIDIチャンネルを返します。
\r
2215 * <p>全てのチャンネルメッセージが同じMIDIチャンネルの場合、
\r
2216 * そのMIDIチャンネルを返します。
\r
2217 * MIDIチャンネルの異なるチャンネルメッセージが一つでも含まれていた場合、
\r
2220 * @return 対象MIDIチャンネル(不統一の場合 -1)
\r
2222 public int getChannel() {
\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
2229 ShortMessage smsg = (ShortMessage)msg;
\r
2230 if( ! MIDISpec.isChannelMessage(smsg) )
\r
2232 int ch = smsg.getChannel();
\r
2233 if( prevCh >= 0 && prevCh != ch ) {
\r
2241 * 指定されたMIDIチャンネルをすべてのチャンネルメッセージに対して設定します。
\r
2242 * @param channel MIDIチャンネル
\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
2250 ShortMessage smsg = (ShortMessage)msg;
\r
2251 if( ! MIDISpec.isChannelMessage(smsg) )
\r
2253 if( smsg.getChannel() == channel )
\r
2257 smsg.getCommand(), channel,
\r
2258 smsg.getData1(), smsg.getData2()
\r
2261 catch( InvalidMidiDataException e ) {
\r
2262 e.printStackTrace();
\r
2264 parent.setModified(true);
\r
2266 parent.fireTrackChanged(track);
\r
2267 fireTableDataChanged();
\r
2270 * 指定の MIDI tick 位置にあるイベントを二分探索し、
\r
2271 * そのイベントの行インデックスを返します。
\r
2272 * @param tick MIDI tick
\r
2275 public int tickToIndex(long tick) {
\r
2276 if( track == null )
\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
2286 else if( tick < currentTick ) {
\r
2287 maxIndex = currentIndex - 1;
\r
2290 return currentIndex;
\r
2293 return (minIndex + maxIndex) / 2;
\r
2296 * NoteOn/NoteOff ペアの一方の行インデックスから、
\r
2297 * もう一方(ペアの相手)の行インデックスを返します。
\r
2298 * @param index 行インデックス
\r
2299 * @return ペアを構成する相手の行インデックス(ない場合は -1)
\r
2301 public int getIndexOfPartnerFor(int index) {
\r
2302 if( track == null || index >= track.size() )
\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
2309 int ch = sm.getChannel();
\r
2310 int note = sm.getData1();
\r
2311 MidiMessage partner_msg;
\r
2312 ShortMessage partner_sm;
\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
2330 if( ch != partner_sm.getChannel() || note != partner_sm.getData1() ) {
\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
2350 if( ch != partner_sm.getChannel() || note != partner_sm.getData1() ) {
\r
2362 * ノートメッセージかどうか調べます。
\r
2363 * @param index 行インデックス
\r
2364 * @return Note On または Note Off のとき true
\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
2373 public boolean hasTrack() { return track != null; }
\r
2375 * 指定の行インデックスのMIDIイベントを返します。
\r
2376 * @param index 行インデックス
\r
2377 * @return MIDIイベント
\r
2379 public MidiEvent getMidiEvent(int index) { return track.get(index); }
\r
2381 * 選択されているMIDIイベントを返します。
\r
2382 * @param selectionModel 選択状態モデル
\r
2383 * @return 選択されているMIDIイベント
\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
2394 return events.toArray(new MidiEvent[1]);
\r
2398 * @param midiEvent 追加するMIDIイベント
\r
2399 * @return 追加できたらtrue
\r
2401 public boolean addMidiEvent(MidiEvent midiEvent) {
\r
2402 if( !(track.add(midiEvent)) )
\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
2413 * @param midiEvents 追加するMIDIイベント
\r
2414 * @param destinationTick 追加先tick
\r
2415 * @param sourcePPQ PPQ値(タイミング解像度)
\r
2416 * @return 追加できたらtrue
\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
2431 newTick += (sourceEventTick - firstSourceEventTick) * destinationPPQ / sourcePPQ;
\r
2433 if( ! track.add(new MidiEvent(msg, newTick)) ) continue;
\r
2435 if( MIDISpec.isTimeSignature(msg) ) hasTimeSignature = true;
\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
2448 * @param midiEvents 除去するMIDIイベント
\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
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
2465 * 引数の選択内容が示すMIDIイベントを除去します。
\r
2466 * @param selectionModel 選択内容
\r
2468 public void removeMidiEvents(ListSelectionModel selectionModel) {
\r
2469 removeMidiEvents(getMidiEvents(selectionModel));
\r
2471 private boolean isRhythmPart(int ch) { return (ch == 9); }
\r
2473 * MIDIメッセージの内容を文字列で返します。
\r
2474 * @param msg MIDIメッセージ
\r
2475 * @return MIDIメッセージの内容を表す文字列
\r
2477 public String msgToString(MidiMessage msg) {
\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
2491 int cmd = shortmsg.getCommand();
\r
2493 case ShortMessage.NOTE_OFF:
\r
2494 case ShortMessage.NOTE_ON:
\r
2495 str += ch_prefix + status_prefix + data1;
\r
2497 if( isRhythmPart(ch) ) {
\r
2498 str += MIDISpec.getPercussionName(data1);
\r
2501 str += Music.NoteSymbol.noteNoToSymbol(data1);
\r
2503 str +="] Velocity=" + data2;
\r
2505 case ShortMessage.POLY_PRESSURE:
\r
2506 str += ch_prefix + status_prefix + "Note=" + data1 + " Pressure=" + data2;
\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
2512 case ShortMessage.CHANNEL_PRESSURE:
\r
2513 str += ch_prefix + status_prefix + data1;
\r
2514 if( data2 != 0 ) str += " data2=" + data2;
\r
2516 case ShortMessage.PITCH_BEND:
\r
2518 int val = ((data1 & 0x7F) | ((data2 & 0x7F) << 7));
\r
2519 str += ch_prefix + status_prefix + ( (val-8192) * 100 / 8191) + "% (" + val + ")";
\r
2522 case ShortMessage.CONTROL_CHANGE:
\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
2533 // Controller's value
\r
2535 case 0x40: case 0x41: case 0x42: case 0x43: case 0x45:
\r
2536 str += " " + ( data2==0x3F?"OFF":data2==0x40?"ON":data2 );
\r
2538 case 0x44: // Legato Footswitch
\r
2539 str += " " + ( data2==0x3F?"Normal":data2==0x40?"Legato":data2 );
\r
2541 case 0x7A: // Local Control
\r
2542 str += " " + ( data2==0x00?"OFF":data2==0x7F?"ON":data2 );
\r
2545 str += " " + data2;
\r
2552 // Never reached here
\r
2556 else { // System Message
\r
2557 str += (status_name == null ? ("status="+status) : status_name );
\r
2558 str += " (" + data1 + "," + data2 + ")";
\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
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
2574 // Add the message type name
\r
2577 // Add the text data
\r
2578 if( MIDISpec.hasMetaText(msgtype) ) {
\r
2579 str +=" ["+(new String(msgdata))+"]";
\r
2582 // Add the numeric data
\r
2584 case 0x00: // Sequence Number (for MIDI Format 2)
\r
2585 if( msgdata.length == 2 ) {
\r
2586 str += String.format(
\r
2588 ((msgdata[0] & 0xFF) << 8) | (msgdata[1] & 0xFF)
\r
2592 str += ": Size not 2 byte : data=(";
\r
2593 for( byte b : msgdata ) str += String.format( " %02X", b );
\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
2602 str += ": Size not 1 byte : data=(";
\r
2603 for( byte b : msgdata ) str += String.format( " %02X", b );
\r
2606 case 0x51: // Tempo
\r
2607 str += ": " + MIDISpec.byteArrayToQpmTempo( msgdata ) + "[QPM] (";
\r
2608 for( byte b : msgdata ) str += String.format( " %02X", b );
\r
2611 case 0x54: // SMPTE Offset
\r
2612 if( msgdata.length == 5 ) {
\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
2621 str += ": Size not 5 byte : data=(";
\r
2622 for( byte b : msgdata ) str += String.format( " %02X", b );
\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
2631 str += ": Size not 4 byte : data=(";
\r
2632 for( byte b : msgdata ) str += String.format( " %02X", b );
\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
2642 str += ": Size not 2 byte : data=(";
\r
2643 for( byte b : msgdata ) str += String.format( " %02X", b );
\r
2646 case 0x7F: // Sequencer Specific Meta Event
\r
2648 for( byte b : msgdata ) str += String.format( " %02X", b );
\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
2663 case SysexMessage.SPECIAL_SYSTEM_EXCLUSIVE:
\r
2664 str += "SysEx(Special): ";
\r
2667 str += "SysEx: Invalid (status="+status+") ";
\r
2670 if( msgdata.length < 1 ) {
\r
2671 str += " Invalid data size: " + msgdata.length;
\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
2681 str += manufacturer_name + String.format( " (DevID=0x%02X)", device_id );
\r
2682 switch( manufacturer_id ) {
\r
2683 case 0x7E: // Non-Realtime Universal
\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
2698 // case 0x7F: // Realtime Universal
\r
2699 case 0x41: // Roland
\r
2701 switch( model_id ) {
\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
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
2713 else if( msgdata[7]==0x01 ) {
\r
2714 str += " [88] System Mode Set (Mode 2: Double Module)"; return str;
\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
2723 port==0?"A":port==1?"B":String.format("0x%02X",port)
\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
2736 switch( msgdata[7] ) {
\r
2737 case 0x00: str += " GS Reset"; return str;
\r
2738 case 0x7F: str += " Exit GS Mode"; return str;
\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
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
2785 else if( msgdata[5]==0x03 ) {
\r
2786 if( msgdata[6] == 0x00 ) {
\r
2787 str += " [Pro] EFX Type: "; data_byte_pos += 3;
\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
2793 else if( msgdata[6] == 0x17 ) {
\r
2794 str += " [Pro] EFX Send Level To Reverb: "; data_byte_pos += 3;
\r
2796 else if( msgdata[6] == 0x18 ) {
\r
2797 str += " [Pro] EFX Send Level To Chorus: "; data_byte_pos += 3;
\r
2799 else if( msgdata[6] == 0x19 ) {
\r
2800 str += " [Pro] EFX Send Level To Delay: "; data_byte_pos += 3;
\r
2802 else if( msgdata[6] == 0x1B ) {
\r
2803 str += " [Pro] EFX Ctrl Src1: "; data_byte_pos += 3;
\r
2805 else if( msgdata[6] == 0x1C ) {
\r
2806 str += " [Pro] EFX Ctrl Depth1: "; data_byte_pos += 3;
\r
2808 else if( msgdata[6] == 0x1D ) {
\r
2809 str += " [Pro] EFX Ctrl Src2: "; data_byte_pos += 3;
\r
2811 else if( msgdata[6] == 0x1E ) {
\r
2812 str += " [Pro] EFX Ctrl Depth2: "; data_byte_pos += 3;
\r
2814 else if( msgdata[6] == 0x1F ) {
\r
2815 str += " [Pro] EFX Send EQ Switch: "; data_byte_pos += 3;
\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
2827 else if( msgdata[6]==0x15 ) {
\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
2835 str += String.format(
\r
2836 " Rhythm Part: Ch=%d(0x%02X) Map=%s",
\r
2837 (ch+1), msgdata[5],
\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
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
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
2878 case 0x14: str += " [D-50]"; data_byte_pos++; break;
\r
2879 case 0x16: str += " [MT-32]"; data_byte_pos++; break;
\r
2882 case 0x43: // Yamaha (XG)
\r
2884 if( model_id == 0x4C ) {
\r
2886 if( msgdata[3]==0 && msgdata[4]==0 && msgdata[5]==0x7E && msgdata[6]==0 ) {
\r
2887 str += " XG System ON"; return str;
\r
2897 for( i = data_byte_pos; i<msgdata.length-1; i++ ) {
\r
2898 str += String.format( " %02X", msgdata[i] );
\r
2900 if( i < msgdata.length && (int)(msgdata[i] & 0xFF) != 0xF7 ) {
\r
2901 str+=" [ Invalid EOX " + String.format( "%02X", msgdata[i] ) + " ]";
\r
2906 byte[] msg_data = msg.getMessage();
\r
2908 for( byte b : msg_data ) {
\r
2909 str += String.format( " %02X", b );
\r
2917 * MIDI シーケンスデータのtickインデックス
\r
2918 * <p>拍子、テンポ、調だけを抜き出したトラックを保持するためのインデックスです。
\r
2919 * 指定の MIDI tick の位置におけるテンポ、調、拍子を取得したり、
\r
2920 * 拍子情報から MIDI tick と小節位置との間の変換を行うために使います。
\r
2923 class SequenceTickIndex {
\r
2927 public static final int TEMPO = 0;
\r
2931 public static final int TIME_SIGNATURE = 1;
\r
2935 public static final int KEY_SIGNATURE = 2;
\r
2937 * メタメッセージタイプ → メタメッセージの種類 変換マップ
\r
2939 private static final Map<Integer,Integer> INDEX_META_TO_TRACK =
\r
2940 new HashMap<Integer,Integer>() {
\r
2943 put(0x58, TIME_SIGNATURE);
\r
2944 put(0x59, KEY_SIGNATURE);
\r
2948 * 新しいMIDIシーケンスデータのインデックスを構築します。
\r
2949 * @param sourceSequence 元のMIDIシーケンス
\r
2951 public SequenceTickIndex(Sequence sourceSequence) {
\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
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
2971 catch ( InvalidMidiDataException e ) {
\r
2972 e.printStackTrace();
\r
2975 private Sequence tmpSequence;
\r
2977 * このtickインデックスのタイミング解像度を返します。
\r
2978 * @return このtickインデックスのタイミング解像度
\r
2980 public int getResolution() {
\r
2981 return tmpSequence.getResolution();
\r
2983 private Track[] tracks;
\r
2985 * 指定されたtick位置以前の最後のメタメッセージを返します。
\r
2986 * @param trackIndex メタメッセージの種類()
\r
2987 * @param tickPosition
\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
2996 MetaMessage metaMessage = (MetaMessage)(event.getMessage());
\r
2997 if( metaMessage.getType() == 0x2F /* skip EOT (last event) */ )
\r
2999 return metaMessage;
\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
3010 * tick位置を小節位置に変換します。
\r
3011 * @param tickPosition tick位置
\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
3026 timesigLowerIndex = 2; // =log2(4)
\r
3027 if( tracks[TIME_SIGNATURE] != null ) {
\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
3037 else { // No event
\r
3038 currentTick = nextTimesigTick = tickPosition;
\r
3040 // Add measure from last event
\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
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
3056 if( currentTick == tickPosition ) break; // Calculation complete
\r
3058 // Calculation incomplete, so prepare for next
\r
3060 if( extraBeats > 0 ) {
\r
3062 // Extra beats are treated as 1 measure
\r
3065 prevTick = currentTick;
\r
3069 lastBeat = extraBeats;
\r
3070 return lastMeasure;
\r
3073 * 小節位置を MIDI tick に変換します。
\r
3074 * @param measure 小節位置
\r
3075 * @return MIDI tick
\r
3077 public long measureToTick(int measure) {
\r
3078 return measureToTick(measure, 0, 0);
\r
3081 * 指定の小節位置、拍、拍内tickを、そのシーケンス全体の MIDI tick に変換します。
\r
3082 * @param measure 小節位置
\r
3084 * @param extraTick 拍内tick
\r
3085 * @return そのシーケンス全体の MIDI tick
\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
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
3099 timesigLowerIndex = 2; // =log2(4)
\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
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
3110 duration = (tick = evt.getTick()) - prev_tick;
\r
3111 if( duration >= estimated_ticks ) {
\r
3112 return duration_sum + estimated_ticks;
\r
3114 // Re-calculate measure (ignore extra beats/ticks)
\r
3115 measure -= ( duration / (ticks_per_beat * timesigUpper) );
\r
3116 duration_sum += duration;
\r
3118 // Get next time-signature
\r
3119 data = ( (MetaMessage)msg ).getData();
\r
3120 timesigUpper = data[0];
\r
3121 timesigLowerIndex = data[1];
\r