 (defonce ^{:private true
            :doc "Functions to be called when MIDI Aftertouch
   (polyphonic key pressure) messages arrive from particular input
-  ports. A set of nested maps whose keys are the
-  `javax.sound.midi.MidiDevice` on which the message should be watched
-  for, the channel to watch, and the note number to watch for. The
-  values are sets of functions to be called with each matching
-  message."}
-  aftertouch-mappings (atom {}))
+  ports. A set of nested maps whose keys are the [[midi-device-key]]
+  on which the message should be watched for, the channel to watch,
+  and the note number to watch for. The values are sets of functions
+  to be called with each matching message."} aftertouch-mappings (atom
+  {}))
 
 (defonce ^{:private true
            :doc "Functions to be called when MIDI System Exclusive
   messages arrive from particular input ports. A map whose keys are
-  the `javax.sound.midi.MidiDevice` on which the message should be
-  watched for. The values are sets of functions to be called with each
-  matching message."} sysex-mappings (atom {}))
+  the [[midi-device-key]] on which the message should be watched for.
+  The values are sets of functions to be called with each matching
+  message."} sysex-mappings (atom {}))
+
+(defonce ^{:private true
+           :doc "Functions to be called when any MIDI message is
+  received from a specific device. A map whose keys are the
+  [[midi-device-key]] on which the message should be watched for, and
+  whose values are sets of functions to be called with each matching
+  message."}
+  device-mappings (atom {}))
 
 (defonce ^{:private true
            :doc "Functions to be called when any MIDI message at all

  {:pre [(= (type device) :midi-device) (fn? f)]}
  (swap! disconnected-device-handlers #(update-in % [(@midi-device-key device)] disj f)))

(defonce ^{:private true
           :doc "The metronomes which are being synced to MIDI clock
  pulses. A map whose keys are the `javax.sound.midi.MidiDevice` on
  which clock pulses are being received, and whose values are in turn
  maps whose keys are the metronomes being synced to pulses from that
  device, and whose values are the sync function to call when each
  pulse is received."} synced-metronomes (atom {}))

(defonce ^{:private true
           :doc "Functions to be called when MIDI Controller Change
  messages arrive from particular input ports. A set of nested maps
  whose keys are the `javax.sound.midi.MidiDevice` on which the
  message should be watched for, the channel to watch, and the
  controller number to watch for. The values are sets of functions to
  be called with each matching message."}
  control-mappings (atom {}))

(defonce ^{:private true
           :doc "Functions to be called when MIDI Note messages arrive
  from particular input ports. A set of nested maps whose keys are the
  `javax.sound.midi.MidiDevice` on which the message should be watched
  for, the channel to watch, and the note number to watch for. The
  values are sets of functions to be called with each matching
  message."}

            ;; First call the global message handlers
            (doseq [handler @global-handlers]
              (when @running
                (run-message-handler handler msg running)))
            
            ;; Then call any registered port listeners for the port on which
            ;; it arrived

            ;; Then call specific message handlers that match
            (when @running
              (cond (#{:timing-clock :start :stop} (:status msg))
                    (clock-message-handler msg running)

                    (= 0xf0 (:status msg))
                    (sysex-message-handler msg running)

                    :else
                    (case (:command msg)
                      :control-change (do (cc-message-handler msg running)
                                          (clock-message-handler msg running)) ; Might be Traktor beat phase message
                      (:note-on :note-off) (note-message-handler msg running)
                      :poly-pressure (aftertouch-message-handler msg running)
                      nil)))

            ;; Finally, keep track of any MIDI clock messages we have seen so the
            ;; user can be informed of them as potential sync sources.
            (run-message-handler watch-for-clock-sources msg running))))

      ;; If we have not been shut down, do it all again for the next message.
      (when @running (recur (.take midi-queue))))))
