-
-
Save jashmenn/66f2806ae6da643a0bb16452629deee8 to your computer and use it in GitHub Desktop.
#!/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); | |
} | |
} |
JeanSemeur
commented
May 4, 2020
•
I finlay choose another way to delimitate those silents parts : with keyword !!!... and wrote a small app in applescript that create an FCPXML... https://vimeo.com/415340397/f28424553d
Thank Jashmaenn
Martin
I finlay choose another way to delimitate those silents parts : with keyword !!!... and wrote a small app in applescript that create an FCPXML... https://vimeo.com/415340397/f28424553d
Thank Jashmaenn
Martin
That looks amazing and is exactly what I need - something to just identify silence in Fcpx. Would you be willing to share your app?
I finlay choose another way to delimitate those silents parts : with keyword !!!... and wrote a small app in applescript that create an FCPXML... https://vimeo.com/415340397/f28424553d
Thank Jashmaenn
MartinThat looks amazing and is exactly what I need - something to just identify silence in Fcpx. Would you be willing to share your app?
Hello hamletcat... this app is now available on my website : (english : http://martingosset.com/boomerang_english/ and french with other app : http://martingosset.com/boomerang-app/)
Thank for your interest...
Martin
I have some insights as well as an issue.
I've gotten this script to work by running the ffmpeg command with an audio file exported from FCPX:
ffmpeg -i instructional.wav -af silencedetect=n=-30dB:d=0.2 -f s24le -ar 48000 -y /dev/null 2>&1 | tee silence.txt
Then running ./final-cut-it-out.js silence.txt works perfectly.
It works great for shorter clips, but when trying to do this with a 6+ minute clip, the cut points are skewed. I tried doing a test with a 30 second clip, a 6 minute gap, and the same 30 second clip. The tool works fine when dealing with the first 30 second clip, but the cut points are off by 0:12 frames for the second 30 second clip.
Any ideas on how to fix the code to account for this?
Fixed!
This article gave me some insight >> via Frame.io
I'm shooting in 23.98fps, and there's 3.6 seconds of drift between real time and timecode.
function moveToTimecode(timeInSeconds) {
delay(0.1);
/*
https://blog.frame.io/2017/07/17/timecode-and-frame-rates/#:~:text=In%20order%20to%20properly%20fit,a%20standalone%20HD%20video%20format.
Just to get an idea of the numbers, with a camera shooting in
Free Run at 23.98fps, the drift will also be 3.6 seconds after
one hour of real time so the timecode count will be 01:00:03:14.
(0.6 seconds x 24 frames a second = 14.4 frames).
3600 seconds in an hour >> timecode is 1:00:03:14
3600 realtime >> 3603.6 timecode seconds
*/
const driftRatio = 3603.6 / 3600
let realTime = parseFloat(timeInSeconds)
let timecodeSeconds = realTime / driftRatio
// convert timeInSeconds in decimal to timecode values.
let [seconds, deciseconds] = timecodeSeconds.toString().split(".");
let d = new Date(null);
d.setSeconds(parseInt(seconds));
let hourmindays = d.toISOString().substr(11, 8);
let decipart = (23.98 * 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
}
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.
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
@rlau1115 - I opened an issue for that over here: geerlingguy/final-cut-it-out#1