From: Akiyoshi Kamide Date: Sun, 26 Mar 2017 16:06:44 +0000 (+0900) Subject: ・シーケンサを閉じてしまった場合の挙動を改善 X-Git-Url: http://git.osdn.net/view?a=commitdiff_plain;h=7382f7045472fe630b16bbef44c4ab745c1078ef;p=midichordhelper%2FMIDIChordHelper.git ・シーケンサを閉じてしまった場合の挙動を改善 ・リファクタリング --- diff --git a/src/camidion/chordhelper/ChordHelperApplet.java b/src/camidion/chordhelper/ChordHelperApplet.java index 93b2eac..74d0521 100644 --- a/src/camidion/chordhelper/ChordHelperApplet.java +++ b/src/camidion/chordhelper/ChordHelperApplet.java @@ -86,10 +86,11 @@ public class ChordHelperApplet extends JApplet { return playlistModel.getSequenceModelList().stream().anyMatch(m -> m.isModified()); } /** - * 指定された小節数の曲を、乱数で自動作曲してプレイリストへ追加します。 + * 指定された小節数の曲を、乱数で自動作曲してプレイリストへ追加し、再生します。 * @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(""+( loadedSequenceIndex < 0 ? "[No MIDI file loaded]" : "MIDI file " + loadedSequenceIndex + ": " + ( - sequenceTableModel == null || - sequenceTableModel.toString().isEmpty() ? + sequenceTrackListTableModel == null || + sequenceTrackListTableModel.toString().isEmpty() ? "[Untitled]" : - ""+sequenceTableModel+"" + ""+sequenceTrackListTableModel+"" ) )+""); 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(); } } diff --git a/src/camidion/chordhelper/mididevice/MidiDeviceModel.java b/src/camidion/chordhelper/mididevice/MidiDeviceModel.java index e0e89a4..a5ed1ba 100644 --- a/src/camidion/chordhelper/mididevice/MidiDeviceModel.java +++ b/src/camidion/chordhelper/mididevice/MidiDeviceModel.java @@ -93,7 +93,7 @@ public class MidiDeviceModel { * それらも全て閉じます。 */ public void close() { - if( rxListModel != null ) rxListModel.closeTransmitters(); + if( rxListModel != null ) rxListModel.closeAllConnectedTransmitters(); device.close(); } } diff --git a/src/camidion/chordhelper/mididevice/MidiDeviceTreeModel.java b/src/camidion/chordhelper/mididevice/MidiDeviceTreeModel.java index ec2729f..aa4dd89 100644 --- a/src/camidion/chordhelper/mididevice/MidiDeviceTreeModel.java +++ b/src/camidion/chordhelper/mididevice/MidiDeviceTreeModel.java @@ -260,8 +260,9 @@ public class MidiDeviceTreeModel extends AbstractList implement deviceModelList.stream().forEach(m -> { ReceiverListModel rxListModel = m.getReceiverListModel(); if( rxListModel == null ) return; - Collection txDeviceModels = rxListModel.closeTransmitters(); - if( ! txDeviceModels.isEmpty() ) rxToTxConnections.put(m, txDeviceModels); + Collection txDeviceModels = rxListModel.closeAllConnectedTransmitters(); + if( txDeviceModels.isEmpty() ) return; + rxToTxConnections.put(m, txDeviceModels); }); return rxToTxConnections; } diff --git a/src/camidion/chordhelper/mididevice/MidiSequencerModel.java b/src/camidion/chordhelper/mididevice/MidiSequencerModel.java index de0050c..75911f5 100644 --- a/src/camidion/chordhelper/mididevice/MidiSequencerModel.java +++ b/src/camidion/chordhelper/mididevice/MidiSequencerModel.java @@ -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シーケンサを開始します。 + * 再生または録音が可能か調べます。 + *

以下の条件が揃ったときに再生または録音が可能と判定されます。

+ *
    + *
  • MIDIシーケンサデバイスが開いている
  • + *
  • MIDIシーケンサに操作対象のMIDIシーケンスが設定されている
  • + *
