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;
* プレイリスト上で現在選択されているMIDIシーケンスを、
* シーケンサへロードして再生します。
* @throws InvalidMidiDataException {@link Sequencer#setSequence(Sequence)} を参照
+ * @throws IllegalStateException MIDIシーケンサデバイスが閉じている場合
*/
public void play() throws InvalidMidiDataException {
play(playlistModel.sequenceListSelectionModel.getMinSelectionIndex());
* シーケンサへロードして再生します。
* @param index インデックス値(0から始まる)
* @throws InvalidMidiDataException {@link Sequencer#setSequence(Sequence)} を参照
+ * @throws IllegalStateException MIDIシーケンサデバイスが閉じている場合
*/
public void play(int index) throws InvalidMidiDataException {
playlistModel.loadToSequencer(index); sequencerModel.start();
*/
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/";
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);
//シーケンサーの時間スライダーの値が変わったときのリスナーを登録
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);
addToPlaylist(midiUrl);
try {
play();
- } catch (InvalidMidiDataException ex) {
+ } catch (Exception ex) {
ex.printStackTrace();
}
}
* それらも全て閉じます。
*/
public void close() {
- if( rxListModel != null ) rxListModel.closeTransmitters();
+ if( rxListModel != null ) rxListModel.closeAllConnectedTransmitters();
device.close();
}
}
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;
}
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;
addChangeListener(e->getSequencer().setTempoFactor(SequencerSpeedSlider.tempoFactorOf(getValue())));
}};
/**
- * MIDIシーケンサを返します。
- * @return MIDIシーケンサ
+ * 対象MIDIシーケンサデバイス({@link #getMidiDevice()}をキャストした結果)を返します。
+ * @return 対象MIDIシーケンサデバイス
*/
public Sequencer getSequencer() { return (Sequencer)device; }
/**
{
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() {
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
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
*/
private javax.swing.Timer timeRangeUpdater = new javax.swing.Timer(20, e->{
if( valueIsAdjusting || ! getSequencer().isRunning() ) {
- // 手動で移動中の場合や、シーケンサが止まっている場合は、
- // タイマーによる更新は不要
+ // 手動で移動中の場合や、シーケンサが止まっている場合は、タイマーによる更新は不要
return;
}
// リスナーに読み込みを促す
/**
* 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();
}
/**
* @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);
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;
* このリストモデルの{@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());
}
}
if( isVisible() ) toFront(); else setVisible(true);
}
};
+ private Action midiDeviceDialogOpenAction;
/**
* エラーメッセージダイアログを表示します。
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);
//
// タイトルに合計シーケンス長を表示
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);
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; }
) {
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);
}
}
/**
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シーケンスはまだ保存されていません。削除しますか?";
}
try {
model.removeSelectedSequence();
- } catch (InvalidMidiDataException ex) {
+ } catch (InvalidMidiDataException|IllegalStateException ex) {
showError(ex);
}
}
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;
sequenceModel.setModified(false);
playlistModel.fireSequenceModified(sequenceModel, false);
}
- catch( IOException ex ) { showError(ex); }
+ catch( Exception ex ) { showError(ex); }
}
};
/**
{ 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); }
}
};
};
* 新しい {@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)
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;
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);
/**
* 空のトラックリストモデル
*/
- SequenceTrackListTableModel emptyTrackListTableModel;
+ SequenceTrackListTableModel emptyTrackListTableModel = new SequenceTrackListTableModel(this, null, null);
/**
* 空のイベントリストモデル
*/
- TrackEventListTableModel emptyEventListTableModel;
+ TrackEventListTableModel emptyEventListTableModel = new TrackEventListTableModel(emptyTrackListTableModel, null);
/**
* 選択されているシーケンスのインデックス
*/
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
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();
}
});
});
- emptyTrackListTableModel = new SequenceTrackListTableModel(this, null, null);
- emptyEventListTableModel = new TrackEventListTableModel(emptyTrackListTableModel, null);
}
/**
* 次の曲へ進みます。
* 次の曲がなければ、そこで停止します。いずれの場合も曲の先頭へ戻ります。
* </p>
* @throws InvalidMidiDataException {@link Sequencer#setSequence(Sequence)} を参照
+ * @throws IllegalStateException MIDIシーケンサデバイスが閉じている場合
*/
private void goNext() throws InvalidMidiDataException {
// とりあえず曲の先頭へ戻る
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 );
}
},
return dtLabel == null ? "[Unknown]" : dtLabel;
}
};
- /**
- * タイミング分割形式に対応するラベル文字列
- */
+ /** タイミング分割形式に対応するラベル文字列 */
private static final Map<Float,String> divisionTypeLabels = new HashMap<Float,String>() {
{
put(Sequence.PPQ, "PPQ");
* このプレイリストに読み込まれた全シーケンスの合計時間長を返します。
* @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シーケンスのテーブルモデルを返します。
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;
}
/**
* @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,"");
* 除去されたシーケンスがシーケンサーにロード済みの場合、アンロードします。
*
* @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);
+ }
}
/**
* テーブル内の指定したインデックス位置にあるシーケンスをシーケンサーにロードします。
*
* @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[] = {
import javax.swing.ListSelectionModel;
import javax.swing.table.AbstractTableModel;
+import camidion.chordhelper.mididevice.MidiSequencerModel;
import camidion.chordhelper.music.MIDISpec;
/**
*/
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インデックス
*/
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();