OSDN Git Service

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