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;
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;
27 import camidion.chordhelper.ButtonIcon;
28 import camidion.chordhelper.mididevice.MidiSequencerModel;
29 import camidion.chordhelper.music.ChordProgression;
32 * プレイリスト(MIDIシーケンスリスト)のテーブルデータモデル
34 public class PlaylistTableModel extends AbstractTableModel {
35 private MidiSequencerModel sequencerModel;
37 * このプレイリストと連携しているMIDIシーケンサモデルを返します。
39 public MidiSequencerModel getSequencerModel() { return sequencerModel; }
43 public final SequenceTrackListTableModel emptyTrackListTableModel = new SequenceTrackListTableModel(this, null, null);
47 public final MidiEventTableModel emptyEventListTableModel = new MidiEventTableModel(emptyTrackListTableModel, null);
49 * このプレイリストの選択モデルを返します。
51 public ListSelectionModel getSelectionModel() { return selectionModel; }
52 private ListSelectionModel selectionModel = new DefaultListSelectionModel() {
54 setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
58 * テーブルモデルの変更を示すイベントが、ファイル名の変更によるものかどうかをチェックします。
59 * @param event テーブルモデルの変更を示すイベント
60 * @return ファイル名の変更による場合true
62 public static boolean filenameChanged(TableModelEvent event) {
63 int c = event.getColumn();
64 return c == Column.FILENAME.ordinal() || c == TableModelEvent.ALL_COLUMNS ;
66 /** 再生中のシーケンサーの秒位置リスナー */
67 private ChangeListener mmssPosition = new ChangeListener() {
68 private int value = 0;
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) {
77 int rowIndex = sequenceModelList.indexOf(sequencerModel.getSequenceTrackListTableModel());
78 fireTableCellUpdated(rowIndex, Column.POSITION.ordinal());
83 public String toString() {
84 return String.format("%02d:%02d", value/60, value%60);
88 * 新しいプレイリストのテーブルモデルを構築します。
89 * @param sequencerModel 連携するMIDIシーケンサーモデル
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(()->{
99 } catch (InvalidMidiDataException e) {
100 throw new RuntimeException("Could not play next sequence after end-of-track",e);
108 * <p>リピートモードの場合は同じ曲をもう一度再生、そうでない場合は次の曲へ進んで再生します。
109 * 次の曲がなければ、そこで停止します。いずれの場合も曲の先頭へ戻ります。
111 * @throws InvalidMidiDataException {@link Sequencer#setSequence(Sequence)} を参照
112 * @throws IllegalStateException MIDIシーケンサデバイスが閉じている場合
114 private void goNext() throws InvalidMidiDataException {
116 sequencerModel.getSequencer().setMicrosecondPosition(0);
117 if( (Boolean)toggleRepeatAction.getValue(Action.SELECTED_KEY) || loadNext(1) ) {
118 // リピートモードのときはもう一度同じ曲を、そうでない場合は次の曲を再生開始
119 sequencerModel.start();
122 // 最後の曲が終わったので、停止状態にする
123 sequencerModel.stop();
124 // ここでボタンが停止状態に変わったはずなので、通常であれば再生ボタンが自力で再描画するところだが、
125 // セルのレンダラーが描く再生ボタンには効かないようなので、セルを突っついて再表示させる。
126 int rowIndex = sequenceModelList.indexOf(sequencerModel.getSequenceTrackListTableModel());
127 fireTableCellUpdated(rowIndex, Column.PLAY.ordinal());
133 private List<SequenceTrackListTableModel> sequenceModelList = new Vector<>();
135 * このプレイリストが保持している {@link SequenceTrackListTableModel} のリストを返します。
137 public List<SequenceTrackListTableModel> getSequenceModelList() {
138 return sequenceModelList;
141 * 行が選択されているときだけイネーブルになるアクション
143 public abstract class SelectedSequenceAction extends AbstractAction implements ListSelectionListener {
144 public SelectedSequenceAction(String name, Icon icon, String tooltip) {
145 super(name,icon); init(tooltip);
147 public SelectedSequenceAction(String name, String tooltip) {
148 super(name); init(tooltip);
151 public void valueChanged(ListSelectionEvent e) {
152 if( e.getValueIsAdjusting() ) return;
153 setEnebledBySelection();
155 protected void setEnebledBySelection() {
156 int index = selectionModel.getMinSelectionIndex();
157 setEnabled(index >= 0);
159 private void init(String tooltip) {
160 putValue(Action.SHORT_DESCRIPTION, tooltip);
161 selectionModel.addListSelectionListener(this);
162 setEnebledBySelection();
166 * 繰り返し再生ON/OFF切り替えアクション
168 public Action getToggleRepeatAction() { return toggleRepeatAction; }
169 private Action toggleRepeatAction = new AbstractAction() {
171 putValue(SHORT_DESCRIPTION, "Repeat - 繰り返し再生");
172 putValue(LARGE_ICON_KEY, new ButtonIcon(ButtonIcon.REPEAT_ICON));
173 putValue(SELECTED_KEY, false);
176 public void actionPerformed(ActionEvent event) { }
181 public Action getMoveToTopAction() { return moveToTopAction; }
182 private Action moveToTopAction = new AbstractAction() {
184 putValue(SHORT_DESCRIPTION,
185 "Move to top or previous song - 曲の先頭または前の曲へ戻る"
187 putValue(LARGE_ICON_KEY, new ButtonIcon(ButtonIcon.TOP_ICON));
190 public void actionPerformed(ActionEvent event) {
191 if( sequencerModel.getSequencer().getTickPosition() <= 40 ) {
194 } catch (InvalidMidiDataException e) {
195 throw new RuntimeException("Could not play previous sequence",e);
198 sequencerModel.setValue(0);
204 public Action getMoveToBottomAction() { return moveToBottomAction; }
205 private Action moveToBottomAction = new AbstractAction() {
207 putValue(SHORT_DESCRIPTION, "Move to next song - 次の曲へ進む");
208 putValue(LARGE_ICON_KEY, new ButtonIcon(ButtonIcon.BOTTOM_ICON));
210 public void actionPerformed(ActionEvent event) {
212 if(loadNext(1)) sequencerModel.setValue(0);
213 } catch (InvalidMidiDataException e) {
214 throw new RuntimeException("Could not play next sequence",e);
222 NUMBER("#", Integer.class, 20),
224 PLAY("Play/Stop", String.class, 60) {
226 public boolean isCellEditable() { return true; }
229 POSITION("Position", String.class, 60) {
231 public boolean isCellEditable() { return true; } // ダブルクリックだけ有効
233 public Object getValueOf(SequenceTrackListTableModel sequenceModel) {
234 if( ! sequenceModel.isOnSequencer() ) return "";
235 return sequenceModel.getParent().mmssPosition;
238 /** シーケンスの時間長(分:秒) */
239 LENGTH("Length", String.class, 80) {
241 public Object getValueOf(SequenceTrackListTableModel sequenceModel) {
242 int sec = (int)( sequenceModel.getMicrosecondLength() / 1000L / 1000L );
243 return String.format( "%02d:%02d", sec/60, sec%60 );
247 FILENAME("Filename", String.class, 100) {
249 public boolean isCellEditable() { return true; }
251 public Object getValueOf(SequenceTrackListTableModel sequenceModel) {
252 String filename = sequenceModel.getFilename();
253 return filename == null ? "" : filename;
257 MODIFIED("Modified", Boolean.class, 50) {
259 public Object getValueOf(SequenceTrackListTableModel sequenceModel) {
260 return sequenceModel.isModified();
263 /** シーケンス名(最初のトラックの名前) */
264 NAME("Sequence name", String.class, 250) {
266 public boolean isCellEditable() { return true; }
268 public Object getValueOf(SequenceTrackListTableModel sequenceModel) {
269 return sequenceModel.toString();
273 CHARSET("CharSet", String.class, 80) {
275 public boolean isCellEditable() { return true; }
277 public Object getValueOf(SequenceTrackListTableModel sequenceModel) {
278 return sequenceModel.getCharset();
282 RESOLUTION("Resolution", Integer.class, 60) {
284 public Object getValueOf(SequenceTrackListTableModel sequenceModel) {
285 return sequenceModel.getSequence().getResolution();
289 TRACKS("Tracks", Integer.class, 40) {
291 public Object getValueOf(SequenceTrackListTableModel sequenceModel) {
292 return sequenceModel.getSequence().getTracks().length;
296 DIVISION_TYPE("DivType", String.class, 50) {
297 private Map<Float,String> labels;
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);
308 public Object getValueOf(SequenceTrackListTableModel sequenceModel) {
309 String label = labels.get(sequenceModel.getSequence().getDivisionType());
310 return label == null ? "[Unknown]" : label;
314 Class<?> columnClass;
318 * @param title 列のタイトル
319 * @param columnClass 列のクラス
320 * @param perferredWidth 列の適切な幅
322 private Column(String title, Class<?> columnClass, int preferredWidth) {
324 this.columnClass = columnClass;
325 this.preferredWidth = preferredWidth;
327 public boolean isCellEditable() { return false; }
328 public Object getValueOf(SequenceTrackListTableModel sequenceModel) { return ""; }
332 public int getRowCount() { return sequenceModelList.size(); }
334 public int getColumnCount() { return Column.values().length; }
336 public String getColumnName(int column) { return Column.values()[column].title; }
338 public Class<?> getColumnClass(int column) { return Column.values()[column].columnClass; }
340 public boolean isCellEditable(int row, int column) {
341 return Column.values()[column].isCellEditable();
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));
349 public void setValueAt(Object val, int row, int column) {
350 switch(Column.values()[column]) {
353 sequenceModelList.get(row).setFilename(val.toString());
354 fireTableCellUpdated(row, column);
358 if( sequenceModelList.get(row).setName(val.toString()) )
359 fireTableCellUpdated(row, Column.MODIFIED.ordinal());
360 fireTableCellUpdated(row, column);
364 SequenceTrackListTableModel seq = sequenceModelList.get(row);
365 seq.setCharset(Charset.forName(val.toString()));
366 fireTableCellUpdated(row, column);
368 fireTableCellUpdated(row, Column.NAME.ordinal());
370 seq.fireTableDataChanged();
376 * このプレイリストに読み込まれた全シーケンスの合計時間長を返します。
377 * @return 全シーケンスの合計時間長 [秒]
379 public int getSecondLength() {
380 // マイクロ秒単位での桁あふれを回避しつつ、丸め誤差を最小限にするため、ミリ秒単位で合計を算出する。
381 return (int)(sequenceModelList.stream().mapToLong(m -> m.getMicrosecondLength() / 1000L).sum() / 1000L);
384 * 選択されたMIDIシーケンスのテーブルモデルを返します。
385 * @return 選択されたMIDIシーケンスのテーブルモデル(非選択時はnull)
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);
395 * @param sequence MIDIシーケンス(nullの場合、シーケンスを自動生成して追加)
396 * @param filename ファイル名(nullの場合、ファイル名なし)
397 * @return 追加されたシーケンスのインデックス(先頭が 0)
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);
408 * MIDIシーケンスを除去します。除去されたMIDIシーケンスがシーケンサーにロード済みだった場合、アンロードします。
409 * @param rowIndex 除去するMIDIシーケンスのインデックス(先頭が 0)
410 * @return 除去されたMIDIシーケンス
411 * @throws InvalidMidiDataException {@link Sequencer#setSequence(Sequence)} を参照
412 * @throws IllegalStateException MIDIシーケンサデバイスが閉じている場合
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;
421 * テーブル内の指定したインデックス位置にあるシーケンスをシーケンサーにロードします。
422 * インデックスに -1 を指定するとアンロードされます。
423 * 変更点がある場合、リスナー(テーブルビュー)に通知します。
425 * @param newRowIndex ロードするシーケンスのインデックス位置、アンロードするときは -1
426 * @throws InvalidMidiDataException {@link Sequencer#setSequence(Sequence)} を参照
427 * @throws IllegalStateException MIDIシーケンサデバイスが閉じているときにアンロードしようとした場合
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(),
438 if( oldSeq != null ) {
439 int oldRowIndex = sequenceModelList.indexOf(oldSeq);
440 for( int columnIndex : columnIndices ) fireTableCellUpdated(oldRowIndex, columnIndex);
442 if( newSeq != null ) {
443 for( int columnIndex : columnIndices ) fireTableCellUpdated(newRowIndex, columnIndex);
447 * 指定されたインデックスのMIDIシーケンスを再生します。
448 * @param index MIDIシーケンスのインデックス(先頭が 0)
449 * @throws InvalidMidiDataException {@link Sequencer#setSequence(Sequence)} を参照
450 * @throws IllegalStateException MIDIシーケンサデバイスが閉じている場合
452 public void play(int index) throws InvalidMidiDataException {
453 loadToSequencer(index);
454 sequencerModel.start();
457 * 指定されたMIDIシーケンスをこのプレイリストに追加し、再生されていなければ追加した曲から再生します。
458 * @param sequence MIDIシーケンス
459 * @return 追加されたシーケンスのインデックス(先頭が 0)
460 * @throws InvalidMidiDataException {@link Sequencer#setSequence(Sequence)} を参照
461 * @throws IllegalStateException MIDIシーケンサデバイスが閉じている場合
463 public int play(Sequence sequence) throws InvalidMidiDataException {
464 int lastIndex = add(sequence,"");
465 if( ! sequencerModel.getSequencer().isRunning() ) play(lastIndex);
469 * 引数で示された数だけ次へ進めたシーケンスをロードします。
470 * @param offset 進みたいシーケンス数
471 * @return 以前と異なるインデックスのシーケンスをロードできた場合true
472 * @throws InvalidMidiDataException {@link Sequencer#setSequence(Sequence)} を参照
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;
481 if( newIndex == loadedIndex ) return false;
482 loadToSequencer(newIndex);