Multifiler

QLab supports audio files with up to 24 channels, but making multichannel files according to your own specifications turns out to be surprisingly tricky. While most DAWs support the output of multichannel file in specific output formats intended for use in broadcast and cinema production, many folks want to use multichannel files in QLab to mix stems, where each channel represents an instrument or group of instruments. This sort of output is possible from some DAWs, but not most of them. This chapter describes a script which converts several individual Audio cues, each with their own channels, into a new cue with its own multichannel file. This can streamline your cue list, speed up editing, and simplify copying or moving the cue around.

Usage

Using this script is very straightforward: select two or more Audio cues that you want to combine, and run the script. You can either run the script using Script Editor, or put the script in a Script cue and run the cue using a hotkey, MIDI trigger, or OSC.

Here it is in action:

Setup

This method relies on an external command-line tool called sox, which is billed as “the Swiss Army knife of audio manipulation.” You can read about sox here. We recommend installing sox using Homebrew, which is a tool for easily installing command-line based software on the Mac. If you haven’t yet installed Homebrew, the instructions to do so are easy to follow. Once homebrew is installed, install sox by opening a Terminal window and entering the command:

brew install sox

Once sox is installed, no further configuration is necessary.

Details

We’ll go through the script section by section. The script is listed in total after that, and the downloadable example includes the same script inside a Script cue.

-- if you've installed sox NOT using homebrew
-- or if this script doesn't seem to be able to find sox
-- you'll need to explicitly set soxPath to the full path
-- to the sox executable.
-- mostly, this command does it properly:

set soxPath to do shell script "bash -l -c '/usr/bin/which sox'"

-- if it doesn't work for you, edit the command below
-- and use it to replace the command above
-- set soxPath to /path/to/sox

This section is necessary because AppleScript runs shell commands without knowledge of your PATH variable. Since Homebrew supports using a custom path, and since it is obligated to use different defaults on different macOS systems, we can’t just hard-code the path to sox into this script. The set soxPath line runs a little shell command which attempts to ascertain the location of sox, but if for some reason that doesn’t work you’ll need to manually set the path to sox yourself.

tell application id "com.figure53.qlab.5"
  set theParent to q name of parent of first item of (selected of front workspace as list)
  set theFirstCue to q list name of first item of (selected of front workspace as list)
  set theDate to do shell script "date \"+%Y%m%d-%H%M%S\""

theParent is the name of the cue that contains the selected cues. We’ll use it to name the new cue later on if the cues are inside a Group. theFirstCue is the name of the first of the selected cues. We’ll use is to name the new cue later on if the cues are not inside a Group.

theDate is today’s date and the time, which we’ll use to make sure we uniquely name the files we’ll be creating.

  -- ask how many outputs we're using
  -- saves time to not go through all 64 outputs if we don't need to
  display dialog "How many cue outputs are we using?" default answer "64" buttons {"Cancel", "OK"} default button "OK" cancel button "Cancel"
  set numberOfOutputs to (text returned of result) as integer

Folks don’t always use all 64 cue outputs, and it’s a nice little timesaver to only deal with the number of cue outputs that are actually in use. Since there’s no way for the script to know what that is, we ask the human and store the answer.

  -- prepare to flip through the selected cues
  set originalCues to (selected of front workspace as list)
  set theFiles to ""
  set totalChannels to 0
  set theLevels to {}
  set theSlices to {}

We’re about to run a big “repeat” loop, and this sets up all the variables that we’ll be using. theLevels is a list which will hold all the levels of all the channels of all the cues. theSlices is a list which will hold all the slices of all the cues.

  -- now do it
  repeat with eachCue in originalCues
    if q type of eachCue is "Audio" then
      -- get file targets of original cues
      set theFile to file target of eachCue
      set preWait to pre wait of eachCue

Make sure we’re only looking at Audio cues, then loop through all the cues, grabbing the file target and pre-wait of each one.

      -- if there's a pre-wait, make a new file
      -- with the pre-wait baked in
      if preWait > 0 then
        set theFilePath to POSIX path of theFile
        set theFileName to q number of (info for theFilePath)
        do shell script soxPath & " " & theFilePath & " ~/Desktop/pad-" & theDate & "-" & theFileName & " pad " & preWait
        set theFiles to theFiles & " ~/Desktop/pad-" & theDate & "-" & theFileName
      else
        set theFiles to theFiles & " " & POSIX path of theFile
      end if

