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.FileNameExtensionFilter;
\r
80 import javax.swing.table.AbstractTableModel;
\r
81 import javax.swing.table.JTableHeader;
\r
82 import javax.swing.table.TableCellEditor;
\r
83 import javax.swing.table.TableCellRenderer;
\r
84 import javax.swing.table.TableColumn;
\r
85 import javax.swing.table.TableColumnModel;
\r
86 import javax.swing.table.TableModel;
\r
89 * MIDIエディタ(MIDI Editor/Playlist for MIDI Chord Helper)
\r
92 * Copyright (C) 2006-2013 Akiyoshi Kamide
\r
93 * http://www.yk.rim.or.jp/~kamide/music/chordhelper/
\r
95 class MidiEditor extends JDialog implements DropTargetListener {
\r
97 * このMIDIエディタの仮想MIDIデバイス
\r
99 VirtualMidiDevice virtualMidiDevice = new AbstractVirtualMidiDevice() {
\r
100 class MyInfo extends Info {
\r
101 protected MyInfo() {
\r
102 super("MIDI Editor","Unknown vendor","MIDI sequence editor","");
\r
105 // 送信のみなので MIDI IN はサポートしない
\r
106 { info = new MyInfo(); setMaxReceivers(0); }
\r
109 * このダイアログを表示するアクション
\r
111 public Action openAction = new AbstractAction(
\r
112 "Edit/Playlist/Speed", new ButtonIcon(ButtonIcon.EDIT_ICON)
\r
115 String tooltip = "MIDIシーケンスの編集/プレイリスト/再生速度調整";
\r
116 putValue(Action.SHORT_DESCRIPTION, tooltip);
\r
119 public void actionPerformed(ActionEvent e) { open(); }
\r
122 * このダイアログを開きます。すでに開かれていた場合は前面に移動します。
\r
124 public void open() {
\r
125 if( isVisible() ) toFront(); else setVisible(true);
\r
128 * エラーメッセージダイアログを表示します。
\r
129 * @param message エラーメッセージ
\r
131 void showError(String message) {
\r
132 JOptionPane.showMessageDialog(
\r
134 ChordHelperApplet.VersionInfo.NAME,
\r
135 JOptionPane.ERROR_MESSAGE
\r
139 * 警告メッセージダイアログを表示します。
\r
140 * @param message 警告メッセージ
\r
142 void showWarning(String message) {
\r
143 JOptionPane.showMessageDialog(
\r
145 ChordHelperApplet.VersionInfo.NAME,
\r
146 JOptionPane.WARNING_MESSAGE
\r
151 * @param message 確認メッセージ
\r
152 * @return 確認OKのときtrue
\r
154 boolean confirm(String message) {
\r
155 return JOptionPane.showConfirmDialog(
\r
157 ChordHelperApplet.VersionInfo.NAME,
\r
158 JOptionPane.YES_NO_OPTION,
\r
159 JOptionPane.WARNING_MESSAGE
\r
160 ) == JOptionPane.YES_OPTION ;
\r
164 public void dragEnter(DropTargetDragEvent event) {
\r
165 if( event.isDataFlavorSupported(DataFlavor.javaFileListFlavor) ) {
\r
166 event.acceptDrag(DnDConstants.ACTION_COPY_OR_MOVE);
\r
170 public void dragExit(DropTargetEvent event) {}
\r
172 public void dragOver(DropTargetDragEvent event) {}
\r
174 public void dropActionChanged(DropTargetDragEvent event) {}
\r
176 @SuppressWarnings("unchecked")
\r
177 public void drop(DropTargetDropEvent event) {
\r
178 event.acceptDrop(DnDConstants.ACTION_COPY_OR_MOVE);
\r
180 int action = event.getDropAction();
\r
181 if ( (action & DnDConstants.ACTION_COPY_OR_MOVE) != 0 ) {
\r
182 Transferable t = event.getTransferable();
\r
183 Object data = t.getTransferData(DataFlavor.javaFileListFlavor);
\r
184 loadAndPlay((List<File>)data);
\r
185 event.dropComplete(true);
\r
188 event.dropComplete(false);
\r
190 catch (Exception ex) {
\r
191 ex.printStackTrace();
\r
192 event.dropComplete(false);
\r
196 public static final Insets ZERO_INSETS = new Insets(0,0,0,0);
\r
197 static final Icon deleteIcon = new ButtonIcon(ButtonIcon.X_ICON);
\r
199 * 新しいMIDIシーケンスを生成するダイアログ
\r
201 NewSequenceDialog newSequenceDialog = new NewSequenceDialog(this);
\r
203 * BASE64テキスト入力ダイアログ
\r
205 Base64Dialog base64Dialog = new Base64Dialog(this);
\r
207 * プレイリストビュー(シーケンスリスト)
\r
209 SequenceListTable sequenceListTable;
\r
211 * MIDIトラックリストテーブルビュー(選択中のシーケンスの中身)
\r
213 private TrackListTable trackListTable;
\r
215 * MIDIイベントリストテーブルビュー(選択中のトラックの中身)
\r
217 private EventListTable eventListTable;
\r
219 * MIDIイベント入力ダイアログ(イベント入力とイベント送出で共用)
\r
221 MidiEventDialog eventDialog = new MidiEventDialog();
\r
224 * プレイリストビュー(シーケンスリスト)
\r
226 class SequenceListTable extends JTable {
\r
228 * ファイル選択ダイアログ(アプレットでは使用不可)
\r
230 private MidiFileChooser midiFileChooser;
\r
232 * BASE64エンコードボタン(ライブラリが見えている場合のみ有効)
\r
234 private Action base64EncodeAction;
\r
237 * @param model プレイリストデータモデル
\r
239 public SequenceListTable(SequenceListTableModel model) {
\r
240 super(model, null, model.sequenceListSelectionModel);
\r
242 midiFileChooser = new MidiFileChooser();
\r
244 catch( ExceptionInInitializerError|NoClassDefFoundError|AccessControlException e ) {
\r
245 // アプレットの場合、Webクライアントマシンのローカルファイルには
\r
246 // アクセスできないので、ファイル選択ダイアログは使用不可。
\r
247 midiFileChooser = null;
\r
250 new PlayButtonCellEditor();
\r
251 setAutoCreateColumnsFromModel(false);
\r
253 // Base64エンコードアクションの生成
\r
254 if( base64Dialog.isBase64Available() ) {
\r
255 base64EncodeAction = getModel().new SelectedSequenceAction(
\r
257 "Encode selected sequence to Base64 textdata - 選択した曲をBase64テキストにエンコード"
\r
260 public void actionPerformed(ActionEvent e) {
\r
261 SequenceTrackListTableModel mstm = getModel().getSelectedSequenceModel();
\r
262 base64Dialog.setMIDIData(mstm.getMIDIdata(), mstm.getFilename());
\r
263 base64Dialog.setVisible(true);
\r
267 TableColumnModel colModel = getColumnModel();
\r
268 for( SequenceListTableModel.Column c : SequenceListTableModel.Column.values() )
\r
269 colModel.getColumn(c.ordinal()).setPreferredWidth(c.preferredWidth);
\r
272 public void tableChanged(TableModelEvent event) {
\r
273 super.tableChanged(event);
\r
274 int sec = getModel().getTotalSeconds();
\r
275 SequenceListTableModel.Column c = SequenceListTableModel.Column.SEQ_LENGTH;
\r
276 String title = String.format(c.title+" [%02d:%02d]", sec/60, sec%60);
\r
277 getColumnModel().getColumn(c.ordinal()).setHeaderValue(title);
\r
279 // シーケンス削除時など、合計シーケンス長が変わっても
\r
280 // 列モデルからではヘッダタイトルが再描画されないことがある。
\r
281 // そこで、ヘッダビューから repaint() で突っついて再描画させる。
\r
282 JTableHeader th = getTableHeader();
\r
288 * プレイボタンを埋め込んだセルエディタ
\r
290 private class PlayButtonCellEditor extends AbstractCellEditor
\r
291 implements TableCellEditor, TableCellRenderer
\r
293 private JToggleButton playButton = new JToggleButton(
\r
294 getModel().sequencerModel.startStopAction
\r
296 public PlayButtonCellEditor() {
\r
297 playButton.setMargin(ZERO_INSETS);
\r
298 int column = SequenceListTableModel.Column.SEQ_PLAY.ordinal();
\r
299 TableColumn tc = getColumnModel().getColumn(column);
\r
300 tc.setCellRenderer(this);
\r
301 tc.setCellEditor(this);
\r
304 public Object getCellEditorValue() {
\r
305 return playButton.getAction().getValue(Action.SELECTED_KEY);
\r
308 public Component getTableCellEditorComponent(
\r
309 JTable table, Object value, boolean isSelected,
\r
310 int row, int column
\r
312 SequenceListTableModel model = getModel();
\r
313 if(model.sequenceList.get(row).isOnSequencer()) {
\r
314 // すでにロードされていたらボタンをレンダリング
\r
317 // ロードされていない場合、そのセルは編集不可
\r
321 public Component getTableCellRendererComponent(
\r
322 JTable table, Object value, boolean isSelected,
\r
323 boolean hasFocus, int row, int column
\r
325 SequenceListTableModel model = getModel();
\r
326 if(model.sequenceList.get(row).isOnSequencer()) {
\r
327 // すでにロードされていたらボタンをレンダリング
\r
331 // デフォルトレンダラーでレンダリングする。こうすれば
\r
332 // レンダラーを設定しなかった場合と全く同じ動作になる。
\r
333 Class<?> cc = model.getColumnClass(column);
\r
334 TableCellRenderer defaultRenderer = table.getDefaultRenderer(cc);
\r
335 return defaultRenderer.getTableCellRendererComponent(
\r
336 table, value, isSelected, hasFocus, row, column
\r
341 * このプレイリスト(シーケンスリスト)が表示するデータを提供する
\r
343 * @return プレイリストモデル
\r
346 public SequenceListTableModel getModel() {
\r
347 return (SequenceListTableModel) super.getModel();
\r
352 Action deleteSequenceAction = getModel().new SelectedSequenceAction(
\r
353 "Delete", MidiEditor.deleteIcon,
\r
354 "Delete selected MIDI sequence - 選択した曲をプレイリストから削除"
\r
357 public void actionPerformed(ActionEvent e) {
\r
358 SequenceListTableModel model = getModel();
\r
359 if( midiFileChooser != null ) {
\r
360 // ファイルに保存できる場合(Javaアプレットではなく、Javaアプリとして動作している場合)
\r
362 SequenceTrackListTableModel seqModel = model.getSelectedSequenceModel();
\r
363 if( seqModel.isModified() ) {
\r
367 "Selected MIDI sequence not saved - delete it ?\n" +
\r
368 "選択したMIDIシーケンスはまだ保存されていません。削除しますか?";
\r
369 if( ! confirm(message) ) {
\r
376 model.removeSelectedSequence();
\r
380 * ファイル選択ダイアログ(アプレットでは使用不可)
\r
382 private class MidiFileChooser extends JFileChooser implements ListSelectionListener {
\r
386 public Action saveMidiFileAction = new AbstractAction("Save") {
\r
388 String tooltip = "Save selected MIDI sequence to file - 選択したMIDIシーケンスをファイルに保存";
\r
389 putValue(Action.SHORT_DESCRIPTION, tooltip);
\r
392 public void actionPerformed(ActionEvent e) {
\r
393 SequenceTrackListTableModel sequenceTableModel =
\r
394 getModel().getSelectedSequenceModel();
\r
395 String filename = sequenceTableModel.getFilename();
\r
397 if( filename != null && ! filename.isEmpty() ) {
\r
398 // プレイリスト上でファイル名が入っていたら、それを初期選択
\r
399 setSelectedFile(midiFile = new File(filename));
\r
401 int response = showSaveDialog(MidiEditor.this);
\r
402 if( response != JFileChooser.APPROVE_OPTION ) {
\r
403 // 保存ダイアログでキャンセルされた場合
\r
406 if( (midiFile = getSelectedFile()).exists() ) {
\r
407 // 指定されたファイルがすでにあった場合
\r
408 String fn = midiFile.getName();
\r
409 String message = "Overwrite " + fn + " ?\n";
\r
410 message += fn + " を上書きしてよろしいですか?";
\r
411 if( ! confirm(message) ) {
\r
417 try ( FileOutputStream out = new FileOutputStream(midiFile) ) {
\r
418 out.write(sequenceTableModel.getMIDIdata());
\r
419 sequenceTableModel.setModified(false);
\r
421 catch( IOException ex ) {
\r
422 showError( ex.getMessage() );
\r
423 ex.printStackTrace();
\r
429 setFileFilter(new FileNameExtensionFilter("MIDI sequence (*.mid)", "mid"));
\r
432 getModel().sequenceListSelectionModel.addListSelectionListener(this);
\r
436 * シーケンスの選択有無に応じて、保存ボタンのイネーブル状態を更新します。
\r
438 private void updateEnabled() {
\r
439 boolean en = (getModel().sequenceListSelectionModel.getMinSelectionIndex() >= 0);
\r
440 saveMidiFileAction.setEnabled(en);
\r
443 public void valueChanged(ListSelectionEvent e) {
\r
444 if( e.getValueIsAdjusting() )
\r
451 public Action openMidiFileAction = new AbstractAction("Open") {
\r
453 String tooltip = "Open MIDI file - MIDIファイルを開く";
\r
454 putValue(Action.SHORT_DESCRIPTION, tooltip);
\r
457 public void actionPerformed(ActionEvent event) {
\r
458 if(showOpenDialog(MidiEditor.this) == JFileChooser.APPROVE_OPTION) {
\r
460 getModel().addSequence(getSelectedFile());
\r
461 } catch( IOException|InvalidMidiDataException e ) {
\r
462 showWarning(e.getMessage());
\r
463 } catch( AccessControlException e ) {
\r
464 showError(e.getMessage());
\r
465 e.printStackTrace();
\r
474 * シーケンス(トラックリスト)テーブルビュー
\r
476 private class TrackListTable extends JTable {
\r
478 * トラックリストテーブルビューを構築します。
\r
479 * @param model シーケンス(トラックリスト)データモデル
\r
481 public TrackListTable(SequenceTrackListTableModel model) {
\r
482 super(model, null, model.trackListSelectionModel);
\r
484 // 録音対象のMIDIチャンネルをコンボボックスで選択できるようにする
\r
485 int colIndex = SequenceTrackListTableModel.Column.RECORD_CHANNEL.ordinal();
\r
486 TableColumn tc = getColumnModel().getColumn(colIndex);
\r
487 tc.setCellEditor(new DefaultCellEditor(new JComboBox<String>(){{
\r
489 for(int i=1; i <= MIDISpec.MAX_CHANNELS; i++)
\r
490 addItem(String.format("%d", i));
\r
493 setAutoCreateColumnsFromModel(false);
\r
495 trackSelectionListener = new TrackSelectionListener();
\r
496 titleLabel = new TitleLabel();
\r
497 model.sequenceListTableModel.sequenceListSelectionModel.addListSelectionListener(titleLabel);
\r
498 TableColumnModel colModel = getColumnModel();
\r
499 for( SequenceTrackListTableModel.Column c : SequenceTrackListTableModel.Column.values() )
\r
500 colModel.getColumn(c.ordinal()).setPreferredWidth(c.preferredWidth);
\r
503 * このテーブルビューが表示するデータを提供する
\r
504 * シーケンス(トラックリスト)データモデルを返します。
\r
505 * @return シーケンス(トラックリスト)データモデル
\r
508 public SequenceTrackListTableModel getModel() {
\r
509 return (SequenceTrackListTableModel) super.getModel();
\r
514 TitleLabel titleLabel;
\r
516 * 親テーブルの選択シーケンスの変更に反応する
\r
519 private class TitleLabel extends JLabel implements ListSelectionListener {
\r
520 private static final String TITLE = "Tracks";
\r
521 public TitleLabel() { setText(TITLE); }
\r
523 public void valueChanged(ListSelectionEvent event) {
\r
524 if( event.getValueIsAdjusting() )
\r
526 SequenceTrackListTableModel oldModel = getModel();
\r
527 SequenceTrackListTableModel newModel = oldModel.sequenceListTableModel.getSelectedSequenceModel();
\r
528 if( oldModel == newModel )
\r
530 int index = oldModel.sequenceListTableModel.sequenceListSelectionModel.getMinSelectionIndex();
\r
531 String text = TITLE;
\r
533 text = String.format(text+" - MIDI file No.%d", index);
\r
536 if( newModel == null ) {
\r
537 newModel = oldModel.sequenceListTableModel.emptyTrackListTableModel;
\r
538 addTrackAction.setEnabled(false);
\r
541 addTrackAction.setEnabled(true);
\r
543 oldModel.trackListSelectionModel.removeListSelectionListener(trackSelectionListener);
\r
544 setModel(newModel);
\r
545 setSelectionModel(newModel.trackListSelectionModel);
\r
546 newModel.trackListSelectionModel.addListSelectionListener(trackSelectionListener);
\r
547 trackSelectionListener.valueChanged(null);
\r
553 TrackSelectionListener trackSelectionListener;
\r
555 * 選択トラックの変更に反応するリスナー
\r
557 private class TrackSelectionListener implements ListSelectionListener {
\r
559 public void valueChanged(ListSelectionEvent e) {
\r
560 if( e != null && e.getValueIsAdjusting() )
\r
562 TrackListSelectionModel tlsm = getModel().trackListSelectionModel;
\r
563 deleteTrackAction.setEnabled(! tlsm.isSelectionEmpty());
\r
564 eventListTable.titleLabel.update(tlsm, getModel());
\r
570 Action addTrackAction = new AbstractAction("New") {
\r
572 String tooltip = "Append new track - 新しいトラックの追加";
\r
573 putValue(Action.SHORT_DESCRIPTION, tooltip);
\r
577 public void actionPerformed(ActionEvent e) {
\r
578 getModel().createTrack();
\r
584 Action deleteTrackAction = new AbstractAction("Delete", deleteIcon) {
\r
586 String tooltip = "Delete selected track - 選択したトラックを削除";
\r
587 putValue(Action.SHORT_DESCRIPTION, tooltip);
\r
591 public void actionPerformed(ActionEvent e) {
\r
592 String message = "Do you want to delete selected track ?\n"
\r
593 + "選択したトラックを削除しますか?";
\r
594 if( confirm(message) ) getModel().deleteSelectedTracks();
\r
600 * MIDIイベントリストテーブルビュー(選択中のトラックの中身)
\r
602 class EventListTable extends JTable {
\r
604 * 新しいイベントリストテーブルを構築します。
\r
605 * <p>データモデルとして一つのトラックのイベントリストを指定できます。
\r
606 * トラックを切り替えたいときは {@link #setModel(TableModel)}
\r
607 * でデータモデルを異なるトラックのものに切り替えます。
\r
610 * @param model トラック(イベントリスト)データモデル
\r
612 public EventListTable(TrackEventListTableModel model) {
\r
613 super(model, null, model.eventSelectionModel);
\r
616 eventCellEditor = new MidiEventCellEditor();
\r
617 setAutoCreateColumnsFromModel(false);
\r
619 eventSelectionListener = new EventSelectionListener();
\r
620 titleLabel = new TitleLabel();
\r
622 TableColumnModel colModel = getColumnModel();
\r
623 for( TrackEventListTableModel.Column c : TrackEventListTableModel.Column.values() )
\r
624 colModel.getColumn(c.ordinal()).setPreferredWidth(c.preferredWidth);
\r
627 * このテーブルビューが表示するデータを提供する
\r
628 * トラック(イベントリスト)データモデルを返します。
\r
629 * @return トラック(イベントリスト)データモデル
\r
632 public TrackEventListTableModel getModel() {
\r
633 return (TrackEventListTableModel) super.getModel();
\r
638 TitleLabel titleLabel;
\r
640 * 親テーブルの選択トラックの変更に反応する
\r
643 private class TitleLabel extends JLabel {
\r
644 private static final String TITLE = "MIDI Events";
\r
645 public TitleLabel() { super(TITLE); }
\r
646 public void update(TrackListSelectionModel tlsm, SequenceTrackListTableModel sequenceModel) {
\r
647 String text = TITLE;
\r
648 TrackEventListTableModel oldTrackModel = getModel();
\r
649 int index = tlsm.getMinSelectionIndex();
\r
651 text = String.format(TITLE+" - track No.%d", index);
\r
654 TrackEventListTableModel newTrackModel = sequenceModel.getSelectedTrackModel();
\r
655 if( oldTrackModel == newTrackModel )
\r
657 if( newTrackModel == null ) {
\r
658 newTrackModel = getModel().sequenceTrackListTableModel.sequenceListTableModel.emptyEventListTableModel;
\r
659 queryJumpEventAction.setEnabled(false);
\r
660 queryAddEventAction.setEnabled(false);
\r
662 queryPasteEventAction.setEnabled(false);
\r
663 copyEventAction.setEnabled(false);
\r
664 deleteEventAction.setEnabled(false);
\r
665 cutEventAction.setEnabled(false);
\r
668 queryJumpEventAction.setEnabled(true);
\r
669 queryAddEventAction.setEnabled(true);
\r
671 oldTrackModel.eventSelectionModel.removeListSelectionListener(eventSelectionListener);
\r
672 setModel(newTrackModel);
\r
673 setSelectionModel(newTrackModel.eventSelectionModel);
\r
674 newTrackModel.eventSelectionModel.addListSelectionListener(eventSelectionListener);
\r
680 private EventSelectionListener eventSelectionListener;
\r
682 * 選択イベントの変更に反応するリスナー
\r
684 private class EventSelectionListener implements ListSelectionListener {
\r
685 public EventSelectionListener() {
\r
686 getModel().eventSelectionModel.addListSelectionListener(this);
\r
689 public void valueChanged(ListSelectionEvent e) {
\r
690 if( e.getValueIsAdjusting() )
\r
692 if( getSelectionModel().isSelectionEmpty() ) {
\r
693 queryPasteEventAction.setEnabled(false);
\r
694 copyEventAction.setEnabled(false);
\r
695 deleteEventAction.setEnabled(false);
\r
696 cutEventAction.setEnabled(false);
\r
699 copyEventAction.setEnabled(true);
\r
700 deleteEventAction.setEnabled(true);
\r
701 cutEventAction.setEnabled(true);
\r
702 TrackEventListTableModel trackModel = getModel();
\r
703 int minIndex = getSelectionModel().getMinSelectionIndex();
\r
704 MidiEvent midiEvent = trackModel.getMidiEvent(minIndex);
\r
705 MidiMessage msg = midiEvent.getMessage();
\r
706 if( msg instanceof ShortMessage ) {
\r
707 ShortMessage sm = (ShortMessage)msg;
\r
708 int cmd = sm.getCommand();
\r
709 if( cmd == 0x80 || cmd == 0x90 || cmd == 0xA0 ) {
\r
710 // ノート番号を持つ場合、音を鳴らす。
\r
711 MidiChannel outMidiChannels[] = virtualMidiDevice.getChannels();
\r
712 int ch = sm.getChannel();
\r
713 int note = sm.getData1();
\r
714 int vel = sm.getData2();
\r
715 outMidiChannels[ch].noteOn(note, vel);
\r
716 outMidiChannels[ch].noteOff(note, vel);
\r
719 if( pairNoteOnOffModel.isSelected() ) {
\r
720 int maxIndex = getSelectionModel().getMaxSelectionIndex();
\r
722 for( int i=minIndex; i<=maxIndex; i++ ) {
\r
723 if( ! getSelectionModel().isSelectedIndex(i) ) continue;
\r
724 partnerIndex = trackModel.getIndexOfPartnerFor(i);
\r
725 if( partnerIndex >= 0 && ! getSelectionModel().isSelectedIndex(partnerIndex) )
\r
726 getSelectionModel().addSelectionInterval(partnerIndex, partnerIndex);
\r
733 * Pair noteON/OFF トグルボタンモデル
\r
735 private JToggleButton.ToggleButtonModel
\r
736 pairNoteOnOffModel = new JToggleButton.ToggleButtonModel() {
\r
739 new ItemListener() {
\r
740 public void itemStateChanged(ItemEvent e) {
\r
741 eventDialog.midiMessageForm.durationForm.setEnabled(isSelected());
\r
751 private TickPositionModel tickPositionModel = new TickPositionModel();
\r
755 private MidiEvent selectedMidiEvent = null;
\r
759 private int selectedIndex = -1;
\r
763 private long currentTick = 0;
\r
764 private void setSelectedEvent() {
\r
765 SequenceTrackListTableModel sequenceTableModel = getModel().sequenceTrackListTableModel;
\r
766 eventDialog.midiMessageForm.durationForm.setPPQ(sequenceTableModel.getSequence().getResolution());
\r
767 tickPositionModel.setSequenceIndex(sequenceTableModel.getSequenceTickIndex());
\r
768 selectedIndex = -1;
\r
770 selectedMidiEvent = null;
\r
771 if( ! getSelectionModel().isSelectionEmpty() ) {
\r
772 selectedIndex = getSelectionModel().getMinSelectionIndex();
\r
773 selectedMidiEvent = getModel().getMidiEvent(selectedIndex);
\r
774 currentTick = selectedMidiEvent.getTick();
\r
775 tickPositionModel.setTickPosition(currentTick);
\r
779 * 指定のTick位置へジャンプするアクション
\r
781 Action queryJumpEventAction = new AbstractAction() {
\r
783 putValue(NAME,"Jump to ...");
\r
786 private Action jumpEventAction = new AbstractAction() {
\r
787 { putValue(NAME,"Jump"); }
\r
788 public void actionPerformed(ActionEvent e) {
\r
789 scrollToEventAt(tickPositionModel.getTickPosition());
\r
790 eventDialog.setVisible(false);
\r
793 public void actionPerformed(ActionEvent e) {
\r
794 setSelectedEvent();
\r
795 eventDialog.setTitle("Jump selection to");
\r
796 eventDialog.okButton.setAction(jumpEventAction);
\r
797 eventDialog.openTickForm();
\r
801 * 指定のTick位置へ貼り付けるアクション
\r
803 Action queryPasteEventAction = new AbstractAction() {
\r
805 putValue(NAME,"Paste to ...");
\r
808 private Action pasteEventAction = new AbstractAction() {
\r
809 { putValue(NAME,"Paste"); }
\r
810 public void actionPerformed(ActionEvent e) {
\r
811 long tick = tickPositionModel.getTickPosition();
\r
812 getModel().addMidiEvents(copiedEventsToPaste, tick, copiedEventsPPQ);
\r
813 scrollToEventAt(tick);
\r
814 // プレイリストの曲の長さ表示を更新
\r
815 getModel().sequenceTrackListTableModel.sequenceListTableModel.fireSelectedSequenceChanged();
\r
816 eventDialog.setVisible(false);
\r
819 public void actionPerformed(ActionEvent e) {
\r
820 setSelectedEvent();
\r
821 eventDialog.setTitle("Paste to");
\r
822 eventDialog.okButton.setAction(pasteEventAction);
\r
823 eventDialog.openTickForm();
\r
827 * 新しいイベントの追加を行うアクション
\r
829 Action queryAddEventAction = new AbstractAction() {
\r
831 putValue(NAME,"New");
\r
834 public void actionPerformed(ActionEvent e) {
\r
835 setSelectedEvent();
\r
836 midiEventsToBeOverwritten = null; // 追加なので、上書きされるイベントなし
\r
837 eventDialog.setTitle("Add a new MIDI event");
\r
838 eventDialog.okButton.setAction(eventCellEditor.applyEventAction);
\r
839 int ch = getModel().getChannel();
\r
841 eventDialog.midiMessageForm.channelText.setSelectedChannel(ch);
\r
843 eventDialog.openEventForm();
\r
847 * 上書きして削除対象にする変更前イベント(null可)
\r
849 private MidiEvent[] midiEventsToBeOverwritten;
\r
851 * ペースト用にコピーされたMIDIイベントの配列
\r
853 private MidiEvent copiedEventsToPaste[];
\r
855 * ペースト用にコピーされたMIDIイベントのタイミング解像度
\r
857 private int copiedEventsPPQ = 0;
\r
861 public Action cutEventAction = new AbstractAction("Cut") {
\r
866 public void actionPerformed(ActionEvent e) {
\r
867 if( ! confirm("Do you want to cut selected event ?\n選択したMIDIイベントを切り取りますか?"))
\r
869 copiedEventsToPaste = getModel().getSelectedMidiEvents();
\r
870 copiedEventsPPQ = getModel().sequenceTrackListTableModel.sequenceListTableModel.getSelectedSequenceModel().getSequence().getResolution();
\r
871 getModel().removeMidiEvents(copiedEventsToPaste);
\r
872 queryPasteEventAction.setEnabled(
\r
873 copiedEventsToPaste != null &&
\r
874 copiedEventsToPaste.length > 0
\r
881 public Action copyEventAction = new AbstractAction("Copy") {
\r
886 public void actionPerformed(ActionEvent e) {
\r
887 copiedEventsToPaste = getModel().getSelectedMidiEvents();
\r
888 copiedEventsPPQ = getModel().sequenceTrackListTableModel.sequenceListTableModel.getSelectedSequenceModel().getSequence().getResolution();
\r
889 queryPasteEventAction.setEnabled(
\r
890 copiedEventsToPaste != null &&
\r
891 copiedEventsToPaste.length > 0
\r
898 public Action deleteEventAction = new AbstractAction("Delete", deleteIcon) {
\r
903 public void actionPerformed(ActionEvent e) {
\r
904 if( ! confirm("Do you want to delete selected event ?\n選択したMIDIイベントを削除しますか?"))
\r
906 getModel().removeSelectedMidiEvents();
\r
912 private MidiEventCellEditor eventCellEditor;
\r
916 class MidiEventCellEditor extends AbstractCellEditor implements TableCellEditor {
\r
918 * MIDIイベントセルエディタを構築します。
\r
920 public MidiEventCellEditor() {
\r
921 eventDialog.midiMessageForm.setOutputMidiChannels(virtualMidiDevice.getChannels());
\r
922 eventDialog.tickPositionInputForm.setModel(tickPositionModel);
\r
923 int index = TrackEventListTableModel.Column.MESSAGE.ordinal();
\r
924 getColumnModel().getColumn(index).setCellEditor(this);
\r
927 * セルをダブルクリックしないと編集できないようにします。
\r
928 * @param e イベント(マウスイベント)
\r
929 * @return 編集可能になったらtrue
\r
932 public boolean isCellEditable(EventObject e) {
\r
933 if( ! (e instanceof MouseEvent) ) return false;
\r
934 return ((MouseEvent)e).getClickCount() == 2;
\r
937 public Object getCellEditorValue() { return ""; }
\r
941 private Action editEventAction = new AbstractAction() {
\r
942 public void actionPerformed(ActionEvent e) {
\r
943 setSelectedEvent();
\r
944 if( selectedMidiEvent == null )
\r
946 MidiEvent partnerEvent = null;
\r
947 eventDialog.midiMessageForm.setMessage(selectedMidiEvent.getMessage());
\r
948 if( eventDialog.midiMessageForm.isNote() ) {
\r
949 int partnerIndex = getModel().getIndexOfPartnerFor(selectedIndex);
\r
950 if( partnerIndex < 0 ) {
\r
951 eventDialog.midiMessageForm.durationForm.setDuration(0);
\r
954 partnerEvent = getModel().getMidiEvent(partnerIndex);
\r
955 long partnerTick = partnerEvent.getTick();
\r
956 long duration = currentTick > partnerTick ?
\r
957 currentTick - partnerTick : partnerTick - currentTick ;
\r
958 eventDialog.midiMessageForm.durationForm.setDuration((int)duration);
\r
961 if(partnerEvent == null)
\r
962 midiEventsToBeOverwritten = new MidiEvent[] {selectedMidiEvent};
\r
964 midiEventsToBeOverwritten = new MidiEvent[] {selectedMidiEvent, partnerEvent};
\r
965 eventDialog.setTitle("Change MIDI event");
\r
966 eventDialog.okButton.setAction(applyEventAction);
\r
967 eventDialog.cancelButton.addActionListener(cancelActionListener);
\r
968 eventDialog.openEventForm();
\r
974 private JButton editEventButton = new JButton(editEventAction){{
\r
975 setHorizontalAlignment(JButton.LEFT);
\r
978 public Component getTableCellEditorComponent(
\r
979 JTable table, Object value, boolean isSelected, int row, int column
\r
981 editEventButton.setText((String)value);
\r
982 return editEventButton;
\r
985 * イベント入力をキャンセルするアクションリスナーです。
\r
987 * <p>セル編集によって表示されたMIDIメッセージダイアログを
\r
988 * キャンセルする場合、セル編集を中止する処理の追加が必要です。
\r
989 * その追加処理をこのリスナーでカバーします。
\r
992 private ActionListener cancelActionListener = new ActionListener() {
\r
993 public void actionPerformed(ActionEvent e) {
\r
994 fireEditingCanceled();
\r
996 eventDialog.cancelButton.removeActionListener(this);
\r
1000 * 入力したイベントを反映するアクション
\r
1002 private Action applyEventAction = new AbstractAction() {
\r
1004 putValue(NAME,"OK");
\r
1006 public void actionPerformed(ActionEvent e) {
\r
1007 long tick = tickPositionModel.getTickPosition();
\r
1008 MidiMessage msg = eventDialog.midiMessageForm.getMessage();
\r
1009 MidiEvent newMidiEvent = new MidiEvent(msg,tick);
\r
1010 if( midiEventsToBeOverwritten != null ) {
\r
1011 // 上書き消去するための選択済イベントがあった場合
\r
1012 getModel().removeMidiEvents(midiEventsToBeOverwritten);
\r
1014 if( ! getModel().addMidiEvent(newMidiEvent) ) {
\r
1015 System.out.println("addMidiEvent failure");
\r
1018 if(pairNoteOnOffModel.isSelected() && eventDialog.midiMessageForm.isNote()) {
\r
1019 ShortMessage sm = eventDialog.midiMessageForm.getPartnerMessage();
\r
1020 if( sm == null ) scrollToEventAt( tick );
\r
1022 int duration = eventDialog.midiMessageForm.durationForm.getDuration();
\r
1023 if( eventDialog.midiMessageForm.isNote(false) ) { // Note Off
\r
1024 duration = -duration;
\r
1026 long partnerTick = tick + (long)duration;
\r
1027 if( partnerTick < 0L ) partnerTick = 0L;
\r
1028 MidiEvent partner_midi_event =
\r
1029 new MidiEvent( (MidiMessage)sm, partnerTick );
\r
1030 if( ! getModel().addMidiEvent(partner_midi_event) ) {
\r
1031 System.out.println("addMidiEvent failure (note on/off partner message)");
\r
1033 scrollToEventAt( partnerTick > tick ? partnerTick : tick );
\r
1036 getModel().sequenceTrackListTableModel.sequenceListTableModel.fireSequenceModified(getModel().sequenceTrackListTableModel);
\r
1037 eventDialog.setVisible(false);
\r
1038 eventCellEditor.fireEditingStopped();
\r
1043 * スクロール可能なMIDIイベントテーブルビュー
\r
1045 private JScrollPane scrollPane = new JScrollPane(this);
\r
1047 * 指定の MIDI tick のイベントへスクロールします。
\r
1048 * @param tick MIDI tick
\r
1050 public void scrollToEventAt(long tick) {
\r
1051 int index = getModel().tickToIndex(tick);
\r
1052 scrollPane.getVerticalScrollBar().setValue(index * getRowHeight());
\r
1053 getSelectionModel().setSelectionInterval(index, index);
\r
1058 * 新しい {@link MidiEditor} を構築します。
\r
1059 * @param deviceModelList MIDIデバイスモデルリスト
\r
1061 public MidiEditor(MidiSequencerModel sequencerModel) {
\r
1062 // テーブルモデルとテーブルビューの生成
\r
1063 sequenceListTable = new SequenceListTable(
\r
1064 new SequenceListTableModel(sequencerModel)
\r
1066 trackListTable = new TrackListTable(
\r
1067 new SequenceTrackListTableModel(
\r
1068 sequenceListTable.getModel(), null, null
\r
1071 eventListTable = new EventListTable(
\r
1072 new TrackEventListTableModel(trackListTable.getModel(), null)
\r
1075 setTitle("MIDI Editor/Playlist - MIDI Chord Helper");
\r
1076 setBounds( 150, 200, 850, 500 );
\r
1077 setLayout(new FlowLayout());
\r
1078 new DropTarget(this, DnDConstants.ACTION_COPY_OR_MOVE, this, true);
\r
1079 JPanel playlistPanel = new JPanel() {{
\r
1080 JPanel playlistOperationPanel = new JPanel() {{
\r
1081 setLayout(new BoxLayout(this, BoxLayout.LINE_AXIS));
\r
1082 add(Box.createRigidArea(new Dimension(10, 0)));
\r
1083 add(new JButton(newSequenceDialog.openAction) {{
\r
1084 setMargin(ZERO_INSETS);
\r
1086 if( sequenceListTable.midiFileChooser != null ) {
\r
1087 add( Box.createRigidArea(new Dimension(5, 0)) );
\r
1089 sequenceListTable.midiFileChooser.openMidiFileAction
\r
1091 setMargin(ZERO_INSETS);
\r
1094 add(Box.createRigidArea(new Dimension(5, 0)));
\r
1095 SequenceListTableModel sequenceListTableModel = sequenceListTable.getModel();
\r
1096 add(new JButton(sequenceListTableModel.moveToTopAction) {{
\r
1097 setMargin(ZERO_INSETS);
\r
1099 add(Box.createRigidArea(new Dimension(5, 0)));
\r
1100 add(new JButton(sequenceListTableModel.loadToSequencerAction){{
\r
1101 setMargin(ZERO_INSETS);
\r
1103 add(Box.createRigidArea(new Dimension(5, 0)));
\r
1104 add(new JButton(sequenceListTableModel.moveToBottomAction) {{
\r
1105 setMargin(ZERO_INSETS);
\r
1107 if( sequenceListTable.midiFileChooser != null ) {
\r
1108 add(Box.createRigidArea(new Dimension(5, 0)));
\r
1110 sequenceListTable.midiFileChooser.saveMidiFileAction
\r
1112 setMargin(ZERO_INSETS);
\r
1115 if(sequenceListTable.base64EncodeAction != null) {
\r
1116 add(Box.createRigidArea(new Dimension(5, 0)));
\r
1117 add(new JButton(sequenceListTable.base64EncodeAction) {{
\r
1118 setMargin(ZERO_INSETS);
\r
1121 add( Box.createRigidArea(new Dimension(5, 0)) );
\r
1122 add(new JButton(sequenceListTable.deleteSequenceAction) {{
\r
1123 setMargin(ZERO_INSETS);
\r
1125 add( Box.createRigidArea(new Dimension(5, 0)) );
\r
1126 add(new SequencerSpeedSlider(
\r
1127 sequenceListTableModel.sequencerModel.speedSliderModel
\r
1130 setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
\r
1131 add(new JScrollPane(sequenceListTable));
\r
1132 add(Box.createRigidArea(new Dimension(0, 10)));
\r
1133 add(playlistOperationPanel);
\r
1134 add(Box.createRigidArea(new Dimension(0, 10)));
\r
1136 JPanel trackListPanel = new JPanel() {{
\r
1137 JPanel trackListOperationPanel = new JPanel() {{
\r
1138 add(new JButton(trackListTable.addTrackAction) {{
\r
1139 setMargin(ZERO_INSETS);
\r
1141 add(new JButton(trackListTable.deleteTrackAction) {{
\r
1142 setMargin(ZERO_INSETS);
\r
1145 setLayout(new BoxLayout(this, BoxLayout.PAGE_AXIS));
\r
1146 add(trackListTable.titleLabel);
\r
1147 add(Box.createRigidArea(new Dimension(0, 5)));
\r
1148 add(new JScrollPane(trackListTable));
\r
1149 add(Box.createRigidArea(new Dimension(0, 5)));
\r
1150 add(trackListOperationPanel);
\r
1152 JPanel eventListPanel = new JPanel() {{
\r
1153 JPanel eventListOperationPanel = new JPanel() {{
\r
1154 add(new JCheckBox("Pair NoteON/OFF") {{
\r
1155 setModel(eventListTable.pairNoteOnOffModel);
\r
1156 setToolTipText("NoteON/OFFをペアで同時選択する");
\r
1158 add(new JButton(eventListTable.queryJumpEventAction) {{
\r
1159 setMargin(ZERO_INSETS);
\r
1161 add(new JButton(eventListTable.queryAddEventAction) {{
\r
1162 setMargin(ZERO_INSETS);
\r
1164 add(new JButton(eventListTable.copyEventAction) {{
\r
1165 setMargin(ZERO_INSETS);
\r
1167 add(new JButton(eventListTable.cutEventAction) {{
\r
1168 setMargin(ZERO_INSETS);
\r
1170 add(new JButton(eventListTable.queryPasteEventAction) {{
\r
1171 setMargin(ZERO_INSETS);
\r
1173 add(new JButton(eventListTable.deleteEventAction) {{
\r
1174 setMargin(ZERO_INSETS);
\r
1177 setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
\r
1178 add(eventListTable.titleLabel);
\r
1179 add(eventListTable.scrollPane);
\r
1180 add(eventListOperationPanel);
\r
1182 Container cp = getContentPane();
\r
1183 cp.setLayout(new BoxLayout(cp, BoxLayout.Y_AXIS));
\r
1184 cp.add(Box.createVerticalStrut(2));
\r
1186 new JSplitPane(JSplitPane.VERTICAL_SPLIT, playlistPanel,
\r
1187 new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, trackListPanel, eventListPanel) {{
\r
1188 setDividerLocation(300);
\r
1191 setDividerLocation(160);
\r
1197 * 複数のMIDIファイルを読み込み、再生されていなかったら再生します。
\r
1198 * すでに再生されていた場合、このエディタダイアログを表示します。
\r
1200 * @param fileList 読み込むMIDIファイルのリスト
\r
1202 public void loadAndPlay(List<File> fileList) {
\r
1203 int firstIndex = -1;
\r
1204 SequenceListTableModel sequenceListTableModel = sequenceListTable.getModel();
\r
1206 firstIndex = sequenceListTableModel.addSequences(fileList);
\r
1207 } catch( IOException|InvalidMidiDataException e ) {
\r
1208 showWarning(e.getMessage());
\r
1209 } catch( AccessControlException e ) {
\r
1210 showError(e.getMessage());
\r
1211 e.printStackTrace();
\r
1213 if(sequenceListTableModel.sequencerModel.getSequencer().isRunning()) {
\r
1216 else if( firstIndex >= 0 ) {
\r
1217 sequenceListTableModel.loadToSequencer(firstIndex);
\r
1218 sequenceListTableModel.sequencerModel.start();
\r
1224 * シーケンサーの再生スピード調整スライダビュー
\r
1226 class SequencerSpeedSlider extends JPanel {
\r
1227 private static final String items[] = {
\r
1235 private JLabel titleLabel;
\r
1236 private JSlider slider;
\r
1237 public SequencerSpeedSlider(BoundedRangeModel model) {
\r
1238 add(titleLabel = new JLabel("Speed:"));
\r
1239 add(slider = new JSlider(model){{
\r
1240 setPaintTicks(true);
\r
1241 setMajorTickSpacing(12);
\r
1242 setMinorTickSpacing(1);
\r
1243 setVisible(false);
\r
1245 add(new JComboBox<String>(items) {{
\r
1246 addActionListener(new ActionListener() {
\r
1248 public void actionPerformed(ActionEvent e) {
\r
1249 int index = getSelectedIndex();
\r
1250 BoundedRangeModel model = slider.getModel();
\r
1251 if( index == 0 ) {
\r
1252 model.setValue(0);
\r
1253 slider.setVisible(false);
\r
1254 titleLabel.setVisible(true);
\r
1257 int maxValue = ( index == 1 ? 7 : (index-1)*12 );
\r
1258 model.setMinimum(-maxValue);
\r
1259 model.setMaximum(maxValue);
\r
1260 slider.setMajorTickSpacing( index == 1 ? 7 : 12 );
\r
1261 slider.setMinorTickSpacing( index > 3 ? 12 : 1 );
\r
1262 slider.setVisible(true);
\r
1263 titleLabel.setVisible(false);
\r
1272 * 選択されているシーケンスのインデックス
\r
1274 class SequenceListSelectionModel extends DefaultListSelectionModel {
\r
1276 setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
\r
1280 * プレイリスト(MIDIシーケンスリスト)のテーブルデータモデル
\r
1282 class SequenceListTableModel extends AbstractTableModel {
\r
1286 MidiSequencerModel sequencerModel;
\r
1290 SequenceTrackListTableModel emptyTrackListTableModel;
\r
1294 TrackEventListTableModel emptyEventListTableModel;
\r
1296 * 新しいプレイリストのテーブルモデルを構築します。
\r
1297 * @param sequencerModel MIDIシーケンサーモデル
\r
1299 public SequenceListTableModel(MidiSequencerModel sequencerModel) {
\r
1300 this.sequencerModel = sequencerModel;
\r
1303 sequencerModel.addChangeListener(secondPosition = new SecondPosition());
\r
1306 sequencerModel.getSequencer().addMetaEventListener(
\r
1307 new MetaEventListener() {
\r
1311 * <p>EOT (End Of Track、type==0x2F) を受信したときの処理です。
\r
1313 * <p>これは MetaEventListener のための実装なので、多くの場合
\r
1314 * Swing EDT ではなく MIDI シーケンサの EDT から起動されます。
\r
1315 * Swing EDT とは違うスレッドで動いていた場合は Swing EDT に振り直されます。
\r
1319 public void meta(MetaMessage msg) {
\r
1320 if( msg.getType() == 0x2F ) {
\r
1321 if( ! SwingUtilities.isEventDispatchThread() ) {
\r
1322 SwingUtilities.invokeLater(
\r
1325 public void run() { goNext(); }
\r
1335 emptyTrackListTableModel = new SequenceTrackListTableModel(this, null, null);
\r
1336 emptyEventListTableModel = new TrackEventListTableModel(emptyTrackListTableModel, null);
\r
1341 * <p>リピートモードの場合は同じ曲をもう一度再生、
\r
1342 * そうでない場合は次の曲へ進んで再生します。
\r
1343 * 次の曲がなければ、そこで停止します。
\r
1344 * いずれの場合も局の先頭へ戻ります。
\r
1347 private void goNext() {
\r
1349 sequencerModel.getSequencer().setMicrosecondPosition(0);
\r
1350 if( (Boolean)toggleRepeatAction.getValue(Action.SELECTED_KEY) || loadNext(1)) {
\r
1351 // リピートモードのときはもう一度同じ曲を、
\r
1352 // そうでない場合は次の曲を再生開始
\r
1353 sequencerModel.start();
\r
1356 // 最後の曲が終わったので、停止状態にする
\r
1357 sequencerModel.stop();
\r
1358 // ここでボタンが停止状態に変わったはずなので、
\r
1359 // 通常であれば再生ボタンが自力で再描画するところだが、
\r
1361 // セルのレンダラーが描く再生ボタンには効かないようなので、
\r
1362 // セルを突っついて再表示させる。
\r
1363 int rowIndex = indexOfSequenceOnSequencer();
\r
1364 int colIndex = Column.SEQ_PLAY.ordinal();
\r
1365 fireTableCellUpdated(rowIndex, colIndex);
\r
1371 List<SequenceTrackListTableModel> sequenceList = new Vector<>();
\r
1373 * 選択されているシーケンスのインデックス
\r
1375 SequenceListSelectionModel sequenceListSelectionModel = new SequenceListSelectionModel();
\r
1377 * 行が選択されているときだけイネーブルになるアクション
\r
1379 public abstract class SelectedSequenceAction extends AbstractAction
\r
1380 implements ListSelectionListener
\r
1382 public SelectedSequenceAction(String name, Icon icon, String tooltip) {
\r
1383 super(name,icon); init(tooltip);
\r
1385 public SelectedSequenceAction(String name, String tooltip) {
\r
1386 super(name); init(tooltip);
\r
1389 public void valueChanged(ListSelectionEvent e) {
\r
1390 if( e.getValueIsAdjusting() ) return;
\r
1391 setEnebledBySelection();
\r
1393 protected void setEnebledBySelection() {
\r
1394 int index = sequenceListSelectionModel.getMinSelectionIndex();
\r
1395 setEnabled(index >= 0);
\r
1397 private void init(String tooltip) {
\r
1398 putValue(Action.SHORT_DESCRIPTION, tooltip);
\r
1399 sequenceListSelectionModel.addListSelectionListener(this);
\r
1400 setEnebledBySelection();
\r
1404 * 選択されたシーケンスへジャンプするアクション
\r
1406 public Action loadToSequencerAction = new SelectedSequenceAction(
\r
1407 "Load to sequencer",
\r
1408 "Load selected MIDI sequence to sequencer - 選択した曲をシーケンサへロード"
\r
1411 public void actionPerformed(ActionEvent e) {
\r
1412 loadToSequencer(sequenceListSelectionModel.getMinSelectionIndex());
\r
1416 * 繰り返し再生ON/OFF切り替えアクション
\r
1418 public Action toggleRepeatAction = new AbstractAction() {
\r
1420 putValue(SHORT_DESCRIPTION, "Repeat - 繰り返し再生");
\r
1421 putValue(LARGE_ICON_KEY, new ButtonIcon(ButtonIcon.REPEAT_ICON));
\r
1422 putValue(SELECTED_KEY, false);
\r
1425 public void actionPerformed(ActionEvent event) { }
\r
1430 private class SecondPosition implements ChangeListener {
\r
1431 private int value = 0;
\r
1433 * 再生中のシーケンサーの秒位置が変わったときに表示を更新します。
\r
1434 * @param event 変更イベント
\r
1437 public void stateChanged(ChangeEvent event) {
\r
1438 Object src = event.getSource();
\r
1439 if( src instanceof MidiSequencerModel ) {
\r
1440 int newValue = ((MidiSequencerModel)src).getValue() / 1000;
\r
1441 if(value == newValue) return;
\r
1443 int rowIndex = indexOfSequenceOnSequencer();
\r
1444 fireTableCellUpdated(rowIndex, Column.SEQ_POSITION.ordinal());
\r
1448 public String toString() {
\r
1449 return String.format("%02d:%02d", value/60, value%60);
\r
1453 * 曲の先頭または前の曲へ戻るアクション
\r
1455 public Action moveToTopAction = new AbstractAction() {
\r
1457 putValue(SHORT_DESCRIPTION,
\r
1458 "Move to top or previous song - 曲の先頭または前の曲へ戻る"
\r
1460 putValue(LARGE_ICON_KEY, new ButtonIcon(ButtonIcon.TOP_ICON));
\r
1462 public void actionPerformed(ActionEvent event) {
\r
1463 if( sequencerModel.getSequencer().getTickPosition() <= 40 )
\r
1465 sequencerModel.setValue(0);
\r
1471 public Action moveToBottomAction = new AbstractAction() {
\r
1473 putValue(SHORT_DESCRIPTION, "Move to next song - 次の曲へ進む");
\r
1474 putValue(LARGE_ICON_KEY, new ButtonIcon(ButtonIcon.BOTTOM_ICON));
\r
1476 public void actionPerformed(ActionEvent event) {
\r
1477 if(loadNext(1)) sequencerModel.setValue(0);
\r
1484 public enum Column {
\r
1485 /** MIDIシーケンスの番号 */
\r
1486 SEQ_NUMBER("No.", Integer.class, 20),
\r
1488 MODIFIED("Modified", Boolean.class, 50) {
\r
1490 public Object getValueOf(SequenceTrackListTableModel sequenceModel) {
\r
1491 return sequenceModel.isModified();
\r
1495 SEQ_PLAY("Play/Stop", String.class, 60) {
\r
1497 public boolean isCellEditable() { return true; }
\r
1499 /** 再生中の時間位置(分:秒) */
\r
1500 SEQ_POSITION("Position", String.class, 60) {
\r
1502 public Object getValueOf(SequenceTrackListTableModel sequenceModel) {
\r
1503 return sequenceModel.isOnSequencer()
\r
1504 ? sequenceModel.sequenceListTableModel.secondPosition : "";
\r
1507 /** シーケンスの時間長(分:秒) */
\r
1508 SEQ_LENGTH("Length", String.class, 80) {
\r
1510 public Object getValueOf(SequenceTrackListTableModel sequenceModel) {
\r
1511 long usec = sequenceModel.getSequence().getMicrosecondLength();
\r
1512 int sec = (int)( (usec < 0 ? usec += 0x100000000L : usec) / 1000L / 1000L );
\r
1513 return String.format( "%02d:%02d", sec/60, sec%60 );
\r
1517 FILENAME("Filename", String.class, 100) {
\r
1519 public boolean isCellEditable() { return true; }
\r
1520 public Object getValueOf(SequenceTrackListTableModel sequenceModel) {
\r
1521 String filename = sequenceModel.getFilename();
\r
1522 return filename == null ? "" : filename;
\r
1525 /** シーケンス名(最初のトラックの名前) */
\r
1526 SEQ_NAME("Sequence name", String.class, 250) {
\r
1528 public boolean isCellEditable() { return true; }
\r
1529 public Object getValueOf(SequenceTrackListTableModel sequenceModel) {
\r
1530 String name = sequenceModel.toString();
\r
1531 return name == null ? "" : name;
\r
1535 RESOLUTION("Resolution", Integer.class, 60) {
\r
1537 public Object getValueOf(SequenceTrackListTableModel sequenceModel) {
\r
1538 return sequenceModel.getSequence().getResolution();
\r
1542 TRACKS("Tracks", Integer.class, 40) {
\r
1544 public Object getValueOf(SequenceTrackListTableModel sequenceModel) {
\r
1545 return sequenceModel.getSequence().getTracks().length;
\r
1549 DIVISION_TYPE("DivType", String.class, 50) {
\r
1551 public Object getValueOf(SequenceTrackListTableModel sequenceModel) {
\r
1552 float divType = sequenceModel.getSequence().getDivisionType();
\r
1553 if( divType == Sequence.PPQ ) return "PPQ";
\r
1554 else if( divType == Sequence.SMPTE_24 ) return "SMPTE_24";
\r
1555 else if( divType == Sequence.SMPTE_25 ) return "SMPTE_25";
\r
1556 else if( divType == Sequence.SMPTE_30 ) return "SMPTE_30";
\r
1557 else if( divType == Sequence.SMPTE_30DROP ) return "SMPTE_30DROP";
\r
1558 else return "[Unknown]";
\r
1562 Class<?> columnClass;
\r
1563 int preferredWidth;
\r
1566 * @param title 列のタイトル
\r
1567 * @param columnClass 列のクラス
\r
1569 private Column(String title, Class<?> columnClass, int preferredWidth) {
\r
1570 this.title = title;
\r
1571 this.columnClass = columnClass;
\r
1572 this.preferredWidth = preferredWidth;
\r
1574 public boolean isCellEditable() { return false; }
\r
1575 public Object getValueOf(SequenceTrackListTableModel sequenceModel) {
\r
1581 public int getRowCount() { return sequenceList.size(); }
\r
1583 public int getColumnCount() { return Column.values().length; }
\r
1585 public String getColumnName(int column) {
\r
1586 return Column.values()[column].title;
\r
1589 public Class<?> getColumnClass(int column) {
\r
1590 return Column.values()[column].columnClass;
\r
1593 public boolean isCellEditable(int row, int column) {
\r
1594 return Column.values()[column].isCellEditable();
\r
1596 /** 再生中のシーケンサーの秒位置 */
\r
1597 private SecondPosition secondPosition;
\r
1599 public Object getValueAt(int row, int column) {
\r
1600 Column c = Column.values()[column];
\r
1601 return c == Column.SEQ_NUMBER ? row : c.getValueOf(sequenceList.get(row));
\r
1604 public void setValueAt(Object val, int row, int column) {
\r
1605 Column c = Column.values()[column];
\r
1609 sequenceList.get(row).setFilename((String)val);
\r
1610 fireTableCellUpdated(row, column);
\r
1614 if( sequenceList.get(row).setName((String)val) )
\r
1615 fireTableCellUpdated(row, Column.MODIFIED.ordinal());
\r
1616 fireTableCellUpdated(row, column);
\r
1623 * このプレイリストに読み込まれた全シーケンスの合計時間長を返します。
\r
1624 * @return 全シーケンスの合計時間長 [秒]
\r
1626 public int getTotalSeconds() {
\r
1629 for( SequenceTrackListTableModel m : sequenceList ) {
\r
1630 usec = m.getSequence().getMicrosecondLength();
\r
1631 total += (int)( (usec < 0 ? usec += 0x100000000L : usec)/1000L/1000L );
\r
1636 * 未保存の修正内容を持つシーケンスがあるか調べます。
\r
1637 * @return 未保存の修正内容を持つシーケンスがあればtrue
\r
1639 public boolean isModified() {
\r
1640 for( SequenceTrackListTableModel m : sequenceList ) {
\r
1641 if( m.isModified() ) return true;
\r
1646 * 選択したシーケンスに未保存の修正内容があることを記録します。
\r
1647 * @param selModel 選択状態
\r
1648 * @param isModified 未保存の修正内容があるときtrue
\r
1650 public void setModified(boolean isModified) {
\r
1651 int minIndex = sequenceListSelectionModel.getMinSelectionIndex();
\r
1652 int maxIndex = sequenceListSelectionModel.getMaxSelectionIndex();
\r
1653 for( int i = minIndex; i <= maxIndex; i++ ) {
\r
1654 if( sequenceListSelectionModel.isSelectedIndex(i) ) {
\r
1655 sequenceList.get(i).setModified(isModified);
\r
1656 fireTableCellUpdated(i, Column.MODIFIED.ordinal());
\r
1661 * 選択されたMIDIシーケンスのテーブルモデルを返します。
\r
1662 * @return 選択されたMIDIシーケンスのテーブルモデル(非選択時はnull)
\r
1664 public SequenceTrackListTableModel getSelectedSequenceModel() {
\r
1665 if( sequenceListSelectionModel.isSelectionEmpty() )
\r
1667 int selectedIndex = sequenceListSelectionModel.getMinSelectionIndex();
\r
1668 if( selectedIndex >= sequenceList.size() )
\r
1670 return sequenceList.get(selectedIndex);
\r
1673 * 指定されたシーケンスが修正されたことを通知します。
\r
1674 * @param sequenceTableModel MIDIシーケンスモデル
\r
1676 public void fireSequenceModified(SequenceTrackListTableModel sequenceTableModel) {
\r
1677 int index = sequenceList.indexOf(sequenceTableModel);
\r
1680 sequenceTableModel.setModified(true);
\r
1681 fireTableRowsUpdated(index, index);
\r
1684 * 指定されている選択範囲のシーケンスが変更されたことを通知します。
\r
1685 * 更新済みフラグをセットし、選択されたシーケンスの全ての列を再表示します。
\r
1687 public void fireSelectedSequenceChanged() {
\r
1688 if( sequenceListSelectionModel.isSelectionEmpty() )
\r
1690 int minIndex = sequenceListSelectionModel.getMinSelectionIndex();
\r
1691 int maxIndex = sequenceListSelectionModel.getMaxSelectionIndex();
\r
1692 for( int index = minIndex; index <= maxIndex; index++ ) {
\r
1693 sequenceList.get(index).setModified(true);
\r
1695 fireTableRowsUpdated(minIndex, maxIndex);
\r
1698 * バイト列とファイル名からMIDIシーケンスを追加します。
\r
1699 * バイト列が null の場合、空のMIDIシーケンスを追加します。
\r
1700 * @param data バイト列
\r
1701 * @param filename ファイル名
\r
1702 * @return 追加先インデックス(先頭が 0)
\r
1703 * @throws IOException ファイル読み込みに失敗した場合
\r
1704 * @throws InvalidMidiDataException MIDIデータが正しくない場合
\r
1706 public int addSequence(byte[] data, String filename)
\r
1707 throws IOException, InvalidMidiDataException
\r
1709 if( data == null ) return addDefaultSequence();
\r
1711 try (InputStream in = new ByteArrayInputStream(data)) {
\r
1712 Sequence seq = MidiSystem.getSequence(in);
\r
1713 lastIndex = addSequence(seq, filename);
\r
1714 } catch( IOException|InvalidMidiDataException e ) {
\r
1717 sequenceListSelectionModel.setSelectionInterval(lastIndex, lastIndex);
\r
1721 * MIDIシーケンスを追加します。
\r
1722 * シーケンサーが停止中の場合、追加したシーケンスから再生を開始します。
\r
1723 * @param sequence MIDIシーケンス
\r
1724 * @return 追加先インデックス(先頭が 0)
\r
1726 public int addSequenceAndPlay(Sequence sequence) {
\r
1727 int lastIndex = addSequence(sequence,"");
\r
1728 if( ! sequencerModel.getSequencer().isRunning() ) {
\r
1729 loadToSequencer(lastIndex);
\r
1730 sequencerModel.start();
\r
1735 * MIDIシーケンスを追加します。
\r
1736 * @param sequence MIDIシーケンス
\r
1737 * @param filename ファイル名
\r
1738 * @return 追加されたシーケンスのインデックス(先頭が 0)
\r
1740 public int addSequence(Sequence sequence, String filename) {
\r
1742 new SequenceTrackListTableModel(this, sequence, filename)
\r
1744 int lastIndex = sequenceList.size() - 1;
\r
1745 fireTableRowsInserted(lastIndex, lastIndex);
\r
1749 * デフォルトの内容でMIDIシーケンスを作成して追加します。
\r
1750 * @return 追加されたMIDIシーケンスのインデックス(先頭が 0)
\r
1752 public int addDefaultSequence() {
\r
1753 Sequence seq = (new Music.ChordProgression()).toMidiSequence();
\r
1754 return seq == null ? -1 : addSequence(seq,null);
\r
1758 * ファイルが null の場合、空のMIDIシーケンスを追加します。
\r
1759 * @param midiFile MIDIファイル
\r
1760 * @return 追加先インデックス(先頭が 0)
\r
1761 * @throws InvalidMidiDataException ファイル内のMIDIデータが正しくない場合
\r
1762 * @throws IOException ファイル入出力に失敗した場合
\r
1764 public int addSequence(File midiFile) throws InvalidMidiDataException, IOException {
\r
1765 if( midiFile == null ) return addDefaultSequence();
\r
1767 try (FileInputStream in = new FileInputStream(midiFile)) {
\r
1768 Sequence seq = MidiSystem.getSequence(in);
\r
1769 String filename = midiFile.getName();
\r
1770 lastIndex = addSequence(seq, filename);
\r
1771 } catch( InvalidMidiDataException|IOException e ) {
\r
1777 * 複数のMIDIファイルを追加します。
\r
1778 * @param fileList 追加するMIDIファイルのリスト
\r
1779 * @return 追加先の最初のインデックス(先頭が 0、追加されなかった場合は -1)
\r
1780 * @throws InvalidMidiDataException ファイル内のMIDIデータが正しくない場合
\r
1781 * @throws IOException ファイル入出力に失敗した場合
\r
1783 public int addSequences(List<File> fileList)
\r
1784 throws InvalidMidiDataException, IOException
\r
1786 int firstIndex = -1;
\r
1787 for( File file : fileList ) {
\r
1788 int lastIndex = addSequence(file);
\r
1789 if( firstIndex == -1 )
\r
1790 firstIndex = lastIndex;
\r
1792 return firstIndex;
\r
1795 * URLから読み込んだMIDIシーケンスを追加します。
\r
1796 * @param midiFileUrl MIDIファイルのURL
\r
1797 * @return 追加先インデックス(先頭が 0、失敗した場合は -1)
\r
1798 * @throws URISyntaxException URLの形式に誤りがある場合
\r
1799 * @throws IOException 入出力に失敗した場合
\r
1800 * @throws InvalidMidiDataException MIDIデータが正しくない場合
\r
1802 public int addSequenceFromURL(String midiFileUrl)
\r
1803 throws URISyntaxException, IOException, InvalidMidiDataException
\r
1805 URI uri = new URI(midiFileUrl);
\r
1806 URL url = uri.toURL();
\r
1807 Sequence seq = MidiSystem.getSequence(url);
\r
1808 String filename = url.getFile().replaceFirst("^.*/","");
\r
1809 return addSequence(seq, filename);
\r
1813 * 選択したシーケンスを除去します。
\r
1814 * @param listSelectionModel 選択状態
\r
1816 public void removeSelectedSequence() {
\r
1817 if( sequenceListSelectionModel.isSelectionEmpty() )
\r
1819 int selectedIndex = sequenceListSelectionModel.getMinSelectionIndex();
\r
1820 if( sequenceList.remove(selectedIndex).isOnSequencer() ) {
\r
1822 // シーケンサーにロード済みだった場合、アンロードする。
\r
1823 sequencerModel.setSequenceTrackListTableModel(null);
\r
1825 fireTableRowsDeleted(selectedIndex, selectedIndex);
\r
1828 * 指定したインデックス位置のシーケンスをシーケンサーにロードします。
\r
1829 * @param index シーケンスのインデックス位置(-1 を指定するとアンロードされます)
\r
1831 public void loadToSequencer(int index) {
\r
1832 SequenceTrackListTableModel oldSeq = sequencerModel.getSequenceTableModel();
\r
1833 SequenceTrackListTableModel newSeq = (index < 0 ? null : sequenceList.get(index));
\r
1834 if(oldSeq == newSeq)
\r
1836 sequencerModel.setSequenceTrackListTableModel(newSeq);
\r
1837 int columnIndices[] = {
\r
1838 Column.SEQ_PLAY.ordinal(),
\r
1839 Column.SEQ_POSITION.ordinal(),
\r
1841 if( oldSeq != null ) {
\r
1842 int oldIndex = sequenceList.indexOf(oldSeq);
\r
1843 for( int columnIndex : columnIndices )
\r
1844 fireTableCellUpdated(oldIndex, columnIndex);
\r
1846 if( newSeq != null )
\r
1847 for( int columnIndex : columnIndices )
\r
1848 fireTableCellUpdated(index, columnIndex);
\r
1851 * 現在シーケンサにロードされているシーケンスのインデックスを返します。
\r
1852 * ロードされていない場合は -1 を返します。
\r
1853 * @return 現在シーケンサにロードされているシーケンスのインデックス
\r
1855 public int indexOfSequenceOnSequencer() {
\r
1856 return sequenceList.indexOf(sequencerModel.getSequenceTableModel());
\r
1859 * 引数で示された数だけ次へ進めたシーケンスをロードします。
\r
1860 * @param offset 進みたいシーケンス数
\r
1861 * @return 成功したらtrue
\r
1863 public boolean loadNext(int offset) {
\r
1864 int loadedIndex = indexOfSequenceOnSequencer();
\r
1865 int index = (loadedIndex < 0 ? 0 : loadedIndex + offset);
\r
1866 if( index < 0 || index >= sequenceList.size() )
\r
1868 loadToSequencer(index);
\r
1874 * 選択されているトラックのインデックス
\r
1876 class TrackListSelectionModel extends DefaultListSelectionModel {
\r
1878 setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
\r
1882 * MIDIシーケンス(トラックリスト)のテーブルデータモデル
\r
1884 class SequenceTrackListTableModel extends AbstractTableModel {
\r
1888 public enum Column {
\r
1890 TRACK_NUMBER("No.", Integer.class, 20),
\r
1892 EVENTS("Events", Integer.class, 40),
\r
1894 MUTE("Mute", Boolean.class, 30),
\r
1896 SOLO("Solo", Boolean.class, 30),
\r
1897 /** 録音するMIDIチャンネル */
\r
1898 RECORD_CHANNEL("RecCh", String.class, 40),
\r
1900 CHANNEL("Ch", String.class, 30),
\r
1902 TRACK_NAME("Track name", String.class, 100);
\r
1904 Class<?> columnClass;
\r
1905 int preferredWidth;
\r
1908 * @param title 列のタイトル
\r
1909 * @param widthRatio 幅の割合
\r
1910 * @param columnClass 列のクラス
\r
1912 private Column(String title, Class<?> columnClass, int preferredWidth) {
\r
1913 this.title = title;
\r
1914 this.columnClass = columnClass;
\r
1915 this.preferredWidth = preferredWidth;
\r
1921 SequenceListTableModel sequenceListTableModel;
\r
1925 private Sequence sequence;
\r
1927 * ラップされたMIDIシーケンスのtickインデックス
\r
1929 private SequenceTickIndex sequenceTickIndex;
\r
1933 private String filename = "";
\r
1937 private List<TrackEventListTableModel> trackModelList = new ArrayList<>();
\r
1939 * 選択されているトラックのインデックス
\r
1941 TrackListSelectionModel trackListSelectionModel = new TrackListSelectionModel();
\r
1943 * MIDIシーケンスとファイル名から {@link SequenceTrackListTableModel} を構築します。
\r
1944 * @param sequenceListTableModel 親のプレイリスト
\r
1945 * @param sequence MIDIシーケンス
\r
1946 * @param filename ファイル名
\r
1948 public SequenceTrackListTableModel(
\r
1949 SequenceListTableModel sequenceListTableModel,
\r
1950 Sequence sequence,
\r
1953 this.sequenceListTableModel = sequenceListTableModel;
\r
1954 setSequence(sequence);
\r
1955 setFilename(filename);
\r
1958 public int getRowCount() {
\r
1959 return sequence == null ? 0 : sequence.getTracks().length;
\r
1962 public int getColumnCount() {
\r
1963 return Column.values().length;
\r
1970 public String getColumnName(int column) {
\r
1971 return Column.values()[column].title;
\r
1975 * @return 指定された列の型
\r
1978 public Class<?> getColumnClass(int column) {
\r
1979 Column c = Column.values()[column];
\r
1982 case SOLO: if( ! isOnSequencer() ) return String.class;
\r
1984 default: return c.columnClass;
\r
1988 public Object getValueAt(int row, int column) {
\r
1989 Column c = Column.values()[column];
\r
1991 case TRACK_NUMBER: return row;
\r
1992 case EVENTS: return sequence.getTracks()[row].size();
\r
1994 return isOnSequencer() ? sequenceListTableModel.sequencerModel.getSequencer().getTrackMute(row) : "";
\r
1996 return isOnSequencer() ? sequenceListTableModel.sequencerModel.getSequencer().getTrackSolo(row) : "";
\r
1997 case RECORD_CHANNEL:
\r
1998 return isOnSequencer() ? trackModelList.get(row).getRecordingChannel() : "";
\r
2000 int ch = trackModelList.get(row).getChannel();
\r
2001 return ch < 0 ? "" : ch + 1 ;
\r
2003 case TRACK_NAME: return trackModelList.get(row).toString();
\r
2004 default: return "";
\r
2008 * セルが編集可能かどうかを返します。
\r
2011 public boolean isCellEditable(int row, int column) {
\r
2012 Column c = Column.values()[column];
\r
2016 case RECORD_CHANNEL: return isOnSequencer();
\r
2018 case TRACK_NAME: return true;
\r
2019 default: return false;
\r
2026 public void setValueAt(Object val, int row, int column) {
\r
2027 Column c = Column.values()[column];
\r
2030 sequenceListTableModel.sequencerModel.getSequencer().setTrackMute(row, ((Boolean)val).booleanValue());
\r
2033 sequenceListTableModel.sequencerModel.getSequencer().setTrackSolo(row, ((Boolean)val).booleanValue());
\r
2035 case RECORD_CHANNEL:
\r
2036 trackModelList.get(row).setRecordingChannel((String)val);
\r
2041 ch = new Integer((String)val);
\r
2043 catch( NumberFormatException e ) {
\r
2047 if( --ch <= 0 || ch > MIDISpec.MAX_CHANNELS )
\r
2049 TrackEventListTableModel trackTableModel = trackModelList.get(row);
\r
2050 if( ch == trackTableModel.getChannel() ) break;
\r
2051 trackTableModel.setChannel(ch);
\r
2052 setModified(true);
\r
2053 fireTableCellUpdated(row, Column.EVENTS.ordinal());
\r
2057 trackModelList.get(row).setString((String)val);
\r
2062 fireTableCellUpdated(row,column);
\r
2066 * @return MIDIシーケンス
\r
2068 public Sequence getSequence() { return sequence; }
\r
2070 * シーケンスtickインデックスを返します。
\r
2071 * @return シーケンスtickインデックス
\r
2073 public SequenceTickIndex getSequenceTickIndex() {
\r
2074 return sequenceTickIndex;
\r
2077 * MIDIシーケンスを設定します。
\r
2078 * @param sequence MIDIシーケンス(nullを指定するとトラックリストが空になる)
\r
2080 private void setSequence(Sequence sequence) {
\r
2082 sequenceListTableModel.sequencerModel.getSequencer().recordDisable(null); // The "null" means all tracks
\r
2084 int oldSize = trackModelList.size();
\r
2085 if( oldSize > 0 ) {
\r
2086 trackModelList.clear();
\r
2087 fireTableRowsDeleted(0, oldSize-1);
\r
2089 if( (this.sequence = sequence) == null ) {
\r
2090 sequenceTickIndex = null;
\r
2093 // 新しいシーケンスからtickインデックスとトラックリストを再構築
\r
2094 fireTimeSignatureChanged();
\r
2095 Track tracks[] = sequence.getTracks();
\r
2096 for(Track track : tracks) {
\r
2097 trackModelList.add(new TrackEventListTableModel(this, track));
\r
2099 fireTableRowsInserted(0, tracks.length-1);
\r
2102 * 拍子が変更されたとき、シーケンスtickインデックスを再作成します。
\r
2104 public void fireTimeSignatureChanged() {
\r
2105 sequenceTickIndex = new SequenceTickIndex(sequence);
\r
2107 private boolean isModified = false;
\r
2110 * @return 変更済みのときtrue
\r
2112 public boolean isModified() { return isModified; }
\r
2114 * 変更されたかどうかを設定します。
\r
2115 * @param isModified 変更されたときtrue
\r
2117 public void setModified(boolean isModified) { this.isModified = isModified; }
\r
2120 * @param filename ファイル名
\r
2122 public void setFilename(String filename) { this.filename = filename; }
\r
2127 public String getFilename() { return filename; }
\r
2129 public String toString() { return MIDISpec.getNameOf(sequence); }
\r
2132 * @param name シーケンス名
\r
2133 * @return 成功したらtrue
\r
2135 public boolean setName(String name) {
\r
2136 if( name.equals(toString()) || ! MIDISpec.setNameOf(sequence,name) )
\r
2138 setModified(true);
\r
2139 fireTableDataChanged();
\r
2143 * このシーケンスのMIDIデータのバイト列を返します。
\r
2144 * @return MIDIデータのバイト列(失敗した場合null)
\r
2146 public byte[] getMIDIdata() {
\r
2147 if( sequence == null || sequence.getTracks().length == 0 ) {
\r
2150 try( ByteArrayOutputStream out = new ByteArrayOutputStream() ) {
\r
2151 MidiSystem.write(sequence, 1, out);
\r
2152 return out.toByteArray();
\r
2153 } catch ( IOException e ) {
\r
2154 e.printStackTrace();
\r
2159 * 指定のトラックが変更されたことを通知します。
\r
2160 * @param track トラック
\r
2162 public void fireTrackChanged(Track track) {
\r
2163 int row = indexOf(track);
\r
2164 if( row < 0 ) return;
\r
2165 fireTableRowsUpdated(row, row);
\r
2166 sequenceListTableModel.fireSequenceModified(this);
\r
2169 * 選択されているトラックモデルを返します。
\r
2170 * @param index トラックのインデックス
\r
2171 * @return トラックモデル(見つからない場合null)
\r
2173 public TrackEventListTableModel getSelectedTrackModel() {
\r
2174 if( trackListSelectionModel.isSelectionEmpty() )
\r
2176 int index = trackListSelectionModel.getMinSelectionIndex();
\r
2177 Track tracks[] = sequence.getTracks();
\r
2178 if( tracks.length != 0 ) {
\r
2179 Track track = tracks[index];
\r
2180 for( TrackEventListTableModel model : trackModelList )
\r
2181 if( model.getTrack() == track )
\r
2187 * 指定のトラックがある位置のインデックスを返します。
\r
2188 * @param track トラック
\r
2189 * @return トラックのインデックス(先頭 0、トラックが見つからない場合 -1)
\r
2191 public int indexOf(Track track) {
\r
2192 Track tracks[] = sequence.getTracks();
\r
2193 for( int i=0; i<tracks.length; i++ )
\r
2194 if( tracks[i] == track )
\r
2199 * 新しいトラックを生成し、末尾に追加します。
\r
2200 * @return 追加したトラックのインデックス(先頭 0)
\r
2202 public int createTrack() {
\r
2203 Track newTrack = sequence.createTrack();
\r
2204 trackModelList.add(new TrackEventListTableModel(this, newTrack));
\r
2205 int lastRow = getRowCount() - 1;
\r
2206 fireTableRowsInserted(lastRow, lastRow);
\r
2207 sequenceListTableModel.fireSelectedSequenceChanged();
\r
2208 trackListSelectionModel.setSelectionInterval(lastRow, lastRow);
\r
2212 * 選択されているトラックを削除します。
\r
2214 public void deleteSelectedTracks() {
\r
2215 if( trackListSelectionModel.isSelectionEmpty() )
\r
2217 int minIndex = trackListSelectionModel.getMinSelectionIndex();
\r
2218 int maxIndex = trackListSelectionModel.getMaxSelectionIndex();
\r
2219 Track tracks[] = sequence.getTracks();
\r
2220 for( int i = maxIndex; i >= minIndex; i-- ) {
\r
2221 if( ! trackListSelectionModel.isSelectedIndex(i) )
\r
2223 sequence.deleteTrack(tracks[i]);
\r
2224 trackModelList.remove(i);
\r
2226 fireTableRowsDeleted(minIndex, maxIndex);
\r
2227 sequenceListTableModel.fireSelectedSequenceChanged();
\r
2230 * このシーケンスモデルのシーケンスをシーケンサーが操作しているか調べます。
\r
2231 * @return シーケンサーが操作していたらtrue
\r
2233 public boolean isOnSequencer() {
\r
2234 return sequence == sequenceListTableModel.sequencerModel.getSequencer().getSequence();
\r
2237 * 録音しようとしているチャンネルの設定されたトラックがあるか調べます。
\r
2238 * @return 該当トラックがあればtrue
\r
2240 public boolean hasRecordChannel() {
\r
2241 int rowCount = getRowCount();
\r
2242 for( int row=0; row < rowCount; row++ ) {
\r
2243 Object value = getValueAt(row, Column.RECORD_CHANNEL.ordinal());
\r
2244 if( ! "OFF".equals(value) ) return true;
\r
2251 * 選択されているイベントのインデックス
\r
2253 class EventListSelectionModel extends DefaultListSelectionModel {
\r
2255 setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
\r
2259 * MIDIトラック(MIDIイベントリスト)テーブルモデル
\r
2261 class TrackEventListTableModel extends AbstractTableModel {
\r
2265 public enum Column {
\r
2267 EVENT_NUMBER("No.", Integer.class, 20),
\r
2269 TICK_POSITION("TickPos.", Long.class, 40),
\r
2270 /** tick位置に対応する小節 */
\r
2271 MEASURE_POSITION("Measure", Integer.class, 40),
\r
2272 /** tick位置に対応する拍 */
\r
2273 BEAT_POSITION("Beat", Integer.class, 20),
\r
2274 /** tick位置に対応する余剰tick(拍に収まらずに余ったtick数) */
\r
2275 EXTRA_TICK_POSITION("ExTick", Integer.class, 20),
\r
2277 MESSAGE("MIDI Message", String.class, 200);
\r
2278 private String title;
\r
2279 private Class<?> columnClass;
\r
2280 int preferredWidth;
\r
2283 * @param title 列のタイトル
\r
2284 * @param widthRatio 幅の割合
\r
2285 * @param columnClass 列のクラス
\r
2287 private Column(String title, Class<?> columnClass, int preferredWidth) {
\r
2288 this.title = title;
\r
2289 this.columnClass = columnClass;
\r
2290 this.preferredWidth = preferredWidth;
\r
2294 * ラップされているMIDIトラック
\r
2296 private Track track;
\r
2300 SequenceTrackListTableModel sequenceTrackListTableModel;
\r
2302 * 選択されているイベントのインデックス
\r
2304 EventListSelectionModel eventSelectionModel = new EventListSelectionModel();
\r
2306 * シーケンスを親にして、その特定のトラックに連動する
\r
2307 * MIDIトラックモデルを構築します。
\r
2309 * @param parent 親のシーケンスモデル
\r
2310 * @param track ラップするMIDIトラック(ない場合はnull)
\r
2312 public TrackEventListTableModel(
\r
2313 SequenceTrackListTableModel sequenceTrackListTableModel, Track track
\r
2315 this.track = track;
\r
2316 this.sequenceTrackListTableModel = sequenceTrackListTableModel;
\r
2319 public int getRowCount() {
\r
2320 return track == null ? 0 : track.size();
\r
2323 public int getColumnCount() {
\r
2324 return Column.values().length;
\r
2330 public String getColumnName(int column) {
\r
2331 return Column.values()[column].title;
\r
2337 public Class<?> getColumnClass(int column) {
\r
2338 return Column.values()[column].columnClass;
\r
2341 public Object getValueAt(int row, int column) {
\r
2342 switch(Column.values()[column]) {
\r
2343 case EVENT_NUMBER: return row;
\r
2344 case TICK_POSITION: return track.get(row).getTick();
\r
2345 case MEASURE_POSITION:
\r
2346 return sequenceTrackListTableModel.getSequenceTickIndex().tickToMeasure(track.get(row).getTick()) + 1;
\r
2347 case BEAT_POSITION:
\r
2348 sequenceTrackListTableModel.getSequenceTickIndex().tickToMeasure(track.get(row).getTick());
\r
2349 return sequenceTrackListTableModel.getSequenceTickIndex().lastBeat + 1;
\r
2350 case EXTRA_TICK_POSITION:
\r
2351 sequenceTrackListTableModel.getSequenceTickIndex().tickToMeasure(track.get(row).getTick());
\r
2352 return sequenceTrackListTableModel.getSequenceTickIndex().lastExtraTick;
\r
2353 case MESSAGE: return msgToString(track.get(row).getMessage());
\r
2354 default: return "";
\r
2358 * セルを編集できるときtrue、編集できないときfalseを返します。
\r
2361 public boolean isCellEditable(int row, int column) {
\r
2362 switch(Column.values()[column]) {
\r
2363 case TICK_POSITION:
\r
2364 case MEASURE_POSITION:
\r
2365 case BEAT_POSITION:
\r
2366 case EXTRA_TICK_POSITION:
\r
2367 case MESSAGE: return true;
\r
2368 default: return false;
\r
2375 public void setValueAt(Object value, int row, int column) {
\r
2377 switch(Column.values()[column]) {
\r
2378 case TICK_POSITION: newTick = (Long)value; break;
\r
2379 case MEASURE_POSITION:
\r
2380 newTick = sequenceTrackListTableModel.getSequenceTickIndex().measureToTick(
\r
2381 (Integer)value - 1,
\r
2382 (Integer)getValueAt( row, Column.BEAT_POSITION.ordinal() ) - 1,
\r
2383 (Integer)getValueAt( row, Column.EXTRA_TICK_POSITION.ordinal() )
\r
2386 case BEAT_POSITION:
\r
2387 newTick = sequenceTrackListTableModel.getSequenceTickIndex().measureToTick(
\r
2388 (Integer)getValueAt( row, Column.MEASURE_POSITION.ordinal() ) - 1,
\r
2389 (Integer)value - 1,
\r
2390 (Integer)getValueAt( row, Column.EXTRA_TICK_POSITION.ordinal() )
\r
2393 case EXTRA_TICK_POSITION:
\r
2394 newTick = sequenceTrackListTableModel.getSequenceTickIndex().measureToTick(
\r
2395 (Integer)getValueAt( row, Column.MEASURE_POSITION.ordinal() ) - 1,
\r
2396 (Integer)getValueAt( row, Column.BEAT_POSITION.ordinal() ) - 1,
\r
2402 MidiEvent oldMidiEvent = track.get(row);
\r
2403 if( oldMidiEvent.getTick() == newTick ) {
\r
2406 MidiMessage msg = oldMidiEvent.getMessage();
\r
2407 MidiEvent newMidiEvent = new MidiEvent(msg,newTick);
\r
2408 track.remove(oldMidiEvent);
\r
2409 track.add(newMidiEvent);
\r
2410 fireTableDataChanged();
\r
2411 if( MIDISpec.isEOT(msg) ) {
\r
2412 // EOTの場所が変わると曲の長さが変わるので、親モデルへ通知する。
\r
2413 sequenceTrackListTableModel.sequenceListTableModel.fireSequenceModified(sequenceTrackListTableModel);
\r
2418 * @return MIDIトラック
\r
2420 public Track getTrack() { return track; }
\r
2425 public String toString() { return MIDISpec.getNameOf(track); }
\r
2428 * @param name トラック名
\r
2429 * @return 設定が行われたらtrue
\r
2431 public boolean setString(String name) {
\r
2432 if(name.equals(toString()) || ! MIDISpec.setNameOf(track, name))
\r
2434 sequenceTrackListTableModel.setModified(true);
\r
2435 sequenceTrackListTableModel.sequenceListTableModel.fireSequenceModified(sequenceTrackListTableModel);
\r
2436 fireTableDataChanged();
\r
2439 private String recordingChannel = "OFF";
\r
2441 * 録音中のMIDIチャンネルを返します。
\r
2442 * @return 録音中のMIDIチャンネル
\r
2444 public String getRecordingChannel() { return recordingChannel; }
\r
2446 * 録音中のMIDIチャンネルを設定します。
\r
2447 * @param recordingChannel 録音中のMIDIチャンネル
\r
2449 public void setRecordingChannel(String recordingChannel) {
\r
2450 Sequencer sequencer = sequenceTrackListTableModel.sequenceListTableModel.sequencerModel.getSequencer();
\r
2451 if( recordingChannel.equals("OFF") ) {
\r
2452 sequencer.recordDisable( track );
\r
2454 else if( recordingChannel.equals("ALL") ) {
\r
2455 sequencer.recordEnable( track, -1 );
\r
2459 int ch = Integer.decode(recordingChannel).intValue() - 1;
\r
2460 sequencer.recordEnable( track, ch );
\r
2461 } catch( NumberFormatException nfe ) {
\r
2462 sequencer.recordDisable( track );
\r
2463 this.recordingChannel = "OFF";
\r
2467 this.recordingChannel = recordingChannel;
\r
2470 * このトラックの対象MIDIチャンネルを返します。
\r
2471 * <p>全てのチャンネルメッセージが同じMIDIチャンネルの場合、
\r
2472 * そのMIDIチャンネルを返します。
\r
2473 * MIDIチャンネルの異なるチャンネルメッセージが一つでも含まれていた場合、
\r
2476 * @return 対象MIDIチャンネル(不統一の場合 -1)
\r
2478 public int getChannel() {
\r
2480 int trackSize = track.size();
\r
2481 for( int index=0; index < trackSize; index++ ) {
\r
2482 MidiMessage msg = track.get(index).getMessage();
\r
2483 if( ! (msg instanceof ShortMessage) )
\r
2485 ShortMessage smsg = (ShortMessage)msg;
\r
2486 if( ! MIDISpec.isChannelMessage(smsg) )
\r
2488 int ch = smsg.getChannel();
\r
2489 if( prevCh >= 0 && prevCh != ch ) {
\r
2497 * 指定されたMIDIチャンネルをすべてのチャンネルメッセージに対して設定します。
\r
2498 * @param channel MIDIチャンネル
\r
2500 public void setChannel(int channel) {
\r
2501 int track_size = track.size();
\r
2502 for( int index=0; index < track_size; index++ ) {
\r
2503 MidiMessage msg = track.get(index).getMessage();
\r
2504 if( ! (msg instanceof ShortMessage) )
\r
2506 ShortMessage smsg = (ShortMessage)msg;
\r
2507 if( ! MIDISpec.isChannelMessage(smsg) )
\r
2509 if( smsg.getChannel() == channel )
\r
2513 smsg.getCommand(), channel,
\r
2514 smsg.getData1(), smsg.getData2()
\r
2517 catch( InvalidMidiDataException e ) {
\r
2518 e.printStackTrace();
\r
2520 sequenceTrackListTableModel.setModified(true);
\r
2522 sequenceTrackListTableModel.fireTrackChanged(track);
\r
2523 fireTableDataChanged();
\r
2526 * 指定の MIDI tick 位置にあるイベントを二分探索し、
\r
2527 * そのイベントの行インデックスを返します。
\r
2528 * @param tick MIDI tick
\r
2531 public int tickToIndex(long tick) {
\r
2532 if( track == null )
\r
2535 int maxIndex = track.size() - 1;
\r
2536 while( minIndex < maxIndex ) {
\r
2537 int currentIndex = (minIndex + maxIndex) / 2 ;
\r
2538 long currentTick = track.get(currentIndex).getTick();
\r
2539 if( tick > currentTick ) {
\r
2540 minIndex = currentIndex + 1;
\r
2542 else if( tick < currentTick ) {
\r
2543 maxIndex = currentIndex - 1;
\r
2546 return currentIndex;
\r
2549 return (minIndex + maxIndex) / 2;
\r
2552 * NoteOn/NoteOff ペアの一方の行インデックスから、
\r
2553 * もう一方(ペアの相手)の行インデックスを返します。
\r
2554 * @param index 行インデックス
\r
2555 * @return ペアを構成する相手の行インデックス(ない場合は -1)
\r
2557 public int getIndexOfPartnerFor(int index) {
\r
2558 if( track == null || index >= track.size() )
\r
2560 MidiMessage msg = track.get(index).getMessage();
\r
2561 if( ! (msg instanceof ShortMessage) ) return -1;
\r
2562 ShortMessage sm = (ShortMessage)msg;
\r
2563 int cmd = sm.getCommand();
\r
2565 int ch = sm.getChannel();
\r
2566 int note = sm.getData1();
\r
2567 MidiMessage partner_msg;
\r
2568 ShortMessage partner_sm;
\r
2572 case 0x90: // NoteOn
\r
2573 if( sm.getData2() > 0 ) {
\r
2574 // Search NoteOff event forward
\r
2575 for( i = index + 1; i < track.size(); i++ ) {
\r
2576 partner_msg = track.get(i).getMessage();
\r
2577 if( ! (partner_msg instanceof ShortMessage ) ) continue;
\r
2578 partner_sm = (ShortMessage)partner_msg;
\r
2579 partner_cmd = partner_sm.getCommand();
\r
2580 if( partner_cmd != 0x80 && partner_cmd != 0x90 ||
\r
2581 partner_cmd == 0x90 && partner_sm.getData2() > 0
\r
2586 if( ch != partner_sm.getChannel() || note != partner_sm.getData1() ) {
\r
2594 // When velocity is 0, it means Note Off, so no break.
\r
2595 case 0x80: // NoteOff
\r
2596 // Search NoteOn event backward
\r
2597 for( i = index - 1; i >= 0; i-- ) {
\r
2598 partner_msg = track.get(i).getMessage();
\r
2599 if( ! (partner_msg instanceof ShortMessage ) ) continue;
\r
2600 partner_sm = (ShortMessage)partner_msg;
\r
2601 partner_cmd = partner_sm.getCommand();
\r
2602 if( partner_cmd != 0x90 || partner_sm.getData2() <= 0 ) {
\r
2606 if( ch != partner_sm.getChannel() || note != partner_sm.getData1() ) {
\r
2618 * ノートメッセージかどうか調べます。
\r
2619 * @param index 行インデックス
\r
2620 * @return Note On または Note Off のとき true
\r
2622 public boolean isNote(int index) {
\r
2623 MidiEvent midi_evt = getMidiEvent(index);
\r
2624 MidiMessage msg = midi_evt.getMessage();
\r
2625 if( ! (msg instanceof ShortMessage) ) return false;
\r
2626 int cmd = ((ShortMessage)msg).getCommand();
\r
2627 return cmd == ShortMessage.NOTE_ON || cmd == ShortMessage.NOTE_OFF ;
\r
2630 * 指定の行インデックスのMIDIイベントを返します。
\r
2631 * @param index 行インデックス
\r
2632 * @return MIDIイベント
\r
2634 public MidiEvent getMidiEvent(int index) { return track.get(index); }
\r
2636 * 選択されているMIDIイベントを返します。
\r
2637 * @return 選択されているMIDIイベント
\r
2639 public MidiEvent[] getSelectedMidiEvents() {
\r
2640 Vector<MidiEvent> events = new Vector<MidiEvent>();
\r
2641 if( ! eventSelectionModel.isSelectionEmpty() ) {
\r
2642 int i = eventSelectionModel.getMinSelectionIndex();
\r
2643 int max = eventSelectionModel.getMaxSelectionIndex();
\r
2644 for( ; i <= max; i++ )
\r
2645 if( eventSelectionModel.isSelectedIndex(i) )
\r
2646 events.add(track.get(i));
\r
2648 return events.toArray(new MidiEvent[1]);
\r
2652 * @param midiEvent 追加するMIDIイベント
\r
2653 * @return 追加できたらtrue
\r
2655 public boolean addMidiEvent(MidiEvent midiEvent) {
\r
2656 if( !(track.add(midiEvent)) )
\r
2658 if( MIDISpec.isTimeSignature(midiEvent.getMessage()) )
\r
2659 sequenceTrackListTableModel.fireTimeSignatureChanged();
\r
2660 sequenceTrackListTableModel.fireTrackChanged(track);
\r
2661 int last_index = track.size() - 1;
\r
2662 fireTableRowsInserted( last_index-1, last_index-1 );
\r
2667 * @param midiEvents 追加するMIDIイベント
\r
2668 * @param destinationTick 追加先tick
\r
2669 * @param sourcePPQ PPQ値(タイミング解像度)
\r
2670 * @return 追加できたらtrue
\r
2672 public boolean addMidiEvents(MidiEvent midiEvents[], long destinationTick, int sourcePPQ) {
\r
2673 int destinationPPQ = sequenceTrackListTableModel.getSequence().getResolution();
\r
2674 boolean done = false;
\r
2675 boolean hasTimeSignature = false;
\r
2676 long firstSourceEventTick = -1;
\r
2677 for( MidiEvent sourceEvent : midiEvents ) {
\r
2678 long sourceEventTick = sourceEvent.getTick();
\r
2679 MidiMessage msg = sourceEvent.getMessage();
\r
2680 long newTick = destinationTick;
\r
2681 if( firstSourceEventTick < 0 ) {
\r
2682 firstSourceEventTick = sourceEventTick;
\r
2685 newTick += (sourceEventTick - firstSourceEventTick) * destinationPPQ / sourcePPQ;
\r
2687 if( ! track.add(new MidiEvent(msg, newTick)) ) continue;
\r
2689 if( MIDISpec.isTimeSignature(msg) ) hasTimeSignature = true;
\r
2692 if( hasTimeSignature ) sequenceTrackListTableModel.fireTimeSignatureChanged();
\r
2693 sequenceTrackListTableModel.fireTrackChanged(track);
\r
2694 int lastIndex = track.size() - 1;
\r
2695 int oldLastIndex = lastIndex - midiEvents.length;
\r
2696 fireTableRowsInserted(oldLastIndex, lastIndex);
\r
2702 * 曲の長さが変わることがあるので、プレイリストにも通知します。
\r
2703 * @param midiEvents 除去するMIDIイベント
\r
2705 public void removeMidiEvents(MidiEvent midiEvents[]) {
\r
2706 boolean hadTimeSignature = false;
\r
2707 for( MidiEvent e : midiEvents ) {
\r
2708 if( MIDISpec.isTimeSignature(e.getMessage()) )
\r
2709 hadTimeSignature = true;
\r
2712 if( hadTimeSignature ) {
\r
2713 sequenceTrackListTableModel.fireTimeSignatureChanged();
\r
2715 sequenceTrackListTableModel.fireTrackChanged(track);
\r
2716 int lastIndex = track.size() - 1;
\r
2717 int oldLastIndex = lastIndex + midiEvents.length;
\r
2718 if(lastIndex < 0) lastIndex = 0;
\r
2719 fireTableRowsDeleted(oldLastIndex, lastIndex);
\r
2720 sequenceTrackListTableModel.sequenceListTableModel.fireSelectedSequenceChanged();
\r
2723 * 引数の選択内容が示すMIDIイベントを除去します。
\r
2724 * @param selectionModel 選択内容
\r
2726 public void removeSelectedMidiEvents() {
\r
2727 removeMidiEvents(getSelectedMidiEvents());
\r
2729 private boolean isRhythmPart(int ch) { return (ch == 9); }
\r
2731 * MIDIメッセージの内容を文字列で返します。
\r
2732 * @param msg MIDIメッセージ
\r
2733 * @return MIDIメッセージの内容を表す文字列
\r
2735 public String msgToString(MidiMessage msg) {
\r
2737 if( msg instanceof ShortMessage ) {
\r
2738 ShortMessage shortmsg = (ShortMessage)msg;
\r
2739 int status = msg.getStatus();
\r
2740 String status_name = MIDISpec.getStatusName(status);
\r
2741 int data1 = shortmsg.getData1();
\r
2742 int data2 = shortmsg.getData2();
\r
2743 if( MIDISpec.isChannelMessage(status) ) {
\r
2744 int ch = shortmsg.getChannel();
\r
2745 String ch_prefix = "Ch."+(ch+1) + ": ";
\r
2746 String status_prefix = (
\r
2747 status_name == null ? String.format("status=0x%02X",status) : status_name
\r
2749 int cmd = shortmsg.getCommand();
\r
2751 case ShortMessage.NOTE_OFF:
\r
2752 case ShortMessage.NOTE_ON:
\r
2753 str += ch_prefix + status_prefix + data1;
\r
2755 if( isRhythmPart(ch) ) {
\r
2756 str += MIDISpec.getPercussionName(data1);
\r
2759 str += Music.NoteSymbol.noteNoToSymbol(data1);
\r
2761 str +="] Velocity=" + data2;
\r
2763 case ShortMessage.POLY_PRESSURE:
\r
2764 str += ch_prefix + status_prefix + "Note=" + data1 + " Pressure=" + data2;
\r
2766 case ShortMessage.PROGRAM_CHANGE:
\r
2767 str += ch_prefix + status_prefix + data1 + ":[" + MIDISpec.instrument_names[data1] + "]";
\r
2768 if( data2 != 0 ) str += " data2=" + data2;
\r
2770 case ShortMessage.CHANNEL_PRESSURE:
\r
2771 str += ch_prefix + status_prefix + data1;
\r
2772 if( data2 != 0 ) str += " data2=" + data2;
\r
2774 case ShortMessage.PITCH_BEND:
\r
2776 int val = ((data1 & 0x7F) | ((data2 & 0x7F) << 7));
\r
2777 str += ch_prefix + status_prefix + ( (val-8192) * 100 / 8191) + "% (" + val + ")";
\r
2780 case ShortMessage.CONTROL_CHANGE:
\r
2782 // Control / Mode message name
\r
2783 String ctrl_name = MIDISpec.getControllerName(data1);
\r
2784 str += ch_prefix + (data1 < 0x78 ? "CtrlChg: " : "ModeMsg: ");
\r
2785 if( ctrl_name == null ) {
\r
2786 str += " No.=" + data1 + " Value=" + data2;
\r
2791 // Controller's value
\r
2793 case 0x40: case 0x41: case 0x42: case 0x43: case 0x45:
\r
2794 str += " " + ( data2==0x3F?"OFF":data2==0x40?"ON":data2 );
\r
2796 case 0x44: // Legato Footswitch
\r
2797 str += " " + ( data2==0x3F?"Normal":data2==0x40?"Legato":data2 );
\r
2799 case 0x7A: // Local Control
\r
2800 str += " " + ( data2==0x00?"OFF":data2==0x7F?"ON":data2 );
\r
2803 str += " " + data2;
\r
2810 // Never reached here
\r
2814 else { // System Message
\r
2815 str += (status_name == null ? ("status="+status) : status_name );
\r
2816 str += " (" + data1 + "," + data2 + ")";
\r
2820 else if( msg instanceof MetaMessage ) {
\r
2821 MetaMessage metamsg = (MetaMessage)msg;
\r
2822 byte[] msgdata = metamsg.getData();
\r
2823 int msgtype = metamsg.getType();
\r
2825 String meta_name = MIDISpec.getMetaName(msgtype);
\r
2826 if( meta_name == null ) {
\r
2827 str += "Unknown MessageType="+msgtype + " Values=(";
\r
2828 for( byte b : msgdata ) str += String.format( " %02X", b );
\r
2832 // Add the message type name
\r
2835 // Add the text data
\r
2836 if( MIDISpec.hasMetaText(msgtype) ) {
\r
2837 str +=" ["+(new String(msgdata))+"]";
\r
2840 // Add the numeric data
\r
2842 case 0x00: // Sequence Number (for MIDI Format 2)
\r
2843 if( msgdata.length == 2 ) {
\r
2844 str += String.format(
\r
2846 ((msgdata[0] & 0xFF) << 8) | (msgdata[1] & 0xFF)
\r
2850 str += ": Size not 2 byte : data=(";
\r
2851 for( byte b : msgdata ) str += String.format( " %02X", b );
\r
2854 case 0x20: // MIDI Ch.Prefix
\r
2855 case 0x21: // MIDI Output Port
\r
2856 if( msgdata.length == 1 ) {
\r
2857 str += String.format( ": %02X", msgdata[0] & 0xFF );
\r
2860 str += ": Size not 1 byte : data=(";
\r
2861 for( byte b : msgdata ) str += String.format( " %02X", b );
\r
2864 case 0x51: // Tempo
\r
2865 str += ": " + MIDISpec.byteArrayToQpmTempo( msgdata ) + "[QPM] (";
\r
2866 for( byte b : msgdata ) str += String.format( " %02X", b );
\r
2869 case 0x54: // SMPTE Offset
\r
2870 if( msgdata.length == 5 ) {
\r
2872 + (msgdata[0] & 0xFF) + ":"
\r
2873 + (msgdata[1] & 0xFF) + ":"
\r
2874 + (msgdata[2] & 0xFF) + "."
\r
2875 + (msgdata[3] & 0xFF) + "."
\r
2876 + (msgdata[4] & 0xFF);
\r
2879 str += ": Size not 5 byte : data=(";
\r
2880 for( byte b : msgdata ) str += String.format( " %02X", b );
\r
2883 case 0x58: // Time Signature
\r
2884 if( msgdata.length == 4 ) {
\r
2885 str +=": " + msgdata[0] + "/" + (1 << msgdata[1]);
\r
2886 str +=", "+msgdata[2]+"[clk/beat], "+msgdata[3]+"[32nds/24clk]";
\r
2889 str += ": Size not 4 byte : data=(";
\r
2890 for( byte b : msgdata ) str += String.format( " %02X", b );
\r
2893 case 0x59: // Key Signature
\r
2894 if( msgdata.length == 2 ) {
\r
2895 Music.Key key = new Music.Key(msgdata);
\r
2896 str += ": " + key.signatureDescription();
\r
2897 str += " (" + key.toStringIn(Music.SymbolLanguage.NAME) + ")";
\r
2900 str += ": Size not 2 byte : data=(";
\r
2901 for( byte b : msgdata ) str += String.format( " %02X", b );
\r
2904 case 0x7F: // Sequencer Specific Meta Event
\r
2906 for( byte b : msgdata ) str += String.format( " %02X", b );
\r
2912 else if( msg instanceof SysexMessage ) {
\r
2913 SysexMessage sysexmsg = (SysexMessage)msg;
\r
2914 int status = sysexmsg.getStatus();
\r
2915 byte[] msgdata = sysexmsg.getData();
\r
2916 int data_byte_pos = 1;
\r
2917 switch( status ) {
\r
2918 case SysexMessage.SYSTEM_EXCLUSIVE:
\r
2921 case SysexMessage.SPECIAL_SYSTEM_EXCLUSIVE:
\r
2922 str += "SysEx(Special): ";
\r
2925 str += "SysEx: Invalid (status="+status+") ";
\r
2928 if( msgdata.length < 1 ) {
\r
2929 str += " Invalid data size: " + msgdata.length;
\r
2932 int manufacturer_id = (int)(msgdata[0] & 0xFF );
\r
2933 int device_id = (int)(msgdata[1] & 0xFF);
\r
2934 int model_id = (int)(msgdata[2] & 0xFF);
\r
2935 String manufacturer_name = MIDISpec.getSysExManufacturerName(manufacturer_id);
\r
2936 if( manufacturer_name == null ) {
\r
2937 manufacturer_name = String.format( "[Manufacturer code %02X]", msgdata[0] );
\r
2939 str += manufacturer_name + String.format( " (DevID=0x%02X)", device_id );
\r
2940 switch( manufacturer_id ) {
\r
2941 case 0x7E: // Non-Realtime Universal
\r
2943 int sub_id_1 = (int)(msgdata[2] & 0xFF);
\r
2944 int sub_id_2 = (int)(msgdata[3] & 0xFF);
\r
2945 switch( sub_id_1 ) {
\r
2946 case 0x09: // General MIDI (GM)
\r
2947 switch( sub_id_2 ) {
\r
2948 case 0x01: str += " GM System ON"; return str;
\r
2949 case 0x02: str += " GM System OFF"; return str;
\r
2956 // case 0x7F: // Realtime Universal
\r
2957 case 0x41: // Roland
\r
2959 switch( model_id ) {
\r
2961 str += " [GS]"; data_byte_pos++;
\r
2962 if( msgdata[3]==0x12 ) {
\r
2963 str += "DT1:"; data_byte_pos++;
\r
2964 switch( msgdata[4] ) {
\r
2966 if( msgdata[5]==0x00 ) {
\r
2967 if( msgdata[6]==0x7F ) {
\r
2968 if( msgdata[7]==0x00 ) {
\r
2969 str += " [88] System Mode Set (Mode 1: Single Module)"; return str;
\r
2971 else if( msgdata[7]==0x01 ) {
\r
2972 str += " [88] System Mode Set (Mode 2: Double Module)"; return str;
\r
2976 else if( msgdata[5]==0x01 ) {
\r
2977 int port = (msgdata[7] & 0xFF);
\r
2978 str += String.format(
\r
2979 " [88] Ch.Msg Rx Port: Block=0x%02X, Port=%s",
\r
2981 port==0?"A":port==1?"B":String.format("0x%02X",port)
\r
2987 if( msgdata[5]==0x00 ) {
\r
2988 switch( msgdata[6] ) {
\r
2989 case 0x00: str += " Master Tune: "; data_byte_pos += 3; break;
\r
2990 case 0x04: str += " Master Volume: "; data_byte_pos += 3; break;
\r
2991 case 0x05: str += " Master Key Shift: "; data_byte_pos += 3; break;
\r
2992 case 0x06: str += " Master Pan: "; data_byte_pos += 3; break;
\r
2994 switch( msgdata[7] ) {
\r
2995 case 0x00: str += " GS Reset"; return str;
\r
2996 case 0x7F: str += " Exit GS Mode"; return str;
\r
3001 else if( msgdata[5]==0x01 ) {
\r
3002 switch( msgdata[6] ) {
\r
3003 // case 0x00: str += ""; break;
\r
3004 // case 0x10: str += ""; break;
\r
3005 case 0x30: str += " Reverb Macro: "; data_byte_pos += 3; break;
\r
3006 case 0x31: str += " Reverb Character: "; data_byte_pos += 3; break;
\r
3007 case 0x32: str += " Reverb Pre-LPF: "; data_byte_pos += 3; break;
\r
3008 case 0x33: str += " Reverb Level: "; data_byte_pos += 3; break;
\r
3009 case 0x34: str += " Reverb Time: "; data_byte_pos += 3; break;
\r
3010 case 0x35: str += " Reverb Delay FB: "; data_byte_pos += 3; break;
\r
3011 case 0x36: str += " Reverb Chorus Level: "; data_byte_pos += 3; break;
\r
3012 case 0x37: str += " [88] Reverb Predelay Time: "; data_byte_pos += 3; break;
\r
3013 case 0x38: str += " Chorus Macro: "; data_byte_pos += 3; break;
\r
3014 case 0x39: str += " Chorus Pre-LPF: "; data_byte_pos += 3; break;
\r
3015 case 0x3A: str += " Chorus Level: "; data_byte_pos += 3; break;
\r
3016 case 0x3B: str += " Chorus FB: "; data_byte_pos += 3; break;
\r
3017 case 0x3C: str += " Chorus Delay: "; data_byte_pos += 3; break;
\r
3018 case 0x3D: str += " Chorus Rate: "; data_byte_pos += 3; break;
\r
3019 case 0x3E: str += " Chorus Depth: "; data_byte_pos += 3; break;
\r
3020 case 0x3F: str += " Chorus Send Level To Reverb: "; data_byte_pos += 3; break;
\r
3021 case 0x40: str += " [88] Chorus Send Level To Delay: "; data_byte_pos += 3; break;
\r
3022 case 0x50: str += " [88] Delay Macro: "; data_byte_pos += 3; break;
\r
3023 case 0x51: str += " [88] Delay Pre-LPF: "; data_byte_pos += 3; break;
\r
3024 case 0x52: str += " [88] Delay Time Center: "; data_byte_pos += 3; break;
\r
3025 case 0x53: str += " [88] Delay Time Ratio Left: "; data_byte_pos += 3; break;
\r
3026 case 0x54: str += " [88] Delay Time Ratio Right: "; data_byte_pos += 3; break;
\r
3027 case 0x55: str += " [88] Delay Level Center: "; data_byte_pos += 3; break;
\r
3028 case 0x56: str += " [88] Delay Level Left: "; data_byte_pos += 3; break;
\r
3029 case 0x57: str += " [88] Delay Level Right: "; data_byte_pos += 3; break;
\r
3030 case 0x58: str += " [88] Delay Level: "; data_byte_pos += 3; break;
\r
3031 case 0x59: str += " [88] Delay FB: "; data_byte_pos += 3; break;
\r
3032 case 0x5A: str += " [88] Delay Send Level To Reverb: "; data_byte_pos += 3; break;
\r
3035 else if( msgdata[5]==0x02 ) {
\r
3036 switch( msgdata[6] ) {
\r
3037 case 0x00: str += " [88] EQ Low Freq: "; data_byte_pos += 3; break;
\r
3038 case 0x01: str += " [88] EQ Low Gain: "; data_byte_pos += 3; break;
\r
3039 case 0x02: str += " [88] EQ High Freq: "; data_byte_pos += 3; break;
\r
3040 case 0x03: str += " [88] EQ High Gain: "; data_byte_pos += 3; break;
\r
3043 else if( msgdata[5]==0x03 ) {
\r
3044 if( msgdata[6] == 0x00 ) {
\r
3045 str += " [Pro] EFX Type: "; data_byte_pos += 3;
\r
3047 else if( msgdata[6] >= 0x03 && msgdata[6] <= 0x16 ) {
\r
3048 str += String.format(" [Pro] EFX Param %d", msgdata[6]-2 );
\r
3049 data_byte_pos += 3;
\r
3051 else if( msgdata[6] == 0x17 ) {
\r
3052 str += " [Pro] EFX Send Level To Reverb: "; data_byte_pos += 3;
\r
3054 else if( msgdata[6] == 0x18 ) {
\r
3055 str += " [Pro] EFX Send Level To Chorus: "; data_byte_pos += 3;
\r
3057 else if( msgdata[6] == 0x19 ) {
\r
3058 str += " [Pro] EFX Send Level To Delay: "; data_byte_pos += 3;
\r
3060 else if( msgdata[6] == 0x1B ) {
\r
3061 str += " [Pro] EFX Ctrl Src1: "; data_byte_pos += 3;
\r
3063 else if( msgdata[6] == 0x1C ) {
\r
3064 str += " [Pro] EFX Ctrl Depth1: "; data_byte_pos += 3;
\r
3066 else if( msgdata[6] == 0x1D ) {
\r
3067 str += " [Pro] EFX Ctrl Src2: "; data_byte_pos += 3;
\r
3069 else if( msgdata[6] == 0x1E ) {
\r
3070 str += " [Pro] EFX Ctrl Depth2: "; data_byte_pos += 3;
\r
3072 else if( msgdata[6] == 0x1F ) {
\r
3073 str += " [Pro] EFX Send EQ Switch: "; data_byte_pos += 3;
\r
3076 else if( (msgdata[5] & 0xF0) == 0x10 ) {
\r
3077 int ch = (msgdata[5] & 0x0F);
\r
3078 if( ch <= 9 ) ch--; else if( ch == 0 ) ch = 9;
\r
3079 if( msgdata[6]==0x02 ) {
\r
3080 str += String.format(
\r
3081 " Rx Ch: Part=%d(0x%02X) Ch=0x%02X", (ch+1), msgdata[5], msgdata[7]
\r
3085 else if( msgdata[6]==0x15 ) {
\r
3087 switch( msgdata[7] ) {
\r
3088 case 0: map = " NormalPart"; break;
\r
3089 case 1: map = " DrumMap1"; break;
\r
3090 case 2: map = " DrumMap2"; break;
\r
3091 default: map = String.format("0x%02X",msgdata[7]); break;
\r
3093 str += String.format(
\r
3094 " Rhythm Part: Ch=%d(0x%02X) Map=%s",
\r
3095 (ch+1), msgdata[5],
\r
3101 else if( (msgdata[5] & 0xF0) == 0x40 ) {
\r
3102 int ch = (msgdata[5] & 0x0F);
\r
3103 if( ch <= 9 ) ch--; else if( ch == 0 ) ch = 9;
\r
3104 int dt = (msgdata[7] & 0xFF);
\r
3105 if( msgdata[6]==0x20 ) {
\r
3106 str += String.format(
\r
3107 " [88] EQ: Ch=%d(0x%02X) %s",
\r
3108 (ch+1), msgdata[5],
\r
3109 dt==0 ? "OFF" : dt==1 ? "ON" : String.format("0x%02X",dt)
\r
3112 else if( msgdata[6]==0x22 ) {
\r
3113 str += String.format(
\r
3114 " [Pro] Part EFX Assign: Ch=%d(0x%02X) %s",
\r
3115 (ch+1), msgdata[5],
\r
3116 dt==0 ? "ByPass" : dt==1 ? "EFX" : String.format("0x%02X",dt)
\r
3125 str += " [GS-LCD]"; data_byte_pos++;
\r
3126 if( msgdata[3]==0x12 ) {
\r
3127 str += " [DT1]"; data_byte_pos++;
\r
3128 if( msgdata[4]==0x10 && msgdata[5]==0x00 && msgdata[6]==0x00 ) {
\r
3129 data_byte_pos += 3;
\r
3130 str += " Disp [" +(new String(
\r
3131 msgdata, data_byte_pos, msgdata.length - data_byte_pos - 2
\r
3136 case 0x14: str += " [D-50]"; data_byte_pos++; break;
\r
3137 case 0x16: str += " [MT-32]"; data_byte_pos++; break;
\r
3140 case 0x43: // Yamaha (XG)
\r
3142 if( model_id == 0x4C ) {
\r
3144 if( msgdata[3]==0 && msgdata[4]==0 && msgdata[5]==0x7E && msgdata[6]==0 ) {
\r
3145 str += " XG System ON"; return str;
\r
3155 for( i = data_byte_pos; i<msgdata.length-1; i++ ) {
\r
3156 str += String.format( " %02X", msgdata[i] );
\r
3158 if( i < msgdata.length && (int)(msgdata[i] & 0xFF) != 0xF7 ) {
\r
3159 str+=" [ Invalid EOX " + String.format( "%02X", msgdata[i] ) + " ]";
\r
3164 byte[] msg_data = msg.getMessage();
\r
3166 for( byte b : msg_data ) {
\r
3167 str += String.format( " %02X", b );
\r
3175 * MIDI シーケンスデータのtickインデックス
\r
3176 * <p>拍子、テンポ、調だけを抜き出したトラックを保持するためのインデックスです。
\r
3177 * 指定の MIDI tick の位置におけるテンポ、調、拍子を取得したり、
\r
3178 * 拍子情報から MIDI tick と小節位置との間の変換を行うために使います。
\r
3181 class SequenceTickIndex {
\r
3185 public static final int TEMPO = 0;
\r
3189 public static final int TIME_SIGNATURE = 1;
\r
3193 public static final int KEY_SIGNATURE = 2;
\r
3195 * メタメッセージタイプ → メタメッセージの種類 変換マップ
\r
3197 private static final Map<Integer,Integer> INDEX_META_TO_TRACK =
\r
3198 new HashMap<Integer,Integer>() {
\r
3201 put(0x58, TIME_SIGNATURE);
\r
3202 put(0x59, KEY_SIGNATURE);
\r
3206 * 新しいMIDIシーケンスデータのインデックスを構築します。
\r
3207 * @param sourceSequence 元のMIDIシーケンス
\r
3209 public SequenceTickIndex(Sequence sourceSequence) {
\r
3211 int ppq = sourceSequence.getResolution();
\r
3212 wholeNoteTickLength = ppq * 4;
\r
3213 tmpSequence = new Sequence(Sequence.PPQ, ppq, 3);
\r
3214 tracks = tmpSequence.getTracks();
\r
3215 Track[] sourceTracks = sourceSequence.getTracks();
\r
3216 for( Track tk : sourceTracks ) {
\r
3217 for( int i_evt = 0 ; i_evt < tk.size(); i_evt++ ) {
\r
3218 MidiEvent evt = tk.get(i_evt);
\r
3219 MidiMessage msg = evt.getMessage();
\r
3220 if( ! (msg instanceof MetaMessage) )
\r
3222 MetaMessage metaMsg = (MetaMessage)msg;
\r
3223 int metaType = metaMsg.getType();
\r
3224 Integer metaIndex = INDEX_META_TO_TRACK.get(metaType);
\r
3225 if( metaIndex != null ) tracks[metaIndex].add(evt);
\r
3229 catch ( InvalidMidiDataException e ) {
\r
3230 e.printStackTrace();
\r
3233 private Sequence tmpSequence;
\r
3235 * このtickインデックスのタイミング解像度を返します。
\r
3236 * @return このtickインデックスのタイミング解像度
\r
3238 public int getResolution() {
\r
3239 return tmpSequence.getResolution();
\r
3241 private Track[] tracks;
\r
3243 * 指定されたtick位置以前の最後のメタメッセージを返します。
\r
3244 * @param trackIndex メタメッセージの種類()
\r
3245 * @param tickPosition
\r
3248 public MetaMessage lastMetaMessageAt(int trackIndex, long tickPosition) {
\r
3249 Track track = tracks[trackIndex];
\r
3250 for(int eventIndex = track.size()-1 ; eventIndex >= 0; eventIndex--) {
\r
3251 MidiEvent event = track.get(eventIndex);
\r
3252 if( event.getTick() > tickPosition )
\r
3254 MetaMessage metaMessage = (MetaMessage)(event.getMessage());
\r
3255 if( metaMessage.getType() == 0x2F /* skip EOT (last event) */ )
\r
3257 return metaMessage;
\r
3262 private int wholeNoteTickLength;
\r
3263 public int lastBeat;
\r
3264 public int lastExtraTick;
\r
3265 public byte timesigUpper;
\r
3266 public byte timesigLowerIndex;
\r
3268 * tick位置を小節位置に変換します。
\r
3269 * @param tickPosition tick位置
\r
3272 int tickToMeasure(long tickPosition) {
\r
3273 byte extraBeats = 0;
\r
3274 MidiEvent event = null;
\r
3275 MidiMessage message = null;
\r
3276 byte[] data = null;
\r
3277 long currentTick = 0L;
\r
3278 long nextTimesigTick = 0L;
\r
3279 long prevTick = 0L;
\r
3280 long duration = 0L;
\r
3281 int lastMeasure = 0;
\r
3282 int eventIndex = 0;
\r
3284 timesigLowerIndex = 2; // =log2(4)
\r
3285 if( tracks[TIME_SIGNATURE] != null ) {
\r
3287 // Check current time-signature event
\r
3288 if( eventIndex < tracks[TIME_SIGNATURE].size() ) {
\r
3289 message = (event = tracks[TIME_SIGNATURE].get(eventIndex)).getMessage();
\r
3290 currentTick = nextTimesigTick = event.getTick();
\r
3291 if(currentTick > tickPosition || (message.getStatus() == 0xFF && ((MetaMessage)message).getType() == 0x2F /* EOT */)) {
\r
3292 currentTick = tickPosition;
\r
3295 else { // No event
\r
3296 currentTick = nextTimesigTick = tickPosition;
\r
3298 // Add measure from last event
\r
3300 int beatTickLength = wholeNoteTickLength >> timesigLowerIndex;
\r
3301 duration = currentTick - prevTick;
\r
3302 int beats = (int)( duration / beatTickLength );
\r
3303 lastExtraTick = (int)(duration % beatTickLength);
\r
3304 int measures = beats / timesigUpper;
\r
3305 extraBeats = (byte)(beats % timesigUpper);
\r
3306 lastMeasure += measures;
\r
3307 if( nextTimesigTick > tickPosition ) break; // Not reached to next time signature
\r
3309 // Reached to the next time signature, so get it.
\r
3310 if( ( data = ((MetaMessage)message).getData() ).length > 0 ) { // To skip EOT, check the data length.
\r
3311 timesigUpper = data[0];
\r
3312 timesigLowerIndex = data[1];
\r
3314 if( currentTick == tickPosition ) break; // Calculation complete
\r
3316 // Calculation incomplete, so prepare for next
\r
3318 if( extraBeats > 0 ) {
\r
3320 // Extra beats are treated as 1 measure
\r
3323 prevTick = currentTick;
\r
3327 lastBeat = extraBeats;
\r
3328 return lastMeasure;
\r
3331 * 小節位置を MIDI tick に変換します。
\r
3332 * @param measure 小節位置
\r
3333 * @return MIDI tick
\r
3335 public long measureToTick(int measure) {
\r
3336 return measureToTick(measure, 0, 0);
\r
3339 * 指定の小節位置、拍、拍内tickを、そのシーケンス全体の MIDI tick に変換します。
\r
3340 * @param measure 小節位置
\r
3342 * @param extraTick 拍内tick
\r
3343 * @return そのシーケンス全体の MIDI tick
\r
3345 public long measureToTick(int measure, int beat, int extraTick) {
\r
3346 MidiEvent evt = null;
\r
3347 MidiMessage msg = null;
\r
3348 byte[] data = null;
\r
3350 long prev_tick = 0L;
\r
3351 long duration = 0L;
\r
3352 long duration_sum = 0L;
\r
3353 long estimated_ticks;
\r
3354 int ticks_per_beat;
\r
3357 timesigLowerIndex = 2; // =log2(4)
\r
3359 ticks_per_beat = wholeNoteTickLength >> timesigLowerIndex;
\r
3360 estimated_ticks = ((measure * timesigUpper) + beat) * ticks_per_beat + extraTick;
\r
3361 if( tracks[TIME_SIGNATURE] == null || i_evt > tracks[TIME_SIGNATURE].size() ) {
\r
3362 return duration_sum + estimated_ticks;
\r
3364 msg = (evt = tracks[TIME_SIGNATURE].get(i_evt)).getMessage();
\r
3365 if( msg.getStatus() == 0xFF && ((MetaMessage)msg).getType() == 0x2F /* EOT */ ) {
\r
3366 return duration_sum + estimated_ticks;
\r
3368 duration = (tick = evt.getTick()) - prev_tick;
\r
3369 if( duration >= estimated_ticks ) {
\r
3370 return duration_sum + estimated_ticks;
\r
3372 // Re-calculate measure (ignore extra beats/ticks)
\r
3373 measure -= ( duration / (ticks_per_beat * timesigUpper) );
\r
3374 duration_sum += duration;
\r
3376 // Get next time-signature
\r
3377 data = ( (MetaMessage)msg ).getData();
\r
3378 timesigUpper = data[0];
\r
3379 timesigLowerIndex = data[1];
\r