No internet connection
  1. Home
  2. How to

Monitor an ongoing process and display results (Stopwatch)

By Andrew Sherman @Andrew_Sherman
    2023-07-17 12:13:18.415Z

    I'm trying to write a stopwatch script that helps me quickly time voiceover scripts.

    Trigger script: stopwatch starts and the time elapsed is displayed somehow, continuously updating
    Toggle script: checks if script is running and if so, terminates the process

    Desired output: Duration: 2m 13s
    Perhaps showing in a notification or alert window

    Can you let me know how you might do something like this?

    Solved in post #3, click to view
    • 13 replies

    There are 13 replies. Estimated reading time: 24 minutes

    1. Dustin Harris @Dustin_Harris
        2023-07-17 20:08:58.548Z

        I can't easily make something that auto-updates as the timer is running, but here's something:

        /**
        * @param {number} s - (seconds) convert milliseconds into HH:MM:SS
        */
        function msToTime(s) {
            try {
                var ms = s % 1000;
                s = (s - ms) / 1000;
                var secs = s % 60;
                s = (s - secs) / 60;
                var mins = s % 60;
                var hrs = (s - mins) / 60;
        
                /**
                * @param {number} valueAsNumber - adds leading zeros to single digit numbers
                */
                function addZerosAsString(valueAsNumber) {
                    let value = valueAsNumber.toString();
                    if (value.length === 1) value = value.padStart(2, "0");
                    return (value === `NaN`) ? "--" : value;
                }
        
                const seconds = addZerosAsString(secs);
                const minutes = addZerosAsString(mins);
                const hours = addZerosAsString(hrs);
        
                const remainingMS = String(ms).padStart(3, "0");
        
                if (hours === "00") {
                    return minutes + ':' + seconds + "." + remainingMS;
                } else {
                    return hours + ':' + minutes + ':' + seconds + "." + remainingMS;
                }
            } catch (err) {
                throw `error in msToTime: ${err}`
            }
        }
        
        
        function main() {
        
            if (!globalState.andrewShermanTimer || globalState.andrewShermanTimer == null) {
                globalState.andrewShermanTimer = new Date();
                log('Timer Started');
            
            } else {
                const now = new Date();
                const elapsedTimeMS = now.valueOf() - globalState.andrewShermanTimer.valueOf()
                log(msToTime(elapsedTimeMS))
                globalState.andrewShermanTimer = null
            }
        
        
        
        }
        
        main();
        
        1. AAndrew Sherman @Andrew_Sherman
            2023-07-18 08:57:59.351Z

            Wow Dustin, this is super cool. Thank you so much! There's lots to learn from this.

            I like how you used global state for it.

            I updated it for my needs, removing hours and milliseconds. It displays the result as an alert and also copies the result to the clipboard.

            Thanks again!
            Here's the updated version in case anyone needs it.

            /**
            * @param {number} s - (seconds) convert milliseconds into MM:SS or SS
            */
            function msToTime(s) {
                try {
                    s = Math.floor(s / 1000);
                    var secs = s % 60;
                    s = Math.floor(s / 60);
                    var mins = s;
            
                    /**
                    * @param {number} valueAsNumber - adds leading zeros to single digit seconds
                    */
                    function addZerosAsString(valueAsNumber) {
                        let value = valueAsNumber.toString();
                        if (value.length === 1) value = value.padStart(2, "0");
                        return (value === `NaN`) ? "--" : value;
                    }
            
                    if (mins === 0) {
                        return `Duration: ${secs}s`;
                    } else {
                        const seconds = addZerosAsString(secs);
                        const minutes = mins.toString();
                        return `Duration: ${minutes}:${seconds}`;
                    }
                } catch (err) {
                    throw `error in msToTime: ${err}`
                }
            }
            
            
            function main() {
                if (!globalState.andrewShermanTimer || globalState.andrewShermanTimer == null) {
                    globalState.andrewShermanTimer = new Date();
                    log('Timer Started');
            
                } else {
                    const now = new Date();
                    const elapsedTimeMS = now.valueOf() - globalState.andrewShermanTimer.valueOf();
                    const timeElapsed = msToTime(elapsedTimeMS);
            
                    sf.system.exec({
                        commandLine: `
                            osascript -e 'display alert "" message "${timeElapsed}"'
                        `
                    });
            
                    // Set clipboard
                    sf.clipboard.setText({ text: timeElapsed });
            
                    globalState.andrewShermanTimer = null;
                }
            }
            
            main();
            
            Reply2 LikesSolution
            1. Dustin Harris @Dustin_Harris
                2023-07-18 17:11:40.516Z

                I did a quick refactor because I had hastily built this from other code I had and a lot of it was pretty misleading (like the msToTime function saying it wants seconds as input 😅)

                /**
                 * @param {number} milliseconds - Convert milliseconds into MM:SS or SS
                 * @returns {string} - The formatted duration string
                 */
                function msToTime(milliseconds) {
                  try {
                    let seconds = Math.floor(milliseconds / 1000) % 60;
                    let minutes = Math.floor(milliseconds / 1000 / 60);
                
                    /**
                     * @param {number} value - Adds leading zeros to single-digit numbers
                     * @returns {string} - The number with leading zeros
                     */
                    function addLeadingZeros(value) {
                      let valueAsString = value.toString();
                      
                      if (valueAsString.length === 1) {
                        valueAsString = valueAsString.padStart(2, '0');
                      }
                      
                      return valueAsString;
                    }
                
                    if (minutes === 0) {
                      return `Duration: ${seconds}s`;
                    
                    } else {
                      const secondsString = addLeadingZeros(seconds);
                      const minutesString = minutes.toString();
                      
                      return `Duration: ${minutesString}:${secondsString}`;
                    }
                
                  } catch (err) {
                    throw `Error in msToTime: ${err}`;
                  }
                }
                
                
                function main() {
                  if (!globalState.andrewShermanTimer) {
                    
                    globalState.andrewShermanTimer = new Date();
                    log('Timer Started');
                  
                  } else {
                      
                    const now = new Date();
                    const elapsedTimeMS = now.valueOf() - globalState.andrewShermanTimer.valueOf();
                    const timeElapsed = msToTime(elapsedTimeMS);
                
                    sf.system.exec({
                      commandLine: `
                          osascript -e 'display alert "" message "${timeElapsed}"'
                      `,
                    });
                
                    // Set clipboard
                    sf.clipboard.setText({ text: timeElapsed });
                
                    globalState.andrewShermanTimer = null;
                  }
                }
                
                main();
                
                1. Ddanielkassulke @danielkassulke
                    2023-09-04 08:37:19.613Z

                    Hi @Dustin_Harris ,

                    This is a really elegant script! Thanks. I'm trying to trigger it on launch of Protools as a way to log work hours, and in an effort to make it visually beautiful, I'm wondering if there's a way to pass the result of the elapsed time to a custom soundflow dialog, essentially bypassing the system.exec display alert. It would basically be identical to what I've got from line 40-44: globalState.danielKassulkeTimer = new Date(); sf.interaction.displayDialog({ title: "Session Time Calculator", prompt: "You were working for HH:MM", icon: "/Users/danielkassulke/Desktop/Clock.png", All attempts I've made so far have sabotaged the pseudo-toggle functionality of the script, meaning that each time I tried triggering the script it would be displaying the 'Timer started' dialog, rather than each trigger being dependent on whether the timer is running or not. Is this possible?

                    /**
                     * @param {number} milliseconds - Convert milliseconds into HH:MM:SS
                     * @returns {string} - The formatted duration string
                     */
                    function msToTime(totalMilliseconds) {
                      try {
                        let minutes = Math.floor(totalMilliseconds / 1000 / 60) % 60;
                        let hours = Math.floor(totalMilliseconds / 1000 / 60 / 60);
                    
                        /**
                         * @param {number} value - Adds leading zeros to single-digit numbers
                         * @param {number} length - Total length of the string representation
                         * @returns {string} - The number with leading zeros
                         */
                        function addLeadingZeros(value, length = 2) {
                          let valueAsString = value.toString();
                          
                          while (valueAsString.length < length) {
                            valueAsString = '0' + valueAsString;
                          }
                          
                          return valueAsString;
                        }
                    
                    
                        const minutesString = addLeadingZeros(minutes);
                        const hoursString = addLeadingZeros(hours);
                        
                        return `${hoursString}:${minutesString}`;
                    
                      } catch (err) {
                        throw `Error in msToTime: ${err}`;
                      }
                    }
                    
                    
                    function main() {
                      if (!globalState.danielKassulkeTimer) {
                        
                        globalState.danielKassulkeTimer = new Date();
                        sf.interaction.displayDialog({
                        title: "Session Time Calculator",
                        prompt: "Session timer started",
                        icon: "/Users/danielkassulke/Desktop/Clock.png",
                    });
                    
                      
                      } else {
                          
                        const now = new Date();
                        const elapsedTimeMS = now.valueOf() - globalState.danielKassulkeTimer.valueOf();
                        const timeElapsed = msToTime(elapsedTimeMS);
                    
                    
                    
                        sf.system.exec({
                          commandLine: `
                              osascript -e 'display alert "" message "${timeElapsed}"'
                          `,
                        });
                    
                        // Set clipboard
                        sf.clipboard.setText({ text: 'You were working for ' + timeElapsed });
                    
                        globalState.danielKassulkeTimer = null;
                      }
                    }
                    
                    main();
                    
                    1. Dustin Harris @Dustin_Harris
                        2023-09-06 18:05:56.484Z

                        On on my mobile at the moment so I can’t reliably write code… but the bigger issue here is that the workday hours logger would take up the execution thread of soundflow and you wouldn’t be able to use Sound Flow for anything else while it is running. Once I’m at a system I can see if there is a way I can do it with applications trigger actions.

                        1. In reply todanielkassulke:
                          AAndrew Sherman @Andrew_Sherman
                            2023-09-11 09:08:06.535Z

                            Could you write the start time to a json file on your hard drive, and then end time as well? That way SF can run for other things. Perhaps you could use an application trigger as the start/stop.

                            const jsonSavePath = '/Users/Path/Filename.json';
                                
                                let settingsToSave = {
                                    setting1,
                                    setting2,
                                    setting3,
                                };
                            
                                let success = sf.file.writeJson({
                                    path: jsonSavePath,
                                    json: settingsToSave
                                }).success;
                            
                                if (!success) throw `Error saving settings`
                            
                            let recallSettings = sf.file.readJson({
                                path: jsonSavePath
                            }).json
                            
                            
                            1. Ddanielkassulke @danielkassulke
                                2023-09-19 20:27:13.988Z

                                Took a while to comprehend the best way to do this, but I'm using Protools launching as an application trigger for part 1:

                                
                                function waitForPtLaunch() {
                                  while (!sf.ui.proTools.invalidate().hasValidUI) {
                                    sf.wait({ intervalMs: 200 });
                                  }
                                  for (var i = 0; i < 10; i++) {
                                    sf.ui.proTools.waitForNoModals();
                                  }
                                }
                                
                                function startTimer() {
                                  if (!sf.ui.proTools.isRunning) {
                                    waitForPtLaunch();
                                  }
                                
                                  const startTime = new Date();
                                  
                                  let timerData = {
                                    startTime: startTime.toISOString(),
                                  };
                                
                                  let success = sf.file.writeJson({
                                    path: jsonSavePath,
                                    json: timerData
                                  }).success;
                                
                                  if (!success) throw `Error saving start time`;
                                
                                  sf.interaction.displayDialog({
                                    title: "Session Time Calculator",
                                    prompt: "Session timer started",
                                    icon: "/Users/XXXXXX/Desktop/Clock.png",
                                  });
                                }
                                
                                sf.system.exec({
                                    commandLine: `say "Your session time calculator has started"`,
                                  });
                                
                                startTimer();
                                

                                and protools quitting as the trigger for part 2:

                                function msToTime(totalMilliseconds) {
                                  let seconds = Math.floor((totalMilliseconds / 1000) % 60);
                                  let minutes = Math.floor(totalMilliseconds / 1000 / 60);
                                
                                  function addLeadingZeros(value, length = 2) {
                                    let valueAsString = value.toString();
                                    while (valueAsString.length < length) {
                                      valueAsString = '0' + valueAsString;
                                    }
                                    return valueAsString;
                                  }
                                
                                  const secondsString = addLeadingZeros(seconds);
                                  const minutesString = addLeadingZeros(minutes);
                                  
                                  return `${minutesString}:${secondsString}`;
                                }
                                
                                const jsonSavePath = '/Users/XXXXXX/Desktop/timerData.json';
                                
                                function stopTimer() {
                                  const endTime = new Date();
                                  const timerData = sf.file.readJson({
                                    path: jsonSavePath
                                  }).json;
                                
                                  const startTime = new Date(timerData.startTime);
                                  const elapsedTimeMS = endTime - startTime;
                                  const timeElapsed = msToTime(elapsedTimeMS);
                                
                                  const minutes = Math.floor(elapsedTimeMS / 1000 / 60);
                                  const seconds = Math.floor((elapsedTimeMS / 1000) % 60);
                                
                                  const minuteText = minutes === 1 ? "minute" : "minutes";
                                  const secondText = seconds === 1 ? "second" : "seconds";
                                
                                  const spokenTime = `${minutes} ${minuteText} and ${seconds} ${secondText}`;
                                
                                  sf.interaction.displayDialog({
                                    title: "Session Time Calculator",
                                    prompt: `Total working time: ${timeElapsed}`,
                                    icon: "/Users/XXXXXX/Desktop/Clock.png",
                                  });
                                
                                  sf.system.exec({
                                    commandLine: `say "You were working for ${spokenTime}"`,
                                  });
                                }
                                
                                stopTimer();
                                

                                For anyone playing along at home, I've used apple's native clock icon for the icon stored within the dialogs.

                                1. JJascha Viehl @Jascha_Viehl
                                    2023-09-19 22:00:16.775Z

                                    @Andrew_Sherman @danielkassulke Impressive!
                                    Is there also a way of additionally logging the active work time in Pro Tools? Meaning the time actually work on the project is done.

                                    When I get a call from another project and do not remember to stop the measurement I‘d rather have is not count towards the time of the current Pro Tools project.

                                    At the moment i’m monitoring active work through the amount of session file backups that increment every 5 minutes where there has been a change that can be undone.

                                    Maybe the script could be monitoring the changes of the menu „Edit > Undo…“
                                    And add 5 mins to the counter but only if a change has happened within that 5 mins.

                                    Super nice would also be a long term history of work/active hours / start end date time m alongside the Pro Tools session name.
                                    So that the entries get just added to the json file.

                                    The json file could be saved in the session folder of the currently opened session.

                                    1. Ddanielkassulke @danielkassulke
                                        2023-09-20 00:08:40.908Z

                                        I think you could tackle this in a few ways:
                                        First is to have the 'say' part issue you a reminder to include inactive time. i.e. when the session timer stopped, you would hear "You were working for HH:MM. Please deduct any necessary time from this total" as a reminder. You could also ask for user input to incorporate inactive periods into the final calculation.

                                        The automated way you described is a little more complex and probably beyond the scope of what I need (and probably beyond my skillset). I think if you based it off significant stretches of inactivity (i.e. 5 minutes or more) based on the Undo History window. You could scrape time data from the undo history window, analyse it for >5m gaps, and then subtract those from the total work hours of the 2nd part of the script.

                                        1. Ddanielkassulke @danielkassulke
                                            2023-09-20 04:57:47.787Z

                                            OK I couldn't resist the temptation - @Chad any idea why I'm getting this within the alert from Undo History: Timestamp: AXTitle : AxStringProperty = '<null>' , Operation: AXTitle : AxStringProperty = '<null>' this script?

                                            const pT = sf.ui.proTools;
                                            pT.appActivateMainWindow();
                                            pT.mainWindow.invalidate();
                                            
                                            if (!sf.ui.proTools.windows.whoseTitle.is("Undo History").first.exists) {
                                                sf.ui.proTools.menuClick({ menuPath: ["Window", "Undo History"] });
                                            }
                                            
                                            let undoHistory = [];
                                            
                                            
                                            const undoHistoryTable = sf.ui.proTools.windows.whoseTitle.is("Undo History").first.tables.whoseTitle.is("Event List").first;
                                            
                                            // Get all the rows in the table
                                            const allRows = undoHistoryTable.children.whoseRole.is("AXRow").allItems;
                                            
                                            // Loop through each row to get the timestamp and operation
                                            for (let i = 0; i < allRows.length; i++) {
                                                const row = allRows[i];
                                                const timeCell = row.children.whoseRole.is("AXCell").allItems[0];
                                                const operationCell = row.children.whoseRole.is("AXCell").allItems[1];
                                            
                                                const timeStaticText = timeCell.children.whoseRole.is("AXStaticText").first;
                                                const operationStaticText = operationCell.children.whoseRole.is("AXStaticText").first;
                                            
                                                // Check if the staticText exists for both time and operation
                                                if (timeStaticText && operationStaticText) {
                                                    const timestamp = timeStaticText.title;
                                                    const operation = operationStaticText.title;
                                            
                                                    if (timestamp && timestamp !== '<null>' && operation && operation !== '<null>') {
                                                        undoHistory.push({ timestamp, operation });
                                                    }
                                                }
                                            }
                                            
                                            // Create an alert with the full list of timestamps and operations
                                            if (undoHistory.length > 0) {
                                                const undoHistoryText = undoHistory.map(item => `Timestamp: ${item.timestamp}, Operation: ${item.operation}`).join("\n");
                                                alert("Undo History:\n" + undoHistoryText);
                                            } else {
                                                alert("No undo history found.");
                                            }
                                            

                                            As a continuation of Jascha's idea, I'm just trying to get a list of the values stored in the undo history window, so that I can eventually find lapses of activity > 5 minutes to ensure the stopwatch is more accurate.

                                            1. Chad Wahlbrink @Chad2023-09-20 13:48:48.953Z

                                              Hey @danielkassulke - I believe the main issue is that you are using

                                              const timestamp = timeStaticText.title;
                                              const operation = operationStaticText.title;
                                              

                                              instead of

                                              const timestamp = timeStaticText.title.value;
                                              const operation = operationStaticText.title.value;
                                              

                                              So you should be able to use something like this:

                                              const pT = sf.ui.proTools;
                                              pT.appActivateMainWindow();
                                              pT.mainWindow.invalidate();
                                              
                                              if (!sf.ui.proTools.windows.whoseTitle.is("Undo History").first.exists) {
                                                  sf.ui.proTools.menuClick({ menuPath: ["Window", "Undo History"] });
                                              }
                                              
                                              let undoHistory = [];
                                              
                                              const undoHistoryTable = sf.ui.proTools.windows.whoseTitle.is("Undo History").first.tables.whoseTitle.is("Event List").first;
                                              
                                              // Get all the rows in the table
                                              const allRows = undoHistoryTable.children.whoseRole.is("AXRow").allItems;
                                              
                                              
                                              
                                              // Loop through each row to get the timestamp and operation
                                              for (let i = 0; i < allRows.length; i++) {
                                                  const row = allRows[i];
                                                  const timeCell = row.children.whoseRole.is("AXCell").allItems[0];
                                                  const operationCell = row.children.whoseRole.is("AXCell").allItems[1];
                                              
                                                  const timeStaticText = timeCell.children.whoseRole.is("AXStaticText").first;
                                                  const operationStaticText = operationCell.children.whoseRole.is("AXStaticText").first;
                                              
                                                  // Check if the staticText exists for both time and operation
                                                  if (timeStaticText && operationStaticText) {
                                                      const timestamp = timeStaticText.title.value;
                                                      const operation = operationStaticText.title.value;
                                                      undoHistory.push({ timestamp, operation });
                                                  }
                                              }
                                              
                                              // Create an alert with the full list of timestamps and operations
                                              if (undoHistory.length > 0) {
                                                  const undoHistoryText = undoHistory.map(item => `Timestamp: ${item.timestamp}, Operation: ${item.operation}`).join("\n");
                                                  alert("Undo History:\n" + undoHistoryText);
                                              } else {
                                                  alert("No undo history found.");
                                              }
                                              ``
                                              1. In reply todanielkassulke:
                                                JJascha Viehl @Jascha_Viehl
                                                  2023-09-21 11:30:13.256Z

                                                  Thank you! @danielkassulke Will try this out!

                                          • In reply toAndrew_Sherman:
                                            Ddanielkassulke @danielkassulke
                                              2023-09-19 21:19:07.848Z

                                              this was a great idea, by the way! thanks. it totally bridged the conceptual gap