If a cue has a pre-wait, we want to “bake” that pre-wait into the final product, so we create a new audio file using sox which creates a copy of the original file target of the cue, padding the beginning of the file by the pre-wait time. That new file is saved to the desktop with the word “pad” and the date and time prepended to the filename.

If the cue doesn’t have a pre-wait, we just add its file target to the list of files that we’ll merge later.

      -- get levels of original cues
      set theChannels to audio input channels of eachCue
      set totalChannels to totalChannels + theChannels
      repeat with input from 1 to theChannels
        repeat with output from 1 to numberOfOutputs
          set theLevel to eachCue getLevel row input column output
          copy theLevel to end of theLevels
        end repeat
      end repeat

First, we find out how many channels the cue has. Then we get the crosspoint levels for each of those channels and append those levels to the end of the list of levels.

      -- get slices of original cues
      set allSlices to slice markers of eachCue
      repeat with eachSlice in allSlices
        copy eachSlice to end of theSlices
      end repeat
    end if
  end repeat

Getting slices from each cue is simple. We do that, then add the slices from each cue to the end of the list of slices.

  -- make combined file using the list we just built
  set theNewFile to "combined-" & theDate & ".aiff"
  do shell script soxPath & " -M " & theFiles & " -t aiff -r 48000 -b 24 ~/Desktop/" & theNewFile

First, prepare the name of the new file as the word “combined-”, the date and time, and the suffix .aiff.

Then actually make the new audio file, which turns out to be pretty simple too. The options used are:

  • -M - merge input files into a new multichannel file.
  • -t aiff - make the new file an AIFF file.
  • -r 48000 - make the sample rate of the new file 48 kHz.
  • -b 24 - make the bit depth of the new file 24 bits.

Then the new file gets saved on the desktop.

  -- make a new cue that uses the new combined file
  make front workspace type "Audio"
  set newCue to last item of (selected of front workspace as list)
  set theTarget to ((path to desktop folder) & theNewFile) as string
  set file target of newCue to theTarget
  if theParent is "Main Cue List" then
    set q name of newCue to "multitrack with " & theFirstCue & " and others"
  else
    set q name of newCue to theParent & " (multitrack)"
  end if

Now we create a new Audio cue, set its target to the new audio file we just created, and give it a name. If the old cues were inside a Group cue, we’ll use the name of that Group cue as the basis for the new cue’s name. If not, we’ll use the name of the first of the original cues as the basis. In either case, of course, you can just change the name to whatever you like after the fact.

  -- set levels of the new cue
  repeat with input from 1 to totalChannels
    repeat with output from 1 to numberOfOutputs
      set theItem to ((input - 1) * numberOfOutputs + output)
      set theLevel to item theItem of theLevels
      newCue setLevel row input column output db theLevel
    end repeat
  end repeat

Now we loop through the list of levels we made before and apply them to the levels in the new cue.

This is a little tricky; there’s an outer repeat loop which steps through the rows, and then an inner loop which steps through the columns. The outer loop counts from 1 up to totalChannels, which is the count we’d been maintaining back when we pulled levels out of the original cues. That way, we know how many rows there are. The inner loop counts up from 1 to the number of outputs that we asked about at the beginning of the script.

This line is the hardest part:

      set theItem to ((input - 1) * numberOfOutputs + output)

The levels from all the crosspoints of all the original cues are stored in one big list. Since we know the order that levels went into that list, we can use this little bit of math to grab the item from the list that corresponds to where we are in both loops. If, for example, we’re in the third iteration of the outer loop (i.e. the levels corresponding to the third channel in the input files) and the fourth iteration of the inner loop (i.e. cue output 4), and we told the script we’re using 10 outputs in total, then this line becomes, effectively, set theItem to ((3 - 1) * 10 + 4). That makes theItem equal to 24, which means “the 24th item in the list of levels,” which checks out: each row is ten items, we’re on the third row, fourth cell. Item number 24.

  -- set slices of the new cue
  set slice markers of newCue to theSlices
  
