Integration Examples

Overview

When explaining the Beat Link Trigger interface and how to extend it with custom expressions, we showed some examples of ways to integrate other systems. This section builds on the concepts introduced there, to demonstrate useful and practical integrations with systems we (or our early adopters) use regularly.

Pangolin BEYOND Advanced

Pangolin BEYOND is such flexible and powerful laser show software that Deep Symmetry invested in a Windows virtual machine purely to be able to use it to control our best laser projector. With an Advanced license, you can send it PangoScript commands over the network to achieve a deep level of integration with other systems. Here are some ways you can use it with Beat Link Trigger.

This section shows how to achieve tight integration using the PangoTalk UDP server, which requires BEYOND Advanced, but you can use MIDI with BEYOND Essentials to get decent tempo tracking and basic cue triggers, as described below.

To begin with, in the Shared Functions, we tell Beat Link Trigger how to communicate with BEYOND, by specifying the broadcast address of the network interface it is listening on, and the port on which the BEYOND Talk UDP server is listening. To determine these things, you can choose Tools  Network Monitor…​ within BEYOND to bring up a window like this:

Network Monitor

By looking at the Adapter IP and Mask lines, we can determine that the broadcast address we want to use to reach the BEYOND Talk server is 172.16.1.255.

In versions of BEYOND prior to 2.1, it was possible to send UDP unicast messages directly to the Adapter IP address. however, starting with version 2.1, you must actually send UDP broadcast packets to the broadcast address of the subnet the server is attached to.

Then, make sure the BEYOND UDP Talk server is enabled (Settings  Network  Network Settings…​):

Network Settings

Choose a port that is not in use by anything else on your system (the default of 16062 is likely fine), check the Enable Talk Server check box, and click OK. Make a note of the broadcast address and UDP port it is listening on, and then make sure the talk server is fully enabled by choosing Settings  Network  BEYOND Talk server:

Talk Server

In older versions of BEYOND, we sometimes had to quit and restart the program after making these configuration changes in order for them to take effect. That is probably no longer true, but we mention this as a potential trouleshooting step. You can also test connectivity using a tool like Packet Sender to send commands like SetBpm 123.4\r\n as UDP packets to the broadcast address and port you determined above, verifying that BEYOND’s BPM updates to the value that you sent. Packet Sender also has a Subnet Calculator found at Tools  Subnet Calculator that can help you determine the broadcast address.

Once you have the UDP Talk server up and working, edit Beat Link Trigger’s Shared Functions to use the broadcast address and port to define a new function, beyond-command, that your other expressions will be able to use to send PangoScript commands to it:

(let [beyond-address (InetSocketAddress. (InetAddress/getByName "172.16.1.255") 16062)
      send-socket (DatagramSocket.)]
   (defn beyond-command
     "Sends a PangoScript command to the configured BEYOND Talk server."
     [command]
     (let [payload (str command \return \newline)
           packet (DatagramPacket. (.getBytes payload) (.length payload) beyond-address)]
       (.send send-socket packet))))
Of course, replace the address and port in the first line with the correct values to use for your BEYOND UDP Talk server.

With that in place, we are ready to integrate laser shows. First, let’s see how to have the tempo within BEYOND always precisely match the tempo of your master player.

Laser Show Tempo Synchronization

Create a new Trigger in Beat Link Trigger (Triggers  New Trigger) and label it something like “Beyond BPM Sync” in the Comment field. Configure it to Watch the Master Player, and give it a Custom Enabled Filter:

Beyond BPM Sync

The Enabled Filter editor will pop open, so you can paste in the following code:

(swap! locals update-in [:beyond-bpm]
       (fn [old-bpm]
         (when (not= effective-tempo old-bpm)
           (beyond-command (str "SetBpm " effective-tempo)))
         effective-tempo))
nil  ;; Never need to actually activate.

What this function will do is look at every status update packet that is received from the Master Player, and see if the BPM being reported is different from what we last told BEYOND to use (it tracks this in a value stored in the trigger locals map under the key :beyond-bpm, and the first time the expression is called, nothing will be found there, so it will always start by sending the current BPM value to BEYOND).

When the current tempo is different from what we have sent to BEYOND, we use the beyond-command function that we defined in the Shared Functions to send a SetBpm command to BEYOND, containing the current tempo at which the Master Player is playing. If there is no difference, we send nothing, because BEYOND is already at the right tempo. Either way, we record the current effective tempo in the locals map for use when the next update packet is received.

Finally, the expression always returns nil, because there is never any reason for it to be enabled. It is not actually triggering anything in response to a particular track playing, it is simply always keeping BEYOND’s tempo tied to the master player. (For the same reason, it doesn’t matter what you choose in the MIDI Output, Message, and Channel menus; they will never be sent.)

Once you have this expression saved, try playing a track on the Master Player, adjust the pitch fader, and watch BEYOND smoothly and precisely track the BPM of the music being played.

Triggering a Laser Cue

With this framework in place, it is very easy to have a laser cue controlled by a trigger. Create another new Trigger, label it to describe the cue you want it to control, and set it up to be activated when an interesting track reaches an interesting beat, using the techniques described above. The only thing you need to do different is set the Message menu to Custom, so it will send its Activation message to Beyond’s Talk server rather than a MIDI message.

Actually, you can map MIDI and OSC messages to BEYOND cues, so once you have the BPM sync working, feel free to go that route if you prefer. But since we already have a Talk server running, here is how to use it.

The easiest way to identify the proper PangoScript message to use to refer to a particular cue is to take advantage of a special mode of the BEYOND laser preview window that shows you all the internal PangoScript messages it is sending itself when you interact with its user interface. Choose Settings  Configuration…​ and click the Laser Preview tab. Check the Display Internal BEYOND Command check box, and click OK:

Beyond Laser Preview configuration

One that is done, as you interact with the interface, you will see small messages at the bottom left of the laser preview section showing you the equivalent PangoScript command for what you just did:

Beyond Laser Preview

In this case, I just activated cue 16, 20 (cue 20 on page 16). So in the trigger’s Activation Expression editor, I would use the following:

(beyond-command "StartCue 16,20")

And finally, adding the corresponding Deactivation Expression rounds out the trigger:

(beyond-command "StopCue 16,20")

With that in place, whenever this trigger activates, the specified BEYOND laser cue will start, and whenever the trigger deactivates, so will the laser cue. And when combined with the tempo synchronization set up in the previous section, the cue will look great with the music.

Pangolin BEYOND Essentials

To use the power of the PangoTalk UDP server, you need a BEYOND Advanced license. But even with just BEYOND Essentials, you can use MIDI mapping to achieve basic tempo synchronization and cue triggering with Beat Link Trigger. Here are some pointers about how to do that.

MIDI and Windows

Because BEYOND Essentials runs on Windows, which has no built-in support for routing MIDI between applications on the same machine or over the network, you need to add some other software to allow Beat Link Trigger to send MIDI to it.

Single Machine

If you are running both programs on the same machine, you can use LoopBe1 to create a virtual MIDI port that Beat Link Trigger can use to send messages to BEYOND Essentials.

Networked Machines

If you want to run Beat Link Trigger on a different machine than BEYOND Essentials, then you can use rtpMIDI to send MIDI messages between them. (If both machines are Windows, you need to install rtpMIDI on each one. If you are running Beat Link Trigger on a Mac, it already has native Core MIDI network support, which rtpMIDI is designed to be compatible with.)

See the documentation of LoopBe1 and/or rtpMIDI for instructions on how to install, configure, and use it.

Connecting to BEYOND Essentials

Once the virtual or network MIDI port is available on the machine that BEYOND is running on, you need to connect it as one of the MIDI devices that BEYOND is watching. Open up the MIDI Devices settings by choosing Settings  MIDI  Device Settings…​ Here, the new LoopBe virtual port has been chosen for input and output as Device 1 within Beyond:

