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.*; 018import java.util.concurrent.ConcurrentHashMap; 019import java.util.concurrent.atomic.AtomicBoolean; 020import java.util.concurrent.atomic.AtomicInteger; 021 022/** 023 * CoreMidiClient class 024 * 025 */ 026 027public class CoreMidiClient { 028 029 private final int midiClientReference; 030 031 private final Set<CoreMidiNotification> notificationListeners = 032 Collections.newSetFromMap(new ConcurrentHashMap<CoreMidiNotification, Boolean>()); 033 034 /** 035 * Keeps track of the latest {@link CoreMidiDeviceProvider} added to our listener list; this is the only one that 036 * we want to call when the MIDI environment changes, since we only need to update the device map once, and Java 037 * creates a vast number of instances of our device provider. 038 */ 039 private CoreMidiNotification mostRecentDeviceProvider = null; 040 041 /** 042 * Constructor for class 043 * 044 * @param name The name of the client 045 * 046 * @throws CoreMidiException if the client cannot be initialized 047 * 048 */ 049 050 public CoreMidiClient(String name) throws CoreMidiException { 051 052 midiClientReference = this.createClient(name); 053 054 } 055 056 /** 057 * Creates a new CoreMidiInputPort 058 * 059 * @param name The name of the port 060 * 061 * @return A new CoreMidiInputPort 062 * 063 * @throws CoreMidiException if the port cannot be created 064 * 065 */ 066 067 public CoreMidiInputPort inputPortCreate(final String name) throws CoreMidiException { 068 069 return new CoreMidiInputPort(midiClientReference,name); 070 071 } 072 073 /** 074 * Creates a new CoreMidiOutputPort 075 * 076 * @param name The name of the port 077 * 078 * @return A new CoreMidiOutputPort 079 * 080 * @throws CoreMidiException if the port cannot be created 081 * 082 */ 083 084 public CoreMidiOutputPort outputPortCreate(final String name) throws CoreMidiException { 085 086 return new CoreMidiOutputPort(midiClientReference,name); 087 088 } 089 090 /** 091 * The message callback for receiving notifications about changes in the MIDI environment from the JNI code. 092 * <p>A reference to this method is created by the C++ method 093 * Java_uk_co_xfactorylibrarians_coremidi4j_CoreMidiClient_createClient 094 * which in turn is called by {@link #createClient(String) }.</p> 095 * 096 * <p>Used to make sure we are only running one callback delivery loop at a time without having to serialize the process 097 * in a way that will block the actual CoreMidi callback.</p> 098 */ 099 100 private final AtomicBoolean runningCallbacks = new AtomicBoolean(false); 101 102 /** 103 * Used to count the number of CoreMidi environment change callbacks we have received, so that if additional ones 104 * come in while we are delivering callback messages to our listeners, we know to start another round at the end. 105 */ 106 107 private final AtomicInteger callbackCount = new AtomicInteger( 0); 108 109 /** 110 * Check whether we are already in the process of delivering callbacks to our listeners; if not, start a background 111 * thread to do so, and at the end of that process, see if additional callbacks were attempted while it was going on. 112 */ 113 114 private void deliverCallbackToListeners() { 115 116 final int initialCallbackCount = callbackCount.get(); 117 118 if (runningCallbacks.compareAndSet(false, true)) { 119 120 new Thread(new Runnable() { 121 122 @Override 123 public void run() { 124 125 try { 126 127 int currentCallbackCount = initialCallbackCount; 128 while ( currentCallbackCount > 0 ) { // Loop until no new callbacks occur while delivering a set. 129 130 // Iterate through the listeners (if any) and call them to advise that the environment has changed. 131 final Set<CoreMidiNotification> listeners = Collections.unmodifiableSet(new HashSet<>(notificationListeners)); 132 133 // First notify the CoreMidiDeviceProvider object itself, so that the device map is 134 // updated before any other listeners, from client code, are called. 135 if (mostRecentDeviceProvider != null) { 136 137 try { 138 139 mostRecentDeviceProvider.midiSystemUpdated(); 140 141 } catch (CoreMidiException e) { 142 143 throw new RuntimeException("Problem delivering MIDI environment change notification to CoreMidiDeviceProvider" , e); 144 145 } 146 147 } 148 149 // Finally, notify any registered client code listeners, now that the device map is properly up to date. 150 for ( CoreMidiNotification listener : listeners ) { 151 152 try { 153 154 listener.midiSystemUpdated(); 155 156 } catch (CoreMidiException e) { 157 158 throw new RuntimeException("Problem delivering MIDI environment change notification" , e); 159 160 } 161 162 } 163 164 synchronized (CoreMidiClient.this) { 165 166 // We have handled however many callbacks occurred before this iteration started 167 currentCallbackCount = callbackCount.addAndGet( -currentCallbackCount ); 168 169 if ( currentCallbackCount < 1 ) { 170 171 runningCallbacks.set(false); // We are terminating; if blocked trying to start another, allow it. 172 173 } 174 175 } 176 177 } 178 179 } finally { 180 181 runningCallbacks.set(false); // Record termination even if we exit due to an uncaught exception. 182 183 } 184 185 } 186 187 }).start(); 188 189 } 190 } 191 192 /** 193 * The message callback for receiving notifications about changes in the MIDI environment from the JNI code 194 * 195 * @throws CoreMidiException if a problem occurs passing along the notification 196 * 197 */ 198 199 public void notifyCallback() throws CoreMidiException { 200 201 // Debug code - uncomment to see this function being called 202 //System.out.println("** CoreMidiClient - MIDI Environment Changed"); 203 204 synchronized(this) { 205 206 callbackCount.incrementAndGet(); // Record that a callback has come in. 207 deliverCallbackToListeners(); // Try to deliver callback notifications to our listeners. 208 209 } 210 211 } 212 213 /** 214 * Adds a notification listener to the listener set maintained by this class 215 * 216 * @param listener The CoreMidiNotification listener to add 217 * 218 */ 219 220 public void addNotificationListener(CoreMidiNotification listener) { 221 222 if ( listener != null ) { 223 224 // Our CoreMidiDeviceProvider is a special case, we only want to notify a single instance of that, even though 225 // Java keeps creating new ones. So keep track of the most recent instance registered, do not add it to the list. 226 if (listener instanceof CoreMidiDeviceProvider) { 227 228 mostRecentDeviceProvider = listener; 229 230 } else { 231 232 notificationListeners.add(listener); 233 234 } 235 236 } 237 238 } 239 240 /** 241 * Removes a notification listener from the listener set maintained by this class 242 * 243 * @param listener The CoreMidiNotification listener to remove 244 * 245 */ 246 247 public void removeNotificationListener(CoreMidiNotification listener) { 248 249 notificationListeners.remove(listener); 250 251 } 252 253 ////////////////////////////// 254 ///// JNI Interfaces 255 ////////////////////////////// 256 257 /* 258 * Static initializer for loading the native library 259 * 260 */ 261 262 static { 263 264 try { 265 266 Loader.load(); 267 268 } catch (Throwable t) { 269 270 System.err.println("Unable to load native library, CoreMIDI4J will stay inactive: " + t); 271 272 } 273 274 } 275 276 /** 277 * Creates the MIDI Client 278 * 279 * @param clientName The name of the client 280 * 281 * @return A reference to the MIDI client 282 * 283 * @throws CoreMidiException if the client cannot be created 284 * 285 */ 286 287 private native int createClient(String clientName) throws CoreMidiException; 288 289 /** 290 * Disposes of a CoreMIDI Client 291 * 292 * @param clientReference The reference of the client to dispose of 293 * 294 * @throws CoreMidiException if there is a problem disposing of the client 295 * 296 */ 297 298 private native void disposeClient(int clientReference) throws CoreMidiException; 299 300}