1 package camidion.chordhelper.mididevice;
3 import java.awt.event.ActionEvent;
4 import java.util.HashMap;
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;
24 import camidion.chordhelper.ButtonIcon;
25 import camidion.chordhelper.midieditor.SequenceTickIndex;
26 import camidion.chordhelper.midieditor.SequenceTrackListTableModel;
27 import camidion.chordhelper.midieditor.SequencerSpeedSlider;
30 * MIDIシーケンサモデル(再生位置モデルのインターフェース付き)
32 public class MidiSequencerModel extends MidiDeviceModel implements BoundedRangeModel {
34 * 再生位置スライダーモデルの最小単位 [μ秒]
36 public static final long RESOLUTION_MICROSECOND = 1000L;
39 * @param sequencer シーケンサーMIDIデバイス
40 * @param deviceModelTree 親のMIDIデバイスツリーモデル
42 public MidiSequencerModel(Sequencer sequencer, MidiDeviceTreeModel deviceModelTree) {
43 super(sequencer, deviceModelTree);
48 public BoundedRangeModel speedSliderModel = new DefaultBoundedRangeModel(0, 0, -7, 7) {{
49 addChangeListener(e->getSequencer().setTempoFactor(SequencerSpeedSlider.tempoFactorOf(getValue())));
52 * 対象MIDIシーケンサデバイス({@link #getMidiDevice()}をキャストした結果)を返します。
53 * @return 対象MIDIシーケンサデバイス
55 public Sequencer getSequencer() { return (Sequencer)device; }
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>() {
64 put(Boolean.FALSE, new ButtonIcon(ButtonIcon.PLAY_ICON));
65 put(Boolean.TRUE, new ButtonIcon(ButtonIcon.PAUSE_ICON));
69 putValue(SHORT_DESCRIPTION, "Start/Stop recording or playing - 録音または再生の開始/停止");
74 public void actionPerformed(ActionEvent event) {
75 if( timeRangeUpdater.isRunning() ) stop(); else start();
78 private void setRunning(boolean isRunning) {
79 putValue(LARGE_ICON_KEY, iconMap.get(isRunning));
80 putValue(SELECTED_KEY, isRunning);
83 * 再生または録音が可能かチェックし、操作可能状態を更新します。
85 public void updateEnableStatus() { setEnabled(isStartable()); }
89 * <p>以下の条件が揃ったときに再生または録音が可能と判定されます。</p>
91 * <li>MIDIシーケンサデバイスが開いている</li>
92 * <li>MIDIシーケンサに操作対象のMIDIシーケンスが設定されている</li>
94 * @return 再生または録音が可能な場合true
96 public boolean isStartable() {
97 return device.isOpen() && getSequencer().getSequence() != null;
101 * <p>シーケンサモデルの場合、録音再生可能状態が変わるので、開始終了アクションにも通知します。</p>
104 public void open() throws MidiUnavailableException {
106 startStopAction.updateEnableStatus();
108 if( sequenceTrackListTableModel != null ) {
109 sequenceTrackListTableModel.getParent().fireTableDataChanged();
114 * <p>シーケンサモデルの場合、再生または録音が不可能になるので、開始終了アクションにも通知します。</p>
117 public void close() {
120 setSequenceTrackListTableModel(null);
121 } catch (InvalidMidiDataException|IllegalStateException e) {
125 startStopAction.updateEnableStatus();
127 if( sequenceTrackListTableModel != null ) {
128 sequenceTrackListTableModel.getParent().fireTableDataChanged();
134 * <p>録音するMIDIチャンネルがMIDIエディタで指定されている場合、
135 * 録音スタート時のタイムスタンプが正しく0になるよう、各MIDIデバイスのタイムスタンプをすべてリセットします。
137 * <p>このシーケンサのMIDIデバイスが閉じている場合、再生や録音は開始されません。</p>
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);
155 Sequencer sequencer = getSequencer();
156 if( sequencer.isOpen() ) sequencer.stop();
157 timeRangeUpdater.stop();
158 startStopAction.setRunning(false);
162 * このシーケンサーにロードされているシーケンスの長さをマイクロ秒単位で返します。
163 * シーケンスが設定されていない場合は0を返します。
164 * 曲が長すぎて {@link Sequencer#getMicrosecondLength()} が負数を返してしまった場合の補正も行います。
165 * @return マイクロ秒単位でのシーケンスの長さ
167 public long getMicrosecondLength() {
169 // Sequencer.getMicrosecondLength() returns NEGATIVE value
170 // when over 0x7FFFFFFF microseconds (== 35.7913941166666... minutes),
171 // should be corrected when negative
173 long usLength = getSequencer().getMicrosecondLength();
174 return usLength < 0 ? 0x100000000L + usLength : usLength ;
177 * シーケンス上の現在位置をマイクロ秒単位で返します。
178 * 曲が長すぎて {@link Sequencer#getMicrosecondPosition()} が負数を返してしまった場合の補正も行います。
179 * @return マイクロ秒単位での現在の位置
181 public long getMicrosecondPosition() {
182 long usPosition = getSequencer().getMicrosecondPosition();
183 return usPosition < 0 ? 0x100000000L + usPosition : usPosition ;
186 public int getMaximum() { return (int)(getMicrosecondLength()/RESOLUTION_MICROSECOND); }
188 public void setMaximum(int newMaximum) {}
190 public int getMinimum() { return 0; }
192 public void setMinimum(int newMinimum) {}
194 public int getExtent() { return 0; }
196 public void setExtent(int newExtent) {}
198 public int getValue() { return (int)(getMicrosecondPosition()/RESOLUTION_MICROSECOND); }
200 public void setValue(int newValue) {
201 getSequencer().setMicrosecondPosition(RESOLUTION_MICROSECOND * (long)newValue);
207 private boolean valueIsAdjusting = false;
209 public boolean getValueIsAdjusting() { return valueIsAdjusting; }
211 public void setValueIsAdjusting(boolean valueIsAdjusting) {
212 this.valueIsAdjusting = valueIsAdjusting;
215 * シーケンサに合わせてミリ秒位置を更新するタイマー
217 private javax.swing.Timer timeRangeUpdater = new javax.swing.Timer(20, e->{
218 if( valueIsAdjusting || ! getSequencer().isRunning() ) {
219 // 手動で移動中の場合や、シーケンサが止まっている場合は、タイマーによる更新は不要
226 public void setRangeProperties(int value, int extent, int min, int max, boolean valueIsAdjusting) {
227 getSequencer().setMicrosecondPosition(RESOLUTION_MICROSECOND * (long)value);
228 setValueIsAdjusting(valueIsAdjusting);
231 protected EventListenerList listenerList = new EventListenerList();
234 * <p>このシーケンサーの再生時間位置または再生対象ファイルが変更されたときに
239 public void addChangeListener(ChangeListener listener) {
240 listenerList.add(ChangeListener.class, listener);
244 * <p>このシーケンサーの再生時間位置または再生対象ファイルが変更されたときに
249 public void removeChangeListener(ChangeListener listener) {
250 listenerList.remove(ChangeListener.class, listener);
253 * 秒位置が変わったことをリスナーに通知します。
254 * <p>登録中のすべての {@link ChangeListener} について
255 * {@link ChangeListener#stateChanged(ChangeEvent)}
256 * を呼び出すことによって状態の変化を通知します。
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));
270 private SequenceTrackListTableModel sequenceTrackListTableModel = null;
272 * このシーケンサーに現在ロードされているシーケンスのMIDIトラックリストテーブルモデルを返します。
273 * @return MIDIトラックリストテーブルモデル(何もロードされていなければnull)
275 public SequenceTrackListTableModel getSequenceTrackListTableModel() {
276 return sequenceTrackListTableModel;
279 * MIDIトラックリストテーブルモデルをこのシーケンサーモデルにセットします。
280 * nullを指定してアンセットすることもできます。
281 * @param sequenceTableModel MIDIトラックリストテーブルモデル
282 * @throws InvalidMidiDataException {@link Sequencer#setSequence(Sequence)} を参照
283 * @throws IllegalStateException MIDIシーケンサデバイスが閉じている状態で引数にnullを指定した場合
285 public void setSequenceTrackListTableModel(SequenceTrackListTableModel sequenceTableModel)
286 throws InvalidMidiDataException
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;
299 * @param measureOffset 何小節進めるか(戻したいときは負数を指定)
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 ) {
312 long tickLength = sequencer.getTickLength();
313 if( newTickPosition > tickLength ) {
315 newTickPosition = tickLength - 1;
318 sequencer.setTickPosition(newTickPosition);
324 public Action getMoveBackwardAction() { return moveBackwardAction; }
325 private Action moveBackwardAction = new AbstractAction() {
327 putValue(SHORT_DESCRIPTION, "Move backward 1 measure - 1小節戻る");
328 putValue(LARGE_ICON_KEY, new ButtonIcon(ButtonIcon.BACKWARD_ICON));
331 public void actionPerformed(ActionEvent event) { moveMeasure(-1); }
336 public Action getMoveForwardAction() { return moveForwardAction; }
337 private Action moveForwardAction = new AbstractAction() {
339 putValue(SHORT_DESCRIPTION, "Move forward 1 measure - 1小節進む");
340 putValue(LARGE_ICON_KEY, new ButtonIcon(ButtonIcon.FORWARD_ICON));
343 public void actionPerformed(ActionEvent event) { moveMeasure(1); }
346 * マスター同期モードのコンボボックスモデル
348 public ComboBoxModel<Sequencer.SyncMode> masterSyncModeModel =
349 new DefaultComboBoxModel<Sequencer.SyncMode>(getSequencer().getMasterSyncModes()) {{
350 addListDataListener(new ListDataListener() {
352 public void intervalAdded(ListDataEvent e) { }
354 public void intervalRemoved(ListDataEvent e) { }
356 public void contentsChanged(ListDataEvent e) {
357 getSequencer().setMasterSyncMode((Sequencer.SyncMode)getSelectedItem());
362 * スレーブ同期モードのコンボボックスモデル
364 public ComboBoxModel<Sequencer.SyncMode> slaveSyncModeModel =
365 new DefaultComboBoxModel<Sequencer.SyncMode>(getSequencer().getSlaveSyncModes()) {{
366 addListDataListener(new ListDataListener() {
368 public void intervalAdded(ListDataEvent e) { }
370 public void intervalRemoved(ListDataEvent e) { }
372 public void contentsChanged(ListDataEvent e) {
373 getSequencer().setSlaveSyncMode((Sequencer.SyncMode)getSelectedItem());