From 411f3d37a693cd07fb0cdfacd83d21c506461ffc Mon Sep 17 00:00:00 2001 From: Akiyoshi Kamide Date: Sun, 9 Apr 2017 01:23:45 +0900 Subject: [PATCH] =?utf8?q?=E3=83=AA=E3=83=95=E3=82=A1=E3=82=AF=E3=82=BF?= =?utf8?q?=E3=83=AA=E3=83=B3=E3=82=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit --- src/camidion/chordhelper/ChordHelperApplet.java | 14 +- .../mididevice/MidiDeviceDesktopPane.java | 127 ++++----- .../chordhelper/mididevice/MidiDeviceDialog.java | 8 +- .../mididevice/MidiDeviceInOutType.java | 6 + .../chordhelper/mididevice/MidiDeviceInfoPane.java | 11 +- .../chordhelper/mididevice/MidiDeviceModel.java | 45 ++-- .../mididevice/MidiDeviceTreeModel.java | 299 ++++++++++----------- .../chordhelper/mididevice/MidiDeviceTreeView.java | 14 +- .../midieditor/MidiSequenceEditorDialog.java | 4 +- .../chordhelper/pianokeyboard/PianoKeyboard.java | 33 +-- 10 files changed, 279 insertions(+), 282 deletions(-) diff --git a/src/camidion/chordhelper/ChordHelperApplet.java b/src/camidion/chordhelper/ChordHelperApplet.java index a4272bc..8bcc915 100644 --- a/src/camidion/chordhelper/ChordHelperApplet.java +++ b/src/camidion/chordhelper/ChordHelperApplet.java @@ -272,7 +272,7 @@ public class ChordHelperApplet extends JApplet { */ public static class VersionInfo { public static final String NAME = "MIDI Chord Helper"; - public static final String VERSION = "Ver.20170401.1"; + public static final String VERSION = "Ver.20170408.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/"; @@ -366,19 +366,13 @@ public class ChordHelperApplet extends JApplet { )); keyboardCenterPanel.keyboard.setPreferredSize(new Dimension(571, 80)); }}; + // MIDIデバイスとMIDIエディタのセットアップ VirtualMidiDevice guiMidiDevice = keyboardPanel.keyboardCenterPanel.keyboard.midiDevice; - // - // MIDIデバイスツリーモデルを構築 deviceTreeModel = new MidiDeviceTreeModel(guiMidiDevice); - // - // MIDIシーケンサと連携するプレイリストモデルを構築 - playlistModel = new PlaylistTableModel(sequencerModel = deviceTreeModel.getSequencerModel()); - // - // MIDIデバイスダイアログの構築 + sequencerModel = deviceTreeModel.getSequencerModel(); + playlistModel = new PlaylistTableModel(sequencerModel); MidiDeviceDialog midiDeviceDialog = new MidiDeviceDialog(deviceTreeModel); midiDeviceDialog.setIconImage(iconImage); - // - // MIDIエディタダイアログの構築 midiEditor = new MidiSequenceEditorDialog(playlistModel, guiMidiDevice, midiDeviceDialog.getOpenAction()); midiEditor.setIconImage(iconImage); // diff --git a/src/camidion/chordhelper/mididevice/MidiDeviceDesktopPane.java b/src/camidion/chordhelper/mididevice/MidiDeviceDesktopPane.java index 2f0ad41..de97d1e 100644 --- a/src/camidion/chordhelper/mididevice/MidiDeviceDesktopPane.java +++ b/src/camidion/chordhelper/mididevice/MidiDeviceDesktopPane.java @@ -7,7 +7,9 @@ import java.beans.PropertyVetoException; import java.util.HashMap; import java.util.Map; import java.util.Objects; +import java.util.Set; import java.util.stream.Collectors; +import java.util.stream.Stream; import javax.sound.midi.MidiUnavailableException; import javax.swing.JDesktopPane; @@ -17,8 +19,6 @@ import javax.swing.JOptionPane; import javax.swing.TransferHandler; import javax.swing.event.TreeModelEvent; import javax.swing.event.TreeModelListener; -import javax.swing.event.TreeSelectionEvent; -import javax.swing.event.TreeSelectionListener; import javax.swing.tree.TreePath; import camidion.chordhelper.ChordHelperApplet; @@ -26,38 +26,29 @@ import camidion.chordhelper.ChordHelperApplet; /** * 開いているMIDIデバイスを置くためのデスクトップビュー */ -public class MidiDeviceDesktopPane extends JDesktopPane implements TreeSelectionListener { +public class MidiDeviceDesktopPane extends JDesktopPane { /** * MIDIデバイスモデルからフレームを割り出すためのマップ */ private Map frameOfModel = new HashMap<>(); /** - * MIDIデバイスツリービューで選択状態が変わったとき、 - * このデスクトップで表示しているフレームの選択状態に反映します。 + * 指定されたMIDIデバイスモデルを表示しているフレームを選択します。 + * nullを指定するとフレームの選択を解除します。 + * @param deviceModel 対象のMIDIデバイスモデル */ - @Override - public void valueChanged(TreeSelectionEvent tse) { - TreePath selectedTreePath = tse.getNewLeadSelectionPath(); - if( selectedTreePath != null ) { - Object selectedLeaf = selectedTreePath.getLastPathComponent(); - if( selectedLeaf instanceof MidiDeviceModel ) { - MidiDeviceModel selectedModel = (MidiDeviceModel)selectedLeaf; - if( selectedModel.getMidiDevice().isOpen() ) { - // 開いているMIDIデバイスがツリー上で選択されたら、対応するフレームを選択 - MidiDeviceFrame deviceFrame = frameOfModel.get(selectedModel); - if( deviceFrame != null ) { - deviceFrame.toFront(); - try { - deviceFrame.setSelected(true); - } catch( PropertyVetoException ex ) { - ex.printStackTrace(); - } - return; - } + public void setSelectedMidiDeviceModel(MidiDeviceModel deviceModel) { + if( deviceModel != null ) { + MidiDeviceFrame deviceFrame = frameOfModel.get(deviceModel); + if( deviceFrame != null ) { + deviceFrame.toFront(); + try { + deviceFrame.setSelected(true); + } catch( PropertyVetoException ex ) { + ex.printStackTrace(); } + return; } } - // それ以外が選択されたら、現在選択されているフレームを非選択 JInternalFrame frame = getSelectedFrame(); if( frame instanceof MidiDeviceFrame ) try { ((MidiDeviceFrame)frame).setSelected(false); @@ -65,24 +56,37 @@ public class MidiDeviceDesktopPane extends JDesktopPane implements TreeSelection ex.printStackTrace(); } } + /** + * 指定されたツリーパスが、オープンされているMIDIデバイスモデルの場合、それを表示しているフレームを選択します。 + * それ以外の場合、フレームの選択を解除します。 + * @param treePath 対象ツリーパス + */ + public void setTreePath(TreePath treePath) { + if( treePath != null ) { + Object leaf = treePath.getLastPathComponent(); + if( leaf instanceof MidiDeviceModel && ((MidiDeviceModel)leaf).getMidiDevice().isOpen() ) { + setSelectedMidiDeviceModel((MidiDeviceModel)leaf); + return; + } + } + setSelectedMidiDeviceModel(null); + } public MidiDeviceDesktopPane(MidiDeviceTreeView deviceTreeView, - MidiDeviceInfoPane deviceInfoPane, MidiDeviceDialog dialog) + MidiDeviceInfoPane deviceInfoPane) { MidiCablePane cablePane = new MidiCablePane(this); add(cablePane, JLayeredPane.PALETTE_LAYER); - // - // リサイズ時、表示時にMIDIケーブルを再描画 addComponentListener(new ComponentAdapter() { @Override public void componentResized(ComponentEvent e) { cablePane.setSize(getSize()); - cablePane.repaint(); } @Override - public void componentShown(ComponentEvent e) { cablePane.repaint(); } + public void componentShown(ComponentEvent e) { + cablePane.repaint(); + } }); - // デバイスツリーが変更されたときの更新処理を予約 MidiDeviceTreeModel deviceTreeModel = deviceTreeView.getModel(); TreeModelListener treeModelListener = new TreeModelListener() { @Override @@ -91,43 +95,46 @@ public class MidiDeviceDesktopPane extends JDesktopPane implements TreeSelection public void treeNodesInserted(TreeModelEvent e) { } @Override public void treeNodesRemoved(TreeModelEvent e) { } + /** + * デバイスツリーの変更に応じてフレームの削除や追加を行います。 + * 起動時のフレーム追加だけでなく、 + * USBからMIDIデバイスが着脱された場合のフレームの削除や追加にも使います。 + * @param e デバイスツリーからのツリーモデルイベント + */ @Override public void treeStructureChanged(TreeModelEvent e) { - // ツリーから削除されたMIDIデバイスモデルのフレームを削除 - frameOfModel.keySet().stream() - .filter(m-> ! deviceTreeModel.contains(m)) - .collect(Collectors.toSet()).stream() - .map(m-> frameOfModel.remove(m)).filter(Objects::nonNull) - .forEach(f-> remove(f)); - // - // ツリーに追加されたMIDIデバイスモデルのフレームを生成 + Set removedUsbMidiDevices = + frameOfModel.keySet().stream() + .filter(dm-> ! deviceTreeModel.contains(dm)) + .collect(Collectors.toSet()); + removedUsbMidiDevices.stream() + .map(dm->frameOfModel.remove(dm)) + .filter(Objects::nonNull) + .forEach(f->remove(f)); deviceTreeModel.stream() - .filter(dm -> ! frameOfModel.containsKey(dm)) + .filter(dm-> ! frameOfModel.containsKey(dm)) .forEach(dm->{ - MidiDeviceFrame frame = new MidiDeviceFrame(dm, cablePane); - frameOfModel.put(dm, frame); - // - // トランスミッタリストモデルが変化したときにMIDIケーブルを再描画 - TransmitterListModel txListModel = dm.getTransmitterListModel(); - if( txListModel != null ) txListModel.addListDataListener(cablePane.midiConnecterListDataListener); - // - // レシーバリストモデルが変化したときにMIDIケーブルを再描画 - ReceiverListModel rxListModel = dm.getReceiverListModel(); - if( rxListModel != null ) rxListModel.addListDataListener(cablePane.midiConnecterListDataListener); + MidiDeviceFrame df; + frameOfModel.put(dm, df = new MidiDeviceFrame(dm, cablePane)); // - // デバイスフレームが開閉したときの動作 - frame.addInternalFrameListener(cablePane.midiDeviceFrameListener); - frame.addInternalFrameListener(deviceTreeView.midiDeviceFrameListener); - frame.addInternalFrameListener(deviceInfoPane.midiDeviceFrameListener); + // トランスミッタ、レシーバの接続変更時の描画予約 + Stream.of(dm.getTransmitterListModel(), dm.getReceiverListModel()) + .filter(Objects::nonNull) + .forEach(lm->lm.addListDataListener(cablePane.midiConnecterListDataListener)); // - // 移動または変形時の動作 - frame.addComponentListener(cablePane.midiDeviceFrameComponentListener); + // フレーム開閉時の描画予約 + Stream.of( + cablePane.midiDeviceFrameListener, + deviceTreeView.midiDeviceFrameListener, + deviceInfoPane.midiDeviceFrameListener + ).forEach(fl->df.addInternalFrameListener(fl)); // - //フレームをデスクトップに追加 - add(frame); + // フレーム移動時、変形時の描画予約 + df.addComponentListener(cablePane.midiDeviceFrameComponentListener); // - // デバイスが開いていたら表示 - if( dm.getMidiDevice().isOpen() ) frame.setVisible(true); + // フレームを追加 + add(df); + if(dm.getMidiDevice().isOpen()) df.setVisible(true); }); } }; diff --git a/src/camidion/chordhelper/mididevice/MidiDeviceDialog.java b/src/camidion/chordhelper/mididevice/MidiDeviceDialog.java index 60d642b..edf7d8d 100644 --- a/src/camidion/chordhelper/mididevice/MidiDeviceDialog.java +++ b/src/camidion/chordhelper/mididevice/MidiDeviceDialog.java @@ -41,9 +41,9 @@ public class MidiDeviceDialog extends JDialog { setBounds( 300, 300, 820, 540 ); MidiDeviceTreeView deviceTreeView = new MidiDeviceTreeView(deviceTreeModel); MidiDeviceInfoPane deviceInfoPane = new MidiDeviceInfoPane(); - deviceTreeView.addTreeSelectionListener(deviceInfoPane); - MidiDeviceDesktopPane desktopPane = new MidiDeviceDesktopPane(deviceTreeView, deviceInfoPane, this); - deviceTreeView.addTreeSelectionListener(desktopPane); + deviceTreeView.addTreeSelectionListener(e->deviceInfoPane.setTreePath(e.getNewLeadSelectionPath())); + MidiDeviceDesktopPane desktopPane = new MidiDeviceDesktopPane(deviceTreeView, deviceInfoPane); + deviceTreeView.addTreeSelectionListener(e->desktopPane.setTreePath(e.getNewLeadSelectionPath())); deviceTreeView.setSelectionRow(0); add(new JSplitPane( JSplitPane.HORIZONTAL_SPLIT, @@ -55,7 +55,7 @@ public class MidiDeviceDialog extends JDialog { add(new JButton("Detect USB MIDI devices", new ButtonIcon(ButtonIcon.REPEAT_ICON)) {{ setToolTipText("Update view for USB MIDI device newly plugged or removed"); addActionListener(e->{ - deviceTreeModel.updateMidiDeviceList(); + deviceTreeModel.update(); deviceTreeView.expandAll(); }); }}); diff --git a/src/camidion/chordhelper/mididevice/MidiDeviceInOutType.java b/src/camidion/chordhelper/mididevice/MidiDeviceInOutType.java index cb34f0c..5ae41cc 100644 --- a/src/camidion/chordhelper/mididevice/MidiDeviceInOutType.java +++ b/src/camidion/chordhelper/mididevice/MidiDeviceInOutType.java @@ -1,5 +1,8 @@ package camidion.chordhelper.mididevice; +import java.util.Arrays; +import java.util.stream.Stream; + import javax.sound.midi.MidiDevice; import javax.sound.midi.MidiEvent; import javax.sound.midi.Receiver; @@ -31,6 +34,9 @@ public enum MidiDeviceInOutType { this.description = description; this.shortName = shortName; } + public static Stream stream() { + return Arrays.stream(values()); + } /** * 指定されたMIDIデバイスがどの入出力タイプに該当するかを返します。 * @param device MIDIデバイス diff --git a/src/camidion/chordhelper/mididevice/MidiDeviceInfoPane.java b/src/camidion/chordhelper/mididevice/MidiDeviceInfoPane.java index 0c0c5c9..1cc9b2b 100644 --- a/src/camidion/chordhelper/mididevice/MidiDeviceInfoPane.java +++ b/src/camidion/chordhelper/mididevice/MidiDeviceInfoPane.java @@ -8,14 +8,12 @@ import javax.swing.JInternalFrame; import javax.swing.event.InternalFrameAdapter; import javax.swing.event.InternalFrameEvent; import javax.swing.event.InternalFrameListener; -import javax.swing.event.TreeSelectionEvent; -import javax.swing.event.TreeSelectionListener; import javax.swing.tree.TreePath; /** * MIDIデバイス情報表示エリア */ -public class MidiDeviceInfoPane extends JEditorPane implements TreeSelectionListener { +public class MidiDeviceInfoPane extends JEditorPane { private String treeNodeTextOf(Object node) { String html = ""; if( node instanceof MidiDeviceModel ) { @@ -75,11 +73,10 @@ public class MidiDeviceInfoPane extends JEditorPane implements TreeSelectionList } }; /** - * ツリー上で選択状態が変わったとき、表示対象のMIDIデバイスを切り替えます。 + * ツリーパスを設定し、その内容を表示します。 + * @param treePath 表示するツリーパス */ - @Override - public void valueChanged(TreeSelectionEvent e) { - TreePath treePath = e.getNewLeadSelectionPath(); + public void setTreePath(TreePath treePath) { setText(treeNodeTextOf(treePath == null ? null : treePath.getLastPathComponent())); } /** diff --git a/src/camidion/chordhelper/mididevice/MidiDeviceModel.java b/src/camidion/chordhelper/mididevice/MidiDeviceModel.java index a5ed1ba..b1dfa46 100644 --- a/src/camidion/chordhelper/mididevice/MidiDeviceModel.java +++ b/src/camidion/chordhelper/mididevice/MidiDeviceModel.java @@ -1,7 +1,10 @@ package camidion.chordhelper.mididevice; +import java.util.Collections; +import java.util.List; +import java.util.Set; + import javax.sound.midi.MidiDevice; -import javax.sound.midi.MidiSystem; import javax.sound.midi.MidiUnavailableException; import javax.sound.midi.Receiver; import javax.sound.midi.Transmitter; @@ -52,16 +55,6 @@ public class MidiDeviceModel { public MidiDeviceTreeModel getDeviceTreeModel() { return deviceTreeModel; } protected MidiDeviceTreeModel deviceTreeModel; /** - * MIDIデバイス情報からMIDIデバイスモデルを構築します。 - * - * @param deviceInfo 対象MIDIデバイス情報 - * @param deviceTreeModel 収容先のMIDIデバイスツリーモデル - * @throws MidiUnavailableException {@link MidiSystem#getMidiDevice(MidiDevice.Info)}からの例外 - */ - public MidiDeviceModel(MidiDevice.Info deviceInfo, MidiDeviceTreeModel deviceTreeModel) throws MidiUnavailableException { - this(MidiSystem.getMidiDevice(deviceInfo), deviceTreeModel); - } - /** * MIDIデバイスからモデルを構築します。 * * @param device 対象MIDIデバイス @@ -69,10 +62,9 @@ public class MidiDeviceModel { */ public MidiDeviceModel(MidiDevice device, MidiDeviceTreeModel deviceTreeModel) { this.deviceTreeModel = deviceTreeModel; - this.device = device; + ioType = MidiDeviceInOutType.getValueFor(this.device = device); if( device.getMaxTransmitters() != 0 ) txListModel = new TransmitterListModel(this); if( device.getMaxReceivers() != 0 ) rxListModel = new ReceiverListModel(this); - ioType = MidiDeviceInOutType.getValueFor(device); treePath = new TreePath(new Object[] {deviceTreeModel, ioType ,this}); } /** @@ -88,12 +80,29 @@ public class MidiDeviceModel { if( rxListModel != null ) rxListModel.openSingleReceiver(); } /** + * 開かれている{@link Receiver}を返します。 + * @return このMIDIデバイスの開かれた{@link Receiver}(ない場合はnull) + */ + public Receiver getReceiver() { + if( rxListModel == null ) return null; + List rxList = rxListModel.getTransceivers(); + return rxList.isEmpty() ? null : rxList.get(0); + } + /** + * このリストモデルの{@link Receiver}に接続された{@link Transmitter}を全て閉じ、 + * 接続相手だったMIDIデバイスモデルのユニークな集合を返します。 + * 接続相手が存在していなかった場合は、空集合を返します。 + * + * @return 閉じた{@link Transmitter}を持っていた接続相手の{@link MidiDeviceModel}の集合 + */ + public Set disconnectPeerTransmitters() { + return rxListModel == null ? Collections.emptySet() : + rxListModel.closeAllConnectedTransmitters(); + } + /** * このMIDIデバイスを {@link MidiDevice#close()} で閉じます。 - * このMIDIデバイスの{@link Receiver}に接続している他デバイスの{@link Transmitter}があれば、 + * このMIDIデバイスの{@link Receiver}に接続している相手デバイスの{@link Transmitter}があれば、 * それらも全て閉じます。 */ - public void close() { - if( rxListModel != null ) rxListModel.closeAllConnectedTransmitters(); - device.close(); - } + public void close() { disconnectPeerTransmitters(); device.close(); } } diff --git a/src/camidion/chordhelper/mididevice/MidiDeviceTreeModel.java b/src/camidion/chordhelper/mididevice/MidiDeviceTreeModel.java index f2086d8..4adc7d5 100644 --- a/src/camidion/chordhelper/mididevice/MidiDeviceTreeModel.java +++ b/src/camidion/chordhelper/mididevice/MidiDeviceTreeModel.java @@ -5,11 +5,9 @@ import java.util.AbstractList; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; -import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.Set; import java.util.Vector; import java.util.function.Function; import java.util.stream.Collectors; @@ -39,20 +37,15 @@ import camidion.chordhelper.ChordHelperApplet; public class MidiDeviceTreeModel extends AbstractList implements TreeModel { @Override public String toString() { return "MIDI devices"; } - /** - * リスト本体 - */ + protected List deviceModelList = new ArrayList<>(); @Override public int size() { return deviceModelList.size(); } @Override public MidiDeviceModel get(int index) { return deviceModelList.get(index); } - /** - * このリストの内容を反映したツリー構造のマップ - */ + protected Map> deviceModelTree - = Arrays.stream(MidiDeviceInOutType.values()) - .collect(Collectors.toMap(Function.identity(), t -> new ArrayList<>())); + = MidiDeviceInOutType.stream().collect(Collectors.toMap(Function.identity(), t-> new ArrayList<>())); /** * {@link AbstractList#add(E)}の操作を内部的に行います。 * 指定された要素をこのリストの最後に追加し、ツリー構造にも反映します。 @@ -64,6 +57,18 @@ public class MidiDeviceTreeModel extends AbstractList implement deviceModelTree.get(dm.getInOutType()).add(dm); return true; } + protected MidiDeviceModel add(MidiDevice device) { + if( device == null ) return null; + MidiDeviceModel m = new MidiDeviceModel(device,this); + addInternally(m); + return m; + } + protected MidiSequencerModel add(Sequencer sequencer) { + if( sequencer == null ) return null; + MidiSequencerModel m = new MidiSequencerModel(sequencer,this); + addInternally(m); + return m; + } /** * {@link AbstractCollection#removeAll(Collection)}の操作を内部的に行います。 * 指定されたコレクションに該当するすべての要素を、このリストから削除します。 @@ -124,18 +129,51 @@ public class MidiDeviceTreeModel extends AbstractList implement } } - /** - * {@link MidiSystem#getMidiDeviceInfo()} が返した配列を不変の {@link List} として返します。 - * - *

