OSDN Git Service

・シーケンサを閉じてしまった場合の挙動を改善
authorAkiyoshi Kamide <kamide@yk.rim.or.jp>
Sun, 26 Mar 2017 16:06:44 +0000 (01:06 +0900)
committerAkiyoshi Kamide <kamide@yk.rim.or.jp>
Sun, 26 Mar 2017 16:06:44 +0000 (01:06 +0900)
・リファクタリング

src/camidion/chordhelper/ChordHelperApplet.java
src/camidion/chordhelper/mididevice/MidiDeviceModel.java
src/camidion/chordhelper/mididevice/MidiDeviceTreeModel.java
src/camidion/chordhelper/mididevice/MidiSequencerModel.java
src/camidion/chordhelper/mididevice/ReceiverListModel.java
src/camidion/chordhelper/midieditor/MidiSequenceEditorDialog.java
src/camidion/chordhelper/midieditor/NewSequenceDialog.java
src/camidion/chordhelper/midieditor/PlaylistTableModel.java
src/camidion/chordhelper/midieditor/SequenceTrackListTableModel.java

index 93b2eac..74d0521 100644 (file)
@@ -86,10 +86,11 @@ public class ChordHelperApplet extends JApplet {
                return playlistModel.getSequenceModelList().stream().anyMatch(m -> m.isModified());
        }
        /**
-        * 指定された小節数の曲を、乱数で自動作曲してプレイリストへ追加します。
+        * æ\8c\87å®\9aã\81\95ã\82\8cã\81\9få°\8fç¯\80æ\95°ã\81®æ\9b²ã\82\92ã\80\81ä¹±æ\95°ã\81§è\87ªå\8b\95ä½\9cæ\9b²ã\81\97ã\81¦ã\83\97ã\83¬ã\82¤ã\83ªã\82¹ã\83\88ã\81¸è¿½å\8a ã\81\97ã\80\81å\86\8dç\94\9fã\81\97ã\81¾ã\81\99ã\80\82
         * @param measureLength 小節数
         * @return 追加先のインデックス値(0から始まる)。追加できなかったときは -1
         * @throws InvalidMidiDataException {@link Sequencer#setSequence(Sequence)} を参照
+        * @throws IllegalStateException MIDIシーケンサデバイスが閉じている場合
         */
        public int addRandomSongToPlaylist(int measureLength) throws InvalidMidiDataException {
                NewSequenceDialog d = midiEditor.newSequenceDialog;
@@ -144,6 +145,7 @@ public class ChordHelperApplet extends JApplet {
         * プレイリスト上で現在選択されているMIDIシーケンスを、
         * シーケンサへロードして再生します。
         * @throws InvalidMidiDataException {@link Sequencer#setSequence(Sequence)} を参照
+        * @throws IllegalStateException MIDIシーケンサデバイスが閉じている場合
         */
        public void play() throws InvalidMidiDataException {
                play(playlistModel.sequenceListSelectionModel.getMinSelectionIndex());
@@ -153,6 +155,7 @@ public class ChordHelperApplet extends JApplet {
         * シーケンサへロードして再生します。
         * @param index インデックス値(0から始まる)
         * @throws InvalidMidiDataException {@link Sequencer#setSequence(Sequence)} を参照
+        * @throws IllegalStateException MIDIシーケンサデバイスが閉じている場合
         */
        public void play(int index) throws InvalidMidiDataException {
                playlistModel.loadToSequencer(index); sequencerModel.start();
@@ -271,7 +274,7 @@ public class ChordHelperApplet extends JApplet {
         */
        public static class VersionInfo {
                public static final String NAME = "MIDI Chord Helper";
-               public static final String VERSION = "Ver.20170324.1";
+               public static final String VERSION = "Ver.20170326.1";
                public static final String COPYRIGHT = "Copyright (C) 2004-2017";
                public static final String AUTHER = "@きよし - Akiyoshi Kamide";
                public static final String URL = "http://www.yk.rim.or.jp/~kamide/music/chordhelper/";
@@ -382,7 +385,8 @@ public class ChordHelperApplet extends JApplet {
                midiDeviceDialog.setIconImage(iconImage);
                //
                // MIDIエディタダイアログの構築
-               (midiEditor = new MidiSequenceEditorDialog(playlistModel, guiMidiDevice)).setIconImage(iconImage);
+               midiEditor = new MidiSequenceEditorDialog(playlistModel, guiMidiDevice, midiDeviceDialog.getOpenAction());
+               midiEditor.setIconImage(iconImage);
                //
                // メイン画面へのMIDIファイルのドラッグ&ドロップ受付開始
                setTransferHandler(midiEditor.transferHandler);
@@ -416,21 +420,21 @@ public class ChordHelperApplet extends JApplet {
                //シーケンサーの時間スライダーの値が変わったときのリスナーを登録
                JLabel songTitleLabel = new JLabel();
                sequencerModel.addChangeListener(e->{
-                       SequenceTrackListTableModel sequenceTableModel = sequencerModel.getSequenceTrackListTableModel();
+                       SequenceTrackListTableModel sequenceTrackListTableModel = sequencerModel.getSequenceTrackListTableModel();
                        int loadedSequenceIndex = playlistModel.indexOfSequenceOnSequencer();
                        songTitleLabel.setText("<html>"+(
                                loadedSequenceIndex < 0 ? "[No MIDI file loaded]" :
                                "MIDI file " + loadedSequenceIndex + ": " + (
-                                       sequenceTableModel == null ||
-                                       sequenceTableModel.toString().isEmpty() ?
+                                       sequenceTrackListTableModel == null ||
+                                       sequenceTrackListTableModel.toString().isEmpty() ?
                                        "[Untitled]" :
-                                       "<font color=maroon>"+sequenceTableModel+"</font>"
+                                       "<font color=maroon>"+sequenceTrackListTableModel+"</font>"
                                )
                        )+"</html>");
                        Sequencer sequencer = sequencerModel.getSequencer();
                        chordMatrix.setPlaying(sequencer.isRunning());
-                       if( sequenceTableModel != null ) {
-                               SequenceTickIndex tickIndex = sequenceTableModel.getSequenceTickIndex();
+                       if( sequenceTrackListTableModel != null ) {
+                               SequenceTickIndex tickIndex = sequenceTrackListTableModel.getSequenceTickIndex();
                                long tickPos = sequencer.getTickPosition();
                                tickIndex.tickToMeasure(tickPos);
                                chordMatrix.setBeat(tickIndex);
@@ -587,7 +591,7 @@ public class ChordHelperApplet extends JApplet {
                        addToPlaylist(midiUrl);
                        try {
                                play();
-                       } catch (InvalidMidiDataException ex) {
+                       } catch (Exception ex) {
                                ex.printStackTrace();
                        }
                }
index e0e89a4..a5ed1ba 100644 (file)
@@ -93,7 +93,7 @@ public class MidiDeviceModel {
         * それらも全て閉じます。
         */
        public void close() {
-               if( rxListModel != null ) rxListModel.closeTransmitters();
+               if( rxListModel != null ) rxListModel.closeAllConnectedTransmitters();
                device.close();
        }
 }
index ec2729f..aa4dd89 100644 (file)
@@ -260,8 +260,9 @@ public class MidiDeviceTreeModel extends AbstractList<MidiDeviceModel> implement
                deviceModelList.stream().forEach(m -> {
                        ReceiverListModel rxListModel = m.getReceiverListModel();
                        if( rxListModel == null ) return;
-                       Collection<MidiDeviceModel> txDeviceModels = rxListModel.closeTransmitters();
-                       if( ! txDeviceModels.isEmpty() ) rxToTxConnections.put(m, txDeviceModels);
+                       Collection<MidiDeviceModel> txDeviceModels = rxListModel.closeAllConnectedTransmitters();
+                       if( txDeviceModels.isEmpty() ) return;
+                       rxToTxConnections.put(m, txDeviceModels);
                });
                return rxToTxConnections;
        }
index de0050c..75911f5 100644 (file)
@@ -5,6 +5,7 @@ import java.util.HashMap;
 import java.util.Map;
 
 import javax.sound.midi.InvalidMidiDataException;
+import javax.sound.midi.MidiUnavailableException;
 import javax.sound.midi.Sequence;
 import javax.sound.midi.Sequencer;
 import javax.swing.AbstractAction;
@@ -48,8 +49,8 @@ public class MidiSequencerModel extends MidiDeviceModel implements BoundedRangeM
                addChangeListener(e->getSequencer().setTempoFactor(SequencerSpeedSlider.tempoFactorOf(getValue())));
        }};
        /**
-        * MIDIシーケンサを返します。
-        * @return MIDIシーケンサ
+        * 対象MIDIシーケンサデバイス({@link #getMidiDevice()}をキャストした結果)を返します。
+        * @return 対象MIDIシーケンサデバイス
         */
        public Sequencer getSequencer() { return (Sequencer)device; }
        /**
@@ -67,57 +68,100 @@ public class MidiSequencerModel extends MidiDeviceModel implements BoundedRangeM
                {
                        putValue(SHORT_DESCRIPTION, "Start/Stop recording or playing - 録音または再生の開始/停止");
                        setRunning(false);
+                       updateEnableStatus();
                }
                @Override
                public void actionPerformed(ActionEvent event) {
-                       if(timeRangeUpdater.isRunning()) stop(); else start();
+                       if( timeRangeUpdater.isRunning() ) stop(); else start();
+                       updateEnableStatus();
                }
                private void setRunning(boolean isRunning) {
                        putValue(LARGE_ICON_KEY, iconMap.get(isRunning));
                        putValue(SELECTED_KEY, isRunning);
                }
+               /**
+                * 再生または録音が可能かチェックし、操作可能状態を更新します。
+                */
+               public void updateEnableStatus() { setEnabled(isStartable()); }
        }
        /**
-        * このモデルのMIDIシーケンサを開始します。
+        * 再生または録音が可能か調べます。
+        * <p>以下の条件が揃ったときに再生または録音が可能と判定されます。</p>
+        * <ul>
+        * <li>MIDIシーケンサデバイスが開いている</li>
+        * <li>MIDIシーケンサに操作対象のMIDIシーケンスが設定されている</li>
+        * </ul>
+        * @return 再生または録音が可能な場合true
+        */
+       public boolean isStartable() {
+               return device.isOpen() && getSequencer().getSequence() != null;
+       }
+       /**
+        * {@inheritDoc}
+        * <p>シーケンサモデルの場合、録音再生可能状態が変わるので、開始終了アクションにも通知します。</p>
+        */
+       @Override
+       public void open() throws MidiUnavailableException {
+               super.open();
+               startStopAction.updateEnableStatus();
+               fireStateChanged();
+               if( sequenceTrackListTableModel != null ) {
+                       sequenceTrackListTableModel.getParent().fireTableDataChanged();
+               }
+       }
+       /**
+        * {@inheritDoc}
+        * <p>シーケンサモデルの場合、再生または録音が不可能になるので、開始終了アクションにも通知します。</p>
+        */
+       @Override
+       public void close() {
+               stop();
+               try {
+                       setSequenceTrackListTableModel(null);
+               } catch (InvalidMidiDataException|IllegalStateException e) {
+                       e.printStackTrace();
+               }
+               super.close();
+               startStopAction.updateEnableStatus();
+               fireStateChanged();
+               if( sequenceTrackListTableModel != null ) {
+                       sequenceTrackListTableModel.getParent().fireTableDataChanged();
+               }
+       }
+       /**
+        * 再生または録音を開始します。
         *
         * <p>録音するMIDIチャンネルがMIDIエディタで指定されている場合、
-        * 録音スタート時のタイムスタンプが正しく0になるよう、
-        * 各MIDIデバイスのタイムスタンプをすべてリセットします。
+        * 録音スタート時のタイムスタンプが正しく0になるよう、各MIDIデバイスのタイムスタンプをすべてリセットします。
         * </p>
+        * <p>このシーケンサのMIDIデバイスが閉じている場合、再生や録音は開始されません。</p>
         */
        public void start() {
+               if( ! device.isOpen() ) return;
                Sequencer sequencer = getSequencer();
-               if( ! sequencer.isOpen() || sequencer.getSequence() == null ) {
-                       timeRangeUpdater.stop();
-                       startStopAction.setRunning(false);
-                       return;
-               }
-               startStopAction.setRunning(true);
-               timeRangeUpdater.start();
-               SequenceTrackListTableModel sequenceTableModel = getSequenceTrackListTableModel();
-               if( sequenceTableModel != null && sequenceTableModel.hasRecordChannel() ) {
+               SequenceTrackListTableModel sequenceTrackListTableModel = getSequenceTrackListTableModel();
+               if( sequenceTrackListTableModel != null && sequenceTrackListTableModel.hasRecordChannel() ) {
                        deviceTreeModel.resetMicrosecondPosition();
-                       System.gc();
                        sequencer.startRecording();
-               }
-               else {
-                       System.gc();
-                       sequencer.start();
-               }
+               } else sequencer.start();
+               timeRangeUpdater.start();
+               startStopAction.setRunning(true);
                fireStateChanged();
        }
        /**
-        * このモデルのMIDIシーケンサを停止します。
+        * 再生または録音を停止します。
         */
        public void stop() {
                Sequencer sequencer = getSequencer();
-               if(sequencer.isOpen()) sequencer.stop();
+               if( sequencer.isOpen() ) sequencer.stop();
                timeRangeUpdater.stop();
                startStopAction.setRunning(false);
                fireStateChanged();
        }
        /**
-        * {@link Sequencer#getMicrosecondLength()} と同じです。
+        * このシーケンサーにロードされているシーケンスの長さをマイクロ秒単位で返します。
+        * シーケンスが設定されていない場合は0を返します。
+        * 曲が長すぎて {@link Sequencer#getMicrosecondLength()} が負数を返してしまった場合の補正も行います。
         * @return マイクロ秒単位でのシーケンスの長さ
         */
        public long getMicrosecondLength() {
@@ -129,6 +173,15 @@ public class MidiSequencerModel extends MidiDeviceModel implements BoundedRangeM
                long usLength = getSequencer().getMicrosecondLength();
                return usLength < 0 ? 0x100000000L + usLength : usLength ;
        }
+       /**
+        * シーケンス上の現在位置をマイクロ秒単位で返します。
+        * 曲が長すぎて {@link Sequencer#getMicrosecondPosition()} が負数を返してしまった場合の補正も行います。
+        * @return マイクロ秒単位での現在の位置
+        */
+       public long getMicrosecondPosition() {
+               long usPosition = getSequencer().getMicrosecondPosition();
+               return usPosition < 0 ? 0x100000000L + usPosition : usPosition ;
+       }
        @Override
        public int getMaximum() { return (int)(getMicrosecondLength()/RESOLUTION_MICROSECOND); }
        @Override
@@ -141,14 +194,6 @@ public class MidiSequencerModel extends MidiDeviceModel implements BoundedRangeM
        public int getExtent() { return 0; }
        @Override
        public void setExtent(int newExtent) {}
-       /**
-        * {@link Sequencer#getMicrosecondPosition()} と同じです。
-        * @return マイクロ秒単位での現在の位置
-        */
-       public long getMicrosecondPosition() {
-               long usPosition = getSequencer().getMicrosecondPosition();
-               return usPosition < 0 ? 0x100000000L + usPosition : usPosition ;
-       }
        @Override
        public int getValue() { return (int)(getMicrosecondPosition()/RESOLUTION_MICROSECOND); }
        @Override
@@ -171,8 +216,7 @@ public class MidiSequencerModel extends MidiDeviceModel implements BoundedRangeM
         */
        private javax.swing.Timer timeRangeUpdater = new javax.swing.Timer(20, e->{
                if( valueIsAdjusting || ! getSequencer().isRunning() ) {
-                       // 手動で移動中の場合や、シーケンサが止まっている場合は、
-                       // タイマーによる更新は不要
+                       // 手動で移動中の場合や、シーケンサが止まっている場合は、タイマーによる更新は不要
                        return;
                }
                // リスナーに読み込みを促す
@@ -223,35 +267,31 @@ public class MidiSequencerModel extends MidiDeviceModel implements BoundedRangeM
        /**
         * MIDIトラックリストテーブルモデル
         */
-       private SequenceTrackListTableModel sequenceTableModel = null;
+       private SequenceTrackListTableModel sequenceTrackListTableModel = null;
        /**
         * このシーケンサーに現在ロードされているシーケンスのMIDIトラックリストテーブルモデルを返します。
         * @return MIDIトラックリストテーブルモデル(何もロードされていなければnull)
         */
        public SequenceTrackListTableModel getSequenceTrackListTableModel() {
-               return sequenceTableModel;
+               return sequenceTrackListTableModel;
        }
        /**
         * MIDIトラックリストテーブルモデルをこのシーケンサーモデルにセットします。
         * nullを指定してアンセットすることもできます。
         * @param sequenceTableModel MIDIトラックリストテーブルモデル
         * @throws InvalidMidiDataException {@link Sequencer#setSequence(Sequence)} を参照
+        * @throws IllegalStateException MIDIシーケンサデバイスが閉じている状態で引数にnullを指定した場合
         */
        public void setSequenceTrackListTableModel(SequenceTrackListTableModel sequenceTableModel)
                throws InvalidMidiDataException
        {
-               // javax.sound.midi:Sequencer.setSequence() のドキュメントにある
-               // 「このメソッドは、Sequencer が閉じている場合でも呼び出すことができます。 」
-               // という記述は、null をセットする場合には当てはまらない。
-               // 連鎖的に stop() が呼ばれるために IllegalStateException sequencer not open が出る。
-               // この現象を回避するため、あらかじめチェックしてから setSequence() を呼び出している。
-               //
-               if( sequenceTableModel != null || getSequencer().isOpen() ) {
-                       getSequencer().setSequence(sequenceTableModel == null ? null : sequenceTableModel.getSequence());
-               }
-               if( this.sequenceTableModel != null ) this.sequenceTableModel.fireTableDataChanged();
+               Sequencer sequencer = getSequencer();
+               Sequence sequence = sequenceTableModel == null ? null : sequenceTableModel.getSequence();
+               sequencer.setSequence(sequence);
+               startStopAction.updateEnableStatus();
+               if( this.sequenceTrackListTableModel != null ) this.sequenceTrackListTableModel.fireTableDataChanged();
                if( sequenceTableModel != null ) sequenceTableModel.fireTableDataChanged();
-               this.sequenceTableModel = sequenceTableModel;
+               this.sequenceTrackListTableModel = sequenceTableModel;
                fireStateChanged();
        }
        /**
@@ -259,8 +299,8 @@ public class MidiSequencerModel extends MidiDeviceModel implements BoundedRangeM
         * @param measureOffset 何小節進めるか(戻したいときは負数を指定)
         */
        private void moveMeasure(int measureOffset) {
-               if( measureOffset == 0 || sequenceTableModel == null ) return;
-               SequenceTickIndex seqIndex = sequenceTableModel.getSequenceTickIndex();
+               if( measureOffset == 0 || sequenceTrackListTableModel == null ) return;
+               SequenceTickIndex seqIndex = sequenceTrackListTableModel.getSequenceTickIndex();
                Sequencer sequencer = getSequencer();
                int measurePosition = seqIndex.tickToMeasure(sequencer.getTickPosition());
                long newTickPosition = seqIndex.measureToTick(measurePosition + measureOffset);
index 0da6495..c5d15c4 100644 (file)
@@ -1,8 +1,8 @@
 package camidion.chordhelper.mididevice;
 
-import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Set;
+import java.util.stream.Collectors;
 
 import javax.sound.midi.MidiDevice;
 import javax.sound.midi.MidiUnavailableException;
@@ -29,21 +29,18 @@ public class ReceiverListModel extends AbstractTransceiverListModel<Receiver> {
         * このリストモデルの{@link Receiver}に接続された{@link Transmitter}を全て閉じ、
         * 接続相手だったMIDIデバイスモデルのユニークな集合を返します。
         *
-        * @return é\96\89ã\81\98ã\81\9f{@link Transmitter}ã\81®{@link MidiDeviceModel}の集合
+        * @return é\96\89ã\81\98ã\81\9f{@link Transmitter}ã\82\92æ\8c\81ã\81£ã\81¦ã\81\84ã\81\9f{@link MidiDeviceModel}の集合
         */
-       public Set<MidiDeviceModel> closeTransmitters() {
-               Set<MidiDeviceModel> peerDeviceModelSet = new LinkedHashSet<>();
-               List<Receiver> rxList = getTransceivers();
-               if( ! rxList.isEmpty() ) {
-                       for( MidiDeviceModel peerDeviceModel : deviceModel.getDeviceTreeModel() ) {
-                               if( peerDeviceModel == deviceModel ) continue;
-                               TransmitterListModel txListModel = peerDeviceModel.getTransmitterListModel();
-                               if( txListModel == null ) continue;
-                               for( Receiver rx : rxList )
-                                       if( ! txListModel.closeTransmittersFor(rx).isEmpty() )
-                                               peerDeviceModelSet.add(peerDeviceModel);
-                       }
-               }
-               return peerDeviceModelSet;
+       public Set<MidiDeviceModel> closeAllConnectedTransmitters() {
+               List<MidiDeviceModel> allDeviceModels = deviceModel.getDeviceTreeModel();
+               return allDeviceModels.stream().filter(
+                       peer -> peer != deviceModel
+                       && peer.getTransmitterListModel() != null
+                       && ! getTransceivers().stream()
+                               .map(rx -> peer.getTransmitterListModel().closeTransmittersFor(rx))
+                               .flatMap(closedTxList -> closedTxList.stream())
+                               .collect(Collectors.toSet())
+                               .isEmpty()
+               ).collect(Collectors.toSet());
        }
 }
index f665c7e..011dfd3 100644 (file)
@@ -91,6 +91,7 @@ public class MidiSequenceEditorDialog extends JDialog {
                        if( isVisible() ) toFront(); else setVisible(true);
                }
        };
+       private Action midiDeviceDialogOpenAction;
 
        /**
         * エラーメッセージダイアログを表示します。
@@ -149,24 +150,28 @@ public class MidiSequenceEditorDialog extends JDialog {
                                if( firstIndex < 0 ) firstIndex = lastIndex;
                        } catch(IOException|InvalidMidiDataException e) {
                                String message = "Could not open as MIDI file "+file+"\n"+e;
-                               if( ! itr.hasNext() ) { showWarning(message); break; }
+                               if( ! itr.hasNext() ) { // No more file to play
+                                       showWarning(message); break;
+                               }
                                if( ! confirm(message + "\n\nContinue to open next file ?") ) break;
                        } catch(AccessControlException e) {
                                showError(e); break;
+                       } catch(Exception e) {
+                               showError(e); break;
                        }
                }
-               MidiSequencerModel sequencerModel = playlist.getSequencerModel();
-               if( sequencerModel.getSequencer().isRunning() ) {
-                       String command = (String)openAction.getValue(Action.NAME);
-                       openAction.actionPerformed(new ActionEvent(this, ActionEvent.ACTION_PERFORMED, command));
-                       return;
-               }
-               if( firstIndex >= 0 ) {
-                       try {
+               try {
+                       MidiSequencerModel sequencerModel = playlist.getSequencerModel();
+                       if( sequencerModel.getSequencer().isRunning() ) {
+                               String command = (String)openAction.getValue(Action.NAME);
+                               openAction.actionPerformed(new ActionEvent(this, ActionEvent.ACTION_PERFORMED, command));
+                               return;
+                       }
+                       if( firstIndex >= 0 ) {
                                playlist.loadToSequencer(firstIndex);
-                       } catch (InvalidMidiDataException e) { showError(e); return; }
-                       sequencerModel.start();
-               }
+                               sequencerModel.start();
+                       }
+               } catch (Exception e) { showError(e); }
        }
 
        private static final Insets ZERO_INSETS = new Insets(0,0,0,0);
@@ -274,7 +279,7 @@ public class MidiSequenceEditorDialog extends JDialog {
                        //
                        // タイトルに合計シーケンス長を表示
                        if( lengthColumn != null ) {
-                               int sec = getModel().getTotalTimeInSeconds();
+                               int sec = getModel().getSecondLength();
                                String title = PlaylistTableModel.Column.LENGTH.title;
                                title = String.format(title+" [%02d:%02d]", sec/60, sec%60);
                                lengthColumn.setHeaderValue(title);
@@ -286,25 +291,20 @@ public class MidiSequenceEditorDialog extends JDialog {
                        JTableHeader th = getTableHeader();
                        if( th != null ) th.repaint();
                }
-               /**
-                * 時間位置表示セルエディタ(ダブルクリック専用)
-                */
+               /** 時間位置を表示し、ダブルクリックによるシーケンサへのロードのみを受け付けるセルエディタ */
                private class PositionCellEditor extends AbstractCellEditor implements TableCellEditor {
                        public PositionCellEditor() {
-                               int column = PlaylistTableModel.Column.POSITION.ordinal();
-                               TableColumn tc = getColumnModel().getColumn(column);
+                               TableColumn tc = getColumnModel().getColumn(PlaylistTableModel.Column.POSITION.ordinal());
                                tc.setCellEditor(this);
                        }
                        /**
                         * セルをダブルクリックしたときだけ編集モードに入るようにします。
                         * @param e イベント(マウスイベント)
-                        * @return ç·¨é\9b\86å\8f¯è\83½ã\81«ã\81ªã\81£ã\81\9fã\82\89true
+                        * @return ç·¨é\9b\86å\8f¯è\83½ã\81ªå ´å\90\88true
                         */
                        @Override
                        public boolean isCellEditable(EventObject e) {
-                               // マウスイベント以外のイベントでは編集不可
-                               if( ! (e instanceof MouseEvent) ) return false;
-                               return ((MouseEvent)e).getClickCount() == 2;
+                               return (e instanceof MouseEvent) && ((MouseEvent)e).getClickCount() == 2;
                        }
                        @Override
                        public Object getCellEditorValue() { return null; }
@@ -320,88 +320,104 @@ public class MidiSequenceEditorDialog extends JDialog {
                        ) {
                                try {
                                        getModel().loadToSequencer(row);
-                               } catch (InvalidMidiDataException ex) { showError(ex); }
+                               } catch (InvalidMidiDataException|IllegalStateException ex) {
+                                       showError(ex);
+                               }
                                fireEditingStopped();
                                return null;
                        }
                }
-               /**
-                * プレイボタンを埋め込んだセルエディタ
-                */
-               private class PlayButtonCellEditor extends AbstractCellEditor
-                       implements TableCellEditor, TableCellRenderer
-               {
-                       private JToggleButton playButton = new JToggleButton(
-                               getModel().getSequencerModel().getStartStopAction()
-                       ) {
+               /** 再生ボタンを埋め込んだセルの編集、描画を行うクラスです。 */
+               private class PlayButtonCellEditor extends AbstractCellEditor implements TableCellEditor, TableCellRenderer {
+                       /** 埋め込み用の再生ボタン */
+                       private JToggleButton playButton = new JToggleButton(getModel().getSequencerModel().getStartStopAction()) {
                                { setMargin(ZERO_INSETS); }
                        };
+                       /**
+                        * 埋め込み用のMIDIデバイス接続ボタン(そのシーケンスをロードしているシーケンサが開いていなかったときに表示)
+                        */
+                       private JButton midiDeviceConnectionButton = new JButton(midiDeviceDialogOpenAction) {
+                               { setMargin(ZERO_INSETS); }
+                       };
+                       /**
+                        * 再生ボタンを埋め込むセルエディタを構築し、列に対するレンダラ、エディタとして登録します。
+                        */
                        public PlayButtonCellEditor() {
-                               int column = PlaylistTableModel.Column.PLAY.ordinal();
-                               TableColumn tc = getColumnModel().getColumn(column);
+                               TableColumn tc = getColumnModel().getColumn(PlaylistTableModel.Column.PLAY.ordinal());
                                tc.setCellRenderer(this);
                                tc.setCellEditor(this);
                        }
                        /**
                         * {@inheritDoc}
                         *
-                        * <p>この実装では、クリックしたセルのシーケンスが
-                        * シーケンサーにロードされている場合に
-                        * trueを返してプレイボタンを押せるようにします。
-                        * そうでない場合はプレイボタンのないセルなので、
+                        * <p>この実装では、クリックしたセルのシーケンスがシーケンサーで再生可能な場合に
+                        * trueを返して再生ボタンを押せるようにします。
+                        * それ以外のセルについては、新たにシーケンサーへのロードを可能にするため、
                         * ダブルクリックされたときだけtrueを返します。
                         * </p>
                         */
                        @Override
                        public boolean isCellEditable(EventObject e) {
-                               // マウスイベント以外はデフォルトメソッドにお任せ
+                               // マウスイベントのみを受け付け、それ以外はデフォルトエディタに振る
                                if( ! (e instanceof MouseEvent) ) return super.isCellEditable(e);
+                               //
+                               // エディタが編集を終了したことをリスナーに通知
                                fireEditingStopped();
-                               MouseEvent me = (MouseEvent)e;
                                //
-                               // クリックされたセルの行を特定
+                               // クリックされたセルの行位置を把握(欄外だったら編集不可)
+                               MouseEvent me = (MouseEvent)e;
                                int row = rowAtPoint(me.getPoint());
                                if( row < 0 ) return false;
-                               PlaylistTableModel model = getModel();
-                               if( row >= model.getRowCount() ) return false;
                                //
-                               // セル内にプレイボタンがあれば、シングルクリックを受け付ける。
-                               // プレイボタンのないセルは、ダブルクリックのみ受け付ける。
-                               return model.getSequenceModelList().get(row).isOnSequencer() || me.getClickCount() == 2;
+                               // シーケンサーにロード済みの場合は、シングルクリックを受け付ける。
+                               // それ以外は、ダブルクリックのみ受け付ける。
+                               PlaylistTableModel model = getModel();
+                               boolean isOnSequencer = model.getSequenceModelList().get(row).isOnSequencer();
+                               return isOnSequencer || me.getClickCount() == 2;
                        }
                        @Override
                        public Object getCellEditorValue() { return null; }
                        /**
                         * {@inheritDoc}
                         *
-                        * <p>この実装では、行の表すシーケンスがシーケンサーにロードされている場合にプレイボタンを返します。
-                        * そうでない場合は、そのシーケンスをシーケンサーにロードしてnullを返します。
+                        * <p>この実装では、行の表すシーケンスの状態に応じたボタンを表示します。
+                        * それ以外の場合は、新たにそのシーケンスをシーケンサーにロードしますが、
+                        * 以降の編集は不可としてnullを返します。
                         * </p>
                         */
                        @Override
                        public Component getTableCellEditorComponent(
-                               JTable table, Object value, boolean isSelected, int row, int column
+                               JTable table, Object value, boolean isSelected,
+                               int row, int column
                        ) {
                                fireEditingStopped();
                                PlaylistTableModel model = getModel();
-                               if( model.getSequenceModelList().get(row).isOnSequencer() ) return playButton;
+                               if( model.getSequenceModelList().get(row).isOnSequencer() ) {
+                                       return model.getSequencerModel().getSequencer().isOpen() ? playButton : midiDeviceConnectionButton;
+                               }
                                try {
                                        model.loadToSequencer(row);
                                } catch (InvalidMidiDataException ex) { showError(ex); }
                                return null;
                        }
+                       /**
+                        * {@inheritDoc}
+                        *
+                        * <p>この実装では、行の表すシーケンスの状態に応じたボタンを表示します。
+                        * それ以外の場合はデフォルトレンダラーに描画させます。
+                        * </p>
+                        */
                        @Override
                        public Component getTableCellRendererComponent(
                                JTable table, Object value, boolean isSelected,
                                boolean hasFocus, int row, int column
                        ) {
                                PlaylistTableModel model = getModel();
-                               if(model.getSequenceModelList().get(row).isOnSequencer()) return playButton;
-                               Class<?> cc = model.getColumnClass(column);
-                               TableCellRenderer defaultRenderer = table.getDefaultRenderer(cc);
-                               return defaultRenderer.getTableCellRendererComponent(
-                                       table, value, isSelected, hasFocus, row, column
-                               );
+                               if( model.getSequenceModelList().get(row).isOnSequencer() ) {
+                                       return model.getSequencerModel().getSequencer().isOpen() ? playButton : midiDeviceConnectionButton;
+                               }
+                               TableCellRenderer defaultRenderer = table.getDefaultRenderer(model.getColumnClass(column));
+                               return defaultRenderer.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
                        }
                }
                /**
@@ -424,7 +440,8 @@ public class MidiSequenceEditorDialog extends JDialog {
                        public void actionPerformed(ActionEvent event) {
                                PlaylistTableModel model = getModel();
                                if( midiFileChooser != null ) {
-                                       if( model.getSelectedSequenceModel().isModified() ) {
+                                       SequenceTrackListTableModel seqModel = model.getSelectedSequenceModel();
+                                       if( seqModel != null && seqModel.isModified() ) {
                                                String message =
                                                        "Selected MIDI sequence not saved - delete it ?\n" +
                                                        "選択したMIDIシーケンスはまだ保存されていません。削除しますか?";
@@ -433,7 +450,7 @@ public class MidiSequenceEditorDialog extends JDialog {
                                }
                                try {
                                        model.removeSelectedSequence();
-                               } catch (InvalidMidiDataException ex) {
+                               } catch (InvalidMidiDataException|IllegalStateException ex) {
                                        showError(ex);
                                }
                        }
@@ -456,6 +473,7 @@ public class MidiSequenceEditorDialog extends JDialog {
                                public void actionPerformed(ActionEvent event) {
                                        PlaylistTableModel playlistModel = getModel();
                                        SequenceTrackListTableModel sequenceModel = playlistModel.getSelectedSequenceModel();
+                                       if( sequenceModel == null ) return;
                                        String fn = sequenceModel.getFilename();
                                        if( fn != null && ! fn.isEmpty() ) setSelectedFile(new File(fn));
                                        if( showSaveDialog((Component)event.getSource()) != JFileChooser.APPROVE_OPTION ) return;
@@ -469,7 +487,7 @@ public class MidiSequenceEditorDialog extends JDialog {
                                                sequenceModel.setModified(false);
                                                playlistModel.fireSequenceModified(sequenceModel, false);
                                        }
-                                       catch( IOException ex ) { showError(ex); }
+                                       catch( Exception ex ) { showError(ex); }
                                }
                        };
                        /**
@@ -479,8 +497,10 @@ public class MidiSequenceEditorDialog extends JDialog {
                                { putValue(Action.SHORT_DESCRIPTION, "Open MIDI file - MIDIファイルを開く"); }
                                @Override
                                public void actionPerformed(ActionEvent event) {
-                                       if( showOpenDialog((Component)event.getSource()) != JFileChooser.APPROVE_OPTION ) return;
-                                       play(Arrays.asList(getSelectedFile()));
+                                       try {
+                                               if( showOpenDialog((Component)event.getSource()) != JFileChooser.APPROVE_OPTION ) return;
+                                               play(Arrays.asList(getSelectedFile()));
+                                       } catch( Exception ex ) { showError(ex); }
                                }
                        };
                };
@@ -1101,9 +1121,11 @@ public class MidiSequenceEditorDialog extends JDialog {
         * 新しい {@link MidiSequenceEditorDialog} を構築します。
         * @param playlistTableModel このエディタが参照するプレイリストモデル
         * @param outputMidiDevice イベントテーブルの操作音出力先MIDIデバイス
+        * @param midiDeviceDialogOpenAction MIDIデバイスダイアログを開くアクション
         */
-       public MidiSequenceEditorDialog(PlaylistTableModel playlistTableModel, VirtualMidiDevice outputMidiDevice) {
+       public MidiSequenceEditorDialog(PlaylistTableModel playlistTableModel, VirtualMidiDevice outputMidiDevice, Action midiDeviceDialogOpenAction) {
                this.outputMidiDevice = outputMidiDevice;
+               this.midiDeviceDialogOpenAction = midiDeviceDialogOpenAction;
                sequenceListTable = new SequenceListTable(playlistTableModel);
                trackListTable = new TrackListTable(
                        new SequenceTrackListTableModel(playlistTableModel, null, null)
index a99d19b..7ff2f03 100644 (file)
@@ -17,7 +17,6 @@ import java.awt.event.MouseListener;
 import java.util.ArrayList;
 import java.util.Vector;
 
-import javax.sound.midi.InvalidMidiDataException;
 import javax.sound.midi.MidiChannel;
 import javax.sound.midi.Sequence;
 import javax.swing.AbstractAction;
@@ -107,10 +106,9 @@ public class NewSequenceDialog extends JDialog {
                public void actionPerformed(ActionEvent event) {
                        try {
                                playlist.getSequenceModelList().get(playlist.play(getMidiSequence())).setModified(true);
-                       } catch (InvalidMidiDataException ex) {
-                               ex.printStackTrace();
+                       } catch (Exception ex) {
                                JOptionPane.showMessageDialog(
-                                       NewSequenceDialog.this, ex.getMessage(),
+                                       NewSequenceDialog.this, ex,
                                        ChordHelperApplet.VersionInfo.NAME, JOptionPane.ERROR_MESSAGE);
                        }
                        setVisible(false);
index fb495fe..6f2e411 100644 (file)
@@ -38,11 +38,11 @@ public class PlaylistTableModel extends AbstractTableModel {
        /**
         * 空のトラックリストモデル
         */
-       SequenceTrackListTableModel emptyTrackListTableModel;
+       SequenceTrackListTableModel emptyTrackListTableModel = new SequenceTrackListTableModel(this, null, null);
        /**
         * 空のイベントリストモデル
         */
-       TrackEventListTableModel emptyEventListTableModel;
+       TrackEventListTableModel emptyEventListTableModel = new TrackEventListTableModel(emptyTrackListTableModel, null);
        /**
         * 選択されているシーケンスのインデックス
         */
@@ -59,9 +59,10 @@ public class PlaylistTableModel extends AbstractTableModel {
                        Object src = event.getSource();
                        if( src instanceof MidiSequencerModel ) {
                                int newValue = ((MidiSequencerModel)src).getValue() / 1000;
-                               if(value == newValue) return;
-                               value = newValue;
-                               fireTableCellUpdated(indexOfSequenceOnSequencer(), Column.POSITION.ordinal());
+                               if(value != newValue) {
+                                       value = newValue;
+                                       fireTableCellUpdated(indexOfSequenceOnSequencer(), Column.POSITION.ordinal());
+                               }
                        }
                }
                @Override
@@ -76,8 +77,8 @@ public class PlaylistTableModel extends AbstractTableModel {
        public PlaylistTableModel(MidiSequencerModel sequencerModel) {
                this.sequencerModel = sequencerModel;
                sequencerModel.addChangeListener(mmssPosition);
-               // EOF(0x2F)が来たら曲の終わりなので次の曲へ進める
                sequencerModel.getSequencer().addMetaEventListener(msg->{
+                       // EOF(0x2F)が来て曲が終わったら次の曲へ進める
                        if(msg.getType() == 0x2F) SwingUtilities.invokeLater(()->{
                                try {
                                        goNext();
@@ -86,8 +87,6 @@ public class PlaylistTableModel extends AbstractTableModel {
                                }
                        });
                });
-               emptyTrackListTableModel = new SequenceTrackListTableModel(this, null, null);
-               emptyEventListTableModel = new TrackEventListTableModel(emptyTrackListTableModel, null);
        }
        /**
         * 次の曲へ進みます。
@@ -96,6 +95,7 @@ public class PlaylistTableModel extends AbstractTableModel {
         * 次の曲がなければ、そこで停止します。いずれの場合も曲の先頭へ戻ります。
         * </p>
         * @throws InvalidMidiDataException {@link Sequencer#setSequence(Sequence)} を参照
+        * @throws IllegalStateException MIDIシーケンサデバイスが閉じている場合
         */
        private void goNext() throws InvalidMidiDataException {
                // とりあえず曲の先頭へ戻る
@@ -217,15 +217,15 @@ public class PlaylistTableModel extends AbstractTableModel {
                        public boolean isCellEditable() { return true; } // ダブルクリックだけ有効
                        @Override
                        public Object getValueOf(SequenceTrackListTableModel sequenceModel) {
-                               return sequenceModel.isOnSequencer() ? sequenceModel.getParent().mmssPosition : "";
+                               if( ! sequenceModel.isOnSequencer() ) return "";
+                               return sequenceModel.getParent().mmssPosition;
                        }
                },
                /** シーケンスの時間長(分:秒) */
                LENGTH("Length", String.class, 80) {
                        @Override
                        public Object getValueOf(SequenceTrackListTableModel sequenceModel) {
-                               long usec = sequenceModel.getSequence().getMicrosecondLength();
-                               int sec = (int)( (usec < 0 ? usec += 0x100000000L : usec) / 1000L / 1000L );
+                               int sec = (int)( sequenceModel.getMicrosecondLength() / 1000L / 1000L );
                                return String.format( "%02d:%02d", sec/60, sec%60 );
                        }
                },
@@ -288,9 +288,7 @@ public class PlaylistTableModel extends AbstractTableModel {
                                return dtLabel == null ? "[Unknown]" : dtLabel;
                        }
                };
-               /**
-                * タイミング分割形式に対応するラベル文字列
-                */
+               /** タイミング分割形式に対応するラベル文字列 */
                private static final Map<Float,String> divisionTypeLabels = new HashMap<Float,String>() {
                        {
                                put(Sequence.PPQ, "PPQ");
@@ -366,11 +364,9 @@ public class PlaylistTableModel extends AbstractTableModel {
         * このプレイリストに読み込まれた全シーケンスの合計時間長を返します。
         * @return 全シーケンスの合計時間長 [秒]
         */
-       public int getTotalTimeInSeconds() {
-               return sequenceModelList.stream().mapToInt(m->{
-                       long usec = m.getSequence().getMicrosecondLength();
-                       return (int)( (usec < 0 ? usec += 0x100000000L : usec)/1000L/1000L );
-               }).sum();
+       public int getSecondLength() {
+               // マイクロ秒単位での桁あふれを回避しつつ、丸め誤差を最小限にするため、ミリ秒単位で合計を算出する。
+               return (int)(sequenceModelList.stream().mapToLong(m -> m.getMicrosecondLength() / 1000L).sum() / 1000L);
        }
        /**
         * 選択されたMIDIシーケンスのテーブルモデルを返します。
@@ -416,10 +412,10 @@ public class PlaylistTableModel extends AbstractTableModel {
                if( sequence == null ) sequence = (new ChordProgression()).toMidiSequence();
                sequenceModelList.add(new SequenceTrackListTableModel(this, sequence, filename));
                //
-               // æ\9c«å°¾ã\81«è¡\8cã\81\8c追å\8a ã\81\95ã\82\8cã\81\9fã\81®ã\81§ã\80\81å\86\8dæ\8f\8fç\94»ã\81\97ã\81¦ã\82\82ã\82\89ã\81\86
+               // æ\9c«å°¾ã\81«è¿½å\8a ã\81\95ã\82\8cã\81\9fè¡\8cã\82\92é\81¸æ\8a\9eã\81\97ã\80\81å\86\8dæ\8f\8fç\94»
                int lastIndex = sequenceModelList.size() - 1;
-               fireTableRowsInserted(lastIndex, lastIndex);
                sequenceListSelectionModel.setSelectionInterval(lastIndex, lastIndex);
+               fireTableRowsInserted(lastIndex, lastIndex);
                return lastIndex;
        }
        /**
@@ -427,6 +423,7 @@ public class PlaylistTableModel extends AbstractTableModel {
         * @param sequence MIDIシーケンス
         * @return 追加されたシーケンスのインデックス(先頭が 0)
         * @throws InvalidMidiDataException {@link Sequencer#setSequence(Sequence)} を参照
+        * @throws IllegalStateException MIDIシーケンサデバイスが閉じている場合
         */
        public int play(Sequence sequence) throws InvalidMidiDataException {
                int lastIndex = add(sequence,"");
@@ -442,13 +439,16 @@ public class PlaylistTableModel extends AbstractTableModel {
         * 除去されたシーケンスがシーケンサーにロード済みの場合、アンロードします。
         *
         * @throws InvalidMidiDataException {@link Sequencer#setSequence(Sequence)} を参照
+        * @throws IllegalStateException MIDIシーケンサデバイスが閉じている場合
         */
        public void removeSelectedSequence() throws InvalidMidiDataException {
                if( sequenceListSelectionModel.isSelectionEmpty() ) return;
                int selectedIndex = sequenceListSelectionModel.getMinSelectionIndex();
                SequenceTrackListTableModel removedSequence = sequenceModelList.remove(selectedIndex);
-               if( removedSequence.isOnSequencer() ) sequencerModel.setSequenceTrackListTableModel(null);
                fireTableRowsDeleted(selectedIndex, selectedIndex);
+               if( removedSequence.isOnSequencer() ) {
+                       sequencerModel.setSequenceTrackListTableModel(null);
+               }
        }
        /**
         * テーブル内の指定したインデックス位置にあるシーケンスをシーケンサーにロードします。
@@ -457,10 +457,11 @@ public class PlaylistTableModel extends AbstractTableModel {
         *
         * @param newRowIndex ロードするシーケンスのインデックス位置、アンロードするときは -1
         * @throws InvalidMidiDataException {@link Sequencer#setSequence(Sequence)} を参照
+        * @throws IllegalStateException MIDIシーケンサデバイスが閉じているときにアンロードしようとした場合
         */
        public void loadToSequencer(int newRowIndex) throws InvalidMidiDataException {
                SequenceTrackListTableModel oldSeq = sequencerModel.getSequenceTrackListTableModel();
-               SequenceTrackListTableModel newSeq = (newRowIndex < 0 ? null : sequenceModelList.get(newRowIndex));
+               SequenceTrackListTableModel newSeq = (newRowIndex < 0 || sequenceModelList.isEmpty() ? null : sequenceModelList.get(newRowIndex));
                if( oldSeq == newSeq ) return;
                sequencerModel.setSequenceTrackListTableModel(newSeq);
                int columnIndices[] = {
index f9f6fa1..78c74ac 100644 (file)
@@ -13,6 +13,7 @@ import javax.swing.DefaultListSelectionModel;
 import javax.swing.ListSelectionModel;
 import javax.swing.table.AbstractTableModel;
 
+import camidion.chordhelper.mididevice.MidiSequencerModel;
 import camidion.chordhelper.music.MIDISpec;
 
 /**
@@ -215,6 +216,15 @@ public class SequenceTrackListTableModel extends AbstractTableModel {
         */
        public Sequence getSequence() { return sequence; }
        /**
+        * MIDIシーケンスのマイクロ秒単位の長さを返します。
+        * 曲が長すぎて {@link Sequence#getMicrosecondLength()} が負数を返してしまった場合の補正も行います。
+        * @return MIDIシーケンスの長さ[マイクロ秒]
+        */
+       public long getMicrosecondLength() {
+               long usec = sequence.getMicrosecondLength();
+               return usec < 0 ? usec += 0x100000000L : usec;
+       }
+       /**
         * シーケンスtickインデックスを返します。
         * @return シーケンスtickインデックス
         */
@@ -226,7 +236,8 @@ public class SequenceTrackListTableModel extends AbstractTableModel {
        private void setSequence(Sequence sequence) {
                //
                // 旧シーケンスの録音モードを解除
-               sequenceListTableModel.getSequencerModel().getSequencer().recordDisable(null); // The "null" means all tracks
+               MidiSequencerModel sequencerModel = sequenceListTableModel.getSequencerModel();
+               if( sequencerModel != null ) sequencerModel.getSequencer().recordDisable(null);
                //
                // トラックリストをクリア
                int oldSize = trackModelList.size();