+ * @return 再生または録音が可能な場合true + */ + public boolean isStartable() { + return device.isOpen() && getSequencer().getSequence() != null; + } + /** + * {@inheritDoc} + *

シーケンサモデルの場合、録音再生可能状態が変わるので、開始終了アクションにも通知します。

+ */ + @Override + public void open() throws MidiUnavailableException { + super.open(); + startStopAction.updateEnableStatus(); + fireStateChanged(); + if( sequenceTrackListTableModel != null ) { + sequenceTrackListTableModel.getParent().fireTableDataChanged(); + } + } + /** + * {@inheritDoc} + *

シーケンサモデルの場合、再生または録音が不可能になるので、開始終了アクションにも通知します。

+ */ + @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(); + } + } + /** + * 再生または録音を開始します。 * *

録音するMIDIチャンネルがMIDIエディタで指定されている場合、 - * 録音スタート時のタイムスタンプが正しく0になるよう、 - * 各MIDIデバイスのタイムスタンプをすべてリセットします。 + * 録音スタート時のタイムスタンプが正しく0になるよう、各MIDIデバイスのタイムスタンプをすべてリセットします。 *

+ *

このシーケンサのMIDIデバイスが閉じている場合、再生や録音は開始されません。

*/ 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); diff --git a/src/camidion/chordhelper/mididevice/ReceiverListModel.java b/src/camidion/chordhelper/mididevice/ReceiverListModel.java index 0da6495..c5d15c4 100644 --- a/src/camidion/chordhelper/mididevice/ReceiverListModel.java +++ b/src/camidion/chordhelper/mididevice/ReceiverListModel.java @@ -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 { * このリストモデルの{@link Receiver}に接続された{@link Transmitter}を全て閉じ、 * 接続相手だったMIDIデバイスモデルのユニークな集合を返します。 * - * @return 閉じた{@link Transmitter}の{@link MidiDeviceModel}の集合 + * @return 閉じた{@link Transmitter}を持っていた{@link MidiDeviceModel}の集合 */ - public Set closeTransmitters() { - Set peerDeviceModelSet = new LinkedHashSet<>(); - List 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 closeAllConnectedTransmitters() { + List 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()); } } diff --git a/src/camidion/chordhelper/midieditor/MidiSequenceEditorDialog.java b/src/camidion/chordhelper/midieditor/MidiSequenceEditorDialog.java index f665c7e..011dfd3 100644 --- a/src/camidion/chordhelper/midieditor/MidiSequenceEditorDialog.java +++ b/src/camidion/chordhelper/midieditor/MidiSequenceEditorDialog.java @@ -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 編集可能になったらtrue + * @return 編集可能な場合true */ @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} * - *

この実装では、クリックしたセルのシーケンスが - * シーケンサーにロードされている場合に - * trueを返してプレイボタンを押せるようにします。 - * そうでない場合はプレイボタンのないセルなので、 + *

この実装では、クリックしたセルのシーケンスがシーケンサーで再生可能な場合に + * trueを返して再生ボタンを押せるようにします。 + * それ以外のセルについては、新たにシーケンサーへのロードを可能にするため、 * ダブルクリックされたときだけtrueを返します。 *

*/ @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} * - *

この実装では、行の表すシーケンスがシーケンサーにロードされている場合にプレイボタンを返します。 - * そうでない場合は、そのシーケンスをシーケンサーにロードしてnullを返します。 + *

この実装では、行の表すシーケンスの状態に応じたボタンを表示します。 + * それ以外の場合は、新たにそのシーケンスをシーケンサーにロードしますが、 + * 以降の編集は不可としてnullを返します。 *

*/ @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} + * + *

この実装では、行の表すシーケンスの状態に応じたボタンを表示します。 + * それ以外の場合はデフォルトレンダラーに描画させます。 + *

+ */ @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) diff --git a/src/camidion/chordhelper/midieditor/NewSequenceDialog.java b/src/camidion/chordhelper/midieditor/NewSequenceDialog.java index a99d19b..7ff2f03 100644 --- a/src/camidion/chordhelper/midieditor/NewSequenceDialog.java +++ b/src/camidion/chordhelper/midieditor/NewSequenceDialog.java @@ -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); diff --git a/src/camidion/chordhelper/midieditor/PlaylistTableModel.java b/src/camidion/chordhelper/midieditor/PlaylistTableModel.java index fb495fe..6f2e411 100644 --- a/src/camidion/chordhelper/midieditor/PlaylistTableModel.java +++ b/src/camidion/chordhelper/midieditor/PlaylistTableModel.java @@ -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 { * 次の曲がなければ、そこで停止します。いずれの場合も曲の先頭へ戻ります。 *

* @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 divisionTypeLabels = new HashMap() { { 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)); // - // 末尾に行が追加されたので、再描画してもらう + // 末尾に追加された行を選択し、再描画 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[] = { diff --git a/src/camidion/chordhelper/midieditor/SequenceTrackListTableModel.java b/src/camidion/chordhelper/midieditor/SequenceTrackListTableModel.java index f9f6fa1..78c74ac 100644 --- a/src/camidion/chordhelper/midieditor/SequenceTrackListTableModel.java +++ b/src/camidion/chordhelper/midieditor/SequenceTrackListTableModel.java @@ -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();