QLab Script - bodging a MIDI player

Contents

Table of Contents

    This is an "external script" - see QLab Scripts and Macros

     
    (* Bodging a MIDI player: turn a Standard MIDI File into a sequence in QLab via a text file from http://jjlee.com/midi/mid2txt.php;
        see second display dialog for further explanation
     
    You can also host the php scripts on your own website: get them from http://staff.dasdeck.de/valentin/midi/; put the folder in ~/Sites;
    give the user "www" read & write permissions to the folder & fire up Web Sharing...
     
    This script is not designed to be run from within QLab!
     
    v1.0: 26/09/09 Rich Walsh (with advice from Jeremy Lee, Sean Dougall & Christopher Ashworth)
    v1.1: 27/09/09 Fixed a little bug with notTheFirstTempo; general tidy up
    v1.2: 29/09/09 Added option to add cue for every line, countdown timer, time taken, more sophisticated ETA;
            also implemented Jeremy Lee's modifications: Note On @ 0 shown as "Note Off" (made optional),
            original text file details into notes of each cue (inc line #), bar|beat|tick added to names (my routine)
    v1.3: 04/10/09 Found more efficient way of getting existingNumbers & handling lists
    v1.4: 12/10/09 Snow Leopard can't "get running", so rewrote a sequence; also minor fix to subroutine
    v1.5: 16/10/09 Now "tested" in Snow Leopard; expanded makeNiceT for hours; fixed a bug with first program change not working;
            general tidying; new ETA figures
    v1.6: 11/01/10 Added ability to deal with escape characters in text file (track names can contain spaces);
            worked out less cumbersome way of moving into groups; byte combo origin offset; corrected minor typos; added tell front workspace for elegance;
            wrapped text for better wiki experience; implemented dialogTitle for cross-script pillaging
     
    <<< Last tested with: QLab 2.2.6; Mac OS 10.5.8 & 10.6.2 >>>
     
    <<< A LITTLE DISCLAIMER: This was quite a complex bit of work for me, so I make no guarantees whatsoever that it will work for you: if it does, use it;
        if it doesn't, I'd like to know, but I may choose not to do anything about it... >>> *)
     
    -- ###FIXME### See 3x embedded comments in script
    -- ###FIXME### QLab and/or the "script runner" is, generally, increasingly unresponsive to scripts the more cues there are in a workspace
    -- ###FIXME### Review dialogs in light of this (especially ETA)
    -- ###BUG TBC### The two's complement hack to extract Key Signature might be wrong (the code might fail, or the reported key might be wrong)
    -- ###BUG TBC### Not yet convinced that bar|beat|tick follows correct musical rules
    -- ###ADD### Add Hot Key § at start of All Notes Off sequence - not currently scriptable...
    -- ###ADD### Bypass the php altogether and pull the binary data from the file (!)
     
    -- Declarations
     
    global dialogTitle
    set dialogTitle to "Bodging a MIDI player"
     
    global currentTIDs
    set currentTIDs to AppleScript's text item delimiters
     
    global ticksPerQuarter, currentBar, currentBeat, currentTick, tickModulus, barModulus, barsBeatsTicks, combinedDeltaTick -- Used in bar|beat|tick counter
    set acceptableStrings to {"On", "Off", "PoPr", "ChPr", "Par", "Pb", "PrCh"} -- List of strings that will be processed for straightforward MIDI events
    set exoticStrings to {"Tempo", "SysEx", "KeySig", "TimeSig"} -- List of more complex strings to deal with
    set keySignatures to {"C flat", "G flat", "D flat", "A flat", "E flat", "B flat", "F", "C", "G", "D", "A", "E", "B", "F sharp", "C sharp"}
    -- Convert numbers in text file to familiar names (this is the order in an SMF)
    set removalStrings to {"ch=", "n=", "v=", "c=", "p="} -- List of strings to remove from the text so as to only have numbers to deal with
    set deltaTick to 0 -- Don't miss out times for lines that aren't "valid" events
    set combinedDeltaTick to 0 -- Used to pass cumulative increments to bar|beat|tick counter
    set channelsUsed to {} -- Tracker for all notes off at end
    set notTheFirstTempo to false -- Don't create duplicate Memo Cue for first tempo event
    set notTheFirstTimeSig to false -- Don't create duplicate Memo Cue for first time signature event
    set skipTheProcessingAsNoCueWasMade to false -- If you don't make a cue, don't try to name it (etc) and end up naming the one before!
    set sysExed to false -- Warning dialog for SysEx
    set trackNames to {} -- Make a list of track names as we go
     
    try -- This overall try makes sure TIDs are reset if any "Cancel" button is pushed
     
        -- Preamble
     
        set theNavigator to "Review instructions"
        repeat until theNavigator is "Get on with it"
            set theNavigator to button returned of (display dialog "Would you like to review the instructions for this script?" with title dialogTitle with icon 1 ¬
                buttons {"Review instructions", "Cancel", "Get on with it"} default button "Get on with it" cancel button "Cancel")
            if theNavigator is "Review instructions" then
                set visitWebsite to button returned of (display dialog "This script will take the text output of the MIDI-to-text converter Jeremy Lee " & ¬
                    "has kindly hosted on his website and attempt to turn it into a Group Cue in QLab of those MIDI events.
     
    For this to work, the files need to be Type 0 (single track) Standard MIDI Files before they are converted to text; if you need to flatten " & ¬
                    "them visit the type conversion website using the button below.
     
    The next step is to run your MIDI file through the converter at the MIDI-to-text website, then copy the output (from \"MFile\" to \"TrkEnd\") " & ¬
                    "into a plain text file. Make sure to use the option \"Delta\" for TimestampType, and save the file with the name you would like to use for the cue.
     
    You'll need a workspace open in QLab, but for best results don't have any cues selected. The bigger the file the longer and longer it takes, " & ¬
                    "in a kind of exponential way. With 500+ lines expect it to take at least 15 minutes!
     
    The order of the events is entirely dictated by the order of the lines in the text file. If you particularly want things in the right order, " & ¬
                    "massage the text file first (the order of any pair of lines that both start with \"0\" can be reversed with no ill effects).
     
    Don't be surprised if this isn't 100% successful (feel free to emit a small whoop if it is though)." with title dialogTitle with icon 1 ¬
                    buttons {"Type conversion website", "MIDI-to-text website", "Back to start"} default button "Back to start")
     
                if visitWebsite is "MIDI-to-text website" then
                    open location "http://jjlee.com/midi/mid2txt.php"
                    return
                else if visitWebsite is "Type conversion website" then
                    open location "http://jjlee.com/midi/convert.php"
                    return
                end if
            end if
        end repeat
     
        -- Check QLab is running
     
        tell application "System Events"
            set qLabIsRunning to count (every process whose name is "QLab")
        end tell
        if qLabIsRunning is 0 then
            display dialog "QLab is not running." with title dialogTitle with icon 0 buttons {"OK"} default button "OK" giving up after 5
            return
        end if
     
        -- Test for a workspace
     
        tell application "QLab"
            try
                get selected of front workspace
            on error
                display dialog "There is no workspace open in QLab." with title dialogTitle with icon 0 buttons {"OK"} default button "OK" giving up after 5
                return
            end try
        end tell
     
        -- Get the file
     
        set theFile to choose file with prompt ¬
            "Please select the file that contains the output of the MIDI-to-text converter:" default location (path to desktop) without invisibles
     
        set AppleScript's text item delimiters to ""
     
        try
            set rawText to read theFile
        on error
            my exitStrategy()
        end try
     
        tell application "System Events" -- Get just the name of the file, without the extension
            set theExtension to name extension of theFile
            if theExtension is "" then
                set theName to name of theFile
            else
                set theFullName to name of theFile
                set theName to text 1 through ((length of theFullName) - (length of theExtension) - 1) of theFullName
            end if
        end tell
     
        -- Check the first line looks promising
     
        set AppleScript's text item delimiters to space
     
        try
            set theRecord to every text item of paragraph 1 of rawText
        on error
            my exitStrategy()
        end try
        if item 1 of theRecord is not "MFile" then
            my exitStrategy()
        else if item 2 of theRecord is "1" then
            set visitWebsite to button returned of (display dialog "This appears to be a Type 1 MIDI file (ie: multi-track), which I can't cope with I'm afraid. " & ¬
                "Would you like to try again?" with title dialogTitle with icon 0 buttons {"Type conversion website", "OK"} default button "OK")
            if visitWebsite is "Type conversion website" then
                open location "http://jjlee.com/midi/convert.php"
            end if
            set AppleScript's text item delimiters to currentTIDs
            return
        end if
     
        -- Check for TimestampType=Delta
     
        if rawText does not contain "TimestampType=Delta" then
            set visitWebsite to button returned of (display dialog "I don't think this file was converted with the \"Delta\" option for TimestampType. " & ¬
                "Would you like to try again?" with title dialogTitle with icon 0 buttons {"MIDI-to-text website", "OK"} default button "OK")
            if visitWebsite is "MIDI-to-text website" then
                open location "http://jjlee.com/midi/mid2txt.php"
            end if
            set AppleScript's text item delimiters to currentTIDs
            return
        end if
     
        -- Find out about $9N@0 = $8N
     
        set noteOffVelocitySentiment to button returned of (display dialog "How do you feel about Note Ons with a velocity of 0?
     
    Display them as Note On, or \"Note Off\"? (The underlying MIDI data won't be changed.)" with title dialogTitle with icon 1 ¬
            buttons {"Note On", "Note Off"} default button "Note Off")
     
        -- Make a cue for every line?
     
        set everyLineIsACue to button returned of ¬
            (display dialog "Would you like me to create a Memo Cue for any lines in the text file that I don't understand " & ¬
                "(along with all the lines that contain just metadata)?" with title dialogTitle with icon 1 buttons {"Yes", "No"} default button "No")
     
        -- How long is this going to take? Quite a bit of faff just to make a grammatically-correct yet, irritatingly, flashing dialog...
     
        set countText to count paragraphs of rawText
        -- ###FIXME### Current estimate based on second order polynomial interpolation of 3 sample data points (!): get more data
        set theETA to 49 - 0.535 * countText + 0.005 * (countText ^ 2)
        if theETA is greater than 40 then -- Don't waste 10s telling you it's going to take 30s!
            set timeString to my makeNiceT(theETA)
            set spuriousPlurals to " seconds"
     
            repeat with i from 10 to 1 by -1
                if i is 1 then
                    set spuriousPlurals to " second"
                end if
                set goOnThen to button returned of (display dialog (("There are " & countText as string) & " lines in this text file.
     
    Based on current tests, this may take about " & timeString & " to process, possibly longer. 
     
    You have " & i as string) & spuriousPlurals & " to hit \"Cancel\"..." with title dialogTitle with icon 0 ¬
                    buttons {"Cancel", "OK"} default button "OK" cancel button "Cancel" giving up after 1)
                if goOnThen is "OK" then
                    exit repeat
                end if
            end repeat
        end if
     
        -- Set the tempo & prepare bar|beat|tick information (my version of Jeremy Lee's modification to add event time to start of each q name)
     
        set tempoFromFile to 500000 -- Use default of 120bpm (ie: 0.5s per quarter) if no tempo found in file
        set timeSigFromFile to "4/4"
        set tempoFound to false
        set timeSigFound to false
     
        try
            set ticksPerQuarter to item 4 of theRecord
            if ticksPerQuarter is less than 0 then
                display dialog "I think this file is in SMPTE timing rather than clicks and I'm not clever enough to deal with that. Sorry." with title ¬
                    dialogTitle with icon 0 buttons {"OK"} default button "OK"
                set AppleScript's text item delimiters to currentTIDs
                return
            end if
            repeat with i from 2 to countText -- Get the first tempo & time signature events that appear in the file (skipping the file header)
                set theRecord to every text item of paragraph i of rawText
                if (count theRecord) is greater than 1 then
                    if item 1 of theRecord > 0 then -- "Initial" values can't come after a delta time has been encountered
                        exit repeat
                    end if
                    if item 2 of theRecord is "Tempo" then
                        set tempoFromFile to item 3 of theRecord
                        set tempoFound to true
                    else if item 2 of theRecord is "TimeSig" then
                        set timeSigFromFile to item 3 of theRecord
                        set timeSigFound to true
                    end if
                    if tempoFound is true and timeSigFound is true then
                        exit repeat
                    end if
                end if
            end repeat
        on error
            my exitStrategy()
        end try
     
        set theTempo to 60 * (1000000 / tempoFromFile) -- This is the tempo as bpm (tempo is expressed in SMFs as the duration of a quarter note in microseconds)
        set tickToTime to (tempoFromFile / 1000000) * (1 / ticksPerQuarter) -- This is the duration of each tick
     
        set {currentBar, currentBeat, currentTick} to {1, 0, 0}
        set AppleScript's text item delimiters to "/" -- Extract the relevant info
        set theNumerator to text item 1 of timeSigFromFile
        set theDenominator to text item 2 of timeSigFromFile
        set AppleScript's text item delimiters to space
        set barModulus to theNumerator
        set tickModulus to ticksPerQuarter * (4 / theDenominator)
     
        -- Remove unnecessary text
     
        set theText to rawText -- Keep a copy of the untouched text for entry into notes
        repeat with eachString in removalStrings
            set AppleScript's text item delimiters to eachString
            set cleanText to text items of theText
            set AppleScript's text item delimiters to ""
            set theText to cleanText as text
        end repeat
        set AppleScript's text item delimiters to space
     
        -- Now, to business
     
        tell application "QLab"
     
            activate
     
            display dialog "One moment caller..." with title dialogTitle with icon 1 buttons {"OK"} default button "OK" giving up after 1
     
            set startTime to time of (current date)
     
            tell front workspace
     
                -- Make a new Group Cue for the sequence
     
                make type "Group"
                set theGroupCue to last item of (selected as list)
                set mode of theGroupCue to fire_first_go_to_next_cue
                set q name of theGroupCue to "Imported from MIDI/text file: " & theName
     
                -- Make the first cue in the group
     
                make type "Memo"
                set cueTheFirst to last item of (selected as list)
                set continue mode of cueTheFirst to auto_continue
                set q name of cueTheFirst to "Ticks per quarter: " & ticksPerQuarter
     
                -- Move the Memo Cue inside the Group Cue
     
                set cueTheFirstID to uniqueID of cueTheFirst
                set cueTheFirstIsIn to the first cue whose (q type is "Group" and cues contains cueTheFirst)
                set theGroupCueID to uniqueID of theGroupCue
                move (the first cue whose uniqueID is cueTheFirstID) of cueTheFirstIsIn to end of (the first cue whose uniqueID is theGroupCueID)
     
                -- Back to the main part
     
                make type "Memo"
                set newCue to last item of (selected as list)
                set continue mode of newCue to auto_continue
                set tempoString to my toThreePlaces(theTempo) -- Express tempo to 3 decimal places
                if tempoFound is false then
                    set qualifyString to " (assumed)"
                    set notes of newCue to "Tempo not found at top of file"
                else
                    set qualifyString to ""
                end if
                set q name of newCue to "Initial tempo: " & tempoString & "bpm" & qualifyString
     
                make type "Memo"
                set newCue to last item of (selected as list)
                set continue mode of newCue to auto_continue
                if timeSigFound is false then
                    set qualifyString to " (assumed)"
                    set notes of newCue to "Time signature not found at top of file"
                else
                    set qualifyString to ""
                end if
                set q name of newCue to "Initial time signature: " & timeSigFromFile & qualifyString
     
                repeat with i from 1 to countText
                    set theRecord to paragraph i of theText
                    set rawRecord to paragraph i of rawText
                    set dirtyFields to text items of theRecord
                    set theFields to my parseSpaceDelimitedTextWithEscapeCharacters(dirtyFields)
                    try -- This protects against empty/incomplete lines
                        set eventType to item 2 of theFields
                        if eventType is in acceptableStrings then
                            set combinedDeltaTick to (item 1 of theFields) + deltaTick
                            my bbtCounter()
                            set preWait to combinedDeltaTick * tickToTime
                            set deltaTick to 0
                            set theChannel to item 3 of theFields
                            set byteOne to item 4 of theFields
                            try -- Not all events have 5 fields
                                set byteTwo to item 5 of theFields
                            end try
                            make type "MIDI"
                            set newCue to last item of (selected as list)
                            if eventType is "On" then
                                set command of newCue to note_on
                                set nameString to "Note On | "
                                if noteOffVelocitySentiment is "Note Off" and byteTwo is "0" then
                                    set nameString to "\"Note Off\" | "
                                end if
                            else if eventType is "Off" then
                                set command of newCue to note_off
                                set nameString to "Note Off | "
                            else if eventType is "PoPr" then
                                set command of newCue to key_pressure
                                set nameString to "Key Pressure | "
                            else if eventType is "ChPr" then
                                set command of newCue to channel_pressure
                                set nameString to "Channel Pressure | "
                            else if eventType is "Par" then
                                set command of newCue to control_change
                                set nameString to "Control Change | "
                            else if eventType is "Pb" then
                                set command of newCue to pitch_bend
                                set nameString to "Pitch Bend | "
                            else if eventType is "PrCh" then
                                set command of newCue to program_change
                                set nameString to "Program Change | "
                            end if
                            if eventType is not "Pb" then
                                set byte one of newCue to byteOne
                                set nameString to nameString & byteOne as string
                                if eventType is not "ChPr" and eventType is not "PrCh" then
                                    set byte two of newCue to byteTwo
                                    set nameString to nameString & " @ " & byteTwo as string -- ###FIXME### Is a Note On @ 0 actually a Note Off @ 0, 
                                    (* or a Note Off @ 64? Current output is the former *)
                                end if
                            else
                                set byte combo of newCue to byteOne
                                set nameString to nameString & (byteOne - 8192) as string -- Pitch bend of 0 will appear as 8192 in the file
                            end if
                            set channel of newCue to theChannel
                            set pre wait of newCue to preWait
                            set q name of newCue to (barsBeatsTicks & " - Channel " & theChannel as string) & " | " & nameString
                            set notes of newCue to "Line " & i & ": " & rawRecord -- Put the text from the file into the notes of the cue (Jeremy Lee)
                            set continue mode of newCue to auto_continue
                            if theChannel is not in channelsUsed then
                                copy theChannel to end of channelsUsed
                            end if
                        else if eventType is in exoticStrings then
                            set combinedDeltaTick to (item 1 of theFields) + deltaTick
                            my bbtCounter()
                            set preWait to combinedDeltaTick * tickToTime
                            set deltaTick to 0
                            if eventType is "Tempo" then
                                if notTheFirstTempo is false and tempoFound is true then
                                    set notTheFirstTempo to true
                                    if everyLineIsACue is "No" then
                                        set skipTheProcessingAsNoCueWasMade to true
                                    else
                                        make type "Memo"
                                        set newCue to last item of (selected as list)
                                        set tempoString to my toThreePlaces(theTempo)
                                        set nameString to "Initial tempo metadata: " & tempoString & "bpm"
                                    end if
                                else
                                    set tempoFromFile to item 3 of theFields
                                    set theTempo to 60 * (1000000 / tempoFromFile)
                                    set tickToTime to (tempoFromFile / 1000000) * (1 / ticksPerQuarter)
                                    make type "Memo"
                                    set newCue to last item of (selected as list)
                                    set tempoString to my toThreePlaces(theTempo)
                                    set nameString to "Tempo: " & tempoString & "bpm"
                                end if
                            end if
                            if eventType is "SysEx" then -- There is no error checking for this!
                                set sysExed to true
                                make type "MIDI SysEx"
                                set newCue to last item of (selected as list)
                                set howManyFields to count theFields
                                set theSysEx to {}
                                repeat with j from 4 to (howManyFields - 1)
                                    copy item j of theFields to end of theSysEx
                                end repeat
                                set theSysExString to theSysEx as text
                                try
                                    set sysex message of newCue to theSysExString
                                on error
                                    my exitStrategy()
                                end try
                                set nameString to ("SysEx | " & (howManyFields - 4) as string) & " bytes |"
                            end if
                            if eventType is "KeySig" then
                                make type "Memo"
                                set newCue to last item of (selected as list)
                                try
                                    set keySigNumber to item 3 of theFields as number
                                    if keySigNumber is greater than 127 then
                                        set keySigNumber to (keySigNumber - 256) -- MIDI-to-text converter not handling this item well
                                    end if
                                    set keySigNumber to keySigNumber + 8
                                    if keySigNumber is greater than 0 then
                                        set keySigString to item keySigNumber of keySignatures
                                    else
                                        set keySigString to "Invalid data..."
                                    end if
                                on error
                                    set keySigString to "Invalid data..."
                                end try
                                set nameString to "Key: " & keySigString & " " & item 4 of theFields
                            end if
                            if eventType is "TimeSig" then
                                if notTheFirstTimeSig is false and timeSigFound is true then
                                    set notTheFirstTimeSig to true
                                    if everyLineIsACue is "No" then
                                        set skipTheProcessingAsNoCueWasMade to true
                                    else
                                        make type "Memo"
                                        set newCue to last item of (selected as list)
                                        set nameString to "Initial time signature metadata: " & timeSigFromFile
                                    end if
                                else
                                    set timeSig to item 3 of theFields
                                    set AppleScript's text item delimiters to "/" -- Extract the relevant info
                                    set theNumerator to text item 1 of timeSig
                                    set theDenominator to text item 2 of timeSig
                                    set AppleScript's text item delimiters to space
                                    set barModulus to theNumerator
                                    set tickModulus to ticksPerQuarter * (4 / theDenominator)
                                    if (currentBar + currentBeat + currentTick) is not 1 then -- Force a new bar, but only if we've got past the very start of the file!
                                        set currentBar to currentBar + 1
                                        set {currentBeat, currentTick} to {0, 0}
                                        my bbtCounter() -- Force the bar|beat|tick counter to display new information
                                    end if
                                    make type "Memo"
                                    set newCue to last item of (selected as list)
                                    set nameString to "Time signature: " & timeSig
                                end if
                            end if
                            if skipTheProcessingAsNoCueWasMade is false then
                                set pre wait of newCue to preWait
                                set q name of newCue to barsBeatsTicks & " - " & nameString
                                set notes of newCue to "Line " & i & ": " & rawRecord -- Put the text from the file into the notes of the cue (Jeremy Lee)
                                set continue mode of newCue to auto_continue
                            else
                                set skipTheProcessingAsNoCueWasMade to false
                            end if
                        else if eventType is "Meta" and item 3 of theFields is "TrkName" then
                            copy item 4 of theFields to end of trackNames
                            if everyLineIsACue is "No" then
                                set deltaTick to deltaTick + (item 1 of theFields)
                            else
                                set combinedDeltaTick to (item 1 of theFields) + deltaTick
                                my bbtCounter()
                                set preWait to combinedDeltaTick * tickToTime
                                set deltaTick to 0
                                make type "Memo"
                                set newCue to last item of (selected as list)
                                set pre wait of newCue to preWait
                                set q name of newCue to barsBeatsTicks & " - Track name: " & item 4 of theFields
                                set notes of newCue to "Line " & i & ": " & rawRecord -- Put the text from the file into the notes of the cue (Jeremy Lee)
                                set continue mode of newCue to auto_continue
                            end if
                        else if eventType is "Meta" and item 3 of theFields is "TrkEnd" then
                            if everyLineIsACue is "No" then
                                try
                                    set deltaTick to deltaTick + (item 1 of theFields)
                                end try
                            else
                                set combinedDeltaTick to (item 1 of theFields) + deltaTick
                                my bbtCounter()
                                set preWait to combinedDeltaTick * tickToTime
                                set deltaTick to 0
                                make type "Memo"
                                set newCue to last item of (selected as list)
                                set pre wait of newCue to preWait
                                set q name of newCue to barsBeatsTicks & " - End of track"
                                set notes of newCue to "Line " & i & ": " & rawRecord -- Put the text from the file into the notes of the cue (Jeremy Lee)
                                set continue mode of newCue to auto_continue
                            end if
                        else
                            if everyLineIsACue is "No" then
                                try
                                    set deltaTick to deltaTick + (item 1 of theFields)
                                end try
                            else
                                set combinedDeltaTick to (item 1 of theFields) + deltaTick
                                my bbtCounter()
                                set preWait to combinedDeltaTick * tickToTime
                                set deltaTick to 0
                                make type "Memo"
                                set newCue to last item of (selected as list)
                                set pre wait of newCue to preWait
                                set q name of newCue to barsBeatsTicks & " - Unprocessed line"
                                set notes of newCue to "Line " & i & ": " & rawRecord -- Put the text from the file into the notes of the cue (Jeremy Lee)
                                set continue mode of newCue to auto_continue
                            end if
                        end if
                    on error
                        if everyLineIsACue is "Yes" then
                            make type "Memo"
                            set newCue to last item of (selected as list)
                            try
                                set processCheck to item 1 of theFields
                                if processCheck is "MFile" then
                                    set q name of newCue to "Start of file"
                                else if processCheck is "TrkEnd" then
                                    set q name of newCue to "End of file"
                                else if processCheck is "0" then
                                    set q name of newCue to "Unprocessed line (no event)"
                                else
                                    set q name of newCue to "Unprocessed line (no time marker)"
                                end if
                            on error
                                set q name of newCue to "Unprocessed line (no time marker)"
                            end try
                            set notes of newCue to "Line " & i & ": " & rawRecord -- Put the text from the file into the notes of the cue (Jeremy Lee)
                            set continue mode of newCue to auto_continue
                        end if
                        try
                            set endCheck to item 1 of theFields
                            if endCheck is "TrkEnd" then -- Special case for end of file: 
                                (* all notes off for all channels used - and always add a Memo to make sure final cue isn't an auto-continue! *)
                                set numberOfChannels to count channelsUsed
                                if numberOfChannels is not 0 then
                                    set currentCue to last item of (selected as list)
                                    set continue mode of currentCue to auto_continue
                                    set sortedChannels to my simple_sort(channelsUsed)
                                    repeat with j from 1 to numberOfChannels
                                        set eachChannel to item j of sortedChannels
                                        make type "MIDI"
                                        set newCue to last item of (selected as list)
                                        if j is 1 then
                                            set pre wait of newCue to deltaTick * tickToTime
                                            -- set hot key of newCue to "§" (###FIXME### Not currently scriptable: QLab 2.2.6)
                                        end if
                                        set command of newCue to control_change
                                        set byte one of newCue to 123
                                        set byte two of newCue to 0
                                        set channel of newCue to eachChannel
                                        set q name of newCue to barsBeatsTicks & " - All Notes Off: Channel " & eachChannel
                                        set continue mode of newCue to auto_continue
                                    end repeat
                                end if
                                make type "Memo"
                                set newCue to last item of (selected as list)
                                set q name of newCue to "Number of named tracks declared in file metadata: " & (count trackNames) as string
                                set AppleScript's text item delimiters to return
                                set allTracks to trackNames as text
                                set AppleScript's text item delimiters to "\""
                                set cleanTracks to text items of allTracks
                                set AppleScript's text item delimiters to ""
                                set allTracks to cleanTracks as text
                                set notes of newCue to allTracks
                                exit repeat -- Exit the overall repeat as no more cues to add after track end!
                            end if
                        end try
                    end try
                    if i mod 50 is 0 and (countText - i) > 25 then -- Countdown timer (and opportunity to escape)
                        set timeTaken to ((time of (current date)) - startTime) as integer
                        set timeString to my makeMMSS(timeTaken)
                        if application "QLab" is frontmost then
                            display dialog (("Time elapsed: " & timeString & " - " & i as string) & " of " & countText as string) & " lines done..." with title ¬
                                dialogTitle with icon 1 buttons {"Cancel", "OK"} default button "OK" cancel button "Cancel" giving up after 1
                        end if
                    end if
                end repeat
     
            end tell
     
            if sysExed is true then
                set sysExDisclaimer to "
     
    I haven't checked those SysEx cues, so look out..."
            else
                set sysExDisclaimer to ""
            end if
     
            set timeTaken to ((time of (current date)) - startTime) as integer
            set timeString to my makeNiceT(timeTaken)
            activate
            display dialog "Done. 
     
    (That took " & timeString & ".)" & sysExDisclaimer with title dialogTitle with icon 1 buttons {"OK"} default button "OK" giving up after 60
     
        end tell
     
        set AppleScript's text item delimiters to currentTIDs
     
    on error number -128
        set AppleScript's text item delimiters to currentTIDs
    end try
     
    -- Subroutines
     
    on exitStrategy()
        display dialog "I'm afraid that file tasted funny so I've had to spit it out. Please check the file and try again. Sorry." with title ¬
            dialogTitle with icon 0 buttons {"OK"} default button "OK"
        set AppleScript's text item delimiters to currentTIDs
        error number -128
    end exitStrategy
     
    on parseSpaceDelimitedTextWithEscapeCharacters(theText) -- Deal with " as escape character
        set tempTIDS to AppleScript's text item delimiters
        set AppleScript's text item delimiters to space
        set dirtyText to text items of theText
        set AppleScript's text item delimiters to ""
        set cleanTextItems to {}
        set midClean to false
        set cleanStore to ""
        repeat with eachItem in dirtyText
            if eachItem starts with "\"" then
                set cleanStore to (rest of characters of eachItem as string)
                set midClean to true
            else
                if midClean is true then
                    if eachItem does not end with "\"" then
                        set cleanStore to cleanStore & space & eachItem
                    else
                        set cleanStore to cleanStore & space & (characters 1 thru ((count eachItem) - 1) of eachItem as string)
                        copy cleanStore as string to end of cleanTextItems
                        set midClean to false
                    end if
                else
                    copy eachItem as string to end of cleanTextItems
                end if
            end if
        end repeat
        set AppleScript's text item delimiters to tempTIDS
        return cleanTextItems
    end parseSpaceDelimitedTextWithEscapeCharacters
     
    on makeNiceT(howLong)
        if howLong is 0 then
            return "less than a second"
        end if
        set howManyHours to howLong div 3600
        if howManyHours is 0 then
            set hourString to ""
        else if howManyHours is 1 then
            set hourString to "1 hour"
        else
            set hourString to (howManyHours as string) & " hours"
        end if
        set howManyMinutes to (howLong mod 3600) div 60
        if howManyMinutes is 0 then
            set minuteString to ""
        else if howManyMinutes is 1 then
            set minuteString to "1 minute"
        else
            set minuteString to (howManyMinutes as string) & " minutes"
        end if
        set howManySeconds to howLong mod 60 as integer
        if howManySeconds is 0 then
            set secondString to ""
        else if howManySeconds is 1 then
            set secondString to "1 second"
        else
            set secondString to (howManySeconds as string) & " seconds"
        end if
        if hourString is not "" then
            if minuteString is not "" and secondString is not "" then
                set theAmpersand to ", "
            else if minuteString is not "" or secondString is not "" then
                set theAmpersand to " and "
            else
                set theAmpersand to ""
            end if
        else
            set theAmpersand to ""
        end if
        if minuteString is not "" and secondString is not "" then
            set theOtherAmpersand to " and "
        else
            set theOtherAmpersand to ""
        end if
        return hourString & theAmpersand & minuteString & theOtherAmpersand & secondString
    end makeNiceT
     
    on toThreePlaces(theNumber)
        set theIntegral to theNumber div 1
        set theFraction to theNumber mod 1
        set theFraction to round (1000 * theFraction) rounding as taught in school
        if theFraction > 99 then
            set theFractionString to theFraction as string
        else if theFraction > 9 then
            set theFractionString to "0" & theFraction as string
        else
            set theFractionString to "00" & theFraction as string
        end if
        return (theIntegral as string) & "." & theFractionString
    end toThreePlaces
     
    on bbtCounter()
        set tickCarry to (currentTick + combinedDeltaTick) div tickModulus
        set currentTick to (currentTick + combinedDeltaTick) mod tickModulus as integer
        set beatCarry to (currentBeat + tickCarry) div barModulus
        set currentBeat to (currentBeat + tickCarry) mod barModulus as integer
        set currentBar to currentBar + beatCarry as integer
        set tickString to currentTick as string
        repeat until length of tickString is 3
            set tickString to "0" & tickString
        end repeat
        set barsBeatsTicks to ((currentBar as string) & " | " & currentBeat + 1 as string) & " | " & tickString
    end bbtCounter
     
    on makeMMSS(howLong)
        set howManyMinutes to howLong div 60
        set minuteString to (howManyMinutes as string)
        set howManySeconds to howLong mod 60 as integer
        if howManySeconds > 9 then
            set secondString to (howManySeconds as string)
        else
            set secondString to "0" & (howManySeconds as string)
        end if
        return minuteString & ":" & secondString
    end makeMMSS
     
    -- This subroutine was taken from http://www.macosxautomation.com/applescript/sbrt/sbrt-05.html
     
    on simple_sort(my_list)
        set the index_list to {}
        set the sorted_list to {}
        repeat (the number of items in my_list) times
            set the low_item to ""
            repeat with i from 1 to (number of items in my_list)
                if i is not in the index_list then
                    set this_item to item i of my_list as text
                    if the low_item is "" then
                        set the low_item to this_item
                        set the low_item_index to i
                    else if this_item comes before the low_item then
                        set the low_item to this_item
                        set the low_item_index to i
                    end if
                end if
            end repeat
            set the end of sorted_list to the low_item
            set the end of the index_list to the low_item_index
        end repeat
        return the sorted_list
    end simple_sort
     
    (* END: Bodging a MIDI player *)