Skip to content

Instantly share code, notes, and snippets.

@jashmenn
Created September 21, 2017 11:44
Show Gist options
  • Save jashmenn/66f2806ae6da643a0bb16452629deee8 to your computer and use it in GitHub Desktop.
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
#!/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);
}
}
@hamletcat
Copy link

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?

@JeanSemeur
Copy link

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?

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

@rlau1115
Copy link

rlau1115 commented Oct 17, 2020

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?

@rlau1115
Copy link

rlau1115 commented Oct 17, 2020

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
  }

@mathguimaraes
Copy link

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.

@mcbrineellis
Copy link

This worked excellently - thanks a bunch!!! You saved me a ton of time!

@geerlingguy
Copy link

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
Copy link

rlau1115 commented Jan 20, 2023 via email

@geerlingguy
Copy link

@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