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.MetaEventListener;
\r
40 import javax.sound.midi.MetaMessage;
\r
41 import javax.sound.midi.MidiChannel;
\r
42 import javax.sound.midi.MidiEvent;
\r
43 import javax.sound.midi.MidiMessage;
\r
44 import javax.sound.midi.MidiSystem;
\r
45 import javax.sound.midi.Sequence;
\r
46 import javax.sound.midi.Sequencer;
\r
47 import javax.sound.midi.ShortMessage;
\r
48 import javax.sound.midi.SysexMessage;
\r
49 import javax.sound.midi.Track;
\r
50 import javax.swing.AbstractAction;
\r
51 import javax.swing.AbstractCellEditor;
\r
52 import javax.swing.Action;
\r
53 import javax.swing.BoundedRangeModel;
\r
54 import javax.swing.Box;
\r
55 import javax.swing.BoxLayout;
\r
56 import javax.swing.DefaultCellEditor;
\r
57 import javax.swing.DefaultListSelectionModel;
\r
58 import javax.swing.Icon;
\r
59 import javax.swing.JButton;
\r
60 import javax.swing.JCheckBox;
\r
61 import javax.swing.JComboBox;
\r
62 import javax.swing.JDialog;
\r
63 import javax.swing.JFileChooser;
\r
64 import javax.swing.JLabel;
\r
65 import javax.swing.JOptionPane;
\r
66 import javax.swing.JPanel;
\r
67 import javax.swing.JScrollPane;
\r
68 import javax.swing.JSlider;
\r
69 import javax.swing.JSplitPane;
\r
70 import javax.swing.JTable;
\r
71 import javax.swing.JToggleButton;
\r
72 import javax.swing.ListSelectionModel;
\r
73 import javax.swing.SwingUtilities;
\r
74 import javax.swing.event.ChangeEvent;
\r
75 import javax.swing.event.ChangeListener;
\r
76 import javax.swing.event.ListSelectionEvent;
\r
77 import javax.swing.event.ListSelectionListener;
\r
78 import javax.swing.event.TableModelEvent;
\r
79 import javax.swing.filechooser.FileFilter;
\r
80 import javax.swing.filechooser.FileNameExtensionFilter;
\r
81 import javax.swing.table.AbstractTableModel;
\r
82 import javax.swing.table.JTableHeader;
\r
83 import javax.swing.table.TableCellEditor;
\r
84 import javax.swing.table.TableCellRenderer;
\r
85 import javax.swing.table.TableColumn;
\r
86 import javax.swing.table.TableColumnModel;
\r
87 import javax.swing.table.TableModel;
\r
90 * MIDIエディタ(MIDI Editor/Playlist for MIDI Chord Helper)
\r
93 * Copyright (C) 2006-2013 Akiyoshi Kamide
\r
94 * http://www.yk.rim.or.jp/~kamide/music/chordhelper/
\r
96 class MidiEditor extends JDialog implements DropTargetListener {
\r
98 * このMIDIエディタの仮想MIDIデバイス
\r
100 VirtualMidiDevice virtualMidiDevice = new AbstractVirtualMidiDevice() {
\r
101 class MyInfo extends Info {
\r
102 protected MyInfo() {
\r
103 super("MIDI Editor","Unknown vendor","MIDI sequence editor","");
\r
106 // 送信のみなので MIDI IN はサポートしない
\r
107 { info = new MyInfo(); setMaxReceivers(0); }
\r
110 * このダイアログを表示するアクション
\r
112 public Action openAction = new AbstractAction(
\r
113 "Edit/Playlist/Speed", new ButtonIcon(ButtonIcon.EDIT_ICON)
\r
116 String tooltip = "MIDIシーケンスの編集/プレイリスト/再生速度調整";
\r
117 putValue(Action.SHORT_DESCRIPTION, tooltip);
\r
120 public void actionPerformed(ActionEvent e) { open(); }
\r
123 * このダイアログを開きます。すでに開かれていた場合は前面に移動します。
\r
125 public void open() {
\r
126 if( isVisible() ) toFront(); else setVisible(true);
\r
129 * エラーメッセージダイアログを表示します。
\r
130 * @param message エラーメッセージ
\r
132 void showError(String message) {
\r
133 JOptionPane.showMessageDialog(
\r
135 ChordHelperApplet.VersionInfo.NAME,
\r
136 JOptionPane.ERROR_MESSAGE
\r
140 * 警告メッセージダイアログを表示します。
\r
141 * @param message 警告メッセージ
\r
143 void showWarning(String message) {
\r
144 JOptionPane.showMessageDialog(
\r
146 ChordHelperApplet.VersionInfo.NAME,
\r
147 JOptionPane.WARNING_MESSAGE
\r
152 * @param message 確認メッセージ
\r
153 * @return 確認OKのときtrue
\r
155 boolean confirm(String message) {
\r
156 return JOptionPane.showConfirmDialog(
\r
158 ChordHelperApplet.VersionInfo.NAME,
\r
159 JOptionPane.YES_NO_OPTION,
\r
160 JOptionPane.WARNING_MESSAGE
\r
161 ) == JOptionPane.YES_OPTION ;
\r
165 public void dragEnter(DropTargetDragEvent event) {
\r
166 if( event.isDataFlavorSupported(DataFlavor.javaFileListFlavor) ) {
\r
167 event.acceptDrag(DnDConstants.ACTION_COPY_OR_MOVE);
\r
171 public void dragExit(DropTargetEvent event) {}
\r
173 public void dragOver(DropTargetDragEvent event) {}
\r
175 public void dropActionChanged(DropTargetDragEvent event) {}
\r
177 @SuppressWarnings("unchecked")
\r
178 public void drop(DropTargetDropEvent event) {
\r
179 event.acceptDrop(DnDConstants.ACTION_COPY_OR_MOVE);
\r
181 int action = event.getDropAction();
\r
182 if ( (action & DnDConstants.ACTION_COPY_OR_MOVE) != 0 ) {
\r
183 Transferable t = event.getTransferable();
\r
184 Object data = t.getTransferData(DataFlavor.javaFileListFlavor);
\r
185 loadAndPlay((List<File>)data);
\r
186 event.dropComplete(true);
\r
189 event.dropComplete(false);
\r
191 catch (Exception ex) {
\r
192 ex.printStackTrace();
\r
193 event.dropComplete(false);
\r
197 * 複数のMIDIファイルを読み込み、再生されていなかったら再生します。
\r
198 * すでに再生されていた場合、このエディタダイアログを表示します。
\r
200 * @param fileList 読み込むMIDIファイルのリスト
\r
202 public void loadAndPlay(List<File> fileList) {
\r
203 int firstIndex = -1;
\r
204 SequenceListTableModel playlist = sequenceListTable.getModel();
\r
206 firstIndex = playlist.addSequences(fileList);
\r
207 } catch(IOException|InvalidMidiDataException e) {
\r
208 showWarning(e.getMessage());
\r
209 } catch(AccessControlException e) {
\r
210 showError(e.getMessage());
\r
211 e.printStackTrace();
\r
213 if(playlist.sequencerModel.getSequencer().isRunning()) {
\r
216 else if( firstIndex >= 0 ) {
\r
217 playlist.loadToSequencer(firstIndex);
\r
218 playlist.sequencerModel.start();
\r
222 public static final Insets ZERO_INSETS = new Insets(0,0,0,0);
\r
223 static final Icon deleteIcon = new ButtonIcon(ButtonIcon.X_ICON);
\r
226 * 新しいMIDIシーケンスを生成するダイアログ
\r
228 NewSequenceDialog newSequenceDialog = new NewSequenceDialog(this);
\r
230 * BASE64テキスト入力ダイアログ
\r
232 Base64Dialog base64Dialog = new Base64Dialog(this);
\r
235 * プレイリストビュー(シーケンスリスト)
\r
237 SequenceListTable sequenceListTable;
\r
239 * MIDIトラックリストテーブルビュー(選択中のシーケンスの中身)
\r
241 private TrackListTable trackListTable;
\r
243 * MIDIイベントリストテーブルビュー(選択中のトラックの中身)
\r
245 private EventListTable eventListTable;
\r
247 * MIDIイベント入力ダイアログ(イベント入力とイベント送出で共用)
\r
249 MidiEventDialog eventDialog = new MidiEventDialog();
\r
252 * プレイリストビュー(シーケンスリスト)
\r
254 class SequenceListTable extends JTable {
\r
256 * ファイル選択ダイアログ(アプレットでは使用不可)
\r
258 private MidiFileChooser midiFileChooser;
\r
260 * BASE64エンコードアクション(ライブラリが見えている場合のみ有効)
\r
262 private Action base64EncodeAction;
\r
265 * @param model プレイリストデータモデル
\r
267 public SequenceListTable(SequenceListTableModel model) {
\r
268 super(model, null, model.sequenceListSelectionModel);
\r
270 midiFileChooser = new MidiFileChooser();
\r
272 catch( ExceptionInInitializerError|NoClassDefFoundError|AccessControlException e ) {
\r
273 // アプレットの場合、Webクライアントマシンのローカルファイルには
\r
274 // アクセスできないので、ファイル選択ダイアログは使用不可。
\r
275 midiFileChooser = null;
\r
278 new PlayButtonCellEditor();
\r
279 new PositionCellEditor();
\r
280 setAutoCreateColumnsFromModel(false);
\r
282 // Base64エンコードアクションの生成
\r
283 if( base64Dialog.isBase64Available() ) {
\r
284 base64EncodeAction = new AbstractAction("Base64") {
\r
286 String tooltip = "Base64 text conversion - Base64テキスト変換";
\r
287 putValue(Action.SHORT_DESCRIPTION, tooltip);
\r
290 public void actionPerformed(ActionEvent e) {
\r
291 SequenceTrackListTableModel mstm = getModel().getSelectedSequenceModel();
\r
292 byte[] data = null;
\r
293 String filename = null;
\r
294 if( mstm != null ) {
\r
295 data = mstm.getMIDIdata();
\r
296 filename = mstm.getFilename();
\r
298 base64Dialog.setMIDIData(data, filename);
\r
299 base64Dialog.setVisible(true);
\r
303 TableColumnModel colModel = getColumnModel();
\r
304 for( SequenceListTableModel.Column c : SequenceListTableModel.Column.values() ) {
\r
305 TableColumn tc = colModel.getColumn(c.ordinal());
\r
306 tc.setPreferredWidth(c.preferredWidth);
\r
307 if( c == SequenceListTableModel.Column.SEQ_LENGTH ) {
\r
312 private TableColumn lengthColumn;
\r
314 public void tableChanged(TableModelEvent event) {
\r
315 super.tableChanged(event);
\r
317 // タイトルに合計シーケンス長を表示
\r
318 if( lengthColumn != null ) {
\r
319 int sec = getModel().getTotalSeconds();
\r
320 String title = SequenceListTableModel.Column.SEQ_LENGTH.title;
\r
321 title = String.format(title+" [%02d:%02d]", sec/60, sec%60);
\r
322 lengthColumn.setHeaderValue(title);
\r
325 // シーケンス削除時など、合計シーケンス長が変わっても
\r
326 // 列モデルからではヘッダタイトルが再描画されないことがある。
\r
327 // そこで、ヘッダビューから repaint() で突っついて再描画させる。
\r
328 JTableHeader th = getTableHeader();
\r
334 * 時間位置表示セルエディタ(ダブルクリック専用)
\r
336 private class PositionCellEditor extends AbstractCellEditor
\r
337 implements TableCellEditor
\r
339 public PositionCellEditor() {
\r
340 int column = SequenceListTableModel.Column.SEQ_POSITION.ordinal();
\r
341 TableColumn tc = getColumnModel().getColumn(column);
\r
342 tc.setCellEditor(this);
\r
345 * セルをダブルクリックしたときだけ編集モードに入るようにします。
\r
346 * @param e イベント(マウスイベント)
\r
347 * @return 編集可能になったらtrue
\r
350 public boolean isCellEditable(EventObject e) {
\r
351 // マウスイベント以外のイベントでは編集不可
\r
352 if( ! (e instanceof MouseEvent) ) return false;
\r
353 return ((MouseEvent)e).getClickCount() == 2;
\r
356 public Object getCellEditorValue() { return null; }
\r
358 * 編集モード時のコンポーネントを返すタイミングで
\r
359 * そのシーケンスをシーケンサーにロードしたあと、
\r
364 public Component getTableCellEditorComponent(
\r
365 JTable table, Object value, boolean isSelected,
\r
366 int row, int column
\r
368 getModel().loadToSequencer(row);
\r
369 fireEditingStopped();
\r
374 * プレイボタンを埋め込んだセルエディタ
\r
376 private class PlayButtonCellEditor extends AbstractCellEditor
\r
377 implements TableCellEditor, TableCellRenderer
\r
379 private JToggleButton playButton = new JToggleButton(
\r
380 getModel().sequencerModel.startStopAction
\r
382 public PlayButtonCellEditor() {
\r
383 playButton.setMargin(ZERO_INSETS);
\r
384 int column = SequenceListTableModel.Column.SEQ_PLAY.ordinal();
\r
385 TableColumn tc = getColumnModel().getColumn(column);
\r
386 tc.setCellRenderer(this);
\r
387 tc.setCellEditor(this);
\r
392 * <p>この実装では、クリックしたセルのシーケンスが
\r
393 * シーケンサーにロードされている場合に
\r
394 * trueを返してプレイボタンを押せるようにします。
\r
395 * そうでない場合はプレイボタンのないセルなので、
\r
396 * ダブルクリックされたときだけtrueを返します。
\r
400 public boolean isCellEditable(EventObject e) {
\r
401 if( ! (e instanceof MouseEvent) ) {
\r
402 // マウスイベント以外はデフォルトメソッドにお任せ
\r
403 return super.isCellEditable(e);
\r
405 fireEditingStopped();
\r
406 MouseEvent me = (MouseEvent)e;
\r
408 int row = rowAtPoint(me.getPoint());
\r
411 SequenceListTableModel model = getModel();
\r
412 if( row >= model.getRowCount() )
\r
414 if( model.sequenceList.get(row).isOnSequencer() ) {
\r
415 // プレイボタン表示中のセルはシングルクリックでもOK
\r
418 // プレイボタンのないセルはダブルクリックのみを受け付ける
\r
419 return me.getClickCount() == 2;
\r
422 public Object getCellEditorValue() { return null; }
\r
426 * <p>この実装では、行の表すシーケンスが
\r
427 * シーケンサーにロードされている場合にプレイボタンを返します。
\r
429 * そのシーケンスをシーケンサーにロードしてnullを返します。
\r
433 public Component getTableCellEditorComponent(
\r
434 JTable table, Object value, boolean isSelected, int row, int column
\r
436 fireEditingStopped();
\r
437 SequenceListTableModel model = getModel();
\r
438 if( model.sequenceList.get(row).isOnSequencer() ) {
\r
441 model.loadToSequencer(row);
\r
445 public Component getTableCellRendererComponent(
\r
446 JTable table, Object value, boolean isSelected,
\r
447 boolean hasFocus, int row, int column
\r
449 SequenceListTableModel model = getModel();
\r
450 if(model.sequenceList.get(row).isOnSequencer()) return playButton;
\r
451 Class<?> cc = model.getColumnClass(column);
\r
452 TableCellRenderer defaultRenderer = table.getDefaultRenderer(cc);
\r
453 return defaultRenderer.getTableCellRendererComponent(
\r
454 table, value, isSelected, hasFocus, row, column
\r
459 * このプレイリスト(シーケンスリスト)が表示するデータを提供する
\r
461 * @return プレイリストモデル
\r
464 public SequenceListTableModel getModel() {
\r
465 return (SequenceListTableModel) super.getModel();
\r
470 Action deleteSequenceAction = getModel().new SelectedSequenceAction(
\r
471 "Delete", MidiEditor.deleteIcon,
\r
472 "Delete selected MIDI sequence - 選択した曲をプレイリストから削除"
\r
475 public void actionPerformed(ActionEvent e) {
\r
476 SequenceListTableModel model = getModel();
\r
477 if( midiFileChooser != null ) {
\r
478 // ファイルに保存できる場合(Javaアプレットではなく、Javaアプリとして動作している場合)
\r
480 SequenceTrackListTableModel seqModel = model.getSelectedSequenceModel();
\r
481 if( seqModel.isModified() ) {
\r
485 "Selected MIDI sequence not saved - delete it ?\n" +
\r
486 "選択したMIDIシーケンスはまだ保存されていません。削除しますか?";
\r
487 if( ! confirm(message) ) {
\r
494 model.removeSelectedSequence();
\r
498 * ファイル選択ダイアログ(アプレットでは使用不可)
\r
500 private class MidiFileChooser extends JFileChooser {
\r
502 String description = "MIDI sequence (*.mid)";
\r
503 String extension = "mid";
\r
504 FileFilter filter = new FileNameExtensionFilter(description, extension);
\r
505 setFileFilter(filter);
\r
510 public Action saveMidiFileAction = getModel().new SelectedSequenceAction(
\r
512 "Save selected MIDI sequence to file - 選択したMIDIシーケンスをファイルに保存"
\r
515 public void actionPerformed(ActionEvent e) {
\r
516 SequenceListTableModel model = getModel();
\r
517 SequenceTrackListTableModel sequenceModel = model.getSelectedSequenceModel();
\r
518 String filename = sequenceModel.getFilename();
\r
520 if( filename != null && ! filename.isEmpty() ) {
\r
521 // プレイリスト上でファイル名が入っていたら、それを初期選択
\r
522 setSelectedFile(selectedFile = new File(filename));
\r
524 int saveOption = showSaveDialog(MidiEditor.this);
\r
525 if( saveOption != JFileChooser.APPROVE_OPTION ) {
\r
526 // 保存ダイアログでキャンセルされた場合
\r
529 if( (selectedFile = getSelectedFile()).exists() ) {
\r
530 // 指定されたファイルがすでにあった場合
\r
531 String fn = selectedFile.getName();
\r
532 String message = "Overwrite " + fn + " ?\n";
\r
533 message += fn + " を上書きしてよろしいですか?";
\r
534 if( ! confirm(message) ) {
\r
540 try ( FileOutputStream out = new FileOutputStream(selectedFile) ) {
\r
541 out.write(sequenceModel.getMIDIdata());
\r
542 sequenceModel.setModified(false);
\r
544 catch( IOException ex ) {
\r
545 showError( ex.getMessage() );
\r
546 ex.printStackTrace();
\r
553 public Action openMidiFileAction = new AbstractAction("Open") {
\r
555 String tooltip = "Open MIDI file - MIDIファイルを開く";
\r
556 putValue(Action.SHORT_DESCRIPTION, tooltip);
\r
559 public void actionPerformed(ActionEvent event) {
\r
560 int openOption = showOpenDialog(MidiEditor.this);
\r
561 if(openOption == JFileChooser.APPROVE_OPTION) {
\r
563 getModel().addSequence(getSelectedFile());
\r
564 } catch( IOException|InvalidMidiDataException e ) {
\r
565 showWarning(e.getMessage());
\r
566 } catch( AccessControlException e ) {
\r
567 showError(e.getMessage());
\r
568 e.printStackTrace();
\r
577 * シーケンス(トラックリスト)テーブルビュー
\r
579 private class TrackListTable extends JTable {
\r
581 * トラックリストテーブルビューを構築します。
\r
582 * @param model シーケンス(トラックリスト)データモデル
\r
584 public TrackListTable(SequenceTrackListTableModel model) {
\r
585 super(model, null, model.trackListSelectionModel);
\r
587 // 録音対象のMIDIチャンネルをコンボボックスで選択できるようにする
\r
588 int colIndex = SequenceTrackListTableModel.Column.RECORD_CHANNEL.ordinal();
\r
589 TableColumn tc = getColumnModel().getColumn(colIndex);
\r
590 tc.setCellEditor(new DefaultCellEditor(new JComboBox<String>(){{
\r
592 for(int i=1; i <= MIDISpec.MAX_CHANNELS; i++)
\r
593 addItem(String.format("%d", i));
\r
596 setAutoCreateColumnsFromModel(false);
\r
598 trackSelectionListener = new TrackSelectionListener();
\r
599 titleLabel = new TitleLabel();
\r
600 model.sequenceListTableModel.sequenceListSelectionModel.addListSelectionListener(titleLabel);
\r
601 TableColumnModel colModel = getColumnModel();
\r
602 for( SequenceTrackListTableModel.Column c : SequenceTrackListTableModel.Column.values() )
\r
603 colModel.getColumn(c.ordinal()).setPreferredWidth(c.preferredWidth);
\r
606 * このテーブルビューが表示するデータを提供する
\r
607 * シーケンス(トラックリスト)データモデルを返します。
\r
608 * @return シーケンス(トラックリスト)データモデル
\r
611 public SequenceTrackListTableModel getModel() {
\r
612 return (SequenceTrackListTableModel) super.getModel();
\r
617 TitleLabel titleLabel;
\r
619 * 親テーブルの選択シーケンスの変更に反応する
\r
622 private class TitleLabel extends JLabel implements ListSelectionListener {
\r
623 private static final String TITLE = "Tracks";
\r
624 public TitleLabel() { setText(TITLE); }
\r
626 public void valueChanged(ListSelectionEvent event) {
\r
627 if( event.getValueIsAdjusting() )
\r
629 SequenceTrackListTableModel oldModel = getModel();
\r
630 SequenceTrackListTableModel newModel = oldModel.sequenceListTableModel.getSelectedSequenceModel();
\r
631 if( oldModel == newModel )
\r
634 // MIDIチャンネル選択中のときはキャンセルする
\r
635 cancelCellEditing();
\r
637 int index = oldModel.sequenceListTableModel.sequenceListSelectionModel.getMinSelectionIndex();
\r
638 String text = TITLE;
\r
640 text = String.format(text+" - MIDI file No.%d", index);
\r
643 if( newModel == null ) {
\r
644 newModel = oldModel.sequenceListTableModel.emptyTrackListTableModel;
\r
645 addTrackAction.setEnabled(false);
\r
648 addTrackAction.setEnabled(true);
\r
650 oldModel.trackListSelectionModel.removeListSelectionListener(trackSelectionListener);
\r
651 setModel(newModel);
\r
652 setSelectionModel(newModel.trackListSelectionModel);
\r
653 newModel.trackListSelectionModel.addListSelectionListener(trackSelectionListener);
\r
654 trackSelectionListener.valueChanged(null);
\r
660 TrackSelectionListener trackSelectionListener;
\r
662 * 選択トラックの変更に反応するリスナー
\r
664 private class TrackSelectionListener implements ListSelectionListener {
\r
666 public void valueChanged(ListSelectionEvent e) {
\r
667 if( e != null && e.getValueIsAdjusting() )
\r
669 TrackListSelectionModel tlsm = getModel().trackListSelectionModel;
\r
670 deleteTrackAction.setEnabled(! tlsm.isSelectionEmpty());
\r
671 eventListTable.titleLabel.update(tlsm, getModel());
\r
677 * <p>このトラックリストテーブルのデータが変わったときに編集を解除します。
\r
678 * 例えば、イベントが編集された場合や、
\r
679 * シーケンサーからこのモデルが外された場合がこれに該当します。
\r
683 public void tableChanged(TableModelEvent e) {
\r
684 super.tableChanged(e);
\r
685 cancelCellEditing();
\r
688 * このトラックリストテーブルが編集モードになっていたら解除します。
\r
690 private void cancelCellEditing() {
\r
691 TableCellEditor currentCellEditor = getCellEditor();
\r
692 if( currentCellEditor != null )
\r
693 currentCellEditor.cancelCellEditing();
\r
698 Action addTrackAction = new AbstractAction("New") {
\r
700 String tooltip = "Append new track - 新しいトラックの追加";
\r
701 putValue(Action.SHORT_DESCRIPTION, tooltip);
\r
705 public void actionPerformed(ActionEvent e) {
\r
706 getModel().createTrack();
\r
712 Action deleteTrackAction = new AbstractAction("Delete", deleteIcon) {
\r
714 String tooltip = "Delete selected track - 選択したトラックを削除";
\r
715 putValue(Action.SHORT_DESCRIPTION, tooltip);
\r
719 public void actionPerformed(ActionEvent e) {
\r
720 String message = "Do you want to delete selected track ?\n"
\r
721 + "選択したトラックを削除しますか?";
\r
722 if( confirm(message) ) getModel().deleteSelectedTracks();
\r
728 * MIDIイベントリストテーブルビュー(選択中のトラックの中身)
\r
730 class EventListTable extends JTable {
\r
732 * 新しいイベントリストテーブルを構築します。
\r
733 * <p>データモデルとして一つのトラックのイベントリストを指定できます。
\r
734 * トラックを切り替えたいときは {@link #setModel(TableModel)}
\r
735 * でデータモデルを異なるトラックのものに切り替えます。
\r
738 * @param model トラック(イベントリスト)データモデル
\r
740 public EventListTable(TrackEventListTableModel model) {
\r
741 super(model, null, model.eventSelectionModel);
\r
744 eventCellEditor = new MidiEventCellEditor();
\r
745 setAutoCreateColumnsFromModel(false);
\r
747 eventSelectionListener = new EventSelectionListener();
\r
748 titleLabel = new TitleLabel();
\r
750 TableColumnModel colModel = getColumnModel();
\r
751 for( TrackEventListTableModel.Column c : TrackEventListTableModel.Column.values() )
\r
752 colModel.getColumn(c.ordinal()).setPreferredWidth(c.preferredWidth);
\r
755 * このテーブルビューが表示するデータを提供する
\r
756 * トラック(イベントリスト)データモデルを返します。
\r
757 * @return トラック(イベントリスト)データモデル
\r
760 public TrackEventListTableModel getModel() {
\r
761 return (TrackEventListTableModel) super.getModel();
\r
766 TitleLabel titleLabel;
\r
768 * 親テーブルの選択トラックの変更に反応する
\r
771 private class TitleLabel extends JLabel {
\r
772 private static final String TITLE = "MIDI Events";
\r
773 public TitleLabel() { super(TITLE); }
\r
774 public void update(TrackListSelectionModel tlsm, SequenceTrackListTableModel sequenceModel) {
\r
775 String text = TITLE;
\r
776 TrackEventListTableModel oldTrackModel = getModel();
\r
777 int index = tlsm.getMinSelectionIndex();
\r
779 text = String.format(TITLE+" - track No.%d", index);
\r
782 TrackEventListTableModel newTrackModel = sequenceModel.getSelectedTrackModel();
\r
783 if( oldTrackModel == newTrackModel )
\r
785 if( newTrackModel == null ) {
\r
786 newTrackModel = getModel().sequenceTrackListTableModel.sequenceListTableModel.emptyEventListTableModel;
\r
787 queryJumpEventAction.setEnabled(false);
\r
788 queryAddEventAction.setEnabled(false);
\r
790 queryPasteEventAction.setEnabled(false);
\r
791 copyEventAction.setEnabled(false);
\r
792 deleteEventAction.setEnabled(false);
\r
793 cutEventAction.setEnabled(false);
\r
796 queryJumpEventAction.setEnabled(true);
\r
797 queryAddEventAction.setEnabled(true);
\r
799 oldTrackModel.eventSelectionModel.removeListSelectionListener(eventSelectionListener);
\r
800 setModel(newTrackModel);
\r
801 setSelectionModel(newTrackModel.eventSelectionModel);
\r
802 newTrackModel.eventSelectionModel.addListSelectionListener(eventSelectionListener);
\r
808 private EventSelectionListener eventSelectionListener;
\r
810 * 選択イベントの変更に反応するリスナー
\r
812 private class EventSelectionListener implements ListSelectionListener {
\r
813 public EventSelectionListener() {
\r
814 getModel().eventSelectionModel.addListSelectionListener(this);
\r
817 public void valueChanged(ListSelectionEvent e) {
\r
818 if( e.getValueIsAdjusting() )
\r
820 if( getSelectionModel().isSelectionEmpty() ) {
\r
821 queryPasteEventAction.setEnabled(false);
\r
822 copyEventAction.setEnabled(false);
\r
823 deleteEventAction.setEnabled(false);
\r
824 cutEventAction.setEnabled(false);
\r
827 copyEventAction.setEnabled(true);
\r
828 deleteEventAction.setEnabled(true);
\r
829 cutEventAction.setEnabled(true);
\r
830 TrackEventListTableModel trackModel = getModel();
\r
831 int minIndex = getSelectionModel().getMinSelectionIndex();
\r
832 MidiEvent midiEvent = trackModel.getMidiEvent(minIndex);
\r
833 if( midiEvent != null ) {
\r
834 MidiMessage msg = midiEvent.getMessage();
\r
835 if( msg instanceof ShortMessage ) {
\r
836 ShortMessage sm = (ShortMessage)msg;
\r
837 int cmd = sm.getCommand();
\r
838 if( cmd == 0x80 || cmd == 0x90 || cmd == 0xA0 ) {
\r
839 // ノート番号を持つ場合、音を鳴らす。
\r
840 MidiChannel outMidiChannels[] = virtualMidiDevice.getChannels();
\r
841 int ch = sm.getChannel();
\r
842 int note = sm.getData1();
\r
843 int vel = sm.getData2();
\r
844 outMidiChannels[ch].noteOn(note, vel);
\r
845 outMidiChannels[ch].noteOff(note, vel);
\r
849 if( pairNoteOnOffModel.isSelected() ) {
\r
850 int maxIndex = getSelectionModel().getMaxSelectionIndex();
\r
852 for( int i=minIndex; i<=maxIndex; i++ ) {
\r
853 if( ! getSelectionModel().isSelectedIndex(i) ) continue;
\r
854 partnerIndex = trackModel.getIndexOfPartnerFor(i);
\r
855 if( partnerIndex >= 0 && ! getSelectionModel().isSelectedIndex(partnerIndex) )
\r
856 getSelectionModel().addSelectionInterval(partnerIndex, partnerIndex);
\r
863 * Pair noteON/OFF トグルボタンモデル
\r
865 private JToggleButton.ToggleButtonModel
\r
866 pairNoteOnOffModel = new JToggleButton.ToggleButtonModel() {
\r
869 new ItemListener() {
\r
870 public void itemStateChanged(ItemEvent e) {
\r
871 eventDialog.midiMessageForm.durationForm.setEnabled(isSelected());
\r
881 private TickPositionModel tickPositionModel = new TickPositionModel();
\r
885 private MidiEvent selectedMidiEvent = null;
\r
889 private int selectedIndex = -1;
\r
893 private long currentTick = 0;
\r
895 * 上書きして削除対象にする変更前イベント(null可)
\r
897 private MidiEvent[] midiEventsToBeOverwritten;
\r
899 * 選択したイベントを入力ダイアログなどに反映します。
\r
900 * @param model 対象データモデル
\r
902 private void setSelectedEvent(TrackEventListTableModel model) {
\r
903 SequenceTrackListTableModel sequenceTableModel = model.sequenceTrackListTableModel;
\r
904 eventDialog.midiMessageForm.durationForm.setPPQ(sequenceTableModel.getSequence().getResolution());
\r
905 tickPositionModel.setSequenceIndex(sequenceTableModel.getSequenceTickIndex());
\r
906 selectedIndex = -1;
\r
908 selectedMidiEvent = null;
\r
909 if( getSelectionModel().isSelectionEmpty() )
\r
911 selectedIndex = model.eventSelectionModel.getMinSelectionIndex();
\r
912 selectedMidiEvent = model.getMidiEvent(selectedIndex);
\r
913 currentTick = selectedMidiEvent.getTick();
\r
914 tickPositionModel.setTickPosition(currentTick);
\r
917 * 指定のTick位置へジャンプするアクション
\r
919 Action queryJumpEventAction = new AbstractAction() {
\r
921 putValue(NAME,"Jump to ...");
\r
924 private Action jumpEventAction = new AbstractAction() {
\r
925 { putValue(NAME,"Jump"); }
\r
926 public void actionPerformed(ActionEvent e) {
\r
927 scrollToEventAt(tickPositionModel.getTickPosition());
\r
928 eventDialog.setVisible(false);
\r
931 public void actionPerformed(ActionEvent e) {
\r
932 setSelectedEvent(getModel());
\r
933 eventDialog.openTickForm("Jump selection to", jumpEventAction);
\r
937 * 新しいイベントの追加を行うアクション
\r
939 Action queryAddEventAction = new AbstractAction() {
\r
941 putValue(NAME,"New");
\r
944 public void actionPerformed(ActionEvent e) {
\r
945 TrackEventListTableModel model = getModel();
\r
946 setSelectedEvent(model);
\r
947 midiEventsToBeOverwritten = null;
\r
948 eventDialog.openEventForm(
\r
950 eventCellEditor.applyEventAction,
\r
956 * MIDIイベントのコピー&ペーストを行うためのクリップボード
\r
958 private class LocalClipBoard {
\r
959 private MidiEvent copiedEventsToPaste[];
\r
960 private int copiedEventsPPQ = 0;
\r
961 public void copy(TrackEventListTableModel model, boolean withRemove) {
\r
962 copiedEventsToPaste = model.getSelectedMidiEvents();
\r
963 copiedEventsPPQ = model.sequenceTrackListTableModel.sequenceListTableModel.getSelectedSequenceModel().getSequence().getResolution();
\r
965 model.removeMidiEvents(copiedEventsToPaste);
\r
967 queryPasteEventAction.setEnabled(
\r
968 copiedEventsToPaste != null &&
\r
969 copiedEventsToPaste.length > 0
\r
972 public void cut(TrackEventListTableModel model) {
\r
975 public void copy(TrackEventListTableModel model) {
\r
976 copy(model, false);
\r
978 public void paste(TrackEventListTableModel model, long tick) {
\r
979 model.addMidiEvents(copiedEventsToPaste, tick, copiedEventsPPQ);
\r
982 private LocalClipBoard clipBoard = new LocalClipBoard();
\r
984 * 指定のTick位置へ貼り付けるアクション
\r
986 Action queryPasteEventAction = new AbstractAction() {
\r
987 private TrackEventListTableModel pastingModel;
\r
989 putValue(NAME,"Paste to ...");
\r
992 private Action pasteEventAction = new AbstractAction() {
\r
994 putValue(NAME,"Paste");
\r
996 public void actionPerformed(ActionEvent e) {
\r
997 long tick = tickPositionModel.getTickPosition();
\r
998 clipBoard.paste(pastingModel, tick);
\r
999 scrollToEventAt(tick);
\r
1000 // プレイリストの曲の長さ表示を更新
\r
1001 pastingModel.sequenceTrackListTableModel.sequenceListTableModel.fireSelectedSequenceChanged();
\r
1002 eventDialog.setVisible(false);
\r
1003 pastingModel = null;
\r
1006 public void actionPerformed(ActionEvent e) {
\r
1007 setSelectedEvent(pastingModel = getModel());
\r
1008 eventDialog.openTickForm("Paste to", pasteEventAction);
\r
1014 public Action cutEventAction = new AbstractAction("Cut") {
\r
1016 setEnabled(false);
\r
1019 public void actionPerformed(ActionEvent e) {
\r
1020 TrackEventListTableModel model = getModel();
\r
1021 if( ! confirm("Do you want to cut selected event ?\n選択したMIDIイベントを切り取りますか?"))
\r
1023 clipBoard.cut(model);
\r
1029 public Action copyEventAction = new AbstractAction("Copy") {
\r
1031 setEnabled(false);
\r
1034 public void actionPerformed(ActionEvent e) {
\r
1035 clipBoard.copy(getModel());
\r
1041 public Action deleteEventAction = new AbstractAction("Delete", deleteIcon) {
\r
1043 setEnabled(false);
\r
1046 public void actionPerformed(ActionEvent e) {
\r
1047 TrackEventListTableModel model = getModel();
\r
1048 if( ! confirm("Do you want to delete selected event ?\n選択したMIDIイベントを削除しますか?"))
\r
1050 model.removeSelectedMidiEvents();
\r
1054 * MIDIイベント表のセルエディタ
\r
1056 private MidiEventCellEditor eventCellEditor;
\r
1058 * MIDIイベント表のセルエディタ
\r
1060 class MidiEventCellEditor extends AbstractCellEditor implements TableCellEditor {
\r
1062 * MIDIイベントセルエディタを構築します。
\r
1064 public MidiEventCellEditor() {
\r
1065 eventDialog.midiMessageForm.setOutputMidiChannels(virtualMidiDevice.getChannels());
\r
1066 eventDialog.tickPositionInputForm.setModel(tickPositionModel);
\r
1067 int index = TrackEventListTableModel.Column.MESSAGE.ordinal();
\r
1068 getColumnModel().getColumn(index).setCellEditor(this);
\r
1071 * セルをダブルクリックしないと編集できないようにします。
\r
1072 * @param e イベント(マウスイベント)
\r
1073 * @return 編集可能になったらtrue
\r
1076 public boolean isCellEditable(EventObject e) {
\r
1077 if( ! (e instanceof MouseEvent) )
\r
1078 return super.isCellEditable(e);
\r
1079 return ((MouseEvent)e).getClickCount() == 2;
\r
1082 public Object getCellEditorValue() { return null; }
\r
1084 * 既存イベントを編集するアクション
\r
1086 private Action editEventAction = new AbstractAction() {
\r
1087 public void actionPerformed(ActionEvent e) {
\r
1088 TrackEventListTableModel model = getModel();
\r
1089 setSelectedEvent(model);
\r
1090 if( selectedMidiEvent == null )
\r
1092 MidiEvent partnerEvent = null;
\r
1093 eventDialog.midiMessageForm.setMessage(selectedMidiEvent.getMessage());
\r
1094 if( eventDialog.midiMessageForm.isNote() ) {
\r
1095 int partnerIndex = model.getIndexOfPartnerFor(selectedIndex);
\r
1096 if( partnerIndex < 0 ) {
\r
1097 eventDialog.midiMessageForm.durationForm.setDuration(0);
\r
1100 partnerEvent = model.getMidiEvent(partnerIndex);
\r
1101 long partnerTick = partnerEvent.getTick();
\r
1102 long duration = currentTick > partnerTick ?
\r
1103 currentTick - partnerTick : partnerTick - currentTick ;
\r
1104 eventDialog.midiMessageForm.durationForm.setDuration((int)duration);
\r
1107 if(partnerEvent == null)
\r
1108 midiEventsToBeOverwritten = new MidiEvent[] {selectedMidiEvent};
\r
1110 midiEventsToBeOverwritten = new MidiEvent[] {selectedMidiEvent, partnerEvent};
\r
1111 eventDialog.cancelButton.addActionListener(cancelActionListener);
\r
1112 eventDialog.openEventForm("Change MIDI event", applyEventAction);
\r
1118 private JButton editEventButton = new JButton(editEventAction){{
\r
1119 setHorizontalAlignment(JButton.LEFT);
\r
1122 public Component getTableCellEditorComponent(
\r
1123 JTable table, Object value, boolean isSelected, int row, int column
\r
1125 editEventButton.setText(value.toString());
\r
1126 return editEventButton;
\r
1129 * イベント入力をキャンセルするアクションリスナーです。
\r
1131 * <p>セル編集によって表示されたMIDIメッセージダイアログを
\r
1132 * キャンセルする場合、セル編集を中止する処理の追加が必要です。
\r
1133 * その追加処理をこのリスナーでカバーします。
\r
1136 private ActionListener cancelActionListener = new ActionListener() {
\r
1137 public void actionPerformed(ActionEvent e) {
\r
1138 fireEditingCanceled();
\r
1140 eventDialog.cancelButton.removeActionListener(this);
\r
1144 * 入力したイベントを反映するアクション
\r
1146 private Action applyEventAction = new AbstractAction() {
\r
1148 putValue(NAME,"OK");
\r
1150 public void actionPerformed(ActionEvent e) {
\r
1151 TrackEventListTableModel trackModel = getModel();
\r
1152 long tick = tickPositionModel.getTickPosition();
\r
1153 MidiMessageForm form = eventDialog.midiMessageForm;
\r
1154 MidiEvent newMidiEvent = new MidiEvent(form.getMessage(), tick);
\r
1155 if( midiEventsToBeOverwritten != null ) {
\r
1156 // 上書き消去するための選択済イベントがあった場合
\r
1157 trackModel.removeMidiEvents(midiEventsToBeOverwritten);
\r
1159 if( ! trackModel.addMidiEvent(newMidiEvent) ) {
\r
1160 System.out.println("addMidiEvent failure");
\r
1163 if(pairNoteOnOffModel.isSelected() && form.isNote()) {
\r
1164 ShortMessage sm = form.createPartnerMessage();
\r
1166 scrollToEventAt( tick );
\r
1168 int duration = form.durationForm.getDuration();
\r
1169 if( form.isNote(false) ) {
\r
1170 duration = -duration;
\r
1172 long partnerTick = tick + (long)duration;
\r
1173 if( partnerTick < 0L ) partnerTick = 0L;
\r
1174 MidiEvent partner = new MidiEvent((MidiMessage)sm, partnerTick);
\r
1175 if( ! trackModel.addMidiEvent(partner) ) {
\r
1176 System.out.println("addMidiEvent failure (note on/off partner message)");
\r
1178 scrollToEventAt(partnerTick > tick ? partnerTick : tick);
\r
1181 trackModel.sequenceTrackListTableModel.sequenceListTableModel.fireSequenceModified(getModel().sequenceTrackListTableModel);
\r
1182 eventDialog.setVisible(false);
\r
1183 fireEditingStopped();
\r
1188 * スクロール可能なMIDIイベントテーブルビュー
\r
1190 private JScrollPane scrollPane = new JScrollPane(this);
\r
1192 * 指定の MIDI tick のイベントへスクロールします。
\r
1193 * @param tick MIDI tick
\r
1195 public void scrollToEventAt(long tick) {
\r
1196 int index = getModel().tickToIndex(tick);
\r
1197 scrollPane.getVerticalScrollBar().setValue(index * getRowHeight());
\r
1198 getSelectionModel().setSelectionInterval(index, index);
\r
1203 * 新しい {@link MidiEditor} を構築します。
\r
1204 * @param deviceModelList MIDIデバイスモデルリスト
\r
1206 public MidiEditor(MidiSequencerModel sequencerModel) {
\r
1207 // テーブルモデルとテーブルビューの生成
\r
1208 sequenceListTable = new SequenceListTable(
\r
1209 new SequenceListTableModel(sequencerModel)
\r
1211 trackListTable = new TrackListTable(
\r
1212 new SequenceTrackListTableModel(
\r
1213 sequenceListTable.getModel(), null, null
\r
1216 eventListTable = new EventListTable(
\r
1217 new TrackEventListTableModel(trackListTable.getModel(), null)
\r
1220 setTitle("MIDI Editor/Playlist - MIDI Chord Helper");
\r
1221 setBounds( 150, 200, 850, 500 );
\r
1222 setLayout(new FlowLayout());
\r
1223 new DropTarget(this, DnDConstants.ACTION_COPY_OR_MOVE, this, true);
\r
1224 JPanel playlistPanel = new JPanel() {{
\r
1225 JPanel playlistOperationPanel = new JPanel() {{
\r
1226 setLayout(new BoxLayout(this, BoxLayout.LINE_AXIS));
\r
1227 add(Box.createRigidArea(new Dimension(10, 0)));
\r
1228 add(new JButton(newSequenceDialog.openAction) {{
\r
1229 setMargin(ZERO_INSETS);
\r
1231 if( sequenceListTable.midiFileChooser != null ) {
\r
1232 add( Box.createRigidArea(new Dimension(5, 0)) );
\r
1234 sequenceListTable.midiFileChooser.openMidiFileAction
\r
1236 setMargin(ZERO_INSETS);
\r
1239 add(Box.createRigidArea(new Dimension(5, 0)));
\r
1240 SequenceListTableModel sequenceListTableModel = sequenceListTable.getModel();
\r
1241 add(new JButton(sequenceListTableModel.moveToTopAction) {{
\r
1242 setMargin(ZERO_INSETS);
\r
1244 add(Box.createRigidArea(new Dimension(5, 0)));
\r
1245 add(new JButton(sequenceListTableModel.moveToBottomAction) {{
\r
1246 setMargin(ZERO_INSETS);
\r
1248 if(sequenceListTable.base64EncodeAction != null) {
\r
1249 add(Box.createRigidArea(new Dimension(5, 0)));
\r
1250 add(new JButton(sequenceListTable.base64EncodeAction) {{
\r
1251 setMargin(ZERO_INSETS);
\r
1254 if( sequenceListTable.midiFileChooser != null ) {
\r
1255 add(Box.createRigidArea(new Dimension(5, 0)));
\r
1257 sequenceListTable.midiFileChooser.saveMidiFileAction
\r
1259 setMargin(ZERO_INSETS);
\r
1262 add( Box.createRigidArea(new Dimension(5, 0)) );
\r
1263 add(new JButton(sequenceListTable.deleteSequenceAction) {{
\r
1264 setMargin(ZERO_INSETS);
\r
1266 add( Box.createRigidArea(new Dimension(5, 0)) );
\r
1267 add(new SequencerSpeedSlider(
\r
1268 sequenceListTableModel.sequencerModel.speedSliderModel
\r
1271 setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
\r
1272 add(new JScrollPane(sequenceListTable));
\r
1273 add(Box.createRigidArea(new Dimension(0, 10)));
\r
1274 add(playlistOperationPanel);
\r
1275 add(Box.createRigidArea(new Dimension(0, 10)));
\r
1277 JPanel trackListPanel = new JPanel() {{
\r
1278 JPanel trackListOperationPanel = new JPanel() {{
\r
1279 add(new JButton(trackListTable.addTrackAction) {{
\r
1280 setMargin(ZERO_INSETS);
\r
1282 add(new JButton(trackListTable.deleteTrackAction) {{
\r
1283 setMargin(ZERO_INSETS);
\r
1286 setLayout(new BoxLayout(this, BoxLayout.PAGE_AXIS));
\r
1287 add(trackListTable.titleLabel);
\r
1288 add(Box.createRigidArea(new Dimension(0, 5)));
\r
1289 add(new JScrollPane(trackListTable));
\r
1290 add(Box.createRigidArea(new Dimension(0, 5)));
\r
1291 add(trackListOperationPanel);
\r
1293 JPanel eventListPanel = new JPanel() {{
\r
1294 JPanel eventListOperationPanel = new JPanel() {{
\r
1295 add(new JCheckBox("Pair NoteON/OFF") {{
\r
1296 setModel(eventListTable.pairNoteOnOffModel);
\r
1297 setToolTipText("NoteON/OFFをペアで同時選択する");
\r
1299 add(new JButton(eventListTable.queryJumpEventAction) {{
\r
1300 setMargin(ZERO_INSETS);
\r
1302 add(new JButton(eventListTable.queryAddEventAction) {{
\r
1303 setMargin(ZERO_INSETS);
\r
1305 add(new JButton(eventListTable.copyEventAction) {{
\r
1306 setMargin(ZERO_INSETS);
\r
1308 add(new JButton(eventListTable.cutEventAction) {{
\r
1309 setMargin(ZERO_INSETS);
\r
1311 add(new JButton(eventListTable.queryPasteEventAction) {{
\r
1312 setMargin(ZERO_INSETS);
\r
1314 add(new JButton(eventListTable.deleteEventAction) {{
\r
1315 setMargin(ZERO_INSETS);
\r
1318 setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
\r
1319 add(eventListTable.titleLabel);
\r
1320 add(eventListTable.scrollPane);
\r
1321 add(eventListOperationPanel);
\r
1323 Container cp = getContentPane();
\r
1324 cp.setLayout(new BoxLayout(cp, BoxLayout.Y_AXIS));
\r
1325 cp.add(Box.createVerticalStrut(2));
\r
1327 new JSplitPane(JSplitPane.VERTICAL_SPLIT, playlistPanel,
\r
1328 new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, trackListPanel, eventListPanel) {{
\r
1329 setDividerLocation(300);
\r
1332 setDividerLocation(160);
\r
1340 * シーケンサーの再生スピード調整スライダビュー
\r
1342 class SequencerSpeedSlider extends JPanel {
\r
1343 private static final String items[] = {
\r
1351 private JLabel titleLabel;
\r
1352 private JSlider slider;
\r
1353 public SequencerSpeedSlider(BoundedRangeModel model) {
\r
1354 add(titleLabel = new JLabel("Speed:"));
\r
1355 add(slider = new JSlider(model){{
\r
1356 setPaintTicks(true);
\r
1357 setMajorTickSpacing(12);
\r
1358 setMinorTickSpacing(1);
\r
1359 setVisible(false);
\r
1361 add(new JComboBox<String>(items) {{
\r
1362 addActionListener(new ActionListener() {
\r
1364 public void actionPerformed(ActionEvent e) {
\r
1365 int index = getSelectedIndex();
\r
1366 BoundedRangeModel model = slider.getModel();
\r
1367 if( index == 0 ) {
\r
1368 model.setValue(0);
\r
1369 slider.setVisible(false);
\r
1370 titleLabel.setVisible(true);
\r
1373 int maxValue = ( index == 1 ? 7 : (index-1)*12 );
\r
1374 model.setMinimum(-maxValue);
\r
1375 model.setMaximum(maxValue);
\r
1376 slider.setMajorTickSpacing( index == 1 ? 7 : 12 );
\r
1377 slider.setMinorTickSpacing( index > 3 ? 12 : 1 );
\r
1378 slider.setVisible(true);
\r
1379 titleLabel.setVisible(false);
\r
1388 * 選択されているシーケンスのインデックス
\r
1390 class SequenceListSelectionModel extends DefaultListSelectionModel {
\r
1392 setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
\r
1396 * プレイリスト(MIDIシーケンスリスト)のテーブルデータモデル
\r
1398 class SequenceListTableModel extends AbstractTableModel {
\r
1402 MidiSequencerModel sequencerModel;
\r
1406 SequenceTrackListTableModel emptyTrackListTableModel;
\r
1410 TrackEventListTableModel emptyEventListTableModel;
\r
1412 * 新しいプレイリストのテーブルモデルを構築します。
\r
1413 * @param sequencerModel MIDIシーケンサーモデル
\r
1415 public SequenceListTableModel(MidiSequencerModel sequencerModel) {
\r
1416 this.sequencerModel = sequencerModel;
\r
1419 sequencerModel.addChangeListener(secondPosition = new SecondPosition());
\r
1422 sequencerModel.getSequencer().addMetaEventListener(
\r
1423 new MetaEventListener() {
\r
1427 * <p>EOT (End Of Track、type==0x2F) を受信したときの処理です。
\r
1429 * <p>これは MetaEventListener のための実装なので、多くの場合
\r
1430 * Swing EDT ではなく MIDI シーケンサの EDT から起動されます。
\r
1431 * Swing EDT とは違うスレッドで動いていた場合は Swing EDT に振り直されます。
\r
1435 public void meta(MetaMessage msg) {
\r
1436 if( msg.getType() == 0x2F ) {
\r
1437 if( ! SwingUtilities.isEventDispatchThread() ) {
\r
1438 SwingUtilities.invokeLater(
\r
1441 public void run() { goNext(); }
\r
1451 emptyTrackListTableModel = new SequenceTrackListTableModel(this, null, null);
\r
1452 emptyEventListTableModel = new TrackEventListTableModel(emptyTrackListTableModel, null);
\r
1457 * <p>リピートモードの場合は同じ曲をもう一度再生、
\r
1458 * そうでない場合は次の曲へ進んで再生します。
\r
1459 * 次の曲がなければ、そこで停止します。
\r
1460 * いずれの場合も局の先頭へ戻ります。
\r
1463 private void goNext() {
\r
1465 sequencerModel.getSequencer().setMicrosecondPosition(0);
\r
1466 if( (Boolean)toggleRepeatAction.getValue(Action.SELECTED_KEY) || loadNext(1)) {
\r
1467 // リピートモードのときはもう一度同じ曲を、
\r
1468 // そうでない場合は次の曲を再生開始
\r
1469 sequencerModel.start();
\r
1472 // 最後の曲が終わったので、停止状態にする
\r
1473 sequencerModel.stop();
\r
1474 // ここでボタンが停止状態に変わったはずなので、
\r
1475 // 通常であれば再生ボタンが自力で再描画するところだが、
\r
1477 // セルのレンダラーが描く再生ボタンには効かないようなので、
\r
1478 // セルを突っついて再表示させる。
\r
1479 int rowIndex = indexOfSequenceOnSequencer();
\r
1480 int colIndex = Column.SEQ_PLAY.ordinal();
\r
1481 fireTableCellUpdated(rowIndex, colIndex);
\r
1487 List<SequenceTrackListTableModel> sequenceList = new Vector<>();
\r
1489 * 選択されているシーケンスのインデックス
\r
1491 SequenceListSelectionModel sequenceListSelectionModel = new SequenceListSelectionModel();
\r
1493 * 行が選択されているときだけイネーブルになるアクション
\r
1495 public abstract class SelectedSequenceAction extends AbstractAction
\r
1496 implements ListSelectionListener
\r
1498 public SelectedSequenceAction(String name, Icon icon, String tooltip) {
\r
1499 super(name,icon); init(tooltip);
\r
1501 public SelectedSequenceAction(String name, String tooltip) {
\r
1502 super(name); init(tooltip);
\r
1505 public void valueChanged(ListSelectionEvent e) {
\r
1506 if( e.getValueIsAdjusting() ) return;
\r
1507 setEnebledBySelection();
\r
1509 protected void setEnebledBySelection() {
\r
1510 int index = sequenceListSelectionModel.getMinSelectionIndex();
\r
1511 setEnabled(index >= 0);
\r
1513 private void init(String tooltip) {
\r
1514 putValue(Action.SHORT_DESCRIPTION, tooltip);
\r
1515 sequenceListSelectionModel.addListSelectionListener(this);
\r
1516 setEnebledBySelection();
\r
1520 * 繰り返し再生ON/OFF切り替えアクション
\r
1522 public Action toggleRepeatAction = new AbstractAction() {
\r
1524 putValue(SHORT_DESCRIPTION, "Repeat - 繰り返し再生");
\r
1525 putValue(LARGE_ICON_KEY, new ButtonIcon(ButtonIcon.REPEAT_ICON));
\r
1526 putValue(SELECTED_KEY, false);
\r
1529 public void actionPerformed(ActionEvent event) { }
\r
1534 private class SecondPosition implements ChangeListener {
\r
1535 private int value = 0;
\r
1537 * 再生中のシーケンサーの秒位置が変わったときに表示を更新します。
\r
1538 * @param event 変更イベント
\r
1541 public void stateChanged(ChangeEvent event) {
\r
1542 Object src = event.getSource();
\r
1543 if( src instanceof MidiSequencerModel ) {
\r
1544 int newValue = ((MidiSequencerModel)src).getValue() / 1000;
\r
1545 if(value == newValue) return;
\r
1547 int rowIndex = indexOfSequenceOnSequencer();
\r
1548 fireTableCellUpdated(rowIndex, Column.SEQ_POSITION.ordinal());
\r
1552 public String toString() {
\r
1553 return String.format("%02d:%02d", value/60, value%60);
\r
1557 * 曲の先頭または前の曲へ戻るアクション
\r
1559 public Action moveToTopAction = new AbstractAction() {
\r
1561 putValue(SHORT_DESCRIPTION,
\r
1562 "Move to top or previous song - 曲の先頭または前の曲へ戻る"
\r
1564 putValue(LARGE_ICON_KEY, new ButtonIcon(ButtonIcon.TOP_ICON));
\r
1566 public void actionPerformed(ActionEvent event) {
\r
1567 if( sequencerModel.getSequencer().getTickPosition() <= 40 )
\r
1569 sequencerModel.setValue(0);
\r
1575 public Action moveToBottomAction = new AbstractAction() {
\r
1577 putValue(SHORT_DESCRIPTION, "Move to next song - 次の曲へ進む");
\r
1578 putValue(LARGE_ICON_KEY, new ButtonIcon(ButtonIcon.BOTTOM_ICON));
\r
1580 public void actionPerformed(ActionEvent event) {
\r
1581 if(loadNext(1)) sequencerModel.setValue(0);
\r
1588 public enum Column {
\r
1589 /** MIDIシーケンスの番号 */
\r
1590 SEQ_NUMBER("No.", Integer.class, 20),
\r
1592 SEQ_PLAY("Play/Stop", String.class, 60) {
\r
1594 public boolean isCellEditable() { return true; }
\r
1596 /** 再生中の時間位置(分:秒) */
\r
1597 SEQ_POSITION("Position", String.class, 60) {
\r
1599 public boolean isCellEditable() { return true; } // ダブルクリックだけ有効
\r
1601 public Object getValueOf(SequenceTrackListTableModel sequenceModel) {
\r
1602 return sequenceModel.isOnSequencer()
\r
1603 ? sequenceModel.sequenceListTableModel.secondPosition : "";
\r
1606 /** シーケンスの時間長(分:秒) */
\r
1607 SEQ_LENGTH("Length", String.class, 80) {
\r
1609 public Object getValueOf(SequenceTrackListTableModel sequenceModel) {
\r
1610 long usec = sequenceModel.getSequence().getMicrosecondLength();
\r
1611 int sec = (int)( (usec < 0 ? usec += 0x100000000L : usec) / 1000L / 1000L );
\r
1612 return String.format( "%02d:%02d", sec/60, sec%60 );
\r
1616 FILENAME("Filename", String.class, 100) {
\r
1618 public boolean isCellEditable() { return true; }
\r
1619 public Object getValueOf(SequenceTrackListTableModel sequenceModel) {
\r
1620 String filename = sequenceModel.getFilename();
\r
1621 return filename == null ? "" : filename;
\r
1625 MODIFIED("Modified", Boolean.class, 50) {
\r
1627 public Object getValueOf(SequenceTrackListTableModel sequenceModel) {
\r
1628 return sequenceModel.isModified();
\r
1631 /** シーケンス名(最初のトラックの名前) */
\r
1632 SEQ_NAME("Sequence name", String.class, 250) {
\r
1634 public boolean isCellEditable() { return true; }
\r
1635 public Object getValueOf(SequenceTrackListTableModel sequenceModel) {
\r
1636 String name = sequenceModel.toString();
\r
1637 return name == null ? "" : name;
\r
1641 RESOLUTION("Resolution", Integer.class, 60) {
\r
1643 public Object getValueOf(SequenceTrackListTableModel sequenceModel) {
\r
1644 return sequenceModel.getSequence().getResolution();
\r
1648 TRACKS("Tracks", Integer.class, 40) {
\r
1650 public Object getValueOf(SequenceTrackListTableModel sequenceModel) {
\r
1651 return sequenceModel.getSequence().getTracks().length;
\r
1655 DIVISION_TYPE("DivType", String.class, 50) {
\r
1657 public Object getValueOf(SequenceTrackListTableModel sequenceModel) {
\r
1658 float divType = sequenceModel.getSequence().getDivisionType();
\r
1659 if( divType == Sequence.PPQ ) return "PPQ";
\r
1660 else if( divType == Sequence.SMPTE_24 ) return "SMPTE_24";
\r
1661 else if( divType == Sequence.SMPTE_25 ) return "SMPTE_25";
\r
1662 else if( divType == Sequence.SMPTE_30 ) return "SMPTE_30";
\r
1663 else if( divType == Sequence.SMPTE_30DROP ) return "SMPTE_30DROP";
\r
1664 else return "[Unknown]";
\r
1668 Class<?> columnClass;
\r
1669 int preferredWidth;
\r
1672 * @param title 列のタイトル
\r
1673 * @param columnClass 列のクラス
\r
1674 * @param perferredWidth 列の適切な幅
\r
1676 private Column(String title, Class<?> columnClass, int preferredWidth) {
\r
1677 this.title = title;
\r
1678 this.columnClass = columnClass;
\r
1679 this.preferredWidth = preferredWidth;
\r
1681 public boolean isCellEditable() { return false; }
\r
1682 public Object getValueOf(SequenceTrackListTableModel sequenceModel) {
\r
1688 public int getRowCount() { return sequenceList.size(); }
\r
1690 public int getColumnCount() { return Column.values().length; }
\r
1692 public String getColumnName(int column) {
\r
1693 return Column.values()[column].title;
\r
1696 public Class<?> getColumnClass(int column) {
\r
1697 return Column.values()[column].columnClass;
\r
1700 public boolean isCellEditable(int row, int column) {
\r
1701 return Column.values()[column].isCellEditable();
\r
1703 /** 再生中のシーケンサーの秒位置 */
\r
1704 private SecondPosition secondPosition;
\r
1706 public Object getValueAt(int row, int column) {
\r
1707 Column c = Column.values()[column];
\r
1708 return c == Column.SEQ_NUMBER ? row : c.getValueOf(sequenceList.get(row));
\r
1711 public void setValueAt(Object val, int row, int column) {
\r
1712 Column c = Column.values()[column];
\r
1716 sequenceList.get(row).setFilename((String)val);
\r
1717 fireTableCellUpdated(row, column);
\r
1721 if( sequenceList.get(row).setName((String)val) )
\r
1722 fireTableCellUpdated(row, Column.MODIFIED.ordinal());
\r
1723 fireTableCellUpdated(row, column);
\r
1730 * このプレイリストに読み込まれた全シーケンスの合計時間長を返します。
\r
1731 * @return 全シーケンスの合計時間長 [秒]
\r
1733 public int getTotalSeconds() {
\r
1736 for( SequenceTrackListTableModel m : sequenceList ) {
\r
1737 usec = m.getSequence().getMicrosecondLength();
\r
1738 total += (int)( (usec < 0 ? usec += 0x100000000L : usec)/1000L/1000L );
\r
1743 * 未保存の修正内容を持つシーケンスがあるか調べます。
\r
1744 * @return 未保存の修正内容を持つシーケンスがあればtrue
\r
1746 public boolean isModified() {
\r
1747 for( SequenceTrackListTableModel m : sequenceList ) {
\r
1748 if( m.isModified() ) return true;
\r
1753 * 選択したシーケンスに未保存の修正内容があることを記録します。
\r
1754 * @param selModel 選択状態
\r
1755 * @param isModified 未保存の修正内容があるときtrue
\r
1757 public void setModified(boolean isModified) {
\r
1758 int minIndex = sequenceListSelectionModel.getMinSelectionIndex();
\r
1759 int maxIndex = sequenceListSelectionModel.getMaxSelectionIndex();
\r
1760 for( int i = minIndex; i <= maxIndex; i++ ) {
\r
1761 if( sequenceListSelectionModel.isSelectedIndex(i) ) {
\r
1762 sequenceList.get(i).setModified(isModified);
\r
1763 fireTableCellUpdated(i, Column.MODIFIED.ordinal());
\r
1768 * 選択されたMIDIシーケンスのテーブルモデルを返します。
\r
1769 * @return 選択されたMIDIシーケンスのテーブルモデル(非選択時はnull)
\r
1771 public SequenceTrackListTableModel getSelectedSequenceModel() {
\r
1772 if( sequenceListSelectionModel.isSelectionEmpty() )
\r
1774 int selectedIndex = sequenceListSelectionModel.getMinSelectionIndex();
\r
1775 if( selectedIndex >= sequenceList.size() )
\r
1777 return sequenceList.get(selectedIndex);
\r
1780 * 指定されたシーケンスが修正されたことを通知します。
\r
1781 * @param sequenceTableModel MIDIシーケンスモデル
\r
1783 public void fireSequenceModified(SequenceTrackListTableModel sequenceTableModel) {
\r
1784 int index = sequenceList.indexOf(sequenceTableModel);
\r
1787 sequenceTableModel.setModified(true);
\r
1788 fireTableRowsUpdated(index, index);
\r
1791 * 指定されている選択範囲のシーケンスが変更されたことを通知します。
\r
1792 * 更新済みフラグをセットし、選択されたシーケンスの全ての列を再表示します。
\r
1794 public void fireSelectedSequenceChanged() {
\r
1795 if( sequenceListSelectionModel.isSelectionEmpty() )
\r
1797 int minIndex = sequenceListSelectionModel.getMinSelectionIndex();
\r
1798 int maxIndex = sequenceListSelectionModel.getMaxSelectionIndex();
\r
1799 for( int index = minIndex; index <= maxIndex; index++ ) {
\r
1800 sequenceList.get(index).setModified(true);
\r
1802 fireTableRowsUpdated(minIndex, maxIndex);
\r
1805 * バイト列とファイル名からMIDIシーケンスを追加します。
\r
1806 * バイト列が null の場合、空のMIDIシーケンスを追加します。
\r
1807 * @param data バイト列
\r
1808 * @param filename ファイル名
\r
1809 * @return 追加先インデックス(先頭が 0)
\r
1810 * @throws IOException ファイル読み込みに失敗した場合
\r
1811 * @throws InvalidMidiDataException MIDIデータが正しくない場合
\r
1813 public int addSequence(byte[] data, String filename)
\r
1814 throws IOException, InvalidMidiDataException
\r
1816 if( data == null ) return addDefaultSequence();
\r
1818 try (InputStream in = new ByteArrayInputStream(data)) {
\r
1819 Sequence seq = MidiSystem.getSequence(in);
\r
1820 lastIndex = addSequence(seq, filename);
\r
1821 } catch( IOException|InvalidMidiDataException e ) {
\r
1824 sequenceListSelectionModel.setSelectionInterval(lastIndex, lastIndex);
\r
1828 * MIDIシーケンスを追加します。
\r
1829 * シーケンサーが停止中の場合、追加したシーケンスから再生を開始します。
\r
1830 * @param sequence MIDIシーケンス
\r
1831 * @return 追加先インデックス(先頭が 0)
\r
1833 public int addSequenceAndPlay(Sequence sequence) {
\r
1834 int lastIndex = addSequence(sequence,"");
\r
1835 if( ! sequencerModel.getSequencer().isRunning() ) {
\r
1836 loadToSequencer(lastIndex);
\r
1837 sequencerModel.start();
\r
1842 * MIDIシーケンスを追加します。
\r
1843 * @param sequence MIDIシーケンス
\r
1844 * @param filename ファイル名
\r
1845 * @return 追加されたシーケンスのインデックス(先頭が 0)
\r
1847 public int addSequence(Sequence sequence, String filename) {
\r
1849 new SequenceTrackListTableModel(this, sequence, filename)
\r
1851 int lastIndex = sequenceList.size() - 1;
\r
1852 fireTableRowsInserted(lastIndex, lastIndex);
\r
1856 * デフォルトの内容でMIDIシーケンスを作成して追加します。
\r
1857 * @return 追加されたMIDIシーケンスのインデックス(先頭が 0)
\r
1859 public int addDefaultSequence() {
\r
1860 Sequence seq = (new Music.ChordProgression()).toMidiSequence();
\r
1861 return seq == null ? -1 : addSequence(seq,null);
\r
1865 * ファイルが null の場合、空のMIDIシーケンスを追加します。
\r
1866 * @param midiFile MIDIファイル
\r
1867 * @return 追加先インデックス(先頭が 0)
\r
1868 * @throws InvalidMidiDataException ファイル内のMIDIデータが正しくない場合
\r
1869 * @throws IOException ファイル入出力に失敗した場合
\r
1871 public int addSequence(File midiFile) throws InvalidMidiDataException, IOException {
\r
1872 if( midiFile == null ) return addDefaultSequence();
\r
1874 try (FileInputStream in = new FileInputStream(midiFile)) {
\r
1875 Sequence seq = MidiSystem.getSequence(in);
\r
1876 String filename = midiFile.getName();
\r
1877 lastIndex = addSequence(seq, filename);
\r
1878 } catch( InvalidMidiDataException|IOException e ) {
\r
1884 * 複数のMIDIファイルを追加します。
\r
1885 * @param fileList 追加するMIDIファイルのリスト
\r
1886 * @return 追加先の最初のインデックス(先頭が 0、追加されなかった場合は -1)
\r
1887 * @throws InvalidMidiDataException ファイル内のMIDIデータが正しくない場合
\r
1888 * @throws IOException ファイル入出力に失敗した場合
\r
1890 public int addSequences(List<File> fileList)
\r
1891 throws InvalidMidiDataException, IOException
\r
1893 int firstIndex = -1;
\r
1894 for( File file : fileList ) {
\r
1895 int lastIndex = addSequence(file);
\r
1896 if( firstIndex == -1 )
\r
1897 firstIndex = lastIndex;
\r
1899 return firstIndex;
\r
1902 * URLから読み込んだMIDIシーケンスを追加します。
\r
1903 * @param midiFileUrl MIDIファイルのURL
\r
1904 * @return 追加先インデックス(先頭が 0、失敗した場合は -1)
\r
1905 * @throws URISyntaxException URLの形式に誤りがある場合
\r
1906 * @throws IOException 入出力に失敗した場合
\r
1907 * @throws InvalidMidiDataException MIDIデータが正しくない場合
\r
1909 public int addSequenceFromURL(String midiFileUrl)
\r
1910 throws URISyntaxException, IOException, InvalidMidiDataException
\r
1912 URI uri = new URI(midiFileUrl);
\r
1913 URL url = uri.toURL();
\r
1914 Sequence seq = MidiSystem.getSequence(url);
\r
1915 String filename = url.getFile().replaceFirst("^.*/","");
\r
1916 return addSequence(seq, filename);
\r
1920 * 選択したシーケンスを除去します。
\r
1921 * @param listSelectionModel 選択状態
\r
1923 public void removeSelectedSequence() {
\r
1924 if( sequenceListSelectionModel.isSelectionEmpty() )
\r
1926 int selectedIndex = sequenceListSelectionModel.getMinSelectionIndex();
\r
1927 if( sequenceList.remove(selectedIndex).isOnSequencer() ) {
\r
1929 // シーケンサーにロード済みだった場合、アンロードする。
\r
1930 sequencerModel.setSequenceTrackListTableModel(null);
\r
1932 fireTableRowsDeleted(selectedIndex, selectedIndex);
\r
1935 * 指定したインデックス位置のシーケンスをシーケンサーにロードします。
\r
1936 * @param index シーケンスのインデックス位置(-1 を指定するとアンロードされます)
\r
1938 public void loadToSequencer(int index) {
\r
1939 SequenceTrackListTableModel oldSeq = sequencerModel.getSequenceTrackListTableModel();
\r
1940 SequenceTrackListTableModel newSeq = (index < 0 ? null : sequenceList.get(index));
\r
1941 if(oldSeq == newSeq)
\r
1943 sequencerModel.setSequenceTrackListTableModel(newSeq);
\r
1944 int columnIndices[] = {
\r
1945 Column.SEQ_PLAY.ordinal(),
\r
1946 Column.SEQ_POSITION.ordinal(),
\r
1948 if( oldSeq != null ) {
\r
1949 int oldIndex = sequenceList.indexOf(oldSeq);
\r
1950 for( int columnIndex : columnIndices )
\r
1951 fireTableCellUpdated(oldIndex, columnIndex);
\r
1953 if( newSeq != null ) {
\r
1954 for( int columnIndex : columnIndices )
\r
1955 fireTableCellUpdated(index, columnIndex);
\r
1959 * 現在シーケンサにロードされているシーケンスのインデックスを返します。
\r
1960 * ロードされていない場合は -1 を返します。
\r
1961 * @return 現在シーケンサにロードされているシーケンスのインデックス
\r
1963 public int indexOfSequenceOnSequencer() {
\r
1964 return sequenceList.indexOf(sequencerModel.getSequenceTrackListTableModel());
\r
1967 * 引数で示された数だけ次へ進めたシーケンスをロードします。
\r
1968 * @param offset 進みたいシーケンス数
\r
1969 * @return 成功したらtrue
\r
1971 public boolean loadNext(int offset) {
\r
1972 int loadedIndex = indexOfSequenceOnSequencer();
\r
1973 int index = (loadedIndex < 0 ? 0 : loadedIndex + offset);
\r
1974 if( index < 0 || index >= sequenceList.size() )
\r
1976 loadToSequencer(index);
\r
1982 * 選択されているトラックのインデックス
\r
1984 class TrackListSelectionModel extends DefaultListSelectionModel {
\r
1986 setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
\r
1990 * MIDIシーケンス(トラックリスト)のテーブルデータモデル
\r
1992 class SequenceTrackListTableModel extends AbstractTableModel {
\r
1996 public enum Column {
\r
1998 TRACK_NUMBER("No.", Integer.class, 20),
\r
2000 EVENTS("Events", Integer.class, 40),
\r
2002 MUTE("Mute", Boolean.class, 30),
\r
2004 SOLO("Solo", Boolean.class, 30),
\r
2005 /** 録音するMIDIチャンネル */
\r
2006 RECORD_CHANNEL("RecCh", String.class, 40),
\r
2008 CHANNEL("Ch", String.class, 30),
\r
2010 TRACK_NAME("Track name", String.class, 100);
\r
2012 Class<?> columnClass;
\r
2013 int preferredWidth;
\r
2016 * @param title 列のタイトル
\r
2017 * @param widthRatio 幅の割合
\r
2018 * @param columnClass 列のクラス
\r
2019 * @param perferredWidth 列の適切な幅
\r
2021 private Column(String title, Class<?> columnClass, int preferredWidth) {
\r
2022 this.title = title;
\r
2023 this.columnClass = columnClass;
\r
2024 this.preferredWidth = preferredWidth;
\r
2030 SequenceListTableModel sequenceListTableModel;
\r
2034 private Sequence sequence;
\r
2036 * ラップされたMIDIシーケンスのtickインデックス
\r
2038 private SequenceTickIndex sequenceTickIndex;
\r
2042 private String filename = "";
\r
2046 private List<TrackEventListTableModel> trackModelList = new ArrayList<>();
\r
2048 * 選択されているトラックのインデックス
\r
2050 TrackListSelectionModel trackListSelectionModel = new TrackListSelectionModel();
\r
2052 * MIDIシーケンスとファイル名から {@link SequenceTrackListTableModel} を構築します。
\r
2053 * @param sequenceListTableModel 親のプレイリスト
\r
2054 * @param sequence MIDIシーケンス
\r
2055 * @param filename ファイル名
\r
2057 public SequenceTrackListTableModel(
\r
2058 SequenceListTableModel sequenceListTableModel,
\r
2059 Sequence sequence,
\r
2062 this.sequenceListTableModel = sequenceListTableModel;
\r
2063 setSequence(sequence);
\r
2064 setFilename(filename);
\r
2067 public int getRowCount() {
\r
2068 return sequence == null ? 0 : sequence.getTracks().length;
\r
2071 public int getColumnCount() {
\r
2072 return Column.values().length;
\r
2079 public String getColumnName(int column) {
\r
2080 return Column.values()[column].title;
\r
2084 * @return 指定された列の型
\r
2087 public Class<?> getColumnClass(int column) {
\r
2088 Column c = Column.values()[column];
\r
2091 case SOLO: if( ! isOnSequencer() ) return String.class;
\r
2093 default: return c.columnClass;
\r
2097 public Object getValueAt(int row, int column) {
\r
2098 Column c = Column.values()[column];
\r
2100 case TRACK_NUMBER: return row;
\r
2101 case EVENTS: return sequence.getTracks()[row].size();
\r
2103 return isOnSequencer() ? sequenceListTableModel.sequencerModel.getSequencer().getTrackMute(row) : "";
\r
2105 return isOnSequencer() ? sequenceListTableModel.sequencerModel.getSequencer().getTrackSolo(row) : "";
\r
2106 case RECORD_CHANNEL:
\r
2107 return isOnSequencer() ? trackModelList.get(row).getRecordingChannel() : "";
\r
2109 int ch = trackModelList.get(row).getChannel();
\r
2110 return ch < 0 ? "" : ch + 1 ;
\r
2112 case TRACK_NAME: return trackModelList.get(row).toString();
\r
2113 default: return "";
\r
2117 * セルが編集可能かどうかを返します。
\r
2120 public boolean isCellEditable(int row, int column) {
\r
2121 Column c = Column.values()[column];
\r
2125 case RECORD_CHANNEL: return isOnSequencer();
\r
2127 case TRACK_NAME: return true;
\r
2128 default: return false;
\r
2135 public void setValueAt(Object val, int row, int column) {
\r
2136 Column c = Column.values()[column];
\r
2139 sequenceListTableModel.sequencerModel.getSequencer().setTrackMute(row, ((Boolean)val).booleanValue());
\r
2142 sequenceListTableModel.sequencerModel.getSequencer().setTrackSolo(row, ((Boolean)val).booleanValue());
\r
2144 case RECORD_CHANNEL:
\r
2145 trackModelList.get(row).setRecordingChannel((String)val);
\r
2150 ch = new Integer((String)val);
\r
2152 catch( NumberFormatException e ) {
\r
2156 if( --ch <= 0 || ch > MIDISpec.MAX_CHANNELS )
\r
2158 TrackEventListTableModel trackTableModel = trackModelList.get(row);
\r
2159 if( ch == trackTableModel.getChannel() ) break;
\r
2160 trackTableModel.setChannel(ch);
\r
2161 setModified(true);
\r
2162 fireTableCellUpdated(row, Column.EVENTS.ordinal());
\r
2166 trackModelList.get(row).setString((String)val);
\r
2171 fireTableCellUpdated(row,column);
\r
2175 * @return MIDIシーケンス
\r
2177 public Sequence getSequence() { return sequence; }
\r
2179 * シーケンスtickインデックスを返します。
\r
2180 * @return シーケンスtickインデックス
\r
2182 public SequenceTickIndex getSequenceTickIndex() {
\r
2183 return sequenceTickIndex;
\r
2186 * MIDIシーケンスを設定します。
\r
2187 * @param sequence MIDIシーケンス(nullを指定するとトラックリストが空になる)
\r
2189 private void setSequence(Sequence sequence) {
\r
2191 sequenceListTableModel.sequencerModel.getSequencer().recordDisable(null); // The "null" means all tracks
\r
2193 int oldSize = trackModelList.size();
\r
2194 if( oldSize > 0 ) {
\r
2195 trackModelList.clear();
\r
2196 fireTableRowsDeleted(0, oldSize-1);
\r
2198 if( (this.sequence = sequence) == null ) {
\r
2199 sequenceTickIndex = null;
\r
2202 // 新しいシーケンスからtickインデックスとトラックリストを再構築
\r
2203 fireTimeSignatureChanged();
\r
2204 Track tracks[] = sequence.getTracks();
\r
2205 for(Track track : tracks) {
\r
2206 trackModelList.add(new TrackEventListTableModel(this, track));
\r
2208 fireTableRowsInserted(0, tracks.length-1);
\r
2211 * 拍子が変更されたとき、シーケンスtickインデックスを再作成します。
\r
2213 public void fireTimeSignatureChanged() {
\r
2214 sequenceTickIndex = new SequenceTickIndex(sequence);
\r
2216 private boolean isModified = false;
\r
2219 * @return 変更済みのときtrue
\r
2221 public boolean isModified() { return isModified; }
\r
2223 * 変更されたかどうかを設定します。
\r
2224 * @param isModified 変更されたときtrue
\r
2226 public void setModified(boolean isModified) { this.isModified = isModified; }
\r
2229 * @param filename ファイル名
\r
2231 public void setFilename(String filename) { this.filename = filename; }
\r
2236 public String getFilename() { return filename; }
\r
2238 public String toString() { return MIDISpec.getNameOf(sequence); }
\r
2241 * @param name シーケンス名
\r
2242 * @return 成功したらtrue
\r
2244 public boolean setName(String name) {
\r
2245 if( name.equals(toString()) || ! MIDISpec.setNameOf(sequence,name) )
\r
2247 setModified(true);
\r
2248 fireTableDataChanged();
\r
2252 * このシーケンスのMIDIデータのバイト列を返します。
\r
2253 * @return MIDIデータのバイト列(失敗した場合null)
\r
2255 public byte[] getMIDIdata() {
\r
2256 if( sequence == null || sequence.getTracks().length == 0 ) {
\r
2259 try( ByteArrayOutputStream out = new ByteArrayOutputStream() ) {
\r
2260 MidiSystem.write(sequence, 1, out);
\r
2261 return out.toByteArray();
\r
2262 } catch ( IOException e ) {
\r
2263 e.printStackTrace();
\r
2268 * 指定のトラックが変更されたことを通知します。
\r
2269 * @param track トラック
\r
2271 public void fireTrackChanged(Track track) {
\r
2272 int row = indexOf(track);
\r
2273 if( row < 0 ) return;
\r
2274 fireTableRowsUpdated(row, row);
\r
2275 sequenceListTableModel.fireSequenceModified(this);
\r
2278 * 選択されているトラックモデルを返します。
\r
2279 * @param index トラックのインデックス
\r
2280 * @return トラックモデル(見つからない場合null)
\r
2282 public TrackEventListTableModel getSelectedTrackModel() {
\r
2283 if( trackListSelectionModel.isSelectionEmpty() )
\r
2285 int index = trackListSelectionModel.getMinSelectionIndex();
\r
2286 Track tracks[] = sequence.getTracks();
\r
2287 if( tracks.length != 0 ) {
\r
2288 Track track = tracks[index];
\r
2289 for( TrackEventListTableModel model : trackModelList )
\r
2290 if( model.getTrack() == track )
\r
2296 * 指定のトラックがある位置のインデックスを返します。
\r
2297 * @param track トラック
\r
2298 * @return トラックのインデックス(先頭 0、トラックが見つからない場合 -1)
\r
2300 public int indexOf(Track track) {
\r
2301 Track tracks[] = sequence.getTracks();
\r
2302 for( int i=0; i<tracks.length; i++ )
\r
2303 if( tracks[i] == track )
\r
2308 * 新しいトラックを生成し、末尾に追加します。
\r
2309 * @return 追加したトラックのインデックス(先頭 0)
\r
2311 public int createTrack() {
\r
2312 Track newTrack = sequence.createTrack();
\r
2313 trackModelList.add(new TrackEventListTableModel(this, newTrack));
\r
2314 int lastRow = getRowCount() - 1;
\r
2315 fireTableRowsInserted(lastRow, lastRow);
\r
2316 sequenceListTableModel.fireSelectedSequenceChanged();
\r
2317 trackListSelectionModel.setSelectionInterval(lastRow, lastRow);
\r
2321 * 選択されているトラックを削除します。
\r
2323 public void deleteSelectedTracks() {
\r
2324 if( trackListSelectionModel.isSelectionEmpty() )
\r
2326 int minIndex = trackListSelectionModel.getMinSelectionIndex();
\r
2327 int maxIndex = trackListSelectionModel.getMaxSelectionIndex();
\r
2328 Track tracks[] = sequence.getTracks();
\r
2329 for( int i = maxIndex; i >= minIndex; i-- ) {
\r
2330 if( ! trackListSelectionModel.isSelectedIndex(i) )
\r
2332 sequence.deleteTrack(tracks[i]);
\r
2333 trackModelList.remove(i);
\r
2335 fireTableRowsDeleted(minIndex, maxIndex);
\r
2336 sequenceListTableModel.fireSelectedSequenceChanged();
\r
2339 * このシーケンスモデルのシーケンスをシーケンサーが操作しているか調べます。
\r
2340 * @return シーケンサーが操作していたらtrue
\r
2342 public boolean isOnSequencer() {
\r
2343 return sequence == sequenceListTableModel.sequencerModel.getSequencer().getSequence();
\r
2346 * 録音しようとしているチャンネルの設定されたトラックがあるか調べます。
\r
2347 * @return 該当トラックがあればtrue
\r
2349 public boolean hasRecordChannel() {
\r
2350 int rowCount = getRowCount();
\r
2351 for( int row=0; row < rowCount; row++ ) {
\r
2352 Object value = getValueAt(row, Column.RECORD_CHANNEL.ordinal());
\r
2353 if( ! "OFF".equals(value) ) return true;
\r
2360 * 選択されているイベントのインデックス
\r
2362 class EventListSelectionModel extends DefaultListSelectionModel {
\r
2364 setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
\r
2368 * MIDIトラック(MIDIイベントリスト)テーブルモデル
\r
2370 class TrackEventListTableModel extends AbstractTableModel {
\r
2374 public enum Column {
\r
2376 EVENT_NUMBER("No.", Integer.class, 20),
\r
2378 TICK_POSITION("TickPos.", Long.class, 40),
\r
2379 /** tick位置に対応する小節 */
\r
2380 MEASURE_POSITION("Measure", Integer.class, 40),
\r
2381 /** tick位置に対応する拍 */
\r
2382 BEAT_POSITION("Beat", Integer.class, 20),
\r
2383 /** tick位置に対応する余剰tick(拍に収まらずに余ったtick数) */
\r
2384 EXTRA_TICK_POSITION("ExTick", Integer.class, 20),
\r
2386 MESSAGE("MIDI Message", String.class, 200);
\r
2387 private String title;
\r
2388 private Class<?> columnClass;
\r
2389 int preferredWidth;
\r
2392 * @param title 列のタイトル
\r
2393 * @param widthRatio 幅の割合
\r
2394 * @param columnClass 列のクラス
\r
2395 * @param perferredWidth 列の適切な幅
\r
2397 private Column(String title, Class<?> columnClass, int preferredWidth) {
\r
2398 this.title = title;
\r
2399 this.columnClass = columnClass;
\r
2400 this.preferredWidth = preferredWidth;
\r
2404 * ラップされているMIDIトラック
\r
2406 private Track track;
\r
2410 SequenceTrackListTableModel sequenceTrackListTableModel;
\r
2412 * 選択されているイベントのインデックス
\r
2414 EventListSelectionModel eventSelectionModel = new EventListSelectionModel();
\r
2416 * シーケンスを親にして、その特定のトラックに連動する
\r
2417 * MIDIトラックモデルを構築します。
\r
2419 * @param parent 親のシーケンスモデル
\r
2420 * @param track ラップするMIDIトラック(ない場合はnull)
\r
2422 public TrackEventListTableModel(
\r
2423 SequenceTrackListTableModel sequenceTrackListTableModel, Track track
\r
2425 this.track = track;
\r
2426 this.sequenceTrackListTableModel = sequenceTrackListTableModel;
\r
2429 public int getRowCount() {
\r
2430 return track == null ? 0 : track.size();
\r
2433 public int getColumnCount() {
\r
2434 return Column.values().length;
\r
2440 public String getColumnName(int column) {
\r
2441 return Column.values()[column].title;
\r
2447 public Class<?> getColumnClass(int column) {
\r
2448 return Column.values()[column].columnClass;
\r
2451 public Object getValueAt(int row, int column) {
\r
2452 switch(Column.values()[column]) {
\r
2453 case EVENT_NUMBER: return row;
\r
2454 case TICK_POSITION: return track.get(row).getTick();
\r
2455 case MEASURE_POSITION:
\r
2456 return sequenceTrackListTableModel.getSequenceTickIndex().tickToMeasure(track.get(row).getTick()) + 1;
\r
2457 case BEAT_POSITION:
\r
2458 sequenceTrackListTableModel.getSequenceTickIndex().tickToMeasure(track.get(row).getTick());
\r
2459 return sequenceTrackListTableModel.getSequenceTickIndex().lastBeat + 1;
\r
2460 case EXTRA_TICK_POSITION:
\r
2461 sequenceTrackListTableModel.getSequenceTickIndex().tickToMeasure(track.get(row).getTick());
\r
2462 return sequenceTrackListTableModel.getSequenceTickIndex().lastExtraTick;
\r
2463 case MESSAGE: return msgToString(track.get(row).getMessage());
\r
2464 default: return "";
\r
2468 * セルを編集できるときtrue、編集できないときfalseを返します。
\r
2471 public boolean isCellEditable(int row, int column) {
\r
2472 switch(Column.values()[column]) {
\r
2473 case TICK_POSITION:
\r
2474 case MEASURE_POSITION:
\r
2475 case BEAT_POSITION:
\r
2476 case EXTRA_TICK_POSITION:
\r
2477 case MESSAGE: return true;
\r
2478 default: return false;
\r
2485 public void setValueAt(Object value, int row, int column) {
\r
2487 switch(Column.values()[column]) {
\r
2488 case TICK_POSITION: newTick = (Long)value; break;
\r
2489 case MEASURE_POSITION:
\r
2490 newTick = sequenceTrackListTableModel.getSequenceTickIndex().measureToTick(
\r
2491 (Integer)value - 1,
\r
2492 (Integer)getValueAt( row, Column.BEAT_POSITION.ordinal() ) - 1,
\r
2493 (Integer)getValueAt( row, Column.EXTRA_TICK_POSITION.ordinal() )
\r
2496 case BEAT_POSITION:
\r
2497 newTick = sequenceTrackListTableModel.getSequenceTickIndex().measureToTick(
\r
2498 (Integer)getValueAt( row, Column.MEASURE_POSITION.ordinal() ) - 1,
\r
2499 (Integer)value - 1,
\r
2500 (Integer)getValueAt( row, Column.EXTRA_TICK_POSITION.ordinal() )
\r
2503 case EXTRA_TICK_POSITION:
\r
2504 newTick = sequenceTrackListTableModel.getSequenceTickIndex().measureToTick(
\r
2505 (Integer)getValueAt( row, Column.MEASURE_POSITION.ordinal() ) - 1,
\r
2506 (Integer)getValueAt( row, Column.BEAT_POSITION.ordinal() ) - 1,
\r
2512 MidiEvent oldMidiEvent = track.get(row);
\r
2513 if( oldMidiEvent.getTick() == newTick ) {
\r
2516 MidiMessage msg = oldMidiEvent.getMessage();
\r
2517 MidiEvent newMidiEvent = new MidiEvent(msg,newTick);
\r
2518 track.remove(oldMidiEvent);
\r
2519 track.add(newMidiEvent);
\r
2520 fireTableDataChanged();
\r
2521 if( MIDISpec.isEOT(msg) ) {
\r
2522 // EOTの場所が変わると曲の長さが変わるので、親モデルへ通知する。
\r
2523 sequenceTrackListTableModel.sequenceListTableModel.fireSequenceModified(sequenceTrackListTableModel);
\r
2528 * @return MIDIトラック
\r
2530 public Track getTrack() { return track; }
\r
2535 public String toString() { return MIDISpec.getNameOf(track); }
\r
2538 * @param name トラック名
\r
2539 * @return 設定が行われたらtrue
\r
2541 public boolean setString(String name) {
\r
2542 if(name.equals(toString()) || ! MIDISpec.setNameOf(track, name))
\r
2544 sequenceTrackListTableModel.setModified(true);
\r
2545 sequenceTrackListTableModel.sequenceListTableModel.fireSequenceModified(sequenceTrackListTableModel);
\r
2546 fireTableDataChanged();
\r
2549 private String recordingChannel = "OFF";
\r
2551 * 録音中のMIDIチャンネルを返します。
\r
2552 * @return 録音中のMIDIチャンネル
\r
2554 public String getRecordingChannel() { return recordingChannel; }
\r
2556 * 録音中のMIDIチャンネルを設定します。
\r
2557 * @param recordingChannel 録音中のMIDIチャンネル
\r
2559 public void setRecordingChannel(String recordingChannel) {
\r
2560 Sequencer sequencer = sequenceTrackListTableModel.sequenceListTableModel.sequencerModel.getSequencer();
\r
2561 if( recordingChannel.equals("OFF") ) {
\r
2562 sequencer.recordDisable( track );
\r
2564 else if( recordingChannel.equals("ALL") ) {
\r
2565 sequencer.recordEnable( track, -1 );
\r
2569 int ch = Integer.decode(recordingChannel).intValue() - 1;
\r
2570 sequencer.recordEnable( track, ch );
\r
2571 } catch( NumberFormatException nfe ) {
\r
2572 sequencer.recordDisable( track );
\r
2573 this.recordingChannel = "OFF";
\r
2577 this.recordingChannel = recordingChannel;
\r
2580 * このトラックの対象MIDIチャンネルを返します。
\r
2581 * <p>全てのチャンネルメッセージが同じMIDIチャンネルの場合、
\r
2582 * そのMIDIチャンネルを返します。
\r
2583 * MIDIチャンネルの異なるチャンネルメッセージが一つでも含まれていた場合、
\r
2586 * @return 対象MIDIチャンネル(不統一の場合 -1)
\r
2588 public int getChannel() {
\r
2590 int trackSize = track.size();
\r
2591 for( int index=0; index < trackSize; index++ ) {
\r
2592 MidiMessage msg = track.get(index).getMessage();
\r
2593 if( ! (msg instanceof ShortMessage) )
\r
2595 ShortMessage smsg = (ShortMessage)msg;
\r
2596 if( ! MIDISpec.isChannelMessage(smsg) )
\r
2598 int ch = smsg.getChannel();
\r
2599 if( prevCh >= 0 && prevCh != ch ) {
\r
2607 * 指定されたMIDIチャンネルをすべてのチャンネルメッセージに対して設定します。
\r
2608 * @param channel MIDIチャンネル
\r
2610 public void setChannel(int channel) {
\r
2611 int track_size = track.size();
\r
2612 for( int index=0; index < track_size; index++ ) {
\r
2613 MidiMessage msg = track.get(index).getMessage();
\r
2614 if( ! (msg instanceof ShortMessage) )
\r
2616 ShortMessage smsg = (ShortMessage)msg;
\r
2617 if( ! MIDISpec.isChannelMessage(smsg) )
\r
2619 if( smsg.getChannel() == channel )
\r
2623 smsg.getCommand(), channel,
\r
2624 smsg.getData1(), smsg.getData2()
\r
2627 catch( InvalidMidiDataException e ) {
\r
2628 e.printStackTrace();
\r
2630 sequenceTrackListTableModel.setModified(true);
\r
2632 sequenceTrackListTableModel.fireTrackChanged(track);
\r
2633 fireTableDataChanged();
\r
2636 * 指定の MIDI tick 位置にあるイベントを二分探索し、
\r
2637 * そのイベントの行インデックスを返します。
\r
2638 * @param tick MIDI tick
\r
2641 public int tickToIndex(long tick) {
\r
2642 if( track == null )
\r
2645 int maxIndex = track.size() - 1;
\r
2646 while( minIndex < maxIndex ) {
\r
2647 int currentIndex = (minIndex + maxIndex) / 2 ;
\r
2648 long currentTick = track.get(currentIndex).getTick();
\r
2649 if( tick > currentTick ) {
\r
2650 minIndex = currentIndex + 1;
\r
2652 else if( tick < currentTick ) {
\r
2653 maxIndex = currentIndex - 1;
\r
2656 return currentIndex;
\r
2659 return (minIndex + maxIndex) / 2;
\r
2662 * NoteOn/NoteOff ペアの一方の行インデックスから、
\r
2663 * もう一方(ペアの相手)の行インデックスを返します。
\r
2664 * @param index 行インデックス
\r
2665 * @return ペアを構成する相手の行インデックス(ない場合は -1)
\r
2667 public int getIndexOfPartnerFor(int index) {
\r
2668 if( track == null || index >= track.size() )
\r
2670 MidiMessage msg = track.get(index).getMessage();
\r
2671 if( ! (msg instanceof ShortMessage) ) return -1;
\r
2672 ShortMessage sm = (ShortMessage)msg;
\r
2673 int cmd = sm.getCommand();
\r
2675 int ch = sm.getChannel();
\r
2676 int note = sm.getData1();
\r
2677 MidiMessage partner_msg;
\r
2678 ShortMessage partner_sm;
\r
2682 case 0x90: // NoteOn
\r
2683 if( sm.getData2() > 0 ) {
\r
2684 // Search NoteOff event forward
\r
2685 for( i = index + 1; i < track.size(); i++ ) {
\r
2686 partner_msg = track.get(i).getMessage();
\r
2687 if( ! (partner_msg instanceof ShortMessage ) ) continue;
\r
2688 partner_sm = (ShortMessage)partner_msg;
\r
2689 partner_cmd = partner_sm.getCommand();
\r
2690 if( partner_cmd != 0x80 && partner_cmd != 0x90 ||
\r
2691 partner_cmd == 0x90 && partner_sm.getData2() > 0
\r
2696 if( ch != partner_sm.getChannel() || note != partner_sm.getData1() ) {
\r
2704 // When velocity is 0, it means Note Off, so no break.
\r
2705 case 0x80: // NoteOff
\r
2706 // Search NoteOn event backward
\r
2707 for( i = index - 1; i >= 0; i-- ) {
\r
2708 partner_msg = track.get(i).getMessage();
\r
2709 if( ! (partner_msg instanceof ShortMessage ) ) continue;
\r
2710 partner_sm = (ShortMessage)partner_msg;
\r
2711 partner_cmd = partner_sm.getCommand();
\r
2712 if( partner_cmd != 0x90 || partner_sm.getData2() <= 0 ) {
\r
2716 if( ch != partner_sm.getChannel() || note != partner_sm.getData1() ) {
\r
2728 * ノートメッセージかどうか調べます。
\r
2729 * @param index 行インデックス
\r
2730 * @return Note On または Note Off のとき true
\r
2732 public boolean isNote(int index) {
\r
2733 MidiEvent midiEvent = getMidiEvent(index);
\r
2734 MidiMessage msg = midiEvent.getMessage();
\r
2735 if( ! (msg instanceof ShortMessage) ) return false;
\r
2736 int cmd = ((ShortMessage)msg).getCommand();
\r
2737 return cmd == ShortMessage.NOTE_ON || cmd == ShortMessage.NOTE_OFF ;
\r
2740 * 指定の行インデックスのMIDIイベントを返します。
\r
2741 * @param index 行インデックス
\r
2742 * @return MIDIイベント
\r
2744 public MidiEvent getMidiEvent(int index) {
\r
2745 return track==null ? null : track.get(index);
\r
2748 * 選択されているMIDIイベントを返します。
\r
2749 * @return 選択されているMIDIイベント
\r
2751 public MidiEvent[] getSelectedMidiEvents() {
\r
2752 Vector<MidiEvent> events = new Vector<MidiEvent>();
\r
2753 if( ! eventSelectionModel.isSelectionEmpty() ) {
\r
2754 int i = eventSelectionModel.getMinSelectionIndex();
\r
2755 int max = eventSelectionModel.getMaxSelectionIndex();
\r
2756 for( ; i <= max; i++ )
\r
2757 if( eventSelectionModel.isSelectedIndex(i) )
\r
2758 events.add(track.get(i));
\r
2760 return events.toArray(new MidiEvent[1]);
\r
2764 * @param midiEvent 追加するMIDIイベント
\r
2765 * @return 追加できたらtrue
\r
2767 public boolean addMidiEvent(MidiEvent midiEvent) {
\r
2768 if( track == null || !(track.add(midiEvent)) )
\r
2770 if( MIDISpec.isTimeSignature(midiEvent.getMessage()) )
\r
2771 sequenceTrackListTableModel.fireTimeSignatureChanged();
\r
2772 sequenceTrackListTableModel.fireTrackChanged(track);
\r
2773 int lastIndex = track.size() - 1;
\r
2774 fireTableRowsInserted( lastIndex-1, lastIndex-1 );
\r
2779 * @param midiEvents 追加するMIDIイベント
\r
2780 * @param destinationTick 追加先tick
\r
2781 * @param sourcePPQ PPQ値(タイミング解像度)
\r
2782 * @return 追加できたらtrue
\r
2784 public boolean addMidiEvents(MidiEvent midiEvents[], long destinationTick, int sourcePPQ) {
\r
2785 if( track == null )
\r
2787 int destinationPPQ = sequenceTrackListTableModel.getSequence().getResolution();
\r
2788 boolean done = false;
\r
2789 boolean hasTimeSignature = false;
\r
2790 long firstSourceEventTick = -1;
\r
2791 for( MidiEvent sourceEvent : midiEvents ) {
\r
2792 long sourceEventTick = sourceEvent.getTick();
\r
2793 MidiMessage msg = sourceEvent.getMessage();
\r
2794 long newTick = destinationTick;
\r
2795 if( firstSourceEventTick < 0 ) {
\r
2796 firstSourceEventTick = sourceEventTick;
\r
2799 newTick += (sourceEventTick - firstSourceEventTick) * destinationPPQ / sourcePPQ;
\r
2801 if( ! track.add(new MidiEvent(msg, newTick)) ) continue;
\r
2803 if( MIDISpec.isTimeSignature(msg) ) hasTimeSignature = true;
\r
2806 if( hasTimeSignature ) sequenceTrackListTableModel.fireTimeSignatureChanged();
\r
2807 sequenceTrackListTableModel.fireTrackChanged(track);
\r
2808 int lastIndex = track.size() - 1;
\r
2809 int oldLastIndex = lastIndex - midiEvents.length;
\r
2810 fireTableRowsInserted(oldLastIndex, lastIndex);
\r
2816 * 曲の長さが変わることがあるので、プレイリストにも通知します。
\r
2817 * @param midiEvents 除去するMIDIイベント
\r
2819 public void removeMidiEvents(MidiEvent midiEvents[]) {
\r
2820 if( track == null )
\r
2822 boolean hadTimeSignature = false;
\r
2823 for( MidiEvent e : midiEvents ) {
\r
2824 if( MIDISpec.isTimeSignature(e.getMessage()) )
\r
2825 hadTimeSignature = true;
\r
2828 if( hadTimeSignature ) {
\r
2829 sequenceTrackListTableModel.fireTimeSignatureChanged();
\r
2831 sequenceTrackListTableModel.fireTrackChanged(track);
\r
2832 int lastIndex = track.size() - 1;
\r
2833 int oldLastIndex = lastIndex + midiEvents.length;
\r
2834 if(lastIndex < 0) lastIndex = 0;
\r
2835 fireTableRowsDeleted(oldLastIndex, lastIndex);
\r
2836 sequenceTrackListTableModel.sequenceListTableModel.fireSelectedSequenceChanged();
\r
2839 * 引数の選択内容が示すMIDIイベントを除去します。
\r
2840 * @param selectionModel 選択内容
\r
2842 public void removeSelectedMidiEvents() {
\r
2843 removeMidiEvents(getSelectedMidiEvents());
\r
2845 private static boolean isRhythmPart(int ch) { return (ch == 9); }
\r
2847 * MIDIメッセージの内容を文字列で返します。
\r
2848 * @param msg MIDIメッセージ
\r
2849 * @return MIDIメッセージの内容を表す文字列
\r
2851 public static String msgToString(MidiMessage msg) {
\r
2853 if( msg instanceof ShortMessage ) {
\r
2854 ShortMessage shortmsg = (ShortMessage)msg;
\r
2855 int status = msg.getStatus();
\r
2856 String statusName = MIDISpec.getStatusName(status);
\r
2857 int data1 = shortmsg.getData1();
\r
2858 int data2 = shortmsg.getData2();
\r
2859 if( MIDISpec.isChannelMessage(status) ) {
\r
2860 int channel = shortmsg.getChannel();
\r
2861 String channelPrefix = "Ch."+(channel+1) + ": ";
\r
2862 String statusPrefix = (
\r
2863 statusName == null ? String.format("status=0x%02X",status) : statusName
\r
2865 int cmd = shortmsg.getCommand();
\r
2867 case ShortMessage.NOTE_OFF:
\r
2868 case ShortMessage.NOTE_ON:
\r
2869 str += channelPrefix + statusPrefix + data1;
\r
2871 if( isRhythmPart(channel) ) {
\r
2872 str += MIDISpec.getPercussionName(data1);
\r
2875 str += Music.NoteSymbol.noteNoToSymbol(data1);
\r
2877 str +="] Velocity=" + data2;
\r
2879 case ShortMessage.POLY_PRESSURE:
\r
2880 str += channelPrefix + statusPrefix + "Note=" + data1 + " Pressure=" + data2;
\r
2882 case ShortMessage.PROGRAM_CHANGE:
\r
2883 str += channelPrefix + statusPrefix + data1 + ":[" + MIDISpec.instrument_names[data1] + "]";
\r
2884 if( data2 != 0 ) str += " data2=" + data2;
\r
2886 case ShortMessage.CHANNEL_PRESSURE:
\r
2887 str += channelPrefix + statusPrefix + data1;
\r
2888 if( data2 != 0 ) str += " data2=" + data2;
\r
2890 case ShortMessage.PITCH_BEND:
\r
2892 int val = ((data1 & 0x7F) | ((data2 & 0x7F) << 7));
\r
2893 str += channelPrefix + statusPrefix + ( (val-8192) * 100 / 8191) + "% (" + val + ")";
\r
2896 case ShortMessage.CONTROL_CHANGE:
\r
2898 // Control / Mode message name
\r
2899 String ctrl_name = MIDISpec.getControllerName(data1);
\r
2900 str += channelPrefix + (data1 < 0x78 ? "CtrlChg: " : "ModeMsg: ");
\r
2901 if( ctrl_name == null ) {
\r
2902 str += " No.=" + data1 + " Value=" + data2;
\r
2907 // Controller's value
\r
2909 case 0x40: case 0x41: case 0x42: case 0x43: case 0x45:
\r
2910 str += " " + ( data2==0x3F?"OFF":data2==0x40?"ON":data2 );
\r
2912 case 0x44: // Legato Footswitch
\r
2913 str += " " + ( data2==0x3F?"Normal":data2==0x40?"Legato":data2 );
\r
2915 case 0x7A: // Local Control
\r
2916 str += " " + ( data2==0x00?"OFF":data2==0x7F?"ON":data2 );
\r
2919 str += " " + data2;
\r
2926 // Never reached here
\r
2930 else { // System Message
\r
2931 str += (statusName == null ? ("status="+status) : statusName );
\r
2932 str += " (" + data1 + "," + data2 + ")";
\r
2936 else if( msg instanceof MetaMessage ) {
\r
2937 MetaMessage metamsg = (MetaMessage)msg;
\r
2938 byte[] msgdata = metamsg.getData();
\r
2939 int msgtype = metamsg.getType();
\r
2941 String meta_name = MIDISpec.getMetaName(msgtype);
\r
2942 if( meta_name == null ) {
\r
2943 str += "Unknown MessageType="+msgtype + " Values=(";
\r
2944 for( byte b : msgdata ) str += String.format( " %02X", b );
\r
2948 // Add the message type name
\r
2951 // Add the text data
\r
2952 if( MIDISpec.hasMetaText(msgtype) ) {
\r
2953 str +=" ["+(new String(msgdata))+"]";
\r
2956 // Add the numeric data
\r
2958 case 0x00: // Sequence Number (for MIDI Format 2)
\r
2959 if( msgdata.length == 2 ) {
\r
2960 str += String.format(
\r
2962 ((msgdata[0] & 0xFF) << 8) | (msgdata[1] & 0xFF)
\r
2966 str += ": Size not 2 byte : data=(";
\r
2967 for( byte b : msgdata ) str += String.format( " %02X", b );
\r
2970 case 0x20: // MIDI Ch.Prefix
\r
2971 case 0x21: // MIDI Output Port
\r
2972 if( msgdata.length == 1 ) {
\r
2973 str += String.format( ": %02X", msgdata[0] & 0xFF );
\r
2976 str += ": Size not 1 byte : data=(";
\r
2977 for( byte b : msgdata ) str += String.format( " %02X", b );
\r
2980 case 0x51: // Tempo
\r
2981 str += ": " + MIDISpec.byteArrayToQpmTempo( msgdata ) + "[QPM] (";
\r
2982 for( byte b : msgdata ) str += String.format( " %02X", b );
\r
2985 case 0x54: // SMPTE Offset
\r
2986 if( msgdata.length == 5 ) {
\r
2988 + (msgdata[0] & 0xFF) + ":"
\r
2989 + (msgdata[1] & 0xFF) + ":"
\r
2990 + (msgdata[2] & 0xFF) + "."
\r
2991 + (msgdata[3] & 0xFF) + "."
\r
2992 + (msgdata[4] & 0xFF);
\r
2995 str += ": Size not 5 byte : data=(";
\r
2996 for( byte b : msgdata ) str += String.format( " %02X", b );
\r
2999 case 0x58: // Time Signature
\r
3000 if( msgdata.length == 4 ) {
\r
3001 str +=": " + msgdata[0] + "/" + (1 << msgdata[1]);
\r
3002 str +=", "+msgdata[2]+"[clk/beat], "+msgdata[3]+"[32nds/24clk]";
\r
3005 str += ": Size not 4 byte : data=(";
\r
3006 for( byte b : msgdata ) str += String.format( " %02X", b );
\r
3009 case 0x59: // Key Signature
\r
3010 if( msgdata.length == 2 ) {
\r
3011 Music.Key key = new Music.Key(msgdata);
\r
3012 str += ": " + key.signatureDescription();
\r
3013 str += " (" + key.toStringIn(Music.SymbolLanguage.NAME) + ")";
\r
3016 str += ": Size not 2 byte : data=(";
\r
3017 for( byte b : msgdata ) str += String.format( " %02X", b );
\r
3020 case 0x7F: // Sequencer Specific Meta Event
\r
3022 for( byte b : msgdata ) str += String.format( " %02X", b );
\r
3028 else if( msg instanceof SysexMessage ) {
\r
3029 SysexMessage sysexmsg = (SysexMessage)msg;
\r
3030 int status = sysexmsg.getStatus();
\r
3031 byte[] msgdata = sysexmsg.getData();
\r
3032 int data_byte_pos = 1;
\r
3033 switch( status ) {
\r
3034 case SysexMessage.SYSTEM_EXCLUSIVE:
\r
3037 case SysexMessage.SPECIAL_SYSTEM_EXCLUSIVE:
\r
3038 str += "SysEx(Special): ";
\r
3041 str += "SysEx: Invalid (status="+status+") ";
\r
3044 if( msgdata.length < 1 ) {
\r
3045 str += " Invalid data size: " + msgdata.length;
\r
3048 int manufacturer_id = (int)(msgdata[0] & 0xFF );
\r
3049 int device_id = (int)(msgdata[1] & 0xFF);
\r
3050 int model_id = (int)(msgdata[2] & 0xFF);
\r
3051 String manufacturer_name = MIDISpec.getSysExManufacturerName(manufacturer_id);
\r
3052 if( manufacturer_name == null ) {
\r
3053 manufacturer_name = String.format( "[Manufacturer code %02X]", msgdata[0] );
\r
3055 str += manufacturer_name + String.format( " (DevID=0x%02X)", device_id );
\r
3056 switch( manufacturer_id ) {
\r
3057 case 0x7E: // Non-Realtime Universal
\r
3059 int sub_id_1 = (int)(msgdata[2] & 0xFF);
\r
3060 int sub_id_2 = (int)(msgdata[3] & 0xFF);
\r
3061 switch( sub_id_1 ) {
\r
3062 case 0x09: // General MIDI (GM)
\r
3063 switch( sub_id_2 ) {
\r
3064 case 0x01: str += " GM System ON"; return str;
\r
3065 case 0x02: str += " GM System OFF"; return str;
\r
3072 // case 0x7F: // Realtime Universal
\r
3073 case 0x41: // Roland
\r
3075 switch( model_id ) {
\r
3077 str += " [GS]"; data_byte_pos++;
\r
3078 if( msgdata[3]==0x12 ) {
\r
3079 str += "DT1:"; data_byte_pos++;
\r
3080 switch( msgdata[4] ) {
\r
3082 if( msgdata[5]==0x00 ) {
\r
3083 if( msgdata[6]==0x7F ) {
\r
3084 if( msgdata[7]==0x00 ) {
\r
3085 str += " [88] System Mode Set (Mode 1: Single Module)"; return str;
\r
3087 else if( msgdata[7]==0x01 ) {
\r
3088 str += " [88] System Mode Set (Mode 2: Double Module)"; return str;
\r
3092 else if( msgdata[5]==0x01 ) {
\r
3093 int port = (msgdata[7] & 0xFF);
\r
3094 str += String.format(
\r
3095 " [88] Ch.Msg Rx Port: Block=0x%02X, Port=%s",
\r
3097 port==0?"A":port==1?"B":String.format("0x%02X",port)
\r
3103 if( msgdata[5]==0x00 ) {
\r
3104 switch( msgdata[6] ) {
\r
3105 case 0x00: str += " Master Tune: "; data_byte_pos += 3; break;
\r
3106 case 0x04: str += " Master Volume: "; data_byte_pos += 3; break;
\r
3107 case 0x05: str += " Master Key Shift: "; data_byte_pos += 3; break;
\r
3108 case 0x06: str += " Master Pan: "; data_byte_pos += 3; break;
\r
3110 switch( msgdata[7] ) {
\r
3111 case 0x00: str += " GS Reset"; return str;
\r
3112 case 0x7F: str += " Exit GS Mode"; return str;
\r
3117 else if( msgdata[5]==0x01 ) {
\r
3118 switch( msgdata[6] ) {
\r
3119 // case 0x00: str += ""; break;
\r
3120 // case 0x10: str += ""; break;
\r
3121 case 0x30: str += " Reverb Macro: "; data_byte_pos += 3; break;
\r
3122 case 0x31: str += " Reverb Character: "; data_byte_pos += 3; break;
\r
3123 case 0x32: str += " Reverb Pre-LPF: "; data_byte_pos += 3; break;
\r
3124 case 0x33: str += " Reverb Level: "; data_byte_pos += 3; break;
\r
3125 case 0x34: str += " Reverb Time: "; data_byte_pos += 3; break;
\r
3126 case 0x35: str += " Reverb Delay FB: "; data_byte_pos += 3; break;
\r
3127 case 0x36: str += " Reverb Chorus Level: "; data_byte_pos += 3; break;
\r
3128 case 0x37: str += " [88] Reverb Predelay Time: "; data_byte_pos += 3; break;
\r
3129 case 0x38: str += " Chorus Macro: "; data_byte_pos += 3; break;
\r
3130 case 0x39: str += " Chorus Pre-LPF: "; data_byte_pos += 3; break;
\r
3131 case 0x3A: str += " Chorus Level: "; data_byte_pos += 3; break;
\r
3132 case 0x3B: str += " Chorus FB: "; data_byte_pos += 3; break;
\r
3133 case 0x3C: str += " Chorus Delay: "; data_byte_pos += 3; break;
\r
3134 case 0x3D: str += " Chorus Rate: "; data_byte_pos += 3; break;
\r
3135 case 0x3E: str += " Chorus Depth: "; data_byte_pos += 3; break;
\r
3136 case 0x3F: str += " Chorus Send Level To Reverb: "; data_byte_pos += 3; break;
\r
3137 case 0x40: str += " [88] Chorus Send Level To Delay: "; data_byte_pos += 3; break;
\r
3138 case 0x50: str += " [88] Delay Macro: "; data_byte_pos += 3; break;
\r
3139 case 0x51: str += " [88] Delay Pre-LPF: "; data_byte_pos += 3; break;
\r
3140 case 0x52: str += " [88] Delay Time Center: "; data_byte_pos += 3; break;
\r
3141 case 0x53: str += " [88] Delay Time Ratio Left: "; data_byte_pos += 3; break;
\r
3142 case 0x54: str += " [88] Delay Time Ratio Right: "; data_byte_pos += 3; break;
\r
3143 case 0x55: str += " [88] Delay Level Center: "; data_byte_pos += 3; break;
\r
3144 case 0x56: str += " [88] Delay Level Left: "; data_byte_pos += 3; break;
\r
3145 case 0x57: str += " [88] Delay Level Right: "; data_byte_pos += 3; break;
\r
3146 case 0x58: str += " [88] Delay Level: "; data_byte_pos += 3; break;
\r
3147 case 0x59: str += " [88] Delay FB: "; data_byte_pos += 3; break;
\r
3148 case 0x5A: str += " [88] Delay Send Level To Reverb: "; data_byte_pos += 3; break;
\r
3151 else if( msgdata[5]==0x02 ) {
\r
3152 switch( msgdata[6] ) {
\r
3153 case 0x00: str += " [88] EQ Low Freq: "; data_byte_pos += 3; break;
\r
3154 case 0x01: str += " [88] EQ Low Gain: "; data_byte_pos += 3; break;
\r
3155 case 0x02: str += " [88] EQ High Freq: "; data_byte_pos += 3; break;
\r
3156 case 0x03: str += " [88] EQ High Gain: "; data_byte_pos += 3; break;
\r
3159 else if( msgdata[5]==0x03 ) {
\r
3160 if( msgdata[6] == 0x00 ) {
\r
3161 str += " [Pro] EFX Type: "; data_byte_pos += 3;
\r
3163 else if( msgdata[6] >= 0x03 && msgdata[6] <= 0x16 ) {
\r
3164 str += String.format(" [Pro] EFX Param %d", msgdata[6]-2 );
\r
3165 data_byte_pos += 3;
\r
3167 else if( msgdata[6] == 0x17 ) {
\r
3168 str += " [Pro] EFX Send Level To Reverb: "; data_byte_pos += 3;
\r
3170 else if( msgdata[6] == 0x18 ) {
\r
3171 str += " [Pro] EFX Send Level To Chorus: "; data_byte_pos += 3;
\r
3173 else if( msgdata[6] == 0x19 ) {
\r
3174 str += " [Pro] EFX Send Level To Delay: "; data_byte_pos += 3;
\r
3176 else if( msgdata[6] == 0x1B ) {
\r
3177 str += " [Pro] EFX Ctrl Src1: "; data_byte_pos += 3;
\r
3179 else if( msgdata[6] == 0x1C ) {
\r
3180 str += " [Pro] EFX Ctrl Depth1: "; data_byte_pos += 3;
\r
3182 else if( msgdata[6] == 0x1D ) {
\r
3183 str += " [Pro] EFX Ctrl Src2: "; data_byte_pos += 3;
\r
3185 else if( msgdata[6] == 0x1E ) {
\r
3186 str += " [Pro] EFX Ctrl Depth2: "; data_byte_pos += 3;
\r
3188 else if( msgdata[6] == 0x1F ) {
\r
3189 str += " [Pro] EFX Send EQ Switch: "; data_byte_pos += 3;
\r
3192 else if( (msgdata[5] & 0xF0) == 0x10 ) {
\r
3193 int ch = (msgdata[5] & 0x0F);
\r
3194 if( ch <= 9 ) ch--; else if( ch == 0 ) ch = 9;
\r
3195 if( msgdata[6]==0x02 ) {
\r
3196 str += String.format(
\r
3197 " Rx Ch: Part=%d(0x%02X) Ch=0x%02X", (ch+1), msgdata[5], msgdata[7]
\r
3201 else if( msgdata[6]==0x15 ) {
\r
3203 switch( msgdata[7] ) {
\r
3204 case 0: map = " NormalPart"; break;
\r
3205 case 1: map = " DrumMap1"; break;
\r
3206 case 2: map = " DrumMap2"; break;
\r
3207 default: map = String.format("0x%02X",msgdata[7]); break;
\r
3209 str += String.format(
\r
3210 " Rhythm Part: Ch=%d(0x%02X) Map=%s",
\r
3211 (ch+1), msgdata[5],
\r
3217 else if( (msgdata[5] & 0xF0) == 0x40 ) {
\r
3218 int ch = (msgdata[5] & 0x0F);
\r
3219 if( ch <= 9 ) ch--; else if( ch == 0 ) ch = 9;
\r
3220 int dt = (msgdata[7] & 0xFF);
\r
3221 if( msgdata[6]==0x20 ) {
\r
3222 str += String.format(
\r
3223 " [88] EQ: Ch=%d(0x%02X) %s",
\r
3224 (ch+1), msgdata[5],
\r
3225 dt==0 ? "OFF" : dt==1 ? "ON" : String.format("0x%02X",dt)
\r
3228 else if( msgdata[6]==0x22 ) {
\r
3229 str += String.format(
\r
3230 " [Pro] Part EFX Assign: Ch=%d(0x%02X) %s",
\r
3231 (ch+1), msgdata[5],
\r
3232 dt==0 ? "ByPass" : dt==1 ? "EFX" : String.format("0x%02X",dt)
\r
3241 str += " [GS-LCD]"; data_byte_pos++;
\r
3242 if( msgdata[3]==0x12 ) {
\r
3243 str += " [DT1]"; data_byte_pos++;
\r
3244 if( msgdata[4]==0x10 && msgdata[5]==0x00 && msgdata[6]==0x00 ) {
\r
3245 data_byte_pos += 3;
\r
3246 str += " Disp [" +(new String(
\r
3247 msgdata, data_byte_pos, msgdata.length - data_byte_pos - 2
\r
3252 case 0x14: str += " [D-50]"; data_byte_pos++; break;
\r
3253 case 0x16: str += " [MT-32]"; data_byte_pos++; break;
\r
3256 case 0x43: // Yamaha (XG)
\r
3258 if( model_id == 0x4C ) {
\r
3260 if( msgdata[3]==0 && msgdata[4]==0 && msgdata[5]==0x7E && msgdata[6]==0 ) {
\r
3261 str += " XG System ON"; return str;
\r
3271 for( i = data_byte_pos; i<msgdata.length-1; i++ ) {
\r
3272 str += String.format( " %02X", msgdata[i] );
\r
3274 if( i < msgdata.length && (int)(msgdata[i] & 0xFF) != 0xF7 ) {
\r
3275 str+=" [ Invalid EOX " + String.format( "%02X", msgdata[i] ) + " ]";
\r
3280 byte[] msg_data = msg.getMessage();
\r
3282 for( byte b : msg_data ) {
\r
3283 str += String.format( " %02X", b );
\r
3291 * MIDI シーケンスデータのtickインデックス
\r
3292 * <p>拍子、テンポ、調だけを抜き出したトラックを保持するためのインデックスです。
\r
3293 * 指定の MIDI tick の位置におけるテンポ、調、拍子を取得したり、
\r
3294 * 拍子情報から MIDI tick と小節位置との間の変換を行うために使います。
\r
3297 class SequenceTickIndex {
\r
3301 public static final int TEMPO = 0;
\r
3305 public static final int TIME_SIGNATURE = 1;
\r
3309 public static final int KEY_SIGNATURE = 2;
\r
3311 * メタメッセージタイプ → メタメッセージの種類 変換マップ
\r
3313 private static final Map<Integer,Integer> INDEX_META_TO_TRACK =
\r
3314 new HashMap<Integer,Integer>() {
\r
3317 put(0x58, TIME_SIGNATURE);
\r
3318 put(0x59, KEY_SIGNATURE);
\r
3322 * 新しいMIDIシーケンスデータのインデックスを構築します。
\r
3323 * @param sourceSequence 元のMIDIシーケンス
\r
3325 public SequenceTickIndex(Sequence sourceSequence) {
\r
3327 int ppq = sourceSequence.getResolution();
\r
3328 wholeNoteTickLength = ppq * 4;
\r
3329 tmpSequence = new Sequence(Sequence.PPQ, ppq, 3);
\r
3330 tracks = tmpSequence.getTracks();
\r
3331 Track[] sourceTracks = sourceSequence.getTracks();
\r
3332 for( Track tk : sourceTracks ) {
\r
3333 for( int i_evt = 0 ; i_evt < tk.size(); i_evt++ ) {
\r
3334 MidiEvent evt = tk.get(i_evt);
\r
3335 MidiMessage msg = evt.getMessage();
\r
3336 if( ! (msg instanceof MetaMessage) )
\r
3338 MetaMessage metaMsg = (MetaMessage)msg;
\r
3339 int metaType = metaMsg.getType();
\r
3340 Integer metaIndex = INDEX_META_TO_TRACK.get(metaType);
\r
3341 if( metaIndex != null ) tracks[metaIndex].add(evt);
\r
3345 catch ( InvalidMidiDataException e ) {
\r
3346 e.printStackTrace();
\r
3349 private Sequence tmpSequence;
\r
3351 * このtickインデックスのタイミング解像度を返します。
\r
3352 * @return このtickインデックスのタイミング解像度
\r
3354 public int getResolution() {
\r
3355 return tmpSequence.getResolution();
\r
3357 private Track[] tracks;
\r
3359 * 指定されたtick位置以前の最後のメタメッセージを返します。
\r
3360 * @param trackIndex メタメッセージの種類()
\r
3361 * @param tickPosition
\r
3364 public MetaMessage lastMetaMessageAt(int trackIndex, long tickPosition) {
\r
3365 Track track = tracks[trackIndex];
\r
3366 for(int eventIndex = track.size()-1 ; eventIndex >= 0; eventIndex--) {
\r
3367 MidiEvent event = track.get(eventIndex);
\r
3368 if( event.getTick() > tickPosition )
\r
3370 MetaMessage metaMessage = (MetaMessage)(event.getMessage());
\r
3371 if( metaMessage.getType() == 0x2F /* skip EOT (last event) */ )
\r
3373 return metaMessage;
\r
3378 private int wholeNoteTickLength;
\r
3379 public int lastBeat;
\r
3380 public int lastExtraTick;
\r
3381 public byte timesigUpper;
\r
3382 public byte timesigLowerIndex;
\r
3384 * tick位置を小節位置に変換します。
\r
3385 * @param tickPosition tick位置
\r
3388 int tickToMeasure(long tickPosition) {
\r
3389 byte extraBeats = 0;
\r
3390 MidiEvent event = null;
\r
3391 MidiMessage message = null;
\r
3392 byte[] data = null;
\r
3393 long currentTick = 0L;
\r
3394 long nextTimesigTick = 0L;
\r
3395 long prevTick = 0L;
\r
3396 long duration = 0L;
\r
3397 int lastMeasure = 0;
\r
3398 int eventIndex = 0;
\r
3400 timesigLowerIndex = 2; // =log2(4)
\r
3401 if( tracks[TIME_SIGNATURE] != null ) {
\r
3403 // Check current time-signature event
\r
3404 if( eventIndex < tracks[TIME_SIGNATURE].size() ) {
\r
3405 message = (event = tracks[TIME_SIGNATURE].get(eventIndex)).getMessage();
\r
3406 currentTick = nextTimesigTick = event.getTick();
\r
3407 if(currentTick > tickPosition || (message.getStatus() == 0xFF && ((MetaMessage)message).getType() == 0x2F /* EOT */)) {
\r
3408 currentTick = tickPosition;
\r
3411 else { // No event
\r
3412 currentTick = nextTimesigTick = tickPosition;
\r
3414 // Add measure from last event
\r
3416 int beatTickLength = wholeNoteTickLength >> timesigLowerIndex;
\r
3417 duration = currentTick - prevTick;
\r
3418 int beats = (int)( duration / beatTickLength );
\r
3419 lastExtraTick = (int)(duration % beatTickLength);
\r
3420 int measures = beats / timesigUpper;
\r
3421 extraBeats = (byte)(beats % timesigUpper);
\r
3422 lastMeasure += measures;
\r
3423 if( nextTimesigTick > tickPosition ) break; // Not reached to next time signature
\r
3425 // Reached to the next time signature, so get it.
\r
3426 if( ( data = ((MetaMessage)message).getData() ).length > 0 ) { // To skip EOT, check the data length.
\r
3427 timesigUpper = data[0];
\r
3428 timesigLowerIndex = data[1];
\r
3430 if( currentTick == tickPosition ) break; // Calculation complete
\r
3432 // Calculation incomplete, so prepare for next
\r
3434 if( extraBeats > 0 ) {
\r
3436 // Extra beats are treated as 1 measure
\r
3439 prevTick = currentTick;
\r
3443 lastBeat = extraBeats;
\r
3444 return lastMeasure;
\r
3447 * 小節位置を MIDI tick に変換します。
\r
3448 * @param measure 小節位置
\r
3449 * @return MIDI tick
\r
3451 public long measureToTick(int measure) {
\r
3452 return measureToTick(measure, 0, 0);
\r
3455 * 指定の小節位置、拍、拍内tickを、そのシーケンス全体の MIDI tick に変換します。
\r
3456 * @param measure 小節位置
\r
3458 * @param extraTick 拍内tick
\r
3459 * @return そのシーケンス全体の MIDI tick
\r
3461 public long measureToTick(int measure, int beat, int extraTick) {
\r
3462 MidiEvent evt = null;
\r
3463 MidiMessage msg = null;
\r
3464 byte[] data = null;
\r
3466 long prev_tick = 0L;
\r
3467 long duration = 0L;
\r
3468 long duration_sum = 0L;
\r
3469 long estimated_ticks;
\r
3470 int ticks_per_beat;
\r
3473 timesigLowerIndex = 2; // =log2(4)
\r
3475 ticks_per_beat = wholeNoteTickLength >> timesigLowerIndex;
\r
3476 estimated_ticks = ((measure * timesigUpper) + beat) * ticks_per_beat + extraTick;
\r
3477 if( tracks[TIME_SIGNATURE] == null || i_evt > tracks[TIME_SIGNATURE].size() ) {
\r
3478 return duration_sum + estimated_ticks;
\r
3480 msg = (evt = tracks[TIME_SIGNATURE].get(i_evt)).getMessage();
\r
3481 if( msg.getStatus() == 0xFF && ((MetaMessage)msg).getType() == 0x2F /* EOT */ ) {
\r
3482 return duration_sum + estimated_ticks;
\r
3484 duration = (tick = evt.getTick()) - prev_tick;
\r
3485 if( duration >= estimated_ticks ) {
\r
3486 return duration_sum + estimated_ticks;
\r
3488 // Re-calculate measure (ignore extra beats/ticks)
\r
3489 measure -= ( duration / (ticks_per_beat * timesigUpper) );
\r
3490 duration_sum += duration;
\r
3492 // Get next time-signature
\r
3493 data = ( (MetaMessage)msg ).getData();
\r
3494 timesigUpper = data[0];
\r
3495 timesigLowerIndex = data[1];
\r