1 package camidion.chordhelper.midieditor;
3 import java.awt.event.ActionEvent;
4 import java.nio.charset.Charset;
5 import java.util.Collections;
6 import java.util.HashMap;
9 import java.util.Vector;
10 import java.util.stream.IntStream;
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;
23 import camidion.chordhelper.ButtonIcon;
24 import camidion.chordhelper.mididevice.MidiSequencerModel;
25 import camidion.chordhelper.music.MIDISpec;
28 * プレイリスト(MIDIシーケンスリスト)のテーブルデータモデル
30 public class PlaylistTableModel extends AbstractTableModel {
31 private MidiSequencerModel sequencerModel;
33 * このプレイリストと連携しているMIDIシーケンサモデルを返します。
35 public MidiSequencerModel getSequencerModel() { return sequencerModel; }
39 public final SequenceTrackListTableModel emptyTrackListTableModel = new SequenceTrackListTableModel(this, null, null, null);
43 public final MidiEventTableModel emptyEventListTableModel = new MidiEventTableModel(emptyTrackListTableModel, null);
44 /** 再生中のシーケンサーの秒位置リスナー */
45 private ChangeListener mmssPosition = new ChangeListener() {
46 private int value = 0;
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;
55 fireTableCellUpdated(sequencerModel.getSequenceTrackListTableModel(), Column.POSITION);
58 public String toString() {
59 return String.format("%02d:%02d", value/60, value%60);
63 * 新しいプレイリストのテーブルモデルを構築します。
64 * @param sequencerModel 連携するMIDIシーケンサーモデル
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());
76 * <p>リピートモードの場合は同じ曲をもう一度再生、そうでない場合は次の曲へ進んで再生します。
77 * 次の曲がなければ、そこで停止します。いずれの場合も曲の先頭へ戻ります。
79 * @throws IllegalStateException {@link #loadNext(int)} から
80 * {@link InvalidMidiDataException} がスローされた場合
81 * (MIDIシーケンサデバイスが閉じている状態で呼び出されたことが主な原因)
83 private void goNext() {
85 sequencerModel.getSequencer().setMicrosecondPosition(0);
87 if( (Boolean)toggleRepeatAction.getValue(Action.SELECTED_KEY) || loadNext(1) ) {
88 // リピートモードのときはもう一度同じ曲を、そうでない場合は次の曲を再生開始
89 sequencerModel.start();
92 // 最後の曲が終わったので、停止状態にする
93 sequencerModel.stop();
94 // ここでボタンが停止状態に変わったはずなので、通常であれば再生ボタンが自力で再描画するところだが、
95 // セルのレンダラーが描く再生ボタンには効かないようなので、セルを突っついて再表示させる。
96 fireTableCellUpdated(sequencerModel.getSequenceTrackListTableModel(), Column.PLAY);
98 } catch (InvalidMidiDataException ex) {
99 throw new IllegalStateException("Could not play next sequence after end-of-track",ex);
105 private List<SequenceTrackListTableModel> sequenceModelList = new Vector<>();
107 * このプレイリストが保持している {@link SequenceTrackListTableModel} のリストを返します。
109 public List<SequenceTrackListTableModel> getSequenceModelList() {
110 return sequenceModelList;
113 * 繰り返し再生ON/OFF切り替えアクション
115 public Action getToggleRepeatAction() { return toggleRepeatAction; }
116 private Action toggleRepeatAction = new AbstractAction() {
118 putValue(SHORT_DESCRIPTION, "Repeat - 繰り返し再生");
119 putValue(LARGE_ICON_KEY, new ButtonIcon(ButtonIcon.REPEAT_ICON));
120 putValue(SELECTED_KEY, false);
123 public void actionPerformed(ActionEvent event) { }
128 public Action getMoveToTopAction() { return moveToTopAction; }
129 private Action moveToTopAction = new AbstractAction() {
131 putValue(SHORT_DESCRIPTION,
132 "Move to top or previous song - 曲の先頭または前の曲へ戻る"
134 putValue(LARGE_ICON_KEY, new ButtonIcon(ButtonIcon.TOP_ICON));
137 public void actionPerformed(ActionEvent event) {
138 if( sequencerModel.getSequencer().getTickPosition() <= 40 ) {
141 } catch (InvalidMidiDataException e) {
142 throw new RuntimeException("Could not play previous sequence",e);
145 sequencerModel.setValue(0);
151 public Action getMoveToBottomAction() { return moveToBottomAction; }
152 private Action moveToBottomAction = new AbstractAction() {
154 putValue(SHORT_DESCRIPTION, "Move to next song - 次の曲へ進む");
155 putValue(LARGE_ICON_KEY, new ButtonIcon(ButtonIcon.BOTTOM_ICON));
157 public void actionPerformed(ActionEvent event) {
159 if(loadNext(1)) sequencerModel.setValue(0);
160 } catch (InvalidMidiDataException e) {
161 throw new RuntimeException("Could not play next sequence",e);
169 NUMBER("#", Integer.class, 20),
171 PLAY("Play/Stop", String.class, 60) {
173 public boolean isCellEditable() { return true; }
176 POSITION("Position", String.class, 60) {
178 public boolean isCellEditable() { return true; } // ダブルクリックだけ有効
180 public Object getValueOf(SequenceTrackListTableModel sequenceModel) {
181 if( ! sequenceModel.isOnSequencer() ) return "";
182 return sequenceModel.getParent().mmssPosition;
185 /** シーケンスの時間長(分:秒) */
186 LENGTH("Length", String.class, 80) {
188 public Object getValueOf(SequenceTrackListTableModel sequenceModel) {
189 int sec = (int)( sequenceModel.getMicrosecondLength() / 1000L / 1000L );
190 return String.format( "%02d:%02d", sec/60, sec%60 );
194 FILENAME("Filename", String.class, 100) {
196 public boolean isCellEditable() { return true; }
198 public Object getValueOf(SequenceTrackListTableModel sequenceModel) {
199 String filename = sequenceModel.getFilename();
200 return filename == null ? "" : filename;
204 MODIFIED("Modified", Boolean.class, 50) {
206 public Object getValueOf(SequenceTrackListTableModel sequenceModel) {
207 return sequenceModel.isModified();
210 /** シーケンス名(最初のトラックの名前) */
211 NAME("Sequence name", String.class, 250) {
213 public boolean isCellEditable() { return true; }
215 public Object getValueOf(SequenceTrackListTableModel sequenceModel) {
216 return sequenceModel.toString();
220 CHARSET("CharSet", String.class, 80) {
222 public boolean isCellEditable() { return true; }
224 public Object getValueOf(SequenceTrackListTableModel sequenceModel) {
225 return sequenceModel.getCharset();
229 RESOLUTION("Resolution", Integer.class, 60) {
231 public Object getValueOf(SequenceTrackListTableModel sequenceModel) {
232 return sequenceModel.getSequence().getResolution();
236 TRACKS("Tracks", Integer.class, 40) {
238 public Object getValueOf(SequenceTrackListTableModel sequenceModel) {
239 return sequenceModel.getSequence().getTracks().length;
243 DIVISION_TYPE("DivType", String.class, 50) {
244 private Map<Float,String> labels;
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);
255 public Object getValueOf(SequenceTrackListTableModel sequenceModel) {
256 String label = labels.get(sequenceModel.getSequence().getDivisionType());
257 return label == null ? "[Unknown]" : label;
261 Class<?> columnClass;
265 * @param title 列のタイトル
266 * @param columnClass 列のクラス
267 * @param perferredWidth 列の適切な幅
269 private Column(String title, Class<?> columnClass, int preferredWidth) {
271 this.columnClass = columnClass;
272 this.preferredWidth = preferredWidth;
274 public boolean isCellEditable() { return false; }
275 public Object getValueOf(SequenceTrackListTableModel sequenceModel) { return ""; }
278 * @param event テーブルモデルの変更を示すイベント
279 * @return この列に変更がある場合true
281 public boolean isChanged(TableModelEvent event) {
282 int index = event.getColumn();
283 return index == ordinal() || index == TableModelEvent.ALL_COLUMNS ;
287 * 連携中のシーケンサにロードされているシーケンスの行の、指定された列が変更されたか調べます。
288 * ロードされているシーケンスがない場合、変更なしとみなされます。
289 * @param event テーブルモデルの変更を示すイベント
290 * @param column 対象の列(nullを指定すると、どの列が変更されても、その行の変更だけで変更ありとみなされる)
291 * @return 変更がある場合true
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 );
300 * [row, column]にあるセルの値が更新されたことを、すべてのリスナーに通知します。
301 * @param row 更新されたセルの行
302 * @param column 更新されたセルの列
303 * @see #fireTableCellUpdated(int, int)
305 public void fireTableCellUpdated(int row, Column column) {
306 fireTableCellUpdated(row, column.ordinal());
309 * [sequence, column]にあるセルの値が更新されたことを、すべてのリスナーに通知します。
310 * @param sequence 更新されたMIDIシーケンス
311 * @param column 更新されたセルの列
312 * @see #fireTableCellUpdated(int, int)
314 public void fireTableCellUpdated(SequenceTrackListTableModel sequence, Column column) {
315 fireTableCellUpdated(sequenceModelList.indexOf(sequence), column);
319 public int getRowCount() { return sequenceModelList.size(); }
321 public int getColumnCount() { return Column.values().length; }
323 public String getColumnName(int column) { return Column.values()[column].title; }
325 public Class<?> getColumnClass(int column) { return Column.values()[column].columnClass; }
327 public boolean isCellEditable(int row, int column) {
328 return Column.values()[column].isCellEditable();
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));
336 public void setValueAt(Object val, int row, int column) {
337 switch(Column.values()[column]) {
340 sequenceModelList.get(row).setFilename(val.toString());
341 fireTableCellUpdated(row, column);
345 if( sequenceModelList.get(row).setName(val.toString()) )
346 fireTableCellUpdated(row, Column.MODIFIED);
347 fireTableCellUpdated(row, column);
351 SequenceTrackListTableModel seq = sequenceModelList.get(row);
352 seq.setCharset(Charset.forName(val.toString()));
353 fireTableCellUpdated(row, column);
355 fireTableCellUpdated(row, Column.NAME);
357 seq.fireTableDataChanged();
363 * このプレイリストに読み込まれた全シーケンスの合計時間長を返します。
364 * @return 全シーケンスの合計時間長 [秒]
366 public int getSecondLength() {
367 // マイクロ秒単位での桁あふれを回避しつつ、丸め誤差を最小限にするため、ミリ秒単位で合計を算出する。
368 return (int)(sequenceModelList.stream().mapToLong(m -> m.getMicrosecondLength() / 1000L).sum() / 1000L);
371 * ファイル名なしでMIDIシーケンスを追加します。
372 * 文字コードは自動的に判別されます(判別に失敗した場合はデフォルトの文字コードが指定されます)。
373 * @param sequence MIDIシーケンス
374 * @return 追加されたシーケンスのインデックス(先頭が 0)
376 public int add(Sequence sequence) {
377 return add(sequence, (String)null);
380 * ファイル名を指定してMIDIシーケンスを追加します。
381 * 文字コードは自動的に判別されます(判別に失敗した場合はデフォルトの文字コードが指定されます)。
382 * @param sequence MIDIシーケンス
383 * @param filename ファイル名(nullの場合、ファイル名なし)
384 * @return 追加されたシーケンスのインデックス(先頭が 0)
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);
392 * ファイル名なしで、文字コードを指定してMIDIシーケンスを追加します。
393 * @param sequence MIDIシーケンス
394 * @param charset MIDIシーケンス内のテキスト文字コード
395 * @return 追加されたシーケンスのインデックス(先頭が 0)
397 public int add(Sequence sequence, Charset charset) {
398 return add(sequence, charset, null);
401 * ファイル名、文字コードを指定してMIDIシーケンスを追加します。
402 * @param sequence MIDIシーケンス
403 * @param charset MIDIシーケンス内のテキスト文字コード
404 * @param filename ファイル名(nullの場合、ファイル名なし)
405 * @return 追加されたシーケンスのインデックス(先頭が 0)
407 public int add(Sequence sequence, Charset charset, String filename) {
408 //if( sequence == null ) {
409 // sequence = (new ChordProgression()).toMidiSequence(charset);
411 SequenceTrackListTableModel sequenceModel =
412 new SequenceTrackListTableModel(this, sequence, charset, filename);
413 int newIndex = sequenceModelList.size();
414 sequenceModelList.add(sequenceModel);
415 fireTableRowsInserted(newIndex, newIndex);
419 * MIDIシーケンスを除去します。除去されたMIDIシーケンスがシーケンサーにロード済みだった場合、アンロードします。
420 * @param rowIndex 除去するMIDIシーケンスのインデックス(先頭が 0)
421 * @return 除去されたMIDIシーケンス
422 * @throws InvalidMidiDataException {@link Sequencer#setSequence(Sequence)} を参照
423 * @throws IllegalStateException MIDIシーケンサデバイスが閉じている場合
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;
432 * テーブル内の指定したインデックス位置にあるシーケンスをシーケンサーにロードします。
433 * インデックスに -1 を指定するとアンロードされます。
434 * 変更点がある場合、リスナー(テーブルビュー)に通知します。
436 * @param newRowIndex ロードするシーケンスのインデックス位置、アンロードするときは -1
437 * @throws InvalidMidiDataException {@link Sequencer#setSequence(Sequence)} を参照
438 * @throws IllegalStateException MIDIシーケンサデバイスが閉じているときにアンロードしようとした場合
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);
448 if( newSeq != null ) {
449 fireTableCellUpdated(newRowIndex, Column.PLAY);
450 fireTableCellUpdated(newRowIndex, Column.POSITION);
454 * 指定されたインデックスのMIDIシーケンスを再生します。
455 * @param index MIDIシーケンスのインデックス(先頭が 0)
456 * @throws InvalidMidiDataException {@link Sequencer#setSequence(Sequence)} を参照
457 * @throws IllegalStateException MIDIシーケンサデバイスが閉じている場合
459 public void play(int index) throws InvalidMidiDataException {
460 loadToSequencer(index);
461 sequencerModel.start();
464 * 指定されたMIDIシーケンスをこのプレイリストに追加し、再生されていなければ追加した曲から再生します。
465 * @param sequence MIDIシーケンス
466 * @param charset 文字コード
467 * @return 追加されたシーケンスのインデックス(先頭が 0)
468 * @throws InvalidMidiDataException {@link Sequencer#setSequence(Sequence)} を参照
469 * @throws IllegalStateException MIDIシーケンサデバイスが閉じている場合
471 public int play(Sequence sequence, Charset charset) throws InvalidMidiDataException {
472 int lastIndex = add(sequence, charset);
473 if( ! sequencerModel.getSequencer().isRunning() ) play(lastIndex);
477 * 引数で示された数だけ次へ進めたシーケンスをロードします。
478 * @param offset 進みたいシーケンス数
479 * @return 以前と異なるインデックスのシーケンスをロードできた場合true
480 * @throws InvalidMidiDataException {@link Sequencer#setSequence(Sequence)} を参照
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;
489 if( newIndex == loadedIndex ) return false;
490 loadToSequencer(newIndex);