1 package camidion.chordhelper.mididevice;
3 import java.util.AbstractCollection;
4 import java.util.AbstractList;
5 import java.util.ArrayList;
6 import java.util.Arrays;
7 import java.util.Collection;
8 import java.util.EnumMap;
9 import java.util.LinkedHashMap;
10 import java.util.List;
13 import java.util.Vector;
14 import java.util.stream.Collectors;
16 import javax.sound.midi.MidiDevice;
17 import javax.sound.midi.MidiSystem;
18 import javax.sound.midi.MidiUnavailableException;
19 import javax.sound.midi.Receiver;
20 import javax.sound.midi.Sequencer;
21 import javax.sound.midi.Synthesizer;
22 import javax.sound.midi.Transmitter;
23 import javax.swing.JOptionPane;
24 import javax.swing.event.EventListenerList;
25 import javax.swing.event.TreeModelEvent;
26 import javax.swing.event.TreeModelListener;
27 import javax.swing.tree.TreeModel;
28 import javax.swing.tree.TreePath;
30 import camidion.chordhelper.ChordHelperApplet;
33 * 仮想MIDIデバイスを含めたすべてのMIDIデバイスモデル{@link MidiDeviceModel}をツリー構造で管理するモデル。
34 * 読み取り専用のMIDIデバイスリストとしても、
35 * I/Oタイプで分類されたMIDIデバイスツリーモデルとしても参照できます。
37 public class MidiDeviceTreeModel extends AbstractList<MidiDeviceModel> implements TreeModel {
39 public String toString() { return "MIDI devices"; }
41 protected List<MidiDeviceModel> deviceModelList = new Vector<>();
43 public int size() { return deviceModelList.size(); }
45 public MidiDeviceModel get(int index) { return deviceModelList.get(index); }
47 * このリストの内容を反映したツリー構造のマップ
49 protected Map<MidiDeviceInOutType, List<MidiDeviceModel>> deviceModelTree; {
50 deviceModelTree = new EnumMap<>(MidiDeviceInOutType.class);
51 deviceModelTree.put(MidiDeviceInOutType.MIDI_OUT, new ArrayList<>());
52 deviceModelTree.put(MidiDeviceInOutType.MIDI_IN, new ArrayList<>());
53 deviceModelTree.put(MidiDeviceInOutType.MIDI_IN_OUT, new ArrayList<>());
56 * {@link AbstractList#add(E)}の操作を内部的に行います。
57 * 指定された要素をこのリストの最後に追加し、ツリー構造にも反映します。
59 * @param dm 追加するMIDIデバイスモデル
60 * @return true({@link AbstractList#add(E)} と同様)
62 protected boolean addInternally(MidiDeviceModel dm) {
63 if( ! deviceModelList.add(dm) ) return false;
64 deviceModelTree.get(dm.getInOutType()).add(dm);
68 * {@link AbstractCollection#removeAll(Collection)}の操作を内部的に行います。
69 * 指定されたコレクションに該当するすべての要素を、このリストから削除します。
70 * このリストが変更された場合、ツリー構造にも反映されます。
71 * @param c 削除する要素のコレクション
72 * @return このリストが変更された場合はtrue({@link AbstractCollection#removeAll(Collection)} と同様)
74 protected boolean removeAllInternally(Collection<?> c) {
75 if( ! deviceModelList.removeAll(c) ) return false;
76 c.stream().filter(o -> o instanceof MidiDeviceModel).map(o -> (MidiDeviceModel)o)
77 .forEach(mdm -> deviceModelTree.get(mdm.getInOutType()).remove(mdm));
81 public Object getRoot() { return this; }
83 public int getChildCount(Object parent) {
84 if( parent == getRoot() ) return MidiDeviceInOutType.values().length - 1;
85 if( parent instanceof MidiDeviceInOutType ) return deviceModelTree.get(parent).size();
89 public Object getChild(Object parent, int index) {
90 if( parent == getRoot() ) return MidiDeviceInOutType.values()[index + 1];
91 if( parent instanceof MidiDeviceInOutType ) return deviceModelTree.get(parent).get(index);
95 public int getIndexOfChild(Object parent, Object child) {
96 if( parent == getRoot() && child instanceof MidiDeviceInOutType ) {
97 return ((MidiDeviceInOutType)child).ordinal() - 1;
99 if( parent instanceof MidiDeviceInOutType ) return deviceModelTree.get(parent).indexOf(child);
103 public boolean isLeaf(Object node) { return node instanceof MidiDeviceModel; }
105 public void valueForPathChanged(TreePath path, Object newValue) {}
107 public void addTreeModelListener(TreeModelListener listener) {
108 listenerList.add(TreeModelListener.class, listener);
111 public void removeTreeModelListener(TreeModelListener listener) {
112 listenerList.remove(TreeModelListener.class, listener);
114 protected EventListenerList listenerList = new EventListenerList();
115 protected void fireTreeStructureChanged(Object source, Object[] path, int[] childIndices, Object[] children) {
116 Object[] listeners = listenerList.getListenerList();
117 for (int i = listeners.length-2; i>=0; i-=2) {
118 if (listeners[i]==TreeModelListener.class) {
119 ((TreeModelListener)listeners[i+1]).treeStructureChanged(
120 new TreeModelEvent(source,path,childIndices,children)
127 * {@link MidiSystem#getMidiDeviceInfo()} が返した配列を不変の {@link List} として返します。
129 * <p>注意点:MIDIデバイスをUSBから抜いて、他のデバイスとの接続を切断せずに
130 * {@link MidiSystem#getMidiDeviceInfo()}を呼び出すと
131 * (少なくとも Windows 10 で)Java VM がクラッシュすることがあります。
133 * @return インストールされているMIDIデバイスの情報のリスト
135 public static List<MidiDevice.Info> getMidiDeviceInfo() {
136 return Arrays.asList(MidiSystem.getMidiDeviceInfo());
140 * このMIDIデバイスツリーモデルに登録されているMIDIシーケンサーモデルを返します。
141 * @return MIDIシーケンサーモデル
143 public MidiSequencerModel getSequencerModel() { return sequencerModel; }
144 protected MidiSequencerModel sequencerModel;
147 * 引数で与えられたGUI仮想MIDIデバイスと、{@link #getMidiDeviceInfo()}から取得したMIDIデバイス情報から、
148 * MIDIデバイスツリーモデルを初期構築します。
150 * @param guiVirtualDevice 管理対象に含めるGUI仮想MIDIデバイス
152 public MidiDeviceTreeModel(VirtualMidiDevice guiVirtualDevice) {
153 MidiDeviceModel synthModel = null;
154 MidiDeviceModel firstMidiInModel = null;
155 MidiDeviceModel firstMidiOutModel = null;
157 MidiDeviceModel guiModel = new MidiDeviceModel(guiVirtualDevice, this);
158 addInternally(guiModel);
161 addInternally(sequencerModel = new MidiSequencerModel(MidiSystem.getSequencer(false), this));
162 } catch( MidiUnavailableException e ) {
163 System.out.println(ChordHelperApplet.VersionInfo.NAME +" : MIDI sequencer unavailable");
166 // システムで使用可能な全MIDIデバイス(シーケンサーはすでに取得済みなので除外)
167 for( MidiDevice device : getMidiDeviceInfo().stream().map(info -> {
169 return MidiSystem.getMidiDevice(info);
170 } catch( MidiUnavailableException e ) {
175 device -> device != null && ! (device instanceof Sequencer)
176 ).collect(Collectors.toList()) ) {
177 if( device instanceof Synthesizer ) { // Java内蔵シンセサイザの場合
179 addInternally(synthModel = new MidiDeviceModel(MidiSystem.getSynthesizer(), this));
180 } catch( MidiUnavailableException e ) {
181 System.out.println(ChordHelperApplet.VersionInfo.NAME +
182 " : Java internal MIDI synthesizer unavailable");
187 MidiDeviceModel m = new MidiDeviceModel(device, this);
189 // 最初の MIDI OUT(Windowsの場合は通常、内蔵音源 Microsoft GS Wavetable SW Synth)
190 if( firstMidiOutModel == null && m.getReceiverListModel() != null ) firstMidiOutModel = m;
192 // 最初の MIDI IN(USB MIDI インターフェースにつながったMIDIキーボードなど)
193 if( firstMidiInModel == null && m.getTransmitterListModel() != null ) firstMidiInModel = m;
199 // NOTE: 必ず MIDI OUT Rx デバイスを先に開くこと。
201 // そうすれば、後から開いた MIDI IN Tx デバイスからのタイムスタンプのほうが「若く」なるので、
202 // 相手の MIDI OUT Rx デバイスは「信号が遅れてやってきた」と認識、遅れを取り戻そうとして
205 // 開く順序が逆になると「進みすぎるから遅らせよう」として無用なレイテンシーが発生する原因になる。
207 List<MidiDeviceModel> openedMidiDeviceModelList = new ArrayList<>();
214 ).stream().filter(mdm -> mdm != null).forEach(mdm->{
217 openedMidiDeviceModelList.add(mdm);
218 } catch( MidiUnavailableException ex ) {
219 String title = ChordHelperApplet.VersionInfo.NAME;
220 String message = "Cannot open MIDI device '"+mdm+"'\n"
221 + "MIDIデバイス "+mdm+" を開くことができません。\n\n" + ex;
222 JOptionPane.showMessageDialog(null, message, title, JOptionPane.ERROR_MESSAGE);
225 // 初期接続マップを作成(開いたデバイスを相互に接続する)
226 // 自身のTx/Rx同士の接続は、シーケンサーモデルはなし、それ以外(GUIデバイスモデル)はあり。
227 Map<MidiDeviceModel, Collection<MidiDeviceModel>> initialConnection = new LinkedHashMap<>();
228 openedMidiDeviceModelList.stream().filter(rxm ->
229 rxm.getReceiverListModel() != null
231 List<MidiDeviceModel> txmList;
232 initialConnection.put(rxm, txmList = new ArrayList<>());
233 openedMidiDeviceModelList.stream().filter(txm ->
234 txm.getTransmitterListModel() != null
235 && !(txm == sequencerModel && txm == rxm)
236 ).forEach(txm -> txmList.add(txm));
239 connectDevices(initialConnection);
244 public void closeAllDevices() {
245 deviceModelList.forEach(m -> m.getMidiDevice().close());
249 * 各{@link Receiver}ごとに相手デバイスの{@link Transmitter}を閉じ、
250 * その時どのように接続されていたかを示すマップを返します。
252 * @return MIDIデバイスモデル接続マップ(再接続時に{@link #connectDevices(Map)}に指定可)
254 * <li>キー:各{@link Receiver}を持つMIDIデバイスモデル</li>
255 * <li>値:接続相手だった{@link Transmitter}を持つMIDIデバイスモデルのコレクション</li>
258 public Map<MidiDeviceModel, Collection<MidiDeviceModel>> disconnectAllDevices() {
259 Map<MidiDeviceModel, Collection<MidiDeviceModel>> rxToTxConnections = new LinkedHashMap<>();
260 deviceModelList.stream().forEach(m -> {
261 ReceiverListModel rxListModel = m.getReceiverListModel();
262 if( rxListModel == null ) return;
263 Collection<MidiDeviceModel> txDeviceModels = rxListModel.closeTransmitters();
264 if( ! txDeviceModels.isEmpty() ) rxToTxConnections.put(m, txDeviceModels);
266 return rxToTxConnections;
271 * @param rxToTxConnections {@link #disconnectAllDevices()}が返したMIDIデバイスモデル接続マップ
273 * <li>キー:{@link Receiver}側デバイスモデル</li>
274 * <li>値:{@link Transmitter}側デバイスモデルのコレクション</li>
277 public void connectDevices(Map<MidiDeviceModel, Collection<MidiDeviceModel>> rxToTxConnections) {
278 rxToTxConnections.keySet().stream().filter(rxm -> rxm != null).forEach(rxm -> {
279 Receiver rx = rxm.getReceiverListModel().getTransceivers().get(0);
280 rxToTxConnections.get(rxm).stream().filter(txm -> txm != null).forEach(txm -> {
282 txm.getTransmitterListModel().openTransmitter().setReceiver(rx);
283 } catch( MidiUnavailableException e ) {
290 * USB-MIDIデバイスの着脱後、MIDIデバイスリストを最新の状態に更新します。
292 * <p>USBからMIDIデバイスを抜いた場合に {@link #getMidiDeviceInfo()} で
293 * Java VM クラッシュが発生する現象を回避するため、更新前に全デバイスの接続を一時切断し、
297 public void updateMidiDeviceList() {
299 Map<MidiDeviceModel, Collection<MidiDeviceModel>> rxToTxConnections = disconnectAllDevices();
301 // 追加・削除されたMIDIデバイスを特定
302 List<MidiDevice.Info> toAdd = new Vector<>(getMidiDeviceInfo());
303 List<MidiDeviceModel> toRemove = deviceModelList.stream().filter(m -> {
304 MidiDevice d = m.getMidiDevice();
305 if( d instanceof VirtualMidiDevice || toAdd.remove(d.getDeviceInfo()) ) return false;
306 d.close(); return true;
307 }).collect(Collectors.toList());
309 if( removeAllInternally(toRemove) ) {
310 Set<MidiDeviceModel> rxModels = rxToTxConnections.keySet();
311 rxModels.removeAll(toRemove);
312 rxModels.forEach(m -> rxToTxConnections.get(m).removeAll(toRemove));
315 toAdd.forEach(info -> {
317 addInternally(new MidiDeviceModel(info, this));
318 } catch( MidiUnavailableException e ) {
323 connectDevices(rxToTxConnections);
325 // リスナーに通知してツリー表示を更新してもらう
326 fireTreeStructureChanged(this, null, null, null);
329 * {@link Transmitter}を持つすべてのデバイス(例:MIDIキーボードなど)について、
330 * {@link MidiDeviceModel#resetMicrosecondPosition()}でマイクロ秒位置をリセットします。
332 public void resetMicrosecondPosition() {
333 deviceModelList.stream().map(dm -> dm.getTransmitterListModel())
334 .filter(tlm -> tlm != null)
335 .forEach(tlm -> tlm.resetMicrosecondPosition());