001/*
002 * Title:        CoreMIDI4J
003 * Description:  Core MIDI Device Provider for Java on OS X
004 * Copyright:    Copyright (c) 2015-2016
005 * Company:      x.factory Librarians
006 *
007 * @author Derek Cook
008 * 
009 * CoreMIDI4J is an open source Service Provider Interface for supporting external MIDI devices on MAC OS X
010 * 
011 * CREDITS - This library uses principles established by OSXMIDI4J, but converted so it operates at the JNI level with no additional libraries required
012 * 
013 */
014
015package uk.co.xfactorylibrarians.coremidi4j;
016
017import java.util.*;
018
019import javax.sound.midi.*;
020import javax.sound.midi.spi.MidiDeviceProvider;
021
022/**
023 * The OS X CoreMIDI Device Provider
024 *
025 */
026
027public class CoreMidiDeviceProvider extends MidiDeviceProvider implements CoreMidiNotification {
028
029  private static final int DEVICE_MAP_SIZE = 20;
030
031  private static final class MidiProperties {
032
033    private CoreMidiClient client;
034    private CoreMidiOutputPort output;
035    private final Map<Integer, MidiDevice> deviceMap = new LinkedHashMap<>(DEVICE_MAP_SIZE);
036
037  }
038
039  private static final MidiProperties midiProperties = new MidiProperties();
040
041  /**
042   * Initialises the system
043   * 
044   * @throws CoreMidiException if there is a problem communicating with CoreMIDI
045   * 
046   */
047
048  private synchronized void initialise() throws CoreMidiException {
049
050    if ( midiProperties.client == null ) {
051
052      midiProperties.client = new CoreMidiClient("Core MIDI Provider");
053      midiProperties.output = midiProperties.client.outputPortCreate("Core Midi Provider Output");
054      buildDeviceMap();
055
056    }
057
058  }
059
060  /**
061   * Class constructor
062   * 
063   * @throws CoreMidiException if there is a problem initializing the provider
064   * 
065   */
066
067  public CoreMidiDeviceProvider() throws CoreMidiException {
068
069    // If the dynamic library failed to load, leave ourselves in an uninitialised state, so we simply always return
070    // an empty device map.
071    if (isLibraryLoaded()) {
072
073      // If the client has not been initialised then we need to set up the static fields in the class
074      if ( midiProperties.client == null ) {
075
076        initialise();
077
078      }
079
080      midiProperties.client.addNotificationListener(this);
081
082    }
083
084  }
085
086  /**
087   * Builds the device map
088   * 
089   * @throws CoreMidiException if there is a problem communicating with CoreMIDI
090   * 
091   */
092
093  private void buildDeviceMap() throws CoreMidiException {
094
095    Set<Integer> devicesSeen = new HashSet<>();
096
097    // Iterate through the sources
098    for (int i = 0; i < getNumberOfSources(); i++) {
099
100      // Get the end point reference and its unique ID
101      final int endPointReference = getSource(i);
102      final int uniqueID = getUniqueID(endPointReference);
103
104      // Keep track of the IDs of all the devices we see
105      devicesSeen.add(uniqueID);
106
107      // If the unique ID of the end point is not in the map then create a CoreMidiSource object and add it to the map.
108      if ( !midiProperties.deviceMap.containsKey(uniqueID) ) {
109
110        midiProperties.deviceMap.put(uniqueID, new CoreMidiSource(getMidiDeviceInfo(endPointReference)));
111
112      } else {  // We already know about the device, but may need to update its information (e.g. user renamed it).
113
114        CoreMidiSource existingDevice = (CoreMidiSource) midiProperties.deviceMap.get(uniqueID);
115        existingDevice.updateDeviceInfo(getMidiDeviceInfo(endPointReference));
116
117      }
118
119    }
120
121    // Iterate through the destinations
122    for (int i = 0; i < getNumberOfDestinations(); i++) {
123
124      // Get the end point reference and its unique ID
125      final int endPointReference = getDestination(i);
126      final int uniqueID = getUniqueID(endPointReference);
127
128      // Keep track of the IDs of all the devices we see
129      devicesSeen.add(uniqueID);
130
131      // If the unique ID of the end point is not in the map then create a CoreMidiDestination object and add it to the map.
132      if ( !midiProperties.deviceMap.containsKey(uniqueID) ) {
133
134        midiProperties.deviceMap.put(uniqueID, new CoreMidiDestination(getMidiDeviceInfo(endPointReference)));
135
136      } else {  // We already know about the device, but may need to update its information (e.g. user renamed it).
137
138        CoreMidiDestination existingDevice = (CoreMidiDestination) midiProperties.deviceMap.get(uniqueID);
139        existingDevice.updateDeviceInfo(getMidiDeviceInfo(endPointReference));
140
141      }
142
143    }
144
145    // Finally, remove any devices from the map which were no longer available according to CoreMIDI, and close them
146    // appropriately as needed.
147    Set<Integer> devicesInMap = new HashSet<>(midiProperties.deviceMap.keySet());
148
149    for (Integer uniqueID : devicesInMap) {
150
151      if ( !devicesSeen.contains(uniqueID) ) {
152
153        MidiDevice vanishedDevice = midiProperties.deviceMap.remove(uniqueID);
154
155        try {
156
157          if (vanishedDevice instanceof CoreMidiSource) {
158
159            // Must handle specially to avoid trying to interact with defunct CoreMIDI device
160            ((CoreMidiSource) vanishedDevice).deviceDisappeared();
161
162          } else {
163
164            vanishedDevice.close();  // CoreMidiDestination close is safe to call even after the device is gone
165
166          }
167
168        } catch (Exception e) {
169
170          System.err.println("Problem trying to clean up vanished MIDI device " + vanishedDevice + ": " + e);
171          e.printStackTrace();
172          
173        }
174
175      }
176
177    }
178
179  }
180
181  /**
182   * Gets the CoreMidiClient object 
183   * 
184   * @return    The CoreMidiClient object 
185   * 
186   * @throws    CoreMidiException if there is a problem communicating with CoreMIDI
187   * 
188   */
189
190  static CoreMidiClient getMIDIClient() throws CoreMidiException {
191
192    // If the client has not been initialized then we need to setup the static fields in the class
193    if (midiProperties.client == null) {
194
195      new CoreMidiDeviceProvider().initialise();
196
197    }
198
199    return midiProperties.client;
200
201  }
202
203  /**
204   * Gets the output port
205   * 
206   * @return    the output port
207   * 
208   */
209
210  static CoreMidiOutputPort getOutputPort() {
211
212    // If the client has not been initialized then we need to setup the static fields in the class
213    if (midiProperties.output == null) {
214
215      try {
216
217        new CoreMidiDeviceProvider().initialise();
218
219      } catch (CoreMidiException e) {
220
221        e.printStackTrace();
222
223      }
224
225    }
226
227    return midiProperties.output;
228
229  }
230
231
232
233  /** 
234   * Gets information on the installed Core MIDI Devices
235   * 
236   * @return an array of MidiDevice.Info objects
237   * 
238   */
239
240  @Override
241  public MidiDevice.Info[] getDeviceInfo() {
242
243    // If there are no devices in the map, then return an empty array
244    if (midiProperties.deviceMap == null) {
245
246      return new MidiDevice.Info[0];
247
248    }
249
250    // Create the array and iterator
251    final MidiDevice.Info[] info = new MidiDevice.Info[midiProperties.deviceMap.size()];
252    final Iterator<MidiDevice> iterator = midiProperties.deviceMap.values().iterator();
253
254    int counter = 0;
255
256    // Iterate over the device map and populate the array
257    while (iterator.hasNext()) {
258
259      final MidiDevice device = iterator.next();
260
261      info[counter] = device.getDeviceInfo();
262
263      counter += 1;
264
265    }
266
267    return info;
268
269  }
270
271  /** 
272   * Gets the MidiDevice specified by the supplied MidiDevice.Info structure
273   * 
274   * @param     info    The specifications of the device we wish to get
275   * 
276   * @return                    The required MidiDevice
277   * 
278   * @throws                    IllegalArgumentException if the device is not one that we provided
279   * 
280   * @see javax.sound.midi.spi.MidiDeviceProvider#getDevice(javax.sound.midi.MidiDevice.Info)
281   * 
282   */
283
284  @Override
285  public MidiDevice getDevice(MidiDevice.Info info) throws IllegalArgumentException {
286
287    if ( !isDeviceSupported(info) ) {
288
289      throw new IllegalArgumentException();
290
291    }
292
293    return midiProperties.deviceMap.get(((CoreMidiDeviceInfo) info).getEndPointUniqueID());
294
295  }
296
297  /**
298   * Checks to see if the required device is supported by this MidiDeviceProvider
299   * 
300   * @param     info    The specifications of the device we wish to check
301   * 
302   * @return                    true if the device is supported, otherwise false
303   * 
304   * @see javax.sound.midi.spi.MidiDeviceProvider#isDeviceSupported(javax.sound.midi.MidiDevice.Info)
305   * 
306   */
307
308  @Override
309  public boolean isDeviceSupported(final MidiDevice.Info info) {
310
311    boolean foundDevice = false;
312
313    // The device map must be created and the info object must be a CoreMIDIDeviceInfo object 
314    if ( ( midiProperties.deviceMap != null ) && ( info instanceof CoreMidiDeviceInfo ) ) {
315
316      // Search for the device info UID within the device map
317      if (midiProperties.deviceMap.containsKey(((CoreMidiDeviceInfo)info).getEndPointUniqueID())) {
318
319        foundDevice = true;
320
321      }
322
323    }
324
325    return foundDevice;
326
327  }
328
329  /**
330   * Called when a change in the MIDI environment occurs
331   *
332   * @throws CoreMidiException if a problem occurs rebuilding the map of available MIDI devices
333   *
334   */
335
336  public void midiSystemUpdated() throws CoreMidiException {
337
338    // Update the device map
339    buildDeviceMap();
340
341  }
342
343  /**
344   * Adds a notification listener to the listener set maintained by this class. If it is a
345   * {@link CoreMidiDeviceProvider}, only keep the most recent one, since Java will create many,
346   * and we only want to update the device map once when the MIDI environment changes.
347   * 
348   * @param listener    The CoreMidiNotification listener to add
349   * 
350   * @throws                                    CoreMidiException if it is not possible to provide change notifications
351   * 
352   */
353
354  public static void addNotificationListener(CoreMidiNotification listener) throws CoreMidiException {
355
356    // If the dynamic library failed to load, we cannot provide notifications
357    if (!isLibraryLoaded()) {
358
359      throw new CoreMidiException("libCoreMidi4J.dylib could not be loaded, CoreMIDI4J is not active.");
360
361    }
362
363    // If the client has not been initialised then we need to setup the static fields in the class
364    if (midiProperties.client == null) {
365
366      new CoreMidiDeviceProvider().initialise();
367
368    }
369
370    midiProperties.client.addNotificationListener(listener);
371
372  }
373
374  /**
375   * Removes a notification listener from the listener list maintained by this class
376   * 
377   * @param listener    The CoreMidiNotification listener to remove
378   * 
379   * @throws                                    CoreMidiException when we are unable to offer change notifications
380   * 
381   */
382
383  public static void removeNotificationListener(CoreMidiNotification listener) throws CoreMidiException {
384
385    // If the dynamic library failed to load, we cannot provide notifications
386    if (!isLibraryLoaded()) {
387
388      throw new CoreMidiException("libCoreMidi4J.dylib could not be loaded, CoreMIDI4J is not active.");
389
390    }
391
392    // If the client has not been initialised then we need to setup the static fields in the class
393    if (midiProperties.client == null) {
394
395      new CoreMidiDeviceProvider().initialise();
396
397    }
398
399    midiProperties.client.removeNotificationListener(listener);
400
401  }
402
403  /**
404   * Check whether we have been able to load the native library.
405   *
406   * @return true if the library was loaded successfully, and we are operational, and false if the library was
407   *         not available, so we are idle and not going to return any devices or post any notifications.
408   *
409   * @throws CoreMidiException if something unexpected happens trying to load the native library on a Mac OS X system.
410   */
411  
412  public static boolean isLibraryLoaded() throws CoreMidiException {
413
414    return Loader.isAvailable();
415
416  }
417
418  /**
419   * Determine the version of the library which is being used.
420   *
421   * @return the implementation version of the library, as compiled into the JAR manifest.
422   * @since 0.9
423   */
424
425  public static String getLibraryVersion() {
426
427    return Package.getPackage("uk.co.xfactorylibrarians.coremidi4j").getImplementationVersion();
428
429  }
430
431  /**
432   * Obtains an array of information objects representing the set of all working MIDI devices available on the system.
433   * This is a replacement for javax.sound.midi.MidiSystem.getMidiDeviceInfo(), and only returns fully-functional
434   * MIDI devices. If you call it on a non-Mac system, it simply delegates to the javax.sound.midi implementation.
435   * On a Mac, it calls that function, but filters out the broken devices, returning only the replacement versions
436   * that CoreMidi4J provides. So by using this method rather than the standard one, you can give your users a
437   * menu of MIDI devices which are guaranteed to properly support MIDI System Exclusive messages.
438   *
439   * A returned information object can then be used to obtain the corresponding device object,
440   * by invoking javax.sound.midi.MidiSystem.getMidiDevice().
441   *
442   * @return an array of MidiDevice.Info objects, one for each installed and fully-functional MIDI device.
443   *         If no such devices are installed, an array of length 0 is returned.
444   */
445
446  public static MidiDevice.Info[] getMidiDeviceInfo() {
447
448    MidiDevice.Info[] allInfo = MidiSystem.getMidiDeviceInfo();
449
450    try {
451
452      if (isLibraryLoaded()) {
453
454        List<MidiDevice.Info> workingDevices = new ArrayList<>(allInfo.length);
455        
456        for (MidiDevice.Info candidate : allInfo) {
457
458          try {
459
460            MidiDevice device = MidiSystem.getMidiDevice(candidate);
461
462            if ( (device instanceof Sequencer) || 
463                 (device instanceof Synthesizer) ||
464                 (device instanceof CoreMidiDestination) || 
465                 (device instanceof CoreMidiSource) ) {
466
467              workingDevices.add(candidate);  // A working device, include it
468
469            }
470          
471          } catch (MidiUnavailableException e) {
472
473            System.err.println("Problem obtaining MIDI device which supposedly exists:" + e.getMessage());
474
475          }
476       
477        }
478
479        return workingDevices.toArray(new MidiDevice.Info[workingDevices.size()]);
480
481      }
482
483    } catch (CoreMidiException e) {
484
485      System.err.println("Problem trying to determine native library status:" + e.getMessage());
486
487    }
488
489    return allInfo;
490
491  }
492
493
494  //////////////////////////////
495  ///// JNI Interfaces
496  //////////////////////////////
497
498  /*
499   * Static initializer for loading the native library
500   *
501   */
502
503  static {
504
505    try {
506
507      Loader.load();
508
509    } catch (Throwable t) {
510
511      System.err.println("Unable to load native library, CoreMIDI4J will stay inactive: " + t);
512
513    }
514
515  }
516
517  /**
518   * Gets the number of sources supported by the system
519   * 
520   * @return    The number of sources supported by the system
521   * 
522   */
523
524  private native int getNumberOfSources();
525
526  /**
527   * Gets the number of destinations supported by the system
528   * 
529   * @return    The number of destinations supported by the system
530   * 
531   */
532
533  private native int getNumberOfDestinations();
534
535  /**
536   * Gets the specified MIDI Source EndPoint
537   * 
538   * @param sourceIndex The index of the source to get
539   * 
540   * @return                                            The specified MIDI Source EndPoint
541   * 
542   * @throws                                            CoreMidiException if the source index is not valid 
543   * 
544   */
545
546  private native int getSource(int sourceIndex) throws CoreMidiException;
547
548  /**
549   * Gets the specified MIDI Destination EndPoint (JNI).
550   * 
551   * @param destinationIndex    The index of the destination to get
552   * 
553   * @return The specified MIDI Destination EndPoint
554   * 
555   * @throws CoreMidiException if the destination index is not valid 
556   * 
557   */
558
559  private native int getDestination(int destinationIndex) throws CoreMidiException;
560
561  /**
562   * Gets the unique ID for an object reference (JNI).
563   * 
564   * @param reference   The reference to the object to get the UID for 
565   * 
566   * @return    The UID of the referenced object
567   * 
568   * @throws CoreMidiException if there is a problem communicating with CoreMIDI
569   * 
570   */
571
572  private native int getUniqueID(int reference) throws CoreMidiException; 
573
574  /**
575   * Gets a MidiDevice.Info class for the specified reference (JNI).
576   * 
577   * @param reference   The Core MIDI endpoint reference to create a MidiDevice.Info class for
578   * 
579   * @return                                    The created MidiDevice.Info class
580   * 
581   * @throws                                    CoreMidiException if the reference is not valid
582   * 
583   */
584
585  private native CoreMidiDeviceInfo getMidiDeviceInfo(int reference) throws CoreMidiException;
586
587}