LoopBe Internal MIDI chosen for Device 1

Click OK and the device will be available as a source of MIDI messages.

Tempo to BEYOND via MIDI

To enable Beat Link Trigger to adjust BEYOND’s tempo using MIDI Clock messages, right-click on the metronome at the top of the BEYOND window, and click the Enable MIDI input to set BPM button in the contextual menu that appears:

Enabling MIDI to set BPM

Once that is done, you can configure a Trigger in Beat Link Trigger to send MIDI output to the device that BEYOND is listening to, and set the Message menu to Clock, so it will send MIDI Clock messages to communicate the current BPM:

Clock Trigger for BEYOND

BEYOND synced to MIDI Clock Once that trigger activates, the BPM display in BEYOND will turn yellow and will track the tempo of the track that activated the trigger, although not quite as precisely as it can using the PangoTalk server, since MIDI clock is a less direct way of communicating it.

If you don’t want Beat Link Trigger to send Start or Stop messages when the trigger activates and deactivates, you can uncheck the corresponding check boxes in the trigger row. You may want to experiment to see how BEYOND responds to them, or ask an expert in BEYOND MIDI integration.

Triggering Laser Cues via MIDI

Once you have the MIDI connection established, getting cues to run when triggers are active is fairly straightforward. You just have to assign each trigger a unique MIDI Note or Controller number, and then map that to the appropriate cue cell in BEYOND.

The screen capture below shows the addition of a basic MIDI Note trigger to the clock trigger from the previous example. This new trigger will send a MIDI Note On message for note 125 on channel 1 when the trigger activates, and the corresponding Note Off message when it deactivates:

Beyond MIDI Trigger

To tie that to a cue cell in BEYOND, choose Settings  MIDI  "(device)" settings…​, picking the name of the device that you connected in order to receive MIDI messages from Beat Link Trigger:

Beyond MIDI Device Settings

That will open a window that gives you access to a great many MIDI mapping options, allowing you to cause BEYOND to react to incoming MIDI events in different ways. For much more information about it, see the BEYOND MIDI Settings manual section, accessible through Help  Documentation  Settings  MIDI settings. In this example we’ll just take a quick look at mapping the first cue cell to respond to the Beat Link Trigger we have just created. To do that, click the Configure…​ button for the Main Grid MIDI surface:

Beyond MIDI Mapping

This section allows you to set the MIDI messages which BEYOND will interpret as a mouse down or mouse up event in each of the cue cells. If you happen to know that the MIDI message we chose above corresponds to the hexadecimal numbers 90 7d 7f for the Note 125 On (with velocity 127) and 80 7d 00 for the Note 125 Off, you could double-click in those cells and enter the values directly. Far more likely, you will select the Cell Down box for the cell you want the trigger to affect, then click the Learn 1+2 button, and while BEYOND is in Learn mode, activate and deactivate the trigger in Beat Link Trigger. The Learn 1+2 command tells BEYOND to watch for the next two MIDI events and enter them into the grid cells for you:

Beyond MIDI Main Grid

Once you have that mapping set up, whenever Beat Link Trigger reports that the trigger is activated, BEYOND will act as though you have clicked the mouse in the first cue cell, and when the trigger is deactivated, BEYOND will act as though you have released the mouse. In order to have cues end when triggers deactivate, you will want to put BEYOND into Flash mode:

Beyond Flash Cue Mode

Alternately, if you want to leave it in the default Toggle mode, you could use a custom Deactivation Expression in Beat Link Trigger to send another Note On message when the trigger deactivates.

If you just jumped to this section to get a look at how to get BEYOND to respond to CDJs, and you think it will be useful, you will want to go back and read this entire user guide to get a better understanding of how to make your triggers activate for just the portions of the tracks that you want them to. And again, this barely scratches the surface of MIDI mapping in BEYOND; see the BEYOND documentation and Pangolin forums for more information about that.

Chauvet ShowXpress Live (SweetLight, QuickDMX)

PouleR pointed out that this lighting control software, which goes by several different names, can be configured to respond to commands on a TCP socket, and asked for some help in figuring out how to take advantage of that from Beat Link Trigger. I was happy to do so, and it turns out to work quite well.

To enable this integration, make sure that External control is turned on in the ShowXpress Live Preferences, and choose a password. Quit and relaunch the application if this was not turned on when you initially opened it.

ShowXpress Live Preferences

Then paste this block of code into Beat Link Trigger’s Shared Functions:

(defn live-response-handler
  "A loop that reads messages from ShowXpress Live and responds
  appropriately."
  []
  (try
    (loop [socket (get-in @globals [:live-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 ShowXpress Live:" message)
              (cond
                (= message "HELLO\r\n")
                (timbre/info "ShowXpress Live login successful.")

                (= message "BEAT_ON\r\n")
                (do (swap! globals assoc-in [:live-connection :beats-requested] true)
                    (timbre/info "Beat message request from ShowXpress Live recorded."))

                (= message "BEAT_OFF\r\n")
                (do (swap! globals assoc-in [:live-connection :beats-requested] false)
                    (timbre/info "Beat message request from ShowXpress Live removed."))

                (.startsWith message "ERROR")
                (timbre/warn "Error message from ShowXpress Live:" message)

                :else
                (timbre/info "Ignoring unrecognized ShowXpress message type.")))
            (recur (get-in @globals [:live-connection :socket]))))))
    (catch Throwable t
      (timbre/error t "Problem reading from ShowXpress Live, loop aborted."))))

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

(defn set-live-tempo
  "Tells ShowXpress Live 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 seem to 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-live-tempo effective-tempo))`"
  [bpm]
  (let [bpm (Math/round bpm)]
    (when-not (= bpm (get-in @globals [:live-connection :bpm]))
      (send-live-command (str "BPM|" bpm))
      (swap! globals assoc-in [:live-connection :bpm] bpm)
      (timbre/info "ShowXpress Live tempo set to" bpm))))

(defn send-live-beat
  "Sends a beat command to ShowXpress Live if we have received a
  request to do so. The expected way to use this is to include the
  following in a trigger’s Beat Expresssion:

  `(when trigger-active? (send-live-beat))`"
  []
  (when (get-in @globals [:live-connection :beats-requested])
    (send-live-command "BEAT")))

(defn send-button-press
  "Sends a BUTTON PRESS command to ShowXpress Live."
  [message]
    (send-live-command (str "BUTTON_PRESS|" message)))

(defn send-button-release
  "Sends a BUTTON RELEASE command to ShowXpress Live."
  [message]
    (send-live-command (str "BUTTON_RELEASE|" message)))

;; Attempt to connect to the Live external application port.
;; Edit the variable definitions below to reflect your setup.
(try
  (let [live-address    "127.0.0.1"
        live-port       7348
        live-password   "pw"
        connect-timeout 5000
        socket-address  (InetSocketAddress. live-address live-port)
        socket          (java.net.Socket.)]
    (.connect socket socket-address connect-timeout)
    (swap! globals assoc :live-connection {:socket socket})
    (future (live-response-handler))
    (send-live-command (str "HELLO|beat-link-trigger|" live-password)))
  (catch Exception e
    (timbre/error e "Unable to connect to ShowXpress Live")))
You will want to edit the values assigned to live-address, live-port, and live-password to match your setup. This code assumes that ShowXpress Live 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 Live external application port.
(when-let [socket (get-in @globals [:live-connection :socket])]
  (.close socket)
  (swap! globals dissoc :live-connection))

With these in place, Beat Link Trigger will maintain a connection to the ShowXpress Live external control 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 cue commands.

If you want to control the Live BPM, 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-live-tempo effective-tempo))

Whenever you have this trigger enabled, it will slave the tempo in ShowXpress Live to the tempo of the Master Player.

You may also want to set this trigger’s Beat Expression to:

(when trigger-active? (send-live-beat))

