Time Flies Like An Arrow, Fruit Flies Like A Banana

By itself, QLab doesn’t know about musical tempo, beats per minute, measure length, or time signature. Since time is just time, though, a little scripting can teach your workspace quite a lot about these concepts.

This cookbook article discusses using tempi in QLab in two ways. First, in Tempo Fugit, we’ll explore applying a tempo to Audio or Video cues. Then, in Tempo Facit, we’ll build a metronome in QLab.

Tempo Fugit - Time Flies

This technique uses slices to mark bars and beats in an Audio or Video cue, and therefore to establish a tempo “on top of” the cue which you can use in a musical manner. The scripts described here and included in the downloadable example make it easier to work with tempo-based time in a QLab workspace.

One of the demos in the downloadable workspace shows how to create a chase effect using Light cues. This demo requires iLEDMapper, which is a free iOS app that receives Art-Net. You can download iLEDMapper here:

https://apps.apple.com/us/app/iledmapper/id404442976

iLEDMapper also runs on Macs with Apple Silicon processors.

The workspace contains three scripts.

Script A - Slice Audio or Video Cue By Tempo

This one is the easiest to use; simply select it and click the GO button or use the keyboard shortcut for GO, which is space by default, or the keyboard shortcut for preview which is V by default. The script will then ask you a few questions:

  • What is the cue number of the cue you want the script to act on? In this workspace, good targets are cue 2 and cue 3.
  • At what time in the cue should the script start making new slices? The default is 0, which is the very beginning of the cue. By running this script several times on the same cue and entering different start times, you can create tempo changes within the cue. All existing slices in the cue after the start time will be deleted.
  • What is the tempo of the cue, measured in beats per minute (BPM)?
  • How should each measure of the cue be divided?
  • How many beats are there in a measure?
  • Would you like to offset the slices from each beat? Sometimes it can be convenient to have a marker that comes just before the beat or just after. If you want that, enter a time here in seconds. A positive number will put slices after the beat, and a negative number will put slices just before the beat.

Once you’ve taken that little quiz, the script will make slices in your chosen cue according to your answers. Then, the script will append the name of the target cue to tell you how much time there is between the slices it just added.

Cue 2 in the example workspace has already been run through the script. It’s a click track at 120 BPM with an emphasis every fourth beat. It’s been sliced into quarter notes with no offset. Let’s use this cue to try some fun things.

Set your workspace to Always Audition by choosing Turn on Always Audition from the Tools menu or by using the keyboard shortcut ⇧⌘A, then start cue 2. It is a click track at 120 BPM with an emphasis every 4th beat.

Now start cue 10. It’s a Start First Group cue which creates a loop of Devamp cues and Text cues. The Devamp cues target cue 2. Every time cue 2 passes a marker, the Devamp cues inside cue 10 it will start the following Text cue which puts a beat number on screen. Handy to let your musicians know what beat of the bar they are on!

Leave cue 2 and cue 10 running, then use script A to make some markers in cue 3 that are different than cue 2. Maybe triplets?

Now, run cue 11. This will swap the targets of the Devamp cues inside cue 10 to cue 3 instead. You’ve changed tempo!

Cue 12 will swap right back.

Cue 20 shows off a simple light chase using Devamp cues and Light cues. To see it in action, you can either open the Light Dashboard and look in the Audition tab, or switch off Always Audition and open iLEDMapper on an iOS device that’s on the same network as your Mac.

Cue 21 is a different approach to making a light chase, advancing through a second cue list so you can arbitrarily add cues as you’d like.

Cue 22 does some random design, in time. It triggers a random group, 22.5, so something happens every slice.

Here is Script A in full, annotated with comments about what each line or section does:

