2 import java.awt.Component;
\r
3 import java.awt.Container;
\r
4 import java.awt.Dimension;
\r
5 import java.awt.FlowLayout;
\r
6 import java.awt.Insets;
\r
7 import java.awt.datatransfer.DataFlavor;
\r
8 import java.awt.datatransfer.Transferable;
\r
9 import java.awt.dnd.DnDConstants;
\r
10 import java.awt.dnd.DropTarget;
\r
11 import java.awt.dnd.DropTargetDragEvent;
\r
12 import java.awt.dnd.DropTargetDropEvent;
\r
13 import java.awt.dnd.DropTargetEvent;
\r
14 import java.awt.dnd.DropTargetListener;
\r
15 import java.awt.event.ActionEvent;
\r
16 import java.awt.event.ActionListener;
\r
17 import java.awt.event.ItemEvent;
\r
18 import java.awt.event.ItemListener;
\r
19 import java.awt.event.MouseEvent;
\r
20 import java.io.ByteArrayInputStream;
\r
21 import java.io.ByteArrayOutputStream;
\r
22 import java.io.File;
\r
23 import java.io.FileInputStream;
\r
24 import java.io.FileOutputStream;
\r
25 import java.io.IOException;
\r
26 import java.io.InputStream;
\r
27 import java.net.URI;
\r
28 import java.net.URISyntaxException;
\r
29 import java.net.URL;
\r
30 import java.security.AccessControlException;
\r
31 import java.util.ArrayList;
\r
32 import java.util.EventObject;
\r
33 import java.util.HashMap;
\r
34 import java.util.List;
\r
35 import java.util.Map;
\r
36 import java.util.Vector;
\r
38 import javax.sound.midi.InvalidMidiDataException;
\r
39 import javax.sound.midi.MetaMessage;
\r
40 import javax.sound.midi.MidiChannel;
\r
41 import javax.sound.midi.MidiEvent;
\r
42 import javax.sound.midi.MidiMessage;
\r
43 import javax.sound.midi.MidiSystem;
\r
44 import javax.sound.midi.Sequence;
\r
45 import javax.sound.midi.Sequencer;
\r
46 import javax.sound.midi.ShortMessage;
\r
47 import javax.sound.midi.SysexMessage;
\r
48 import javax.sound.midi.Track;
\r
49 import javax.swing.AbstractAction;
\r
50 import javax.swing.AbstractCellEditor;
\r
51 import javax.swing.Action;
\r
52 import javax.swing.BoundedRangeModel;
\r
53 import javax.swing.Box;
\r
54 import javax.swing.BoxLayout;
\r
55 import javax.swing.DefaultCellEditor;
\r
56 import javax.swing.DefaultListSelectionModel;
\r
57 import javax.swing.Icon;
\r
58 import javax.swing.JButton;
\r
59 import javax.swing.JCheckBox;
\r
60 import javax.swing.JComboBox;
\r
61 import javax.swing.JDialog;
\r
62 import javax.swing.JFileChooser;
\r
63 import javax.swing.JLabel;
\r
64 import javax.swing.JOptionPane;
\r
65 import javax.swing.JPanel;
\r
66 import javax.swing.JScrollPane;
\r
67 import javax.swing.JSlider;
\r
68 import javax.swing.JSplitPane;
\r
69 import javax.swing.JTable;
\r
70 import javax.swing.JToggleButton;
\r
71 import javax.swing.ListSelectionModel;
\r
72 import javax.swing.event.ChangeEvent;
\r
73 import javax.swing.event.ChangeListener;
\r
74 import javax.swing.event.ListSelectionEvent;
\r
75 import javax.swing.event.ListSelectionListener;
\r
76 import javax.swing.event.TableModelEvent;
\r
77 import javax.swing.event.TableModelListener;
\r
78 import javax.swing.filechooser.FileNameExtensionFilter;
\r
79 import javax.swing.table.AbstractTableModel;
\r
80 import javax.swing.table.TableCellEditor;
\r
81 import javax.swing.table.TableCellRenderer;
\r
82 import javax.swing.table.TableColumn;
\r
83 import javax.swing.table.TableColumnModel;
\r
84 import javax.swing.table.TableModel;
\r
87 * MIDIエディタ(MIDI Editor/Playlist for MIDI Chord Helper)
\r
90 * Copyright (C) 2006-2013 Akiyoshi Kamide
\r
91 * http://www.yk.rim.or.jp/~kamide/music/chordhelper/
\r
93 class MidiEditor extends JDialog implements DropTargetListener {
\r
94 public static final Insets ZERO_INSETS = new Insets(0,0,0,0);
\r
95 private static final Icon deleteIcon = new ButtonIcon(ButtonIcon.X_ICON);
\r
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 void open() {
\r
112 if( isVisible() ) toFront(); else setVisible(true);
\r
115 * このダイアログを表示するアクション
\r
117 public Action openAction = new AbstractAction(
\r
118 "Edit/Playlist/Speed", new ButtonIcon(ButtonIcon.EDIT_ICON)
\r
121 String tooltip = "MIDIシーケンスの編集/プレイリスト/再生速度調整";
\r
122 putValue(Action.SHORT_DESCRIPTION, tooltip);
\r
125 public void actionPerformed(ActionEvent e) { open(); }
\r
130 SequenceListTableModel sequenceListTableModel;
\r
132 * 新しいMIDIシーケンスを生成するダイアログ
\r
134 NewSequenceDialog newSequenceDialog = new NewSequenceDialog(this);
\r
136 * 選択されたシーケンスへジャンプするアクション
\r
138 public Action loadToSequencerAction = new AbstractAction("Load to sequencer") {
\r
140 String tooltip = "Load selected MIDI sequence to sequencer - 選択した曲をシーケンサへロード";
\r
141 putValue(Action.SHORT_DESCRIPTION, tooltip);
\r
144 public void actionPerformed(ActionEvent e) {
\r
145 sequenceListTableModel.loadToSequencer(sequenceListSelectionModel.getMinSelectionIndex());
\r
151 public Action deleteSequenceAction = new AbstractAction("Delete",deleteIcon) {
\r
153 String tooltip = "Delete selected MIDI sequence - 選択した曲をプレイリストから削除";
\r
154 putValue(Action.SHORT_DESCRIPTION, tooltip);
\r
157 public void actionPerformed(ActionEvent e) {
\r
158 if( midiFileChooser != null ) {
\r
159 // ファイルに保存できる場合(Javaアプレットではなく、Javaアプリとして動作している場合)
\r
161 SequenceTrackListTableModel seqModel =
\r
162 sequenceListTableModel.getSequenceModel(sequenceListSelectionModel);
\r
163 if( seqModel.isModified() ) {
\r
167 "Selected MIDI sequence not saved - delete it ?\n" +
\r
168 "選択したMIDIシーケンスはまだ保存されていません。削除しますか?";
\r
169 if( ! confirm(message) ) {
\r
176 sequenceListTableModel.removeSequence(sequenceListSelectionModel);
\r
180 * BASE64テキスト入力ダイアログ
\r
182 Base64Dialog base64Dialog = new Base64Dialog(this);
\r
184 * BASE64エンコードボタン(ライブラリが見えている場合のみ有効)
\r
186 private Action base64EncodeAction;
\r
188 * プレイリストのMIDIシーケンス選択状態
\r
190 ListSelectionModel sequenceListSelectionModel = new DefaultListSelectionModel() {
\r
192 setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
\r
193 addListSelectionListener(new ListSelectionListener() {
\r
195 public void valueChanged(ListSelectionEvent e) {
\r
196 if( e.getValueIsAdjusting() )
\r
198 updateButtonStatus();
\r
202 if( base64Dialog.isBase64Available() ) {
\r
203 base64EncodeAction = new AbstractAction("Base64 Encode") {
\r
205 String tooltip = "Encode selected sequence to Base64 textdata - 選択した曲をBase64テキストにエンコード";
\r
206 putValue(Action.SHORT_DESCRIPTION, tooltip);
\r
209 public void actionPerformed(ActionEvent e) {
\r
210 SequenceTrackListTableModel mstm = sequenceListTableModel.getSequenceModel(sequenceListSelectionModel);
\r
211 base64Dialog.setMIDIData(mstm.getMIDIdata(), mstm.getFilename());
\r
212 base64Dialog.setVisible(true);
\r
218 private void updateEnabled() {
\r
219 int selIndex = getMinSelectionIndex();
\r
220 boolean isSelected = (selIndex >= 0);
\r
221 if(base64EncodeAction != null)
\r
222 base64EncodeAction.setEnabled(isSelected);
\r
223 deleteSequenceAction.setEnabled(isSelected);
\r
224 loadToSequencerAction.setEnabled(isSelected);
\r
230 JTable sequenceListTableView;
\r
232 * ファイル選択ダイアログ(アプレットでは使用不可)
\r
234 private MidiFileChooser midiFileChooser;
\r
236 * ファイル選択ダイアログ(アプレットでは使用不可)
\r
238 private class MidiFileChooser extends JFileChooser implements ListSelectionListener {
\r
242 public Action saveMidiFileAction = new AbstractAction("Save") {
\r
244 String tooltip = "Save selected MIDI sequence to file - 選択したMIDIシーケンスをファイルに保存";
\r
245 putValue(Action.SHORT_DESCRIPTION, tooltip);
\r
248 public void actionPerformed(ActionEvent e) {
\r
249 SequenceTrackListTableModel sequenceTableModel =
\r
250 sequenceListTableModel.getSequenceModel(sequenceListSelectionModel);
\r
251 String filename = sequenceTableModel.getFilename();
\r
253 if( filename != null && ! filename.isEmpty() ) {
\r
254 // プレイリスト上でファイル名が入っていたら、それを初期選択
\r
255 setSelectedFile(midiFile = new File(filename));
\r
257 int response = showSaveDialog(MidiEditor.this);
\r
258 if( response != JFileChooser.APPROVE_OPTION ) {
\r
259 // 保存ダイアログでキャンセルされた場合
\r
262 if( (midiFile = getSelectedFile()).exists() ) {
\r
263 // 指定されたファイルがすでにあった場合
\r
264 String fn = midiFile.getName();
\r
265 String message = "Overwrite " + fn + " ?\n";
\r
266 message += fn + " を上書きしてよろしいですか?";
\r
267 if( ! confirm(message) ) {
\r
273 try ( FileOutputStream out = new FileOutputStream(midiFile) ) {
\r
274 out.write(sequenceTableModel.getMIDIdata());
\r
275 sequenceTableModel.setModified(false);
\r
277 catch( IOException ex ) {
\r
278 showError( ex.getMessage() );
\r
279 ex.printStackTrace();
\r
284 * シーケンスの選択有無に応じて、保存ボタンのイネーブル状態を更新します。
\r
286 private void updateEnabled() {
\r
287 boolean en = (sequenceListSelectionModel.getMinSelectionIndex() >= 0);
\r
288 saveMidiFileAction.setEnabled(en);
\r
292 setFileFilter(new FileNameExtensionFilter("MIDI sequence (*.mid)", "mid"));
\r
295 sequenceListSelectionModel.addListSelectionListener(this);
\r
299 public void valueChanged(ListSelectionEvent e) {
\r
300 if( e.getValueIsAdjusting() )
\r
307 public Action openMidiFileAction = new AbstractAction("Open") {
\r
309 String tooltip = "Open MIDI file - MIDIファイルを開く";
\r
310 putValue(Action.SHORT_DESCRIPTION, tooltip);
\r
313 public void actionPerformed(ActionEvent e) {
\r
314 if(showOpenDialog(MidiEditor.this) == JFileChooser.APPROVE_OPTION)
\r
315 addSequence(getSelectedFile());
\r
323 private ListSelectionModel trackSelectionModel = new DefaultListSelectionModel() {{
\r
324 setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
\r
325 addListSelectionListener(new ListSelectionListener() {
\r
327 public void valueChanged(ListSelectionEvent e) {
\r
328 if( e.getValueIsAdjusting() )
\r
330 SequenceTrackListTableModel sequenceModel = sequenceListTableModel.getSequenceModel(sequenceListSelectionModel);
\r
331 if( sequenceModel == null || isSelectionEmpty() ) {
\r
332 trackEventListTableView.setModel(new TrackEventListTableModel());
\r
335 int selIndex = getMinSelectionIndex();
\r
336 TrackEventListTableModel trackModel = sequenceModel.getTrackModel(selIndex);
\r
337 if( trackModel == null ) {
\r
338 trackEventListTableView.setModel(new TrackEventListTableModel());
\r
341 trackEventListTableView.setModel(trackModel);
\r
342 TableColumnModel tcm = trackEventListTableView.getColumnModel();
\r
343 trackModel.sizeColumnWidthToFit(tcm);
\r
344 TableColumn midiMessageColumn = tcm.getColumn(TrackEventListTableModel.Column.MESSAGE.ordinal());
\r
345 midiMessageColumn.setCellEditor(eventCellEditor);
\r
348 updateButtonStatus();
\r
355 public Action addTrackAction = new AbstractAction("New") {
\r
357 String tooltip = "Append new track - 新しいトラックの追加";
\r
358 putValue(Action.SHORT_DESCRIPTION, tooltip);
\r
361 public void actionPerformed(ActionEvent e) {
\r
362 int index = sequenceListTableModel.getSequenceModel(sequenceListSelectionModel).createTrack();
\r
363 trackSelectionModel.setSelectionInterval(index, index);
\r
364 sequenceListTableModel.fireSequenceChanged(sequenceListSelectionModel);
\r
370 public Action deleteTrackAction = new AbstractAction("Delete", deleteIcon) {
\r
372 String tooltip = "Delete selected track - 選択したトラックを削除";
\r
373 putValue(Action.SHORT_DESCRIPTION, tooltip);
\r
376 public void actionPerformed(ActionEvent e) {
\r
377 if( ! confirm("Do you want to delete selected track ?\n選択したトラックを削除しますか?"))
\r
379 sequenceListTableModel.getSequenceModel(sequenceListSelectionModel).deleteTracks(trackSelectionModel);
\r
380 sequenceListTableModel.fireSequenceChanged(sequenceListSelectionModel);
\r
384 * MIDIトラックリストテーブルビュー(選択中のシーケンスの中身)
\r
386 private JTable trackListTableView = new JTable(
\r
387 new SequenceTrackListTableModel(sequenceListTableModel),
\r
388 null, trackSelectionModel
\r
390 // 録音対象のMIDIチャンネルをコンボボックスで選択できるよう、
\r
392 getColumnModel().getColumn(
\r
393 SequenceTrackListTableModel.Column.RECORD_CHANNEL.ordinal()
\r
395 new DefaultCellEditor(new JComboBox<String>() {{
\r
397 for(int i=1; i <= MIDISpec.MAX_CHANNELS; i++)
\r
398 addItem(String.format("%d", i));
\r
402 // デフォルトでは、データモデルが差し替えられると列が再作成される。
\r
403 // しかしこれでは、シーケンスモデルを差し替えたとたん、
\r
404 // せっかく差し替えたセルエディタがデフォルトに戻ってしまう。
\r
406 // そこで、一度列データモデルが自動作成されたら、
\r
407 // 以後は自動作成しないようにする。
\r
408 setAutoCreateColumnsFromModel(false);
\r
411 * MIDIトラックリストのタイトルラベル
\r
413 private JLabel trackListTitleLabel = new JLabel() {
\r
414 private static final String TITLE = "Tracks";
\r
416 sequenceListSelectionModel.addListSelectionListener(
\r
417 new ListSelectionListener() {
\r
419 public void valueChanged(ListSelectionEvent e) {
\r
420 if( e.getValueIsAdjusting() )
\r
422 int index = sequenceListSelectionModel.getMinSelectionIndex();
\r
423 String text = TITLE;
\r
425 text = String.format(text+" - MIDI file No.%d", index);
\r
437 private ListSelectionModel eventSelectionModel = new DefaultListSelectionModel() {{
\r
438 setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
\r
439 addListSelectionListener(new ListSelectionListener() {
\r
441 public void valueChanged(ListSelectionEvent e) {
\r
442 if( e.getValueIsAdjusting() ) return;
\r
443 if( ! isSelectionEmpty() ) {
\r
444 TrackEventListTableModel trackModel = (TrackEventListTableModel)trackEventListTableView.getModel();
\r
445 int minIndex = getMinSelectionIndex();
\r
446 if( trackModel.hasTrack() ) {
\r
447 MidiEvent midiEvent = trackModel.getMidiEvent(minIndex);
\r
448 MidiMessage msg = midiEvent.getMessage();
\r
449 if( msg instanceof ShortMessage ) {
\r
450 ShortMessage sm = (ShortMessage)msg;
\r
451 int cmd = sm.getCommand();
\r
452 if( cmd == 0x80 || cmd == 0x90 || cmd == 0xA0 ) {
\r
453 // ノート番号を持つ場合、音を鳴らす。
\r
454 MidiChannel outMidiChannels[] = virtualMidiDevice.getChannels();
\r
455 int ch = sm.getChannel();
\r
456 int note = sm.getData1();
\r
457 int vel = sm.getData2();
\r
458 outMidiChannels[ch].noteOn(note, vel);
\r
459 outMidiChannels[ch].noteOff(note, vel);
\r
463 if( pairNoteCheckbox.isSelected() ) {
\r
464 int maxIndex = getMaxSelectionIndex();
\r
466 for( int i=minIndex; i<=maxIndex; i++ )
\r
468 isSelectedIndex(i) &&
\r
469 (partnerIndex = trackModel.getIndexOfPartnerFor(i)) >= 0 &&
\r
470 ! isSelectedIndex(partnerIndex)
\r
471 ) addSelectionInterval(partnerIndex, partnerIndex);
\r
474 updateButtonStatus();
\r
479 * MIDIイベントリストテーブルビュー
\r
481 private JTable trackEventListTableView = new JTable(
\r
482 new TrackEventListTableModel(),
\r
484 eventSelectionModel
\r
486 private MidiEventsLabel midiEventsLabel = new MidiEventsLabel();
\r
487 private class MidiEventsLabel extends JLabel implements ListSelectionListener {
\r
488 private static final String TITLE = "MIDI Events";
\r
489 public MidiEventsLabel() {
\r
491 trackSelectionModel.addListSelectionListener(this);
\r
494 public void valueChanged(ListSelectionEvent e) {
\r
495 String text = TITLE;
\r
496 int index = trackSelectionModel.getMinSelectionIndex();
\r
498 text = String.format(TITLE+" - track No.%d", index);
\r
503 * スクロール可能なMIDIイベントテーブルビュー
\r
505 private JScrollPane scrollableEventTableView = new JScrollPane(trackEventListTableView);
\r
507 * 指定の MIDI tick のイベントへスクロールします。
\r
508 * @param tick MIDI tick
\r
510 public void scrollToEventAt(long tick) {
\r
511 TrackEventListTableModel trackModel = (TrackEventListTableModel)trackEventListTableView.getModel();
\r
512 int index = trackModel.tickToIndex(tick);
\r
513 scrollableEventTableView.getVerticalScrollBar().setValue(
\r
514 index * trackEventListTableView.getRowHeight()
\r
516 eventSelectionModel.setSelectionInterval(index, index);
\r
521 MidiEventCellEditor eventCellEditor = new MidiEventCellEditor();
\r
523 * Pair note on/off チェックボックス
\r
525 private JCheckBox pairNoteCheckbox = new JCheckBox("Pair NoteON/OFF") {
\r
526 { setModel(eventCellEditor.pairNoteOnOffModel); }
\r
531 class MidiEventCellEditor extends AbstractCellEditor implements TableCellEditor {
\r
535 MidiEventDialog eventDialog = new MidiEventDialog();
\r
537 * 削除対象にする変更前イベント(null可)
\r
539 private MidiEvent[] midiEventsToBeRemoved;
\r
543 private TrackEventListTableModel midiTrackTableModel;
\r
547 private SequenceTrackListTableModel sequenceTableModel;
\r
551 private MidiEvent selectedMidiEvent = null;
\r
555 private int selectedIndex = -1;
\r
559 private long currentTick = 0;
\r
563 private TickPositionModel tickPositionModel = new TickPositionModel();
\r
565 * Pair noteON/OFF トグルボタンモデル
\r
567 private JToggleButton.ToggleButtonModel pairNoteOnOffModel =
\r
568 new JToggleButton.ToggleButtonModel() {{
\r
569 addItemListener(new ItemListener() {
\r
570 public void itemStateChanged(ItemEvent e) {
\r
571 eventDialog.midiMessageForm.durationForm.setEnabled(isSelected());
\r
577 private void setSelectedEvent() {
\r
578 sequenceTableModel = sequenceListTableModel.getSequenceModel(sequenceListSelectionModel);
\r
579 eventDialog.midiMessageForm.durationForm.setPPQ(sequenceTableModel.getSequence().getResolution());
\r
580 tickPositionModel.setSequenceIndex(sequenceTableModel.getSequenceTickIndex());
\r
581 selectedIndex = -1;
\r
583 selectedMidiEvent = null;
\r
584 midiTrackTableModel = (TrackEventListTableModel)trackEventListTableView.getModel();
\r
585 if( ! eventSelectionModel.isSelectionEmpty() ) {
\r
586 selectedIndex = eventSelectionModel.getMinSelectionIndex();
\r
587 selectedMidiEvent = midiTrackTableModel.getMidiEvent(selectedIndex);
\r
588 currentTick = selectedMidiEvent.getTick();
\r
589 tickPositionModel.setTickPosition(currentTick);
\r
593 * イベント入力をキャンセルするアクション
\r
595 Action cancelAction = new AbstractAction() {
\r
597 putValue(NAME,"Cancel");
\r
598 eventDialog.cancelButton.setAction(this);
\r
600 public void actionPerformed(ActionEvent e) {
\r
601 fireEditingCanceled();
\r
602 eventDialog.setVisible(false);
\r
606 * 指定のTick位置へジャンプするアクション
\r
608 Action queryJumpEventAction = new AbstractAction() {
\r
609 private Action jumpEventAction = new AbstractAction() {
\r
610 { putValue(NAME,"Jump"); }
\r
611 public void actionPerformed(ActionEvent e) {
\r
612 scrollToEventAt(tickPositionModel.getTickPosition());
\r
613 eventDialog.setVisible(false);
\r
616 { putValue(NAME,"Jump to ..."); }
\r
617 public void actionPerformed(ActionEvent e) {
\r
618 setSelectedEvent();
\r
619 eventDialog.setTitle("Jump selection to");
\r
620 eventDialog.okButton.setAction(jumpEventAction);
\r
621 eventDialog.openTickForm();
\r
625 * 指定のTick位置へ貼り付けるアクション
\r
627 Action queryPasteEventAction = new AbstractAction() {
\r
628 { putValue(NAME,"Paste to ..."); }
\r
629 private Action pasteEventAction = new AbstractAction() {
\r
630 { putValue(NAME,"Paste"); }
\r
631 public void actionPerformed(ActionEvent e) {
\r
632 long tick = tickPositionModel.getTickPosition();
\r
633 ((TrackEventListTableModel)trackEventListTableView.getModel()).addMidiEvents(
\r
634 copiedEventsToPaste, tick, copiedEventsPPQ
\r
636 scrollToEventAt(tick);
\r
637 sequenceListTableModel.fireSequenceChanged(sequenceListSelectionModel);
\r
638 eventDialog.setVisible(false);
\r
641 public void actionPerformed(ActionEvent e) {
\r
642 setSelectedEvent();
\r
643 eventDialog.setTitle("Paste to");
\r
644 eventDialog.okButton.setAction(pasteEventAction);
\r
645 eventDialog.openTickForm();
\r
649 * 新しいイベントの追加を行うアクション
\r
651 Action queryAddEventAction = new AbstractAction() {
\r
652 { putValue(NAME,"New"); }
\r
653 public void actionPerformed(ActionEvent e) {
\r
654 setSelectedEvent();
\r
655 midiEventsToBeRemoved = null;
\r
656 eventDialog.setTitle("Add a new MIDI event");
\r
657 eventDialog.okButton.setAction(addEventAction);
\r
658 int ch = midiTrackTableModel.getChannel();
\r
660 eventDialog.midiMessageForm.channelText.setSelectedChannel(ch);
\r
662 eventDialog.openEventForm();
\r
666 * イベントの追加(または変更)を行うアクション
\r
668 private Action addEventAction = new AbstractAction() {
\r
669 { putValue(NAME,"OK"); }
\r
670 public void actionPerformed(ActionEvent e) {
\r
671 long tick = tickPositionModel.getTickPosition();
\r
672 MidiMessage msg = eventDialog.midiMessageForm.getMessage();
\r
673 MidiEvent newMidiEvent = new MidiEvent(msg,tick);
\r
674 if( midiEventsToBeRemoved != null ) {
\r
675 midiTrackTableModel.removeMidiEvents(midiEventsToBeRemoved);
\r
677 if( ! midiTrackTableModel.addMidiEvent(newMidiEvent) ) {
\r
678 System.out.println("addMidiEvent failure");
\r
681 if(pairNoteOnOffModel.isSelected() && eventDialog.midiMessageForm.isNote()) {
\r
682 ShortMessage sm = eventDialog.midiMessageForm.getPartnerMessage();
\r
683 if( sm == null ) scrollToEventAt( tick );
\r
685 int duration = eventDialog.midiMessageForm.durationForm.getDuration();
\r
686 if( eventDialog.midiMessageForm.isNote(false) ) { // Note Off
\r
687 duration = -duration;
\r
689 long partnerTick = tick + (long)duration;
\r
690 if( partnerTick < 0L ) partnerTick = 0L;
\r
691 MidiEvent partner_midi_event =
\r
692 new MidiEvent( (MidiMessage)sm, partnerTick );
\r
693 if( ! midiTrackTableModel.addMidiEvent(partner_midi_event) ) {
\r
694 System.out.println("addMidiEvent failure (note on/off partner message)");
\r
696 scrollToEventAt( partnerTick > tick ? partnerTick : tick );
\r
699 sequenceListTableModel.fireSequenceChanged(sequenceTableModel);
\r
700 eventDialog.setVisible(false);
\r
701 fireEditingStopped();
\r
707 private Action editEventAction = new AbstractAction() {
\r
708 public void actionPerformed(ActionEvent e) {
\r
709 setSelectedEvent();
\r
710 if( selectedMidiEvent == null )
\r
712 MidiEvent partnerEvent = null;
\r
713 eventDialog.midiMessageForm.setMessage(selectedMidiEvent.getMessage());
\r
714 if( eventDialog.midiMessageForm.isNote() ) {
\r
715 int partnerIndex = midiTrackTableModel.getIndexOfPartnerFor(selectedIndex);
\r
716 if( partnerIndex < 0 ) {
\r
717 eventDialog.midiMessageForm.durationForm.setDuration(0);
\r
720 partnerEvent = midiTrackTableModel.getMidiEvent(partnerIndex);
\r
721 long partnerTick = partnerEvent.getTick();
\r
722 long duration = currentTick > partnerTick ?
\r
723 currentTick - partnerTick : partnerTick - currentTick ;
\r
724 eventDialog.midiMessageForm.durationForm.setDuration((int)duration);
\r
727 MidiEvent events[];
\r
728 if( partnerEvent == null ) {
\r
729 events = new MidiEvent[1];
\r
730 events[0] = selectedMidiEvent;
\r
733 events = new MidiEvent[2];
\r
734 events[0] = selectedMidiEvent;
\r
735 events[1] = partnerEvent;
\r
737 midiEventsToBeRemoved = events;
\r
738 eventDialog.setTitle("Change MIDI event");
\r
739 eventDialog.okButton.setAction(addEventAction);
\r
740 eventDialog.openEventForm();
\r
743 public MidiEventCellEditor() {
\r
744 eventDialog.midiMessageForm.setOutputMidiChannels(virtualMidiDevice.getChannels());
\r
745 eventDialog.tickPositionInputForm.setModel(tickPositionModel);
\r
748 public boolean isCellEditable(EventObject e) {
\r
750 return e instanceof MouseEvent && ((MouseEvent)e).getClickCount() == 2;
\r
753 public Object getCellEditorValue() { return ""; }
\r
754 private JButton editEventButton = new JButton(editEventAction){
\r
756 setHorizontalAlignment(JButton.LEFT);
\r
760 public Component getTableCellEditorComponent(
\r
761 JTable table, Object value, boolean isSelected, int row, int column
\r
763 editEventButton.setText((String)value);
\r
764 return editEventButton;
\r
769 * ペースト用にコピーされたMIDIイベントの配列
\r
771 private MidiEvent copiedEventsToPaste[];
\r
773 * ペースト用にコピーされたMIDIイベントのタイミング解像度
\r
775 private int copiedEventsPPQ = 0;
\r
779 public Action cutEventAction = new AbstractAction("Cut") {
\r
781 public void actionPerformed(ActionEvent e) {
\r
782 if( ! confirm("Do you want to cut selected event ?\n選択したMIDIイベントを切り取りますか?"))
\r
784 TrackEventListTableModel trackTableModel = (TrackEventListTableModel)trackEventListTableView.getModel();
\r
785 copiedEventsToPaste = trackTableModel.getMidiEvents(eventSelectionModel);
\r
786 copiedEventsPPQ = sequenceListTableModel.getSequenceModel(sequenceListSelectionModel).getSequence().getResolution();
\r
787 trackTableModel.removeMidiEvents(copiedEventsToPaste);
\r
788 sequenceListTableModel.fireSequenceChanged(sequenceListSelectionModel);
\r
794 public Action copyEventAction = new AbstractAction("Copy") {
\r
796 public void actionPerformed(ActionEvent e) {
\r
797 TrackEventListTableModel trackTableModel = (TrackEventListTableModel)trackEventListTableView.getModel();
\r
798 copiedEventsToPaste = trackTableModel.getMidiEvents(eventSelectionModel);
\r
799 copiedEventsPPQ = sequenceListTableModel.getSequenceModel(sequenceListSelectionModel).getSequence().getResolution();
\r
800 updateButtonStatus();
\r
806 public Action deleteEventAction = new AbstractAction("Delete", deleteIcon) {
\r
808 public void actionPerformed(ActionEvent e) {
\r
809 if( ! confirm("Do you want to delete selected event ?\n選択したMIDIイベントを削除しますか?"))
\r
811 ((TrackEventListTableModel)trackEventListTableView.getModel()).removeMidiEvents(eventSelectionModel);
\r
812 sequenceListTableModel.fireSequenceChanged(sequenceListSelectionModel);
\r
816 * 新しい {@link MidiEditor} を構築します。
\r
817 * @param deviceModelList MIDIデバイスモデルリスト
\r
819 public MidiEditor(MidiSequencerModel sequencerModel) {
\r
820 sequenceListTableModel = new SequenceListTableModel(sequencerModel) ;
\r
821 setTitle("MIDI Editor/Playlist - MIDI Chord Helper");
\r
822 setBounds( 150, 200, 850, 500 );
\r
823 setLayout(new FlowLayout());
\r
824 new DropTarget(this, DnDConstants.ACTION_COPY_OR_MOVE, this, true);
\r
826 midiFileChooser = new MidiFileChooser();
\r
828 catch( ExceptionInInitializerError|NoClassDefFoundError|AccessControlException e ) {
\r
829 // アプレットの場合、Webクライアントマシンのローカルファイルには
\r
830 // アクセスできないので、ファイル選択ダイアログは使用不可。
\r
831 midiFileChooser = null;
\r
833 sequenceListTableView = new JTable(
\r
834 sequenceListTableModel, null, sequenceListSelectionModel
\r
836 private JToggleButton playButton = new JToggleButton(
\r
837 sequenceListTableModel.sequencerModel.startStopAction
\r
839 setMargin(ZERO_INSETS);
\r
842 sequenceListTableModel.addTableModelListener(new TableModelListener() {
\r
844 * 全シーケンスの合計時間長をヘッダータイトルに反映します。
\r
845 * @param e テーブルモデルイベント
\r
848 public void tableChanged(TableModelEvent e) {
\r
849 int sec = sequenceListTableModel.getTotalSeconds();
\r
850 SequenceListTableModel.Column c = SequenceListTableModel.Column.SEQ_LENGTH;
\r
851 TableColumn tc = getColumnModel().getColumn(c.ordinal());
\r
852 tc.setHeaderValue(String.format(c.title+" [%02d:%02d]", sec/60, sec%60));
\r
854 // シーケンス削除時など、合計シーケンス長が変わっても
\r
855 // 列モデルからではヘッダタイトルが再描画されないことがある。
\r
856 // そこで、ヘッダビューから repaint() で突っついて再描画させる。
\r
857 getTableHeader().repaint();
\r
860 TableColumn tc = getColumnModel().getColumn(
\r
861 SequenceListTableModel.Column.SEQ_PLAY.ordinal()
\r
863 tc.setCellRenderer(new TableCellRenderer() {
\r
865 public Component getTableCellRendererComponent(
\r
866 JTable table, Object value, boolean isSelected,
\r
867 boolean hasFocus, int row, int column
\r
869 if(sequenceListTableModel.sequenceList.get(row).isOnSequencer()) {
\r
870 // すでにロードされていたらボタンをレンダリング
\r
874 // デフォルトレンダラーでレンダリングする。こうすれば
\r
875 // レンダラーを設定しなかった場合と全く同じ動作になる。
\r
876 Class<?> columnClass =
\r
877 sequenceListTableModel.getColumnClass(column);
\r
878 TableCellRenderer defaultRenderer =
\r
879 table.getDefaultRenderer(columnClass);
\r
880 return defaultRenderer.getTableCellRendererComponent(
\r
881 table, value, isSelected, hasFocus, row, column
\r
885 tc.setCellEditor(new PlayButtonCellEditor());
\r
886 setAutoCreateColumnsFromModel(false);
\r
888 class PlayButtonCellEditor extends AbstractCellEditor implements TableCellEditor {
\r
890 public Object getCellEditorValue() { return ""; }
\r
892 public Component getTableCellEditorComponent(
\r
893 JTable table, Object value, boolean isSelected,
\r
894 int row, int column
\r
896 return sequenceListTableModel.sequenceList.get(row).isOnSequencer() ? playButton : null;
\r
900 JPanel playlistPanel = new JPanel() {{
\r
901 setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
\r
902 add(new JScrollPane(sequenceListTableView));
\r
903 add(Box.createRigidArea(new Dimension(0, 10)));
\r
904 add(new JPanel() {{
\r
905 setLayout(new BoxLayout(this, BoxLayout.LINE_AXIS));
\r
906 add(Box.createRigidArea(new Dimension(10, 0)));
\r
907 add(new JButton(newSequenceDialog.openAction) {{
\r
908 setMargin(ZERO_INSETS);
\r
910 if( midiFileChooser != null ) {
\r
911 add( Box.createRigidArea(new Dimension(5, 0)) );
\r
912 add(new JButton(midiFileChooser.openMidiFileAction) {{
\r
913 setMargin(ZERO_INSETS);
\r
916 add(Box.createRigidArea(new Dimension(5, 0)));
\r
917 add(new JButton(sequenceListTableModel.moveToTopAction) {{
\r
918 setMargin(ZERO_INSETS);
\r
920 add(Box.createRigidArea(new Dimension(5, 0)));
\r
921 add(new JButton(loadToSequencerAction){{ setMargin(ZERO_INSETS); }});
\r
922 add(Box.createRigidArea(new Dimension(5, 0)));
\r
923 add(new JButton(sequenceListTableModel.moveToBottomAction) {{
\r
924 setMargin(ZERO_INSETS);
\r
926 if( midiFileChooser != null ) {
\r
927 add(Box.createRigidArea(new Dimension(5, 0)));
\r
928 add(new JButton(midiFileChooser.saveMidiFileAction) {{
\r
929 setMargin(ZERO_INSETS);
\r
932 if(base64EncodeAction != null) {
\r
933 add(Box.createRigidArea(new Dimension(5, 0)));
\r
934 add(new JButton(base64EncodeAction) {{
\r
935 setMargin(ZERO_INSETS);
\r
938 add( Box.createRigidArea(new Dimension(5, 0)) );
\r
939 add(new JButton(deleteSequenceAction) {{
\r
940 setMargin(ZERO_INSETS);
\r
942 add( Box.createRigidArea(new Dimension(5, 0)) );
\r
943 add(new SequencerSpeedSlider(
\r
944 sequenceListTableModel.sequencerModel.speedSliderModel
\r
947 add( Box.createRigidArea(new Dimension(0, 10)) );
\r
949 JPanel trackListPanel = new JPanel() {{
\r
950 setLayout(new BoxLayout(this, BoxLayout.PAGE_AXIS));
\r
951 add(trackListTitleLabel);
\r
952 add(Box.createRigidArea(new Dimension(0, 5)));
\r
953 add(new JScrollPane(trackListTableView));
\r
954 add(Box.createRigidArea(new Dimension(0, 5)));
\r
955 add(new JPanel() {{
\r
956 add(new JButton(addTrackAction) {{ setMargin(ZERO_INSETS); }});
\r
957 add(new JButton(deleteTrackAction) {{ setMargin(ZERO_INSETS); }});
\r
960 JPanel eventListPanel = new JPanel() {{
\r
961 add(midiEventsLabel);
\r
962 add(scrollableEventTableView);
\r
963 add(new JPanel() {{
\r
964 add(pairNoteCheckbox);
\r
965 add(new JButton(eventCellEditor.queryJumpEventAction) {{
\r
966 setMargin(ZERO_INSETS);
\r
968 add(new JButton(eventCellEditor.queryAddEventAction) {{
\r
969 setMargin(ZERO_INSETS);
\r
971 add(new JButton(copyEventAction) {{ setMargin(ZERO_INSETS); }});
\r
972 add(new JButton(cutEventAction) {{ setMargin(ZERO_INSETS); }});
\r
973 add(new JButton(eventCellEditor.queryPasteEventAction) {{
\r
974 setMargin(ZERO_INSETS);
\r
976 add(new JButton(deleteEventAction) {{ setMargin(ZERO_INSETS); }});
\r
978 setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
\r
980 Container cp = getContentPane();
\r
981 cp.setLayout(new BoxLayout(cp, BoxLayout.Y_AXIS));
\r
982 cp.add(Box.createVerticalStrut(2));
\r
984 new JSplitPane(JSplitPane.VERTICAL_SPLIT, playlistPanel,
\r
985 new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, trackListPanel, eventListPanel) {{
\r
986 setDividerLocation(300);
\r
989 setDividerLocation(160);
\r
992 updateButtonStatus();
\r
996 public void dragEnter(DropTargetDragEvent event) {
\r
997 if( event.isDataFlavorSupported(DataFlavor.javaFileListFlavor) )
\r
998 event.acceptDrag(DnDConstants.ACTION_COPY_OR_MOVE);
\r
1000 public void dragExit(DropTargetEvent event) {}
\r
1001 public void dragOver(DropTargetDragEvent event) {}
\r
1002 public void dropActionChanged(DropTargetDragEvent event) {}
\r
1003 @SuppressWarnings("unchecked")
\r
1004 public void drop(DropTargetDropEvent event) {
\r
1005 event.acceptDrop(DnDConstants.ACTION_COPY_OR_MOVE);
\r
1007 int action = event.getDropAction();
\r
1008 if ( (action & DnDConstants.ACTION_COPY_OR_MOVE) != 0 ) {
\r
1009 Transferable t = event.getTransferable();
\r
1010 Object data = t.getTransferData(DataFlavor.javaFileListFlavor);
\r
1011 loadAndPlay((List<File>)data);
\r
1012 event.dropComplete(true);
\r
1015 event.dropComplete(false);
\r
1017 catch (Exception ex) {
\r
1018 ex.printStackTrace();
\r
1019 event.dropComplete(false);
\r
1023 // Short message dialogs
\r
1024 private void showError(String message) {
\r
1025 JOptionPane.showMessageDialog(
\r
1027 ChordHelperApplet.VersionInfo.NAME,
\r
1028 JOptionPane.ERROR_MESSAGE
\r
1031 private void showWarning(String message) {
\r
1032 JOptionPane.showMessageDialog(
\r
1034 ChordHelperApplet.VersionInfo.NAME,
\r
1035 JOptionPane.WARNING_MESSAGE
\r
1038 private boolean confirm(String message) {
\r
1039 return JOptionPane.showConfirmDialog(
\r
1041 ChordHelperApplet.VersionInfo.NAME,
\r
1042 JOptionPane.YES_NO_OPTION,
\r
1043 JOptionPane.WARNING_MESSAGE
\r
1044 ) == JOptionPane.YES_OPTION ;
\r
1050 public void updateButtonStatus() {
\r
1051 SequenceTrackListTableModel sequenceModel =
\r
1052 sequenceListTableModel.getSequenceModel(sequenceListSelectionModel);
\r
1053 boolean isSequenceSelected = (sequenceModel != null);
\r
1054 if(isSequenceSelected) {
\r
1055 trackListTableView.setModel(sequenceModel);
\r
1058 trackListTableView.setModel(new SequenceTrackListTableModel(sequenceListTableModel));
\r
1060 addTrackAction.setEnabled(isSequenceSelected);
\r
1061 boolean isTrackSelected = (
\r
1062 ! trackSelectionModel.isSelectionEmpty()
\r
1064 isSequenceSelected && sequenceModel.getRowCount() > 0
\r
1066 deleteTrackAction.setEnabled(isTrackSelected);
\r
1068 TableModel tm = trackEventListTableView.getModel();
\r
1069 if( ! (tm instanceof TrackEventListTableModel) )
\r
1071 TrackEventListTableModel trackTableModel = (TrackEventListTableModel)tm;
\r
1072 boolean isEventSelected = (
\r
1073 isTrackSelected &&
\r
1074 ! eventSelectionModel.isSelectionEmpty() &&
\r
1075 trackTableModel != null && trackTableModel.getRowCount() > 0
\r
1077 copyEventAction.setEnabled(isEventSelected);
\r
1078 deleteEventAction.setEnabled(isEventSelected);
\r
1079 cutEventAction.setEnabled(isEventSelected);
\r
1080 eventCellEditor.queryJumpEventAction.setEnabled(
\r
1081 trackTableModel != null && isTrackSelected
\r
1083 eventCellEditor.queryAddEventAction.setEnabled(
\r
1084 trackTableModel != null && isTrackSelected
\r
1086 eventCellEditor.queryPasteEventAction.setEnabled(
\r
1087 trackTableModel != null && isTrackSelected &&
\r
1088 copiedEventsToPaste != null && copiedEventsToPaste.length > 0
\r
1092 * MIDIシーケンスを追加します。
\r
1093 * シーケンサーが停止中の場合、追加したシーケンスから再生を開始します。
\r
1094 * @param sequence MIDIシーケンス
\r
1095 * @return 追加先インデックス(先頭が 0)
\r
1097 public int addSequenceAndPlay(Sequence sequence) {
\r
1098 int lastIndex = sequenceListTableModel.addSequence(sequence,"");
\r
1099 if( ! sequenceListTableModel.sequencerModel.getSequencer().isRunning() ) {
\r
1100 sequenceListTableModel.loadToSequencer(lastIndex);
\r
1101 sequenceListTableModel.sequencerModel.start();
\r
1106 * バイト列とファイル名からMIDIシーケンスを追加します。
\r
1107 * バイト列が null の場合、空のMIDIシーケンスを追加します。
\r
1108 * @param data バイト列
\r
1109 * @param filename ファイル名
\r
1110 * @return 追加先インデックス(先頭が 0、失敗した場合は -1)
\r
1112 public int addSequence(byte[] data, String filename) {
\r
1113 if( data == null ) {
\r
1114 return sequenceListTableModel.addDefaultSequence();
\r
1117 try (InputStream in = new ByteArrayInputStream(data)) {
\r
1118 Sequence seq = MidiSystem.getSequence(in);
\r
1119 lastIndex =sequenceListTableModel.addSequence(seq, filename);
\r
1120 } catch( IOException|InvalidMidiDataException e ) {
\r
1121 showWarning(e.getMessage());
\r
1127 * MIDIファイルから読み込んだシーケンスを追加します。
\r
1128 * ファイルが null の場合、空のMIDIシーケンスを追加します。
\r
1129 * @param midiFile MIDIファイル
\r
1130 * @return 追加先インデックス(先頭が 0、失敗した場合は -1)
\r
1132 public int addSequence(File midiFile) {
\r
1133 if( midiFile == null ) {
\r
1134 return sequenceListTableModel.addDefaultSequence();
\r
1137 try (FileInputStream in = new FileInputStream(midiFile)) {
\r
1138 Sequence seq = MidiSystem.getSequence(in);
\r
1139 String filename = midiFile.getName();
\r
1140 lastIndex = sequenceListTableModel.addSequence(seq, filename);
\r
1141 } catch( IOException|InvalidMidiDataException e ) {
\r
1142 showWarning(e.getMessage());
\r
1144 } catch( AccessControlException e ) {
\r
1145 showError(e.getMessage());
\r
1146 e.printStackTrace();
\r
1152 * URLから読み込んだMIDIシーケンスを追加します。
\r
1153 * @param midiFileUrl MIDIファイルのURL
\r
1154 * @return 追加先インデックス(先頭が 0、失敗した場合は -1)
\r
1156 public int addSequenceFromURL(String midiFileUrl) {
\r
1157 Sequence seq = null;
\r
1158 String filename = null;
\r
1160 URI uri = new URI(midiFileUrl);
\r
1161 URL url = uri.toURL();
\r
1162 seq = MidiSystem.getSequence(url);
\r
1163 filename = url.getFile().replaceFirst("^.*/","");
\r
1164 } catch( URISyntaxException|IOException|InvalidMidiDataException e ) {
\r
1165 showWarning(e.getMessage());
\r
1166 } catch( AccessControlException e ) {
\r
1167 showError(e.getMessage());
\r
1168 e.printStackTrace();
\r
1170 if( seq == null ) return -1;
\r
1171 return sequenceListTableModel.addSequence(seq, filename);
\r
1175 * 複数のMIDIファイルを読み込み、再生されていなかったら再生します。
\r
1176 * すでに再生されていた場合、このエディタダイアログを表示します。
\r
1178 * @param fileList 読み込むMIDIファイルのリスト
\r
1180 public void loadAndPlay(List<File> fileList) {
\r
1181 int firstIndex = -1;
\r
1182 for( File file : fileList ) {
\r
1183 int lastIndex = addSequence(file);
\r
1184 if( firstIndex == -1 )
\r
1185 firstIndex = lastIndex;
\r
1187 if(sequenceListTableModel.sequencerModel.getSequencer().isRunning()) {
\r
1190 else if( firstIndex >= 0 ) {
\r
1191 sequenceListTableModel.loadToSequencer(firstIndex);
\r
1192 sequenceListTableModel.sequencerModel.start();
\r
1197 * ユーザ操作により録音可能な設定になったかどうか調べます。
\r
1198 * @return 選択されているシーケンスが録音可能な設定ならtrue
\r
1200 public boolean isRecordable() {
\r
1201 SequenceTrackListTableModel sequenceTableModel =
\r
1202 sequenceListTableModel.getSequenceModel(sequenceListSelectionModel);
\r
1203 return sequenceTableModel == null ? false : sequenceTableModel.isRecordable();
\r
1208 * シーケンサーの再生スピード調整スライダビュー
\r
1210 class SequencerSpeedSlider extends JPanel {
\r
1211 private static final String items[] = {
\r
1219 private JLabel titleLabel;
\r
1220 private JSlider slider;
\r
1221 public SequencerSpeedSlider(BoundedRangeModel model) {
\r
1222 add(titleLabel = new JLabel("Speed:"));
\r
1223 add(slider = new JSlider(model){{
\r
1224 setPaintTicks(true);
\r
1225 setMajorTickSpacing(12);
\r
1226 setMinorTickSpacing(1);
\r
1227 setVisible(false);
\r
1229 add(new JComboBox<String>(items) {{
\r
1230 addActionListener(new ActionListener() {
\r
1232 public void actionPerformed(ActionEvent e) {
\r
1233 int index = getSelectedIndex();
\r
1234 BoundedRangeModel model = slider.getModel();
\r
1235 if( index == 0 ) {
\r
1236 model.setValue(0);
\r
1237 slider.setVisible(false);
\r
1238 titleLabel.setVisible(true);
\r
1241 int maxValue = ( index == 1 ? 7 : (index-1)*12 );
\r
1242 model.setMinimum(-maxValue);
\r
1243 model.setMaximum(maxValue);
\r
1244 slider.setMajorTickSpacing( index == 1 ? 7 : 12 );
\r
1245 slider.setMinorTickSpacing( index > 3 ? 12 : 1 );
\r
1246 slider.setVisible(true);
\r
1247 titleLabel.setVisible(false);
\r
1257 * プレイリスト(MIDIシーケンスリスト)のテーブルデータモデル
\r
1259 class SequenceListTableModel extends AbstractTableModel implements ChangeListener {
\r
1263 public enum Column {
\r
1264 /** MIDIシーケンスの番号 */
\r
1265 SEQ_NUMBER("No.", Integer.class),
\r
1267 MODIFIED("Modified", Boolean.class),
\r
1269 SEQ_PLAY("Sequencer", String.class) {
\r
1271 public boolean isCellEditable() { return true; }
\r
1273 /** 再生中の時間位置(分:秒) */
\r
1274 SEQ_POSITION("Position", String.class),
\r
1275 /** シーケンスの時間長(分:秒) */
\r
1276 SEQ_LENGTH("Length", String.class),
\r
1278 FILENAME("Filename", String.class) {
\r
1280 public boolean isCellEditable() { return true; }
\r
1282 /** シーケンス名(最初のトラックの名前) */
\r
1283 SEQ_NAME("Sequence name", String.class) {
\r
1285 public boolean isCellEditable() { return true; }
\r
1288 RESOLUTION("Resolution", Integer.class),
\r
1290 TRACKS("Tracks", Integer.class),
\r
1292 DIVISION_TYPE("DivType", String.class);
\r
1294 Class<?> columnClass;
\r
1297 * @param title 列のタイトル
\r
1298 * @param columnClass 列のクラス
\r
1300 private Column(String title, Class<?> columnClass) {
\r
1301 this.title = title;
\r
1302 this.columnClass = columnClass;
\r
1304 public boolean isCellEditable() { return false; }
\r
1309 MidiSequencerModel sequencerModel;
\r
1311 * 曲の先頭または前の曲へ戻るアクション
\r
1313 public Action moveToTopAction = new AbstractAction() {
\r
1315 putValue(SHORT_DESCRIPTION,
\r
1316 "Move to top or previous song - 曲の先頭または前の曲へ戻る"
\r
1318 putValue(LARGE_ICON_KEY, new ButtonIcon(ButtonIcon.TOP_ICON));
\r
1320 public void actionPerformed(ActionEvent event) {
\r
1321 if( sequencerModel.getSequencer().getTickPosition() <= 40 )
\r
1323 sequencerModel.setValue(0);
\r
1329 public Action moveToBottomAction = new AbstractAction() {
\r
1331 putValue(SHORT_DESCRIPTION, "Move to next song - 次の曲へ進む");
\r
1332 putValue(LARGE_ICON_KEY, new ButtonIcon(ButtonIcon.BOTTOM_ICON));
\r
1334 public void actionPerformed(ActionEvent event) {
\r
1335 if(loadNext(1)) sequencerModel.setValue(0);
\r
1339 * 新しいプレイリストのテーブルモデルを構築します。
\r
1340 * @param sequencerModel MIDIシーケンサーモデル
\r
1342 public SequenceListTableModel(MidiSequencerModel sequencerModel) {
\r
1343 (this.sequencerModel = sequencerModel).addChangeListener(this);
\r
1348 private int secondPosition = 0;
\r
1350 * 再生中のシーケンサーの秒位置が変わったときに表示を更新します。
\r
1353 public void stateChanged(ChangeEvent e) {
\r
1354 int sec = sequencerModel.getValue() / 1000;
\r
1355 if(secondPosition == sec)
\r
1357 // 秒が変わったときだけ更新(小数点以下は無視)
\r
1358 secondPosition = sec;
\r
1359 fireTableCellUpdated(getLoadedIndex(), Column.SEQ_POSITION.ordinal());
\r
1361 List<SequenceTrackListTableModel> sequenceList = new Vector<>();
\r
1363 public int getRowCount() { return sequenceList.size(); }
\r
1365 public int getColumnCount() {
\r
1366 return Column.values().length;
\r
1369 public String getColumnName(int column) {
\r
1370 return Column.values()[column].title;
\r
1373 public Class<?> getColumnClass(int column) {
\r
1374 return Column.values()[column].columnClass;
\r
1377 public boolean isCellEditable(int row, int column) {
\r
1378 return Column.values()[column].isCellEditable();
\r
1381 public Object getValueAt(int row, int column) {
\r
1382 switch(Column.values()[column]) {
\r
1383 case SEQ_NUMBER: return row;
\r
1385 return sequenceList.get(row).isModified();
\r
1386 case DIVISION_TYPE: {
\r
1387 float divType = sequenceList.get(row).getSequence().getDivisionType();
\r
1388 if( divType == Sequence.PPQ ) return "PPQ";
\r
1389 else if( divType == Sequence.SMPTE_24 ) return "SMPTE_24";
\r
1390 else if( divType == Sequence.SMPTE_25 ) return "SMPTE_25";
\r
1391 else if( divType == Sequence.SMPTE_30 ) return "SMPTE_30";
\r
1392 else if( divType == Sequence.SMPTE_30DROP ) return "SMPTE_30DROP";
\r
1393 else return "[Unknown]";
\r
1396 return sequenceList.get(row).getSequence().getResolution();
\r
1398 return sequenceList.get(row).getSequence().getTracks().length;
\r
1399 case SEQ_POSITION: {
\r
1400 if( getLoadedIndex() == row )
\r
1401 return String.format("%02d:%02d", secondPosition/60, secondPosition%60);
\r
1405 case SEQ_LENGTH: {
\r
1406 long usec = sequenceList.get(row).getSequence().getMicrosecondLength();
\r
1407 int sec = (int)( (usec < 0 ? usec += 0x100000000L : usec) / 1000L / 1000L );
\r
1408 return String.format( "%02d:%02d", sec/60, sec%60 );
\r
1411 String filename = sequenceList.get(row).getFilename();
\r
1412 return filename == null ? "" : filename;
\r
1415 String name = sequenceList.get(row).toString();
\r
1416 return name == null ? "" : name;
\r
1418 default: return "";
\r
1422 public void setValueAt(Object val, int row, int column) {
\r
1423 switch(Column.values()[column]) {
\r
1426 String filename = (String)val;
\r
1427 sequenceList.get(row).setFilename(filename);
\r
1428 fireTableCellUpdated(row, column);
\r
1432 if( sequenceList.get(row).setName((String)val) )
\r
1433 fireTableCellUpdated(row, Column.MODIFIED.ordinal());
\r
1440 * このプレイリストに読み込まれた全シーケンスの合計時間長を返します。
\r
1441 * @return 全シーケンスの合計時間長 [秒]
\r
1443 public int getTotalSeconds() {
\r
1446 for( SequenceTrackListTableModel m : sequenceList ) {
\r
1447 usec = m.getSequence().getMicrosecondLength();
\r
1448 total += (int)( (usec < 0 ? usec += 0x100000000L : usec)/1000L/1000L );
\r
1453 * 未保存の修正内容を持つシーケンスがあるか調べます。
\r
1454 * @return 未保存の修正内容を持つシーケンスがあればtrue
\r
1456 public boolean isModified() {
\r
1457 for( SequenceTrackListTableModel m : sequenceList ) {
\r
1458 if( m.isModified() ) return true;
\r
1463 * 選択したシーケンスに未保存の修正内容があることを記録します。
\r
1464 * @param selModel 選択状態
\r
1465 * @param isModified 未保存の修正内容があるときtrue
\r
1467 public void setModified(ListSelectionModel selModel, boolean isModified) {
\r
1468 int minIndex = selModel.getMinSelectionIndex();
\r
1469 int maxIndex = selModel.getMaxSelectionIndex();
\r
1470 for( int i = minIndex; i <= maxIndex; i++ ) {
\r
1471 if( selModel.isSelectedIndex(i) ) {
\r
1472 sequenceList.get(i).setModified(isModified);
\r
1473 fireTableCellUpdated(i, Column.MODIFIED.ordinal());
\r
1478 * 選択されたMIDIシーケンスのテーブルモデルを返します。
\r
1479 * @param selectionModel 選択状態
\r
1480 * @return 選択されたMIDIシーケンスのテーブルモデル
\r
1482 public SequenceTrackListTableModel getSequenceModel(ListSelectionModel selectionModel) {
\r
1483 if( selectionModel.isSelectionEmpty() )
\r
1485 int selectedIndex = selectionModel.getMinSelectionIndex();
\r
1486 if( selectedIndex >= sequenceList.size() )
\r
1488 return sequenceList.get(selectedIndex);
\r
1491 * 指定されたシーケンスが変更されたことを通知します。
\r
1492 * @param sequenceTableModel MIDIシーケンスモデル
\r
1494 public void fireSequenceChanged(SequenceTrackListTableModel sequenceTableModel) {
\r
1495 int index = sequenceList.indexOf(sequenceTableModel);
\r
1498 sequenceTableModel.setModified(true);
\r
1499 fireTableRowsUpdated(index, index);
\r
1502 * 指定された選択範囲のシーケンスが変更されたことを通知します。
\r
1503 * @param selectionModel 選択状態
\r
1505 public void fireSequenceChanged(ListSelectionModel selectionModel) {
\r
1506 if( selectionModel.isSelectionEmpty() )
\r
1508 int minIndex = selectionModel.getMinSelectionIndex();
\r
1509 int maxIndex = selectionModel.getMaxSelectionIndex();
\r
1510 for( int index = minIndex; index <= maxIndex; index++ ) {
\r
1511 sequenceList.get(index).setModified(true);
\r
1513 fireTableRowsUpdated(minIndex, maxIndex);
\r
1516 * デフォルトの内容でシーケンスを作成して追加します。
\r
1517 * @return 追加されたシーケンスのインデックス(先頭が 0)
\r
1519 public int addDefaultSequence() {
\r
1520 Sequence seq = (new Music.ChordProgression()).toMidiSequence();
\r
1521 return seq == null ? -1 : addSequence(seq,null);
\r
1525 * @param seq MIDIシーケンス
\r
1526 * @param filename ファイル名
\r
1527 * @return 追加されたシーケンスのインデックス(先頭が 0)
\r
1529 public int addSequence(Sequence seq, String filename) {
\r
1530 sequenceList.add(new SequenceTrackListTableModel(this, seq, filename));
\r
1531 int lastIndex = sequenceList.size() - 1;
\r
1532 fireTableRowsInserted(lastIndex, lastIndex);
\r
1536 * 選択したシーケンスを除去します。
\r
1537 * @param listSelectionModel 選択状態
\r
1539 public void removeSequence(ListSelectionModel listSelectionModel) {
\r
1540 if( listSelectionModel.isSelectionEmpty() )
\r
1542 int selectedIndex = listSelectionModel.getMinSelectionIndex();
\r
1543 if( sequenceList.remove(selectedIndex).isOnSequencer() ) {
\r
1545 // シーケンサーにロード済みだった場合、アンロードする。
\r
1546 sequencerModel.setSequenceTrackListTableModel(null);
\r
1548 fireTableRowsDeleted(selectedIndex, selectedIndex);
\r
1551 * 指定したインデックス位置のシーケンスをシーケンサーにロードします。
\r
1552 * @param index シーケンスのインデックス位置
\r
1554 public void loadToSequencer(int index) {
\r
1555 int oldIndex = getLoadedIndex();
\r
1556 if(index == oldIndex)
\r
1558 sequencerModel.setSequenceTrackListTableModel(sequenceList.get(index));
\r
1559 int columnIndices[] = {
\r
1560 Column.SEQ_PLAY.ordinal(),
\r
1561 Column.SEQ_POSITION.ordinal(),
\r
1563 for( int columnIndex : columnIndices ) {
\r
1564 fireTableCellUpdated(oldIndex, columnIndex);
\r
1566 for( int columnIndex : columnIndices ) {
\r
1567 fireTableCellUpdated(index, columnIndex);
\r
1571 * 現在シーケンサにロードされているシーケンスのインデックスを返します。
\r
1572 * ロードされていない場合は -1 を返します。
\r
1573 * @return 現在シーケンサにロードされているシーケンスのインデックス
\r
1575 public int getLoadedIndex() {
\r
1576 return sequenceList.indexOf(sequencerModel.getSequenceTableModel());
\r
1579 * 引数で示された数だけ次へ進めたシーケンスをロードします。
\r
1580 * @param offset 進みたいシーケンス数
\r
1581 * @return 成功したらtrue
\r
1583 public boolean loadNext(int offset) {
\r
1584 int loadedIndex = getLoadedIndex();
\r
1585 int index = (loadedIndex < 0 ? 0 : loadedIndex + offset);
\r
1586 if( index < 0 || index >= sequenceList.size() )
\r
1588 loadToSequencer(index);
\r
1594 * MIDIシーケンス(トラックリスト)のテーブルデータモデル
\r
1596 class SequenceTrackListTableModel extends AbstractTableModel {
\r
1600 public enum Column {
\r
1602 TRACK_NUMBER("No.", Integer.class),
\r
1604 EVENTS("Events", Integer.class),
\r
1606 MUTE("Mute", Boolean.class),
\r
1608 SOLO("Solo", Boolean.class),
\r
1609 /** 録音するMIDIチャンネル */
\r
1610 RECORD_CHANNEL("RecCh", String.class),
\r
1612 CHANNEL("Ch", String.class),
\r
1614 TRACK_NAME("Track name", String.class);
\r
1616 Class<?> columnClass;
\r
1619 * @param title 列のタイトル
\r
1620 * @param widthRatio 幅の割合
\r
1621 * @param columnClass 列のクラス
\r
1623 private Column(String title, Class<?> columnClass) {
\r
1624 this.title = title;
\r
1625 this.columnClass = columnClass;
\r
1631 SequenceListTableModel sequenceListTableModel;
\r
1635 private Sequence sequence;
\r
1637 * ラップされたMIDIシーケンスのtickインデックス
\r
1639 private SequenceTickIndex sequenceTickIndex;
\r
1643 private String filename = "";
\r
1647 private List<TrackEventListTableModel> trackModelList = new ArrayList<>();
\r
1649 * 空の {@link SequenceTrackListTableModel} を構築します。
\r
1650 * @param sequenceListTableModel 親のプレイリスト
\r
1652 public SequenceTrackListTableModel(SequenceListTableModel sequenceListTableModel) {
\r
1653 this.sequenceListTableModel = sequenceListTableModel;
\r
1656 * MIDIシーケンスとファイル名から {@link SequenceTrackListTableModel} を構築します。
\r
1657 * @param sequenceListTableModel 親のプレイリスト
\r
1658 * @param sequence MIDIシーケンス
\r
1659 * @param filename ファイル名
\r
1661 public SequenceTrackListTableModel(
\r
1662 SequenceListTableModel sequenceListTableModel,
\r
1663 Sequence sequence,
\r
1666 this(sequenceListTableModel);
\r
1667 setSequence(sequence);
\r
1668 setFilename(filename);
\r
1671 public int getRowCount() {
\r
1672 return sequence == null ? 0 : sequence.getTracks().length;
\r
1675 public int getColumnCount() {
\r
1676 return Column.values().length;
\r
1683 public String getColumnName(int column) {
\r
1684 return Column.values()[column].title;
\r
1688 * @return 指定された列の型
\r
1691 public Class<?> getColumnClass(int column) {
\r
1692 Column c = Column.values()[column];
\r
1695 case SOLO: if( ! isOnSequencer() ) return String.class;
\r
1697 default: return c.columnClass;
\r
1701 public Object getValueAt(int row, int column) {
\r
1702 Column c = Column.values()[column];
\r
1704 case TRACK_NUMBER: return row;
\r
1705 case EVENTS: return sequence.getTracks()[row].size();
\r
1707 return isOnSequencer() ? getSequencer().getTrackMute(row) : "";
\r
1709 return isOnSequencer() ? getSequencer().getTrackSolo(row) : "";
\r
1710 case RECORD_CHANNEL:
\r
1711 return isOnSequencer() ? trackModelList.get(row).getRecordingChannel() : "";
\r
1713 int ch = trackModelList.get(row).getChannel();
\r
1714 return ch < 0 ? "" : ch + 1 ;
\r
1716 case TRACK_NAME: return trackModelList.get(row).toString();
\r
1717 default: return "";
\r
1721 * セルが編集可能かどうかを返します。
\r
1724 public boolean isCellEditable(int row, int column) {
\r
1725 Column c = Column.values()[column];
\r
1729 case RECORD_CHANNEL: return isOnSequencer();
\r
1731 case TRACK_NAME: return true;
\r
1732 default: return false;
\r
1739 public void setValueAt(Object val, int row, int column) {
\r
1740 Column c = Column.values()[column];
\r
1743 getSequencer().setTrackMute(row, ((Boolean)val).booleanValue());
\r
1746 getSequencer().setTrackSolo(row, ((Boolean)val).booleanValue());
\r
1748 case RECORD_CHANNEL:
\r
1749 trackModelList.get(row).setRecordingChannel((String)val);
\r
1754 ch = new Integer((String)val);
\r
1756 catch( NumberFormatException e ) {
\r
1760 if( --ch <= 0 || ch > MIDISpec.MAX_CHANNELS )
\r
1762 TrackEventListTableModel trackTableModel = trackModelList.get(row);
\r
1763 if( ch == trackTableModel.getChannel() ) break;
\r
1764 trackTableModel.setChannel(ch);
\r
1765 setModified(true);
\r
1766 fireTableCellUpdated(row, Column.EVENTS.ordinal());
\r
1770 trackModelList.get(row).setString((String)val);
\r
1775 fireTableCellUpdated(row,column);
\r
1779 * @return MIDIシーケンス
\r
1781 public Sequence getSequence() { return sequence; }
\r
1783 * シーケンスtickインデックスを返します。
\r
1784 * @return シーケンスtickインデックス
\r
1786 public SequenceTickIndex getSequenceTickIndex() {
\r
1787 return sequenceTickIndex;
\r
1790 * MIDIシーケンスを設定します。
\r
1791 * @param sequence MIDIシーケンス
\r
1793 private void setSequence(Sequence sequence) {
\r
1794 getSequencer().recordDisable(null); // The "null" means all tracks
\r
1795 this.sequence = sequence;
\r
1796 int oldSize = trackModelList.size();
\r
1797 if( oldSize > 0 ) {
\r
1798 trackModelList.clear();
\r
1799 fireTableRowsDeleted(0, oldSize-1);
\r
1801 if( sequence == null ) {
\r
1802 sequenceTickIndex = null;
\r
1805 fireTimeSignatureChanged();
\r
1806 Track tracks[] = sequence.getTracks();
\r
1807 for(Track track : tracks) {
\r
1808 trackModelList.add(new TrackEventListTableModel(track, this));
\r
1810 fireTableRowsInserted(0, tracks.length-1);
\r
1814 * 拍子が変更されたとき、シーケンスtickインデックスを再作成します。
\r
1816 public void fireTimeSignatureChanged() {
\r
1817 sequenceTickIndex = new SequenceTickIndex(sequence);
\r
1819 private boolean isModified = false;
\r
1822 * @return 変更済みのときtrue
\r
1824 public boolean isModified() { return isModified; }
\r
1826 * 変更されたかどうかを設定します。
\r
1827 * @param isModified 変更されたときtrue
\r
1829 public void setModified(boolean isModified) { this.isModified = isModified; }
\r
1832 * @param filename ファイル名
\r
1834 public void setFilename(String filename) { this.filename = filename; }
\r
1839 public String getFilename() { return filename; }
\r
1841 public String toString() { return MIDISpec.getNameOf(sequence); }
\r
1844 * @param name シーケンス名
\r
1845 * @return 成功したらtrue
\r
1847 public boolean setName(String name) {
\r
1848 if( name.equals(toString()) || ! MIDISpec.setNameOf(sequence,name) )
\r
1850 setModified(true);
\r
1851 fireTableDataChanged();
\r
1855 * このシーケンスのMIDIデータのバイト列を返します。
\r
1856 * @return MIDIデータのバイト列(失敗した場合null)
\r
1858 public byte[] getMIDIdata() {
\r
1859 if( sequence == null || sequence.getTracks().length == 0 ) {
\r
1862 try( ByteArrayOutputStream out = new ByteArrayOutputStream() ) {
\r
1863 MidiSystem.write(sequence, 1, out);
\r
1864 return out.toByteArray();
\r
1865 } catch ( IOException e ) {
\r
1866 e.printStackTrace();
\r
1871 * 指定のトラックが変更されたことを通知します。
\r
1872 * @param track トラック
\r
1874 public void fireTrackChanged(Track track) {
\r
1875 int row = indexOf(track);
\r
1876 if( row < 0 ) return;
\r
1877 fireTableRowsUpdated(row, row);
\r
1878 sequenceListTableModel.fireSequenceChanged(this);
\r
1881 * 指定のインデックスのトラックモデルを返します。
\r
1882 * @param index トラックのインデックス
\r
1883 * @return トラックモデル(見つからない場合null)
\r
1885 public TrackEventListTableModel getTrackModel(int index) {
\r
1886 Track tracks[] = sequence.getTracks();
\r
1887 if( tracks.length != 0 ) {
\r
1888 Track track = tracks[index];
\r
1889 for( TrackEventListTableModel model : trackModelList )
\r
1890 if( model.getTrack() == track )
\r
1896 * 指定のトラックがある位置のインデックスを返します。
\r
1897 * @param track トラック
\r
1898 * @return トラックのインデックス(先頭 0、トラックが見つからない場合 -1)
\r
1900 public int indexOf(Track track) {
\r
1901 Track tracks[] = sequence.getTracks();
\r
1902 for( int i=0; i<tracks.length; i++ )
\r
1903 if( tracks[i] == track )
\r
1908 * 新しいトラックを生成し、末尾に追加します。
\r
1909 * @return 追加したトラックのインデックス(先頭 0)
\r
1911 public int createTrack() {
\r
1912 trackModelList.add(new TrackEventListTableModel(sequence.createTrack(), this));
\r
1913 int lastRow = sequence.getTracks().length - 1;
\r
1914 fireTableRowsInserted(lastRow, lastRow);
\r
1918 * 選択されているトラックを削除します。
\r
1919 * @param selectionModel 選択状態
\r
1921 public void deleteTracks(ListSelectionModel selectionModel) {
\r
1922 if( selectionModel.isSelectionEmpty() )
\r
1924 int minIndex = selectionModel.getMinSelectionIndex();
\r
1925 int maxIndex = selectionModel.getMaxSelectionIndex();
\r
1926 Track tracks[] = sequence.getTracks();
\r
1927 for( int i = maxIndex; i >= minIndex; i-- ) {
\r
1928 if( ! selectionModel.isSelectedIndex(i) )
\r
1930 sequence.deleteTrack(tracks[i]);
\r
1931 trackModelList.remove(i);
\r
1933 fireTableRowsDeleted(minIndex, maxIndex);
\r
1937 * @return MIDIシーケンサ
\r
1939 public Sequencer getSequencer() {
\r
1940 return sequenceListTableModel.sequencerModel.getSequencer();
\r
1943 * このシーケンスモデルのシーケンスをシーケンサーが操作しているか調べます。
\r
1944 * @return シーケンサーが操作していたらtrue
\r
1946 public boolean isOnSequencer() {
\r
1947 return sequence == getSequencer().getSequence();
\r
1952 * <p>シーケンサーにロード済みで、
\r
1953 * かつ録音しようとしているチャンネルの設定されたトラックが一つでもあれば、
\r
1956 * @return 録音可能であればtrue
\r
1958 public boolean isRecordable() {
\r
1959 if( isOnSequencer() ) {
\r
1960 int rowCount = getRowCount();
\r
1961 int col = Column.RECORD_CHANNEL.ordinal();
\r
1962 for( int row=0; row < rowCount; row++ )
\r
1963 if( ! "OFF".equals(getValueAt(row, col)) ) return true;
\r
1970 * MIDIトラック(MIDIイベントリスト)テーブルモデル
\r
1972 class TrackEventListTableModel extends AbstractTableModel {
\r
1976 public enum Column {
\r
1978 EVENT_NUMBER("No.", 30, Integer.class),
\r
1980 TICK_POSITION("TickPos.", 40, Long.class),
\r
1981 /** tick位置に対応する小節 */
\r
1982 MEASURE_POSITION("Measure", 20, Integer.class),
\r
1983 /** tick位置に対応する拍 */
\r
1984 BEAT_POSITION("Beat", 20, Integer.class),
\r
1985 /** tick位置に対応する余剰tick(拍に収まらずに余ったtick数) */
\r
1986 EXTRA_TICK_POSITION("ExTick", 20, Integer.class),
\r
1988 MESSAGE("MIDI Message", 280, String.class);
\r
1989 private String title;
\r
1990 private int widthRatio;
\r
1991 private Class<?> columnClass;
\r
1994 * @param title 列のタイトル
\r
1995 * @param widthRatio 幅の割合
\r
1996 * @param columnClass 列のクラス
\r
1998 private Column(String title, int widthRatio, Class<?> columnClass) {
\r
1999 this.title = title;
\r
2000 this.widthRatio = widthRatio;
\r
2001 this.columnClass = columnClass;
\r
2007 public static int totalWidthRatio() {
\r
2009 for( Column c : values() ) total += c.widthRatio;
\r
2014 * ラップされているMIDIトラック
\r
2016 private Track track;
\r
2020 private SequenceTrackListTableModel parent;
\r
2022 * 空のMIDIトラックモデルを構築します。
\r
2024 public TrackEventListTableModel() { }
\r
2026 * シーケンスに連動する空のMIDIトラックモデルを構築します。
\r
2027 * @parent 親のシーケンステーブルモデル
\r
2029 public TrackEventListTableModel(SequenceTrackListTableModel parent) {
\r
2030 this.parent = parent;
\r
2033 * シーケンスを親にして、その特定のトラックに連動する
\r
2034 * MIDIトラックモデルを構築します。
\r
2036 * @param track ラップするMIDIトラック
\r
2037 * @param parent 親のシーケンスモデル
\r
2039 public TrackEventListTableModel(Track track, SequenceTrackListTableModel parent) {
\r
2040 this.track = track;
\r
2041 this.parent = parent;
\r
2044 public int getRowCount() {
\r
2045 return track == null ? 0 : track.size();
\r
2048 public int getColumnCount() {
\r
2049 return Column.values().length;
\r
2055 public String getColumnName(int column) {
\r
2056 return Column.values()[column].title;
\r
2062 public Class<?> getColumnClass(int column) {
\r
2063 return Column.values()[column].columnClass;
\r
2066 public Object getValueAt(int row, int column) {
\r
2067 switch(Column.values()[column]) {
\r
2068 case EVENT_NUMBER: return row;
\r
2069 case TICK_POSITION: return track.get(row).getTick();
\r
2070 case MEASURE_POSITION:
\r
2071 return parent.getSequenceTickIndex().tickToMeasure(track.get(row).getTick()) + 1;
\r
2072 case BEAT_POSITION:
\r
2073 parent.getSequenceTickIndex().tickToMeasure(track.get(row).getTick());
\r
2074 return parent.getSequenceTickIndex().lastBeat + 1;
\r
2075 case EXTRA_TICK_POSITION:
\r
2076 parent.getSequenceTickIndex().tickToMeasure(track.get(row).getTick());
\r
2077 return parent.getSequenceTickIndex().lastExtraTick;
\r
2078 case MESSAGE: return msgToString(track.get(row).getMessage());
\r
2079 default: return "";
\r
2083 * セルを編集できるときtrue、編集できないときfalseを返します。
\r
2086 public boolean isCellEditable(int row, int column) {
\r
2087 switch(Column.values()[column]) {
\r
2088 case TICK_POSITION:
\r
2089 case MEASURE_POSITION:
\r
2090 case BEAT_POSITION:
\r
2091 case EXTRA_TICK_POSITION:
\r
2092 case MESSAGE: return true;
\r
2093 default: return false;
\r
2100 public void setValueAt(Object value, int row, int column) {
\r
2102 switch(Column.values()[column]) {
\r
2103 case TICK_POSITION: newTick = (Long)value; break;
\r
2104 case MEASURE_POSITION:
\r
2105 newTick = parent.getSequenceTickIndex().measureToTick(
\r
2106 (Integer)value - 1,
\r
2107 (Integer)getValueAt( row, Column.BEAT_POSITION.ordinal() ) - 1,
\r
2108 (Integer)getValueAt( row, Column.TICK_POSITION.ordinal() )
\r
2111 case BEAT_POSITION:
\r
2112 newTick = parent.getSequenceTickIndex().measureToTick(
\r
2113 (Integer)getValueAt( row, Column.MEASURE_POSITION.ordinal() ) - 1,
\r
2114 (Integer)value - 1,
\r
2115 (Integer)getValueAt( row, Column.EXTRA_TICK_POSITION.ordinal() )
\r
2118 case EXTRA_TICK_POSITION:
\r
2119 newTick = parent.getSequenceTickIndex().measureToTick(
\r
2120 (Integer)getValueAt( row, Column.MEASURE_POSITION.ordinal() ) - 1,
\r
2121 (Integer)getValueAt( row, Column.BEAT_POSITION.ordinal() ) - 1,
\r
2127 MidiEvent oldMidiEvent = track.get(row);
\r
2128 if( oldMidiEvent.getTick() == newTick ) {
\r
2131 MidiMessage msg = oldMidiEvent.getMessage();
\r
2132 MidiEvent newMidiEvent = new MidiEvent(msg,newTick);
\r
2133 track.remove(oldMidiEvent);
\r
2134 track.add(newMidiEvent);
\r
2135 fireTableDataChanged();
\r
2136 if( MIDISpec.isEOT(msg) ) {
\r
2137 // EOTの場所が変わると曲の長さが変わるので、親モデルへ通知する。
\r
2138 parent.sequenceListTableModel.fireSequenceChanged(parent);
\r
2143 * @param columnModel テーブル列モデル
\r
2145 public void sizeColumnWidthToFit(TableColumnModel columnModel) {
\r
2146 int totalWidth = columnModel.getTotalColumnWidth();
\r
2147 int totalWidthRatio = Column.totalWidthRatio();
\r
2148 for( Column c : Column.values() ) {
\r
2149 int w = totalWidth * c.widthRatio / totalWidthRatio;
\r
2150 columnModel.getColumn(c.ordinal()).setPreferredWidth(w);
\r
2155 * @return MIDIトラック
\r
2157 public Track getTrack() { return track; }
\r
2162 public String toString() { return MIDISpec.getNameOf(track); }
\r
2165 * @param name トラック名
\r
2166 * @return 設定が行われたらtrue
\r
2168 public boolean setString(String name) {
\r
2169 if(name.equals(toString()) || ! MIDISpec.setNameOf(track, name))
\r
2171 parent.setModified(true);
\r
2172 parent.sequenceListTableModel.fireSequenceChanged(parent);
\r
2173 fireTableDataChanged();
\r
2176 private String recordingChannel = "OFF";
\r
2178 * 録音中のMIDIチャンネルを返します。
\r
2179 * @return 録音中のMIDIチャンネル
\r
2181 public String getRecordingChannel() { return recordingChannel; }
\r
2183 * 録音中のMIDIチャンネルを設定します。
\r
2184 * @param recordingChannel 録音中のMIDIチャンネル
\r
2186 public void setRecordingChannel(String recordingChannel) {
\r
2187 Sequencer sequencer = parent.getSequencer();
\r
2188 if( recordingChannel.equals("OFF") ) {
\r
2189 sequencer.recordDisable( track );
\r
2191 else if( recordingChannel.equals("ALL") ) {
\r
2192 sequencer.recordEnable( track, -1 );
\r
2196 int ch = Integer.decode(recordingChannel).intValue() - 1;
\r
2197 sequencer.recordEnable( track, ch );
\r
2198 } catch( NumberFormatException nfe ) {
\r
2199 sequencer.recordDisable( track );
\r
2200 this.recordingChannel = "OFF";
\r
2204 this.recordingChannel = recordingChannel;
\r
2207 * このトラックの対象MIDIチャンネルを返します。
\r
2208 * <p>全てのチャンネルメッセージが同じMIDIチャンネルの場合、
\r
2209 * そのMIDIチャンネルを返します。
\r
2210 * MIDIチャンネルの異なるチャンネルメッセージが一つでも含まれていた場合、
\r
2213 * @return 対象MIDIチャンネル(不統一の場合 -1)
\r
2215 public int getChannel() {
\r
2217 int trackSize = track.size();
\r
2218 for( int index=0; index < trackSize; index++ ) {
\r
2219 MidiMessage msg = track.get(index).getMessage();
\r
2220 if( ! (msg instanceof ShortMessage) )
\r
2222 ShortMessage smsg = (ShortMessage)msg;
\r
2223 if( ! MIDISpec.isChannelMessage(smsg) )
\r
2225 int ch = smsg.getChannel();
\r
2226 if( prevCh >= 0 && prevCh != ch ) {
\r
2234 * 指定されたMIDIチャンネルをすべてのチャンネルメッセージに対して設定します。
\r
2235 * @param channel MIDIチャンネル
\r
2237 public void setChannel(int channel) {
\r
2238 int track_size = track.size();
\r
2239 for( int index=0; index < track_size; index++ ) {
\r
2240 MidiMessage msg = track.get(index).getMessage();
\r
2241 if( ! (msg instanceof ShortMessage) )
\r
2243 ShortMessage smsg = (ShortMessage)msg;
\r
2244 if( ! MIDISpec.isChannelMessage(smsg) )
\r
2246 if( smsg.getChannel() == channel )
\r
2250 smsg.getCommand(), channel,
\r
2251 smsg.getData1(), smsg.getData2()
\r
2254 catch( InvalidMidiDataException e ) {
\r
2255 e.printStackTrace();
\r
2257 parent.setModified(true);
\r
2259 parent.fireTrackChanged(track);
\r
2260 fireTableDataChanged();
\r
2263 * 指定の MIDI tick 位置にあるイベントを二分探索し、
\r
2264 * そのイベントの行インデックスを返します。
\r
2265 * @param tick MIDI tick
\r
2268 public int tickToIndex(long tick) {
\r
2269 if( track == null )
\r
2272 int maxIndex = track.size() - 1;
\r
2273 while( minIndex < maxIndex ) {
\r
2274 int currentIndex = (minIndex + maxIndex) / 2 ;
\r
2275 long currentTick = track.get(currentIndex).getTick();
\r
2276 if( tick > currentTick ) {
\r
2277 minIndex = currentIndex + 1;
\r
2279 else if( tick < currentTick ) {
\r
2280 maxIndex = currentIndex - 1;
\r
2283 return currentIndex;
\r
2286 return (minIndex + maxIndex) / 2;
\r
2289 * NoteOn/NoteOff ペアの一方の行インデックスから、
\r
2290 * もう一方(ペアの相手)の行インデックスを返します。
\r
2291 * @param index 行インデックス
\r
2292 * @return ペアを構成する相手の行インデックス(ない場合は -1)
\r
2294 public int getIndexOfPartnerFor(int index) {
\r
2295 if( track == null || index >= track.size() )
\r
2297 MidiMessage msg = track.get(index).getMessage();
\r
2298 if( ! (msg instanceof ShortMessage) ) return -1;
\r
2299 ShortMessage sm = (ShortMessage)msg;
\r
2300 int cmd = sm.getCommand();
\r
2302 int ch = sm.getChannel();
\r
2303 int note = sm.getData1();
\r
2304 MidiMessage partner_msg;
\r
2305 ShortMessage partner_sm;
\r
2309 case 0x90: // NoteOn
\r
2310 if( sm.getData2() > 0 ) {
\r
2311 // Search NoteOff event forward
\r
2312 for( i = index + 1; i < track.size(); i++ ) {
\r
2313 partner_msg = track.get(i).getMessage();
\r
2314 if( ! (partner_msg instanceof ShortMessage ) ) continue;
\r
2315 partner_sm = (ShortMessage)partner_msg;
\r
2316 partner_cmd = partner_sm.getCommand();
\r
2317 if( partner_cmd != 0x80 && partner_cmd != 0x90 ||
\r
2318 partner_cmd == 0x90 && partner_sm.getData2() > 0
\r
2323 if( ch != partner_sm.getChannel() || note != partner_sm.getData1() ) {
\r
2331 // When velocity is 0, it means Note Off, so no break.
\r
2332 case 0x80: // NoteOff
\r
2333 // Search NoteOn event backward
\r
2334 for( i = index - 1; i >= 0; i-- ) {
\r
2335 partner_msg = track.get(i).getMessage();
\r
2336 if( ! (partner_msg instanceof ShortMessage ) ) continue;
\r
2337 partner_sm = (ShortMessage)partner_msg;
\r
2338 partner_cmd = partner_sm.getCommand();
\r
2339 if( partner_cmd != 0x90 || partner_sm.getData2() <= 0 ) {
\r
2343 if( ch != partner_sm.getChannel() || note != partner_sm.getData1() ) {
\r
2355 * ノートメッセージかどうか調べます。
\r
2356 * @param index 行インデックス
\r
2357 * @return Note On または Note Off のとき true
\r
2359 public boolean isNote(int index) {
\r
2360 MidiEvent midi_evt = getMidiEvent(index);
\r
2361 MidiMessage msg = midi_evt.getMessage();
\r
2362 if( ! (msg instanceof ShortMessage) ) return false;
\r
2363 int cmd = ((ShortMessage)msg).getCommand();
\r
2364 return cmd == ShortMessage.NOTE_ON || cmd == ShortMessage.NOTE_OFF ;
\r
2366 public boolean hasTrack() { return track != null; }
\r
2368 * 指定の行インデックスのMIDIイベントを返します。
\r
2369 * @param index 行インデックス
\r
2370 * @return MIDIイベント
\r
2372 public MidiEvent getMidiEvent(int index) { return track.get(index); }
\r
2374 * 選択されているMIDIイベントを返します。
\r
2375 * @param selectionModel 選択状態モデル
\r
2376 * @return 選択されているMIDIイベント
\r
2378 public MidiEvent[] getMidiEvents(ListSelectionModel selectionModel) {
\r
2379 Vector<MidiEvent> events = new Vector<MidiEvent>();
\r
2380 if( ! selectionModel.isSelectionEmpty() ) {
\r
2381 int i = selectionModel.getMinSelectionIndex();
\r
2382 int max = selectionModel.getMaxSelectionIndex();
\r
2383 for( ; i <= max; i++ )
\r
2384 if( selectionModel.isSelectedIndex(i) )
\r
2385 events.add(track.get(i));
\r
2387 return events.toArray(new MidiEvent[1]);
\r
2391 * @param midiEvent 追加するMIDIイベント
\r
2392 * @return 追加できたらtrue
\r
2394 public boolean addMidiEvent(MidiEvent midiEvent) {
\r
2395 if( !(track.add(midiEvent)) )
\r
2397 if( MIDISpec.isTimeSignature(midiEvent.getMessage()) )
\r
2398 parent.fireTimeSignatureChanged();
\r
2399 parent.fireTrackChanged(track);
\r
2400 int last_index = track.size() - 1;
\r
2401 fireTableRowsInserted( last_index-1, last_index-1 );
\r
2406 * @param midiEvents 追加するMIDIイベント
\r
2407 * @param destinationTick 追加先tick
\r
2408 * @param sourcePPQ PPQ値(タイミング解像度)
\r
2409 * @return 追加できたらtrue
\r
2411 public boolean addMidiEvents(MidiEvent midiEvents[], long destinationTick, int sourcePPQ) {
\r
2412 int destinationPPQ = parent.getSequence().getResolution();
\r
2413 boolean done = false;
\r
2414 boolean hasTimeSignature = false;
\r
2415 long firstSourceEventTick = -1;
\r
2416 for( MidiEvent sourceEvent : midiEvents ) {
\r
2417 long sourceEventTick = sourceEvent.getTick();
\r
2418 MidiMessage msg = sourceEvent.getMessage();
\r
2419 long newTick = destinationTick;
\r
2420 if( firstSourceEventTick < 0 ) {
\r
2421 firstSourceEventTick = sourceEventTick;
\r
2424 newTick += (sourceEventTick - firstSourceEventTick) * destinationPPQ / sourcePPQ;
\r
2426 if( ! track.add(new MidiEvent(msg, newTick)) ) continue;
\r
2428 if( MIDISpec.isTimeSignature(msg) ) hasTimeSignature = true;
\r
2431 if( hasTimeSignature ) parent.fireTimeSignatureChanged();
\r
2432 parent.fireTrackChanged(track);
\r
2433 int lastIndex = track.size() - 1;
\r
2434 int oldLastIndex = lastIndex - midiEvents.length;
\r
2435 fireTableRowsInserted(oldLastIndex, lastIndex);
\r
2441 * @param midiEvents 除去するMIDIイベント
\r
2443 public void removeMidiEvents(MidiEvent midiEvents[]) {
\r
2444 boolean hadTimeSignature = false;
\r
2445 for( MidiEvent e : midiEvents ) {
\r
2446 if( MIDISpec.isTimeSignature(e.getMessage()) )
\r
2447 hadTimeSignature = true;
\r
2450 if( hadTimeSignature ) parent.fireTimeSignatureChanged();
\r
2451 parent.fireTrackChanged(track);
\r
2452 int lastIndex = track.size() - 1;
\r
2453 int oldLastIndex = lastIndex + midiEvents.length;
\r
2454 if(lastIndex < 0) lastIndex = 0;
\r
2455 fireTableRowsDeleted(oldLastIndex, lastIndex);
\r
2458 * 引数の選択内容が示すMIDIイベントを除去します。
\r
2459 * @param selectionModel 選択内容
\r
2461 public void removeMidiEvents(ListSelectionModel selectionModel) {
\r
2462 removeMidiEvents(getMidiEvents(selectionModel));
\r
2464 private boolean isRhythmPart(int ch) { return (ch == 9); }
\r
2466 * MIDIメッセージの内容を文字列で返します。
\r
2467 * @param msg MIDIメッセージ
\r
2468 * @return MIDIメッセージの内容を表す文字列
\r
2470 public String msgToString(MidiMessage msg) {
\r
2472 if( msg instanceof ShortMessage ) {
\r
2473 ShortMessage shortmsg = (ShortMessage)msg;
\r
2474 int status = msg.getStatus();
\r
2475 String status_name = MIDISpec.getStatusName(status);
\r
2476 int data1 = shortmsg.getData1();
\r
2477 int data2 = shortmsg.getData2();
\r
2478 if( MIDISpec.isChannelMessage(status) ) {
\r
2479 int ch = shortmsg.getChannel();
\r
2480 String ch_prefix = "Ch."+(ch+1) + ": ";
\r
2481 String status_prefix = (
\r
2482 status_name == null ? String.format("status=0x%02X",status) : status_name
\r
2484 int cmd = shortmsg.getCommand();
\r
2486 case ShortMessage.NOTE_OFF:
\r
2487 case ShortMessage.NOTE_ON:
\r
2488 str += ch_prefix + status_prefix + data1;
\r
2490 if( isRhythmPart(ch) ) {
\r
2491 str += MIDISpec.getPercussionName(data1);
\r
2494 str += Music.NoteSymbol.noteNoToSymbol(data1);
\r
2496 str +="] Velocity=" + data2;
\r
2498 case ShortMessage.POLY_PRESSURE:
\r
2499 str += ch_prefix + status_prefix + "Note=" + data1 + " Pressure=" + data2;
\r
2501 case ShortMessage.PROGRAM_CHANGE:
\r
2502 str += ch_prefix + status_prefix + data1 + ":[" + MIDISpec.instrument_names[data1] + "]";
\r
2503 if( data2 != 0 ) str += " data2=" + data2;
\r
2505 case ShortMessage.CHANNEL_PRESSURE:
\r
2506 str += ch_prefix + status_prefix + data1;
\r
2507 if( data2 != 0 ) str += " data2=" + data2;
\r
2509 case ShortMessage.PITCH_BEND:
\r
2511 int val = ((data1 & 0x7F) | ((data2 & 0x7F) << 7));
\r
2512 str += ch_prefix + status_prefix + ( (val-8192) * 100 / 8191) + "% (" + val + ")";
\r
2515 case ShortMessage.CONTROL_CHANGE:
\r
2517 // Control / Mode message name
\r
2518 String ctrl_name = MIDISpec.getControllerName(data1);
\r
2519 str += ch_prefix + (data1 < 0x78 ? "CtrlChg: " : "ModeMsg: ");
\r
2520 if( ctrl_name == null ) {
\r
2521 str += " No.=" + data1 + " Value=" + data2;
\r
2526 // Controller's value
\r
2528 case 0x40: case 0x41: case 0x42: case 0x43: case 0x45:
\r
2529 str += " " + ( data2==0x3F?"OFF":data2==0x40?"ON":data2 );
\r
2531 case 0x44: // Legato Footswitch
\r
2532 str += " " + ( data2==0x3F?"Normal":data2==0x40?"Legato":data2 );
\r
2534 case 0x7A: // Local Control
\r
2535 str += " " + ( data2==0x00?"OFF":data2==0x7F?"ON":data2 );
\r
2538 str += " " + data2;
\r
2545 // Never reached here
\r
2549 else { // System Message
\r
2550 str += (status_name == null ? ("status="+status) : status_name );
\r
2551 str += " (" + data1 + "," + data2 + ")";
\r
2555 else if( msg instanceof MetaMessage ) {
\r
2556 MetaMessage metamsg = (MetaMessage)msg;
\r
2557 byte[] msgdata = metamsg.getData();
\r
2558 int msgtype = metamsg.getType();
\r
2560 String meta_name = MIDISpec.getMetaName(msgtype);
\r
2561 if( meta_name == null ) {
\r
2562 str += "Unknown MessageType="+msgtype + " Values=(";
\r
2563 for( byte b : msgdata ) str += String.format( " %02X", b );
\r
2567 // Add the message type name
\r
2570 // Add the text data
\r
2571 if( MIDISpec.hasMetaText(msgtype) ) {
\r
2572 str +=" ["+(new String(msgdata))+"]";
\r
2575 // Add the numeric data
\r
2577 case 0x00: // Sequence Number (for MIDI Format 2)
\r
2578 if( msgdata.length == 2 ) {
\r
2579 str += String.format(
\r
2581 ((msgdata[0] & 0xFF) << 8) | (msgdata[1] & 0xFF)
\r
2585 str += ": Size not 2 byte : data=(";
\r
2586 for( byte b : msgdata ) str += String.format( " %02X", b );
\r
2589 case 0x20: // MIDI Ch.Prefix
\r
2590 case 0x21: // MIDI Output Port
\r
2591 if( msgdata.length == 1 ) {
\r
2592 str += String.format( ": %02X", msgdata[0] & 0xFF );
\r
2595 str += ": Size not 1 byte : data=(";
\r
2596 for( byte b : msgdata ) str += String.format( " %02X", b );
\r
2599 case 0x51: // Tempo
\r
2600 str += ": " + MIDISpec.byteArrayToQpmTempo( msgdata ) + "[QPM] (";
\r
2601 for( byte b : msgdata ) str += String.format( " %02X", b );
\r
2604 case 0x54: // SMPTE Offset
\r
2605 if( msgdata.length == 5 ) {
\r
2607 + (msgdata[0] & 0xFF) + ":"
\r
2608 + (msgdata[1] & 0xFF) + ":"
\r
2609 + (msgdata[2] & 0xFF) + "."
\r
2610 + (msgdata[3] & 0xFF) + "."
\r
2611 + (msgdata[4] & 0xFF);
\r
2614 str += ": Size not 5 byte : data=(";
\r
2615 for( byte b : msgdata ) str += String.format( " %02X", b );
\r
2618 case 0x58: // Time Signature
\r
2619 if( msgdata.length == 4 ) {
\r
2620 str +=": " + msgdata[0] + "/" + (1 << msgdata[1]);
\r
2621 str +=", "+msgdata[2]+"[clk/beat], "+msgdata[3]+"[32nds/24clk]";
\r
2624 str += ": Size not 4 byte : data=(";
\r
2625 for( byte b : msgdata ) str += String.format( " %02X", b );
\r
2628 case 0x59: // Key Signature
\r
2629 if( msgdata.length == 2 ) {
\r
2630 Music.Key key = new Music.Key(msgdata);
\r
2631 str += ": " + key.signatureDescription();
\r
2632 str += " (" + key.toStringIn(Music.SymbolLanguage.NAME) + ")";
\r
2635 str += ": Size not 2 byte : data=(";
\r
2636 for( byte b : msgdata ) str += String.format( " %02X", b );
\r
2639 case 0x7F: // Sequencer Specific Meta Event
\r
2641 for( byte b : msgdata ) str += String.format( " %02X", b );
\r
2647 else if( msg instanceof SysexMessage ) {
\r
2648 SysexMessage sysexmsg = (SysexMessage)msg;
\r
2649 int status = sysexmsg.getStatus();
\r
2650 byte[] msgdata = sysexmsg.getData();
\r
2651 int data_byte_pos = 1;
\r
2652 switch( status ) {
\r
2653 case SysexMessage.SYSTEM_EXCLUSIVE:
\r
2656 case SysexMessage.SPECIAL_SYSTEM_EXCLUSIVE:
\r
2657 str += "SysEx(Special): ";
\r
2660 str += "SysEx: Invalid (status="+status+") ";
\r
2663 if( msgdata.length < 1 ) {
\r
2664 str += " Invalid data size: " + msgdata.length;
\r
2667 int manufacturer_id = (int)(msgdata[0] & 0xFF );
\r
2668 int device_id = (int)(msgdata[1] & 0xFF);
\r
2669 int model_id = (int)(msgdata[2] & 0xFF);
\r
2670 String manufacturer_name = MIDISpec.getSysExManufacturerName(manufacturer_id);
\r
2671 if( manufacturer_name == null ) {
\r
2672 manufacturer_name = String.format( "[Manufacturer code %02X]", msgdata[0] );
\r
2674 str += manufacturer_name + String.format( " (DevID=0x%02X)", device_id );
\r
2675 switch( manufacturer_id ) {
\r
2676 case 0x7E: // Non-Realtime Universal
\r
2678 int sub_id_1 = (int)(msgdata[2] & 0xFF);
\r
2679 int sub_id_2 = (int)(msgdata[3] & 0xFF);
\r
2680 switch( sub_id_1 ) {
\r
2681 case 0x09: // General MIDI (GM)
\r
2682 switch( sub_id_2 ) {
\r
2683 case 0x01: str += " GM System ON"; return str;
\r
2684 case 0x02: str += " GM System OFF"; return str;
\r
2691 // case 0x7F: // Realtime Universal
\r
2692 case 0x41: // Roland
\r
2694 switch( model_id ) {
\r
2696 str += " [GS]"; data_byte_pos++;
\r
2697 if( msgdata[3]==0x12 ) {
\r
2698 str += "DT1:"; data_byte_pos++;
\r
2699 switch( msgdata[4] ) {
\r
2701 if( msgdata[5]==0x00 ) {
\r
2702 if( msgdata[6]==0x7F ) {
\r
2703 if( msgdata[7]==0x00 ) {
\r
2704 str += " [88] System Mode Set (Mode 1: Single Module)"; return str;
\r
2706 else if( msgdata[7]==0x01 ) {
\r
2707 str += " [88] System Mode Set (Mode 2: Double Module)"; return str;
\r
2711 else if( msgdata[5]==0x01 ) {
\r
2712 int port = (msgdata[7] & 0xFF);
\r
2713 str += String.format(
\r
2714 " [88] Ch.Msg Rx Port: Block=0x%02X, Port=%s",
\r
2716 port==0?"A":port==1?"B":String.format("0x%02X",port)
\r
2722 if( msgdata[5]==0x00 ) {
\r
2723 switch( msgdata[6] ) {
\r
2724 case 0x00: str += " Master Tune: "; data_byte_pos += 3; break;
\r
2725 case 0x04: str += " Master Volume: "; data_byte_pos += 3; break;
\r
2726 case 0x05: str += " Master Key Shift: "; data_byte_pos += 3; break;
\r
2727 case 0x06: str += " Master Pan: "; data_byte_pos += 3; break;
\r
2729 switch( msgdata[7] ) {
\r
2730 case 0x00: str += " GS Reset"; return str;
\r
2731 case 0x7F: str += " Exit GS Mode"; return str;
\r
2736 else if( msgdata[5]==0x01 ) {
\r
2737 switch( msgdata[6] ) {
\r
2738 // case 0x00: str += ""; break;
\r
2739 // case 0x10: str += ""; break;
\r
2740 case 0x30: str += " Reverb Macro: "; data_byte_pos += 3; break;
\r
2741 case 0x31: str += " Reverb Character: "; data_byte_pos += 3; break;
\r
2742 case 0x32: str += " Reverb Pre-LPF: "; data_byte_pos += 3; break;
\r
2743 case 0x33: str += " Reverb Level: "; data_byte_pos += 3; break;
\r
2744 case 0x34: str += " Reverb Time: "; data_byte_pos += 3; break;
\r
2745 case 0x35: str += " Reverb Delay FB: "; data_byte_pos += 3; break;
\r
2746 case 0x36: str += " Reverb Chorus Level: "; data_byte_pos += 3; break;
\r
2747 case 0x37: str += " [88] Reverb Predelay Time: "; data_byte_pos += 3; break;
\r
2748 case 0x38: str += " Chorus Macro: "; data_byte_pos += 3; break;
\r
2749 case 0x39: str += " Chorus Pre-LPF: "; data_byte_pos += 3; break;
\r
2750 case 0x3A: str += " Chorus Level: "; data_byte_pos += 3; break;
\r
2751 case 0x3B: str += " Chorus FB: "; data_byte_pos += 3; break;
\r
2752 case 0x3C: str += " Chorus Delay: "; data_byte_pos += 3; break;
\r
2753 case 0x3D: str += " Chorus Rate: "; data_byte_pos += 3; break;
\r
2754 case 0x3E: str += " Chorus Depth: "; data_byte_pos += 3; break;
\r
2755 case 0x3F: str += " Chorus Send Level To Reverb: "; data_byte_pos += 3; break;
\r
2756 case 0x40: str += " [88] Chorus Send Level To Delay: "; data_byte_pos += 3; break;
\r
2757 case 0x50: str += " [88] Delay Macro: "; data_byte_pos += 3; break;
\r
2758 case 0x51: str += " [88] Delay Pre-LPF: "; data_byte_pos += 3; break;
\r
2759 case 0x52: str += " [88] Delay Time Center: "; data_byte_pos += 3; break;
\r
2760 case 0x53: str += " [88] Delay Time Ratio Left: "; data_byte_pos += 3; break;
\r
2761 case 0x54: str += " [88] Delay Time Ratio Right: "; data_byte_pos += 3; break;
\r
2762 case 0x55: str += " [88] Delay Level Center: "; data_byte_pos += 3; break;
\r
2763 case 0x56: str += " [88] Delay Level Left: "; data_byte_pos += 3; break;
\r
2764 case 0x57: str += " [88] Delay Level Right: "; data_byte_pos += 3; break;
\r
2765 case 0x58: str += " [88] Delay Level: "; data_byte_pos += 3; break;
\r
2766 case 0x59: str += " [88] Delay FB: "; data_byte_pos += 3; break;
\r
2767 case 0x5A: str += " [88] Delay Send Level To Reverb: "; data_byte_pos += 3; break;
\r
2770 else if( msgdata[5]==0x02 ) {
\r
2771 switch( msgdata[6] ) {
\r
2772 case 0x00: str += " [88] EQ Low Freq: "; data_byte_pos += 3; break;
\r
2773 case 0x01: str += " [88] EQ Low Gain: "; data_byte_pos += 3; break;
\r
2774 case 0x02: str += " [88] EQ High Freq: "; data_byte_pos += 3; break;
\r
2775 case 0x03: str += " [88] EQ High Gain: "; data_byte_pos += 3; break;
\r
2778 else if( msgdata[5]==0x03 ) {
\r
2779 if( msgdata[6] == 0x00 ) {
\r
2780 str += " [Pro] EFX Type: "; data_byte_pos += 3;
\r
2782 else if( msgdata[6] >= 0x03 && msgdata[6] <= 0x16 ) {
\r
2783 str += String.format(" [Pro] EFX Param %d", msgdata[6]-2 );
\r
2784 data_byte_pos += 3;
\r
2786 else if( msgdata[6] == 0x17 ) {
\r
2787 str += " [Pro] EFX Send Level To Reverb: "; data_byte_pos += 3;
\r
2789 else if( msgdata[6] == 0x18 ) {
\r
2790 str += " [Pro] EFX Send Level To Chorus: "; data_byte_pos += 3;
\r
2792 else if( msgdata[6] == 0x19 ) {
\r
2793 str += " [Pro] EFX Send Level To Delay: "; data_byte_pos += 3;
\r
2795 else if( msgdata[6] == 0x1B ) {
\r
2796 str += " [Pro] EFX Ctrl Src1: "; data_byte_pos += 3;
\r
2798 else if( msgdata[6] == 0x1C ) {
\r
2799 str += " [Pro] EFX Ctrl Depth1: "; data_byte_pos += 3;
\r
2801 else if( msgdata[6] == 0x1D ) {
\r
2802 str += " [Pro] EFX Ctrl Src2: "; data_byte_pos += 3;
\r
2804 else if( msgdata[6] == 0x1E ) {
\r
2805 str += " [Pro] EFX Ctrl Depth2: "; data_byte_pos += 3;
\r
2807 else if( msgdata[6] == 0x1F ) {
\r
2808 str += " [Pro] EFX Send EQ Switch: "; data_byte_pos += 3;
\r
2811 else if( (msgdata[5] & 0xF0) == 0x10 ) {
\r
2812 int ch = (msgdata[5] & 0x0F);
\r
2813 if( ch <= 9 ) ch--; else if( ch == 0 ) ch = 9;
\r
2814 if( msgdata[6]==0x02 ) {
\r
2815 str += String.format(
\r
2816 " Rx Ch: Part=%d(0x%02X) Ch=0x%02X", (ch+1), msgdata[5], msgdata[7]
\r
2820 else if( msgdata[6]==0x15 ) {
\r
2822 switch( msgdata[7] ) {
\r
2823 case 0: map = " NormalPart"; break;
\r
2824 case 1: map = " DrumMap1"; break;
\r
2825 case 2: map = " DrumMap2"; break;
\r
2826 default: map = String.format("0x%02X",msgdata[7]); break;
\r
2828 str += String.format(
\r
2829 " Rhythm Part: Ch=%d(0x%02X) Map=%s",
\r
2830 (ch+1), msgdata[5],
\r
2836 else if( (msgdata[5] & 0xF0) == 0x40 ) {
\r
2837 int ch = (msgdata[5] & 0x0F);
\r
2838 if( ch <= 9 ) ch--; else if( ch == 0 ) ch = 9;
\r
2839 int dt = (msgdata[7] & 0xFF);
\r
2840 if( msgdata[6]==0x20 ) {
\r
2841 str += String.format(
\r
2842 " [88] EQ: Ch=%d(0x%02X) %s",
\r
2843 (ch+1), msgdata[5],
\r
2844 dt==0 ? "OFF" : dt==1 ? "ON" : String.format("0x%02X",dt)
\r
2847 else if( msgdata[6]==0x22 ) {
\r
2848 str += String.format(
\r
2849 " [Pro] Part EFX Assign: Ch=%d(0x%02X) %s",
\r
2850 (ch+1), msgdata[5],
\r
2851 dt==0 ? "ByPass" : dt==1 ? "EFX" : String.format("0x%02X",dt)
\r
2860 str += " [GS-LCD]"; data_byte_pos++;
\r
2861 if( msgdata[3]==0x12 ) {
\r
2862 str += " [DT1]"; data_byte_pos++;
\r
2863 if( msgdata[4]==0x10 && msgdata[5]==0x00 && msgdata[6]==0x00 ) {
\r
2864 data_byte_pos += 3;
\r
2865 str += " Disp [" +(new String(
\r
2866 msgdata, data_byte_pos, msgdata.length - data_byte_pos - 2
\r
2871 case 0x14: str += " [D-50]"; data_byte_pos++; break;
\r
2872 case 0x16: str += " [MT-32]"; data_byte_pos++; break;
\r
2875 case 0x43: // Yamaha (XG)
\r
2877 if( model_id == 0x4C ) {
\r
2879 if( msgdata[3]==0 && msgdata[4]==0 && msgdata[5]==0x7E && msgdata[6]==0 ) {
\r
2880 str += " XG System ON"; return str;
\r
2890 for( i = data_byte_pos; i<msgdata.length-1; i++ ) {
\r
2891 str += String.format( " %02X", msgdata[i] );
\r
2893 if( i < msgdata.length && (int)(msgdata[i] & 0xFF) != 0xF7 ) {
\r
2894 str+=" [ Invalid EOX " + String.format( "%02X", msgdata[i] ) + " ]";
\r
2899 byte[] msg_data = msg.getMessage();
\r
2901 for( byte b : msg_data ) {
\r
2902 str += String.format( " %02X", b );
\r
2910 * MIDI シーケンスデータのtickインデックス
\r
2911 * <p>拍子、テンポ、調だけを抜き出したトラックを保持するためのインデックスです。
\r
2912 * 指定の MIDI tick の位置におけるテンポ、調、拍子を取得したり、
\r
2913 * 拍子情報から MIDI tick と小節位置との間の変換を行うために使います。
\r
2916 class SequenceTickIndex {
\r
2920 public static final int TEMPO = 0;
\r
2924 public static final int TIME_SIGNATURE = 1;
\r
2928 public static final int KEY_SIGNATURE = 2;
\r
2930 * メタメッセージタイプ → メタメッセージの種類 変換マップ
\r
2932 private static final Map<Integer,Integer> INDEX_META_TO_TRACK =
\r
2933 new HashMap<Integer,Integer>() {
\r
2936 put(0x58, TIME_SIGNATURE);
\r
2937 put(0x59, KEY_SIGNATURE);
\r
2941 * 新しいMIDIシーケンスデータのインデックスを構築します。
\r
2942 * @param sourceSequence 元のMIDIシーケンス
\r
2944 public SequenceTickIndex(Sequence sourceSequence) {
\r
2946 int ppq = sourceSequence.getResolution();
\r
2947 wholeNoteTickLength = ppq * 4;
\r
2948 tmpSequence = new Sequence(Sequence.PPQ, ppq, 3);
\r
2949 tracks = tmpSequence.getTracks();
\r
2950 Track[] sourceTracks = sourceSequence.getTracks();
\r
2951 for( Track tk : sourceTracks ) {
\r
2952 for( int i_evt = 0 ; i_evt < tk.size(); i_evt++ ) {
\r
2953 MidiEvent evt = tk.get(i_evt);
\r
2954 MidiMessage msg = evt.getMessage();
\r
2955 if( ! (msg instanceof MetaMessage) )
\r
2957 MetaMessage metaMsg = (MetaMessage)msg;
\r
2958 int metaType = metaMsg.getType();
\r
2959 Integer metaIndex = INDEX_META_TO_TRACK.get(metaType);
\r
2960 if( metaIndex != null ) tracks[metaIndex].add(evt);
\r
2964 catch ( InvalidMidiDataException e ) {
\r
2965 e.printStackTrace();
\r
2968 private Sequence tmpSequence;
\r
2970 * このtickインデックスのタイミング解像度を返します。
\r
2971 * @return このtickインデックスのタイミング解像度
\r
2973 public int getResolution() {
\r
2974 return tmpSequence.getResolution();
\r
2976 private Track[] tracks;
\r
2978 * 指定されたtick位置以前の最後のメタメッセージを返します。
\r
2979 * @param trackIndex メタメッセージの種類()
\r
2980 * @param tickPosition
\r
2983 public MetaMessage lastMetaMessageAt(int trackIndex, long tickPosition) {
\r
2984 Track track = tracks[trackIndex];
\r
2985 for(int eventIndex = track.size()-1 ; eventIndex >= 0; eventIndex--) {
\r
2986 MidiEvent event = track.get(eventIndex);
\r
2987 if( event.getTick() > tickPosition )
\r
2989 MetaMessage metaMessage = (MetaMessage)(event.getMessage());
\r
2990 if( metaMessage.getType() == 0x2F /* skip EOT (last event) */ )
\r
2992 return metaMessage;
\r
2997 private int wholeNoteTickLength;
\r
2998 public int lastBeat;
\r
2999 public int lastExtraTick;
\r
3000 public byte timesigUpper;
\r
3001 public byte timesigLowerIndex;
\r
3003 * tick位置を小節位置に変換します。
\r
3004 * @param tickPosition tick位置
\r
3007 int tickToMeasure(long tickPosition) {
\r
3008 byte extraBeats = 0;
\r
3009 MidiEvent event = null;
\r
3010 MidiMessage message = null;
\r
3011 byte[] data = null;
\r
3012 long currentTick = 0L;
\r
3013 long nextTimesigTick = 0L;
\r
3014 long prevTick = 0L;
\r
3015 long duration = 0L;
\r
3016 int lastMeasure = 0;
\r
3017 int eventIndex = 0;
\r
3019 timesigLowerIndex = 2; // =log2(4)
\r
3020 if( tracks[TIME_SIGNATURE] != null ) {
\r
3022 // Check current time-signature event
\r
3023 if( eventIndex < tracks[TIME_SIGNATURE].size() ) {
\r
3024 message = (event = tracks[TIME_SIGNATURE].get(eventIndex)).getMessage();
\r
3025 currentTick = nextTimesigTick = event.getTick();
\r
3026 if(currentTick > tickPosition || (message.getStatus() == 0xFF && ((MetaMessage)message).getType() == 0x2F /* EOT */)) {
\r
3027 currentTick = tickPosition;
\r
3030 else { // No event
\r
3031 currentTick = nextTimesigTick = tickPosition;
\r
3033 // Add measure from last event
\r
3035 int beatTickLength = wholeNoteTickLength >> timesigLowerIndex;
\r
3036 duration = currentTick - prevTick;
\r
3037 int beats = (int)( duration / beatTickLength );
\r
3038 lastExtraTick = (int)(duration % beatTickLength);
\r
3039 int measures = beats / timesigUpper;
\r
3040 extraBeats = (byte)(beats % timesigUpper);
\r
3041 lastMeasure += measures;
\r
3042 if( nextTimesigTick > tickPosition ) break; // Not reached to next time signature
\r
3044 // Reached to the next time signature, so get it.
\r
3045 if( ( data = ((MetaMessage)message).getData() ).length > 0 ) { // To skip EOT, check the data length.
\r
3046 timesigUpper = data[0];
\r
3047 timesigLowerIndex = data[1];
\r
3049 if( currentTick == tickPosition ) break; // Calculation complete
\r
3051 // Calculation incomplete, so prepare for next
\r
3053 if( extraBeats > 0 ) {
\r
3055 // Extra beats are treated as 1 measure
\r
3058 prevTick = currentTick;
\r
3062 lastBeat = extraBeats;
\r
3063 return lastMeasure;
\r
3066 * 小節位置を MIDI tick に変換します。
\r
3067 * @param measure 小節位置
\r
3068 * @return MIDI tick
\r
3070 public long measureToTick(int measure) {
\r
3071 return measureToTick(measure, 0, 0);
\r
3074 * 指定の小節位置、拍、拍内tickを、そのシーケンス全体の MIDI tick に変換します。
\r
3075 * @param measure 小節位置
\r
3077 * @param extraTick 拍内tick
\r
3078 * @return そのシーケンス全体の MIDI tick
\r
3080 public long measureToTick(int measure, int beat, int extraTick) {
\r
3081 MidiEvent evt = null;
\r
3082 MidiMessage msg = null;
\r
3083 byte[] data = null;
\r
3085 long prev_tick = 0L;
\r
3086 long duration = 0L;
\r
3087 long duration_sum = 0L;
\r
3088 long estimated_ticks;
\r
3089 int ticks_per_beat;
\r
3092 timesigLowerIndex = 2; // =log2(4)
\r
3094 ticks_per_beat = wholeNoteTickLength >> timesigLowerIndex;
\r
3095 estimated_ticks = ((measure * timesigUpper) + beat) * ticks_per_beat + extraTick;
\r
3096 if( tracks[TIME_SIGNATURE] == null || i_evt > tracks[TIME_SIGNATURE].size() ) {
\r
3097 return duration_sum + estimated_ticks;
\r
3099 msg = (evt = tracks[TIME_SIGNATURE].get(i_evt)).getMessage();
\r
3100 if( msg.getStatus() == 0xFF && ((MetaMessage)msg).getType() == 0x2F /* EOT */ ) {
\r
3101 return duration_sum + estimated_ticks;
\r
3103 duration = (tick = evt.getTick()) - prev_tick;
\r
3104 if( duration >= estimated_ticks ) {
\r
3105 return duration_sum + estimated_ticks;
\r
3107 // Re-calculate measure (ignore extra beats/ticks)
\r
3108 measure -= ( duration / (ticks_per_beat * timesigUpper) );
\r
3109 duration_sum += duration;
\r
3111 // Get next time-signature
\r
3112 data = ( (MetaMessage)msg ).getData();
\r
3113 timesigUpper = data[0];
\r
3114 timesigLowerIndex = data[1];
\r