That way, if Live has requested that we send BEAT messages on each beat, the triggers will do so when they are active. (But if it has not requested that, they will not.)

It is not entirely clear to me what the purpose of the BEAT messages is, so sending them might be redundant given that we are already sending BPM messages whenever the BPM value changes, rounded to the nearest integer, which is the most precision that the protocol seems to support.

Of course you will also want to be able to trigger light cues when triggers activate, which is as simple as setting the trigger’s Activation Expression to something like:

(send-button-press "Chill 3")

This causes the button labeled "Chill 3" in Live to be pressed when the trigger activates. To have the cue released when the trigger deactivates, as you might expect, you set the trigger’s Deactivation Expression to something like:

(send-button-release "Chill 3")

And, as with all triggers, you can configure it 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 earlier. This allows you to have certain looks called up automatically when the right parts of the right tracks are played.

If you jumped to this section to learn about how to integrate the lighting controller 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.

Additionally, you can send any other command supported by the external control protocol (documented here), like this, which would tell it to set fader number 2 to position 0:

(send-live-command "FADER_CHANGE|2|0")

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 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 would probably only want to use one of set-gm-tempo
;; (above) and send-gm-beat (below), depending on which 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.

SMPTE Linear Timecode

Many people want to create SMPTE timecode audio streams that are synchronized with the current playback position of a track. Now that metadata analysis has proceeded to the point that we can read the track beat grids and translate beat numbers to times, this is possible. All that is needed is for someone to write a program that can generate the SMPTE audio, and which can be controlled by triggers in Beat Link Trigger, ideally over a simple protocol like Open Sound Control. There is at least one team working on this, using the open-source libltc library.

However, they have gotten busy with other projects, and it is unclear when they (or anyone) will have time to finish and release their solution. So in the mean time I am sharing some very experimental daemons that can be used for this purpose, built using Max/MSP. Because these embed Mattijs Knepperssmpte~ object to generate the timecode audio stream, and this Max external is available only for Mac OS X, my daemons only work on the Mac as well. Also, since they embed the Max/MSP runtime, they are larger and use more system resources than a targeted C implementation based on libltc would.

However, if you really want to experiment with SMPTE right now, and can live with these limitations, read on to see how. And please keep in mind the warning in the Player Status Window section about how time information can only be reliable when tracks are being played forwards, without loops.

This is wandering outside the core goals of Beat Link Trigger, so the amount of help and support I am going to be able to offer are very limited. You may not want to dive too deep into this unless you are, or have access to, a Max/MSP expert.

Generating a Single SMPTE Stream

The original request people had was to be able to set up a trigger that was enabled when a particular track is playing on a player, and generated SMPTE timecode audio corresponding to the playback position and speed of that track. The first daemon and trigger I created support this approach. You can download the daemon app at http://deepsymmetry.org/media/smpted.zip and the corresponding trigger at http://deepsymmetry.org/media/SMPTE.bltx (to use an exported trigger like that, create a trigger row in Beat Link Trigger, then click on its action [gear] menu and choose Import Trigger):

Import Trigger option

As downloaded, that trigger is configured to watch Player 3, but you can set it to watch whatever you want, including the Master Player or Any Player, using the normal Beat Link Trigger interface.

Working with track times requires solid metadata access, and also needs the Beat Link TimeFinder object to be running. The easiest way to make sure of that is to open the Player Status window, Network  Show Player Status. The trigger uses an Enabled Filter to make sure it does not try to generate timecode when the TimeFinder isn’t running:

(.isRunning (org.deepsymmetry.beatlink.data.TimeFinder/getInstance))

If you also want your trigger to only be enabled when a particular track is loaded, you should combine that logic with this check, for example:

(and
  (.isRunning (org.deepsymmetry.beatlink.data.TimeFinder/getInstance))
  (= rekordbox-id 142))

When you run the daemon, it opens a small window which shows its configuration and status:

SMPTE daemon

The main thing you are likely to want to change here is the SMPTE frame rate, which you can do in the dropdown menu. You can also pick the sound card that will be used to send the audio by clicking the Audio Settings button, and you can choose which two channels of that audio card are used by the daemon in the channel boxes to the right. See the Max/MSP documentation for more information on audio configuration.

If you need to change the port number that the daemon uses, you can do so at the top left of the window, but you will also need to edit the trigger’s Setup Expression to match (the port number appears at the end of the first line):

(let [client  (osc/osc-client "localhost" 17001)
	 handler (reify org.deepsymmetry.beatlink.data.TrackPositionListener
                (movementChanged [this update]
                  (overtone.osc/osc-send client "/time" (int (.milliseconds update)))
                  (overtone.osc/osc-send client "/speed" (float (.pitch update)))))]
  (swap! locals assoc :smpted client
                      :handler handler))

You can also, if needed, adjust the gain (volume) of the SMPTE signal using the live.gain~ slider at the top right.

With the daemon running and configured, when your trigger activates, SMPTE LTC audio will be generated on the specified outputs, synchronized to the current playback position of the track being watched by the trigger. You will be able to see the time and frame being output by the daemon just below the frame rate.

You can explore more details of how the trigger works by looking at its Activation and Deactivation expressions, and the Shutdown expression which cleans up the resources used to communicate with the daemon.

If you have Max/MSP and want to study and perhaps modify the implementation of the daemon itself, you can find the patch that builds the application at http://deepsymmetry.org/media/SMPTE%20daemon.maxpat.zip (here is what it looks like in patcher mode, unlocked):

SMPTE patcher

As noted above, you need Mattijs Knepperssmpte~ object to work with this patch; you can find that at https://cycling74.com/tools/smpte/

Generating Two SMPTE Streams

Once people discovered the single stream implementation, it turned out that another common desire was to be able to generate two SMPTE streams at the same time, to sync to two different active players. So I eventually created a modified version of my daemon that supports this scenario. You can download the dual-stream daemon app at http://deepsymmetry.org/media/smpted-dual.zip and the corresponding triggers at http://deepsymmetry.org/media/SMPTE-Left.bltx and http://deepsymmetry.org/media/SMPTE-Right.bltx (please read the single-stream explanation above for details about how to import the trigger files, and about audio configuration of the daemon, which is the same here).

As downloaded, the left trigger is configured to watch Player 2, and the right trigger to watch Player 3, but you can change that using the normal Beat Link Trigger interface.

When you run the dual daemon, it opens a slightly larger window for its configuration and status, but the content should be familiar compared to what you saw above:

SMPTE dual daemon

The top section allows you to configure global settings like the port number, audio configuration, and gain. Then there are two separate sections for the left and right channel where you can configure which port on on the audio interface they should use, the SMPTE frame rate for each, and view the current state and time being generated for each.

Again, you can study the trigger expressions to learn more about how they work, and if you have Max/MSP and want to study or modify the daemon itself, the patch source for it is at http://deepsymmetry.org/media/SMPTE%20dual%20daemon.maxpat.zip (here is what it looks like in patcher mode, unlocked):

Dual SMPTE patcher

Again, I hope this is useful to intrepid explorers who want to try working with SMPTE, but please don’t expect me to be able to offer intensive or detailed support: I don’t use SMPTE myself, created these experimental daemons to prove that it is possible, and we are all waiting for someone to create a more robust and permanent solution. If you can help do that, please let us know!

Break Buddy “Robo DJ”

One of my most enthusiastic users came up with an interesting question shortly after figuring out how to achieve dramatic synchronized lighting effects and pyro without having to follow a preset playlist. His question: since we can load tracks on the players, and start and stop playback, is there any way to have Beat Link Trigger perform a programmed automatic mix if the DJ needs to urgently leave the booth for a bit, such as for a bathroom break? I thought thiw was something the new Show interface could make possible, with some careful setup and an understanding of the limitations, so I built this example for him.