tell application id "com.figure53.QLab.5" to tell front workspace
  
  -- make a list of possible divisions of a measure
  set beatDivisions to {"Measure", "Eighth Note", "Quarter Note", "Half Note", ¬
    "1/8 Triplet", "1/4 Triplet"} as list
  
  -- ask which cue to slice (using cue number)
  set targetCue to text returned of (display dialog "Enter the cue number ¬
    of the cue to slice:" default answer " " with title "Slice Cue by Tempo")
  
  -- ask at what time in the cue to start slicing  
  set startTime to text returned of (display dialog "Start slicing the cue ¬
    at time:" default answer "0" with title "Slice Cue by Tempo") as number
  
  -- the script defaults to add slices all the way to the end of the file
  set endTime to end time of cue targetCue
  
  -- ask what tempo to use
  set BPM to text returned of (display dialog "Enter the desired tempo ¬
    in beats per minute (BPM):" default answer "120" with title ¬
    "Slice Cue by Tempo") as integer
  
  -- use the list above to ask what beat division to use
  set beatdivision to choose from list beatDivisions with prompt "Choose a beat ¬
    division:" default items {"Measure"} as string
  
  -- how many beats in a measure?
  set measureLength to text returned of (display dialog "Enter the number of ¬
    beats per measure:" default answer "4" with title "Slice Cue by Tempo") ¬
    as integer
  
  -- how many beats in a measure?
  set sliceOffset to text returned of (display dialog "Offset slices ¬
    from the beat? Positive numbers will place slices after the beat, ¬
    negative numbers will place slices before the beat." default answer "0" ¬
    with title "Slice Cue by Tempo") as number
  
  -- this section creates the appropriate mathematical representation
  -- of the selected beat division and measure length
  if beatdivision as string = "Measure" then
    set beatDiv to measureLength
  else if beatdivision as string = "Eighth Note" then
    set beatDiv to (measureLength / 8)
  else if beatdivision as string = "Quarter Note" then
    set beatDiv to (measureLength / 4)
  else if beatdivision as string = "Half Note" then
    set beatDiv to (measureLength / 2)
  else if beatdivision as string = "1/8 Triplet" then
    set beatDiv to ((measureLength / 4) / 3)
  else if beatdivision as string = "1/4 Triplet" then
    set beatDiv to ((measureLength / 2) / 3)
  end if
  
  -- set the slice spacing based on the BPM and the beatDiv above
  set sliceSpacing to ((60 / BPM) * beatDiv)
  
  -- where the first slice should go, not counting offset
  set currentTrueSlice to (startTime + sliceSpacing)
  
  -- where the first slice should go including the offset
  set currentSlice to (currentTrueSlice + sliceOffset)
  
  -- get all the slices already in the cue
  set allSlices to slice markers of cue targetCue
  set sliceCount to (count allSlices)
  
  -- make an empty list which we'll later fill with slices
  set sliceArray to {} as list
  
  -- iterate through the slices which are already in the cue.
  -- take all the ones that fall before the startTime.
  -- and put them into that empty list
  repeat with eachItem from 1 to sliceCount
    set eachSlice to item eachItem of allSlices
    set eachTime to time of eachSlice
    if eachTime is less than startTime then
      copy eachSlice to end of sliceArray
    end if
  end repeat
  
  -- add new slices to the list based on our calcuations above
  repeat while currentSlice < endTime
    set sliceAdd to {time:currentSlice, playCount:1}
    copy sliceAdd to end of sliceArray
    set currentTrueSlice to (currentTrueSlice + sliceSpacing)
    set currentSlice to (currentTrueSlice + sliceOffset)
  end repeat
  
  -- replace the current slice markers in the target cue
  -- with the slices in the list
  set slice markers of cue targetCue to sliceArray
  
  -- rename the target cue by appending some information
  -- about the slices added by this script
  set cueName to q name of cue targetCue
  set newCueName to (cueName & " space between slices is " & sliceSpacing ¬
    & " seconds")
  set q name of cue targetCue to newCueName
  
end tell

Script B - Set Selected Cue’s Duration By Tempo

This script is a bit less fancy, but can still be very useful. It sets the duration of Audio, Video, Text, Light, or Fade cues to a length based on tempo. It may be easiest to see the value of this script using Video cues which target still images or Light cues.

This script relies on a hotkey trigger, which is F1. Select some cues, for example all of those light cues in cue 22.5, then press F1 on your keyboard. That will set the duration of each selected cue to the length of a single beat at the given tempo.

Script C - Adjust Of Selected Cue By Tempo.

This one is for Audio cues or Video cues. If you know the original tempo in BPM, you can use this script to adjust the playback rate of the cue to a different tempo. The script will also adjust any slices in the cue too. It can be handy for cues using lots of slices, and cues that are just too slow.

Tempo Facit - Time Does

This workspace demonstrates using two click sounds, one for a downbeat and one for a regular beat, to create a metronome in QLab using a looping Playlist Group cue. Audio cues in the Group are separated by Wait cues, where the wait duration is equal to the beat’s duration minus the click’s duration.

The workspace contains two Script cues. The first one, cue MM, prompts you to enter a tempo and a number of beats per measure, then creates a “metronome” matching those specifications.

For this approach to work, the duration of both clicks must be specific and known. In this example, it’s 0.1 seconds.

Here’s the script:

tell application id "com.figure53.QLab.5" to tell front workspace
  
  set downbeatCue to cue "DwnBt"
  set dwnBtTarget to file target of downbeatCue
  set beatCue to cue "Bt"
  set btTarget to file target of beatCue
  
  -- this method requires a fixed, known duration for the click cues
  set clickDuration to 0.1
  
  -- enforce the that duration for both cues
  set end time of downbeatCue to clickDuration
  set end time of beatCue to clickDuration
  
  set BPM to text returned of (display dialog "Enter BPM between 20-600:" ¬
    default answer "100") as integer
  set measureLength to text returned of (display dialog "How many beats per ¬
    measure?" default answer "4") as integer
  
  -- 1 second divided by beats per second
  -- gives us the duration of one beat in seconds
  set beatDuration to (1 / (BPM / 60))
  
  -- the wait cue's duration is equal to
  -- the duration of a beat minus the length of a click
  
  set waitDuration to (beatDuration - clickDuration)
  
  -- make the cues
  set newCues to {} as list
  set downBeat to true
  repeat measureLength times
    make type "Audio"
    set newCue to last item of (selected as list)
    set q number of newCue to ""
    if downBeat is true then
      set file target of newCue to dwnBtTarget
      set downBeat to false
    else
      set file target of newCue to btTarget
    end if
    copy newCue to end of newCues
    make type "Wait"
    set newCue to last item of (selected as list)
    set q number of newCue to ""
    set duration of newCue to waitDuration
    copy newCue to end of newCues
  end repeat
  
  -- put the cues into a playlist group
  set selected to newCues
  make type "Group"
  set newCue to last item of (selected as list)
  set q number of newCue to ""
  set mode of newCue to playlist
  set playlist loop of newCue to true
  set q name of newCue to ((measureLength & " Beats at " & BPM & "bpm") ¬
    as string)
  
end tell

Cue “110” is an example of a “metronome” created by the script.

The second script in the workspace, cue DV, allows you to “devamp” the metronome by setting the Playlist group to stop looping. Here’s that script in detail:

tell application id "com.figure53.QLab.5" to tell front workspace
  
  set playlistCue to cue "110"
  set loopEnabled to playlist loop of playlistCue
  if loopEnabled is true then
    set playlist loop of playlistCue to false
  else -- reset to original state
    set playlist loop of playlistCue to true
  end if
  
end tell

If you run the script again, it resets the Playlist group to loop once more.