Hi all -
I've got a specific script I want to make. This is easy in Keyboard Maestro, but I don't know how to do it here as I can't figure out how to manipulate multiple bits of text into the clipboard.
Script function is: copy at Pro Tools TC in/out points to clipboard in the format: "06:04:06:08 - 06:04:06:16"
Use case: making a selection in PT, activate script, then paste TC range into a note or email.
Optional variant: add a dash-space to top, and two spaces to end of output format: "- 06:04:06:08 - 06:04:06:16 "
this allows for pasting into some notes formats which turns the item into a bullet point.
Pseudocode:
- activate Pro Tools
- type "/" , super tiny pause, then "cmd-C" (enter timecode in field, copy to clipboard)
- (?how?) store contents of clipboard to variable string
- type "//", super tiny pause, then "cmd-C" (enter timecode OUT field, copy to clipboard)
- (?how?) append " - " to variable string
- (?how?) append current clipboard to variable string
- (?how?) copy variable string to clipboard then discard variable string.
result: clipboard now has formatted text of Pro Tools in/out timecodes in it.
I've fiddled around for a good half hour and can't figure out anything that works.
decided to try in Keyboard maestro, and had it done in about six minutes.
thoughts?
- JJeremiah Moore @Jeremiah_Moore
screenshot of my KM script here: https://capture.dropbox.com/iMjNYRx8kCIHBQu1
- JJeremiah Moore @Jeremiah_Moore
hey - @Kitch @Christian_Scheuer3 - any thoughts on this?
Maybe it's necessary to use javascript to access variables? (or maybe there's a way to accomplish this without variables...)
Kitch Membery @Kitch2023-02-23 22:26:01.277Z
Hi @Jeremiah_Moore,
I'll take a look shortly. Much can be improved by using UI automation rather than Keyboard simulation for workflows like this.
Rock on!
- In reply toJeremiah_Moore⬆:Kitch Membery @Kitch2023-02-23 22:40:04.493Z
Hi @Jeremiah_Moore,
This should do the trick
//Activate Pro Tools Main Window - Optional Step sf.ui.proTools.appActivateMainWindow(); //Get the selections in & out points. const selection = sf.ui.proTools.selectionGet(); //Format the Timecode in and out string " - " + Timecode in point + " - " + Timecode out point + " " let timecodeInAndOut = ` - ${selection.selectionStart} - ${selection.selectionEnd} `; //Store the Timecode in and out string into the clipboard. sf.clipboard.setText({ text: timecodeInAndOut, });
The great thing about this script is that it requires no Keyboard simulation and does things in the background. Just run the script and the Timecode information will be in your clipboard.
Note: This script assumes your main counter is set to Timecode already.
I hope that helps. :-)
- JJeremiah Moore @Jeremiah_Moore
Fantastic! Yes precisely. Thx @Kitch
Obviously Javascript is where the power tools are... pretty much exactly what I wanted to do (could almost make a person want to learn a little javascript)
(It's got me thinking about one more thing I'd like to accomplish, to do with selecting a clip or section of clips, and moving the selection up or down the tracks using keys.)
-jeremiah
- In reply toKitch⬆:Ddanielkassulke @danielkassulke
Hi Kitch!
Quick Q: I'm hoping to be able to automate the calculation of HW insert delays with this, among other things. The value will obviously always be in milliseconds - is there a way to capture the timecode selection but ignore (i.e. not copy) the HH:MM component of that value when copying to the clipboard?
Kitch Membery @Kitch2023-04-19 07:12:58.615Z
Hi @danielkassulke,
So are you trying to get the length of a selection in samples?
- Ddanielkassulke @danielkassulke
Hi Kitch,
No - at least I don't think so. I've modified the above script to get value in H:MM:SS already. The HW delay value [Setup > I/O > H/W Insert Delay] is in milliseconds. The issue I'm running into is that the main counter's length value and the H/W insert delay value - while both in milliseconds - are displayed differently if you paste from the clipboard, so I'm unsure how to reconcile that difference. The H/W insert delay value shows 0.00 in ms.
Kitch Membery @Kitch2023-04-19 07:38:00.419Z
I think I understand, so what you need to get a selection length in 0.00 ms format?
- Ddanielkassulke @danielkassulke
exactly!
Kitch Membery @Kitch2023-04-19 07:43:28.925Z
Like this...
function main() { // Get the selection const selection = sf.ui.proTools.selectionGet(); // Get the selection length then remove HH:MM and convert to S.MS format const selectionLength = selection.selectionLength.split(":").slice(2).join("."); //Store the Timecode in and out string into the clipboard sf.clipboard.setText({ text: selectionLength, }); } sf.ui.proTools.mainCounterDoWithValue({ targetValue: "Timecode", action: main });
UPDATED
Let me know if that does the trick :-)
- Ddanielkassulke @danielkassulke
It does most of the trick! I thought there might have been a PT rounding error, but I suspect there's something funky going on at line 6, based on your comment. It should actually be a value showing exclusively milliseconds. Sorry for the lack of clarity
Kitch Membery @Kitch2023-04-19 08:03:21.764Z
Hi @danielkassulke,
To clarify... Are you trying to get the selection "length" as a value in milliseconds only?
- Ddanielkassulke @danielkassulke
yup - a milliseconds-only value where 1.50 would be 1 1/2 milliseconds
Kitch Membery @Kitch2023-04-19 08:20:41.847Z
I think I'm confused again...
So, if the timecode length reads "00:00:02:12" like this...
Do you want "02.00" copied to the clipboard?
- Ddanielkassulke @danielkassulke
I'm confused too!
Somehow we're looking at different length settings, but in my attached screenshot 0:00:010 is 10 milliseconds.
So copying the value in my screenshot, the clipboard should read: 10.00
Kitch Membery @Kitch2023-04-19 08:41:30.955Z
Great that makes way more sense. Stand by!
- In reply todanielkassulke⬆:
Kitch Membery @Kitch2023-04-19 08:45:01.708Z
I believe this is what you're after.
function main() { const selectionLength = sf.ui.proTools.selectionGet().selectionLength; const [minutes, seconds, milliseconds] = selectionLength.split(/:|\./); const totalMilliseconds = (Number(milliseconds) + (Number(seconds) * 1000) + (Number(minutes) * 60 * 1000)).toFixed(2) sf.clipboard.setText({ text: totalMilliseconds.toString(), }); } sf.ui.proTools.mainCounterDoWithValue({ targetValue: "Min:Secs", action: main });
- Ddanielkassulke @danielkassulke
Sorry for totally derailing this post, Kitch - let me know if you want me to start a new thread for this. I'm not sure what's going on with the code, but here's a screen recording:
It's hard to pinpoint exactly what's happening because the text output of the totalMilliseconds string is 0.00. Yesterday you were most of the way there i think, but I think the decimal places were rounding the ms values up/down.
- In reply todanielkassulke⬆:
Kitch Membery @Kitch2023-04-20 08:27:10.828Z
I'll take a look at this in the morning. :-)
- In reply todanielkassulke⬆:
Kitch Membery @Kitch2023-04-20 08:46:02.515Z
@danielkassulke, One last try before I head to bed. Hopefully this is it :-)
function timeToMs(timeStr) { const [minutes, secondsMs] = timeStr.split(":"); const [seconds, milliseconds] = secondsMs.split("."); const totalMs = (+minutes * 60 + +seconds) * 1000 + +milliseconds; return totalMs; } function main() { const selectionLength = sf.ui.proTools.selectionGet().selectionLength; let totalMilliseconds = timeToMs(selectionLength).toFixed(2); log(totalMilliseconds); sf.clipboard.setText({ text: totalMilliseconds.toString(), }); } sf.ui.proTools.mainCounterDoWithValue({ targetValue: "Min:Secs", action: main });
Note: line 12 is for logging purposes only. Feel free to comment it out, or remove it once you've tested it.
Be sure to create a new script and paste this code in and run it without any other code to check that it works. I it looks like you have other code running in the video you made.
- In reply toKitch⬆:OOwen Granich-Young @Owen_Granich_Young
I knew someone would have this in the forum somewhere! Sweetened it a little, if you don't have a range selected it will only add the single TC into your clipboard not the range. Also if you are in another app when you run this script it pops you back to it after running. Nice when trying to copy TC into emails.
function copyProToolsSelectionTimecode() { // Save the frontmost app's bundle ID const frontmostAppBundleId = sf.ui.frontmostApp.activeBundleID; // Activate Pro Tools main window sf.ui.proTools.appActivateMainWindow(); // Get the selections in & out points const selection = sf.ui.proTools.selectionGet(); const start = selection.selectionStart; const end = selection.selectionEnd; // Format the Timecode string const timecodeInAndOut = (start === end) ? start : `${start} - ${end}`; // Store the Timecode string into the clipboard sf.clipboard.setText({ text: timecodeInAndOut, }); // Restore the previously frontmost app sf.ui.app(frontmostAppBundleId).appActivate(); } copyProToolsSelectionTimecode();
- OOwen Granich-Young @Owen_Granich_Young
@Jeremiah_Moore asked me offline if we could do the inverse, highlight a TC in an email or whatnot and then press button to set your protools selection:
function setProToolsSelectionFromClipboard(defaultHour = "01") { // Copy current selection sf.keyboard.press({ keys: "cmd+c", }); // Wait for clipboard update sf.wait({ intervalMs: 75 }); // Get clipboard text const clipboardText = sf.clipboard.getText().text.trim(); // Regex split: dash with optional spaces around it let start, end; const parts = clipboardText.split(/\s*-\s*/); if (parts.length === 2) { [start, end] = parts; } else { start = end = clipboardText; // Single timecode case } // Pad and normalize timecode function normalizeTimecode(tc) { const tcParts = tc.split(":").map(p => p.trim()); let hh, mm, ss, ff; if (tcParts.length === 2) { // mm:ss → defaultHour:mm:ss:00 [mm, ss] = tcParts; hh = defaultHour; ff = "00"; } else if (tcParts.length === 3) { // hh:mm:ss → hh:mm:ss:00 [hh, mm, ss] = tcParts; ff = "00"; } else if (tcParts.length === 4) { // hh:mm:ss:ff → already full [hh, mm, ss, ff] = tcParts; } else { // Unexpected format, return as-is return tc; } // Zero-pad everything hh = hh.padStart(2, "0"); mm = mm.padStart(2, "0"); ss = ss.padStart(2, "0"); ff = ff.padStart(2, "0"); return `${hh}:${mm}:${ss}:${ff}`; } start = normalizeTimecode(start); end = normalizeTimecode(end); // Set Pro Tools timeline selection sf.app.proTools.setTimelineSelection({ inTime: start, outTime: end }); } // Run it (default hour 01) setProToolsSelectionFromClipboard(); sf.ui.proTools.appActivateMainWindow();
Handles both full TC's and min and seconds only. Pretty cool! Turn off code line 68 if you don't want Protools to be focused when you run the script
- OOwen Granich-Young @Owen_Granich_Young
One more version combines the 2 scripts together. Press normally to copy range to clipboard. Hold CMD and press to select range or TC of Highlighted text or table Cell.
function copyProToolsSelectionTimecode() { // Save the frontmost app's bundle ID const frontmostAppBundleId = sf.ui.frontmostApp.activeBundleID; // Activate Pro Tools main window sf.ui.proTools.appActivateMainWindow(); // Get the selections in & out points const selection = sf.ui.proTools.selectionGet(); const start = selection.selectionStart; const end = selection.selectionEnd; // Format the Timecode string const timecodeInAndOut = (start === end) ? start : `${start} - ${end}`; // Store the Timecode string into the clipboard sf.clipboard.setText({ text: timecodeInAndOut, }); // Restore the previously frontmost app sf.ui.app(frontmostAppBundleId).appActivate(); } function setProToolsSelectionFromClipboard(defaultHour = "01") { // Copy current selection sf.keyboard.press({ keys: "cmd+c" }); // Wait for clipboard update sf.wait({ intervalMs: 75 }); // Get clipboard text const clipboardText = sf.clipboard.getText().text.trim(); // Regex split: dash with optional spaces around it let start, end; const parts = clipboardText.split(/\s*-\s*/); if (parts.length === 2) { [start, end] = parts; } else { start = end = clipboardText; // Single timecode case } // Get current session frame rate const fpsSetting = sf.app.proTools.getSessionTimeCodeRate().CurrentSetting; // Lookup numeric FPS const fpsMap = { "Fps23976": 24, "Fps24": 24, "Fps25": 25, "Fps2997": 30, "Fps2997Drop": 30, "Fps30": 30, "Fps30Drop": 30, "Fps47952": 48, "Fps48": 48, "Fps50": 50, "Fps5994": 60, "Fps5994Drop": 60, "Fps60": 60, "Fps60Drop": 60, "Fps100": 100, "Fps11988": 120, "Fps11988Drop": 120, "Fps120": 120, "Fps120Drop": 120 }; const fps = fpsMap[fpsSetting] || 30; // fallback 30fps // Pad and normalize timecode function normalizeTimecode(tc) { const tcParts = tc.split(":").map(p => p.trim()); let hh, mm, ss, ff; if (tcParts.length === 2) { // mm:ss → defaultHour:mm:ss:00 [mm, ss] = tcParts; hh = defaultHour; ff = "00"; } else if (tcParts.length === 3) { // hh:mm:ss → hh:mm:ss:00 [hh, mm, ss] = tcParts; ff = "00"; } else if (tcParts.length === 4) { // hh:mm:ss:ff → already full [hh, mm, ss, ff] = tcParts; } else { return tc; // Unexpected format } // Zero-pad hours, minutes, seconds hh = hh.padStart(2, "0"); mm = mm.padStart(2, "0"); ss = ss.padStart(2, "0"); // Clamp frames ff = parseInt(ff, 10); if (isNaN(ff) || ff < 0) ff = 0; if (ff >= fps) ff = fps - 1; ff = ff.toString().padStart(2, "0"); return `${hh}:${mm}:${ss}:${ff}`; } start = normalizeTimecode(start); end = normalizeTimecode(end); // Set Pro Tools timeline selection sf.app.proTools.setTimelineSelection({ inTime: start, outTime: end }); } // Dispatcher: choose function based on modifier key const modifierState = event.keyboardState.asString; switch (modifierState) { case "": copyProToolsSelectionTimecode(); break; case "cmd": setProToolsSelectionFromClipboard(); break; }
Had AI make a cute Icon for it too :P
- OOwen Granich-Young @Owen_Granich_Young
Ok... Last and best version because I became annoyed today. NOW hold CMD+Button to switch between modes (Copy or Set), it will store current mode as a .json until you change back to the other mode. And then normal button press will trigger whichever mode you're in.
// Paths const jsonSavePath = sf.soundflow.thisPackage.getDirectory().path + '/savedSettings.json'; // Default settings if file doesn't exist yet function loadSettings() { if (sf.file.exists({ path: jsonSavePath }).exists) { return sf.file.readJson({ path: jsonSavePath }).json; } return { mode: "copy" }; // default mode } function saveSettings(settings) { let success = sf.file.writeJson({ path: jsonSavePath, json: settings }).success; if (!success) throw `Error saving settings`; } // === SCRIPT 1 === function copyProToolsSelectionTimecode() { const frontmostAppBundleId = sf.ui.frontmostApp.activeBundleID; sf.ui.proTools.appActivateMainWindow(); const selection = sf.ui.proTools.selectionGet(); const start = selection.selectionStart; const end = selection.selectionEnd; const timecodeInAndOut = (start === end) ? start : `${start} - ${end}`; sf.clipboard.setText({ text: timecodeInAndOut }); sf.ui.app(frontmostAppBundleId).appActivate(); } // === SCRIPT 2 === function setProToolsSelectionFromClipboard(defaultHour = "01") { sf.keyboard.press({ keys: "cmd+c" }); sf.wait({ intervalMs: 75 }); const clipboardText = sf.clipboard.getText().text.trim(); let start, end; const parts = clipboardText.split(/\s*-\s*/); if (parts.length === 2) { [start, end] = parts; } else { start = end = clipboardText; } const fpsSetting = sf.app.proTools.getSessionTimeCodeRate().currentSetting; const fpsMap = { "Fps23976": 24, "Fps24": 24, "Fps25": 25, "Fps2997": 30, "Fps2997Drop": 30, "Fps30": 30, "Fps30Drop": 30, "Fps47952": 48, "Fps48": 48, "Fps50": 50, "Fps5994": 60, "Fps5994Drop": 60, "Fps60": 60, "Fps60Drop": 60, "Fps100": 100, "Fps11988": 120, "Fps11988Drop": 120, "Fps120": 120, "Fps120Drop": 120 }; const fps = fpsMap[fpsSetting] || 30; function normalizeTimecode(tc) { const tcParts = tc.split(":").map(p => p.trim()); let hh, mm, ss, ff; if (tcParts.length === 2) { [mm, ss] = tcParts; hh = defaultHour; ff = "00"; } else if (tcParts.length === 3) { [hh, mm, ss] = tcParts; ff = "00"; } else if (tcParts.length === 4) { [hh, mm, ss, ff] = tcParts; } else { return tc; } hh = hh.padStart(2, "0"); mm = mm.padStart(2, "0"); ss = ss.padStart(2, "0"); ff = parseInt(ff, 10); if (isNaN(ff) || ff < 0) ff = 0; if (ff >= fps) ff = fps - 1; ff = ff.toString().padStart(2, "0"); return `${hh}:${mm}:${ss}:${ff}`; } start = normalizeTimecode(start); end = normalizeTimecode(end); sf.app.proTools.setTimelineSelection({ inTime: start, outTime: end }); sf.ui.proTools.appActivateMainWindow(); sf.keyboard.press({ keys: "right", repetitions: 2, fast: true }); } // === MAIN DISPATCHER === const settings = loadSettings(); const modifierState = event.keyboardState.asString; if (modifierState === "cmd") { // Toggle mode on Cmd press const newMode = (settings.mode === "copy") ? "set" : "copy"; saveSettings({ mode: newMode }); log(`🔀 Mode switched to: ${newMode}`); } else { // Run current mode if (settings.mode === "copy") { copyProToolsSelectionTimecode(); } else { setProToolsSelectionFromClipboard(); } }
Really I'm done now.
Curtis Macdonald @Curtis_Macdonald
This is so useful! Thank you for sharing.
- TIn reply toJeremiah_Moore⬆:T Francis @T_Francis
Hi there! I've modified this script to create a version which just copies the current TC point into the clipboard:
//Activate Pro Tools Main Window - Optional Step sf.ui.proTools.appActivateMainWindow(); //Get the selections in & out points. const selection = sf.ui.proTools.selectionGet(); //Format the Timecode string let timecodeInAndOut = `${selection.selectionStart}`; //Store the Timecode string into the clipboard. sf.clipboard.setText({ text: timecodeInAndOut, });
It spits out a perfect 00:00:00:00 string. Is there a way of changing the format to 00.00.00.00 for a different document?
Many Thanks!
Trystan
- Ddanielkassulke @danielkassulke
Try this:
sf.ui.proTools.appActivateMainWindow(); //Get the selections in & out points. const selection = sf.ui.proTools.selectionGet(); //Format the Timecode string let timecodeInAndOut = `${selection.selectionStart}`; // Replace : with . timecodeInAndOut = timecodeInAndOut.replace(/:/g, '.'); //Store the Timecode string into the clipboard. sf.clipboard.setText({ text: timecodeInAndOut, });
- TT Francis @T_Francis
Awesome, thankyou :)
- In reply todanielkassulke⬆:TT Francis @T_Francis
I've managed to sort this into a great script which copies the TC and Cue Name to the Clipboard and has saved loads of time!
sf.ui.proTools.mainWindow.invalidate(); `` //Change Main Counter to Timecode sf.ui.proTools.menuClick({ menuPath: ['View', 'Main Counter', 'Timecode'] }); //Populate Text String const sessionPath = sf.ui.proTools.mainWindow.sessionPath; const sessionName = sessionPath.split("/").slice(-1)[0].split(".ptx")[0] const selection = sf.ui.proTools.selectionGet(); //Format the Timecode string let currentTC = selection.selectionStart.split(':').join('.'); //Construct Clipboard const PopulateClipboard = `${sessionName} ${currentTC}` //Populate Clipboard sf.clipboard.setText({ text: PopulateClipboard });
I have a question - is there any way to make the ruler switching conditional?
It'd be great if, when in Bars|Beats, the ruler switches to Timecode in order to populate the Clipboard, then switches BACK to Bars|Beats; however if already in Timecode view, it stays there once the clipboard is populated.
Do you think this is possible?
Continued thanks for your help!!
Trystan