Cascade Audio In Mastering Application
Prepping mastering sessions in Pro Tools is quite frankly, annoying. After seeing the potential in the SDK from using it yesterday, I was wondering why this functionality isn't already built in. The screen recording makes it pretty apparent what it's doing but essentially it's allowing each song to be proceeded by the next song with markers. This makes printing masters incredibly easy and navigating the mastering session equally so. I was imagining it would look something like this:
sf.app.proTools.cascadeTracks({
numberTracks: true, // Adds numbering to track names
gaplessTransitions: false,
startPadding: 80ms,
endPadding: 120ms,
startMarker: trackName, // Accounts for padding
endMarker: clipName, // Accounts for padding
});
Below is the script I wrote and the screen recording, let me know your thoughts!
function returnToZero() {
const trasnport = sf.ui.proTools.mainWindow.transportViewCluster.groups.whoseTitle.is("Normal Transport Buttons");
trasnport.first.buttons.whoseTitle.is("Return to Zero").first.elementClick();
}
////////////////////////////////////////////////////////////////////////////////////////////////////
function getOriginalCounter() {
let originalCounter;
if (sf.ui.proTools.getMenuItem('View', 'Main Counter', 'Bars|Beats').isMenuChecked) {
originalCounter = ['View', 'Main Counter', 'Bars|Beats'];
}
if (sf.ui.proTools.getMenuItem('View', 'Main Counter', 'Min:Secs').isMenuChecked) {
originalCounter = ['View', 'Main Counter', 'Min:Secs'];
}
if (sf.ui.proTools.getMenuItem('View', 'Main Counter', 'Timecode').isMenuChecked) {
originalCounter = ['View', 'Main Counter', 'Timecode'];
}
if (sf.ui.proTools.getMenuItem('View', 'Main Counter', 'Feet+Frames').isMenuChecked) {
originalCounter = ['View', 'Main Counter', 'Feet+Frames'];
}
if (sf.ui.proTools.getMenuItem('View', 'Main Counter', 'Samples').isMenuChecked) {
originalCounter = ['View', 'Main Counter', 'Samples'];
}
return originalCounter;
}
////////////////////////////////////////////////////////////////////////////////////////////////////
function getOriginalEditMode() {
let editMode;
const slipModeButton = sf.ui.proTools.mainWindow.editModeCluster.buttons.whoseTitle.is("EditModeSlip").first;
const gridModeButton = sf.ui.proTools.mainWindow.editModeCluster.buttons.whoseTitle.is("Absolute Grid mode").first;
const shuffleModeButton = sf.ui.proTools.mainWindow.editModeCluster.buttons.whoseTitle.is("EditModeShuffle").first;
const spotModeButton = sf.ui.proTools.mainWindow.editModeCluster.buttons.whoseTitle.is("EditModeSpot").first;
if (slipModeButton.value.invalidate().value === 'Selected') {
editMode = 'Slip';
}
if (gridModeButton.value.invalidate().value === 'Selected') {
editMode = 'Grid';
}
if (shuffleModeButton.value.invalidate().value === 'Selected') {
editMode = 'Shuffle';
}
if (spotModeButton.value.invalidate().value === 'Selected') {
editMode = 'Spot';
}
return editMode;
}
////////////////////////////////////////////////////////////////////////////////////////////////////
function batchRename() {
sf.ui.proTools.selectedTrack.titleButton.popupMenuSelect({
isRightClick: true,
relativePosition: { x: 5, y: 5 },
menuPath: ['Batch Rename...']
});
const batchRenameWin = sf.ui.proTools.windows.whoseTitle.is('Batch Track Rename').first;
batchRenameWin.elementWaitFor();
batchRenameWin.checkBoxes.whoseTitle.is('Replace').first.checkboxSet({
targetValue: 'Disable',
});
batchRenameWin.checkBoxes.whoseTitle.is('Trim').first.checkboxSet({
targetValue: 'Disable',
});
batchRenameWin.checkBoxes.whoseTitle.is('Add').first.checkboxSet({
targetValue: 'Disable',
});
batchRenameWin.checkBoxes.whoseTitle.is('Numbering').first.checkboxSet({
targetValue: 'Enable',
});
batchRenameWin.popupButtons.first.popupMenuSelect({
menuPath: ['Beginning'],
});
// Starting Number
batchRenameWin.textFields.allItems[11].elementSetTextFieldWithAreaValue({
useMouseKeyboard: true,
value: '1'
});
// Number of Places
batchRenameWin.textFields.allItems[12].elementSetTextFieldWithAreaValue({
useMouseKeyboard: true,
value: '1'
});
// Increment
batchRenameWin.textFields.allItems[13].elementSetTextFieldWithAreaValue({
useMouseKeyboard: true,
value: '1'
});
batchRenameWin.checkBoxes.whoseTitle.is('Separate With:').first.checkboxSet({
targetValue: 'Enable',
});
batchRenameWin.buttons.whoseTitle.is('OK').first.elementClick();
}
////////////////////////////////////////////////////////////////////////////////////////////////////
function randomTrackColor() {
const color = ((Math.random() * 22) + 1).toFixed(0);
const brightnessList = ['Light', 'Medium', 'Dark'];
const brightness = brightnessList[(Math.random() * 2).toFixed(0)];
sf.ui.proTools.colorsSelect({
colorTarget: 'Tracks',
colorNumber: Number(color),
colorBrightness: brightness
});
}
////////////////////////////////////////////////////////////////////////////////////////////////////
function spotClip(start) {
const spotModeButton = sf.ui.proTools.mainWindow.editModeCluster.buttons.whoseTitle.is("EditModeSpot").first;
if (!spotModeButton.isFocused) {
sf.ui.proTools.editModeSet({
mode: 'Spot'
});
}
returnToZero();
// Move Edit Selection to Next Clip
sf.keyboard.press({
keys: 'control+tab'
});
// Zoom to Fit Horizontal Selection
sf.keyboard.press({
keys: 'control+alt+f'
});
sf.ui.proTools.toolsSelect({
tool: "Grabber",
});
const selectedTrack = sf.ui.proTools.selectedTrack.frame;
sf.mouse.setPosition({
position: { x: selectedTrack.x + 600, y: selectedTrack.y + (selectedTrack.h / 2) }
});
sf.mouse.click({
position: { x: selectedTrack.x + 600, y: selectedTrack.y + (selectedTrack.h / 2) }
});
const spotDialog = sf.ui.proTools.clipSpotDialog;
spotDialog.elementWaitFor();
spotDialog.textFields.first.elementSetTextFieldWithAreaValue({
useMouseKeyboard: true,
value: `${start}`
});
spotDialog.buttons.whoseTitle.is('OK').first.elementClick();
}
////////////////////////////////////////////////////////////////////////////////////////////////////
function createMarker(selectedTrackName) {
const match = selectedTrackName.match(/^(\d+)[\s_](.+)$/);
const markerName = match[2];
const markerNumber = Number(match[1]);
sf.ui.proTools.memoryLocationsCreate({
name: markerName,
memoryLocationNumber: markerNumber,
});
}
////////////////////////////////////////////////////////////////////////////////////////////////////
function getPreviousTrackEnd() {
returnToZero();
sf.ui.proTools.menuClick({
menuPath: ['Edit', 'Selection', 'Extend Edit Up']
});
sf.ui.proTools.menuClick({
menuPath: ['Edit', 'Selection', 'Remove Edit from Bottom']
});
// Move Edit Selection to Next Clip
sf.keyboard.press({
keys: 'control+tab'
});
const previousTrackEnd = sf.ui.proTools.selectionGetInSamples().selectionEnd;
return previousTrackEnd
}
////////////////////////////////////////////////////////////////////////////////////////////////////
function restore(originalCounter, editMode) {
sf.ui.proTools.menuClick({
menuPath: ['Edit', 'Select All']
});
// Zoom to Fit Horizontal Selection
sf.keyboard.press({
keys: 'control+alt+f'
});
sf.ui.proTools.trackDeselectAll();
returnToZero();
sf.ui.proTools.toolsSelect({
tool: "Smart",
});
sf.ui.proTools.menuClick({
menuPath: originalCounter
});
sf.ui.proTools.editModeSet({
mode: editMode
});
}
////////////////////////////////////////////////////////////////////////////////////////////////////
function main() {
sf.ui.proTools.appActivateMainWindow();
sf.ui.proTools.mainWindow.invalidate();
const originalCounter = getOriginalCounter();
const editMode = getOriginalEditMode();
if (!sf.ui.proTools.getMenuItem('View', 'Main Counter', 'Samples').isMenuChecked) {
sf.ui.proTools.menuClick({
menuPath: ['View', 'Main Counter', 'Samples']
});
}
batchRename();
sf.ui.proTools.selectedTrackNames.forEach((trackName, trackNameIndex) => {
sf.ui.proTools.trackSelectByName({ names: [trackName]});
sf.ui.proTools.selectedTrack.trackScrollToView();
randomTrackColor();
// IF FIRST TRACK JUST CREATE MARKER
if (trackNameIndex === 0) {
createMarker(trackName);
return
} else {
let previousTrackEnd = getPreviousTrackEnd();
sf.ui.proTools.trackSelectByName({
names: [trackName]
});
spotClip(previousTrackEnd);
createMarker(trackName);
returnToZero();
}
});
restore(originalCounter, editMode);
}
main();
- In reply tonathansalefski⬆:Nathan Salefski @nathansalefski
No related to the SDK per se but an entire new Mastering Track Type, with assignable pre and post fader inserts would be amazing for mastering. It would also allow the clip on the track to be used as the reference by some sort of toggle
- In reply tonathansalefski⬆:Dustin Harris @Dustin_Harris
I haven't tested your code, but you're closer than you think to having the example you listed at the top. If you pass those parameters as an object (curly braces) with default assignments, you can then use the parameters provided, like this:
function nathanSafalskiCascadeTracks({ numberTracks = true, gaplessTransitions = false, startPaddingInMs = 0, endPaddingInMs = 0, startMarker = 'trackName', // Accounts for padding endMarker = 'clipName', // Accounts for padding }) { /* the rest of you main() function code goes here, but you use the input parameters to decide how the script behaves */ //example if (numberTracks == true) { batchRename(); } //another example if (startMarker == "trackName") { createMarker(trackName); } else if (startMarker == "somethingElse") { createMarker(somethingElse) } }
and then when you call the function, the parameters pop up, just like the SoundFlow ones:
Dustin Harris @Dustin_Harris
you would need to add some code to convert the ms values to samples:
/**@param {number} ms - duration in milliseconds */ function msToSamples(ms) { const sampleRate = Number(sf.app.proTools.getSessionSampleRate().sampleRate.replace("SR", "")) //round to the closest sample return Math.round(ms/1000 * sampleRate) } const samplesToPad = msToSamples(117) log(samplesToPad)
- In reply toDustin_Harris⬆:
Nathan Salefski @nathansalefski
Oh that makes a lot of sense. I remember @Chad mentioned this to me before but it's just now clicking. That's awesome! That would be really helpful because I was considering switching to WaveLab for this functionality. This might even work a little more smoothly if converted into a template. Thanks man!
Dustin Harris @Dustin_Harris
and then you can use the gaplessTransitions parameter to decide if the padding is necessary, like this:
const padInSamples = msToSamples(startPaddingInMs) //ignore pad if gaplessTransitions is set to true const nextSongStartInSamples = (gaplessTransitions == true) ? previousTrackEnd : previousTrackEnd + padInSamples spotClip(nextSongStartInSamples)
Nathan Salefski @nathansalefski
Incredible. I don't exactly know the what this is called where you use a ?. Like a conditional variable
const nextSongStartInSamples = (gaplessTransitions == true) ? previousTrackEnd : previousTrackEnd + padInSamples
, but that seems very handy in other instancesDustin Harris @Dustin_Harris
that's a code shortcut called a ternary operator:
const nextSongStartInSamples = (gaplessTransitions == true) ? previousTrackEnd : previousTrackEnd + padInSamples
is the same as this:
let nextSongStartInSamples = previousTrackEnd; if (gaplessTransitions == false) { nextSongStartInSamples = previousTrackEnd + padInSamples }
- In reply toDustin_Harris⬆:
Dustin Harris @Dustin_Harris
and just for the sake of fun and learning, you can simplify your getOriginalEditMode and restore functions like this:
function getOriginalEditMode() { const modeButtons = sf.ui.proTools.mainWindow.editModeCluster.buttons const selectedButton = modeButtons.filter(btn => btn.value.invalidate().value == "Selected")[0] return selectedButton.title.value } const originalEditMode = getOriginalEditMode(); function restoreEditMode(modeButtonTitle) { sf.ui.proTools.mainWindow.editModeCluster.buttons.whoseTitle.is(modeButtonTitle).first.elementClick(); }
array methods like .filter and .map are really powerful and will kick your code up a notch, and make it faster to write :)
Dustin Harris @Dustin_Harris
and to make the default assignments clearer:
function thisIsHowDefaultAssignmentsWork({ firstName = "Dustin", lastName = "Harris" }) { log(firstName + " " + lastName) } //returns "Dustin Harris" thisIsHowDefaultAssignmentsWork({}) //returns "Nathan Salefski" thisIsHowDefaultAssignmentsWork({firstName: "Nathan", lastName: "Salefski"}) //returns "Dustin Salefski" thisIsHowDefaultAssignmentsWork({lastName: "Salefski"})
Dustin Harris @Dustin_Harris
AND just because you'll see it elsewhere on the forum:
//this if (gaplessTransitions) { //..doSomething } //is the same as this if (gaplessTransitions == true) { //..doSomething } //and this if (!gaplessTransitions) { //..doAnotherThing } //is the same as if (gaplessTransitions == false) { //..doAnotherThing }
Nathan Salefski @nathansalefski
I do at least know those lol. Than you man. I really appreciate the information because I'm constantly learning. I have absolutely no coding background but programming SoundFlow has been incredibly fun and challenging. I really appreciate it!
Dustin Harris @Dustin_Harris
I started the exact same way :) Just keep writing scripts, reading the forum, and asking questions and osmosis will do its thing! There are awesome free JavaScript tutorials on the web too
- In reply toDustin_Harris⬆:
Nathan Salefski @nathansalefski
How can I make an each parameter have some predefined options to choose from? For example when you do something like
.checkboxSet()
you havetargetValue
as a parameter but you can then see options fortargetValue
like"Enable"
and"Disable"
Dustin Harris @Dustin_Harris
do you that by documenting the parameters with something called JSDOC, like this:
/** * This function demonstrates how default assignments work in JavaScript. * If no parameters are provided, it uses default values for firstName and lastName. * @param {Object} options - An object containing optional parameters. * @param {"Dustin" | "Nathan"} [options.firstName="Dustin"] - The first name. Defaults to "Dustin" if not provided. * @param {"Harris" | "Salefski"} [options.lastName="Harris"] - The last name. Defaults to "Harris" if not provided. */ function thisIsHowDefaultAssignmentsWork({ firstName = "Dustin", lastName = "Harris" }) { log(firstName + " " + lastName); } thisIsHowDefaultAssignmentsWork({})
You can see from the code above that I've limited the firstName and lastName to be my name OR your name (using the "|" character), so now they are the only two suggestions:
Nathan Salefski @nathansalefski
Amazing thank you!