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}