OSDN Git Service

dc57c63cc26d075d3bf0227e34d7f0bfa1953dfb
[midichordhelper/MIDIChordHelper.git] / src / camidion / chordhelper / midieditor / MidiEventTableModel.java
1 package camidion.chordhelper.midieditor;
2
3 import java.nio.charset.Charset;
4 import java.util.Vector;
5
6 import javax.sound.midi.InvalidMidiDataException;
7 import javax.sound.midi.MidiEvent;
8 import javax.sound.midi.MidiMessage;
9 import javax.sound.midi.Sequencer;
10 import javax.sound.midi.ShortMessage;
11 import javax.sound.midi.Track;
12 import javax.swing.DefaultListSelectionModel;
13 import javax.swing.ListSelectionModel;
14 import javax.swing.table.AbstractTableModel;
15
16 import camidion.chordhelper.music.MIDISpec;
17
18 /**
19  * MIDIトラック(MIDIイベントリスト)テーブルモデル
20  */
21 public class MidiEventTableModel extends AbstractTableModel {
22         /**
23          * 列
24          */
25         public enum Column {
26                 /** MIDIイベント番号 */
27                 EVENT_NUMBER("#", Integer.class, 15) {
28                         @Override
29                         public boolean isCellEditable() { return false; }
30                 },
31                 /** tick位置 */
32                 TICK_POSITION("TickPos.", Long.class, 40) {
33                         @Override
34                         public Object getValue(MidiEvent event) {
35                                 return event.getTick();
36                         }
37                 },
38                 /** tick位置に対応する小節 */
39                 MEASURE_POSITION("Measure", Integer.class, 30) {
40                         public Object getValue(SequenceTrackListTableModel seq, MidiEvent event) {
41                                 return seq.getSequenceTickIndex().tickToMeasure(event.getTick()) + 1;
42                         }
43                 },
44                 /** tick位置に対応する拍 */
45                 BEAT_POSITION("Beat", Integer.class, 20) {
46                         @Override
47                         public Object getValue(SequenceTrackListTableModel seq, MidiEvent event) {
48                                 SequenceTickIndex sti = seq.getSequenceTickIndex();
49                                 sti.tickToMeasure(event.getTick());
50                                 return sti.lastBeat + 1;
51                         }
52                 },
53                 /** tick位置に対応する余剰tick(拍に収まらずに余ったtick数) */
54                 EXTRA_TICK_POSITION("ExTick", Integer.class, 20) {
55                         @Override
56                         public Object getValue(SequenceTrackListTableModel seq, MidiEvent event) {
57                                 SequenceTickIndex sti = seq.getSequenceTickIndex();
58                                 sti.tickToMeasure(event.getTick());
59                                 return sti.lastExtraTick;
60                         }
61                 },
62                 /** MIDIメッセージ */
63                 MESSAGE("MIDI Message", String.class, 300) {
64                         @Override
65                         public Object getValue(SequenceTrackListTableModel seq, MidiEvent event) {
66                                 return MIDISpec.msgToString(event.getMessage(), seq.getCharset());
67                         }
68                 };
69                 private String title;
70                 private Class<?> columnClass;
71                 int preferredWidth;
72                 /**
73                  * 列の識別子を構築します。
74                  * @param title 列のタイトル
75                  * @param widthRatio 幅の割合
76                  * @param columnClass 列のクラス
77                  * @param perferredWidth 列の適切な幅
78                  */
79                 private Column(String title, Class<?> columnClass, int preferredWidth) {
80                         this.title = title;
81                         this.columnClass = columnClass;
82                         this.preferredWidth = preferredWidth;
83                 }
84                 /**
85                  * セルを編集できるときtrue、編集できないときfalseを返します。
86                  */
87                 public boolean isCellEditable() { return true; }
88                 /**
89                  * 列の値を返します。
90                  * @param event 対象イベント
91                  * @return この列の対象イベントにおける値
92                  */
93                 public Object getValue(MidiEvent event) { return ""; }
94                 /**
95                  * 列の値を返します。
96                  * @param sti 対象シーケンスモデル
97                  * @param event 対象イベント
98                  * @return この列の対象イベントにおける値
99                  */
100                 public Object getValue(SequenceTrackListTableModel seq, MidiEvent event) {
101                         return getValue(event);
102                 }
103         }
104         /**
105          * ラップされているMIDIトラック
106          */
107         private Track track;
108         private SequenceTrackListTableModel sequenceTrackListTableModel;
109         /**
110          * このトラックモデルを収容している親のシーケンスモデルを返します。
111          */
112         public SequenceTrackListTableModel getParent() {
113                 return sequenceTrackListTableModel;
114         }
115         /**
116          * 選択状態を返します。
117          */
118         public ListSelectionModel getSelectionModel() { return selectionModel; }
119         private ListSelectionModel selectionModel = new DefaultListSelectionModel() {
120                 {
121                         setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
122                 }
123         };
124         /**
125          * シーケンスを親にして、その特定のトラックに連動する
126          * MIDIトラックモデルを構築します。
127          *
128          * @param parent 親のシーケンスモデル
129          * @param track ラップするMIDIトラック(ない場合はnull)
130          */
131         public MidiEventTableModel(SequenceTrackListTableModel sequenceTrackListTableModel, Track track) {
132                 this.track = track;
133                 this.sequenceTrackListTableModel = sequenceTrackListTableModel;
134         }
135         @Override
136         public int getRowCount() { return track == null ? 0 : track.size(); }
137         @Override
138         public int getColumnCount() { return Column.values().length; }
139         /**
140          * 列名を返します。
141          */
142         @Override
143         public String getColumnName(int column) {
144                 return Column.values()[column].title;
145         }
146         /**
147          * 列のクラスを返します。
148          */
149         @Override
150         public Class<?> getColumnClass(int column) {
151                 return Column.values()[column].columnClass;
152         }
153         @Override
154         public Object getValueAt(int row, int column) {
155                 MidiEventTableModel.Column c = Column.values()[column];
156                 if( c == Column.EVENT_NUMBER ) return row;
157                 MidiEvent event = track.get(row);
158                 switch(c) {
159                 case MEASURE_POSITION:
160                 case BEAT_POSITION:
161                 case EXTRA_TICK_POSITION:
162                 case MESSAGE: return c.getValue(sequenceTrackListTableModel, event);
163                 default: return c.getValue(event);
164                 }
165         }
166         /**
167          * セルを編集できるときtrue、編集できないときfalseを返します。
168          */
169         @Override
170         public boolean isCellEditable(int row, int column) {
171                 return Column.values()[column].isCellEditable();
172         }
173         /**
174          * セルの値を変更します。
175          */
176         @Override
177         public void setValueAt(Object value, int row, int column) {
178                 long newTick;
179                 switch(Column.values()[column]) {
180                 case TICK_POSITION: newTick = (Long)value; break;
181                 case MEASURE_POSITION:
182                         newTick = sequenceTrackListTableModel.getSequenceTickIndex().measureToTick(
183                                 (Integer)value - 1,
184                                 (Integer)getValueAt( row, Column.BEAT_POSITION.ordinal() ) - 1,
185                                 (Integer)getValueAt( row, Column.EXTRA_TICK_POSITION.ordinal() )
186                         );
187                         break;
188                 case BEAT_POSITION:
189                         newTick = sequenceTrackListTableModel.getSequenceTickIndex().measureToTick(
190                                 (Integer)getValueAt( row, Column.MEASURE_POSITION.ordinal() ) - 1,
191                                 (Integer)value - 1,
192                                 (Integer)getValueAt( row, Column.EXTRA_TICK_POSITION.ordinal() )
193                         );
194                         break;
195                 case EXTRA_TICK_POSITION:
196                         newTick = sequenceTrackListTableModel.getSequenceTickIndex().measureToTick(
197                                 (Integer)getValueAt( row, Column.MEASURE_POSITION.ordinal() ) - 1,
198                                 (Integer)getValueAt( row, Column.BEAT_POSITION.ordinal() ) - 1,
199                                 (Integer)value
200                         );
201                         break;
202                 default: return;
203                 }
204                 MidiEvent oldMidiEvent = track.get(row);
205                 if( oldMidiEvent.getTick() == newTick ) return;
206                 MidiEvent newMidiEvent = new MidiEvent(oldMidiEvent.getMessage(), newTick);
207                 track.remove(oldMidiEvent);
208                 track.add(newMidiEvent);
209                 fireTableDataChanged();
210                 sequenceTrackListTableModel.setModified(true);
211         }
212         /**
213          * MIDIトラックを返します。
214          * @return MIDIトラック
215          */
216         public Track getTrack() { return track; }
217         /**
218          * トラック名を返します。
219          */
220         @Override
221         public String toString() {
222                 byte b[] = MIDISpec.getNameBytesOf(track);
223                 if( b == null ) return "";
224                 Charset cs = Charset.defaultCharset();
225                 if( sequenceTrackListTableModel != null )
226                         cs = sequenceTrackListTableModel.getCharset();
227                 return new String(b, cs);
228         }
229         /**
230          * トラック名を設定します。
231          * @param name トラック名
232          * @return 設定が行われたらtrue
233          */
234         public boolean setString(String name) {
235                 if( name.equals(toString()) || ! MIDISpec.setNameBytesOf(
236                         track, name.getBytes(sequenceTrackListTableModel.getCharset()))
237                 ) return false;
238                 sequenceTrackListTableModel.setModified(true);
239                 fireTableDataChanged();
240                 return true;
241         }
242         private String recordingChannel = "OFF";
243         /**
244          * 録音中のMIDIチャンネルを返します。
245          * @return 録音中のMIDIチャンネル
246          */
247         public String getRecordingChannel() { return recordingChannel; }
248         /**
249          * 録音中のMIDIチャンネルを設定します。
250          * @param recordingChannel 録音中のMIDIチャンネル
251          */
252         public void setRecordingChannel(String recordingChannel) {
253                 Sequencer sequencer = sequenceTrackListTableModel.getParent().getSequencerModel().getSequencer();
254                 if( recordingChannel.equals("OFF") ) sequencer.recordDisable(track);
255                 else if( recordingChannel.equals("ALL") ) sequencer.recordEnable(track,-1);
256                 else try {
257                         sequencer.recordEnable(track, Integer.decode(recordingChannel).intValue()-1);
258                 } catch( NumberFormatException nfe ) {
259                         sequencer.recordDisable(track);
260                         this.recordingChannel = "OFF";
261                         return;
262                 }
263                 this.recordingChannel = recordingChannel;
264         }
265         /**
266          * このトラックの対象MIDIチャンネルを返します。
267          * <p>全てのチャンネルメッセージが同じMIDIチャンネルの場合、
268          * そのMIDIチャンネルを返します。
269          * MIDIチャンネルの異なるチャンネルメッセージが一つでも含まれていた場合、
270          * -1 を返します。
271          * </p>
272          * @return 対象MIDIチャンネル(不統一の場合 -1)
273          */
274         public int getChannel() {
275                 int prevCh = -1;
276                 int trackSize = track.size();
277                 for( int index=0; index < trackSize; index++ ) {
278                         MidiMessage msg = track.get(index).getMessage();
279                         if( ! (msg instanceof ShortMessage) ) continue;
280                         ShortMessage smsg = (ShortMessage)msg;
281                         if( ! MIDISpec.isChannelMessage(smsg) ) continue;
282                         int ch = smsg.getChannel();
283                         if( prevCh >= 0 && prevCh != ch ) return -1;
284                         prevCh = ch;
285                 }
286                 return prevCh;
287         }
288         /**
289          * 指定されたMIDIチャンネルをすべてのチャンネルメッセージに対して設定します。
290          * @param channel MIDIチャンネル
291          */
292         public void setChannel(int channel) {
293                 boolean isModified = false;
294                 int trackSize = track.size();
295                 for( int index=0; index < trackSize; index++ ) {
296                         MidiMessage m = track.get(index).getMessage();
297                         if( ! (m instanceof ShortMessage) ) continue;
298                         ShortMessage sm = (ShortMessage)m;
299                         if( ! MIDISpec.isChannelMessage(sm) || sm.getChannel() == channel ) continue;
300                         try {
301                                 sm.setMessage(sm.getCommand(), channel, sm.getData1(), sm.getData2());
302                                 isModified = true;
303                         }
304                         catch( InvalidMidiDataException e ) {
305                                 e.printStackTrace();
306                         }
307                 }
308                 if( isModified ) {
309                         sequenceTrackListTableModel.fireTrackChanged(track);
310                         fireTableDataChanged();
311                 }
312         }
313         /**
314          * 指定の MIDI tick 位置にあるイベントを二分探索し、
315          * そのイベントの行インデックスを返します。
316          * @param tick MIDI tick
317          * @return 行インデックス
318          */
319         public int tickToIndex(long tick) {
320                 if( track == null ) return 0;
321                 int minIndex = 0;
322                 int maxIndex = track.size() - 1;
323                 while( minIndex < maxIndex ) {
324                         int currentIndex = (minIndex + maxIndex) / 2 ;
325                         long currentTick = track.get(currentIndex).getTick();
326                         if( tick > currentTick ) minIndex = currentIndex + 1;
327                         else if( tick < currentTick ) maxIndex = currentIndex - 1;
328                         else return currentIndex;
329                 }
330                 return (minIndex + maxIndex) / 2;
331         }
332         /**
333          * NoteOn/NoteOff ペアの一方の行インデックスから、
334          * もう一方(ペアの相手)の行インデックスを返します。
335          * @param index 行インデックス
336          * @return ペアを構成する相手の行インデックス(ない場合は -1)
337          */
338         public int getIndexOfPartnerFor(int index) {
339                 if( track == null || index >= track.size() ) return -1;
340                 MidiMessage msg = track.get(index).getMessage();
341                 if( ! (msg instanceof ShortMessage) ) return -1;
342                 ShortMessage sm = (ShortMessage)msg;
343                 int cmd = sm.getCommand();
344                 int i;
345                 int ch = sm.getChannel();
346                 int note = sm.getData1();
347                 MidiMessage partner_msg;
348                 ShortMessage partner_sm;
349                 int partner_cmd;
350
351                 switch( cmd ) {
352                 case 0x90: // NoteOn
353                 if( sm.getData2() > 0 ) {
354                         // Search NoteOff event forward
355                         for( i = index + 1; i < track.size(); i++ ) {
356                                 partner_msg = track.get(i).getMessage();
357                                 if( ! (partner_msg instanceof ShortMessage ) ) continue;
358                                 partner_sm = (ShortMessage)partner_msg;
359                                 partner_cmd = partner_sm.getCommand();
360                                 if( partner_cmd != 0x80 && partner_cmd != 0x90 ||
361                                                 partner_cmd == 0x90 && partner_sm.getData2() > 0
362                                                 ) {
363                                         // Not NoteOff
364                                         continue;
365                                 }
366                                 if( ch != partner_sm.getChannel() || note != partner_sm.getData1() ) {
367                                         // Not my partner
368                                         continue;
369                                 }
370                                 return i;
371                         }
372                         break;
373                 }
374                 // When velocity is 0, it means Note Off, so no break.
375                 case 0x80: // NoteOff
376                         // Search NoteOn event backward
377                         for( i = index - 1; i >= 0; i-- ) {
378                                 partner_msg = track.get(i).getMessage();
379                                 if( ! (partner_msg instanceof ShortMessage ) ) continue;
380                                 partner_sm = (ShortMessage)partner_msg;
381                                 partner_cmd = partner_sm.getCommand();
382                                 if( partner_cmd != 0x90 || partner_sm.getData2() <= 0 ) {
383                                         // Not NoteOn
384                                         continue;
385                                 }
386                                 if( ch != partner_sm.getChannel() || note != partner_sm.getData1() ) {
387                                         // Not my partner
388                                         continue;
389                                 }
390                                 return i;
391                         }
392                         break;
393                 }
394                 // Not found
395                 return -1;
396         }
397         /**
398          * ノートメッセージかどうか調べます。
399          * @param index 行インデックス
400          * @return Note On または Note Off のとき true
401          */
402         public boolean isNote(int index) {
403                 MidiEvent midiEvent = getMidiEvent(index);
404                 MidiMessage msg = midiEvent.getMessage();
405                 if( ! (msg instanceof ShortMessage) ) return false;
406                 int cmd = ((ShortMessage)msg).getCommand();
407                 return cmd == ShortMessage.NOTE_ON || cmd == ShortMessage.NOTE_OFF ;
408         }
409         /**
410          * 指定の行インデックスのMIDIイベントを返します。
411          * @param index 行インデックス
412          * @return MIDIイベント
413          */
414         public MidiEvent getMidiEvent(int index) {
415                 return track==null ? null : track.get(index);
416         }
417         /**
418          * 選択されているMIDIイベントを返します。
419          * @param eventSelectionModel 選択モデル
420          * @return 選択されているMIDIイベント
421          */
422         public MidiEvent[] getSelectedMidiEvents(ListSelectionModel eventSelectionModel) {
423                 Vector<MidiEvent> events = new Vector<MidiEvent>();
424                 if( ! eventSelectionModel.isSelectionEmpty() ) {
425                         int i = eventSelectionModel.getMinSelectionIndex();
426                         int max = eventSelectionModel.getMaxSelectionIndex();
427                         for( ; i <= max; i++ )
428                                 if( eventSelectionModel.isSelectedIndex(i) )
429                                         events.add(track.get(i));
430                 }
431                 return events.toArray(new MidiEvent[1]);
432         }
433         /**
434          * MIDIイベントを追加します。
435          * @param midiEvent 追加するMIDIイベント
436          * @return 追加できたらtrue
437          */
438         public boolean addMidiEvent(MidiEvent midiEvent) {
439                 if( track == null || !(track.add(midiEvent)) )
440                         return false;
441                 if( MIDISpec.isTimeSignature(midiEvent.getMessage()) )
442                         sequenceTrackListTableModel.fireTimeSignatureChanged();
443                 sequenceTrackListTableModel.fireTrackChanged(track);
444                 int lastIndex = track.size() - 1;
445                 fireTableRowsInserted( lastIndex-1, lastIndex-1 );
446                 return true;
447         }
448         /**
449          * MIDIイベントを追加します。
450          * @param midiEvents 追加するMIDIイベント
451          * @param destinationTick 追加先tick
452          * @param sourcePPQ PPQ値(タイミング解像度)
453          * @return 追加できたらtrue
454          */
455         public boolean addMidiEvents(MidiEvent midiEvents[], long destinationTick, int sourcePPQ) {
456                 if( track == null ) return false;
457                 int destinationPPQ = sequenceTrackListTableModel.getSequence().getResolution();
458                 boolean done = false;
459                 boolean hasTimeSignature = false;
460                 long firstSourceEventTick = -1;
461                 for( MidiEvent sourceEvent : midiEvents ) {
462                         long sourceEventTick = sourceEvent.getTick();
463                         MidiMessage msg = sourceEvent.getMessage();
464                         long newTick = destinationTick;
465                         if( firstSourceEventTick < 0 ) {
466                                 firstSourceEventTick = sourceEventTick;
467                         }
468                         else {
469                                 newTick += (sourceEventTick - firstSourceEventTick) * destinationPPQ / sourcePPQ;
470                         }
471                         if( ! track.add(new MidiEvent(msg, newTick)) ) continue;
472                         done = true;
473                         if( MIDISpec.isTimeSignature(msg) ) hasTimeSignature = true;
474                 }
475                 if( done ) {
476                         if( hasTimeSignature ) sequenceTrackListTableModel.fireTimeSignatureChanged();
477                         sequenceTrackListTableModel.fireTrackChanged(track);
478                         int lastIndex = track.size() - 1;
479                         int oldLastIndex = lastIndex - midiEvents.length;
480                         fireTableRowsInserted(oldLastIndex, lastIndex);
481                 }
482                 return done;
483         }
484         /**
485          * MIDIイベントを除去します。
486          * @param midiEvents 除去するMIDIイベント
487          */
488         public void removeMidiEvents(MidiEvent midiEvents[]) {
489                 if( track == null ) return;
490                 boolean hadTimeSignature = false;
491                 for( MidiEvent e : midiEvents ) {
492                         if( MIDISpec.isTimeSignature(e.getMessage()) ) hadTimeSignature = true;
493                         track.remove(e);
494                 }
495                 if( hadTimeSignature ) {
496                         sequenceTrackListTableModel.fireTimeSignatureChanged();
497                 }
498                 sequenceTrackListTableModel.fireTrackChanged(track);
499                 int lastIndex = track.size() - 1;
500                 int oldLastIndex = lastIndex + midiEvents.length;
501                 if(lastIndex < 0) lastIndex = 0;
502                 fireTableRowsDeleted(oldLastIndex, lastIndex);
503         }
504 }