This will never replace real mixing, because there is no way to control the crossfaders, EQ, or any effects. You will need to either pick tracks that can be harmonically mixed, or use hard cuts between tracks, stopping one player while simultaneously starting the next.

But if you follow a few setup steps, this actually works better than I initially expected. If nothing else, it is a really fun demonstration of the power of the Show interface. Here’s how to do it!

Start by saving the Break Buddy show where you like to keep your Beat Link Trigger shows. It has all the necessary shared functions built in, and some example tracks showing how to configure your own, as described below.

  • Set up some tracks in rekordbox so their first cue or memory point is where you want to mix into, put them all onto the same USB or SD card (or leave them in rekordbox if you will be using it in export mode), and import them into the Break Buddy show (opening it first if you haven’t yet done so).

  • Make sure your CDJs are configured to Auto Cue to the first memory cue (in Preferences  My Settings within rekordbox).

  • If you are going to be adjusting the tempo of tracks, be sure that the CDJs also have Master Tempo turned on, so the track pitches are unaffected by the tempo changes, and configure BLT to use a standard player number by checking Network  Use Real Player Number (you will have to turn off one of your CDJs first, if you have four).

    Using a Real Player Number

  • It would not hurt to put the players into Sync mode when you are about to start using the show, but the show’s shared functions will try to turn on Sync and Master as needed.

  • Load and play one of the tracks in the show, and let the show take it from there!

The way it works is that the tracks in the show are chosen to mix well into each other, and have BLT cues painted on them so that each track loads the next one on the other player while it is playing, and then transitions to it at an appropriate point and in an appropriate way.

The order in which the tracks appear in the show will be alphabetic by name, as always, not the order in which they will play. That is determined by the cues you create. So you will need some way to remind yourself the track you want to load and play first, such as by putting a comment in it.

Break Buddy Shared Functions

The show makes extensive use of Shared Functions to enable the cues to perform their tricky operations. They (and the variables they use) are all named starting with the prefix bb- (short for Break Buddy) so as not to conflict with shared functions used by other shows or the Triggers window. Here is a listing of all the shared functions, with a little additional explanation of how they work beyond the doc comments that are already part of them, to set the stage for the discussion of the cue library that uses them.

