1 package camidion.chordhelper.midieditor;
3 import java.io.ByteArrayOutputStream;
4 import java.io.IOException;
5 import java.nio.charset.Charset;
6 import java.util.ArrayList;
9 import javax.sound.midi.MidiSystem;
10 import javax.sound.midi.Sequence;
11 import javax.sound.midi.Track;
12 import javax.swing.DefaultListSelectionModel;
13 import javax.swing.ListSelectionModel;
14 import javax.swing.table.AbstractTableModel;
16 import camidion.chordhelper.mididevice.MidiSequencerModel;
17 import camidion.chordhelper.music.MIDISpec;
20 * MIDIシーケンス(トラックリスト)のテーブルデータモデル
22 public class SequenceTrackListTableModel extends AbstractTableModel {
27 TRACK_NUMBER("#", Integer.class, 20),
28 EVENTS("Events", Integer.class, 40),
29 MUTE("Mute", Boolean.class, 30),
30 SOLO("Solo", Boolean.class, 30),
31 RECORD_CHANNEL("RecCh", String.class, 40),
32 CHANNEL("Ch", String.class, 30),
33 TRACK_NAME("Track name", String.class, 100);
40 * @param widthRatio 幅の割合
41 * @param columnClass 列のクラス
42 * @param perferredWidth 列の適切な幅
44 private Column(String title, Class<?> columnClass, int preferredWidth) {
46 this.columnClass = columnClass;
47 this.preferredWidth = preferredWidth;
50 private PlaylistTableModel sequenceListTableModel;
52 * このモデルを収容している親のプレイリストを返します。
54 public PlaylistTableModel getParent() { return sequenceListTableModel; }
58 private Sequence sequence;
60 * ラップされたMIDIシーケンスのtickインデックス
62 private SequenceTickIndex sequenceTickIndex;
66 private String filename;
68 * テキスト部分の文字コード(タイトル、歌詞など)
70 public Charset charset = Charset.defaultCharset();
74 private List<TrackEventListTableModel> trackModelList = new ArrayList<>();
75 private ListSelectionModel trackListSelectionModel = new DefaultListSelectionModel(){
77 setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
83 public ListSelectionModel getSelectionModel() { return trackListSelectionModel; }
85 * MIDIシーケンスとファイル名から {@link SequenceTrackListTableModel} を構築します。
86 * @param sequenceListTableModel 親のプレイリスト
87 * @param sequence MIDIシーケンス
88 * @param filename ファイル名
90 public SequenceTrackListTableModel(
91 PlaylistTableModel sequenceListTableModel,
95 this.sequenceListTableModel = sequenceListTableModel;
96 setSequence(sequence);
97 setFilename(filename);
100 public int getRowCount() {
101 return sequence == null ? 0 : sequence.getTracks().length;
104 public int getColumnCount() { return Column.values().length; }
110 public String getColumnName(int column) {
111 return Column.values()[column].title;
118 public Class<?> getColumnClass(int column) {
119 SequenceTrackListTableModel.Column c = Column.values()[column];
122 case SOLO: if( ! isOnSequencer() ) return String.class;
124 default: return c.columnClass;
128 public Object getValueAt(int row, int column) {
129 SequenceTrackListTableModel.Column c = Column.values()[column];
131 case TRACK_NUMBER: return row;
132 case EVENTS: return sequence.getTracks()[row].size();
134 return isOnSequencer() ? sequenceListTableModel.getSequencerModel().getSequencer().getTrackMute(row) : "";
136 return isOnSequencer() ? sequenceListTableModel.getSequencerModel().getSequencer().getTrackSolo(row) : "";
138 return isOnSequencer() ? trackModelList.get(row).getRecordingChannel() : "";
140 int ch = trackModelList.get(row).getChannel();
141 return ch < 0 ? "" : ch + 1 ;
143 case TRACK_NAME: return trackModelList.get(row).toString();
151 public boolean isCellEditable(int row, int column) {
152 SequenceTrackListTableModel.Column c = Column.values()[column];
156 case RECORD_CHANNEL: return isOnSequencer();
158 case TRACK_NAME: return true;
159 default: return false;
166 public void setValueAt(Object val, int row, int column) {
167 SequenceTrackListTableModel.Column c = Column.values()[column];
170 sequenceListTableModel.getSequencerModel().getSequencer().setTrackMute(row, ((Boolean)val).booleanValue());
173 sequenceListTableModel.getSequencerModel().getSequencer().setTrackSolo(row, ((Boolean)val).booleanValue());
176 trackModelList.get(row).setRecordingChannel((String)val);
181 ch = new Integer((String)val);
183 catch( NumberFormatException e ) {
187 if( --ch <= 0 || ch > MIDISpec.MAX_CHANNELS )
189 TrackEventListTableModel trackTableModel = trackModelList.get(row);
190 if( ch == trackTableModel.getChannel() ) break;
191 trackTableModel.setChannel(ch);
193 fireTableCellUpdated(row, Column.EVENTS.ordinal());
196 case TRACK_NAME: trackModelList.get(row).setString((String)val); break;
199 fireTableCellUpdated(row,column);
205 public Sequence getSequence() { return sequence; }
207 * MIDIシーケンスのマイクロ秒単位の長さを返します。
208 * 曲が長すぎて {@link Sequence#getMicrosecondLength()} が負数を返してしまった場合の補正も行います。
209 * @return MIDIシーケンスの長さ[マイクロ秒]
211 public long getMicrosecondLength() {
212 long usec = sequence.getMicrosecondLength();
213 return usec < 0 ? usec += 0x100000000L : usec;
216 * シーケンスtickインデックスを返します。
217 * @return シーケンスtickインデックス
219 public SequenceTickIndex getSequenceTickIndex() { return sequenceTickIndex; }
222 * @param sequence MIDIシーケンス(nullを指定するとトラックリストが空になる)
224 private void setSequence(Sequence sequence) {
226 MidiSequencerModel sequencerModel = sequenceListTableModel.getSequencerModel();
227 if( sequencerModel != null ) sequencerModel.getSequencer().recordDisable(null);
230 int oldSize = trackModelList.size();
232 trackModelList.clear();
233 fireTableRowsDeleted(0, oldSize-1);
236 if( (this.sequence = sequence) == null ) {
238 sequenceTickIndex = null;
242 fireTimeSignatureChanged();
245 Track tracks[] = sequence.getTracks();
246 for(Track track : tracks) {
247 trackModelList.add(new TrackEventListTableModel(this, track));
250 Charset cs = MIDISpec.getCharsetOf(sequence);
251 charset = cs==null ? Charset.defaultCharset() : cs;
254 fireTableRowsInserted(0, tracks.length-1);
257 * 拍子が変更されたとき、シーケンスtickインデックスを再作成します。
259 public void fireTimeSignatureChanged() {
260 sequenceTickIndex = new SequenceTickIndex(sequence);
262 private boolean isModified = false;
265 * @return 変更済みのときtrue
267 public boolean isModified() { return isModified; }
270 * @param isModified 変更されたときtrue
272 public void setModified(boolean isModified) {
273 this.isModified = isModified;
274 int index = sequenceListTableModel.getSequenceModelList().indexOf(this);
275 if( index >= 0 ) sequenceListTableModel.fireTableRowsUpdated(index, index);
279 * @param filename ファイル名
281 public void setFilename(String filename) { this.filename = filename; }
286 public String getFilename() { return filename; }
288 * このシーケンスを表す文字列としてシーケンス名を返します。シーケンス名がない場合は空文字列を返します。
291 public String toString() {
292 byte b[] = MIDISpec.getNameBytesOf(sequence);
293 return b == null ? "" : new String(b, charset);
300 public boolean setName(String name) {
301 if( name.equals(toString()) ) return false;
302 if( ! MIDISpec.setNameBytesOf(sequence, name.getBytes(charset)) ) return false;
304 fireTableDataChanged();
308 * このシーケンスのMIDIデータのバイト列を返します。
309 * @return MIDIデータのバイト列(ない場合はnull)
310 * @throws IOException バイト列の出力に失敗した場合
312 public byte[] getMIDIdata() throws IOException {
313 if( sequence == null || sequence.getTracks().length == 0 ) {
316 try( ByteArrayOutputStream out = new ByteArrayOutputStream() ) {
317 MidiSystem.write(sequence, 1, out);
318 return out.toByteArray();
319 } catch ( IOException e ) {
324 * 指定のトラックが変更されたことを通知します。
327 public void fireTrackChanged(Track track) {
328 int row = indexOf(track);
329 if( row < 0 ) return;
330 fireTableRowsUpdated(row, row);
334 * 選択されているトラックモデルを返します。
335 * @param index トラックのインデックス
336 * @return トラックモデル(見つからない場合null)
338 public TrackEventListTableModel getSelectedTrackModel() {
339 if( trackListSelectionModel.isSelectionEmpty() ) return null;
340 Track tracks[] = sequence.getTracks();
341 if( tracks.length == 0 ) return null;
342 Track t = tracks[trackListSelectionModel.getMinSelectionIndex()];
343 return trackModelList.stream().filter(tm -> tm.getTrack() == t).findFirst().orElse(null);
346 * 指定のトラックがある位置のインデックスを返します。
348 * @return トラックのインデックス(先頭 0、トラックが見つからない場合 -1)
350 public int indexOf(Track track) {
351 Track tracks[] = sequence.getTracks();
352 for( int i=0; i<tracks.length; i++ ) if( tracks[i] == track ) return i;
356 * 新しいトラックを生成し、末尾に追加します。
357 * @return 追加したトラックのインデックス(先頭 0)
359 public int createTrack() {
360 trackModelList.add(new TrackEventListTableModel(this, sequence.createTrack()));
362 int lastRow = getRowCount() - 1;
363 fireTableRowsInserted(lastRow, lastRow);
364 trackListSelectionModel.setSelectionInterval(lastRow, lastRow);
370 public void deleteSelectedTracks() {
371 if( trackListSelectionModel.isSelectionEmpty() )
373 int minIndex = trackListSelectionModel.getMinSelectionIndex();
374 int maxIndex = trackListSelectionModel.getMaxSelectionIndex();
375 Track tracks[] = sequence.getTracks();
376 for( int i = maxIndex; i >= minIndex; i-- ) {
377 if( ! trackListSelectionModel.isSelectedIndex(i) ) continue;
378 sequence.deleteTrack(tracks[i]);
379 trackModelList.remove(i);
381 fireTableRowsDeleted(minIndex, maxIndex);
385 * このシーケンスモデルのシーケンスをシーケンサーが操作しているか調べます。
386 * @return シーケンサーが操作していたらtrue
388 public boolean isOnSequencer() {
389 return sequence == sequenceListTableModel.getSequencerModel().getSequencer().getSequence();
392 * 録音しようとしているチャンネルの設定されたトラックがあるか調べます。
393 * @return 該当トラックがあればtrue
395 public boolean hasRecordChannel() {
396 int rowCount = getRowCount();
397 for( int row=0; row < rowCount; row++ ) {
398 Object value = getValueAt(row, Column.RECORD_CHANNEL.ordinal());
399 if( ! "OFF".equals(value) ) return true;