OSDN Git Service

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