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}