1 package camidion.chordhelper.midieditor;
3 import java.nio.charset.Charset;
4 import java.util.Vector;
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;
16 import camidion.chordhelper.music.MIDISpec;
19 * MIDIトラック(MIDIイベントリスト)テーブルモデル
21 public class MidiEventTableModel extends AbstractTableModel {
27 EVENT_NUMBER("#", Integer.class, 15) {
29 public boolean isCellEditable() { return false; }
32 TICK_POSITION("TickPos.", Long.class, 40) {
34 public Object getValue(MidiEvent event) {
35 return event.getTick();
39 MEASURE_POSITION("Measure", Integer.class, 30) {
40 public Object getValue(SequenceTrackListTableModel seq, MidiEvent event) {
41 return seq.getSequenceTickIndex().tickToMeasure(event.getTick()) + 1;
45 BEAT_POSITION("Beat", Integer.class, 20) {
47 public Object getValue(SequenceTrackListTableModel seq, MidiEvent event) {
48 SequenceTickIndex sti = seq.getSequenceTickIndex();
49 sti.tickToMeasure(event.getTick());
50 return sti.lastBeat + 1;
53 /** tick位置に対応する余剰tick(拍に収まらずに余ったtick数) */
54 EXTRA_TICK_POSITION("ExTick", Integer.class, 20) {
56 public Object getValue(SequenceTrackListTableModel seq, MidiEvent event) {
57 SequenceTickIndex sti = seq.getSequenceTickIndex();
58 sti.tickToMeasure(event.getTick());
59 return sti.lastExtraTick;
63 MESSAGE("MIDI Message", String.class, 300) {
65 public Object getValue(SequenceTrackListTableModel seq, MidiEvent event) {
66 return MIDISpec.msgToString(event.getMessage(), seq.getCharset());
70 private Class<?> columnClass;
75 * @param widthRatio 幅の割合
76 * @param columnClass 列のクラス
77 * @param perferredWidth 列の適切な幅
79 private Column(String title, Class<?> columnClass, int preferredWidth) {
81 this.columnClass = columnClass;
82 this.preferredWidth = preferredWidth;
85 * セルを編集できるときtrue、編集できないときfalseを返します。
87 public boolean isCellEditable() { return true; }
91 * @return この列の対象イベントにおける値
93 public Object getValue(MidiEvent event) { return ""; }
96 * @param sti 対象シーケンスモデル
98 * @return この列の対象イベントにおける値
100 public Object getValue(SequenceTrackListTableModel seq, MidiEvent event) {
101 return getValue(event);
108 private SequenceTrackListTableModel sequenceTrackListTableModel;
110 * このトラックモデルを収容している親のシーケンスモデルを返します。
112 public SequenceTrackListTableModel getParent() {
113 return sequenceTrackListTableModel;
118 public ListSelectionModel getSelectionModel() { return selectionModel; }
119 private ListSelectionModel selectionModel = new DefaultListSelectionModel() {
121 setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
125 * シーケンスを親にして、その特定のトラックに連動する
128 * @param parent 親のシーケンスモデル
129 * @param track ラップするMIDIトラック(ない場合はnull)
131 public MidiEventTableModel(SequenceTrackListTableModel sequenceTrackListTableModel, Track track) {
133 this.sequenceTrackListTableModel = sequenceTrackListTableModel;
136 public int getRowCount() { return track == null ? 0 : track.size(); }
138 public int getColumnCount() { return Column.values().length; }
143 public String getColumnName(int column) {
144 return Column.values()[column].title;
150 public Class<?> getColumnClass(int column) {
151 return Column.values()[column].columnClass;
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);
159 case MEASURE_POSITION:
161 case EXTRA_TICK_POSITION:
162 case MESSAGE: return c.getValue(sequenceTrackListTableModel, event);
163 default: return c.getValue(event);
167 * セルを編集できるときtrue、編集できないときfalseを返します。
170 public boolean isCellEditable(int row, int column) {
171 return Column.values()[column].isCellEditable();
177 public void setValueAt(Object value, int row, int column) {
179 switch(Column.values()[column]) {
180 case TICK_POSITION: newTick = (Long)value; break;
181 case MEASURE_POSITION:
182 newTick = sequenceTrackListTableModel.getSequenceTickIndex().measureToTick(
184 (Integer)getValueAt( row, Column.BEAT_POSITION.ordinal() ) - 1,
185 (Integer)getValueAt( row, Column.EXTRA_TICK_POSITION.ordinal() )
189 newTick = sequenceTrackListTableModel.getSequenceTickIndex().measureToTick(
190 (Integer)getValueAt( row, Column.MEASURE_POSITION.ordinal() ) - 1,
192 (Integer)getValueAt( row, Column.EXTRA_TICK_POSITION.ordinal() )
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,
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);
216 public Track getTrack() { return track; }
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);
232 * @return 設定が行われたらtrue
234 public boolean setString(String name) {
235 if( name.equals(toString()) || ! MIDISpec.setNameBytesOf(
236 track, name.getBytes(sequenceTrackListTableModel.getCharset()))
238 sequenceTrackListTableModel.setModified(true);
239 fireTableDataChanged();
242 private String recordingChannel = "OFF";
244 * 録音中のMIDIチャンネルを返します。
245 * @return 録音中のMIDIチャンネル
247 public String getRecordingChannel() { return recordingChannel; }
249 * 録音中のMIDIチャンネルを設定します。
250 * @param recordingChannel 録音中のMIDIチャンネル
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);
257 sequencer.recordEnable(track, Integer.decode(recordingChannel).intValue()-1);
258 } catch( NumberFormatException nfe ) {
259 sequencer.recordDisable(track);
260 this.recordingChannel = "OFF";
263 this.recordingChannel = recordingChannel;
266 * このトラックの対象MIDIチャンネルを返します。
267 * <p>全てのチャンネルメッセージが同じMIDIチャンネルの場合、
269 * MIDIチャンネルの異なるチャンネルメッセージが一つでも含まれていた場合、
272 * @return 対象MIDIチャンネル(不統一の場合 -1)
274 public int getChannel() {
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;
289 * 指定されたMIDIチャンネルをすべてのチャンネルメッセージに対して設定します。
290 * @param channel MIDIチャンネル
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;
301 sm.setMessage(sm.getCommand(), channel, sm.getData1(), sm.getData2());
304 catch( InvalidMidiDataException e ) {
309 sequenceTrackListTableModel.fireTrackChanged(track);
310 fireTableDataChanged();
314 * 指定の MIDI tick 位置にあるイベントを二分探索し、
315 * そのイベントの行インデックスを返します。
316 * @param tick MIDI tick
319 public int tickToIndex(long tick) {
320 if( track == null ) return 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;
330 return (minIndex + maxIndex) / 2;
333 * NoteOn/NoteOff ペアの一方の行インデックスから、
334 * もう一方(ペアの相手)の行インデックスを返します。
335 * @param index 行インデックス
336 * @return ペアを構成する相手の行インデックス(ない場合は -1)
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();
345 int ch = sm.getChannel();
346 int note = sm.getData1();
347 MidiMessage partner_msg;
348 ShortMessage partner_sm;
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
366 if( ch != partner_sm.getChannel() || note != partner_sm.getData1() ) {
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 ) {
386 if( ch != partner_sm.getChannel() || note != partner_sm.getData1() ) {
399 * @param index 行インデックス
400 * @return Note On または Note Off のとき true
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 ;
410 * 指定の行インデックスのMIDIイベントを返します。
411 * @param index 行インデックス
414 public MidiEvent getMidiEvent(int index) {
415 return track==null ? null : track.get(index);
418 * 選択されているMIDIイベントを返します。
419 * @param eventSelectionModel 選択モデル
420 * @return 選択されているMIDIイベント
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));
431 return events.toArray(new MidiEvent[1]);
435 * @param midiEvent 追加するMIDIイベント
438 public boolean addMidiEvent(MidiEvent midiEvent) {
439 if( track == null || !(track.add(midiEvent)) )
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 );
450 * @param midiEvents 追加するMIDIイベント
451 * @param destinationTick 追加先tick
452 * @param sourcePPQ PPQ値(タイミング解像度)
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;
469 newTick += (sourceEventTick - firstSourceEventTick) * destinationPPQ / sourcePPQ;
471 if( ! track.add(new MidiEvent(msg, newTick)) ) continue;
473 if( MIDISpec.isTimeSignature(msg) ) hasTimeSignature = true;
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);
486 * @param midiEvents 除去するMIDIイベント
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;
495 if( hadTimeSignature ) {
496 sequenceTrackListTableModel.fireTimeSignatureChanged();
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);