OSDN Git Service

b745b54bff48c16ca872fe469a25376d3c7c2a97
[midichordhelper/MIDIChordHelper.git] / src / MIDISequencer.java
1 \r
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
8 \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
25 \r
26 /**\r
27  * シーケンサの現在位置(分:秒)を表示するビュー\r
28  */\r
29 class TimeIndicator extends JPanel implements ChangeListener {\r
30         private static abstract class TimeLabel extends JLabel {\r
31                 /**\r
32                  * 時間の値(秒)\r
33                  */\r
34                 private int valueInSec;\r
35                 /**\r
36                  * 時間の値を秒単位で設定します。\r
37                  * @param sec 秒単位の時間\r
38                  */\r
39                 public void setTimeInSecond(int sec) {\r
40                         if(valueInSec != sec) setText(toTimeString(valueInSec = sec));\r
41                 }\r
42                 /**\r
43                  * 時間の値を文字列に変換します。\r
44                  * @param sec 秒単位の時間\r
45                  * @return 変換結果(分:秒)\r
46                  */\r
47                 protected String toTimeString(int sec) {\r
48                         return String.format("%02d:%02d", sec/60, sec%60);\r
49                 }\r
50         }\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
57                 }\r
58         }\r
59         private static class TimeLengthLabel extends TimeLabel {\r
60                 public TimeLengthLabel() {\r
61                         setToolTipText("Time length - 曲の長さ(分:秒)");\r
62                         setText(toTimeString(0));\r
63                 }\r
64                 @Override\r
65                 protected String toTimeString(int sec) {\r
66                         return "/"+super.toTimeString(sec);\r
67                 }\r
68         }\r
69         private TimeLabel timePositionLabel = new TimePositionLabel();\r
70         private TimeLabel timeLengthLabel = new TimeLengthLabel();\r
71         private MidiSequencerModel model;\r
72         /**\r
73          * シーケンサの現在位置(分:秒)を表示するビューを構築します。\r
74          * @param model MIDIシーケンサモデル\r
75          */\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
81         }\r
82         @Override\r
83         public void stateChanged(ChangeEvent e) {\r
84                 timeLengthLabel.setTimeInSecond(model.getMaximum()/1000);\r
85                 timePositionLabel.setTimeInSecond(model.getValue()/1000);\r
86         }\r
87 }\r
88 \r
89 /**\r
90  * 小節表示ビュー\r
91  */\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
98                         return true;\r
99                 }\r
100         }\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
108                 }\r
109                 public boolean setMeasure(int measure, int beat) {\r
110                         if( ! super.setMeasure(measure) && this.beat == beat )\r
111                                 return false;\r
112                         setText(String.format("%04d:%02d", measure+1, beat+1));\r
113                         return true;\r
114                 }\r
115         }\r
116         private static class MeasureLengthLabel extends MeasureLabel {\r
117                 public MeasureLengthLabel() {\r
118                         setText("/0000");\r
119                         setToolTipText("Measure length - 小節の数");\r
120                 }\r
121                 public boolean setMeasure(int measure) {\r
122                         if( ! super.setMeasure(measure) )\r
123                                 return false;\r
124                         setText(String.format("/%04d", measure));\r
125                         return true;\r
126                 }\r
127         }\r
128         private MeasurePositionLabel measurePositionLabel;\r
129         private MeasureLengthLabel measureLengthLabel;\r
130         private MidiSequencerModel model;\r
131         /**\r
132          * シーケンサの現在の小節位置を表示するビューを構築します。\r
133          * @param model スライダー用の時間範囲データモデル\r
134          */\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
140         }\r
141         @Override\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
147                 );\r
148                 if( ! sequencer.isRunning() || sequencer.isRecording() ) {\r
149                         // 停止中または録音中の場合、長さが変わることがあるので表示を更新\r
150                         if( tickIndex == null ) {\r
151                                 measureLengthLabel.setMeasure(0);\r
152                         }\r
153                         else {\r
154                                 long tickLength = sequencer.getTickLength();\r
155                                 int measureLength = tickIndex.tickToMeasure(tickLength);\r
156                                 measureLengthLabel.setMeasure(measureLength);\r
157                         }\r
158                 }\r
159                 // 小節位置の表示を更新\r
160                 if( tickIndex == null ) {\r
161                         measurePositionLabel.setMeasure(0, 0);\r
162                 }\r
163                 else {\r
164                         long tickPosition = sequencer.getTickPosition();\r
165                         int measurePosition = tickIndex.tickToMeasure(tickPosition);\r
166                         measurePositionLabel.setMeasure(measurePosition, tickIndex.lastBeat);\r
167                 }\r
168         }\r
169 }\r
170 \r
171 /**\r
172  * MIDIシーケンサモデル\r
173  */\r
174 class MidiSequencerModel extends MidiConnecterListModel implements BoundedRangeModel {\r
175         /**\r
176          * MIDIシーケンサモデルを構築します。\r
177          * @param deviceModelList 親のMIDIデバイスモデルリスト\r
178          * @param sequencer シーケンサーMIDIデバイス\r
179          * @param modelList MIDIコネクタリストモデルのリスト\r
180          */\r
181         public MidiSequencerModel(\r
182                 MidiDeviceModelList deviceModelList,\r
183                 Sequencer sequencer,\r
184                 List<MidiConnecterListModel> modelList\r
185         ) {\r
186                 super(sequencer, modelList);\r
187                 this.deviceModelList = deviceModelList;\r
188                 sequencer.addMetaEventListener(new MetaEventListener() {\r
189                         /**\r
190                          * {@inheritDoc}\r
191                          *\r
192                          * この実装では EOT (End Of Track、type==0x2F) を受信したときに、\r
193                          * 曲の先頭に戻し、次の曲があればその曲を再生し、\r
194                          * なければ秒位置更新タイマーを停止します。\r
195                          */\r
196                         @Override\r
197                         public void meta(MetaMessage msg) {\r
198                                 if( msg.getType() == 0x2F ) {\r
199                                         getSequencer().setMicrosecondPosition(0);\r
200                                         // リピートモードの場合、同じ曲をもう一度再生する。\r
201                                         // そうでない場合、次の曲へ進んで再生する。\r
202                                         // 次の曲がなければ、そこで終了。\r
203                                         boolean isRepeatMode = (Boolean)toggleRepeatAction.getValue(Action.SELECTED_KEY);\r
204                                         if( isRepeatMode || MidiSequencerModel.this.deviceModelList.editorDialog.sequenceListTableModel.loadNext(1) ) {\r
205                                                 start();\r
206                                         }\r
207                                         else {\r
208                                                 stop();\r
209                                         }\r
210                                 }\r
211                         }\r
212                 });\r
213         }\r
214         /**\r
215          * MIDIデバイスモデルリスト\r
216          */\r
217         private MidiDeviceModelList deviceModelList;\r
218         /**\r
219          * このシーケンサーの再生スピード調整モデル\r
220          */\r
221         BoundedRangeModel speedSliderModel = new DefaultBoundedRangeModel(0, 0, -7, 7) {{\r
222                 addChangeListener(\r
223                         new ChangeListener() {\r
224                                 @Override\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
229                                         ));\r
230                                 }\r
231                         }\r
232                 );\r
233         }};\r
234         /**\r
235          * MIDIシーケンサを返します。\r
236          * @return MIDIシーケンサ\r
237          */\r
238         public Sequencer getSequencer() { return (Sequencer)device; }\r
239         /**\r
240          * 開始終了アクション\r
241          */\r
242         StartStopAction startStopAction = new StartStopAction();\r
243         /**\r
244          * 開始終了アクション\r
245          */\r
246         class StartStopAction extends AbstractAction {\r
247                 private Map<Boolean,Icon> iconMap = new HashMap<Boolean,Icon>() {\r
248                         {\r
249                                 put(Boolean.FALSE, new ButtonIcon(ButtonIcon.PLAY_ICON));\r
250                                 put(Boolean.TRUE, new ButtonIcon(ButtonIcon.PAUSE_ICON));\r
251                         }\r
252                 };\r
253                 {\r
254                         putValue(\r
255                                 SHORT_DESCRIPTION,\r
256                                 "Start/Stop recording or playing - 録音または再生の開始/停止"\r
257                         );\r
258                         setRunning(false);\r
259                 }\r
260                 @Override\r
261                 public void actionPerformed(ActionEvent event) {\r
262                         if(timeRangeUpdater.isRunning()) stop(); else start();\r
263                 }\r
264                 /**\r
265                  * 開始されているかどうかを設定します。\r
266                  * @param isRunning 開始されていたらtrue\r
267                  */\r
268                 public void setRunning(boolean isRunning) {\r
269                         putValue(LARGE_ICON_KEY, iconMap.get(isRunning));\r
270                         putValue(SELECTED_KEY, isRunning);\r
271                 }\r
272         }\r
273         /**\r
274          * シーケンサに合わせてミリ秒位置を更新するタイマー\r
275          */\r
276         private javax.swing.Timer timeRangeUpdater = new javax.swing.Timer(\r
277                 20,\r
278                 new ActionListener(){\r
279                         @Override\r
280                         public void actionPerformed(ActionEvent e) {\r
281                                 if( valueIsAdjusting || ! getSequencer().isRunning() ) {\r
282                                         // 手動で移動中の場合や、シーケンサが止まっている場合は、\r
283                                         // タイマーによる更新は不要\r
284                                         return;\r
285                                 }\r
286                                 // リスナーに読み込みを促す\r
287                                 fireStateChanged();\r
288                         }\r
289                 }\r
290         );\r
291         /**\r
292          * このモデルのMIDIシーケンサを開始します。\r
293          */\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
299                         return;\r
300                 }\r
301                 startStopAction.setRunning(true);\r
302                 timeRangeUpdater.start();\r
303                 deviceModelList.startSequencerWithResetTimestamps();\r
304                 fireStateChanged();\r
305         }\r
306         /**\r
307          * このモデルのMIDIシーケンサを停止します。\r
308          */\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
315         }\r
316         /**\r
317          * {@link Sequencer#getMicrosecondLength()} と同じです。\r
318          * @return マイクロ秒単位でのシーケンスの長さ\r
319          */\r
320         public long getMicrosecondLength() {\r
321                 //\r
322                 // Sequencer.getMicrosecondLength() returns NEGATIVE value\r
323                 //  when over 0x7FFFFFFF microseconds (== 35.7913941166666... minutes),\r
324                 //  should be corrected when negative\r
325                 //\r
326                 long usLength = getSequencer().getMicrosecondLength();\r
327                 return usLength < 0 ? 0x100000000L + usLength : usLength ;\r
328         }\r
329         @Override\r
330         public int getMaximum() { return (int)(getMicrosecondLength()/1000L); }\r
331         @Override\r
332         public void setMaximum(int newMaximum) {}\r
333         @Override\r
334         public int getMinimum() { return 0; }\r
335         @Override\r
336         public void setMinimum(int newMinimum) {}\r
337         @Override\r
338         public int getExtent() { return 0; }\r
339         @Override\r
340         public void setExtent(int newExtent) {}\r
341         /**\r
342          * {@link Sequencer#getMicrosecondPosition()} と同じです。\r
343          * @return マイクロ秒単位での現在の位置\r
344          */\r
345         public long getMicrosecondPosition() {\r
346                 long usPosition = getSequencer().getMicrosecondPosition();\r
347                 return usPosition < 0 ? 0x100000000L + usPosition : usPosition ;\r
348         }\r
349         @Override\r
350         public int getValue() { return (int)(getMicrosecondPosition()/1000L); }\r
351         @Override\r
352         public void setValue(int newValue) {\r
353                 getSequencer().setMicrosecondPosition(1000L * (long)newValue);\r
354                 fireStateChanged();\r
355         }\r
356         /**\r
357          * 値調整中のときtrue\r
358          */\r
359         private boolean valueIsAdjusting = false;\r
360         @Override\r
361         public boolean getValueIsAdjusting() {\r
362                 return valueIsAdjusting;\r
363         }\r
364         @Override\r
365         public void setValueIsAdjusting(boolean valueIsAdjusting) {\r
366                 this.valueIsAdjusting = valueIsAdjusting;\r
367         }\r
368         @Override\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
373         }\r
374         /**\r
375          * イベントリスナーのリスト\r
376          */\r
377         protected EventListenerList listenerList = new EventListenerList();\r
378         @Override\r
379         public void addChangeListener(ChangeListener listener) {\r
380                 listenerList.add(ChangeListener.class, listener);\r
381         }\r
382         @Override\r
383         public void removeChangeListener(ChangeListener listener) {\r
384                 listenerList.remove(ChangeListener.class, listener);\r
385         }\r
386         /**\r
387          * 秒位置が変わったことをリスナーに通知します。\r
388          * <p>登録中のすべての {@link ChangeListener} について\r
389          * {@link ChangeListener#stateChanged(ChangeEvent)}\r
390          * を呼び出すことによって状態の変化を通知します。\r
391          * </p>\r
392          */\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
398                         }\r
399                 }\r
400         }\r
401         /**\r
402          * 繰り返し再生ON/OFF切り替えアクション\r
403          */\r
404         public Action toggleRepeatAction = new AbstractAction() {\r
405                 {\r
406                         putValue(SHORT_DESCRIPTION, "Repeat - 繰り返し再生");\r
407                         putValue(LARGE_ICON_KEY, new ButtonIcon(ButtonIcon.REPEAT_ICON));\r
408                         putValue(SELECTED_KEY, false);\r
409                 }\r
410                 @Override\r
411                 public void actionPerformed(ActionEvent event) {\r
412                         // 特にやることなし\r
413                 }\r
414         };\r
415         /**\r
416          * MIDIトラックリストテーブルモデル\r
417          */\r
418         private SequenceTrackListTableModel sequenceTableModel = null;\r
419         /**\r
420          * このシーケンサーに現在ロードされているシーケンスのMIDIトラックリストテーブルモデルを返します。\r
421          * @return MIDIトラックリストテーブルモデル\r
422          */\r
423         public SequenceTrackListTableModel getSequenceTableModel() {\r
424                 return sequenceTableModel;\r
425         }\r
426         /**\r
427          * MIDIトラックリストテーブルモデルを\r
428          * このシーケンサーモデルにセットします。\r
429          * @param sequenceTableModel MIDIトラックリストテーブルモデル\r
430          * @return 成功したらtrue\r
431          */\r
432         public boolean setSequenceTableModel(SequenceTrackListTableModel sequenceTableModel) {\r
433                 //\r
434                 // javax.sound.midi:Sequencer.setSequence() のドキュメントにある\r
435                 // 「このメソッドは、Sequencer が閉じている場合でも呼び出すことができます。 」\r
436                 // という記述は、null をセットする場合には当てはまらない。\r
437                 // 連鎖的に stop() が呼ばれるために IllegalStateException sequencer not open が出る。\r
438                 // この現象を回避するため、あらかじめチェックしてから setSequence() を呼び出している。\r
439                 //\r
440                 if( sequenceTableModel != null || getSequencer().isOpen() ) {\r
441                         Sequence seq = null;\r
442                         if( sequenceTableModel != null ) {\r
443                                 seq = sequenceTableModel.getSequence();\r
444                         }\r
445                         try {\r
446                                 getSequencer().setSequence(seq);\r
447                         } catch ( InvalidMidiDataException e ) {\r
448                                 e.printStackTrace();\r
449                                 return false;\r
450                         }\r
451                 }\r
452                 this.sequenceTableModel = sequenceTableModel;\r
453                 fireStateChanged();\r
454                 return true;\r
455         }\r
456 \r
457         /**\r
458          * 小節単位で位置を移動します。\r
459          * @param measureOffset 何小節進めるか(戻したいときは負数を指定)\r
460          */\r
461         private void moveMeasure(int measureOffset) {\r
462                 if( measureOffset == 0 || sequenceTableModel == null )\r
463                         return;\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
469                         // 下限\r
470                         newTickPosition = 0;\r
471                 }\r
472                 else {\r
473                         long tickLength = sequencer.getTickLength();\r
474                         if( newTickPosition > tickLength ) {\r
475                                 // 上限\r
476                                 newTickPosition = tickLength - 1;\r
477                         }\r
478                 }\r
479                 sequencer.setTickPosition(newTickPosition);\r
480                 fireStateChanged();\r
481         }\r
482         /**\r
483          * 1小節戻るアクション\r
484          */\r
485         public Action moveBackwardAction = new AbstractAction() {\r
486                 {\r
487                         putValue(SHORT_DESCRIPTION, "Move backward 1 measure - 1小節戻る");\r
488                         putValue(LARGE_ICON_KEY, new ButtonIcon(ButtonIcon.BACKWARD_ICON));\r
489                 }\r
490                 @Override\r
491                 public void actionPerformed(ActionEvent event) { moveMeasure(-1); }\r
492         };\r
493         /**\r
494          *1小節進むアクション\r
495          */\r
496         public Action moveForwardAction = new AbstractAction() {\r
497                 {\r
498                         putValue(SHORT_DESCRIPTION, "Move forward 1 measure - 1小節進む");\r
499                         putValue(LARGE_ICON_KEY, new ButtonIcon(ButtonIcon.FORWARD_ICON));\r
500                 }\r
501                 @Override\r
502                 public void actionPerformed(ActionEvent event) { moveMeasure(1); }\r
503         };\r
504 }\r
505 \r