Created
September 21, 2017 11:44
-
-
Save jashmenn/66f2806ae6da643a0bb16452629deee8 to your computer and use it in GitHub Desktop.
Remove Silence from Final Cut Pro clips, automatically, using ffmpeg timecodes and OSX JavaScript Automation - Demo: https://imgur.com/a/Zisav
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env osascript -l JavaScript | |
/** | |
* Delete silence from Final Cut Pro timeline using a script. | |
* Demo: https://imgur.com/a/Zisav | |
* | |
* This script accepts an ffmpeg silencedetect log as input. | |
* | |
* To setup, have fcp running along with your clip selected. Ensure that the | |
* timecode will start at zero before running this script. That is, if your clip | |
* is in the middle of your project, create a compound clip first and then enter | |
* into that clip before running this script. | |
* | |
* To generate silence.txt: | |
* /usr/local/bin/ffmpeg -i $1 -af silencedetect=n=-50dB:d=1 -f s16le -y /dev/null 2>&1 | tee silence.txt | |
* | |
* Currently it adds a little margin of silence at the end of each noised clip. You can adjust this below. | |
* If you adjust the silencedetection dB, then this number may need tweaking. | |
* | |
* For example, if your dB filter is higher, you'll filter out more background | |
* noise, but you might clip the audio you want. Extending the margins can help | |
* prevent clipping a natural trailing off of the sound you want. | |
* | |
* Nate Murray 2017 <nate@natemurray.com> | |
**/ | |
function run(argv) { | |
ObjC.import("stdlib"); | |
ObjC.import("Foundation"); | |
console.log(JSON.stringify(argv)); | |
console.log("Start Final Cut Pro and click the matching clip"); | |
// Adjust these to your liking | |
// One way to test, is comment out the delete section below and visually inspect | |
const startMargin = 0.100; | |
const endMargin = 0.000; | |
if (!Application("Final Cut Pro").running()) { | |
console.log("Final Cut Pro isn't running"); | |
$.exit(1); | |
} | |
// let app = Application("Finder"); | |
let app = Application.currentApplication(); | |
app.includeStandardAdditions = true; | |
let se = Application("System Events"); | |
function loadFile(path) { | |
let fm = $.NSFileManager.defaultManager; | |
let contents = fm.contentsAtPath(path.toString()); // NSData | |
contents = $.NSString.alloc.initWithDataEncoding( | |
contents, | |
$.NSUTF8StringEncoding | |
); | |
return ObjC.unwrap(contents); | |
} | |
function parseSilenceFile(contents) { | |
let lines = contents.split("\n"); | |
let silences = []; | |
let pair = {}; | |
for (let i = 0; i < lines.length; i++) { | |
let l = lines[i]; | |
// [silencedetect @ 0x7fd895407da0] silence_start: 272.972 | |
// [silencedetect @ 0x7fd895407da0] silence_end: 274.762 | silence_duration: 1.78948 | |
let startReg = /silence_start: (\d+.?\d+)\b/; | |
let endReg = /silence_end: (\d+.?\d+)\b/; | |
let startMatch = startReg.exec(l); | |
let endMatch = endReg.exec(l); | |
if (startMatch && startMatch.length > 0) { | |
pair["start"] = startMatch[1]; | |
pair["end"] = null; | |
} | |
if (endMatch && endMatch.length > 0) { | |
pair["end"] = endMatch[1]; | |
if (pair["start"]) { | |
(pair => silences.push(pair))(pair); | |
pair = {}; | |
} | |
} | |
} | |
return silences; | |
} | |
// parse the silence points | |
let path = argv[0]; | |
let rawSilenceFileContents = loadFile(path); | |
let silencePoints = parseSilenceFile(rawSilenceFileContents); | |
console.log(JSON.stringify(silencePoints)); | |
// activate fcp | |
let fcp = Application("Final Cut Pro"); | |
fcp.activate(); | |
delay(1.0); | |
function moveToTimecode(timeInSeconds) { | |
delay(0.2); | |
// convert timeInSeconds in decimal to timecode values. | |
let [seconds, deciseconds] = timeInSeconds.split("."); | |
let d = new Date(null); | |
d.setSeconds(parseInt(seconds)); | |
let hourmindays = d.toISOString().substr(11, 8); | |
let decipart = (60 * parseFloat("0." + deciseconds)) | |
.toString() | |
.split(".")[0] | |
.substr(0, 2); | |
if (decipart.length < 2) { | |
decipart = "0" + decipart; | |
} | |
let timecodekeystroke = (hourmindays + decipart).replace(/:/g, ""); | |
console.log( | |
" timecodekeystroke ", | |
timeInSeconds, | |
hourmindays, | |
decipart, | |
timecodekeystroke | |
); | |
se.keystroke("p", { using: "control down" }); | |
se.keystroke(timecodekeystroke); | |
se.keyCode(36); // Press Enter | |
} | |
function blade() { | |
se.keystroke("b", { using: "command down" }); | |
} | |
// extend edges | |
silencePoints = silencePoints.map(sp => ({ | |
start: (parseFloat(sp.start) + startMargin).toString(), | |
end: (parseFloat(sp.end) - endMargin).toString() | |
})); | |
for (let i = 0; i < silencePoints.length; i++) { | |
let sp = silencePoints[i]; | |
console.log(i, JSON.stringify(sp)); | |
delay(0.05); | |
moveToTimecode(sp.start); | |
delay(0.05); | |
blade(); | |
delay(0.05); | |
moveToTimecode(sp.end); | |
delay(0.05); | |
blade(); | |
} | |
console.log("Deleting Silence"); | |
// Go backwards, becase we're changing the total time as we go | |
for (let i = silencePoints.length - 1; i > 0; i--) { | |
let sp = silencePoints[i]; | |
console.log("D", i, JSON.stringify(sp)); | |
moveToTimecode(sp.start); | |
delay(0.1); | |
// select current clip | |
se.keystroke("c"); | |
delay(0.1); | |
// delete the silence | |
se.keyCode(51); // Press Delete | |
delay(0.3); | |
} | |
} |
This worked excellently - thanks a bunch!!! You saved me a ton of time!
I just wanted to say thanks for posting this script—I messed around a bit with it, and using @rlau1115's modification, I was able to get it to cut gaps in my videos a lot quicker than I was doing by hand before!
I posted a blog entry showing more how I use it (with a step-by-step, since that's kind of lacking here): https://www.jeffgeerling.com/blog/2023/shaving-hours-my-workflow-trimming-silence-fcpx-and-applescript
Love it!
So there's actually a way to do this by generating a .fcpXML file... (which avoids locking up your computer for however long it takes to automate the keyboard), but I haven't figured out how to sync up the timing + deal with boundaries between different clips.
I haven't touched that project in a while (got really busy!) but I feel like that'd be the next evolution of this.
—
Made With Lau | Chinese family recipes, from our kitchen to yours :)
Meet our family
Connect with us: YouTube | Blog | Instagram | Pinterest | Tiktok
Support us on Patreon :)
Featured in: YouTube (youtube.com, App Store, Creator on the Rise, Mini Documentary, Podcast), TODAY, Buzzfeed Tasty, US Chamber of Commerce, CNN
…--- original message ---
On January 19, 2023 at 4:01 PM PST ***@***.*** wrote:
@geerlingguy commented on this gist.
I just wanted to say thanks for posting this script—I messed around a bit with it, and using @rlau1115's modification, I was able to get it to cut gaps in my videos a lot quicker than I was doing by hand before!
I posted a blog entry showing more how I use it (with a step-by-step, since that's kind of lacking here): https://www.jeffgeerling.com/blog/2023/shaving-hours-my-workflow-trimming-silence-fcpx-and-applescript
—
Reply to this email directly, view it on GitHub or unsubscribe.
You are receiving this email because you were mentioned.
Triage notifications on the go with GitHub Mobile for iOS or Android.
--- end of original message ---
@rlau1115 - I opened an issue for that over here: geerlingguy/final-cut-it-out#1
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Hey! Can you guys create a video tutorial from zero teaching how to use this code?
I don't know nothing about programming and I have been looking for this feature for a long time ago.
Unfortunately I haven't been able to use this code.