2 import java.awt.Color;
\r
3 import java.awt.event.ActionEvent;
\r
4 import java.awt.event.ActionListener;
\r
5 import java.util.HashMap;
\r
6 import java.util.List;
\r
7 import java.util.Map;
\r
9 import javax.sound.midi.InvalidMidiDataException;
\r
10 import javax.sound.midi.MetaEventListener;
\r
11 import javax.sound.midi.MetaMessage;
\r
12 import javax.sound.midi.Sequence;
\r
13 import javax.sound.midi.Sequencer;
\r
14 import javax.swing.AbstractAction;
\r
15 import javax.swing.Action;
\r
16 import javax.swing.BoundedRangeModel;
\r
17 import javax.swing.BoxLayout;
\r
18 import javax.swing.DefaultBoundedRangeModel;
\r
19 import javax.swing.Icon;
\r
20 import javax.swing.JLabel;
\r
21 import javax.swing.JPanel;
\r
22 import javax.swing.event.ChangeEvent;
\r
23 import javax.swing.event.ChangeListener;
\r
24 import javax.swing.event.EventListenerList;
\r
27 * シーケンサの現在位置(分:秒)を表示するビュー
\r
29 class TimeIndicator extends JPanel implements ChangeListener {
\r
30 private static abstract class TimeLabel extends JLabel {
\r
34 private int valueInSec;
\r
39 public void setTimeInSecond(int sec) {
\r
40 if(valueInSec != sec) setText(toTimeString(valueInSec = sec));
\r
47 protected String toTimeString(int sec) {
\r
48 return String.format("%02d:%02d", sec/60, sec%60);
\r
51 private static class TimePositionLabel extends TimeLabel {
\r
52 public TimePositionLabel() {
\r
53 setFont( getFont().deriveFont(getFont().getSize2D() + 4) );
\r
54 setForeground( new Color(0x80,0x00,0x00) );
\r
55 setToolTipText("Time position - 現在位置(分:秒)");
\r
56 setText(toTimeString(0));
\r
59 private static class TimeLengthLabel extends TimeLabel {
\r
60 public TimeLengthLabel() {
\r
61 setToolTipText("Time length - 曲の長さ(分:秒)");
\r
62 setText(toTimeString(0));
\r
65 protected String toTimeString(int sec) {
\r
66 return "/"+super.toTimeString(sec);
\r
69 private TimeLabel timePositionLabel = new TimePositionLabel();
\r
70 private TimeLabel timeLengthLabel = new TimeLengthLabel();
\r
71 private MidiSequencerModel model;
\r
73 * シーケンサの現在位置(分:秒)を表示するビューを構築します。
\r
74 * @param model MIDIシーケンサモデル
\r
76 public TimeIndicator(MidiSequencerModel model) {
\r
77 setLayout(new BoxLayout(this, BoxLayout.X_AXIS));
\r
78 add(timePositionLabel);
\r
79 add(timeLengthLabel);
\r
80 (this.model = model).addChangeListener(this);
\r
83 public void stateChanged(ChangeEvent e) {
\r
84 timeLengthLabel.setTimeInSecond(model.getMaximum()/1000);
\r
85 timePositionLabel.setTimeInSecond(model.getValue()/1000);
\r
92 class MeasureIndicator extends JPanel implements ChangeListener {
\r
93 private static abstract class MeasureLabel extends JLabel {
\r
94 protected int measure = -1;
\r
95 public boolean setMeasure(int measure) {
\r
96 if( this.measure == measure ) return false;
\r
97 this.measure = measure;
\r
101 private static class MeasurePositionLabel extends MeasureLabel {
\r
102 protected int beat = 0;
\r
103 public MeasurePositionLabel() {
\r
104 setFont( getFont().deriveFont(getFont().getSize2D() + 4) );
\r
105 setForeground( new Color(0x80,0x00,0x00) );
\r
106 setText("0001:01");
\r
107 setToolTipText("Measure:beat position - 何小節目:何拍目");
\r
109 public boolean setMeasure(int measure, int beat) {
\r
110 if( ! super.setMeasure(measure) && this.beat == beat )
\r
112 setText(String.format("%04d:%02d", measure+1, beat+1));
\r
116 private static class MeasureLengthLabel extends MeasureLabel {
\r
117 public MeasureLengthLabel() {
\r
119 setToolTipText("Measure length - 小節の数");
\r
121 public boolean setMeasure(int measure) {
\r
122 if( ! super.setMeasure(measure) )
\r
124 setText(String.format("/%04d", measure));
\r
128 private MeasurePositionLabel measurePositionLabel;
\r
129 private MeasureLengthLabel measureLengthLabel;
\r
130 private MidiSequencerModel model;
\r
132 * シーケンサの現在の小節位置を表示するビューを構築します。
\r
133 * @param model スライダー用の時間範囲データモデル
\r
135 public MeasureIndicator(MidiSequencerModel model) {
\r
136 setLayout(new BoxLayout(this, BoxLayout.X_AXIS));
\r
137 add(measurePositionLabel = new MeasurePositionLabel());
\r
138 add(measureLengthLabel = new MeasureLengthLabel());
\r
139 (this.model = model).addChangeListener(this);
\r
142 public void stateChanged(ChangeEvent e) {
\r
143 Sequencer sequencer = model.getSequencer();
\r
144 SequenceTrackListTableModel sequenceTableModel = model.getSequenceTableModel();
\r
145 SequenceTickIndex tickIndex = (
\r
146 sequenceTableModel == null ? null : sequenceTableModel.getSequenceTickIndex()
\r
148 if( ! sequencer.isRunning() || sequencer.isRecording() ) {
\r
149 // 停止中または録音中の場合、長さが変わることがあるので表示を更新
\r
150 if( tickIndex == null ) {
\r
151 measureLengthLabel.setMeasure(0);
\r
154 long tickLength = sequencer.getTickLength();
\r
155 int measureLength = tickIndex.tickToMeasure(tickLength);
\r
156 measureLengthLabel.setMeasure(measureLength);
\r
160 if( tickIndex == null ) {
\r
161 measurePositionLabel.setMeasure(0, 0);
\r
164 long tickPosition = sequencer.getTickPosition();
\r
165 int measurePosition = tickIndex.tickToMeasure(tickPosition);
\r
166 measurePositionLabel.setMeasure(measurePosition, tickIndex.lastBeat);
\r
174 class MidiSequencerModel extends MidiConnecterListModel implements BoundedRangeModel {
\r
176 * MIDIシーケンサモデルを構築します。
\r
177 * @param deviceModelList 親のMIDIデバイスモデルリスト
\r
178 * @param sequencer シーケンサーMIDIデバイス
\r
179 * @param modelList MIDIコネクタリストモデルのリスト
\r
181 public MidiSequencerModel(
\r
182 MidiDeviceModelList deviceModelList,
\r
183 Sequencer sequencer,
\r
184 List<MidiConnecterListModel> modelList
\r
186 super(sequencer, modelList);
\r
187 this.deviceModelList = deviceModelList;
\r
188 sequencer.addMetaEventListener(new MetaEventListener() {
\r
192 * この実装では EOT (End Of Track、type==0x2F) を受信したときに、
\r
193 * 曲の先頭に戻し、次の曲があればその曲を再生し、
\r
194 * なければ秒位置更新タイマーを停止します。
\r
197 public void meta(MetaMessage msg) {
\r
198 if( msg.getType() == 0x2F ) {
\r
199 getSequencer().setMicrosecondPosition(0);
\r
200 // リピートモードの場合、同じ曲をもう一度再生する。
\r
201 // そうでない場合、次の曲へ進んで再生する。
\r
203 boolean isRepeatMode = (Boolean)toggleRepeatAction.getValue(Action.SELECTED_KEY);
\r
204 if( isRepeatMode || MidiSequencerModel.this.deviceModelList.editorDialog.sequenceListTableModel.loadNext(1) ) {
\r
217 private MidiDeviceModelList deviceModelList;
\r
219 * このシーケンサーの再生スピード調整モデル
\r
221 BoundedRangeModel speedSliderModel = new DefaultBoundedRangeModel(0, 0, -7, 7) {{
\r
223 new ChangeListener() {
\r
225 public void stateChanged(ChangeEvent e) {
\r
226 int val = getValue();
\r
227 getSequencer().setTempoFactor((float)(
\r
228 val == 0 ? 1.0 : Math.pow( 2.0, ((double)val)/12.0 )
\r
236 * @return MIDIシーケンサ
\r
238 public Sequencer getSequencer() { return (Sequencer)device; }
\r
242 StartStopAction startStopAction = new StartStopAction();
\r
246 class StartStopAction extends AbstractAction {
\r
247 private Map<Boolean,Icon> iconMap = new HashMap<Boolean,Icon>() {
\r
249 put(Boolean.FALSE, new ButtonIcon(ButtonIcon.PLAY_ICON));
\r
250 put(Boolean.TRUE, new ButtonIcon(ButtonIcon.PAUSE_ICON));
\r
256 "Start/Stop recording or playing - 録音または再生の開始/停止"
\r
261 public void actionPerformed(ActionEvent event) {
\r
262 if(timeRangeUpdater.isRunning()) stop(); else start();
\r
265 * 開始されているかどうかを設定します。
\r
266 * @param isRunning 開始されていたらtrue
\r
268 public void setRunning(boolean isRunning) {
\r
269 putValue(LARGE_ICON_KEY, iconMap.get(isRunning));
\r
270 putValue(SELECTED_KEY, isRunning);
\r
274 * シーケンサに合わせてミリ秒位置を更新するタイマー
\r
276 private javax.swing.Timer timeRangeUpdater = new javax.swing.Timer(
\r
278 new ActionListener(){
\r
280 public void actionPerformed(ActionEvent e) {
\r
281 if( valueIsAdjusting || ! getSequencer().isRunning() ) {
\r
282 // 手動で移動中の場合や、シーケンサが止まっている場合は、
\r
287 fireStateChanged();
\r
292 * このモデルのMIDIシーケンサを開始します。
\r
294 public void start() {
\r
295 Sequencer sequencer = getSequencer();
\r
296 if( ! sequencer.isOpen() || sequencer.getSequence() == null ) {
\r
297 timeRangeUpdater.stop();
\r
298 startStopAction.setRunning(false);
\r
301 startStopAction.setRunning(true);
\r
302 timeRangeUpdater.start();
\r
303 deviceModelList.startSequencerWithResetTimestamps();
\r
304 fireStateChanged();
\r
307 * このモデルのMIDIシーケンサを停止します。
\r
309 public void stop() {
\r
310 Sequencer sequencer = getSequencer();
\r
311 if(sequencer.isOpen()) sequencer.stop();
\r
312 timeRangeUpdater.stop();
\r
313 startStopAction.setRunning(false);
\r
314 fireStateChanged();
\r
317 * {@link Sequencer#getMicrosecondLength()} と同じです。
\r
318 * @return マイクロ秒単位でのシーケンスの長さ
\r
320 public long getMicrosecondLength() {
\r
322 // Sequencer.getMicrosecondLength() returns NEGATIVE value
\r
323 // when over 0x7FFFFFFF microseconds (== 35.7913941166666... minutes),
\r
324 // should be corrected when negative
\r
326 long usLength = getSequencer().getMicrosecondLength();
\r
327 return usLength < 0 ? 0x100000000L + usLength : usLength ;
\r
330 public int getMaximum() { return (int)(getMicrosecondLength()/1000L); }
\r
332 public void setMaximum(int newMaximum) {}
\r
334 public int getMinimum() { return 0; }
\r
336 public void setMinimum(int newMinimum) {}
\r
338 public int getExtent() { return 0; }
\r
340 public void setExtent(int newExtent) {}
\r
342 * {@link Sequencer#getMicrosecondPosition()} と同じです。
\r
343 * @return マイクロ秒単位での現在の位置
\r
345 public long getMicrosecondPosition() {
\r
346 long usPosition = getSequencer().getMicrosecondPosition();
\r
347 return usPosition < 0 ? 0x100000000L + usPosition : usPosition ;
\r
350 public int getValue() { return (int)(getMicrosecondPosition()/1000L); }
\r
352 public void setValue(int newValue) {
\r
353 getSequencer().setMicrosecondPosition(1000L * (long)newValue);
\r
354 fireStateChanged();
\r
359 private boolean valueIsAdjusting = false;
\r
361 public boolean getValueIsAdjusting() {
\r
362 return valueIsAdjusting;
\r
365 public void setValueIsAdjusting(boolean valueIsAdjusting) {
\r
366 this.valueIsAdjusting = valueIsAdjusting;
\r
369 public void setRangeProperties(int value, int extent, int min, int max, boolean valueIsAdjusting) {
\r
370 getSequencer().setMicrosecondPosition(1000L * (long)value);
\r
371 setValueIsAdjusting(valueIsAdjusting);
\r
372 fireStateChanged();
\r
377 protected EventListenerList listenerList = new EventListenerList();
\r
379 public void addChangeListener(ChangeListener listener) {
\r
380 listenerList.add(ChangeListener.class, listener);
\r
383 public void removeChangeListener(ChangeListener listener) {
\r
384 listenerList.remove(ChangeListener.class, listener);
\r
387 * 秒位置が変わったことをリスナーに通知します。
\r
388 * <p>登録中のすべての {@link ChangeListener} について
\r
389 * {@link ChangeListener#stateChanged(ChangeEvent)}
\r
390 * を呼び出すことによって状態の変化を通知します。
\r
393 public void fireStateChanged() {
\r
394 Object[] listeners = listenerList.getListenerList();
\r
395 for (int i = listeners.length-2; i>=0; i-=2) {
\r
396 if (listeners[i]==ChangeListener.class) {
\r
397 ((ChangeListener)listeners[i+1]).stateChanged(new ChangeEvent(this));
\r
402 * 繰り返し再生ON/OFF切り替えアクション
\r
404 public Action toggleRepeatAction = new AbstractAction() {
\r
406 putValue(SHORT_DESCRIPTION, "Repeat - 繰り返し再生");
\r
407 putValue(LARGE_ICON_KEY, new ButtonIcon(ButtonIcon.REPEAT_ICON));
\r
408 putValue(SELECTED_KEY, false);
\r
411 public void actionPerformed(ActionEvent event) {
\r
416 * MIDIトラックリストテーブルモデル
\r
418 private SequenceTrackListTableModel sequenceTableModel = null;
\r
420 * このシーケンサーに現在ロードされているシーケンスのMIDIトラックリストテーブルモデルを返します。
\r
421 * @return MIDIトラックリストテーブルモデル
\r
423 public SequenceTrackListTableModel getSequenceTableModel() {
\r
424 return sequenceTableModel;
\r
427 * MIDIトラックリストテーブルモデルを
\r
428 * このシーケンサーモデルにセットします。
\r
429 * @param sequenceTableModel MIDIトラックリストテーブルモデル
\r
430 * @return 成功したらtrue
\r
432 public boolean setSequenceTableModel(SequenceTrackListTableModel sequenceTableModel) {
\r
434 // javax.sound.midi:Sequencer.setSequence() のドキュメントにある
\r
435 // 「このメソッドは、Sequencer が閉じている場合でも呼び出すことができます。 」
\r
436 // という記述は、null をセットする場合には当てはまらない。
\r
437 // 連鎖的に stop() が呼ばれるために IllegalStateException sequencer not open が出る。
\r
438 // この現象を回避するため、あらかじめチェックしてから setSequence() を呼び出している。
\r
440 if( sequenceTableModel != null || getSequencer().isOpen() ) {
\r
441 Sequence seq = null;
\r
442 if( sequenceTableModel != null ) {
\r
443 seq = sequenceTableModel.getSequence();
\r
446 getSequencer().setSequence(seq);
\r
447 } catch ( InvalidMidiDataException e ) {
\r
448 e.printStackTrace();
\r
452 this.sequenceTableModel = sequenceTableModel;
\r
453 fireStateChanged();
\r
459 * @param measureOffset 何小節進めるか(戻したいときは負数を指定)
\r
461 private void moveMeasure(int measureOffset) {
\r
462 if( measureOffset == 0 || sequenceTableModel == null )
\r
464 SequenceTickIndex seqIndex = sequenceTableModel.getSequenceTickIndex();
\r
465 Sequencer sequencer = getSequencer();
\r
466 int measurePosition = seqIndex.tickToMeasure(sequencer.getTickPosition());
\r
467 long newTickPosition = seqIndex.measureToTick(measurePosition + measureOffset);
\r
468 if( newTickPosition < 0 ) {
\r
470 newTickPosition = 0;
\r
473 long tickLength = sequencer.getTickLength();
\r
474 if( newTickPosition > tickLength ) {
\r
476 newTickPosition = tickLength - 1;
\r
479 sequencer.setTickPosition(newTickPosition);
\r
480 fireStateChanged();
\r
485 public Action moveBackwardAction = new AbstractAction() {
\r
487 putValue(SHORT_DESCRIPTION, "Move backward 1 measure - 1小節戻る");
\r
488 putValue(LARGE_ICON_KEY, new ButtonIcon(ButtonIcon.BACKWARD_ICON));
\r
491 public void actionPerformed(ActionEvent event) { moveMeasure(-1); }
\r
496 public Action moveForwardAction = new AbstractAction() {
\r
498 putValue(SHORT_DESCRIPTION, "Move forward 1 measure - 1小節進む");
\r
499 putValue(LARGE_ICON_KEY, new ButtonIcon(ButtonIcon.FORWARD_ICON));
\r
502 public void actionPerformed(ActionEvent event) { moveMeasure(1); }
\r