RX 11 Progress Bar UI element BUG - Automating "Music Rebalance" Stem Splitting from Finder
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();
- 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();
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]
- JJake Morton @Jake_Morton
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.
- In reply toChad⬆:JJake Morton @Jake_Morton
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();
Chad Wahlbrink @Chad2025-04-22 15:56:04.853Z