OSDN Git Service

・未保存確認の改善:自動生成したMIDIシーケンスが未保存の場合にも確認ダイアログを出すようにした
[midichordhelper/MIDIChordHelper.git] / src / camidion / chordhelper / mididevice / MidiDeviceTreeModel.java
1 package camidion.chordhelper.mididevice;
2
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;
11 import java.util.Map;
12 import java.util.Set;
13 import java.util.Vector;
14 import java.util.stream.Collectors;
15
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;
29
30 import camidion.chordhelper.ChordHelperApplet;
31
32 /**
33  * 仮想MIDIデバイスを含めたすべてのMIDIデバイスモデル{@link MidiDeviceModel}をツリー構造で管理するモデル。
34  * 読み取り専用のMIDIデバイスリストとしても、
35  * I/Oタイプで分類されたMIDIデバイスツリーモデルとしても参照できます。
36  */
37 public class MidiDeviceTreeModel extends AbstractList<MidiDeviceModel> implements TreeModel {
38         @Override
39         public String toString() { return "MIDI devices"; }
40
41         protected List<MidiDeviceModel> deviceModelList = new Vector<>();
42         @Override
43         public int size() { return deviceModelList.size(); }
44         @Override
45         public MidiDeviceModel get(int index) { return deviceModelList.get(index); }
46         /**
47          * このリストの内容を反映したツリー構造のマップ
48          */
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<>());
54         };
55         /**
56          * {@link AbstractList#add(E)}の操作を内部的に行います。
57          * 指定された要素をこのリストの最後に追加し、ツリー構造にも反映します。
58          *
59          * @param dm 追加するMIDIデバイスモデル
60          * @return true({@link AbstractList#add(E)} と同様)
61          */
62         protected boolean addInternally(MidiDeviceModel dm) {
63                 if( ! deviceModelList.add(dm) ) return false;
64                 deviceModelTree.get(dm.getInOutType()).add(dm);
65                 return true;
66         }
67         /**
68          * {@link AbstractCollection#removeAll(Collection)}の操作を内部的に行います。
69          * 指定されたコレクションに該当するすべての要素を、このリストから削除します。
70          * このリストが変更された場合、ツリー構造にも反映されます。
71          * @param c 削除する要素のコレクション
72          * @return このリストが変更された場合はtrue({@link AbstractCollection#removeAll(Collection)} と同様)
73          */
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));
78                 return true;
79         }
80         @Override
81         public Object getRoot() { return this; }
82         @Override
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();
86                 return 0;
87         }
88         @Override
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);
92                 return null;
93         }
94         @Override
95         public int getIndexOfChild(Object parent, Object child) {
96                 if( parent == getRoot() && child instanceof MidiDeviceInOutType ) {
97                         return ((MidiDeviceInOutType)child).ordinal() - 1;
98                 }
99                 if( parent instanceof MidiDeviceInOutType ) return deviceModelTree.get(parent).indexOf(child);
100                 return -1;
101         }
102         @Override
103         public boolean isLeaf(Object node) { return node instanceof MidiDeviceModel; }
104         @Override
105         public void valueForPathChanged(TreePath path, Object newValue) {}
106         @Override
107         public void addTreeModelListener(TreeModelListener listener) {
108                 listenerList.add(TreeModelListener.class, listener);
109         }
110         @Override
111         public void removeTreeModelListener(TreeModelListener listener) {
112                 listenerList.remove(TreeModelListener.class, listener);
113         }
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)
121                                 );
122                         }
123                 }
124         }
125
126         /**
127          * {@link MidiSystem#getMidiDeviceInfo()} が返した配列を不変の {@link List} として返します。
128          *
129          * <p>注意点:MIDIデバイスをUSBから抜いて、他のデバイスとの接続を切断せずに
130          * {@link MidiSystem#getMidiDeviceInfo()}を呼び出すと
131          * (少なくとも Windows 10 で)Java VM がクラッシュすることがあります。
132          * </p>
133          * @return インストールされているMIDIデバイスの情報のリスト
134          */
135         public static List<MidiDevice.Info> getMidiDeviceInfo() {
136                 return Arrays.asList(MidiSystem.getMidiDeviceInfo());
137         }
138
139         /**
140          * このMIDIデバイスツリーモデルに登録されているMIDIシーケンサーモデルを返します。
141          * @return MIDIシーケンサーモデル
142          */
143         public MidiSequencerModel getSequencerModel() { return sequencerModel; }
144         protected MidiSequencerModel sequencerModel;
145
146         /**
147          * 引数で与えられたGUI仮想MIDIデバイスと、{@link #getMidiDeviceInfo()}から取得したMIDIデバイス情報から、
148          * MIDIデバイスツリーモデルを初期構築します。
149          *
150          * @param guiVirtualDevice 管理対象に含めるGUI仮想MIDIデバイス
151          */
152         public MidiDeviceTreeModel(VirtualMidiDevice guiVirtualDevice) {
153                 MidiDeviceModel synthModel = null;
154                 MidiDeviceModel firstMidiInModel = null;
155                 MidiDeviceModel firstMidiOutModel = null;
156                 // GUI
157                 MidiDeviceModel guiModel = new MidiDeviceModel(guiVirtualDevice, this);
158                 addInternally(guiModel);
159                 // シーケンサー
160                 try {
161                         addInternally(sequencerModel = new MidiSequencerModel(MidiSystem.getSequencer(false), this));
162                 } catch( MidiUnavailableException e ) {
163                         System.out.println(ChordHelperApplet.VersionInfo.NAME +" : MIDI sequencer unavailable");
164                         e.printStackTrace();
165                 }
166                 // システムで使用可能な全MIDIデバイス(シーケンサーはすでに取得済みなので除外)
167                 for( MidiDevice device : getMidiDeviceInfo().stream().map(info -> {
168                         try {
169                                 return MidiSystem.getMidiDevice(info);
170                         } catch( MidiUnavailableException e ) {
171                                 e.printStackTrace();
172                                 return null;
173                         }
174                 }).filter(
175                         device -> device != null && ! (device instanceof Sequencer)
176                 ).collect(Collectors.toList()) ) {
177                         if( device instanceof Synthesizer ) { // Java内蔵シンセサイザの場合
178                                 try {
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");
183                                         e.printStackTrace();
184                                 }
185                                 continue;
186                         }
187                         MidiDeviceModel m = new MidiDeviceModel(device, this);
188                         //
189                         // 最初の MIDI OUT(Windowsの場合は通常、内蔵音源 Microsoft GS Wavetable SW Synth)
190                         if( firstMidiOutModel == null && m.getReceiverListModel() != null ) firstMidiOutModel = m;
191                         //
192                         // 最初の MIDI IN(USB MIDI インターフェースにつながったMIDIキーボードなど)
193                         if( firstMidiInModel == null && m.getTransmitterListModel() != null ) firstMidiInModel = m;
194                         //
195                         addInternally(m);
196                 }
197                 // MIDIデバイスモデルを開く
198                 //
199                 //   NOTE: 必ず MIDI OUT Rx デバイスを先に開くこと。
200                 //
201                 //   そうすれば、後から開いた MIDI IN Tx デバイスからのタイムスタンプのほうが「若く」なるので、
202                 //   相手の MIDI OUT Rx デバイスは「信号が遅れてやってきた」と認識、遅れを取り戻そうとして
203                 //   即座に音を出してくれる。
204                 //
205                 //   開く順序が逆になると「進みすぎるから遅らせよう」として無用なレイテンシーが発生する原因になる。
206                 //
207                 List<MidiDeviceModel> openedMidiDeviceModelList = new ArrayList<>();
208                 Arrays.asList(
209                         synthModel,
210                         firstMidiOutModel,
211                         sequencerModel,
212                         guiModel,
213                         firstMidiInModel
214                 ).stream().filter(mdm -> mdm != null).forEach(mdm->{
215                         try {
216                                 mdm.open();
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);
223                         }
224                 });
225                 // 初期接続マップを作成(開いたデバイスを相互に接続する)
226                 // 自身のTx/Rx同士の接続は、シーケンサーモデルはなし、それ以外(GUIデバイスモデル)はあり。
227                 Map<MidiDeviceModel, Collection<MidiDeviceModel>> initialConnection = new LinkedHashMap<>();
228                 openedMidiDeviceModelList.stream().filter(rxm ->
229                         rxm.getReceiverListModel() != null
230                 ).forEach(rxm -> {
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));
237                 });
238                 // 初期接続を実行
239                 connectDevices(initialConnection);
240         }
241         /**
242          * すべてのMIDIデバイスを閉じます。
243          */
244         public void closeAllDevices() {
245                 deviceModelList.forEach(m -> m.getMidiDevice().close());
246         }
247         /**
248          * デバイス間の接続をすべて切断します。
249          * 各{@link Receiver}ごとに相手デバイスの{@link Transmitter}を閉じ、
250          * その時どのように接続されていたかを示すマップを返します。
251          *
252          * @return MIDIデバイスモデル接続マップ(再接続時に{@link #connectDevices(Map)}に指定可)
253          * <ul>
254          * <li>キー:各{@link Receiver}を持つMIDIデバイスモデル</li>
255          * <li>値:接続相手だった{@link Transmitter}を持つMIDIデバイスモデルのコレクション</li>
256          * </ul>
257          */
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);
265                 });
266                 return rxToTxConnections;
267         }
268         /**
269          * デバイス間の接続を復元します。
270          *
271          * @param rxToTxConnections {@link #disconnectAllDevices()}が返したMIDIデバイスモデル接続マップ
272          * <ul>
273          * <li>キー:{@link Receiver}側デバイスモデル</li>
274          * <li>値:{@link Transmitter}側デバイスモデルのコレクション</li>
275          * </ul>
276          */
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 -> {
281                                 try {
282                                         txm.getTransmitterListModel().openTransmitter().setReceiver(rx);
283                                 } catch( MidiUnavailableException e ) {
284                                         e.printStackTrace();
285                                 }
286                         });
287                 });
288         }
289         /**
290          * USB-MIDIデバイスの着脱後、MIDIデバイスリストを最新の状態に更新します。
291          *
292          * <p>USBからMIDIデバイスを抜いた場合に {@link #getMidiDeviceInfo()} で
293          * Java VM クラッシュが発生する現象を回避するため、更新前に全デバイスの接続を一時切断し、
294          * 更新完了後に接続を復元します。
295          * </p>
296          */
297         public void updateMidiDeviceList() {
298                 // 一時切断
299                 Map<MidiDeviceModel, Collection<MidiDeviceModel>> rxToTxConnections = disconnectAllDevices();
300                 //
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());
308                 // 削除されたデバイスのモデルを除去
309                 if( removeAllInternally(toRemove) ) {
310                         Set<MidiDeviceModel> rxModels = rxToTxConnections.keySet();
311                         rxModels.removeAll(toRemove);
312                         rxModels.forEach(m -> rxToTxConnections.get(m).removeAll(toRemove));
313                 }
314                 // 追加されたデバイスのモデルを登録
315                 toAdd.forEach(info -> {
316                         try {
317                                 addInternally(new MidiDeviceModel(info, this));
318                         } catch( MidiUnavailableException e ) {
319                                 e.printStackTrace();
320                         }
321                 });
322                 // 再接続
323                 connectDevices(rxToTxConnections);
324                 //
325                 // リスナーに通知してツリー表示を更新してもらう
326                 fireTreeStructureChanged(this, null, null, null);
327         }
328         /**
329          * {@link Transmitter}を持つすべてのデバイス(例:MIDIキーボードなど)について、
330          * {@link MidiDeviceModel#resetMicrosecondPosition()}でマイクロ秒位置をリセットします。
331          */
332         public void resetMicrosecondPosition() {
333                 deviceModelList.stream().map(dm -> dm.getTransmitterListModel())
334                         .filter(tlm -> tlm != null)
335                         .forEach(tlm -> tlm.resetMicrosecondPosition());
336         }
337
338 }