end tell

Finally, we apply our list of slices to the new cue, so that all the slice markers and slice counts from the original cues are applied to the new cue.

Wrap-up

There are a few loose ends you need to tie up manually after running the script. First of all, file targets set via AppleScript don’t get automatically copied into the project folder, so you’ll need to manually move the new combined-....aiff file into the project folder yourself. If any intermediary “pad-” files were created, you’ll need to delete those yourself. And if any slices from the original cues overlapped in a weird way, you’ll need to tidy that up yourself.

The Script In Total

Here’s the script in its totality:

-- if you've installed sox NOT using homebrew
-- or if this script doesn't seem to be able to find sox
-- you'll need to explicitly set soxPath to the full path
-- to the sox executable.
-- mostly, this command does it properly:

set soxPath to do shell script "bash -l -c '/usr/bin/which sox'"

-- if it doesn't work for you, edit the command below
-- and use it to replace the command above
-- set soxPath to /path/to/sox

tell application id "com.figure53.qlab.5"
  set theParent to q name of parent of first item of (selected of front workspace as list)
  set theFirstCue to q list name of first item of (selected of front workspace as list)
  set theDate to do shell script "date \"+%Y%m%d-%H%M%S\""
  
  -- ask how many outputs we're using
  -- saves time to not go through all 64 outputs if we don't need to
  display dialog "How many cue outputs are we using?" default answer "64" buttons {"Cancel", "OK"} default button "OK" cancel button "Cancel"
  set numberOfOutputs to (text returned of result) as integer
  
  -- prepare to flip through the selected cues
  set originalCues to (selected of front workspace as list)
  set theFiles to ""
  set totalChannels to 0
  set theLevels to {}
  set theSlices to {}
  
  -- now do it
  repeat with eachCue in originalCues
    if q type of eachCue is "Audio" then
      -- get file targets of original cues
      set theFile to file target of eachCue
      set preWait to pre wait of eachCue
      
      -- if there's a pre-wait, make a new file
      -- with the pre-wait baked in
      if preWait > 0 then
        set theFilePath to POSIX path of theFile
        set theFileName to q number of (info for theFilePath)
        do shell script soxPath & " " & theFilePath & " ~/Desktop/pad-" & theDate & "-" & theFileName & " pad " & preWait
        set theFiles to theFiles & " ~/Desktop/pad-" & theDate & "-" & theFileName
      else
        set theFiles to theFiles & " " & POSIX path of theFile
      end if
      
      -- get levels of original cues
      set theChannels to audio input channels of eachCue
      set totalChannels to totalChannels + theChannels
      repeat with input from 1 to theChannels
        repeat with output from 1 to numberOfOutputs
          set theLevel to eachCue getLevel row input column output
          copy theLevel to end of theLevels
        end repeat
      end repeat
      
      -- get slices of original cues
      set allSlices to slice markers of eachCue
      repeat with eachSlice in allSlices
        copy eachSlice to end of theSlices
      end repeat
    end if
  end repeat
  
  -- make combined file using the list we just built
  set theNewFile to "combined-" & theDate & ".aiff"
  do shell script soxPath & " -M " & theFiles & " -t aiff -r 48000 -b 24 ~/Desktop/" & theNewFile
  
  -- make a new cue that uses the new combined file
  make front workspace type "Audio"
  set newCue to last item of (selected of front workspace as list)
  set theTarget to ((path to desktop folder) & theNewFile) as string
  set file target of newCue to theTarget
  if theParent is "Main Cue List" then
    set q name of newCue to "multitrack with " & theFirstCue & " and others"
  else
    set q name of newCue to theParent & " (multitrack)"
  end if
  
  -- set levels of the new cue
  repeat with input from 1 to totalChannels
    repeat with output from 1 to numberOfOutputs
      set theItem to ((input - 1) * numberOfOutputs + output)
      set theLevel to item theItem of theLevels
      newCue setLevel row input column output db theLevel
    end repeat
  end repeat
  
  -- set slices of the new cue
  set slice markers of newCue to theSlices
  
end tell