MA Lighting grandMA2

Alex Hughes inquired if it would be possible to use a trigger to synchronize a speed master for effects on a grandMA2 lighting control system. With his help and pointers to the relevant documentation, we were able to achieve that. The approach is described and explained in detail below, or you can start by downloading the corresponding configuration file and loading that within Beat Link Trigger.

If you already have triggers of your own that you want to keep, be sure to save your configuration before opening another one! In that case you may want to export your triggers, or manually cut and paste the relevant pieces of code into your Triggers window Shared Functions.

To begin with, paste this block of code into Beat Link Trigger’s Shared Functions:

(defn gm-response-handler
  "A loop that reads messages from grandMA2 and responds
  appropriately. (Currently we don’t respond in any way, but simply
  consume responses as they arrive.)"
  []
  (try
    (loop [socket (get-in @globals [:gm-connection :socket])]
      (when (and socket (not (.isClosed socket)))
        (let [buffer (byte-array 1024)
              input  (.getInputStream socket)
              n      (.read input buffer)]
          (when (pos? n)  ; We got data, so the socket has not yet been closed.
            (let [message (String. buffer 0 n "UTF-8")]
              (timbre/info "Received from grandMA2:" message)
              ;; TODO: Here is where we would analyze and respond if needed;
              ;;       see the ShowXPress example.
              )
            (recur (get-in @globals [:gm-connection :socket]))))))
    (catch Throwable t
      (timbre/error t "Problem reading from grandMA2, loop aborted."))))

(defn send-gm-command
  "Sends a command message to grandMA2."
  [message]
  (let [socket (get-in @globals [:gm-connection :socket])]
    (if (and socket (not (.isClosed socket)))
      (.write (.getOutputStream socket) (.getBytes (str message "\r\n")
              "UTF-8"))
      (timbre/warn
       "Cannot write to grandMA2, no open socket, discarding:" message))))

(defn set-gm-tempo
  "Tells grandMA2 the current tempo if it is different than the
  value we last reported. Rounds to the nearest beat per minute
  because the protocol does not accept any fractional values.
  The expected way to use this is to include the following in a
  trigger’s Tracked Update Expression:

  `(when trigger-active? (set-gm-tempo effective-tempo))`"
  [bpm]
  (let [bpm    (Math/round bpm)
        master (get-in @globals [:gm-connection :bpm-master])]
    (when-not (= bpm (get-in @globals [:gm-connection :bpm]))
      (send-gm-command (str "SpecialMaster " master " At " bpm))
      (swap! globals assoc-in [:gm-connection :bpm] bpm)
      (timbre/info "grandMA2 tempo set to" bpm))))

;; An alternate approach. You probably only want to use one of set-gm-tempo
;; (above) and send-gm-beat (below), whichever works best in your setup.

(defn send-gm-beat
  "Sends a learn command to grandMA2. The expected way to use this is
  to include the following in a trigger’s Beat Expresssion:

  `(when trigger-active? (send-gm-beat))`"
  []
  (let [master (get-in @globals [:gm-connection :bpm-master])]
    (send-gm-command (str "Learn SpecialMaster " master))))

Then paste this setup code in to the Global Setup Expression (notice how it builds on the Shared Functions we just created):

;; Attempt to connect to the grandMA2 telnet command port.
;; Edit the variable definitions below to reflect your setup.
(try
  (let [gm-address      "127.0.0.1"
        gm-port         30000
        gm-user         "Administrator"
        gm-password     "admin"
        gm-speedmaster  "3.1"
        connect-timeout 5000
        socket-address  (InetSocketAddress. gm-address gm-port)
        socket          (java.net.Socket.)]
    (.connect socket socket-address connect-timeout)
    (swap! globals assoc :gm-connection {:socket socket
                                         :bpm-master gm-speedmaster})
    (future (gm-response-handler))
    (send-gm-command (str "login \"" gm-user "\" \"" gm-password "\"")))
  (catch Exception e
    (timbre/error e "Unable to connect to grandMA2")))
You will want to edit the values assigned to gm-address, gm-port, gm-user, gm-password, and gm-speedmaster to match your setup. This code assumes that the lighting desk is already running and configured to listen on the specified port before you launch Beat Link Trigger. If nothing seems to be working, check the log file for error messages, and see if the login process was successful. Unfortunately, there is no friendly user interface to tell it to try again if it was not, but you can do so by editing the Global Setup Expression and saving it—​even without making any changes, that will run both the shutdown and setup code again for you.

Also paste this smaller block of code into the Global Shutdown Expression:

;; Disconnect from the grandMA2 telnet command port.
(when-let [socket (get-in @globals [:gm-connection :socket])]
  (.close socket)
  (swap! globals dissoc :gm-connection))

With these in place, Beat Link Trigger will maintain a connection to the lighting desk command port while it runs, and make a new set of functions available to all your trigger expressions which make it easy to send tempo information and other commands.

If you want to control the speed master to match the tempo of the Pioneer network, it is probably easiest to set up a single trigger to Watch the Master Player, and set its Tracked Update Expression to:

(when trigger-active? (set-gm-tempo effective-tempo))

Whenever you have this trigger enabled, it will slave the value of the configured grandMA2 SpecialMaster to the tempo of the Master Player. To have the speed set to zero when playback stops, set the trigger’s Deactivation Expression to:

(set-gm-tempo 0)

If you have other things that you want to happen when particular tracks start or stop playing or reach particular sections, then you want to set up a Show. If you really want to do it the older, more complicated way, then you can set up other triggers that send whatever commands you like in their Activation and Deactivation expressions using the send-gm-command function that was created by the Global Setup Expression, and configure them to be active only when a CDJ is playing a particular track, or is within a particular range of beats within that track, as shown in Matching Tracks. This allows you to have certain looks called up automatically when the right parts of the right tracks are played. But honestly, learn about Shows, they are so much easier to set up, and do all that work for you!

If you jumped to this section to learn about how to integrate the lighting desk with CDJs, and you think it looks promising, you will want to go back and read this entire user guide to get a better understanding of how to make your triggers activate, and the other things you can do with Beat Link Trigger.