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
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
(you will have to turn off one of your CDJs first, if you have four). -
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. |
(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): |
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:
;; 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.
(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 must use a real player number to adjust tempo."
: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:
;; 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.
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:
;; 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:
;; 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:
(bb-continue-tempo-adjust status track cue)
(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:
(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:
(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:
;; 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:
(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.
;; 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 Zulip stream.