OSDN Git Service

・シーケンサを閉じてしまった場合の挙動を改善
[midichordhelper/MIDIChordHelper.git] / src / camidion / chordhelper / mididevice / MidiSequencerModel.java
1 package camidion.chordhelper.mididevice;
2
3 import java.awt.event.ActionEvent;
4 import java.util.HashMap;
5 import java.util.Map;
6
7 import javax.sound.midi.InvalidMidiDataException;
8 import javax.sound.midi.MidiUnavailableException;
9 import javax.sound.midi.Sequence;
10 import javax.sound.midi.Sequencer;
11 import javax.swing.AbstractAction;
12 import javax.swing.Action;
13 import javax.swing.BoundedRangeModel;
14 import javax.swing.ComboBoxModel;
15 import javax.swing.DefaultBoundedRangeModel;
16 import javax.swing.DefaultComboBoxModel;
17 import javax.swing.Icon;
18 import javax.swing.event.ChangeEvent;
19 import javax.swing.event.ChangeListener;
20 import javax.swing.event.EventListenerList;
21 import javax.swing.event.ListDataEvent;
22 import javax.swing.event.ListDataListener;
23
24 import camidion.chordhelper.ButtonIcon;
25 import camidion.chordhelper.midieditor.SequenceTickIndex;
26 import camidion.chordhelper.midieditor.SequenceTrackListTableModel;
27 import camidion.chordhelper.midieditor.SequencerSpeedSlider;
28
29 /**
30  * MIDIシーケンサモデル(再生位置モデルのインターフェース付き)
31  */
32 public class MidiSequencerModel extends MidiDeviceModel implements BoundedRangeModel {
33         /**
34          * 再生位置スライダーモデルの最小単位 [μ秒]
35          */
36         public static final long RESOLUTION_MICROSECOND = 1000L;
37         /**
38          * MIDIシーケンサモデルを構築します。
39          * @param sequencer シーケンサーMIDIデバイス
40          * @param deviceModelTree 親のMIDIデバイスツリーモデル
41          */
42         public MidiSequencerModel(Sequencer sequencer, MidiDeviceTreeModel deviceModelTree) {
43                 super(sequencer, deviceModelTree);
44         }
45         /**
46          * このシーケンサーの再生スピード
47          */
48         public BoundedRangeModel speedSliderModel = new DefaultBoundedRangeModel(0, 0, -7, 7) {{
49                 addChangeListener(e->getSequencer().setTempoFactor(SequencerSpeedSlider.tempoFactorOf(getValue())));
50         }};
51         /**
52          * 対象MIDIシーケンサデバイス({@link #getMidiDevice()}をキャストした結果)を返します。
53          * @return 対象MIDIシーケンサデバイス
54          */
55         public Sequencer getSequencer() { return (Sequencer)device; }
56         /**
57          * 開始終了アクション
58          */
59         public Action getStartStopAction() { return startStopAction; }
60         private StartStopAction startStopAction = new StartStopAction();
61         private class StartStopAction extends AbstractAction {
62                 private Map<Boolean,Icon> iconMap = new HashMap<Boolean,Icon>() {
63                         {
64                                 put(Boolean.FALSE, new ButtonIcon(ButtonIcon.PLAY_ICON));
65                                 put(Boolean.TRUE, new ButtonIcon(ButtonIcon.PAUSE_ICON));
66                         }
67                 };
68                 {
69                         putValue(SHORT_DESCRIPTION, "Start/Stop recording or playing - 録音または再生の開始/停止");
70                         setRunning(false);
71                         updateEnableStatus();
72                 }
73                 @Override
74                 public void actionPerformed(ActionEvent event) {
75                         if( timeRangeUpdater.isRunning() ) stop(); else start();
76                         updateEnableStatus();
77                 }
78                 private void setRunning(boolean isRunning) {
79                         putValue(LARGE_ICON_KEY, iconMap.get(isRunning));
80                         putValue(SELECTED_KEY, isRunning);
81                 }
82                 /**
83                  * 再生または録音が可能かチェックし、操作可能状態を更新します。
84                  */
85                 public void updateEnableStatus() { setEnabled(isStartable()); }
86         }
87         /**
88          * 再生または録音が可能か調べます。
89          * <p>以下の条件が揃ったときに再生または録音が可能と判定されます。</p>
90          * <ul>
91          * <li>MIDIシーケンサデバイスが開いている</li>
92          * <li>MIDIシーケンサに操作対象のMIDIシーケンスが設定されている</li>
93          * </ul>
94          * @return 再生または録音が可能な場合true
95          */
96         public boolean isStartable() {
97                 return device.isOpen() && getSequencer().getSequence() != null;
98         }
99         /**
100          * {@inheritDoc}
101          * <p>シーケンサモデルの場合、録音再生可能状態が変わるので、開始終了アクションにも通知します。</p>
102          */
103         @Override
104         public void open() throws MidiUnavailableException {
105                 super.open();
106                 startStopAction.updateEnableStatus();
107                 fireStateChanged();
108                 if( sequenceTrackListTableModel != null ) {
109                         sequenceTrackListTableModel.getParent().fireTableDataChanged();
110                 }
111         }
112         /**
113          * {@inheritDoc}
114          * <p>シーケンサモデルの場合、再生または録音が不可能になるので、開始終了アクションにも通知します。</p>
115          */
116         @Override
117         public void close() {
118                 stop();
119                 try {
120                         setSequenceTrackListTableModel(null);
121                 } catch (InvalidMidiDataException|IllegalStateException e) {
122                         e.printStackTrace();
123                 }
124                 super.close();
125                 startStopAction.updateEnableStatus();
126                 fireStateChanged();
127                 if( sequenceTrackListTableModel != null ) {
128                         sequenceTrackListTableModel.getParent().fireTableDataChanged();
129                 }
130         }
131         /**
132          * 再生または録音を開始します。
133          *
134          * <p>録音するMIDIチャンネルがMIDIエディタで指定されている場合、
135          * 録音スタート時のタイムスタンプが正しく0になるよう、各MIDIデバイスのタイムスタンプをすべてリセットします。
136          * </p>
137          * <p>このシーケンサのMIDIデバイスが閉じている場合、再生や録音は開始されません。</p>
138          */
139         public void start() {
140                 if( ! device.isOpen() ) return;
141                 Sequencer sequencer = getSequencer();
142                 SequenceTrackListTableModel sequenceTrackListTableModel = getSequenceTrackListTableModel();
143                 if( sequenceTrackListTableModel != null && sequenceTrackListTableModel.hasRecordChannel() ) {
144                         deviceTreeModel.resetMicrosecondPosition();
145                         sequencer.startRecording();
146                 } else sequencer.start();
147                 timeRangeUpdater.start();
148                 startStopAction.setRunning(true);
149                 fireStateChanged();
150         }
151         /**
152          * 再生または録音を停止します。
153          */
154         public void stop() {
155                 Sequencer sequencer = getSequencer();
156                 if( sequencer.isOpen() ) sequencer.stop();
157                 timeRangeUpdater.stop();
158                 startStopAction.setRunning(false);
159                 fireStateChanged();
160         }
161         /**
162          * このシーケンサーにロードされているシーケンスの長さをマイクロ秒単位で返します。
163          * シーケンスが設定されていない場合は0を返します。
164          * 曲が長すぎて {@link Sequencer#getMicrosecondLength()} が負数を返してしまった場合の補正も行います。
165          * @return マイクロ秒単位でのシーケンスの長さ
166          */
167         public long getMicrosecondLength() {
168                 //
169                 // Sequencer.getMicrosecondLength() returns NEGATIVE value
170                 //  when over 0x7FFFFFFF microseconds (== 35.7913941166666... minutes),
171                 //  should be corrected when negative
172                 //
173                 long usLength = getSequencer().getMicrosecondLength();
174                 return usLength < 0 ? 0x100000000L + usLength : usLength ;
175         }
176         /**
177          * シーケンス上の現在位置をマイクロ秒単位で返します。
178          * 曲が長すぎて {@link Sequencer#getMicrosecondPosition()} が負数を返してしまった場合の補正も行います。
179          * @return マイクロ秒単位での現在の位置
180          */
181         public long getMicrosecondPosition() {
182                 long usPosition = getSequencer().getMicrosecondPosition();
183                 return usPosition < 0 ? 0x100000000L + usPosition : usPosition ;
184         }
185         @Override
186         public int getMaximum() { return (int)(getMicrosecondLength()/RESOLUTION_MICROSECOND); }
187         @Override
188         public void setMaximum(int newMaximum) {}
189         @Override
190         public int getMinimum() { return 0; }
191         @Override
192         public void setMinimum(int newMinimum) {}
193         @Override
194         public int getExtent() { return 0; }
195         @Override
196         public void setExtent(int newExtent) {}
197         @Override
198         public int getValue() { return (int)(getMicrosecondPosition()/RESOLUTION_MICROSECOND); }
199         @Override
200         public void setValue(int newValue) {
201                 getSequencer().setMicrosecondPosition(RESOLUTION_MICROSECOND * (long)newValue);
202                 fireStateChanged();
203         }
204         /**
205          * 値調整中のときtrue
206          */
207         private boolean valueIsAdjusting = false;
208         @Override
209         public boolean getValueIsAdjusting() { return valueIsAdjusting; }
210         @Override
211         public void setValueIsAdjusting(boolean valueIsAdjusting) {
212                 this.valueIsAdjusting = valueIsAdjusting;
213         }
214         /**
215          * シーケンサに合わせてミリ秒位置を更新するタイマー
216          */
217         private javax.swing.Timer timeRangeUpdater = new javax.swing.Timer(20, e->{
218                 if( valueIsAdjusting || ! getSequencer().isRunning() ) {
219                         // 手動で移動中の場合や、シーケンサが止まっている場合は、タイマーによる更新は不要
220                         return;
221                 }
222                 // リスナーに読み込みを促す
223                 fireStateChanged();
224         });
225         @Override
226         public void setRangeProperties(int value, int extent, int min, int max, boolean valueIsAdjusting) {
227                 getSequencer().setMicrosecondPosition(RESOLUTION_MICROSECOND * (long)value);
228                 setValueIsAdjusting(valueIsAdjusting);
229                 fireStateChanged();
230         }
231         protected EventListenerList listenerList = new EventListenerList();
232         /**
233          * {@inheritDoc}
234          * <p>このシーケンサーの再生時間位置または再生対象ファイルが変更されたときに
235          * 通知を受けるリスナーを追加します。
236          * </p>
237          */
238         @Override
239         public void addChangeListener(ChangeListener listener) {
240                 listenerList.add(ChangeListener.class, listener);
241         }
242         /**
243          * {@inheritDoc}
244          * <p>このシーケンサーの再生時間位置または再生対象ファイルが変更されたときに
245          * 通知を受けるリスナーを除去します。
246          * </p>
247          */
248         @Override
249         public void removeChangeListener(ChangeListener listener) {
250                 listenerList.remove(ChangeListener.class, listener);
251         }
252         /**
253          * 秒位置が変わったことをリスナーに通知します。
254          * <p>登録中のすべての {@link ChangeListener} について
255          * {@link ChangeListener#stateChanged(ChangeEvent)}
256          * を呼び出すことによって状態の変化を通知します。
257          * </p>
258          */
259         public void fireStateChanged() {
260                 Object[] listeners = listenerList.getListenerList();
261                 for (int i = listeners.length-2; i>=0; i-=2) {
262                         if (listeners[i]==ChangeListener.class) {
263                                 ((ChangeListener)listeners[i+1]).stateChanged(new ChangeEvent(this));
264                         }
265                 }
266         }
267         /**
268          * MIDIトラックリストテーブルモデル
269          */
270         private SequenceTrackListTableModel sequenceTrackListTableModel = null;
271         /**
272          * このシーケンサーに現在ロードされているシーケンスのMIDIトラックリストテーブルモデルを返します。
273          * @return MIDIトラックリストテーブルモデル(何もロードされていなければnull)
274          */
275         public SequenceTrackListTableModel getSequenceTrackListTableModel() {
276                 return sequenceTrackListTableModel;
277         }
278         /**
279          * MIDIトラックリストテーブルモデルをこのシーケンサーモデルにセットします。
280          * nullを指定してアンセットすることもできます。
281          * @param sequenceTableModel MIDIトラックリストテーブルモデル
282          * @throws InvalidMidiDataException {@link Sequencer#setSequence(Sequence)} を参照
283          * @throws IllegalStateException MIDIシーケンサデバイスが閉じている状態で引数にnullを指定した場合
284          */
285         public void setSequenceTrackListTableModel(SequenceTrackListTableModel sequenceTableModel)
286                 throws InvalidMidiDataException
287         {
288                 Sequencer sequencer = getSequencer();
289                 Sequence sequence = sequenceTableModel == null ? null : sequenceTableModel.getSequence();
290                 sequencer.setSequence(sequence);
291                 startStopAction.updateEnableStatus();
292                 if( this.sequenceTrackListTableModel != null ) this.sequenceTrackListTableModel.fireTableDataChanged();
293                 if( sequenceTableModel != null ) sequenceTableModel.fireTableDataChanged();
294                 this.sequenceTrackListTableModel = sequenceTableModel;
295                 fireStateChanged();
296         }
297         /**
298          * 小節単位で位置を移動します。
299          * @param measureOffset 何小節進めるか(戻したいときは負数を指定)
300          */
301         private void moveMeasure(int measureOffset) {
302                 if( measureOffset == 0 || sequenceTrackListTableModel == null ) return;
303                 SequenceTickIndex seqIndex = sequenceTrackListTableModel.getSequenceTickIndex();
304                 Sequencer sequencer = getSequencer();
305                 int measurePosition = seqIndex.tickToMeasure(sequencer.getTickPosition());
306                 long newTickPosition = seqIndex.measureToTick(measurePosition + measureOffset);
307                 if( newTickPosition < 0 ) {
308                         // 下限
309                         newTickPosition = 0;
310                 }
311                 else {
312                         long tickLength = sequencer.getTickLength();
313                         if( newTickPosition > tickLength ) {
314                                 // 上限
315                                 newTickPosition = tickLength - 1;
316                         }
317                 }
318                 sequencer.setTickPosition(newTickPosition);
319                 fireStateChanged();
320         }
321         /**
322          * 1小節戻るアクション
323          */
324         public Action getMoveBackwardAction() { return moveBackwardAction; }
325         private Action moveBackwardAction = new AbstractAction() {
326                 {
327                         putValue(SHORT_DESCRIPTION, "Move backward 1 measure - 1小節戻る");
328                         putValue(LARGE_ICON_KEY, new ButtonIcon(ButtonIcon.BACKWARD_ICON));
329                 }
330                 @Override
331                 public void actionPerformed(ActionEvent event) { moveMeasure(-1); }
332         };
333         /**
334          *1小節進むアクション
335          */
336         public Action getMoveForwardAction() { return moveForwardAction; }
337         private Action moveForwardAction = new AbstractAction() {
338                 {
339                         putValue(SHORT_DESCRIPTION, "Move forward 1 measure - 1小節進む");
340                         putValue(LARGE_ICON_KEY, new ButtonIcon(ButtonIcon.FORWARD_ICON));
341                 }
342                 @Override
343                 public void actionPerformed(ActionEvent event) { moveMeasure(1); }
344         };
345         /**
346          * マスター同期モードのコンボボックスモデル
347          */
348         public ComboBoxModel<Sequencer.SyncMode> masterSyncModeModel =
349                 new DefaultComboBoxModel<Sequencer.SyncMode>(getSequencer().getMasterSyncModes()) {{
350                         addListDataListener(new ListDataListener() {
351                                 @Override
352                                 public void intervalAdded(ListDataEvent e) { }
353                                 @Override
354                                 public void intervalRemoved(ListDataEvent e) { }
355                                 @Override
356                                 public void contentsChanged(ListDataEvent e) {
357                                         getSequencer().setMasterSyncMode((Sequencer.SyncMode)getSelectedItem());
358                                 }
359                         });
360                 }};
361         /**
362          * スレーブ同期モードのコンボボックスモデル
363          */
364         public ComboBoxModel<Sequencer.SyncMode> slaveSyncModeModel =
365                 new DefaultComboBoxModel<Sequencer.SyncMode>(getSequencer().getSlaveSyncModes()) {{
366                         addListDataListener(new ListDataListener() {
367                                 @Override
368                                 public void intervalAdded(ListDataEvent e) { }
369                                 @Override
370                                 public void intervalRemoved(ListDataEvent e) { }
371                                 @Override
372                                 public void contentsChanged(ListDataEvent e) {
373                                         getSequencer().setSlaveSyncMode((Sequencer.SyncMode)getSelectedItem());
374                                 }
375                         });
376                 }};
377 }