OSDN Git Service

1aca48cc2306fe0923f24c6dbcfb0eeeb45a175a
[midichordhelper/MIDIChordHelper.git] / src / camidion / chordhelper / midieditor / PlaylistTableModel.java
1 package camidion.chordhelper.midieditor;
2
3 import java.awt.event.ActionEvent;
4 import java.nio.charset.Charset;
5 import java.util.Collections;
6 import java.util.HashMap;
7 import java.util.List;
8 import java.util.Map;
9 import java.util.Vector;
10
11 import javax.sound.midi.InvalidMidiDataException;
12 import javax.sound.midi.Sequence;
13 import javax.sound.midi.Sequencer;
14 import javax.swing.AbstractAction;
15 import javax.swing.Action;
16 import javax.swing.DefaultListSelectionModel;
17 import javax.swing.Icon;
18 import javax.swing.ListSelectionModel;
19 import javax.swing.SwingUtilities;
20 import javax.swing.event.ChangeEvent;
21 import javax.swing.event.ChangeListener;
22 import javax.swing.event.ListSelectionEvent;
23 import javax.swing.event.ListSelectionListener;
24 import javax.swing.event.TableModelEvent;
25 import javax.swing.table.AbstractTableModel;
26
27 import camidion.chordhelper.ButtonIcon;
28 import camidion.chordhelper.mididevice.MidiSequencerModel;
29 import camidion.chordhelper.music.ChordProgression;
30
31 /**
32  * プレイリスト(MIDIシーケンスリスト)のテーブルデータモデル
33  */
34 public class PlaylistTableModel extends AbstractTableModel {
35         private MidiSequencerModel sequencerModel;
36         /**
37          * このプレイリストと連携しているMIDIシーケンサモデルを返します。
38          */
39         public MidiSequencerModel getSequencerModel() { return sequencerModel; }
40         /**
41          * 空のトラックリストモデル
42          */
43         public final SequenceTrackListTableModel emptyTrackListTableModel = new SequenceTrackListTableModel(this, null, null);
44         /**
45          * 空のイベントリストモデル
46          */
47         public final MidiEventTableModel emptyEventListTableModel = new MidiEventTableModel(emptyTrackListTableModel, null);
48         /**
49          * このプレイリストの選択モデルを返します。
50          */
51         public ListSelectionModel getSelectionModel() { return selectionModel; }
52         private ListSelectionModel selectionModel = new DefaultListSelectionModel() {
53                 {
54                         setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
55                 }
56         };
57         /**
58          * テーブルモデルの変更を示すイベントが、ファイル名の変更によるものかどうかをチェックします。
59          * @param event テーブルモデルの変更を示すイベント
60          * @return ファイル名の変更による場合true
61          */
62         public static boolean filenameChanged(TableModelEvent event) {
63                 int c = event.getColumn();
64                 return c == Column.FILENAME.ordinal() || c == TableModelEvent.ALL_COLUMNS ;
65         }
66         /** 再生中のシーケンサーの秒位置リスナー */
67         private ChangeListener mmssPosition = new ChangeListener() {
68                 private int value = 0;
69                 @Override
70                 public void stateChanged(ChangeEvent event) {
71                         Object src = event.getSource();
72                         if( src instanceof MidiSequencerModel ) {
73                                 MidiSequencerModel sequencerModel = (MidiSequencerModel)src;
74                                 int newValue = sequencerModel.getValue() / 1000;
75                                 if(value != newValue) {
76                                         value = newValue;
77                                         int rowIndex = sequenceModelList.indexOf(sequencerModel.getSequenceTrackListTableModel());
78                                         fireTableCellUpdated(rowIndex, Column.POSITION.ordinal());
79                                 }
80                         }
81                 }
82                 @Override
83                 public String toString() {
84                         return String.format("%02d:%02d", value/60, value%60);
85                 }
86         };
87         /**
88          * 新しいプレイリストのテーブルモデルを構築します。
89          * @param sequencerModel 連携するMIDIシーケンサーモデル
90          */
91         public PlaylistTableModel(MidiSequencerModel sequencerModel) {
92                 this.sequencerModel = sequencerModel;
93                 sequencerModel.addChangeListener(mmssPosition);
94                 sequencerModel.getSequencer().addMetaEventListener(msg->{
95                         // EOF(0x2F)が来て曲が終わったら次の曲へ進める
96                         if(msg.getType() == 0x2F) SwingUtilities.invokeLater(()->{
97                                 try {
98                                         goNext();
99                                 } catch (InvalidMidiDataException e) {
100                                         throw new RuntimeException("Could not play next sequence after end-of-track",e);
101                                 }
102                         });
103                 });
104         }
105         /**
106          * 次の曲へ進みます。
107          *
108          * <p>リピートモードの場合は同じ曲をもう一度再生、そうでない場合は次の曲へ進んで再生します。
109          * 次の曲がなければ、そこで停止します。いずれの場合も曲の先頭へ戻ります。
110          * </p>
111          * @throws InvalidMidiDataException {@link Sequencer#setSequence(Sequence)} を参照
112          * @throws IllegalStateException MIDIシーケンサデバイスが閉じている場合
113          */
114         private void goNext() throws InvalidMidiDataException {
115                 // とりあえず曲の先頭へ戻る
116                 sequencerModel.getSequencer().setMicrosecondPosition(0);
117                 if( (Boolean)toggleRepeatAction.getValue(Action.SELECTED_KEY) || loadNext(1) ) {
118                         // リピートモードのときはもう一度同じ曲を、そうでない場合は次の曲を再生開始
119                         sequencerModel.start();
120                 }
121                 else {
122                         // 最後の曲が終わったので、停止状態にする
123                         sequencerModel.stop();
124                         // ここでボタンが停止状態に変わったはずなので、通常であれば再生ボタンが自力で再描画するところだが、
125                         // セルのレンダラーが描く再生ボタンには効かないようなので、セルを突っついて再表示させる。
126                         int rowIndex = sequenceModelList.indexOf(sequencerModel.getSequenceTrackListTableModel());
127                         fireTableCellUpdated(rowIndex, Column.PLAY.ordinal());
128                 }
129         }
130         /**
131          * シーケンスリスト
132          */
133         private List<SequenceTrackListTableModel> sequenceModelList = new Vector<>();
134         /**
135          * このプレイリストが保持している {@link SequenceTrackListTableModel} のリストを返します。
136          */
137         public List<SequenceTrackListTableModel> getSequenceModelList() {
138                 return sequenceModelList;
139         }
140         /**
141          * 行が選択されているときだけイネーブルになるアクション
142          */
143         public abstract class SelectedSequenceAction extends AbstractAction implements ListSelectionListener {
144                 public SelectedSequenceAction(String name, Icon icon, String tooltip) {
145                         super(name,icon); init(tooltip);
146                 }
147                 public SelectedSequenceAction(String name, String tooltip) {
148                         super(name); init(tooltip);
149                 }
150                 @Override
151                 public void valueChanged(ListSelectionEvent e) {
152                         if( e.getValueIsAdjusting() ) return;
153                         setEnebledBySelection();
154                 }
155                 protected void setEnebledBySelection() {
156                         int index = selectionModel.getMinSelectionIndex();
157                         setEnabled(index >= 0);
158                 }
159                 private void init(String tooltip) {
160                         putValue(Action.SHORT_DESCRIPTION, tooltip);
161                         selectionModel.addListSelectionListener(this);
162                         setEnebledBySelection();
163                 }
164         }
165         /**
166          * 繰り返し再生ON/OFF切り替えアクション
167          */
168         public Action getToggleRepeatAction() { return toggleRepeatAction; }
169         private Action toggleRepeatAction = new AbstractAction() {
170                 {
171                         putValue(SHORT_DESCRIPTION, "Repeat - 繰り返し再生");
172                         putValue(LARGE_ICON_KEY, new ButtonIcon(ButtonIcon.REPEAT_ICON));
173                         putValue(SELECTED_KEY, false);
174                 }
175                 @Override
176                 public void actionPerformed(ActionEvent event) { }
177         };
178         /**
179          * 曲の先頭または前の曲へ戻るアクション
180          */
181         public Action getMoveToTopAction() { return moveToTopAction; }
182         private Action moveToTopAction = new AbstractAction() {
183                 {
184                         putValue(SHORT_DESCRIPTION,
185                                 "Move to top or previous song - 曲の先頭または前の曲へ戻る"
186                         );
187                         putValue(LARGE_ICON_KEY, new ButtonIcon(ButtonIcon.TOP_ICON));
188                 }
189                 @Override
190                 public void actionPerformed(ActionEvent event) {
191                         if( sequencerModel.getSequencer().getTickPosition() <= 40 ) {
192                                 try {
193                                         loadNext(-1);
194                                 } catch (InvalidMidiDataException e) {
195                                         throw new RuntimeException("Could not play previous sequence",e);
196                                 }
197                         }
198                         sequencerModel.setValue(0);
199                 }
200         };
201         /**
202          * 次の曲へ進むアクション
203          */
204         public Action getMoveToBottomAction() { return moveToBottomAction; }
205         private Action moveToBottomAction = new AbstractAction() {
206                 {
207                         putValue(SHORT_DESCRIPTION, "Move to next song - 次の曲へ進む");
208                         putValue(LARGE_ICON_KEY, new ButtonIcon(ButtonIcon.BOTTOM_ICON));
209                 }
210                 public void actionPerformed(ActionEvent event) {
211                         try {
212                                 if(loadNext(1)) sequencerModel.setValue(0);
213                         } catch (InvalidMidiDataException e) {
214                                 throw new RuntimeException("Could not play next sequence",e);
215                         }
216                 }
217         };
218         /**
219          * 列の列挙型
220          */
221         public enum Column {
222                 NUMBER("#", Integer.class, 20),
223                 /** 再生ボタン */
224                 PLAY("Play/Stop", String.class, 60) {
225                         @Override
226                         public boolean isCellEditable() { return true; }
227                 },
228                 /** 再生中の時間位置(分:秒) */
229                 POSITION("Position", String.class, 60) {
230                         @Override
231                         public boolean isCellEditable() { return true; } // ダブルクリックだけ有効
232                         @Override
233                         public Object getValueOf(SequenceTrackListTableModel sequenceModel) {
234                                 if( ! sequenceModel.isOnSequencer() ) return "";
235                                 return sequenceModel.getParent().mmssPosition;
236                         }
237                 },
238                 /** シーケンスの時間長(分:秒) */
239                 LENGTH("Length", String.class, 80) {
240                         @Override
241                         public Object getValueOf(SequenceTrackListTableModel sequenceModel) {
242                                 int sec = (int)( sequenceModel.getMicrosecondLength() / 1000L / 1000L );
243                                 return String.format( "%02d:%02d", sec/60, sec%60 );
244                         }
245                 },
246                 /** ファイル名 */
247                 FILENAME("Filename", String.class, 100) {
248                         @Override
249                         public boolean isCellEditable() { return true; }
250                         @Override
251                         public Object getValueOf(SequenceTrackListTableModel sequenceModel) {
252                                 String filename = sequenceModel.getFilename();
253                                 return filename == null ? "" : filename;
254                         }
255                 },
256                 /** 変更済みフラグ */
257                 MODIFIED("Modified", Boolean.class, 50) {
258                         @Override
259                         public Object getValueOf(SequenceTrackListTableModel sequenceModel) {
260                                 return sequenceModel.isModified();
261                         }
262                 },
263                 /** シーケンス名(最初のトラックの名前) */
264                 NAME("Sequence name", String.class, 250) {
265                         @Override
266                         public boolean isCellEditable() { return true; }
267                         @Override
268                         public Object getValueOf(SequenceTrackListTableModel sequenceModel) {
269                                 return sequenceModel.toString();
270                         }
271                 },
272                 /** 文字コード */
273                 CHARSET("CharSet", String.class, 80) {
274                         @Override
275                         public boolean isCellEditable() { return true; }
276                         @Override
277                         public Object getValueOf(SequenceTrackListTableModel sequenceModel) {
278                                 return sequenceModel.getCharset();
279                         }
280                 },
281                 /** タイミング解像度 */
282                 RESOLUTION("Resolution", Integer.class, 60) {
283                         @Override
284                         public Object getValueOf(SequenceTrackListTableModel sequenceModel) {
285                                 return sequenceModel.getSequence().getResolution();
286                         }
287                 },
288                 /** トラック数 */
289                 TRACKS("Tracks", Integer.class, 40) {
290                         @Override
291                         public Object getValueOf(SequenceTrackListTableModel sequenceModel) {
292                                 return sequenceModel.getSequence().getTracks().length;
293                         }
294                 },
295                 /** タイミング分割形式 */
296                 DIVISION_TYPE("DivType", String.class, 50) {
297                         private Map<Float,String> labels;
298                         {
299                                 Map<Float,String> m = new HashMap<Float,String>();
300                                 m.put(Sequence.PPQ, "PPQ");
301                                 m.put(Sequence.SMPTE_24, "SMPTE_24");
302                                 m.put(Sequence.SMPTE_25, "SMPTE_25");
303                                 m.put(Sequence.SMPTE_30, "SMPTE_30");
304                                 m.put(Sequence.SMPTE_30DROP, "SMPTE_30DROP");
305                                 labels = Collections.unmodifiableMap(m);
306                         }
307                         @Override
308                         public Object getValueOf(SequenceTrackListTableModel sequenceModel) {
309                                 String label = labels.get(sequenceModel.getSequence().getDivisionType());
310                                 return label == null ? "[Unknown]" : label;
311                         }
312                 };
313                 String title;
314                 Class<?> columnClass;
315                 int preferredWidth;
316                 /**
317                  * 列の識別子を構築します。
318                  * @param title 列のタイトル
319                  * @param columnClass 列のクラス
320                  * @param perferredWidth 列の適切な幅
321                  */
322                 private Column(String title, Class<?> columnClass, int preferredWidth) {
323                         this.title = title;
324                         this.columnClass = columnClass;
325                         this.preferredWidth = preferredWidth;
326                 }
327                 public boolean isCellEditable() { return false; }
328                 public Object getValueOf(SequenceTrackListTableModel sequenceModel) { return ""; }
329         }
330
331         @Override
332         public int getRowCount() { return sequenceModelList.size(); }
333         @Override
334         public int getColumnCount() { return Column.values().length; }
335         @Override
336         public String getColumnName(int column) { return Column.values()[column].title; }
337         @Override
338         public Class<?> getColumnClass(int column) { return Column.values()[column].columnClass; }
339         @Override
340         public boolean isCellEditable(int row, int column) {
341                 return Column.values()[column].isCellEditable();
342         }
343         @Override
344         public Object getValueAt(int row, int column) {
345                 PlaylistTableModel.Column c = Column.values()[column];
346                 return c == Column.NUMBER ? row : c.getValueOf(sequenceModelList.get(row));
347         }
348         @Override
349         public void setValueAt(Object val, int row, int column) {
350                 switch(Column.values()[column]) {
351                 case FILENAME:
352                         // ファイル名の変更
353                         sequenceModelList.get(row).setFilename(val.toString());
354                         fireTableCellUpdated(row, column);
355                         break;
356                 case NAME:
357                         // シーケンス名の設定または変更
358                         if( sequenceModelList.get(row).setName(val.toString()) )
359                                 fireTableCellUpdated(row, Column.MODIFIED.ordinal());
360                         fireTableCellUpdated(row, column);
361                         break;
362                 case CHARSET:
363                         // 文字コードの変更
364                         SequenceTrackListTableModel seq = sequenceModelList.get(row);
365                         seq.setCharset(Charset.forName(val.toString()));
366                         fireTableCellUpdated(row, column);
367                         // シーケンス名の表示更新
368                         fireTableCellUpdated(row, Column.NAME.ordinal());
369                         // トラック名の表示更新
370                         seq.fireTableDataChanged();
371                 default:
372                         break;
373                 }
374         }
375         /**
376          * このプレイリストに読み込まれた全シーケンスの合計時間長を返します。
377          * @return 全シーケンスの合計時間長 [秒]
378          */
379         public int getSecondLength() {
380                 // マイクロ秒単位での桁あふれを回避しつつ、丸め誤差を最小限にするため、ミリ秒単位で合計を算出する。
381                 return (int)(sequenceModelList.stream().mapToLong(m -> m.getMicrosecondLength() / 1000L).sum() / 1000L);
382         }
383         /**
384          * 選択されたMIDIシーケンスのテーブルモデルを返します。
385          * @return 選択されたMIDIシーケンスのテーブルモデル(非選択時はnull)
386          */
387         public SequenceTrackListTableModel getSelectedSequenceModel() {
388                 if( selectionModel.isSelectionEmpty() ) return null;
389                 int selectedIndex = selectionModel.getMinSelectionIndex();
390                 if( selectedIndex >= sequenceModelList.size() ) return null;
391                 return sequenceModelList.get(selectedIndex);
392         }
393         /**
394          * MIDIシーケンスを追加します。
395          * @param sequence MIDIシーケンス(nullの場合、シーケンスを自動生成して追加)
396          * @param filename ファイル名(nullの場合、ファイル名なし)
397          * @return 追加されたシーケンスのインデックス(先頭が 0)
398          */
399         public int add(Sequence sequence, String filename) {
400                 if( sequence == null ) sequence = (new ChordProgression()).toMidiSequence();
401                 sequenceModelList.add(new SequenceTrackListTableModel(this, sequence, filename));
402                 int lastIndex = sequenceModelList.size() - 1;
403                 fireTableRowsInserted(lastIndex, lastIndex);
404                 selectionModel.setSelectionInterval(lastIndex, lastIndex);
405                 return lastIndex;
406         }
407         /**
408          * MIDIシーケンスを除去します。除去されたMIDIシーケンスがシーケンサーにロード済みだった場合、アンロードします。
409          * @param rowIndex 除去するMIDIシーケンスのインデックス(先頭が 0)
410          * @return 除去されたMIDIシーケンス
411          * @throws InvalidMidiDataException {@link Sequencer#setSequence(Sequence)} を参照
412          * @throws IllegalStateException MIDIシーケンサデバイスが閉じている場合
413          */
414         public SequenceTrackListTableModel remove(int rowIndex) throws InvalidMidiDataException {
415                 SequenceTrackListTableModel removedSequence = sequenceModelList.remove(rowIndex);
416                 fireTableRowsDeleted(rowIndex, rowIndex);
417                 if(removedSequence.isOnSequencer()) sequencerModel.setSequenceTrackListTableModel(null);
418                 return removedSequence;
419         }
420         /**
421          * テーブル内の指定したインデックス位置にあるシーケンスをシーケンサーにロードします。
422          * インデックスに -1 を指定するとアンロードされます。
423          * 変更点がある場合、リスナー(テーブルビュー)に通知します。
424          *
425          * @param newRowIndex ロードするシーケンスのインデックス位置、アンロードするときは -1
426          * @throws InvalidMidiDataException {@link Sequencer#setSequence(Sequence)} を参照
427          * @throws IllegalStateException MIDIシーケンサデバイスが閉じているときにアンロードしようとした場合
428          */
429         public void loadToSequencer(int newRowIndex) throws InvalidMidiDataException {
430                 SequenceTrackListTableModel oldSeq = sequencerModel.getSequenceTrackListTableModel();
431                 SequenceTrackListTableModel newSeq = (newRowIndex < 0 || sequenceModelList.isEmpty() ? null : sequenceModelList.get(newRowIndex));
432                 if( oldSeq == newSeq ) return;
433                 sequencerModel.setSequenceTrackListTableModel(newSeq);
434                 int columnIndices[] = {
435                         Column.PLAY.ordinal(),
436                         Column.POSITION.ordinal(),
437                 };
438                 if( oldSeq != null ) {
439                         int oldRowIndex = sequenceModelList.indexOf(oldSeq);
440                         for( int columnIndex : columnIndices ) fireTableCellUpdated(oldRowIndex, columnIndex);
441                 }
442                 if( newSeq != null ) {
443                         for( int columnIndex : columnIndices ) fireTableCellUpdated(newRowIndex, columnIndex);
444                 }
445         }
446         /**
447          * 指定されたインデックスのMIDIシーケンスを再生します。
448          * @param index MIDIシーケンスのインデックス(先頭が 0)
449          * @throws InvalidMidiDataException {@link Sequencer#setSequence(Sequence)} を参照
450          * @throws IllegalStateException MIDIシーケンサデバイスが閉じている場合
451          */
452         public void play(int index) throws InvalidMidiDataException {
453                 loadToSequencer(index);
454                 sequencerModel.start();
455         }
456         /**
457          * 指定されたMIDIシーケンスをこのプレイリストに追加し、再生されていなければ追加した曲から再生します。
458          * @param sequence MIDIシーケンス
459          * @return 追加されたシーケンスのインデックス(先頭が 0)
460          * @throws InvalidMidiDataException {@link Sequencer#setSequence(Sequence)} を参照
461          * @throws IllegalStateException MIDIシーケンサデバイスが閉じている場合
462          */
463         public int play(Sequence sequence) throws InvalidMidiDataException {
464                 int lastIndex = add(sequence,"");
465                 if( ! sequencerModel.getSequencer().isRunning() ) play(lastIndex);
466                 return lastIndex;
467         }
468         /**
469          * 引数で示された数だけ次へ進めたシーケンスをロードします。
470          * @param offset 進みたいシーケンス数
471          * @return 以前と異なるインデックスのシーケンスをロードできた場合true
472          * @throws InvalidMidiDataException {@link Sequencer#setSequence(Sequence)} を参照
473          */
474         private boolean loadNext(int offset) throws InvalidMidiDataException {
475                 int loadedIndex = sequenceModelList.indexOf(sequencerModel.getSequenceTrackListTableModel());
476                 int newIndex = loadedIndex + offset;
477                 if( newIndex < 0 ) newIndex = 0; else {
478                         int sz = sequenceModelList.size();
479                         if( newIndex >= sz ) newIndex = sz - 1;
480                 }
481                 if( newIndex == loadedIndex ) return false;
482                 loadToSequencer(newIndex);
483                 return true;
484         }
485 }