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;
51 * このモデルを収容している親のプレイリストを返します。
53 public PlaylistTableModel getParent() { return sequenceListTableModel; }
54 private PlaylistTableModel sequenceListTableModel;
56 * ラップされたMIDIシーケンスのtickインデックス
58 private SequenceTickIndex sequenceTickIndex;
63 public String getFilename() { return filename; }
64 private String filename;
67 * @param filename ファイル名
69 public void setFilename(String filename) { this.filename = filename; }
71 * タイトルや歌詞などで使うテキストの文字コードを返します。
74 public Charset getCharset() { return charset; }
75 private Charset charset = Charset.defaultCharset();
77 * タイトルや歌詞などで使うテキストの文字コードを設定します。
78 * @param charset テキストの文字コード
80 public void setCharset(Charset charset) { this.charset = charset; }
84 private List<TrackEventListTableModel> trackModelList = new ArrayList<>();
85 private ListSelectionModel selectionModel = new DefaultListSelectionModel(){
87 setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
91 * このトラックリストの選択モデルを返します。
93 public ListSelectionModel getSelectionModel() { return selectionModel; }
95 * MIDIシーケンスとファイル名から {@link SequenceTrackListTableModel} を構築します。
96 * @param sequenceListTableModel 親のプレイリスト
97 * @param sequence MIDIシーケンス
98 * @param filename ファイル名
100 public SequenceTrackListTableModel(
101 PlaylistTableModel sequenceListTableModel,
105 this.sequenceListTableModel = sequenceListTableModel;
106 setSequence(sequence);
107 setFilename(filename);
110 public int getRowCount() {
111 return sequence == null ? 0 : sequence.getTracks().length;
114 public int getColumnCount() { return Column.values().length; }
120 public String getColumnName(int column) {
121 return Column.values()[column].title;
128 public Class<?> getColumnClass(int column) {
129 SequenceTrackListTableModel.Column c = Column.values()[column];
132 case SOLO: if( ! isOnSequencer() ) return String.class;
134 default: return c.columnClass;
138 public Object getValueAt(int row, int column) {
139 SequenceTrackListTableModel.Column c = Column.values()[column];
141 case TRACK_NUMBER: return row;
142 case EVENTS: return sequence.getTracks()[row].size();
144 return isOnSequencer() ? sequenceListTableModel.getSequencerModel().getSequencer().getTrackMute(row) : "";
146 return isOnSequencer() ? sequenceListTableModel.getSequencerModel().getSequencer().getTrackSolo(row) : "";
148 return isOnSequencer() ? trackModelList.get(row).getRecordingChannel() : "";
150 int ch = trackModelList.get(row).getChannel();
151 return ch < 0 ? "" : ch + 1 ;
153 case TRACK_NAME: return trackModelList.get(row).toString();
161 public boolean isCellEditable(int row, int column) {
162 SequenceTrackListTableModel.Column c = Column.values()[column];
166 case RECORD_CHANNEL: return isOnSequencer();
168 case TRACK_NAME: return true;
169 default: return false;
176 public void setValueAt(Object val, int row, int column) {
177 SequenceTrackListTableModel.Column c = Column.values()[column];
180 sequenceListTableModel.getSequencerModel().getSequencer().setTrackMute(row, ((Boolean)val).booleanValue());
183 sequenceListTableModel.getSequencerModel().getSequencer().setTrackSolo(row, ((Boolean)val).booleanValue());
186 trackModelList.get(row).setRecordingChannel((String)val);
191 ch = new Integer((String)val);
193 catch( NumberFormatException e ) {
197 if( --ch <= 0 || ch > MIDISpec.MAX_CHANNELS )
199 TrackEventListTableModel trackTableModel = trackModelList.get(row);
200 if( ch == trackTableModel.getChannel() ) break;
201 trackTableModel.setChannel(ch);
203 fireTableCellUpdated(row, Column.EVENTS.ordinal());
206 case TRACK_NAME: trackModelList.get(row).setString((String)val); break;
209 fireTableCellUpdated(row,column);
215 public Sequence getSequence() { return sequence; }
216 private Sequence sequence;
218 * MIDIシーケンスのマイクロ秒単位の長さを返します。
219 * 曲が長すぎて {@link Sequence#getMicrosecondLength()} が負数を返してしまった場合の補正も行います。
220 * @return MIDIシーケンスの長さ[マイクロ秒]
222 public long getMicrosecondLength() {
223 long usec = sequence.getMicrosecondLength();
224 return usec < 0 ? usec += 0x100000000L : usec;
227 * シーケンスtickインデックスを返します。
228 * @return シーケンスtickインデックス
230 public SequenceTickIndex getSequenceTickIndex() { return sequenceTickIndex; }
233 * @param sequence MIDIシーケンス(nullを指定するとトラックリストが空になる)
235 private void setSequence(Sequence sequence) {
237 MidiSequencerModel sequencerModel = sequenceListTableModel.getSequencerModel();
238 if( sequencerModel != null ) sequencerModel.getSequencer().recordDisable(null);
241 int oldSize = trackModelList.size();
243 trackModelList.clear();
244 fireTableRowsDeleted(0, oldSize-1);
247 if( (this.sequence = sequence) == null ) {
249 sequenceTickIndex = null;
253 fireTimeSignatureChanged();
256 Track tracks[] = sequence.getTracks();
257 for(Track track : tracks) {
258 trackModelList.add(new TrackEventListTableModel(this, track));
261 Charset cs = MIDISpec.getCharsetOf(sequence);
262 charset = cs==null ? Charset.defaultCharset() : cs;
265 fireTableRowsInserted(0, tracks.length-1);
268 * 拍子が変更されたとき、シーケンスtickインデックスを再作成します。
270 public void fireTimeSignatureChanged() {
271 sequenceTickIndex = new SequenceTickIndex(sequence);
275 * @return 変更済みのときtrue
277 public boolean isModified() { return isModified; }
278 private boolean isModified = false;
281 * @param isModified 変更されたときtrue
283 public void setModified(boolean isModified) {
284 this.isModified = isModified;
285 int index = sequenceListTableModel.getSequenceModelList().indexOf(this);
286 if( index >= 0 ) sequenceListTableModel.fireTableRowsUpdated(index, index);
289 * このシーケンスを表す文字列としてシーケンス名を返します。シーケンス名がない場合は空文字列を返します。
292 public String toString() {
293 byte b[] = MIDISpec.getNameBytesOf(sequence);
294 return b == null ? "" : new String(b, charset);
302 public boolean setName(String name) {
303 if( name.equals(toString()) ) return false;
304 if( ! MIDISpec.setNameBytesOf(sequence, name.getBytes(charset)) ) return false;
306 fireTableDataChanged();
310 * このシーケンスのMIDIデータのバイト列を返します。
311 * @return MIDIデータのバイト列(ない場合はnull)
312 * @throws IOException バイト列の出力に失敗した場合
314 public byte[] getMIDIdata() throws IOException {
315 if( sequence == null || sequence.getTracks().length == 0 ) {
318 try( ByteArrayOutputStream out = new ByteArrayOutputStream() ) {
319 MidiSystem.write(sequence, 1, out);
320 return out.toByteArray();
321 } catch ( IOException e ) {
326 * 指定のトラックが変更されたことを通知します。
329 public void fireTrackChanged(Track track) {
330 int row = indexOf(track);
331 if( row < 0 ) return;
332 fireTableRowsUpdated(row, row);
336 * 選択されているトラックモデルを返します。
337 * @param index トラックのインデックス
338 * @return トラックモデル(見つからない場合null)
340 public TrackEventListTableModel getSelectedTrackModel() {
341 if( selectionModel.isSelectionEmpty() ) return null;
342 Track tracks[] = sequence.getTracks();
343 if( tracks.length == 0 ) return null;
344 Track t = tracks[selectionModel.getMinSelectionIndex()];
345 return trackModelList.stream().filter(tm -> tm.getTrack() == t).findFirst().orElse(null);
348 * 指定のトラックがある位置のインデックスを返します。
350 * @return トラックのインデックス(先頭 0、トラックが見つからない場合 -1)
352 public int indexOf(Track track) {
353 Track tracks[] = sequence.getTracks();
354 for( int i=0; i<tracks.length; i++ ) if( tracks[i] == track ) return i;
358 * 新しいトラックを生成し、末尾に追加します。
359 * @return 追加したトラックのインデックス(先頭 0)
361 public int createTrack() {
362 trackModelList.add(new TrackEventListTableModel(this, sequence.createTrack()));
364 int lastRow = getRowCount() - 1;
365 fireTableRowsInserted(lastRow, lastRow);
366 selectionModel.setSelectionInterval(lastRow, lastRow);
372 public void deleteSelectedTracks() {
373 if( selectionModel.isSelectionEmpty() )
375 int minIndex = selectionModel.getMinSelectionIndex();
376 int maxIndex = selectionModel.getMaxSelectionIndex();
377 Track tracks[] = sequence.getTracks();
378 for( int i = maxIndex; i >= minIndex; i-- ) {
379 if( ! selectionModel.isSelectedIndex(i) ) continue;
380 sequence.deleteTrack(tracks[i]);
381 trackModelList.remove(i);
383 fireTableRowsDeleted(minIndex, maxIndex);
387 * このシーケンスモデルのシーケンスをシーケンサーが操作しているか調べます。
388 * @return シーケンサーが操作していたらtrue
390 public boolean isOnSequencer() {
391 return sequence == sequenceListTableModel.getSequencerModel().getSequencer().getSequence();
394 * 録音しようとしているチャンネルの設定されたトラックがあるか調べます。
395 * @return 該当トラックがあればtrue
397 public boolean hasRecordChannel() {
398 int rowCount = getRowCount();
399 for( int row=0; row < rowCount; row++ ) {
400 Object value = getValueAt(row, Column.RECORD_CHANNEL.ordinal());
401 if( ! "OFF".equals(value) ) return true;