注意点:MIDIデバイスをUSBから抜いて、他のデバイスとの接続を切断せずに - * {@link MidiSystem#getMidiDeviceInfo()}を呼び出すと - * (少なくとも Windows 10 で)Java VM がクラッシュすることがあります。 - *

- * @return インストールされているMIDIデバイスの情報のリスト - */ - public static List getMidiDeviceInfo() { + private static List getMidiDeviceInfo() { return Arrays.asList(MidiSystem.getMidiDeviceInfo()); } + private static MidiDevice getMidiDevice(MidiDevice.Info info) { + try { + return MidiSystem.getMidiDevice(info); + } catch( Exception ex ) { + String title = ChordHelperApplet.VersionInfo.NAME; + String message = "MIDI device '" + info + "' unavailable\n" + ex; + JOptionPane.showMessageDialog(null, message, title, JOptionPane.ERROR_MESSAGE); + return null; + } + } + private static Sequencer getSequencer() { + try { + return MidiSystem.getSequencer(false); + } catch( MidiUnavailableException ex ) { + String title = ChordHelperApplet.VersionInfo.NAME; + String message = "MIDI sequencer unavailable\n" + ex; + JOptionPane.showMessageDialog(null, message, title, JOptionPane.ERROR_MESSAGE); + } + return null; + } + private static Synthesizer getSynthesizer() { + try { + return MidiSystem.getSynthesizer(); + } catch( MidiUnavailableException e ) { + String title = ChordHelperApplet.VersionInfo.NAME; + String message = "Java internal MIDI synthesizer unavailable\n" + e; + JOptionPane.showMessageDialog(null, message, title, JOptionPane.ERROR_MESSAGE); + } + return null; + } + private static boolean openMidiDeviceModel(MidiDeviceModel deviceModel) { + if( Objects.isNull(deviceModel) ) return false; + try { + deviceModel.open(); return true; + } catch(Exception e) { + String title = ChordHelperApplet.VersionInfo.NAME; + String message = "Cannot open MIDI device '"+deviceModel+"'\n" + + "MIDIデバイス "+deviceModel+" を開くことができません。\n\n" + e; + JOptionPane.showMessageDialog(null, message, title, JOptionPane.ERROR_MESSAGE); + } + return false; + } /** * このMIDIデバイスツリーモデルに登録されているMIDIシーケンサーモデルを返します。 @@ -143,7 +181,6 @@ public class MidiDeviceTreeModel extends AbstractList implement */ public MidiSequencerModel getSequencerModel() { return sequencerModel; } protected MidiSequencerModel sequencerModel; - /** * 引数で与えられたGUI仮想MIDIデバイスと、{@link #getMidiDeviceInfo()}から取得したMIDIデバイス情報から、 * MIDIデバイスツリーモデルを初期構築します。 @@ -154,96 +191,68 @@ public class MidiDeviceTreeModel extends AbstractList implement MidiDeviceModel synthModel = null; MidiDeviceModel firstMidiInModel = null; MidiDeviceModel firstMidiOutModel = null; - // GUI - MidiDeviceModel guiModel = new MidiDeviceModel(guiVirtualDevice, this); - addInternally(guiModel); - // シーケンサー - try { - addInternally(sequencerModel = new MidiSequencerModel(MidiSystem.getSequencer(false), this)); - } catch( MidiUnavailableException ex ) { - String title = ChordHelperApplet.VersionInfo.NAME; - String message = "MIDI sequencer unavailable\n" + ex; - JOptionPane.showMessageDialog(null, message, title, JOptionPane.ERROR_MESSAGE); - } - // システムで使用可能な全MIDIデバイス(シーケンサーはすでに取得済みなので除外) - for( MidiDevice device : getMidiDeviceInfo().stream().map(info -> { - try { - return MidiSystem.getMidiDevice(info); - } catch( MidiUnavailableException ex ) { - String title = ChordHelperApplet.VersionInfo.NAME; - String message = "MIDI device '" + info + "' unavailable\n" + ex; - JOptionPane.showMessageDialog(null, message, title, JOptionPane.ERROR_MESSAGE); - return null; - } - }).filter( - device -> Objects.nonNull(device) && ! (device instanceof Sequencer) - ).collect(Collectors.toList()) ) { - if( device instanceof Synthesizer ) { // Java内蔵シンセサイザの場合 - try { - addInternally(synthModel = new MidiDeviceModel(MidiSystem.getSynthesizer(), this)); - } catch( MidiUnavailableException ex ) { - String title = ChordHelperApplet.VersionInfo.NAME; - String message = "Java internal MIDI synthesizer unavailable\n" + ex; - JOptionPane.showMessageDialog(null, message, title, JOptionPane.ERROR_MESSAGE); - } + MidiDeviceModel guiModel = add(guiVirtualDevice); + sequencerModel = add(getSequencer()); + for( MidiDevice device : getMidiDeviceInfo().stream() + .map(info->getMidiDevice(info)) + .filter(Objects::nonNull) + .filter(d -> !(d instanceof Sequencer)) + .collect(Collectors.toList()) + ) { + if( device instanceof Synthesizer ) { + synthModel = add(getSynthesizer()); continue; } - MidiDeviceModel m = new MidiDeviceModel(device, this); + MidiDeviceModel m = add(device); // // 最初の MIDI OUT(Windowsの場合は通常、内蔵音源 Microsoft GS Wavetable SW Synth) - if( firstMidiOutModel == null && m.getReceiverListModel() != null ) firstMidiOutModel = m; - // + if( firstMidiOutModel == null && m.getReceiverListModel() != null ) { + firstMidiOutModel = m; + } // 最初の MIDI IN(USB MIDI インターフェースにつながったMIDIキーボードなど) - if( firstMidiInModel == null && m.getTransmitterListModel() != null ) firstMidiInModel = m; - // - addInternally(m); + if( firstMidiInModel == null && m.getTransmitterListModel() != null ) { + firstMidiInModel = m; + } } - // MIDIデバイスモデルを開く。 - // - // NOTE: 必ず MIDI OUT Rx デバイスを先に開くこと。 - // - // そうすれば、後から開いた MIDI IN Tx デバイスからのタイムスタンプのほうが「若く」なるので、 - // 相手の MIDI OUT Rx デバイスは「信号が遅れてやってきた」と認識、遅れを取り戻そうとして - // 即座に音を出してくれる。 - // - // 開く順序が逆になると「進みすぎるから遅らせよう」として無用なレイテンシーが発生する原因になる。 - // List openedDeviceModels = Stream.of( - synthModel, - firstMidiOutModel, - sequencerModel, - guiModel, - firstMidiInModel - ).filter(Objects::nonNull).filter(dm->{ - try { - dm.open(); - return true; - } catch( MidiUnavailableException ex ) { - String title = ChordHelperApplet.VersionInfo.NAME; - String message = "Cannot open MIDI device '"+dm+"'\n" - + "MIDIデバイス "+dm+" を開くことができません。\n\n" + ex; - JOptionPane.showMessageDialog(null, message, title, JOptionPane.ERROR_MESSAGE); - return false; - } - }).collect(Collectors.toList()); + // NOTE: 必ず MIDI OUT Rx デバイスを先に開くこと。 + // + // そうすれば、後から開いた MIDI IN Tx デバイスからのタイムスタンプのほうが「若く」なるので、 + // 相手の MIDI OUT Rx デバイスは「信号が遅れてやってきた」と認識、遅れを取り戻そうとして + // 即座に音を出してくれる。 + // + // 開く順序が逆になると「進みすぎるから遅らせよう」として無用なレイテンシーが発生する原因になる。 + synthModel, firstMidiOutModel, // MIDI OUT Rx + sequencerModel, guiModel, // Both + firstMidiInModel // MIDI IN Tx + ).filter(dm->openMidiDeviceModel(dm)).collect(Collectors.toList()); // // 開いたデバイスを相互に接続する。 // 自身のTx/Rx同士の接続は、シーケンサーモデルはなし、それ以外(GUIデバイスモデル)はあり。 connectDevices( - openedDeviceModels.stream().filter(rxm-> - Objects.nonNull(rxm.getReceiverListModel()) - ).collect(Collectors.toMap(Function.identity(), rxm-> - openedDeviceModels.stream().filter(txm-> - Objects.nonNull(txm.getTransmitterListModel()) - && !(txm == sequencerModel && txm == rxm) - ).collect(Collectors.toList()) + openedDeviceModels.stream().filter( + m->Objects.nonNull(m.getReceiverListModel()) + ).collect(Collectors.toMap( + Function.identity(), + r->openedDeviceModels.stream() + .filter(m->Objects.nonNull(m.getTransmitterListModel())) + .filter(t-> !(t == sequencerModel && t == r)) + .collect(Collectors.toList()) )) ); } /** - * デバイス間の接続をすべて切断します。 - * 各{@link Receiver}ごとに相手デバイスの{@link Transmitter}を閉じ、 - * その時どのように接続されていたかを示すマップを返します。 + * {@link Transmitter}を持つすべてのデバイス(例:MIDIキーボードなど)について、 + * {@link MidiDeviceModel#resetMicrosecondPosition()}でマイクロ秒位置をリセットします。 + */ + public void resetMicrosecondPosition() { + stream().map(m->m.getTransmitterListModel()).filter(Objects::nonNull) + .forEach(tlm->tlm.resetMicrosecondPosition()); + } + /** + * MIDIデバイス間の接続をすべて切断します。 + * 各{@link Receiver}ごとに相手デバイスの{@link Transmitter}を閉じながら、 + * 閉じる前の接続状態をマップに保存し、そのマップを返します。 * * @return MIDIデバイスモデル接続マップ(再接続時に{@link #connectDevices(Map)}に指定可) *
    @@ -251,85 +260,67 @@ public class MidiDeviceTreeModel extends AbstractList implement *
  • 値:接続相手だった{@link Transmitter}を持つMIDIデバイスモデルのコレクション
  • *
*/ - public Map> disconnectAllDevices() { - Map> rxToTxConnections = new LinkedHashMap<>(); - deviceModelList.stream().forEach(m -> { - ReceiverListModel rxListModel = m.getReceiverListModel(); - if( rxListModel == null ) return; - Collection txDeviceModels = rxListModel.closeAllConnectedTransmitters(); - if( txDeviceModels.isEmpty() ) return; - rxToTxConnections.put(m, txDeviceModels); - }); - return rxToTxConnections; + private Map> disconnectAllDevices() { + return stream().collect( + Collectors.toMap(Function.identity(), m->m.disconnectPeerTransmitters()) + ); } /** - * デバイス間の接続を復元します。 + * 指定された接続マップに従ってMIDIデバイス間を接続します。 * - * @param rxToTxConnections {@link #disconnectAllDevices()}が返したMIDIデバイスモデル接続マップ + * @param rxToTxConnections {@link #disconnectAllDevices()} + * が返した(あるいはそれと同じ形式の)MIDIデバイスモデル接続マップ *
    *
  • キー:{@link Receiver}側デバイスモデル
  • *
  • 値:{@link Transmitter}側デバイスモデルのコレクション
  • *
*/ - public void connectDevices(Map> rxToTxConnections) { - rxToTxConnections.keySet().stream().forEach(rxm -> { - Receiver rx = rxm.getReceiverListModel().getTransceivers().get(0); - rxToTxConnections.get(rxm).stream().filter(Objects::nonNull).forEach(txm -> { + private void connectDevices(Map> rxToTxConnections) { + rxToTxConnections.keySet().stream().forEach(rxm->{ + Receiver rx = rxm.getReceiver(); + if( rx == null ) return; + rxToTxConnections.get(rxm).stream().filter(Objects::nonNull).forEach(txm->{ try { txm.getTransmitterListModel().openTransmitter().setReceiver(rx); - } catch( MidiUnavailableException e ) { - e.printStackTrace(); + } catch( Exception ex ) { + String title = ChordHelperApplet.VersionInfo.NAME; + String message = "MIDIデバイス同士の接続に失敗しました。\n送信側(Tx):"+txm+" → 受信側(Rx):"+rxm+"\n\n" + ex; + JOptionPane.showMessageDialog(null, message, title, JOptionPane.ERROR_MESSAGE); } }); }); } /** - * USB-MIDIデバイスの着脱後、MIDIデバイスリストを最新の状態に更新します。 + * MIDIデバイスリストを最新の状態に更新し、このモデルを参照しているビューに通知します。 * - *

USBからMIDIデバイスを抜いた場合に {@link #getMidiDeviceInfo()} で - * Java VM クラッシュが発生する現象を回避するため、更新前に全デバイスの接続を一時切断し、 - * 更新完了後に接続を復元します。 + *

USB-MIDIデバイスが着脱されたときにこのメソッドを呼び出すと、 + * 新しく装着されたMIDIデバイスを開くことができるようになります。 + * 同時に、取り外されたMIDIデバイスが閉じられ、そのデバイスの表示も消えます。 + *

+ *

別のMIDIデバイスに + * {@link Transmitter} や {@link Receiver} + * を接続したままUSB端子からMIDIデバイスを抜くと、 + * {@link MidiSystem#getMidiDeviceInfo()} を呼び出したときに Java VM がクラッシュしてしまうため、 + * 最初に{@link #disconnectAllDevices()}で接続をすべて切断してから + * MIDIデバイスリストを最新の状態に更新し、その後{@link #connectDevices(Map)}で接続を復元します。 *

*/ - public void updateMidiDeviceList() { - // 一時切断 - Map> rxToTxConnections = disconnectAllDevices(); - // - // 追加・削除されたMIDIデバイスを特定 - List toAdd = new Vector<>(getMidiDeviceInfo()); - List toRemove = deviceModelList.stream().filter(m -> { - MidiDevice d = m.getMidiDevice(); - if( d instanceof VirtualMidiDevice || toAdd.remove(d.getDeviceInfo()) ) return false; - d.close(); return true; + public void update() { + Map> savedConnections = disconnectAllDevices(); + List additionalInfo = new Vector<>(getMidiDeviceInfo()); + List closedModels = stream().filter(model->{ + MidiDevice device = model.getMidiDevice(); + if( device instanceof VirtualMidiDevice ) return false; + if( additionalInfo.remove(device.getDeviceInfo()) ) return false; + device.close(); return true; }).collect(Collectors.toList()); - // 削除されたデバイスのモデルを除去 - if( removeAllInternally(toRemove) ) { - Set rxModels = rxToTxConnections.keySet(); - rxModels.removeAll(toRemove); - rxModels.forEach(m -> rxToTxConnections.get(m).removeAll(toRemove)); + if( removeAllInternally(closedModels) ) { + savedConnections.keySet().removeAll(closedModels); // Rx + savedConnections.values().forEach(m->m.removeAll(closedModels)); // Tx } - // 追加されたデバイスのモデルを登録 - toAdd.forEach(info -> { - try { - addInternally(new MidiDeviceModel(info, this)); - } catch( MidiUnavailableException e ) { - e.printStackTrace(); - } - }); - // 再接続 - connectDevices(rxToTxConnections); - // - // リスナーに通知してツリー表示を更新してもらう + additionalInfo.forEach(info->add(getMidiDevice(info))); + connectDevices(savedConnections); fireTreeStructureChanged(this, null, null, null); } - /** - * {@link Transmitter}を持つすべてのデバイス(例:MIDIキーボードなど)について、 - * {@link MidiDeviceModel#resetMicrosecondPosition()}でマイクロ秒位置をリセットします。 - */ - public void resetMicrosecondPosition() { - deviceModelList.stream().map(dm -> dm.getTransmitterListModel()) - .filter(Objects::nonNull) - .forEach(tlm -> tlm.resetMicrosecondPosition()); - } } diff --git a/src/camidion/chordhelper/mididevice/MidiDeviceTreeView.java b/src/camidion/chordhelper/mididevice/MidiDeviceTreeView.java index 4e59840..74944f8 100644 --- a/src/camidion/chordhelper/mididevice/MidiDeviceTreeView.java +++ b/src/camidion/chordhelper/mididevice/MidiDeviceTreeView.java @@ -57,19 +57,17 @@ public class MidiDeviceTreeView extends JTree { boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus) { super.getTreeCellRendererComponent(tree, value, selected, expanded, leaf, row, hasFocus); - setToolTipText(value.toString()); if(leaf) { - MidiDeviceModel deviceModel = (MidiDeviceModel)value; - if( deviceModel.getMidiDevice().isOpen() ) { + boolean open = ((MidiDeviceModel)value).getMidiDevice().isOpen(); + if(open) { setDisabledIcon(MidiDeviceDialog.MIDI_CONNECTER_ICON); - setEnabled(false); - setToolTipText(getToolTipText()+"はすでに開いています"); + setToolTipText(value+"はすでに開いています"); } else { setIcon(MidiDeviceDialog.MIDI_CONNECTER_ICON); - setEnabled(true); - setToolTipText("ドラッグ&ドロップで"+getToolTipText()+"が開きます"); + setToolTipText("ドラッグ&ドロップで"+value+"が開きます"); } - } + setEnabled(!open); + } else setToolTipText(value.toString()); return this; } }); diff --git a/src/camidion/chordhelper/midieditor/MidiSequenceEditorDialog.java b/src/camidion/chordhelper/midieditor/MidiSequenceEditorDialog.java index d7915f5..fbc15f4 100644 --- a/src/camidion/chordhelper/midieditor/MidiSequenceEditorDialog.java +++ b/src/camidion/chordhelper/midieditor/MidiSequenceEditorDialog.java @@ -650,9 +650,9 @@ public class MidiSequenceEditorDialog extends JDialog { eventSelectionListener = new EventSelectionListener(); titleLabel = new TitleLabel(); // - TableColumnModel colModel = getColumnModel(); + TableColumnModel cm = getColumnModel(); Arrays.stream(TrackEventListTableModel.Column.values()).forEach(c-> - colModel.getColumn(c.ordinal()).setPreferredWidth(c.preferredWidth) + cm.getColumn(c.ordinal()).setPreferredWidth(c.preferredWidth) ); } /** diff --git a/src/camidion/chordhelper/pianokeyboard/PianoKeyboard.java b/src/camidion/chordhelper/pianokeyboard/PianoKeyboard.java index dac88ab..66267f9 100644 --- a/src/camidion/chordhelper/pianokeyboard/PianoKeyboard.java +++ b/src/camidion/chordhelper/pianokeyboard/PianoKeyboard.java @@ -434,24 +434,19 @@ public class PianoKeyboard extends JComponent { addMouseMotionListener(mkl); addKeyListener(mkl); int octaves = getPerferredOctaves(); - octaveSizeModel = new DefaultBoundedRangeModel(octaves, 0, MIN_OCTAVE_WIDTH, MAX_OCTAVE_WIDTH) { - { - addChangeListener(e->{ - fireOctaveResized(e); - octaveSizeChanged(); - }); - } - }; - octaveRangeModel = new DefaultBoundedRangeModel( - (MAX_OCTAVE_WIDTH - octaves) / 2, octaves, 0, MAX_OCTAVE_WIDTH) { - { - addChangeListener(e->{ - fireOctaveMoved(e); - checkOutOfBounds(); - repaint(); - }); - } - }; + octaveSizeModel = new DefaultBoundedRangeModel + (octaves, 0, MIN_OCTAVE_WIDTH, MAX_OCTAVE_WIDTH); + octaveSizeModel.addChangeListener(e->{ + fireOctaveResized(e); + octaveSizeChanged(); + }); + octaveRangeModel = new DefaultBoundedRangeModel + ((MAX_OCTAVE_WIDTH - octaves) / 2, octaves, 0, MAX_OCTAVE_WIDTH); + octaveRangeModel.addChangeListener(e->{ + fireOctaveMoved(e); + checkOutOfBounds(); + repaint(); + }); addComponentListener(new ComponentAdapter() { @Override public void componentResized(ComponentEvent e) { @@ -714,7 +709,7 @@ public class PianoKeyboard extends JComponent { public Integer[] getSelectedNotes() { return selectedKeyNoteList.toArray(new Integer[0]); } - Chord getChord() { return chord; } + public Chord getChord() { return chord; } public void setChord(Chord c) { chordDisplay.setChord(chord = c); } public void setKeySignature(Key ks) { keySignature = ks; repaint(); } private int maxSelectable = 1; -- 2.11.0