OSDN Git Service

8a9fe9b187f6c731adb79fa734fc0fb9b2d438b1
[midichordhelper/MIDIChordHelper.git] / src / camidion / chordhelper / midieditor / SequenceTrackListTableModel.java
1 package camidion.chordhelper.midieditor;
2
3 import java.io.ByteArrayOutputStream;
4 import java.io.IOException;
5 import java.nio.charset.Charset;
6 import java.util.ArrayList;
7 import java.util.List;
8
9 import javax.sound.midi.MidiSystem;
10 import javax.sound.midi.Sequence;
11 import javax.sound.midi.Track;
12 import javax.swing.ListSelectionModel;
13 import javax.swing.table.AbstractTableModel;
14
15 import camidion.chordhelper.mididevice.MidiSequencerModel;
16 import camidion.chordhelper.music.MIDISpec;
17
18 /**
19  * MIDIシーケンス(トラックリスト)のテーブルデータモデル
20  */
21 public class SequenceTrackListTableModel extends AbstractTableModel {
22         /**
23          * 列の列挙型
24          */
25         public enum Column {
26                 TRACK_NUMBER("#", Integer.class, 20),
27                 EVENTS("Events", Integer.class, 40),
28                 MUTE("Mute", Boolean.class, 30),
29                 SOLO("Solo", Boolean.class, 30),
30                 RECORD_CHANNEL("RecCh", String.class, 40),
31                 CHANNEL("Ch", String.class, 30),
32                 TRACK_NAME("Track name", String.class, 100);
33                 String title;
34                 Class<?> columnClass;
35                 int preferredWidth;
36                 /**
37                  * 列の識別子を構築します。
38                  * @param title 列のタイトル
39                  * @param widthRatio 幅の割合
40                  * @param columnClass 列のクラス
41                  * @param perferredWidth 列の適切な幅
42                  */
43                 private Column(String title, Class<?> columnClass, int preferredWidth) {
44                         this.title = title;
45                         this.columnClass = columnClass;
46                         this.preferredWidth = preferredWidth;
47                 }
48         }
49         /**
50          * このモデルを収容している親のプレイリストを返します。
51          */
52         public PlaylistTableModel getParent() { return sequenceListTableModel; }
53         private PlaylistTableModel sequenceListTableModel;
54         /**
55          * ラップされたMIDIシーケンスのtickインデックス
56          */
57         private SequenceTickIndex sequenceTickIndex;
58         /**
59          * ファイル名を返します。
60          * @return ファイル名
61          */
62         public String getFilename() { return filename; }
63         private String filename;
64         /**
65          * ファイル名を設定します。
66          * @param filename ファイル名
67          */
68         public void setFilename(String filename) { this.filename = filename; }
69         /**
70          * タイトルや歌詞などで使うテキストの文字コードを返します。
71          * @return テキストの文字コード
72          */
73         public Charset getCharset() { return charset; }
74         private Charset charset = Charset.defaultCharset();
75         /**
76          * タイトルや歌詞などで使うテキストの文字コードを設定します。
77          * @param charset テキストの文字コード
78          */
79         public void setCharset(Charset charset) { this.charset = charset; }
80         /**
81          * トラックリスト
82          */
83         private List<MidiEventTableModel> trackModelList = new ArrayList<>();
84         /**
85          * MIDIシーケンスとファイル名から {@link SequenceTrackListTableModel} を構築します。
86          * @param sequenceListTableModel 親のプレイリスト
87          * @param sequence MIDIシーケンス
88          * @param filename ファイル名
89          */
90         public SequenceTrackListTableModel(
91                 PlaylistTableModel sequenceListTableModel,
92                 Sequence sequence,
93                 String filename
94         ) {
95                 this.sequenceListTableModel = sequenceListTableModel;
96                 setSequence(sequence);
97                 setFilename(filename);
98         }
99         @Override
100         public int getRowCount() {
101                 return sequence == null ? 0 : sequence.getTracks().length;
102         }
103         @Override
104         public int getColumnCount() { return Column.values().length; }
105         /**
106          * 列名を返します。
107          * @return 列名
108          */
109         @Override
110         public String getColumnName(int column) {
111                 return Column.values()[column].title;
112         }
113         /**
114          * 指定された列の型を返します。
115          * @return 指定された列の型
116          */
117         @Override
118         public Class<?> getColumnClass(int column) {
119                 SequenceTrackListTableModel.Column c = Column.values()[column];
120                 switch(c) {
121                 case MUTE:
122                 case SOLO: if( ! isOnSequencer() ) return String.class;
123                         // FALLTHROUGH
124                 default: return c.columnClass;
125                 }
126         }
127         @Override
128         public Object getValueAt(int row, int column) {
129                 SequenceTrackListTableModel.Column c = Column.values()[column];
130                 switch(c) {
131                 case TRACK_NUMBER: return row;
132                 case EVENTS: return sequence.getTracks()[row].size();
133                 case MUTE:
134                         return isOnSequencer() ? sequenceListTableModel.getSequencerModel().getSequencer().getTrackMute(row) : "";
135                 case SOLO:
136                         return isOnSequencer() ? sequenceListTableModel.getSequencerModel().getSequencer().getTrackSolo(row) : "";
137                 case RECORD_CHANNEL:
138                         return isOnSequencer() ? trackModelList.get(row).getRecordingChannel() : "";
139                 case CHANNEL: {
140                         int ch = trackModelList.get(row).getChannel();
141                         return ch < 0 ? "" : ch + 1 ;
142                 }
143                 case TRACK_NAME: return trackModelList.get(row).toString();
144                 default: return "";
145                 }
146         }
147         /**
148          * セルが編集可能かどうかを返します。
149          */
150         @Override
151         public boolean isCellEditable(int row, int column) {
152                 SequenceTrackListTableModel.Column c = Column.values()[column];
153                 switch(c) {
154                 case MUTE:
155                 case SOLO:
156                 case RECORD_CHANNEL: return isOnSequencer();
157                 case CHANNEL:
158                 case TRACK_NAME: return true;
159                 default: return false;
160                 }
161         }
162         /**
163          * 列の値を設定します。
164          */
165         @Override
166         public void setValueAt(Object val, int row, int column) {
167                 SequenceTrackListTableModel.Column c = Column.values()[column];
168                 switch(c) {
169                 case MUTE:
170                         sequenceListTableModel.getSequencerModel().getSequencer().setTrackMute(row, ((Boolean)val).booleanValue());
171                         break;
172                 case SOLO:
173                         sequenceListTableModel.getSequencerModel().getSequencer().setTrackSolo(row, ((Boolean)val).booleanValue());
174                         break;
175                 case RECORD_CHANNEL:
176                         trackModelList.get(row).setRecordingChannel((String)val);
177                         break;
178                 case CHANNEL: {
179                         Integer ch;
180                         try {
181                                 ch = new Integer((String)val);
182                         }
183                         catch( NumberFormatException e ) {
184                                 ch = -1;
185                                 break;
186                         }
187                         if( --ch <= 0 || ch > MIDISpec.MAX_CHANNELS )
188                                 break;
189                         MidiEventTableModel trackTableModel = trackModelList.get(row);
190                         if( ch == trackTableModel.getChannel() ) break;
191                         trackTableModel.setChannel(ch);
192                         setModified(true);
193                         fireTableCellUpdated(row, Column.EVENTS.ordinal());
194                         break;
195                 }
196                 case TRACK_NAME: trackModelList.get(row).setString((String)val); break;
197                 default: break;
198                 }
199                 fireTableCellUpdated(row,column);
200         }
201         /**
202          * MIDIシーケンスを返します。
203          * @return MIDIシーケンス
204          */
205         public Sequence getSequence() { return sequence; }
206         private Sequence sequence;
207         /**
208          * MIDIシーケンスのマイクロ秒単位の長さを返します。
209          * 曲が長すぎて {@link Sequence#getMicrosecondLength()} が負数を返してしまった場合の補正も行います。
210          * @return MIDIシーケンスの長さ[マイクロ秒]
211          */
212         public long getMicrosecondLength() {
213                 long usec = sequence.getMicrosecondLength();
214                 return usec < 0 ? usec += 0x100000000L : usec;
215         }
216         /**
217          * シーケンスtickインデックスを返します。
218          * @return シーケンスtickインデックス
219          */
220         public SequenceTickIndex getSequenceTickIndex() { return sequenceTickIndex; }
221         /**
222          * MIDIシーケンスを設定します。
223          * @param sequence MIDIシーケンス(nullを指定するとトラックリストが空になる)
224          */
225         private void setSequence(Sequence sequence) {
226                 // 旧シーケンスの録音モードを解除
227                 MidiSequencerModel sequencerModel = sequenceListTableModel.getSequencerModel();
228                 if( sequencerModel != null ) sequencerModel.getSequencer().recordDisable(null);
229                 //
230                 // トラックリストをクリア
231                 int oldSize = trackModelList.size();
232                 if( oldSize > 0 ) {
233                         trackModelList.clear();
234                         fireTableRowsDeleted(0, oldSize-1);
235                 }
236                 // 新シーケンスに置き換える
237                 if( (this.sequence = sequence) == null ) {
238                         // 新シーケンスがない場合
239                         sequenceTickIndex = null;
240                         return;
241                 }
242                 // tickインデックスを再構築
243                 fireTimeSignatureChanged();
244                 //
245                 // トラックリストを再構築
246                 Track tracks[] = sequence.getTracks();
247                 for(Track track : tracks) {
248                         trackModelList.add(new MidiEventTableModel(this, track));
249                 }
250                 // 文字コードの判定
251                 Charset cs = MIDISpec.getCharsetOf(sequence);
252                 charset = cs==null ? Charset.defaultCharset() : cs;
253                 //
254                 // トラックが挿入されたことを通知
255                 fireTableRowsInserted(0, tracks.length-1);
256         }
257         /**
258          * 拍子が変更されたとき、シーケンスtickインデックスを再作成します。
259          */
260         public void fireTimeSignatureChanged() {
261                 sequenceTickIndex = new SequenceTickIndex(sequence);
262         }
263         /**
264          * 変更されたかどうかを返します。
265          * @return 変更済みのときtrue
266          */
267         public boolean isModified() { return isModified; }
268         private boolean isModified = false;
269         /**
270          * 変更されたかどうかを設定します。
271          * @param isModified 変更されたときtrue
272          */
273         public void setModified(boolean isModified) {
274                 this.isModified = isModified;
275                 int index = sequenceListTableModel.getSequenceModelList().indexOf(this);
276                 if( index >= 0 ) sequenceListTableModel.fireTableRowsUpdated(index, index);
277         }
278         /**
279          * このシーケンスを表す文字列としてシーケンス名を返します。シーケンス名がない場合は空文字列を返します。
280          */
281         @Override
282         public String toString() {
283                 byte b[] = MIDISpec.getNameBytesOf(sequence);
284                 return b == null ? "" : new String(b, charset);
285         }
286
287         /**
288          * シーケンス名を設定します。
289          * @param name シーケンス名
290          * @return 成功したらtrue
291          */
292         public boolean setName(String name) {
293                 if( name.equals(toString()) ) return false;
294                 if( ! MIDISpec.setNameBytesOf(sequence, name.getBytes(charset)) ) return false;
295                 setModified(true);
296                 fireTableDataChanged();
297                 return true;
298         }
299         /**
300          * このシーケンスのMIDIデータのバイト列を返します。
301          * @return MIDIデータのバイト列(ない場合はnull)
302          * @throws IOException バイト列の出力に失敗した場合
303          */
304         public byte[] getMIDIdata() throws IOException {
305                 if( sequence == null || sequence.getTracks().length == 0 ) {
306                         return null;
307                 }
308                 try( ByteArrayOutputStream out = new ByteArrayOutputStream() ) {
309                         MidiSystem.write(sequence, 1, out);
310                         return out.toByteArray();
311                 } catch ( IOException e ) {
312                         throw e;
313                 }
314         }
315         /**
316          * 指定のトラックが変更されたことを通知します。
317          * @param track トラック
318          */
319         public void fireTrackChanged(Track track) {
320                 int row = indexOf(track);
321                 if( row < 0 ) return;
322                 fireTableRowsUpdated(row, row);
323                 setModified(true);
324         }
325         /**
326          * 選択されているトラックモデルを返します。
327          * @param index トラックのインデックス
328          * @return トラックモデル(見つからない場合null)
329          */
330         public MidiEventTableModel getSelectedTrackModel(ListSelectionModel selectionModel) {
331                 if( selectionModel.isSelectionEmpty() ) return null;
332                 Track tracks[] = sequence.getTracks();
333                 if( tracks.length == 0 ) return null;
334                 Track t = tracks[selectionModel.getMinSelectionIndex()];
335                 return trackModelList.stream().filter(tm -> tm.getTrack() == t).findFirst().orElse(null);
336         }
337         /**
338          * 指定のトラックがある位置のインデックスを返します。
339          * @param track トラック
340          * @return トラックのインデックス(先頭 0、トラックが見つからない場合 -1)
341          */
342         public int indexOf(Track track) {
343                 Track tracks[] = sequence.getTracks();
344                 for( int i=0; i<tracks.length; i++ ) if( tracks[i] == track ) return i;
345                 return -1;
346         }
347         /**
348          * 新しいトラックを生成し、末尾に追加します。
349          * @return 追加したトラックのインデックス(先頭 0)
350          */
351         public int createTrack() {
352                 trackModelList.add(new MidiEventTableModel(this, sequence.createTrack()));
353                 setModified(true);
354                 int newIndex = getRowCount() - 1;
355                 fireTableRowsInserted(newIndex, newIndex);
356                 return newIndex;
357         }
358         /**
359          * 選択されているトラックを削除します。
360          */
361         public void deleteSelectedTracks(ListSelectionModel selectionModel) {
362                 if( selectionModel.isSelectionEmpty() ) return;
363                 int minIndex = selectionModel.getMinSelectionIndex();
364                 int maxIndex = selectionModel.getMaxSelectionIndex();
365                 Track tracks[] = sequence.getTracks();
366                 for( int i = maxIndex; i >= minIndex; i-- ) {
367                         if( ! selectionModel.isSelectedIndex(i) ) continue;
368                         sequence.deleteTrack(tracks[i]);
369                         trackModelList.remove(i);
370                 }
371                 fireTableRowsDeleted(minIndex, maxIndex);
372                 setModified(true);
373         }
374         /**
375          * このシーケンスモデルのシーケンスをシーケンサーが操作しているか調べます。
376          * @return シーケンサーが操作していたらtrue
377          */
378         public boolean isOnSequencer() {
379                 return sequence == sequenceListTableModel.getSequencerModel().getSequencer().getSequence();
380         }
381         /**
382          * 録音しようとしているチャンネルの設定されたトラックがあるか調べます。
383          * @return 該当トラックがあればtrue
384          */
385         public boolean hasRecordChannel() {
386                 int rowCount = getRowCount();
387                 for( int row=0; row < rowCount; row++ ) {
388                         Object value = getValueAt(row, Column.RECORD_CHANNEL.ordinal());
389                         if( ! "OFF".equals(value) ) return true;
390                 }
391                 return false;
392         }
393 }