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.ListSelectionModel;
13 import javax.swing.table.AbstractTableModel;
15 import camidion.chordhelper.mididevice.MidiSequencerModel;
16 import camidion.chordhelper.music.MIDISpec;
19 * MIDIシーケンス(トラックリスト)のテーブルデータモデル
21 public class SequenceTrackListTableModel extends AbstractTableModel {
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);
39 * @param widthRatio 幅の割合
40 * @param columnClass 列のクラス
41 * @param perferredWidth 列の適切な幅
43 private Column(String title, Class<?> columnClass, int preferredWidth) {
45 this.columnClass = columnClass;
46 this.preferredWidth = preferredWidth;
50 * このモデルを収容している親のプレイリストを返します。
52 public PlaylistTableModel getParent() { return sequenceListTableModel; }
53 private PlaylistTableModel sequenceListTableModel;
55 * ラップされたMIDIシーケンスのtickインデックス
57 private SequenceTickIndex sequenceTickIndex;
62 public String getFilename() { return filename; }
63 private String filename;
66 * @param filename ファイル名
68 public void setFilename(String filename) { this.filename = filename; }
70 * タイトルや歌詞などで使うテキストの文字コードを返します。
73 public Charset getCharset() { return charset; }
74 private Charset charset = Charset.defaultCharset();
76 * タイトルや歌詞などで使うテキストの文字コードを設定します。
77 * @param charset テキストの文字コード
79 public void setCharset(Charset charset) { this.charset = charset; }
83 private List<MidiEventTableModel> trackModelList = new ArrayList<>();
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 MidiEventTableModel 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; }
206 private Sequence sequence;
208 * MIDIシーケンスのマイクロ秒単位の長さを返します。
209 * 曲が長すぎて {@link Sequence#getMicrosecondLength()} が負数を返してしまった場合の補正も行います。
210 * @return MIDIシーケンスの長さ[マイクロ秒]
212 public long getMicrosecondLength() {
213 long usec = sequence.getMicrosecondLength();
214 return usec < 0 ? usec += 0x100000000L : usec;
217 * シーケンスtickインデックスを返します。
218 * @return シーケンスtickインデックス
220 public SequenceTickIndex getSequenceTickIndex() { return sequenceTickIndex; }
223 * @param sequence MIDIシーケンス(nullを指定するとトラックリストが空になる)
225 private void setSequence(Sequence sequence) {
227 MidiSequencerModel sequencerModel = sequenceListTableModel.getSequencerModel();
228 if( sequencerModel != null ) sequencerModel.getSequencer().recordDisable(null);
231 int oldSize = trackModelList.size();
233 trackModelList.clear();
234 fireTableRowsDeleted(0, oldSize-1);
237 if( (this.sequence = sequence) == null ) {
239 sequenceTickIndex = null;
243 fireTimeSignatureChanged();
246 Track tracks[] = sequence.getTracks();
247 for(Track track : tracks) {
248 trackModelList.add(new MidiEventTableModel(this, track));
251 Charset cs = MIDISpec.getCharsetOf(sequence);
252 charset = cs==null ? Charset.defaultCharset() : cs;
255 fireTableRowsInserted(0, tracks.length-1);
258 * 拍子が変更されたとき、シーケンスtickインデックスを再作成します。
260 public void fireTimeSignatureChanged() {
261 sequenceTickIndex = new SequenceTickIndex(sequence);
265 * @return 変更済みのときtrue
267 public boolean isModified() { return isModified; }
268 private boolean isModified = false;
271 * @param isModified 変更されたときtrue
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);
279 * このシーケンスを表す文字列としてシーケンス名を返します。シーケンス名がない場合は空文字列を返します。
282 public String toString() {
283 byte b[] = MIDISpec.getNameBytesOf(sequence);
284 return b == null ? "" : new String(b, charset);
292 public boolean setName(String name) {
293 if( name.equals(toString()) ) return false;
294 if( ! MIDISpec.setNameBytesOf(sequence, name.getBytes(charset)) ) return false;
296 fireTableDataChanged();
300 * このシーケンスのMIDIデータのバイト列を返します。
301 * @return MIDIデータのバイト列(ない場合はnull)
302 * @throws IOException バイト列の出力に失敗した場合
304 public byte[] getMIDIdata() throws IOException {
305 if( sequence == null || sequence.getTracks().length == 0 ) {
308 try( ByteArrayOutputStream out = new ByteArrayOutputStream() ) {
309 MidiSystem.write(sequence, 1, out);
310 return out.toByteArray();
311 } catch ( IOException e ) {
316 * 指定のトラックが変更されたことを通知します。
319 public void fireTrackChanged(Track track) {
320 int row = indexOf(track);
321 if( row < 0 ) return;
322 fireTableRowsUpdated(row, row);
326 * 選択されているトラックモデルを返します。
327 * @param index トラックのインデックス
328 * @return トラックモデル(見つからない場合null)
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);
338 * 指定のトラックがある位置のインデックスを返します。
340 * @return トラックのインデックス(先頭 0、トラックが見つからない場合 -1)
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;
348 * 新しいトラックを生成し、末尾に追加します。
349 * @return 追加したトラックのインデックス(先頭 0)
351 public int createTrack() {
352 trackModelList.add(new MidiEventTableModel(this, sequence.createTrack()));
354 int newIndex = getRowCount() - 1;
355 fireTableRowsInserted(newIndex, newIndex);
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);
371 fireTableRowsDeleted(minIndex, maxIndex);
375 * このシーケンスモデルのシーケンスをシーケンサーが操作しているか調べます。
376 * @return シーケンサーが操作していたらtrue
378 public boolean isOnSequencer() {
379 return sequence == sequenceListTableModel.getSequencerModel().getSequencer().getSequence();
382 * 録音しようとしているチャンネルの設定されたトラックがあるか調べます。
383 * @return 該当トラックがあればtrue
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;