No internet connection
  1. Home
  2. Macro and Script Help

RX 11 Progress Bar UI element BUG - Automating "Music Rebalance" Stem Splitting from Finder

By Jake Morton @Jake_Morton
    2025-01-21 03:48:10.526Z

    Hi SoundFlow community,

    Jake here.

    I've been working on a new set of performance repoitoire recently and the list of songs keeps growing.
    I've been using Izotope's RX to spleet/split stems from master recordings in order to help transcribe and rehearse songs.

    I find it frustrating waiting for RX to split stems and then dealing with all the tedious file-naming steps to ensure smooth importing into my DAW templates. It is particularly debillitating in a teaching context as it can often chew up session time.
    To tackle this, I decided to script away the hassle—and for the most part, the design of this script works well.

    However, it’s still very much a work-in-progress and incredibly idiosyncratic. The biggest issue I’m facing is attaching a reliable "Wait for UI element to disappear" step to RX's Music Rebalance progress bar. Without this, the script isn’t robust and doesn’t reliably adapt to RX’s processing times, leading to inconsistencies in operation.

    If anyone has experience solving this or ideas to make the script more comprehensive and functional, I’d love to hear from you!

    Looking forward to refining this further and sharing a tool that makes stem-splitting less of a chore.

    Thanks for your time and any insights you can offer!

    You can see the script in action with a tak through of the steps and issues at youtube here:
    https://www.youtube.com/watch?v=95IycOhstJk

    P.S, Apologies for the GPT-Emoji riddled script and terrible audio quality in the video.

    Cheers,
    Jake

    function main() {
        log("🎬 Script started...");
    
        // Step 1: Prompt the user for required inputs
        log("📥 Prompting the user for required inputs...");
        const songTitle = prompt("Please enter the song title:");
        const tempo = prompt("Please enter the tempo (e.g., 120):");
        const destinationDirectory = sf.interaction.selectFolder({
            prompt: "Please choose the destination folder for exporting stems",
        }).path;
    
        if (!songTitle || !tempo || !destinationDirectory) {
            throw new Error("❌ Missing required input. Ensure song title, tempo, and destination directory are provided.");
        }
    
        log(`🎵 Song Title: ${songTitle}`);
        log(`🎚️ Tempo: ${tempo}`);
        log(`📂 Destination Directory: ${destinationDirectory}`);
    
        // Step 2: Create a uniquely timestamped folder for exporting all stems
        const timestamp = getCurrentTimestamp();
        const sanitizedSongTitle = songTitle.replace(/[\/:*?"<>|]/g, "_"); // Remove invalid characters
        const exportFolder = `${destinationDirectory}/STEMS_${sanitizedSongTitle}_${tempo}bpm_${timestamp}`;
        log("📂 Creating a single uniquely timestamped folder for all stems...");
        sf.file.directoryCreate({ path: exportFolder });
        log(`📂 Export folder created: ${exportFolder}`);
    
        // Step 3: Copy the file path from Finder
        log("📋 Copying the file path from Finder...");
        sf.keyboard.press({ keys: "cmd+alt+c" });
    
        sf.wait({ intervalMs: 500 }); // Wait to ensure clipboard is updated
    
        let clipboardContent = sf.clipboard.getText().value;
        log(`📋 Clipboard content: ${clipboardContent}`);
    
        // Validate the clipboard content
        if (!clipboardContent || !clipboardContent.includes("/")) {
            log("❗ Clipboard does not contain a valid file path. Prompting the user to manually input the file path...");
            clipboardContent = prompt("Please manually enter the full file path of the audio file:");
            if (!clipboardContent || !clipboardContent.includes("/")) {
                throw new Error("❌ Invalid file path provided. Aborting script.");
            }
        }
    
        const fullFilePath = clipboardContent.trim();
        log(`🎶 Full Filepath: ${fullFilePath}`);
    
        // Step 4: Launch RX 11 and open the file directly
        log("🚀 Launching RX 11 and opening the file...");
        sf.file.open({
            path: fullFilePath,
            applicationPath: "/Applications/iZotope RX 11 Audio Editor.app",
        });
    
        // Wait briefly to ensure the file is loaded
        log("⏳ Waiting for RX 11 to load the file...");
        sf.ui.izotope.appWaitForActive();
        sf.ui.izotope.mainWindow.elementWaitFor({ timeout: 5000 });
        log("✅ File loaded successfully.");
    
        // Step 5: Process Music Rebalance
        log("🎛️ Processing Music Rebalance...");
        processMusicRebalance();
    
          // Brute force a wait so that the short recording can be processed. 
        // This solution is not robust and needs a new design. 
        // However, no "wait for UI element to dissapear" options were functioning.
        log("⏳ Waiting for RX 11 to process the file... (arbitrary 10 seconds)");
        sf.wait({
        intervalMs: 10000,
        });
    
        // Step 6: Export Files
        log("💾 Exporting files...");
        exportStems(songTitle, tempo, exportFolder);
    
        log("🎉 Script completed successfully!");
    }
    
    // Music Rebalance Function
    function processMusicRebalance() {
        log("🎛️ Opening the Music Rebalance module...");
        sf.ui.izotope.menuClick({ menuPath: ["Modules", "Music Rebalance..."] });
    
        log("⏳ Waiting for the Music Rebalance window to appear...");
        sf.ui.izotope.windows.whoseTitle.is("Music Rebalance").first.elementWaitFor({
            waitType: "Appear",
            timeoutMs: 10000,
            failIfNotFound: true,
        });
        log("✅ Music Rebalance window is active.");
    
        // Set the quality for Music Rebalance
        log("🎚️ Clicking the 'Quality' dropdown menu...");
        sf.ui.izotope.windows.whoseTitle.is("Music Rebalance").first.groups
            .whoseDescription.is("EffectPanel Music Rebalance").first.popupButtons
            .whoseTitle.is("Quality").first.elementClick();
    
        log("🎚️ Selecting the quality option...");
        sf.ui.izotope.windows.whoseTitle.is("Music Rebalance").first.groups
            .whoseDescription.is("EffectPanel Music Rebalance").first.popupButtons
            .whoseTitle.is("Quality").first.mouseClickElement({
                relativePosition: { x: 25, y: 79 },
            });
    
        // Click "Split Stems" button with retry mechanism
        log("🔀 Attempting to click 'Split Stems' button...");
        clickSplitStemsButton();
    }
    
    // Retry logic for clicking "Split Stems" button
    function clickSplitStemsButton() {
        const splitStemsButton = sf.ui.izotope.windows
            .whoseTitle.is("Music Rebalance").first.groups
            .whoseDescription.is("EffectPanel Music Rebalance Footer Background").first.groups
            .whoseDescription.is("Apply Group Container").first.buttons
            .whoseDescription.is("Split Stems").first;
    
        const maxRetries = 3; // Number of retry attempts
        const retryIntervalMs = 2000; // Wait time between retries in milliseconds
        let attempt = 0;
    
        while (attempt < maxRetries) {
            try {
                log(`🔄 Attempt ${attempt + 1}/${maxRetries}: Clicking 'Split Stems' button...`);
                splitStemsButton.elementClick(); // Try to click the button
                log("✅ 'Split Stems' button clicked successfully.");
                return; // Exit the loop on success
            } catch (err) {
                log(`❗ Attempt ${attempt + 1} failed: ${err.message}`);
                attempt++;
                sf.wait({ intervalMs: retryIntervalMs }); // Wait before retrying
            }
        }
    
        // If all retries fail, fallback to brute force relative click
        log("❌ All retries failed. Attempting brute force relative click...");
        bruteForceClickSplitStems();
    }
    
    // Brute force click using relative position
    function bruteForceClickSplitStems() {
        try {
            sf.ui.izotope.windows.whoseTitle.is("Music Rebalance").first.groups.whoseDescription.is("EffectPanel Music Rebalance").first.groups.whoseDescription.is("EffectPanel Music Rebalance").first.groups.whoseDescription.is("EffectPanel Music Rebalance Footer Background").first.mouseClickElement({
        relativePosition: {"x":340,"y":20}, // Updated coordinates
                });
            log("✅ 'Split Stems' button clicked using brute force relative click.");
        } catch (err) {
            throw new Error(`❌ Failed to brute-force click 'Split Stems' button: ${err.message}`);
        }
    }
    
    // Export Stems Function
    function exportStems(songTitle, tempo, exportFolder) {
        log("💾 Exporting stems...");
    
        const fileNames = [
            { prefix: "00", name: "Original Recording" },
            { prefix: "01", name: "Vocals" },
            { prefix: "02", name: "Bass" },
            { prefix: "03", name: "Percussion" },
            { prefix: "04", name: "Other Instruments" },
        ];
    
        const currentDate = getCurrentTimestamp();
        log(`🗓️ Current date: ${currentDate}`);
    
        // Track whether it's the first export
        let isFirstExport = true;
    
        fileNames.forEach((file, index) => {
            const exportFileName = `${file.prefix}_${file.name}_${songTitle}_${tempo}_${currentDate}.wav`;
            const exportPath = `${exportFolder}/${exportFileName}`;
            log(`🎙️ Processing file: ${file.name} (${index + 1}/${fileNames.length})`);
            log(`📂 Export path: ${exportPath}`);
    
            if (isFirstExport) {
                log("🗂️ Processing the first file (no Next File navigation needed)...");
            } else {
                log(`🗂️ Selecting File Tab ${index}...`);
                sf.ui.izotope.menuClick({
                    menuPath: ["Window", "Next File"], // Move to the next file tab
                });
                sf.wait({ intervalMs: 1000 }); // Give the UI time to switch tabs
            }
    
            log("💾 Opening Export dialog...");
            sf.ui.izotope.menuClick({ menuPath: ["File", "Export..."] });
            sf.wait({ intervalMs: 1000 });
    
            if (!isFirstExport) {
                log("🔧 Adjusting export bit depth (not required for the first file)...");
                sf.keyboard.press({
                    keys: "tab, tab, tab, tab, tab, tab, tab, tab, tab",
                });
    
                sf.wait({ intervalMs: 500 });
                sf.keyboard.press({ keys: "up" }); // Adjust bit depth
                sf.wait({ intervalMs: 500 });
            }
            
            sf.keyboard.press({ keys: "return" });
            
            log("⏳ Waiting for RX 11 export UI to load...");
            sf.wait({ intervalMs: 1000 });
    
            log("📁 Navigating to folder and typing export path...");
            sf.keyboard.press({ keys: "cmd+shift+g" });
            sf.wait({ intervalMs: 500 });
    
            exportPath.split("").forEach((char) => {
                sf.keyboard.type({ text: char });
            });
    
            sf.keyboard.press({ keys: "return" });
            sf.wait({ intervalMs: 1000 });
    
            sf.keyboard.press({ keys: "return" });
            sf.wait({ intervalMs: 1000 });
    
            log(`✅ Export completed for file: ${file.name}`);
    
            // Mark the first export as complete after the first iteration
            isFirstExport = false;
        });
    
        log("🎉 All stems exported successfully.");
    }
    
    // Utility: Get Current Timestamp
    function getCurrentTimestamp() {
        const now = new Date();
        const year = now.getFullYear();
        const month = String(now.getMonth() + 1).padStart(2, "0");
        const day = String(now.getDate()).padStart(2, "0");
        const hours = String(now.getHours()).padStart(2, "0");
        const minutes = String(now.getMinutes()).padStart(2, "0");
        const seconds = String(now.getSeconds()).padStart(2, "0");
        return `${year}${month}${day}_${hours}${minutes}${seconds}`;
    }
    
    // Run the main script
    main();
    Solved in post #8, click to view
    • 11 replies

    There are 11 replies. Estimated reading time: 112 minutes

    1. Chad Wahlbrink @Chad2025-01-21 19:48:03.712Z2025-01-21 19:55:52.084Z

      Hi @Jake_Morton!

      Here's a version of the script I worked up. This requires that you start the script with the audio file selected in Finder.

      
      // Defining iZotope RX is required if you have multiple versions of RX installed
      let izotopeRX = sf.ui.app("com.izotope.RX11")
      
      /** 
       * WAIT FOR MODALS - Wait for RX Modal
       */
      function waitForModals() {
          try {
              // Get the title of the current Audio File in iZotope RX
              const audioFileName = izotopeRX.mainWindow.getElement("AXTitleUIElement").value.invalidate().value;
              
              // Possible modal window titles
              const modalWindowNames = [audioFileName, "Composite"];
      
              // If any modal with this title exists, then wait
              while (modalWindowNames.some(modalWindowName => izotopeRX.floatingWindows.getByTitle(modalWindowName).exists)) {
                  sf.wait({ intervalMs: 100 });
              }
          } catch (err) {
      
              throw `Wait for Modals Failed`;
          }
      }
      
      /** 
       * Export Navigate to Path 
       * @param {string} path
      */
      function navigateTo(path) {
          //Get a reference to the focused window in the frontmost app:
          var win = sf.ui.frontmostApp.focusedWindow;
      
          //Sanity check - make sure window is either Save or Open dialog
          if (!(win.title.value.indexOf('Export File') >= 0 || win.title.value.indexOf('Open') >= 0 || win.title.value.indexOf('Save As') >= 0))
              throw 'Focused window is not an Open or Save dialog. Title: ' + win.title.value;
      
          // Open the Go to... sheet
          sf.keyboard.type({ text: '/' });
      
          // Wait for the sheet to appear
          win.sheets.first.elementWaitFor({ timeout: 500 }, 'Could not find "Go to" sheet in the Save/Open dialog').element;
      
          // Set the value of the combo box
          sf.keyboard.type({
              text: path,
          }, 'Could not paste the path');
      
          // Press return
          sf.keyboard.press({
              keys: "return",
          }, "Could not hit return");
      
          //Wait for sheet to close
          win.sheets.first.elementWaitFor({ waitForNoElement: true, timeout: 500 }, '"Go to" sheet didn\'t close in time');
      }
      
      // Utility: Get Current Timestamp
      function getCurrentTimestamp() {
          const now = new Date();
          const year = now.getFullYear();
          const month = String(now.getMonth() + 1).padStart(2, "0");
          const day = String(now.getDate()).padStart(2, "0");
          const hours = String(now.getHours()).padStart(2, "0");
          const minutes = String(now.getMinutes()).padStart(2, "0");
          const seconds = String(now.getSeconds()).padStart(2, "0");
          return `${year}${month}${day}_${hours}${minutes}${seconds}`;
      }
      
      function main() {
          log("🎬 Script started...");
      
          // Step 1: Prompt the user for required inputs
          log("📥 Prompting the user for required inputs...");
      
          // Declare Variables 
          let tempo, songTitle, destinationDirectory;
          
          songTitle = prompt("Please enter the song title:");
          // If I hit Cancel on the songTitle prompt, and songTitle is undefined, then bail. 
          if (songTitle !== undefined) {
              tempo = prompt("Please enter the tempo (e.g., 120):");
          } else {
              throw 0
          }
          // If I hit Cancel on the tempo prompt, and tempo is undefined, then bail. 
          if (tempo !== undefined) {
              destinationDirectory = sf.interaction.selectFolder({
                  prompt: "Please choose the destination folder for exporting stems",
              }).path;
          } else {
              throw 0
          }
      
          log(`🎵 Song Title: ${songTitle}`);
          log(`🎚️ Tempo: ${tempo}`);
          log(`📂 Destination Directory: ${destinationDirectory}`);
      
          // Step 2: Create a uniquely timestamped folder for exporting all stems
          const timestamp = getCurrentTimestamp();
      
          // remove invalid characters from the songName
          const sanitizedSongTitle = songTitle.replace(/[\/:*?"<>|]/g, "_"); // Remove invalid characters
      
          // Define the Export Folder with songTitle, tempo and timestamp
          const exportFolder = `${destinationDirectory}/STEMS_${sanitizedSongTitle}_${tempo}bpm_${timestamp}`;
          log("📂 Creating a single uniquely timestamped folder for all stems...");
          sf.file.directoryCreate({ path: exportFolder });
          log(`📂 Export folder created: ${exportFolder}`);
      
          // Step 3: Fetch the file path from Finder
          let filePath = sf.ui.finder.selectedPaths[0]
          let fileName = filePath.split('/').slice(-1)[0]
      
          log(`🎶 Full Filepath: ${filePath}`);
      
          // Step 4: Launch RX 11 and open the file directly
          log("🚀 Launching RX 11 and opening the file...");
          sf.file.open({
              path: filePath,
              applicationPath: "/Applications/iZotope RX 11 Audio Editor.app",
          });
      
          // Wait briefly to ensure the file is loaded
          log("⏳ Waiting for RX 11 to load the file...");
      
          izotopeRX.appWaitForActive();
      
          // Wait for Currently Open File to Match the Selected File
          sf.waitFor({
              callback: () => {
                  izotopeRX.invalidate();
                  return izotopeRX.mainWindow.getElement("AXTitleUIElement").value.value == fileName;
              },
              timeout: 10000
          }, `Failed waiting for fileName to Match`);
      
          // Step 5: Process Music Rebalance
          log("🎛️ Processing Music Rebalance...");
          processMusicRebalance();
      
          log("⏳ Waiting for RX 11 to process the file...");
          // Use "Wait for Modals" function to wait
          waitForModals();
      
          // Step 6: Export Files
          log("💾 Exporting files...");
          exportStems(songTitle, tempo, exportFolder);
      
          log("🎉 Script completed successfully!");
      }
      
      // Music Rebalance Function
      function processMusicRebalance() {
          log("🎛️ Opening the Music Rebalance module...");
      
          // If the Music Balance Module is already active, then don't click the menu again - this would hide it. 
          if (!izotopeRX.getMenuItem("Modules", "Music Rebalance...").isMenuChecked) {
              izotopeRX.menuClick({ menuPath: ["Modules", "Music Rebalance..."] });
              log("⏳ Waiting for the Music Rebalance window to appear...");
              izotopeRX.windows.whoseTitle.is("Music Rebalance").first.elementWaitFor({
                  waitType: "Appear",
                  failIfNotFound: true,
              });
          }
      
          log("✅ Music Rebalance window is active.");
      
          log("🎚️ Selecting the quality option...");
      
          // Set the Quality of Music Rebalance to "Best"
          izotopeRX.windows.whoseTitle.is("Music Rebalance").first.groups.whoseDescription.is("EffectPanel Music Rebalance")
              .first.popupButtons.whoseTitle.is("Quality").first.value.value = 'Best';
      
          // Click "Split Stems" button with retry mechanism
          log("🔀 Attempting to click 'Split Stems' button...");
      
          // Click "Split Stems" Button on "Music Balance" module
          sf.ui.izotope.windows.whoseTitle.is("Music Rebalance").first.groups.whoseDescription.is("EffectPanel Music Rebalance")
              .first.groups.whoseDescription.is("EffectPanel Music Rebalance").first.groups.whoseDescription.is("EffectPanel Music Rebalance Footer Background")
              .first.groups.whoseDescription.is("Apply Group Container").first.buttons.whoseDescription.is("Split Stems").first.elementClick();
      }
      
      // Export Stems Function
      function exportStems(songTitle, tempo, exportFolder) {
          log("💾 Exporting stems...");
      
          const fileNames = [
              { prefix: "00", name: "Original Recording" },
              { prefix: "01", name: "Vocals" },
              { prefix: "02", name: "Bass" },
              { prefix: "03", name: "Percussion" },
              { prefix: "04", name: "Other Instruments" },
          ];
      
          const currentDate = getCurrentTimestamp();
          log(`🗓️ Current date: ${currentDate}`);
      
          // Track whether it's the first export
          let isFirstExport = true;
      
          fileNames.forEach((file, index) => {
              const exportFileName = `${file.prefix}_${file.name}_${songTitle}_${tempo}_${currentDate}.wav`;
              const exportPath = `${exportFolder}/${exportFileName}`;
              log(`🎙️ Processing file: ${file.name} (${index + 1}/${fileNames.length})`);
              log(`📂 Export path: ${exportPath}`);
              log("💾 Opening Export dialog...");
      
              izotopeRX.menuClick({ menuPath: ["File", "Export..."] });
      
              // Wait for the Export Module to Appear
              izotopeRX.mainWindow.groups.whoseDescription.is("ExportPanel Module Wrapper").first.elementWaitFor();
      
              // We want the original file to be at the original bit-depth
              if (!isFirstExport) {
                  log("🔧 Adjusting export bit depth (not required for the first file)...");
      
                  // Select WAV File
                  izotopeRX.mainWindow.buttons.whoseDescription.is("WAVButton").first.elementClick();
      
                  // Set Bit Depth to '24-bit'
                  izotopeRX.mainWindow.popupButtons.whoseTitle.is("Bit depth").first.value.value = '24-bit';
              }
      
              // Click "OK" in the Export Panel Window
              izotopeRX.mainWindow.groups.whoseDescription.is("ExportPanel Module Wrapper").first.groups.whoseDescription.is("ExportPanel Module Wrapper Footer Background")
                  .first.buttons.whoseDescription.is("OK").first.elementClick({ asyncSwallow: true });
      
              log("⏳ Waiting for RX 11 export UI to load...");
              // wait for Export File Dialog with "Save" Button to Appear
              izotopeRX.windows.whoseTitle.is('Export File').first.buttons.whoseTitle.is("Save").first.elementWaitFor();
      
              log("📁 Navigating to folder and typing export path...");
      
              // Using "Navigate To" utility function
              navigateTo(exportPath);
      
              // Wait for "Save" button to be clickable, then Click "Save" Button, then wait for "Save" button to disappear
              sf.waitFor({
                  callback: () => {
                      return izotopeRX.windows.whoseTitle.is('Export File').first.buttons.whoseTitle.is("Save").first.isEnabled;
                  },
                  timeout: 1000
              }, `Failed waiting`);
      
              izotopeRX.windows.whoseTitle.is('Export File').first.buttons.whoseTitle.is("Save").first.elementClick();
      
              izotopeRX.windows.whoseTitle.is('Export File').first.buttons.whoseTitle.is("Save").first.elementWaitFor({ waitType: "Disappear" });
      
              // Wait for Export Panel to Disappear
              izotopeRX.mainWindow.groups.whoseDescription.is("ExportPanel Module Wrapper").first.elementWaitFor({ waitType: "Disappear" });
      
              // Check the undo queue length
              let undoQueueLength = izotopeRX.mainWindow.groups.whoseDescription.is("Undo Panel").first.radioButtons.length > 1
      
              // Close the File 
              izotopeRX.menuClick({ menuPath: ['File', 'Close File'] });
      
              // If Undo Queue is longer than 1, then wait for alert prompting to save changes.  
              if (undoQueueLength) {
                  izotopeRX.windows.whoseDescription.is("alert").first.buttons.whoseTitle.is("No").first.elementWaitFor();
                  izotopeRX.windows.whoseDescription.is("alert").first.buttons.whoseTitle.is("No").first.elementClick();
                  izotopeRX.windows.whoseDescription.is("alert").first.buttons.whoseTitle.is("No").first.elementWaitFor({ waitForNoElement: true });
              }
      
              log(`✅ Export completed for file: ${file.name}`);
      
              // Mark the first export as complete after the first iteration
              isFirstExport = false;
          });
      
          log("🎉 All stems exported successfully.");
      }
      
      // Run the main script
      main();
      
      1. Chad Wahlbrink @Chad2025-01-21 20:03:52.953Z

        Awesome idea as usual!

        A few notes for you, @Jake_Morton.

        Instead of using actions like

        sf.keyboard.press({
            keys: "tab, tab, tab, tab, tab, tab, tab, tab, tab",
        });
        
        sf.wait({ intervalMs: 500 });
        sf.keyboard.press({ keys: "up" }); // Adjust bit depth
        sf.wait({ intervalMs: 500 });
        

        to navigate the Export Dialog, you can use the pick button to select the popup menu, and, in iZotope RX at least, you can use something like this to directly assign a value:

        izotopeRX.mainWindow.popupButtons.whoseTitle.is("Bit depth").first.value.value = '24-bit';
        

        Similarly, with the "Music Rebalance" module, you can set the quality like this:

         izotopeRX.windows.whoseTitle.is("Music Rebalance").first.groups.whoseDescription.is("EffectPanel Music Rebalance")
                .first.popupButtons.whoseTitle.is("Quality").first.value.value = 'Best';
        

        instead of using mouse clicks and relative position! This also ensures that the popup window doesn't get in the way of clicking the "Stem split" button - which removes the need to force click or try multiple times.

        And then, finally, instead of relying on the clipboard to copy the file path, I'm just pulling the file path from the selected file in Finder directly using:

        let filePath = sf.ui.finder.selectedPaths[0]
        let fileName = filePath.split('/').slice(-1)[0]
        
        1. JJake Morton @Jake_Morton
            2025-01-23 13:42:53.403Z

            Thank so much for these notes!
            I'll do some more testing over the coming days to refine this further!

            I appreciate you spending some time on it.

            Hope you're well.

            1. In reply toChad:
              JJake Morton @Jake_Morton
                2025-04-22 13:35:44.803Z

                Hi Chad and the wider SoundFlow community,

                I’ve recently updated my script to make it a bit more robust after encountering a few recurring issues. Specifically, I addressed:

                The various alert windows prompting to save files.

                Timeout issues during the Music Rebalance process, which can take several minutes to complete.

                I’ve managed to work around these by implementing targeted waits and UI-relative mouse clicks. It’s a bit of a brute-force approach in places, but the script is now fully functional and reliable in my workflow.

                As a bonus, I’ve also added a randomized chime player that triggers custom samples when the process completes — a small touch to keep me from getting too distracted while waiting for longer tasks to finish!

                At this point, I’m wondering if it’s possible to take things a step further — specifically, to batch process or queue multiple files. For example, if I need to stem out an entire setlist of master recordings for my bandmates to rehearse with, it would be a huge time-saver to automate that and step away from the computer entirely.

                I’d love to hear your thoughts on whether this is achievable within SoundFlow, and if so, any pointers on how to approach it.
                @Chad Wahlbrink — I’d especially appreciate your insight here!

                Thanks in advance for your time and consideration.

                Here’s the current version of my script for reference:

                // Defining iZotope RX is required if you have multiple versions of RX installed
                let izotopeRX = sf.ui.app("com.izotope.RX11")
                
                /** 
                 * WAIT FOR MODALS - Wait for RX Modal
                 */
                function waitForModals() {
                    try {
                        // Get the title of the current Audio File in iZotope RX
                        const audioFileName = izotopeRX.mainWindow.getElement("AXTitleUIElement").value.invalidate().value;
                        
                        // Possible modal window titles
                        const modalWindowNames = [audioFileName, "Composite"];
                
                        // If any modal with this title exists, then wait
                        while (modalWindowNames.some(modalWindowName => izotopeRX.floatingWindows.getByTitle(modalWindowName).exists)) {
                            sf.wait({ intervalMs: 100 });
                        }
                    } catch (err) {
                
                        throw `Wait for Modals Failed`;
                    }
                }
                
                /** 
                 * Export Navigate to Path 
                 * @param {string} path
                */
                function navigateTo(path) {
                    //Get a reference to the focused window in the frontmost app:
                    var win = sf.ui.frontmostApp.focusedWindow;
                
                    //Sanity check - make sure window is either Save or Open dialog
                    if (!(win.title.value.indexOf('Export File') >= 0 || win.title.value.indexOf('Open') >= 0 || win.title.value.indexOf('Save As') >= 0))
                        throw 'Focused window is not an Open or Save dialog. Title: ' + win.title.value;
                
                    // Open the Go to... sheet
                    sf.keyboard.type({ text: '/' });
                
                    // Wait for the sheet to appear
                    win.sheets.first.elementWaitFor({ timeout: 500 }, 'Could not find "Go to" sheet in the Save/Open dialog').element;
                
                    // Set the value of the combo box
                    sf.keyboard.type({
                        text: path,
                    }, 'Could not paste the path');
                
                    // Press return
                    sf.keyboard.press({
                        keys: "return",
                    }, "Could not hit return");
                
                    //Wait for sheet to close
                    win.sheets.first.elementWaitFor({ waitForNoElement: true, timeout: 500 }, '"Go to" sheet didn\'t close in time');
                }
                
                // Utility: Get Current Timestamp
                function getCurrentTimestamp() {
                    const now = new Date();
                    const year = now.getFullYear();
                    const month = String(now.getMonth() + 1).padStart(2, "0");
                    const day = String(now.getDate()).padStart(2, "0");
                    const hours = String(now.getHours()).padStart(2, "0");
                    const minutes = String(now.getMinutes()).padStart(2, "0");
                    const seconds = String(now.getSeconds()).padStart(2, "0");
                    return `${year}${month}${day}_${hours}${minutes}${seconds}`;
                }
                
                function main() {
                    log("🎬 Script started...");
                
                    // Step 1: Prompt the user for required inputs
                    log("📥 Prompting the user for required inputs...");
                
                    // Declare Variables 
                    let tempo, songTitle, destinationDirectory;
                    
                    songTitle = prompt("Please enter the song title:");
                    // If I hit Cancel on the songTitle prompt, and songTitle is undefined, then bail. 
                    if (songTitle !== undefined) {
                        tempo = prompt("Please enter the tempo (e.g., 120):");
                    } else {
                        throw 0
                    }
                    // If I hit Cancel on the tempo prompt, and tempo is undefined, then bail. 
                    if (tempo !== undefined) {
                        destinationDirectory = sf.interaction.selectFolder({
                            prompt: "Please choose the destination folder for exporting stems",
                        }).path;
                    } else {
                        throw 0
                    }
                
                    log(`🎵 Song Title: ${songTitle}`);
                    log(`🎚️ Tempo: ${tempo}`);
                    log(`📂 Destination Directory: ${destinationDirectory}`);
                
                    // Step 2: Create a uniquely timestamped folder for exporting all stems
                    const timestamp = getCurrentTimestamp();
                
                    // remove invalid characters from the songName
                    const sanitizedSongTitle = songTitle.replace(/[\/:*?"<>|]/g, "_"); // Remove invalid characters
                
                    // Define the Export Folder with songTitle, tempo and timestamp
                    const exportFolder = `${destinationDirectory}/STEMS_${sanitizedSongTitle}_${tempo}bpm_${timestamp}`;
                    log("📂 Creating a single uniquely timestamped folder for all stems...");
                    sf.file.directoryCreate({ path: exportFolder });
                    log(`📂 Export folder created: ${exportFolder}`);
                
                    // Step 3: Fetch the file path from Finder
                    let filePath = sf.ui.finder.selectedPaths[0]
                    let fileName = filePath.split('/').slice(-1)[0]
                
                    log(`🎶 Full Filepath: ${filePath}`);
                
                    // Step 4: Launch RX 11 and open the file directly
                    log("🚀 Launching RX 11 and opening the file...");
                    sf.file.open({
                        path: filePath,
                        applicationPath: "/Applications/iZotope RX 11 Audio Editor.app",
                    });
                
                    // Wait briefly to ensure the file is loaded
                    log("⏳ Waiting for RX 11 to load the file...");
                
                    izotopeRX.appWaitForActive();
                
                    // Wait for Currently Open File to Match the Selected File
                    sf.waitFor({
                        callback: () => {
                            izotopeRX.invalidate();
                            return izotopeRX.mainWindow.getElement("AXTitleUIElement").value.value == fileName;
                        },
                        timeout: 10000
                    }, `Failed waiting for fileName to Match`);
                
                    // Step 5: Process Music Rebalance
                    log("🎛️ Processing Music Rebalance...");
                    processMusicRebalance();
                
                    log("⏳ Waiting for RX 11 to process the file...");
                    // Use "Wait for Modals" function to wait
                    waitForModals();
                
                    // Step 6: Export Files
                    log("💾 Exporting files...");
                    exportStems(songTitle, tempo, exportFolder);
                
                    log("🎉 Script completed successfully!");
                    playRandomChime();
                }
                
                // Play Random Chime Function
                function playRandomChime() {
                    const sounds = [
                        "Metroid_Item_Find_1",
                        "Metroid_Item_Find_2",
                        "Metroid_Item_Find_3",
                        "Metroid_Item_Find_4",
                        "Metroid_Item_Find_5",
                        "Metroid_Item_Find_6"
                    ];
                
                    const randomIndex = Math.floor(Math.random() * sounds.length);
                    const selectedSound = sounds[randomIndex];
                    const filePath = `/Users/JakeMorton/Dropbox/Soundflow/Audio_Assets/Metroid Item Find Sounds/Wavs/${selectedSound}.wav`;
                
                    sf.system.exec({ commandLine: `afplay "${filePath}"` });
                }
                
                // Music Rebalance Function
                function processMusicRebalance() {
                    log("🎛️ Opening the Music Rebalance module...");
                
                    // If the Music Balance Module is already active, then don't click the menu again - this would hide it. 
                    if (!izotopeRX.getMenuItem("Modules", "Music Rebalance...").isMenuChecked) {
                        izotopeRX.menuClick({ menuPath: ["Modules", "Music Rebalance..."] });
                        log("⏳ Waiting for the Music Rebalance window to appear...");
                        izotopeRX.windows.whoseTitle.is("Music Rebalance").first.elementWaitFor({
                            waitType: "Appear",
                            failIfNotFound: true,
                        });
                    }
                
                    log("✅ Music Rebalance window is active.");
                
                    log("🎚️ Selecting the quality option...");
                
                    // Set the Quality of Music Rebalance to "Best"
                    izotopeRX.windows.whoseTitle.is("Music Rebalance").first.groups.whoseDescription.is("EffectPanel Music Rebalance")
                        .first.popupButtons.whoseTitle.is("Quality").first.value.value = 'Best';
                
                    // Click "Split Stems" button with retry mechanism
                    log("🔀 Attempting to click 'Split Stems' button...");
                
                    // Click "Split Stems" Button on "Music Balance" module
                    sf.ui.izotope.windows.whoseTitle.is("Music Rebalance").first.groups.whoseDescription.is("EffectPanel Music Rebalance")
                        .first.groups.whoseDescription.is("EffectPanel Music Rebalance").first.groups.whoseDescription.is("EffectPanel Music Rebalance Footer Background")
                        .first.groups.whoseDescription.is("Apply Group Container").first.buttons.whoseDescription.is("Split Stems").first.elementClick();
                
                     // Wait for "Music Re-balance" to finish processing
                    sf.ui.izotope.windows.first.getElement("AXTitleUIElement").elementWaitFor({
                        waitType: "Disappear",
                        });
                
                        //Brute forced a wait to ensure no problems re: "additional processing"  occur when exporting files
                        sf.wait({
                        intervalMs: 800,
                        });
                }
                // Handle Save Alert Function
                function handleSaveAlert() {
                    try {
                        sf.ui.izotope.windows.whoseDescription.is("alert").first.elementWaitFor({
                            waitType: "Appear",
                            timeout: 1000
                        });
                        log("⚠️ Save alert appeared.");
                
                        sf.ui.izotope.windows.whoseDescription.is("alert").first.buttons.whoseTitle.is("No").first.elementClick();
                        log("🛑 Clicked 'No' on save alert.");
                    } catch (err) {
                        log("✅ No save alert appeared, continuing...");
                    }
                }
                
                // Export Stems Function
                function exportStems(songTitle, tempo, exportFolder) {
                    log("💾 Exporting stems...");
                
                    const fileNames = [
                        { prefix: "00", name: "Original Recording" },
                        { prefix: "01", name: "Vocals" },
                        { prefix: "02", name: "Bass" },
                        { prefix: "03", name: "Percussion" },
                        { prefix: "04", name: "Other Instruments" },
                    ];
                
                    const currentDate = getCurrentTimestamp();
                    log(`🗓️ Current date: ${currentDate}`);
                
                    // Track whether it's the first export
                    let isFirstExport = true;
                
                    fileNames.forEach((file, index) => {
                        const exportFileName = `${file.prefix}_${file.name}_${songTitle}_${tempo}_${currentDate}.wav`;
                        const exportPath = `${exportFolder}/${exportFileName}`;
                        log(`🎙️ Processing file: ${file.name} (${index + 1}/${fileNames.length})`);
                        log(`📂 Export path: ${exportPath}`);
                        log("💾 Opening Export dialog...");
                
                        izotopeRX.menuClick({ menuPath: ["File", "Export..."] });
                
                        // Wait for the Export Module to Appear
                        waitForExportPanelWithRetry();
                
                function waitForExportPanelWithRetry(attempts = 3) {
                    for (let i = 0; i < attempts; i++) {
                        try {
                            izotopeRX.mainWindow.groups.whoseDescription.is("ExportPanel Module Wrapper").first.elementWaitFor({ timeout: 3000 });
                            log("✅ Export Panel detected.");
                            return true;
                        } catch (err) {
                            log(`⚠️ Attempt ${i + 1}: Export Panel not found.`);
                        }
                    }
                    log("⚠️ Export Panel did not appear after retries, continuing...");
                    return false;
                }        
                
                        // We want the original file to be at the original bit-depth
                        if (!isFirstExport) {
                            log("🔧 Adjusting export bit depth (not required for the first file)...");
                
                            // Select WAV File
                            izotopeRX.mainWindow.buttons.whoseDescription.is("WAVButton").first.elementClick();
                
                            // Set Bit Depth to '24-bit'
                            izotopeRX.mainWindow.popupButtons.whoseTitle.is("Bit depth").first.value.value = '24-bit';
                        }
                
                        // Click "OK" in the Export Panel Window
                        izotopeRX.mainWindow.groups.whoseDescription.is("ExportPanel Module Wrapper").first.groups.whoseDescription.is("ExportPanel Module Wrapper Footer Background")
                            .first.buttons.whoseDescription.is("OK").first.elementClick({ asyncSwallow: true });
                
                        log("⏳ Waiting for RX 11 export UI to load...");
                        // wait for Export File Dialog with "Save" Button to Appear
                        izotopeRX.windows.whoseTitle.is('Export File').first.buttons.whoseTitle.is("Save").first.elementWaitFor();
                
                        log("📁 Navigating to folder and typing export path...");
                
                        // Using "Navigate To" utility function
                        navigateTo(exportPath);
                
                        // Wait for "Save" button to be clickable, then Click "Save" Button, then wait for "Save" button to disappear
                        sf.waitFor({
                            callback: () => {
                                return izotopeRX.windows.whoseTitle.is('Export File').first.buttons.whoseTitle.is("Save").first.isEnabled;
                            },
                            timeout: 1000
                        }, `Failed waiting`);
                
                        izotopeRX.windows.whoseTitle.is('Export File').first.buttons.whoseTitle.is("Save").first.elementClick();
                
                        izotopeRX.windows.whoseTitle.is('Export File').first.buttons.whoseTitle.is("Save").first.elementWaitFor({ waitType: "Disappear" });
                
                        // Wait for Export Panel to Disappear
                        izotopeRX.mainWindow.groups.whoseDescription.is("ExportPanel Module Wrapper").first.elementWaitFor({ waitType: "Disappear" });
                
                        // Try and brute force a wait to see if it allows time for the export to occur and the close function to work.
                        sf.wait({
                        intervalMs: 800,
                        });
                
                
                        // Check the undo queue length
                        let undoQueueLength = izotopeRX.mainWindow.groups.whoseDescription.is("Undo Panel").first.radioButtons.length > 1
                
                        // Close the File 
                        izotopeRX.menuClick({ menuPath: ['File', 'Close File'] });
                
                        handleSaveAlert();
                
                
                         // Handle Alert "additional processing unfinished" nuisance
                        function handleAlertWindow() {
                        try {
                            sf.ui.izotope.windows.whoseDescription.is("alert").first.elementWaitFor({
                                waitType: "Appear",
                                timeout: 1000
                            });
                        } catch (err) {
                            log("Alert window did not appear, continuing...");
                        }
                
                        try {
                            sf.ui.izotope.windows.whoseDescription.is("alert").first.buttons.whoseTitle.is("Yes").first.elementClick();
                        } catch (err) {
                            log("Yes button not found or not clickable, continuing...");
                        }
                     }
                
                         // Usage:
                     handleAlertWindow();
                
                        // If Undo Queue is longer than 1, then wait for alert prompting to save changes.  
                        //if (undoQueueLength) {
                        //    izotopeRX.windows.whoseDescription.is("alert").first.buttons.whoseTitle.is("No").first.elementWaitFor();
                        //    izotopeRX.windows.whoseDescription.is("alert").first.buttons.whoseTitle.is("No").first.elementClick();
                        //    izotopeRX.windows.whoseDescription.is("alert").first.buttons.whoseTitle.is("No").first.elementWaitFor({ waitForNoElement: true });
                        //}
                
                        log(`✅ Export completed for file: ${file.name}`);
                
                        // Mark the first export as complete after the first iteration
                        isFirstExport = false;
                    });
                
                    log("🎉 All stems exported successfully.");
                }
                
                // Run the main script
                main();
                
                
                
                1. Chad Wahlbrink @Chad2025-04-22 15:56:04.853Z

                  Awesome, @Jake_Morton!

                  Great progress.

                  I'll take a look at this next week and see what is possible!

                  1. JJake Morton @Jake_Morton
                      2025-04-30 05:39:35.709Z

                      Just posting a quick update to the script.
                      I've included a "wait until RX's main window appears" factor so that opening the program doesn't halt the script.

                      // Defining iZotope RX is required if you have multiple versions of RX installed
                      let izotopeRX = sf.ui.app("com.izotope.RX11")
                      
                      /** 
                       * WAIT FOR MODALS - Wait for RX Modal
                       */
                      function waitForModals() {
                          try {
                              // Get the title of the current Audio File in iZotope RX
                              const audioFileName = izotopeRX.mainWindow.getElement("AXTitleUIElement").value.invalidate().value;
                              
                              // Possible modal window titles
                              const modalWindowNames = [audioFileName, "Composite"];
                      
                              // If any modal with this title exists, then wait
                              while (modalWindowNames.some(modalWindowName => izotopeRX.floatingWindows.getByTitle(modalWindowName).exists)) {
                                  sf.wait({ intervalMs: 100 });
                              }
                          } catch (err) {
                      
                              throw `Wait for Modals Failed`;
                          }
                      }
                      
                      /** 
                       * Export Navigate to Path 
                       * @param {string} path
                      */
                      function navigateTo(path) {
                          //Get a reference to the focused window in the frontmost app:
                          var win = sf.ui.frontmostApp.focusedWindow;
                      
                          //Sanity check - make sure window is either Save or Open dialog
                          if (!(win.title.value.indexOf('Export File') >= 0 || win.title.value.indexOf('Open') >= 0 || win.title.value.indexOf('Save As') >= 0))
                              throw 'Focused window is not an Open or Save dialog. Title: ' + win.title.value;
                      
                          // Open the Go to... sheet
                          sf.keyboard.type({ text: '/' });
                      
                          // Wait for the sheet to appear
                          win.sheets.first.elementWaitFor({ timeout: 500 }, 'Could not find "Go to" sheet in the Save/Open dialog').element;
                      
                          // Set the value of the combo box
                          sf.keyboard.type({
                              text: path,
                          }, 'Could not paste the path');
                      
                          // Press return
                          sf.keyboard.press({
                              keys: "return",
                          }, "Could not hit return");
                      
                          //Wait for sheet to close
                          win.sheets.first.elementWaitFor({ waitForNoElement: true, timeout: 500 }, '"Go to" sheet didn\'t close in time');
                      }
                      
                      // Utility: Get Current Timestamp
                      function getCurrentTimestamp() {
                          const now = new Date();
                          const year = now.getFullYear();
                          const month = String(now.getMonth() + 1).padStart(2, "0");
                          const day = String(now.getDate()).padStart(2, "0");
                          const hours = String(now.getHours()).padStart(2, "0");
                          const minutes = String(now.getMinutes()).padStart(2, "0");
                          const seconds = String(now.getSeconds()).padStart(2, "0");
                          return `${year}${month}${day}_${hours}${minutes}${seconds}`;
                      }
                      
                      function main() {
                          log("🎬 Script started...");
                      
                          // Step 1: Prompt the user for required inputs
                          log("📥 Prompting the user for required inputs...");
                      
                          // Declare Variables 
                          let tempo, songTitle, destinationDirectory;
                          
                          songTitle = prompt("Please enter the song title:");
                          // If I hit Cancel on the songTitle prompt, and songTitle is undefined, then bail. 
                          if (songTitle !== undefined) {
                              tempo = prompt("Please enter the tempo (e.g., 120):");
                          } else {
                              throw 0
                          }
                          // If I hit Cancel on the tempo prompt, and tempo is undefined, then bail. 
                          if (tempo !== undefined) {
                              destinationDirectory = sf.interaction.selectFolder({
                                  prompt: "Please choose the destination folder for exporting stems",
                              }).path;
                          } else {
                              throw 0
                          }
                      
                          log(`🎵 Song Title: ${songTitle}`);
                          log(`🎚️ Tempo: ${tempo}`);
                          log(`📂 Destination Directory: ${destinationDirectory}`);
                      
                          // Step 2: Create a uniquely timestamped folder for exporting all stems
                          const timestamp = getCurrentTimestamp();
                      
                          // remove invalid characters from the songName
                          const sanitizedSongTitle = songTitle.replace(/[\/:*?"<>|]/g, "_"); // Remove invalid characters
                      
                          // Define the Export Folder with songTitle, tempo and timestamp
                          const exportFolder = `${destinationDirectory}/STEMS_${sanitizedSongTitle}_${tempo}bpm_${timestamp}`;
                          log("📂 Creating a single uniquely timestamped folder for all stems...");
                          sf.file.directoryCreate({ path: exportFolder });
                          log(`📂 Export folder created: ${exportFolder}`);
                      
                          // Step 3: Fetch the file path from Finder
                          let filePath = sf.ui.finder.selectedPaths[0]
                          let fileName = filePath.split('/').slice(-1)[0]
                      
                          log(`🎶 Full Filepath: ${filePath}`);
                      
                          // Step 4: Launch RX 11 and open the file directly
                          log("🚀 Launching RX 11 and opening the file...");
                          sf.file.open({
                              path: filePath,
                              applicationPath: "/Applications/iZotope RX 11 Audio Editor.app",
                          });
                      
                          // Wait briefly to ensure the file is loaded
                          log("⏳ Waiting for RX 11 to load the file...");
                      
                          izotopeRX.appWaitForActive();
                      
                          sf.ui.izotope.mainWindow.elementWaitFor({
                          waitType: "Appear",
                          });
                      
                      
                          // Wait for Currently Open File to Match the Selected File
                          sf.waitFor({
                              callback: () => {
                                  izotopeRX.invalidate();
                                  return izotopeRX.mainWindow.getElement("AXTitleUIElement").value.value == fileName;
                              },
                              timeout: 10000
                          }, `Failed waiting for fileName to Match`);
                      
                          // Step 5: Process Music Rebalance
                          log("🎛️ Processing Music Rebalance...");
                          processMusicRebalance();
                      
                          log("⏳ Waiting for RX 11 to process the file...");
                          // Use "Wait for Modals" function to wait
                          waitForModals();
                      
                          // Step 6: Export Files
                          log("💾 Exporting files...");
                          exportStems(songTitle, tempo, exportFolder);
                      
                          log("🎉 Script completed successfully!");
                          playRandomChime();
                      }
                      
                      // Play Random Chime Function
                      function playRandomChime() {
                          const sounds = [
                              "Metroid_Item_Find_1",
                              "Metroid_Item_Find_2",
                              "Metroid_Item_Find_3",
                              "Metroid_Item_Find_4",
                              "Metroid_Item_Find_5",
                              "Metroid_Item_Find_6"
                          ];
                      
                          const randomIndex = Math.floor(Math.random() * sounds.length);
                          const selectedSound = sounds[randomIndex];
                          const filePath = `/Users/JakeMorton/Dropbox/Soundflow/Audio_Assets/Metroid Item Find Sounds/Wavs/${selectedSound}.wav`;
                      
                          sf.system.exec({ commandLine: `afplay "${filePath}"` });
                      }
                      
                      // Music Rebalance Function
                      function processMusicRebalance() {
                          log("🎛️ Opening the Music Rebalance module...");
                      
                          // If the Music Balance Module is already active, then don't click the menu again - this would hide it. 
                          if (!izotopeRX.getMenuItem("Modules", "Music Rebalance...").isMenuChecked) {
                              izotopeRX.menuClick({ menuPath: ["Modules", "Music Rebalance..."] });
                              log("⏳ Waiting for the Music Rebalance window to appear...");
                              izotopeRX.windows.whoseTitle.is("Music Rebalance").first.elementWaitFor({
                                  waitType: "Appear",
                                  failIfNotFound: true,
                              });
                          }
                      
                          log("✅ Music Rebalance window is active.");
                      
                          log("🎚️ Selecting the quality option...");
                      
                          // Set the Quality of Music Rebalance to "Best"
                          izotopeRX.windows.whoseTitle.is("Music Rebalance").first.groups.whoseDescription.is("EffectPanel Music Rebalance")
                              .first.popupButtons.whoseTitle.is("Quality").first.value.value = 'Best';
                      
                          // Click "Split Stems" button with retry mechanism
                          log("🔀 Attempting to click 'Split Stems' button...");
                      
                          // Click "Split Stems" Button on "Music Balance" module
                          sf.ui.izotope.windows.whoseTitle.is("Music Rebalance").first.groups.whoseDescription.is("EffectPanel Music Rebalance")
                              .first.groups.whoseDescription.is("EffectPanel Music Rebalance").first.groups.whoseDescription.is("EffectPanel Music Rebalance Footer Background")
                              .first.groups.whoseDescription.is("Apply Group Container").first.buttons.whoseDescription.is("Split Stems").first.elementClick();
                      
                           // Wait for "Music Re-balance" to finish processing
                          sf.ui.izotope.windows.first.getElement("AXTitleUIElement").elementWaitFor({
                              waitType: "Disappear",
                              });
                      
                              //Brute forced a wait to ensure no problems re: "additional processing"  occur when exporting files
                              sf.wait({
                              intervalMs: 800,
                              });
                      }
                      // Handle Save Alert Function
                      function handleSaveAlert() {
                          try {
                              sf.ui.izotope.windows.whoseDescription.is("alert").first.elementWaitFor({
                                  waitType: "Appear",
                                  timeout: 1000
                              });
                              log("⚠️ Save alert appeared.");
                      
                              sf.ui.izotope.windows.whoseDescription.is("alert").first.buttons.whoseTitle.is("No").first.elementClick();
                              log("🛑 Clicked 'No' on save alert.");
                          } catch (err) {
                              log("✅ No save alert appeared, continuing...");
                          }
                      }
                      
                      // Export Stems Function
                      function exportStems(songTitle, tempo, exportFolder) {
                          log("💾 Exporting stems...");
                      
                          const fileNames = [
                              { prefix: "00", name: "Original Recording" },
                              { prefix: "01", name: "Vocals" },
                              { prefix: "02", name: "Bass" },
                              { prefix: "03", name: "Percussion" },
                              { prefix: "04", name: "Other Instruments" },
                          ];
                      
                          const currentDate = getCurrentTimestamp();
                          log(`🗓️ Current date: ${currentDate}`);
                      
                          // Track whether it's the first export
                          let isFirstExport = true;
                      
                          fileNames.forEach((file, index) => {
                              const exportFileName = `${file.prefix}_${file.name}_${songTitle}_${tempo}_${currentDate}.wav`;
                              const exportPath = `${exportFolder}/${exportFileName}`;
                              log(`🎙️ Processing file: ${file.name} (${index + 1}/${fileNames.length})`);
                              log(`📂 Export path: ${exportPath}`);
                              log("💾 Opening Export dialog...");
                      
                              izotopeRX.menuClick({ menuPath: ["File", "Export..."] });
                      
                              // Wait for the Export Module to Appear
                              waitForExportPanelWithRetry();
                      
                      function waitForExportPanelWithRetry(attempts = 3) {
                          for (let i = 0; i < attempts; i++) {
                              try {
                                  izotopeRX.mainWindow.groups.whoseDescription.is("ExportPanel Module Wrapper").first.elementWaitFor({ timeout: 3000 });
                                  log("✅ Export Panel detected.");
                                  return true;
                              } catch (err) {
                                  log(`⚠️ Attempt ${i + 1}: Export Panel not found.`);
                              }
                          }
                          log("⚠️ Export Panel did not appear after retries, continuing...");
                          return false;
                      }        
                      
                              // We want the original file to be at the original bit-depth
                              if (!isFirstExport) {
                                  log("🔧 Adjusting export bit depth (not required for the first file)...");
                      
                                  // Select WAV File
                                  izotopeRX.mainWindow.buttons.whoseDescription.is("WAVButton").first.elementClick();
                      
                                  // Set Bit Depth to '24-bit'
                                  izotopeRX.mainWindow.popupButtons.whoseTitle.is("Bit depth").first.value.value = '24-bit';
                              }
                      
                              // Click "OK" in the Export Panel Window
                              izotopeRX.mainWindow.groups.whoseDescription.is("ExportPanel Module Wrapper").first.groups.whoseDescription.is("ExportPanel Module Wrapper Footer Background")
                                  .first.buttons.whoseDescription.is("OK").first.elementClick({ asyncSwallow: true });
                      
                              log("⏳ Waiting for RX 11 export UI to load...");
                              // wait for Export File Dialog with "Save" Button to Appear
                              izotopeRX.windows.whoseTitle.is('Export File').first.buttons.whoseTitle.is("Save").first.elementWaitFor();
                      
                              log("📁 Navigating to folder and typing export path...");
                      
                              // Using "Navigate To" utility function
                              navigateTo(exportPath);
                      
                              // Wait for "Save" button to be clickable, then Click "Save" Button, then wait for "Save" button to disappear
                              sf.waitFor({
                                  callback: () => {
                                      return izotopeRX.windows.whoseTitle.is('Export File').first.buttons.whoseTitle.is("Save").first.isEnabled;
                                  },
                                  timeout: 1000
                              }, `Failed waiting`);
                      
                              izotopeRX.windows.whoseTitle.is('Export File').first.buttons.whoseTitle.is("Save").first.elementClick();
                      
                              izotopeRX.windows.whoseTitle.is('Export File').first.buttons.whoseTitle.is("Save").first.elementWaitFor({ waitType: "Disappear" });
                      
                              // Wait for Export Panel to Disappear
                              izotopeRX.mainWindow.groups.whoseDescription.is("ExportPanel Module Wrapper").first.elementWaitFor({ waitType: "Disappear" });
                      
                              // Try and brute force a wait to see if it allows time for the export to occur and the close function to work.
                              sf.wait({
                              intervalMs: 800,
                              });
                      
                      
                              // Check the undo queue length
                              let undoQueueLength = izotopeRX.mainWindow.groups.whoseDescription.is("Undo Panel").first.radioButtons.length > 1
                      
                              // Close the File 
                              izotopeRX.menuClick({ menuPath: ['File', 'Close File'] });
                      
                              handleSaveAlert();
                      
                      
                               // Handle Alert "additional processing unfinished" nuisance
                              function handleAlertWindow() {
                              try {
                                  sf.ui.izotope.windows.whoseDescription.is("alert").first.elementWaitFor({
                                      waitType: "Appear",
                                      timeout: 1000
                                  });
                              } catch (err) {
                                  log("Alert window did not appear, continuing...");
                              }
                      
                              try {
                                  sf.ui.izotope.windows.whoseDescription.is("alert").first.buttons.whoseTitle.is("Yes").first.elementClick();
                              } catch (err) {
                                  log("Yes button not found or not clickable, continuing...");
                              }
                           }
                      
                               // Usage:
                           handleAlertWindow();
                      
                              // If Undo Queue is longer than 1, then wait for alert prompting to save changes.  
                              //if (undoQueueLength) {
                              //    izotopeRX.windows.whoseDescription.is("alert").first.buttons.whoseTitle.is("No").first.elementWaitFor();
                              //    izotopeRX.windows.whoseDescription.is("alert").first.buttons.whoseTitle.is("No").first.elementClick();
                              //    izotopeRX.windows.whoseDescription.is("alert").first.buttons.whoseTitle.is("No").first.elementWaitFor({ waitForNoElement: true });
                              //}
                      
                              log(`✅ Export completed for file: ${file.name}`);
                      
                              // Mark the first export as complete after the first iteration
                              isFirstExport = false;
                          });
                      
                          log("🎉 All stems exported successfully.");
                      }
                      
                      // Run the main script
                      main();
                      
                      
                      1. Chad Wahlbrink @Chad2025-05-05 21:57:38.572Z

                        Excellent work, @Jake_Morton!

                        I've expanded the script a bit below. This version will allow you to select multiple files for processing. It uses the source file names to extract a song title and bpm - this way, you don't have to have prompts for each song. The song files should be named like: "My Track_85BPM_v2.wav" or "Something_else_100BPM_final.WAV" - the name before "_XXXBPM" will be used as the song title, and then the BPM will be extracted as well.

                        This will prompt you twice - first to select the source files and then second to select a destination folder.

                        I also implemented a text message feature so that you can have it send iMessage updates after each completed file and then an SMS at the end of the script. Just replace line 3 with your phone number to try it out. This way, you can truly batch process and walk away while SoundFlow works.

                        I also replaced a few of your manual sf.wait commands with :

                        while (!izotopeRX.getMenuItem('Edit', 'Select All').isEnabled) {
                                        sf.wait({ intervalMs: 200 });
                                    }
                        

                        Let me know how this goes for you!

                        Cheers ✨✨

                        
                        let sendTextMessages = true;
                        let phoneNumber = `xxxxxxxxx`
                        
                        const sounds = [
                            "Metroid_Item_Find_1",
                            "Metroid_Item_Find_2",
                            "Metroid_Item_Find_3",
                            "Metroid_Item_Find_4",
                            "Metroid_Item_Find_5",
                            "Metroid_Item_Find_6"
                        ];
                        
                        const soundsFilePath = '/Users/JakeMorton/Dropbox/Soundflow/Audio_Assets/Metroid Item Find Sounds/Wavs/' 
                        
                        // Defining iZotope RX is required if you have multiple versions of RX installed
                        let izotopeRX = sf.ui.app("com.izotope.RX11")
                        
                        ////////////////////////////////////////////////////////////////////////////////
                        
                        /** 
                         * Play Random Chime
                         */
                        function playRandomChime() {
                            const randomIndex = Math.floor(Math.random() * sounds.length);
                            const selectedSound = sounds[randomIndex];
                        
                            const filePath = `${soundsFilePath}${selectedSound}.wav`;
                        
                            sf.system.exec({ commandLine: `afplay "${filePath}"` });
                        }
                        
                        /** 
                         * WAIT FOR MODALS - Wait for RX Modal
                         */
                        function waitForModals() {
                            try {
                                // Get the title of the current Audio File in iZotope RX
                                const audioFileName = izotopeRX.mainWindow.getElement("AXTitleUIElement").value.invalidate().value;
                        
                                // Possible modal window titles
                                const modalWindowNames = [audioFileName, "Composite"];
                        
                                // If any modal with this title exists, then wait
                                while (modalWindowNames.some(modalWindowName => izotopeRX.floatingWindows.getByTitle(modalWindowName).exists)) {
                                    sf.wait({ intervalMs: 100 });
                                }
                            } catch (err) {
                        
                                throw `Wait for Modals Failed`;
                            }
                        }
                        
                        /** 
                         * Send Text Message
                         * @param {Object} messageObject - The message Object
                         * @param {string} messageObject.type - 'SMS' or 'iMessage'
                         * @param {string} messageObject.contact - Phone Number
                         * @param {string} messageObject.message - The Text Message
                         */
                        function sendText(messageObject) {
                            sf.system.execAppleScript({
                                script: `
                                    tell application "Messages"
                                        set a to first account whose service type = ${messageObject.type} and enabled is true
                                        set p to participant "${messageObject.contact}" of a
                        	            send "${messageObject.message}" to p
                                    end tell
                               `,
                                implementation: "NSAppleScript",
                            });
                        }
                        
                        /** 
                         * Export Navigate to Path 
                         * @param {string} path
                        */
                        function navigateTo(path) {
                            //Get a reference to the focused window in the frontmost app:
                            var win = sf.ui.frontmostApp.focusedWindow;
                        
                            //Sanity check - make sure window is either Save or Open dialog
                            if (!(win.title.value.indexOf('Export File') >= 0 || win.title.value.indexOf('Open') >= 0 || win.title.value.indexOf('Save As') >= 0))
                                throw 'Focused window is not an Open or Save dialog. Title: ' + win.title.value;
                        
                            // Open the Go to... sheet
                            sf.keyboard.type({ text: '/' });
                        
                            // Wait for the sheet to appear
                            win.sheets.first.elementWaitFor({ timeout: 500 }, 'Could not find "Go to" sheet in the Save/Open dialog').element;
                        
                            // Set the value of the combo box
                            sf.keyboard.type({
                                text: path,
                            }, 'Could not paste the path');
                        
                            // Press return
                            sf.keyboard.press({
                                keys: "return",
                            }, "Could not hit return");
                        
                            //Wait for sheet to close
                            win.sheets.first.elementWaitFor({ waitForNoElement: true, timeout: 500 }, '"Go to" sheet didn\'t close in time');
                        }
                        
                        // Utility: Get Current Timestamp
                        function getCurrentTimestamp() {
                            const now = new Date();
                            const year = now.getFullYear();
                            const month = String(now.getMonth() + 1).padStart(2, "0");
                            const day = String(now.getDate()).padStart(2, "0");
                            const hours = String(now.getHours()).padStart(2, "0");
                            const minutes = String(now.getMinutes()).padStart(2, "0");
                            const seconds = String(now.getSeconds()).padStart(2, "0");
                            return `${year}${month}${day}_${hours}${minutes}${seconds}`;
                        }
                        
                        function main() {
                        
                            try {
                                log("🎬 Script started...");
                                let defaultLocation = `~/Desktop/`
                                if (sf.ui.finder.selectedPaths[0] != undefined) {
                                    defaultLocation = sf.ui.finder.selectedPaths[0];
                                }
                        
                                // Step 1: Prompt the user for required inputs
                                log("📥 Prompting the user for required inputs...");
                                let filePathsToProcess = sf.interaction.selectFiles({
                                    prompt: `Please the files you'd like to process with Music Rebalance`,
                                    allowedFileTypes: ['wav', 'aif', 'aiff'],
                                    defaultLocation,
                                });
                        
                                let destinationDirectory = sf.interaction.selectFolder({
                                    prompt: "Please choose the destination folder for exporting stems",
                                    defaultLocation: filePathsToProcess[0]
                                }).path;
                        
                                let filesToProcess = [];
                        
                                // Regex to match Song Name and BPM by file name based on files like "My Track_85BPM_v2.wav", "Something_else_100BPM_final.mp3"
                                let bpmAndFileNameRegex = /^(.+?)_(\d{2,3})BPM(?=_|\.|$)/
                        
                                // For each path selected, push a new "files to process" object into the array
                                filePathsToProcess.paths.forEach(path => {
                        
                                    // Extract the file path
                                    let filePath = path;
                        
                                    // Extract the File name 
                                    let fileName = filePath.split('/').slice(-1)[0];
                        
                                    // Try to match for Song Name and BPM
                                    const match = fileName.match(bpmAndFileNameRegex);
                        
                                    let songTitle, tempo;
                        
                                    // If we matched, then extract song name and BPM
                                    if (match) {
                                        songTitle = match[1];
                                        tempo = match[2];
                                    } else {
                                        throw (`Could Not Match Song Name and BPM in file: ${fileName}`)
                                    }
                        
                                    // Push a new object with the file path, file name, song name, and bpm onto the queue 
                                    filesToProcess.push({
                                        filePath,
                                        fileName,
                                        songTitle,
                                        tempo
                                    })
                                })
                        
                                filesToProcess.forEach((file, index, allFiles) => {
                                    let { filePath, fileName, songTitle, tempo, } = file;
                        
                                    log(`🎶 Full Filepath: ${filePath}`);
                                    log(`🎵 Song Title: ${songTitle}`);
                                    log(`🎚️ Tempo: ${tempo}`);
                                    log(`📂 Destination Directory: ${destinationDirectory}`);
                        
                                    // Step 2: Create a uniquely timestamped folder for exporting all stems
                                    const timestamp = getCurrentTimestamp();
                        
                                    // remove invalid characters from the songName
                                    const sanitizedSongTitle = songTitle.replace(/[\/:*?"<>|]/g, "_"); // Remove invalid characters
                        
                                    // Define the Export Folder with songTitle, tempo and timestamp
                                    const exportFolder = `${destinationDirectory}/STEMS_${sanitizedSongTitle}_${tempo}bpm_${timestamp}`;
                                    log("📂 Creating a single uniquely timestamped folder for all stems...");
                                    sf.file.directoryCreate({ path: exportFolder });
                                    log(`📂 Export folder created: ${exportFolder}`);
                        
                                    // Step 4: Launch RX 11 and open the file directly
                                    log("🚀 Launching RX 11 and opening the file...");
                                    sf.file.open({
                                        path: filePath,
                                        applicationPath: "/Applications/iZotope RX 11 Audio Editor.app",
                                    });
                        
                                    // Wait briefly to ensure the file is loaded
                                    log("⏳ Waiting for RX 11 to load the file...");
                        
                                    izotopeRX.appWaitForActive();
                        
                                    sf.ui.izotope.mainWindow.elementWaitFor({
                                        waitType: "Appear",
                                    });
                        
                                    // Wait for Currently Open File to Match the Selected File
                                    sf.waitFor({
                                        callback: () => {
                                            izotopeRX.invalidate();
                                            return izotopeRX.mainWindow.getElement("AXTitleUIElement").value.value == fileName;
                                        },
                                        timeout: 10000
                                    }, `Failed waiting for fileName to Match`);
                        
                                    // Step 5: Process Music Rebalance
                                    log("🎛️ Processing Music Rebalance...");
                                    processMusicRebalance();
                        
                                    log("⏳ Waiting for RX 11 to process the file...");
                                    // Use "Wait for Modals" function to wait
                                    waitForModals();
                        
                                    // Step 6: Export Files
                                    log("💾 Exporting files...");
                                    exportStems(songTitle, tempo, exportFolder);
                        
                                    // Send an iMessage with Progress
                                    if (sendTextMessages) {
                                        // Send an iMessage of Progress Update
                                        sendText({
                                            contact: phoneNumber,
                                            type: 'iMessage',
                                            message: `${index + 1}/${filesToProcess.length} - '${songTitle}_${tempo}' Finished Processing`,
                                        })
                                    }
                                })
                        
                                // Send an SMS if we Finish all Files
                                log("🎉 Script completed successfully!");
                                if (sendTextMessages) {
                                    sendText({
                                        contact: phoneNumber,
                                        type: 'SMS',
                                        message: `🎉 All Files Finished Processing with Music Rebalance`,
                                    })
                                }
                                playRandomChime();
                            } catch (err) {
                                // Send an SMS if we hit an Error
                                if (sendTextMessages) {
                                    sendText({
                                        contact: phoneNumber,
                                        type: 'SMS',
                                        message: `Music Rebalance Processing Hit an Error`,
                                    })
                        
                                    throw `🛑 Music Rebalance Processing Hit an Error: ${err}`
                                }
                            }
                        
                            // Music Rebalance Function
                            function processMusicRebalance() {
                                log("🎛️ Opening the Music Rebalance module...");
                        
                                // If the Music Balance Module is already active, then don't click the menu again - this would hide it. 
                                if (!izotopeRX.getMenuItem("Modules", "Music Rebalance...").isMenuChecked) {
                                    izotopeRX.menuClick({ menuPath: ["Modules", "Music Rebalance..."] });
                                    log("⏳ Waiting for the Music Rebalance window to appear...");
                                    izotopeRX.windows.whoseTitle.is("Music Rebalance").first.elementWaitFor({
                                        waitType: "Appear",
                                        failIfNotFound: true,
                                    });
                                }
                        
                                log("✅ Music Rebalance window is active.");
                        
                                log("🎚️ Selecting the quality option...");
                        
                                // Set the Quality of Music Rebalance to "Best"
                                izotopeRX.windows.whoseTitle.is("Music Rebalance").first.groups.whoseDescription.is("EffectPanel Music Rebalance")
                                    .first.popupButtons.whoseTitle.is("Quality").first.value.value = 'Best';
                        
                                // Click "Split Stems" button with retry mechanism
                                log("🔀 Attempting to click 'Split Stems' button...");
                        
                                // Click "Split Stems" Button on "Music Balance" module
                                sf.ui.izotope.windows.whoseTitle.is("Music Rebalance").first.groups.whoseDescription.is("EffectPanel Music Rebalance")
                                    .first.groups.whoseDescription.is("EffectPanel Music Rebalance").first.groups.whoseDescription.is("EffectPanel Music Rebalance Footer Background")
                                    .first.groups.whoseDescription.is("Apply Group Container").first.buttons.whoseDescription.is("Split Stems").first.elementClick();
                        
                                // Wait for "Music Re-balance" to finish processing
                                sf.ui.izotope.windows.first.getElement("AXTitleUIElement").elementWaitFor({
                                    waitType: "Disappear",
                                });
                        
                                //Brute forced a wait to ensure no problems re: "additional processing"  occur when exporting files
                                // sf.wait({
                                //     intervalMs: 800,
                                // });
                        
                                // CW - Use this to wait for processing or export to finished
                                while (!izotopeRX.getMenuItem('Edit', 'Select All').isEnabled) {
                                    sf.wait({ intervalMs: 200 });
                                }
                            }
                            // Handle Save Alert Function
                            function handleSaveAlert() {
                                try {
                                    sf.ui.izotope.windows.whoseDescription.is("alert").first.elementWaitFor({
                                        waitType: "Appear",
                                        timeout: 1000
                                    });
                                    log("⚠️ Save alert appeared.");
                        
                                    sf.ui.izotope.windows.whoseDescription.is("alert").first.buttons.whoseTitle.is("No").first.elementClick();
                                    log("🛑 Clicked 'No' on save alert.");
                                } catch (err) {
                                    log("✅ No save alert appeared, continuing...");
                                }
                            }
                        
                            // Handle Alert "additional processing unfinished" nuisance
                            function handleAlertWindow() {
                                try {
                                    sf.ui.izotope.windows.whoseDescription.is("alert").first.elementWaitFor({
                                        waitType: "Appear",
                                        timeout: 1000
                                    });
                                } catch (err) {
                                    log("Alert window did not appear, continuing...");
                                }
                        
                                try {
                                    sf.ui.izotope.windows.whoseDescription.is("alert").first.buttons.whoseTitle.is("Yes").first.elementClick();
                                } catch (err) {
                                    log("Yes button not found or not clickable, continuing...");
                                }
                            }
                        
                            // Export Stems Function
                            function exportStems(songTitle, tempo, exportFolder) {
                                log("💾 Exporting stems...");
                        
                                const fileNames = [
                                    { prefix: "00", name: "Original Recording" },
                                    { prefix: "01", name: "Vocals" },
                                    { prefix: "02", name: "Bass" },
                                    { prefix: "03", name: "Percussion" },
                                    { prefix: "04", name: "Other Instruments" },
                                ];
                        
                                const currentDate = getCurrentTimestamp();
                                log(`🗓️ Current date: ${currentDate}`);
                        
                                // Track whether it's the first export
                                let isFirstExport = true;
                        
                                fileNames.forEach((file, index) => {
                                    const exportFileName = `${file.prefix}_${file.name}_${songTitle}_${tempo}_${currentDate}.wav`;
                                    const exportPath = `${exportFolder}/${exportFileName}`;
                                    log(`🎙️ Processing file: ${file.name} (${index + 1}/${fileNames.length})`);
                                    log(`📂 Export path: ${exportPath}`);
                                    log("💾 Opening Export dialog...");
                        
                                    izotopeRX.menuClick({ menuPath: ["File", "Export..."] });
                        
                                    // Wait for the Export Module to Appear
                                    waitForExportPanelWithRetry();
                        
                                    function waitForExportPanelWithRetry(attempts = 3) {
                                        for (let i = 0; i < attempts; i++) {
                                            try {
                                                izotopeRX.mainWindow.groups.whoseDescription.is("ExportPanel Module Wrapper").first.elementWaitFor({ timeout: 3000 });
                                                log("✅ Export Panel detected.");
                                                return true;
                                            } catch (err) {
                                                log(`⚠️ Attempt ${i + 1}: Export Panel not found.`);
                                            }
                                        }
                                        log("⚠️ Export Panel did not appear after retries, continuing...");
                                        return false;
                                    }
                        
                                    // We want the original file to be at the original bit-depth
                                    if (!isFirstExport) {
                                        log("🔧 Adjusting export bit depth (not required for the first file)...");
                        
                                        // Select WAV File
                                        izotopeRX.mainWindow.buttons.whoseDescription.is("WAVButton").first.elementClick();
                        
                                        // Set Bit Depth to '24-bit'
                                        izotopeRX.mainWindow.popupButtons.whoseTitle.is("Bit depth").first.value.value = '24-bit';
                                    }
                        
                                    // Click "OK" in the Export Panel Window
                                    izotopeRX.mainWindow.groups.whoseDescription.is("ExportPanel Module Wrapper").first.groups.whoseDescription.is("ExportPanel Module Wrapper Footer Background")
                                        .first.buttons.whoseDescription.is("OK").first.elementClick({ asyncSwallow: true });
                        
                                    log("⏳ Waiting for RX 11 export UI to load...");
                                    // wait for Export File Dialog with "Save" Button to Appear
                                    izotopeRX.windows.whoseTitle.is('Export File').first.buttons.whoseTitle.is("Save").first.elementWaitFor();
                        
                                    log("📁 Navigating to folder and typing export path...");
                        
                                    // Using "Navigate To" utility function
                                    navigateTo(exportPath);
                        
                                    // Wait for "Save" button to be clickable, then Click "Save" Button, then wait for "Save" button to disappear
                                    sf.waitFor({
                                        callback: () => {
                                            return izotopeRX.windows.whoseTitle.is('Export File').first.buttons.whoseTitle.is("Save").first.isEnabled;
                                        },
                                        timeout: 1000
                                    }, `Failed waiting`);
                        
                                    izotopeRX.windows.whoseTitle.is('Export File').first.buttons.whoseTitle.is("Save").first.elementClick();
                        
                                    izotopeRX.windows.whoseTitle.is('Export File').first.buttons.whoseTitle.is("Save").first.elementWaitFor({ waitType: "Disappear" });
                        
                                    // Wait for Export Panel to Disappear
                                    izotopeRX.mainWindow.groups.whoseDescription.is("ExportPanel Module Wrapper").first.elementWaitFor({ waitType: "Disappear" });
                        
                                    // Try and brute force a wait to see if it allows time for the export to occur and the close function to work.
                                    // sf.wait({
                                    //     intervalMs: 800,
                                    // });
                        
                                    // CW - Use this to wait for processing or export to finished
                                    while (!izotopeRX.getMenuItem('Edit', 'Select All').isEnabled) {
                                        sf.wait({ intervalMs: 200 });
                                    }
                        
                                    // Close the File 
                                    izotopeRX.menuClick({ menuPath: ['File', 'Close File'] });
                        
                                    handleSaveAlert();
                        
                                    handleAlertWindow();
                        
                                    log(`✅ Export completed for file: ${file.name}`);
                        
                                    // Mark the first export as complete after the first iteration
                                    isFirstExport = false;
                                });
                        
                                log("🎉 All stems exported successfully.");
                            }
                        }
                        
                        // Run the main script
                        main()
                        
                        ReplySolution
              • J
                In reply toJake_Morton:
                Jake Morton @Jake_Morton
                  2025-05-12 02:10:54.288Z

                  Wow!

                  Thank you Chad.
                  What a massive help!

                  I'm wondering if there is a way to get the text messages functional on Android devices as well?
                  I don't know enough about iMessage to know.

                  No rush on this.

                  Hope you're well.

                  1. Chad Wahlbrink @Chad2025-05-12 14:07:31.452Z

                    Hi, @Jake_Morton,

                    I'm not 100% sure on that. I THINK you could set this up by changing the functions to use type SMS. However, you may still need to have that set up via the Apple Messages app on your computer, and I'm not 100% sure on how that would work.

                    Let me know if it does work for you with Android, though. If not, maybe I can find some other approaches.

                    sendText({
                    contact: phoneNumber,
                    type: 'SMS',
                    message: `Music Rebalance Processing Hit an Error`,
                    })
                    
                  2. T
                    In reply toJake_Morton:
                    The Balance King @The_Balance_King
                      2025-05-16 00:58:48.004Z

                      Wow, this is fantastic - thank you for this! I was just messing around with RX 11 Stem separation and thinking it would be nice to be able to batch automate stem separation, and stumbled across this!

                      I haven't used SoundFlow before so I'm still getting oriented. I copied the script over but I use a different file name convention. How would I need to edit the script to handle files named with the following format?

                      "Artist(s) - Song Title - Key - BPM"

                      Thanks in advance for any insight and assistance!!

                      1. Chad Wahlbrink @Chad2025-05-19 18:37:03.752Z2025-05-19 18:52:09.770Z

                        Hi, @The_Balance_King,

                        Here's a more generic version of the script (omitting the playRandomChime() function that was referencing files on Jake's machine).

                        This version should accept song titles in the format: "Artist(s) - Song Title - Key - BPM" OR in the format Jake was using ("My Track_85BPM_v2.wav" or "Something_else_100BPM_final.WAV").

                        It still outputs the name fairly similarly to the last version. Let me know if you need it to change the output naming convention at all.

                        
                        let sendTextMessages = false;
                        let phoneNumber = `xxxxxxxxxx`
                        
                        // Defining iZotope RX is required if you have multiple versions of RX installed
                        let izotopeRX = sf.ui.app("com.izotope.RX11");
                        
                        ////////////////////////////////////////////////////////////////////////////////
                        
                        /** 
                         * WAIT FOR MODALS - Wait for RX Modal
                         */
                        function waitForModals() {
                            try {
                                // Get the title of the current Audio File in iZotope RX
                                const audioFileName = izotopeRX.mainWindow.getElement("AXTitleUIElement").value.invalidate().value;
                        
                                // Possible modal window titles
                                const modalWindowNames = [audioFileName, "Composite"];
                        
                                // If any modal with this title exists, then wait
                                while (modalWindowNames.some(modalWindowName => izotopeRX.floatingWindows.getByTitle(modalWindowName).exists)) {
                                    sf.wait({ intervalMs: 100 });
                                }
                            } catch (err) {
                        
                                throw `Wait for Modals Failed`;
                            }
                        }
                        
                        /** 
                         * Send Text Message
                         * @param {Object} messageObject - The message Object
                         * @param {string} messageObject.type - 'SMS' or 'iMessage'
                         * @param {string} messageObject.contact - Phone Number
                         * @param {string} messageObject.message - The Text Message
                         */
                        function sendText(messageObject) {
                            sf.system.execAppleScript({
                                script: `
                                    tell application "Messages"
                                        set a to first account whose service type = ${messageObject.type} and enabled is true
                                        set p to participant "${messageObject.contact}" of a
                        	            send "${messageObject.message}" to p
                                    end tell
                               `,
                                implementation: "NSAppleScript",
                            });
                        }
                        
                        /** 
                         * Export Navigate to Path 
                         * @param {string} path
                        */
                        function navigateTo(path) {
                            //Get a reference to the focused window in the frontmost app:
                            var win = sf.ui.frontmostApp.focusedWindow;
                        
                            //Sanity check - make sure window is either Save or Open dialog
                            if (!(win.title.value.indexOf('Export File') >= 0 || win.title.value.indexOf('Open') >= 0 || win.title.value.indexOf('Save As') >= 0))
                                throw 'Focused window is not an Open or Save dialog. Title: ' + win.title.value;
                        
                            // Open the Go to... sheet
                            sf.keyboard.type({ text: '/' });
                        
                            // Wait for the sheet to appear
                            win.sheets.first.elementWaitFor({ timeout: 500 }, 'Could not find "Go to" sheet in the Save/Open dialog').element;
                        
                            // Set the value of the combo box
                            sf.keyboard.type({
                                text: path,
                            }, 'Could not paste the path');
                        
                            // Press return
                            sf.keyboard.press({
                                keys: "return",
                            }, "Could not hit return");
                        
                            //Wait for sheet to close
                            win.sheets.first.elementWaitFor({ waitForNoElement: true, timeout: 500 }, '"Go to" sheet didn\'t close in time');
                        }
                        
                        // Utility: Get Current Timestamp
                        function getCurrentTimestamp() {
                            const now = new Date();
                            const year = now.getFullYear();
                            const month = String(now.getMonth() + 1).padStart(2, "0");
                            const day = String(now.getDate()).padStart(2, "0");
                            const hours = String(now.getHours()).padStart(2, "0");
                            const minutes = String(now.getMinutes()).padStart(2, "0");
                            const seconds = String(now.getSeconds()).padStart(2, "0");
                            return `${year}${month}${day}_${hours}${minutes}${seconds}`;
                        }
                        
                        function main() {
                        
                            try {
                                log("🎬 Script started...");
                                let defaultLocation = `~/Desktop/`
                                if (sf.ui.finder.selectedPaths[0] != undefined) {
                                    defaultLocation = sf.ui.finder.selectedPaths[0];
                                }
                        
                                // Step 1: Prompt the user for required inputs
                                log("📥 Prompting the user for required inputs...");
                                let filePathsToProcess = sf.interaction.selectFiles({
                                    prompt: `Please the files you'd like to process with Music Rebalance`,
                                    allowedFileTypes: ['wav', 'aif', 'aiff'],
                                    defaultLocation,
                                });
                        
                                let destinationDirectory = sf.interaction.selectFolder({
                                    prompt: "Please choose the destination folder for exporting stems",
                                    defaultLocation: filePathsToProcess[0]
                                }).path;
                        
                                let filesToProcess = [];
                        
                                // Regex to match Song Name and BPM by file name based on files like "My Track_85BPM_v2.wav", "Something_else_100BPM_final.mp3"
                                let bpmAndFileNameRegex = /^(.+?)(?:\s*[-_]\s*(?:[A-G][#b]?\s?(?:Major|Minor|min|maj|M|m)))?\s*[-_]\s*(\d{2,3})BPM(?:[_\s-]*.*)?(?=\.|$)/;
                        
                                // For each path selected, push a new "files to process" object into the array
                                filePathsToProcess.paths.forEach(path => {
                        
                                    // Extract the file path
                                    let filePath = path;
                        
                                    // Extract the File name 
                                    let fileName = filePath.split('/').slice(-1)[0];
                        
                                    // Try to match for Song Name and BPM
                                    const match = fileName.match(bpmAndFileNameRegex);
                        
                                    let songTitle, tempo;
                        
                                    // If we matched, then extract song name and BPM
                                    if (match) {
                                        songTitle = match[1].trim();
                                        tempo = match[2];
                                    } else {
                                        throw (`Could Not Match Song Name and BPM in file: ${fileName}`)
                                    }
                        
                                    // Push a new object with the file path, file name, song name, and bpm onto the queue 
                                    filesToProcess.push({
                                        filePath,
                                        fileName,
                                        songTitle,
                                        tempo
                                    })
                                })
                        
                                if (filesToProcess.length === 0) {
                                    alert(`Rename File to include BPM like "My Track_85BPM_v2.wav" or "Something_else_100BPM_final.WAV" or "Artist(s) - Song Title - Key - 100BPM"`;)
                                    throw 0;
                                }
                        
                                filesToProcess.forEach((file, index, allFiles) => {
                                    let { filePath, fileName, songTitle, tempo, } = file;
                        
                                    log(`🎶 Full Filepath: ${filePath}`);
                                    log(`🎵 Song Title: ${songTitle}`);
                                    log(`🎚️ Tempo: ${tempo}`);
                                    log(`📂 Destination Directory: ${destinationDirectory}`);
                        
                                    // Step 2: Create a uniquely timestamped folder for exporting all stems
                                    const timestamp = getCurrentTimestamp();
                        
                                    // remove invalid characters from the songName
                                    const sanitizedSongTitle = songTitle.replace(/[\/:*?"<>|]/g, "_"); // Remove invalid characters
                        
                                    // Step 4: Launch RX 11 and open the file directly
                                    log("🚀 Launching RX 11 and opening the file...");
                                    sf.file.open({
                                        path: filePath,
                                        applicationPath: "/Applications/iZotope RX 11 Audio Editor.app",
                                    });
                        
                                    // Wait briefly to ensure the file is loaded
                                    log(`⏳ Waiting for RX 11 to load the file... ${fileName}`);
                        
                                    izotopeRX.appWaitForActive({onError:"ThrowError"}, `Failed waiting for iZotope RX to activate`);
                        
                                    // Wait for Currently Open File to Match the Selected File
                                    sf.waitFor({
                                        callback: () => {
                                            izotopeRX.invalidate();
                                            return izotopeRX.mainWindow.getElement("AXTitleUIElement").value.value == fileName;
                                        },
                                        timeout: 10000
                                    }, `Failed waiting for fileName to Match`);
                        
                                    // Step 5: Process Music Rebalance
                                    log("🎛️ Processing Music Rebalance...");
                                    processMusicRebalance();
                        
                                    log("⏳ Waiting for RX 11 to process the file...");
                                    // Use "Wait for Modals" function to wait
                                    waitForModals();
                        
                                    // Define the Export Folder with songTitle, tempo and timestamp
                                    const exportFolder = `${destinationDirectory}/STEMS_${sanitizedSongTitle}_${tempo}bpm_${timestamp}`;
                                    log("📂 Creating a single uniquely timestamped folder for all stems...");
                                    sf.file.directoryCreate({ path: exportFolder });
                                    log(`📂 Export folder created: ${exportFolder}`);
                        
                                    // Step 6: Export Files
                                    log("💾 Exporting files...");
                                    exportStems(songTitle, tempo, exportFolder);
                        
                                    // Send an iMessage with Progress
                                    if (sendTextMessages) {
                                        // Send an iMessage of Progress Update
                                        sendText({
                                            contact: phoneNumber,
                                            type: 'iMessage',
                                            message: `${index + 1}/${filesToProcess.length} - '${songTitle}_${tempo}' Finished Processing`,
                                        })
                                    }
                                })
                        
                                // Send an SMS if we Finish all Files
                                log("🎉 Script completed successfully!");
                                if (sendTextMessages) {
                                    sendText({
                                        contact: phoneNumber,
                                        type: 'SMS',
                                        message: `🎉 All Files Finished Processing with Music Rebalance`,
                                    })
                                }
                        
                            } catch (err) {
                                // Send an SMS if we hit an Error
                                if (sendTextMessages) {
                                    sendText({
                                        contact: phoneNumber,
                                        type: 'SMS',
                                        message: `Music Rebalance Processing Hit an Error`,
                                    })
                        
                                    throw `🛑 Music Rebalance Processing Hit an Error: ${err}`
                                }
                            }
                        
                            // Music Rebalance Function
                            function processMusicRebalance() {
                                log("🎛️ Opening the Music Rebalance module...");
                        
                                // If the Music Balance Module is already active, then don't click the menu again - this would hide it. 
                                if (!izotopeRX.getMenuItem("Modules", "Music Rebalance...").isMenuChecked) {
                                    izotopeRX.menuClick({ menuPath: ["Modules", "Music Rebalance..."] });
                                    log("⏳ Waiting for the Music Rebalance window to appear...");
                                    izotopeRX.windows.whoseTitle.is("Music Rebalance").first.elementWaitFor({
                                        waitType: "Appear",
                                        failIfNotFound: true,
                                    });
                                }
                        
                                log("✅ Music Rebalance window is active.");
                        
                                log("🎚️ Selecting the quality option...");
                        
                                // Set the Quality of Music Rebalance to "Best"
                                izotopeRX.windows.whoseTitle.is("Music Rebalance").first.groups.whoseDescription.is("EffectPanel Music Rebalance")
                                    .first.popupButtons.whoseTitle.is("Quality").first.value.value = 'Best';
                        
                                // Click "Split Stems" button with retry mechanism
                                log("🔀 Attempting to click 'Split Stems' button...");
                        
                                // Click "Split Stems" Button on "Music Balance" module
                                izotopeRX.windows.whoseTitle.is("Music Rebalance").first.groups.whoseDescription.is("EffectPanel Music Rebalance")
                                    .first.groups.whoseDescription.is("EffectPanel Music Rebalance").first.groups.whoseDescription.is("EffectPanel Music Rebalance Footer Background")
                                    .first.groups.whoseDescription.is("Apply Group Container").first.buttons.whoseDescription.is("Split Stems").first.elementClick();
                        
                                // Wait for "Music Re-balance" to finish processing
                                izotopeRX.windows.first.getElement("AXTitleUIElement").elementWaitFor({
                                    waitType: "Disappear",
                                });
                        
                                //Brute forced a wait to ensure no problems re: "additional processing"  occur when exporting files
                                // sf.wait({
                                //     intervalMs: 800,
                                // });
                        
                                // CW - Use this to wait for processing or export to finished
                                while (!izotopeRX.getMenuItem('Edit', 'Select All').isEnabled) {
                                    sf.wait({ intervalMs: 200 });
                                }
                            }
                            // Handle Save Alert Function
                            function handleSaveAlert() {
                                try {
                                    izotopeRX.windows.whoseDescription.is("alert").first.elementWaitFor({
                                        waitType: "Appear",
                                        timeout: 1000
                                    });
                                    log("⚠️ Save alert appeared.");
                        
                                    izotopeRX.windows.whoseDescription.is("alert").first.buttons.whoseTitle.is("No").first.elementClick();
                                    log("🛑 Clicked 'No' on save alert.");
                                } catch (err) {
                                    log("✅ No save alert appeared, continuing...");
                                }
                            }
                        
                            // Handle Alert "additional processing unfinished" nuisance
                            function handleAlertWindow() {
                                try {
                                    izotopeRX.windows.whoseDescription.is("alert").first.elementWaitFor({
                                        waitType: "Appear",
                                        timeout: 1000
                                    });
                                } catch (err) {
                                    log("Alert window did not appear, continuing...");
                                }
                        
                                try {
                                    izotopeRX.windows.whoseDescription.is("alert").first.buttons.whoseTitle.is("Yes").first.elementClick();
                                } catch (err) {
                                    log("Yes button not found or not clickable, continuing...");
                                }
                            }
                        
                            // Export Stems Function
                            function exportStems(songTitle, tempo, exportFolder) {
                                log("💾 Exporting stems...");
                        
                                const fileNames = [
                                    { prefix: "00", name: "Original Recording" },
                                    { prefix: "01", name: "Vocals" },
                                    { prefix: "02", name: "Bass" },
                                    { prefix: "03", name: "Percussion" },
                                    { prefix: "04", name: "Other Instruments" },
                                ];
                        
                                const currentDate = getCurrentTimestamp();
                                log(`🗓️ Current date: ${currentDate}`);
                        
                                // Track whether it's the first export
                                let isFirstExport = true;
                        
                                fileNames.forEach((file, index) => {
                                    const exportFileName = `${file.prefix}_${file.name}_${songTitle}_${tempo}_${currentDate}.wav`;
                                    const exportPath = `${exportFolder}/${exportFileName}`;
                                    log(`🎙️ Processing file: ${file.name} (${index + 1}/${fileNames.length})`);
                                    log(`📂 Export path: ${exportPath}`);
                                    log("💾 Opening Export dialog...");
                        
                                    izotopeRX.menuClick({ menuPath: ["File", "Export..."] });
                        
                                    // Wait for the Export Module to Appear
                                    waitForExportPanelWithRetry();
                        
                                    function waitForExportPanelWithRetry(attempts = 3) {
                                        for (let i = 0; i < attempts; i++) {
                                            try {
                                                izotopeRX.mainWindow.groups.whoseDescription.is("ExportPanel Module Wrapper").first.elementWaitFor({ timeout: 3000 });
                                                log("✅ Export Panel detected.");
                                                return true;
                                            } catch (err) {
                                                log(`⚠️ Attempt ${i + 1}: Export Panel not found.`);
                                            }
                                        }
                                        log("⚠️ Export Panel did not appear after retries, continuing...");
                                        return false;
                                    }
                        
                                    // We want the original file to be at the original bit-depth
                                    if (!isFirstExport) {
                                        log("🔧 Adjusting export bit depth (not required for the first file)...");
                        
                                        // Select WAV File
                                        izotopeRX.mainWindow.buttons.whoseDescription.is("WAVButton").first.elementClick();
                        
                                        // Set Bit Depth to '24-bit'
                                        izotopeRX.mainWindow.popupButtons.whoseTitle.is("Bit depth").first.value.value = '24-bit';
                                    }
                        
                                    // Click "OK" in the Export Panel Window
                                    izotopeRX.mainWindow.groups.whoseDescription.is("ExportPanel Module Wrapper").first.groups.whoseDescription.is("ExportPanel Module Wrapper Footer Background")
                                        .first.buttons.whoseDescription.is("OK").first.elementClick({ asyncSwallow: true });
                        
                                    log("⏳ Waiting for RX 11 export UI to load...");
                        
                                    // Wait for "Save" button to be clickable, then Click "Save" Button, then wait for "Save" button to disappear
                                    sf.waitFor({
                                        callback: () => {
                                            return izotopeRX.windows.whoseTitle.is('Export File').first.buttons.whoseTitle.is("Save").first.isEnabled;
                                        },
                                        timeout: 1000
                                    }, `Failed waiting for Save to Be Enabled before Navigating to Folder`);
                        
                        
                                    log("📁 Navigating to folder and typing export path...");
                        
                                    // Using "Navigate To" utility function
                                    navigateTo(exportPath);
                        
                                    // Wait for "Save" button to be clickable, then Click "Save" Button, then wait for "Save" button to disappear
                                    sf.waitFor({
                                        callback: () => {
                                            return izotopeRX.windows.whoseTitle.is('Export File').first.buttons.whoseTitle.is("Save").first.isEnabled;
                                        },
                                        timeout: 1000
                                    }, `Failed waiting for Save to Be Enabled after Navigating to Folder`);
                        
                                    izotopeRX.windows.whoseTitle.is('Export File').first.buttons.whoseTitle.is("Save").first.elementClick();
                        
                                    izotopeRX.windows.whoseTitle.is('Export File').first.buttons.whoseTitle.is("Save").first.elementWaitFor({ waitType: "Disappear" });
                        
                                    // Wait for Export Panel to Disappear
                                    izotopeRX.mainWindow.groups.whoseDescription.is("ExportPanel Module Wrapper").first.elementWaitFor({ waitType: "Disappear" });
                        
                                    // Try and brute force a wait to see if it allows time for the export to occur and the close function to work.
                                    // sf.wait({
                                    //     intervalMs: 800,
                                    // });
                        
                                    // CW - Use this to wait for processing or export to finished
                                    while (!izotopeRX.getMenuItem('Edit', 'Select All').isEnabled) {
                                        sf.wait({ intervalMs: 200 });
                                    }
                        
                                    // Close the File 
                                    izotopeRX.menuClick({ menuPath: ['File', 'Close File'] });
                        
                                    handleSaveAlert();
                        
                                    handleAlertWindow();
                        
                                    log(`✅ Export completed for file: ${file.name}`);
                        
                                    // Mark the first export as complete after the first iteration
                                    isFirstExport = false;
                                });
                        
                                log("🎉 All stems exported successfully.");
                            }
                        }
                        
                        // Run the main script
                        main();