Don’t panic if this seems a lot of code detail, you can just skim it to your level of comfort, and focus on the explanation of the cues themselves, which comes next.
Shared Functions, part 1
(def bb-players-for-mixing
  "The player numbers that will be swapped between when using the show
  mixing helper functions. Edit the numbers in the line below if you
  don't want to have Break Buddy alternate between players 2 and 3."
  (atom #{(int 2) (int 3)})) (1)

(defn bb-this-player (2)
  "Given a status value from a show cue's started-on-beat or
  started-late expression, return the player number that started."
  [cue-status]
  (.getDeviceNumber (extract-raw-cue-update cue-status)))

(defn bb-other-player
  "Given a status value from a show cue's started-on-beat or
  started-late expression, return the player number of the other
  player we are supposed to be mixing with."
  [cue-status]
  (first (clojure.set/difference @bb-players-for-mixing
                                 #{(.getDeviceNumber (extract-raw-cue-update cue-status))})))

(defn bb-start-player (3)
  "Tells the specified player number to start playing as long as it is
  positioned at a cue (argument must be a `java.lang.Integer`). Also
  makes sure it is synced first."
  [player-number]
  (timbre/info "starting player" player-number "in sync mode")
  (.sendSyncModeCommand virtual-cdj player-number true)
  (.sendFaderStartCommand virtual-cdj #{player-number} #{}))

(defn bb-stop-player
  "Tells the specified player number to stop playing and return to its
  cue (argument must be a `java.lang.Integer`)."
  [player-number]
  (.sendFaderStartCommand virtual-cdj #{} #{player-number}))

(defn bb-swap-players
  "Given a status value from a show cue's started-on-beat or
  started-late expression, stop that player and start the other one as
  a single network message. First ensures the other player is synced."
  [cue-status]
  (.sendSyncModeCommand virtual-cdj (bb-other-player cue-status) true)
  (.sendFaderStartCommand virtual-cdj
                          #{(bb-other-player cue-status)} #{(bb-this-player cue-status)}))

(defn bb-load-track-on-other-player (4)
  "Tells the other player to load the track with the specified rekordbox ID
  from the same USB that the current player is using. The other player must
  currently be stopped. Also sets this player as the master player to prepare
  for any necessary tempo changes."
  [cue-status track-id]
  (let [status (.getLatestStatusFor virtual-cdj (bb-this-player cue-status))]
    (.sendLoadTrackCommand virtual-cdj (bb-other-player cue-status) track-id
                           (.getTrackSourcePlayer status) (.getTrackSourceSlot status)
                           (.getTrackType status)))
  (.appointTempoMaster virtual-cdj (bb-this-player cue-status)))

(defn bb-load-track-on-this-player (5)
  "Tells the current player to load the track with the specified rekordbox ID
  from the same USB that the current track is using. The player must be stopped,
  so this only makes sense to use in an Ended Expression for a cue that does a
  hard cut to the other player and stops this one."
  [cue-status track-id]
  (let [status (.getLatestStatusFor virtual-cdj (bb-this-player cue-status))]
    (.sendLoadTrackCommand virtual-cdj (bb-this-player cue-status) track-id
                           (.getTrackSourcePlayer status) (.getTrackSourceSlot status)
                           (.getTrackType status))))
1 The bb-players-for-mixing set is how the functions can determine the "other player" when you tell them to do something to it. As shipped, the show assumes you want it to mix between players 2 and 3. If you want it to use different player numbers, replace the 2 and/or 3 in this line with the numbers you actually want to use.
2 The bb-this-player and bb-other-player are able to translate between whatever gets passed to a cue expression as the status value and the player number of the player that is playing the cue, or the other player that you want to mix to. They are primarily used by other functions as you will see below.
3 You can call bb-start-player and bb-stop-player with a player number to start or stop that player. bb-swap-players takes the status value that was passed to a cue expression and stops the player that was playing the cue, and starts the other player, at the same instant. That allows you to perform a hard cut between tracks.
4 You can call bb-load-track-on-other-player once you know the other player is stopped, to get it ready to mix into a new track. You need to pass it the rekordbox ID of the track as it exists in the media your show is working from.
5 Similarly, you can call bb-load-track-on-this-player with a cue status track ID when you know the player has just been stopped by the cue.
The easiest way to find the rekordbox IDs to use in your cues is to load the tracks into a player (using the same media or collection you will be using to run the Break Buddy show), and then look in the Triggers window at a trigger that is showing that player. It will show the Track ID in the blue Player Status Summary section (The Track ID is 81 in the figure below):

Finding a Track ID

The functions we’ve looked at so far enable basic automatic mixes. But sometimes you need to nudge the tempo before, during, and after the mix. The rest of the shared functions are more complicated, but they enable clever cues to do just that:

Shared Functions, part 2
;; This last set of functions are when you want to be able to control tempo during your mixes,
;; and will only work when you have Beat Link Trigger using a real player number (so there can't
;; be more than three CDJs on the network).
(defn bb-prepare-for-tempo-adjustments   (1)
  "Gets the Virtual CDJ ready to tweak the tempo when Break Buddy cues
  need it to, or reports an error if BLT is not configured to use a real
  player number."
  []
  (if (.isSendingStatus virtual-cdj)
    (do
      (.setSynced virtual-cdj true)
      (.setPlaying virtual-cdj true))
    (seesaw/alert "Beat Link Trigger needs use a real player number to make tempo adjustments."
                  :title "Show Shared Functions prepare-for-tempo-adjustments failed" :type :error)))

(defn bb-tempo-adjust-step   (2)
  "Called periodically while we are adjusting tempo. Figures out what
  the current tempo should be, and sets the Virtual CD to that tempo."
  [cue-status {:keys [start-time start-tempo tempo-distance time-distance]}]
  (when-let [current-time (playback-time (extract-raw-cue-update cue-status))]
    (let [time-passed (- current-time start-time)
          new-tempo   (+ start-tempo (* tempo-distance (/ time-passed time-distance)))]
      (.setTempo virtual-cdj new-tempo))))

(defn bb-start-tempo-adjust   (3)
  "Called when a cue that is going to adjust the tempo has begun. Puts
  both players we are working with into Sync mode, and then causes the
  Virtual CDJ to become tempo master, and calculates the tempo
  adjustment parameters based on the current tempo, and the start and
  end times of the cue."
  [cue-status cue track target-tempo]
  (timbre/info "Adjusting tempo to" target-tempo)
  (doseq [player @bb-players-for-mixing]
    (.sendSyncModeCommand virtual-cdj player true))
  (.becomeTempoMaster virtual-cdj)
  (let [{:keys [grid expression-locals]} track
        start-tempo                      (.getTempo (VirtualCdj/getInstance))
        start-time                       (.getTimeWithinTrack grid (:start cue))
        end-time                         (.getTimeWithinTrack grid (:end cue))
        specs                            {:start-time     start-time
                                          :end-time       end-time
                                          :start-tempo    start-tempo
                                          :target-tempo   (double target-tempo)
                                          :tempo-distance (- target-tempo start-tempo)
                                          :time-distance  (- end-time start-time)}]
    (swap! expression-locals assoc-in [:tempo-adjust-cues (:uuid cue)] specs)
    (bb-tempo-adjust-step cue-status specs)))

(defn bb-continue-tempo-adjust   (4)
  "Called by the Tracked Update expression of tempo-adjustment cues to
  make the next step in adjusting the tempo."
  [cue-status track cue]
  (when-let [specs (get-in @(:expression-locals track) [:tempo-adjust-cues (:uuid cue)])]
    (bb-tempo-adjust-step cue-status specs)))

(defn bb-finish-tempo-adjust   (5)
  "Called by the Ended expression of tempo-adjustment cues to set the
  final tempo and clean up."
  [track cue]
  (when-let [target-tempo (get-in @(:expression-locals track)
                                   [:tempo-adjust-cues (:uuid cue) :target-tempo])]
    (.setTempo virtual-cdj target-tempo))
  (swap! (:expression-locals track) update :tempo-adjust-cues dissoc (:uuid cue)))
1 The bb-prepare-for-tempo-adjustments function is called by the show’s Came Online Expression to warn you that the tempo-adjustment cues won’t work if you don’t have Beat Link Trigger configured to use a real player number. (It does nothing if you have things set up correctly.)
2 The bb-tempo-adjust-step function is called several time a second by cues that are adjusting tempo. It sets a new tempo, by seeing how much longer the cue lasts, and how far the tempo still needs to be changed.
3 The bb-start-tempo-adjust function is called at the beginning of a cue that will be adjusting tempo. It sets up all the calculations that will be needed so bb-tempo-adjust-step can work, and then calls it for the first time.
4 The bb-continue-tempo-adjust function is called by the cue’s Tracked Update expression, finds the calculations that were made by bb-start-tempo-adjust, and passes them to bb-tempo-adjust-step.
5 Finally, the bb-finish-tempo-adjust function is called when the cue ends to clean things up and establish the final target tempo.

Because the whole concept of Shared Functions did not exist in earlier versions of Beat Link Trigger, it is polite to give the user a warning of why their show won’t work if they try to open it in an old version. We can do that with this short expression:

Global Setup Expression
;; Make sure we are running a new enough version of Beat Link Trigger
;; for this show to work correctly.
(show/require-version show "0.5.5-SNAPSHOT-75")

This code runs after the shared functions have been defined, when the show is starting up. It checks that it is running in a recent enough version of Beat Link Trigger to work properly. If not, a dialog explaining that problem is displayed, and the show is closed.

If you are running an even older version of Beat Link Trigger, the require-version check will not exist, and you will see a compilation error reported instead. Either way, you should upgrade to a newer version, and things will be reported more nicely from now on.

All right! With that, we have all the infrastructure we need in place to create tempo-adjusting mixes. But how do the Shared Functions know what tempo you want to get to? That’s configured when you set up the tempo adjustment cue itself! Time to make this more concrete by looking at some actual cues.

The Break Buddy Cue Library

To make it easy to get all the pieces right, Break Buddy takes advantage of the Cue Library feature of Beat Link Trigger shows. It includes some cues that are already set up with the right code in their Expressions to make this magic work; you just need to paint them on your tracks, and then edit the track IDs or tempos in the expressions as needed for the details of your mix.

Break Buddy Cue Library

See the Cue Library guide if you need a reminder of how to place such cues in a show track. This section describes how three of them are used to perform a smooth, tempo-adjusted mix between Concrete Angel and Transcendence in this sample show.

Because Concrete Angel is designed to be the first track in the automatic mix, once playing it needs to load the track it is supposed to mix into. It uses the Load track on other player cue to accomplish that. The cue is positioned many measures before the mix happens, so that the other player has time to finish loading it and positioning itself at the first memory point before the mix needs to take place. This kind of cue is simple, and uses only the Entered expression.

The contents of that expression use the bb-load-track-on-other-player function described in the previous section to tell the player that isn’t currently playing the cue to load a track:

"Load Track on other player" cue’s Entered expression
;; Replace the number at the end with the ID of the track you want to load.
(bb-load-track-on-other-player status 853)

See the tip above for the easiest way to find the ID of the track you want to load.

So when playback of Concrete Angel reaches this cue, the other player is told to load Transcendence. The tracks are very harmonically compatible, so this mix is going to be able to play out the end of Concrete Angel on top of the beginning of Transcendence. They have different enough tempos that it’s worth ramping from the 130 BPM of Concrete Angel to the 127 BPM of Transcendence during the mix. That is accomplished by painting an Adjust Tempo cue over four bars of Concrete Angel, and having the cue that starts the other player happen in the middle of that tempo ramp.

The Adjust Tempo cue is a more complex cue, with three different expressions to make it work. The first is the Started On Beat expression (and also notice in the screen shot above that the cue is configured with Same as its Late Message, so that even if the cue starts a bit late, the Started On Beat expression is invoked. The Shared Function code handles that case smoothly).

The Started On Beat expression is where you tell the cue what tempo you want to ramp towards:

"Adjust Tempo" cue’s Started On Beat expression
;; Edit the number at the end of the next line to specify your desired tempo.
;; The cue will gradually move towards that tempo over however many beats you
;; paint it.
(bb-start-tempo-adjust status cue track 127)

That 127 was the only thing we needed to configure for the cue, because we want to ramp to the 127 BPM that is the natural tempo of Transcendence. The tempo adjustment Shared Functions can figure out the current tempo, and how much time is left in the cue, to control how quickly (and in what direction) they need to ramp the tempo.

The cue’s other expressions simply use the Shared Functions detailed in the previous section to make the adjustment happen as the cue plays:

"Adjust Tempo" cue’s Tracked Update expression
(bb-continue-tempo-adjust status track cue)
"Adjust Tempo" cue’s Ended expression
(bb-finish-tempo-adjust track cue)

So with this setup, starting at beat 793 of Concrete Angel, and over a period of sixteen beats, the tempo of both players will smoothly adjust from 130 down to 127 BPM. And halfway through that process, we start Transcendence playing, using a Start other player cue.

This is another simple cue, with a single expression that you don’t need to edit. It uses the Started On Beat expression to tell the other player to start playing:

"Start Other Player" cue’s Started On Beat expression
(bb-start-player (bb-other-player status))

It combines the bb-start-player and bb-other-player functions described in the previous section to make that happen.

These three cues combined perform a smooth tempo-adjusting mix between these two very harmonically compatible tracks. You could then use another Load track on other player cue in the new track to set up the next track that it wanted to mix into, and carry on from there.

Sometimes you need to do different kinds of mixes, though. Often you don’t want to let your outgoing track play to completion, and the Stop this player and load new track cue is great in such situations. It accomplishes those tasks using two expressions. The Started On Beat expression stops the player that was playing the cue:

"Stop this player and load new track" cue’s Started On Beat expression
(bb-stop-player (bb-this-player status))

Much like the previous expression we looked at, this combines two Shared Functions to stop the player playing the track in which the cue was placed. As soon as the player stops, the cue’s Ended expression will be run. Its content is functionally the same as the Load track on this player cue we have already seen:

"Stop this player and load new track" cue’s Ended expression
;; Replace the number at the end with the ID of the track you want to load once stopped.
(bb-load-track-on-this-player status 93)

And of course sometimes tracks are so incompatible with each other that you want to just do a hard cut between them, stopping the outgoing player simultaneously with starting the incoming player. The Cut to other player and load track cue lets you do exactly that. It is very similar to the Stop this player and load new track cue; the only difference is the function called in the Started On Beat expression:

"Cut to other player and load new track" cue’s Started On Beat expression
(bb-swap-players status)

This uses a Shared Function that knows how to stop the player that is playing the track holding the cue, and start the other one, in a single network message.

As with the previous cue, you edit the Ended expression to put in the ID of the track you want to load once the player has stopped.

"Cut to other player and load new track" cue’s Ended expression
;; Replace the number at the end with the ID of the track you want to load once stopped.
(bb-load-track-on-this-player status 852)

Going Further

Hopefully you can see ways to combine these cues to create a variety of mixes between tracks to keep the dance floor alive as you take your desperately needed break! It is also possible to combine the Shared Functions in other ways to do slightly different mixes, for example turning off Sync before loading a track with a radically different tempo, and doing a hard cut over to it. Try experimenting! If you come up with great ideas (or just get stuck and want help), come talk about it on the Gitter channel.

Xone:96 Channels On Air Support

This started as a request for assistance on the Beat Link project, but it turns out at least one Beat Link Trigger user also has this great mixer, and missed the On Air display that CDJs only give when talking to DJMs. But since the Xone:96 can send MIDI events to report the positions of its faders, by plugging in a USB cable you can have Beat Link Trigger translate those into Channels On Air messages that update the CDJ lights for you.

First Version

To make it easy to turn this feature on and off along with other things you are doing in Beat Link Trigger, I created a standalone show file that implements the integration. If you have that show open, it tries to translate mixer MIDI into CDJ on-air messages, and if you close it, it stops. The show looks a little strange because it has no tracks, but it doesn’t need any, all the action happens in the Shared Functions and the Came Online and Going Offline Expressions.

As soon as this was working, it got me thinking about some simple things I could add to Beat Link Trigger that could make non-track-oriented "shows" like this a lot more powerful, which led to the second version below. But understanding this much simpler version is a good idea before diving into how we can add a graphical interface to it!

To try it out, start by saving the Xone On Air show where you like to keep your Beat Link Trigger shows, then open it. Assuming your mixer is plugged into a USB port, it should start working. Move channel faders up and down, and move the cross-fader back and forth, and your CDJs should change color appropriately (as long as you have the On Air feature enabled on the CDJs themselves, and Beat Link Trigger is online and talking to them).

If the mixer wasn’t connected, you will see an error dialog about the MIDI device not being found when Beat Link Trigger goes online with the show open. Plug in the mixer, then take BLT offline and back online, and things should work better. If it still complains, your MIDI device must be named differently than expected: the show is looking for a port with "XONE" in the name. Figure out what your mixer’s MIDI port is called on your system, then edit line 3 of the Came Online Expression so it is a substring that matches only that port, and try again.

As shipped, the show assumes channels 1 and 2 are tied to the left side of the cross fader, and 3 and 4 to the right, but that is easily edited at the top of the Shared Functions, as shown in the code listing below.

For the channel on-air messages to affect your correct CDJs, you need to make sure each CDJ is displaying the actual mixer channel number that it is connected to. That happens automatically if you plug them into a DJM-2000, but most people have to set them manually.

Here are some instructions from the Pioneer forum about assigning player channel numbers. You need to have the network cable and all USB connections (cables and media sticks) disconnected for this to work:

  1. Power on your CDJ

  2. Press and hold the "MENU" button on the top (This enters the Utility mode)

  3. Use the control knob to scroll down through the list until you reach "Player No."

  4. Press down on the knob to select it

  5. Scroll through your available channels (Auto, 1, 2, 3, 4)

  6. Press down on the knob to select your desired channel

  7. Press the "MENU" button again to exit the Utility setting

It may be necessary to wait a few seconds while an existing "Auto" selection assigns your player number before you can edit it. And again, You must have all LAN and USB connections removed in order to assign the player number.

If you are curious how the Xone:96 On-Air show actually works, here is all the code!

Shared Functions
(def xone-left-channels
  "The set of channels assigned to the left side of the cross-fader"
  #{1 2}) (1)

(def xone-right-channels
  "The set of channels assigned to the right side of the cross-fader"
  #{3 4}) (2)

(def xone-min-on-air-value
  "The MIDI value of the fader must be this far from zero (or, in the
  case of the cross-fader, from the opposite end of travel), for a
  channel to be considered on the air."
  2) (3)

(defn xone-on-air-via-channel-faders  (4)
  "Returns the channel numbers that are currently on the air based
  solely on the known positions of the channel faders, given the
  updated state of the show globals following a MIDI event."
  [state]
  (set (filter (fn [num]
                 (>= (get state (keyword (str "channel-" num))) xone-min-on-air-value))
               (range 1 5))))

(defn xone-blocked-by-cross-fader
  "Returns set of the channel numbers that are currently muted because
  of the cross-fader position, if any, given the updated state of the
  show globals following a MIDI event."
  [state]
  (let [fader-position (:cross-fader state)]
    (cond
      (< fader-position xone-min-on-air-value) xone-right-channels
      (< (- 127 fader-position) xone-min-on-air-value) xone-left-channels
      :else #{})))

(defn xone-midi-received
  "This function is called with each MIDI message received from the mixer,
  and also given access to the show globals so that it can update the known
  mixer state and determine the resulting on-air channels."
  [globals msg]
  ;; Default Xone configuration sends faders as CC on MIDI Channel 16.
  (when (and (= :control-change (:command msg)) (= 15 (:channel msg)))
    ;; Check if it is one of the faders we care about.
    (when-let [recognized (get {0 :channel-1
                                1 :channel-2
                                2 :channel-3
                                3 :channel-4
                                4 :cross-fader}
                               (long (:note msg)))]
      ;; It is, so update known mixer state with the current fader value, and
      ;; calculate which channels are now on-air.
      (let [state  (swap! globals assoc recognized (:velocity msg))
            on-air (clojure.set/difference (xone-on-air-via-channel-faders state)
                                           (xone-blocked-by-cross-fader state))]

        ;; If the Virtual CDJ is running (Beat Link Trigger itself is online),
        ;; send a Channels On Air message to update the actual CDJ's state.
        ;; (We need to convert the values to integers, rather than the longs
        ;; that Clojure uses natively, to be compatible with the Beat Link API.)
        (when (.isRunning virtual-cdj)
          (.sendOnAirCommand virtual-cdj (set (map int on-air))))))))
1 Any channel numbers listed in this set will be considered off the air when the cross fader is all the way to the right.
2 Any channel numbers listed in this set will be considered off the air when the cross fader is all the way to the left. (If you omit any channels numbers from both sets, they will be unaffected by the cross fader.)
3 You can change this value to adjust how far from the end of a fader a channel goes off the air. The MIDI values for the faders run from 0 to 127.
4 The rest of this code is functions that are called when MIDI events arrive from the mixer to decide how they affect the channels. Hopefully the inline comments show how they work.
Global Setup Expression
;; Make sure we are running a new enough version of Beat Link Trigger
;; for this show to work correctly.
(show/require-version show "0.5.5-SNAPSHOT-76")

;; Make the UI understand that this show does not use tracks.
(show/block-tracks show true)

;; Track the current state we believe the mixer to have. Will not be
;; accurate for a given fader until it is moved. We assume all channels
;; are on the air until we get actual data.
(swap! globals assoc
  :cross-fader 63
  :channel-1  127
  :channel-2  127
  :channel-3  127
  :channel-4  127)

This code runs after the shared functions have been defined, when the show is starting up. It first makes sure that it is running in a recent enough version of Beat Link Trigger to work properly. If not, a dialog explaining that is displayed, and the show is closed.

If you are running an even older version of Beat Link Trigger, the require-version check will not exist, and you will see a compilation error reported instead. Either way, you should upgrade to a newer version, and things will be reported more nicely from now on.

It also tweaks the Show window slightly, changing the Tracks menu to an Expressions menu, which omits the Import Tracks submenu: this show does not have any need to import tracks or create cues for them, it simply talks to a mixer over MIDI. This also removes options in any other shows' track context menus which would offer to copy the tracks to this show.

It then sets up some assumed values for the mixer’s channel faders and cross fader, which will be replaced with actual values as soon as we receive MIDI from the mixer reporting their movement.

Came Online Expression
;; Try to open the MIDI input with which we receive data from the mixer. Edit the MIDI
;; device name in the first line if needed to match your Xone's MIDI input device name.
(let [mixer-device-name "XONE"]  (1)
  (try
    ;; Open a MIDI input with the specified name, which should be coming from the mixer.
    (let [device (midi/midi-in mixer-device-name)]
      (swap! globals assoc :mixer-midi device)

      ;; Arrange for xone-midi-received to be called whenever we get a message from the mixer.
      (midi/midi-handle-events device (fn [msg] (xone-midi-received globals msg))))

    (catch Throwable t
      (timbre/warn t "Unable to open Xone MIDI device" mixer-device-name)
      (seesaw/invoke-later  (2)
       (seesaw/alert (:frame show)
        (str "<html>The XoneOnAir show was unable to open a MIDI port matching \"" mixer-device-name
             "\"<br>to receive messages from the mixer. Please make sure the mixer<br>"
             "is connected, verify the device name that is showing up, and if needed<br>"
             "edit the definition of <code>mixer-device-name</code> in the Came Online Expression."
             "<br><br>Take Beat Link Trigger offline and back online to try again.")
        :title "MIDI Device Not Found, On-Air Detection Disabled"
        :type :error)))))

This is run whenever Beat Link Trigger is brought online, (or, if it was already online when the show is opened, it is run right after the Global Setup expression). It tries to connect to the mixer, and sets up to be informed whenever faders are moved, so we can figure out which channels are on the air.

1 This is where you edit the mixer MIDI port name if it doesn’t contain "XONE" on your system.
2 This is the code that displays the error window when the MIDI port can’t be found.
Going Offline Expression
;; Close the mixer MIDI connection if we opened one.
(swap! globals
  (fn [current]
    (when-let [device (:mixer-midi current)]
      (.close (:transmitter device)))
    (dissoc current :mixer-midi)))

This code runs when Beat Link Trigger is being taken offline (or the show is closing with BLT still online), and closes the mixer’s MIDI port gracefully if it was open.

Experimenting Without the Mixer

I don’t actually have a Xone:96, so to develop this code I had to make a "fake one" based on its MIDI mapping manual, which I did in the form of a Max patch. If you have Max, you can save it and play with it too. It has four simulated channel faders and a cross fader:

Xone:96 Simulator

I set my environment up so that MIDI output port c in Max had a name that matched what the Global Setup Expression was looking for, and I could control my CDJ’s on-air lights by sliding the faders in the patch.

@GuyHardwood, who was the inspiration for this integration, discovered through MIDI Monitor that the official documentation is incorrect about the CC number used for the cross-fader. It appears to actually be 4 rather than 5.

Second Version, with GUI

As mentioned above, while I was working on this show I realized there were probably many other kinds of integrations that would be convenient to share as show files but which had nothing to do with tracks. So I added the mechanism that turns off the import tracks feature, but that left most of the show window empty, with a useless Enabled Default menu and Filter box at the top. And building the mixer simulator I used for testing made me wish I could have a similar UI in the show itself, to show what is going on with the mixer. And I realized that it would only take a few small additions to Beat Link Trigger to enable me (and anyone) to build things like that, so I added them!

Here is what the final result looks like in action:

Xone:96 Show UI

The top row shows the mixer channels that are currently calculated as being on the air by lighting them up in red. Immediately below that is a row of controls that allow you to tell the show which side of the cross-fader each channel has been assigned on the mixer, just like the physical switches above the faders on the mixer itself. If the switch is in the middle, the channel is unaffected by the cross-fader. If it is on the left (X) or right (Y) side, then the channel is heard when the cross-fader is on that side.

Below these are the channel numbers, and below those are MIDI activity indicators whose centers flash blue whenever the mixer reports motion on that channel fader. At the same time, the graphical representation of the channel fader will jump to where the mixer reported, and the on-air indicators will update. If the show’s channel faders are not in the right place (because the mixer was adjusted while this show was not open or online), you can either drag the GUI version to the right spot using your mouse, or just wiggle the physical mixer channel fader, which will send MIDI events that snap the GUI for that channel into the right place.

Finally, at the bottom is the MIDI indicator and GUI representation of the cross-fader position, which works the same way. The positions of the X-Y selectors and faders are saved when you close the show, so they will appear in the same place as you left them, and you will rarely need to tweak anything by editing expression code.

Save the full-featured GUI version where you keep your shows and you can start using it, but for those of you who want to know how it works, let’s take a tour of the code!

The Shared Functions are much larger in this version, because that is where all the GUI elements are constructed, so we will save them for last. The Global Setup Expression is actually simpler here:

Global Setup Expression
;; Make sure we are running a new enough version of Beat Link Trigger
;; for this show to work correctly.
(show/require-version show "0.5.5-SNAPSHOT-83")  (1)

;; Make the UI understand that this show does not use tracks, and
;; build and install the custom GUI that takes over the window.
(show/block-tracks show (xone-build-ui show globals))  (2)
1 This show needs a newer version of BLT, because it takes advantage of new features for drawing the UI and saving settings. But that also means the section that set up default values for the fader positions is no longer necessary.
2 In addition to telling BLT this show doesn’t import any tracks, the show/block-tracks function can install a custom user interface to be drawn as the content of the window. We achieve that by calling a new Shared Function here that creates and returns that custom interface.

The Came Online expression is almost identical to the previous version, and only needed one additional line:

Came Online Expression
;; Try to open the MIDI input with which we receive data from the mixer. Edit the MIDI
;; device name in the first line if needed to match your Xone's MIDI input device name.
(let [mixer-device-name "XONE"]
  (try
    ;; Open a MIDI input with the specified name, which should be coming from the mixer.
    (let [device (midi/midi-in mixer-device-name)]
      (swap! globals assoc :mixer-midi device)

      ;; Arrange for xone-midi-received to be called whenever we get a message from the mixer.
      (midi/midi-handle-events device (fn [msg] (xone-midi-received show globals msg))))

      ;; Start the UI animation thread that draws the MIDI activity.
      (xone-animate-ui show globals)  (1)

    (catch Throwable t
      (timbre/warn t "Unable to open Xone MIDI device" mixer-device-name)
      (seesaw/invoke-later
       (seesaw/alert (:frame show)
        (str "<html>The XoneOnAir show was unable to open a MIDI port matching \"" mixer-device-name
             "\"<br>to receive messages from the mixer. Please make sure the mixer<br>"
             "is connected, verify the device name that is showing up, and if needed<br>"
             "edit the definition of <code>mixer-device-name</code> in the Came Online Expression."
             "<br><br>Take Beat Link Trigger offline and back online to try again.")
        :title "MIDI Device Not Found, On-Air Detection Disabled"
        :type :error)))))
1 This line, which starts up a background thread to draw and fade out the MIDI activity indicators is the only thing that is different from the first version. The implementation of the function itself is in the Shared Functions, of course.

Similarly, there is only one addition needed to the Going Offline expression:

Going Offline Expression
;; Close the mixer MIDI connection if we opened one.
(swap! globals
  (fn [current]
    (when-let [device (:mixer-midi current)]
      (.close (:transmitter device)))
    (dissoc current :mixer-midi)))

;; Update the MIDI status indicators to show we are now offline.
(xone-repaint-midi-indicators globals)  (1)
1 This new line redraws the MIDI activity indicators after the mixer connection has been closed, which gives them an obvious "offline" appearance.

We do this because the act of closing the mixer connection also terminates the background thread that is animating the indicators, so it won’t have a chance to reflect this change. All this stuff is done with our new Shared Functions, so let’s take a look at them.

Shared Functions, part 1
(def xone-min-on-air-value  (1)
  "The MIDI value of the fader must be this far from zero (or, in the
  case of the cross-fader, from the opposite end of travel), for a
  channel to be considered on the air."
  2)

(defn xone-on-air-via-channel-faders  (2)
  "Returns the channel numbers that are currently on the air based
  solely on the known positions of the channel faders, given the
  updated state of the show user data following a MIDI event."
  [state]
  (set (filter (fn [i]
                 (>= (get state (keyword (str "channel-" i)) 0) xone-min-on-air-value))
               (range 1 5))))

(defn xone-blocked-by-cross-fader  (3)
  "Returns set of the channel numbers that are currently muted because
  of the cross-fader position, if any, given the updated state of the
  show user data following a MIDI event."
  [state]
  (let [fader-position (:cross-fader state 0)]
    (cond
      (< fader-position xone-min-on-air-value) (:y-channels state #{3 4})
      (< (- 127 fader-position) xone-min-on-air-value) (:x-channels state #{1 2})
      :else #{})))
1 This is the only variable definition we still need from the original version, because the rest are now managed by stored settings controlled by graphical elements in the interface. Tweaking this one value still requires you to edit code.
2 This function is almost identical to its old version. The comment explains that the value comes from a different place than it used to (the new saved show data rather than globals), and we added a default value of 0 to the get for when no data has yet been saved in the show.
3 Again, nearly identical to the non-GUI version, but it is receiving saved show data, and here is where the default values of the channel assignments come from if the data isn’t yet saved.

But that’s the end of familiar stuff for a long while. Now we get into the new part that builds the graphical user interface.

Shared Functions, part 2
(defn- xone-recompute-on-air  (1)
  "Recalculates the current set of on-air channels, given an updated
  state of channel fader and cross-fader positions and channel x-y
  assignments. Updates the user interface and the CDJs accordingly."
  [state globals]
  (let [on-air (clojure.set/difference (xone-on-air-via-channel-faders state)
                                       (xone-blocked-by-cross-fader state))]
    ;; Store the computed on-air states and update the UI to reflect them.
    (swap! globals assoc :on-air on-air)
    (seesaw/repaint! (:channel-on-air @globals))

    ;; If the Virtual CDJ is running (Beat Link Trigger itself is online),
    ;; send a Channels On Air message to update the actual CDJ's state.
    ;; (We need to convert the values to integers, rather than the longs
    ;; that Clojure uses natively, to be compatible with the Beat Link API.)
    (when (.isRunning virtual-cdj)
      (.sendOnAirCommand virtual-cdj (set (map int on-air))))))

(defn- xone-build-channel-labels  (2)
  "Creates the vector of four labels identifying the channel faders."
  []
  (vec (for [i (range 4)]
         (vec (concat [(seesaw/label (str (inc i))) (str "push 1" (when (= i 3) ", wrap"))])))))
1 This code used to be part of xone-midi-received. That function still exists below, but now the calculation of what channels are on the air, and sending the results to the CDJs, happens in response to the graphical faders moving because that can be caused by the user dragging them or by MIDI events.
2 This function creates the numbers that appear above the channel faders. BLT uses Seesaw to provide a Clojure wrapper around Java Swing GUI components, and MIG Layout to arrange them.
Shared Functions, part 3
(defn xone-react-to-xy-change  (1)
  "Handles a change in one of the tiny XY slider values, with `channel`
  holding the channel number controlled by the slider, and `v` is the
  new position."
  [show globals channel v]
  (let [current (show/user-data show)        ; Find the current state of the X and Y channel assignments.
        x       (:x-channels current #{1 2}) ; Apply the default assignments if they haven't yet been stored.
        y       (:y-channels current #{3 4})
        new-x   (if (neg? v) (conj x channel) (disj x channel))  ; Update assignments based on the new slider value.
        new-y   (if (pos? v) (conj y channel) (disj y channel))
        state   (show/swap-user-data! show assoc :x-channels new-x :y-channels new-y)]
    (xone-recompute-on-air state globals)))

(defn xone-build-xy-sliders  (2)
  "Creates the vector of four tiny horizontal sliders that specify which
  side of the cross fader, if any, each channel fader is assigned."
  [show globals]
  (apply concat
         (for [i (range 1 5)]
           (let [state       (show/user-data show)
                 saved-value (cond  ; See if the show has a saved position for this slider.
                               ((:x-channels state #{1 2}) i) -1 ; Apply default positions if not found.
                               ((:y-channels state #{3 4}) i) 1
                               :else 0)]
             [["X" "split 3, gapright 0"]
              [(seesaw/slider :orientation :horizontal ; Build the slider UI object itself.
                              :min -1
                              :max 1
                              :value saved-value
                              :listen [:state-changed #(xone-react-to-xy-change
                                                        show globals i (seesaw/value %))])
               "width 45, gapright 0"]
              ["Y" (when (= i 4) "wrap")]]))))
1 This function reacts to the user changing the position of one of the X-Y channel assignment controls to update the saved show data that keeps track of which channels belong on which side. It then calls xone-recompute-on-air because of course changing channel assignments can affect which channels are on the air.
2 This function builds the actual X-Y channel assignment controls themselves, and hooks them up to the previous function.
Shared Functions, part 4
(defn- xone-react-to-slider-change  (1)
  "Handles a change in slider value, where `k` identifies the slider and
  `v` is the new value."
  [show globals k v]
  (xone-recompute-on-air (show/swap-user-data! show assoc k v) globals))

(defn- xone-build-channel-sliders  (2)
  "Creates the vector of four vertical sliders that represent the mixer
  channel faders."
  [show globals]
  (vec (for [i (range 4)]
         (let [k (keyword (str "channel-" (inc i)))]  ; Build the keyword identifying the slider.
           (seesaw/slider :orientation :vertical      ; Build the slider UI object itself.
                          :min 0
                          :max 127
                          :value (k (show/user-data show) 63)  ; Restore saved value, if one exists.
                          :listen [:state-changed #(xone-react-to-slider-change
                                                    show globals k (seesaw/value %))])))))
1 Similarly, this function reacts to the user (or an incoming MIDI message) changing the position of one of the faders (channel or cross-fader). All it needs to do is call xone-recompute-on-air.
2 And this function builds the channel fader controls themselves and hooks them up to the previous function.

To be continued…​

Learning More

License

Deep Symmetry logo Copyright © 2016–2019 Deep Symmetry, LLC

Distributed under the Eclipse Public License 1.0, the same as Clojure. By using this software in any fashion, you are agreeing to be bound by the terms of this license. You must not remove this notice, or any other, from this software. A copy of the license can be found in LICENSE within this project.