1 package camidion.chordhelper.mididevice;
3 import java.awt.event.ActionEvent;
4 import java.awt.event.ActionListener;
5 import java.util.HashMap;
8 import javax.sound.midi.InvalidMidiDataException;
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.ListDataEvent;
21 import javax.swing.event.ListDataListener;
23 import camidion.chordhelper.ButtonIcon;
24 import camidion.chordhelper.midieditor.SequenceTickIndex;
25 import camidion.chordhelper.midieditor.SequenceTrackListTableModel;
26 import camidion.chordhelper.midieditor.SequencerSpeedSlider;
31 public class MidiSequencerModel extends MidiTransceiverListModel implements BoundedRangeModel {
34 * @param sequencer シーケンサーMIDIデバイス
35 * @param deviceModelList 親のMIDIデバイスモデルリスト
37 public MidiSequencerModel(Sequencer sequencer, MidiTransceiverListModelList deviceModelList) {
38 super(sequencer, deviceModelList);
41 * このシーケンサーの再生スピード調整モデル
43 public BoundedRangeModel speedSliderModel = new DefaultBoundedRangeModel(0, 0, -7, 7) {{
44 addChangeListener(new ChangeListener() {
46 public void stateChanged(ChangeEvent e) {
47 getSequencer().setTempoFactor(SequencerSpeedSlider.tempoFactorOf(getValue()));
55 public Sequencer getSequencer() { return (Sequencer)device; }
59 public StartStopAction startStopAction = new StartStopAction();
63 class StartStopAction extends AbstractAction {
64 private Map<Boolean,Icon> iconMap = new HashMap<Boolean,Icon>() {
66 put(Boolean.FALSE, new ButtonIcon(ButtonIcon.PLAY_ICON));
67 put(Boolean.TRUE, new ButtonIcon(ButtonIcon.PAUSE_ICON));
73 "Start/Stop recording or playing - 録音または再生の開始/停止"
78 public void actionPerformed(ActionEvent event) {
79 if(timeRangeUpdater.isRunning()) stop(); else start();
83 * @param isRunning 開始されていたらtrue
85 private void setRunning(boolean isRunning) {
86 putValue(LARGE_ICON_KEY, iconMap.get(isRunning));
87 putValue(SELECTED_KEY, isRunning);
91 * シーケンサに合わせてミリ秒位置を更新するタイマー
93 private javax.swing.Timer timeRangeUpdater = new javax.swing.Timer( 20,
96 public void actionPerformed(ActionEvent e) {
97 if( valueIsAdjusting || ! getSequencer().isRunning() ) {
98 // 手動で移動中の場合や、シーケンサが止まっている場合は、
108 * このモデルのMIDIシーケンサを開始します。
110 * <p>録音するMIDIチャンネルがMIDIエディタで指定されている場合、
111 * 録音スタート時のタイムスタンプが正しく0になるよう、
112 * 各MIDIデバイスのタイムスタンプをすべてリセットします。
115 public void start() {
116 Sequencer sequencer = getSequencer();
117 if( ! sequencer.isOpen() || sequencer.getSequence() == null ) {
118 timeRangeUpdater.stop();
119 startStopAction.setRunning(false);
122 startStopAction.setRunning(true);
123 timeRangeUpdater.start();
124 SequenceTrackListTableModel sequenceTableModel = getSequenceTrackListTableModel();
125 if( sequenceTableModel != null && sequenceTableModel.hasRecordChannel() ) {
126 deviceModelList.resetMicrosecondPosition();
128 sequencer.startRecording();
137 * このモデルのMIDIシーケンサを停止します。
140 Sequencer sequencer = getSequencer();
141 if(sequencer.isOpen()) sequencer.stop();
142 timeRangeUpdater.stop();
143 startStopAction.setRunning(false);
147 * {@link Sequencer#getMicrosecondLength()} と同じです。
148 * @return マイクロ秒単位でのシーケンスの長さ
150 public long getMicrosecondLength() {
152 // Sequencer.getMicrosecondLength() returns NEGATIVE value
153 // when over 0x7FFFFFFF microseconds (== 35.7913941166666... minutes),
154 // should be corrected when negative
156 long usLength = getSequencer().getMicrosecondLength();
157 return usLength < 0 ? 0x100000000L + usLength : usLength ;
160 public int getMaximum() { return (int)(getMicrosecondLength()/1000L); }
162 public void setMaximum(int newMaximum) {}
164 public int getMinimum() { return 0; }
166 public void setMinimum(int newMinimum) {}
168 public int getExtent() { return 0; }
170 public void setExtent(int newExtent) {}
172 * {@link Sequencer#getMicrosecondPosition()} と同じです。
173 * @return マイクロ秒単位での現在の位置
175 public long getMicrosecondPosition() {
176 long usPosition = getSequencer().getMicrosecondPosition();
177 return usPosition < 0 ? 0x100000000L + usPosition : usPosition ;
180 public int getValue() { return (int)(getMicrosecondPosition()/1000L); }
182 public void setValue(int newValue) {
183 getSequencer().setMicrosecondPosition(1000L * (long)newValue);
189 private boolean valueIsAdjusting = false;
191 public boolean getValueIsAdjusting() { return valueIsAdjusting; }
193 public void setValueIsAdjusting(boolean valueIsAdjusting) {
194 this.valueIsAdjusting = valueIsAdjusting;
197 public void setRangeProperties(int value, int extent, int min, int max, boolean valueIsAdjusting) {
198 getSequencer().setMicrosecondPosition(1000L * (long)value);
199 setValueIsAdjusting(valueIsAdjusting);
204 * <p>このシーケンサーの再生時間位置または再生対象ファイルが変更されたときに
209 public void addChangeListener(ChangeListener listener) {
210 listenerList.add(ChangeListener.class, listener);
214 * <p>このシーケンサーの再生時間位置または再生対象ファイルが変更されたときに
219 public void removeChangeListener(ChangeListener listener) {
220 listenerList.remove(ChangeListener.class, listener);
223 * 秒位置が変わったことをリスナーに通知します。
224 * <p>登録中のすべての {@link ChangeListener} について
225 * {@link ChangeListener#stateChanged(ChangeEvent)}
226 * を呼び出すことによって状態の変化を通知します。
229 public void fireStateChanged() {
230 Object[] listeners = listenerList.getListenerList();
231 for (int i = listeners.length-2; i>=0; i-=2) {
232 if (listeners[i]==ChangeListener.class) {
233 ((ChangeListener)listeners[i+1]).stateChanged(new ChangeEvent(this));
240 private SequenceTrackListTableModel sequenceTableModel = null;
242 * このシーケンサーに現在ロードされているシーケンスのMIDIトラックリストテーブルモデルを返します。
243 * @return MIDIトラックリストテーブルモデル(何もロードされていなければnull)
245 public SequenceTrackListTableModel getSequenceTrackListTableModel() {
246 return sequenceTableModel;
249 * MIDIトラックリストテーブルモデルをこのシーケンサーモデルにセットします。
250 * nullを指定してアンセットすることもできます。
251 * @param sequenceTableModel MIDIトラックリストテーブルモデル
252 * @throws InvalidMidiDataException {@link Sequencer#setSequence(Sequence)} を参照
254 public void setSequenceTrackListTableModel(SequenceTrackListTableModel sequenceTableModel)
255 throws InvalidMidiDataException
257 // javax.sound.midi:Sequencer.setSequence() のドキュメントにある
258 // 「このメソッドは、Sequencer が閉じている場合でも呼び出すことができます。 」
259 // という記述は、null をセットする場合には当てはまらない。
260 // 連鎖的に stop() が呼ばれるために IllegalStateException sequencer not open が出る。
261 // この現象を回避するため、あらかじめチェックしてから setSequence() を呼び出している。
263 if( sequenceTableModel != null || getSequencer().isOpen() ) {
264 getSequencer().setSequence(sequenceTableModel == null ? null : sequenceTableModel.getSequence());
266 if( this.sequenceTableModel != null ) this.sequenceTableModel.fireTableDataChanged();
267 if( sequenceTableModel != null ) sequenceTableModel.fireTableDataChanged();
268 this.sequenceTableModel = sequenceTableModel;
273 * @param measureOffset 何小節進めるか(戻したいときは負数を指定)
275 private void moveMeasure(int measureOffset) {
276 if( measureOffset == 0 || sequenceTableModel == null ) return;
277 SequenceTickIndex seqIndex = sequenceTableModel.getSequenceTickIndex();
278 Sequencer sequencer = getSequencer();
279 int measurePosition = seqIndex.tickToMeasure(sequencer.getTickPosition());
280 long newTickPosition = seqIndex.measureToTick(measurePosition + measureOffset);
281 if( newTickPosition < 0 ) {
286 long tickLength = sequencer.getTickLength();
287 if( newTickPosition > tickLength ) {
289 newTickPosition = tickLength - 1;
292 sequencer.setTickPosition(newTickPosition);
298 public Action moveBackwardAction = new AbstractAction() {
300 putValue(SHORT_DESCRIPTION, "Move backward 1 measure - 1小節戻る");
301 putValue(LARGE_ICON_KEY, new ButtonIcon(ButtonIcon.BACKWARD_ICON));
304 public void actionPerformed(ActionEvent event) { moveMeasure(-1); }
309 public Action moveForwardAction = new AbstractAction() {
311 putValue(SHORT_DESCRIPTION, "Move forward 1 measure - 1小節進む");
312 putValue(LARGE_ICON_KEY, new ButtonIcon(ButtonIcon.FORWARD_ICON));
315 public void actionPerformed(ActionEvent event) { moveMeasure(1); }
318 * マスター同期モードのコンボボックスモデル
320 public ComboBoxModel<Sequencer.SyncMode> masterSyncModeModel =
321 new DefaultComboBoxModel<Sequencer.SyncMode>(getSequencer().getMasterSyncModes()) {{
322 addListDataListener(new ListDataListener() {
324 public void intervalAdded(ListDataEvent e) { }
326 public void intervalRemoved(ListDataEvent e) { }
328 public void contentsChanged(ListDataEvent e) {
329 getSequencer().setMasterSyncMode((Sequencer.SyncMode)getSelectedItem());
334 * スレーブ同期モードのコンボボックスモデル
336 public ComboBoxModel<Sequencer.SyncMode> slaveSyncModeModel =
337 new DefaultComboBoxModel<Sequencer.SyncMode>(getSequencer().getSlaveSyncModes()) {{
338 addListDataListener(new ListDataListener() {
340 public void intervalAdded(ListDataEvent e) { }
342 public void intervalRemoved(ListDataEvent e) { }
344 public void contentsChanged(ListDataEvent e) {
345 getSequencer().setSlaveSyncMode((Sequencer.SyncMode)getSelectedItem());