Hi there! This is a no code post, at least as of yet. I'd like to be able to copy my edits (cuts and fades), from one track to another. I expect I'd use this a lot when cutting backgrounds and I'm asked to add another layer.
Here's what the situation would look like:

And here's one of the empty spaces zoomed in so you can see the fades:

What I usually do is click into all of the silences and break the long region in the middle of each, then go to the beginning, top track, and press:
' ; a p ' ; d p ' ' ; s p l ; g p '
Thoughts?
Thanks!
Linked from:
- Ryan DeRemer @Ryan_DeRemer
Hey Nick! Funny, I went to high school with a guy with the same name!
The script below should do what you're looking for. It can also be accomplished in a macro by using Press Keys, Wait, and Click Menu Item actions. You can use the specifics in from the code in the macro actions. As far as I know there's no way for SF to know what's in the actual timeline, but using PT's menus to navigate and do the edits will work for this purpose. Hope this helps!
sf.ui.proTools.appActivateMainWindow(); sf.keyboard.press({ keys: `l` }); sf.wait({ intervalMs: 50 }); sf.ui.proTools.menuClick({ menuPath: ['Edit', 'Selection', 'Extend Edit Down'] }); sf.ui.proTools.menuClick({ menuPath: ['Edit', 'Selection', 'Remove Edit from Top'] }); sf.ui.proTools.menuClick({ menuPath: ['Edit', 'Separate Clip', 'At Selection'] }); sf.ui.proTools.menuClick({ menuPath: ['Edit', 'Selection', 'Extend Edit Up'] }); sf.ui.proTools.menuClick({ menuPath: ['Edit', 'Selection', 'Remove Edit from Bottom'] }); sf.wait({ intervalMs: 50 }); sf.keyboard.press({ keys: `l` }); sf.wait({ intervalMs: 50 }); sf.ui.proTools.menuClick({ menuPath: ['Edit', 'Selection', 'Extend Edit Down'] }); sf.ui.proTools.menuClick({ menuPath: ['Edit', 'Selection', 'Remove Edit from Top'] }); sf.ui.proTools.menuClick({ menuPath: ['Edit', 'Fades', 'Fade to End'] }); sf.ui.proTools.menuClick({ menuPath: ['Edit', 'Selection', 'Extend Edit Up'] }); sf.ui.proTools.menuClick({ menuPath: ['Edit', 'Selection', 'Remove Edit from Bottom'] }); sf.wait({ intervalMs: 50 }); sf.keyboard.press({ keys: `tab`, repetitions: 2 }); sf.wait({ intervalMs: 50 }); sf.ui.proTools.menuClick({ menuPath: ['Edit', 'Selection', 'Extend Edit Down'] }); sf.ui.proTools.menuClick({ menuPath: ['Edit', 'Selection', 'Remove Edit from Top'] }); sf.ui.proTools.menuClick({ menuPath: ['Edit', 'Trim Clip', 'Start to Insertion'] }); sf.ui.proTools.menuClick({ menuPath: ['Edit', 'Selection', 'Extend Edit Up'] }); sf.ui.proTools.menuClick({ menuPath: ['Edit', 'Selection', 'Remove Edit from Bottom'] }); sf.wait({ intervalMs: 50 }); sf.keyboard.press({ keys: `tab` }); sf.wait({ intervalMs: 50 }); sf.ui.proTools.menuClick({ menuPath: ['Edit', 'Selection', 'Extend Edit Down'] }); sf.ui.proTools.menuClick({ menuPath: ['Edit', 'Selection', 'Remove Edit from Top'] }); sf.ui.proTools.menuClick({ menuPath: ['Edit', 'Fades', 'Fade to Start'] }); sf.ui.proTools.menuClick({ menuPath: ['Edit', 'Selection', 'Extend Edit Up'] }); sf.ui.proTools.menuClick({ menuPath: ['Edit', 'Selection', 'Remove Edit from Bottom'] });
Nick Norton @notNickNorton
DUDE I THINK THAT WAS ME. Newbury Park High School?
Thank you for this! Gonna test drive it today.
Nick Norton @notNickNorton
This works great!
The only thing I'd hope for would be a way to select the whole of the edited part, rather than clicking into each silence, and having it loop through. But this is already a huge timesaver, thank you!
Ryan DeRemer @Ryan_DeRemer
DUDE! What a small world! How you been man? Hit me on IG @ryanmixedit!
Oh ok I took the pics literally lol. The goal with SF is to use built in functions or UI automation as much as possible, only doing keyboard/mouse simulation when absolutely necessary. The script I wrote was just the simplest/easiest to write and understand. Hopefully it'll help until I or someone else can write something a bit more substantial. But from what I've seen, it should be possible to automate what you want to do for the entire track, not just one clip at a time. It is a bit more involved though and I would imagine it can't be done properly with macros.
Long processes also need to take workflow into account, so I'd probably need more detail on how you'd want this command to run.
Nick Norton @notNickNorton
Oh yeah, more detail for how I'd like it to run!
I imagine I'd just drag my selection around the edited section of the top track and run it, having it make all the cuts on the lower track.
- DDenis Estevez @Denis_Estevez
Did this ever get improved upon even further (and would you share the outcome, please?)?? I would also find this very useful!
Nick Norton @notNickNorton
Not yet!
I ran into @Kitch at an event last weekend and he had some thoughts on how to do this too...Kitch?
Kitch Membery @Kitch2022-09-30 18:44:09.064Z
Hi Nick,
Great to meet you on the weekend. And great to hear you are friends with @Ryan_DeRemer. Such a small world.
I for sure have some thoughts on this script. I made a similar script for my own use a while back. However it will need some editing to get it working the way you want.
See this post. Need the 'voiceover with some light background noise' script #post-2
I'll see if I can edit it today to get it working for you. :-)
- In reply tonotNickNorton⬆:
Kitch Membery @Kitch2022-09-30 19:29:32.558Z
Hi @notNickNorton,
Try the following script.
/** * @param {'Up'|'Down'} direction */ function navigateTrack(direction) { if (direction === 'Up') { sf.ui.proTools.getMenuItem("Edit", "Selection", "Extend Edit Up").elementClick(); sf.ui.proTools.getMenuItem("Edit", "Selection", "Remove Edit from Bottom").elementClick(); } if (direction === 'Down') { sf.ui.proTools.getMenuItem("Edit", "Selection", "Extend Edit Down").elementClick(); sf.ui.proTools.getMenuItem("Edit", "Selection", "Remove Edit from Top").elementClick(); } } function copy() { sf.ui.proTools.getMenuItem('Edit', 'Copy').elementClick(); } function paste() { sf.ui.proTools.getMenuItem('Edit', 'Paste').elementClick(); } function paste() { sf.ui.proTools.getMenuItem('Edit', 'Paste').elementClick(); } function main() { sf.ui.proTools.appActivateMainWindow(); var originalSelection = sf.ui.proTools.selectionGet(); const oldEditMode = sf.ui.proTools.editModeSet({ mode: "Slip", }).oldMode; const cursorToolCluster = sf.ui.proTools.mainWindow.cursorToolCluster; cursorToolCluster.buttons.whoseTitle.startsWith('Grabber tool').first.popupMenuSelect({ isRightClick: true, menuPath: ['Object'] }); navigateTrack("Down"); navigateTrack("Up"); copy() navigateTrack("Down"); paste() sf.ui.proTools.toolsSelect({ tool: "Selector", }); sf.ui.proTools.selectionSet({ selectionStart: originalSelection.selectionStart, selectionLength: originalSelection.selectionLength, }); navigateTrack("Up"); sf.ui.proTools.editModeSet({ mode: oldEditMode, }); } //Change the Main Counter to Samples for the duration of the main function sf.ui.proTools.mainCounterDoWithValue({ targetValue: 'Samples', action: () => { main(); } });
Select the audio regions and fades on the source track, then run the script.
This
Becomes this
Let me know how it goes for you. :-)
Nick Norton @notNickNorton
That's amazing! And it actually does a step better than your screenshot, here's where I ended up:
Which is exactly what I was asking for. Beautiful!
- In reply toKitch⬆:DDenis Estevez @Denis_Estevez
Hi @Kitch & @notNickNorton ! For me this seems to just copy the edits from the first lane, but also the media. I'm after adding extra a few layers onto my existing atmos regions below (not just copy them). this can be of something different (example adding birds onto two existing wind and meadow sfx regions). With this script, for me it seems to just copy and paste my previous layer with the edits, and replaces the second new layer with this former? Am I doing something wrong? Nick's image seems to convey this as well. It has just copied '95 Room Tone' clips to the below track.
Nick Norton @notNickNorton
Aw man, you're right. I was so excited by seeing my edits pop up that I didn't read the region names. Just checked it on a pink noise region and you're exactly right - it copies the regions as well as the edits, not just the edits.
@Kitch any ideas?
For reference, here's a before/after for where Ryan's script gets it, but you have to manually click into each silence and then run it a bunch of times:
Before:
After:
Kitch Membery @Kitch2022-10-06 18:09:17.832Z
Hi @Denis_Estevez & @notNickNorton,
I must have edited my original script incorrectly. I'll take a look at it when I have a moment. :-)
- In reply tonotNickNorton⬆:
Kitch Membery @Kitch2022-10-06 19:38:24.787Z
Hi Hi @Denis_Estevez & @notNickNorton,
Looking at the script I posted, the behavior I intended worked as expected.
I'm not sure I quite understand what you are trying to achieve. In the original post you asked how to "copy my edits (cuts and fades)" to the track below. Does the script I shared not do this?
All my script was designed to do was copy the clips with edits from one track and place them on the track below. keeping the areas between the copied clips intact.
If you could share before and after screenshots of what you are trying to achieve that would be helpful. It would also be great if you could list out each step (in clear point form) you'd take to complete the workflow, that may make it more clear to me what you are after.
Even better would be to do a screen recording walking me through the process.
Rock on!
Kitch Membery @Kitch2022-10-06 20:02:16.359Z
Looking at Ryan's script more closely, I think I now understand what you are trying to achieve.
Correct me if I'm wrong...
1.) You want to cut/trim the lower track audio to match the lengths of the upper tracks clips including fades.
2.) You want to reproduce (not copy) the fades from the upper track on the lower room tone track.
So essentially the lower track would be room tone cut and faded to match the upper track's edit.
Let me know if that seems right?
Nick Norton @notNickNorton
Yup that's right!
- In reply toKitch⬆:
Nick Norton @notNickNorton
The trick is not to copy the clips to the track below, but to apply the same edits to a long clip on the track below. But no problem on the walk through!
Say I've got BGs cut like this:
And the director is like "add birds". So I lay a clip of birds across the whole scene (in this case represented by noise, but you get the idea):
And then I manually match all the cuts, just using the navigation keys, to end up with this:
So, the goal is to skip the manual adding cuts to the second track's long region, and just automate that process.
Let me know if you need the key command combo I'd use!
Kitch Membery @Kitch2022-10-06 20:10:23.174Z
Thanks Nick,
That makes way more sense. :-)
This process is quite a bit more in depth, so I'll see what I can do when I get a chance.
Nick Norton @notNickNorton
I appreciate it!
- In reply toKitch⬆:
Brenden @nednednerb
Did you happen to get a chance to look at this again? I tried the script above and saw the same result!
I am interested in this script! I sometimes find I've got to replace a file I've started editing and I wish I could copy the "edits" to the new file. PT has an option to relink files in the Workspace, but this is not always ideal.
When I saw the script above seem to finish that quickly, the idea of the laborious process of copying edits is relieved.
For now I might make a basic loop, because my clips have no fades at the moment!
- MIn reply tonotNickNorton⬆:Martin Pavey @Martin_Pavey
+1 here for this.
If Kitch could get it to work it would be a fantastic timesaver!
I agree the best workflow would be make a selection over the clips you want to mirror the edits/fades on
and then run the shortcut.
It seems tantalisingly close to working and would save me and I'm sure lots of other people
loads of boring keypresses.
Pretty please. ;-) - In reply tonotNickNorton⬆:Nick Norton @notNickNorton
Just tagging @Kitch because I've got a show starting this week this would be a huge help for (lots of perspective cuts), and it seems like other people want it too!
Kitch Membery @Kitch2023-12-11 00:35:48.734Z
Hi @notNickNorton,
No promises but I'll see how I go.
- In reply tonotNickNorton⬆:Kitch Membery @Kitch2023-12-11 08:22:05.406Z
Hi @notNickNorton,
I did not have a lot of time today to look at this, but take this script for a spin.
Note: I discovered a bug with the
sf.ui.proTools.selectionGet()
method while writing the script. It seems that for SoundFlow to determine if there is a "fade-in", there needs to be a clip on the track preceding it. And the same goes for fade-outs, there needs to be a clip somewhere on the track after the fade-out. (Mentioning this in case the script does not apply a fade in or fade out at the start/end of the fill region)./** * @param {'Up'|'Down'} direction */ function navigateTrack(direction) { if (direction === 'Up') { sf.ui.proTools.getMenuItem("Edit", "Selection", "Extend Edit Up").elementClick(); sf.ui.proTools.getMenuItem("Edit", "Selection", "Remove Edit from Bottom").elementClick(); } if (direction === 'Down') { sf.ui.proTools.getMenuItem("Edit", "Selection", "Extend Edit Down").elementClick(); sf.ui.proTools.getMenuItem("Edit", "Selection", "Remove Edit from Top").elementClick(); } } function getClipBoundaries() { const selection = sf.ui.proTools.selectionGet(); const boundaries = { start: selection.selectionStart, end: selection.selectionEnd, }; return boundaries; } function moveToNextClipOrFade() { // Select next Clip or Fade sf.keyboard.press({ keys: "ctrl+tab", }); } function deselectSelection() { // Deslect Selection sf.keyboard.press({ keys: "down", }); } function clearSelection() { sf.ui.proTools.getMenuItem("Edit", "Clear").elementClick(); } function createFadeIn() { sf.ui.proTools.getMenuItem("Edit", "Fades", "Fade to Start").elementClick(); } function createFadeOut() { sf.ui.proTools.getMenuItem("Edit", "Fades", "Fade to End").elementClick(); } function main() { sf.ui.proTools.appActivateMainWindow(); sf.ui.proTools.invalidate(); const oldEditMode = sf.ui.proTools.editModeSet({ mode: "Slip", }).oldMode; // Move down a track navigateTrack("Down"); // Trim Fill Clip sf.ui.proTools.menuClick({ menuPath: ["Edit", "Trim Clip", "To Selection"] }); // Move up a track navigateTrack("Up"); const initialSelection = getClipBoundaries(); const originalSelectionStart = initialSelection.start; const originalSelectionEnd = initialSelection.end; let previousClipsEndPoint; let storedSelections = []; // Deselect selection deselectSelection(); while (getClipBoundaries().end !== originalSelectionEnd) { moveToNextClipOrFade(); let selectionInfo = sf.ui.proTools.selectionGetInfo(); let hasStartFade = selectionInfo.hasStartFade; let hasEndFade = selectionInfo.hasEndFade; let currentClipBoundaries = getClipBoundaries(); let start = currentClipBoundaries.start; let end = currentClipBoundaries.end; if (start !== previousClipsEndPoint) { storedSelections.push({ task: "Clear", start: previousClipsEndPoint, end: getClipBoundaries().start, }); } if (hasStartFade) { storedSelections.push({ task: "Fade in", start, end, }); } if (hasEndFade) { storedSelections.push({ task: "Fade out", start, end, }); } previousClipsEndPoint = getClipBoundaries().end; } // Move down a track navigateTrack("Down"); // Clear Selections (Run this first before fades). storedSelections.filter(selection => selection.task === "Clear").forEach(selection => { sf.ui.proTools.selectionSet({ selectionStart: selection.start, selectionEnd: selection.end, }); clearSelection(); }); // Create Fades storedSelections.filter(selection => selection.task !== "Clear").forEach(selection => { if (selection.task === "Fade in") { sf.ui.proTools.selectionSet({ selectionStart: selection.end, selectionEnd: selection.end, }); createFadeIn(); } if (selection.task === "Fade out") { sf.ui.proTools.selectionSet({ selectionStart: selection.start, selectionEnd: selection.start, }); createFadeOut(); } }); // Move up a track navigateTrack("Up"); // Restore Selection sf.ui.proTools.selectionSet({ selectionStart: originalSelectionStart, selectionEnd: originalSelectionEnd, }); sf.ui.proTools.editModeSet({ mode: oldEditMode, }); } sf.ui.proTools.mainCounterDoWithValue({ targetValue: 'Samples', action: main, });
Hopefully, this will get you through the week of heavy perspective cuts. :-)
Rock on!
CC: @Martin_Pavey - I unfortunately did not see your reply back in late August, as you replied to Nick's thread not to one of my replies. (Sorry I missed it.)
Nick Norton @notNickNorton
Holy cow, this is awesome!
I discovered a second use case, which is cutting back and forth between two clips (for perspective edits for instance).
So everyone reading this knows what's up, here's how it works. Select this:
To get this:
And for perspective cuts, select this:
To get this:
It does feel a little counterintuitive for what you select before running it, so I might try to tweak that myself a bit.
Thank you Kitch!
- MMartin Pavey @Martin_Pavey
Hi Nick
Did you get time to tweak this script at all?
It would be great to have the perspective cuts thing working etc.I was using it yesterday on a show and it's saving me loads of mousing and clicking,
love it!
- MIn reply tonotNickNorton⬆:Martin Pavey @Martin_Pavey
Thanks Kitch, this is great!
It's a shame it doesn't do the first or last fades if there's nothing on the track ahead, but I can live with that.
Nick, I don't get the same results as you when making the selections on your pictures.
Soundflow just goes into some weird loop and I have to quit out to get it to stop.
The only way it works for me is to select the regions I want to copy. Then it will duplicate the cuts and fades to the track underneath.
How are you getting that to work? Perspective cuts would be massive I agree, big timesaver.
Maybe it's a difference in our default PT setups? - OIn reply tonotNickNorton⬆:Owen Granich-Young @Owen_Granich_Young
UPDATE -- Many problems addressed (including if you only have 1 region selected also If you have Stereo tracks :) way more work on both process clips and how it handles cross fades. Not to mention realizing there's a bunch more functions i can do with SDK ... you could do the fades with SDK too but then user needs presets set so I decided to keep that simple for now.
Here's one I've given waaaaaay too much time to today, but it takes advantage of SDK and works like gangbusters.
processCLipsWithTransitions
is the sweet sweet sweetness to redefine clips with fades and cross fades as whole clips. Feels only right I share it and give back to all who have helped me on here along the way. Report any edge cases please... I put in some Adjust Bounds Guardrails last second:const trimClip = () => sf.app.proTools.trimToSelection() const fadeToStart = () => sf.ui.proTools.menuClick({ menuPath: [`Edit`, `Fades`, `Fade to Start`] }); const fadeToEnd = () => sf.ui.proTools.menuClick({ menuPath: [`Edit`, `Fades`, `Fade to End`] }); const clear = () => sf.app.proTools.clear() const buttonFade = () => sf.keyboard.press({ keys: "f", }); //// try out SDK fade const separateClip = () => sf.ui.proTools.menuClick({ menuPath: [`Edit`, `Separate Clip`, `At Selection`] }); function dismissDialog() { const dlg = sf.ui.proTools.confirmationDialog; //Wait 100ms for dialog box to appear dlg.elementWaitFor({ timeout: 100, pollingInterval: 10, onError: 'Continue' }); if (dlg.children.whoseRole.is("AXStaticText").whoseValue.is("One or more fade requests are invalid due to insufficient audio data within the fade bounds. You may skip the invalid fade request(s), or adjust the bounds for those fades (where possible).").first.exists) { dlg.buttons.whoseTitle.is("Adjust Bounds").first.elementClick(); dlg.elementWaitFor({ waitType: 'Disappear', timeout: 100, pollingInterval: 10, onError: 'Continue' }); } } function combineMultiTrack(clips) { // Filter out clips with unique start and end times const uniqueClips = clips.filter((clip, index, array) => { return array.findIndex(c => c.StartTime === clip.StartTime && c.EndTime === clip.EndTime) === index; }); return uniqueClips; } function processClipsWithTransitions(makeWholeCips) { let clips = combineMultiTrack(makeWholeCips) let combinedClips = []; let crossFadeInProgress = false; let crossFadeStartTime = 0; let crossFadeEndTime = 0; for (let i = 0; i < clips.length; i++) { let clip = clips[i]; if (clip.ClipName.includes("(cross fade)") && i > 0 && i < clips.length - 1) { if (!crossFadeInProgress) { crossFadeInProgress = true; crossFadeStartTime = clips[i - 1].StartTime; crossFadeEndTime = clips[i + 1].EndTime; } else { crossFadeEndTime = clips[i + 1].EndTime; } } else if (crossFadeInProgress) { combinedClips[combinedClips.length - 1].EndTime = crossFadeEndTime; crossFadeInProgress = false; } else if (clip.ClipName.includes("(fade in)") && i < clips.length - 1) { let nextClip = clips[i + 1]; let combinedClip = { TrackName: clip.TrackName, ClipName: nextClip.ClipName, StartTime: clip.StartTime, EndTime: nextClip.EndTime }; combinedClips.push(combinedClip); i++; // Skip the next clip as it's already combined } else if (clip.ClipName.includes("(fade out)") && i > 0) { combinedClips[combinedClips.length - 1].EndTime = clip.EndTime; } else { combinedClips.push(clip); } } return combinedClips; } function extractUniqueTrackNames(clipsArray) { let uniqueTrackNames = []; clipsArray.forEach(clip => { if (!uniqueTrackNames.includes(clip.TrackName)) { uniqueTrackNames.push(clip.TrackName); } }); return uniqueTrackNames; } function extractClipsFromFirstTrack(clipsArray, originalTracks, pickIndex) { // Get the name of the first track const firstTrackName = originalTracks[pickIndex]; // Filter the clips array to include only clips from the first track const clipsFromFirstTrack = clipsArray.filter(clip => clip.TrackName === firstTrackName); return clipsFromFirstTrack; } function createNewArrayWithAdjustedTimes(clipsArray) { let newArray = []; for (let i = 1; i < clipsArray.length; i++) { let startTime = clipsArray[i - 1].EndTime; let endTime = clipsArray[i].StartTime; newArray.push({ StartTime: startTime, EndTime: endTime }); } return newArray; } function performFadeActions(clips, originalTracks) { let highestEndTime = Math.max(...clips.map(clip => clip.EndTime)); let processedCrossfades = new Set(); clips.some((clip, index) => { if (clip.EndTime >= highestEndTime) { if (clip.ClipName === "(fade out)") { // Process the last "(fade out)" clip sf.ui.proTools.selectionSetInSamples({ selectionStart: clip.StartTime, selectionEnd: clip.StartTime }); fadeToEnd(); } return true; // Break out of the loop if the clip's end time is greater than or equal to the highest end time } if (clip.ClipName === "(fade in)") { // Move to the end time of the fade in clip and perform fadeToStart function sf.ui.proTools.selectionSetInSamples({ selectionStart: clip.EndTime, selectionEnd: clip.EndTime }); fadeToStart(); } else if (clip.ClipName === "(fade out)") { // Move to the start time of the fade out clip and perform fadeToEnd function sf.ui.proTools.selectionSetInSamples({ selectionStart: clip.StartTime, selectionEnd: clip.StartTime }); fadeToEnd(); } else if (clip.ClipName === "(cross fade)" && !processedCrossfades.has(index)) { sf.ui.proTools.selectionSetInSamples({ selectionStart: clip.StartTime, selectionEnd: clip.EndTime }); //// this is not working right now... if (!sf.ui.proTools.getMenuItem("Edit", "Fades", "Create...").isEnabled) { // Calculate halfway point const halfwayTime = (clip.StartTime + clip.EndTime) / 2; // Select halfway point sf.ui.proTools.selectionSetInSamples({ selectionStart: halfwayTime, selectionEnd: halfwayTime }); // Execute separateClip() function separateClip(); // Continue with the rest of the actions sf.ui.proTools.selectionSetInSamples({ selectionStart: clip.StartTime, selectionEnd: clip.EndTime }); } buttonFade(); dismissDialog(); sf.app.proTools.selectTracksByName({ trackNames: originalTracks.slice(1), selectionMode: "Replace" }) sf.wait({ intervalMs: 50 }); // Mark crossfade as processed processedCrossfades.add(index); } return false; }); } function main() { sf.ui.proTools.invalidate(); ///get Info on all Selected Clips & Tracks let getInfo = sf.app.proTools.getSelectedClipInfo(); /// get Unique Track Names for Navigating let originalTracks = extractUniqueTrackNames(getInfo.clips) /// Get Clip Info on Tracks 1 let firstTrackClips = extractClipsFromFirstTrack(getInfo.clips, originalTracks, 0); /// Get full clip info on Track 1 let cuts = processClipsWithTransitions(firstTrackClips) /// Sets Clips only range let originalSelection = () => sf.ui.proTools.selectionSetInSamples({ selectionStart: firstTrackClips[0].StartTime, selectionEnd: firstTrackClips[firstTrackClips.length - 1].EndTime }); //select all tracks but the guide track /* sf.ui.proTools.trackSelectByName({ names: originalTracks.slice(1) }) */ sf.app.proTools.selectTracksByName({ trackNames: originalTracks.slice(1), selectionMode: "Replace" }) if (cuts.length > 1) { /// Create Array of ranges to Match Track 2 let bside = createNewArrayWithAdjustedTimes(cuts) bside.forEach(clip => { sf.ui.proTools.selectionSetInSamples({ selectionStart: clip.StartTime, selectionEnd: clip.EndTime }); clear() }); } originalSelection() trimClip() performFadeActions(firstTrackClips, originalTracks) } sf.ui.proTools.mainCounterDoWithValue({ targetValue: `Samples`, action: main })
Nick Norton @notNickNorton
DUDE.
I had not worked on this in a while so hadn't been checking in. This script is now an F-ing miracle. Holy cow! Thank you!
- In reply toOwen_Granich_Young⬆:MMatt Still @Matt_Still
It's hard to explain the euphoria I'm feeling now that I've found this!! You are a genius!! It even does it to multiple tracks at once. THANK YOU, THANK YOU, THANK YOU!!
- In reply toOwen_Granich_Young⬆:MMartin Pavey @Martin_Pavey
Owen you are a star!
The old script was a massive timesaver, but this new one is super cool.
Thanks so much for making my life much easier. - In reply toOwen_Granich_Young⬆:
James Wasserman @James_Wasserman
Hi SF fam -- I posted a quick demo video about a very similar task/script idea I was playing with on the facebook group and it seemed a few people were interested. Huge thanks to @Owen_Granich_Young for pointing me in the direction of this already existing thread to continue the discussion.
I will link to two videos on the script:
1st is a quick demo in PT I posted on FB: https://youtu.be/97VUm13BhLs
2nd is an in-depth walkthrough and breakdown of the script made specifically for the forum: https://youtu.be/uAm1lvDKqrcI know the 2nd video is quite long-winded--sorry!! lol
Here is a quick walkthrough for those of you who don't like video demos:
Step 1: Make any selection of an edit you want to "match" or duplicate the "geometry" of eventually.Step 2: Fire the macro and you will be greeted with two core options: "Learn" or "Apply"
Step 3: Click "Learn" -- you will see SF automation of PT happen where fades, clip boundaries, and "gaps" are learned.
Step 4: Select a target clip(s) or selection length equal to or greater in length to the OG learned selection/edit.
Step 5 (Final): Fire the Macro again and click "Apply" and you will see this "new" content trimmed and matched perfectly to your OG edit
You will notice that throughout the process you will always have two options... "Gaps Only" and "Full Edit Fade/Gaps". Gaps only will simply delete content around the clips, while Full Edit matches all clip lengths, fades, and fade types perfectly.
I know the script is a little long, but so far I am having a lot of fun with it and hope to have some of ya'll give it a try. Always open to improvements or suggestions.
Future Plans: look into multi-track-OG-edit learning aka "checkerboard edits"... you can still do this by running the exact process above for A checkerboad, then B checkerboard. I know it's definitely possible to handle this as one script or an alternative one. If anyone is dying for that I can look into trying something. As mentioned in the videos, I am still sorta confused as to whether or not crossfade handling should be considered.Hope you enjoy and would love some feedback or testers! Watch the videos if you are curious or confused about anything.
/** Match Edits - Learn & Apply * 4-Button System: * 1. Learn Gaps - Learn gap deletions only * 2. Learn Full Edit - Learn gaps + fades (ignore crossfades) * 3. Apply Gaps - Apply gap deletions only * 4. Apply Full Edit - Apply gaps + recreate fades */ sf.ui.proTools.appActivateMainWindow(); function L(s){ try{ log(s); }catch(e){} } // PORTABLE: Use system temp directory for any user var GAPS_FILE = "/tmp/match_edits_gaps.json"; var FULL_EDIT_FILE = "/tmp/match_edits_full_edit.json"; // ===== SPEED DEMON FADE LEARNING FUNCTIONS ===== function S(ms){ sf.wait({ intervalMs: ms }); } function closeFadeIfOpen(){ try{ var w = sf.ui.proTools.windows.whoseTitle.contains("Fade").first; if (w && w.exists) { sf.keyboard.press({ keys: "escape" }); S(100); } }catch(e){} } function waitFadeWin(timeoutMs){ var start = Date.now(); while (Date.now() - start < timeoutMs) { try{ var w = sf.ui.proTools.windows.whoseTitle.contains("Fade").first; if (w && w.exists) return w; }catch(e){} S(20); } return null; } function waitFadeClosed(timeoutMs){ var start = Date.now(); while (Date.now() - start < timeoutMs) { try{ var w = sf.ui.proTools.windows.whoseTitle.contains("Fade").first; if (!w || !w.exists) return true; }catch(e){ return true; } S(20); } return false; } function pick(group, options){ if (!group || !group.exists) return null; try{ for (var i=0;i<options.length;i++){ var opt = group.radioButtons.whoseTitle.is(options[i]).first; if (opt && opt.exists && opt.value.invalidate().isSelected) return options[i]; } }catch(e){} return null; } function readOpenFast(){ var w = sf.ui.proTools.windows.whoseTitle.contains("Fade").first; if (!w || !w.exists) return null; function ttl(el){ try{ return el.title.invalidate().value || ""; } catch(e){ return ""; } } function iv(el){ try{ return el.value.invalidate().intValue; } catch(e){ return null; } } function isSel(rb){ var i = iv(rb); if (i === 1) return true; if (i === 0) return false; try{ if (rb.value.invalidate().isSelected === true) return true; }catch(e){} try{ if (rb.value.invalidate().valueDescription === "selected") return true; }catch(e){} return false; } function pick(group, titles){ if (!group || !group.exists) return null; for (var i=0;i<titles.length;i++){ try{ var rb = group.radioButtons.whoseTitle.is(titles[i]).first; if (rb.exists && isSel(rb)) return titles[i]; }catch(e){} } try{ var rbs = group.radioButtons.allItems; for (var j=0;j<rbs.length;j++){ if (isSel(rbs[j])) return ttl(rbs[j]) || "(unnamed)"; } }catch(e){} return null; } var title = ""; try{ title = ttl(w); }catch(e){} var kind = /cross\s*fade/i.test(title) ? "xfade" : (/in/i.test(title) ? "in" : "out"); L("Reading fade properties from window: " + title + " (kind: " + kind + ")"); var slope = (function(){ try{ var g = w.groups.whoseTitle.is("Slope").first; if (!g || !g.exists) { L("No Slope group found"); return "unknown"; } var result = pick(g, ["Equal Power","Equal Gain"]); L("Slope found: " + result); return result || "unknown"; }catch(e){ L("Slope error: " + e.message); return "unknown"; } })(); var inShape="n/a", outShape="n/a"; if (kind === "in"){ try{ var gIn = w.groups.whoseTitle.contains("In Shape").first; if (!gIn.exists) gIn = w.groups.whoseTitle.contains("Shape").first; if (gIn && gIn.exists) { inShape = pick(gIn, ["Standard","S-Curve"]) || "Standard"; L("In Shape found: " + inShape); } }catch(e){ L("In Shape error: " + e.message); } } else if (kind === "out"){ try{ var gOut = w.groups.whoseTitle.contains("Out Shape").first; if (!gOut.exists) gOut = w.groups.whoseTitle.contains("Shape").last; if (gOut && gOut.exists) { outShape = pick(gOut, ["Standard","S-Curve"]) || "Standard"; L("Out Shape found: " + outShape); } }catch(e){ L("Out Shape error: " + e.message); } } else { try{ var gIn2 = w.groups.whoseTitle.contains("In Shape").first; var gOut2 = w.groups.whoseTitle.contains("Out Shape").first; if (!gIn2.exists) gIn2 = w.groups.whoseTitle.contains("Shape").first; if (!gOut2.exists) gOut2 = w.groups.whoseTitle.contains("Shape").last; if (gIn2 && gIn2.exists) { inShape = pick(gIn2, ["Standard","S-Curve"]) || "Standard"; L("Crossfade In Shape found: " + inShape); } if (gOut2 && gOut2.exists) { outShape = pick(gOut2, ["Standard","S-Curve"]) || "Standard"; L("Crossfade Out Shape found: " + outShape); } }catch(e){ L("Crossfade shapes error: " + e.message); } } var result = { kind: kind, slope: slope, inShape: inShape, outShape: outShape, preset: null }; L("Final fade properties: " + JSON.stringify(result)); return result; } function near(a,b,t){ return Math.abs((+a)-(+b)) <= t; } function uniqueFades(info){ var raw = []; for (var i=0;i<info.clips.length;i++){ var c = info.clips[i], n = String((c && (c.name || c.ClipName)) || ""); if (n.indexOf("(fade") === 0 || n === "(cross fade)") raw.push(c); } var groups = []; for (var j=0;j<raw.length;j++){ var s = +raw[j].StartTime, e = +raw[j].EndTime, len = e - s, g = null; for (var k=0;k<groups.length;k++){ var G = groups[k]; if (near(G.start,s,240) && near(G.length,len,240)) { g = G; break; } // 5ms tolerance } if (g) g.reps.push(raw[j]); else groups.push({ start:s, end:e, length:len, reps:[raw[j]] }); } groups.sort(function(a,b){ return a.start - b.start; }); return groups; } // Ask user which phase (2-step approach due to 3-button limit) var phaseChoice = sf.interaction.displayDialog({ title: "Match Edits", prompt: "Choose phase:", buttons: ["Learn", "Apply"], defaultButton: "Learn" }); if (!phaseChoice) return; var phase = phaseChoice && phaseChoice.button ? phaseChoice.button : phaseChoice; if (phase === "Learn") { // Learn phase - ask for type var learnChoice = sf.interaction.displayDialog({ title: "Learn Phase", prompt: "What to learn:", buttons: ["Gaps Only", "Full Edit (Gaps + Fades)"], defaultButton: "Full Edit (Gaps + Fades)" }); if (!learnChoice) return; var learnType = learnChoice && learnChoice.button ? learnChoice.button : learnChoice; if (learnType === "Gaps Only") { learnGaps(); } else if (learnType === "Full Edit (Gaps + Fades)") { learnFullEdit(); } } else if (phase === "Apply") { // Apply phase - ask for type var applyChoice = sf.interaction.displayDialog({ title: "Apply Phase", prompt: "What to apply:", buttons: ["Gaps Only", "Full Edit (Gaps + Fades)"], defaultButton: "Full Edit (Gaps + Fades)" }); if (!applyChoice) return; var applyType = applyChoice && applyChoice.button ? applyChoice.button : applyChoice; if (applyType === "Gaps Only") { applyGaps(); } else if (applyType === "Full Edit (Gaps + Fades)") { applyFullEdit(); } } function learnGaps() { L("=== LEARNING GAPS ==="); var sel = sf.ui.proTools.selectionGetInSamples(); if (!sel) { L("ERROR: Make a selection on reference track"); return; } var selectionStart = sel.selectionStart; var selectionEnd = sel.selectionEnd; var selectionLength = selectionEnd - selectionStart; L("Reference selection: " + selectionStart + " → " + selectionEnd + " (" + selectionLength + ")"); var info = sf.app.proTools.getSelectedClipInfo(); if (!info || !info.clips) { L("ERROR: No clips found"); return; } // Collect all boundaries var boundaries = [selectionStart, selectionEnd]; for (var i = 0; i < info.clips.length; i++) { var clip = info.clips[i]; var start = +clip.StartTime; var end = +clip.EndTime; if (start >= selectionStart && start <= selectionEnd) boundaries.push(start); if (end >= selectionStart && end <= selectionEnd) boundaries.push(end); } // Sort and dedupe boundaries = boundaries.sort(function(a,b){return a-b;}); var unique = []; for (var j = 0; j < boundaries.length; j++) { if (unique.length === 0 || boundaries[j] !== unique[unique.length-1]) { unique.push(boundaries[j]); } } L("Found " + unique.length + " boundaries"); // Calculate gaps (inverse of clips) var gaps = []; for (var k = 0; k < unique.length - 1; k++) { var gapStart = unique[k]; var gapEnd = unique[k + 1]; // Check if this is a gap (no clip spans this exact range) var isGap = true; for (var m = 0; m < info.clips.length; m++) { var c = info.clips[m]; if (c.StartTime === gapStart && c.EndTime === gapEnd) { isGap = false; break; } } if (isGap) { // Store as relative positions (will work after trimming) gaps.push({ start: gapStart - selectionStart, end: gapEnd - selectionStart }); L("Gap: " + (gapStart - selectionStart) + " → " + (gapEnd - selectionStart) + " (" + (gapEnd - gapStart) + ")"); } } // Save gaps var data = { referenceStart: selectionStart, // Store absolute start position referenceEnd: selectionEnd, // Store absolute end position referenceLength: selectionLength, gaps: gaps, timestamp: Date.now() }; try { sf.system.exec({ commandLine: `bash -lc 'cat > "${GAPS_FILE}" << "JSON"\n${JSON.stringify(data, null, 2)}\nJSON'`, timeout: 5000 }); L("Saved " + gaps.length + " gaps to " + GAPS_FILE); } catch (e) { L("ERROR: Could not save gaps: " + e.message); } } function applyGaps() { L("=== APPLYING GAPS (MULTI-TRACK) ==="); // Load gaps var data; try { var result = sf.system.exec({ commandLine: `bash -lc 'cat "${GAPS_FILE}"'`, timeout: 5000 }); data = JSON.parse(result.result); } catch (e) { L("ERROR: Could not load gaps: " + e.message); return; } var sel = sf.ui.proTools.selectionGetInSamples(); if (!sel) { L("ERROR: Make a selection on target track(s)"); return; } var targetStart = sel.selectionStart; var targetEnd = sel.selectionEnd; L("Target selection: " + targetStart + " → " + targetEnd + " (multi-track)"); // RESTORE EXACT ORIGINAL SELECTION BOUNDARIES (preserves height) L("Restoring original selection boundaries: " + data.referenceStart + " → " + data.referenceEnd); sf.ui.proTools.selectionSetInSamples({ selectionStart: data.referenceStart, selectionEnd: data.referenceEnd }); L("Selection restored to original boundaries (height preserved)"); // TRIM THE CLIP TO MATCH THE SELECTION (works on all selected tracks) sf.keyboard.press({ keys: "cmd+t" }); L("Trimmed clips to match selection (all tracks)"); // Update our variables to match the restored selection targetStart = data.referenceStart; targetEnd = data.referenceEnd; targetLength = data.referenceLength; // Now apply gaps using simple math (identical selection lengths) - works on all selected tracks L("Applying " + data.gaps.length + " gaps to all selected tracks..."); for (var i = 0; i < data.gaps.length; i++) { var gap = data.gaps[i]; var absStart = targetStart + gap.start; var absEnd = targetStart + gap.end; L("Deleting gap: " + absStart + " → " + absEnd + " (" + (absEnd - absStart) + ")"); sf.ui.proTools.selectionSetInSamples({ selectionStart: absStart, selectionEnd: absEnd }); sf.keyboard.press({ keys: "backspace" }); } L("Applied " + data.gaps.length + " gaps to all selected tracks"); // CLEANUP: Delete JSON file to avoid clutter try { sf.system.exec({ commandLine: `rm -f "${GAPS_FILE}"`, timeout: 2000 }); L("Cleaned up temporary file: " + GAPS_FILE); } catch (e) { L("Note: Could not clean up temp file (non-critical)"); } } function learnFullEdit() { L("=== LEARNING FULL EDIT (GAPS + FADES) ==="); var sel = sf.ui.proTools.selectionGetInSamples(); if (!sel) { L("ERROR: Make a selection on reference track"); return; } var selectionStart = sel.selectionStart; var selectionEnd = sel.selectionEnd; var selectionLength = selectionEnd - selectionStart; L("Reference selection: " + selectionStart + " → " + selectionEnd + " (" + selectionLength + ")"); var info = sf.app.proTools.getSelectedClipInfo(); if (!info || !info.clips) { L("ERROR: No clips found"); return; } // Collect all boundaries (same as learnGaps) var boundaries = [selectionStart, selectionEnd]; for (var i = 0; i < info.clips.length; i++) { var clip = info.clips[i]; var start = +clip.StartTime; var end = +clip.EndTime; if (start >= selectionStart && start <= selectionEnd) boundaries.push(start); if (end >= selectionStart && end <= selectionEnd) boundaries.push(end); } // Sort and dedupe boundaries = boundaries.sort(function(a,b){return a-b;}); var unique = []; for (var j = 0; j < boundaries.length; j++) { if (unique.length === 0 || boundaries[j] !== unique[unique.length-1]) { unique.push(boundaries[j]); } } L("Found " + unique.length + " boundaries"); // Calculate gaps (same as learnGaps) var gaps = []; for (var k = 0; k < unique.length - 1; k++) { var gapStart = unique[k]; var gapEnd = unique[k + 1]; // Check if this is a gap (no clip spans this exact range) var isGap = true; for (var m = 0; m < info.clips.length; m++) { var c = info.clips[m]; if (c.StartTime === gapStart && c.EndTime === gapEnd) { isGap = false; break; } } if (isGap) { gaps.push({ start: gapStart - selectionStart, end: gapEnd - selectionStart }); L("Gap: " + (gapStart - selectionStart) + " → " + (gapEnd - selectionStart) + " (" + (gapEnd - gapStart) + ")"); } } // NEW: Learn fades using Speed Demon approach (with properties) var fades = []; var fadeGroups = uniqueFades(info); L("Found " + fadeGroups.length + " unique fade group(s) in selection"); // Close any open fade dialogs first closeFadeIfOpen(); // Learn each fade group for (var i = 0; i < fadeGroups.length; i++) { var fadeGroup = fadeGroups[i]; // Skip if fade is outside our selection if (fadeGroup.start >= selectionEnd || fadeGroup.end <= selectionStart) { L("Skipping fade outside selection: " + fadeGroup.start + " → " + fadeGroup.end); continue; } // Read fade properties using Speed Demon approach first sf.ui.proTools.selectionSetInSamples({ selectionStart: fadeGroup.start, selectionEnd: fadeGroup.end }); sf.keyboard.press({ keys: "cmd+f" }); var w = waitFadeWin(2000); if (!w) { L("— Fade " + (i+1) + ": no window"); continue; } var props = readOpenFast() || { kind:"unknown", slope:"unknown", inShape:"unknown", outShape:"unknown", preset:null }; sf.keyboard.press({ keys: "escape" }); waitFadeClosed(120); // Check if this is a crossfade (skip it) if (props.kind === "xfade") { L("Skipping crossfade: " + fadeGroup.start + " → " + fadeGroup.end + " (kind: " + props.kind + ")"); continue; } // Also check if it spans across clip boundaries var isCrossfade = false; for (var j = 0; j < unique.length - 1; j++) { var boundary = unique[j]; if (fadeGroup.start < boundary && fadeGroup.end > boundary) { isCrossfade = true; break; } } if (isCrossfade) { L("Skipping crossfade: " + fadeGroup.start + " → " + fadeGroup.end + " (spans boundaries)"); continue; } // Store relative to selection start var relativeStart = fadeGroup.start - selectionStart; var relativeEnd = fadeGroup.end - selectionStart; fades.push({ start: relativeStart, end: relativeEnd, duration: relativeEnd - relativeStart, kind: props.kind, slope: props.slope, inShape: props.inShape, outShape: props.outShape, preset: props.preset }); L("Learned fade: " + props.kind + " @ " + relativeStart + "→" + relativeEnd + " (" + props.slope + ", " + props.inShape + "/" + props.outShape + ")"); } // Save full edit data var data = { referenceStart: selectionStart, referenceEnd: selectionEnd, referenceLength: selectionLength, gaps: gaps, fades: fades, timestamp: Date.now() }; try { sf.system.exec({ commandLine: `bash -lc 'cat > "${FULL_EDIT_FILE}" << "JSON"\n${JSON.stringify(data, null, 2)}\nJSON'`, timeout: 5000 }); L("Saved " + gaps.length + " gaps and " + fades.length + " fades to " + FULL_EDIT_FILE); } catch (e) { L("ERROR: Could not save full edit: " + e.message); } } function applyFullEdit() { L("=== APPLYING FULL EDIT (GAPS + FADES) - MULTI-TRACK ==="); // Load full edit data var data; try { var result = sf.system.exec({ commandLine: `bash -lc 'cat "${FULL_EDIT_FILE}"'`, timeout: 5000 }); data = JSON.parse(result.result); } catch (e) { L("ERROR: Could not load full edit: " + e.message); return; } var sel = sf.ui.proTools.selectionGetInSamples(); if (!sel) { L("ERROR: Make a selection on target track(s)"); return; } var targetStart = sel.selectionStart; var targetEnd = sel.selectionEnd; L("Target selection: " + targetStart + " → " + targetEnd + " (multi-track)"); // RESTORE EXACT ORIGINAL SELECTION BOUNDARIES (preserves height) L("Restoring original selection boundaries: " + data.referenceStart + " → " + data.referenceEnd); sf.ui.proTools.selectionSetInSamples({ selectionStart: data.referenceStart, selectionEnd: data.referenceEnd }); L("Selection restored to original boundaries (height preserved)"); // TRIM THE CLIP TO MATCH THE SELECTION (works on all selected tracks) sf.keyboard.press({ keys: "cmd+t" }); L("Trimmed clips to match selection (all tracks)"); // Update our variables to match the restored selection targetStart = data.referenceStart; targetEnd = data.referenceEnd; targetLength = data.referenceLength; // Apply gaps first (works on all selected tracks) L("Applying " + data.gaps.length + " gaps to all selected tracks..."); for (var i = 0; i < data.gaps.length; i++) { var gap = data.gaps[i]; var absStart = targetStart + gap.start; var absEnd = targetStart + gap.end; L("Deleting gap: " + absStart + " → " + absEnd + " (" + (absEnd - absStart) + ")"); sf.ui.proTools.selectionSetInSamples({ selectionStart: absStart, selectionEnd: absEnd }); sf.keyboard.press({ keys: "backspace" }); } // NEW: Apply fades with detailed properties (works on all selected tracks) L("Applying " + data.fades.length + " fades with properties to all selected tracks..."); for (var i = 0; i < data.fades.length; i++) { var fade = data.fades[i]; var absStart = targetStart + fade.start; var absEnd = targetStart + fade.end; L("Creating " + fade.kind + " fade: " + absStart + " → " + absEnd + " (" + fade.duration + ") - " + fade.slope + ", " + fade.inShape + "/" + fade.outShape); sf.ui.proTools.selectionSetInSamples({ selectionStart: absStart, selectionEnd: absEnd }); // Create fade using Pro Tools menu (exact path with ellipsis) sf.ui.proTools.menuClick({ menuPath: ['Edit', 'Fades', 'Create...'] }); S(200); // Apply fade properties if we have detailed info if (fade.kind !== "unknown" && fade.slope !== "unknown") { try { var fadeWin = waitFadeWin(2000); if (fadeWin) { // Set slope if (fade.slope !== "unknown") { try { var slopeGroup = fadeWin.groups.whoseTitle.is("Slope").first; if (slopeGroup && slopeGroup.exists) { var slopeRadio = slopeGroup.radioButtons.whoseTitle.is(fade.slope).first; if (slopeRadio && slopeRadio.exists) { slopeRadio.elementClick(); S(50); } } } catch(e) { L("Could not set slope: " + e.message); } } // Set shapes based on fade kind if (fade.kind === "in" && fade.inShape !== "unknown") { try { var inGroup = fadeWin.groups.whoseTitle.contains("In Shape").first; if (!inGroup.exists) inGroup = fadeWin.groups.whoseTitle.contains("Shape").first; if (inGroup && inGroup.exists) { var inRadio = inGroup.radioButtons.whoseTitle.is(fade.inShape).first; if (inRadio && inRadio.exists) { inRadio.elementClick(); S(50); } } } catch(e) { L("Could not set in shape: " + e.message); } } else if (fade.kind === "out" && fade.outShape !== "unknown") { try { var outGroup = fadeWin.groups.whoseTitle.contains("Out Shape").first; if (!outGroup.exists) outGroup = fadeWin.groups.whoseTitle.contains("Shape").last; if (outGroup && outGroup.exists) { var outRadio = outGroup.radioButtons.whoseTitle.is(fade.outShape).first; if (outRadio && outRadio.exists) { outRadio.elementClick(); S(50); } } } catch(e) { L("Could not set out shape: " + e.message); } } // Close fade window sf.keyboard.press({ keys: "return" }); waitFadeClosed(120); } } catch(e) { L("Error applying fade properties: " + e.message); // Close window if open try { sf.keyboard.press({ keys: "escape" }); waitFadeClosed(120); } catch(e2) {} } } else { // Simple fade creation without properties sf.keyboard.press({ keys: "return" }); S(100); } } L("Applied " + data.gaps.length + " gaps and " + data.fades.length + " fades to all selected tracks"); // CLEANUP: Delete JSON file to avoid clutter try { sf.system.exec({ commandLine: `rm -f "${FULL_EDIT_FILE}"`, timeout: 2000 }); L("Cleaned up temporary file: " + FULL_EDIT_FILE); } catch (e) { L("Note: Could not clean up temp file (non-critical)"); } }
Cheers!
James